Files
gen1recomp/tests/gen2_canlose_test.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

215 lines
7.6 KiB
Lua

-- BATTLETYPE_CANLOSE: losing the Cherrygrove rival battle must NOT whiteout.
--
-- GOLD_CACHE=".../gold" luajit tests/gen2_canlose_test.lua
--
-- maps/CherrygroveCity.asm arms all three rival arms with
--
-- loadvar VAR_BATTLETYPE, BATTLETYPE_CANLOSE
-- startbattle
-- dontrestartmapmusic
-- reloadmap
-- iftrue .AfterYourDefeat
--
-- and the type changes what a loss MEANS. LostBattle (engine/battle/
-- core.asm) answers BATTLETYPE_CANLOSE by sliding the winner's pic in and
-- printing the loss text, then RETURNS: no grayscale, no whiteout script, no
-- money halved. The script continues at the battle site -- `reloadmap`, not
-- `reloadmapafterbattle`, so Script_BattleWhiteout is unreachable -- and its
-- .AfterYourDefeat arm plays the "you lost" line, the shove and the rival's
-- walk-off along his own scripted path, ending on `special HealParty`.
--
-- The port warped every loss to the spawn point and then let the script
-- resume, so the rival's exit movement replayed at the Pokemon Center door,
-- walking him through the trees that stand there. What is asserted here is
-- the loss arm of the REAL World:startScriptedBattle -> World:startBattle
-- chain, with the type armed the way Script_loadvar arms it.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 canlose")
local check, eq = S.check, S.eq
local World = require("src.world.gen2.World")
local Map = require("src.world.gen2.Map")
local Mon = require("src.battle.gen2.Mon")
local cache = os.getenv("GOLD_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
end
local probe = io.open(cache .. "/data/generated/maps.lua", "r")
if not probe then
check(true, "gold cache absent (SKIP)")
S.finish()
return
end
probe:close()
local function loadLua(rel) return assert(loadfile(cache .. "/" .. rel))() end
local maps = loadLua("data/generated/maps.lua")
local tilesets = loadLua("data/generated/tilesets.lua")
local pokemon = loadLua("data/generated/pokemon.lua")
local moves = loadLua("data/generated/moves.lua")
-- constants/battle_constants.asm / engine/overworld/variables.asm.
local VAR_BATTLETYPE = 0x03
local BATTLETYPE_CANLOSE = 1
-- The two battle screens as registry fakes: the transition hands off
-- immediately, the battle parks its onDone for the test to lose with.
local battleDone
local registry = {
Gen2BattleTransition = { new = function(_g, opts)
opts.onDone()
return { screenId = "Gen2BattleTransition" }
end },
Gen2BattleState = { new = function(_g, opts)
battleDone = opts.onDone
return { screenId = "Gen2BattleState" }
end },
}
local function makeStack()
local stack = { items = {} }
function stack:push(inst) self.items[#self.items + 1] = inst end
function stack:pop()
local top = self.items[#self.items]
self.items[#self.items] = nil
return top
end
return stack
end
-- The rival's record, in the shape World:trainerParty answers `loadtrainer`
-- with: a roster Trainers.party builds real mons from.
local function rivalRecord()
return {
class = "RIVAL1", classId = 9, id = 1,
name = "?", className = "RIVAL", trainerName = "?",
roster = { { species = "TOTODILE", level = 5 } },
baseMoney = 35,
}
end
local function makeWorld()
battleDone = nil
local game = {
data = { audio = {}, screens = registry, pokemon = pokemon,
moves = moves },
save = {
player = { name = "GOLD", money = 3000 },
-- The loser's party: wiped, the way a lost battle leaves it.
party = {},
-- Walking into the Cherrygrove centre banked the respawn there, so a
-- whiteout that DOES fire has somewhere deterministic to land.
blackoutMap = "CHERRYGROVE_POKECENTER_1F",
},
stack = makeStack(),
}
local mon = Mon.new(game.data, "CYNDAQUIL", 5)
check(mon ~= nil, "the cache can build the player's starter")
mon.hp = 0
game.save.party[1] = mon
local w = World.new(game)
w.maps, w.tilesets = maps, tilesets
w.map = Map.new(maps.CHERRYGROVE_CITY, tilesets[maps.CHERRYGROVE_CITY.tileset])
w.player = { cellX = 39, cellY = 7, facing = "right", moving = false }
w.loaded = nil
w.setMap = function(self, id, x, y, facing)
self.loaded = { id = id, x = x, y = y, facing = facing }
return true
end
return w, game
end
-- ---- the rival's loss: armed CANLOSE, no whiteout -------------------------
do
local w, game = makeWorld()
-- Script_loadvar's write, exactly as the extracted script runs it.
w:writeVar(VAR_BATTLETYPE, BATTLETYPE_CANLOSE)
eq(w:battleType(), BATTLETYPE_CANLOSE, "the type is armed")
local outcome
check(w:startScriptedBattle(rivalRecord(), nil, function(result)
outcome = result
end), "the scripted battle starts")
check(battleDone ~= nil, "the battle screen is up")
battleDone("lose")
eq(outcome, "lose",
"the script resumes with LOSE, so `iftrue` takes .AfterYourDefeat")
check(w.loaded == nil,
"no warp: the loser is still standing at the battle site")
eq(game.save.player.money, 3000, "no HalveMoney either")
eq(game.save.party[1].hp, 0,
"and no engine heal -- .FinishRival's `special HealParty` owns that")
end
-- ---- the same loss without CANLOSE whiteouts as before --------------------
do
local w, game = makeWorld()
eq(w:battleType(), 0, "no type armed: an ordinary trainer loss")
local outcome
check(w:startScriptedBattle(rivalRecord(), nil, function(result)
outcome = result
end), "the battle starts")
battleDone("lose")
eq(outcome, "lose", "the resume still reports the loss")
check(w.loaded ~= nil, "Script_BattleWhiteout warps home")
eq(w.loaded.id, "CHERRYGROVE_POKECENTER_1F", "to the banked centre")
eq(game.save.player.money, 1500, "with the wallet halved")
check(game.save.party[1].hp > 0, "and the party healed")
end
-- ---- the type is a one-shot, as BattleStart resets wBattleType ------------
do
local w = makeWorld()
w:writeVar(VAR_BATTLETYPE, BATTLETYPE_CANLOSE)
check(w:startScriptedBattle(rivalRecord(), nil, function() end),
"a CANLOSE battle starts")
eq(w:battleType(), 0, "and consumes the type on the way in")
battleDone("win")
end
-- ---- the premise, pinned against the cache --------------------------------
-- The extracted rival scene really is the cart's shape: `loadvar 3, 1` (VAR_
-- BATTLETYPE, BATTLETYPE_CANLOSE), `startbattle`, then a bare `reloadmap` --
-- never `reloadmapafterbattle` -- and a loss arm whose walk-off movements and
-- `special` HealParty run at the battle site. A re-import that lost any of
-- this would green the units above while the game diverged.
do
local scripts = loadLua("data/generated/scripts.lua")
local seen = 0
for _, ev in ipairs(maps.CHERRYGROVE_CITY.coordEvents or {}) do
local body = scripts[ev.scriptKey]
if body then
seen = seen + 1
local armed, battled, reloaded, afterBattle
for index, cmd in ipairs(body) do
if cmd.op == "loadvar" and cmd.args and cmd.args[1] == VAR_BATTLETYPE
then
eq(cmd.args[2], BATTLETYPE_CANLOSE,
ev.scriptKey .. " arms BATTLETYPE_CANLOSE")
armed = index
elseif cmd.op == "startbattle" then
battled = index
elseif cmd.op == "reloadmap" then
reloaded = index
elseif cmd.op == "reloadmapafterbattle" then
afterBattle = index
end
end
check(armed and battled and reloaded and armed < battled
and battled < reloaded,
ev.scriptKey .. ": loadvar before startbattle before reloadmap")
check(afterBattle == nil,
ev.scriptKey .. " never runs reloadmapafterbattle")
end
end
check(seen >= 2, "both rival coord events carry extracted bodies")
end
S.finish()