pokecenter fix

This commit is contained in:
bryanthaboi
2026-07-23 15:15:35 -04:00
parent 2c9970a643
commit aad86fb351
5 changed files with 84 additions and 19 deletions
+4 -2
View File
@@ -42,8 +42,10 @@ Game Boy equivalent:
rows above the player recede and rows below come toward the viewer. Only
things that actually *stand* on the ground draw as upright billboards,
unscaled and pixel-identical to flat mode: the player, NPCs, item balls,
and the screen-anchored FX attached to them (heal machine glow, emote
bubbles, the fishing rod, the FLY bird). An earlier revision tried
and the standing FX attached to them (emote bubbles, the fishing rod,
the FLY bird). The Poké Center heal-machine overlay stays on the ground
plane with the machine tiles (it is OAM glued to a BG graphic, not a
standing sprite). An earlier revision tried
billboarding buildings/trees/signs too (cutting them out of the ground
per hand-curated per-tileset tables); that chased an endless tail of
special cases, dense tree canopy, fences fused into grass, building
+34
View File
@@ -230,6 +230,25 @@ function ChipAudio.ensureMusicPlaying()
end
end
-- Threaded playMusic returns an empty QueueableSource and only calls
-- Source:play once the first worker buffer lands (~1 frame later). Until
-- then Source:isPlaying is false -- callers that treat that as "song over"
-- (Music.oneShotPlaying / pendingRestore) must wait here instead, or a
-- playOnce jingle like Music_PkmnHealed is cut off before it starts.
local forceAwaitingFirstBuffer -- test-only override (see _simulate*)
function ChipAudio.awaitingFirstBuffer()
if forceAwaitingFirstBuffer then return true end
local m = currentMusic
if not (m and m.threaded and not m.started and not m.finished) then
return false
end
-- a dead worker will never deliver the first buffer
if workerReady == false then return false end
if worker and worker.getError and worker:getError() then return false end
return true
end
function ChipAudio.stopMusic()
if currentMusic and currentMusic.source then
pcall(currentMusic.source.stop, currentMusic.source)
@@ -240,6 +259,7 @@ function ChipAudio.stopMusic()
end
pendingBuf = nil
currentMusic = nil
forceAwaitingFirstBuffer = nil
end
-- hot reload: the next play re-reads programs.bin (a mod may have swapped the
@@ -302,6 +322,20 @@ end
-- test hooks (headless): synchronous synthesis straight through ChipSynth
-- ---------------------------------------------------------------------------
-- Force the "threaded, first buffer not yet queued" window so Music's
-- playOnce / pendingRestore race can be asserted without love.thread.
-- Returns a clear() that drops the override (call after the assertion).
function ChipAudio._simulateAwaitingFirstBufferForTest()
local m = currentMusic
if not m or not m.source then return nil end
m.threaded = true
m.started = false
m.finished = false
pcall(function() m.source.playing = false end)
forceAwaitingFirstBuffer = true
return function() forceAwaitingFirstBuffer = nil end
end
function ChipAudio._renderMusicForTest(data, header, seconds)
local engine = ChipSynth.newEngine(data, header, { allowLoops = true })
return ChipSynth.soundData(engine, math.floor(seconds * SAMPLE_RATE), 2)
+14 -1
View File
@@ -254,6 +254,7 @@ function Music.stop()
require("src.core.ChipAudio").stopMusic()
state.current, state.source, state.loopSource, state.fade = nil, nil, nil, nil
state.chip = false
state.pendingRestore = nil
if previous and Runtime.wants("music.stopped") then
Runtime.emit("music.stopped", { song = previous })
end
@@ -347,14 +348,24 @@ end
function Music.playOnce(data, song)
if not songDef(data, song) then return false end
Music.play(data, song, false, { reason = "once" })
-- play() can no-op (hook silence, failed def); only arm restore when
-- the jingle actually became current
if state.current ~= song then return false end
state.pendingRestore = true
return true
end
local function chipAwaitingFirstBuffer()
return state.chip
and require("src.core.ChipAudio").awaitingFirstBuffer()
end
-- is a playOnce jingle still sounding? (AnimateHealingMachine's
-- .waitLoop2 holds the healing machine until MUSIC_PKMN_HEALED ends)
function Music.oneShotPlaying()
if not state.pendingRestore then return false end
-- threaded chip songs start silent for ~1 frame; that gap is not "over"
if chipAwaitingFirstBuffer() then return true end
local src = state.source
if not src then return false end
local ok, playing = pcall(src.isPlaying, src)
@@ -441,8 +452,10 @@ function Music.update(data)
state.source = loopSrc
pcall(loopSrc.play, loopSrc)
end
-- do not treat "threaded source still waiting on its first buffer" as
-- ended, or playOnce jingles get restored over before they can sound
if state.pendingRestore and sourceStopped(state.source)
and not state.loopSource then
and not state.loopSource and not chipAwaitingFirstBuffer() then
Music.restoreMap(data)
end
end
+14 -16
View File
@@ -3415,8 +3415,11 @@ function OverworldState:drawWorld()
love.graphics.setShader(shader)
end
end
local ox = ha.px - 64 - cam.x
local oy = ha.py - 64 - cam.y
-- TileRenderer windows with -floor(cam), so the overlay must use the
-- same snap or a fractional camera (odd fill/tilt view sizes) parks
-- the balls a pixel off the machine tiles
local ox = ha.px - 64 - math.floor(cam.x)
local oy = ha.py - 64 - math.floor(cam.y)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(img, self.healMachineQuads[1], ox + 44, oy + 20)
for i = 1, math.min(ha.lit, #HEAL_BALL_XY) do
@@ -3632,11 +3635,13 @@ function OverworldState:drawWorld()
else
-- === TILT PATH: ground-hugging FX stay on the projected ground, all
-- standing things billboard upright over it in a separate pass. ======
-- Dust is ground-hugging smoke -> ground canvas (puts it
-- with the flat layer, so it projects with the ground). Flat mode
-- draws it last, over the sprites, in the same canvas; here the two
-- Dust / cut / the Poké Center heal overlay hug the BG (the heal
-- machine is a tileset graphic; its OAM balls must ride that plane or
-- they float off the machine once the ground foreshortens). Flat mode
-- draws them last, over the sprites, in the same canvas; here the two
-- layers are separate and composited ground-under-upright, so drawing
-- it now into the still-active ground canvas is order-equivalent.
-- them now into the still-active ground canvas is order-equivalent.
fxHeal()
fxDust()
fxCutTree()
@@ -3692,18 +3697,11 @@ function OverworldState:drawWorld()
end
end
-- Screen-anchored world FX : each billboards at the
-- ground foot of the character it belongs to, so it stands upright and
-- scales with that character's depth.
-- heal machine -> the healed player's foot (the machine stands on
-- the ground in front of where the player was)
-- Standing world FX: each billboards at the ground foot of the
-- character it belongs to, so it stays upright over the tilted ground.
-- emote bubble -> the spotting NPC's foot (rides above its head)
-- fly bird, rod -> the player's foot
if self.healAnim then
local fx = self.healAnim.px - cam.x + 8
local fy = self.healAnim.py - cam.y + 16
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxHeal)
end
-- (heal machine is ground-hugging -- drawn above with dust/cut)
if self.emote and self.emote.npc then
local fx = self.emote.npc.px - cam.x + 8
local fy = self.emote.npc.py - cam.y + 16
+18
View File
@@ -344,6 +344,24 @@ check(lastSource().queueable and lastSource().playing,
"a chip song still plays after a file song")
check(not body.playing, "the outgoing file song was stopped")
-- playOnce must survive the threaded "empty QueueableSource" window:
-- Source:isPlaying is false until the first worker buffer lands, and that
-- gap must not look like the jingle already ended (Poké Center heal).
data = reset(fixtureData())
Music.playMap(data, "PALLET_TOWN", false, false)
check(Music.playOnce(data, "Music_Chip"), "playOnce starts a chip jingle")
local jingle = lastSource()
local clearAwait = ChipAudio._simulateAwaitingFirstBufferForTest()
check(clearAwait ~= nil, "test can force the awaiting-first-buffer window")
check(Music.oneShotPlaying(),
"oneShotPlaying stays true while the first buffer is still in flight")
Music.update(data)
check(jingle == lastSource() and jingle.queueable,
"pendingRestore does not swap the map theme over a pending chip jingle")
check(ChipAudio.awaitingFirstBuffer(),
"awaitingFirstBuffer reports the forced window")
clearAwait()
-- sfx shape dispatch
check(Sound.play(data, "Beep") == nil, "Sound.play returns nothing")
check(lastSource().file == "assets/beep.wav", "a bare string sfx is a static source")