Play the full Fly departure and landing animation (#702)

This commit is contained in:
johnjohto
2026-08-03 13:11:40 -04:00
parent 8e5501a23b
commit f19f4e9341
3 changed files with 231 additions and 18 deletions
+90 -18
View File
@@ -35,6 +35,32 @@ local mapScripts -- registry of hand-ported map scripts
local COMPASS = { up = "north", down = "south", left = "west", right = "east" }
local DIRVEC = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
-- Fly animation coord paths (engine/overworld/player_animations.asm):
-- y/x pairs in GB screen pixels, one pair every 3 frames (DoFlyAnimation's
-- Delay3). The port anchors a path on the player's own position instead
-- of the GB screen center: FLY_ANCHOR is the pair where the original has
-- the player's sprite, so path1 starts exactly on the player.
local FLY_ANCHOR = { 0x3C, 0x48 }
local FLY_PATH1 = { -- FlyAnimationScreenCoords1: up and off to the right
{ 0x3C, 0x48 }, { 0x3C, 0x50 }, { 0x3B, 0x58 }, { 0x3A, 0x60 },
{ 0x39, 0x68 }, { 0x37, 0x70 }, { 0x37, 0x78 }, { 0x33, 0x80 },
{ 0x30, 0x88 }, { 0x2D, 0x90 }, { 0x2A, 0x98 }, { 0x27, 0xA0 },
}
local FLY_PATH2 = { -- FlyAnimationScreenCoords2: out over the top-left;
-- the 11th step reads the ($F0,$00) terminator, fully off screen
{ 0x1A, 0x90 }, { 0x19, 0x80 }, { 0x17, 0x70 }, { 0x15, 0x60 },
{ 0x12, 0x50 }, { 0x0F, 0x40 }, { 0x0C, 0x30 }, { 0x09, 0x20 },
{ 0x05, 0x10 }, { 0x00, 0x00 }, { -16, 0x00 },
}
-- FlyAnimationEnterScreenCoords: in from off the top-right. Its own last
-- pair is ($3C,$40), so the arrival anchors there and lands on the player.
local FLY_ARRIVE_ANCHOR = { 0x3C, 0x40 }
local FLY_PATH_IN = {
{ 0x05, 0x98 }, { 0x0F, 0x90 }, { 0x18, 0x88 }, { 0x20, 0x80 },
{ 0x27, 0x78 }, { 0x2D, 0x70 }, { 0x32, 0x68 }, { 0x36, 0x60 },
{ 0x39, 0x58 }, { 0x3B, 0x50 }, { 0x3C, 0x48 }, { 0x3C, 0x40 },
}
-- healing machine ball screen positions (PokeCenterOAMData dbsprite
-- rows are raw shadow-OAM bytes, so the hardware's -8/-16 OAM origin
-- applies: screen = tile*8 + pixel offset - 8/16); [3] = OAM_XFLIP
@@ -907,10 +933,20 @@ function OverworldState:update(dt)
return
end
if self.flyAnim then
self.flyAnim.frames = self.flyAnim.frames - 1
if self.flyAnim.frames <= 0 then
-- DoFlyAnimation runs one coord pair every Delay3 (3 frames); the
-- in-place flap is 8 pairs, then the two paths with a 40-frame beat
-- while the bird is parked off screen between them
local anim = self.flyAnim
anim.t = anim.t + 1
if anim.phase == "flap" and anim.t >= 8 * 3 then
anim.phase, anim.t = "path1", 0
require("src.core.Sound").play(Game.data, "Fly")
elseif anim.phase == "path1" and anim.t >= #FLY_PATH1 * 3 then
anim.phase, anim.t = "hold", 0
elseif anim.phase == "hold" and anim.t >= 40 then
anim.phase, anim.t = "path2", 0
elseif anim.phase == "path2" and anim.t >= #FLY_PATH2 * 3 then
self.flyAnim = nil
self.player.inputLocked = false
local d = self.flyDest
self.flyDest = nil
if d then
@@ -918,10 +954,21 @@ function OverworldState:update(dt)
-- SFX_FLY (EnterMapAnim .flyAnimation)
self.arriveWarp = "fly"
self:startWarpTo(d.map, d.x, d.y, "down", nil, { via = "fly" })
else
self.player.inputLocked = false
end
return
end
end
if self.flyArrive then
-- EnterMapAnim .flyAnimation: one swoop in from the top-right, then
-- LoadPlayerSpriteGraphics -- the player reappears where it lands
self.flyArrive.t = self.flyArrive.t + 1
if self.flyArrive.t >= #FLY_PATH_IN * 3 then
self.flyArrive = nil
self.player.inputLocked = false
end
end
-- Dig/Teleport/Escape-Rope departure spin (beginTeleportOut). The sprite
-- spins UP out of the map before the fade (player_animations.asm
@@ -1600,14 +1647,15 @@ end
function OverworldState:flyTo(mapId)
local spot = Game.data.field.flyWarps[mapId]
if not spot then return end
require("src.core.Sound").play(Game.data, "Fly")
Game.save.onBike = false
Game.save.forcedBike = nil -- HandleFlyWarpOrDungeonWarp res BIT_ALWAYS_ON_BIKE
self.player.surfing = false
self:syncSurfingPikachu()
-- the bird carries the player off westward before the warp
-- (engine/overworld/player_animations.asm LoadBirdSpriteGraphics)
self.flyAnim = { frames = 48 }
-- _LeaveMapAnim .flyAnimation: the bird flaps in place (8 x Delay3),
-- then SFX_FLY and the up-right path, a 40-frame beat off screen, and
-- the exit over the top-left -- the warp fades only once the bird is
-- gone (#702). fxBird draws it; the player hides for the whole flight.
self.flyAnim = { phase = "flap", t = 0 }
self.player.inputLocked = true
self.flyDest = { map = mapId, x = spot.x, y = spot.y }
end
@@ -3878,6 +3926,10 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
-- door warps never take this branch.
if arriveWarp == "fly" then
require("src.core.Sound").play(Game.data, "Fly")
-- EnterMapAnim .flyAnimation: the bird swoops in off the top-right
-- edge and the player reappears where it lands (#702); the input
-- lock from flyTo releases when the swoop finishes
self.flyArrive = { t = 0 }
elseif arriveWarp == "teleport" then
require("src.core.Sound").play(Game.data, "Teleport_Enter1")
-- ENTER_2 caps the spin-down a moment later
@@ -4385,20 +4437,40 @@ function OverworldState:drawWorld()
-- the FLY bird sweeping off with the player
local function fxBird()
if not self.flyAnim then return end
local anim = self.flyAnim or self.flyArrive
if not anim then return end
local birdId = FieldDefaults.fieldValue(Game.data, "playerSprites", "fly")
if not self.birdSprite and birdId and Game.data.sprites[birdId] then
local SR = require("src.render.SpriteRenderer")
self.birdSprite = SR.new(Game.data.sprites[birdId])
end
if self.birdSprite then
local t = 48 - self.flyAnim.frames
local bx = self.player.px - t * 4
local by = self.player.py - math.floor(t * 1.5)
love.graphics.setColor(1, 1, 1, 1)
self.birdSprite:draw(bx, by, cam.x, cam.y, "left",
math.floor(t / 4) % 2, false)
if not self.birdSprite then return end
-- DoFlyAnimation: the bird flaps its wings every Delay3; each path is
-- anchored on the player's cell (FLY_ANCHOR / FLY_ARRIVE_ANCHOR) so
-- the flight rides any screen position, and it faces its travel
-- direction (rightward travel flips the left-drawn sheet)
local phase = anim.phase or "arrive"
if phase == "hold" then return end -- parked off screen between paths
local path, anchor, facing
if phase == "path1" then
path, anchor, facing = FLY_PATH1, FLY_ANCHOR, "right"
elseif phase == "path2" then
path, anchor, facing = FLY_PATH2, FLY_ANCHOR, "left"
elseif phase == "arrive" then
path, anchor, facing = FLY_PATH_IN, FLY_ARRIVE_ANCHOR, "left"
end
local step = math.floor(anim.t / 3)
local sx, sy
if path then
local pair = path[math.min(#path, step + 1)]
sx, sy = pair[2] - anchor[2], pair[1] - anchor[1]
else
sx, sy = 0, 0 -- the in-place flap sits on the player
facing = "right"
end
love.graphics.setColor(1, 1, 1, 1)
self.birdSprite:draw(self.player.px + sx, self.player.py + sy,
cam.x, cam.y, facing, step % 2, false)
end
-- fishing pose: the rod tile over the faced water (gfx/fishing.asm)
@@ -4548,7 +4620,7 @@ function OverworldState:drawWorld()
g.npc:draw(cam.x - g.ox, cam.y - g.oy)
end
for _, e in ipairs(self.entities) do
if not (self.flyAnim and e == self.player) then
if not ((self.flyAnim or self.flyArrive) and e == self.player) then
e:draw(cam.x, cam.y)
-- tall grass overdraws the sprite's feet (GB sprite priority);
-- the overdraw is BG tiles, so it rides the shake offset too
@@ -4599,7 +4671,7 @@ function OverworldState:drawWorld()
items[#items + 1] = { y = g.npc.py + g.oy + 16, kind = "ghost", g = g }
end
for _, e in ipairs(self.entities) do
if not (self.flyAnim and e == self.player) then
if not ((self.flyAnim or self.flyArrive) and e == self.player) then
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
end
end
@@ -4650,7 +4722,7 @@ function OverworldState:drawWorld()
local fy = self.emote.npc.py - cam.y + 16
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxEmote)
end
if self.flyAnim then
if self.flyAnim or self.flyArrive then
local fx = self.player.px - cam.x + 8
local fy = self.player.py - cam.y + 16
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxBird)