Files
gen1recomp/tests/game3_battle_anims_phase4_test.lua
bryanthaboi d920f7acdb clean up
2026-09-22 17:05:12 -04:00

501 lines
19 KiB
Lua

-- tests/game3_battle_anims_phase4_test.lua
-- Unit and integration tests for Phase 4: Dynamic Backgrounds, Clones, Distortions, Spotlights, Substitute & Evaluators
package.path = package.path .. ";./?.lua"
require("tests.game3_cache").requireData("game3_battle_anims_phase4_test", "pokemon/battle_anims/pack.lua")
local AnimTasks = require("src.core.game3.battle.anim_tasks")
local AnimSprites = require("src.core.game3.battle.anim_sprites")
local AnimCallbacks = require("src.core.game3.battle.anim_callbacks")
local AnimVm = require("src.core.game3.battle.anim_vm")
local Anim = require("src.core.game3.battle.anim")
local passed = 0
local failed = 0
local function test(name, fn)
AnimTasks.reset()
AnimSprites.reset()
Anim.reset({ headless = true })
local ok, err = pcall(fn)
if ok then
passed = passed + 1
print(" PASS: " .. name)
else
failed = failed + 1
print(" FAIL: " .. name .. " -> " .. tostring(err))
end
end
local function assert_eq(a, b, msg)
if a ~= b then
error(string.format("%s: expected %s, got %s", msg or "assert_eq failed", tostring(b), tostring(a)), 2)
end
end
local function assert_true(val, msg)
if not val then
error(msg or "assert_true failed", 2)
end
end
local function make_vm(attackerSide)
attackerSide = attackerSide or "player"
local targetSide = (attackerSide == "player") and "enemy" or "player"
local vm = AnimVm.new()
vm._attackerSide = attackerSide
vm._targetSide = targetSide
return vm
end
print("=== Phase 4: Dynamic Backgrounds, Clones, Distortions & Specialized Systems Tests ===")
-- ---------------------------------------------------------------------------
-- 1. Dynamic Scrolling Backgrounds & High-Altitude Environments
-- ---------------------------------------------------------------------------
test("MoveSkyUppercutBg - vertical ascending clouds background lifetime and draw hook", function()
local vm = make_vm("player")
-- pokefirered/src/battle_anim_fight.c:934
local t = AnimTasks.spawn("MoveSkyUppercutBg", 2, { 55 }, vm)
assert_true(t ~= nil and t.active, "task should be spawned")
for f = 1, 36 do
AnimTasks.update(vm)
assert_true(t.active, "should remain active at frame " .. f)
end
assert_true(vm.bg3 and vm.bg3.y ~= 0, "BG3 scrolls vertically")
vm.args[7] = -1
AnimTasks.update(vm)
assert_true(not t.active, "should destroy once args[7] is -1")
assert_eq(vm.bg3.x, 0, "BG3 x reset")
end)
test("MoveSeismicTossBg - accelerating descent space background", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("MoveSeismicTossBg", 2, {}, vm)
assert_true(t ~= nil and t.active, "task should be spawned")
AnimTasks.update(vm)
-- pokefirered/src/battle_anim_rock.c:788
assert_eq(vm.bg3.y, 20, "BG3 scrolls by data[1] / 10")
for f = 2, 120 do
AnimTasks.update(vm)
assert_true(t.active, "should remain active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not t.active, "should destroy after duration")
end)
test("PositionFissureBgOnBattler - ground abyss fissure", function()
local vm = make_vm("player")
vm.args[0], vm.args[1], vm.args[2] = 1, 5, -1
local t = AnimTasks.spawn("PositionFissureBgOnBattler", 2, {}, vm)
assert_true(t ~= nil and t.active, "task should be spawned")
AnimTasks.update(vm)
-- pokefirered/src/battle_anim_ground.c:719
assert_true(not t.active, "task hands off to WaitForFissureCompletion at once")
assert_eq(vm.bg3.x, (32 - 176) % 512, "BG3 x centred on the target")
for _ = 1, 40 do
AnimTasks.update(vm)
end
assert_eq(vm.bg3.x, (32 - 176) % 512, "BG3 held until args[7] terminator")
vm.args[7] = -1
AnimTasks.update(vm)
assert_eq(vm.bg3.x, 0, "BG3 released")
assert_eq(AnimTasks.activeCount(), 0, "fissure helper finished")
end)
-- pokefirered/src/battle_anim_effects_3.c:1356
test("SetPsychicBackground - runs until gBattleAnimArgs[7] == 0xFFFF", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("SetPsychicBackground", 2, {}, vm)
assert_true(t ~= nil and t.active, "task should be spawned")
for f = 1, 32 do
AnimTasks.update(vm)
assert_true(t.active, "should remain active at frame " .. f)
end
vm.args[7] = -1
AnimTasks.update(vm)
assert_true(not t.active, "should destroy once args[7] is 0xFFFF")
end)
test("HeartsBackground - floating hearts background for Attract", function()
local vm = make_vm("player")
-- pokefirered/src/battle_anim_effects_2.c:3290
local t = AnimTasks.spawn("HeartsBackground", 2, {}, vm)
assert_true(t ~= nil and t.active, "task should be spawned")
for f = 1, 271 do
AnimTasks.update(vm)
assert_true(t.active, "should remain active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not t.active, "should destroy after duration")
end)
test("ScaryFace - looming demonic phantom mask", function()
local vm = make_vm("player")
-- pokefirered/src/battle_anim_effects_2.c:3378
local t = AnimTasks.spawn("ScaryFace", 2, {}, vm)
assert_true(t ~= nil and t.active, "task should be spawned")
AnimTasks.update(vm)
assert_true(t.z > AnimSprites.Z.PLAYER_MON, "BG1 priority 1 layers over battler sprites")
for f = 2, 78 do
AnimTasks.update(vm)
assert_true(t.active, "should remain active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not t.active, "should destroy after duration")
end)
test("CreateRaindrops - diagonal falling raindrop particles", function()
local vm = make_vm("player")
vm.args[0], vm.args[1], vm.args[2] = 0, 3, 60
local t = AnimTasks.spawn("CreateRaindrops", 2, {}, vm)
assert_true(t ~= nil and t.active, "task should be spawned")
-- pokefirered/src/battle_anim_water.c:475
AnimTasks.update(vm)
assert_eq(AnimSprites.activeCount(), 1, "first raindrop on frame 1")
local drop
for i = 1, AnimSprites.MAX do
if AnimSprites._pool[i].active then drop = AnimSprites._pool[i] end
end
assert_true(drop and drop.z > 200, "raindrop subpriority 4 draws in front of both mons")
for f = 2, 59 do
AnimTasks.update(vm)
AnimSprites.update()
assert_true(t.active, "should remain active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not t.active, "should destroy after duration")
end)
-- ---------------------------------------------------------------------------
-- 2. Shadow Clones, Afterimages & Silhouette Transfers
-- ---------------------------------------------------------------------------
-- pokefirered/src/battle_anim_ghost.c:335
test("NightShadeClone & NightmareClone - pret blend/scale timing", function()
local vm = make_vm("player")
local t1 = AnimTasks.spawn("NightShadeClone", 2, {}, vm)
assert_true(t1 ~= nil and t1.active, "NightShadeClone should spawn")
for f = 1, 43 do
AnimTasks.update(vm)
assert_true(t1.active, "should remain active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not t1.active, "should destroy after 1 + 27 blend + 16 scale frames")
local p = Anim.present("player")
assert_eq(p.sx, 1, "attacker scale restored")
local t2 = AnimTasks.spawn("NightmareClone", 2, {}, vm)
assert_true(t2 ~= nil and t2.active, "NightmareClone should spawn")
for _ = 1, 84 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_true(t2.active, "NightmareClone still fading at frame 84")
AnimTasks.update(vm)
assert_true(not t2.active, "NightmareClone done after 85 frames")
end)
-- pokefirered/src/battle_anim_ghost.c:786
test("DestinyBondWhiteShadow - pret blend in/out timing", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("DestinyBondWhiteShadow", 2, {}, vm)
assert_true(t ~= nil and t.active, "DestinyBondWhiteShadow should spawn")
for f = 1, 98 do
AnimTasks.update(vm)
assert_true(t.active, "should remain active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not t.active, "should destroy after 99 frames")
end)
-- pokefirered/src/battle_anim_dark.c:729
test("InitMementoShadow - one-shot setup task", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("InitMementoShadow", 2, {}, vm)
assert_true(t ~= nil and t.active, "InitMementoShadow should spawn")
AnimTasks.update(vm)
assert_true(not t.active, "InitMementoShadow finishes on its first call")
end)
-- pokefirered/src/battle_anim_effects_3.c:3122
test("RolePlaySilhouette - 30 frame fade then 9 frame squash", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("RolePlaySilhouette", 2, {}, vm)
assert_true(t ~= nil and t.active, "RolePlaySilhouette should spawn")
for f = 1, 40 do
AnimTasks.update(vm)
AnimSprites.update()
assert_true(t.active, "should remain active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not t.active, "should destroy after 41 frames")
end)
-- ---------------------------------------------------------------------------
-- 3. Screen Distortions, Spotlights & Combat FX
-- ---------------------------------------------------------------------------
-- pokefirered/src/battle_anim_psychic.c:891
test("ExtrasensoryDistortion - scanline HOFS on the monbg target, no screen tint", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("ExtrasensoryDistortion", 2, {}, vm)
assert_true(t ~= nil and t.active, "ExtrasensoryDistortion should spawn")
for _ = 1, 26 do AnimTasks.update(vm) end
assert_true(t.active, "still distorting at frame 26")
AnimTasks.update(vm)
assert_true(not t.active, "should destroy after 27 frames")
assert_eq(Anim.screenEffect().type, "none", "never tints the whole screen")
assert_eq(Anim.present("enemy").hShift, nil, "scanline shift cleared")
end)
-- pokefirered/src/battle_anim_effects_3.c:1653
test("CreateSpotlight / RemoveSpotlight - WIN1 text-area window on, then off", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("CreateSpotlight", 2, {}, vm)
assert_true(t ~= nil and t.active, "CreateSpotlight should spawn")
AnimTasks.update(vm)
assert_true(not t.active, "CreateSpotlight is a one-shot register task")
assert_true(vm._g3win and vm._g3win.win1 ~= nil, "WIN1 covers the text area")
local r = AnimTasks.spawn("RemoveSpotlight", 2, {}, vm)
AnimTasks.update(vm)
assert_true(not r.active, "RemoveSpotlight is a one-shot register task")
assert_eq(vm._g3win.win1, nil, "WIN1 removed")
end)
test("MorningSunLightBeam & GlareEyeDots - descending sunbeams and crimson eyes", function()
local vm = make_vm("player")
-- pokefirered/src/battle_anim_effects_3.c:2315
local tSun = AnimTasks.spawn("MorningSunLightBeam", 2, {}, vm)
assert_true(tSun ~= nil and tSun.active, "MorningSunLightBeam should spawn")
for _ = 1, 157 do AnimTasks.update(vm) end
assert_true(tSun.active, "MorningSunLightBeam still running at 157")
AnimTasks.update(vm)
assert_true(not tSun.active, "MorningSunLightBeam should destroy after 158 frames")
-- pokefirered/src/battle_anim_effects_3.c:3904
local tGlare = AnimTasks.spawn("GlareEyeDots", 2, {}, vm)
assert_true(tGlare ~= nil and tGlare.active, "GlareEyeDots should spawn")
for _ = 1, 90 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_true(not tGlare.active, "GlareEyeDots should destroy once all 26 dots expired")
end)
test("Generic combat FX tasks - ElectricCharging, DrillPeck, GrudgeFlames, BarrageBall", function()
local vm = make_vm("player")
local list = {
"ElectricChargingParticles", "RotateAuroraRingColors"
}
for _, name in ipairs(list) do
vm.args[0] = (name == "RotateAuroraRingColors") and 20 or 0
local t = AnimTasks.spawn(name, 2, {}, vm)
assert_true(t ~= nil and t.active, "Task " .. name .. " should spawn")
for _ = 1, 25 do AnimTasks.update(vm) end
assert_true(not t.active, "Task " .. name .. " should finish cleanly in duration")
end
-- pokefirered/src/battle_anim_effects_1.c:4896
vm.args[7] = 0
local tc = AnimTasks.spawn("ConversionAlphaBlend", 2, {}, vm)
for _ = 1, 65 do AnimTasks.update(vm) end
assert_true(tc.active, "ConversionAlphaBlend still fading at frame 65")
assert_eq(vm.args[7], -1, "ConversionAlphaBlend signals the script via args[7] once the blend is full")
AnimTasks.update(vm)
assert_true(not tc.active, "ConversionAlphaBlend ends after 16 steps of 4 frames plus 2")
end)
-- pokefirered/src/battle_anim_flying.c:1025
test("G2 combat FX tasks - pret frame counts", function()
local vm = make_vm("player")
local tPeck = AnimTasks.spawn("DrillPeckHitSplats", 2, {}, vm)
for f = 1, 31 do
AnimTasks.update(vm)
AnimSprites.update()
assert_true(tPeck.active, "DrillPeckHitSplats active at frame " .. f)
end
AnimTasks.update(vm)
assert_true(not tPeck.active, "DrillPeckHitSplats ends after 32 frames")
local tSketch = AnimTasks.spawn("SketchDrawMon", 2, {}, vm)
for _ = 1, 21 + 4 * 64 do AnimTasks.update(vm) end
assert_true(not tSketch.active, "SketchDrawMon ends after 21 + 4 * height frames")
end)
-- pokefirered/src/battle_anim_ghost.c:1142
test("G3 combat FX tasks - pret frame counts", function()
local vm = make_vm("player")
local pretFrames = {
GrudgeFlames = 89, BarrageBall = 59, StatusClearedEffect = 91,
ImprisonOrbs = 80, SkillSwap = 97, Teleport = 30,
}
for name, frames in pairs(pretFrames) do
AnimTasks.reset()
AnimSprites.reset()
local t = AnimTasks.spawn(name, 2, {}, vm)
assert_true(t ~= nil and t.active, "Task " .. name .. " should spawn")
for _ = 1, frames - 1 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_true(t.active, "Task " .. name .. " still running at frame " .. (frames - 1))
AnimTasks.update(vm)
AnimSprites.update()
assert_true(not t.active, "Task " .. name .. " finishes at pret frame " .. frames)
end
end)
-- ---------------------------------------------------------------------------
-- 4. Battler Movement & Substitute Doll Swap
-- ---------------------------------------------------------------------------
test("DoubleTeam - horizontal jitter, alpha pulse and clean restoration", function()
local vm = make_vm("player")
local p = Anim.present("player")
p.ox = 0
p.alpha = 1.0
-- pokefirered/src/battle_anim_effects_1.c:5209
AnimSprites.reset()
local t = AnimTasks.spawn("DoubleTeam", 2, { 30 }, vm)
assert_true(t ~= nil and t.active, "DoubleTeam should spawn")
local cloneOx = 0
for _ = 1, 130 do
AnimTasks.update(vm)
AnimSprites.update()
for i = 1, AnimSprites.MAX do
local s = AnimSprites._pool[i]
if s.active then cloneOx = math.max(cloneOx, math.abs(s.ox or 0)) end
end
end
assert_true(cloneOx > 0, "DoubleTeam clones sway around the battler")
assert_true(t.active, "DoubleTeam still running while its two clones live")
AnimTasks.update(vm)
AnimSprites.update()
assert_true(not t.active, "DoubleTeam ends when both clones expire (65 steps of 2 frames)")
assert_eq(p.ox, 0, "DoubleTeam leaves the battler in place")
assert_eq(p.alpha, 1.0, "DoubleTeam leaves the battler opaque")
end)
test("MonToSubstitute - squash and stretch scaling and clean restoration", function()
local vm = make_vm("player")
local p = Anim.present("player")
p.sx = 1.0
p.sy = 1.0
-- pokefirered/src/battle_anim_effects_3.c:4681
local t = AnimTasks.spawn("MonToSubstitute", 2, {}, vm)
assert_true(t ~= nil and t.active, "MonToSubstitute should spawn")
for _ = 1, 8 do AnimTasks.update(vm) end
assert_true(p.sx < 1.0 and p.sy > 1.0, "matrix x grows by 0x60 and y shrinks by 0xD: narrower and taller")
for _ = 9, 67 do AnimTasks.update(vm) end
assert_true(t.active, "doll still dropping at frame 67")
AnimTasks.update(vm)
assert_true(not t.active, "MonToSubstitute finishes after the doll drop and bounce (68 frames)")
assert_eq(p.sx, 1.0, "sx must restore to 1.0")
assert_eq(p.sy, 1.0, "sy must restore to 1.0")
end)
test("AttackerPunchWithTrace - forward lunge and reset", function()
local vm = make_vm("player")
local p = Anim.present("player")
p.ox = 0
local t = AnimTasks.spawn("AttackerPunchWithTrace", 2, {}, vm)
assert_true(t ~= nil and t.active, "AttackerPunchWithTrace should spawn")
-- pokefirered/src/battle_anim_mons.c:2213
for _ = 1, 17 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_true(t.active, "still waiting on the last trace at frame 17")
AnimTasks.update(vm)
AnimSprites.update()
assert_true(not t.active, "ends once the lunge and its 8-frame traces finish")
assert_eq(p.ox, 0, "ox must reset to 0")
end)
-- ---------------------------------------------------------------------------
-- 5. Metadata Evaluators (Immediate Query Tasks)
-- ---------------------------------------------------------------------------
test("QueryStateTask evaluators - immediate resolution without blocking VM", function()
local vm = make_vm("player")
local evaluators = {
"GetRolloutCounter", "GetFuryCutterHitCount", "IsFuryCutterHitRight",
"GetReturnPowerLevel", "GetFrustrationPowerLevel", "GetSeismicTossDamageLevel",
"IsPowerOver99", "GetBattleTerrain", "GetWeather", "IsContest",
"IsTargetPlayerSide", "GetIsDoomDesireHitTurn", "IsHealingMove"
}
for _, ev in ipairs(evaluators) do
local t = AnimTasks.spawn(ev, 2, {}, vm)
assert_true(t ~= nil, "Evaluator " .. ev .. " should spawn")
AnimTasks.update(vm)
assert_true(not t.active, "Evaluator " .. ev .. " should immediately resolve")
end
end)
-- ---------------------------------------------------------------------------
-- 6. Complex Script Execution & Integration
-- ---------------------------------------------------------------------------
local f = io.open("data/generated/gba/pokemon/battle_anims/pack.lua", "r")
local packSrc = f and f:read("*a")
if f then f:close() end
if not packSrc then
local okDs, Dataset = pcall(require, "src.core.game3.dataset")
local cache = okDs and Dataset.cache and Dataset.cache() or nil
packSrc = cache and cache.read and cache:read("data/generated/gba/pokemon/battle_anims/pack.lua")
end
local chunk = loadstring and loadstring(packSrc) or load(packSrc)
local pack = chunk()
local function run_move_script(moveId, moveName)
test(string.format("%s (Move #%d) - bytecode script execution", moveName, moveId), function()
local script = pack.moves[moveId]
assert_true(script ~= nil, "move script must exist for ID " .. tostring(moveId))
local vm = AnimVm.new()
vm:setPack(pack)
vm:launch(script, { attackerSide = "player", isReversed = false })
assert_true(vm:busy(), "VM should start busy")
local maxTicks = 900
local ticks = 0
while vm:busy() and ticks < maxTicks do
ticks = ticks + 1
vm:update(1 / 60)
end
assert_true(not vm:busy(), string.format("move %d timed out after %d ticks", moveId, maxTicks))
end)
end
run_move_script(164, "Substitute")
run_move_script(104, "Double Team")
run_move_script(327, "Sky Uppercut")
run_move_script(69, "Seismic Toss")
run_move_script(90, "Fissure")
run_move_script(213, "Attract")
run_move_script(262, "Memento")
run_move_script(272, "Role Play")
run_move_script(326, "Extrasensory")
run_move_script(266, "Follow Me")
print(string.format("\nPhase 4 Results: %d passed, %d failed", passed, failed))
if failed > 0 then
os.exit(1)
end