Files
gen1recomp/tests/game3_anim_port_g1_test.lua
2026-09-23 16:34:15 -04:00

319 lines
12 KiB
Lua

package.path = "./?.lua;" .. package.path
local AnimSprites = require("src.core.game3.battle.anim_sprites")
local AnimTasks = require("src.core.game3.battle.anim_tasks")
local AnimCallbacks = require("src.core.game3.battle.anim_callbacks")
local Anim = require("src.core.game3.battle.anim")
local P = require("src.core.game3.battle.anim_port.g1_pret")
local C = require("src.core.game3.battle.anim_port.g1_callbacks")
local T = require("src.core.game3.battle.anim_port.g1_tasks")
local passed, failed = 0, 0
local function check(ok, msg)
if ok then
passed = passed + 1
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local CB_SCOPE = {
"ConfusionDuck", "HitSplatOnMonEdge", "HornHit", "HyperBeamOrb", "ItemSteal", "KnockOffItem", "MimicOrb",
"PresentHealParticle", "SpinningSparkle", "WavyMusicNotes", "WhipHit", "AirCutterSlice", "BellyDrumHand",
"ConstrictBinding", "Conversion", "Conversion2", "CrossImpact", "EndureEnergy", "FalseSwipePositionedSlice",
"FalseSwipeSlice", "FlashingHitSplat", "FlyingMusicNotes", "FollowMeFinger", "FrenzyPlantRoot",
"HitSplatHandleInvert", "HitSplatPersistent", "IngrainOrb", "IngrainRoot", "LeechSeed", "LockOnMoveTarget",
"LockOnTarget", "MilkBottle", "Moon", "MoonlightSparkle", "MoveTwisterParticle", "PetalDanceBigFlower",
"PetalDanceSmallFlower", "PowerAbsorptionOrb", "Present", "Protect", "RazorLeafParticle", "SharpenSphere",
"SlowFlyingMusicNotes", "SparklingStars", "SporeParticle", "SuperFang", "TauntFinger", "ThoughtBubble",
"TranslateLinearSingleSineWave", "TravelDiagonally", "TrickBag", "WeatherBallUp",
"AbsorptionOrb", "CuttingSlice", "FlyingParticle", "GrantingStars", "HitSplatBasic", "HitSplatRandom",
"MetronomeFinger", "MovePowderParticle", "NeedleArmSpike", "SlashSlice", "SleepLetterZ", "SolarBeamBigOrb",
"SpriteOnMonPos", "ThrowProjectile", "TranslateAnimSpriteToTargetMonLocation", "WeatherBallDown",
}
local TASK_SCOPE = {
"AllocBackupPalBuffer", "CopyPalFadedToUnfaded", "CopyPalUnfadedFromBackup", "CopyPalUnfadedToBackup",
"FreeBackupPalBuffer", "MoonlightEndFade", "SetAttackerInvisibleWaitForSignal", "BlendColorCycleByTag",
"BlendColorCycleExclude", "BlendPalInAndOutByTag", "Conversion2AlphaBlend", "ConversionAlphaBlend",
"CycleMagicalLeafPal", "GetBattleTerrain", "GetFrustrationPowerLevel", "IsContest", "IsTargetSameSide",
"LeafBlade", "MusicNotesClearRainbowBlend", "MusicNotesRainbowBlend", "RotateMonToSideAndRestore",
"SetAnimAttackerAndTargetForEffectAtk", "SetAnimAttackerAndTargetForEffectTgt", "SetAnimTargetToBattlerTarget",
"ShakeBattleTerrain", "SporeDoubleBattle",
"BlendColorCycle", "FlashAnimTagWithColor", "InvertScreenColor", "BlendBattleAnimPal", "BlendBattleAnimPalExclude",
"SetCamouflageBlend", "BlendParticle", "BlendMonInAndOut", "HardwarePaletteFade", "CreateSmallSolarBeamOrbs",
"ShakeMon", "ShakeMon2", "ShakeMonInPlace", "ShakeAndSinkMon", "TranslateMonElliptical",
"TranslateMonEllipticalRespectSide", "WindUpLunge", "SlideOffScreen", "SwayMon", "ScaleMonAndRestore",
"RotateMonSpriteToSide", "ShakeTargetBasedOnMovePowerOrDmg",
"HorizontalLunge", "VerticalDip", "SlideMonToOriginalPos", "SlideMonToOffset", "SlideMonToOffsetAndBack",
"BowMon", "ShakeMonOrBattleTerrain", "SimplePaletteBlend", "ComplexPaletteBlend",
"AlphaFadeIn", "ShrinkTargetCopy", "AttackerPunchWithTrace", "TraceMonBlended", "DoubleTeam", "Flash",
"StartSlidingBg", "BlendNonAttackerPalettes", "SetAllNonAttackersInvisiblity", "SkullBashPosition",
}
for _, n in ipairs(CB_SCOPE) do
check(C[n] ~= nil and AnimCallbacks[n] == C[n], "callback ported and hooked: " .. n)
end
for _, n in ipairs(TASK_SCOPE) do
check(T[n] ~= nil and AnimTasks.REGISTRY[n] == T[n], "task ported and hooked: " .. n)
end
local function find_pack()
local env = os.getenv("POKEPORT_ANIM_PACK")
local root = require("tests.game3_cache").root("pokemon/battle_anims/pack.lua")
local cands = {
env,
root and (root .. "/pokemon/battle_anims/pack.lua"),
}
for ci = 1, 2 do
local p = cands[ci]
if p then
local f = io.open(p, "r")
if f then
f:close()
local ok, pack = pcall(dofile, p)
if ok and type(pack) == "table" then return pack end
end
end
end
return nil
end
local pack = find_pack()
if not pack then
print("[skip] no extracted battle_anims pack (set POKEPORT_ANIM_PACK)")
print(string.format("g1 anim port: %d passed, %d failed", passed, failed))
os.exit(failed == 0 and 0 or 1)
end
Anim.loadPack(pack)
local function refs_of(script, seen, out)
if type(script) ~= "table" or seen[script] then return end
seen[script] = true
for _, op in ipairs(script) do
if op.op == "createsprite" and op.callback then out.cb[op.callback] = true end
if (op.op == "createvisualtask" or op.op == "createsoundtask") and op.task then out.task[op.task] = true end
for _, k in ipairs({ "label", "label1", "label2", "target" }) do
local l = op[k]
if l and pack.labels[l] then refs_of(pack.labels[l], seen, out) end
end
end
end
local scripts = {}
local function add_group(group, names)
for i, sc in pairs(pack[group] or {}) do
local out = { cb = {}, task = {} }
refs_of(sc, {}, out)
scripts[#scripts + 1] = { name = group .. ":" .. tostring((names and names[i]) or i), script = sc, refs = out }
end
end
add_group("moves")
add_group("general", pack.generalNames)
add_group("status", pack.statusNames)
add_group("special", pack.specialNames)
local errors = {}
local realPrint = print
local function capture_print(...)
local parts = {}
for i = 1, select("#", ...) do parts[#parts + 1] = tostring(select(i, ...)) end
local line = table.concat(parts, " ")
if line:find("%[battle%.anim%]") or line:find("%[g1%]") then errors[#errors + 1] = line end
end
local function run_script(sc, attackerSide, turn, maxFrames)
Anim.reset({ headless = false })
Anim.loadPack(pack)
P.Pal.reset()
P.Fade.active = false
local vm = Anim._vm
vm.headless = false
errors = {}
print = capture_print
local frames = 0
local ok, err = pcall(function()
vm:launch(sc, {
isReversed = attackerSide == "enemy",
attackerSide = attackerSide,
targetSide = attackerSide == "player" and "enemy" or "player",
attackerSpecies = 6,
targetSpecies = 9,
})
vm._turn = turn
while vm.active and frames < maxFrames do
vm:update(1 / 60)
frames = frames + 1
end
end)
print = realPrint
local g1Alive, others = false, {}
for i = 1, AnimTasks.MAX do
local t = AnimTasks._pool[i]
if t.active then
if t._g1 or t._g1FadeTicker then g1Alive = true else others[#others + 1] = tostring(t.name) end
end
end
AnimSprites.forEachActive(function(sp)
if sp._g1 then g1Alive = true else others[#others + 1] = tostring(sp.template) end
end)
return ok, err, vm, frames, g1Alive, others
end
local inScope = {}
for _, n in ipairs(CB_SCOPE) do inScope["cb:" .. n] = true end
for _, n in ipairs(TASK_SCOPE) do inScope["task:" .. n] = true end
local covered = {}
local ran = 0
for _, entry in ipairs(scripts) do
local uses = false
for n in pairs(entry.refs.cb) do if inScope["cb:" .. n] then uses = true; covered["cb:" .. n] = true end end
for n in pairs(entry.refs.task) do if inScope["task:" .. n] then uses = true; covered["task:" .. n] = true end end
if uses then
for _, side in ipairs({ "player", "enemy" }) do
for _, turn in ipairs({ 0, 1 }) do
local ok, err, vm, frames, g1Alive, others = run_script(entry.script, side, turn, 1500)
ran = ran + 1
local label = entry.name .. " atk=" .. side .. " turn=" .. turn
check(ok, label .. " runs: " .. tostring(err))
if vm.active and not g1Alive then
print("[info] " .. label .. " hangs on non-g1 work: " .. table.concat(others, ","))
else
check(not vm.active, label .. " terminates (frames=" .. frames .. ")")
local g1err = nil
for _, e in ipairs(errors) do
if not e:find("waitforvisualfinish cap") then g1err = g1err or e end
end
check(g1err == nil, label .. " no runtime errors: " .. tostring(g1err))
check(AnimSprites.activeCount() == 0, label .. " leaves no live sprites")
end
end
end
end
end
for k in pairs(inScope) do
if not covered[k] then print("[info] not referenced by any pack script: " .. k) end
end
local function fresh_vm(attackerSide)
Anim.reset({ headless = false })
Anim.loadPack(pack)
P.Pal.reset()
P.Fade.active = false
local vm = Anim._vm
vm:launch({ { op = "delay", frames = 400 }, { op = "end" } }, {
isReversed = attackerSide == "enemy",
attackerSide = attackerSide,
targetSide = attackerSide == "player" and "enemy" or "player",
attackerSpecies = 6,
targetSpecies = 9,
})
return vm
end
local function spawn(vm, template, cbName, args, animBattler, subpri)
local s = AnimSprites.acquire({ template = template, tag = "IMPACT", w = 32, h = 32 })
s._vm = vm
s._args = args
s._op = { op = "createsprite", template = template, callback = cbName, args = args, animBattler = animBattler or "attacker", subpriority = subpri or 2 }
s.template = template
s.callback = AnimCallbacks.get(cbName)
s.callback(s)
return s
end
local function lifetime(s, cap)
local n = 1
while s.active and n < (cap or 1000) do
AnimSprites.update()
n = n + 1
end
return n
end
do
local vm = fresh_vm("player")
local s = spawn(vm, "gHornHitSpriteTemplate", "HornHit", { 0, 0, 10 }, "target", 4)
check(s.x == 176 - 40 and s.data[3] == 512, "HornHit player: x-40 start, 0x1400/d1 velocity")
check(s.data[6] == 176, "HornHit remembers target X_2")
check(lifetime(s) == 11, "HornHit destroyed on the arg2-th frame after creation")
end
do
local vm = fresh_vm("enemy")
local s = spawn(vm, "gHornHitSpriteTemplate", "HornHit", { 0, 0, 10 }, "target", 4)
check(s._oamH and s._oamV, "HornHit enemy attacker flips H+V")
end
do
local vm = fresh_vm("player")
local s = spawn(vm, "gConfusionDuckSpriteTemplate", "ConfusionDuck", { 0, -15, 0, 3, 90 }, "attacker")
check(s.ox == P.Cos(0, 30) and s.oy == P.Sin(0, 10), "ConfusionDuck starts on Cos(0,30)/Sin(0,10)")
check(lifetime(s) == 90, "ConfusionDuck lives arg4 frames")
end
do
local vm = fresh_vm("player")
local s = spawn(vm, "gBasicHitSplatSpriteTemplate", "HitSplatBasic", { 0, 0, 1, 1 }, "target")
check(s.x == 176, "HitSplatBasic on target X_2")
check(s._aff and s._aff.xScale == 0xD8, "HitSplatBasic affine anim 1 starts at 0xD8 scale")
check(lifetime(s) == 12, "HitSplatBasic: 0-dur frame, 8-frame hold, END, stored cb, destroy")
end
do
local vm = fresh_vm("player")
local s = spawn(vm, "gSleepLetterZSpriteTemplate", "SleepLetterZ", { 4, -20, 30 }, "attacker")
check(s.x == 72 + 4, "SleepLetterZ player offset +x")
check(lifetime(s) == 62, "SleepLetterZ destroyed when ++data[1] > 60")
end
do
local vm = fresh_vm("player")
local s = spawn(vm, "gMovePowderParticleSpriteTemplate", "MovePowderParticle", { 0, -20, 84, 70, 10, 5 }, "target")
check(lifetime(s) == 86, "MovePowderParticle counts down arg2 then destroys")
end
do
local vm = fresh_vm("player")
local s = spawn(vm, "gTrickBagSpriteTemplate", "TrickBag", { -40, 80 }, "attacker")
check(s.x == 120 and s.y == -40, "TrickBag starts at x=120, y=arg0")
check(s.ox == P.Cos(80, 60) and s.oy == P.Sin(80, 20), "TrickBag circle offset")
end
do
local vm = fresh_vm("player")
local s = spawn(vm, "gLockOnTargetSpriteTemplate", "LockOnTarget", {}, "attacker", 40)
check(s.x == 176 - 32, "LockOnTarget starts 32px up-left of target")
end
do
local vm = fresh_vm("player")
local t = AnimTasks.spawn("ShakeMon", 2, { 1, 3, 0, 6, 1 }, vm)
t.func(t, vm)
local p = Anim.present("enemy")
check(p.ox == 3, "ShakeMon applies x offset immediately")
local n = 1
while t.active and n < 100 do AnimTasks.update(vm); n = n + 1 end
check(p.ox == 0 and n == 12, "ShakeMon ends centred after numShakes*(delay+1) frames (" .. n .. ")")
end
do
local vm = fresh_vm("player")
P.beginNormalPaletteFade({ "enemy" }, 0, 0, 16, 0x7FFF)
local n = 1
while P.fadeActive() and n < 100 do
P.updatePaletteFade()
n = n + 1
end
local st = P.Pal.get("enemy")
check(st.m == 0 and math.abs(st.r - 1) < 1e-6, "palette fade reaches full white")
check(n == 23, "normal palette fade 0->16 takes 9 steps x2 frames + 5 finishing (" .. n .. ")")
end
print(string.format("g1 anim port: %d passed, %d failed (%d script runs)", passed, failed, ran))
os.exit(failed == 0 and 0 or 1)