-- The evolution screen survives being pushed onto a stack. -- -- luajit tests/gen2_evolution_anim_test.lua -- -- `enter` is the stack's lifecycle hook: StateStack:push calls `state:enter(...)` -- (src/core/StateStack.lua:18), and Gold runs that same stack -- (src/core/Game2.lua:makeStack). EvolutionAnim used to define `enter(phase)` -- as its own phase-transition method, so pushing it called that method with -- no argument, set phase to nil, -- and left update() falling through every branch -- decrementing its timer -- forever while World:busy() stayed true. A post-battle evolution became an -- unrecoverable hang; the Gold route bot found it the first time its starter -- reached level 14, roughly two hours into a run. -- -- The regression this pins is not the rename but the PROPERTY: after a push, -- the screen still runs to completion. package.path = "./?.lua;./?/init.lua;" .. package.path local S = require("tests.harness").suite("gen2 evolution anim") local check, eq = S.check, S.eq local EvolutionAnim = require("src.ui.gen2.EvolutionAnim") -- No screen may use the stack's hook name for its own purposes. check(rawget(EvolutionAnim, "enter") == nil, "EvolutionAnim does not define `enter` (the stack lifecycle hook)") check(type(rawget(EvolutionAnim, "setPhase")) == "function", "its phase transition has a name of its own") local cache = os.getenv("GOLD_CACHE") if not cache then cache = (os.getenv("HOME") or "") .. "/Library/Application Support/LOVE/gold-dev/gold" end local function loadGen(name) local path = cache .. "/data/generated/" .. name local fh = io.open(path, "r") if not fh then return nil end fh:close() return assert(loadfile(path))() end local pokemon = loadGen("pokemon.lua") if not pokemon then check(true, "gold cache absent : name check only (SKIP the run-through)") S.finish() return end local Evolution = require("src.core.gen2.Evolution") local data = { pokemon = pokemon, moves = loadGen("moves.lua"), audio = {}, gen2Palettes = loadGen("palettes.lua") } -- CYNDAQUIL -> QUILAVA at 14, the exact evolution the bot wedged on. local mon = { species = "CYNDAQUIL", level = 14, hp = 20, maxHp = 20, moves = { { id = "EMBER", pp = 10, maxPp = 25 } } } local save = { party = { mon }, pokedex = { seen = {}, caught = {} } } local game = { data = data, save = save } local plan = Evolution.plan(data, { mon }, { [1] = true }, { timeOfDay = 1 }) eq(#plan, 1, "CYNDAQUIL at 14 has an evolution to play") local done = false local anim = EvolutionAnim.new(game, { mon = plan[1].mon, entry = plan[1].entry, index = plan[1].index, party = save.party, save = save, onDone = function() done = true end, }) -- The push, exactly as Screens.push does it: no extra arguments. This is the -- line that used to kill the screen. -- StateStack is used as a singleton (Game calls StateStack:init()), so the -- test drives it directly rather than inventing an instance it does not have. local StateStack = require("src.core.StateStack") StateStack:init() StateStack:push(anim) eq(anim.phase, "evolving", "being pushed does not clear the phase") -- Mashing A is what a bot (or an impatient player) does; it must not stall it. game.input = { wasPressed = function(_, b) return b == "a" end } local frames = 0 for _ = 1, 5000 do if done then break end frames = frames + 1 check(anim.phase ~= nil, "phase stays set while the animation runs") if anim.phase == nil then break end anim:update(1 / 60) end check(done, "the evolution runs to completion and calls onDone") check(frames < 2000, "and finishes promptly (" .. frames .. " frames)") eq(save.party[1].species, "QUILAVA", "the party record is written back") S.finish()