mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #747 from johnjohto/fix-fly-animation-702
Play the full Fly departure and landing animation (#702)
This commit is contained in:
@@ -37,6 +37,32 @@ local mapScripts -- registry of hand-ported map scripts
|
|||||||
local COMPASS = { up = "north", down = "south", left = "west", right = "east" }
|
local COMPASS = { up = "north", down = "south", left = "west", right = "east" }
|
||||||
local DIRVEC = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
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
|
-- healing machine ball screen positions (PokeCenterOAMData dbsprite
|
||||||
-- rows are raw shadow-OAM bytes, so the hardware's -8/-16 OAM origin
|
-- 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
|
-- applies: screen = tile*8 + pixel offset - 8/16); [3] = OAM_XFLIP
|
||||||
@@ -916,10 +942,20 @@ function OverworldState:update(dt)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self.flyAnim then
|
if self.flyAnim then
|
||||||
self.flyAnim.frames = self.flyAnim.frames - 1
|
-- DoFlyAnimation runs one coord pair every Delay3 (3 frames); the
|
||||||
if self.flyAnim.frames <= 0 then
|
-- 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.flyAnim = nil
|
||||||
self.player.inputLocked = false
|
|
||||||
local d = self.flyDest
|
local d = self.flyDest
|
||||||
self.flyDest = nil
|
self.flyDest = nil
|
||||||
if d then
|
if d then
|
||||||
@@ -927,10 +963,21 @@ function OverworldState:update(dt)
|
|||||||
-- SFX_FLY (EnterMapAnim .flyAnimation)
|
-- SFX_FLY (EnterMapAnim .flyAnimation)
|
||||||
self.arriveWarp = "fly"
|
self.arriveWarp = "fly"
|
||||||
self:startWarpTo(d.map, d.x, d.y, "down", nil, { via = "fly" })
|
self:startWarpTo(d.map, d.x, d.y, "down", nil, { via = "fly" })
|
||||||
|
else
|
||||||
|
self.player.inputLocked = false
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
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
|
-- Dig/Teleport/Escape-Rope departure spin (beginTeleportOut). The sprite
|
||||||
-- spins UP out of the map before the fade (player_animations.asm
|
-- spins UP out of the map before the fade (player_animations.asm
|
||||||
@@ -1609,14 +1656,15 @@ end
|
|||||||
function OverworldState:flyTo(mapId)
|
function OverworldState:flyTo(mapId)
|
||||||
local spot = Game.data.field.flyWarps[mapId]
|
local spot = Game.data.field.flyWarps[mapId]
|
||||||
if not spot then return end
|
if not spot then return end
|
||||||
require("src.core.Sound").play(Game.data, "Fly")
|
|
||||||
Game.save.onBike = false
|
Game.save.onBike = false
|
||||||
Game.save.forcedBike = nil -- HandleFlyWarpOrDungeonWarp res BIT_ALWAYS_ON_BIKE
|
Game.save.forcedBike = nil -- HandleFlyWarpOrDungeonWarp res BIT_ALWAYS_ON_BIKE
|
||||||
self.player.surfing = false
|
self.player.surfing = false
|
||||||
self:syncSurfingPikachu()
|
self:syncSurfingPikachu()
|
||||||
-- the bird carries the player off westward before the warp
|
-- _LeaveMapAnim .flyAnimation: the bird flaps in place (8 x Delay3),
|
||||||
-- (engine/overworld/player_animations.asm LoadBirdSpriteGraphics)
|
-- then SFX_FLY and the up-right path, a 40-frame beat off screen, and
|
||||||
self.flyAnim = { frames = 48 }
|
-- 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.player.inputLocked = true
|
||||||
self.flyDest = { map = mapId, x = spot.x, y = spot.y }
|
self.flyDest = { map = mapId, x = spot.x, y = spot.y }
|
||||||
end
|
end
|
||||||
@@ -3887,6 +3935,10 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
|
|||||||
-- door warps never take this branch.
|
-- door warps never take this branch.
|
||||||
if arriveWarp == "fly" then
|
if arriveWarp == "fly" then
|
||||||
require("src.core.Sound").play(Game.data, "Fly")
|
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
|
elseif arriveWarp == "teleport" then
|
||||||
require("src.core.Sound").play(Game.data, "Teleport_Enter1")
|
require("src.core.Sound").play(Game.data, "Teleport_Enter1")
|
||||||
-- ENTER_2 caps the spin-down a moment later
|
-- ENTER_2 caps the spin-down a moment later
|
||||||
@@ -4394,20 +4446,40 @@ function OverworldState:drawWorld()
|
|||||||
|
|
||||||
-- the FLY bird sweeping off with the player
|
-- the FLY bird sweeping off with the player
|
||||||
local function fxBird()
|
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")
|
local birdId = FieldDefaults.fieldValue(Game.data, "playerSprites", "fly")
|
||||||
if not self.birdSprite and birdId and Game.data.sprites[birdId] then
|
if not self.birdSprite and birdId and Game.data.sprites[birdId] then
|
||||||
local SR = require("src.render.SpriteRenderer")
|
local SR = require("src.render.SpriteRenderer")
|
||||||
self.birdSprite = SR.new(Game.data.sprites[birdId])
|
self.birdSprite = SR.new(Game.data.sprites[birdId])
|
||||||
end
|
end
|
||||||
if self.birdSprite then
|
if not self.birdSprite then return end
|
||||||
local t = 48 - self.flyAnim.frames
|
-- DoFlyAnimation: the bird flaps its wings every Delay3; each path is
|
||||||
local bx = self.player.px - t * 4
|
-- anchored on the player's cell (FLY_ANCHOR / FLY_ARRIVE_ANCHOR) so
|
||||||
local by = self.player.py - math.floor(t * 1.5)
|
-- the flight rides any screen position, and it faces its travel
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
-- direction (rightward travel flips the left-drawn sheet)
|
||||||
self.birdSprite:draw(bx, by, cam.x, cam.y, "left",
|
local phase = anim.phase or "arrive"
|
||||||
math.floor(t / 4) % 2, false)
|
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
|
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
|
end
|
||||||
|
|
||||||
-- fishing pose: the rod tile over the faced water (gfx/fishing.asm)
|
-- fishing pose: the rod tile over the faced water (gfx/fishing.asm)
|
||||||
@@ -4557,7 +4629,7 @@ function OverworldState:drawWorld()
|
|||||||
g.npc:draw(cam.x - g.ox, cam.y - g.oy)
|
g.npc:draw(cam.x - g.ox, cam.y - g.oy)
|
||||||
end
|
end
|
||||||
for _, e in ipairs(self.entities) do
|
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)
|
e:draw(cam.x, cam.y)
|
||||||
-- tall grass overdraws the sprite's feet (GB sprite priority);
|
-- tall grass overdraws the sprite's feet (GB sprite priority);
|
||||||
-- the overdraw is BG tiles, so it rides the shake offset too
|
-- the overdraw is BG tiles, so it rides the shake offset too
|
||||||
@@ -4608,7 +4680,7 @@ function OverworldState:drawWorld()
|
|||||||
items[#items + 1] = { y = g.npc.py + g.oy + 16, kind = "ghost", g = g }
|
items[#items + 1] = { y = g.npc.py + g.oy + 16, kind = "ghost", g = g }
|
||||||
end
|
end
|
||||||
for _, e in ipairs(self.entities) do
|
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 }
|
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4659,7 +4731,7 @@ function OverworldState:drawWorld()
|
|||||||
local fy = self.emote.npc.py - cam.y + 16
|
local fy = self.emote.npc.py - cam.y + 16
|
||||||
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxEmote)
|
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxEmote)
|
||||||
end
|
end
|
||||||
if self.flyAnim then
|
if self.flyAnim or self.flyArrive then
|
||||||
local fx = self.player.px - cam.x + 8
|
local fx = self.player.px - cam.x + 8
|
||||||
local fy = self.player.py - cam.y + 16
|
local fy = self.player.py - cam.y + 16
|
||||||
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxBird)
|
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