Files
gen1recomp/tests/game3_battle_intro_test.lua
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

260 lines
10 KiB
Lua

#!/usr/bin/env luajit
-- Battle intro sequencer + trainer string / offset smoke tests.
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
print("[test] 1. Versions trainer offsets")
local Versions = require("src.import.gba.versions")
check(Versions.TRAINER_FRONT_PIC_TABLE == 0x23957C, "FRONT_PIC_TABLE")
check(Versions.TRAINER_FRONT_PIC_PAL_TABLE == 0x239A1C, "FRONT_PIC_PAL_TABLE")
check(Versions.TRAINER_BACK_PIC_TABLE == 0x239FA4, "BACK_PIC_TABLE")
check(Versions.TRAINERS_TABLE == 0x23EAC8, "TRAINERS_TABLE")
check(Versions.TRAINER_CLASS_NAMES == 0x23E558, "CLASS_NAMES")
check(Versions.BATTLE_UI and Versions.BATTLE_UI.party_summary_bar == 0xE7BB04, "party_summary_bar")
print("[test] 2. Trainer intro strings")
if require("tests.game3_cache").mount() then
local Trainers = require("src.core.game3.scripting.trainers")
local s = Trainers.introStrings(326, "SQUIRTLE")
check(s.wants == "RIVAL TERRY\nwould like to battle!\\p", "wants string")
check(s.sentOut == "RIVAL TERRY sent\nout SQUIRTLE!", "sentOut string")
local s2 = Trainers.introStrings(326, "SQUIRTLE", { rivalName = "BLUE" })
check(s2.wants:find("BLUE", 1, true) ~= nil, "rivalName override")
else
print("[skip] trainer 326 comes from the ROM trainer pack: " .. tostring(require("tests.game3_cache").reason))
end
if not require("tests.game3_cache").mount() then
print("[skip] the intro messages are ROM battle text: " .. tostring(require("tests.game3_cache").reason))
os.exit(failed > 0 and 1 or 0)
end
print("[test] 3. IntroSeq step shapes")
-- Stub love-less anim/task path via headless begin false; build via private tables
-- by starting a minimal state through Battle.start headless.
local State = require("src.core.game3.battle.state")
local IntroSeq = require("src.core.game3.battle.intro_seq")
local Anim = require("src.core.game3.battle.anim")
Anim.reset({ headless = false })
local stWild = State.new({
wild = true,
playerParty = { { species = 1, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } } },
foeMon = { species = 16, level = 3, hp = 15, maxHp = 15, moves = { 33 }, pp = { 35 } },
})
local msgs = {}
local okWild = IntroSeq.begin(stWild, {
pushMsg = function(t) msgs[#msgs + 1] = t end,
headless = false,
})
check(okWild == true, "wild begin")
local kinds = {}
for _, step in ipairs(IntroSeq._steps or {}) do
kinds[#kinds + 1] = step.kind
end
local function has(kind)
for _, k in ipairs(kinds) do if k == kind then return true end end
return false
end
check(has("bgslide") and has("player_throw"), "wild has bgslide+throw")
check(not has("partybar") and not has("trainerexit"), "wild has no partybar/exit")
local bg = IntroSeq._steps and IntroSeq._steps[2]
check(bg and bg.kind == "bgslide" and bg.data.slidePlayer and bg.data.slideEnemyMon,
"wild bgslide slides player + enemy mon")
check(msgs[1] == nil, "msgs deferred to update steps")
IntroSeq.reset()
msgs = {}
local stTr = State.new({
wild = false,
playerParty = { { species = 1, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } } },
foeMon = { species = 7, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } },
})
stTr.trainerId = 326
local okTr = IntroSeq.begin(stTr, {
pushMsg = function(t) msgs[#msgs + 1] = t end,
headless = false,
trainerId = 326,
trainerPicId = 106,
playerGender = 0,
})
check(okTr == true, "trainer begin")
kinds = {}
local partyStep
for _, step in ipairs(IntroSeq._steps or {}) do
kinds[#kinds + 1] = step.kind
if step.kind == "partybar" then partyStep = step end
end
check(has("partybar") and has("opponent_sendout"), "trainer party+sendout")
check(partyStep and #partyStep.data.playerBalls == 6, "player has 6 ball slots")
check(partyStep and partyStep.data.playerBalls[1] == "ok" and partyStep.data.playerBalls[2] == "empty", "player 1 mon has 1 ok on left and 5 empty")
check(partyStep and #partyStep.data.enemyBalls == 6, "opponent has 6 ball slots")
check(partyStep and partyStep.data.enemyBalls[1] == "empty" and partyStep.data.enemyBalls[6] == "ok", "opponent 1 mon has 5 empty on left and 1 ok on right")
-- Test opponent with 3 mons
local stTr3 = State.new({
wild = false,
playerParty = { { species = 1, hp = 20, maxHp = 20 } },
foeParty = { { species = 10, hp = 10 }, { species = 11, hp = 10 }, { species = 12, hp = 10 } },
})
IntroSeq.reset()
IntroSeq.begin(stTr3, { headless = false })
local partyStep3
for _, step in ipairs(IntroSeq._steps or {}) do
if step.kind == "partybar" then partyStep3 = step break end
end
check(partyStep3 and partyStep3.data.enemyBalls[1] == "empty" and partyStep3.data.enemyBalls[3] == "empty"
and partyStep3.data.enemyBalls[4] == "ok" and partyStep3.data.enemyBalls[5] == "ok" and partyStep3.data.enemyBalls[6] == "ok",
"opponent 3 mons has 3 empty on left and 3 ok on right")
-- pret: wants → sentOut → (then) IntroTrainerBallThrow (opponent_sendout)
local wi, si, xi
for i, k in ipairs(kinds) do
if k == "msg" and not wi then wi = i
elseif k == "msg" and wi and not si then si = i
elseif k == "opponent_sendout" then xi = i end
end
check(wi and si and xi and wi < si and si < xi, "msgs before opponent_sendout (wants/sentOut then sendout)")
local bgTr
for _, step in ipairs(IntroSeq._steps or {}) do
if step.kind == "bgslide" then bgTr = step break end
end
check(bgTr and bgTr.data.slidePlayer and bgTr.data.slideEnemy,
"trainer bgslide slides both trainers")
print("[test] 3b. Trainer stays until dialog drained")
local Ui = require("src.core.game3.battle.ui")
local Task = require("src.core.game3.task")
local Fade = require("src.ui.game3.fade")
local _fadeBegin = Fade.begin
Fade.begin = function(_, _, cb) if cb then cb() end end
Ui.reset({ headless = false })
Anim.reset({ headless = false })
IntroSeq.reset()
local held = {}
local stHold = State.new({
wild = false,
playerParty = { { species = 1, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } } },
foeMon = { species = 7, level = 5, hp = 20, maxHp = 20, moves = { 33 }, pp = { 35 } },
})
IntroSeq.begin(stHold, {
pushMsg = function(t)
held[#held + 1] = t
Ui.push(t)
end,
headless = false,
trainerId = 326,
trainerPicId = 106,
playerGender = 0,
})
local sawMsgWait = false
local exitedEarly = false
local playerMissing = false
for _ = 1, 400 do
Task.update(1 / 60)
Fade.tick(1 / 60)
Anim.update(1 / 60)
if IntroSeq._waitingMsg then
sawMsgWait = true
local te = Anim.stage().trainer.enemy
local tp = Anim.stage().trainer.player
if not te.visible then exitedEarly = true end
if not (tp.visible and math.abs(tp.ox or 999) < 1) then playerMissing = true end
break
end
if IntroSeq.update() then break end
end
Fade.begin = _fadeBegin
check(sawMsgWait == true, "intro waits on msg before trainerexit")
check(exitedEarly == false, "trainer still visible when dialog wait starts")
check(playerMissing == false, "player back sprite on-screen before wants dialog")
check(held[1] ~= nil and held[1]:find("battle", 1, true) ~= nil, "wants dialog queued first")
-- Drain first line; ensure trainerexit still not done until second line drained
Ui._queue = {}
Ui._showing = false
local Message = package.loaded["src.ui.game3.message"]
if Message and Message.close then Message.close() end
IntroSeq.update() -- should advance past first msg, push sentOut, wait again
check(IntroSeq._waitingMsg == true, "waits on sentOut msg")
check(Anim.stage().trainer.enemy.visible == true, "trainer still visible during sentOut")
print("[test] 4. Headless Battle.start strings")
local Battle = require("src.core.game3.battle")
-- Ensure not already active
if Battle.isActive and Battle.isActive() then
Battle.abort("win")
end
local okB, err = Battle.start({ playerName = "RED",
wild = false,
headless = true,
trainerId = 326,
playerParty = {
{ species = 4, level = 5, hp = 20, maxHp = 20, moves = { 10, 45 }, pp = { 35, 40 }, maxPp = { 35, 40 } },
},
foe = { species = 7, level = 5, trainerId = 326 },
})
check(okB == true, "headless start " .. tostring(err))
local log = Ui.log and Ui.log() or {}
local joined = table.concat(log, " || ")
check(joined:find("would like to battle", 1, true) ~= nil, "log has FRLG wants")
check(joined:find("wants\nto battle", 1, true) == nil, "log lacks Gen1 wants")
check(IntroSeq.busy() == false, "intro not busy headless")
print("[test] 5. Visibility + IntroSeq completes under Task/Fade")
if Battle.isActive and Battle.isActive() then Battle.abort("win") end
Anim.reset({ headless = false })
IntroSeq.reset()
Ui.reset({ headless = false })
okB = Battle.start({ playerName = "RED",
wild = true,
headless = false,
playerParty = {
{ species = 4, level = 5, hp = 20, maxHp = 20, moves = { 10 }, pp = { 35 }, maxPp = { 35 } },
},
foe = { species = 16, level = 3 },
onDone = function() end,
})
check(okB == true, "non-headless wild start")
local p = Anim.present("player")
local stage = Anim.stage()
check(p.visible == false, "player mon hidden at intro start")
check(stage.healthbox.player.visible == false, "player healthbox hidden")
-- Drive sequencer without Message UI (no love input): auto-dismiss text.
local MessageMod = package.loaded["src.ui.game3.message"]
local guard = 0
local done = false
while not done and guard < 2000 do
guard = guard + 1
Task.update(1 / 60)
Fade.tick(1 / 60)
Anim.update(1 / 60)
local okA, Audio = pcall(require, "src.core.game3.audio")
if okA and Audio and Audio.tickCry then Audio.tickCry(1 / 60) end
-- Instant-dismiss battle dialog so msg waits don't soft-lock the smoke test.
Ui._queue = {}
Ui._showing = false
if MessageMod and MessageMod.close then MessageMod.close() end
done = IntroSeq.update()
end
check(done == true, "IntroSeq completed in " .. guard .. " frames")
check(Anim.present("player").visible == true, "player visible after intro")
check(Anim.stage().healthbox.player.visible == true, "player healthbox visible after intro")
if Battle.isActive and Battle.isActive() then Battle.abort("win") end
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")