CLOSES #339, CLOSES #354, CLOSES #360, CLOSES #372, CLOSES #373, CLOSES #374, CLOSES #375, CLOSES #378, CLOSES #379, CLOSES #383, CLOSES #384, CLOSES #385, CLOSES #391, CLOSES #392, CLOSES #393, CLOSES #394, CLOSES #395, CLOSES #396, CLOSES #397, CLOSES #398, CLOSES #413, CLOSES #420, CLOSES #423, CLOSES #424, CLOSES #425, CLOSES #426, CLOSES #427, CLOSES #429, CLOSES #430, CLOSES #431, CLOSES #433, CLOSES #435, CLOSES #436, CLOSES #438, CLOSES #439, CLOSES #441, CLOSES #442, CLOSES #444

This commit is contained in:
bryanthaboi
2026-07-30 11:14:51 -04:00
parent 0f581e2f69
commit 4b9a326f6b
46 changed files with 4171 additions and 173 deletions
+160 -9
View File
@@ -181,9 +181,29 @@ local function spawnCell(ow)
return p.cellX, p.cellY
end
function PikachuFollower.onMapEntered(game, ow)
-- the live follower, for a caller that has to carry it across a setMap
-- that rebuilds ow.npcs (OverworldState:crossConnection, #427)
function PikachuFollower.current(ow)
local npc = findFollower(ow)
return npc
end
function PikachuFollower.onMapEntered(game, ow, opts)
remove(ow)
if not shouldSpawn(game, ow) then return end
-- opts.keepPikachu is the follower a connection crossing kept alive:
-- LoadMapHeader's connection path sets wPikachuSpawnState = 2 and bit 4
-- of wPikachuOverworldStateFlags, so SchedulePikachuSpawnForAfterText
-- takes .normal_spawn_state -- map coords rebased, sprite data and
-- follow command buffer left alone. Re-list the same instance and let
-- rebase() shift its cell; a warp arrives without it and respawns
-- behind the player, the full spawn path of that same routine.
local keep = opts and opts.keepPikachu
if keep then
table.insert(ow.npcs, keep)
table.insert(ow.entities, keep)
return
end
local x, y = spawnCell(ow)
local npc = makeFollower(game, ow, x, y, ow.player.facing)
table.insert(ow.npcs, npc)
@@ -271,6 +291,14 @@ end
local function idleTick(ow, npc)
-- Func_fc82e: a step in progress ends the idle state outright
if ow.player.moving then idleReset(npc) return end
-- Every counter below burns one unit per UpdateSprites call, and a
-- standing OverworldLoop spends two DelayFrames on each pass (home/
-- overworld.asm: OverworldLoop delays, falls into OverworldLoopLessDelay
-- which delays again, then .noDirectionButtonsPressed loops back), so the
-- whole Func_fc803 family runs at half this port's 60Hz fixed step: the
-- first glance is $20 CALLS, 64 frames, not 32 (#424).
npc.idleClock = ((npc.idleClock or 0) + 1) % 2
if npc.idleClock ~= 0 then return end
local idle = npc.idle
if not idle then
idle = { kind = "wait", frames = IDLE_LOOK }
@@ -339,6 +367,28 @@ local function ledgeStep(game, ow, cx, cy, dir)
return false
end
-- Slide the follower into the connected map's coordinate frame by the
-- delta crossConnection applied to the player: the two maps are one
-- continuous world, so a seam is a pure translation. MapX/MapY in
-- wSpritePikachuStateData2 are all .normal_spawn_state rewrites there,
-- never the pixel coords, which is why the original walks through a seam
-- instead of popping (#427).
function PikachuFollower.rebase(ow, dx, dy)
local npc = findFollower(ow)
if npc then
npc.cellX, npc.cellY = npc.cellX + dx, npc.cellY + dy
npc.px, npc.py = npc.px + dx * 16, npc.py + dy * 16
if npc.targetX then npc.targetX = npc.targetX + dx end
if npc.targetY then npc.targetY = npc.targetY + dy end
if npc.goalX then npc.goalX = npc.goalX + dx end
if npc.goalY then npc.goalY = npc.goalY + dy end
-- Func_fc82e: the player is taking a step, so any idle pose is over
if npc.idle then idleReset(npc) end
end
local trail = ow.pikachuTrail
if trail then trail.x, trail.y = trail.x + dx, trail.y + dy end
end
-- one follow step per frame: chase the cell the player last vacated
-- (pikachu_follow.asm keeps it one walk step behind)
function PikachuFollower.update(game, ow)
@@ -371,8 +421,27 @@ function PikachuFollower.update(game, ow)
local destX = p.targetX or p.cellX
local destY = p.targetY or p.cellY
if destX ~= trail.x or destY ~= trail.y then
npc.goalX, npc.goalY = trail.x, trail.y
trail.x, trail.y = destX, destY
local stepDir = destY > trail.y and "down" or destY < trail.y and "up"
or destX > trail.x and "right" or "left"
-- A ledge hop commits TWO steps (checkLedgeHop -> scriptMove(p, dir, 2),
-- the two simulated presses of HandleLedges) but only ONE follow
-- command: Func_fcc08 sees BIT_LEDGE_OR_FISHING and defers to
-- Func_fcc64, which appends the $5-$8 hop on the takeoff step and
-- appends nothing on the landing step (bit 6 of
-- wPikachuOverworldStateFlags toggles between the two). With no command
-- behind it the hop cannot leave the buffer -- Func_fcc92 only pops once
-- a second command is queued -- so Pikachu walks up to the cell the
-- player took off from, waits there two cells behind (the Func_fc842
-- idle rolls), and hops one player step later (#424, after #409).
if trail.ledgeHop == stepDir then
trail.ledgeHop = nil
trail.x, trail.y = destX, destY
else
trail.ledgeHop = ledgeStep(game, ow, trail.x, trail.y, stepDir)
and stepDir or nil
npc.goalX, npc.goalY = trail.x, trail.y
trail.x, trail.y = destX, destY
end
end
-- standing still with nothing to chase is the idle state (Func_fc803);
-- once a step is under way NPC:update owns px/py, so only the idle
@@ -404,7 +473,9 @@ function PikachuFollower.update(game, ow)
npc.targetX = npc.cellX + (dir == "right" and 1 or dir == "left" and -1 or 0)
npc.targetY = npc.cellY + (dir == "down" and 1 or dir == "up" and -1 or 0)
-- the cell ahead is the ledge the player hopped: clear both cells in one
-- step instead of stopping on the ledge (#409). pikachu_follow.asm
-- step instead of stopping on the ledge (#409). The trail above holds
-- this back until the player commits a further step, so it fires from the
-- cell on top of the ledge, a step late (#424). pikachu_follow.asm
-- Func_fcc08 appends the $5-$8 hop commands while BIT_LEDGE_OR_FISHING
-- is set, and Func_fca0a runs them as two AddPikachuStepVector cells over
-- one normal step's frames -- no arc and no shadow, the hop command only
@@ -422,7 +493,12 @@ function PikachuFollower.update(game, ow)
-- steps are queued (AreThereAtLeastTwoStepsInPikachuFollowCommandBuffer:
-- walk counter $4 instead of NormalPikachuFollow's $8).
local stepLen = p.stepFramesCur or p.stepFrames or 16
if far > 1 then stepLen = math.max(1, math.floor(stepLen / 2)) end
-- the hop is never a Fast step: Func_fc7aa jumps to Func_fca0a on the $4
-- movement status BEFORE it asks AreThereAtLeastTwoSteps..., so its two
-- cells ride one normal step's frames even though the goal is two away.
if far > 1 and not npc.hopStep then
stepLen = math.max(1, math.floor(stepLen / 2))
end
npc.stepFrames = stepLen
npc.moving = true
npc.progress = 0
@@ -500,6 +576,60 @@ local MOOD_MATRIX = {
-- .Emotions): scripted one-shots -- 21 is the fishing-rod reaction
local MODIFIER_EMOTIONS = { 18, 21, 23, 24, 25 }
-- ExecutePikaPicAnimScript spends a Delay3 on every pass of its loop
-- (pikachu_pic_animation.asm PikaPicAnimTimerAndJoypad), so one script tick
-- is three 60Hz frames: the flat 50 frame hold this port used was under a
-- third of even the shortest script (#424).
local PIKAPIC_TICK = 3
local PIKAPIC_LIFT = 4 -- px the stand-in pic rises on an overlay run
-- pikaemotion_pikapic's script id per emotion: emotion N takes
-- PikaPicAnimScript N, except the four listed here (data/pikachu/
-- pikachu_emotions.asm).
local PIKAPIC_SCRIPT = { [29] = 10, [30] = 20, [31] = 23, [32] = 23 }
-- Per script: pikapic_setduration's tick count, and for the scripts whose
-- overlay is a whole second pose, that frameset's run lengths in ticks
-- (data/pikachu/pikachu_pic_objects.asm PikaPicAnimBGFrames_*, which script
-- N reaches as frameset N+5, or N+6 from script 10 up). The list alternates
-- pikaframedelay (the base pic alone) and pikaframe (the overlay) starting
-- with a delay, so a frameset that opens on a pikaframe opens with a zero
-- here; the frameset restarts until pikapic_looptofinish runs the duration
-- out. Scripts 1, 2, 3, 5, 6, 8 and 9 are left without a list on purpose:
-- their overlays (PikaAnimTilemap_14 to _22) only paint a few tiles over a
-- pic that otherwise stands still, so with no tiles to paint the port has
-- nothing to show for them and must not bob the whole picture instead.
local PIKAPIC = {
[1] = { dur = 40 },
[2] = { dur = 44 },
[3] = { dur = 80 },
[4] = { dur = 70, seq = { 8, 8, 20, 8 } },
[5] = { dur = 32 },
[6] = { dur = 50 },
[7] = { dur = 58, seq = { 0, 8, 2, 8, 2, 8 } },
[8] = { dur = 44 },
[9] = { dur = 56 },
[10] = { dur = 56, seq = { 8, 11, 5 } },
[11] = { dur = 100, seq = { 20, 8, 20, 8 } },
[12] = { dur = 50, seq = { 13, 12, 100, 8 } },
[13] = { dur = 50, seq = { 5, 5, 5, 5, 100 } },
[14] = { dur = 40, seq = { 2, 2, 2, 2 } },
[15] = { dur = 50, seq = { 5, 5, 5, 5 } },
[16] = { dur = 32, seq = { 0, 8, 100 } },
[17] = { dur = 100, seq = { 10, 3, 3, 3, 100 } },
[18] = { dur = 32, seq = { 3, 100, 8, 8 } },
[19] = { dur = 44, seq = { 0, 6, 6, 6, 6 } },
[20] = { dur = 50, seq = { 8, 12, 8, 12 } },
[21] = { dur = 40, seq = { 8, 104 } },
[22] = { dur = 40, seq = { 8, 100 } },
[23] = { dur = 70, seq = { 16, 16, 16, 16 } },
[24] = { dur = 60, seq = { 6, 6, 6, 6, 100 } },
[25] = { dur = 50, seq = { 6, 106 } },
[26] = { dur = 100, seq = { 20, 8, 20, 116 } },
[27] = { dur = 30, seq = { 4, 100 } },
[28] = { dur = 64, seq = { 12, 12, 12, 100 } },
}
local function moodEmotion(save)
local mood = save.pikachuMood or 128
local column = 5
@@ -579,17 +709,38 @@ function PikachuFollower.talk(game, ow, npc, done)
-- ends with one, and its box is the only thing most of them put on
-- screen (emotion 5, the fresh-save cell, has no bubble at all). The
-- 40x40 front pic is the size of PikaAnimTilemap_1's 5x5 base frame;
-- Sprites.path keeps a mod's replacement skin in play. The scripts'
-- 32-58 frame durations bracket the hold below, so it stays at 50.
-- Sprites.path keeps a mod's replacement skin in play.
local Sprites = require("src.pokemon.Sprites")
local pic = Sprites.path(game.data, "PIKACHU", "front",
{ kind = "overworld" })
local anim = PIKAPIC[PIKAPIC_SCRIPT[emotion] or emotion] or PIKAPIC[1]
local hold = anim.dur * PIKAPIC_TICK
ow.emote = {
npc = npc, frames = 50, bubble = bi or false, pikaPic = pic,
onDone = done,
npc = npc, frames = hold, bubble = bi or false, pikaPic = pic,
pikaSeq = anim.seq, pikaTotal = hold, skippable = true, onDone = done,
}
end
-- Where the framed pic sits this frame. The overlay a pikaframe run draws
-- is a second full-body pose (PikaAnimTilemap_23 and up replace all 5x5
-- tiles) out of gfx/pikachu/unknown_*, which the cache does not carry, so
-- the port lifts the one pic it has for the length of those runs -- the jump
-- the happy emotions make inside the box (#424, still on #407's stand-in).
function PikachuFollower.picLift(emote)
local seq = emote and emote.pikaSeq
if not seq then return 0 end
local loop = 0
for _, run in ipairs(seq) do loop = loop + run end
if loop <= 0 then return 0 end
local elapsed = math.max(0, (emote.pikaTotal or 0) - (emote.frames or 0))
local tick = math.floor(elapsed / PIKAPIC_TICK) % loop
for i, run in ipairs(seq) do
if tick < run then return i % 2 == 0 and PIKAPIC_LIFT or 0 end
tick = tick - run
end
return 0
end
-- ---------------------------------------------------------------------
-- PikachuWalksToNurseJoy (engine/pikachu/pikachu_emotions.asm, run by
-- engine/events/pokecenter.asm once the heal is accepted): the companion