diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index bf9e3b0..f769d2e 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -17,6 +17,8 @@ import { createMorphSystem } from './fight/morphs' import { createEntranceSystem } from './fight/entrances' import { createRoundSystem } from './fight/rounds' import { createFinisherSystem } from './fight/finishers' +import { drawArenaDecor } from './fight/arena-renderer' +import { createShowboatSystem } from './fight/showboats' import { sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit, sfxJetpack, sfxExplosion, sfxBlock, sfxDodge, sfxClash, sfxRandomSilly, @@ -349,1241 +351,6 @@ export async function createFightScene(config: FightSceneConfig) { // 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 === @@ -1666,7 +433,7 @@ export async function createFightScene(config: FightSceneConfig) { } // Arena-specific decorations - drawArenaDecor() + drawArenaDecor({ k, W, H, GROUND_Y, arena, theme, safeColor }) // Umpire chair (center-back, behind fighters) const CHAIR_X = W / 2 @@ -2389,876 +1156,10 @@ export async function createFightScene(config: FightSceneConfig) { } } - // === 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 + // === SHOWBOAT SYSTEM (extracted to fight/showboats.ts) === + const showboatSystem = createShowboatSystem(choreoCtx, { HOME_A, HOME_B, SF, botA, botB, spawnEmoteText }) - // 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 }) @@ -3292,14 +1193,8 @@ export async function createFightScene(config: FightSceneConfig) { 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 - }, + startShowboating(side: 'a' | 'b') { showboatSystem.startShowboating(side) }, + stopShowboating(side: 'a' | 'b') { showboatSystem.stopShowboating(side) }, async playEntrance() { return entranceSystem.playEntrance() diff --git a/frontend/src/game/fight/arena-renderer.ts b/frontend/src/game/fight/arena-renderer.ts new file mode 100644 index 0000000..b735ebe --- /dev/null +++ b/frontend/src/game/fight/arena-renderer.ts @@ -0,0 +1,1251 @@ +import type kaplay from 'kaplay' +import { isPerfMode } from './config' + +type KaplayInstance = ReturnType +type SafeColorFn = (k: KaplayInstance, color: string) => ReturnType + +interface ArenaRenderCtx { + k: KaplayInstance + W: number + H: number + GROUND_Y: number + arena: string + theme: { bg: string; ground: string; accent: string } + safeColor: SafeColorFn +} + +export function drawArenaDecor(ctx: ArenaRenderCtx): void { + const { k, W, H, GROUND_Y, arena, theme, safeColor } = ctx + 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 + }) + } + } +} diff --git a/frontend/src/game/fight/showboats.ts b/frontend/src/game/fight/showboats.ts new file mode 100644 index 0000000..6b7a1a8 --- /dev/null +++ b/frontend/src/game/fight/showboats.ts @@ -0,0 +1,901 @@ +import type { GameObj } from 'kaplay' +import type { Fighter, ChoreoContext } from './types' +import { sfxRandomComedy, sfxPowerUp, sfxDodge, sfxSlideUp } from '../audio' + +interface ShowboatDeps { + HOME_A: number + HOME_B: number + SF: number + botA: { name: string; archetype?: string } + botB: { name: string; archetype?: string } + spawnEmoteText: (x: number, y: number, text: string, color: string, duration?: number) => void +} + +type ShowboatFn = (fighter: Fighter, homeX: number, homeY: number, sx: number, sy: number, dir: number) => Promise + +export function createShowboatSystem(ctx: ChoreoContext, deps: ShowboatDeps) { + const { + k, W, H, GROUND_Y, theme, safeColor, spawnSparks, screenFlash, spawnShockwave, spawnAfterimages, + sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion, sfxBoing, sfxBonk, sfxCoin, + sfxZap, sfxZoomWhoosh, sfxBlock, + } = ctx + const { HOME_A, HOME_B, SF, botA, botB, spawnEmoteText } = deps + + const _showboatActive: Record = { a: false, b: false } + + 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(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('#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('#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('#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('#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('#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(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(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('#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('#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('#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('#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('#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('#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(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('#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('#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('#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('#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('#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('#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('#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('#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('#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('#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) + } + } + + function startShowboating(side: 'a' | 'b') { + if (_showboatActive[side]) return + _showboatActive[side] = true + _showboatLoop(side) + } + + function stopShowboating(side: 'a' | 'b') { + _showboatActive[side] = false + } + + return { startShowboating, stopShowboating } +}