Files
gen1recomp/tests/save_compat/_gen1_random.lua
T
2026-10-01 17:30:52 -04:00

276 lines
9.7 KiB
Lua

local K = require("tests.save_compat._codec")
local GenSave = require("src.save_convert.GenSave")
local SaveData = require("src.core.SaveData")
local Pokemon = require("src.pokemon.Pokemon")
local Stats = require("src.pokemon.Stats")
local Growth = require("src.pokemon.Growth")
local Boxes = require("src.pokemon.Boxes")
local Party = require("src.pokemon.Party")
local Bag = require("src.inventory.Bag")
local R = {}
local LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
local ALIASES = { EVENT_RECEIVED_BIKE_VOUCHER = true, EVENT_GOT_HM_FLASH = true }
local BADGES = { "BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE", "RAINBOWBADGE", "SOULBADGE", "MARSHBADGE",
"VOLCANOBADGE", "EARTHBADGE" }
local STATUSES = { "PSN", "BRN", "FRZ", "PAR", "SLP" }
local function lcg(seed)
local state = (seed * 2654435761 + 12345) % 4294967296
local function nextInt()
state = (state * 1664525 + 1013904223) % 4294967296
return state
end
return function(lo, hi)
if hi == nil then lo, hi = 1, lo end
return lo + math.floor(nextInt() / 4294967296 * (hi - lo + 1))
end
end
local function sortedKeys(t, filter)
local out = {}
for k, v in pairs(t) do
if type(k) == "string" and type(v) == "table" and (not filter or filter(k, v)) then out[#out + 1] = k end
end
table.sort(out)
return out
end
local pools = {}
local cacheApplied = {}
function R.data(version)
local data = K.gen1Data(version)
local dir = os.getenv("GEN1_" .. version:upper() .. "_CACHE")
if dir and not cacheApplied[version] then
cacheApplied[version] = true
for _, name in ipairs({ "pokemon", "moves", "items", "maps", "field", "tilesets", "audio", "encounters" }) do
local chunk = assert(loadfile(dir .. "/data/generated/" .. name .. ".lua"))
data[name] = chunk()
end
end
return data
end
local function poolFor(version)
if pools[version] then return pools[version] end
local data = R.data(version)
local cw = GenSave.crosswalks(data)
local pool = { data = data, cw = cw }
pool.species = sortedKeys(data.pokemon, function(id, def) return cw.pokemonIndex[id] and def.index end)
pool.moves = sortedKeys(data.moves, function(id) return cw.movesIndex[id] end)
pool.items = sortedKeys(data.items, function(id)
return cw.itemsIndex[id] and not id:match("BADGE$") and id ~= "COIN_CASE" and id ~= "COIN"
end)
pool.flags = {}
for _, name in pairs(data.eventFlags.byBit) do
if not ALIASES[name] and not name:match("^EVENT_CHOSE_") and not name:match("SAFARI")
and name ~= "EVENT_GAVE_FOSSIL_TO_LAB" and name ~= "EVENT_GOT_STARTER" then
pool.flags[#pool.flags + 1] = name
end
end
table.sort(pool.flags)
pool.maps = sortedKeys(data.maps, function(id, m) return m.sram and cw.mapsIndex[id] end)
pool.towns = {}
for _, row in ipairs(require("src.save_convert.data.blackout_maps")) do pool.towns[#pool.towns + 1] = row end
pools[version] = pool
return pool
end
local function name(rng, max)
local t = {}
for i = 1, rng(1, max or 7) do
local k = rng(1, #LETTERS)
t[i] = LETTERS:sub(k, k)
end
return table.concat(t)
end
local function randomMon(rng, pool, ownerName, ownerId, boxed)
local data = pool.data
local id = pool.species[rng(#pool.species)]
local level = rng(2, 100)
local mon = Pokemon.new(data, id, level, rng)
local def = data.pokemon[id]
local lo = Growth.expForLevel(def.growthRate, level)
local hi = level >= 100 and lo or Growth.expForLevel(def.growthRate, level + 1) - 1
mon.exp = rng(lo, math.max(lo, hi))
mon.statExp = { hp = rng(0, 65535), attack = rng(0, 65535), defense = rng(0, 65535), speed = rng(0, 65535),
special = rng(0, 65535) }
mon.stats = Stats.calc(def, level, mon.dvs, mon.statExp)
mon.hp = rng(0, 4) == 0 and 0 or rng(1, mon.stats.hp)
mon.moves = {}
local seen = {}
for _ = 1, rng(1, 4) do
local mid = pool.moves[rng(#pool.moves)]
if not seen[mid] then
seen[mid] = true
local mdef = data.moves[mid]
local ups = rng(0, 3)
mon.moves[#mon.moves + 1] = { id = mid, pp = rng(0, math.min(63, (mdef.pp or 10))), ppUps = ups }
end
end
if rng(1, 3) == 1 then
local st = STATUSES[rng(#STATUSES)]
mon.status = st
if st == "SLP" then mon.sleepTurns = rng(1, 7) end
end
mon.catchRate = rng(1, 255)
if rng(1, 3) == 1 then mon.nickname = name(rng, 10) end
if rng(1, 4) == 1 then
mon.traded = true
mon.ot = name(rng, 7)
mon.otId = rng(0, 65535)
else
mon.ot = ownerName
mon.otId = ownerId
end
if boxed then
mon.stats = nil
end
return mon
end
function R.build(seed, version)
local pool = poolFor(version)
local data, cw = pool.data, pool.cw
local rng = lcg(seed)
math.randomseed(seed)
local save = SaveData.newGame({})
save.version = version
save.player.name = name(rng, 7)
save.player.rival = name(rng, 7)
save.player.id = rng(0, 65535)
save.money = rng(0, 999999)
save.coins = rng(0, 9999)
save.options = { textSpeed = ({ 1, 3, 5 })[rng(3)], battleStyle = rng(2) == 1 and "set" or "shift", animations = rng(2) == 1 }
save.party = {}
for _ = 1, rng(1, 6) do Party.add(save.party, randomMon(rng, pool, save.player.name, save.player.id)) end
save.boxes = nil
Boxes.ensure(save)
for b = 1, 12 do
for _ = 1, rng(0, 20) do
if rng(1, 3) ~= 1 then
table.insert(save.boxes[b], randomMon(rng, pool, save.player.name, save.player.id, true))
end
end
end
save.currentBox = rng(1, 12)
for _ = 1, rng(0, 6) do Boxes.deposit(save, randomMon(rng, pool, save.player.name, save.player.id, true)) end
save.inventory = {}
save.bagOrder = nil
for _ = 1, rng(0, 20) do
Bag.add(save, pool.items[rng(#pool.items)], rng(1, 40), data)
end
for _, b in ipairs(BADGES) do
if rng(1, 2) == 1 then save.inventory[b] = 1 end
end
save.pcItems = {}
local pcCount = 0
for _ = 1, rng(0, 50) do
local id = pool.items[rng(#pool.items)]
if not save.pcItems[id] and pcCount < 50 then
save.pcItems[id] = rng(1, 99)
pcCount = pcCount + 1
end
end
if rng(1, 2) == 1 then Bag.add(save, "COIN_CASE", 1, data) end
save.pokedex = { seen = {}, owned = {} }
for _, id in ipairs(pool.species) do
if rng(1, 2) == 1 then
save.pokedex.seen[id] = true
if rng(1, 2) == 1 then save.pokedex.owned[id] = true end
end
end
save.flags = {}
for _ = 1, rng(0, 60) do save.flags[pool.flags[rng(#pool.flags)]] = true end
if rng(1, 2) == 1 then
save.flags.EVENT_GOT_STARTER = true
local starters = version == "yellow" and { "PIKACHU" } or { "BULBASAUR", "CHARMANDER", "SQUIRTLE" }
save.flags["EVENT_CHOSE_" .. starters[rng(#starters)]] = true
end
if version == "yellow" then save.rivalStarter = rng(0, 3) end
if rng(1, 6) == 1 then
save.flags.EVENT_RECEIVED_BIKE_VOUCHER = true
save.flags.EVENT_GOT_BIKE_VOUCHER = true
end
save.hiddenTaken = {}
for _, row in ipairs(data.hiddenItems) do
if rng(1, 3) == 1 then save.hiddenTaken[row[1] .. "_" .. row[2] .. "_" .. row[3]] = true end
end
for _, row in ipairs(require("src.save_convert.data.hidden_coins")) do
if rng(1, 3) == 1 then save.hiddenTaken[row[1] .. "_" .. row[2] .. "_" .. row[3]] = true end
end
save.visited = {}
for idx = 0, 10 do
local id = cw.mapsByIndex[idx]
if id and rng(1, 2) == 1 then save.visited[id] = true end
end
local safariOn = rng(1, 4) == 1
local forcedOn = not safariOn and rng(1, 4) == 1
local mapId = pool.maps[rng(#pool.maps)]
if safariOn then mapId = ({ "SAFARI_ZONE_CENTER", "SAFARI_ZONE_EAST", "SAFARI_ZONE_NORTH", "SAFARI_ZONE_WEST" })[rng(4)] end
if forcedOn then mapId = "ROUTE_17" end
local def = data.maps[mapId]
save.player.map = mapId
save.player.x = rng(0, def.width * 2 - 1)
save.player.y = rng(0, def.height * 2 - 1)
save.player.facing = ({ "up", "down", "left", "right" })[rng(4)]
save.lastOutdoor = { id = pool.towns[rng(#pool.towns)][1] }
local heal = pool.towns[rng(#pool.towns)]
if rng(1, 2) == 1 then
save.lastHeal = { map = heal[1], x = heal[2], y = heal[3] }
else
save.lastHeal = { map = heal[1] == "ROUTE_4" and "MT_MOON_POKECENTER" or "VIRIDIAN_POKECENTER", x = 3, y = 4,
outdoor = { id = heal[1], x = heal[2], y = heal[3] } }
end
local ride = rng(3)
if ride == 2 then save.onBike = true elseif ride == 3 then save.player.surfing = true end
if forcedOn then save.onBike, save.player.surfing = true, nil end
save.playTime = rng(0, 255 * 3600 - 1) + rng(0, 59) / 60
if rng(1, 2) == 1 then
save.hallOfFame = {}
for t = 1, rng(1, 8) do
local team = {}
for _ = 1, rng(1, 6) do
local team1 = { species = pool.species[rng(#pool.species)], level = rng(1, 100) }
if rng(1, 3) == 1 then team1.nickname = name(rng, 10) end
team[#team + 1] = team1
end
save.hallOfFame[t] = team
end
end
if safariOn then save.safari = { balls = rng(1, 30), steps = rng(1, 502) } end
if rng(1, 3) == 1 then
local mon = randomMon(rng, pool, save.player.name, save.player.id, true)
save.daycare = { mon = mon, steps = rng(0, 200), depositLevel = mon.level }
end
if forcedOn then save.forcedBike = true end
if rng(1, 2) == 1 then save.usedPokecenter = true end
if rng(1, 3) == 1 then save.trashPuzzle = { first = rng(0, 7) * 2, second = rng(0, 14) } end
if rng(1, 4) == 1 then
save.labFossilMon = ({ "KABUTO", "OMANYTE", "AERODACTYL" })[rng(3)]
save.flags.EVENT_GAVE_FOSSIL_TO_LAB = true
end
local dark = data.field.darkMaps and data.field.darkMaps.maps or {}
for _, id in ipairs(dark) do if id == mapId and rng(1, 2) == 1 then save.flashLit = true end end
if version == "yellow" then
save.pikachuHappiness = rng(0, 255)
save.pikachuMood = rng(0, 255)
if rng(1, 2) == 1 then save.pikachuEmotionModifier = rng(1, 255) end
if rng(1, 2) == 1 then save.surfingHighScore = rng(1, 9999) end
end
return save
end
return R