mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
237 lines
8.3 KiB
Lua
237 lines
8.3 KiB
Lua
-- PlayAnimation (engine/battle/animations.asm:437), and every type that
|
|
-- (home/delay.asm:15), which blocks while CHAN5/CHAN6/CHAN8 still sound.
|
|
-- POKEPORT_DRIVER=tests/drivers/bind_sfx_wait_bug1998_test.lua POKEPORT_VERSION=red POKEPORT_TOUCH=0 POKEPORT_IDENTITY=bug1998 SHOT_DIR=/tmp/bind1998 love .
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local BattleState = require("src.battle.BattleState")
|
|
local AnimPlayer = require("src.battle.AnimPlayer")
|
|
local Sound = require("src.core.Sound")
|
|
|
|
local pass, fail = 0, 0
|
|
local function check(label, ok)
|
|
if ok then pass = pass + 1 else fail = fail + 1 end
|
|
U.log(ok and "PASS" or "FAIL", label)
|
|
return ok
|
|
end
|
|
|
|
U.log("#1998 applying-attack shake waits out the move sfx: machine checks")
|
|
|
|
check("Sound exposes the CHAN5/6/8 busy predicate",
|
|
type(Sound.moveSfxBusy) == "function")
|
|
check("Sound exposes the WaitForSoundToFinish frame budget",
|
|
type(Sound.moveSfxWaitFrames) == "function")
|
|
check("nothing is sounding before a battle starts", Sound.moveSfxBusy() == false)
|
|
|
|
-- data/moves/animations.asm:326 BindAnim is SUBANIM_0_BIND twice at delay 4,
|
|
-- (delay + 1) per frame block (:346, home/copy2.asm:62).
|
|
local ba = game.data.battle_anims
|
|
check("battle_anims is in the cache", ba ~= nil)
|
|
|
|
local function compile(move)
|
|
if not (ba and (ba.moveAnims or {})[move]) then return nil end
|
|
local p = AnimPlayer.new(ba)
|
|
local ok = pcall(p.start, p, move, false)
|
|
if not ok then return nil end
|
|
return p
|
|
end
|
|
|
|
local function shapeOf(p)
|
|
local out = {}
|
|
for _, s in ipairs(p.steps) do out[#out + 1] = tostring(s.dur) end
|
|
return table.concat(out, " ")
|
|
end
|
|
|
|
local function total(p)
|
|
local n = 0
|
|
for _, s in ipairs(p.steps) do n = n + s.dur end
|
|
return n
|
|
end
|
|
|
|
local FENCE = {
|
|
{ move = "BIND", frames = 40, shape = "10 5 5 10 5 5" },
|
|
{ move = "WRAP", frames = 60, shape = "10 5 5 10 5 5 10 5 5" },
|
|
{ move = "CONSTRICT", frames = 72, shape = "10 7 7 10 7 7 10 7 7" },
|
|
}
|
|
for _, f in ipairs(FENCE) do
|
|
local p = compile(f.move)
|
|
check(f.move .. " compiles into a timeline", p ~= nil)
|
|
if p then
|
|
U.log((" %s steps: %s"):format(f.move, shapeOf(p)))
|
|
check(("%s is %d frames (want %d): the subanimation is unchanged")
|
|
:format(f.move, total(p), f.frames), total(p) == f.frames)
|
|
check(("%s keeps its %s shape"):format(f.move, f.shape),
|
|
shapeOf(p) == f.shape)
|
|
end
|
|
end
|
|
|
|
U.log(("machine checks: %d passed, %d failed"):format(pass, fail))
|
|
|
|
if not (game.data.moves.BIND and ba) then
|
|
U.log("no BIND animation in this cache, so there is nothing to watch")
|
|
while true do coroutine.yield() end
|
|
end
|
|
|
|
local MAP, SX, SY = "ROUTE_1", 5, 5
|
|
local ekans = Pokemon.new(game.data, "EKANS", 30)
|
|
ekans.moves = { { id = "BIND", pp = game.data.moves.BIND.pp } }
|
|
game.save.party = { ekans }
|
|
game.save.player.name = "RED"
|
|
|
|
U.teleport(game, MAP, SX, SY, "down")
|
|
U.wait(12)
|
|
local ow = game.overworld
|
|
if ow and not ow.map:isWalkableCell(SX, SY) then
|
|
for _, d in ipairs({ { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 } }) do
|
|
local cx, cy = SX + d[1], SY + d[2]
|
|
if ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then
|
|
U.teleport(game, MAP, cx, cy, "down")
|
|
U.wait(12)
|
|
ow = game.overworld
|
|
break
|
|
end
|
|
end
|
|
end
|
|
check("the overworld is up on " .. MAP, ow ~= nil and ow.map.id == MAP)
|
|
|
|
local battle = BattleState.newWild(game, "PIDGEY", 20)
|
|
battle.onFinish = function() end
|
|
ow:pushBattle(battle)
|
|
for _ = 1, 400 do
|
|
if game.stack:top() == battle and (battle.introSlide or 0) == 0 then break end
|
|
U.wait(1)
|
|
end
|
|
check("the battle reached the screen", game.stack:top() == battle)
|
|
|
|
battle.enemy.mon.stats.hp = 500
|
|
battle.enemy.mon.hp = 500
|
|
battle.enemy.shownHP = 500
|
|
for i = #battle.enemy.mon.moves, 1, -1 do
|
|
local id = battle.enemy.mon.moves[i].id
|
|
if id == "WHIRLWIND" or id == "ROAR" or id == "TELEPORT" then
|
|
table.remove(battle.enemy.mon.moves, i)
|
|
end
|
|
end
|
|
|
|
local held, sawBusy, shakeAt, animEnd = 0, false, nil, nil
|
|
local budgetSeen, tailSeen = 0, 0
|
|
local frame = 0
|
|
local curMove
|
|
if battle.animPlayer then
|
|
local realStart = battle.animPlayer.start
|
|
battle.animPlayer.start = function(self, moveId, isPlayer, o)
|
|
local r = realStart(self, moveId, isPlayer, o)
|
|
curMove = moveId
|
|
return r
|
|
end
|
|
end
|
|
local function sample()
|
|
frame = frame + 1
|
|
if curMove ~= "BIND" then return end
|
|
if battle.animPlaying then
|
|
local n = Sound.moveSfxWaitFrames()
|
|
if n > tailSeen then tailSeen = n end
|
|
end
|
|
if battle.hitSfxWait then
|
|
held = held + 1
|
|
sawBusy = true
|
|
animEnd = animEnd or frame
|
|
if battle.hitSfxWait + 1 > budgetSeen then budgetSeen = battle.hitSfxWait + 1 end
|
|
end
|
|
if not shakeAt and battle.fx and (battle.fx.shakeProg or battle.fx.blink) then
|
|
shakeAt = frame
|
|
end
|
|
end
|
|
|
|
local function pump(n, mash, stop)
|
|
for i = 1, n do
|
|
if mash and i % mash == 0 and battle.phase == "messages" then
|
|
table.insert(game.input.pressQueue, "a")
|
|
end
|
|
U.wait(1)
|
|
game.input.state.a = false
|
|
sample()
|
|
if stop and stop() then return end
|
|
end
|
|
end
|
|
|
|
local function toMenu()
|
|
pump(1200, 6, function() return battle.phase == "menu" and #battle.queue == 0 end)
|
|
return battle.phase == "menu"
|
|
end
|
|
|
|
local function useBind()
|
|
for _ = 1, 80 do
|
|
if battle.phase == "moveSelect" then break end
|
|
if battle.phase == "menu" then
|
|
if battle.menuIndex ~= 1 then
|
|
U.tap(game, battle.menuIndex > 2 and "up" or "left")
|
|
else
|
|
U.tap(game, "a")
|
|
end
|
|
else
|
|
U.tap(game, "a")
|
|
end
|
|
for _ = 1, 3 do U.wait(1) sample() end
|
|
end
|
|
if battle.phase ~= "moveSelect" then return false end
|
|
for _ = 1, 20 do
|
|
if battle.phase ~= "moveSelect" then return true end
|
|
U.tap(game, "a")
|
|
for _ = 1, 3 do U.wait(1) sample() end
|
|
end
|
|
return false
|
|
end
|
|
|
|
check("the battle reached its FIGHT menu", toMenu())
|
|
|
|
local sent = false
|
|
for _ = 1, 6 do
|
|
battle.player.stages.accuracy = 0
|
|
shakeAt, animEnd, held, curMove = nil, nil, 0, nil
|
|
budgetSeen, tailSeen = 0, 0
|
|
sent = useBind()
|
|
if not sent then break end
|
|
pump(900, 8, function() return shakeAt ~= nil and not battle.animPlaying end)
|
|
if shakeAt then break end
|
|
U.log(" BIND did not connect that turn; using it again")
|
|
toMenu()
|
|
end
|
|
check("chose BIND from the move menu", sent)
|
|
check("BIND's applying-attack fx eventually ran", shakeAt ~= nil)
|
|
|
|
U.log((" the shake was held for %d frames after the animation finished"):format(held))
|
|
U.log((" budget taken at the gate: %d frames; longest BIND sfx tail seen "
|
|
.. "during the animation: %d frames"):format(budgetSeen, tailSeen))
|
|
if sawBusy then
|
|
check("the shake waited on the BIND sfx instead of starting the same frame",
|
|
held > 0)
|
|
else
|
|
U.log(" the BIND sfx had already finished when the animation ended, so")
|
|
U.log(" there was nothing to wait for on this turn (short sfx, or SFX is")
|
|
U.log(" muted in OPTION). raise SFX and try the pad below before judging.")
|
|
end
|
|
U.shot(game, DIR .. "/bug1998_bind_after.png")
|
|
|
|
local opts = game.save.options or {}
|
|
if opts.animations == false then
|
|
U.log(" ANIMATION is off in OPTION, which skips the whole row: turn it on")
|
|
end
|
|
if opts.sfxVol == 0 then
|
|
U.log(" sfxVol is 0, so there is no sfx tail to wait on: raise SFX")
|
|
end
|
|
|
|
U.log("the pad is yours in a battle where the EKANS knows only BIND (#1998).")
|
|
U.log("pick FIGHT then BIND and watch the END of the animation: the two coil")
|
|
U.log("frames should finish, the screen should sit completely still for about")
|
|
U.log("half a second while the squeeze sfx rings out, and only then should the")
|
|
U.log("screen shake and the HP bar drain.")
|
|
U.log("the bug was the shake starting on the very next frame, which wiped the")
|
|
U.log("second coil off the screen mid-sound and read as a cut-off animation.")
|
|
U.log("BIND traps the foe, so the next few turns replay it as \"attack")
|
|
U.log("continues!\" and you can watch the beat more than once.")
|
|
U.log("screenshots: " .. DIR .. "/bug1998_*.png")
|
|
while true do coroutine.yield() end
|
|
end
|