refactor: extract arena renderer and showboat system from FightScene.ts
- drawArenaDecor (1,234 lines) → fight/arena-renderer.ts - showboat/taunt system (870 lines) → fight/showboats.ts - FightScene.ts: 3,468 → 1,266 lines (scene setup + orchestration only) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b6033a90e5
commit
b28d0b220e
+7
-2112
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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<void>
|
||||||
|
|
||||||
|
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<string, boolean> = { 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 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user