Compare commits

..

2 Commits

Author SHA1 Message Date
bryanthaboi 895e904d75 too many comments 2026-07-23 14:25:00 -04:00
bryanthaboi 1ce9e32dd1 frame cap update 2026-07-23 14:20:23 -04:00
5 changed files with 19 additions and 84 deletions
+2 -4
View File
@@ -42,10 +42,8 @@ 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 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
and the screen-anchored FX attached to them (heal machine glow, emote
bubbles, the fishing rod, the FLY bird). 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,25 +230,6 @@ 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)
@@ -259,7 +240,6 @@ 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
@@ -322,20 +302,6 @@ 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)
+1 -14
View File
@@ -254,7 +254,6 @@ 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
@@ -348,24 +347,14 @@ 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)
@@ -452,10 +441,8 @@ 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 and not chipAwaitingFirstBuffer() then
and not state.loopSource then
Music.restoreMap(data)
end
end
+16 -14
View File
@@ -3415,11 +3415,8 @@ function OverworldState:drawWorld()
love.graphics.setShader(shader)
end
end
-- 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)
local ox = ha.px - 64 - cam.x
local oy = ha.py - 64 - 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
@@ -3635,13 +3632,11 @@ 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 / 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
-- 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
-- layers are separate and composited ground-under-upright, so drawing
-- them now into the still-active ground canvas is order-equivalent.
fxHeal()
-- it now into the still-active ground canvas is order-equivalent.
fxDust()
fxCutTree()
@@ -3697,11 +3692,18 @@ function OverworldState:drawWorld()
end
end
-- Standing world FX: each billboards at the ground foot of the
-- character it belongs to, so it stays upright over the tilted ground.
-- 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)
-- emote bubble -> the spotting NPC's foot (rides above its head)
-- fly bird, rod -> the player's foot
-- (heal machine is ground-hugging -- drawn above with dust/cut)
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
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,24 +344,6 @@ 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")