Files
gen1recomp/tests/game3_encounters_cave_test.lua

305 lines
11 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/wild_encounter.c:355, src/fieldmap.c:68
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function finish()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
local Encounters = require("src.core.game3.encounters")
print("[test] 1. the encounter-type attribute has a runtime entry point")
check(type(Encounters.installEncounterTypes) == "function", "Encounters.installEncounterTypes exists")
check(type(Encounters.encounterTypeAt) == "function", "Encounters.encounterTypeAt exists")
check(type(Encounters.terrainAt) == "function", "Encounters.terrainAt exists")
if type(Encounters.encounterTypeAt) ~= "function" then
print("[test] no encounter-type attribute path at all; the rest cannot run")
finish()
end
local Cache = require("tests.game3_cache")
local cacheRoot = Cache.mount("objects/pack.lua", { native = true })
if not cacheRoot then
print("[skip] game3_encounters_cave_test: " .. tostring(Cache.reason))
finish()
end
print("[info] FireRed cache at " .. cacheRoot)
local ExtractScripts = require("src.import.gba.extract_scripts")
local NativePack = require("src.import.gba.native_pack")
local LayoutNative = require("src.core.game3.layout_native")
local Collision = require("src.core.game3.collision")
local Rng = require("src.core.game3.rng")
local function read_file(rel)
local f = io.open(rel, "rb")
if not f then return nil end
local data = f:read("*a")
f:close()
return data
end
local function load_lua(rel)
local src = read_file(rel)
if not src then return nil end
local chunk = load(src, "@" .. rel, "t", {})
if not chunk then return nil end
local ok, val = pcall(chunk)
return ok and val or nil
end
print("[test] 2. the importer bakes the attribute into objects/pack.lua")
local pack = load_lua(cacheRoot .. "/objects/pack.lua")
check(type(pack) == "table", "objects/pack.lua loads")
check(type(pack and pack.encounterTypes) == "table", "pack.encounterTypes present")
local pairsWithLand = 0
local landMids, waterMids = 0, 0
for _, forPair in pairs((pack and pack.encounterTypes) or {}) do
local has = false
for _, e in pairs(forPair) do
if e == 1 then landMids = landMids + 1; has = true
elseif e == 2 then waterMids = waterMids + 1 end
end
if has then pairsWithLand = pairsWithLand + 1 end
end
print(("[info] land-tagged mids %d, water-tagged mids %d over %d pairs")
:format(landMids, waterMids, pairsWithLand))
check(landMids > 500, "many metatiles carry TILE_ENCOUNTER_LAND (" .. landMids .. ")")
check(waterMids > 100, "metatiles carry TILE_ENCOUNTER_WATER (" .. waterMids .. ")")
print("[test] 3. loadBundle installs the table on the live Encounters module")
local bundle = ExtractScripts.loadBundle(Cache.cache(), cacheRoot, { allowIncomplete = true })
check(bundle ~= nil, "script bundle loads")
check(type(Encounters._encounterTypes) == "table", "Encounters._encounterTypes installed by loadBundle")
local manifest = load_lua(cacheRoot .. "/native/manifest.lua") or {}
local layouts = manifest.layouts or {}
local function bind(mapId)
local info = layouts[mapId]
if not info then return nil end
local blob = read_file(cacheRoot .. "/native/" .. (info.file or ("layouts/" .. mapId .. ".mid")))
if not blob then return nil end
local decoded = NativePack.decodeMidLayout(blob)
if not decoded then return nil end
local layout = LayoutNative.fromDecoded(decoded, mapId, info.pair)
local def = { midLayout = layout, pair = info.pair, warps = {} }
Collision.bindMap(nil, mapId, def)
return layout
end
local function cells_of_type(layout, want)
local out = {}
for cy = 0, layout.height - 1 do
for cx = 0, layout.width - 1 do
if Encounters.encounterTypeAt(cx, cy) == want and Collision.isWalkable(cx, cy) then
out[#out + 1] = { cx, cy }
end
end
end
return out
end
local function walk(mapId, cells, steps)
Encounters.resetRateModifiers()
for i = 1, steps do
local c = cells[((i - 1) % #cells) + 1]
local enc = Encounters.onStep(mapId, nil, { x = c[1], y = c[2] })
if enc then return enc, i end
end
return nil, steps
end
print("[test] 4. Mt Moon 1F cave floor is TILE_ENCOUNTER_LAND and rolls")
local MT_MOON = "FR_MT_MOON_1F"
local mtLayout = bind(MT_MOON)
check(mtLayout ~= nil, "FR_MT_MOON_1F native layout decodes")
if not mtLayout then finish() end
local caveCells = cells_of_type(mtLayout, 1)
print("[info] Mt Moon 1F land-encounter walkable cells: " .. #caveCells)
check(#caveCells > 200, "Mt Moon 1F has land-encounter floor (" .. #caveCells .. ")")
local mtTable = Encounters.tableFor(MT_MOON)
check(type(mtTable) == "table" and type(mtTable.land) == "table", "Mt Moon 1F has a land table")
local allowed = {}
local tblMinLevel, tblMaxLevel
for _, slot in ipairs((mtTable and mtTable.land and mtTable.land.slots) or {}) do
allowed[slot.species] = true
local lo = tonumber(slot.minLevel or slot.level) or 0
local hi = tonumber(slot.maxLevel or slot.level) or lo
if not tblMinLevel or lo < tblMinLevel then tblMinLevel = lo end
if not tblMaxLevel or hi > tblMaxLevel then tblMaxLevel = hi end
end
if #caveCells >= 2 then
Rng.SeedRng(0x1234)
Rng.SeedWildEncounterRng(0x1234)
local route = { caveCells[1], caveCells[2] }
local enc, step = walk(MT_MOON, route, 2000)
check(enc ~= nil, "walking Mt Moon 1F starts a wild encounter (step " .. tostring(step) .. ")")
if enc then
print(("[info] Mt Moon encounter: species=%s level=%s on step %d")
:format(tostring(enc.species), tostring(enc.level), step))
check(allowed[enc.species] == true,
"species " .. tostring(enc.species) .. " is in the Mt Moon 1F land table")
check(tblMinLevel ~= nil
and (enc.level or 0) >= tblMinLevel and (enc.level or 0) <= tblMaxLevel,
("level inside the table range (%s..%s)")
:format(tostring(tblMinLevel), tostring(tblMaxLevel)))
end
end
print("[test] 5. a plain indoor floor with no encounter attribute never rolls")
local deadCells = cells_of_type(mtLayout, 0)
print("[info] Mt Moon 1F walkable cells with TILE_ENCOUNTER_NONE: " .. #deadCells)
if #deadCells >= 2 then
Rng.SeedRng(0x1234)
Rng.SeedWildEncounterRng(0x1234)
local enc = walk(MT_MOON, { deadCells[1], deadCells[2] }, 3000)
check(enc == nil, "3000 steps on TILE_ENCOUNTER_NONE floor produce no encounter")
end
print("[test] 6. Route 1 road still never rolls, its grass still does")
local ROUTE1 = "FR_ROUTE_1"
local r1Layout = bind(ROUTE1)
check(r1Layout ~= nil, "FR_ROUTE_1 native layout decodes")
if r1Layout then
local road = cells_of_type(r1Layout, 0)
local grass = cells_of_type(r1Layout, 1)
print(("[info] Route 1: %d road cells, %d grass cells"):format(#road, #grass))
check(#road > 50, "Route 1 has plain road cells")
check(#grass > 20, "Route 1 has land-encounter cells")
if #road >= 2 then
Rng.SeedRng(0x2222)
Rng.SeedWildEncounterRng(0x2222)
local enc = walk(ROUTE1, { road[1], road[2] }, 3000)
check(enc == nil, "3000 steps on the Route 1 road produce no encounter")
end
if #grass >= 2 then
Rng.SeedRng(0x2222)
Rng.SeedWildEncounterRng(0x2222)
local enc, step = walk(ROUTE1, { grass[1], grass[2] }, 2000)
check(enc ~= nil, "Route 1 grass still starts a wild encounter (step " .. tostring(step) .. ")")
end
end
print("[test] 7. water tiles still roll from the water table")
local surfMap, surfLayout, surfCells
for _, mapId in ipairs({ "FR_ROUTE_21_NORTH", "FR_ROUTE_20", "FR_ROUTE_19", "FR_PALLET_TOWN", "FR_ROUTE_4" }) do
local layout = bind(mapId)
local t = Encounters.tableFor(mapId)
if layout and t and t.water then
local cells = {}
for cy = 0, layout.height - 1 do
for cx = 0, layout.width - 1 do
if Encounters.encounterTypeAt(cx, cy) == 2 then cells[#cells + 1] = { cx, cy } end
end
end
if #cells >= 2 then
surfMap, surfLayout, surfCells = mapId, layout, cells
break
end
end
end
check(surfCells ~= nil, "found a map with TILE_ENCOUNTER_WATER cells and a water table")
if surfCells then
bind(surfMap)
print(("[info] %s water-encounter cells: %d"):format(surfMap, #surfCells))
local waterAllowed = {}
for _, slot in ipairs(Encounters.tableFor(surfMap).water.slots or {}) do
waterAllowed[slot.species] = true
end
Rng.SeedRng(0x4321)
Rng.SeedWildEncounterRng(0x4321)
local enc, step = walk(surfMap, { surfCells[1], surfCells[2] }, 2000)
check(enc ~= nil, "surfing " .. surfMap .. " starts a wild encounter (step " .. tostring(step) .. ")")
if enc then
check(waterAllowed[enc.species] == true,
"species " .. tostring(enc.species) .. " is in the " .. surfMap .. " water table")
end
end
print("[test] 8. prevMetatileBehavior gates the global 60% dice roll")
bind(MT_MOON)
Encounters.resetRateModifiers()
Encounters._prevMetatileBehavior = -1
local c = caveCells[1]
Encounters.onStep(MT_MOON, nil, { x = c[1], y = c[2] })
check(Encounters._prevMetatileBehavior == (Collision.behavior(c[1], c[2]) or 0),
"onStep stamps the cell behavior into prevMetatileBehavior")
if #deadCells >= 1 then
local d = deadCells[1]
Encounters.onStep(MT_MOON, nil, { x = d[1], y = d[2] })
check(Encounters._prevMetatileBehavior == (Collision.behavior(d[1], d[2]) or 0),
"a non-encounter cell still stamps prevMetatileBehavior")
end
print("[test] 9. Repel keeps wilds at or above the lead's level")
-- pokefirered/src/wild_encounter.c:601
local Runtime = require("src.core.game3.runtime")
local prevSession = Runtime.session
bind(MT_MOON)
local function repelWalk(leadLevel, steps)
Runtime.session = {
map = MT_MOON,
repelSteps = 250,
party = { { species = 41, level = leadLevel, hp = 20 } },
}
Rng.SeedRng(0x1234)
Rng.SeedWildEncounterRng(0x1234)
Encounters.resetRateModifiers()
local got, minLevel = {}, nil
for i = 1, steps do
local cell = caveCells[((i - 1) % 2) + 1]
local enc = Encounters.onStep(MT_MOON, nil, { x = cell[1], y = cell[2] })
if enc then
got[#got + 1] = enc.level
if not minLevel or enc.level < minLevel then minLevel = enc.level end
end
end
return got, minLevel
end
if #caveCells >= 2 and tblMaxLevel then
local atMax, minAtMax = repelWalk(tblMaxLevel, 4000)
print(("[info] lead L%d under Repel: %d encounters, lowest level %s")
:format(tblMaxLevel, #atMax, tostring(minAtMax)))
check(#atMax > 0,
"a wild at exactly the lead's level still battles under Repel (wildLevel < ourLevel denies)")
check(minAtMax == nil or minAtMax >= tblMaxLevel,
"no wild below the lead's level got through (lowest " .. tostring(minAtMax) .. ")")
local aboveMax = repelWalk(tblMaxLevel + 1, 4000)
print(("[info] lead L%d under Repel: %d encounters"):format(tblMaxLevel + 1, #aboveMax))
check(#aboveMax == 0,
"a lead above the whole table repels every wild (" .. #aboveMax .. " got through)")
local minSteps = Encounters.cooldownMinSteps("land", (mtTable.land or {}).rate or 21)
print("[info] stepsSinceLastEncounter after the repelled walk: "
.. tostring(Encounters._stepsSinceLastEncounter) .. " / minSteps " .. tostring(minSteps))
check(minSteps ~= nil and Encounters._stepsSinceLastEncounter >= minSteps,
"a repelled roll does not re-arm the immunity steps")
check(Encounters._encounterRateBuff == 0, "an active Repel keeps the rate bank at zero")
end
Runtime.session = prevSession
finish()