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

387 lines
13 KiB
Lua

-- tests/game3_battle_anims_phase3_test.lua
-- Unit and integration tests for Phase 3: Elemental FX, Particle Generators & Wave Systems
package.path = package.path .. ";./?.lua"
require("tests.game3_cache").requireData("game3_battle_anims_phase3_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 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 3: Elemental FX & Particle Generators Tests ===")
-- 1. Water & Wave Callbacks
test("SmallDriftingBubbles - Q8.8 drift and 21-frame lifetime", function()
local spr = AnimSprites.acquire({
x = 100, y = 80, z = AnimSprites.Z.FRONT,
callback = AnimCallbacks.SmallDriftingBubbles
})
assert_eq(AnimSprites.activeCount(), 1)
-- pokefirered/src/battle_anim_water.c:1027
for _ = 1, 21 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 1)
AnimSprites.update()
assert_eq(AnimSprites.activeCount(), 0, "should destroy after the init frame + 21 steps")
end)
test("BubbleEffect - sine wave oscillation and burst", function()
local spr = AnimSprites.acquire({
x = 100, y = 80, z = AnimSprites.Z.FRONT, template = "gPoisonBubbleSpriteTemplate",
callback = AnimCallbacks.BubbleEffect
})
-- pokefirered/src/battle_anim_poison.c:290
for _ = 1, 10 do
AnimSprites.update()
end
assert_eq(spr.oy < 0, true, "bubble rises upward")
for _ = 11, 35 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 0, "bubble bursts when its affine anim ends")
end)
test("CreateSurfWave - surging wave with scanline traversal and collision", function()
local vm = make_vm("player")
local pTgt = Anim.present("enemy")
local t = AnimTasks.spawn("CreateSurfWave", 2, {}, vm)
assert_eq(AnimTasks.activeCount(), 1)
AnimTasks.update(vm)
assert_eq(type(t.draw), "function", "CreateSurfWave must attach a draw function")
-- pokefirered/src/battle_anim_water.c:877
for f = 2, 25 do
AnimTasks.update(vm)
AnimSprites.update()
assert_eq(t._bg1x < 0, true, "wave scrolls horizontally across arena")
end
assert_eq(bit.band(t._scan[1], 0x1F) > 0, true, "scanline blend band is active")
assert_eq(AnimTasks.activeCount(), 1)
for _ = 26, 200 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_eq(AnimTasks.activeCount(), 0, "wave task completes cleanly")
assert_eq(pTgt.ox, 0, "target ox restored")
assert_eq(pTgt.oy, 0, "target oy restored")
end)
test("LoadSandstormBackground - swirling sandstorm background and dust streaks", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("LoadSandstormBackground", 2, {}, vm)
assert_eq(AnimTasks.activeCount(), 1)
AnimTasks.update(vm)
assert_eq(type(t.draw), "function", "LoadSandstormBackground must attach a draw function")
-- pokefirered/src/battle_anim_rock.c:416
for _ = 2, 40 do AnimTasks.update(vm) end
assert_eq(vm.bldAlpha and vm.bldAlpha.eva, 7, "sandstorm fades BG1 in to eva 7")
for _ = 41, 200 do
AnimTasks.update(vm)
end
assert_eq(AnimTasks.activeCount(), 0, "sandstorm finishes cleanly")
end)
test("AnimTasks.draw - safely iterates and executes active task draw routines", function()
local vm = make_vm("player")
local t = AnimTasks.spawn("CreateSurfWave", 2, {}, vm)
AnimTasks.update(vm)
-- Should execute without throw even if love.graphics is absent/headless
AnimTasks.draw(0, 999, vm)
assert_eq(t.active, true)
end)
-- 2. Fire & Volcano Callbacks
test("FirePlume - flame pillar with upward velocity", function()
-- pokefirered/src/battle_anim_fire.c:486
local spr = AnimSprites.acquire({
x = 100, y = 100, z = AnimSprites.Z.FRONT,
callback = AnimCallbacks.FirePlume
})
spr._args = { 0, 0, 20, 10, 0, -3 }
AnimSprites.update()
AnimSprites.update()
assert_eq(spr.oy < 0, true, "flame moves upward")
for _ = 3, 21 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 0, "flame extinguishes")
end)
test("FireSpiralOutward - spiral vortex expanding trajectory", function()
-- pokefirered/src/battle_anim_fire.c:703
local spr = AnimSprites.acquire({
x = 100, y = 100, z = AnimSprites.Z.FRONT,
callback = AnimCallbacks.FireSpiralOutward
})
spr._args = { 0, 0, 24, 0, 0 }
AnimSprites.update()
assert_eq(spr.ox ~= nil and spr.oy ~= nil, true)
for _ = 2, 27 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 0, "spiral finishes")
end)
test("EruptionLaunchRocks - volcanic ballistic rock launch", function()
local vm = make_vm("player")
AnimTasks.spawn("EruptionLaunchRocks", 2, {}, vm)
assert_eq(AnimTasks.activeCount(), 1)
-- pokefirered/src/battle_anim_fire.c:770
for _ = 1, 200 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_eq(AnimTasks.activeCount(), 0, "eruption completes")
end)
-- 3. Electric & Energy FX
test("VoltTackleBolt - stepped lightning path", function()
local vm = make_vm("player")
AnimTasks.spawn("VoltTackleBolt", 2, { 0 }, vm)
assert_eq(AnimTasks.activeCount(), 1)
for _ = 1, 30 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_eq(AnimTasks.activeCount(), 0, "volt tackle bolt reaches destination")
end)
test("ShockWaveProgressingBolt - progressing electrical arcs", function()
local vm = make_vm("player")
AnimTasks.spawn("ShockWaveProgressingBolt", 2, {}, vm)
assert_eq(AnimTasks.activeCount(), 1)
local sawBolts = false
-- pokefirered/src/battle_anim_electric.c:1107
for _ = 1, 200 do
AnimTasks.update(vm)
AnimSprites.update()
if AnimSprites.activeCount() > 0 then sawBolts = true end
end
assert_eq(sawBolts, true, "bolt segments spawned")
assert_eq(AnimTasks.activeCount(), 0, "shock wave progresses and finishes")
end)
test("SolarBeamBigOrb - expanding charge orb", function()
local spr = AnimSprites.acquire({
x = 100, y = 80, z = AnimSprites.Z.FRONT,
callback = AnimCallbacks.SolarBeamBigOrb
})
AnimSprites.update()
assert_eq(spr.w > 0, true)
for _ = 2, 26 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 0, "charge orb completes")
end)
test("WeatherBallDown - descending atmospheric projectile", function()
local spr = AnimSprites.acquire({
x = 100, y = 20, z = AnimSprites.Z.FRONT,
callback = AnimCallbacks.WeatherBallDown
})
AnimSprites.update()
assert_eq(spr.oy >= 0, true)
for _ = 2, 22 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 0, "weather ball impacts")
end)
-- 4. Ice, Ground & Acoustic FX
test("IceEffectParticle - falling crystals and shatter", function()
local spr = AnimSprites.acquire({
x = 100, y = 20, z = AnimSprites.Z.FRONT, template = "gIceCrystalHitLargeSpriteTemplate",
callback = AnimCallbacks.IceEffectParticle
})
-- pokefirered/src/battle_anim_ice.c:615
for _ = 1, 10 do
AnimSprites.update()
end
assert_eq(spr.active and spr.visible ~= false, true, "crystal holds while its affine anim grows")
local flicker = false
for _ = 11, 45 do
AnimSprites.update()
if spr.active and spr.visible == false then flicker = true end
end
assert_eq(flicker, true, "crystal flickers after the affine anim ends")
assert_eq(AnimSprites.activeCount(), 0, "ice crystal shatters")
end)
test("FrozenIceCube - encasement in ice matrix", function()
local vm = make_vm("player")
local pTgt = Anim.present("enemy")
local t = AnimTasks.spawn("FrozenIceCube", 2, {}, vm)
AnimTasks.update(vm)
assert_eq(type(t.draw), "function", "ice cube sprite drawn")
-- pokefirered/src/battle_anim_status_effects.c:372
for _ = 2, 11 do AnimTasks.update(vm) end
assert_eq(t._cube.eva, 9, "ice cube fades in")
for _ = 12, 120 do
AnimTasks.update(vm)
end
assert_eq(AnimTasks.activeCount(), 0, "ice cube thaws")
assert_eq(pTgt.blendCoeff or 0, 0, "target palette untouched")
end)
test("Hail - falling hailstorm", function()
local vm = make_vm("player")
AnimTasks.spawn("Hail", 2, {}, vm)
assert_eq(AnimTasks.activeCount(), 1)
-- pokefirered/src/battle_anim_ice.c:1260
for _ = 1, 400 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_eq(AnimTasks.activeCount(), 0, "hailstorm finishes")
end)
test("DirtPlumeParticle - ground explosion with gravity", function()
local vm = Anim.vm()
-- pokefirered/data/battle_anim_scripts.s
vm.args[0], vm.args[1], vm.args[2], vm.args[3], vm.args[4], vm.args[5] = 0, 0, 12, 4, -16, 18
local spr = AnimSprites.acquire({
x = 100, y = 100, z = AnimSprites.Z.FRONT, template = "gDirtPlumeSpriteTemplate",
callback = AnimCallbacks.DirtPlumeParticle
})
spr._vm = vm
for _ = 1, 6 do
AnimSprites.update()
end
assert_eq(spr.oy < 0, true, "initial upward blast")
for _ = 7, 25 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 0, "dirt falls back and clears")
end)
test("WaveFromCenterOfTarget - expanding acoustic shockwave", function()
local spr = AnimSprites.acquire({
x = 120, y = 80, z = AnimSprites.Z.FRONT, template = "gIceGroundSpikeSpriteTemplate",
callback = AnimCallbacks.WaveFromCenterOfTarget
})
AnimSprites.update()
assert_eq(spr.w > 0, true)
-- pokefirered/src/battle_anim_ice.c:272
for _ = 2, 34 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 1, "ice spike still animating")
for _ = 35, 40 do
AnimSprites.update()
end
assert_eq(AnimSprites.activeCount(), 0, "sound wave expands and dissipates")
end)
test("AtmosphericFog - mist and spore clouds", function()
local vm = make_vm("player")
AnimTasks.spawn("MistBallFog", 2, { 20 }, vm)
assert_eq(AnimTasks.activeCount(), 1)
-- pokefirered/src/battle_anim_ice.c:1053
for _ = 1, 200 do
AnimTasks.update(vm)
AnimSprites.update()
end
assert_eq(AnimTasks.activeCount(), 0, "fog dissipates")
end)
-- 5. Full Elemental Move Script Executions
local phase3Moves = {
{ id = 57, name = "MOVE_SURF" },
{ id = 330, name = "MOVE_MUDDY_WATER" },
{ id = 56, name = "MOVE_HYDRO_PUMP" },
{ id = 61, name = "MOVE_BUBBLE_BEAM" },
{ id = 55, name = "MOVE_WATER_GUN" },
{ id = 352, name = "MOVE_WATER_PULSE" },
{ id = 126, name = "MOVE_FIRE_BLAST" },
{ id = 53, name = "MOVE_FLAMETHROWER" },
{ id = 83, name = "MOVE_FIRE_SPIN" },
{ id = 284, name = "MOVE_ERUPTION" },
{ id = 315, name = "MOVE_OVERHEAT" },
{ id = 87, name = "MOVE_THUNDER" },
{ id = 85, name = "MOVE_THUNDERBOLT" },
{ id = 351, name = "MOVE_SHOCK_WAVE" },
{ id = 342, name = "MOVE_VOLT_TACKLE" },
{ id = 76, name = "MOVE_SOLAR_BEAM" },
{ id = 311, name = "MOVE_WEATHER_BALL" },
{ id = 192, name = "MOVE_ZAP_CANNON" },
{ id = 59, name = "MOVE_BLIZZARD" },
{ id = 58, name = "MOVE_ICE_BEAM" },
{ id = 258, name = "MOVE_HAIL" },
{ id = 89, name = "MOVE_EARTHQUAKE" },
{ id = 157, name = "MOVE_ROCK_SLIDE" },
{ id = 47, name = "MOVE_SING" },
{ id = 304, name = "MOVE_HYPER_VOICE" },
{ id = 54, name = "MOVE_MIST" },
{ id = 114, name = "MOVE_HAZE" },
{ id = 147, name = "MOVE_SPORE" },
}
local okDs, Dataset = pcall(require, "src.core.game3.dataset")
local cache = okDs and Dataset.cache and Dataset.cache() or nil
local packSrc = cache and cache.read and cache:read("data/generated/gba/pokemon/battle_anims/pack.lua")
if not packSrc then
local f = io.open("data/generated/gba/pokemon/battle_anims/pack.lua", "r")
if f then packSrc = f:read("*a"); f:close() end
end
local pack = (loadstring or load)(packSrc)()
local vm = AnimVm.new()
vm:setPack(pack)
for _, m in ipairs(phase3Moves) do
if pack.moves[m.id] then
test("Full Move Execution: " .. m.name, function()
AnimTasks.reset()
AnimSprites.reset()
Anim.reset({ headless = true })
vm:launch(pack.moves[m.id], { attackerSide = "player", isReversed = false })
local maxFrames = 1200
local frames = 0
while vm:busy() and frames < maxFrames do
frames = frames + 1
vm:update()
end
assert_eq(vm:busy(), false, string.format("Move %s timed out after %d frames", m.name, frames))
end)
end
end
print(string.format("\nPhase 3 Summary: %d passed, %d failed\n", passed, failed))
if failed > 0 then
os.exit(1)
end