Fire intro.oak_speech.finished only once (#308) (#309)

The shrink timeline called finish() every frame once past its end.
finish() emits finished and then pops the stack, so a listener that
pushes a screen (a warp, a menu) had that screen popped in the speech's
place: the speech stayed alive and re-fired the event every frame,
repeating the listener's side effects each time. Clear the shrink
state before finishing so the timeline cannot run finish() twice.

Co-authored-by: johnjohto <johnjohto@users.noreply.github.com>
This commit is contained in:
johnjohto
2026-07-27 13:39:12 -04:00
committed by GitHub
parent 43158d724b
commit 5113cfd951
2 changed files with 25 additions and 0 deletions
+4
View File
@@ -549,6 +549,10 @@ function OakSpeech:update(dt)
elseif s.frame >= 79 and s.frame <= 102 then
self.fadeLevel = math.floor((s.frame - 79) / 8) + 1
elseif s.frame > 102 then
-- clear before finish(): a finished-listener that pushes a state gets
-- ITS state popped in the speech's place, and a live shrink would call
-- finish() again next frame, re-firing the event every frame (#308)
self.shrink = nil
self:finish()
end
end
+21
View File
@@ -788,6 +788,27 @@ check(type("intro.oak_speech.started") == "string"
"intro.oak_speech lifecycle event names are stable")
events:removeOwner("fixture")
-- issue #308: finish runs once even when a finished-listener pushes a
-- screen (the pop in finish() then removes that screen instead of the
-- speech, and a live shrink re-fires the event every frame)
do
local fgame = { data = {}, stack = newStack(), input = newInput(),
save = { player = {} } }
local fspeech = OakSpeech.new(fgame, nil)
fspeech.shrink = { frame = 103 } -- past the shrink timeline's end
fgame.stack:push(fspeech)
fspeech.picReveal = nil -- skip the intro fade so update reaches shrink
local finishes = 0
events:on("intro.oak_speech.finished", function()
finishes = finishes + 1
fgame.stack:push({ pushedByListener = true }) -- the #308 trap
end, 0, "t308")
for _ = 1, 3 do fspeech:update(1 / 60) end
events:removeOwner("t308")
check(finishes == 1,
"finished fires once when a listener pushes a screen")
end
-- ModUI step helpers
local tiny = { { id = "a" }, { id = "c" } }
ModUI.insertStepAfter(tiny, "a", { id = "b" })