CLOSES #916: hide trainer sprite through the Fly/Dig warp fade

After the Fly departure animation finished (bird off-screen) and during
Dig/teleport, the trainer sprite popped back in standing at the old cell
for the whole 32-frame black fade-out before the transition.  The
player-hide guard only held while a departure animation was live:
flyAnim went nil the instant path2 completed and the teleportOut
countdown cleared the spin fields at 0, but startWarpTo's Transition
(not isOpaque) keeps the overworld drawing beneath the veil, and the
arrival animation is not armed until setMap's midpoint.

Add a playerHidden flag on OverworldState that bridges the gap:
- set when each departure completes (flyAnim path2 / teleportOut hit 0),
  immediately before the warp starts;
- cleared in startWarpTo's Transition enter callback, synchronously
  after setMap and before the arrival arms flyArrive / spinDrop, so the
  player is never drawable mid-fade and never bare on the landing frame;
- folded into both player-draw guards.

ROM-free regression test (tests/engine/warp_sprite_hidden_bug916.lua)
drives the REAL Transition + setMap headlessly for Dig and Fly and
asserts zero fade frames leave the player drawable bare (would have
observed 31/32 gap frames before the fix).  Runs in the CI headless T2
tier.

Dig spin timing/lift and the black fade color are left as-is (fade is
intentional per #607).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Shane McGovern
2026-08-07 11:14:20 +01:00
parent 112120e8fe
commit ce204aaf16
2 changed files with 170 additions and 2 deletions
+19 -2
View File
@@ -969,8 +969,14 @@ function OverworldState:update(dt)
-- the bird carries the player in on landing, with its own
-- SFX_FLY (EnterMapAnim .flyAnimation)
self.arriveWarp = "fly"
-- keep the sprite hidden through the warp fade-out (#916): flyAnim
-- just went nil but flyArrive is not armed until startWarpTo's
-- midpoint, and the overworld keeps drawing beneath the veil, so
-- without this the trainer pops back in at the old cell for 32 frames
self.playerHidden = true
self:startWarpTo(d.map, d.x, d.y, "down", nil, { via = "fly" })
else
self.playerHidden = false
self.player.inputLocked = false
end
return
@@ -1002,6 +1008,10 @@ function OverworldState:update(dt)
self.player.spinFrames = nil
self.player.spinRise = nil
self.player.inputLocked = false
-- keep the sprite hidden through the warp fade-out (#916): the spin is
-- over but the arrival spin-drop is not armed until startWarpTo's
-- midpoint, so without this the standing trainer shows under the veil
self.playerHidden = true
self:warpToHealPoint(onDone, { arrive = "teleport" })
return
end
@@ -4136,6 +4146,11 @@ function OverworldState:startWarpTo(mapId, x, y, facing, onDone, opts)
self.arriveWarp = nil
Game.stack:push(Transition.new(Game, function()
self:setMap(mapId, x, y, facing or "down", opts)
-- the departure-side hide from flyAnim/teleportOut ends here, on the new
-- map; the arrival arms its own cover (flyArrive / spinDrop) a few lines
-- down, so the player is never drawable mid-fade nor standing bare on the
-- landing frame (#916)
self.playerHidden = false
-- The warp we land ON stays inert for the completed-step check until we
-- physically step off it, so a warp whose destination cell is itself a
-- warp cannot bounce us straight back (elevator cars, stacked stair/door
@@ -4860,7 +4875,8 @@ 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 or self.flyArrive) and e == self.player) then
if not ((self.flyAnim or self.flyArrive or self.playerHidden)
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
@@ -4911,7 +4927,8 @@ 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 or self.flyArrive) and e == self.player) then
if not ((self.flyAnim or self.flyArrive or self.playerHidden)
and e == self.player) then
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
end
end