mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
259 lines
10 KiB
Lua
259 lines
10 KiB
Lua
-- pokeyellow data/pikachu/pikachu_pic_animation.asm:281
|
|
-- pokeyellow engine/pikachu/pikachu_pic_animation.asm:790
|
|
-- pokeyellow data/pikachu/pikachu_pic_animation.asm:1
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
love = love or require("tests.love_stub")
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local PikachuFollower = require("src.world.PikachuFollower")
|
|
local Sound = require("src.core.Sound")
|
|
local Music = require("src.core.Music")
|
|
local Assets = require("src.render.Assets")
|
|
|
|
GameVersion.set("yellow")
|
|
|
|
local moveCalls, cryCalls, ducks = {}, {}, {}
|
|
local busyFrames = 0
|
|
local realPlayMove, realPikaCry, realCry = Sound.playMove, Sound.playPikaCry, Sound.playCry
|
|
local realBusy, realDuck = Sound.moveSfxBusy, Music.duckForFanfare
|
|
Sound.playMove = function(_, anim) moveCalls[#moveCalls + 1] = anim end
|
|
Sound.playPikaCry = function(_, n) cryCalls[#cryCalls + 1] = n return true end
|
|
Sound.playCry = function() return nil end
|
|
Sound.moveSfxBusy = function()
|
|
if busyFrames > 0 then busyFrames = busyFrames - 1 return true end
|
|
return false
|
|
end
|
|
Music.duckForFanfare = function(src) ducks[#ducks + 1] = src end
|
|
local realExists = Assets.exists
|
|
Assets.exists = function() return false end
|
|
|
|
local tbAnim = { sound = "Battle_2F", pitch = 32, tempo = 128 }
|
|
local game = {
|
|
data = {
|
|
moves = { THUNDERBOLT = { anim = tbAnim } },
|
|
field = { emotionBubbles = { bubbles = {
|
|
{ name = "EXCLAMATION_BUBBLE" }, { name = "QUESTION_BUBBLE" },
|
|
{ name = "SMILE_BUBBLE" }, { name = "BOLT_BUBBLE" },
|
|
} } },
|
|
},
|
|
save = {
|
|
player = { name = "YELLOW", id = 1234 },
|
|
party = { { species = "PIKACHU", hp = 40, ot = "YELLOW", otId = 1234 } },
|
|
pikachuHappiness = 120, pikachuMood = 128, flags = {},
|
|
},
|
|
}
|
|
|
|
PikachuFollower.onMoveLearned(game.save, game.save.party[1], "THUNDERBOLT")
|
|
eq(game.save.pikachuEmotionModifier, 5, "learning THUNDERBOLT arms modifier 5")
|
|
|
|
local function newOw()
|
|
return {
|
|
map = { id = "ROCK_TUNNEL_1F" },
|
|
player = { facing = "up" },
|
|
}
|
|
end
|
|
|
|
local function runPress(ow, pressAt)
|
|
local log = { frames = 0, bgp = {}, skippableAt = {}, pics = {}, lifts = {},
|
|
close = {} }
|
|
local guard = 0
|
|
while ow.emote and guard < 2000 do
|
|
guard = guard + 1
|
|
local e = ow.emote
|
|
if e.boltAt then PikachuFollower.tickBolt(game, ow, e) end
|
|
e.frames = e.frames - 1
|
|
local pressed = pressAt and log.frames + 1 == pressAt
|
|
log.frames = log.frames + 1
|
|
if e.pikaPic then
|
|
log.pic = log.pic or e
|
|
local path, lift = PikachuFollower.picFrame(e)
|
|
log.pics[#log.bgp + 1] = path and path:match("([^/]+)%.png$") or false
|
|
log.lifts[#log.bgp + 1] = lift
|
|
log.bgp[#log.bgp + 1] = e.bgp or false
|
|
log.skippableAt[#log.skippableAt + 1] = e.skippable and true or false
|
|
if e.boltDone and e.frames > 0 then
|
|
log.close[#log.close + 1] = tostring(log.pics[#log.pics])
|
|
end
|
|
end
|
|
if (e.skippable and pressed) or e.frames <= 0 then
|
|
local done = e.onDone
|
|
ow.emote = nil
|
|
if done then done() end
|
|
end
|
|
end
|
|
return log
|
|
end
|
|
|
|
local function newNpc()
|
|
return { cellX = 5, cellY = 5, px = 80, py = 80, facing = "down" }
|
|
end
|
|
|
|
local ow = newOw()
|
|
PikachuFollower.talk(game, ow, newNpc(), function() end)
|
|
check(ow.emote ~= nil, "talking to Pikachu opens an emote")
|
|
eq(ow.emote.bubble, 4, "modifier 5 opens with the BOLT bubble (PikachuEmotion25)")
|
|
eq(ow.emote.pikaPic, nil, "the bubble comes before the pikapic")
|
|
|
|
local log = runPress(ow)
|
|
check(log.pic ~= nil, "the pikapic box follows the bubble")
|
|
eq(log.pic.boltAt, 45, "the bolt fires after 2 setup ticks + writebyte 13 (45 frames)")
|
|
eq(#cryCalls >= 1 and cryCalls[1], 35, "PikachuCry35 plays")
|
|
eq(#moveCalls, 1, "the THUNDERBOLT move sound plays once")
|
|
check(moveCalls[1] == tbAnim, "it is THUNDERBOLT's MoveSoundTable entry (Battle_2F)")
|
|
eq(#ducks, 1, "music is muted for the bolt")
|
|
|
|
local pre = 0
|
|
for i = 1, #log.bgp do
|
|
if log.bgp[i] then break end
|
|
pre = pre + 1
|
|
end
|
|
eq(pre, 46, "no palette write until the mute DelayFrame has passed")
|
|
local strobeOk = true
|
|
for row = 1, 20 do
|
|
local want = (row % 2 == 1) and 0xC0 or 0xE4
|
|
for f = 1, 4 do
|
|
if log.bgp[pre + (row - 1) * 4 + f] ~= want then strobeOk = false end
|
|
end
|
|
end
|
|
check(strobeOk, "20 rows of 4 frames alternate %11000000 / %11100100")
|
|
local tail = #log.bgp - (pre + 80)
|
|
check(tail >= 1, "the box stays up after the strobe")
|
|
local tailLit = true
|
|
for i = pre + 81, #log.bgp do
|
|
if log.bgp[i] ~= 0xE4 then tailLit = false end
|
|
end
|
|
check(tailLit, "the lit %11100100 BGP holds until the box closes")
|
|
|
|
eq(log.pic.pikaPic, "assets/generated/pikachu/pikapic_25.png",
|
|
"the base pic is the cache rip even when Assets.exists says no (no fallback)")
|
|
local function runOf(from, to, want)
|
|
for i = from, to do
|
|
if log.pics[i] ~= want or log.lifts[i] ~= 0 then return false end
|
|
end
|
|
return true
|
|
end
|
|
check(runOf(1, 17, "pikapic_25"), "ticks 0-5: the base frown Pic_e77cf alone (pikaframedelay 6)")
|
|
check(runOf(18, 35, "gfx_e7863"), "ticks 6-11: PikaAnimTilemap_9 draws GFX_e7863")
|
|
check(runOf(36, pre, "gfx_e79f3"), "tick 12 on: PikaAnimTilemap_10 draws GFX_e79f3")
|
|
local strobePose = true
|
|
for i = pre + 1, pre + 80 do
|
|
if log.pics[i] ~= "gfx_e79f3" or log.lifts[i] ~= 0 then strobePose = false end
|
|
end
|
|
check(strobePose, "the Thunderbolt pose GFX_e79f3 holds through every strobe frame, unlifted")
|
|
local tailPose, blank = true, 0
|
|
for i = pre + 81, #log.pics do
|
|
if log.pics[i] == false then blank = blank + 1
|
|
elseif blank > 0 or log.pics[i] ~= "gfx_e79f3" then tailPose = false end
|
|
end
|
|
check(tailPose, "and through the lit tail")
|
|
check(blank >= 1, "the box is emptied before it closes (.RunPikapic PlacePikapicTextBoxBorder)")
|
|
eq(table.concat(log.close, ",", 1, math.min(#log.close, 6)) .. "|" .. #log.close,
|
|
"gfx_e79f3,gfx_e79f3,gfx_e79f3,false,false,false|6",
|
|
"PlacePikapicTextBoxBorder: the pose stays up for the first Delay3, the empty box for the second")
|
|
eq(log.skippableAt[45], true, "A/B can still cut the pic before the bolt")
|
|
eq(log.skippableAt[46], false, "the bolt itself is not skippable")
|
|
check(not ducks[1].isPlaying(), "the mute releases once the box closes")
|
|
|
|
eq(game.save.pikachuEmotionModifier, 5, "the talk keeps the modifier")
|
|
moveCalls, ducks = {}, {}
|
|
ow = newOw()
|
|
PikachuFollower.talk(game, ow, newNpc(), function() end)
|
|
eq(ow.emote and ow.emote.bubble, 4, "a second talk inside the window replays PikachuEmotion25")
|
|
busyFrames = 30
|
|
log = runPress(ow)
|
|
eq(#moveCalls, 1, "the second talk bolts again")
|
|
eq(#log.bgp - (46 + 80), 30 + 7, "WaitForSoundToFinish holds the box until the sound ends")
|
|
|
|
moveCalls, ducks = {}, {}
|
|
ow = newOw()
|
|
PikachuFollower.talk(game, ow, newNpc(), function() end)
|
|
local bubbleFrames = ow.emote.frames
|
|
log = runPress(ow, bubbleFrames + 10)
|
|
eq(#moveCalls, 0, "cutting the pic short before the bolt skips the flash")
|
|
eq(#ducks, 0, "and never mutes the music")
|
|
|
|
for _ = 1, 5 do PikachuFollower.onStep(game.save) end
|
|
eq(game.save.pikachuEmotionModifier, nil, "five steps from $85 clear the modifier")
|
|
ow = newOw()
|
|
PikachuFollower.talk(game, ow, newNpc(), function() end)
|
|
moveCalls = {}
|
|
log = runPress(ow)
|
|
eq(#moveCalls, 0, "a plain mood talk has no bolt")
|
|
check(log.pic == nil or log.pic.boltAt == nil, "and no bolt spec")
|
|
|
|
local function poseAt(e, tick)
|
|
e.frames = e.pikaTotal - tick * 3
|
|
local path, lift = PikachuFollower.picFrame(e)
|
|
return path and path:match("([^/]+)%.png$") or false, lift
|
|
end
|
|
game.save.pikachuHappiness, game.save.pikachuMood = 255, 255
|
|
ow = newOw()
|
|
PikachuFollower.talk(game, ow, newNpc(), function() end)
|
|
local e20 = ow.emote
|
|
check(e20 and e20.pikaPoses ~= nil, "emotion 20 plays PikaPicAnimBGFrames_26 from ripped poses")
|
|
if e20 and e20.pikaPoses then
|
|
eq(poseAt(e20, 0), "pikapic_20", "script 20 opens on its base pic (pikaframedelay 8)")
|
|
eq(poseAt(e20, 8), "gfx_e6646", "tick 8: PikaAnimTilemap_34 draws GFX_e6646")
|
|
eq(poseAt(e20, 20), "pikapic_20", "tick 20: back to the base")
|
|
eq(poseAt(e20, 28), "gfx_e6646", "tick 28: the pose again")
|
|
eq(poseAt(e20, 40), "pikapic_20", "tick 40: the frameset restarts at pikaframeend")
|
|
local _, lift = poseAt(e20, 8)
|
|
eq(lift, 0, "a ripped pose is drawn in place, not lifted")
|
|
eq(PikachuFollower.picLift(e20), 0, "picLift stays 0 for a pose script")
|
|
end
|
|
|
|
local P = PikachuFollower.PIKAPIC
|
|
local function fakeEmote(script)
|
|
local a = P[script]
|
|
local poses
|
|
if a.poses then
|
|
poses = {}
|
|
for i = 1, #a.seq do
|
|
poses[i] = a.poses[i] and ("assets/generated/pikachu/gfx_" .. a.poses[i] .. ".png") or false
|
|
end
|
|
end
|
|
return { pikaPic = "assets/generated/pikachu/pikapic_" .. script .. ".png",
|
|
pikaSeq = a.seq, pikaPoses = poses, pikaTotal = a.dur * 3, frames = a.dur * 3 }
|
|
end
|
|
local e21 = fakeEmote(21)
|
|
eq(poseAt(e21, 8), "gfx_e682f", "script 21 tick 8: PikaAnimTilemap_9 -> GFX_e682f")
|
|
eq(poseAt(e21, 10), "gfx_e69bf", "script 21 tick 10: PikaAnimTilemap_10 -> GFX_e69bf")
|
|
eq(poseAt(e21, 11), "gfx_e6b4f", "script 21 tick 11: PikaAnimTilemap_11 -> GFX_e6b4f")
|
|
eq(poseAt(e21, 12), "gfx_e6cdf", "script 21 tick 12: PikaAnimTilemap_12 -> GFX_e6cdf")
|
|
local e26 = fakeEmote(26)
|
|
eq(poseAt(e26, 56), "gfx_e7b83", "script 26 tick 56: GFX_e7b83")
|
|
eq(poseAt(e26, 64), "gfx_e7d13", "script 26 tick 64: GFX_e7d13 holds")
|
|
local e7 = fakeEmote(7)
|
|
eq(poseAt(e7, 0), "gfx_e4841", "script 7 opens on PikaAnimTilemap_20 (GFX_e4841)")
|
|
eq(poseAt(e7, 8), "pikapic_7", "script 7 tick 8: base")
|
|
local e4 = fakeEmote(4)
|
|
local p4, l4 = poseAt(e4, 8)
|
|
check(p4 == "pikapic_4" and l4 > 0, "script 4 (20-tile GFX_e444b overlay) keeps the picLift stand-in")
|
|
|
|
local CacheContract = require("src.import.CacheContract")
|
|
local required = {}
|
|
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES.yellow or {}) do required[path] = true end
|
|
local listed, missing = 0, {}
|
|
for script, a in pairs(P) do
|
|
for _, id in ipairs(a.poses or {}) do
|
|
if id then
|
|
local path = "assets/generated/pikachu/gfx_" .. id .. ".png"
|
|
if required[path] then listed = listed + 1 else missing[#missing + 1] = script .. ":" .. id end
|
|
end
|
|
end
|
|
if a.poses then eq(#a.poses, #a.seq, "script " .. script .. " has one pose slot per run") end
|
|
end
|
|
check(listed > 0, "the frameset table names ripped poses")
|
|
check(#missing == 0, "every pose the frameset table draws is a required yellow cache file "
|
|
.. table.concat(missing, ","))
|
|
|
|
Sound.playMove, Sound.playPikaCry, Sound.playCry = realPlayMove, realPikaCry, realCry
|
|
Sound.moveSfxBusy, Music.duckForFanfare = realBusy, realDuck
|
|
Assets.exists = realExists
|
|
GameVersion.set("red")
|
|
|
|
T.finish("pikachu_thunderbolt_flash_2347")
|