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:
co-authored by
Claude Opus 4.6
parent
57703a28ad
commit
78f83a7520
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
||||
export function createBasicChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -267,7 +267,7 @@ async function fullScreenDash(atk: Fighter, def: Fighter, dir: number, origAX: n
|
||||
k.opacity(0.3),
|
||||
k.z(12),
|
||||
])
|
||||
setTimeout(() => { if (line.exists()) line.destroy() }, 300)
|
||||
trackedTimeout(() => { if (line.exists()) line.destroy() }, 300)
|
||||
}
|
||||
await k.tween(atk.pos.x, targetX, 0.18, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad)
|
||||
// Hit happens when passing through defender
|
||||
@@ -491,7 +491,7 @@ async function afterimageDash(atk: Fighter, def: Fighter, dir: number, origAX: n
|
||||
k.opacity(0.4),
|
||||
k.z(9),
|
||||
])
|
||||
setTimeout(() => { k.tween(0.4, 0, 0.3, (v) => { if (ghost.exists()) ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() }) }, 50)
|
||||
trackedTimeout(() => { k.tween(0.4, 0, 0.3, (v) => { if (ghost.exists()) ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() }) }, 50)
|
||||
// Move attacker a step forward
|
||||
const stepX = startX + ((contactX - startX) / ghostCount) * (i + 1)
|
||||
await k.tween(atk.pos.x, stepX, 0.03, (v) => { atk.pos.x = v })
|
||||
@@ -577,7 +577,7 @@ async function bulletTimeAttack(atk: Fighter, def: Fighter, dir: number, origAX:
|
||||
k.z(24),
|
||||
k.anchor('bot'),
|
||||
])
|
||||
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 400)
|
||||
trackedTimeout(() => { if (ghost.exists()) ghost.destroy() }, 400)
|
||||
}
|
||||
await k.tween(atk.pos.x, atk.pos.x + leanBack, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
|
||||
|
||||
@@ -596,7 +596,7 @@ async function bulletTimeAttack(atk: Fighter, def: Fighter, dir: number, origAX:
|
||||
k.z(9),
|
||||
k.anchor('bot'),
|
||||
])
|
||||
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 250)
|
||||
trackedTimeout(() => { if (ghost.exists()) ghost.destroy() }, 250)
|
||||
}
|
||||
await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
|
||||
atk.play('attack')
|
||||
@@ -716,7 +716,7 @@ async function fireBreath(atk: Fighter, def: Fighter, dir: number, origAX: numbe
|
||||
})
|
||||
if (i % 5 === 0) {
|
||||
def.opacity = 0.5
|
||||
setTimeout(() => { if (def.exists()) def.opacity = 1 }, 50)
|
||||
trackedTimeout(() => { if (def.exists()) def.opacity = 1 }, 50)
|
||||
}
|
||||
await k.wait(0.02)
|
||||
}
|
||||
@@ -916,7 +916,7 @@ async function penStab(atk: Fighter, def: Fighter, dir: number, origAX: number,
|
||||
k.opacity(0.8),
|
||||
k.z(9),
|
||||
])
|
||||
setTimeout(() => {
|
||||
trackedTimeout(() => {
|
||||
k.tween(0.8, 0, 1.5, (v) => { if (splat.exists()) splat.opacity = v }).then(() => { if (splat.exists()) splat.destroy() })
|
||||
}, 800)
|
||||
}
|
||||
@@ -1097,7 +1097,7 @@ async function pocketCannon(atk: Fighter, def: Fighter, dir: number, origAX: num
|
||||
k.color(safeColor('#ffee00')),
|
||||
k.opacity(0.9), k.z(18),
|
||||
])
|
||||
setTimeout(() => { if (flash.exists()) flash.destroy() }, 60)
|
||||
trackedTimeout(() => { if (flash.exists()) flash.destroy() }, 60)
|
||||
// Recoil
|
||||
const recoilX = atk.pos.x - dir * 15
|
||||
k.tween(atk.pos.x, recoilX, 0.04, (v) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
||||
export function createCreatorChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -81,7 +81,7 @@ async function lightningInvoice(atk: Fighter, def: Fighter, dir: number, origAX:
|
||||
k.opacity(0.95),
|
||||
k.z(20),
|
||||
])
|
||||
setTimeout(() => { if (bolt.exists()) bolt.destroy() }, 300)
|
||||
trackedTimeout(() => { if (bolt.exists()) bolt.destroy() }, 300)
|
||||
}
|
||||
// ₿ symbol flash at impact
|
||||
const btcFlash = k.add([
|
||||
@@ -231,7 +231,7 @@ async function creatorMode(atk: Fighter, def: Fighter, dir: number, origAX: numb
|
||||
k.opacity(0.8),
|
||||
k.z(16),
|
||||
])
|
||||
setTimeout(() => { if (sp.exists()) sp.destroy() }, 200)
|
||||
trackedTimeout(() => { if (sp.exists()) sp.destroy() }, 200)
|
||||
await k.wait(0.05)
|
||||
}
|
||||
// Reality warp — defender's sprite distorts
|
||||
|
||||
@@ -248,7 +248,7 @@ export type ChoreoFn = (atk: Fighter, def: Fighter, dir: number, origAX: number,
|
||||
export function createFactories(ctx: ChoreoContext) {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -398,7 +398,7 @@ function makeGunProp(propName: string, bulletCount: number = 4, bulletColor: str
|
||||
// Muzzle flash
|
||||
const mfx = gx + dir * 30, mfy = gy
|
||||
const flash = k.add([k.circle(5 + Math.random() * 4), k.pos(mfx, mfy), k.color(k.Color.fromHex(bulletColor)), k.opacity(0.9), k.z(19), k.anchor('center')])
|
||||
setTimeout(() => { if (flash.exists()) flash.destroy() }, 40)
|
||||
trackedTimeout(() => { if (flash.exists()) flash.destroy() }, 40)
|
||||
// Bullet trail
|
||||
const targetY = def.pos.y - 20 - Math.random() * 30
|
||||
await spawnProjectile(mfx, mfy, def.pos.x, targetY, bulletColor, 4)
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
||||
export function createRangedChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -57,7 +57,7 @@ async function gunBurst(atk: Fighter, def: Fighter, dir: number, origAX: number,
|
||||
sfxGunshot()
|
||||
// Muzzle flash
|
||||
const flash = k.add([k.circle(5), k.pos(muzzleX, gunY), k.color(k.Color.fromHex('#ffee00')), k.opacity(0.9), k.z(19), k.anchor('center')])
|
||||
setTimeout(() => { if (flash.exists()) flash.destroy() }, 35)
|
||||
trackedTimeout(() => { if (flash.exists()) flash.destroy() }, 35)
|
||||
const targetY = def.pos.y - 20 - Math.random() * 50
|
||||
spawnBullet(muzzleX, gunY, def.pos.x, targetY)
|
||||
// Recoil: bounce gun back
|
||||
@@ -140,7 +140,7 @@ async function rocketLauncher(atk: Fighter, def: Fighter, dir: number, origAX: n
|
||||
screenFlash('#ff6600', 0.2)
|
||||
// Explosion rings
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setTimeout(() => spawnShockwave(def.pos.x, def.pos.y - 20, ['#ff6600', '#ff2d2d', '#ffcc00'][i]), i * 60)
|
||||
trackedTimeout(() => spawnShockwave(def.pos.x, def.pos.y - 20, ['#ff6600', '#ff2d2d', '#ffcc00'][i]), i * 60)
|
||||
}
|
||||
spawnSparks(def.pos.x, def.pos.y - 30, 25, '#ff6600')
|
||||
def.play(isCritical ? 'knockback' : 'hit')
|
||||
@@ -206,7 +206,7 @@ async function minigunSpray(atk: Fighter, def: Fighter, dir: number, origAX: num
|
||||
sfxGunshot()
|
||||
// Muzzle flash
|
||||
const flash = k.add([k.circle(4 + Math.random() * 4), k.pos(muzzleX, gpy + (Math.random() - 0.5) * 6), k.color(k.Color.fromHex('#ffee00')), k.opacity(0.9), k.z(19), k.anchor('center')])
|
||||
setTimeout(() => { if (flash.exists()) flash.destroy() }, 30)
|
||||
trackedTimeout(() => { if (flash.exists()) flash.destroy() }, 30)
|
||||
const spread = (Math.random() - 0.5) * 50
|
||||
spawnBullet(muzzleX, gpy, def.pos.x + (Math.random() - 0.5) * 30, def.pos.y - 20 + spread)
|
||||
// Shell casing
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
||||
export function createUltimatesChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -103,7 +103,7 @@ async function ultimateGatlingFury(atk: Fighter, def: Fighter, dir: number, orig
|
||||
sfxGunshot()
|
||||
k.shake(3)
|
||||
const flash = k.add([k.circle(8), k.pos(gx + dir * gunLen, gy + (Math.random() - 0.5) * 10), k.color(safeColor('#ffee00')), k.opacity(0.9), k.z(18)])
|
||||
setTimeout(() => { if (flash.exists()) flash.destroy() }, 40)
|
||||
trackedTimeout(() => { if (flash.exists()) flash.destroy() }, 40)
|
||||
if (i % 3 === 0) {
|
||||
def.play('hit')
|
||||
spawnSparks(def.pos.x, def.pos.y - 20 - Math.random() * 20, 4, '#ff6600')
|
||||
@@ -127,7 +127,7 @@ async function ultimateEarthquake(atk: Fighter, def: Fighter, dir: number, origA
|
||||
sfxExplosion(); k.shake(25); screenFlash('#ff8800', 0.2)
|
||||
// 3 shockwaves radiating outward
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setTimeout(() => { spawnShockwave(atk.pos.x + (i - 1) * 100, GROUND_Y, '#ff8800'); k.shake(8) }, i * 100)
|
||||
trackedTimeout(() => { spawnShockwave(atk.pos.x + (i - 1) * 100, GROUND_Y, '#ff8800'); k.shake(8) }, i * 100)
|
||||
}
|
||||
// Ground cracks
|
||||
for (let i = 0; i < 6; i++) {
|
||||
@@ -325,7 +325,7 @@ async function ultimateBulletTimeBarrage(atk: Fighter, def: Fighter, dir: number
|
||||
k.opacity(0.4),
|
||||
k.z(25),
|
||||
])
|
||||
setTimeout(() => { if (trace.exists()) trace.destroy() }, 300)
|
||||
trackedTimeout(() => { if (trace.exists()) trace.destroy() }, 300)
|
||||
// Bullet travels slowly
|
||||
k.tween(bullet.pos.x, atk.pos.x + (Math.random() - 0.5) * 30, 0.3, (v) => { if (bullet.exists()) bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() })
|
||||
await k.wait(0.05)
|
||||
@@ -343,7 +343,7 @@ async function ultimateBulletTimeBarrage(atk: Fighter, def: Fighter, dir: number
|
||||
k.z(24),
|
||||
k.anchor('bot'),
|
||||
])
|
||||
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 350)
|
||||
trackedTimeout(() => { if (ghost.exists()) ghost.destroy() }, 350)
|
||||
await k.tween(atk.pos.y, GROUND_Y + leanDir * 15, 0.08, (v) => { atk.pos.y = v }, k.easings.easeOutQuad)
|
||||
await k.tween(atk.pos.y, GROUND_Y, 0.08, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
|
||||
}
|
||||
@@ -367,7 +367,7 @@ async function ultimateBulletTimeBarrage(atk: Fighter, def: Fighter, dir: number
|
||||
k.z(9),
|
||||
k.anchor('bot'),
|
||||
])
|
||||
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 300)
|
||||
trackedTimeout(() => { if (ghost.exists()) ghost.destroy() }, 300)
|
||||
}
|
||||
await k.tween(atk.pos.x, contactX, 0.06, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
|
||||
|
||||
@@ -700,7 +700,7 @@ async function ultimateWorldEnder(atk: Fighter, def: Fighter, dir: number, origA
|
||||
def.play('knockback')
|
||||
// Massive aftermath
|
||||
for (let i = 0; i < 8; i++) {
|
||||
setTimeout(() => {
|
||||
trackedTimeout(() => {
|
||||
spawnShockwave(def.pos.x + (Math.random() - 0.5) * 100, GROUND_Y, ['#ff2d7b', '#ffe14d', '#00f0ff', '#b83dff', '#39ff14', '#ff6600', '#ffffff', '#ff2d2d'][i])
|
||||
spawnSparks(def.pos.x + (Math.random() - 0.5) * 60, GROUND_Y - 10 - Math.random() * 40, 5, '#ffffff')
|
||||
}, i * 50)
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
||||
export function createWeaponsChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -69,7 +69,7 @@ async function hammerSmash(atk: Fighter, def: Fighter, dir: number, origAX: numb
|
||||
// Ground crack effect
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const crack = k.add([k.rect(2, 15 + Math.random() * 20), k.pos(def.pos.x + (i - 3) * 12, GROUND_Y - 5), k.color(safeColor('#ff6600')), k.opacity(0.8), k.z(4)])
|
||||
setTimeout(() => { k.tween(0.8, 0, 0.6, (v) => { if (crack.exists()) crack.opacity = v }).then(() => { if (crack.exists()) crack.destroy() }) }, 400)
|
||||
trackedTimeout(() => { k.tween(0.8, 0, 0.6, (v) => { if (crack.exists()) crack.opacity = v }).then(() => { if (crack.exists()) crack.destroy() }) }, 400)
|
||||
}
|
||||
def.play(isCritical ? 'knockback' : 'hit')
|
||||
k.shake(isCritical ? 25 : 14)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { GameObj } from 'kaplay'
|
||||
import type { FightContext } from './types'
|
||||
|
||||
export function glitchRGB(ctx: FightContext, duration: number = 0.2): void {
|
||||
const { k, W, H, safeColor } = ctx
|
||||
const { k, W, H, safeColor, trackedTimeout } = ctx
|
||||
const layers = [
|
||||
{ color: '#ff0000', dx: 4 + Math.random() * 6, dy: -2 },
|
||||
{ color: '#0000ff', dx: -(4 + Math.random() * 6), dy: 2 },
|
||||
@@ -16,7 +16,7 @@ export function glitchRGB(ctx: FightContext, duration: number = 0.2): void {
|
||||
overlay.pos.x = l.dx + (Math.random() - 0.5) * 4
|
||||
overlay.pos.y = l.dy + (Math.random() - 0.5) * 2
|
||||
})
|
||||
setTimeout(() => { if (overlay.exists()) overlay.destroy() }, duration * 1000)
|
||||
trackedTimeout(() => { if (overlay.exists()) overlay.destroy() }, duration * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function scanlineGlitch(ctx: FightContext, duration: number = 0.3): void
|
||||
})
|
||||
lines.push(line)
|
||||
}
|
||||
setTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
|
||||
ctx.trackedTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
|
||||
}
|
||||
|
||||
export function vhsTracking(ctx: FightContext, duration: number = 0.4): void {
|
||||
@@ -58,7 +58,7 @@ export function vhsTracking(ctx: FightContext, duration: number = 0.4): void {
|
||||
})
|
||||
bands.push(band)
|
||||
}
|
||||
setTimeout(() => { bands.forEach(b => { if (b.exists()) b.destroy() }) }, duration * 1000)
|
||||
ctx.trackedTimeout(() => { bands.forEach(b => { if (b.exists()) b.destroy() }) }, duration * 1000)
|
||||
}
|
||||
|
||||
export function dimensionalShift(ctx: FightContext, duration: number = 0.6): void {
|
||||
|
||||
@@ -11,7 +11,7 @@ interface EntranceDeps {
|
||||
export function createEntranceSystem(ctx: ChoreoContext, deps: EntranceDeps) {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -63,7 +63,7 @@ async function playEntrance() {
|
||||
// Trail of fire
|
||||
const trail: any[] = []
|
||||
for (let i = 0; i < 6; i++) {
|
||||
setTimeout(() => {
|
||||
trackedTimeout(() => {
|
||||
const t = k.add([k.circle(4 + Math.random() * 6), k.pos(homeX + (Math.random() - 0.5) * 20, fighter.pos.y + 20), k.color(safeColor(i < 3 ? '#ff6600' : '#ffcc00')), k.opacity(0.7), k.z(9)])
|
||||
trail.push(t)
|
||||
k.tween(t.opacity, 0, 0.4, (v) => { t.opacity = v }).then(() => { if (t.exists()) t.destroy() })
|
||||
@@ -280,7 +280,7 @@ async function playEntrance() {
|
||||
// Slide to stop
|
||||
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutCubic)
|
||||
sfxBlock(); k.shake(3)
|
||||
setTimeout(() => { iceTrail.forEach(i => { if (i.exists()) i.destroy() }) }, 800)
|
||||
trackedTimeout(() => { iceTrail.forEach(i => { if (i.exists()) i.destroy() }) }, 800)
|
||||
},
|
||||
// 12: Parachute drop
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
@@ -420,7 +420,7 @@ async function playEntrance() {
|
||||
fighter.opacity = 1
|
||||
screenFlash('#ffcc00', 0.1)
|
||||
const flashCircle = k.add([k.circle(20), k.pos(cannonX + (fromLeft ? 25 : -25), GROUND_Y - 35), k.color(safeColor('#ffee00')), k.opacity(0.9), k.z(12)])
|
||||
setTimeout(() => { if (flashCircle.exists()) flashCircle.destroy() }, 80)
|
||||
trackedTimeout(() => { if (flashCircle.exists()) flashCircle.destroy() }, 80)
|
||||
// Arc to position
|
||||
await Promise.all([
|
||||
k.tween(cannonX, homeX, 0.35, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
|
||||
|
||||
@@ -34,7 +34,7 @@ interface FinisherDeps {
|
||||
export function createFinisherSystem(ctx: ChoreoContext, deps: FinisherDeps) {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
trackedTimeout, trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
@@ -174,7 +174,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
||||
for (let s = 0; s < 3; s++) {
|
||||
sfxGunshot(); sfxExplosion(); k.shake(12 + s * 3)
|
||||
const flash = k.add([k.circle(15), k.pos(gx + dir * gunLen, gy), k.color(safeColor('#ffee00')), k.opacity(0.9), k.z(18)])
|
||||
setTimeout(() => { if (flash.exists()) flash.destroy() }, 50)
|
||||
trackedTimeout(() => { if (flash.exists()) flash.destroy() }, 50)
|
||||
await spawnProjectile(gx + dir * gunLen, gy, loser.pos.x, loser.pos.y - 20, '#ffcc00', 12)
|
||||
sfxBulletHit()
|
||||
loser.play(s < 2 ? 'hit' : 'knockback')
|
||||
@@ -317,7 +317,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
||||
fatalityTagline = 'GROOVY DEATH!'
|
||||
announceSilly('Get down! No really, GET DOWN!')
|
||||
const discoColors = ['#ff0044', '#44ff00', '#0044ff', '#ff8800', '#ff00ff', '#00ffff']
|
||||
for (let i = 0; i < 6; i++) { setTimeout(() => screenFlash(discoColors[i], 0.06), i * 80) }
|
||||
for (let i = 0; i < 6; i++) { trackedTimeout(() => screenFlash(discoColors[i], 0.06), i * 80) }
|
||||
const disco = spawnWeaponProp('disco_ball', loser.pos.x, -30, false, 20)
|
||||
sfxSlideDown(); sfxBoing()
|
||||
for (const p of disco) {
|
||||
@@ -695,7 +695,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
||||
])
|
||||
k.tween(block.pos.y, GROUND_Y - 5 - i * 6, 0.15 + i * 0.02, (v) => { block.pos.y = v }, k.easings.easeInQuad).then(() => {
|
||||
sfxBonk(); k.shake(3)
|
||||
setTimeout(() => { if (block.exists()) k.tween(1, 0, 0.8, (v) => { block.opacity = v }).then(() => { if (block.exists()) block.destroy() }) }, 800)
|
||||
trackedTimeout(() => { if (block.exists()) k.tween(1, 0, 0.8, (v) => { block.opacity = v }).then(() => { if (block.exists()) block.destroy() }) }, 800)
|
||||
})
|
||||
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 30 - Math.random() * 30, ['{', '}', '()', '=>', '[]', ';;', 'nil', 'NaN', '???', '//'][i], '#00ff00')
|
||||
loser.play(i % 3 === 0 ? 'hit' : 'idle')
|
||||
@@ -877,7 +877,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
||||
k.shake(18); sfxExplosion()
|
||||
await k.wait(0.3)
|
||||
loser.play('ko')
|
||||
if (Math.random() < 0.5) setTimeout(() => sfxRandomFail(), 200)
|
||||
if (Math.random() < 0.5) trackedTimeout(() => sfxRandomFail(), 200)
|
||||
await k.wait(0.4) // pause for visual impact before fatality voice
|
||||
const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype
|
||||
const loserArch = winningSide === 'a' ? botB.archetype : botA.archetype
|
||||
@@ -901,7 +901,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
||||
}
|
||||
spawnSparks(winner.pos.x, winner.pos.y - 50, 15, '#ffe14d')
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTimeout(() => {
|
||||
trackedTimeout(() => {
|
||||
spawnSparks(winner.pos.x + (Math.random() - 0.5) * 60, winner.pos.y - 40 - Math.random() * 30, 8, '#ffe14d')
|
||||
}, i * 200)
|
||||
}
|
||||
@@ -1034,7 +1034,7 @@ async function playPerfect(winningSide: 'a' | 'b', winnerName: string) {
|
||||
winner.scale.y = origScaleY
|
||||
await k.tween(winner.pos.x, winningSide === 'a' ? HOME_A : HOME_B, 0.3, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
|
||||
winner.play('win')
|
||||
setTimeout(() => {
|
||||
trackedTimeout(() => {
|
||||
sfxWin()
|
||||
sfxWinAnnounce(winnerName)
|
||||
}, 300)
|
||||
@@ -1042,7 +1042,7 @@ async function playPerfect(winningSide: 'a' | 'b', winnerName: string) {
|
||||
announceCrowdReaction('cheer')
|
||||
// Massive fireworks
|
||||
for (let i = 0; i < 8; i++) {
|
||||
setTimeout(() => {
|
||||
trackedTimeout(() => {
|
||||
spawnSparks(
|
||||
Math.random() * W,
|
||||
Math.random() * H * 0.5,
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface ChoreoContext {
|
||||
FRAME_SIZE: number
|
||||
theme: { bg: string; ground: string; accent: string }
|
||||
safeColor: (color: string) => any
|
||||
trackedTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>
|
||||
trackedInterval: (fn: () => void, ms: number) => ReturnType<typeof setInterval>
|
||||
clearTracked: (id: ReturnType<typeof setTimeout>) => void
|
||||
// Particles & effects
|
||||
|
||||
Reference in New Issue
Block a user