From 4250e3aa32416f4705ef821ca4af06068ce8847c Mon Sep 17 00:00:00 2001 From: johnjohto Date: Thu, 30 Jul 2026 10:25:08 -0400 Subject: [PATCH] Delay evolution until trainer text ends --- src/script/Commands.lua | 13 +++- src/script/ScriptRunner.lua | 4 ++ tests/parity_trainer_evolution_order.lua | 84 ++++++++++++++++++++++++ tests/run_tests.lua | 1 + 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 tests/parity_trainer_evolution_order.lua diff --git a/src/script/Commands.lua b/src/script/Commands.lua index 0f95614c..329563f5 100644 --- a/src/script/Commands.lua +++ b/src/script/Commands.lua @@ -265,7 +265,18 @@ function Commands.start_battle(ctx, kind, a, b) ctx.lastBattleResult = result ctx.lastCheck = result == "win" if ctx.overworld then - ctx.overworld:afterBattle(result, battle) + -- A map script often follows a trainer battle with its own text. + -- Keep a level evolution behind that text: otherwise afterBattle + -- pushes the evolution screen, then this runner resumes and pushes + -- the trainer's text on top of it. + if result == "win" then + ctx.afterScript = ctx.afterScript or {} + table.insert(ctx.afterScript, function() + ctx.overworld:afterBattle(result, battle) + end) + else + ctx.overworld:afterBattle(result, battle) + end end runner:resume() end diff --git a/src/script/ScriptRunner.lua b/src/script/ScriptRunner.lua index 94fd1ffa..5ad244ea 100644 --- a/src/script/ScriptRunner.lua +++ b/src/script/ScriptRunner.lua @@ -125,6 +125,10 @@ function ScriptRunner:run(script, extra) end self.co = coroutine.create(function() self:exec(script, ctx) + -- Commands can defer a game action until the script's own dialogue is + -- done. start_battle uses this for win-path evolutions, which must not + -- be covered by post-battle trainer text. + for _, callback in ipairs(ctx.afterScript or {}) do callback() end if ctx.onDone then ctx.onDone() end if Runtime.wants("script.ended") then Runtime.emit("script.ended", { ctx = ctx, completed = true }) diff --git a/tests/parity_trainer_evolution_order.lua b/tests/parity_trainer_evolution_order.lua new file mode 100644 index 00000000..f4f39b8e --- /dev/null +++ b/tests/parity_trainer_evolution_order.lua @@ -0,0 +1,84 @@ +-- A trainer script's post-battle text must finish before a level evolution. +-- Its start_battle row resumes the script after OverworldState:afterBattle, +-- so this covers the same handoff used by trainer and Rocket encounters. + +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end + +local Data = require("src.core.Data") +if not Data.maps then Data:load() end +local S = require("tests.harness").suite("parity trainer evolution order") +local check = S.check + +local Game = require("src.core.Game") +local SaveData = require("src.core.SaveData") +local StateStack = require("src.core.StateStack") +local ScriptRunner = require("src.script.ScriptRunner") +local OverworldState = require("src.world.OverworldController") +local Pokemon = require("src.pokemon.Pokemon") +require("src.render.Font").load(Data) + +Game.data = Data +Game.save = SaveData.newGame() +Game.stack = StateStack; StateStack:init() +Game.renderer = { worldViewSize = function() return 160, 144 end } + +local caterpie = Pokemon.new(Data, "CATERPIE", 7) +Game.save.party = { caterpie } + +local fakeBattle +local originalBattleState = package.loaded["src.battle.BattleState"] +package.loaded["src.battle.BattleState"] = { + newTrainer = function() + fakeBattle = { leveledUp = { [caterpie] = true } } + return fakeBattle + end, +} + +local overworld = setmetatable({}, { __index = OverworldState }) +overworld:enter("ROUTE_1", 5, 5, "down") +local runner = ScriptRunner.new(Game, overworld) +runner:run({ + { "start_battle", "trainer", "OPP_YOUNGSTER", 1 }, + { "show_text", "THE TRAINER TALKS AFTER THE FIGHT" }, + { "show_text", "THE TRAINER HAS MORE TO SAY" }, +}) + +check(fakeBattle ~= nil, "trainer battle starts and yields the script") +Game.stack:pop() +fakeBattle.onFinish("win") + +package.loaded["src.battle.BattleState"] = originalBattleState + +local function stateHas(state, text) + for _, page in ipairs(state.pages or {}) do + for _, line in ipairs(page) do + if type(line) == "string" and line:find(text, 1, true) then return true end + end + end + return false +end + +local afterText = Game.stack:top() +check(stateHas(afterText, "THE TRAINER TALKS"), + "the trainer's after-battle text is shown first") +check(#Game.stack.states == 1, + "the evolution screen waits until trainer after-text closes") + +Game.stack:pop() +afterText.onDone() + +local moreText = Game.stack:top() +check(stateHas(moreText, "MORE TO SAY"), + "all trainer after-text finishes before evolution") +check(#Game.stack.states == 1, + "the evolution still waits for the last trainer text") + +Game.stack:pop() +moreText.onDone() + +local evolutionText = Game.stack:top() +check(stateHas(evolutionText, "evolving"), + "the level evolution starts after trainer after-text closes") + +S.finish() diff --git a/tests/run_tests.lua b/tests/run_tests.lua index 1df4e706..1590c258 100644 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -3279,6 +3279,7 @@ runSuites(orderedGlob("tests/parity_*.lua", { "tests/parity_hof.lua", "tests/parity_trade_gift.lua", "tests/parity_yellow_trades.lua", "tests/parity_yellow_bills_pikachu.lua", + "tests/parity_trainer_evolution_order.lua", "tests/parity_intro.lua", "tests/parity_tilt.lua", "tests/parity_gbcfx.lua", }))