mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
5ad701022b
BattleTransition.pickTrainer told the Elite Four and the champion by class
names ("ELITE_FOUR", "CHAMPION", "RIVAL") or by class ids 57 and 58, and
the Elite Four members by trainer ids 412-419. None of these is
FireRed's: TRAINER_CLASS_ELITE_FOUR is 87, TRAINER_CLASS_CHAMPION 90, and
Lorelei, Bruno, Agatha and Lance are trainers 410-413 (735-738 for the
rematch). The bridge also never passed the class, so these battles always
got the terrain transition. The bridge now hands over the foe's class id
and pickTrainer follows GetTrainerBattleTransition
(pokefirered/src/battle_setup.c:624): the Elite Four member's own
transition, BLUE for the champion, and none for the rival's class (a
caller's isRival flag still asks for BLUE).
A Trainer Tower or e-Reader foe carries a facility class instead, whose
numbers overlap the trainer classes (FACILITY_CLASS_LASS is 90, the
champion's class), and pret never picks their transition by class
(battle_setup.c:660), so pickTrainer ignores the class for them.
200 lines
9.9 KiB
Lua
200 lines
9.9 KiB
Lua
#!/usr/bin/env luajit
|
|
-- Battle transition table parity, selection, asset contract, and runner 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. Transition IDs & Constants Parity (battle_transition.h)")
|
|
local BattleTransition = require("src.core.game3.battle_transition")
|
|
local ID = BattleTransition.ID
|
|
check(ID.BLUR == 0, "BLUR = 0")
|
|
check(ID.SWIRL == 1, "SWIRL = 1")
|
|
check(ID.SHUFFLE == 2, "SHUFFLE = 2")
|
|
check(ID.BIG_POKEBALL == 3, "BIG_POKEBALL = 3")
|
|
check(ID.POKEBALLS_TRAIL == 4, "POKEBALLS_TRAIL = 4")
|
|
check(ID.CLOCKWISE_WIPE == 5, "CLOCKWISE_WIPE = 5")
|
|
check(ID.RIPPLE == 6, "RIPPLE = 6")
|
|
check(ID.WAVE == 7, "WAVE = 7")
|
|
check(ID.SLICE == 8, "SLICE = 8")
|
|
check(ID.WHITE_BARS_FADE == 9, "WHITE_BARS_FADE = 9")
|
|
check(ID.GRID_SQUARES == 10, "GRID_SQUARES = 10")
|
|
check(ID.ANGLED_WIPES == 11, "ANGLED_WIPES = 11")
|
|
check(ID.LORELEI == 12, "LORELEI = 12")
|
|
check(ID.BRUNO == 13, "BRUNO = 13")
|
|
check(ID.AGATHA == 14, "AGATHA = 14")
|
|
check(ID.LANCE == 15, "LANCE = 15")
|
|
check(ID.BLUE == 16, "BLUE = 16")
|
|
check(ID.SPIRAL == 17, "SPIRAL = 17")
|
|
|
|
print("[test] 2. Map Terrain Classification (battle_setup.c)")
|
|
local TERRAIN = BattleTransition.TERRAIN
|
|
check(BattleTransition.getTerrainByMap({ flashLevel = 1 }) == TERRAIN.FLASH, "flash terrain")
|
|
check(BattleTransition.getTerrainByMap({ surfing = true }) == TERRAIN.WATER, "water terrain (surfing)")
|
|
check(BattleTransition.getTerrainByMap({ mapKind = "water" }) == TERRAIN.WATER, "water terrain (mapKind)")
|
|
check(BattleTransition.getTerrainByMap({ isCave = true }) == TERRAIN.CAVE, "cave terrain (isCave)")
|
|
check(BattleTransition.getTerrainByMap({ mapKind = "dungeon" }) == TERRAIN.CAVE, "cave terrain (dungeon)")
|
|
check(BattleTransition.getTerrainByMap({}) == TERRAIN.NORMAL, "normal terrain default")
|
|
|
|
print("[test] 3. Wild Battle Transition Selection (battle_setup.c sBattleTransitionTable_Wild)")
|
|
-- Normal
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.NORMAL, playerLevel = 10, enemyLevel = 5 }) == ID.SLICE,
|
|
"wild normal low-level foe -> SLICE")
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.NORMAL, playerLevel = 5, enemyLevel = 10 }) == ID.WHITE_BARS_FADE,
|
|
"wild normal high-level foe -> WHITE_BARS_FADE")
|
|
-- Cave
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.CAVE, playerLevel = 10, enemyLevel = 5 }) == ID.CLOCKWISE_WIPE,
|
|
"wild cave low-level foe -> CLOCKWISE_WIPE")
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.CAVE, playerLevel = 5, enemyLevel = 10 }) == ID.GRID_SQUARES,
|
|
"wild cave high-level foe -> GRID_SQUARES")
|
|
-- Flash
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.FLASH, playerLevel = 10, enemyLevel = 5 }) == ID.BLUR,
|
|
"wild flash low-level foe -> BLUR")
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.FLASH, playerLevel = 5, enemyLevel = 10 }) == ID.GRID_SQUARES,
|
|
"wild flash high-level foe -> GRID_SQUARES")
|
|
-- Water
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.WATER, playerLevel = 10, enemyLevel = 5 }) == ID.WAVE,
|
|
"wild water low-level foe -> WAVE")
|
|
check(BattleTransition.pickWild({ terrain = TERRAIN.WATER, playerLevel = 5, enemyLevel = 10 }) == ID.RIPPLE,
|
|
"wild water high-level foe -> RIPPLE")
|
|
|
|
print("[test] 4. Trainer Battle Transition Selection (battle_setup.c sBattleTransitionTable_Trainer)")
|
|
-- Normal
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.NORMAL, playerLevel = 10, enemyLevel = 5 }) == ID.POKEBALLS_TRAIL,
|
|
"trainer normal low-level foe -> POKEBALLS_TRAIL")
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.NORMAL, playerLevel = 5, enemyLevel = 10 }) == ID.ANGLED_WIPES,
|
|
"trainer normal high-level foe -> ANGLED_WIPES")
|
|
-- Cave
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.CAVE, playerLevel = 10, enemyLevel = 5 }) == ID.SHUFFLE,
|
|
"trainer cave low-level foe -> SHUFFLE")
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.CAVE, playerLevel = 5, enemyLevel = 10 }) == ID.BIG_POKEBALL,
|
|
"trainer cave high-level foe -> BIG_POKEBALL")
|
|
-- Flash
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.FLASH, playerLevel = 10, enemyLevel = 5 }) == ID.BLUR,
|
|
"trainer flash low-level foe -> BLUR")
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.FLASH, playerLevel = 5, enemyLevel = 10 }) == ID.GRID_SQUARES,
|
|
"trainer flash high-level foe -> GRID_SQUARES")
|
|
-- Water
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.WATER, playerLevel = 10, enemyLevel = 5 }) == ID.SWIRL,
|
|
"trainer water low-level foe -> SWIRL")
|
|
check(BattleTransition.pickTrainer({ terrain = TERRAIN.WATER, playerLevel = 5, enemyLevel = 10 }) == ID.RIPPLE,
|
|
"trainer water high-level foe -> RIPPLE")
|
|
|
|
print("[test] 5. Elite Four & Rival Mugshots Routing")
|
|
-- pokefirered/src/battle_setup.c:633, opponents.h:416-419 and :741-744
|
|
check(BattleTransition.pickTrainer({ trainerClass = 87, trainerId = 410 }) == ID.LORELEI, "Lorelei -> LORELEI")
|
|
check(BattleTransition.pickTrainer({ trainerClass = 87, trainerId = 411 }) == ID.BRUNO, "Bruno -> BRUNO")
|
|
check(BattleTransition.pickTrainer({ trainerClass = 87, trainerId = 412 }) == ID.AGATHA, "Agatha -> AGATHA")
|
|
check(BattleTransition.pickTrainer({ trainerClass = 87, trainerId = 413 }) == ID.LANCE, "Lance -> LANCE")
|
|
check(BattleTransition.pickTrainer({ trainerClass = 87, trainerId = 737 }) == ID.AGATHA, "Agatha's rematch -> AGATHA")
|
|
check(BattleTransition.pickTrainer({ trainerClass = 87, trainerId = 999 }) == ID.BLUE, "other Elite Four -> BLUE")
|
|
check(BattleTransition.pickTrainer({ trainerClass = 90, trainerId = 438 }) == ID.BLUE, "Champion -> BLUE")
|
|
check(BattleTransition.pickTrainer({ trainerClass = 81, trainerId = 326, terrain = TERRAIN.NORMAL,
|
|
playerLevel = 5, enemyLevel = 5 }) ~= ID.BLUE, "an early rival battle keeps the terrain transition")
|
|
check(BattleTransition.pickTrainer({ trainerClass = "CHAMPION", playerLevel = 5, enemyLevel = 5 }) ~= ID.BLUE,
|
|
"a class is recognised by its id, not by its (translatable) name")
|
|
-- pokefirered/src/trainer_tower_sets.c:8663 MIKAELA, FACILITY_CLASS_LASS (90)
|
|
check(BattleTransition.pickTrainer({ trainerTower = true, trainerClass = 90, trainerId = 0,
|
|
playerLevel = 5, enemyLevel = 5 }) ~= ID.BLUE, "a Trainer Tower LASS is not taken for the champion")
|
|
check(BattleTransition.pickTrainer({ eReader = true, trainerClass = 87, trainerId = 0,
|
|
playerLevel = 5, enemyLevel = 5 }) ~= ID.BLUE, "nor an e-Reader trainer for the Elite Four")
|
|
check(BattleTransition.pickTrainer({ isRival = true }) == ID.BLUE, "Rival -> BLUE")
|
|
|
|
print("[test] 6. Cache Contract & Extract Pipeline")
|
|
local CacheContract = require("src.import.CacheContract")
|
|
local req = CacheContract.requiredFilesFor("firered")
|
|
local reqSet = {}
|
|
for _, f in ipairs(req) do reqSet[f] = true end
|
|
check(reqSet["data/generated/gba/pokemon/battle_transition/manifest.lua"] == true, "contract has manifest.lua")
|
|
check(reqSet["data/generated/gba/pokemon/battle_transition/big_pokeball.rgba"] == true, "contract has big_pokeball.rgba")
|
|
check(reqSet["data/generated/gba/pokemon/battle_transition/sliding_pokeball.rgba"] == true, "contract has sliding_pokeball.rgba")
|
|
|
|
print("[test] 7. Transition Runner State Lifecycle")
|
|
local doneCalled = false
|
|
BattleTransition.start(ID.SLICE, { headless = true }, function()
|
|
doneCalled = true
|
|
end)
|
|
check(doneCalled == true, "headless transition finishes immediately")
|
|
|
|
-- Non-headless simulated tick cycle
|
|
doneCalled = false
|
|
BattleTransition.start(ID.SLICE, { skipIntro = false }, function()
|
|
doneCalled = true
|
|
end)
|
|
check(BattleTransition.isActive() == true, "transition active on start")
|
|
check(BattleTransition._phase == "intro", "starts in intro phase")
|
|
|
|
-- src/battle_transition.c:703
|
|
for f = 1, 33 do
|
|
BattleTransition.tick(1 / 60)
|
|
end
|
|
check(BattleTransition._phase == "intro", "intro still running after 33 frames")
|
|
BattleTransition.tick(1 / 60)
|
|
check(BattleTransition._phase == "main", "advances to main phase after 34 intro frames")
|
|
|
|
local ticks = 0
|
|
while BattleTransition.isActive() and ticks < 300 do
|
|
BattleTransition.tick(1 / 60)
|
|
ticks = ticks + 1
|
|
end
|
|
check(doneCalled == true, "transition completes and invokes done callback")
|
|
check(BattleTransition.isActive() == false, "transition inactive after finish")
|
|
|
|
print("[test] 8. Coverage never stops short or reverses")
|
|
local function run(id)
|
|
local covs, finished = {}, false
|
|
BattleTransition.start(id, { skipIntro = true }, function() finished = true end)
|
|
local guard = 0
|
|
while not finished and guard < 600 do
|
|
BattleTransition.tick(1 / 60)
|
|
if BattleTransition._fx then covs[#covs + 1] = BattleTransition.screenCoverage() end
|
|
guard = guard + 1
|
|
end
|
|
return covs, finished
|
|
end
|
|
|
|
local covs, finished = run(ID.SLICE)
|
|
local monotonic = true
|
|
for i = 2, #covs do
|
|
if covs[i] < covs[i - 1] then monotonic = false end
|
|
end
|
|
check(finished, "SLICE finishes")
|
|
check(covs[1] < 0.02, "SLICE starts uncovered (no instant black)")
|
|
check(monotonic, "SLICE coverage never reverses")
|
|
check(covs[#covs] == 1, "SLICE ends fully covered")
|
|
local firstFull
|
|
for i, c in ipairs(covs) do
|
|
if c == 1 and not firstFull then firstFull = i end
|
|
end
|
|
check(firstFull ~= nil and #covs - firstFull <= 2, "SLICE hands off right after full coverage")
|
|
|
|
for _, id in ipairs({ ID.WAVE, ID.ANGLED_WIPES, ID.CLOCKWISE_WIPE, ID.GRID_SQUARES, ID.WHITE_BARS_FADE,
|
|
ID.BLUR, ID.SWIRL, ID.SHUFFLE, ID.RIPPLE, ID.BIG_POKEBALL, ID.POKEBALLS_TRAIL, ID.BLUE }) do
|
|
local c, done = run(id)
|
|
check(done and c[#c] == 1, "transition " .. id .. " ends on a black screen")
|
|
end
|
|
|
|
print("[test] 9. SLICE rows extend to the full window width")
|
|
BattleTransition.start(ID.SLICE, { skipIntro = true })
|
|
for _ = 1, 20 do BattleTransition.tick(1 / 60) end
|
|
local even = BattleTransition.blackSpans(0, -120, 360)
|
|
local odd = BattleTransition.blackSpans(1, -120, 360)
|
|
check(even[1] == -120 and even[2] > -120, "even rows grow from the left window edge")
|
|
check(odd[2] == 360 and odd[1] < 360, "odd rows grow from the right window edge")
|
|
BattleTransition.abort()
|
|
|
|
if failed > 0 then
|
|
print(string.format("[FAIL] %d test(s) failed", failed))
|
|
os.exit(1)
|
|
else
|
|
print("[test] all passed")
|
|
end
|