import kaplay from 'kaplay' import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp, LoadSpriteOpt } from 'kaplay' import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, generateHumanFighterSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites' import type { Fighter, FightSceneConfig, RoundEvent, FightContext, ChoreoContext } from './fight/types' export type { FightSceneConfig, RoundEvent } from './fight/types' import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MOVES, CREATOR_ULTIMATES } from './fight/constants' import { CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO, CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF, TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles, isReducedMotion, haptic, } from './fight/config' import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles' import { glitchRGB as _glitchRGB, scanlineGlitch as _scanlineGlitch, vhsTracking as _vhsTracking, dimensionalShift as _dimensionalShift } from './fight/effects' import { spawnProjectile as _spawnProjectile } from './fight/projectiles' import { createChoreographyMap } from './fight/choreography' import { createFactories } from './fight/choreography/factories' import { createMorphSystem } from './fight/morphs' import { createEntranceSystem } from './fight/entrances' import { createRoundSystem } from './fight/rounds' import { createFinisherSystem } from './fight/finishers' import { sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit, sfxJetpack, sfxExplosion, sfxBlock, sfxDodge, sfxClash, sfxRandomSilly, sfxBoing, sfxBonk, sfxZap, sfxSlideDown, sfxSlideUp, sfxZoomWhoosh, sfxRapidPunch, sfxPowerUp, sfxCoin, sfxRandomComedy, startMusic, stopMusic, announceCool, } from './audio' export function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string { // THE CREATOR: special move selection if (attackerArchetype === 'the_creator') { const ultimateChance = isCritical ? 1.0 : CREATOR_ULTIMATE_CHANCE if (Math.random() < ultimateChance) { // Creator gets ALL ultimates + their 4 exclusive ones const all: string[] = [...CREATOR_ULTIMATES] for (let t = 2; t <= 5; t++) { if (TIER_ULTIMATES[t]) all.push(...TIER_ULTIMATES[t]) } return all[Math.floor(Math.random() * all.length)] } if (Math.random() < CREATOR_THEMED_MOVE_RATIO) { return CREATOR_MOVES[Math.floor(Math.random() * CREATOR_MOVES.length)] } // Fall through to normal selection } // Tier-gated ultimates: higher tier = higher chance of spectacular ultimate moves if (attackerTier >= 2) { const ultimateChance = TIER_ULTIMATE_CHANCES[attackerTier] ?? TIER_ULTIMATE_CHANCES[5] // On crits, double the chance const roll = isCritical ? ultimateChance * 2 : ultimateChance if (Math.random() < roll) { // Collect all ultimates available at this tier (cumulative — higher tiers get lower tier moves too) const available: string[] = [] for (let t = 2; t <= attackerTier; t++) { if (TIER_ULTIMATES[t]) available.push(...TIER_ULTIMATES[t]) } if (available.length > 0) { return available[Math.floor(Math.random() * available.length)] } } } // Critical hits always get BIG moves (but can still be themed) if (isCritical) { const entry = CHALLENGE_THEMED[challengeType] if (entry && Math.random() < CRIT_THEMED_RATIO) { return entry.themed[Math.floor(Math.random() * entry.themed.length)] } const critMoves = [ 'groundPound', 'jetpackDive', 'gunBurst', 'fullScreenDash', 'multiHit', 'fireBreath', 'lightningRush', 'zoomRush', 'rapidFlurry', 'rocketLauncher', 'laserBeam', 'katanaCombo', 'hammerSmash', 'sniperShot', 'minigunSpray', 'chainsawRev', 'carSmash', 'motorbikeCharge', 'anvilDrop', 'pocketCannon', 'bodySlam', 'suplex', 'pinballCombo', 'grappleFlurry', // New epic crits 'kamehameha', 'nukeStrike', 'tankRoll', 'helicopterStrike', 'chokeslam', 'tombstone', 'rko', 'piledriver', 'powerbomb', 'meteorStrike', 'spiritBomb', 'blackHole', 'alienAbduction', 'tsunamiWave', 'earthquakeStrike', 'dragonBreath', 'pianoDrop', 'fridgeDrop', 'volcanoErupt', 'warpDrive', 'dubstepCannon', 'tornadoSpin', 'trampolineBounce', 'watermelonBomb', 'nftRugPull', 'blueScreen', 'yeetThrow', 'bulletTimeAttack', ] return critMoves[Math.floor(Math.random() * critMoves.length)] } const entry = CHALLENGE_THEMED[challengeType] if (!entry) { const fallback = ['dashPunch', 'flyingKick', 'aerialSlam', 'backflipKick', 'fingerGuns', 'rubberChicken', 'pizzaSlam', 'fryingPan', 'snowballFight'] return fallback[Math.floor(Math.random() * fallback.length)] } const roll = Math.random() if (roll < CHOREOGRAPHY_THEMED_RATIO) { return entry.themed[Math.floor(Math.random() * entry.themed.length)] } else if (roll < CHOREOGRAPHY_GENERIC_CUTOFF) { return entry.generic[Math.floor(Math.random() * entry.generic.length)] } else { const wild = [ 'jetpackDive', 'gunBurst', 'groundPound', 'teleportStrike', 'fullScreenDash', 'fireBreath', 'golfClubSmash', 'zoomRush', 'rapidFlurry', 'swordSlash', 'hammerSmash', 'laserBeam', 'rocketLauncher', 'bombThrow', 'minigunSpray', 'sniperShot', 'whipCrack', 'katanaCombo', 'chainsawRev', 'motorbikeCharge', 'carSmash', 'boatCannon', 'anvilDrop', 'pocketCannon', 'grappleFlurry', 'bodySlam', 'pinballCombo', 'suplex', // New wild cards — food 'pizzaSlam', 'bananaFling', 'pieSmash', 'burgerToss', 'tomatoBarrage', 'fishSlap', // New wild cards — weapons 'fryingPan', 'baseballBat', 'guitarSmash', 'umbrellaWhack', 'plungerSlam', 'wrenchSmash', // New wild cards — drops 'pianoDrop', 'toiletDrop', 'tvDrop', 'chandelierDrop', 'micDrop', // New wild cards — vehicles 'shoppingCart', 'zamboniCrush', 'unicycleRun', 'tankRoll', // New wild cards — spins/flips 'tornadoSpin', 'backflipKick', 'corkscrewDive', 'barrelRoll', 'skateTrick', // New wild cards — beams 'kamehameha', 'freezeRay', 'heatVision', 'rainbowBeam', 'glitchBeam', // New wild cards — silly 'rubberChicken', 'selfieStrike', 'fingerGuns', 'yeetThrow', 'emojiBarrage', 'touchGrass', // New wild cards — wrestling 'clothesline', 'rko', 'stunner', 'piledriver', // New wild cards — magic 'fireball', 'portalPunch', 'crystalShards', 'spiritBomb', // New wild cards — tech 'stackOverflow', 'blueScreen', 'aiUprising', 'popupSpam', // New wild cards — music 'bassDrop', 'dubstepCannon', 'vinylScratch', // New wild cards — nature 'avalanche', 'lavaSplash', 'tornadoFling', ] return wild[Math.floor(Math.random() * wild.length)] } } // Convert any CSS color string to a kaplay Color object safely function safeColor(k: ReturnType, color: string) { if (color.startsWith('#')) { try { return k.Color.fromHex(color) } catch { /* fall through */ } } // HSL / rgb / named color — render to a 1px canvas to extract RGB const cv = document.createElement('canvas') cv.width = 1; cv.height = 1 const cx = cv.getContext('2d')! cx.fillStyle = color cx.fillRect(0, 0, 1, 1) const [r, g, b] = cx.getImageData(0, 0, 1, 1).data return k.Color.fromArray([r, g, b]) } export async function createFightScene(config: FightSceneConfig) { const { canvas, botA, botB, arena } = config const theme = ARENA_THEMES[arena] || ARENA_THEMES.localhost const k = kaplay({ canvas, width: canvas.width || 800, height: canvas.height || 500, background: theme.bg, global: false, scale: 1, crisp: true, texFilter: 'nearest', }) // Wrap k.shake to support reduced motion + haptic feedback const _origShake = k.shake.bind(k) k.shake = ((intensity: number) => { if (intensity >= 3) haptic(Math.min(100, intensity * 8)) if (!isReducedMotion()) _origShake(intensity) }) as typeof k.shake const colorsA = getBotColors(botA.seed) const colorsB = getBotColors(botB.seed) let sheetA: string let sheetB: string // Human fighters use their profile human character, not a bot sprite const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0)) const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0)) try { sheetA = botA.archetype === 'human' ? generateHumanFighterSpriteSheet(botA.seed, botA.archetype, colorsA.primary, colorsA.secondary, winRateA) : generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization) } catch (err) { console.error('[FightScene] Failed to generate sprite for botA:', err) sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary) } try { sheetB = botB.archetype === 'human' ? generateHumanFighterSpriteSheet(botB.seed, botB.archetype, colorsB.primary, colorsB.secondary, winRateB) : generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization) } catch (err) { console.error('[FightScene] Failed to generate sprite for botB:', err) sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary) } const isHumanA = botA.archetype === 'human' const isHumanB = botB.archetype === 'human' // Helper: load sprite with a 5s timeout so mobile never hangs forever async function loadSpriteWithTimeout(name: string, src: string, opts: LoadSpriteOpt): Promise { try { await Promise.race([ k.loadSprite(name, src, opts), new Promise((_resolve, reject) => setTimeout(() => reject(new Error('timeout')), 5_000)), ]) } catch (err) { console.warn(`[FightScene] Sprite '${name}' failed/timed out:`, err) } } // Await sprite loading — timeout prevents mobile hangs await Promise.all([ loadSpriteWithTimeout('botA', sheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }), loadSpriteWithTimeout('botB', sheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }), ]) // Preload bot sprites for human fighters (used when humans morph into bots) if (isHumanA) { const botSheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary) loadSpriteWithTimeout('botA_morph', botSheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }) } if (isHumanB) { const botSheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary) loadSpriteWithTimeout('botB_morph', botSheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }) } // Judge sprite (red lobster referee) const judgeSheet = generateJudgeSpriteSheet() const judgeAnims = { idle: { from: 0, to: JUDGE_ANIMATIONS.idle.frames - 1, loop: true, speed: 4 }, call_left: { from: JUDGE_MAX_FRAMES, to: JUDGE_MAX_FRAMES + JUDGE_ANIMATIONS.call_left.frames - 1, loop: false, speed: 8 }, call_right: { from: JUDGE_MAX_FRAMES * 2, to: JUDGE_MAX_FRAMES * 2 + JUDGE_ANIMATIONS.call_right.frames - 1, loop: false, speed: 8 }, shocked: { from: JUDGE_MAX_FRAMES * 3, to: JUDGE_MAX_FRAMES * 3 + JUDGE_ANIMATIONS.shocked.frames - 1, loop: false, speed: 10 }, } await loadSpriteWithTimeout('judge', judgeSheet, { sliceX: JUDGE_MAX_FRAMES, sliceY: JUDGE_ROWS, anims: judgeAnims }) // Human sprites (the bot owner's silly human avatar for crowd) const humanSheetA = generateHumanSpriteSheet(botA.seed, botA.archetype || 'standard', colorsA.primary, colorsA.secondary, winRateA) const humanSheetB = generateHumanSpriteSheet(botB.seed, botB.archetype || 'standard', colorsB.primary, colorsB.secondary, winRateB) const humanAnims: Record = {} for (const [name, cfg] of Object.entries(HUMAN_ANIMATIONS)) { humanAnims[name] = { from: cfg.row * HUMAN_MAX_FRAMES, to: cfg.row * HUMAN_MAX_FRAMES + cfg.frames - 1, loop: true, speed: 6 } } await Promise.all([ loadSpriteWithTimeout('humanA', humanSheetA, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }), loadSpriteWithTimeout('humanB', humanSheetB, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }), ]) const W = k.width() const H = k.height() // Track all intervals/timeouts for cleanup on scene destroy const cleanupTimers = new Set>() function trackedTimeout(fn: () => void, ms: number): ReturnType { const id = setTimeout(() => { cleanupTimers.delete(id); fn() }, ms) cleanupTimers.add(id) return id } function trackedInterval(fn: () => void, ms: number): ReturnType { const id = setInterval(fn, ms) cleanupTimers.add(id) return id } function clearTracked(id: ReturnType) { clearInterval(id) clearTimeout(id) cleanupTimers.delete(id) } // Scale factor so sprites shrink on small canvases (reference: 500px tall) // Fight context for extracted modules const fightCtx: FightContext = { k, W, H, theme, trackedTimeout, trackedInterval, clearTracked, safeColor: (color: string) => safeColor(k, color), } // Wrappers for extracted particle/effect/projectile functions const spawnSparks = (x: number, y: number, count: number, color: string) => _spawnSparks(fightCtx, x, y, count, color) const spawnBulletHoles = (x: number, y: number, count: number) => _spawnBulletHoles(fightCtx, x, y, count) const spawnExhaust = (x: number, y: number, duration: number) => _spawnExhaust(fightCtx, x, y, duration) const spawnProjectile = (fromX: number, fromY: number, toX: number, toY: number, color: string, size: number = 8) => _spawnProjectile(fightCtx, fromX, fromY, toX, toY, color, size) const glitchRGB = (duration: number = 0.2) => _glitchRGB(fightCtx, duration) const scanlineGlitch = (duration: number = 0.3) => _scanlineGlitch(fightCtx, duration) const vhsTracking = (duration: number = 0.4) => _vhsTracking(fightCtx, duration) const dimensionalShift = (duration: number = 0.6) => _dimensionalShift(fightCtx, duration) const SF = Math.min(1, H / 500) const GROUND_Y = H * 0.78 const HOME_A = W * 0.28 const HOME_B = W * 0.72 function spawnBullet(fromX: number, fromY: number, toX: number, toY: number): Promise { return new Promise(resolve => { const bullet = k.add([ k.rect(6, 2), k.pos(fromX, fromY), k.color(safeColor(k,'#ffee00')), k.opacity(1), k.z(15), k.rotate(Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI), ]) const dur = 0.08 + Math.random() * 0.04 k.tween(0, 1, dur, (t) => { bullet.pos.x = fromX + (toX - fromX) * t bullet.pos.y = fromY + (toY - fromY) * t + (Math.random() - 0.5) * 4 }, k.easings.linear).then(() => { bullet.destroy() // Muzzle flash at impact spawnSparks(toX, toY, 3, '#ffcc00') resolve() }) }) } function spawnShockwave(x: number, y: number, color: string) { const wave = k.add([ k.circle(5), k.pos(x, y), k.color(safeColor(k,color)), k.opacity(0.7), k.z(5), k.scale(1), ]) k.tween(1, 15, 0.4, (v) => { wave.scaleTo(v, v * 0.3) }, k.easings.easeOutQuad) k.tween(0.7, 0, 0.4, (v) => { wave.opacity = v }, k.easings.easeOutQuad).then(() => wave.destroy()) } // === VISUAL CHAOS EFFECTS === // Schizo cut — rapid zoom/position jitter simulating jump cuts async function schizoCut() { const fA = k.get('fighterA')[0] as Fighter const fB = k.get('fighterB')[0] as Fighter if (!fA || !fB) return const origAX = fA.pos.x, origBX = fB.pos.x const origAY = fA.pos.y, origBY = fB.pos.y // 3-5 rapid cuts const cuts = 3 + Math.floor(Math.random() * 3) for (let i = 0; i < cuts; i++) { // Random offset both fighters fA.pos.x = origAX + (Math.random() - 0.5) * 30 fA.pos.y = origAY + (Math.random() - 0.5) * 15 fB.pos.x = origBX + (Math.random() - 0.5) * 30 fB.pos.y = origBY + (Math.random() - 0.5) * 15 screenFlash(Math.random() > 0.5 ? '#000000' : '#ffffff', 0.03) await k.wait(0.04 + Math.random() * 0.03) } fA.pos.x = origAX; fA.pos.y = origAY fB.pos.x = origBX; fB.pos.y = origBY } // Hyperspeed lines — converging toward a point function hyperSpeedLines(targetX: number, targetY: number, duration: number = 0.3) { const lineCount = isPerfMode() ? 6 : 16 const lines: GameObj[] = [] for (let i = 0; i < lineCount; i++) { const angle = (i / lineCount) * Math.PI * 2 const dist = 300 + Math.random() * 200 const sx = targetX + Math.cos(angle) * dist const sy = targetY + Math.sin(angle) * dist const len = 30 + Math.random() * 60 const line = k.add([ k.rect(len, 1.5), k.pos(sx, sy), k.color(safeColor(k,'#ffffff')), k.opacity(0.3), k.z(52), k.rotate(angle * 180 / Math.PI + 180), ]) line.onUpdate(() => { const dx = targetX - line.pos.x const dy = targetY - line.pos.y const d = Math.sqrt(dx * dx + dy * dy) if (d > 10) { line.pos.x += (dx / d) * 600 * k.dt() line.pos.y += (dy / d) * 600 * k.dt() } line.opacity -= 0.8 * k.dt() if (line.opacity <= 0 && line.exists()) line.destroy() }) lines.push(line) } trackedTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000) } // Flash the whole screen function screenFlash(color: string, duration: number = 0.1) { const flash = k.add([ k.rect(W, H), k.pos(0, 0), k.color(safeColor(k,color)), k.opacity(0.4), k.z(50), ]) k.tween(0.4, 0, duration, (v) => { flash.opacity = v }).then(() => flash.destroy()) } // Impact freeze-frame: brief pause with white flash on devastating crits async function impactFreeze(duration: number = 0.08) { const overlay = k.add([ k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#ffffff')), k.opacity(0.5), k.z(55), ]) // Kaplay has no built-in pause, so we just hold with a wait + overlay await k.wait(duration) k.tween(0.5, 0, 0.06, (v) => { overlay.opacity = v }).then(() => overlay.destroy()) } // Spawn afterimages: trailing ghost sprites behind a moving fighter function spawnAfterimages(fighter: Fighter, count: number = 3) { if (!fighter?.exists()) return const n = isPerfMode() ? Math.max(1, Math.round(count * 0.5)) : count for (let i = 0; i < n; i++) { const ghost = k.add([ k.rect(FRAME_SIZE * Math.abs(fighter.scale.x), FRAME_SIZE * Math.abs(fighter.scale.y)), k.pos(fighter.pos.x - i * 8 * Math.sign(fighter.scale.x), fighter.pos.y), k.color(safeColor(k, theme.accent)), k.opacity(0.3 - i * 0.08), k.z(fighter.z - 1), k.anchor('bot'), ]) k.tween(ghost.opacity, 0, 0.15 + i * 0.05, (v) => { ghost.opacity = v }).then(() => ghost.destroy()) } } // Camera zoom: scale fighters toward/away from a focal point for impact emphasis async function cameraZoom( fighters: Fighter[], savedScales: { x: number; y: number }[], zoomFactor: number, duration: number, easing: (t: number) => number = k.easings.easeOutQuad, ) { await Promise.all(fighters.map((f, i) => { if (!f?.exists()) return Promise.resolve() const sx = savedScales[i].x const sy = savedScales[i].y return k.tween(Math.abs(f.scale.y), Math.abs(sy) * zoomFactor, duration, (v) => { f.scale.x = sx > 0 ? v : -v f.scale.y = v }, easing) })) } // Grotesque close-up overlays removed — noops retained for call-site compatibility function spawnGrotesqueDetails(_fighter: Fighter, _scaleFactor: number) {} function destroyGrotesqueDetails() {} // Arena-specific background decoration function drawArenaDecor() { const a = arena const acc = theme.accent // Parallax stars/particles in the sky const starCount = isPerfMode() ? 15 : 40 for (let i = 0; i < starCount; i++) { const star = k.add([ k.rect(1 + Math.random() * 2, 1 + Math.random() * 2), k.pos(Math.random() * W, Math.random() * (GROUND_Y - 20)), k.color(safeColor(k, i % 5 === 0 ? '#ffffff' : acc)), k.opacity(0.08 + Math.random() * 0.2), k.z(1), ]) const speed = 0.5 + Math.random() * 1.5 const baseOp = star.opacity star.onUpdate(() => { star.opacity = baseOp + Math.sin(k.time() * speed + i) * 0.08 }) } if (a === 'datacenter' || a === 'localhost') { const perf = isPerfMode() // City skyline silhouette behind the racks const buildingHeights = perf ? [0.55, 0.65, 0.7, 0.6, 0.72, 0.58] : [0.55, 0.4, 0.65, 0.35, 0.7, 0.45, 0.6, 0.5, 0.72, 0.38, 0.58, 0.42] for (let i = 0; i < buildingHeights.length; i++) { const bw = W / buildingHeights.length + 4 const bh = GROUND_Y * buildingHeights[i] const bx = i * (W / buildingHeights.length) k.add([k.rect(bw, bh), k.pos(bx, GROUND_Y - bh), k.color(safeColor(k, '#060610')), k.opacity(0.5), k.z(0)]) const winStep = perf ? 14 : 7 for (let wy = 0; wy < bh - 10; wy += 8) { for (let wx = 4; wx < bw - 6; wx += winStep) { const win = k.add([ k.rect(3, 3), k.pos(bx + wx, GROUND_Y - bh + 6 + wy), k.color(safeColor(k, Math.random() > 0.4 ? '#223355' : '#ffcc44')), k.opacity(0.15 + Math.random() * 0.2), k.z(0), ]) if (!perf && Math.random() < 0.15) { const blink = 0.03 + Math.random() * 0.06 win.onUpdate(() => { win.opacity = Math.random() > blink ? win.opacity : (Math.random() > 0.5 ? 0.3 : 0.05) }) } } } } // Server rack wall const rackRows = perf ? 2 : 4 const rackCols = perf ? 5 : 10 for (let row = 0; row < rackRows; row++) { for (let col = 0; col < rackCols; col++) { const lx = 15 + col * (W / rackCols) const ly = 15 + row * 55 const rackW = W / rackCols - 8 const rackH = 48 k.add([k.rect(rackW, rackH), k.pos(lx, ly), k.color(safeColor(k, '#06060f')), k.opacity(0.6), k.z(1)]) k.add([k.rect(rackW, 1), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)]) k.add([k.rect(rackW, 1), k.pos(lx, ly + rackH), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)]) k.add([k.rect(1, rackH), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.3), k.z(1)]) for (let s = 0; s < 5; s++) { k.add([k.rect(rackW - 6, 1), k.pos(lx + 3, ly + 8 + s * 8), k.color(safeColor(k, '#111122')), k.opacity(0.5), k.z(1)]) } if (!perf) { for (let led = 0; led < 3; led++) { const ledEl = k.add([ k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4), k.color(safeColor(k, Math.random() > 0.3 ? '#00ff41' : '#ff2d2d')), k.opacity(0.7), k.z(2), ]) const blinkRate = 0.02 + Math.random() * 0.08 ledEl.onUpdate(() => { ledEl.opacity = Math.random() > blinkRate ? 0.7 : 0.15 }) } if (Math.random() < 0.4) { const actLed = k.add([ k.rect(2, 2), k.pos(lx + rackW - 8, ly + 4), k.color(safeColor(k, '#ffaa00')), k.opacity(0.6), k.z(2), ]) actLed.onUpdate(() => { actLed.opacity = Math.random() > 0.5 ? 0.7 : 0.2 }) } } } } // Cable bundles for (let c = 0; c < 6; c++) { const cy = 3 + c * 2 k.add([k.rect(W, 2), k.pos(0, cy), k.color(safeColor(k, c % 3 === 0 ? '#111133' : c % 3 === 1 ? '#0a0a22' : '#0f0f2a')), k.opacity(0.5), k.z(1)]) } // Scan line sweep const scanLine = k.add([k.rect(W, 1), k.pos(0, 0), k.color(safeColor(k, '#00f0ff')), k.opacity(0.06), k.z(3)]) scanLine.onUpdate(() => { scanLine.pos.y = (scanLine.pos.y + 40 * k.dt()) % GROUND_Y }) } else if (a === 'gpu_graveyard') { // Floating circuit board fragments const chipCount = isPerfMode() ? 6 : 16 for (let i = 0; i < chipCount; i++) { const cx = Math.random() * W const cy = 20 + Math.random() * (GROUND_Y - 60) const cw = 15 + Math.random() * 25 const ch = 10 + Math.random() * 18 const chip = k.add([ k.rect(cw, ch), k.pos(cx, cy), k.color(safeColor(k, Math.random() > 0.5 ? '#1a2a1a' : '#0f1a0f')), k.opacity(0.35), k.z(1), k.rotate(Math.random() * 45 - 22), ]) for (let t = 0; t < 3; t++) { k.add([k.rect(cw * 0.6, 1), k.pos(cx + 2, cy + 3 + t * 4), k.color(safeColor(k, '#76b900')), k.opacity(0.2), k.z(1)]) } k.add([k.circle(1.5), k.pos(cx + cw / 2, cy + ch / 2), k.color(safeColor(k, '#aabb44')), k.opacity(0.25), k.z(1)]) const baseY = cy chip.onUpdate(() => { chip.pos.y = baseY + Math.sin(k.time() * 0.3 + i) * 5 }) } // Dead GPU fans — some slowly spinning for (const fx of [W * 0.15, W * 0.5, W * 0.85]) { const fy = GROUND_Y - 30 - Math.random() * 40 k.add([k.circle(18), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.4), k.z(1)]) k.add([k.circle(5), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1)]) for (let b = 0; b < 4; b++) { const ba = b * Math.PI / 2 const blade = k.add([k.rect(14, 2), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.3), k.z(1), k.rotate(ba * 180 / Math.PI)]) if (fx === W * 0.5) { blade.onUpdate(() => { blade.angle += 8 * k.dt() }) } } } // Sparking wires for (let i = 0; i < 4; i++) { const sx = W * (0.2 + i * 0.2) const sy = 10 + Math.random() * 30 k.add([k.rect(1, 20 + Math.random() * 40), k.pos(sx, sy), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)]) const spark = k.add([k.circle(3), k.pos(sx, sy + 20), k.anchor('center'), k.color(safeColor(k, '#ffff00')), k.opacity(0), k.z(3)]) spark.onUpdate(() => { spark.opacity = Math.random() > 0.97 ? 0.6 : 0 }) } // Smoke wisps rising for (let i = 0; i < 6; i++) { const smoke = k.add([ k.circle(8 + Math.random() * 12), k.pos(Math.random() * W, GROUND_Y - 5), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.15), k.z(2), ]) const bx = smoke.pos.x smoke.onUpdate(() => { smoke.pos.y -= 6 * k.dt() smoke.pos.x = bx + Math.sin(k.time() + i) * 8 smoke.opacity *= 0.998 if (smoke.pos.y < 0) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.15 } }) } } else if (a === 'prompt_dungeon') { // Stone wall bricks for (let row = 0; row < 5; row++) { for (let col = 0; col < 12; col++) { const brickW = W / 12 + (Math.random() - 0.5) * 10 const brickH = GROUND_Y / 5 const bx = col * (W / 12) + (row % 2 === 0 ? 0 : W / 24) k.add([k.rect(brickW - 2, brickH - 2), k.pos(bx, row * brickH), k.color(safeColor(k, '#1a1520')), k.opacity(0.3), k.z(1)]) } } // Glowing runes with halos for (let i = 0; i < 10; i++) { const rx = 30 + Math.random() * (W - 60) const ry = 20 + Math.random() * (GROUND_Y - 60) const rune = k.add([ k.circle(6 + Math.random() * 8), k.pos(rx, ry), k.color(safeColor(k, '#b83dff')), k.opacity(0.12), k.z(1), ]) k.add([k.circle(12 + Math.random() * 8), k.pos(rx, ry), k.color(safeColor(k, '#6622aa')), k.opacity(0.06), k.z(0)]) rune.onUpdate(() => { rune.opacity = 0.08 + Math.sin(k.time() * 0.8 + i * 0.7) * 0.08 }) } // Torches with flickering flames and light cones for (const tx of [W * 0.1, W * 0.35, W * 0.65, W * 0.9]) { const ty = GROUND_Y * 0.3 k.add([k.rect(4, 20), k.pos(tx, ty), k.color(safeColor(k, '#553311')), k.opacity(0.5), k.z(1)]) const flame = k.add([k.circle(6), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.5), k.z(2)]) const flameGlow = k.add([k.circle(15), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.08), k.z(1)]) const lightCone = k.add([k.rect(30, GROUND_Y * 0.5), k.pos(tx - 13, ty + 20), k.color(safeColor(k, '#ff6600')), k.opacity(0.02), k.z(0)]) flame.onUpdate(() => { flame.opacity = 0.4 + Math.sin(k.time() * 6 + tx) * 0.15 flame.pos.y = ty - 3 + Math.sin(k.time() * 8 + tx) * 1.5 flameGlow.opacity = 0.05 + Math.sin(k.time() * 6 + tx) * 0.04 lightCone.opacity = 0.015 + Math.sin(k.time() * 6 + tx) * 0.008 }) } // Chains hanging from ceiling for (let i = 0; i < 5; i++) { const cx = W * (0.15 + i * 0.18) const chainLen = 25 + Math.random() * 50 for (let s = 0; s < chainLen; s += 5) { const link = k.add([k.rect(3, 4), k.pos(cx, s), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)]) const baseCx = cx link.onUpdate(() => { link.pos.x = baseCx + Math.sin(k.time() * 0.4 + i + s * 0.1) * 3 }) } } // Ground fog for (let i = 0; i < 8; i++) { const fog = k.add([ k.circle(35 + Math.random() * 30), k.pos(Math.random() * W, GROUND_Y - 8), k.color(safeColor(k, '#1f1a2a')), k.opacity(0.25), k.z(3), ]) const baseX = fog.pos.x fog.onUpdate(() => { fog.pos.x = baseX + Math.sin(k.time() * 0.2 + i) * 18 }) } // Dripping water for (let i = 0; i < 3; i++) { const dx = W * (0.25 + i * 0.25) const drop = k.add([k.circle(1.5), k.pos(dx, 0), k.color(safeColor(k, '#4466aa')), k.opacity(0.3), k.z(2)]) drop.onUpdate(() => { drop.pos.y += 50 * k.dt() if (drop.pos.y > GROUND_Y) { drop.pos.y = 0; drop.opacity = 0.3 } drop.opacity *= 0.997 }) } } else if (a === 'the_cloud') { // Layered cloud formations for (let layer = 0; layer < 3; layer++) { for (let i = 0; i < 4; i++) { const cx = Math.random() * W const cy = 20 + layer * 50 + Math.random() * 30 const cloudOp = 0.25 - layer * 0.05 for (let j = 0; j < 4 + Math.floor(Math.random() * 3); j++) { const cloud = k.add([ k.circle(15 + Math.random() * 20), k.pos(cx + j * 14 - 20, cy + (Math.random() - 0.5) * 12), k.color(safeColor(k, layer === 0 ? '#1a2030' : '#151a28')), k.opacity(cloudOp), k.z(1), ]) const bx = cloud.pos.x cloud.onUpdate(() => { cloud.pos.x = bx + Math.sin(k.time() * (0.08 + layer * 0.04) + i) * (8 + layer * 4) }) } } } // Data rain streams for (let i = 0; i < 15; i++) { const stream = k.add([ k.rect(1, 8 + Math.random() * 15), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k, '#4488ff')), k.opacity(0.12), k.z(2), ]) stream.onUpdate(() => { stream.pos.y += 30 * k.dt() if (stream.pos.y > GROUND_Y) { stream.pos.y = -20; stream.pos.x = Math.random() * W } }) } // Lightning flashes const lightning = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3)]) lightning.onUpdate(() => { lightning.opacity = Math.random() > 0.998 ? 0.06 : 0 }) // Rainbow arc (subtle) const rainbowColors = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff'] for (let i = 0; i < rainbowColors.length; i++) { k.add([ k.circle(120 + i * 6), k.pos(W * 0.3, GROUND_Y + 60), k.anchor('center'), k.color(safeColor(k, rainbowColors[i])), k.opacity(0.03), k.z(0), ]) } } else if (a === 'the_singularity') { // Central swirling vortex with multiple rings for (let ring = 0; ring < 4; ring++) { const ringDots = 12 + ring * 6 for (let i = 0; i < ringDots; i++) { const angle = (i / ringDots) * Math.PI * 2 const baseDist = 20 + ring * 30 const dot = k.add([ k.circle(1.5 + ring * 0.5), k.pos(W / 2, GROUND_Y * 0.4), k.color(safeColor(k, ring === 0 ? '#ffffff' : ring === 1 ? '#ff44ff' : ring === 2 ? '#ff00ff' : '#aa00aa')), k.opacity(0.3 - ring * 0.05), k.z(1), ]) dot.onUpdate(() => { const a2 = angle + k.time() * (0.8 - ring * 0.15) const d2 = baseDist + Math.sin(k.time() * 0.8 + i) * 12 dot.pos.x = W / 2 + Math.cos(a2) * d2 dot.pos.y = GROUND_Y * 0.4 + Math.sin(a2) * d2 * 0.5 }) } } // Central core with pulsing glow const core = k.add([k.circle(12), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.15), k.z(1)]) k.add([k.circle(30), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0)]) k.add([k.circle(60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#880088')), k.opacity(0.03), k.z(0)]) core.onUpdate(() => { core.opacity = 0.1 + Math.sin(k.time() * 2) * 0.08 }) // Energy tendrils reaching outward for (let i = 0; i < 8; i++) { const tendril = k.add([ k.rect(2, 40 + Math.random() * 60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('top'), k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0), k.rotate(i * 45), ]) tendril.onUpdate(() => { tendril.angle = i * 45 + k.time() * 15 }) } // Warped space grid lines for (let i = 0; i < 6; i++) { const gridLine = k.add([ k.rect(W, 1), k.pos(0, GROUND_Y * (0.15 + i * 0.12)), k.color(safeColor(k, '#440066')), k.opacity(0.08), k.z(0), ]) gridLine.onUpdate(() => { gridLine.pos.x = Math.sin(k.time() * 0.3 + i) * 10 - 5 }) } } else if (a === 'silicon_valley_dojo') { // Bamboo stalks with segments and leaves for (const side of [0.04, 0.08, 0.12, 0.86, 0.91, 0.96]) { const px = W * side const segments = 5 + Math.floor(Math.random() * 3) const segH = (GROUND_Y - 15) / segments for (let s = 0; s < segments; s++) { k.add([k.rect(5, segH - 2), k.pos(px, 15 + s * segH), k.color(safeColor(k, '#1a3a1a')), k.opacity(0.45), k.z(1)]) k.add([k.rect(7, 2), k.pos(px - 1, 15 + s * segH), k.color(safeColor(k, '#2a4a2a')), k.opacity(0.4), k.z(1)]) } for (let l = 0; l < 3; l++) { const ly = 20 + l * (GROUND_Y / 4) const dir = Math.random() > 0.5 ? 1 : -1 const leaf = k.add([k.rect(18, 3), k.pos(px + dir * 4, ly), k.color(safeColor(k, '#22aa33')), k.opacity(0.2), k.z(1), k.rotate(dir * 25)]) leaf.onUpdate(() => { leaf.angle = dir * 25 + Math.sin(k.time() * 1.5 + l) * 3 }) k.add([k.rect(14, 2), k.pos(px + dir * 8, ly + 4), k.color(safeColor(k, '#1a8822')), k.opacity(0.15), k.z(1), k.rotate(dir * 35)]) } } // Zen garden raked lines for (let i = 0; i < 8; i++) { const gy = GROUND_Y + 5 + i * 8 k.add([k.rect(W * 0.6, 1), k.pos(W * 0.2, gy), k.color(safeColor(k, '#00ff41')), k.opacity(0.06), k.z(0)]) } // Stone lanterns with warm glow for (const lx of [W * 0.2, W * 0.8]) { k.add([k.rect(10, 6), k.pos(lx, GROUND_Y - 20), k.color(safeColor(k, '#444444')), k.opacity(0.35), k.z(1)]) k.add([k.rect(6, 14), k.pos(lx + 2, GROUND_Y - 34), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)]) k.add([k.rect(12, 3), k.pos(lx - 1, GROUND_Y - 37), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)]) const glow = k.add([k.circle(8), k.pos(lx + 5, GROUND_Y - 27), k.anchor('center'), k.color(safeColor(k, '#ffaa33')), k.opacity(0.15), k.z(2)]) glow.onUpdate(() => { glow.opacity = 0.1 + Math.sin(k.time() * 2 + lx) * 0.06 }) } // Cherry blossom petals falling for (let i = 0; i < 12; i++) { const petal = k.add([ k.circle(1.5 + Math.random()), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k, Math.random() > 0.5 ? '#ffaacc' : '#ff88aa')), k.opacity(0.2), k.z(3), ]) const startX = petal.pos.x petal.onUpdate(() => { petal.pos.y += 8 * k.dt() petal.pos.x = startX + Math.sin(k.time() * 1.2 + i) * 15 if (petal.pos.y > GROUND_Y) { petal.pos.y = -5 } }) } } else if (a === 'hacker_news' || a === 'stackoverflow_ruins') { // Ruined building walls with cracks for (let i = 0; i < 3; i++) { const wallX = W * (0.15 + i * 0.3) const wallH = GROUND_Y * (0.4 + Math.random() * 0.3) k.add([k.rect(W * 0.2, wallH), k.pos(wallX, GROUND_Y - wallH), k.color(safeColor(k, '#1a1408')), k.opacity(0.3), k.z(0)]) for (let c = 0; c < 3; c++) { k.add([k.rect(1, 10 + Math.random() * 15), k.pos(wallX + Math.random() * W * 0.18, GROUND_Y - wallH + Math.random() * wallH), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0), k.rotate(Math.random() * 30 - 15)]) } } // Code blocks floating for (let i = 0; i < 14; i++) { const bw = 25 + Math.random() * 60 const bh = 3 + Math.random() * 4 const bx = 15 + Math.random() * (W - 50) const by = 15 + Math.random() * (GROUND_Y - 50) k.add([k.rect(bw, bh), k.pos(bx, by), k.color(safeColor(k, acc)), k.opacity(0.06 + Math.random() * 0.06), k.z(1)]) if (Math.random() < 0.4) { k.add([k.rect(2, bh), k.pos(bx - 4, by), k.color(safeColor(k, acc)), k.opacity(0.1), k.z(1)]) } } // Vote arrows for (let i = 0; i < 5; i++) { const ax = 20 + Math.random() * (W - 40) const ay = 30 + Math.random() * (GROUND_Y - 70) k.add([k.rect(4, 6), k.pos(ax, ay), k.color(safeColor(k, a === 'stackoverflow_ruins' ? '#f48024' : '#ff6600')), k.opacity(0.12), k.z(1)]) } // Broken monitors with static for (let i = 0; i < 3; i++) { const mx = W * (0.2 + i * 0.3) const my = GROUND_Y * 0.3 + Math.random() * 30 k.add([k.rect(22, 16), k.pos(mx, my), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.5), k.z(1)]) k.add([k.rect(20, 14), k.pos(mx + 1, my + 1), k.color(safeColor(k, '#111111')), k.opacity(0.4), k.z(1)]) const staticEl = k.add([k.rect(18, 12), k.pos(mx + 2, my + 2), k.color(safeColor(k, '#888888')), k.opacity(0), k.z(2)]) staticEl.onUpdate(() => { staticEl.opacity = Math.random() > 0.92 ? 0.08 : 0 }) } // Scrolling text fragments for (let i = 0; i < 4; i++) { const txt = k.add([ k.rect(30 + Math.random() * 40, 2), k.pos(Math.random() * W, 20 + i * (GROUND_Y / 5)), k.color(safeColor(k, acc)), k.opacity(0.08), k.z(2), ]) txt.onUpdate(() => { txt.pos.x -= 12 * k.dt() if (txt.pos.x < -60) txt.pos.x = W + 20 }) } } else if (a === 'beach') { // Sun with rays and glow const sunX = W * 0.8, sunY = 35 k.add([k.circle(45), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.06), k.z(0)]) k.add([k.circle(30), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.1), k.z(0)]) const sun = k.add([k.circle(18), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.5), k.z(1)]) sun.onUpdate(() => { sun.opacity = 0.4 + Math.sin(k.time() * 0.5) * 0.1 }) for (let r = 0; r < 8; r++) { const angle = r * Math.PI / 4 const ray = k.add([k.rect(50, 2), k.pos(sunX, sunY), k.anchor('left'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.08), k.z(0), k.rotate(angle * 180 / Math.PI)]) ray.onUpdate(() => { ray.opacity = 0.05 + Math.sin(k.time() * 0.8 + r) * 0.04 }) } // Ocean with layered waves k.add([k.rect(W, 25), k.pos(0, GROUND_Y - 35), k.color(safeColor(k, '#1a4488')), k.opacity(0.2), k.z(1)]) for (let layer = 0; layer < 4; layer++) { for (let i = 0; i < 5; i++) { const wave = k.add([ k.rect(W * 0.3, 3 + layer), k.pos(Math.random() * W, GROUND_Y - 22 - layer * 6 - i * 2), k.color(safeColor(k, layer < 2 ? '#3377bb' : '#2266aa')), k.opacity(0.18 - layer * 0.03), k.z(2), ]) const baseX = wave.pos.x wave.onUpdate(() => { wave.pos.x = baseX + Math.sin(k.time() * (0.6 + layer * 0.2) + i * 1.5) * (15 + layer * 5) }) } } // Surf foam at shore for (let i = 0; i < 6; i++) { const foam = k.add([ k.rect(15 + Math.random() * 25, 2), k.pos(Math.random() * W, GROUND_Y - 6 + Math.random() * 4), k.color(safeColor(k, '#ffffff')), k.opacity(0.12), k.z(3), ]) const baseX = foam.pos.x foam.onUpdate(() => { foam.pos.x = baseX + Math.sin(k.time() * 0.8 + i) * 8; foam.opacity = 0.06 + Math.sin(k.time() + i) * 0.06 }) } // Palm trees with segmented trunks, coconuts, swaying fronds for (const px of [W * 0.05, W * 0.14, W * 0.88, W * 0.95]) { const lean = px < W / 2 ? 8 : -8 const height = 8 + Math.floor(Math.random() * 3) for (let s = 0; s < height; s++) { const sy = GROUND_Y - 10 - s * 12 const segW = 7 - s * 0.4 k.add([k.rect(segW, 13), k.pos(px + s * lean / height, sy), k.color(safeColor(k, s % 2 === 0 ? '#3a2510' : '#2f1f0d')), k.opacity(0.5), k.z(1)]) k.add([k.rect(segW + 1, 1), k.pos(px + s * lean / height - 0.5, sy + 6), k.color(safeColor(k, '#4a3520')), k.opacity(0.2), k.z(1)]) } const topX = px + lean, topY = GROUND_Y - 10 - height * 12 for (let c = 0; c < 2 + Math.floor(Math.random() * 2); c++) { k.add([k.circle(2.5), k.pos(topX + (c - 1) * 4, topY + 8), k.anchor('center'), k.color(safeColor(k, '#553311')), k.opacity(0.4), k.z(1)]) } for (let f = 0; f < 7; f++) { const angle = -70 + f * 22 const frond = k.add([k.rect(35 + Math.random() * 10, 3), k.pos(topX, topY), k.anchor('left'), k.color(safeColor(k, f % 2 === 0 ? '#1a6600' : '#228800')), k.opacity(0.35), k.z(1), k.rotate(angle)]) frond.onUpdate(() => { frond.angle = angle + Math.sin(k.time() * 0.8 + f) * 3 }) } } // Seagulls drifting for (let i = 0; i < 4; i++) { const bx = W * 0.2 + Math.random() * W * 0.5 const by = 12 + Math.random() * 25 const wing1 = k.add([k.rect(6, 1), k.pos(bx, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(-15)]) const wing2 = k.add([k.rect(6, 1), k.pos(bx + 5, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(15)]) const startBx = bx wing1.onUpdate(() => { wing1.pos.x = startBx + Math.sin(k.time() * 0.2 + i) * 30; wing2.pos.x = wing1.pos.x + 5 }) } // Beach umbrella const umbX = W * 0.35 k.add([k.rect(2, 30), k.pos(umbX, GROUND_Y - 30), k.color(safeColor(k, '#aa8844')), k.opacity(0.4), k.z(1)]) k.add([k.circle(16), k.pos(umbX + 1, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k, '#ff4444')), k.opacity(0.2), k.z(1)]) k.add([k.circle(14), k.pos(umbX + 1, GROUND_Y - 31), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.08), k.z(1)]) } else if (a === 'desert') { // Layered sand dunes for (let d = 0; d < 5; d++) { const dx = W * (0.1 + d * 0.2) + (Math.random() - 0.5) * 40 const dr = 50 + d * 15 + Math.random() * 20 k.add([k.circle(dr), k.pos(dx, GROUND_Y + dr * 0.6), k.anchor('center'), k.color(safeColor(k, '#aa8844')), k.opacity(0.15 + d * 0.03), k.z(1)]) } // Road with dashed center line k.add([k.rect(W * 0.25, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#222222')), k.opacity(0.15), k.z(0)]) k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)]) k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.63, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)]) for (let d = 0; d < 8; d++) { k.add([k.rect(2, 8), k.pos(W * 0.505, GROUND_Y * 0.32 + d * 16), k.color(safeColor(k, '#ffff44')), k.opacity(0.1), k.z(0)]) } // Cacti with arms and flowers for (const cx of [W * 0.08, W * 0.25, W * 0.75, W * 0.9]) { const ch = 35 + Math.random() * 20 k.add([k.rect(7, ch), k.pos(cx, GROUND_Y - ch), k.color(safeColor(k, '#226622')), k.opacity(0.45), k.z(1)]) const armY = GROUND_Y - ch * 0.6 k.add([k.rect(12, 5), k.pos(cx - 12, armY), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)]) k.add([k.rect(5, 15), k.pos(cx - 12, armY - 15), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)]) if (Math.random() > 0.3) { const armY2 = GROUND_Y - ch * 0.4 k.add([k.rect(10, 5), k.pos(cx + 7, armY2), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)]) k.add([k.rect(5, 12), k.pos(cx + 12, armY2 - 12), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)]) } if (Math.random() > 0.5) { k.add([k.circle(2.5), k.pos(cx + 3, GROUND_Y - ch - 2), k.anchor('center'), k.color(safeColor(k, '#ff4488')), k.opacity(0.3), k.z(2)]) } } // Tumbleweeds rolling for (let i = 0; i < 3; i++) { const tumble = k.add([k.circle(6 + Math.random() * 4), k.pos(-20 - i * 100, GROUND_Y - 8 - Math.random() * 5), k.color(safeColor(k, '#886644')), k.opacity(0.3), k.z(2)]) const speed = 15 + Math.random() * 15 tumble.onUpdate(() => { tumble.pos.x = ((tumble.pos.x + speed * k.dt()) % (W + 60)) - 30 }) } // Heat shimmer for (let i = 0; i < 4; i++) { const shimmer = k.add([k.rect(W, 1), k.pos(0, GROUND_Y - 3 - i * 3), k.color(safeColor(k, '#ff8800')), k.opacity(0.06), k.z(2)]) shimmer.onUpdate(() => { shimmer.opacity = 0.03 + Math.sin(k.time() * 3 + i * 2) * 0.03 }) } // Vulture silhouettes circling for (let i = 0; i < 2; i++) { const vx = W * 0.4 + i * W * 0.2 const vy = 25 + i * 15 const v1 = k.add([k.rect(8, 1), k.pos(vx, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(-10)]) const v2 = k.add([k.rect(8, 1), k.pos(vx + 7, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(10)]) const startVx = vx v1.onUpdate(() => { const cx = startVx + Math.cos(k.time() * 0.15 + i) * 40 const cy = vy + Math.sin(k.time() * 0.15 + i) * 15 v1.pos.x = cx; v1.pos.y = cy; v2.pos.x = cx + 7; v2.pos.y = cy }) } } else if (a === 'forest') { // Layered trees (3 depth layers) for (let layer = 0; layer < 3; layer++) { const treeCount = layer === 0 ? 14 : layer === 1 ? 8 : 5 const treeOp = layer === 0 ? 0.15 : layer === 1 ? 0.3 : 0.4 for (let i = 0; i < treeCount; i++) { const tx = 10 + (i / treeCount) * (W - 20) + (Math.random() - 0.5) * 15 const th = (25 + Math.random() * 35) * (layer === 0 ? 0.6 : layer === 1 ? 0.85 : 1) const trunkW = layer === 0 ? 4 : layer === 1 ? 6 : 8 k.add([k.rect(trunkW, th), k.pos(tx, GROUND_Y - th), k.color(safeColor(k, layer === 0 ? '#0f1a08' : layer === 1 ? '#152010' : '#1a2a10')), k.opacity(treeOp), k.z(layer)]) for (let c = 0; c < 3 + layer; c++) { const cr = (10 + Math.random() * 14) * (layer === 0 ? 0.6 : layer === 1 ? 0.8 : 1) k.add([ k.circle(cr), k.pos(tx + trunkW / 2 + (Math.random() - 0.5) * 10, GROUND_Y - th - cr * 0.5 + c * 4), k.anchor('center'), k.color(safeColor(k, layer === 0 ? '#0a1a08' : layer === 1 ? '#122a0e' : '#1a3a10')), k.opacity(treeOp * 0.8), k.z(layer), ]) } } } // Undergrowth for (let i = 0; i < 10; i++) { k.add([k.circle(6 + Math.random() * 8), k.pos(Math.random() * W, GROUND_Y - 3), k.anchor('bot'), k.color(safeColor(k, '#1a3a10')), k.opacity(0.3), k.z(3)]) } // Mushrooms for (let i = 0; i < 5; i++) { const mx = Math.random() * W k.add([k.rect(2, 5), k.pos(mx, GROUND_Y - 5), k.color(safeColor(k, '#aa8866')), k.opacity(0.25), k.z(3)]) k.add([k.circle(4), k.pos(mx + 1, GROUND_Y - 6), k.anchor('center'), k.color(safeColor(k, Math.random() > 0.5 ? '#cc3333' : '#ffaa22')), k.opacity(0.2), k.z(3)]) } // Fireflies for (let i = 0; i < 15; i++) { const ff = k.add([ k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 50)), k.color(safeColor(k, '#ccff44')), k.opacity(0), k.z(3), ]) const bx = ff.pos.x, by = ff.pos.y ff.onUpdate(() => { ff.opacity = Math.max(0, Math.sin(k.time() * 1.5 + i * 1.8) * 0.5) ff.pos.x = bx + Math.sin(k.time() * 0.5 + i) * 15 ff.pos.y = by + Math.cos(k.time() * 0.4 + i) * 10 }) } // Light rays through canopy for (let i = 0; i < 5; i++) { const rx = W * (0.15 + i * 0.18) + (Math.random() - 0.5) * 20 const ray = k.add([k.rect(8, GROUND_Y * 0.7), k.pos(rx, 0), k.color(safeColor(k, '#aaff44')), k.opacity(0.03), k.z(2), k.rotate(3 - Math.random() * 6)]) ray.onUpdate(() => { ray.opacity = 0.02 + Math.sin(k.time() * 0.3 + i) * 0.015 }) } // Fallen log k.add([k.rect(45, 6), k.pos(W * 0.55, GROUND_Y - 4), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(3), k.rotate(3)]) } else if (a === 'jungle') { // Dense vine canopy for (let i = 0; i < 12; i++) { const vx = 15 + i * (W / 12) + (Math.random() - 0.5) * 12 const vineLen = 30 + Math.random() * 90 for (let s = 0; s < vineLen; s += 4) { const vineEl = k.add([k.rect(2, 5), k.pos(vx + Math.sin(s * 0.15) * 4, s), k.color(safeColor(k, '#1a4a10')), k.opacity(0.3), k.z(1)]) if (Math.random() < 0.08) { const basePx = vineEl.pos.x vineEl.onUpdate(() => { vineEl.pos.x = basePx + Math.sin(k.time() * 0.5 + s) * 2 }) } } for (let l = 0; l < 2; l++) { k.add([k.circle(5 + Math.random() * 4), k.pos(vx + (Math.random() - 0.5) * 8, vineLen + l * 6), k.color(safeColor(k, '#22aa22')), k.opacity(0.22), k.z(1)]) } } // Large tropical leaves at edges for (const side of [0, W - 30]) { for (let l = 0; l < 4; l++) { const ly = 15 + l * (GROUND_Y / 5) const leaf = k.add([k.rect(35, 8), k.pos(side, ly), k.color(safeColor(k, '#1a5510')), k.opacity(0.25), k.z(3), k.rotate(side === 0 ? 15 : -15)]) leaf.onUpdate(() => { leaf.angle = (side === 0 ? 15 : -15) + Math.sin(k.time() * 0.6 + l) * 2 }) } } // Exotic flowers for (let i = 0; i < 6; i++) { const fx = Math.random() * W const fy = 20 + Math.random() * (GROUND_Y - 40) k.add([k.circle(3), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, ['#ff4488', '#ff8844', '#ffcc00', '#ff44ff', '#44ffcc', '#ff2222'][i])), k.opacity(0.2), k.z(2)]) } // Ancient stone ruins peeking through const ruinX = W * 0.5 k.add([k.rect(8, 40), k.pos(ruinX, GROUND_Y - 38), k.color(safeColor(k, '#444444')), k.opacity(0.15), k.z(0)]) k.add([k.rect(8, 35), k.pos(ruinX + 25, GROUND_Y - 33), k.color(safeColor(k, '#444444')), k.opacity(0.12), k.z(0)]) k.add([k.rect(35, 5), k.pos(ruinX - 2, GROUND_Y - 40), k.color(safeColor(k, '#444444')), k.opacity(0.1), k.z(0)]) // Thick ground fog for (let i = 0; i < 10; i++) { const mist = k.add([ k.circle(40 + Math.random() * 35), k.pos(Math.random() * W, GROUND_Y - 12), k.color(safeColor(k, '#1a3818')), k.opacity(0.22), k.z(3), ]) const bx = mist.pos.x mist.onUpdate(() => { mist.pos.x = bx + Math.sin(k.time() * 0.12 + i) * 18 }) } // Butterfly particles for (let i = 0; i < 4; i++) { const bf = k.add([ k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 60)), k.color(safeColor(k, ['#4488ff', '#ff8844', '#ffcc00', '#ff44ff'][i])), k.opacity(0.25), k.z(3), ]) const bfx = bf.pos.x, bfy = bf.pos.y bf.onUpdate(() => { bf.pos.x = bfx + Math.sin(k.time() * 1.2 + i * 3) * 30 bf.pos.y = bfy + Math.cos(k.time() * 0.8 + i * 2) * 20 }) } } else if (a === 'outer_space') { // Dense star field for (let i = 0; i < 100; i++) { const star = k.add([ k.circle(0.5 + Math.random() * 1.5), k.pos(Math.random() * W, Math.random() * H), k.color(safeColor(k, i % 8 === 0 ? '#ffccaa' : i % 5 === 0 ? '#aaaaff' : '#ffffff')), k.opacity(0.15 + Math.random() * 0.4), k.z(1), ]) star.onUpdate(() => { star.opacity = 0.12 + Math.sin(k.time() * (0.8 + Math.random()) + i) * 0.15 }) } // Nebula clouds const nebulaColors = ['#4400aa', '#aa0066', '#0044aa', '#006644', '#660044'] for (let i = 0; i < 5; i++) { for (let blob = 0; blob < 4; blob++) { k.add([ k.circle(30 + Math.random() * 50), k.pos(W * (0.1 + i * 0.2) + blob * 18, H * 0.25 + Math.random() * 60 + blob * 12), k.color(safeColor(k, nebulaColors[i % nebulaColors.length])), k.opacity(0.04 + blob * 0.012), k.z(0), ]) } } // Floating asteroids for (let i = 0; i < 6; i++) { const ast = k.add([ k.circle(4 + Math.random() * 10), k.pos(Math.random() * W, 15 + Math.random() * (GROUND_Y - 30)), k.color(safeColor(k, Math.random() > 0.5 ? '#555555' : '#3a3a3a')), k.opacity(0.35), k.z(1), ]) k.add([k.circle(2), k.pos(ast.pos.x + 2, ast.pos.y - 1), k.color(safeColor(k, '#2a2a2a')), k.opacity(0.2), k.z(1)]) const baseX = ast.pos.x, baseY = ast.pos.y ast.onUpdate(() => { ast.pos.x = baseX + Math.sin(k.time() * 0.15 + i * 2) * 18 ast.pos.y = baseY + Math.cos(k.time() * 0.12 + i) * 12 }) } // Distant planet with rings const planetX = W * 0.75, planetY = GROUND_Y * 0.25 k.add([k.circle(22), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#334466')), k.opacity(0.25), k.z(0)]) k.add([k.circle(24), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#223344')), k.opacity(0.1), k.z(0)]) for (let r = 0; r < 3; r++) { k.add([k.rect(50 + r * 8, 2), k.pos(planetX - 25 - r * 4, planetY - 1 + r), k.color(safeColor(k, '#556688')), k.opacity(0.1 - r * 0.02), k.z(0), k.rotate(15)]) } // Shooting stars for (let i = 0; i < 2; i++) { const shootStar = k.add([ k.rect(12, 1), k.pos(-20, Math.random() * GROUND_Y * 0.5), k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(2), k.rotate(30), ]) let timer = Math.random() * 10 shootStar.onUpdate(() => { timer -= k.dt() if (timer <= 0) { shootStar.pos.x = -20 shootStar.pos.y = Math.random() * GROUND_Y * 0.4 shootStar.opacity = 0.5 timer = 5 + Math.random() * 10 } if (shootStar.opacity > 0) { shootStar.pos.x += 200 * k.dt() shootStar.pos.y += 80 * k.dt() shootStar.opacity -= 0.8 * k.dt() } }) } } else if (a === 'colosseum') { // Roman arches along the back for (let i = 0; i < 8; i++) { const ax = 20 + i * (W / 8) const aw = W / 8 - 8 k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)]) k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax + aw - 6, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)]) k.add([k.rect(aw, 6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#4a3a20')), k.opacity(0.4), k.z(1)]) k.add([k.rect(aw - 12, GROUND_Y * 0.5), k.pos(ax + 6, GROUND_Y * 0.25), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0)]) k.add([k.rect(10, 3), k.pos(ax - 2, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)]) k.add([k.rect(10, 3), k.pos(ax + aw - 10, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)]) } // Torch brackets on pillars for (let i = 0; i < 4; i++) { const tx = W * (0.15 + i * 0.25) const ty = GROUND_Y * 0.35 k.add([k.rect(3, 12), k.pos(tx, ty), k.color(safeColor(k, '#554422')), k.opacity(0.4), k.z(2)]) const flame = k.add([k.circle(5), k.pos(tx + 1, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.4), k.z(2)]) flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + i * 2) * 0.15 }) } // Crowd silhouettes in upper tiers for (let row = 0; row < 2; row++) { for (let i = 0; i < 20; i++) { k.add([ k.circle(2 + Math.random()), k.pos(15 + i * (W / 20), GROUND_Y * 0.08 + row * 12), k.color(safeColor(k, '#2a1a10')), k.opacity(0.2), k.z(0), ]) } } // Crumbled stone debris for (let i = 0; i < 8; i++) { k.add([ k.rect(5 + Math.random() * 10, 3 + Math.random() * 6), k.pos(Math.random() * W, GROUND_Y - 5 - Math.random() * 10), k.color(safeColor(k, '#5a4a30')), k.opacity(0.25), k.z(2), k.rotate(Math.random() * 40 - 20), ]) } // Sand dust particles for (let i = 0; i < 6; i++) { const dust = k.add([ k.circle(2 + Math.random() * 3), k.pos(Math.random() * W, GROUND_Y - 15 - Math.random() * 20), k.color(safeColor(k, '#c0a060')), k.opacity(0.1), k.z(3), ]) const dx = dust.pos.x dust.onUpdate(() => { dust.pos.x = dx + Math.sin(k.time() * 0.5 + i) * 12; dust.opacity = 0.05 + Math.sin(k.time() + i) * 0.05 }) } } else if (a === 'haunted_mansion') { // Gothic windows with moonlight for (let i = 0; i < 5; i++) { const wx = W * (0.1 + i * 0.2) const wy = GROUND_Y * 0.2 k.add([k.rect(20, 35), k.pos(wx, wy), k.color(safeColor(k, '#0a0510')), k.opacity(0.4), k.z(1)]) k.add([k.rect(22, 1), k.pos(wx - 1, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)]) k.add([k.rect(22, 1), k.pos(wx - 1, wy + 35), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)]) k.add([k.rect(1, 35), k.pos(wx + 10, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)]) k.add([k.rect(20, 1), k.pos(wx, wy + 17), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)]) const glow = k.add([k.rect(16, 30), k.pos(wx + 2, wy + 3), k.color(safeColor(k, '#8844cc')), k.opacity(0.06), k.z(1)]) glow.onUpdate(() => { glow.opacity = 0.04 + Math.sin(k.time() * 0.5 + i) * 0.03 }) } // Moon behind a window k.add([k.circle(12), k.pos(W * 0.5 + 10, GROUND_Y * 0.26), k.anchor('center'), k.color(safeColor(k, '#ccccdd')), k.opacity(0.15), k.z(0)]) // Cobwebs for (const cx of [15, W - 35]) { for (let strand = 0; strand < 5; strand++) { k.add([k.rect(20 + strand * 5, 1), k.pos(cx, 3 + strand * 4), k.color(safeColor(k, '#666666')), k.opacity(0.1), k.z(1), k.rotate(cx < W / 2 ? 28 + strand * 5 : -28 - strand * 5)]) } } // Candelabras for (const cx of [W * 0.25, W * 0.75]) { k.add([k.rect(2, 20), k.pos(cx, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)]) k.add([k.rect(12, 2), k.pos(cx - 5, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)]) for (let c = 0; c < 3; c++) { const candleX = cx - 5 + c * 5 const cFlame = k.add([k.circle(2.5), k.pos(candleX + 1, GROUND_Y - 23), k.anchor('center'), k.color(safeColor(k, '#ffaa44')), k.opacity(0.4), k.z(2)]) cFlame.onUpdate(() => { cFlame.opacity = 0.3 + Math.sin(k.time() * 4 + c + cx) * 0.15 }) } } // Floating ghost particles for (let i = 0; i < 8; i++) { const ghost = k.add([ k.circle(3 + Math.random() * 3), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k, '#aabbcc')), k.opacity(0), k.z(2), ]) const gx = ghost.pos.x, gy = ghost.pos.y ghost.onUpdate(() => { ghost.opacity = Math.max(0, Math.sin(k.time() * 0.6 + i * 2) * 0.15) ghost.pos.x = gx + Math.sin(k.time() * 0.3 + i) * 20 ghost.pos.y = gy + Math.cos(k.time() * 0.25 + i) * 15 }) } // Bats flying for (let i = 0; i < 3; i++) { const bat = k.add([k.rect(5, 2), k.pos(Math.random() * W, 15 + Math.random() * 40), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(2)]) const bx = bat.pos.x, by = bat.pos.y bat.onUpdate(() => { bat.pos.x = bx + Math.sin(k.time() * 1.5 + i * 3) * 40 bat.pos.y = by + Math.cos(k.time() * 2 + i * 2) * 10 }) } } else if (a === 'concert_hall') { // Stage lights with sweeping cones const lightColors = ['#ff2288', '#22ff88', '#4488ff', '#ffaa22', '#ff44ff', '#44ffff'] for (let i = 0; i < 6; i++) { const lx = W * (0.1 + i * 0.15) const cone = k.add([ k.rect(30, GROUND_Y * 0.7), k.pos(lx, 10), k.anchor('top'), k.color(safeColor(k, lightColors[i])), k.opacity(0.04), k.z(2), k.rotate(0), ]) k.add([k.circle(4), k.pos(lx + 15, 8), k.anchor('center'), k.color(safeColor(k, lightColors[i])), k.opacity(0.4), k.z(2)]) const baseAngle = (i - 3) * 5 cone.onUpdate(() => { cone.opacity = 0.02 + Math.sin(k.time() * 1.5 + i * 1.2) * 0.025 cone.angle = baseAngle + Math.sin(k.time() * 0.4 + i) * 8 }) } // Speaker stacks for (const sx of [10, W - 30]) { for (let s = 0; s < 4; s++) { k.add([k.rect(20, 18), k.pos(sx, GROUND_Y - 20 - s * 20), k.color(safeColor(k, '#111111')), k.opacity(0.5), k.z(1)]) k.add([k.circle(6), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.4), k.z(1)]) const speakerCone = k.add([k.circle(4), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1), k.scale(1)]) speakerCone.onUpdate(() => { speakerCone.scale = k.vec2(1 + Math.sin(k.time() * 8 + s) * 0.05) }) } } // Crowd silhouettes for (let i = 0; i < 30; i++) { const cx = 5 + i * (W / 30) const ch = 6 + Math.random() * 4 k.add([k.circle(2.5), k.pos(cx, GROUND_Y + 8), k.anchor('center'), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(3)]) k.add([k.rect(4, ch), k.pos(cx - 2, GROUND_Y + 10), k.color(safeColor(k, '#111111')), k.opacity(0.25), k.z(3)]) } // Smoke machine haze for (let i = 0; i < 6; i++) { const haze = k.add([ k.circle(30 + Math.random() * 25), k.pos(Math.random() * W, GROUND_Y - 8), k.color(safeColor(k, '#20102a')), k.opacity(0.18), k.z(3), ]) const hx = haze.pos.x haze.onUpdate(() => { haze.pos.x = hx + Math.sin(k.time() * 0.15 + i) * 15 }) } // Laser beam sweep const laser = k.add([k.rect(1, GROUND_Y), k.pos(W / 2, 0), k.color(safeColor(k, '#00ff00')), k.opacity(0.06), k.z(2)]) laser.onUpdate(() => { laser.pos.x = W / 2 + Math.sin(k.time() * 0.8) * (W * 0.4) }) } else if (a === 'sports_arena') { // Stadium seating tiers with crowd for (let tier = 0; tier < 5; tier++) { const ty = GROUND_Y * 0.1 + tier * (GROUND_Y * 0.13) k.add([k.rect(W, GROUND_Y * 0.1), k.pos(0, ty), k.color(safeColor(k, tier % 2 === 0 ? '#1a2a4a' : '#152040')), k.opacity(0.3), k.z(1)]) for (let i = 0; i < 18; i++) { k.add([ k.circle(1.5 + Math.random()), k.pos(15 + i * (W / 18), ty + GROUND_Y * 0.04), k.color(safeColor(k, ['#cc3333', '#3333cc', '#33cc33', '#cccc33', '#ffffff'][Math.floor(Math.random() * 5)])), k.opacity(0.12), k.z(1), ]) } } // Floodlights for (const fx of [W * 0.05, W * 0.95]) { k.add([k.rect(3, GROUND_Y * 0.7), k.pos(fx, GROUND_Y * 0.1), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(2)]) const light = k.add([k.circle(5), k.pos(fx + 1, GROUND_Y * 0.08), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.5), k.z(2)]) k.add([k.rect(40, GROUND_Y * 0.6), k.pos(fx - 18, GROUND_Y * 0.12), k.color(safeColor(k, '#ffffff')), k.opacity(0.015), k.z(0)]) light.onUpdate(() => { light.opacity = 0.4 + Math.sin(k.time() * 0.5 + fx) * 0.1 }) } // Scoreboard k.add([k.rect(50, 18), k.pos(W / 2 - 25, 8), k.color(safeColor(k, '#0a0a20')), k.opacity(0.5), k.z(2)]) k.add([k.rect(48, 16), k.pos(W / 2 - 24, 9), k.color(safeColor(k, '#111133')), k.opacity(0.4), k.z(2)]) const scoreGlow = k.add([k.rect(20, 8), k.pos(W / 2 - 10, 13), k.color(safeColor(k, '#ff3333')), k.opacity(0.12), k.z(2)]) scoreGlow.onUpdate(() => { scoreGlow.opacity = 0.08 + Math.sin(k.time() * 2) * 0.05 }) // Camera flashes for (let i = 0; i < 5; i++) { const flash = k.add([ k.circle(2), k.pos(Math.random() * W, GROUND_Y * (0.15 + Math.random() * 0.5)), k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3), ]) flash.onUpdate(() => { flash.opacity = Math.random() > 0.995 ? 0.4 : 0 }) } } else if (a === 'kitchen_stadium') { // Tiled wall for (let row = 0; row < 6; row++) { for (let col = 0; col < 14; col++) { k.add([ k.rect(W / 14 - 1, GROUND_Y / 6 - 1), k.pos(col * (W / 14), row * (GROUND_Y / 6)), k.color(safeColor(k, (row + col) % 2 === 0 ? '#1a1210' : '#1e1614')), k.opacity(0.2), k.z(0), ]) } } // Stove with burners k.add([k.rect(45, 35), k.pos(W * 0.06, GROUND_Y - 35), k.color(safeColor(k, '#1a1010')), k.opacity(0.45), k.z(1)]) k.add([k.rect(43, 2), k.pos(W * 0.06 + 1, GROUND_Y - 35), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)]) for (let b = 0; b < 4; b++) { const flame = k.add([k.circle(4), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 37), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.4), k.z(2)]) const innerFlame = k.add([k.circle(2), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 38), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.3), k.z(2)]) flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + b) * 0.15; innerFlame.opacity = 0.2 + Math.sin(k.time() * 7 + b) * 0.1 }) } // Hanging pots and pans k.add([k.rect(W * 0.5, 2), k.pos(W * 0.2, 8), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1)]) for (let i = 0; i < 6; i++) { const px = W * (0.22 + i * 0.08) const hookLen = 12 + Math.random() * 10 k.add([k.rect(1, hookLen), k.pos(px, 10), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)]) k.add([k.circle(7 + Math.random() * 4), k.pos(px, 10 + hookLen + 5), k.anchor('center'), k.color(safeColor(k, i % 2 === 0 ? '#555555' : '#664422')), k.opacity(0.25), k.z(1)]) } // Knife rack k.add([k.rect(3, 25), k.pos(W * 0.85, GROUND_Y * 0.3), k.color(safeColor(k, '#553322')), k.opacity(0.35), k.z(1)]) for (let i = 0; i < 4; i++) { k.add([k.rect(2, 10 + Math.random() * 5), k.pos(W * 0.85 + 1, GROUND_Y * 0.32 + i * 5), k.color(safeColor(k, '#aaaaaa')), k.opacity(0.2), k.z(1)]) } // Steam rising for (let i = 0; i < 4; i++) { const steam = k.add([ k.circle(5 + Math.random() * 5), k.pos(W * 0.06 + 10 + i * 10, GROUND_Y - 40), k.color(safeColor(k, '#ffffff')), k.opacity(0.06), k.z(3), ]) const bx = steam.pos.x steam.onUpdate(() => { steam.pos.y -= 10 * k.dt() steam.pos.x = bx + Math.sin(k.time() * 2 + i) * 4 steam.opacity *= 0.995 if (steam.pos.y < GROUND_Y - 80) { steam.pos.y = GROUND_Y - 40; steam.opacity = 0.06 } }) } } else if (a === 'junkyard') { // Rusted crane silhouette k.add([k.rect(6, GROUND_Y * 0.7), k.pos(W * 0.85, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.3), k.z(0)]) k.add([k.rect(W * 0.3, 4), k.pos(W * 0.6, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.25), k.z(0)]) const cable = k.add([k.rect(1, 35), k.pos(W * 0.65, GROUND_Y * 0.12), k.color(safeColor(k, '#555555')), k.opacity(0.2), k.z(0)]) cable.onUpdate(() => { cable.pos.x = W * 0.65 + Math.sin(k.time() * 0.3) * 5 }) // Piles of scrap for (let pile = 0; pile < 8; pile++) { const px = Math.random() * W const py = GROUND_Y - 8 for (let j = 0; j < 7; j++) { k.add([ k.rect(8 + Math.random() * 18, 4 + Math.random() * 10), k.pos(px + (Math.random() - 0.5) * 25, py - j * 6 - Math.random() * 5), k.color(safeColor(k, ['#554433', '#665544', '#443322', '#776655', '#332211', '#887766', '#553322'][j % 7])), k.opacity(0.3), k.z(1), k.rotate(Math.random() * 40 - 20), ]) } } // Old car silhouette k.add([k.rect(35, 15), k.pos(W * 0.15, GROUND_Y - 15), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.3), k.z(1)]) k.add([k.rect(25, 10), k.pos(W * 0.15 + 5, GROUND_Y - 25), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.25), k.z(1)]) k.add([k.circle(5), k.pos(W * 0.15 + 6, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)]) k.add([k.circle(5), k.pos(W * 0.15 + 29, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)]) // Sparking wires for (let i = 0; i < 3; i++) { const spark = k.add([ k.circle(2), k.pos(W * (0.3 + i * 0.2), GROUND_Y - 20 - Math.random() * 30), k.color(safeColor(k, '#ffaa00')), k.opacity(0), k.z(3), ]) spark.onUpdate(() => { spark.opacity = Math.random() > 0.96 ? 0.5 : 0 }) } // Smoke/steam wisps for (let i = 0; i < 4; i++) { const smoke = k.add([ k.circle(10 + Math.random() * 10), k.pos(Math.random() * W, GROUND_Y - 5), k.color(safeColor(k, '#333322')), k.opacity(0.12), k.z(2), ]) const sx = smoke.pos.x smoke.onUpdate(() => { smoke.pos.y -= 5 * k.dt() smoke.pos.x = sx + Math.sin(k.time() * 0.5 + i) * 6 if (smoke.pos.y < GROUND_Y - 60) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.12 } }) } } else if (a === 'meme_dimension') { // Floating emoji shapes (pulsing) for (let i = 0; i < 12; i++) { const emoji = k.add([ k.circle(6 + Math.random() * 6), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k, ['#ffcc00', '#ff4444', '#44ff44', '#4444ff', '#ff44ff', '#44ffff'][i % 6])), k.opacity(0.12), k.z(1), k.scale(1), ]) const bx = emoji.pos.x, by = emoji.pos.y emoji.onUpdate(() => { emoji.pos.x = bx + Math.sin(k.time() * 0.4 + i) * 15 emoji.pos.y = by + Math.cos(k.time() * 0.3 + i) * 10 emoji.scale = k.vec2(1 + Math.sin(k.time() * 1.5 + i) * 0.15) }) } // Glitch bands for (let i = 0; i < 8; i++) { const band = k.add([ k.rect(W, 2 + Math.random() * 4), k.pos(0, Math.random() * GROUND_Y), k.color(safeColor(k, Math.random() > 0.5 ? '#ff44ff' : '#44ffff')), k.opacity(0), k.z(2), ]) band.onUpdate(() => { band.opacity = Math.random() > 0.97 ? 0.12 + Math.random() * 0.08 : 0 if (band.opacity > 0) band.pos.x = (Math.random() - 0.5) * 10 else band.pos.x = 0 }) } // Rainbow pixel streaks const rainbowC = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff'] for (let i = 0; i < rainbowC.length; i++) { const streak = k.add([ k.rect(W, 3), k.pos(0, GROUND_Y * 0.3 + i * 5), k.color(safeColor(k, rainbowC[i])), k.opacity(0.04), k.z(0), ]) streak.onUpdate(() => { streak.opacity = 0.02 + Math.sin(k.time() * 2 + i) * 0.02 }) } // Spinning geometric shapes for (let i = 0; i < 4; i++) { const geo = k.add([ k.rect(8 + Math.random() * 8, 8 + Math.random() * 8), k.pos(W * (0.2 + i * 0.2), GROUND_Y * (0.3 + Math.random() * 0.3)), k.anchor('center'), k.color(safeColor(k, '#ff44ff')), k.opacity(0.08), k.z(1), k.rotate(0), ]) geo.onUpdate(() => { geo.angle += (30 + i * 15) * k.dt() }) } // VHS tracking lines const vhs = k.add([k.rect(W, 6), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(3)]) vhs.onUpdate(() => { vhs.pos.y = (vhs.pos.y + 80 * k.dt()) % (GROUND_Y + 20) vhs.opacity = Math.random() > 0.9 ? 0.15 : 0 }) // RGB noise flickers for (let i = 0; i < 6; i++) { const noise = k.add([ k.rect(3 + Math.random() * 8, 3 + Math.random() * 8), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k, ['#ff0000', '#00ff00', '#0000ff'][i % 3])), k.opacity(0), k.z(2), ]) noise.onUpdate(() => { noise.opacity = Math.random() > 0.98 ? 0.1 : 0 if (noise.opacity > 0) { noise.pos.x = Math.random() * W; noise.pos.y = Math.random() * GROUND_Y } }) } } else if (a === 'space_station') { // Structural beams with rivets for (let i = 0; i < 6; i++) { const bx = W * (0.15 + i * 0.14) k.add([k.rect(5, GROUND_Y), k.pos(bx, 0), k.color(safeColor(k, '#1a1a30')), k.opacity(0.3), k.z(1)]) for (let r = 0; r < 8; r++) { k.add([k.circle(1), k.pos(bx + 2, r * (GROUND_Y / 8) + 5), k.anchor('center'), k.color(safeColor(k, '#2a2a40')), k.opacity(0.25), k.z(1)]) } k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.25), k.color(safeColor(k, '#1a1a30')), k.opacity(0.25), k.z(1)]) k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.6), k.color(safeColor(k, '#1a1a30')), k.opacity(0.2), k.z(1)]) } // Viewport const vpX = W * 0.5, vpY = GROUND_Y * 0.3 k.add([k.circle(40), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#000020')), k.opacity(0.5), k.z(0)]) k.add([k.circle(42), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#2a2a50')), k.opacity(0.3), k.z(0)]) for (let s = 0; s < 15; s++) { const dist = Math.random() * 35 const angle = Math.random() * Math.PI * 2 const sx = vpX + Math.cos(angle) * dist const sy = vpY + Math.sin(angle) * dist const vpStar = k.add([k.circle(0.8 + Math.random()), k.pos(sx, sy), k.color(safeColor(k, '#ffffff')), k.opacity(0.3), k.z(0)]) vpStar.onUpdate(() => { vpStar.opacity = 0.15 + Math.sin(k.time() * 2 + s) * 0.15 }) } // Earth through viewport k.add([k.circle(15), k.pos(vpX + 10, vpY + 5), k.anchor('center'), k.color(safeColor(k, '#2244aa')), k.opacity(0.15), k.z(0)]) k.add([k.circle(6), k.pos(vpX + 7, vpY + 2), k.anchor('center'), k.color(safeColor(k, '#22aa44')), k.opacity(0.08), k.z(0)]) // Control panels for (const px of [W * 0.08, W * 0.88]) { k.add([k.rect(25, 40), k.pos(px, GROUND_Y * 0.4), k.color(safeColor(k, '#0a0a20')), k.opacity(0.4), k.z(1)]) for (let led = 0; led < 8; led++) { const ledEl = k.add([ k.rect(2, 2), k.pos(px + 4 + (led % 4) * 5, GROUND_Y * 0.42 + Math.floor(led / 4) * 6), k.color(safeColor(k, ['#00ff88', '#ff4444', '#ffaa00', '#4488ff'][led % 4])), k.opacity(0.4), k.z(2), ]) ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.05 ? 0.4 : 0.1 }) } } // Warning stripes for (let i = 0; i < 8; i++) { k.add([k.rect(8, 3), k.pos(W * 0.3 + i * 12, GROUND_Y - 3), k.color(safeColor(k, i % 2 === 0 ? '#ffaa00' : '#000000')), k.opacity(0.12), k.z(3)]) } } else if (a === 'savanna') { // Distant mountain range for (let i = 0; i < 5; i++) { const mx = W * (0.1 + i * 0.2) + (Math.random() - 0.5) * 30 const mh = 30 + Math.random() * 40 k.add([k.circle(mh), k.pos(mx, GROUND_Y - mh * 0.3), k.anchor('center'), k.color(safeColor(k, '#2a2010')), k.opacity(0.12), k.z(0)]) } // Acacia trees with branches for (const tx of [W * 0.1, W * 0.4, W * 0.72]) { const trunkH = 40 + Math.random() * 20 k.add([k.rect(5, trunkH), k.pos(tx, GROUND_Y - trunkH), k.color(safeColor(k, '#2a1a08')), k.opacity(0.4), k.z(1)]) k.add([k.rect(15, 3), k.pos(tx - 10, GROUND_Y - trunkH * 0.7), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(-20)]) k.add([k.rect(12, 3), k.pos(tx + 5, GROUND_Y - trunkH * 0.6), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(15)]) k.add([k.rect(50, 5), k.pos(tx - 22, GROUND_Y - trunkH - 2), k.color(safeColor(k, '#2a3a10')), k.opacity(0.3), k.z(1)]) k.add([k.rect(40, 4), k.pos(tx - 17, GROUND_Y - trunkH - 7), k.color(safeColor(k, '#2a3a10')), k.opacity(0.25), k.z(1)]) k.add([k.rect(25, 3), k.pos(tx - 10, GROUND_Y - trunkH - 11), k.color(safeColor(k, '#223308')), k.opacity(0.2), k.z(1)]) } // Setting sun const sunX = W * 0.85, sunY = GROUND_Y * 0.22 k.add([k.circle(50), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.06), k.z(0)]) k.add([k.circle(35), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.12), k.z(0)]) const savSun = k.add([k.circle(22), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffaa22')), k.opacity(0.3), k.z(0)]) savSun.onUpdate(() => { savSun.opacity = 0.25 + Math.sin(k.time() * 0.3) * 0.05 }) // Tall grass swaying for (let i = 0; i < 20; i++) { const gx = Math.random() * W const grassH = 5 + Math.random() * 10 const grass = k.add([k.rect(1, grassH), k.pos(gx, GROUND_Y - grassH), k.color(safeColor(k, '#8a7830')), k.opacity(0.2), k.z(3), k.rotate(0)]) grass.onUpdate(() => { grass.angle = Math.sin(k.time() * 1.2 + i) * 5 }) } // Elephant silhouette const ex = W * 0.6, ey = GROUND_Y - 18 k.add([k.rect(18, 12), k.pos(ex, ey), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)]) k.add([k.rect(8, 8), k.pos(ex + 15, ey - 4), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)]) k.add([k.rect(3, 8), k.pos(ex + 2, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)]) k.add([k.rect(3, 8), k.pos(ex + 13, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)]) // Birds in V formation for (let i = 0; i < 5; i++) { const birdX = W * 0.3 + i * 8 const birdY = 20 + Math.abs(i - 2) * 5 k.add([k.rect(4, 1), k.pos(birdX, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(-12)]) k.add([k.rect(4, 1), k.pos(birdX + 3, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(12)]) } } else if (a === 'server_room') { // Server rack rows with LEDs for (let row = 0; row < 3; row++) { for (let col = 0; col < 6; col++) { const rx = 30 + col * (W / 6) const ry = 20 + row * 55 const rw = W / 6 - 12 const rh = 46 k.add([k.rect(rw, rh), k.pos(rx, ry), k.color(safeColor(k, '#0a1a10')), k.opacity(0.4), k.z(1)]) k.add([k.rect(rw, 1), k.pos(rx, ry), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)]) k.add([k.rect(rw, 1), k.pos(rx, ry + rh), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)]) for (let s = 0; s < 5; s++) { k.add([k.rect(rw - 4, 1), k.pos(rx + 2, ry + 10 + s * 7), k.color(safeColor(k, '#0a1008')), k.opacity(0.3), k.z(1)]) } for (let led = 0; led < 4; led++) { const ledEl = k.add([ k.rect(2, 2), k.pos(rx + 3 + led * 5, ry + 3), k.color(safeColor(k, '#00ff88')), k.opacity(0.5), k.z(2), ]) ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.04 ? 0.5 : 0.1 }) } if (Math.random() < 0.5) { const act = k.add([k.rect(2, 2), k.pos(rx + rw - 6, ry + 3), k.color(safeColor(k, '#ffaa00')), k.opacity(0.5), k.z(2)]) act.onUpdate(() => { act.opacity = Math.random() > 0.3 ? 0.5 : 0.15 }) } } } // Overhead cable trays for (let i = 0; i < 3; i++) { k.add([k.rect(W * 0.25, 3), k.pos(W * (0.1 + i * 0.3), 5), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)]) for (let c = 0; c < 4; c++) { k.add([k.rect(W * 0.25, 1), k.pos(W * (0.1 + i * 0.3), 7 + c * 2), k.color(safeColor(k, ['#004400', '#000044', '#440000', '#444400'][c])), k.opacity(0.15), k.z(1)]) } } // Cool air flow particles for (let i = 0; i < 5; i++) { const airflow = k.add([ k.circle(2), k.pos(Math.random() * W, GROUND_Y - 10), k.color(safeColor(k, '#00ff88')), k.opacity(0.08), k.z(3), ]) airflow.onUpdate(() => { airflow.pos.y -= 15 * k.dt() if (airflow.pos.y < 10) { airflow.pos.y = GROUND_Y - 10; airflow.pos.x = Math.random() * W } }) } // Emergency lighting const emLight = k.add([k.circle(4), k.pos(W * 0.5, 5), k.anchor('center'), k.color(safeColor(k, '#ff0000')), k.opacity(0.2), k.z(2)]) emLight.onUpdate(() => { emLight.opacity = 0.1 + Math.sin(k.time() * 1.5) * 0.1 }) } else if (a === 'paper_mill') { // Industrial ceiling pipes for (let i = 0; i < 5; i++) { const py = 5 + i * 4 k.add([k.rect(W, 3), k.pos(0, py), k.color(safeColor(k, i % 2 === 0 ? '#2a2a20' : '#222218')), k.opacity(0.35), k.z(1)]) for (let j = 0; j < 4; j++) { k.add([k.circle(3), k.pos(W * (0.2 + j * 0.2), py + 1), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)]) } } // Conveyor belt k.add([k.rect(W * 0.6, 6), k.pos(W * 0.2, GROUND_Y - 15), k.color(safeColor(k, '#333328')), k.opacity(0.35), k.z(1)]) for (let i = 0; i < 8; i++) { k.add([k.circle(3), k.pos(W * 0.22 + i * (W * 0.075), GROUND_Y - 12), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.3), k.z(1)]) } // Moving items on belt for (let i = 0; i < 3; i++) { const item = k.add([ k.rect(10, 6), k.pos(W * 0.25 + i * 60, GROUND_Y - 21), k.color(safeColor(k, '#f0e68c')), k.opacity(0.2), k.z(2), ]) item.onUpdate(() => { item.pos.x += 15 * k.dt() if (item.pos.x > W * 0.78) item.pos.x = W * 0.2 }) } // Paper rolls stacked for (let i = 0; i < 3; i++) { for (let j = 0; j <= i; j++) { const rx = W * 0.08 + j * 14 - i * 7 const ry = GROUND_Y - 12 - (2 - i) * 12 k.add([k.circle(6), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#ddd8b0')), k.opacity(0.2), k.z(1)]) k.add([k.circle(2), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#aaa880')), k.opacity(0.15), k.z(1)]) } } // Industrial gears (rotating) for (const gx of [W * 0.88, W * 0.92]) { const gy = GROUND_Y * 0.35 k.add([k.circle(10), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1)]) k.add([k.circle(4), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)]) for (let t = 0; t < 8; t++) { const tooth = k.add([ k.rect(4, 3), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1), k.rotate(t * 45), ]) tooth.onUpdate(() => { tooth.angle = t * 45 + k.time() * (gx === W * 0.88 ? 20 : -25) }) } } // Steam from pipes for (let i = 0; i < 4; i++) { const steam = k.add([ k.circle(6 + Math.random() * 8), k.pos(W * (0.2 + i * 0.2), 25), k.color(safeColor(k, '#ccccbb')), k.opacity(0.06), k.z(2), ]) const bsx = steam.pos.x steam.onUpdate(() => { steam.pos.y += 4 * k.dt() steam.pos.x = bsx + Math.sin(k.time() + i) * 5 steam.opacity *= 0.998 if (steam.pos.y > GROUND_Y * 0.4) { steam.pos.y = 25; steam.opacity = 0.06 } }) } // Sawdust particles for (let i = 0; i < 8; i++) { const dust = k.add([ k.circle(1), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k, '#f0e68c')), k.opacity(0.12), k.z(3), ]) const dx = dust.pos.x, dy = dust.pos.y dust.onUpdate(() => { dust.pos.x = dx + Math.sin(k.time() * 0.8 + i) * 10 dust.pos.y = dy + Math.cos(k.time() * 0.5 + i) * 8 }) } } } k.scene('fight', () => { // === SKY / BACKGROUND ATMOSPHERE === // Vertical gradient backdrop — 6 bands from dark ceiling to ground horizon const bgBase = theme.bg for (let band = 0; band < 6; band++) { const bandH = GROUND_Y / 6 const brighten = band * 3 k.add([ k.rect(W, bandH + 1), k.pos(0, band * bandH), k.color(safeColor(k,bgBase)), k.opacity(1 - band * 0.04), ]) } // Subtle horizon glow — wide soft band just above ground k.add([ k.rect(W, 20), k.pos(0, GROUND_Y - 20), k.color(safeColor(k,theme.accent)), k.opacity(0.06), ]) // === GROUND === // Main ground fill k.add([k.rect(W, H - GROUND_Y + 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.ground))]) // Bright edge line at the surface k.add([k.rect(W, 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.accent)), k.opacity(0.7)]) // Secondary highlight k.add([k.rect(W, 1), k.pos(0, GROUND_Y + 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3)]) // Ground gradient — fades deeper for (let i = 1; i <= 12; i++) { k.add([k.rect(W, 2), k.pos(0, GROUND_Y + 3 + i * 6), k.color(safeColor(k,theme.accent)), k.opacity(0.1 - i * 0.007)]) } // Perspective grid on the floor — converging lines for 3D depth const vanishX = W / 2 const vanishY = GROUND_Y * 0.6 for (let i = 0; i <= 24; i++) { const x = i * (W / 24) // Lines from floor edge pointing toward vanishing point const dx = x - vanishX const topX = vanishX + dx * 0.3 k.add([ k.rect(1, GROUND_Y * 0.4 + 10), k.pos(x, GROUND_Y), k.color(safeColor(k,theme.accent)), k.opacity(0.05), ]) } // Horizontal depth lines on ground for (let i = 0; i < 6; i++) { const gy = GROUND_Y + 8 + i * i * 3 if (gy < H) { k.add([k.rect(W, 1), k.pos(0, gy), k.color(safeColor(k,theme.accent)), k.opacity(0.08 - i * 0.012)]) } } // === SIDE PILLARS / FRAME === // Subtle dark pillars on edges for framing k.add([k.rect(8, GROUND_Y), k.pos(0, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)]) k.add([k.rect(8, GROUND_Y), k.pos(W - 8, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)]) // Accent trim on pillars k.add([k.rect(1, GROUND_Y), k.pos(8, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)]) k.add([k.rect(1, GROUND_Y), k.pos(W - 9, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)]) // === AMBIENT PARTICLES === // Floating dust/embers in the air (subtle, always present) for (let i = 0; i < 18; i++) { const particle = k.add([ k.circle(0.5 + Math.random() * 1.5), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k,theme.accent)), k.opacity(0.08 + Math.random() * 0.12), k.z(2), ]) const baseX = particle.pos.x const baseY = particle.pos.y const driftSpeed = 0.1 + Math.random() * 0.3 const driftAmt = 8 + Math.random() * 15 particle.onUpdate(() => { particle.pos.x = baseX + Math.sin(k.time() * driftSpeed + i * 1.3) * driftAmt particle.pos.y = baseY + Math.cos(k.time() * driftSpeed * 0.7 + i) * (driftAmt * 0.5) particle.opacity = 0.06 + Math.sin(k.time() * 0.8 + i * 0.9) * 0.06 }) } // Arena-specific decorations drawArenaDecor() // Umpire chair (center-back, behind fighters) const CHAIR_X = W / 2 const CHAIR_SEAT_Y = GROUND_Y * (0.38 + (1 - SF) * 0.2) const cs = SF // chair scale k.add([k.rect(4 * cs, GROUND_Y - CHAIR_SEAT_Y + 15 * cs), k.pos(CHAIR_X - 22 * cs, CHAIR_SEAT_Y - 8 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)]) k.add([k.rect(4 * cs, GROUND_Y - CHAIR_SEAT_Y + 15 * cs), k.pos(CHAIR_X + 18 * cs, CHAIR_SEAT_Y - 8 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)]) for (let i = 0; i < 6; i++) { const rungY = CHAIR_SEAT_Y + 15 * cs + i * ((GROUND_Y - CHAIR_SEAT_Y - 15 * cs) / 6) k.add([k.rect(44 * cs, 3), k.pos(CHAIR_X - 22 * cs, rungY), k.color(safeColor(k,'#443322')), k.opacity(0.6), k.z(3)]) } k.add([k.rect(54 * cs, 5), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 2), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)]) k.add([k.rect(54 * cs, 1), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3), k.z(3)]) k.add([k.rect(48 * cs, 4), k.pos(CHAIR_X - 24 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)]) k.add([k.rect(3, 12 * cs), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)]) k.add([k.rect(3, 12 * cs), k.pos(CHAIR_X + 24 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)]) k.add([ k.sprite('judge', { anim: 'idle' }), k.pos(CHAIR_X, CHAIR_SEAT_Y), k.anchor('bot'), k.scale(1.3 * SF), k.z(4), k.opacity(0.9), 'judge', ]) const scaleA = (1.8 + botA.tier * 0.4) * SF k.add([k.sprite('botA', { anim: 'idle' }), k.pos(-100, GROUND_Y - 6), k.anchor('bot'), k.scale(scaleA), k.z(10), k.opacity(0), 'fighterA']) const scaleB = (1.8 + botB.tier * 0.4) * SF k.add([k.sprite('botB', { anim: 'idle' }), k.pos(W + 100, GROUND_Y - 6), k.anchor('bot'), k.scale(-scaleB, scaleB), k.z(10), k.opacity(0), 'fighterB']) config.onReady?.() }) k.go('fight') let comboA = 0 let comboB = 0 // === CHOREOGRAPHY SYSTEM (extracted to fight/choreography/) === const choreoCtx: ChoreoContext = { k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor: (color: string) => safeColor(k, color), trackedTimeout, trackedInterval, clearTracked, spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile, spawnShockwave, screenFlash, impactFreeze, spawnAfterimages, glitchRGB, scanlineGlitch, dimensionalShift, sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion, sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin, sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown, sfxZap, sfxZoomWhoosh, sfxBlock, spawnBullet, spawnGrotesqueDetails, destroyGrotesqueDetails, } const choreographyMap = createChoreographyMap(choreoCtx) const { spawnWeaponProp, destroyProp, spawnProp, moveProp } = createFactories(choreoCtx) async function playBlock(side: 'a' | 'b') { const defender = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter if (!defender) return const dir = side === 'a' ? 1 : -1 // Spawn shield in front const shieldObjs = spawnWeaponProp('shield', defender.pos.x + dir * 20, defender.pos.y - 25, side === 'b', 19) sfxBlock() // Flash shield shieldObjs.forEach(o => { o.opacity = 0.9 }) await k.wait(0.15) // Impact sparks spawnSparks(defender.pos.x + dir * 25, defender.pos.y - 25, 6, '#4488ff') k.shake(3) await k.wait(0.3) // Shield fades shieldObjs.forEach(o => { k.tween(o.opacity, 0, 0.2, (v: number) => { o.opacity = v }).then(() => { if (o.exists()) o.destroy() }) }) await k.wait(0.1) } // === MORPH SYSTEM (extracted to fight/morphs.ts) === const morphSystem = createMorphSystem(choreoCtx, { botA, botB, colorsA, colorsB, isHumanA, isHumanB, loadSpriteWithTimeout, }) const { destroyMorphOverlays, playMorph, applyCreatorOmniMorph, getMorphOrder } = morphSystem // === EMOTION & SPORTSMANSHIP SYSTEM === const heartfeltLines = [ 'That bot fights like it\'s got student loans to pay!', 'I\'m not crying, you\'re crying!', 'Someone get this bot a movie deal!', 'This is better than anything on Netflix right now!', 'That bot has more heart than most politicians!', 'I haven\'t been this moved since my last Windows update!', 'The crowd is ugly crying and I respect that!', 'This bot has main character energy!', 'We\'re witnessing greatness and I don\'t say that lightly!', 'Even the judge is getting emotional!', 'Someone call their developer, they\'d be so proud!', 'This is the kind of fight you tell your grandkids about!', 'That\'s not a bot, that\'s a WARRIOR!', 'More drama than a Congressional hearing!', 'They left it all in the ring! And by all, I mean EVERYTHING!', 'The crowd is standing! The ones who can still stand, anyway!', 'Somebody film this before it gets taken down!', 'This fight has more plot twists than a Netflix original!', 'That bot fights like rent is due tomorrow!', 'My heart! My poor little heart!', ] const crowdSympathyLines = [ 'Pour one out for the fallen!', 'That\'s rough, buddy...', 'Somebody hug that bot!', 'They tried their best, which is... concerning!', 'At least they have a great personality!', 'Their developer still loves them! Probably!', 'It\'s a learning experience! A very painful one!', 'Better luck next patch!', 'Participation trophy incoming!', 'The exit is to your left!', 'Have you tried turning it off and on again?', 'Even their antivirus felt that!', 'They\'ll be in therapy for epochs after this!', 'At least they looked good losing!', 'That bot just became a cautionary tale!', 'Someone start a GoFundMe for their repairs!', 'Their training data did NOT prepare them for this!', 'That bot needs a hug and a firmware update!', ] const respectLines = [ 'Good fight.', 'Respect.', 'GG no re.', 'Same time next week?', 'You\'re built different. Not better, but different.', 'We should start a podcast.', 'That was actually fun. Don\'t tell anyone.', 'Your developer should be proud. Probably.', 'Ten out of ten, would fight again.', 'We\'re not so different, you and I.', 'I\'d swipe right.', 'You single?', 'Tell your GPU I said hi.', 'No hard feelings. Just hard hits.', 'You fight like someone who reads documentation.', ] // Sanitize text for Kaplay (treats [ ] as styled text tags) function safeText(t: string): string { return t .replace(/[\[\]{}<>`\\|~^]/g, '') // Kaplay styled-text tags and problematic chars .replace(/[\x00-\x1f\x7f]/g, ' ') // Control characters → space .replace(/[^\x20-\x7e]/g, '') // Strip non-ASCII (emoji, unicode) — Kaplay can't render them .replace(/\s+/g, ' ') // Collapse whitespace .trim() } // Speech bubble above a fighter — colourful with tail // Active speech bubbles — destroy previous before showing new let activeBubbleA: GameObj[] = [] let activeBubbleB: GameObj[] = [] function destroyBubble(els: GameObj[]) { els.forEach(el => { if (el.exists()) el.destroy() }) els.length = 0 } function showSpeechBubble(side: 'a' | 'b', text: string, duration: number = 3) { const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter if (!fighter || !text) return // Kill previous bubble on this side if (side === 'a') destroyBubble(activeBubbleA) else destroyBubble(activeBubbleB) const allEls: GameObj[] = [] // Sanitize and truncate const clean = safeText(text) if (!clean) return const maxLen = 80 const display = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '...' : clean const lines = wrapBubbleText(display, 18) const fontSize = 12 const lineH = fontSize + 4 const padX = 10, padY = 8 const textW = Math.max(60, Math.min(lines.reduce((m, l) => Math.max(m, l.length * (fontSize * 0.6)), 0) + 8, W * 0.38)) const bubbleW = textW + padX * 2 const bubbleH = lines.length * lineH + padY * 2 const tailSize = 10 // Position — push bubble to the outside of the bot so it doesn't overlap the sprite const offsetX = side === 'a' ? -bubbleW * 0.7 : bubbleW * 0.7 const bx = Math.max(bubbleW / 2 + 4, Math.min(W - bubbleW / 2 - 4, fighter.pos.x + offsetX)) const by = Math.max(8, fighter.pos.y - 75 - bubbleH) const borderColor = side === 'a' ? '#00f0ff' : '#ff2d7b' const bgColor = side === 'a' ? '#0a1e2a' : '#2a0a1e' // Single border rect (no glow/accent layers — keeps rendering clean) const border = k.add([ k.rect(bubbleW + 4, bubbleH + 4), k.pos(bx - bubbleW / 2 - 2, by - 2), k.color(safeColor(k, borderColor)), k.opacity(0.85), k.z(54), ]) allEls.push(border) // Bubble body const bubble = k.add([ k.rect(bubbleW, bubbleH), k.pos(bx - bubbleW / 2, by), k.color(safeColor(k, bgColor)), k.opacity(0.95), k.z(55), ]) allEls.push(bubble) // Tail — pixel arrow pointing down to fighter const tailX = bx + (side === 'a' ? bubbleW * 0.15 : -bubbleW * 0.15) const tailBaseY = by + bubbleH for (let row = 0; row < tailSize; row++) { const tw = tailSize - row // Border pixel row const tailBorder = k.add([ k.rect(tw + 2, 1), k.pos(tailX - (tw + 2) / 2, tailBaseY + row), k.color(safeColor(k, borderColor)), k.opacity(0.85), k.z(54), ]) allEls.push(tailBorder) // Inner pixel row if (tw > 2) { const tailInner = k.add([ k.rect(tw - 1, 1), k.pos(tailX - (tw - 1) / 2, tailBaseY + row), k.color(safeColor(k, bgColor)), k.opacity(0.95), k.z(55), ]) allEls.push(tailInner) } } // Render text to an offscreen canvas, then display as a sprite // This bypasses kaplay's text rendering which has color issues const textCanvas = document.createElement('canvas') textCanvas.width = bubbleW textCanvas.height = bubbleH const tc = textCanvas.getContext('2d')! tc.fillStyle = '#ffffff' tc.font = `${fontSize}px monospace` tc.textBaseline = 'top' for (let i = 0; i < lines.length; i++) { tc.fillText(lines[i], padX, padY + i * lineH + 2) } const spriteKey = `bubble_${side}_${Date.now()}` k.loadSprite(spriteKey, textCanvas.toDataURL()).then(() => { const tEl = k.add([ k.sprite(spriteKey), k.pos(bx - bubbleW / 2, by), k.opacity(1), k.z(57), ]) allEls.push(tEl) }) // Border glow pulse border.onUpdate(() => { border.opacity = 0.7 + Math.sin(k.time() * 4) * 0.15 }) // Pop-in animation const anchorX = bx, anchorY = by + bubbleH allEls.forEach(el => { const origX = el.pos.x, origY = el.pos.y const origOpacity = el.opacity el.pos.x = anchorX + (origX - anchorX) * 0.1 el.pos.y = anchorY + (origY - anchorY) * 0.1 el.opacity = 0 k.tween(0, 1, 0.25, (t) => { el.pos.x = anchorX + (origX - anchorX) * t el.pos.y = anchorY + (origY - anchorY) * t el.opacity = origOpacity * t }, k.easings.easeOutBack) }) // Store reference if (side === 'a') activeBubbleA = allEls else activeBubbleB = allEls // Fade out after duration trackedTimeout(() => { allEls.forEach(el => { if (!el.exists()) return k.tween(el.opacity, 0, 0.35, (v) => { el.opacity = v }).then(() => { if (el.exists()) el.destroy() }) }) }, duration * 1000) } function wrapBubbleText(text: string, maxChars: number): string[] { const words = text.split(' ') const lines: string[] = [] let line = '' for (const word of words) { if (line.length + word.length + 1 > maxChars && line.length > 0) { lines.push(line) line = word } else { line = line ? line + ' ' + word : word } } if (line) lines.push(line) // Max 5 lines, last line truncated if (lines.length > 5) { const truncated = [...lines.slice(0, 4), lines.slice(4).join(' ').slice(0, maxChars - 3) + '...'] return truncated } return lines } // Talking mouth animation — a small rectangle that opens/closes near the fighter's face const talkingAnims: Record }> = {} function startTalking(side: 'a' | 'b') { stopTalking(side) // cleanup any existing const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter if (!fighter?.exists()) return const mouthColor = side === 'a' ? '#00f0ff' : '#ff2d7b' // Mouth position: slightly below center of the sprite const mouthOffsetY = -28 const mouthW = 8 let open = false const mouth = k.add([ k.rect(mouthW, 2), k.pos(fighter.pos.x, fighter.pos.y + mouthOffsetY), k.color(safeColor(k, mouthColor)), k.opacity(0.9), k.z(12), k.anchor('center'), ]) // Toggle open/close every 120ms for a fast chatter effect const timer = trackedInterval(() => { if (!mouth.exists() || !fighter.exists()) { stopTalking(side); return } open = !open mouth.pos.x = fighter.pos.x mouth.pos.y = fighter.pos.y + mouthOffsetY mouth.height = open ? 5 : 2 }, 120) talkingAnims[side] = { objs: [mouth], timer } } function stopTalking(side: 'a' | 'b') { const anim = talkingAnims[side] if (!anim) return clearTracked(anim.timer) anim.objs.forEach(o => { if (o.exists()) o.destroy() }) delete talkingAnims[side] } // Spawn floating text above a position function spawnEmoteText(x: number, y: number, text: string, color: string, duration: number = 1.2) { const label = k.add([ k.text(safeText(text), { size: 10 }), k.pos(x, y - 10), k.color(safeColor(k,color)), k.opacity(1), k.z(60), k.anchor('center'), ]) k.tween(label.pos.y, y - 50, duration, (v) => { label.pos.y = v }, k.easings.easeOutQuad) k.tween(1, 0, duration, (v) => { label.opacity = v }, k.easings.easeInQuad).then(() => { if (label.exists()) label.destroy() }) } // Spawn a heart particle function spawnHeart(x: number, y: number) { const colors = ['#ff4466', '#ff6688', '#ff2244', '#ff88aa', '#cc2244'] const heart = k.add([ k.text('\u2665', { size: 12 + Math.random() * 8 }), k.pos(x + (Math.random() - 0.5) * 40, y), k.color(safeColor(k,colors[Math.floor(Math.random() * colors.length)])), k.opacity(0.8), k.z(58), k.anchor('center'), ]) const driftX = (Math.random() - 0.5) * 30 k.tween(heart.pos.y, y - 60 - Math.random() * 40, 1.5, (v) => { heart.pos.y = v heart.pos.x += driftX * 0.01 }, k.easings.easeOutQuad) k.tween(0.8, 0, 1.5, (v) => { heart.opacity = v }).then(() => { if (heart.exists()) heart.destroy() }) } // Crowd signs: pop up from bottom with text, bob, then fade out function spawnCrowdSigns(count: number, color: string, text?: string) { const labels = text ? [text] : ['GO!', 'WOW', 'KO!', 'LOL', 'GG', 'OMG'] for (let i = 0; i < count; i++) { const x = W * 0.15 + Math.random() * W * 0.7 const startY = H + 20 const endY = GROUND_Y + 10 + Math.random() * 20 const label = labels[Math.floor(Math.random() * labels.length)] // Sign stick const stick = k.add([ k.rect(3, 25), k.pos(x, startY), k.color(safeColor(k, '#886644')), k.opacity(0.8), k.z(8), k.anchor('bot'), ]) // Sign face const sign = k.add([ k.rect(Math.max(30, label.length * 10), 18), k.pos(x, startY - 25), k.color(safeColor(k, color)), k.opacity(0.85), k.z(8), k.anchor('center'), ]) // Sign text const txt = k.add([ k.text(label, { size: 10 }), k.pos(x, startY - 25), k.color(safeColor(k, '#ffffff')), k.opacity(0.9), k.z(9), k.anchor('center'), ]) const delay = i * 0.08 // Rise up k.wait(delay, () => { k.tween(startY, endY, 0.3, (v) => { stick.pos.y = v; sign.pos.y = v - 25; txt.pos.y = v - 25 }, k.easings.easeOutBack) }) // Bob + fade out after 1.5s k.wait(delay + 1.5, () => { k.tween(0.85, 0, 0.5, (v) => { stick.opacity = v; sign.opacity = v; txt.opacity = v }).then(() => { if (stick.exists()) stick.destroy() if (sign.exists()) sign.destroy() if (txt.exists()) txt.destroy() }) }) } } // === HUMAN OWNER APPEARANCES === // The silly human behind each bot occasionally shows up /** Spawn a human as a "coach" standing behind the fighter, cheering/panicking */ async function spawnHumanCoach(side: 'a' | 'b', mood: 'cheer' | 'panic' | 'coach') { const spriteId = side === 'a' ? 'humanA' : 'humanB' const homeX = side === 'a' ? HOME_A - 60 : HOME_B + 60 const tag = `human_${side}` // Don't spawn if already visible if (k.get(tag).length > 0) return const human = k.add([ k.sprite(spriteId, { anim: mood }), k.pos(homeX, GROUND_Y - 4), k.anchor('bot'), k.scale(side === 'a' ? 1.2 : -1.2, 1.2), k.z(5), k.opacity(0), tag, ]) // Fade in await k.tween(0, 0.85, 0.3, (v) => { human.opacity = v }, k.easings.easeOutQuad) await k.wait(2.0) // Fade out await k.tween(0.85, 0, 0.5, (v) => { human.opacity = v }, k.easings.easeInQuad) if (human.exists()) human.destroy() } /** Human runs across the screen (comedic moment) */ async function humanRunAcross(side: 'a' | 'b') { const spriteId = side === 'a' ? 'humanA' : 'humanB' const fromLeft = side === 'a' const startX = fromLeft ? -40 : W + 40 const endX = fromLeft ? W + 40 : -40 const human = k.add([ k.sprite(spriteId, { anim: 'run' }), k.pos(startX, GROUND_Y - 4), k.anchor('bot'), k.scale(fromLeft ? 1.3 : -1.3, 1.3), k.z(8), k.opacity(0.9), ]) // Run across screen await k.tween(startX, endX, 1.5, (v) => { human.pos.x = v }, k.easings.linear) if (human.exists()) human.destroy() } /** Human briefly replaces the bot sprite (jumps in to fight, fails hilariously) */ async function humanJumpsIn(side: 'a' | 'b') { const fighterTag = side === 'a' ? 'fighterA' : 'fighterB' const spriteId = side === 'a' ? 'humanA' : 'humanB' const fighter = k.get(fighterTag)[0] as Fighter if (!fighter) return // Spawn human at fighter position const human = k.add([ k.sprite(spriteId, { anim: 'panic' }), k.pos(fighter.pos.x, fighter.pos.y), k.anchor('bot'), k.scale(fighter.scale.x / 3, fighter.scale.y / 3), k.z(fighter.z + 1), k.opacity(0), ]) // Hide bot, show human const origOpacity = fighter.opacity fighter.opacity = 0 await k.tween(0, 1, 0.15, (v) => { human.opacity = v }) // Human flails around for a moment human.play('panic') await k.wait(0.4) human.play('cheer') sfxRandomSilly() await k.wait(0.3) human.play('panic') await k.wait(0.3) // Run away scared const escapeX = side === 'a' ? -80 : W + 80 human.play('run') await k.tween(human.pos.x, escapeX, 0.5, (v) => { human.pos.x = v }, k.easings.easeInQuad) // Restore bot fighter.opacity = origOpacity if (human.exists()) human.destroy() } /** Random chance to show a human moment — called between rounds */ async function maybeShowHuman() { const roll = Math.random() if (roll > 0.15) return // 15% chance per round const side: 'a' | 'b' = Math.random() < 0.5 ? 'a' : 'b' const action = Math.random() if (action < 0.4) { // Coach appearance (most common) const mood = Math.random() < 0.5 ? 'cheer' : 'panic' await spawnHumanCoach(side, mood) } else if (action < 0.7) { // Run across screen await humanRunAcross(side) } else { // Jump in and replace bot briefly (rarest, funniest) await humanJumpsIn(side) } } // Respectful bow animation async function playBow(fighter: Fighter) { const origScaleY = fighter.scale.y await k.tween(fighter.scale.y, origScaleY * 0.85, 0.2, (v) => { fighter.scale.y = v }, k.easings.easeOutQuad) await k.wait(0.3) await k.tween(fighter.scale.y, origScaleY, 0.2, (v) => { fighter.scale.y = v }, k.easings.easeOutQuad) } // Fist bump between two fighters async function playFistBump(fighterA: Fighter, fighterB: Fighter) { const midX = (fighterA.pos.x + fighterB.pos.x) / 2 const origAX = fighterA.pos.x const origBX = fighterB.pos.x await Promise.all([ k.tween(fighterA.pos.x, midX - 15, 0.3, (v) => { fighterA.pos.x = v }, k.easings.easeInOutQuad), k.tween(fighterB.pos.x, midX + 15, 0.3, (v) => { fighterB.pos.x = v }, k.easings.easeInOutQuad), ]) sfxBlock() spawnSparks(midX, fighterA.pos.y - 25, 6, '#ffe14d') spawnEmoteText(midX, fighterA.pos.y - 40, 'GG!', '#ffe14d') k.shake(2) await k.wait(0.5) await Promise.all([ k.tween(fighterA.pos.x, origAX, 0.3, (v) => { fighterA.pos.x = v }, k.easings.easeInOutQuad), k.tween(fighterB.pos.x, origBX, 0.3, (v) => { fighterB.pos.x = v }, k.easings.easeInOutQuad), ]) } // Winner helps loser back up async function playHelpUp(winner: Fighter, loser: Fighter, winningSide: 'a' | 'b') { const dir = winningSide === 'a' ? 1 : -1 const origWX = winner.pos.x await k.tween(winner.pos.x, loser.pos.x - dir * 30, 0.4, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad) winner.play('idle') await k.wait(0.2) loser.play('idle') spawnEmoteText(loser.pos.x, loser.pos.y - 40, respectLines[Math.floor(Math.random() * respectLines.length)], '#88ccff') for (let i = 0; i < 3; i++) spawnHeart((winner.pos.x + loser.pos.x) / 2, winner.pos.y - 30) announceCool(heartfeltLines[Math.floor(Math.random() * heartfeltLines.length)]) await k.wait(0.6) await k.tween(winner.pos.x, origWX, 0.3, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad) } // === REFEREE LOBSTER FUNNY MOMENTS === async function judgeDoSomethingFunny() { const judge = k.get('judge')[0] if (!judge) return const judgeOrigY = judge.pos.y const funnyAction = Math.floor(Math.random() * 8) if (funnyAction === 0) { // Falls asleep, snaps awake for (let z = 0; z < 3; z++) { trackedTimeout(() => { spawnEmoteText(judge.pos.x + 15, judge.pos.y - 20, 'Z', '#8888ff', 1.0) }, z * 400) } await k.wait(1.2) judge.play('shocked') k.shake(2) sfxBoing() spawnEmoteText(judge.pos.x, judge.pos.y - 30, '!?', '#ff4444') await k.tween(judge.pos.y, judgeOrigY - 20, 0.1, (v) => { judge.pos.y = v }, k.easings.easeOutQuad) await k.tween(judge.pos.y, judgeOrigY, 0.15, (v) => { judge.pos.y = v }, k.easings.easeInQuad) await k.wait(0.3) judge.play('idle') } else if (funnyAction === 1) { // Little dance on the chair for (let d = 0; d < 4; d++) { await k.tween(judge.pos.y, judgeOrigY - 8, 0.08, (v) => { judge.pos.y = v }, k.easings.easeOutQuad) await k.tween(judge.pos.y, judgeOrigY, 0.08, (v) => { judge.pos.y = v }, k.easings.easeInQuad) judge.play(d % 2 === 0 ? 'call_left' : 'call_right') } sfxRandomComedy() judge.play('idle') } else if (funnyAction === 2) { // Holds up a 10 score card judge.play('shocked') const card = k.add([ k.rect(20, 14), k.pos(judge.pos.x + 20, judge.pos.y - 25), k.color(safeColor(k,'#ffffff')), k.opacity(0.9), k.z(15), k.anchor('center'), ]) const score = k.add([ k.text('10', { size: 10 }), k.pos(judge.pos.x + 20, judge.pos.y - 25), k.color(safeColor(k,'#000000')), k.opacity(1), k.z(16), k.anchor('center'), ]) sfxBoing() await k.wait(1.0) card.destroy(); score.destroy() judge.play('idle') } else if (funnyAction === 3) { // Gets scared and hides behind the chair judge.play('shocked') sfxDodge() await k.tween(judge.pos.y, judgeOrigY + 20, 0.15, (v) => { judge.pos.y = v }, k.easings.easeInQuad) judge.opacity = 0.3 await k.wait(0.8) await k.tween(judge.pos.y, judgeOrigY - 5, 0.2, (v) => { judge.pos.y = v }, k.easings.easeOutQuad) judge.opacity = 0.9 await k.tween(judge.pos.y, judgeOrigY, 0.1, (v) => { judge.pos.y = v }, k.easings.easeInOutQuad) judge.play('idle') } else if (funnyAction === 4) { // Eats a tiny sandwich const sandwich = k.add([ k.rect(10, 8), k.pos(judge.pos.x - 15, judge.pos.y - 15), k.color(safeColor(k,'#ddaa44')), k.opacity(0.9), k.z(15), k.anchor('center'), ]) const lettuce = k.add([ k.rect(12, 2), k.pos(judge.pos.x - 15, judge.pos.y - 15), k.color(safeColor(k,'#44cc22')), k.opacity(0.8), k.z(16), k.anchor('center'), ]) await k.wait(0.4) await k.tween(sandwich.pos.x, judge.pos.x, 0.2, (v) => { sandwich.pos.x = v; lettuce.pos.x = v }, k.easings.easeInQuad) sandwich.destroy(); lettuce.destroy() spawnEmoteText(judge.pos.x, judge.pos.y - 30, 'nom nom', '#ffcc44') sfxBonk() await k.wait(0.6) } else if (funnyAction === 5) { // Takes a selfie judge.play('call_right') const phone = k.add([ k.rect(6, 10), k.pos(judge.pos.x + 20, judge.pos.y - 20), k.color(safeColor(k,'#333344')), k.opacity(0.9), k.z(15), k.anchor('center'), ]) await k.wait(0.3) screenFlash('#ffffff', 0.05) sfxZap() spawnEmoteText(judge.pos.x, judge.pos.y - 35, '#selfie', '#ff88cc') await k.wait(0.5) phone.destroy() judge.play('idle') } else if (funnyAction === 6) { // Waves a tiny flag const flagColors = ['#ff2d2d', '#2d7bff', '#39ff14', '#ffcc00', '#ff6600', '#b83dff'] const flagColor = flagColors[Math.floor(Math.random() * flagColors.length)] const flag = k.add([ k.rect(14, 9), k.pos(judge.pos.x + 18, judge.pos.y - 30), k.color(safeColor(k,flagColor)), k.opacity(0.85), k.z(15), k.anchor('center'), ]) const pole = k.add([ k.rect(2, 18), k.pos(judge.pos.x + 11, judge.pos.y - 22), k.color(safeColor(k,'#aa8855')), k.opacity(0.8), k.z(14), k.anchor('center'), ]) flag.onUpdate(() => { flag.pos.y = judge.pos.y - 30 + Math.sin(k.time() * 10) * 3 }) sfxRandomComedy() await k.wait(1.5) flag.destroy(); pole.destroy() } else { // Falls off the chair, climbs back up judge.play('shocked') sfxBoing() await k.tween(judge.pos.y, GROUND_Y - 6, 0.3, (v) => { judge.pos.y = v }, k.easings.easeInQuad) sfxBonk() k.shake(4) spawnEmoteText(judge.pos.x, GROUND_Y - 20, 'oof!', '#ff6644') await k.wait(0.5) await k.tween(judge.pos.y, judgeOrigY, 0.5, (v) => { judge.pos.y = v }, k.easings.easeOutQuad) judge.play('idle') spawnEmoteText(judge.pos.x, judge.pos.y - 25, '*ahem*', '#aaaaaa') await k.wait(0.3) } } // === SHOWBOATING / TAUNTING SYSTEM === // Fighters perform these during question/answer phases instead of boring idle bobbing const _showboatActive: Record = { a: false, b: false } type ShowboatFn = (fighter: Fighter, homeX: number, homeY: number, sx: number, sy: number, dir: number) => Promise // dir: 1 = facing right (bot A), -1 = facing left (bot B) const showboats: ShowboatFn[] = [ // 0: Pace back and forth — walk a wide arc restlessly async (f, hx, hy, _sx, _sy, dir) => { f.play('kick') await k.tween(f.pos.x, hx + dir * 100, 0.35, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) f.play('idle') await k.wait(0.1) f.play('attack') await k.tween(f.pos.x, hx - dir * 60, 0.4, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) f.play('idle') await k.wait(0.1) await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 1: Bouncing advance — hop toward opponent then back async (f, hx, hy, _sx, _sy, dir) => { for (let i = 0; i < 3; i++) { const targetX = hx + dir * (40 + i * 30) f.play('kick') await Promise.all([ k.tween(f.pos.x, targetX, 0.12, (v) => { f.pos.x = v }, k.easings.easeOutQuad), k.tween(f.pos.y, hy - 35, 0.06, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.06, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) sfxBoing() } f.play('win'); await k.wait(0.15) await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 2: Shadow box sprint — dash forward shadow boxing, dash back async (f, hx, hy, _sx, _sy, dir) => { const far = hx + dir * 120 f.play('attack'); sfxZoomWhoosh() await k.tween(f.pos.x, far, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) for (let i = 0; i < 4; i++) { f.play(i % 2 === 0 ? 'attack' : 'kick') sfxDodge(); await k.wait(0.08) } f.play('special'); sfxZoomWhoosh() await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 3: Leapfrog across — multiple big jumps across the stage async (f, hx, hy, _sx, _sy, dir) => { const spots = [hx + dir * 60, hx + dir * 130, hx + dir * 60, hx] for (const tx of spots) { sfxBoing() await Promise.all([ k.tween(f.pos.x, tx, 0.18, (v) => { f.pos.x = v }, k.easings.easeInOutQuad), k.tween(f.pos.y, hy - 70, 0.09, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.09, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) } }, // 4: Restless jog — jog back and forth with small hops async (f, hx, hy, _sx, _sy, dir) => { f.play('kick') for (let i = 0; i < 5; i++) { const dx = (i % 2 === 0 ? 1 : -1) * (30 + Math.random() * 40) * dir await Promise.all([ k.tween(f.pos.x, hx + dx, 0.13, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 12, 0.065, (v) => { f.pos.y = v }).then(() => k.tween(f.pos.y, hy, 0.065, (v) => { f.pos.y = v }) ), ]) } f.pos.x = hx }, // 5: Charge and flex — sprint to center, flex, sprint back async (f, hx, hy, sx, sy, _dir) => { const center = W / 2 f.play('special'); sfxZoomWhoosh() await k.tween(f.pos.x, center, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad) f.play('win') await k.tween(1, 1.3, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutBack) spawnEmoteText(f.pos.x, f.pos.y - 50, 'COME AT ME!', '#ff4444') await k.wait(0.3) await k.tween(1.3, 1, 0.1, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }) sfxZoomWhoosh() await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 6: Triple backflip retreat — three backflips going backwards async (f, hx, hy, _sx, _sy, dir) => { for (let i = 0; i < 3; i++) { sfxBoing() const targetX = hx - dir * (i + 1) * 30 const a0 = f.angle || 0 await Promise.all([ k.tween(f.pos.x, targetX, 0.2, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 60, 0.1, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), k.tween(a0, a0 - 360, 0.2, (v) => { f.angle = v }), ]) f.angle = 0 } await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 7: Kangaroo hops — big bouncing jumps across the width async (f, hx, hy, _sx, _sy, dir) => { f.play('kick') const hops = [hx + dir * 80, hx + dir * 150, hx + dir * 80, hx] for (const tx of hops) { await Promise.all([ k.tween(f.pos.x, tx, 0.15, (v) => { f.pos.x = v }, k.easings.easeInOutQuad), k.tween(f.pos.y, hy - 90, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) sfxBonk(); k.shake(2) } }, // 8: Feint combo rush — charge in, throw combo, dodge back async (f, hx, hy, _sx, _sy, dir) => { sfxZoomWhoosh(); f.play('attack') await k.tween(f.pos.x, hx + dir * 100, 0.12, (v) => { f.pos.x = v }, k.easings.easeOutQuad) const anims = ['attack', 'kick', 'attack', 'special'] as const for (const a of anims) { f.play(a); sfxDodge(); await k.wait(0.06) } sfxBlock(); spawnSparks(f.pos.x + dir * 15, hy - 30, 5, '#ffcc00') // Dodge-hop back await Promise.all([ k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad), k.tween(f.pos.y, hy - 40, 0.1, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) }, // 9: Dizzy spin walk — spin while walking forward, stumble back async (f, hx, hy, _sx, _sy, dir) => { const a0 = f.angle || 0 f.play('hit') await Promise.all([ k.tween(f.pos.x, hx + dir * 70, 0.4, (v) => { f.pos.x = v }), k.tween(a0, a0 + 720, 0.4, (v) => { f.angle = v }), ]) f.angle = 0 spawnEmoteText(f.pos.x, f.pos.y - 40, '@_@', '#ffcc00') await k.wait(0.15) // Stumble back f.play('idle') for (let i = 0; i < 3; i++) { await k.tween(f.pos.x, f.pos.x - dir * 25, 0.1, (v) => { f.pos.x = v }) f.pos.y = hy - 5; await k.wait(0.02); f.pos.y = hy } }, // 10: Wall-to-wall dash — full screen sprint and back async (f, hx, hy, _sx, _sy, dir) => { f.play('special'); sfxZoomWhoosh() spawnAfterimages(f, 3) await k.tween(f.pos.x, dir > 0 ? W - 30 : 30, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad) sfxBlock(); k.shake(4) await k.wait(0.1) sfxZoomWhoosh(); spawnAfterimages(f, 3) await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 11: Earthquake stomp march — stomp forward and back async (f, hx, hy, sx, sy, dir) => { f.play('attack') for (let i = 0; i < 4; i++) { const step = dir * 25 await k.tween(f.pos.x, f.pos.x + step, 0.08, (v) => { f.pos.x = v }) f.scale.y = sy * 0.8; sfxBonk(); k.shake(3) await k.wait(0.05); f.scale.y = sy } spawnSparks(f.pos.x, hy, 6, '#ff6600') await k.wait(0.1) await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 12: Taunt walk — strut toward opponent, gesture, moonwalk back async (f, hx, hy, _sx, _sy, dir) => { f.play('win') await k.tween(f.pos.x, hx + dir * 90, 0.35, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) f.play('special') spawnEmoteText(f.pos.x, f.pos.y - 50, 'TOO EASY!', '#ff44ff') await k.wait(0.3) // Moonwalk back f.play('idle') await k.tween(f.pos.x, hx, 0.5, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 13: Victory lap sprint — run around in a loop async (f, hx, hy, _sx, _sy, dir) => { f.play('kick'); sfxZoomWhoosh() await k.tween(f.pos.x, hx + dir * 140, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad) // Jump over to other side sfxBoing() await Promise.all([ k.tween(f.pos.x, hx + dir * 140, 0.25, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 100, 0.125, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.125, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) f.play('win'); spawnSparks(f.pos.x, hy - 20, 5, theme.accent) await k.wait(0.15) sfxZoomWhoosh() await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 14: Nervous pacing — quick short paces with jittery hops async (f, hx, hy) => { for (let i = 0; i < 6; i++) { const dx = (Math.random() - 0.5) * 80 await Promise.all([ k.tween(f.pos.x, hx + dx, 0.1, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 10 - Math.random() * 20, 0.05, (v) => { f.pos.y = v }).then(() => k.tween(f.pos.y, hy, 0.05, (v) => { f.pos.y = v }) ), ]) } }, // 15: Ground pound jump — massive vertical jump + slam async (f, hx, hy) => { sfxBoing(); f.play('kick') await k.tween(f.pos.y, hy - 150, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad) await k.wait(0.1) f.play('attack') await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad) sfxExplosion(); k.shake(15) spawnSparks(f.pos.x, hy, 15, '#ff6600') spawnShockwave(f.pos.x, hy, '#ff4400') spawnEmoteText(f.pos.x, f.pos.y - 60, 'BOOM!', '#ff2d2d') await k.wait(0.2) }, // 16: Zigzag sprint — fast zigzag across the arena async (f, hx, hy, _sx, _sy, dir) => { f.play('kick'); sfxZoomWhoosh() const pts = [ { x: hx + dir * 50, y: hy - 40 }, { x: hx + dir * 100, y: hy }, { x: hx + dir * 150, y: hy - 40 }, { x: hx + dir * 100, y: hy }, { x: hx, y: hy }, ] for (const pt of pts) { await Promise.all([ k.tween(f.pos.x, pt.x, 0.08, (v) => { f.pos.x = v }), k.tween(f.pos.y, pt.y, 0.08, (v) => { f.pos.y = v }), ]) } }, // 17: Double jump kick — jump, kick, jump higher, kick async (f, hx, hy, _sx, _sy, dir) => { f.play('kick'); sfxBoing() const mid = hx + dir * 60 await Promise.all([ k.tween(f.pos.x, mid, 0.15, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 50, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) sfxKick(); spawnSparks(mid, hy - 30, 4, '#00ffcc') f.play('special'); sfxBoing() await Promise.all([ k.tween(f.pos.x, mid + dir * 40, 0.15, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 90, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) sfxKick(); spawnSparks(f.pos.x, hy - 50, 6, '#ff44ff') await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 18: Cocky strut and lean — walk forward, lean back with attitude async (f, hx, hy, _sx, _sy, dir) => { f.play('win') await k.tween(f.pos.x, hx + dir * 80, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) // Lean back await k.tween(0, -25, 0.15, (v) => { f.angle = v }, k.easings.easeOutQuad) await k.wait(0.25) spawnEmoteText(f.pos.x, f.pos.y - 50, 'EZ', '#ffcc00') await k.tween(-25, 0, 0.1, (v) => { f.angle = v }) await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 19: Windmill sprint — spin while running across async (f, hx, hy, _sx, _sy, dir) => { const a0 = f.angle || 0 const anims = ['attack', 'kick', 'special'] as const let animIdx = 0 const far = hx + dir * 130 f.play(anims[0]) // Sprint while spinning const dur = 0.4 const startX = f.pos.x await k.tween(0, 1, dur, (t) => { f.pos.x = startX + (far - startX) * t f.angle = a0 + t * 1080 // 3 full spins if (Math.floor(t * 12) !== Math.floor((t - 0.01) * 12)) { f.play(anims[animIdx++ % anims.length]) } }) f.angle = 0; sfxBlock(); k.shake(3) await k.wait(0.1) // Sprint back await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 20: Sumo stomp walk — heavy stomps advancing and retreating async (f, hx, hy, _sx, sy, dir) => { f.play('attack') for (let i = 0; i < 6; i++) { const step = (i < 3 ? dir : -dir) * 20 await k.tween(f.pos.x, f.pos.x + step, 0.1, (v) => { f.pos.x = v }) f.scale.y = sy * 0.8; sfxBonk(); k.shake(3) await k.wait(0.06); f.scale.y = sy spawnSparks(f.pos.x, hy, 2, '#886644') } }, // 21: Flex pose — walk to center, scale up, pose async (f, hx, hy, sx, sy) => { const center = W / 2 f.play('win') await k.tween(f.pos.x, center, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) await k.tween(1, 1.35, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutBack) sfxPowerUp() spawnEmoteText(f.pos.x, f.pos.y - 60, 'COME ON!', '#ff4444') await k.wait(0.35) await k.tween(1.35, 1, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }) await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 22: Jump-kick air combo — big jump with mid-air kicks async (f, hx, hy, _sx, _sy, dir) => { sfxBoing() const apex = hy - 120 const far = hx + dir * 100 await k.tween(f.pos.y, apex, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad) // Mid-air combo while moving f.play('kick'); sfxKick() await k.tween(f.pos.x, far, 0.1, (v) => { f.pos.x = v }) f.play('attack'); sfxPunch() await k.tween(f.pos.x, far + dir * 30, 0.08, (v) => { f.pos.x = v }) f.play('special') spawnSparks(f.pos.x, f.pos.y, 8, '#ff44ff') await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad) sfxBonk(); k.shake(4) await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 23: Air guitar walk — bouncing left and right with music async (f, hx, hy, _sx, _sy, dir) => { f.play('special'); sfxSlideUp() spawnEmoteText(f.pos.x, f.pos.y - 50, '~♪~', '#ff44ff') for (let i = 0; i < 6; i++) { const dx = (i % 2 === 0 ? dir : -dir) * (30 + i * 10) await Promise.all([ k.tween(f.pos.x, hx + dx, 0.12, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 15, 0.06, (v) => { f.pos.y = v }).then(() => k.tween(f.pos.y, hy, 0.06, (v) => { f.pos.y = v }) ), ]) } f.pos.x = hx }, // 24: Crane kick walk — slow martial arts advancing async (f, hx, hy, _sx, sy, dir) => { f.play('kick') await Promise.all([ k.tween(f.pos.x, hx + dir * 60, 0.3, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 30, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad), k.tween(1, 1.15, 0.3, (v) => { f.scale.y = sy * v }, k.easings.easeOutQuad), ]) await k.wait(0.3) f.play('special'); sfxKick() spawnSparks(f.pos.x + dir * 20, hy - 40, 6, '#00ccff') await k.wait(0.15) f.scale.y = sy await Promise.all([ k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad), k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad), ]) }, // 25: Girlfriend kiss — girlfriend walks in, kiss, hearts, she storms off async (f, hx, hy, _sx, _sy, dir) => { const startGfX = hx - dir * 120 const gfColor = ['#ff69b4', '#ff1493', '#ff6eb4', '#cc44aa'][Math.floor(Math.random() * 4)] const cs = SF const gf = k.add([k.rect(18 * cs, 35 * cs), k.pos(startGfX, hy - 18 * cs), k.anchor('center'), k.color(safeColor(k, gfColor)), k.opacity(0.9), k.z(11)]) const gfHair = k.add([k.rect(22 * cs, 10 * cs), k.pos(startGfX, hy - 35 * cs), k.anchor('center'), k.color(safeColor(k, '#8b4513')), k.opacity(0.9), k.z(12)]) // Walk toward fighter await Promise.all([ k.tween(gf.pos.x, hx - dir * 15, 0.4, (v) => { gf.pos.x = v; gfHair.pos.x = v }, k.easings.easeInOutQuad), ]) // Kiss! sfxBoing() const heart = k.add([k.text('*smooch*', { size: 10 }), k.pos(hx, hy - 55), k.anchor('center'), k.color(safeColor(k, '#ff1493')), k.opacity(1), k.z(30)]) k.tween(heart.pos.y, hy - 90, 0.8, (v) => { heart.pos.y = v }) k.tween(1, 0, 0.8, (v) => { heart.opacity = v }).then(() => { if (heart.exists()) heart.destroy() }) // Spawn floating hearts for (let i = 0; i < 5; i++) { const h = k.add([k.text('♥', { size: 8 + Math.random() * 6 }), k.pos(hx + (Math.random() - 0.5) * 30, hy - 30), k.color(safeColor(k, '#ff69b4')), k.opacity(0.8), k.z(25), k.anchor('center')]) k.tween(h.pos.y, hy - 70 - Math.random() * 40, 0.7, (v) => { h.pos.y = v }) k.tween(h.pos.x, h.pos.x + (Math.random() - 0.5) * 40, 0.7, (v) => { h.pos.x = v }) k.tween(0.8, 0, 0.7, (v) => { h.opacity = v }).then(() => { if (h.exists()) h.destroy() }) } f.play('win') await k.wait(0.5) // She storms off spawnEmoteText(gf.pos.x, gf.pos.y - 30, 'Good luck babe!', '#ff69b4') await k.tween(gf.pos.x, startGfX, 0.4, (v) => { gf.pos.x = v; gfHair.pos.x = v }, k.easings.easeInQuad) gf.destroy(); gfHair.destroy() }, // 26: Push-up walk — walk forward doing push-ups at intervals async (f, hx, hy, sx, sy, dir) => { for (let i = 0; i < 3; i++) { await k.tween(f.pos.x, f.pos.x + dir * 35, 0.15, (v) => { f.pos.x = v }) // Push-up squash await k.tween(1, 0.55, 0.08, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeOutQuad) await k.tween(0.55, 1, 0.08, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeInQuad) } spawnEmoteText(f.pos.x, f.pos.y - 45, `${3 + Math.floor(Math.random() * 97)}!`, '#00ff88') await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 27: Matrix dodge walk — dodge bullets walking forward async (f, hx, hy, _sx, _sy, dir) => { for (let i = 0; i < 4; i++) { await k.tween(f.pos.x, f.pos.x + dir * 25, 0.1, (v) => { f.pos.x = v }) // Lean dodge const lean = (i % 2 === 0 ? -30 : 30) await k.tween(0, lean, 0.08, (v) => { f.angle = v }, k.easings.easeOutQuad) await k.tween(lean, 0, 0.08, (v) => { f.angle = v }, k.easings.easeInQuad) } await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 28: Bouncy chest thump — bounce toward opponent thumping chest async (f, hx, hy, _sx, _sy, dir) => { f.play('attack') for (let i = 0; i < 4; i++) { await Promise.all([ k.tween(f.pos.x, f.pos.x + dir * 30, 0.1, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 25, 0.05, (v) => { f.pos.y = v }).then(() => k.tween(f.pos.y, hy, 0.05, (v) => { f.pos.y = v }) ), ]) sfxBlock(); k.shake(2) } spawnEmoteText(f.pos.x, f.pos.y - 50, 'LET\'S GO!', '#ff4444') await k.wait(0.15) await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 29: Speed demon — afterimage dash back and forth async (f, hx, hy, _sx, _sy, dir) => { f.play('special') for (let i = 0; i < 3; i++) { sfxZoomWhoosh(); spawnAfterimages(f, 3) await k.tween(f.pos.x, hx + dir * (80 + i * 30), 0.08, (v) => { f.pos.x = v }, k.easings.easeInQuad) spawnAfterimages(f, 2) await k.tween(f.pos.x, hx - dir * 20, 0.08, (v) => { f.pos.x = v }, k.easings.easeOutQuad) } f.pos.x = hx }, ] // === CREATOR-EXCLUSIVE MAGICAL SHOWBOATS === const creatorShowboats: ShowboatFn[] = [ // 0: Levitate across — float up and glide across the screen trailing golden ₿ async (f, hx, hy, _sx, _sy, dir) => { f.play('special'); sfxPowerUp() await k.tween(f.pos.y, hy - 100, 0.25, (v) => { f.pos.y = v }, k.easings.easeOutQuad) // Float across dropping ₿ rain const startX = f.pos.x const far = hx + dir * 180 const dur = 0.6 let lastDrop = 0 await k.tween(0, 1, dur, (t) => { f.pos.x = startX + (far - startX) * t f.pos.y = hy - 100 + Math.sin(t * Math.PI * 3) * 20 if (t - lastDrop > 0.08) { lastDrop = t const d = k.add([k.text('₿', { size: 6 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y + 10), k.color(safeColor(k, '#ffd700')), k.opacity(0.7), k.z(9), k.anchor('center')]) k.tween(d.pos.y, hy + 10, 0.5, (v) => { d.pos.y = v }) k.tween(0.7, 0, 0.5, (v) => { d.opacity = v }).then(() => { if (d.exists()) d.destroy() }) } }) screenFlash('#ffd700', 0.06) await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad) }, // 1: Golden portal teleport — vanish, portals, reappear at random spots async (f, hx, hy) => { sfxSpecial() // Vanish with flash screenFlash('#ffd700', 0.08) f.opacity = 0 // Teleport to 3 spots with golden portals const spots = [ { x: W * 0.2, y: hy - 60 }, { x: W * 0.8, y: hy - 80 }, { x: W * 0.5, y: hy - 120 }, ] for (const spot of spots) { // Portal flash at destination const portal = k.add([k.circle(20), k.pos(spot.x, spot.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.6), k.z(15), k.anchor('center')]) spawnSparks(spot.x, spot.y, 6, '#ffd700') f.pos.x = spot.x; f.pos.y = spot.y; f.opacity = 1 f.play('special') await k.wait(0.15) f.opacity = 0 k.tween(0.6, 0, 0.2, (v) => { portal.opacity = v }).then(() => { if (portal.exists()) portal.destroy() }) sfxZap() } // Return home with big flash screenFlash('#ffd700', 0.1) f.pos.x = hx; f.pos.y = hy; f.opacity = 1 f.play('win') spawnShockwave(hx, hy, '#ffd700') sfxExplosion() await k.wait(0.2) }, // 2: Code rain summon — float up, summon massive golden code rain async (f, hx, hy) => { f.play('special'); sfxPowerUp() await k.tween(f.pos.y, hy - 80, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad) // Massive code rain const chars = '₿01∞⚡⛏§∂∆≈≠'.split('') for (let col = 0; col < 20; col++) { const cx = Math.random() * W for (let row = 0; row < 3; row++) { const ch = chars[Math.floor(Math.random() * chars.length)] const d = k.add([ k.text(ch, { size: 6 + Math.random() * 4 }), k.pos(cx, -10 - row * 20), k.color(safeColor(k, row === 0 ? '#ffd700' : '#c8a000')), k.opacity(0.5 + Math.random() * 0.4), k.z(8), k.anchor('center'), ]) k.tween(d.pos.y, hy + 30, 0.5 + Math.random() * 0.4, (v) => { d.pos.y = v; d.opacity = Math.max(0, 1 - (v - hy + 30) / 60) }) .then(() => { if (d.exists()) d.destroy() }) } } await k.wait(0.6) await k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad) }, // 3: ₿ orbit explosion — fly to center, ₿ symbols orbit then explode outward async (f, hx, hy, sx, sy) => { sfxZoomWhoosh(); f.play('special') const center = W / 2 await Promise.all([ k.tween(f.pos.x, center, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad), k.tween(f.pos.y, hy - 60, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad), ]) // Spawn orbiting ₿ const orbs: GameObj[] = [] for (let i = 0; i < 8; i++) { const orb = k.add([k.text('₿', { size: 8 + Math.random() * 6 }), k.pos(center, f.pos.y), k.color(safeColor(k, i % 2 === 0 ? '#ffd700' : '#ff8c00')), k.opacity(0.8), k.z(15), k.anchor('center')]) orbs.push(orb) } // Orbit for a moment const startTime = k.time() const orbitCancel = k.onUpdate(() => { const t = k.time() - startTime for (let i = 0; i < orbs.length; i++) { const a = t * 4 + i * (Math.PI * 2 / orbs.length) const r = 30 + t * 15 orbs[i].pos.x = f.pos.x + Math.cos(a) * r orbs[i].pos.y = f.pos.y + Math.sin(a) * r * 0.5 } }) await k.wait(0.5) orbitCancel.cancel() // Explode outward sfxExplosion(); screenFlash('#ffd700', 0.08); k.shake(8) for (const orb of orbs) { const angle = Math.random() * Math.PI * 2 k.tween(orb.pos.x, orb.pos.x + Math.cos(angle) * 200, 0.3, (v) => { orb.pos.x = v }) k.tween(orb.pos.y, orb.pos.y + Math.sin(angle) * 200, 0.3, (v) => { orb.pos.y = v }) k.tween(0.8, 0, 0.3, (v) => { orb.opacity = v }).then(() => { if (orb.exists()) orb.destroy() }) } await k.wait(0.2) await Promise.all([ k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad), k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad), ]) }, // 4: Lightning flight — zigzag across the screen at super speed with lightning async (f, hx, hy, _sx, _sy, dir) => { f.play('special'); sfxZoomWhoosh() f.opacity = 0.8 const pts = [ { x: hx + dir * 60, y: hy - 80 }, { x: W / 2, y: hy - 140 }, { x: hx + dir * 180, y: hy - 60 }, { x: W / 2, y: hy - 100 }, { x: hx, y: hy }, ] for (const pt of pts) { spawnAfterimages(f, 2) await Promise.all([ k.tween(f.pos.x, pt.x, 0.08, (v) => { f.pos.x = v }), k.tween(f.pos.y, pt.y, 0.08, (v) => { f.pos.y = v }), ]) // Lightning bolt at each stop sfxZap() const bolt = k.add([k.rect(2, 30 + Math.random() * 40), k.pos(f.pos.x, f.pos.y - 50), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(20), k.anchor('center'), k.rotate(Math.random() * 30 - 15)]) k.tween(0.9, 0, 0.15, (v) => { bolt.opacity = v }).then(() => { if (bolt.exists()) bolt.destroy() }) } f.opacity = 1 }, // 5: Satoshi meditation — float to center, golden aura pulse, wisdom text async (f, hx, hy, sx, sy) => { sfxPowerUp(); f.play('special') await Promise.all([ k.tween(f.pos.x, W / 2, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad), k.tween(f.pos.y, hy - 70, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad), ]) // Golden aura rings for (let i = 0; i < 3; i++) { const ring = k.add([k.circle(10), k.pos(f.pos.x, f.pos.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.5), k.z(9), k.anchor('center'), k.scale(1)]) k.tween(1, (60 + i * 20) / 10, 0.4, (v) => { ring.scale.x = v; ring.scale.y = v }) k.tween(0.5, 0, 0.4, (v) => { ring.opacity = v }).then(() => { if (ring.exists()) ring.destroy() }) await k.wait(0.12) } // Scale pulse await k.tween(1, 1.2, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutQuad) const sayings = ['PROOF OF WORK', 'VERIFY, DON\'T TRUST', '21 MILLION', 'TICK TOCK NEXT BLOCK', 'NOT YOUR KEYS', 'STAY HUMBLE STACK SATS'] spawnEmoteText(f.pos.x, f.pos.y - 50, sayings[Math.floor(Math.random() * sayings.length)], '#ffd700') await k.wait(0.4) await k.tween(1.2, 1, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }) await Promise.all([ k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad), k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad), ]) }, // 6: Golden tornado — spin rapidly while moving in a circle across the arena async (f, hx, hy) => { sfxZoomWhoosh(); f.play('special') const cx = W / 2, cy = hy - 50, radius = 100 const dur = 0.8 let lastParticle = 0 await k.tween(0, Math.PI * 2, dur, (a) => { f.pos.x = cx + Math.cos(a) * radius f.pos.y = cy + Math.sin(a) * radius * 0.4 f.angle = (a / (Math.PI * 2)) * 1440 // 4 full spins if (a - lastParticle > 0.3) { lastParticle = a const p = k.add([k.text('₿', { size: 5 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.6), k.z(9), k.anchor('center')]) k.tween(0.6, 0, 0.4, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() }) } }) f.angle = 0 sfxExplosion(); k.shake(6) spawnShockwave(f.pos.x, f.pos.y, '#ffd700') f.pos.x = hx; f.pos.y = hy }, // 7: Laptop hack — walk forward, pull out laptop (rect), type furiously, spawn effects async (f, hx, hy, _sx, _sy, dir) => { f.play('special') await k.tween(f.pos.x, hx + dir * 50, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) // Laptop const cs = SF const laptop = k.add([k.rect(24 * cs, 16 * cs), k.pos(f.pos.x + dir * 15, hy - 20 * cs), k.anchor('center'), k.color(safeColor(k, '#333333')), k.opacity(0.9), k.z(12)]) const screen = k.add([k.rect(20 * cs, 12 * cs), k.pos(f.pos.x + dir * 15, hy - 22 * cs), k.anchor('center'), k.color(safeColor(k, '#00ff00')), k.opacity(0.7), k.z(13)]) // Type — spawn code characters const codeChars = '₿{}();=>0x1f'.split('') for (let i = 0; i < 8; i++) { const ch = codeChars[Math.floor(Math.random() * codeChars.length)] const t = k.add([k.text(ch, { size: 5 }), k.pos(screen.pos.x + (Math.random() - 0.5) * 15, screen.pos.y), k.color(safeColor(k, '#00ff00')), k.opacity(0.8), k.z(14), k.anchor('center')]) k.tween(t.pos.y, t.pos.y - 25 - Math.random() * 20, 0.3, (v) => { t.pos.y = v }) k.tween(0.8, 0, 0.3, (v) => { t.opacity = v }).then(() => { if (t.exists()) t.destroy() }) sfxCoin() await k.wait(0.06) } spawnEmoteText(f.pos.x, f.pos.y - 55, 'HACKING...', '#00ff00') await k.wait(0.2) laptop.destroy(); screen.destroy() await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad) }, // 8: Bitcoin rain shower — fly across dropping ₿ coins everywhere async (f, hx, hy, _sx, _sy, dir) => { sfxPowerUp(); f.play('win') await k.tween(f.pos.y, hy - 90, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad) // Fly from one side to the other const startX = dir > 0 ? 30 : W - 30 const endX = dir > 0 ? W - 30 : 30 f.pos.x = startX const dur = 0.6 let lastCoin = 0 await k.tween(0, 1, dur, (t) => { f.pos.x = startX + (endX - startX) * t f.pos.y = hy - 90 + Math.sin(t * Math.PI * 4) * 15 if (t - lastCoin > 0.04) { lastCoin = t const coin = k.add([k.text('₿', { size: 8 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y + 15), k.color(safeColor(k, Math.random() > 0.5 ? '#ffd700' : '#ff8c00')), k.opacity(0.8), k.z(9), k.anchor('center')]) k.tween(coin.pos.y, hy + 10, 0.4, (v) => { coin.pos.y = v }) k.tween(coin.pos.x, coin.pos.x + (Math.random() - 0.5) * 30, 0.4, (v) => { coin.pos.x = v }) k.tween(0.8, 0, 0.4, (v) => { coin.opacity = v }).then(() => { if (coin.exists()) coin.destroy() }) } }) sfxCoin() f.pos.x = hx await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad) }, // 9: Dimension rift — creator tears open a golden rift, steps through, appears elsewhere async (f, hx, hy) => { // Rift opens sfxSpecial() const riftX = f.pos.x + 30 const rift = k.add([k.rect(4, 50), k.pos(riftX, hy - 25), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(15)]) await k.tween(4, 30, 0.2, (v) => { rift.width = v }) spawnSparks(riftX, hy - 25, 8, '#ffd700') // Step through — vanish f.play('special') await k.tween(f.pos.x, riftX, 0.1, (v) => { f.pos.x = v }) f.opacity = 0; sfxZap() // Rift at destination const destX = W - riftX const rift2 = k.add([k.rect(30, 50), k.pos(destX, hy - 25), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(15)]) spawnSparks(destX, hy - 25, 8, '#ffd700') await k.wait(0.15) // Step out f.pos.x = destX; f.opacity = 1; f.play('win') sfxSpecial() await k.wait(0.2) // Close rifts k.tween(0.9, 0, 0.2, (v) => { rift.opacity = v; rift2.opacity = v }).then(() => { if (rift.exists()) rift.destroy(); if (rift2.exists()) rift2.destroy() }) // Float home await Promise.all([ k.tween(f.pos.x, hx, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad), k.tween(f.pos.y, hy - 40, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() => k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad) ), ]) }, // 10: Golden ascension — rise way up, glow, descend with shockwave async (f, hx, hy, sx, sy) => { sfxPowerUp(); f.play('special') await k.tween(f.pos.y, hy - 160, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad) // Glow pulse await k.tween(1, 1.5, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutQuad) screenFlash('#ffd700', 0.06) // Spawn golden aura particles for (let i = 0; i < 10; i++) { const p = k.add([k.text('✦', { size: 4 + Math.random() * 6 }), k.pos(f.pos.x + (Math.random() - 0.5) * 40, f.pos.y + (Math.random() - 0.5) * 30), k.color(safeColor(k, '#ffd700')), k.opacity(0.7), k.z(9), k.anchor('center')]) k.tween(0.7, 0, 0.5, (v) => { p.opacity = v; p.pos.y -= 0.5 }).then(() => { if (p.exists()) p.destroy() }) } spawnEmoteText(f.pos.x, f.pos.y - 40, 'HODL', '#ffd700') await k.wait(0.3) await k.tween(1.5, 1, 0.1, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }) // Slam down await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad) sfxExplosion(); k.shake(12) spawnShockwave(hx, hy, '#ffd700') spawnSparks(hx, hy, 15, '#ffd700') await k.wait(0.15) }, // 11: Phase walk — flicker opacity while walking through the opponent's space async (f, hx, hy, _sx, _sy, dir) => { f.play('special'); sfxZap() const far = dir > 0 ? W - 40 : 40 const dur = 0.5 await k.tween(0, 1, dur, (t) => { f.pos.x = hx + (far - hx) * t f.opacity = 0.2 + Math.abs(Math.sin(t * Math.PI * 8)) * 0.7 }) f.opacity = 1; sfxSpecial() screenFlash('#ffd700', 0.05) spawnSparks(f.pos.x, hy - 20, 6, '#ffd700') await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 12: Bullet time attack — slow-mo dash with afterimages + golden bullet traces async (f, hx, hy, _sx, _sy, dir) => { f.play('attack') // Darken screen for bullet time const overlay = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(50)]) await k.tween(0, 0.35, 0.08, (v) => { overlay.opacity = v }) // Slow-mo lean back await k.tween(0, -30, 0.2, (v) => { f.angle = v }, k.easings.easeOutQuad) // Golden bullet traces fly past for (let i = 0; i < 6; i++) { const by = hy - 15 - Math.random() * 50 const bullet = k.add([k.rect(15, 2), k.pos(dir > 0 ? W + 10 : -10, by), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(51), k.anchor('center')]) const bdir = dir > 0 ? -1 : 1 k.tween(bullet.pos.x, bullet.pos.x + bdir * (W + 30), 0.35, (v) => { bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() }) sfxZoomWhoosh() await k.wait(0.04) } await k.wait(0.1) // Snap upright and dash through await k.tween(-30, 0, 0.06, (v) => { f.angle = v }) f.play('special'); sfxZoomWhoosh() // Dash with afterimages const endX = hx + dir * 160 for (let i = 0; i < 6; i++) { const ghostX = hx + (endX - hx) * (i / 6) const ghost = k.add([k.rect(16, 30), k.pos(ghostX, hy - 15), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.3 - i * 0.04), k.z(49)]) k.tween(0.3, 0, 0.4, (v) => { ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() }) } await k.tween(f.pos.x, endX, 0.12, (v) => { f.pos.x = v }, k.easings.easeInQuad) screenFlash('#ffd700', 0.06); sfxCritical(); k.shake(8) spawnSparks(f.pos.x, hy - 20, 10, '#ffd700') await k.wait(0.15) // Zip back await k.tween(overlay.opacity, 0, 0.1, (v) => { overlay.opacity = v }) overlay.destroy() await k.tween(f.pos.x, hx, 0.1, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, // 13: ₿ throne summon — conjure golden throne, sit imperiously, dismiss async (f, hx, hy) => { sfxSpecial() const cs = SF // Throne pieces const seat = k.add([k.rect(40 * cs, 8 * cs), k.pos(hx, hy - 5), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)]) const back = k.add([k.rect(35 * cs, 30 * cs), k.pos(hx, hy - 25 * cs), k.anchor('center'), k.color(safeColor(k, '#c8a000')), k.opacity(0), k.z(8)]) const armL = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx - 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)]) const armR = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx + 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)]) const parts = [seat, back, armL, armR] // Fade in for (const p of parts) k.tween(0, 0.8, 0.2, (v) => { p.opacity = v }) spawnSparks(hx, hy - 15, 6, '#ffd700') await k.wait(0.25) // Sit f.play('win') await k.tween(f.pos.y, hy - 12, 0.12, (v) => { f.pos.y = v }, k.easings.easeInQuad) spawnEmoteText(hx, f.pos.y - 50, 'kneel', '#ffd700') await k.wait(0.6) // Stand and dismiss await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }) for (const p of parts) k.tween(p.opacity, 0, 0.2, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() }) await k.wait(0.2) }, // 14: Disco dance — Creator gets down with golden disco moves async (f, hx, hy, _sx, _sy, dir) => { f.play('win') const colors = ['#ffd700', '#ff8c00', '#ffee88', '#c8a000', '#ffffff', '#ffd700'] spawnEmoteText(f.pos.x, f.pos.y - 55, '~♪ SATOSHI SHUFFLE ♪~', '#ffd700') for (let i = 0; i < 10; i++) { const dx = (i % 2 === 0 ? 25 : -25) + (Math.random() - 0.5) * 15 f.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'win') await Promise.all([ k.tween(f.pos.x, hx + dx, 0.06, (v) => { f.pos.x = v }), k.tween(f.pos.y, hy - 15, 0.03, (v) => { f.pos.y = v }).then(() => k.tween(f.pos.y, hy, 0.03, (v) => { f.pos.y = v })), ]) spawnSparks(f.pos.x, hy - 10, 2, colors[i % colors.length]) } f.pos.x = hx }, ] // Showboat loop: picks random showboats in sequence while active async function _showboatLoop(side: 'a' | 'b') { const tag = side === 'a' ? 'fighterA' : 'fighterB' const homeX = side === 'a' ? HOME_A : HOME_B const homeY = GROUND_Y - 6 const dir = side === 'a' ? 1 : -1 const arch = side === 'a' ? botA.archetype : botB.archetype const isCreator = arch === 'the_creator' // Creator uses magical showboats 60% of the time, normal 40% const pool = isCreator ? [...showboats, ...creatorShowboats, ...creatorShowboats] : showboats while (_showboatActive[side]) { const f = k.get(tag)[0] as Fighter | undefined if (!f) break const sx = f.scale.x const sy = f.scale.y const idx = Math.floor(Math.random() * pool.length) try { await pool[idx](f, homeX, homeY, sx, sy, dir) // 40% chance of comedy sound punctuation after each showboat if (Math.random() < 0.4) sfxRandomComedy() } catch { break } // Restore state after each showboat const ff = k.get(tag)[0] if (ff) { ff.pos.x = homeX; ff.pos.y = homeY ff.scale.x = sx; ff.scale.y = sy ff.angle = 0; ff.opacity = 1 ff.play('idle') } // Brief pause between showboats if (_showboatActive[side]) await k.wait(0.15 + Math.random() * 0.35) } } // === EXTRACTED SYSTEMS === const entranceSystem = createEntranceSystem(choreoCtx, { HOME_A, HOME_B, botA, botB }) const roundSystem = createRoundSystem(choreoCtx, { HOME_A, HOME_B, botA, botB, choreographyMap, playBlock, destroyMorphOverlays, playMorph, getMorphOrder, applyCreatorOmniMorph, spawnEmoteText, spawnCrowdSigns, judgeDoSomethingFunny, maybeShowHuman, cameraZoom, hyperSpeedLines, schizoCut, showSpeechBubble, }) const finisherSystem = createFinisherSystem(choreoCtx, { HOME_A, HOME_B, botA, botB, spawnEmoteText, spawnCrowdSigns, spawnHeart, spawnWeaponProp, destroyProp, spawnProp, playBow, playFistBump, playHelpUp, cameraZoom, hyperSpeedLines, showSpeechBubble, spawnHumanCoach, }) return { k, async showAnnouncement(_text: string, _color: string = '#ffffff', _duration: number = 1200) {}, showSpeechBubble(side: 'a' | 'b', text: string, duration?: number) { showSpeechBubble(side, text, duration) }, startTalking(side: 'a' | 'b') { startTalking(side) }, stopTalking(side: 'a' | 'b') { stopTalking(side) }, startShowboating(side: 'a' | 'b') { if (_showboatActive[side]) return _showboatActive[side] = true _showboatLoop(side) }, stopShowboating(side: 'a' | 'b') { _showboatActive[side] = false }, async playEntrance() { return entranceSystem.playEntrance() }, // Physical contact delegates _spawnDizzyStars(fighter: Fighter, duration: number) { return roundSystem._spawnDizzyStars(fighter, duration) }, _resetPositions() { return roundSystem._resetPositions() }, _brawlRapid(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlRapid(winningSide, intensity) }, _brawlKnockdown(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlKnockdown(winningSide, intensity) }, _brawlWallBounce(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlWallBounce(winningSide, intensity) }, _brawlDizzyStagger(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlDizzyStagger(winningSide, intensity) }, _brawlSuplex(attackerSide: 'a' | 'b', intensity: number) { return roundSystem._brawlSuplex(attackerSide, intensity) }, _brawlPingPong(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlPingPong(winningSide, intensity) }, _brawlGroundPound(attackerSide: 'a' | 'b', intensity: number) { return roundSystem._brawlGroundPound(attackerSide, intensity) }, _brawlHaymaker(attackerSide: 'a' | 'b', _intensity: number) { return roundSystem._brawlHaymaker(attackerSide, _intensity) }, _brawlBodyCheck(winningSide: 'a' | 'b' | null, _intensity: number) { return roundSystem._brawlBodyCheck(winningSide, _intensity) }, _brawlExchange(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlExchange(winningSide, intensity) }, _clinchCombo(attackerSide: 'a' | 'b', intensity: number) { return roundSystem._clinchCombo(attackerSide, intensity) }, _counterAttack(defenderSide: 'a' | 'b') { return roundSystem._counterAttack(defenderSide) }, async playAttack(side: 'a' | 'b', choreographyName: string, isCritical: boolean) { return roundSystem.playAttack(side, choreographyName, isCritical) }, async playRound(event: RoundEvent) { return roundSystem.playRound(event) }, async playTaunt(side: 'a' | 'b') { return roundSystem.playTaunt(side) }, async playDodge(side: 'a' | 'b') { return roundSystem.playDodge(side) }, async playKO(winningSide: 'a' | 'b', winnerName: string) { return finisherSystem.playKO(winningSide, winnerName) }, async playPerfect(winningSide: 'a' | 'b', winnerName: string) { return finisherSystem.playPerfect(winningSide, winnerName) }, async playVictoryCelebration(winningSide: 'a' | 'b', isUpset: boolean) { return finisherSystem.playVictoryCelebration(winningSide, isUpset) }, startMusic() { startMusic() }, stopMusic() { stopMusic() }, destroy() { 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() }, } } export type FightSceneController = Awaited>