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 } }