mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
260 lines
10 KiB
Lua
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")
|