mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
aed3bbced7
- Battle Animation Pipeline:
* Eliminate 90-frame audio polling stall in anim_vm end/waitsound opcodes,
allowing attack animations to transition instantly into target hit flicker
and HP bar drain.
* Correct visual task counting in AnimVm:visualCount to match pret gAnimVisualTaskCount.
* Signal bg task completion on restorebg (vm.args[7] = -1).
* Fix termination conditions on shake tasks (ShakeMon, ShakeMon2, ShakeBattleTerrain),
WaterSport droplet arcs, and StartSinAnimTimer.
* Handle RGB555 color args in start_blend_anim_sprite_color and route P.destroyTask
to AnimTasks.destroy.
- RNG System & Seeding Parity:
* Replace math.random in Party.giveMon with Rng.Random32() for personality values
and Rng.Random() for IV bitfield extraction (HP/Atk/Def/Spe/SpA/SpD) matching GBA format.
* Seed RNG on Title Screen A/Start press via Rng.seedNewGame() (matching SeedRngAndSetTrainerId)
and perturb RNG state on title menu inputs.
* Route NPC overworld idle/wander timers and wild encounter slot rolls through Rng.compat.
- Pokédex Habitat & Area Maps:
* Fix encounter tables extraction and map indexing to resolve Pokemon habitat
locations across Kanto/Sevii maps rather than showing Area Unknown.
* Render steady semi-transparent red overlays for Pokédex area map locations.
- Multichoice Menus & UI:
* Implement multichoice table extraction and data population for Viridian blackboard
status ailments and special dialog interactions.
* Improve choice box borders, pagination, and Town Map wall viewing.
- Overworld Events & Flags:
* Fix Route 1 entrance girl NPC spawning and script triggers after starter selection.
* Ensure consistent save reload state in Oak's Lab.
400 lines
14 KiB
Lua
400 lines
14 KiB
Lua
-- Test suite for Phase 2: Palette Blends & Affine Battler Transformations
|
|
-- Run: luajit tests/game3_battle_anims_phase2_test.lua
|
|
|
|
package.path = "?.lua;?/init.lua;" .. package.path
|
|
|
|
local Anim = require("src.core.game3.battle.anim")
|
|
local AnimVm = require("src.core.game3.battle.anim_vm")
|
|
local AnimTasks = require("src.core.game3.battle.anim_tasks")
|
|
local AnimSprites = require("src.core.game3.battle.anim_sprites")
|
|
|
|
local passed = 0
|
|
local failed = 0
|
|
|
|
local function test(name, fn)
|
|
Anim.reset({ headless = true })
|
|
AnimTasks.reset()
|
|
AnimSprites.reset()
|
|
local ok, err = pcall(fn)
|
|
if ok then
|
|
passed = passed + 1
|
|
print(string.format(" PASS: %s", name))
|
|
else
|
|
failed = failed + 1
|
|
print(string.format(" FAIL: %s -> %s", 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_near(a, b, eps, msg)
|
|
eps = eps or 0.001
|
|
if math.abs((a or 0) - (b or 0)) > eps then
|
|
error(string.format("%s (expected ~%s, got %s)", msg or "assert_near failed", tostring(b), tostring(a)), 2)
|
|
end
|
|
end
|
|
|
|
local function spawn_now(name, args, v)
|
|
local t = AnimTasks.spawn(name, 2, args, v)
|
|
if t and t.func then t.func(t, v) end
|
|
return t
|
|
end
|
|
|
|
local function make_vm(atkSide)
|
|
local vm = AnimVm.new()
|
|
vm.headless = true
|
|
vm.attacker = (atkSide == "enemy") and 1 or 0
|
|
vm.target = (atkSide == "enemy") and 0 or 1
|
|
return vm
|
|
end
|
|
|
|
print("=== Phase 2: Palette, Blend & Affine Transformations Tests ===")
|
|
|
|
-- 1. BlendBattleAnimPal
|
|
test("BlendBattleAnimPal - background fade_black and restore", function()
|
|
local vm = make_vm("player")
|
|
-- palMask=1 (BG), delay=0, startCoeff=0, targetCoeff=8, color=0 (black)
|
|
AnimTasks.spawn("BlendBattleAnimPal", 2, { 1, 0, 0, 8, 0 }, vm)
|
|
assert_eq(AnimTasks.activeCount(), 1)
|
|
|
|
for _ = 1, 9 do
|
|
AnimTasks.update(vm)
|
|
end
|
|
assert_eq(AnimTasks.activeCount(), 0, "task should finish after 9 steps (0..8)")
|
|
local bb = Anim._bgBlend
|
|
assert_eq(bb and bb.coeff, 8, "bg palette blended 8/16")
|
|
assert_eq(bb and bb.color, 0, "bg blend color black")
|
|
|
|
-- Fade back out to 0
|
|
AnimTasks.spawn("BlendBattleAnimPal", 2, { 1, 0, 8, 0, 0 }, vm)
|
|
for _ = 1, 9 do
|
|
AnimTasks.update(vm)
|
|
end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(Anim._bgBlend, nil, "bg blend restored")
|
|
end)
|
|
|
|
test("BlendBattleAnimPal - battler palette tinting", function()
|
|
local vm = make_vm("player")
|
|
local pAtk = Anim.present("player")
|
|
local pTgt = Anim.present("enemy")
|
|
-- palMask=2 (attacker), delay=0, start=0, target=16, color=RGB_WHITE (0x7FFF)
|
|
AnimTasks.spawn("BlendBattleAnimPal", 2, { 2, 0, 0, 16, 0x7FFF }, vm)
|
|
for _ = 1, 17 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_near(pAtk.blendCoeff, 1.0)
|
|
assert_near(pAtk.blendColor[1], 1.0)
|
|
assert_eq(pTgt.blendCoeff or 0, 0)
|
|
end)
|
|
|
|
-- 2. BlendBattleAnimPalExclude
|
|
test("BlendBattleAnimPalExclude - exclude target", function()
|
|
local vm = make_vm("player")
|
|
-- cmd 1: Not target -> blends attacker & BG (mask 3)
|
|
AnimTasks.spawn("BlendBattleAnimPalExclude", 2, { 1, 0, 0, 4, 0 }, vm)
|
|
for _ = 1, 5 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
local pAtk = Anim.present("player")
|
|
local pTgt = Anim.present("enemy")
|
|
assert_near(pAtk.blendCoeff, 0.25)
|
|
assert_eq(pTgt.blendCoeff or 0, 0)
|
|
end)
|
|
|
|
-- 3. BlendMonInAndOut
|
|
test("BlendMonInAndOut - pulse in and out", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
-- whichMon=0 (atk), color=0x7FFF (white), targetCoeff=4, delay=0, repeats=2
|
|
AnimTasks.spawn("BlendMonInAndOut", 2, { 0, 0x7FFF, 4, 0, 2 }, vm)
|
|
assert_eq(AnimTasks.activeCount(), 1)
|
|
-- 2 repeats of 4 steps in + 4 steps out = 16 updates
|
|
for _ = 1, 20 do
|
|
AnimTasks.update(vm)
|
|
end
|
|
assert_eq(AnimTasks.activeCount(), 0, "task should finish after repeat cycles")
|
|
assert_eq(p.blendCoeff, 0)
|
|
assert_eq(p.flash, 0)
|
|
end)
|
|
|
|
-- 4. TraceMonBlended
|
|
test("TraceMonBlended - spawn afterimages and lifetime cleanup", function()
|
|
local vm = make_vm("player")
|
|
-- whichMon=0, interval=2, lifetime=4, count=3
|
|
AnimTasks.spawn("TraceMonBlended", 2, { 0, 2, 4, 3 }, vm)
|
|
assert_eq(AnimTasks.activeCount(), 1)
|
|
for _ = 1, 20 do
|
|
AnimTasks.update(vm)
|
|
AnimSprites.update()
|
|
end
|
|
assert_eq(AnimTasks.activeCount(), 0, "task finishes after all afterimages expire")
|
|
assert_eq(AnimSprites.activeCount(), 0, "all afterimages should be released")
|
|
end)
|
|
|
|
-- 5. AttackerFadeToInvisible & AttackerFadeFromInvisible
|
|
test("AttackerFadeToInvisible & FromInvisible", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("AttackerFadeToInvisible", 2, { 0 }, vm)
|
|
-- pokefirered/src/battle_anim_dark.c:187
|
|
for _ = 1, 17 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.visible, false)
|
|
|
|
AnimTasks.spawn("AttackerFadeFromInvisible", 2, { 0 }, vm)
|
|
AnimTasks.update(vm)
|
|
assert_eq(p.visible, true)
|
|
for _ = 2, 17 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.alpha, 1)
|
|
end)
|
|
|
|
-- 6. RotateMonSpriteToSide
|
|
test("RotateMonSpriteToSide - rotate and restore", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
-- duration=10, rotDelta=0x100, whichMon=0, returnMode=1 (reset on end)
|
|
spawn_now("RotateMonSpriteToSide", { 10, 0x100, 0, 1 }, vm)
|
|
AnimTasks.update(vm)
|
|
assert_eq(p.rotation ~= 0, true, "rotation should change")
|
|
for _ = 2, 10 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.rotation, 0, "rotation should be reset")
|
|
assert_eq(p.oy, 0, "vertical offset should be reset")
|
|
end)
|
|
|
|
-- 7. ScaleMonAndRestore
|
|
test("ScaleMonAndRestore - scale and bounce back", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
-- dx=8, dy=8, duration=8, whichMon=0
|
|
spawn_now("ScaleMonAndRestore", { 8, 8, 8, 0 }, vm)
|
|
AnimTasks.update(vm)
|
|
assert_eq(p.sx ~= 1, true, "sx should change")
|
|
assert_eq(p.sy ~= 1, true, "sy should change")
|
|
-- 8 frames forward + 8 frames reverse = 16 frames
|
|
for _ = 2, 16 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.sx, 1, "sx restored")
|
|
assert_eq(p.sy, 1, "sy restored")
|
|
assert_eq(p.oy, 0, "oy restored")
|
|
end)
|
|
|
|
-- 8. Minimize
|
|
test("Minimize - 3 cycles and recover", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("Minimize", 2, {}, vm)
|
|
-- pokefirered/src/battle_anim_effects_2.c:2050
|
|
for _ = 1, 200 do
|
|
AnimSprites.update()
|
|
AnimTasks.update(vm)
|
|
end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.sx, 1)
|
|
assert_eq(p.sy, 1)
|
|
assert_eq(p.oy, 0)
|
|
end)
|
|
|
|
-- 9. Withdraw
|
|
test("Withdraw - downward roll, pause, and roll back", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("Withdraw", 2, {}, vm)
|
|
-- 22 roll + 30 pause + 22 back = 74 frames
|
|
for _ = 1, 80 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.rotation, 0)
|
|
assert_eq(p.oy, 0)
|
|
end)
|
|
|
|
-- 10. General Affine Tasks: DefenseCurl, Stockpile, SpitUp, Swallow, StretchTargetUp, GrowAndShrink
|
|
test("Affine Animations - DefenseCurl", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("DefenseCurlDeformMon", 2, {}, vm)
|
|
-- pokefirered/src/battle_anim_effects_3.c:2019
|
|
for _ = 1, 53 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.sx, 1)
|
|
assert_eq(p.sy, 1)
|
|
assert_eq(p.oy, 0)
|
|
end)
|
|
|
|
test("Affine Animations - StockpileDeformMon", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("StockpileDeformMon", 2, {}, vm)
|
|
-- pokefirered/src/battle_anim_effects_3.c:2162
|
|
for _ = 1, 76 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.sx, 1)
|
|
assert_eq(p.sy, 1)
|
|
assert_eq(p.oy, 0)
|
|
end)
|
|
|
|
test("Affine Animations - StretchTargetUp", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("enemy")
|
|
AnimTasks.spawn("StretchTargetUp", 2, {}, vm)
|
|
for _ = 1, 25 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.sx, 1)
|
|
assert_eq(p.sy, 1)
|
|
assert_eq(p.oy, 0)
|
|
end)
|
|
|
|
test("Affine Animations - GrowAndShrink (Bulk Up / Swagger)", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("GrowAndShrink", 2, {}, vm)
|
|
for _ = 1, 70 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.sx, 1)
|
|
assert_eq(p.sy, 1)
|
|
assert_eq(p.oy, 0)
|
|
end)
|
|
|
|
test("AcidArmor - wave and fade cycle", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("AcidArmor", 2, { 0 }, vm)
|
|
-- pokefirered/src/battle_anim_effects_3.c:3222
|
|
for _ = 1, 110 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.alpha, 1)
|
|
assert_eq(p.ox, 0)
|
|
end)
|
|
|
|
test("FacadeColorBlend - color cycle", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
AnimTasks.spawn("FacadeColorBlend", 2, { 0, 24 }, vm)
|
|
-- pokefirered/src/battle_anim_effects_3.c:3802
|
|
AnimTasks.update(vm)
|
|
AnimTasks.update(vm)
|
|
assert_eq(p.blendCoeff, 0.5)
|
|
for _ = 3, 26 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0)
|
|
assert_eq(p.blendCoeff, 0)
|
|
end)
|
|
|
|
test("RockMonBackAndForth - rocking and rotation with side-awareness", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
-- whichMon=0, numRocks=2, speedInc=0
|
|
AnimTasks.spawn("RockMonBackAndForth", 2, { 0, 2, 0 }, vm)
|
|
-- pokefirered/src/battle_anim_effects_3.c:2643
|
|
AnimTasks.update(vm)
|
|
AnimTasks.update(vm)
|
|
assert_eq(AnimTasks.activeCount(), 1)
|
|
assert_eq(p.ox ~= 0, true, "mon should move horizontally while rocking")
|
|
-- Step through all rocking cycles (halfDur=8: 8 + 16 + 8 = 32 frames per rock * 2 = 64 frames)
|
|
for _ = 2, 70 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0, "rocking task should complete")
|
|
assert_eq(p.ox, 0, "ox restored")
|
|
assert_eq(p.oy, 0, "oy restored")
|
|
assert_eq(p.rotation, 0, "rotation restored")
|
|
end)
|
|
|
|
test("ComplexPaletteBlend - alternate tint cycling", function()
|
|
local vm = make_vm("player")
|
|
local p = Anim.present("player")
|
|
-- palMask=2 (attacker), delay=2, numCycles=2, color1=32767 (white), coeff1=16, color2=0 (black), coeff2=0
|
|
AnimTasks.spawn("ComplexPaletteBlend", 2, { 2, 2, 2, 32767, 16, 0, 0 }, vm)
|
|
AnimTasks.update(vm)
|
|
assert_eq(AnimTasks.activeCount(), 1)
|
|
assert_eq(p.blendCoeff > 0, true, "initial tint active")
|
|
for _ = 2, 20 do AnimTasks.update(vm) end
|
|
assert_eq(AnimTasks.activeCount(), 0, "complex blend finishes")
|
|
assert_eq(p.blendCoeff, 0, "blend restored")
|
|
end)
|
|
|
|
test("FlashAnimTagWithColor - flash cycle count and termination", function()
|
|
local vm = make_vm("player")
|
|
-- tag=10005, delay=2, numFlashes=3, color=32767, coeff=16
|
|
spawn_now("FlashAnimTagWithColor", { 10005, 2, 3, 32767, 16 }, vm)
|
|
assert_eq(AnimTasks.activeCount(), 1)
|
|
local n = 0
|
|
while AnimTasks.activeCount() > 0 and n < 200 do AnimTasks.update(vm) n = n + 1 end
|
|
assert_eq(AnimTasks.activeCount(), 0, "flash task terminates after 3 palette-fade cycles")
|
|
end)
|
|
|
|
-- 11. Full Move Execution Verification for Phase 2 Moves
|
|
local phase2Moves = {
|
|
{ id = 111, name = "MOVE_DEFENSE_CURL" },
|
|
{ id = 107, name = "MOVE_MINIMIZE" },
|
|
{ id = 110, name = "MOVE_WITHDRAW" },
|
|
{ id = 339, name = "MOVE_BULK_UP" },
|
|
{ id = 207, name = "MOVE_SWAGGER" },
|
|
{ id = 263, name = "MOVE_FACADE" },
|
|
{ id = 97, name = "MOVE_AGILITY" },
|
|
{ id = 151, name = "MOVE_ACID_ARMOR" },
|
|
{ id = 144, name = "MOVE_TRANSFORM" },
|
|
{ id = 254, name = "MOVE_STOCKPILE" },
|
|
{ id = 255, name = "MOVE_SPIT_UP" },
|
|
{ id = 256, name = "MOVE_SWALLOW" },
|
|
{ id = 96, name = "MOVE_MEDITATE" },
|
|
{ id = 37, name = "MOVE_THRASH" },
|
|
{ id = 148, name = "MOVE_FLASH" },
|
|
{ id = 267, name = "MOVE_CAMOUFLAGE" },
|
|
{ id = 345, name = "MOVE_MAGICAL_LEAF" },
|
|
{ id = 174, name = "MOVE_CURSE" },
|
|
{ id = 204, name = "MOVE_CHARM" },
|
|
{ id = 321, name = "MOVE_TICKLE" },
|
|
{ id = 343, name = "MOVE_COVET" },
|
|
{ id = 38, name = "MOVE_DOUBLE_EDGE" },
|
|
{ id = 63, name = "MOVE_HYPER_BEAM" },
|
|
{ id = 14, name = "MOVE_SWORDS_DANCE" },
|
|
{ id = 160, name = "MOVE_CONVERSION" },
|
|
{ id = 153, name = "MOVE_EXPLOSION" },
|
|
}
|
|
|
|
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
|
|
|
|
if packSrc then
|
|
local chunk = loadstring and loadstring(packSrc) or load(packSrc)
|
|
local pack = chunk()
|
|
for _, m in ipairs(phase2Moves) do
|
|
test("Full Move Execution: " .. m.name, function()
|
|
local script = pack.moves[m.id]
|
|
if not script then
|
|
error("Move " .. m.name .. " (id " .. m.id .. ") not found in pack")
|
|
end
|
|
Anim.reset({ headless = true })
|
|
AnimTasks.reset()
|
|
AnimSprites.reset()
|
|
local vm = AnimVm.new()
|
|
vm:setPack(pack)
|
|
vm:launch(script, { attackerSide = "player", isReversed = false })
|
|
local maxTicks = 600
|
|
local ticks = 0
|
|
while vm:busy() and ticks < maxTicks do
|
|
ticks = ticks + 1
|
|
vm:update(1 / 60)
|
|
end
|
|
assert_eq(vm:busy(), false, m.name .. " should finish cleanly")
|
|
-- Present state restored
|
|
local p = Anim.present("player")
|
|
assert_eq(p.sx, 1, m.name .. " sx restored")
|
|
assert_eq(p.sy, 1, m.name .. " sy restored")
|
|
assert_eq(p.rotation, 0, m.name .. " rotation restored")
|
|
assert_eq(p.oy, 0, m.name .. " oy restored")
|
|
end)
|
|
end
|
|
end
|
|
|
|
print(string.format("\nPhase 2 Summary: %d passed, %d failed", passed, failed))
|
|
if failed > 0 then
|
|
os.exit(1)
|
|
end
|