#!/usr/bin/env luajit package.path = "./?.lua;./?/init.lua;" .. package.path package.loaded["src.core.game3.rom_text"] = { plain = function(key) return key end, box = function(key) return key end, ascii = function(key) return key end, has = function() return true end, key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, count = function() return 0 end, list = function() return {} end, lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end, } 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 eq(a, b, msg) check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b))) end require("src.core.GameVersion").set("firered") package.loaded["src.core.game3.audio"] = { playSe = function() end, playCry = function() end, playSong = function() end, stopAll = function() end, } local Cache = require("tests.game3_cache") if not Cache.mount("meta.json") then print("[skip] " .. tostring(Cache.reason)) os.exit(0) end local Dataset = require("src.core.game3.dataset") local DEFS = Dataset.buildMaps() local MAPS = { PalletTown = DEFS.FR_PALLET_TOWN, PalletTown_PlayersHouse_1F = DEFS.FR_PLAYERS_HOUSE_1F, ViridianForest = DEFS.FR_VIRIDIAN_FOREST, MtMoon_1F = DEFS.FR_MT_MOON_1F, RockTunnel_1F = DEFS.FR_ROCK_TUNNEL_1F, } for name, def in pairs(MAPS) do if not def then print("[skip] the cache has no live map def for " .. name) os.exit(0) end end -- pokefirered/src/item_use.c:614 print("[test] 0. the live map def carries both header fields") check(MAPS.MtMoon_1F.cave == 0, "dataset.lua puts Mt Moon's requires_flash on the def") check(MAPS.MtMoon_1F.allowEscaping == 1, "and its allow_escaping too") local currentMap = MAPS.PalletTown package.loaded["src.core.game3.map"] = { currentDef = function() return currentMap end, } package.loaded["src.core.game3.collision"] = { isWater = function() return false end, isGrass = function() return false end, behavior = function() return 0 end, } package.loaded["src.core.game3.objects"] = { at = function() return nil end, } package.loaded["src.core.game3.scripting.space"] = { store = nil } local executed = nil package.loaded["src.core.game3.field"] = { executeFieldMove = function(payload) executed = payload end, } local PartyMenu = require("src.ui.game3.party_menu") local FieldMoves = require("src.core.game3.field_moves") local seenCtx = nil local realFromMenu = FieldMoves.fromMenu FieldMoves.fromMenu = function(moveId, ctx) seenCtx = ctx return realFromMenu(moveId, ctx) end local function make_input(pressed) return { wasPressed = function(_, key) return pressed[key] == true end, isDown = function(_, key) return pressed[key] == true end, } end local function press(key) PartyMenu.handleInput(make_input({ [key] = true })) end local function make_mon(moves) return { species = 6, level = 40, hp = 100, maxHp = 100, stats = { hp = 100 }, moves = moves, pp = { 10, 10, 10, 10 }, } end local function use_field_move(mapDef, moveName, badges) currentMap = mapDef seenCtx, executed = nil, nil PartyMenu.show({ make_mon({ moveName }) }, nil, { session = { badges = badges or {} }, }) press("a") local target = nil for i, label in ipairs(PartyMenu.ACTIONS) do if label == moveName:gsub("_", " ") then target = i end end if not target then PartyMenu.close() return nil, nil, "no action row for " .. moveName end while PartyMenu.actionCursor ~= target do press("down") end press("a") local refusal = PartyMenu._messageText local prompt = nil -- pokefirered/src/party_menu.c:3999 Task_HandleFieldMoveExitAreaYesNoInput if PartyMenu.mode == "yesno" then prompt = PartyMenu._yesNoPrompt press("a") end local ctx = seenCtx local ran = executed if PartyMenu.open then PartyMenu.close() end return ctx, ran, refusal, prompt end print("[test] 1. the field-move context carries the map header fields") local ctx = use_field_move(MAPS.PalletTown, "FLY", { FLY = true }) eq(ctx and ctx.mapType, 1, "PalletTown ctx.mapType is MAP_TYPE_TOWN") check(ctx and ctx.mapType ~= nil, "ctx.mapType is not nil (mapDef.mapType, not mapDef.type)") eq(ctx and ctx.isCave, false, "PalletTown ctx.isCave is false") eq(ctx and ctx.canEscapeRope, false, "PalletTown ctx.canEscapeRope is false") ctx = use_field_move(MAPS.RockTunnel_1F, "DIG", {}) eq(ctx and ctx.mapType, 4, "RockTunnel ctx.mapType is MAP_TYPE_UNDERGROUND") eq(ctx and ctx.isCave, true, "RockTunnel ctx.isCave is read out of header.json") eq(ctx and ctx.canEscapeRope, true, "RockTunnel ctx.canEscapeRope is read out of header.json") ctx = use_field_move(MAPS.ViridianForest, "DIG", {}) eq(ctx and ctx.canEscapeRope, true, "ViridianForest ctx.canEscapeRope is true (allowEscaping 1)") print("[test] 2. Fly is accepted outdoors and refused indoors") local _, ran, refusal = use_field_move(MAPS.PalletTown, "FLY", { FLY = true }) eq(ran and ran.action, "fly", "FLY outdoors reaches Field.executeFieldMove") eq(refusal, nil, "FLY outdoors prints no refusal") _, ran, refusal = use_field_move(MAPS.PalletTown_PlayersHouse_1F, "FLY", { FLY = true }) eq(ran, nil, "FLY indoors does not execute") check(refusal ~= nil, "FLY indoors prints a refusal (" .. tostring(refusal) .. ")") print("[test] 3. Teleport is accepted outdoors and refused indoors") local prompt _, ran, refusal, prompt = use_field_move(MAPS.PalletTown, "TELEPORT", {}) eq(ran and ran.action, "teleport", "TELEPORT outdoors reaches Field.executeFieldMove") -- pokefirered/src/party_menu.c:3939 check(tostring(prompt):find("gText_ReturnToHealingSpot", 1, true) ~= nil, "after YES to gText_ReturnToHealingSpot (" .. tostring(prompt) .. ")") _, ran, refusal = use_field_move(MAPS.PalletTown_PlayersHouse_1F, "TELEPORT", {}) eq(ran, nil, "TELEPORT indoors does not execute") check(refusal ~= nil, "TELEPORT indoors prints a refusal") print("[test] 4. Dig follows the map header, not a nil map type") _, ran, refusal, prompt = use_field_move(MAPS.MtMoon_1F, "DIG", {}) eq(ran and ran.action, "dig", "DIG in Mt Moon reaches Field.executeFieldMove") -- pokefirered/src/party_menu.c:3946 check(tostring(prompt):find("gText_EscapeFromHereAndReturnTo", 1, true) ~= nil, "after YES to gText_EscapeFromHereAndReturnTo (" .. tostring(prompt) .. ")") _, ran, refusal = use_field_move(MAPS.PalletTown_PlayersHouse_1F, "DIG", {}) eq(ran, nil, "DIG indoors (allowEscaping 0) does not execute") check(refusal ~= nil, "DIG indoors prints a refusal") _, ran, refusal = use_field_move(MAPS.PalletTown, "DIG", {}) eq(ran, nil, "DIG in Pallet Town (allowEscaping 0) does not execute") -- pokefirered/src/item_use.c:614 print("[test] 4b. the ctx reads the def only, never header.json") local stripped = {} for k, v in pairs(MAPS.RockTunnel_1F) do stripped[k] = v end stripped.cave, stripped.allowEscaping = nil, nil ctx = use_field_move(stripped, "DIG", {}) eq(ctx and ctx.isCave, false, "a def with no cave field reads false, not the cached header") eq(ctx and ctx.canEscapeRope, false, "and no allowEscaping field reads false too") eq(stripped.cave, nil, "the def is not stamped from header.json") eq(stripped.allowEscaping, nil, "neither field is stamped") print("[test] 5. an accepted field move tears the start menu down too") -- pokefirered/src/party_menu.c:3958 local StartMenu = require("src.ui.game3.start_menu") currentMap = MAPS.PalletTown local function via_start_menu(moveName, badges) seenCtx, executed = nil, nil local session = { badges = badges or {}, party = { make_mon({ moveName }) } } StartMenu.show({ session = session }) for _ = 1, #StartMenu.ENTRIES do local e = StartMenu.ENTRIES[StartMenu.cursor] if e and e.id == "pokemon" then break end StartMenu.move(1) end StartMenu.confirm() press("a") local target = nil for i, label in ipairs(PartyMenu.ACTIONS) do if label == moveName then target = i end end while target and PartyMenu.actionCursor ~= target do press("down") end press("a") if PartyMenu.mode == "yesno" then press("a") end end via_start_menu("TELEPORT", {}) eq(executed and executed.action, "teleport", "TELEPORT from the start menu executes") eq(StartMenu.isOpen(), false, "the start menu is gone when the move runs") eq(PartyMenu.isOpen(), false, "the party menu is gone when the move runs") via_start_menu("FLY", {}) eq(executed, nil, "FLY without the Thunder Badge is refused") eq(StartMenu.isOpen(), true, "a refusal leaves the start menu underneath") if PartyMenu.open then PartyMenu.close() end if StartMenu.isOpen() then StartMenu.close(true) end print("[test] 6. a cancelled fly map comes back to the party menu") -- pokefirered/src/region_map.c:4019 do local RegionMap = require("src.ui.game3.region_map") currentMap = MAPS.PalletTown local _, ran = use_field_move(MAPS.PalletTown, "FLY", { FLY = true }) eq(ran and ran.action, "fly", "FLY was accepted, so the party menu closed for the fly map") eq(PartyMenu.isOpen(), false, "the party menu is closed while the fly map is up") require("src.core.game3.runtime")._game = { data = { maps = DEFS } } local idle = { wasPressed = function() return false end, isDown = function() return false end } RegionMap.show({ mode = "fly", session = { map = "FR_PALLET_TOWN", x = 10, y = 8 } }) for _ = 1, 200 do if RegionMap.inputReady() then break end RegionMap.handleInput(idle) end RegionMap.handleInput({ wasPressed = function(_, k) return k == "b" end, isDown = function() return false end, }) for _ = 1, 200 do if not RegionMap.isOpen() then break end RegionMap.handleInput(idle) end eq(RegionMap.isOpen(), false, "B closed the fly map") eq(PartyMenu.isOpen(), true, "B on the fly map lands back in the party menu") PartyMenu.close() local _, ran2 = use_field_move(MAPS.PalletTown, "FLY", { FLY = true }) eq(ran2 and ran2.action, "fly", "FLY accepted a second time") RegionMap.show({ mode = "fly", session = { map = "FR_PALLET_TOWN", x = 10, y = 8 } }) RegionMap.close(true) eq(PartyMenu.isOpen(), false, "a picked destination does not reopen the party menu") end FieldMoves.fromMenu = realFromMenu if failed > 0 then print(string.format("\n%d CHECK(S) FAILED", failed)) os.exit(1) end print("\nALL PARTY FIELD-MOVE CONTEXT TESTS PASSED")