mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
123feef3a1
CLOSES #2425, CLOSES #2428, CLOSES #2431, CLOSES #2432, CLOSES #2433, CLOSES #2435, CLOSES #2436, CLOSES #2437, CLOSES #2440
315 lines
14 KiB
Lua
315 lines
14 KiB
Lua
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local passed, failed = 0, 0
|
|
local function check(cond, name)
|
|
if cond then
|
|
passed = passed + 1
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. tostring(name))
|
|
end
|
|
end
|
|
|
|
local function find_pack()
|
|
local root = require("tests.game3_cache").root("pokemon/battle_anims/pack.lua")
|
|
local cands = {
|
|
os.getenv("G4_ANIM_PACK") or "",
|
|
root and (root .. "/pokemon/battle_anims/pack.lua") or "",
|
|
}
|
|
for _, p in ipairs(cands) do
|
|
if p and p ~= "" then
|
|
local f = io.open(p, "rb")
|
|
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 errs = {}
|
|
local rawPrint = print
|
|
print = function(...)
|
|
local s = table.concat({ ... }, " ")
|
|
if s:find("%[battle%.anim%]") then errs[#errs + 1] = s end
|
|
rawPrint(...)
|
|
end
|
|
|
|
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 AnimCallbacks = require("src.core.game3.battle.anim_callbacks")
|
|
local P = require("src.core.game3.battle.anim_port.g4_pret")
|
|
|
|
rawPrint("=== G4 anim port ===")
|
|
|
|
local function fresh_vm(pack)
|
|
Anim.reset({ headless = false })
|
|
if pack then Anim.loadPack(pack) end
|
|
Anim.present("player").visible = true
|
|
Anim.present("enemy").visible = true
|
|
return Anim.vm()
|
|
end
|
|
|
|
local function run_to_end(vm, cap)
|
|
local frames = 0
|
|
while vm.active and frames < (cap or 3000) do
|
|
Anim.update(1 / 60)
|
|
frames = frames + 1
|
|
end
|
|
return frames
|
|
end
|
|
|
|
rawPrint("[test] 1. pret trig / panning")
|
|
check(P.Sin(64, 256) == 256, "Sin(64,256) == 256")
|
|
check(P.Cos(0, 40) == 40, "Cos(0,40) == 40")
|
|
check(P.Sin(192, 10) == -10, "Sin(192,10) == -10")
|
|
check(P.Sin(-2, 100) == P.Sin(254, 100), "Sin masks to u8")
|
|
do
|
|
local vm = fresh_vm(nil)
|
|
vm:setBattlers("enemy", "player")
|
|
check(vm:adjustPanning(-64) == 63, "enemy attacker pans SOUND_PAN_ATTACKER to 63")
|
|
check(vm:adjustPanning(63) == -63, "enemy attacker negates SOUND_PAN_TARGET to -63")
|
|
vm:setBattlers("player", "enemy")
|
|
check(vm:adjustPanning(-64) == -64, "player attacker keeps pan")
|
|
vm:setBattlers("player", "player")
|
|
check(vm:adjustPanning(63) == -64, "self-target maps TARGET to ATTACKER")
|
|
end
|
|
|
|
rawPrint("[test] 2. VM opcode semantics")
|
|
do
|
|
local vm = fresh_vm(nil)
|
|
vm:launch({ { op = "delay", frames = 5 }, { op = "end" } }, { attackerSide = "player", targetSide = "enemy" })
|
|
local n = 0
|
|
while vm.active and n < 100 do vm:update(); n = n + 1 end
|
|
check(n == 8, "delay 5 resumes on the 8th callback frame like WaitAnimFrameCount (got " .. n .. ")")
|
|
|
|
vm:launch({ { op = "delay", frames = 0 }, { op = "end" } }, {})
|
|
n = 0
|
|
while vm.active and n < 100 do vm:update(); n = n + 1 end
|
|
check(n == 3, "delay 0 is -1 and resumes 2 frames later (got " .. n .. ")")
|
|
|
|
vm:launch({ { op = "setarg", argId = 7, value = -1 }, { op = "jumpargeq", argId = 7, value = -1, label = "X" }, { op = "delay", frames = 50 }, { op = "end" } }, {})
|
|
vm._pack = { labels = { X = { { op = "end" } } } }
|
|
n = 0
|
|
while vm.active and n < 100 do vm:update(); n = n + 1 end
|
|
check(n == 1, "setarg + jumpargeq take the branch (got " .. n .. ")")
|
|
|
|
local tick = 0
|
|
AnimTasks.REGISTRY._G4TestVisual = function(t)
|
|
tick = tick + 1
|
|
if tick >= 10 then AnimTasks.destroy(t) end
|
|
end
|
|
vm:launch({ { op = "createvisualtask", task = "_G4TestVisual", priority = 2, args = { 3, 4 } }, { op = "waitforvisualfinish" }, { op = "end" } }, {})
|
|
vm:update()
|
|
check(tick == 2, "createvisualtask runs the task immediately, then again in RunTasks the same frame")
|
|
check(vm.args[0] == 3 and vm.args[1] == 4, "createvisualtask writes gBattleAnimArgs")
|
|
n = 1
|
|
while vm.active and n < 100 do vm:update(); n = n + 1 end
|
|
check(tick == 10 and n == 10, "waitforvisualfinish waits for the visual task (frames " .. n .. ")")
|
|
end
|
|
|
|
local pack = find_pack()
|
|
if not pack then
|
|
rawPrint("[skip] no battle anim pack found (set G4_ANIM_PACK); pack-driven checks skipped")
|
|
print = rawPrint
|
|
rawPrint(string.format("G4 anim port: %d passed, %d failed", passed, failed))
|
|
os.exit(failed == 0 and 0 or 1)
|
|
end
|
|
|
|
rawPrint("[test] 3. createsoundtask spawns SoundTask_*")
|
|
do
|
|
local vm = fresh_vm(pack)
|
|
vm:launchTable("moves", 126, { attackerSide = "player", targetSide = "enemy", attackerSpecies = 6, targetSpecies = 9 })
|
|
local seen = false
|
|
for _ = 1, 30 do
|
|
Anim.update(1 / 60)
|
|
for i = 1, AnimTasks.MAX do
|
|
local t = AnimTasks._pool[i]
|
|
if t.active and t._g4kind == "sound" and tostring(t.name):find("SoundTask_FireBlast") then seen = true end
|
|
end
|
|
if seen then break end
|
|
end
|
|
check(seen, "FIRE_BLAST createsoundtask spawns SoundTask_FireBlast as a sound task")
|
|
check(vm:soundCount() >= 1, "sound task counted by gAnimSoundTaskCount")
|
|
run_to_end(vm)
|
|
check(not vm.active, "FIRE_BLAST runs to end")
|
|
end
|
|
|
|
rawPrint("[test] 4. every general/special/status script runs to end")
|
|
local function run_table(vm, kind, idx, opts)
|
|
errs = {}
|
|
opts = opts or {}
|
|
local o = { attackerSide = opts.a or "player", targetSide = opts.t or "enemy", attackerSpecies = 6, targetSpecies = 9,
|
|
ctx = opts.ctx or { ballThrowCaseId = 0, lastUsedItem = 4 }, animArg = opts.animArg }
|
|
vm:launchTable(kind, idx, o)
|
|
local frames = run_to_end(vm, 2000)
|
|
local capped = false
|
|
for _, e in ipairs(errs) do if e:find("cap") then capped = true end end
|
|
return frames, (not vm.active) and not capped, errs
|
|
end
|
|
do
|
|
local vm = fresh_vm(pack)
|
|
for _, kind in ipairs({ "general", "special", "status" }) do
|
|
if pack[kind] then
|
|
for i = 0, 40 do
|
|
if pack[kind][i] then
|
|
for _, sides in ipairs({ { "player", "enemy" }, { "enemy", "player" } }) do
|
|
Anim.present("player").visible = true
|
|
Anim.present("enemy").visible = true
|
|
local frames, ok, e = run_table(vm, kind, i, { a = sides[1], t = sides[2] })
|
|
local name = (pack[kind .. "Names"] and pack[kind .. "Names"][i]) or i
|
|
check(ok and #e == 0, string.format("%s[%d] %s (%s) ends cleanly in %d frames %s", kind, i, tostring(name), sides[1], frames, table.concat(e, " | ")))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
rawPrint("[test] 5. every move using a G4-scope function runs to end")
|
|
local SCOPE = {}
|
|
for _, n in ipairs({
|
|
"SpriteCB_SafariBaitOrRock_Init", "MegahornHorn", "LeechLifeNeedle", "TranslateWebThread", "StringWrap", "SpiderWeb",
|
|
"TranslateStinger", "MissileArc", "TailGlowOrb", "Lightning", "SparkElectricity", "ZapCannonSpark", "ThunderboltOrb",
|
|
"SparkElectricityFlashing", "Electricity", "ThunderWave", "GrowingChargeOrb", "ElectricPuff", "VoltTackleOrbSlide",
|
|
"GrowingShockWaveOrb", "BonemerangProjectile", "BoneHitProjectile", "DirtScatter", "MudSportDirt", "DirtPlumeParticle",
|
|
"DigDirtMound", "SludgeProjectile", "AcidPoisonBubble", "SludgeBombHitParticle", "AcidPoisonDroplet", "BubbleEffect",
|
|
"IcePunchSwirlingParticle", "IceBeamParticle", "IceEffectParticle", "SwirlingSnowball", "MoveParticleBeyondTarget",
|
|
"WaveFromCenterOfTarget", "InitSwirlingFogAnim", "ThrowMistBall", "InitPoisonGasCloudAnim", "InitIceBallAnim",
|
|
"InitIceBallParticle", "FallingRock", "RockFragment", "ParticleInVortex", "FlyingSandCrescent", "RaiseSprite",
|
|
"RockTomb", "RockBlastRock", "RockScatter", "WaterBubbleProjectile", "AuroraBeamRings", "ToTargetInSinWave",
|
|
"HydroCannonCharge", "HydroCannonBeam", "WaterGunDroplet", "SmallBubblePair", "SmallDriftingBubbles",
|
|
"WaterPulseBubble", "WaterPulseRing",
|
|
"ElectricBolt", "ElectricChargingParticles", "ShockWaveLightning", "VoltTackleAttackerReappear", "VoltTackleBolt",
|
|
"ShockWaveProgressingBolt", "HorizontalShake", "IsPowerOver99", "PositionFissureBgOnBattler", "GetRolloutCounter",
|
|
"HazeScrollingFog", "MistBallFog", "Hail", "Rollout", "GetSeismicTossDamageLevel", "MoveSeismicTossBg",
|
|
"SeismicTossBgAccelerateDownAtEnd", "CreateRaindrops", "RotateAuroraRingColors", "StartSinAnimTimer",
|
|
"WaterSport", "WaterSpoutLaunch", "WaterSpoutRain",
|
|
"SoundTask_FireBlast", "SoundTask_LoopSEAdjustPanning", "SoundTask_PlayCryWithEcho", "SoundTask_PlaySE2WithPanning",
|
|
"SoundTask_PlaySE1WithPanning", "SoundTask_AdjustPanningVar", "SoundTask_PlayCryHighPitch", "SoundTask_PlayDoubleCry",
|
|
"SoundTask_WaitForCry",
|
|
}) do SCOPE[n] = true end
|
|
|
|
local function uses_scope(ops, seen)
|
|
seen = seen or {}
|
|
if seen[ops] then return false end
|
|
seen[ops] = true
|
|
for _, op in ipairs(ops) do
|
|
local n = op.callback or op.task
|
|
if n and SCOPE[n] then return true end
|
|
for _, l in ipairs({ op.label, op.label1, op.label2 }) do
|
|
if l and pack.labels[l] and uses_scope(pack.labels[l], seen) then return true end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
do
|
|
local vm = fresh_vm(pack)
|
|
local count = 0
|
|
for id = 1, 354 do
|
|
local s = pack.moves[id]
|
|
if s and uses_scope(s) then
|
|
count = count + 1
|
|
for _, sides in ipairs({ { "player", "enemy" }, { "enemy", "player" } }) do
|
|
Anim.present("player").visible = true
|
|
Anim.present("enemy").visible = true
|
|
local frames, ok, e = run_table(vm, "moves", id, { a = sides[1], t = sides[2], ctx = { movePower = 120, moveDamage = 50, rolloutTimerStartValue = 5, rolloutTimer = 3 } })
|
|
check(ok and #e == 0, string.format("move %d (%s) ends cleanly in %d frames %s", id, sides[1], frames, table.concat(e, " | ")))
|
|
end
|
|
end
|
|
end
|
|
check(count >= 60, "scope covers the expected move scripts (" .. count .. ")")
|
|
end
|
|
|
|
rawPrint("[test] 6. pret-derived values")
|
|
do
|
|
local vm = fresh_vm(pack)
|
|
vm:launch({ { op = "createsprite", template = "gThunderWaveSpriteTemplate", callback = "ThunderWave", tag = "SPARK_H", w = 32, h = 16, animBattler = "target", subpriority = 2, args = { -16, -16 } }, { op = "waitforvisualfinish" }, { op = "end" } },
|
|
{ attackerSide = "player", targetSide = "enemy", attackerSpecies = 6, targetSpecies = 9 })
|
|
vm:update()
|
|
local counted = 0
|
|
for i = 1, AnimSprites.MAX do
|
|
local s = AnimSprites._pool[i]
|
|
if s.active and s._g4counted then counted = counted + 1 end
|
|
end
|
|
check(counted == 2, "ThunderWave creates a second counted sprite (gAnimVisualTaskCount++)")
|
|
local frames = run_to_end(vm)
|
|
check(frames >= 51 and frames <= 55, "ThunderWave bands live 51 frames (script ended at " .. frames .. ")")
|
|
|
|
vm = fresh_vm(pack)
|
|
vm:launch({ { op = "createsprite", template = "gLinearStingerSpriteTemplate", callback = "TranslateStinger", tag = "NEEDLE", w = 16, h = 16, animBattler = "attacker", subpriority = 2, args = { 20, 0, 0, 0, 20 } }, { op = "end" } },
|
|
{ attackerSide = "player", targetSide = "enemy", attackerSpecies = 6, targetSpecies = 9 })
|
|
vm:update()
|
|
local st
|
|
for i = 1, AnimSprites.MAX do
|
|
local s = AnimSprites._pool[i]
|
|
if s.active and s._cbName == "TranslateStinger" then st = s end
|
|
end
|
|
check(st and st.x == 72 + 20, "TranslateStinger starts at attacker X_2 + arg0")
|
|
check(st and st._mat and st._mat.rot > 0xC000 - 0x1000 and st._mat.rot < 0xFFFF, "TranslateStinger rotation = ArcTan2Neg + 0xC000")
|
|
run_to_end(vm)
|
|
|
|
vm = fresh_vm(pack)
|
|
vm:launch({ { op = "createvisualtask", task = "SwitchOutShrinkMon", priority = 2, args = {} }, { op = "waitforvisualfinish" }, { op = "end" } },
|
|
{ attackerSide = "player", targetSide = "enemy" })
|
|
local p = Anim.present("player")
|
|
local n = 0
|
|
while vm.active and n < 200 do
|
|
Anim.update(1 / 60)
|
|
n = n + 1
|
|
end
|
|
check(p.visible == false and p.sx == 1, "SwitchOutShrinkMon hides the mon and resets rot/scale")
|
|
check(n >= 12 and n <= 16, "SwitchOutShrinkMon: 0x100 -> 0x2D0 step 0x30 (" .. n .. " frames)")
|
|
|
|
vm = fresh_vm(pack)
|
|
vm:launch({ { op = "createvisualtask", task = "GetTrappedMoveAnimId", priority = 2, args = {} }, { op = "end" } }, { animArg = 83 })
|
|
vm:update()
|
|
check(vm.args[0] == 1, "GetTrappedMoveAnimId: Fire Spin -> TRAP_ANIM_FIRE_SPIN")
|
|
run_to_end(vm)
|
|
|
|
vm = fresh_vm(pack)
|
|
vm:launch({ { op = "createvisualtask", task = "IsBallBlockedByTrainerOrDodged", priority = 2, args = {} }, { op = "end" } }, { ctx = { ballThrowCaseId = 5 } })
|
|
vm:update()
|
|
check(vm.args[7] == -1, "IsBallBlockedByTrainerOrDodged: trainer block -> -1")
|
|
run_to_end(vm)
|
|
|
|
vm = fresh_vm(pack)
|
|
vm:launch({ { op = "createvisualtask", task = "SafariOrGhost_DecideAnimSides", priority = 2, args = { 1 } }, { op = "delay", frames = 3 }, { op = "end" } }, { attackerSide = "player", targetSide = "enemy" })
|
|
vm:update()
|
|
check(vm:attackerSide() == "enemy" and vm:targetSide() == "player", "SafariOrGhost_DecideAnimSides(1) swaps attacker/target")
|
|
run_to_end(vm)
|
|
|
|
vm = fresh_vm(pack)
|
|
vm:launch({ { op = "createvisualtask", task = "GetSeismicTossDamageLevel", priority = 2, args = {} }, { op = "end" } }, { ctx = { moveDamage = 40 } })
|
|
vm:update()
|
|
check(vm.args[7] == 1, "GetSeismicTossDamageLevel: 33..65 -> 1")
|
|
run_to_end(vm)
|
|
|
|
vm = fresh_vm(pack)
|
|
local played = {}
|
|
local Audio = require("src.core.game3.audio")
|
|
local realPlay = Audio.playSe
|
|
Audio.playSe = function(id, o) played[#played + 1] = { id = id, pan = o and o.pan } return true end
|
|
vm:launch({ { op = "loopsewithpan", se = 120, pan = -64, wait = 4, times = 3 }, { op = "end" } }, { attackerSide = "enemy", targetSide = "player" })
|
|
vm:update()
|
|
check(#played == 1 and played[1].pan == 63, "loopsewithpan plays immediately with adjusted pan")
|
|
run_to_end(vm)
|
|
check(#played == 3, "loopsewithpan plays 'times' sounds (" .. #played .. ")")
|
|
played = {}
|
|
vm:launch({ { op = "createsoundtask", task = "SoundTask_LoopSEAdjustPanning", args = { 130, -64, 63, 16, 4, 0, 5 } }, { op = "end" } }, { attackerSide = "player", targetSide = "enemy" })
|
|
local frames = run_to_end(vm)
|
|
check(#played == 4, "LoopSEAdjustPanning plays arg4 times (" .. #played .. ")")
|
|
check(played[1] and played[1].pan == -64 and played[2] and played[2].pan > played[1].pan, "LoopSEAdjustPanning pans attacker -> target")
|
|
Audio.playSe = realPlay
|
|
end
|
|
|
|
print = rawPrint
|
|
rawPrint(string.format("G4 anim port: %d passed, %d failed", passed, failed))
|
|
if failed > 0 then os.exit(1) end
|