mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
387 lines
13 KiB
Lua
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
|