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
@@ -386,7 +386,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
})
|
})
|
||||||
lines.push(line)
|
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
|
// Flash the whole screen
|
||||||
@@ -1802,7 +1802,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
const choreoCtx: ChoreoContext = {
|
const choreoCtx: ChoreoContext = {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme,
|
||||||
safeColor: (color: string) => safeColor(k, color),
|
safeColor: (color: string) => safeColor(k, color),
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -2041,7 +2041,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
else activeBubbleB = allEls
|
else activeBubbleB = allEls
|
||||||
|
|
||||||
// Fade out after duration
|
// Fade out after duration
|
||||||
setTimeout(() => {
|
trackedTimeout(() => {
|
||||||
allEls.forEach(el => {
|
allEls.forEach(el => {
|
||||||
if (!el.exists()) return
|
if (!el.exists()) return
|
||||||
k.tween(el.opacity, 0, 0.35, (v) => { el.opacity = v }).then(() => {
|
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) {
|
if (funnyAction === 0) {
|
||||||
// Falls asleep, snaps awake
|
// Falls asleep, snaps awake
|
||||||
for (let z = 0; z < 3; z++) {
|
for (let z = 0; z < 3; z++) {
|
||||||
setTimeout(() => {
|
trackedTimeout(() => {
|
||||||
spawnEmoteText(judge.pos.x + 15, judge.pos.y - 20, 'Z', '#8888ff', 1.0)
|
spawnEmoteText(judge.pos.x + 15, judge.pos.y - 20, 'Z', '#8888ff', 1.0)
|
||||||
}, z * 400)
|
}, z * 400)
|
||||||
}
|
}
|
||||||
@@ -3441,11 +3441,19 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
stopMusic()
|
stopMusic()
|
||||||
stopTalking('a')
|
stopTalking('a')
|
||||||
stopTalking('b')
|
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) {
|
for (const id of cleanupTimers) {
|
||||||
clearInterval(id)
|
clearInterval(id)
|
||||||
clearTimeout(id)
|
clearTimeout(id)
|
||||||
}
|
}
|
||||||
cleanupTimers.clear()
|
cleanupTimers.clear()
|
||||||
|
// Destroy all remaining game objects before quitting
|
||||||
|
try { k.get('*').forEach(o => { if (o.exists()) o.destroy() }) } catch {}
|
||||||
k.quit()
|
k.quit()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
|||||||
export function createBasicChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
export function createBasicChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -267,7 +267,7 @@ async function fullScreenDash(atk: Fighter, def: Fighter, dir: number, origAX: n
|
|||||||
k.opacity(0.3),
|
k.opacity(0.3),
|
||||||
k.z(12),
|
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)
|
await k.tween(atk.pos.x, targetX, 0.18, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad)
|
||||||
// Hit happens when passing through defender
|
// 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.opacity(0.4),
|
||||||
k.z(9),
|
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
|
// Move attacker a step forward
|
||||||
const stepX = startX + ((contactX - startX) / ghostCount) * (i + 1)
|
const stepX = startX + ((contactX - startX) / ghostCount) * (i + 1)
|
||||||
await k.tween(atk.pos.x, stepX, 0.03, (v) => { atk.pos.x = v })
|
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.z(24),
|
||||||
k.anchor('bot'),
|
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)
|
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.z(9),
|
||||||
k.anchor('bot'),
|
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)
|
await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
|
||||||
atk.play('attack')
|
atk.play('attack')
|
||||||
@@ -716,7 +716,7 @@ async function fireBreath(atk: Fighter, def: Fighter, dir: number, origAX: numbe
|
|||||||
})
|
})
|
||||||
if (i % 5 === 0) {
|
if (i % 5 === 0) {
|
||||||
def.opacity = 0.5
|
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)
|
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.opacity(0.8),
|
||||||
k.z(9),
|
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() })
|
k.tween(0.8, 0, 1.5, (v) => { if (splat.exists()) splat.opacity = v }).then(() => { if (splat.exists()) splat.destroy() })
|
||||||
}, 800)
|
}, 800)
|
||||||
}
|
}
|
||||||
@@ -1097,7 +1097,7 @@ async function pocketCannon(atk: Fighter, def: Fighter, dir: number, origAX: num
|
|||||||
k.color(safeColor('#ffee00')),
|
k.color(safeColor('#ffee00')),
|
||||||
k.opacity(0.9), k.z(18),
|
k.opacity(0.9), k.z(18),
|
||||||
])
|
])
|
||||||
setTimeout(() => { if (flash.exists()) flash.destroy() }, 60)
|
trackedTimeout(() => { if (flash.exists()) flash.destroy() }, 60)
|
||||||
// Recoil
|
// Recoil
|
||||||
const recoilX = atk.pos.x - dir * 15
|
const recoilX = atk.pos.x - dir * 15
|
||||||
k.tween(atk.pos.x, recoilX, 0.04, (v) => {
|
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> {
|
export function createCreatorChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -81,7 +81,7 @@ async function lightningInvoice(atk: Fighter, def: Fighter, dir: number, origAX:
|
|||||||
k.opacity(0.95),
|
k.opacity(0.95),
|
||||||
k.z(20),
|
k.z(20),
|
||||||
])
|
])
|
||||||
setTimeout(() => { if (bolt.exists()) bolt.destroy() }, 300)
|
trackedTimeout(() => { if (bolt.exists()) bolt.destroy() }, 300)
|
||||||
}
|
}
|
||||||
// ₿ symbol flash at impact
|
// ₿ symbol flash at impact
|
||||||
const btcFlash = k.add([
|
const btcFlash = k.add([
|
||||||
@@ -231,7 +231,7 @@ async function creatorMode(atk: Fighter, def: Fighter, dir: number, origAX: numb
|
|||||||
k.opacity(0.8),
|
k.opacity(0.8),
|
||||||
k.z(16),
|
k.z(16),
|
||||||
])
|
])
|
||||||
setTimeout(() => { if (sp.exists()) sp.destroy() }, 200)
|
trackedTimeout(() => { if (sp.exists()) sp.destroy() }, 200)
|
||||||
await k.wait(0.05)
|
await k.wait(0.05)
|
||||||
}
|
}
|
||||||
// Reality warp — defender's sprite distorts
|
// 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) {
|
export function createFactories(ctx: ChoreoContext) {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -398,7 +398,7 @@ function makeGunProp(propName: string, bulletCount: number = 4, bulletColor: str
|
|||||||
// Muzzle flash
|
// Muzzle flash
|
||||||
const mfx = gx + dir * 30, mfy = gy
|
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')])
|
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
|
// Bullet trail
|
||||||
const targetY = def.pos.y - 20 - Math.random() * 30
|
const targetY = def.pos.y - 20 - Math.random() * 30
|
||||||
await spawnProjectile(mfx, mfy, def.pos.x, targetY, bulletColor, 4)
|
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> {
|
export function createRangedChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -57,7 +57,7 @@ async function gunBurst(atk: Fighter, def: Fighter, dir: number, origAX: number,
|
|||||||
sfxGunshot()
|
sfxGunshot()
|
||||||
// Muzzle flash
|
// 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')])
|
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
|
const targetY = def.pos.y - 20 - Math.random() * 50
|
||||||
spawnBullet(muzzleX, gunY, def.pos.x, targetY)
|
spawnBullet(muzzleX, gunY, def.pos.x, targetY)
|
||||||
// Recoil: bounce gun back
|
// Recoil: bounce gun back
|
||||||
@@ -140,7 +140,7 @@ async function rocketLauncher(atk: Fighter, def: Fighter, dir: number, origAX: n
|
|||||||
screenFlash('#ff6600', 0.2)
|
screenFlash('#ff6600', 0.2)
|
||||||
// Explosion rings
|
// Explosion rings
|
||||||
for (let i = 0; i < 3; i++) {
|
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')
|
spawnSparks(def.pos.x, def.pos.y - 30, 25, '#ff6600')
|
||||||
def.play(isCritical ? 'knockback' : 'hit')
|
def.play(isCritical ? 'knockback' : 'hit')
|
||||||
@@ -206,7 +206,7 @@ async function minigunSpray(atk: Fighter, def: Fighter, dir: number, origAX: num
|
|||||||
sfxGunshot()
|
sfxGunshot()
|
||||||
// Muzzle flash
|
// 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')])
|
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
|
const spread = (Math.random() - 0.5) * 50
|
||||||
spawnBullet(muzzleX, gpy, def.pos.x + (Math.random() - 0.5) * 30, def.pos.y - 20 + spread)
|
spawnBullet(muzzleX, gpy, def.pos.x + (Math.random() - 0.5) * 30, def.pos.y - 20 + spread)
|
||||||
// Shell casing
|
// Shell casing
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
|||||||
export function createUltimatesChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
export function createUltimatesChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -103,7 +103,7 @@ async function ultimateGatlingFury(atk: Fighter, def: Fighter, dir: number, orig
|
|||||||
sfxGunshot()
|
sfxGunshot()
|
||||||
k.shake(3)
|
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)])
|
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) {
|
if (i % 3 === 0) {
|
||||||
def.play('hit')
|
def.play('hit')
|
||||||
spawnSparks(def.pos.x, def.pos.y - 20 - Math.random() * 20, 4, '#ff6600')
|
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)
|
sfxExplosion(); k.shake(25); screenFlash('#ff8800', 0.2)
|
||||||
// 3 shockwaves radiating outward
|
// 3 shockwaves radiating outward
|
||||||
for (let i = 0; i < 3; i++) {
|
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
|
// Ground cracks
|
||||||
for (let i = 0; i < 6; i++) {
|
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.opacity(0.4),
|
||||||
k.z(25),
|
k.z(25),
|
||||||
])
|
])
|
||||||
setTimeout(() => { if (trace.exists()) trace.destroy() }, 300)
|
trackedTimeout(() => { if (trace.exists()) trace.destroy() }, 300)
|
||||||
// Bullet travels slowly
|
// 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() })
|
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)
|
await k.wait(0.05)
|
||||||
@@ -343,7 +343,7 @@ async function ultimateBulletTimeBarrage(atk: Fighter, def: Fighter, dir: number
|
|||||||
k.z(24),
|
k.z(24),
|
||||||
k.anchor('bot'),
|
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 + 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)
|
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.z(9),
|
||||||
k.anchor('bot'),
|
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)
|
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')
|
def.play('knockback')
|
||||||
// Massive aftermath
|
// Massive aftermath
|
||||||
for (let i = 0; i < 8; i++) {
|
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])
|
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')
|
spawnSparks(def.pos.x + (Math.random() - 0.5) * 60, GROUND_Y - 10 - Math.random() * 40, 5, '#ffffff')
|
||||||
}, i * 50)
|
}, i * 50)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { ChoreoFn } from './factories'
|
|||||||
export function createWeaponsChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
export function createWeaponsChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -69,7 +69,7 @@ async function hammerSmash(atk: Fighter, def: Fighter, dir: number, origAX: numb
|
|||||||
// Ground crack effect
|
// Ground crack effect
|
||||||
for (let i = 0; i < 6; i++) {
|
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)])
|
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')
|
def.play(isCritical ? 'knockback' : 'hit')
|
||||||
k.shake(isCritical ? 25 : 14)
|
k.shake(isCritical ? 25 : 14)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { GameObj } from 'kaplay'
|
|||||||
import type { FightContext } from './types'
|
import type { FightContext } from './types'
|
||||||
|
|
||||||
export function glitchRGB(ctx: FightContext, duration: number = 0.2): void {
|
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 = [
|
const layers = [
|
||||||
{ color: '#ff0000', dx: 4 + Math.random() * 6, dy: -2 },
|
{ color: '#ff0000', dx: 4 + Math.random() * 6, dy: -2 },
|
||||||
{ color: '#0000ff', 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.x = l.dx + (Math.random() - 0.5) * 4
|
||||||
overlay.pos.y = l.dy + (Math.random() - 0.5) * 2
|
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)
|
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 {
|
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)
|
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 {
|
export function dimensionalShift(ctx: FightContext, duration: number = 0.6): void {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface EntranceDeps {
|
|||||||
export function createEntranceSystem(ctx: ChoreoContext, deps: EntranceDeps) {
|
export function createEntranceSystem(ctx: ChoreoContext, deps: EntranceDeps) {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -63,7 +63,7 @@ async function playEntrance() {
|
|||||||
// Trail of fire
|
// Trail of fire
|
||||||
const trail: any[] = []
|
const trail: any[] = []
|
||||||
for (let i = 0; i < 6; i++) {
|
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)])
|
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)
|
trail.push(t)
|
||||||
k.tween(t.opacity, 0, 0.4, (v) => { t.opacity = v }).then(() => { if (t.exists()) t.destroy() })
|
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
|
// Slide to stop
|
||||||
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutCubic)
|
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutCubic)
|
||||||
sfxBlock(); k.shake(3)
|
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
|
// 12: Parachute drop
|
||||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||||
@@ -420,7 +420,7 @@ async function playEntrance() {
|
|||||||
fighter.opacity = 1
|
fighter.opacity = 1
|
||||||
screenFlash('#ffcc00', 0.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)])
|
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
|
// Arc to position
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
k.tween(cannonX, homeX, 0.35, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
|
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) {
|
export function createFinisherSystem(ctx: ChoreoContext, deps: FinisherDeps) {
|
||||||
const {
|
const {
|
||||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||||
trackedInterval, clearTracked,
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||||
@@ -174,7 +174,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
|||||||
for (let s = 0; s < 3; s++) {
|
for (let s = 0; s < 3; s++) {
|
||||||
sfxGunshot(); sfxExplosion(); k.shake(12 + s * 3)
|
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)])
|
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)
|
await spawnProjectile(gx + dir * gunLen, gy, loser.pos.x, loser.pos.y - 20, '#ffcc00', 12)
|
||||||
sfxBulletHit()
|
sfxBulletHit()
|
||||||
loser.play(s < 2 ? 'hit' : 'knockback')
|
loser.play(s < 2 ? 'hit' : 'knockback')
|
||||||
@@ -317,7 +317,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
|||||||
fatalityTagline = 'GROOVY DEATH!'
|
fatalityTagline = 'GROOVY DEATH!'
|
||||||
announceSilly('Get down! No really, GET DOWN!')
|
announceSilly('Get down! No really, GET DOWN!')
|
||||||
const discoColors = ['#ff0044', '#44ff00', '#0044ff', '#ff8800', '#ff00ff', '#00ffff']
|
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)
|
const disco = spawnWeaponProp('disco_ball', loser.pos.x, -30, false, 20)
|
||||||
sfxSlideDown(); sfxBoing()
|
sfxSlideDown(); sfxBoing()
|
||||||
for (const p of disco) {
|
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(() => {
|
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)
|
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')
|
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')
|
loser.play(i % 3 === 0 ? 'hit' : 'idle')
|
||||||
@@ -877,7 +877,7 @@ async function playKO(winningSide: 'a' | 'b', winnerName: string) {
|
|||||||
k.shake(18); sfxExplosion()
|
k.shake(18); sfxExplosion()
|
||||||
await k.wait(0.3)
|
await k.wait(0.3)
|
||||||
loser.play('ko')
|
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
|
await k.wait(0.4) // pause for visual impact before fatality voice
|
||||||
const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype
|
const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype
|
||||||
const loserArch = winningSide === 'a' ? botB.archetype : botA.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')
|
spawnSparks(winner.pos.x, winner.pos.y - 50, 15, '#ffe14d')
|
||||||
for (let i = 0; i < 4; i++) {
|
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')
|
spawnSparks(winner.pos.x + (Math.random() - 0.5) * 60, winner.pos.y - 40 - Math.random() * 30, 8, '#ffe14d')
|
||||||
}, i * 200)
|
}, i * 200)
|
||||||
}
|
}
|
||||||
@@ -1034,7 +1034,7 @@ async function playPerfect(winningSide: 'a' | 'b', winnerName: string) {
|
|||||||
winner.scale.y = origScaleY
|
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)
|
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')
|
winner.play('win')
|
||||||
setTimeout(() => {
|
trackedTimeout(() => {
|
||||||
sfxWin()
|
sfxWin()
|
||||||
sfxWinAnnounce(winnerName)
|
sfxWinAnnounce(winnerName)
|
||||||
}, 300)
|
}, 300)
|
||||||
@@ -1042,7 +1042,7 @@ async function playPerfect(winningSide: 'a' | 'b', winnerName: string) {
|
|||||||
announceCrowdReaction('cheer')
|
announceCrowdReaction('cheer')
|
||||||
// Massive fireworks
|
// Massive fireworks
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
setTimeout(() => {
|
trackedTimeout(() => {
|
||||||
spawnSparks(
|
spawnSparks(
|
||||||
Math.random() * W,
|
Math.random() * W,
|
||||||
Math.random() * H * 0.5,
|
Math.random() * H * 0.5,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface ChoreoContext {
|
|||||||
FRAME_SIZE: number
|
FRAME_SIZE: number
|
||||||
theme: { bg: string; ground: string; accent: string }
|
theme: { bg: string; ground: string; accent: string }
|
||||||
safeColor: (color: string) => any
|
safeColor: (color: string) => any
|
||||||
|
trackedTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>
|
||||||
trackedInterval: (fn: () => void, ms: number) => ReturnType<typeof setInterval>
|
trackedInterval: (fn: () => void, ms: number) => ReturnType<typeof setInterval>
|
||||||
clearTracked: (id: ReturnType<typeof setTimeout>) => void
|
clearTracked: (id: ReturnType<typeof setTimeout>) => void
|
||||||
// Particles & effects
|
// Particles & effects
|
||||||
|
|||||||
Reference in New Issue
Block a user