mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
Play the full Fly departure and landing animation (#702)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
-- Driver: Fly overworld animation (#702).
|
||||
--
|
||||
-- POKEPORT_DRIVER=tests/drivers/fly_anim_bug702_test.lua \
|
||||
-- POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love .
|
||||
--
|
||||
-- Teleports to Route 17, starts Fly to Pallet Town and captures the
|
||||
-- departure (in-place flap, up-right path, top-left exit) and the
|
||||
-- landing swoop.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
U.teleport(game, "ROUTE_17", 4, 10, "down")
|
||||
local ow = game.stack:top()
|
||||
ow:flyTo("PALLET_TOWN")
|
||||
|
||||
local function waitFrames(n)
|
||||
for _ = 1, n do coroutine.yield() end
|
||||
end
|
||||
|
||||
waitFrames(12) -- mid in-place flap
|
||||
U.shot(game, DIR .. "/fly_1_flap.png")
|
||||
waitFrames(24) -- path1 ~half-way (24 + 12 = 36 into the anim)
|
||||
U.shot(game, DIR .. "/fly_2_path1.png")
|
||||
waitFrames(50) -- hold + start of path2
|
||||
U.shot(game, DIR .. "/fly_3_path2.png")
|
||||
|
||||
-- wait out the warp transition, then catch the swoop mid-flight
|
||||
local guard = 0
|
||||
while ow.map.id == "ROUTE_17" and guard < 600 do
|
||||
guard = guard + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
guard = 0
|
||||
while not ow.flyArrive and guard < 600 do
|
||||
guard = guard + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
waitFrames(12) -- mid swoop
|
||||
U.shot(game, DIR .. "/fly_4_arrive.png")
|
||||
guard = 0
|
||||
while ow.flyArrive and guard < 600 do
|
||||
guard = guard + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
U.shot(game, DIR .. "/fly_5_landed.png")
|
||||
|
||||
U.log("Screenshots are under " .. DIR)
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
-- Parity: the Fly overworld animation (#702).
|
||||
--
|
||||
-- Oracle: engine/overworld/player_animations.asm. Departure
|
||||
-- (_LeaveMapAnim .flyAnimation) flaps the bird in place for 8 x Delay3,
|
||||
-- plays SFX_FLY, flies FlyAnimationScreenCoords1 up and off to the right
|
||||
-- (12 pairs, 3 frames each), waits 40 frames, then exits over the
|
||||
-- top-left along FlyAnimationScreenCoords2 (11 pairs). Arrival
|
||||
-- (EnterMapAnim .flyAnimation) plays SFX_FLY again and swoops in along
|
||||
-- FlyAnimationEnterScreenCoords (12 pairs), and only then does
|
||||
-- LoadPlayerSpriteGraphics bring the player back.
|
||||
--
|
||||
-- Self-contained: `luajit tests/parity_fly_anim.lua`; also globbed by
|
||||
-- tests/run_tests.lua.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
local Data = require("src.core.Data")
|
||||
if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end
|
||||
local S = require("tests.harness").suite("parity fly anim (#702)")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
require("src.render.Font").load(Data)
|
||||
local Game = require("src.core.Game")
|
||||
local Input = require("src.core.Input")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local OW = require("src.world.OverworldController")
|
||||
|
||||
Game.data = Data
|
||||
Game.input = Input; Input:init()
|
||||
Game.renderer = Renderer; Renderer:init()
|
||||
Game.stack = StateStack; StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
|
||||
-- record SFX without touching the audio backend
|
||||
local plays = {}
|
||||
local Sound = require("src.core.Sound")
|
||||
local realPlay = Sound.play
|
||||
Sound.play = function(_, key) plays[#plays + 1] = key end
|
||||
|
||||
local function popAll() while Game.stack:top() do Game.stack:pop() end end
|
||||
local function frame()
|
||||
Input.pressed = {}
|
||||
StateStack:update(1 / 60)
|
||||
end
|
||||
local function frames(n) for _ = 1, n do frame() end end
|
||||
|
||||
Game.stack:push(OW, "ROUTE_17", 4, 10, "down")
|
||||
local ow = Game.stack:top()
|
||||
|
||||
ow:flyTo("PALLET_TOWN")
|
||||
check(ow.flyAnim ~= nil, "the bird lead-in starts on FLY")
|
||||
eq(ow.flyAnim and ow.flyAnim.phase, "flap", "the bird flaps in place first")
|
||||
eq(ow.player.inputLocked, true, "input is locked for the flight")
|
||||
eq(#plays, 0, "no SFX during the in-place flap")
|
||||
|
||||
frames(23)
|
||||
eq(ow.flyAnim and ow.flyAnim.phase, "flap", "still flapping 23 frames in")
|
||||
frame()
|
||||
eq(ow.flyAnim and ow.flyAnim.phase, "path1",
|
||||
"the up-right path starts after 8 x Delay3")
|
||||
eq(plays[#plays], "Fly", "SFX_FLY plays as the bird takes off")
|
||||
|
||||
frames(36)
|
||||
eq(ow.flyAnim and ow.flyAnim.phase, "hold",
|
||||
"the bird parks off screen after the 12-pair path")
|
||||
frames(40)
|
||||
eq(ow.flyAnim and ow.flyAnim.phase, "path2",
|
||||
"the top-left exit follows the 40-frame beat")
|
||||
frames(33)
|
||||
check(ow.flyAnim == nil, "the departure ends after the 11-pair exit")
|
||||
|
||||
-- the warp transition runs its fade out/in; the map switches inside it
|
||||
local guard = 0
|
||||
while ow.map.id == "ROUTE_17" and guard < 400 do
|
||||
guard = guard + 1
|
||||
frame()
|
||||
end
|
||||
eq(ow.map.id, "PALLET_TOWN", "the warp lands in Pallet Town")
|
||||
check(ow.flyArrive ~= nil, "the landing swoop starts on arrival")
|
||||
eq(plays[#plays], "Fly", "SFX_FLY plays again for the landing")
|
||||
eq(ow.player.inputLocked, true, "input stays locked for the swoop")
|
||||
|
||||
frames(35)
|
||||
check(ow.flyArrive ~= nil, "the swoop is still flying 35 frames in")
|
||||
frame()
|
||||
check(ow.flyArrive == nil, "the swoop ends after the 12-pair path")
|
||||
eq(ow.player.inputLocked, false, "and hands input back")
|
||||
|
||||
Sound.play = realPlay
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user