perf: fight replay memory leak fixes

Replace 32 bare setTimeout calls with tracked timeouts across all fight
modules (effects, entrances, finishers, choreographies). All timers are
now registered in cleanupTimers Set and cleared on scene destroy.

Enhanced destroy() to also clean up active speech bubbles and explicitly
destroy all remaining game objects before k.quit().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 23:55:58 +00:00
co-authored by Claude Opus 4.6
parent 57703a28ad
commit 78f83a7520
11 changed files with 55 additions and 46 deletions
+12 -4
View File
@@ -386,7 +386,7 @@ export async function createFightScene(config: FightSceneConfig) {
})
lines.push(line)
}
setTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
trackedTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
}
// Flash the whole screen
@@ -1802,7 +1802,7 @@ export async function createFightScene(config: FightSceneConfig) {
const choreoCtx: ChoreoContext = {
k, W, H, GROUND_Y, FRAME_SIZE, theme,
safeColor: (color: string) => safeColor(k, color),
trackedInterval, clearTracked,
trackedTimeout, trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
@@ -2041,7 +2041,7 @@ export async function createFightScene(config: FightSceneConfig) {
else activeBubbleB = allEls
// Fade out after duration
setTimeout(() => {
trackedTimeout(() => {
allEls.forEach(el => {
if (!el.exists()) return
k.tween(el.opacity, 0, 0.35, (v) => { el.opacity = v }).then(() => {
@@ -2368,7 +2368,7 @@ export async function createFightScene(config: FightSceneConfig) {
if (funnyAction === 0) {
// Falls asleep, snaps awake
for (let z = 0; z < 3; z++) {
setTimeout(() => {
trackedTimeout(() => {
spawnEmoteText(judge.pos.x + 15, judge.pos.y - 20, 'Z', '#8888ff', 1.0)
}, z * 400)
}
@@ -3441,11 +3441,19 @@ export async function createFightScene(config: FightSceneConfig) {
stopMusic()
stopTalking('a')
stopTalking('b')
// Destroy lingering speech bubbles
activeBubbleA.forEach(o => { if (o.exists()) o.destroy() })
activeBubbleB.forEach(o => { if (o.exists()) o.destroy() })
activeBubbleA.length = 0
activeBubbleB.length = 0
// Clear all tracked timers
for (const id of cleanupTimers) {
clearInterval(id)
clearTimeout(id)
}
cleanupTimers.clear()
// Destroy all remaining game objects before quitting
try { k.get('*').forEach(o => { if (o.exists()) o.destroy() }) } catch {}
k.quit()
},
}