Files
gen1recomp/tests/game3_town_map_test.lua
2026-09-23 16:34:15 -04:00

287 lines
12 KiB
Lua

#!/usr/bin/env luajit
-- Kanto Town Map & Region Map Unit Test Suite
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").mountOrSkip("town_map")
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local played = {}
package.loaded["src.core.game3.audio"] = {
playSe = function(id) played[#played + 1] = id end,
stopSe = function() end,
playCry = function() end,
playSong = function() end,
stopAll = function() end,
}
local RegionExtract = require("src.import.gba.region_map_extract")
local RegionMap = require("src.ui.game3.region_map")
local Position = require("src.ui.game3.region_map_position")
local ItemUse = require("src.core.game3.item_use")
local Bag = require("src.core.game3.bag")
local Dataset = require("src.core.game3.dataset")
local DEFS = Dataset.buildMaps()
require("src.core.game3.runtime")._game = { data = { maps = DEFS } }
local IDLE = { wasPressed = function() return false end, isDown = function() return false end }
local function frame(input) RegionMap.handleInput(input or IDLE) end
local function settle()
for _ = 1, 400 do
if RegionMap.inputReady() then return true end
frame()
end
return false
end
local function press(btn, frames)
frame({ wasPressed = function(_, k) return k == btn end, isDown = function(_, k) return k == btn end })
for _ = 1, frames or 5 do frame() end
end
local function runUntil(cond)
for _ = 1, 400 do
if cond() then return true end
frame()
end
return cond()
end
local function cell(session)
RegionExtract.ensureGenerated()
session.def = function(id) return assert(DEFS[id], id) end
return Position.playerCell(session, RegionExtract.GEOMETRY)
end
print("=== [TEST 1] GetPlayerPositionOnRegionMap ===")
do
-- src/region_map.c:3096
local x, y = cell({ map = "FR_PALLET_TOWN", x = 10, y = 8 })
check(x == 4 and y == 11, "Pallet Town resolves to (4, 11)")
x, y = cell({ map = "FR_VIRIDIAN_CITY", x = 20, y = 20 })
check(x == 4 and y == 8, "Viridian City resolves to (4, 8)")
x, y = cell({ map = "FR_PEWTER_CITY", x = 20, y = 20 })
check(x == 4 and y == 4, "Pewter City resolves to (4, 4)")
x, y = cell({ map = "FR_CERULEAN_CITY", x = 20, y = 20 })
check(x == 14 and y == 3, "Cerulean City resolves to (14, 3)")
-- src/region_map.c:3138
x, y = cell({ map = "FR_PLAYERS_HOUSE_1F", x = 3, y = 3, escapeWarp = { map = "FR_PALLET_TOWN", x = 6, y = 8 } })
check(x == 4 and y == 11, "the player's house resolves through the escape warp to Pallet Town")
-- src/region_map.c:3271
x, y = cell({ map = "FR_VIRIDIAN_FOREST", x = 30, y = 30 })
check(x == 4 and y == 6, "Viridian Forest uses its override (4, 6)")
-- src/region_map.c:3291-3301
x, y = cell({ map = "FR_ROUTE_21_NORTH", x = 5, y = 5 })
check(x == 4 and y == 12, "Route 21 North uses its override (4, 12)")
-- src/region_map.c:3158-3171
local tx, ty = cell({ map = "FR_ROUTE_1", x = 10, y = 0 })
local bx, by = cell({ map = "FR_ROUTE_1", x = 10, y = 39 })
check(tx == bx and ty < by, "Route 1 scales the player's y into its section")
local secTop = RegionExtract.LAYOUTS[0].map[ty][tx]
local secBottom = RegionExtract.LAYOUTS[0].map[by][bx]
check(secTop == secBottom and RegionExtract.KANTO_GRID[ty][tx] == "MAPSEC_ROUTE_1", "both ends stay on ROUTE 1")
-- src/region_map.c:3121
x, y = cell({ map = "FR_MT_MOON_1F", x = 10, y = 10, escapeWarp = { map = "FR_ROUTE_4", x = 19, y = 5 } })
check(RegionExtract.KANTO_GRID[y][x] == "MAPSEC_ROUTE_4", "Mt. Moon places the player on ROUTE 4 through the escape warp")
-- src/region_map.c:1030-1049
check(Position.regionFor(DEFS.FR_FOUR_ISLAND.regionMapSectionId, RegionExtract.LAYOUTS) == 2,
"Four Island belongs to the SEVII 4-5 map")
check(Position.regionFor(DEFS.SEVII_ONE_ISLAND.regionMapSectionId, RegionExtract.LAYOUTS) == 1,
"One Island belongs to the SEVII 1-2-3 map")
check(Position.regionFor(DEFS.FR_SEVEN_ISLAND.regionMapSectionId, RegionExtract.LAYOUTS) == 3,
"Seven Island belongs to the SEVII 6-7 map")
check(Position.regionFor(DEFS.FR_PALLET_TOWN.regionMapSectionId, RegionExtract.LAYOUTS) == 0,
"Pallet Town belongs to Kanto")
end
print("=== [TEST 2] RegionMap Interactive Cursor Navigation & Landmark Updates ===")
do
local closed = false
local session = { map = "FR_PALLET_TOWN", x = 10, y = 8, gender = 0 }
RegionMap.show({
session = session,
onClose = function() closed = true end,
})
check(RegionMap.isOpen() == true, "RegionMap is open")
check(settle(), "the open animation reaches the input state")
check(RegionMap.playerX == 4 and RegionMap.playerY == 11, "player location initialized at Pallet Town (4, 11)")
check(RegionMap.cursorX == 4 and RegionMap.cursorY == 11, "cursor location initialized at (4, 11)")
check(RegionMap.currentLocationName() == "PALLET TOWN", "initial landmark name is PALLET TOWN")
-- src/region_map.c:2754 HandleRegionMapInput, :2836 MoveMapCursor
frame({ wasPressed = function() return false end, isDown = function(_, k) return k == "up" end })
check(RegionMap.cursorY == 11, "a held direction slides the cursor before it moves a cell")
for _ = 1, 3 do frame() end
check(RegionMap.cursorY == 11, "the slide takes four frames")
played = {}
frame()
check(RegionMap.cursorX == 4 and RegionMap.cursorY == 10, "cursor moved UP to (4, 10)")
check(RegionMap.currentLocationName() == "ROUTE 1", "landmark name updated to ROUTE 1")
check(#played == 0, "a route plays no scroll sound")
press("up")
played = {}
press("up")
check(RegionMap.cursorX == 4 and RegionMap.cursorY == 8, "cursor moved UP to (4, 8)")
check(RegionMap.currentLocationName() == "VIRIDIAN CITY", "landmark name updated to VIRIDIAN CITY")
-- src/region_map.c:1174
check(played[1] == 101, "a town plays SE_DEX_SCROLL")
frame({ wasPressed = function(_, k) return k == "up" or k == "right" end,
isDown = function(_, k) return k == "up" or k == "right" end })
for _ = 1, 4 do frame() end
check(RegionMap.cursorX == 5 and RegionMap.cursorY == 7, "a diagonal hold moves both axes at once")
check(RegionMap.state().palTinted == true, "the open map draws the 95% tinted bank 2")
frame({ wasPressed = function(_, k) return k == "b" end, isDown = function(_, k) return k == "b" end })
local tintOk, sawClose, sawUntinted = true, false, false
for _ = 1, 400 do
local s = RegionMap.state()
if not s then break end
if s.task == "mapCloseAnim" then
sawClose = true
local cs = s.anim.closeState
if cs <= 2 and s.palTinted ~= true then tintOk = false end
if cs >= 3 then
if s.palTinted ~= false then tintOk = false end
sawUntinted = true
end
elseif sawClose and s.palTinted ~= false then
tintOk = false
end
frame()
end
-- src/region_map.c:2572
check(sawClose and sawUntinted and tintOk, "close state 2 reloads sRegionMap_Pal untinted and it stays untinted")
check(not RegionMap.isOpen(), "B runs the close animation and fade")
check(closed == true, "onClose callback executed")
RegionMap.show({ session = session })
check(RegionMap.state().palTinted == true, "the next show draws tinted again")
RegionMap.close()
end
print("=== [TEST 3] Inventory TOWN_MAP Item Use Integration ===")
do
local bag = Bag.new()
Bag.add(bag, 361, 1) -- Town Map
local session = { map = "FR_CELADON_CITY", x = 30, y = 20, bag = bag }
local ok, reason = ItemUse.useField(session, bag, 361, nil)
check(ok == true, "ItemUse.useField accepts TOWN_MAP (item 361)")
check(reason == "map", "useField returns 'map' reason")
check(RegionMap.isOpen() == true, "RegionMap opened via ItemUse")
settle()
check(RegionMap.playerX == 11 and RegionMap.playerY == 6, "player position placed at Celadon City (11, 6)")
check(RegionMap.currentLocationName() == "CELADON CITY", "Celadon City landmark active")
-- src/region_map.c:2819
check(RegionMap.state().fromField == true, "no BAG is open, so this is the field Town Map")
press("select")
check(runUntil(function() return not RegionMap.isOpen() end), "SELECT closes the field Town Map")
end
print("=== [TEST 4] Start Snapping & Dungeon Guide Modal ===")
do
local session = { map = "FR_VIRIDIAN_CITY", x = 20, y = 20, gender = 0 }
RegionMap.show({ session = session })
settle()
check(RegionMap.cursorX == 4 and RegionMap.cursorY == 8, "cursor at Viridian City (4, 8)")
press("start")
check(RegionMap.cursorX == 21 and RegionMap.cursorY == 13, "START snapped to Cancel button (21, 13)")
local _, right = RegionMap.topBarText()
check(right == "gText_RegionMap_AButtonCancel", "the top bar offers A CANCEL")
press("start")
check(RegionMap.cursorX == 4 and RegionMap.cursorY == 8, "START snapped back to Player Icon (4, 8)")
press("up")
press("up")
check(RegionMap.cursorX == 4 and RegionMap.cursorY == 6, "cursor at Viridian Forest (4, 6)")
check(RegionMap.currentDungeonName() == "VIRIDIAN FOREST", "current dungeon is VIRIDIAN FOREST")
check(RegionMap.state().text.dungeonType == RegionMap.MAPSECTYPE.NOT_VISITED,
"the unvisited dungeon name uses the red text colour")
press("a")
check(RegionMap.previewDungeon == nil, "A on an unvisited dungeon opens no preview")
check(RegionMap.isOpen() == true, "RegionMap stays open after the refused GUIDE")
check(RegionMap.canGuideCursor() == false, "GUIDE is refused on a dungeon that has not been visited")
-- pokefirered/data/maps/ViridianForest/scripts.inc:6
session.flags = session.flags or {}
session.flags["FLAG_WORLD_MAP_VIRIDIAN_FOREST"] = true
check(RegionMap.canGuideCursor() == true, "GUIDE is offered on a visited dungeon")
played = {}
press("a", 0)
check(RegionMap.previewDungeon == "MAPSEC_VIRIDIAN_FOREST", "Dungeon Preview Modal opened for Viridian Forest")
-- src/region_map.c:2021-2030
press("b", 20)
check(RegionMap.previewDungeon == "MAPSEC_VIRIDIAN_FOREST", "B is ignored until the flavour text is printed")
check(runUntil(function() local s = RegionMap.state() return s.preview and s.preview.text ~= nil end),
"the GUIDE prints its text")
press("b")
check(runUntil(function() return RegionMap.previewDungeon == nil end), "B shrinks and closes the GUIDE")
check(#played == 0, "the GUIDE plays no sound effects")
check(RegionMap.isOpen() == true, "RegionMap still open after closing modal")
settle()
press("b")
runUntil(function() return not RegionMap.isOpen() end)
check(RegionMap.isOpen() == false, "RegionMap closed on B")
end
print("=== [TEST 5] Wall Town Map Metatile & Script Execution ===")
do
local Interaction = require("src.core.game3.scripting.interaction_scripts")
local CollisionStd = require("src.core.game3.scripting.collision_std")
local scriptKey = Interaction.scriptFor(0x85, "up")
check(scriptKey == "EventScript_WallTownMap", "Behavior 0x85 (MB_TOWN_MAP) maps to EventScript_WallTownMap")
local collScript = CollisionStd.scriptFor(0x95)
check(collScript == "EventScript_WallTownMap", "COLL_TOWN_MAP (0x95) maps to EventScript_WallTownMap")
local Adapters = require("src.core.game3.scripting.adapters")
local session = { map = "FR_VIRIDIAN_CITY", x = 20, y = 20 }
package.loaded["src.core.game3.runtime"].getSession = function() return session end
local hostAdapters = Adapters.host(nil, { session = session }, nil)
local mapOpened = false
hostAdapters.showTownMap(function()
mapOpened = true
end)
check(RegionMap.isOpen() == true, "adapters.showTownMap opens RegionMap")
RegionMap.close()
check(RegionMap.isOpen() == false, "RegionMap closed cleanly")
check(mapOpened == true, "showTownMap callback was executed")
end
print("=== [TEST 6] The Sevii maps (#2430) ===")
do
RegionMap.show({ session = { map = "FR_FOUR_ISLAND", x = 12, y = 14 } })
settle()
check(RegionMap.state().selectedRegion == 2, "the Town Map on Four Island shows SEVII 4-5")
check(RegionMap.currentLocationName() == "FOUR ISLAND", "and the cursor starts on FOUR ISLAND")
RegionMap.close()
for _, m in ipairs({ "FR_FOUR_ISLAND_POKEMON_CENTER_1F", "FR_FOUR_ISLAND_ICEFALL_CAVE_ENTRANCE", "FR_FOUR_ISLAND_ICEFALL_CAVE_BACK" }) do
RegionMap.show({ session = { map = m, x = 5, y = 5 } })
settle()
check(RegionMap.state().selectedRegion == 2, m .. " opens SEVII 4-5")
RegionMap.close()
end
end
if failed > 0 then
print(string.format("\n[FAILED] %d test(s) failed", failed))
os.exit(1)
else
print("\nALL TOWN MAP & REGION MAP TESTS PASSED CLEANLY!")
end