refactor: split choreographies into category modules

Extract 457 choreography functions from FightScene.ts into 11 modular files
under fight/choreography/. ChoreoContext interface bundles all closure deps
(k, effects, SFX) so function bodies remain unchanged. Factory pattern
(createFactories) shares prop system and make* helpers across categories.

Categories: basic (72), ranged (94), silly (80), food (45), weapons (45),
ultimates (41), bitcoin (34), heavy (30), creator (16), plus factories
and index. FightScene.ts reduced from 11,917 to 6,515 lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 23:00:57 +00:00
co-authored by Claude Opus 4.6
parent 08786a9b06
commit cabe0adb31
13 changed files with 5874 additions and 5422 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,565 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createBitcoinChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
async function lightningBolt(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.12)
const segments = isCritical ? 8 : 5
let px = atk.pos.x + dir * 30, py = atk.pos.y - 40
const bolts: any[] = []
for (let i = 0; i < segments; i++) {
const nx = px + dir * 30 + (Math.random() - 0.5) * 20
const ny = py + (Math.random() - 0.5) * 30
const bolt = k.add([k.rect(3, 25), k.pos(px, py), k.color(safeColor('#ffdd00')), k.opacity(1), k.z(18), k.rotate(Math.atan2(ny - py, nx - px) * 180 / Math.PI)])
bolts.push(bolt)
sfxBulletHit(); px = nx; py = ny; await k.wait(0.02)
}
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 6)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 14 : 6, '#ffdd00')
if (isCritical) { screenFlash('#ffff00'); sfxCritical() }
await k.wait(0.2)
bolts.forEach(b => b.destroy())
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function hashSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const count = isCritical ? 16 : 8
for (let i = 0; i < count; i++) {
const hx = def.pos.x + (Math.random() - 0.5) * 70
const hash = k.add([k.text('#', { size: 14 }), k.pos(hx, -10), k.color(safeColor('#00ff88')), k.opacity(0.9), k.z(18)])
const ty = def.pos.y - 10 - Math.random() * 40
k.tween(hash.pos.y, ty, 0.12 + Math.random() * 0.08, (v: number) => { hash.pos.y = v }, k.easings.easeInQuad).then(() => { hash.destroy(); spawnSparks(hx, ty, 2, '#00ff88'); sfxBulletHit() })
await k.wait(0.025)
}
await k.wait(0.1); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 5)
if (isCritical) { screenFlash('#00ff88'); sfxExplosion() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const blockDrop = makeDrop(28, 28, '#f7931a', '#ffd700', false)
async function mempoolFlood(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const txCount = isCritical ? 24 : 12
for (let i = 0; i < txCount; i++) {
const tx = k.add([k.rect(6, 3), k.pos(atk.pos.x + dir * 20, atk.pos.y - 30 + (Math.random() - 0.5) * 40), k.color(safeColor(Math.random() > 0.5 ? '#8844ff' : '#aa66ff')), k.opacity(0.8), k.z(16)])
k.tween(tx.pos.x, def.pos.x + (Math.random() - 0.5) * 30, 0.2, (v: number) => { tx.pos.x = v }).then(() => { tx.destroy(); spawnSparks(tx.pos.x, tx.pos.y, 1, '#aa66ff') })
k.tween(tx.pos.y, def.pos.y - 20 + (Math.random() - 0.5) * 30, 0.2, (v: number) => { tx.pos.y = v })
if (i % 4 === 0) sfxBulletHit()
await k.wait(0.015)
}
await k.wait(0.15); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 5)
if (isCritical) { screenFlash('#8844ff'); sfxCritical() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function difficultyBomb(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const bomb = k.add([k.circle(8), k.pos(def.pos.x, def.pos.y - 30), k.color(safeColor('#ff4400')), k.opacity(0.9), k.z(18)])
await k.tween(8, isCritical ? 50 : 30, 0.3, (v: number) => { bomb.radius = v }, k.easings.easeOutQuad)
sfxExplosion(); k.shake(isCritical ? 18 : 10); screenFlash(isCritical ? '#ffffff' : '#ff4400', 0.12)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 18 : 10, '#ff4400')
spawnShockwave(def.pos.x, def.pos.y - 20, '#ff6600')
bomb.destroy(); def.play(isCritical ? 'knockback' : 'hit')
const push = dir * (isCritical ? 100 : 45)
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function feeSpike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack'); sfxPunch(); await k.wait(0.08)
const spike = k.add([k.rect(4, 40), k.pos(def.pos.x, GROUND_Y), k.color(safeColor('#ff2222')), k.opacity(0.9), k.z(16)])
sfxSlideDown()
await k.tween(GROUND_Y, def.pos.y - 60, 0.12, (v: number) => { spike.pos.y = v }, k.easings.easeOutQuad)
sfxBonk(); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 40, isCritical ? 10 : 5, '#ff2222')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#ff0000'); sfxCritical() }
await k.wait(0.15)
k.tween(spike.opacity, 0, 0.2, (v: number) => { spike.opacity = v }).then(() => spike.destroy())
const push = dir * (isCritical ? 75 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function utxoScatter(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const count = isCritical ? 14 : 7
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI - Math.PI / 2
const coin = k.add([k.circle(4), k.pos(atk.pos.x + dir * 15, atk.pos.y - 30), k.color(safeColor('#ffd700')), k.opacity(1), k.z(17)])
const tx = def.pos.x + Math.cos(angle) * 40 + (Math.random() - 0.5) * 20
const ty = def.pos.y - 30 + Math.sin(angle) * 30
k.tween(coin.pos.x, tx, 0.18, (v: number) => { coin.pos.x = v }, k.easings.easeOutQuad)
k.tween(coin.pos.y, ty, 0.18, (v: number) => { coin.pos.y = v }, k.easings.easeOutQuad).then(() => { coin.destroy(); spawnSparks(tx, ty, 2, '#ffd700'); sfxBulletHit() })
await k.wait(0.02)
}
await k.wait(0.12); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 5)
if (isCritical) { screenFlash('#ffd700'); sfxCritical() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const nodeSync = makeBeam('#00ff88', 5, 2)
async function relayBounce(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack'); sfxPunch(); await k.wait(0.08)
const ball = k.add([k.circle(6), k.pos(atk.pos.x + dir * 20, atk.pos.y - 30), k.color(safeColor('#8844ff')), k.opacity(1), k.z(18)])
// Bounce off top
await k.tween(ball.pos.y, -10, 0.1, (v: number) => { ball.pos.y = v }, k.easings.easeOutQuad)
k.tween(ball.pos.x, (atk.pos.x + def.pos.x) / 2, 0.1, (v: number) => { ball.pos.x = v })
sfxBoing()
// Bounce down to defender
await k.tween(ball.pos.y, def.pos.y - 25, 0.12, (v: number) => { ball.pos.y = v }, k.easings.easeInQuad)
k.tween(ball.pos.x, def.pos.x, 0.12, (v: number) => { ball.pos.x = v })
sfxBonk(); k.shake(isCritical ? 14 : 6)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 12 : 5, '#8844ff')
ball.destroy(); def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#8844ff'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function channelForceClose(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial()
// Draw lightning chain between fighters
const chain = k.add([k.rect(Math.abs(def.pos.x - atk.pos.x), 3), k.pos(Math.min(atk.pos.x, def.pos.x), atk.pos.y - 35), k.color(safeColor('#ffdd00')), k.opacity(0.8), k.z(17)])
await k.wait(0.2); sfxSpecial()
// Snap! Chain breaks
chain.destroy(); sfxExplosion()
spawnSparks((atk.pos.x + def.pos.x) / 2, atk.pos.y - 35, isCritical ? 16 : 8, '#ffdd00')
k.shake(isCritical ? 16 : 8); screenFlash('#ffdd00', 0.1)
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) sfxCritical()
const push = dir * (isCritical ? 95 : 42)
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function halvingStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 45
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.1, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
// First hit
atk.play('attack'); sfxPunch(); def.play('hit'); k.shake(4)
spawnSparks(def.pos.x, def.pos.y - 25, 4, '#f7931a')
await k.wait(0.12)
// Second hit (the halving)
atk.play('kick'); sfxPunch(); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 6)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 10 : 5, '#ffd700')
if (isCritical) { screenFlash('#f7931a'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function coldStorage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
// Freeze aura around defender
const ice = k.add([k.circle(isCritical ? 40 : 25), k.pos(def.pos.x, def.pos.y - 25), k.color(safeColor('#88ddff')), k.opacity(0.4), k.z(15)])
spawnSparks(def.pos.x, def.pos.y - 30, 8, '#aaeeff')
sfxBoing(); await k.wait(0.25)
// Shatter
sfxExplosion(); ice.destroy()
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 16 : 8, '#88ddff')
k.shake(isCritical ? 14 : 7); def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#88ddff'); sfxCritical() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const paperWalletSlash = makeSwing(30, 2, '#f0e8d0', '#ffd700', 8, 8)
async function hardFork(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
// Two beams split from one
const startX = atk.pos.x + dir * 25
const beamA = k.add([k.rect(3, 60), k.pos(startX, atk.pos.y - 50), k.color(safeColor('#ff4400')), k.opacity(0.8), k.z(17), k.rotate(15 * dir)])
const beamB = k.add([k.rect(3, 60), k.pos(startX, atk.pos.y - 30), k.color(safeColor('#4488ff')), k.opacity(0.8), k.z(17), k.rotate(-15 * dir)])
sfxSpecial()
await Promise.all([
k.tween(beamA.pos.x, def.pos.x, 0.15, (v: number) => { beamA.pos.x = v }),
k.tween(beamB.pos.x, def.pos.x, 0.15, (v: number) => { beamB.pos.x = v }),
])
beamA.destroy(); beamB.destroy()
sfxExplosion(); k.shake(isCritical ? 16 : 8)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 14 : 7, '#ff4400')
spawnSparks(def.pos.x, def.pos.y - 20, isCritical ? 14 : 7, '#4488ff')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#ffffff'); sfxCritical() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function doubleSpend(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 45
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.1, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
// Two rapid identical hits
for (let i = 0; i < 2; i++) {
atk.play(i === 0 ? 'attack' : 'kick'); sfxPunch()
def.play('hit'); k.shake(5 + i * 3)
spawnSparks(def.pos.x, def.pos.y - 25, 5, '#ff2222')
spawnAfterimages(atk, 1)
await k.wait(0.08)
}
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 7)
if (isCritical) { screenFlash('#ff2222'); sfxCritical() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
const proofOfWork = makeSwing(12, 30, '#888888', '#f7931a', 14, 14)
async function merkleRoot(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
// Roots grow up from ground under defender
const roots: any[] = []
for (let i = 0; i < (isCritical ? 6 : 3); i++) {
const rx = def.pos.x + (i - (isCritical ? 2.5 : 1)) * 12
const root = k.add([k.rect(3, 0), k.pos(rx, GROUND_Y), k.color(safeColor('#44aa22')), k.opacity(0.8), k.z(15)])
roots.push(root)
k.tween(0, 60 + Math.random() * 20, 0.15, (v: number) => { root.height = v; root.pos.y = GROUND_Y - v })
await k.wait(0.03)
}
sfxBonk(); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 20, isCritical ? 12 : 6, '#44aa22')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#44aa22'); sfxCritical() }
await k.wait(0.2)
roots.forEach(r => r.destroy())
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function blockReward(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxBoing()
// Gold coins gather around attacker then blast at defender
const coins: any[] = []
for (let i = 0; i < (isCritical ? 10 : 5); i++) {
const c = k.add([k.circle(4), k.pos(atk.pos.x + (Math.random() - 0.5) * 40, atk.pos.y - 50 + (Math.random() - 0.5) * 30), k.color(safeColor('#ffd700')), k.opacity(1), k.z(17)])
coins.push(c)
}
await k.wait(0.2); sfxZoomWhoosh()
// All coins fly at defender
for (const c of coins) {
k.tween(c.pos.x, def.pos.x + (Math.random() - 0.5) * 20, 0.12, (v: number) => { c.pos.x = v })
k.tween(c.pos.y, def.pos.y - 25 + (Math.random() - 0.5) * 20, 0.12, (v: number) => { c.pos.y = v }).then(() => { c.destroy(); sfxBulletHit() })
}
await k.wait(0.15); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 14 : 7, '#ffd700')
if (isCritical) { screenFlash('#ffd700'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function difficultyRetarget(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial()
// Screen warps then snaps
const overlay = k.add([k.rect(k.width(), k.height()), k.pos(0, 0), k.color(safeColor('#ff00ff')), k.opacity(0), k.z(25)])
await k.tween(0, 0.3, 0.2, (v: number) => { overlay.opacity = v })
k.shake(isCritical ? 18 : 9)
await k.tween(0.3, 0, 0.15, (v: number) => { overlay.opacity = v })
overlay.destroy(); sfxExplosion()
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 14 : 7, '#ff00ff')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#ff00ff'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function taprootStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
// Single root shoots up from below defender
const root = k.add([k.rect(5, 0), k.pos(def.pos.x, GROUND_Y), k.color(safeColor('#22cc44')), k.opacity(0.9), k.z(16)])
sfxSlideDown()
await k.tween(0, 80, 0.15, (v: number) => { root.height = v; root.pos.y = GROUND_Y - v }, k.easings.easeOutQuad)
sfxBonk(); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 12 : 6, '#22cc44')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#22cc44'); sfxCritical() }
await k.wait(0.15)
k.tween(root.opacity, 0, 0.2, (v: number) => { root.opacity = v }).then(() => root.destroy())
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function segwitSlash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 40
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.08, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack'); sfxPunch()
// Compressed slash visual — thin line
const slash = k.add([k.rect(60, 2), k.pos(def.pos.x - 30, def.pos.y - 30), k.color(safeColor('#44aaff')), k.opacity(0.9), k.z(18)])
await k.wait(0.06); slash.destroy()
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 5)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 10 : 5, '#44aaff')
if (isCritical) { screenFlash('#44aaff'); sfxCritical() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function timestampAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const clock = k.add([k.circle(12), k.pos(atk.pos.x + dir * 20, atk.pos.y - 35), k.color(safeColor('#ffcc00')), k.opacity(0.8), k.z(18)])
sfxZoomWhoosh()
await k.tween(clock.pos.x, def.pos.x, 0.15, (v: number) => { clock.pos.x = v })
clock.destroy(); sfxBonk(); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 12 : 6, '#ffcc00')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#ffcc00'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function dustAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.08)
const count = isCritical ? 30 : 15
for (let i = 0; i < count; i++) {
const p = k.add([k.circle(1), k.pos(atk.pos.x + dir * 15, atk.pos.y - 30), k.color(safeColor('#aaaaaa')), k.opacity(0.6), k.z(16)])
const tx = def.pos.x + (Math.random() - 0.5) * 50
const ty = def.pos.y - 30 + (Math.random() - 0.5) * 40
k.tween(p.pos.x, tx, 0.15, (v: number) => { p.pos.x = v })
k.tween(p.pos.y, ty, 0.15, (v: number) => { p.pos.y = v }).then(() => p.destroy())
await k.wait(0.008)
}
await k.wait(0.1); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 10 : 4)
if (isCritical) { screenFlash('#aaaaaa'); sfxCritical() }
const push = dir * (isCritical ? 70 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const opReturnBeam = makeBeam('#ff8800', 8, 1)
async function orphanBlock(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const block = k.add([k.rect(20, 20), k.pos(def.pos.x + dir * 40, -20), k.color(safeColor('#ff6600')), k.opacity(0.7), k.z(17)])
sfxSlideDown()
// Drops, misses slightly, then rebounds into defender
await k.tween(block.pos.y, def.pos.y - 20, 0.15, (v: number) => { block.pos.y = v }, k.easings.easeInQuad)
sfxBoing()
await k.tween(block.pos.y, def.pos.y - 50, 0.08, (v: number) => { block.pos.y = v }, k.easings.easeOutQuad)
k.tween(block.pos.x, def.pos.x, 0.08, (v: number) => { block.pos.x = v })
await k.tween(block.pos.y, def.pos.y - 25, 0.08, (v: number) => { block.pos.y = v }, k.easings.easeInQuad)
block.destroy(); sfxBonk(); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 12 : 6, '#ff6600')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#ff6600'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function miningRig(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const count = isCritical ? 12 : 6
for (let i = 0; i < count; i++) {
const hash = k.add([k.rect(4, 4), k.pos(atk.pos.x + dir * 20, atk.pos.y - 30 + (Math.random() - 0.5) * 20), k.color(safeColor('#00ff88')), k.opacity(0.8), k.z(17)])
k.tween(hash.pos.x, def.pos.x, 0.12, (v: number) => { hash.pos.x = v }).then(() => { hash.destroy(); sfxBulletHit(); spawnSparks(def.pos.x, hash.pos.y, 2, '#00ff88') })
await k.wait(0.04)
}
await k.wait(0.12); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 5)
if (isCritical) { screenFlash('#00ff88'); sfxCritical() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function satPerByte(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial()
// Fee meter rises under defender
const meter = k.add([k.rect(6, 0), k.pos(def.pos.x, GROUND_Y), k.color(safeColor('#ff2222')), k.opacity(0.8), k.z(15)])
await k.tween(0, isCritical ? 90 : 60, 0.25, (v: number) => { meter.height = v; meter.pos.y = GROUND_Y - v })
// Explodes
sfxExplosion(); meter.destroy(); k.shake(isCritical ? 16 : 8)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 14 : 7, '#ff2222')
spawnShockwave(def.pos.x, def.pos.y - 20, '#ff4400')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#ff2222'); sfxCritical() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function fullNodeGuard(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Shield forms, then counter-attacks
atk.play('special'); sfxBoing()
const shield = k.add([k.circle(25), k.pos(atk.pos.x, atk.pos.y - 25), k.color(safeColor('#00ff88')), k.opacity(0.3), k.z(16)])
await k.wait(0.2)
shield.destroy(); sfxZoomWhoosh()
// Counter-dash
const contactX = origDX - dir * 45
await k.tween(atk.pos.x, contactX, 0.1, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack'); sfxPunch()
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 12 : 6, '#00ff88')
if (isCritical) { screenFlash('#00ff88'); sfxCritical() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function multisigStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// 3 simultaneous hits from different angles
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const angles = [-40, 0, 40]
for (const offset of angles) {
const proj = k.add([k.circle(5), k.pos(atk.pos.x + dir * 15, atk.pos.y - 30 + offset), k.color(safeColor('#f7931a')), k.opacity(0.9), k.z(17)])
k.tween(proj.pos.x, def.pos.x, 0.12, (v: number) => { proj.pos.x = v })
k.tween(proj.pos.y, def.pos.y - 25, 0.12, (v: number) => { proj.pos.y = v }).then(() => { proj.destroy(); sfxBulletHit() })
}
await k.wait(0.15); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 14 : 7, '#f7931a')
if (isCritical) { screenFlash('#f7931a'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function nonceGrind(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 45
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.1, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
// Rapid grinding hits
const hits = isCritical ? 6 : 3
for (let i = 0; i < hits; i++) {
atk.play(i % 2 === 0 ? 'attack' : 'kick'); sfxPunch()
def.play('hit'); k.shake(3 + i)
spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 25, 3, '#ffaa00')
await k.wait(0.04)
}
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 7)
if (isCritical) { screenFlash('#ffaa00'); sfxCritical() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function whitePaperRoll(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack'); sfxPunch(); await k.wait(0.08)
// Paper scroll flies at defender
const paper = k.add([k.rect(20, 8), k.pos(atk.pos.x + dir * 20, atk.pos.y - 30), k.color(safeColor('#f0e8d0')), k.opacity(0.9), k.z(17), k.rotate(0)])
sfxZoomWhoosh()
k.tween(paper.angle, 720, 0.18, (v: number) => { paper.angle = v })
await k.tween(paper.pos.x, def.pos.x, 0.18, (v: number) => { paper.pos.x = v })
paper.destroy(); sfxBonk(); k.shake(isCritical ? 12 : 5)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 10 : 5, '#f0e8d0')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#f0e8d0'); sfxCritical() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const asicHammer = makeSwing(10, 25, '#666666', '#f7931a', 12, 12)
async function rugPullYank(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial()
// Ground slides away under defender
const rug = k.add([k.rect(80, 4), k.pos(def.pos.x - 40, GROUND_Y - 2), k.color(safeColor('#ff4488')), k.opacity(0.6), k.z(4)])
await k.wait(0.1)
sfxZoomWhoosh()
await k.tween(rug.pos.x, rug.pos.x + dir * 100, 0.15, (v: number) => { rug.pos.x = v })
rug.destroy()
// Defender falls
await k.tween(def.pos.y, def.pos.y + 15, 0.08, (v: number) => { def.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(isCritical ? 14 : 7)
await k.tween(def.pos.y, GROUND_Y, 0.08, (v: number) => { def.pos.y = v })
def.play(isCritical ? 'knockback' : 'hit')
spawnSparks(def.pos.x, GROUND_Y, isCritical ? 10 : 5, '#ff4488')
if (isCritical) { screenFlash('#ff4488'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const stackSatsBlast = makeThrow('#ffd700', 5, 8, 50, true)
return {
lightningBolt, hashSmash, blockDrop, mempoolFlood, difficultyBomb, feeSpike, utxoScatter,
nodeSync, relayBounce, channelForceClose, halvingStrike, coldStorage, paperWalletSlash,
hardFork, doubleSpend, proofOfWork, merkleRoot, blockReward, difficultyRetarget, taprootStrike,
segwitSlash, timestampAttack, dustAttack, opReturnBeam, orphanBlock, miningRig, satPerByte,
fullNodeGuard, multisigStrike, nonceGrind, whitePaperRoll, asicHammer, rugPullYank,
stackSatsBlast,
}
}
@@ -0,0 +1,684 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createCreatorChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
async function bitcoinRain(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
await k.wait(0.2)
// Rain golden ₿ letters from above
const coinCount = isCritical ? 40 : 20
for (let i = 0; i < coinCount; i++) {
const cx = def.pos.x + (Math.random() - 0.5) * 140
const startY = def.pos.y - 200 - Math.random() * 120
const sz = isCritical ? 8 + Math.random() * 10 : 6 + Math.random() * 6
const coin = k.add([
k.text('₿', { size: sz }),
k.pos(cx, startY),
k.color(safeColor(Math.random() > 0.3 ? '#ffd700' : Math.random() > 0.5 ? '#ffee88' : '#ff8c00')),
k.opacity(0.9),
k.z(18),
k.anchor('center'),
k.rotate(Math.random() * 360),
])
const spin = (Math.random() - 0.5) * 400
coin.onUpdate(() => {
coin.pos.y += 650 * k.dt()
coin.angle += spin * k.dt()
if (coin.pos.y > def.pos.y + 10) {
coin.destroy()
}
})
if (i % 3 === 0) sfxCoin()
await k.wait(0.02)
}
await k.wait(0.15)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 18 : 8)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 20 : 10, '#ffd700')
if (isCritical) { screenFlash('#ffd700', 0.12); sfxExplosion() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function lightningInvoice(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxZap()
await k.wait(0.15)
// Jagged lightning bolt from top of screen to defender
const segments = 8
const topY = -50
const botY = def.pos.y - 20
for (let i = 0; i < segments; i++) {
const t = i / segments
const bx = def.pos.x + (Math.random() - 0.5) * 40
const by2 = topY + t * (botY - topY)
const bolt = k.add([
k.rect(3, Math.abs(botY - topY) / segments + 5),
k.pos(bx, by2),
k.color(safeColor(i % 2 === 0 ? '#ffd700' : '#ffffff')),
k.opacity(0.95),
k.z(20),
])
setTimeout(() => { if (bolt.exists()) bolt.destroy() }, 300)
}
// ₿ symbol flash at impact
const btcFlash = k.add([
k.circle(isCritical ? 20 : 12),
k.pos(def.pos.x, def.pos.y - 30),
k.color(safeColor('#ffd700')),
k.opacity(0.8),
k.z(22),
])
k.tween(btcFlash.opacity, 0, 0.4, (v: number) => { btcFlash.opacity = v })
.then(() => { if (btcFlash.exists()) btcFlash.destroy() })
await k.wait(0.1)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 20 : 10)
sfxExplosion()
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ffd700')
if (isCritical) { screenFlash('#ffd700'); glitchRGB() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function satoshiStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
await k.wait(0.1)
// Rapid-fire gold particle stream
const shotCount = isCritical ? 25 : 15
for (let i = 0; i < shotCount; i++) {
const p = k.add([
k.circle(2),
k.pos(atk.pos.x + dir * 30, atk.pos.y - 40 + (Math.random() - 0.5) * 15),
k.color(safeColor(i % 3 === 0 ? '#ffd700' : i % 3 === 1 ? '#ffee88' : '#ff8c00')),
k.opacity(0.9),
k.z(16),
])
const speed = 800 + Math.random() * 400
p.onUpdate(() => {
p.pos.x += dir * speed * k.dt()
p.opacity -= 1.5 * k.dt()
if (p.opacity <= 0 || Math.abs(p.pos.x - atk.pos.x) > 500) p.destroy()
})
if (i % 3 === 0) sfxCoin()
if (i % 5 === 0) {
def.play('hit')
k.shake(2)
spawnSparks(def.pos.x, def.pos.y - 25 - Math.random() * 20, 3, '#ffd700')
}
await k.wait(0.025)
}
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 12 : 5)
if (isCritical) { screenFlash('#ffd700', 0.08); sfxCritical() }
const push = dir * (isCritical ? 70 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function hashRateOverload(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
// Screen fills with scrolling hash text (simulated with falling green/gold rects)
const hashCount = isCritical ? 40 : 20
for (let i = 0; i < hashCount; i++) {
const hx = Math.random() * k.width()
const hy = Math.random() * k.height()
const h = k.add([
k.rect(8 + Math.random() * 12, 2),
k.pos(hx, hy),
k.color(safeColor(Math.random() > 0.5 ? '#00ff41' : '#ffd700')),
k.opacity(0.6),
k.z(25),
])
h.onUpdate(() => {
h.pos.y += 200 * k.dt()
h.opacity -= 1.5 * k.dt()
if (h.opacity <= 0) h.destroy()
})
}
await k.wait(0.5)
// Explosion
screenFlash(isCritical ? '#ffd700' : '#00ff41', 0.15)
sfxExplosion()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 25 : 12)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 25 : 12, '#ffd700')
if (isCritical) glitchRGB()
const push = dir * (isCritical ? 100 : 50)
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function blockchainSlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 60
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.12, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack')
// Chain of blocks swinging as weapon
const blocks: any[] = []
const blockCount = isCritical ? 6 : 4
for (let i = 0; i < blockCount; i++) {
const bk = k.add([
k.rect(10, 10),
k.pos(atk.pos.x + dir * (i * 14), atk.pos.y - 40),
k.color(safeColor(i % 2 === 0 ? '#ffd700' : '#c8a000')),
k.opacity(0.9),
k.z(17),
])
blocks.push(bk)
}
// Swing chain toward defender
for (let i = 0; i < blockCount; i++) {
await k.tween(blocks[i].pos.x, def.pos.x + (i - blockCount / 2) * 5, 0.08, (v: number) => { blocks[i].pos.x = v }, k.easings.easeOutQuad)
await k.tween(blocks[i].pos.y, def.pos.y - 30, 0.04, (v: number) => { blocks[i].pos.y = v })
}
sfxPunch()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 15 : 7)
spawnSparks(def.pos.x, def.pos.y - 30, 10, '#ffd700')
if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() }
blocks.forEach(b => b.destroy())
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
atk.play('idle')
}
async function creatorMode(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
// Typing effect — rapid small flashes from attacker
for (let i = 0; i < 8; i++) {
const sp = k.add([
k.circle(2),
k.pos(atk.pos.x + dir * 20, atk.pos.y - 45 + (Math.random() - 0.5) * 10),
k.color(safeColor(i % 2 === 0 ? '#00ff41' : '#ffd700')),
k.opacity(0.8),
k.z(16),
])
setTimeout(() => { if (sp.exists()) sp.destroy() }, 200)
await k.wait(0.05)
}
// Reality warp — defender's sprite distorts
if (def.exists()) {
const origScaleX = def.scale?.x ?? 1
const origScaleY = def.scale?.y ?? 1
for (let w = 0; w < 4; w++) {
def.scaleTo(origScaleX * (1 + (Math.random() - 0.5) * 0.5), origScaleY * (1 + (Math.random() - 0.5) * 0.5))
await k.wait(0.06)
}
def.scaleTo(origScaleX, origScaleY)
}
sfxExplosion()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 18 : 8)
spawnSparks(def.pos.x, def.pos.y - 30, 12, '#00ff41')
if (isCritical) { screenFlash('#00ff41'); glitchRGB() }
const push = dir * (isCritical ? 70 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function morphStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Flash golden — the morph is handled by the morph system
// This move itself is a quick dash-hit with gold sparks
screenFlash('#ffd700', 0.08)
sfxZoomWhoosh()
const contactX = origDX - dir * 45
await k.tween(atk.pos.x, contactX, 0.1, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack')
sfxPunch()
await k.wait(0.08)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 14 : 6)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 15 : 8, '#ffd700')
if (isCritical) { sfxCritical(); screenFlash('#ffd700') }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
atk.play('idle')
}
async function divineIntervention(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Ascend off screen
sfxZoomWhoosh()
atk.play('win')
await k.tween(atk.pos.y, atk.pos.y - 300, 0.25, (v: number) => { atk.pos.y = v }, k.easings.easeInQuad)
atk.opacity = 0
// Golden glow expanding where they were
const glowX = origAX
const glow = k.add([
k.circle(5),
k.pos(glowX, GROUND_Y - 40),
k.color(safeColor('#ffd700')),
k.opacity(0.6),
k.z(19),
])
await k.tween(5, 60, 0.3, (v: number) => { glow.radius = v; glow.opacity = 0.6 - v / 150 })
glow.destroy()
// Slam down onto defender from above
atk.opacity = 1
atk.pos.x = def.pos.x
atk.pos.y = -100
atk.play('kick')
sfxZoomWhoosh()
await k.tween(atk.pos.y, GROUND_Y, 0.15, (v: number) => { atk.pos.y = v }, k.easings.easeInQuad)
// Massive impact
sfxExplosion()
k.shake(isCritical ? 30 : 18)
screenFlash('#ffd700', 0.15)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 25 : 15, '#ffd700')
spawnShockwave(def.pos.x, GROUND_Y, '#ffd700')
if (isCritical) glitchRGB()
def.play('knockback')
const push = dir * (isCritical ? 120 : 60)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.5)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.25, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(atk.pos.y, GROUND_Y, 0.01, (v: number) => { atk.pos.y = v }),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
atk.play('idle')
}
async function bitcoinSwarm(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Swarm of ₿ letters fly from creator toward defender with zigzag paths
atk.play('special')
sfxSpecial()
await k.wait(0.15)
const total = isCritical ? 30 : 18
const swarmThings: any[] = []
for (let i = 0; i < total; i++) {
const sx = atk.pos.x + dir * 10 + (Math.random() - 0.5) * 30
const sy = atk.pos.y - 50 + (Math.random() - 0.5) * 60
const sz = 6 + Math.random() * 8
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000']
const p = k.add([
k.text('₿', { size: sz }),
k.pos(sx, sy),
k.color(safeColor(colors[i % colors.length])),
k.opacity(0.9),
k.z(16),
k.anchor('center'),
k.rotate(Math.random() * 360),
])
const tx = def.pos.x + (Math.random() - 0.5) * 30
const ty = def.pos.y - 30 + (Math.random() - 0.5) * 40
const zigFreq = 4 + Math.random() * 4
const zigAmp = 12 + Math.random() * 15
const spinRate = (Math.random() - 0.5) * 600
swarmThings.push(p)
k.tween(0, 1, 0.3 + Math.random() * 0.15, (t) => {
p.pos.x = sx + (tx - sx) * t
p.pos.y = sy + (ty - sy) * t + Math.sin(t * Math.PI * zigFreq) * zigAmp
p.angle += spinRate * k.dt()
p.opacity = 0.9 - t * 0.3
}).then(() => { if (p.exists()) { spawnSparks(p.pos.x, p.pos.y, 2, '#ffd700'); p.destroy() } })
if (i % 3 === 0) sfxCoin()
await k.wait(0.02)
}
await k.wait(0.25)
sfxBonk()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 18 : 8)
if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 18 : 10, '#ffd700')
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
swarmThings.forEach(s => { if (s.exists()) s.destroy() })
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function satoshiTornado(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// ₿ letters spiral around the defender in a tightening vortex then explode
atk.play('special')
sfxSpecial()
sfxZoomWhoosh()
const total = isCritical ? 24 : 14
const vortex: any[] = []
for (let i = 0; i < total; i++) {
const sz = 7 + Math.random() * 7
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#ffffff']
const p = k.add([
k.text('₿', { size: sz }),
k.pos(def.pos.x, def.pos.y - 30),
k.color(safeColor(colors[i % colors.length])),
k.opacity(0.85),
k.z(18),
k.anchor('center'),
k.rotate(0),
])
vortex.push(p)
const baseAngle = (i / total) * Math.PI * 2
const startRadius = 80 + Math.random() * 30
p.onUpdate(() => {
const elapsed = k.time()
const shrink = Math.max(5, startRadius - elapsed * 40)
const a = baseAngle + elapsed * (6 + i * 0.3)
p.pos.x = def.pos.x + Math.cos(a) * shrink
p.pos.y = def.pos.y - 30 + Math.sin(a) * shrink * 0.6
p.angle = elapsed * 200 + i * 45
p.opacity = 0.6 + Math.sin(elapsed * 8 + i) * 0.3
})
await k.wait(0.04)
}
// Let the tornado spin for a moment
await k.wait(0.7)
// Explode outward
screenFlash('#ffd700', 0.12)
sfxExplosion()
k.shake(isCritical ? 22 : 10)
vortex.forEach((p, i) => {
if (!p.exists()) return
const angle = (i / total) * Math.PI * 2
const dist = 100 + Math.random() * 60
k.tween(0, 1, 0.3, (t) => {
p.pos.x = def.pos.x + Math.cos(angle) * dist * t
p.pos.y = def.pos.y - 30 + Math.sin(angle) * dist * 0.6 * t
p.opacity = 1 - t
}).then(() => { if (p.exists()) p.destroy() })
})
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { sfxCritical(); glitchRGB() }
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 20 : 10, '#ffd700')
const push = dir * (isCritical ? 100 : 45)
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
vortex.forEach(p => { if (p.exists()) p.destroy() })
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function hodlWave(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Wall of ₿ letters advancing across the screen like a tidal wave
atk.play('special')
sfxSpecial()
await k.wait(0.1)
const rows = isCritical ? 6 : 4
const cols = isCritical ? 10 : 7
const wave: any[] = []
const startX = atk.pos.x + dir * 25
const screenTop = -20
const screenBot = GROUND_Y + 10
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const sz = 8 + Math.random() * 6
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000', '#ffffff']
const py = screenTop + (screenBot - screenTop) * (r / rows) + (Math.random() - 0.5) * 20
const p = k.add([
k.text('₿', { size: sz }),
k.pos(startX + c * dir * 8, py),
k.color(safeColor(colors[(r + c) % colors.length])),
k.opacity(0.85),
k.z(17),
k.anchor('center'),
k.rotate(Math.random() * 40 - 20),
])
wave.push(p)
}
// Stagger each column
const targetX = def.pos.x + dir * (10 + c * 3)
wave.slice(-rows).forEach((p) => {
const sx = p.pos.x
k.tween(0, 1, 0.35 + c * 0.04, (t) => {
p.pos.x = sx + (targetX - sx) * t
p.angle += 120 * k.dt()
p.pos.y += Math.sin(t * Math.PI * 3) * 2
}).then(() => { if (p.exists()) { spawnSparks(p.pos.x, p.pos.y, 1, '#ffd700'); p.destroy() } })
})
if (c % 2 === 0) sfxCoin()
await k.wait(0.04)
}
await k.wait(0.3)
sfxExplosion()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 20 : 9)
if (isCritical) { screenFlash('#ffd700', 0.12); glitchRGB(0.2) }
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 22 : 12, '#ffd700')
const push = dir * (isCritical ? 95 : 42)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
atk.play('idle')
wave.forEach(p => { if (p.exists()) p.destroy() })
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function bitcoinBarrage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Rapid-fire ₿ letters shot from the creator's laptop like bullets
atk.play('special')
sfxSpecial()
await k.wait(0.1)
const shotCount = isCritical ? 35 : 20
for (let i = 0; i < shotCount; i++) {
const sz = 5 + Math.random() * 8
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000']
const spreadY = (Math.random() - 0.5) * (15 + i * 0.5)
const p = k.add([
k.text('₿', { size: sz }),
k.pos(atk.pos.x + dir * 25, atk.pos.y - 42 + spreadY),
k.color(safeColor(colors[i % colors.length])),
k.opacity(0.9),
k.z(16),
k.anchor('center'),
k.rotate(Math.random() * 360),
])
const speed = 900 + Math.random() * 500
const spinRate = (Math.random() - 0.5) * 800
p.onUpdate(() => {
p.pos.x += dir * speed * k.dt()
p.angle += spinRate * k.dt()
p.opacity -= 1.2 * k.dt()
if (p.opacity <= 0 || Math.abs(p.pos.x - atk.pos.x) > 500) p.destroy()
})
if (i % 2 === 0) sfxCoin()
if (i % 4 === 0) {
def.play('hit')
k.shake(2)
spawnSparks(def.pos.x, def.pos.y - 25 - Math.random() * 20, 3, '#ffd700')
}
await k.wait(0.018)
}
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 15 : 6)
if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 18 : 8, '#ffd700')
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function ultimateGenesisBlock(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Screen goes dark, massive genesis block materializes and detonates
atk.play('special')
sfxSpecial()
const darkness = k.add([k.rect(k.width(), k.height()), k.pos(0, 0), k.color(safeColor('#000000')), k.opacity(0), k.z(30)])
await k.tween(darkness.opacity, 0.7, 0.3, (v: number) => { darkness.opacity = v })
// Genesis block appears at center
const block = k.add([
k.rect(60, 60),
k.pos(k.width() / 2 - 30, k.height() / 2 - 30),
k.color(safeColor('#ffd700')),
k.opacity(0),
k.z(35),
])
await k.tween(block.opacity, 1, 0.3, (v: number) => { block.opacity = v })
await k.wait(0.3)
// Detonate!
sfxExplosion()
block.destroy()
darkness.destroy()
screenFlash('#ffd700', 0.2)
k.shake(35)
glitchRGB()
spawnShockwave(k.width() / 2, k.height() / 2, '#ffd700')
spawnSparks(def.pos.x, def.pos.y - 30, 30, '#ffd700')
def.play('knockback')
const push = dir * 120
k.tween(def.pos.x, origDX + push, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.6)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function ultimateTheHalving(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Screen splits horizontally with a golden line
atk.play('special')
sfxZap()
const line = k.add([
k.rect(k.width(), 3),
k.pos(0, k.height() / 2),
k.color(safeColor('#ffd700')),
k.opacity(0),
k.z(32),
])
await k.tween(line.opacity, 1, 0.15, (v: number) => { line.opacity = v })
screenFlash('#ffd700', 0.1)
glitchRGB()
sfxExplosion()
k.shake(25)
// Briefly scale defender to half
if (def.exists()) {
const origSY = def.scale?.y ?? 1
def.scaleTo(def.scale?.x ?? 1, origSY * 0.5)
await k.wait(0.2)
def.scaleTo(def.scale?.x ?? 1, origSY)
}
line.destroy()
def.play('knockback')
spawnSparks(def.pos.x, def.pos.y - 30, 25, '#ffd700')
const push = dir * 100
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.5)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function ultimateFullNodeCrush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Giant server rack drops from above
atk.play('special')
sfxSpecial()
await k.wait(0.3)
const rack = k.add([
k.rect(40, 80),
k.pos(def.pos.x - 20, -100),
k.color(safeColor('#333333')),
k.opacity(0.9),
k.z(28),
])
// LEDs on the rack
for (let led = 0; led < 6; led++) {
k.add([
k.circle(1),
k.pos(rack.pos.x + 5 + led * 5, rack.pos.y + 10),
k.color(safeColor(led % 2 === 0 ? '#00ff41' : '#ff0000')),
k.opacity(0.8),
k.z(29),
])
}
sfxZoomWhoosh()
await k.tween(rack.pos.y, def.pos.y - 80, 0.2, (v: number) => { rack.pos.y = v }, k.easings.easeInQuad)
// Impact
sfxExplosion()
k.shake(30)
screenFlash('#ffffff', 0.15)
spawnSparks(def.pos.x, def.pos.y - 30, 20, '#ffd700')
spawnShockwave(def.pos.x, GROUND_Y, '#333333')
rack.destroy()
def.play('knockback')
const push = dir * 100
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.6)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function ultimateSatoshiVision(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Everything turns gold — defender trapped in bitcoin block
atk.play('special')
sfxSpecial()
// Gold overlay
const overlay = k.add([k.rect(k.width(), k.height()), k.pos(0, 0), k.color(safeColor('#ffd700')), k.opacity(0), k.z(30)])
await k.tween(overlay.opacity, 0.4, 0.3, (v: number) => { overlay.opacity = v })
// Wireframe box around defender
const bx = def.pos.x - 30
const by2 = def.pos.y - 70
const bw = 60
const bh2 = 80
const top = k.add([k.rect(bw, 2), k.pos(bx, by2), k.color(safeColor('#ffd700')), k.opacity(0.9), k.z(33)])
const bot = k.add([k.rect(bw, 2), k.pos(bx, by2 + bh2), k.color(safeColor('#ffd700')), k.opacity(0.9), k.z(33)])
const left = k.add([k.rect(2, bh2), k.pos(bx, by2), k.color(safeColor('#ffd700')), k.opacity(0.9), k.z(33)])
const right = k.add([k.rect(2, bh2), k.pos(bx + bw, by2), k.color(safeColor('#ffd700')), k.opacity(0.9), k.z(33)])
await k.wait(0.3)
// Crush — shrink box
sfxZoomWhoosh()
await Promise.all([
k.tween(bx, def.pos.x - 5, 0.3, (v: number) => { left.pos.x = v; top.pos.x = v; bot.pos.x = v }),
k.tween(bx + bw, def.pos.x + 5, 0.3, (v: number) => { right.pos.x = v }),
])
sfxExplosion()
k.shake(30)
screenFlash('#ffffff', 0.15)
glitchRGB();
[top, bot, left, right].forEach(b => b.destroy())
overlay.destroy()
spawnSparks(def.pos.x, def.pos.y - 30, 30, '#ffd700')
def.play('knockback')
const push = dir * 110
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.6)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
return {
bitcoinRain, lightningInvoice, satoshiStrike, hashRateOverload, blockchainSlam, creatorMode,
morphStrike, divineIntervention, bitcoinSwarm, satoshiTornado, hodlWave, bitcoinBarrage,
ultimateGenesisBlock, ultimateTheHalving, ultimateFullNodeCrush, ultimateSatoshiVision,
}
}
@@ -0,0 +1,728 @@
import type { Fighter, ChoreoContext } from '../types'
export type PropDef = { parts: { shape: 'rect' | 'circle'; x: number; y: number; w: number; h: number; color: string }[] }
export const PROPS: Record<string, PropDef> = {
// Food
pizza: { parts: [
{ shape: 'rect', x: -7, y: -3, w: 14, h: 3, color: '#ffcc44' }, // crust
{ shape: 'rect', x: -6, y: 0, w: 12, h: 8, color: '#ff8800' }, // cheese
{ shape: 'rect', x: -5, y: 1, w: 10, h: 6, color: '#cc4400' }, // sauce
{ shape: 'circle', x: -3, y: 3, w: 3, h: 3, color: '#aa2200' }, // pepperoni
{ shape: 'circle', x: 2, y: 4, w: 3, h: 3, color: '#aa2200' }, // pepperoni
{ shape: 'circle', x: 0, y: 1, w: 2, h: 2, color: '#44aa22' }, // olive
]},
burger: { parts: [
{ shape: 'rect', x: -8, y: -4, w: 16, h: 4, color: '#cc8833' }, // top bun
{ shape: 'rect', x: -7, y: 0, w: 14, h: 3, color: '#44aa22' }, // lettuce
{ shape: 'rect', x: -7, y: 3, w: 14, h: 4, color: '#883311' }, // patty
{ shape: 'rect', x: -8, y: 7, w: 16, h: 3, color: '#cc8833' }, // bottom bun
{ shape: 'rect', x: -7, y: -1, w: 14, h: 1, color: '#ffcc00' }, // cheese
]},
hotdog: { parts: [
{ shape: 'rect', x: -10, y: -2, w: 20, h: 4, color: '#ffcc66' }, // bun
{ shape: 'rect', x: -9, y: -1, w: 18, h: 3, color: '#cc4422' }, // sausage
{ shape: 'rect', x: -8, y: -2, w: 16, h: 1, color: '#ffee00' }, // mustard
]},
banana: { parts: [
{ shape: 'rect', x: -3, y: -8, w: 5, h: 16, color: '#ffe135' },
{ shape: 'rect', x: -2, y: -9, w: 3, h: 2, color: '#88660d' }, // stem
{ shape: 'rect', x: -2, y: -7, w: 3, h: 14, color: '#ffee55' }, // highlight
]},
pie: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 16, h: 16, color: '#f5deb3' }, // crust
{ shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' }, // cream
{ shape: 'circle', x: -2, y: -2, w: 3, h: 3, color: '#ff4444' }, // cherry
]},
// Weapons
gun: { parts: [
{ shape: 'rect', x: 0, y: -3, w: 24, h: 6, color: '#444444' }, // barrel
{ shape: 'rect', x: -4, y: -5, w: 12, h: 10, color: '#333333' }, // body
{ shape: 'rect', x: -2, y: 5, w: 6, h: 10, color: '#553322' }, // grip
{ shape: 'rect', x: 22, y: -2, w: 4, h: 4, color: '#555555' }, // muzzle
{ shape: 'rect', x: 6, y: -6, w: 8, h: 3, color: '#555555' }, // sight
]},
shotgun: { parts: [
{ shape: 'rect', x: 0, y: -2, w: 35, h: 5, color: '#333333' }, // barrel
{ shape: 'rect', x: 0, y: 3, w: 30, h: 4, color: '#333333' }, // under barrel
{ shape: 'rect', x: -6, y: -4, w: 10, h: 12, color: '#553322' }, // stock
{ shape: 'rect', x: -2, y: 7, w: 6, h: 8, color: '#664433' }, // grip
{ shape: 'rect', x: 33, y: -1, w: 4, h: 3, color: '#555555' }, // muzzle
]},
rocketLauncher: { parts: [
{ shape: 'rect', x: 0, y: -5, w: 30, h: 10, color: '#446633' }, // tube
{ shape: 'circle', x: 28, y: 0, w: 10, h: 10, color: '#335522' }, // end
{ shape: 'circle', x: 2, y: 0, w: 10, h: 10, color: '#335522' }, // back end
{ shape: 'rect', x: 8, y: 5, w: 6, h: 8, color: '#553322' }, // grip
{ shape: 'rect', x: 14, y: -8, w: 6, h: 4, color: '#555555' }, // sight
]},
minigun: { parts: [
{ shape: 'rect', x: 0, y: -6, w: 28, h: 3, color: '#444444' }, // barrel 1
{ shape: 'rect', x: 0, y: -2, w: 28, h: 3, color: '#555555' }, // barrel 2
{ shape: 'rect', x: 0, y: 2, w: 28, h: 3, color: '#444444' }, // barrel 3
{ shape: 'rect', x: -8, y: -8, w: 10, h: 16, color: '#333333' }, // housing
{ shape: 'rect', x: -4, y: 5, w: 6, h: 10, color: '#553322' }, // grip
{ shape: 'circle', x: 27, y: -2, w: 8, h: 8, color: '#666666' }, // muzzle ring
]},
sniper: { parts: [
{ shape: 'rect', x: 0, y: -1, w: 38, h: 3, color: '#333333' }, // barrel
{ shape: 'rect', x: -6, y: -4, w: 14, h: 8, color: '#444444' }, // body
{ shape: 'rect', x: -10, y: -3, w: 8, h: 4, color: '#553322' }, // stock
{ shape: 'circle', x: 12, y: -6, w: 6, h: 6, color: '#225588' }, // scope
{ shape: 'rect', x: 8, y: -7, w: 10, h: 2, color: '#555555' }, // scope mount
]},
katana: { parts: [
{ shape: 'rect', x: 0, y: -1, w: 40, h: 2, color: '#ccccdd' }, // blade
{ shape: 'rect', x: 38, y: -1, w: 4, h: 2, color: '#ffffff' }, // tip
{ shape: 'rect', x: -2, y: -3, w: 4, h: 6, color: '#ffcc00' }, // guard
{ shape: 'rect', x: -10, y: -2, w: 10, h: 4, color: '#442211' }, // handle
{ shape: 'rect', x: -8, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap
{ shape: 'rect', x: -5, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap
]},
// Vehicles (used as props in throws/drops)
car_mini: { parts: [
{ shape: 'rect', x: -12, y: -4, w: 24, h: 8, color: '#ff4444' }, // body
{ shape: 'rect', x: -6, y: -8, w: 12, h: 5, color: '#4488ff' }, // roof/windows
{ shape: 'circle', x: -8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel
{ shape: 'circle', x: 8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel
{ shape: 'rect', x: 11, y: -2, w: 3, h: 3, color: '#ffee00' }, // headlight
]},
tank_mini: { parts: [
{ shape: 'rect', x: -14, y: 0, w: 28, h: 8, color: '#556633' }, // hull
{ shape: 'rect', x: -8, y: -6, w: 16, h: 7, color: '#445522' }, // turret
{ shape: 'rect', x: 6, y: -5, w: 16, h: 3, color: '#333322' }, // barrel
{ shape: 'rect', x: -13, y: 7, w: 26, h: 4, color: '#333333' }, // tracks
{ shape: 'circle', x: -10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel
{ shape: 'circle', x: 10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel
]},
// Misc props
bomb: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#222222' },
{ shape: 'rect', x: -1, y: -9, w: 3, h: 5, color: '#888888' }, // fuse
{ shape: 'circle', x: 0, y: -10, w: 4, h: 4, color: '#ff6600' }, // spark
]},
shield: { parts: [
{ shape: 'rect', x: -10, y: -14, w: 20, h: 28, color: '#4466aa' },
{ shape: 'rect', x: -8, y: -12, w: 16, h: 24, color: '#5577bb' },
{ shape: 'rect', x: -3, y: -8, w: 6, h: 16, color: '#ffcc00' }, // emblem cross
{ shape: 'rect', x: -7, y: -3, w: 14, h: 6, color: '#ffcc00' }, // emblem cross
{ shape: 'rect', x: -10, y: -14, w: 20, h: 2, color: '#6688cc' }, // top edge
]},
soccerball: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' },
{ shape: 'rect', x: -3, y: -3, w: 6, h: 6, color: '#222222' }, // pentagon
]},
donut: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff88cc' }, // icing
{ shape: 'circle', x: 0, y: 0, w: 6, h: 6, color: '#cc8833' }, // hole (body color)
{ shape: 'circle', x: -3, y: -3, w: 2, h: 2, color: '#ff4444' }, // sprinkle
{ shape: 'circle', x: 3, y: -2, w: 2, h: 2, color: '#44ff44' }, // sprinkle
{ shape: 'circle', x: 1, y: 4, w: 2, h: 2, color: '#ffff00' }, // sprinkle
]},
watermelon: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#33aa33' }, // rind
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff4466' }, // flesh
{ shape: 'circle', x: -2, y: -1, w: 2, h: 2, color: '#222222' }, // seed
{ shape: 'circle', x: 3, y: 2, w: 2, h: 2, color: '#222222' }, // seed
{ shape: 'circle', x: 0, y: 4, w: 2, h: 2, color: '#222222' }, // seed
]},
chainsaw: { parts: [
{ shape: 'rect', x: 0, y: -2, w: 28, h: 5, color: '#888888' }, // blade
{ shape: 'rect', x: -8, y: -5, w: 12, h: 10, color: '#ff6600' }, // body
{ shape: 'rect', x: -4, y: 5, w: 6, h: 7, color: '#553322' }, // grip
{ shape: 'rect', x: 26, y: -1, w: 4, h: 3, color: '#aaaaaa' }, // tip
{ shape: 'rect', x: 0, y: -3, w: 26, h: 1, color: '#666666' }, // chain top
{ shape: 'rect', x: 0, y: 3, w: 26, h: 1, color: '#666666' }, // chain bottom
]},
// Fatality props
rubber_chicken: { parts: [
{ shape: 'rect', x: -2, y: -12, w: 5, h: 20, color: '#ffdd44' }, // body
{ shape: 'circle', x: 0, y: -14, w: 8, h: 8, color: '#ffee55' }, // head
{ shape: 'rect', x: 2, y: -16, w: 8, h: 3, color: '#ff8800' }, // beak
{ shape: 'circle', x: -1, y: -15, w: 2, h: 2, color: '#000000' }, // eye
{ shape: 'rect', x: -1, y: 6, w: 3, h: 5, color: '#ff6600' }, // leg
]},
anvil: { parts: [
{ shape: 'rect', x: -14, y: -4, w: 28, h: 12, color: '#555555' }, // body
{ shape: 'rect', x: -18, y: 6, w: 36, h: 5, color: '#444444' }, // base
{ shape: 'rect', x: -10, y: -8, w: 20, h: 5, color: '#666666' }, // top
{ shape: 'rect', x: 8, y: -12, w: 6, h: 5, color: '#777777' }, // horn
]},
flyswatter: { parts: [
{ shape: 'rect', x: -2, y: 0, w: 4, h: 35, color: '#cc8833' }, // handle
{ shape: 'rect', x: -12, y: -14, w: 24, h: 16, color: '#44aa44' }, // paddle
{ shape: 'rect', x: -10, y: -12, w: 20, h: 12, color: '#338833' }, // grid
]},
toilet: { parts: [
{ shape: 'rect', x: -12, y: -4, w: 24, h: 16, color: '#eeeeee' }, // bowl
{ shape: 'rect', x: -14, y: -14, w: 28, h: 12, color: '#dddddd' }, // tank
{ shape: 'circle', x: 0, y: 4, w: 16, h: 16, color: '#aaddff' }, // water
{ shape: 'rect', x: -12, y: -5, w: 24, h: 3, color: '#cccccc' }, // seat
{ shape: 'rect', x: 8, y: -12, w: 4, h: 6, color: '#888888' }, // handle
]},
keyboard: { parts: [
{ shape: 'rect', x: -20, y: -3, w: 40, h: 8, color: '#333333' }, // base
{ shape: 'rect', x: -18, y: -2, w: 36, h: 6, color: '#444444' }, // keys area
{ shape: 'rect', x: -16, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: -10, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: -4, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: 2, y: -1, w: 12, h: 3, color: '#666666' }, // spacebar
]},
disco_ball: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#cccccc' }, // ball
{ shape: 'rect', x: -1, y: -12, w: 2, h: 5, color: '#888888' }, // chain
{ shape: 'rect', x: -3, y: -2, w: 3, h: 3, color: '#ffffff' }, // mirror
{ shape: 'rect', x: 2, y: 1, w: 3, h: 3, color: '#eeeeee' }, // mirror
{ shape: 'rect', x: -1, y: 4, w: 3, h: 3, color: '#ffffff' }, // mirror
]},
rubber_duck: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 10, h: 10, color: '#ffdd00' }, // body
{ shape: 'circle', x: 3, y: -5, w: 7, h: 7, color: '#ffee22' }, // head
{ shape: 'rect', x: 5, y: -6, w: 5, h: 2, color: '#ff8800' }, // beak
{ shape: 'circle', x: 4, y: -6, w: 2, h: 2, color: '#000000' }, // eye
]},
baguette: { parts: [
{ shape: 'rect', x: -16, y: -3, w: 32, h: 6, color: '#cc9944' }, // bread
{ shape: 'rect', x: -14, y: -2, w: 28, h: 4, color: '#ddaa55' }, // crust highlight
{ shape: 'rect', x: -8, y: -4, w: 4, h: 1, color: '#bb8833' }, // score
{ shape: 'rect', x: 2, y: -4, w: 4, h: 1, color: '#bb8833' }, // score
]},
boot: { parts: [
{ shape: 'rect', x: -8, y: -16, w: 14, h: 24, color: '#553322' }, // shaft
{ shape: 'rect', x: -8, y: 6, w: 22, h: 8, color: '#442211' }, // sole
{ shape: 'rect', x: -6, y: -14, w: 10, h: 4, color: '#664433' }, // top band
{ shape: 'rect', x: 12, y: 4, w: 4, h: 6, color: '#442211' }, // toe
]},
cannon: { parts: [
{ shape: 'rect', x: -10, y: -6, w: 28, h: 12, color: '#444444' }, // barrel
{ shape: 'circle', x: -10, y: 0, w: 14, h: 14, color: '#333333' }, // base
{ shape: 'circle', x: 16, y: 0, w: 10, h: 10, color: '#555555' }, // muzzle
{ shape: 'rect', x: -6, y: 6, w: 6, h: 8, color: '#664433' }, // wheel
]},
stamp: { parts: [
{ shape: 'rect', x: -3, y: -18, w: 6, h: 14, color: '#884422' }, // handle
{ shape: 'rect', x: -14, y: -6, w: 28, h: 8, color: '#cc4444' }, // stamp face
{ shape: 'rect', x: -12, y: -4, w: 24, h: 4, color: '#aa2222' }, // ink
]},
vending_machine: { parts: [
{ shape: 'rect', x: -12, y: -18, w: 24, h: 36, color: '#cc3333' }, // body
{ shape: 'rect', x: -10, y: -14, w: 12, h: 20, color: '#222222' }, // window
{ shape: 'rect', x: 3, y: -8, w: 6, h: 4, color: '#44aa44' }, // button
{ shape: 'rect', x: 3, y: 0, w: 6, h: 4, color: '#4444aa' }, // button
{ shape: 'rect', x: -8, y: 10, w: 10, h: 5, color: '#333333' }, // dispenser
]},
lawn_mower: { parts: [
{ shape: 'rect', x: -10, y: -2, w: 20, h: 8, color: '#44aa44' }, // body
{ shape: 'circle', x: -8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel
{ shape: 'circle', x: 8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel
{ shape: 'rect', x: -2, y: -12, w: 3, h: 12, color: '#888888' }, // handle
{ shape: 'rect', x: -4, y: -14, w: 8, h: 3, color: '#666666' }, // grip
]},
}
export interface BotWeapons {
melee: string // close range weapon prop
ranged: string // gun/launcher prop
thrown: string // throwable prop
}
export const MELEE_WEAPONS = ['katana', 'chainsaw', 'shield']
export const RANGED_WEAPONS = ['gun', 'shotgun', 'sniper', 'minigun', 'rocketLauncher']
export const THROWN_WEAPONS = ['pizza', 'burger', 'bomb', 'watermelon', 'donut', 'soccerball', 'banana']
export function getBotWeapons(seed: string): BotWeapons {
let h = 0
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
return {
melee: MELEE_WEAPONS[Math.abs(h) % MELEE_WEAPONS.length],
ranged: RANGED_WEAPONS[Math.abs(h >> 3) % RANGED_WEAPONS.length],
thrown: THROWN_WEAPONS[Math.abs(h >> 7) % THROWN_WEAPONS.length],
}
}
export type ChoreoFn = (atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) => Promise<void>
export function createFactories(ctx: ChoreoContext) {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
} = ctx
function spawnProp(
fromX: number, fromY: number, toX: number, toY: number,
w: number, h: number, color: string, speed: number = 600, isCircle: boolean = false,
): Promise<void> {
return new Promise(resolve => {
const prop = k.add([
isCircle ? k.circle(w / 2) : k.rect(w, h),
k.pos(fromX, fromY),
k.color(safeColor(color)),
k.opacity(1),
k.z(16),
k.rotate(Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI),
])
const dx = toX - fromX
const dy = toY - fromY
const dist = Math.sqrt(dx * dx + dy * dy)
const dur = dist / speed
k.tween(0, 1, dur, (t) => {
prop.pos.x = fromX + dx * t
prop.pos.y = fromY + dy * t
}, k.easings.linear).then(() => {
prop.destroy()
resolve()
})
})
}
async function spawnBarrage(
fromX: number, fromY: number, toX: number, toY: number,
count: number, color: string, size: number, spread: number, delay: number, isCircle: boolean = false,
) {
for (let i = 0; i < count; i++) {
const offY = (Math.random() - 0.5) * spread
const offX = (Math.random() - 0.5) * spread * 0.3
spawnProp(fromX + offX, fromY + offY, toX + offX, toY + offY, size, size, color, 500 + Math.random() * 300, isCircle)
await k.wait(delay)
}
}
function spawnWeaponProp(propName: string, x: number, y: number, flipX: boolean = false, zIndex: number = 16): any[] {
const def = PROPS[propName]
if (!def) {
// Fallback: single colored circle
return [k.add([k.circle(8), k.pos(x, y), k.color(k.Color.fromHex('#ff8800')), k.opacity(1), k.z(zIndex), k.anchor('center')])]
}
const objs: any[] = []
for (const p of def.parts) {
const px = flipX ? x - p.x : x + p.x
const obj = k.add([
p.shape === 'circle' ? k.circle(p.w / 2) : k.rect(p.w, p.h),
k.pos(px, y + p.y),
k.color(k.Color.fromHex(p.color)),
k.opacity(1),
k.z(zIndex),
k.anchor('center'),
])
objs.push(obj)
}
return objs
}
function moveProp(objs: any[], def: PropDef, x: number, y: number, flipX: boolean = false) {
for (let i = 0; i < objs.length && i < def.parts.length; i++) {
const p = def.parts[i]
objs[i].pos.x = flipX ? x - p.x : x + p.x
objs[i].pos.y = y + p.y
}
}
function destroyProp(objs: any[]) {
objs.forEach(o => { if (o.exists()) o.destroy() })
}
function makeThrow(color: string, size: number, count: number = 1, arc: number = 60, circle: boolean = false, trail?: string): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial(); await k.wait(0.12)
const total = count + (isCritical ? 2 : 0)
for (let i = 0; i < total; i++) {
const fx = atk.pos.x + dir * 25, fy = atk.pos.y - 35
const tx = def.pos.x + (Math.random() - 0.5) * 30, ty = def.pos.y - 25 + (Math.random() - 0.5) * 20
// Try to find a prop for this choreography, fall back to colored shape
const p = k.add([circle ? k.circle(size / 2) : k.rect(size, size), k.pos(fx, fy), k.color(k.Color.fromHex(color)), k.opacity(1), k.z(16), k.rotate(Math.random() * 360), k.anchor('center')])
k.tween(0, 1, 0.18, (t) => { p.pos.x = fx + (tx - fx) * t; p.pos.y = fy + (ty - fy) * t - Math.sin(t * Math.PI) * arc; p.angle += 720 * k.dt() }).then(() => { p.destroy(); spawnSparks(tx, ty, 3, trail || color) })
if (i < total - 1) await k.wait(0.05)
}
await k.wait(0.12); sfxBonk()
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 6)
if (isCritical) { screenFlash(color); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 14 : 7, color)
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeThrowProp(propName: string, count: number = 1, arc: number = 60, trail: string = '#ffcc00'): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial(); await k.wait(0.12)
const total = count + (isCritical ? 2 : 0)
const propDef = PROPS[propName]
for (let i = 0; i < total; i++) {
const fx = atk.pos.x + dir * 25, fy = atk.pos.y - 35
const tx = def.pos.x + (Math.random() - 0.5) * 30, ty = def.pos.y - 25 + (Math.random() - 0.5) * 20
const objs = spawnWeaponProp(propName, fx, fy, dir < 0)
k.tween(0, 1, 0.2, (t) => {
const cx = fx + (tx - fx) * t
const cy = fy + (ty - fy) * t - Math.sin(t * Math.PI) * arc
if (propDef) moveProp(objs, propDef, cx, cy, dir < 0)
else if (objs[0]) { objs[0].pos.x = cx; objs[0].pos.y = cy }
}).then(() => { destroyProp(objs); spawnSparks(tx, ty, 5, trail) })
if (i < total - 1) await k.wait(0.06)
}
await k.wait(0.15); sfxBonk()
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 6)
if (isCritical) { screenFlash(trail); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 14 : 7, trail)
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeGunProp(propName: string, bulletCount: number = 4, bulletColor: string = '#ffee00'): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial()
// Spawn gun prop on attacker
const gx = atk.pos.x + dir * 15, gy = atk.pos.y - 35
const gunObjs = spawnWeaponProp(propName, gx, gy, dir < 0, 18)
const gunDef = PROPS[propName]
await k.wait(0.15)
// Fire bullets
const total = bulletCount + (isCritical ? 3 : 0)
for (let i = 0; i < total; i++) {
sfxGunshot()
// Muzzle flash
const mfx = gx + dir * 30, mfy = gy
const flash = k.add([k.circle(5 + Math.random() * 4), k.pos(mfx, mfy), k.color(k.Color.fromHex(bulletColor)), k.opacity(0.9), k.z(19), k.anchor('center')])
setTimeout(() => { if (flash.exists()) flash.destroy() }, 40)
// Bullet trail
const targetY = def.pos.y - 20 - Math.random() * 30
await spawnProjectile(mfx, mfy, def.pos.x, targetY, bulletColor, 4)
sfxBulletHit()
spawnSparks(def.pos.x + (Math.random() - 0.5) * 20, targetY, 3, bulletColor)
// Recoil — bounce gun
if (gunDef) {
const rx = gx - dir * 4
moveProp(gunObjs, gunDef, rx, gy, dir < 0)
await k.wait(0.02)
moveProp(gunObjs, gunDef, gx, gy, dir < 0)
}
def.opacity = 0.5; await k.wait(0.03); def.opacity = 1
}
spawnBulletHoles(def.pos.x, def.pos.y - 30, isCritical ? 5 : 3)
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 16 : 8)
if (isCritical) { screenFlash(bulletColor); sfxCritical(); sfxExplosion() }
// Remove gun
destroyProp(gunObjs)
const push = dir * (isCritical ? 100 : 45)
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeSwing(weaponW: number, weaponH: number, weaponColor: string, headColor?: string, headW?: number, headH?: number): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
const cx = origDX - dir * 55
await k.tween(atk.pos.x, cx, 0.14, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
const wx = atk.pos.x + dir * 18, wy = atk.pos.y - 55
const shaft = k.add([k.rect(weaponW, weaponH), k.pos(wx, wy), k.color(safeColor(weaponColor)), k.anchor('bot'), k.z(18), k.rotate(dir === 1 ? -40 : 40)])
let head: any = null
if (headColor) { head = k.add([k.rect(headW || 18, headH || 14), k.pos(wx + dir * 10, wy - weaponH + 5), k.color(safeColor(headColor)), k.z(18)]) }
atk.play('attack'); sfxSpecial()
await k.tween(dir === 1 ? -40 : 40, dir === 1 ? 85 : -85, 0.13, (v) => { shaft.angle = v }, k.easings.easeInQuad)
sfxBonk(); shaft.destroy(); if (head) head.destroy()
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 18 : 9)
if (isCritical) { screenFlash(weaponColor); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 14 : 7, headColor || weaponColor)
const push = dir * (isCritical ? 100 : 48)
k.tween(def.pos.x, origDX + push, 0.22, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
}
function makeDrop(w: number, h: number, color: string, bounceColor?: string, circle: boolean = false): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial(); await k.wait(0.12)
const shadow = k.add([k.circle(5), k.pos(def.pos.x, GROUND_Y - 2), k.color(safeColor('#000000')), k.opacity(0.3), k.z(4)])
k.tween(5, w * 0.7, 0.25, (v) => { shadow.radius = v })
const obj = k.add([circle ? k.circle(w / 2) : k.rect(w, h), k.pos(def.pos.x - (circle ? 0 : w / 2), -h - 10), k.color(safeColor(color)), k.opacity(0.9), k.z(18)])
sfxSlideDown()
await k.tween(-h - 10, def.pos.y - 45, 0.22, (v) => { obj.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); sfxExplosion()
k.shake(isCritical ? 22 : 13); screenFlash(isCritical ? '#ffffff' : (bounceColor || color), 0.15)
spawnSparks(def.pos.x, def.pos.y - 30, 14, bounceColor || color)
spawnShockwave(def.pos.x, GROUND_Y, bounceColor || color)
shadow.destroy(); def.play(isCritical ? 'knockback' : 'hit')
await k.tween(obj.pos.y, obj.pos.y - 35, 0.08, (v) => { obj.pos.y = v }, k.easings.easeOutQuad)
await k.tween(obj.pos.y, obj.pos.y + 35, 0.08, (v) => { obj.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); await k.wait(0.1)
k.tween(0.9, 0, 0.25, (v) => { obj.opacity = v }).then(() => obj.destroy())
const push = dir * (isCritical ? 95 : 42)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.25); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeVehicle(bodyW: number, bodyH: number, bodyColor: string, wheelR: number = 8, parts?: { w: number, h: number, color: string, ox: number, oy: number }[]): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
const startX = dir === 1 ? -bodyW - 20 : W + bodyW + 20
atk.opacity = 0; atk.pos.x = startX
const vY = GROUND_Y - bodyH / 2 - wheelR
const body = k.add([k.rect(bodyW, bodyH), k.pos(startX, vY), k.color(safeColor(isCritical ? '#ff2d2d' : bodyColor)), k.z(16)])
const w1 = k.add([k.circle(wheelR), k.pos(startX + dir * (bodyW / 3), vY + bodyH / 2 + wheelR - 2), k.color(safeColor('#222222')), k.z(16)])
const w2 = k.add([k.circle(wheelR), k.pos(startX - dir * (bodyW / 3), vY + bodyH / 2 + wheelR - 2), k.color(safeColor('#222222')), k.z(16)])
const extras: any[] = []
if (parts) for (const p of parts) {
extras.push(k.add([k.rect(p.w, p.h), k.pos(startX + p.ox * dir, vY + p.oy), k.color(safeColor(p.color)), k.z(17)]))
}
sfxJetpack()
const exh = trackedInterval(() => {
const ep = k.add([k.circle(3), k.pos(body.pos.x - dir * bodyW / 2, vY + 5), k.color(safeColor('#888888')), k.opacity(0.5), k.z(4)])
ep.onUpdate(() => { ep.opacity -= 2 * k.dt(); if (ep.opacity <= 0) ep.destroy() })
}, 30)
const tgt = origDX + dir * 20
await k.tween(startX, tgt, 0.32, (v) => {
body.pos.x = v; w1.pos.x = v + dir * (bodyW / 3); w2.pos.x = v - dir * (bodyW / 3)
extras.forEach((e, i) => { if (parts) e.pos.x = v + parts[i].ox * dir })
}, k.easings.easeInQuad)
clearTracked(exh)
sfxExplosion(); sfxCritical()
body.destroy(); w1.destroy(); w2.destroy(); extras.forEach(e => e.destroy())
k.shake(isCritical ? 25 : 14); screenFlash(isCritical ? '#ff2d2d' : bodyColor, 0.18)
spawnSparks(def.pos.x, def.pos.y - 20, 18, '#ff6600'); spawnShockwave(def.pos.x, GROUND_Y, '#ff6600')
def.play('knockback')
const push = dir * (isCritical ? 150 : 70)
const origDY = def.pos.y
await Promise.all([
k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.y, origDY - 90, 0.14, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(def.pos.y, origDY, 0.18, (v) => { def.pos.y = v }, k.easings.easeInQuad))
])
atk.pos.x = origAX; atk.opacity = 1; atk.play('idle')
await k.wait(0.15)
await k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeBeam(color: string, width: number = 6, pulses: number = 1): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial()
// Charge up
const chargeOrb = k.add([k.circle(3), k.pos(atk.pos.x + dir * 30, atk.pos.y - 35), k.color(safeColor(color)), k.opacity(0.8), k.z(18)])
await k.tween(3, 12 + (isCritical ? 5 : 0), 0.2, (v) => { chargeOrb.radius = v }, k.easings.easeOutQuad)
chargeOrb.destroy()
// Fire beam
const total = pulses + (isCritical ? 1 : 0)
for (let p = 0; p < total; p++) {
sfxZap()
const bx = atk.pos.x + dir * 30, by = atk.pos.y - 35
const beamLen = Math.abs(def.pos.x - atk.pos.x) + 30
const beam = k.add([k.rect(beamLen, width + (isCritical ? 4 : 0)), k.pos(bx, by - width / 2), k.color(safeColor(color)), k.opacity(0.9), k.z(17)])
const glow = k.add([k.rect(beamLen, width * 2.5), k.pos(bx, by - width * 1.25), k.color(safeColor(color)), k.opacity(0.25), k.z(16)])
if (dir === -1) { beam.pos.x = bx - beamLen; glow.pos.x = bx - beamLen }
await k.wait(0.08)
def.play('hit'); k.shake(isCritical ? 12 : 5)
spawnSparks(def.pos.x, def.pos.y - 30, 8, color)
await k.wait(0.08)
k.tween(0.9, 0, 0.12, (v) => { beam.opacity = v; glow.opacity = v * 0.3 }).then(() => { beam.destroy(); glow.destroy() })
if (p < total - 1) await k.wait(0.06)
}
if (isCritical) { screenFlash(color); sfxCritical() }
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 16 : 7)
const push = dir * (isCritical ? 100 : 45)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeSpin(spinType: 'horizontal' | 'vertical' | 'corkscrew', height: number = 120, hits: number = 1): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
const origAY = atk.pos.y
const cx = origDX - dir * 35
atk.play('kick'); sfxKick()
if (spinType === 'vertical') {
// Frontflip / backflip — jump up, arc to opponent
await Promise.all([
k.tween(atk.pos.x, cx, 0.25, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad),
k.tween(origAY, origAY - height, 0.12, (v) => { atk.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(origAY - height, origAY, 0.13, (v) => { atk.pos.y = v }, k.easings.easeInQuad)),
k.tween(0, 360 * (isCritical ? 2 : 1), 0.25, (v) => { atk.angle = v }).then(() => { atk.angle = 0 }),
])
} else if (spinType === 'corkscrew') {
// Corkscrew — spiral path
await k.tween(0, 1, 0.28, (t) => {
atk.pos.x = origAX + (cx - origAX) * t
atk.pos.y = origAY - Math.sin(t * Math.PI * 3) * height * 0.4
atk.angle = t * 720
}, k.easings.easeInOutQuad)
atk.angle = 0
} else {
// Horizontal tornado spin
await Promise.all([
k.tween(atk.pos.x, cx, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(0, 720 * (isCritical ? 1.5 : 1), 0.2, (v) => { atk.angle = v }).then(() => { atk.angle = 0 }),
])
}
// Hit(s)
const hc = hits + (isCritical ? 1 : 0)
for (let i = 0; i < hc; i++) {
sfxPunch(); def.play('hit'); k.shake(4 + i * 2)
spawnSparks(def.pos.x + (Math.random() - 0.5) * 20, def.pos.y - 25, 5, theme.accent)
def.pos.x += dir * 10
if (i < hc - 1) await k.wait(0.06)
}
if (isCritical) { screenFlash(theme.accent); sfxCritical() }
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 16 : 8)
const push = dir * (isCritical ? 100 : 45)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
}
function makeSwarm(color: string, count: number, size: number, speed: number = 400, zigzag: boolean = false): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial(); await k.wait(0.15)
const total = count + (isCritical ? 3 : 0)
const swarmThings: any[] = []
for (let i = 0; i < total; i++) {
const sx = atk.pos.x - dir * 20 + (Math.random() - 0.5) * 30
const sy = atk.pos.y - 40 + (Math.random() - 0.5) * 50
const p = k.add([k.circle(size / 2 + Math.random() * 2), k.pos(sx, sy), k.color(safeColor(color)), k.opacity(0.9), k.z(16)])
const tx = def.pos.x + (Math.random() - 0.5) * 25, ty = def.pos.y - 30 + (Math.random() - 0.5) * 30
swarmThings.push(p)
k.tween(0, 1, (Math.abs(tx - sx) / speed) + Math.random() * 0.1, (t) => {
p.pos.x = sx + (tx - sx) * t
p.pos.y = sy + (ty - sy) * t + (zigzag ? Math.sin(t * Math.PI * 6) * 15 : 0)
}).then(() => { p.destroy(); spawnSparks(tx, ty, 2, color) })
await k.wait(0.03)
}
await k.wait(0.2)
sfxBonk(); def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 16 : 8)
if (isCritical) { screenFlash(color); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, 10, color)
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
swarmThings.forEach(s => { if (s.exists()) s.destroy() })
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeExplosion(objW: number, objH: number, objColor: string, fuseTime: number = 0.3, blastColor: string = '#ff6600', circle: boolean = false): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial()
// Throw the thing
const fromX = atk.pos.x + dir * 25, fromY = atk.pos.y - 35
const toX = def.pos.x, toY = def.pos.y - 20
const obj = k.add([circle ? k.circle(objW / 2) : k.rect(objW, objH), k.pos(fromX, fromY), k.color(safeColor(objColor)), k.opacity(1), k.z(16), k.rotate(0)])
await k.tween(0, 1, 0.2, (t) => {
obj.pos.x = fromX + (toX - fromX) * t
obj.pos.y = fromY + (toY - fromY) * t - Math.sin(t * Math.PI) * 80
obj.angle = t * 360
})
obj.angle = 0
// Fuse / flash
for (let i = 0; i < 3; i++) {
obj.opacity = 0.4; await k.wait(fuseTime / 6); obj.opacity = 1; await k.wait(fuseTime / 6)
}
// BOOM
obj.destroy(); sfxExplosion()
const blastR = isCritical ? 45 : 28
const blast = k.add([k.circle(blastR), k.pos(toX, toY), k.color(safeColor(blastColor)), k.opacity(0.8), k.z(19)])
k.tween(blastR, blastR * 2.5, 0.15, (v) => { blast.radius = v })
k.tween(0.8, 0, 0.2, (v) => { blast.opacity = v }).then(() => blast.destroy())
k.shake(isCritical ? 25 : 14); screenFlash(blastColor, 0.15)
spawnSparks(toX, toY, isCritical ? 25 : 12, blastColor); spawnShockwave(toX, GROUND_Y, blastColor)
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) sfxCritical()
const push = dir * (isCritical ? 120 : 55)
const origDY = def.pos.y
await Promise.all([
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(origDY, origDY - 60, 0.12, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(origDY - 60, origDY, 0.15, (v) => { def.pos.y = v }, k.easings.easeInQuad))
])
await k.wait(0.15); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
}
function makeGrab(slamType: 'up' | 'down' | 'spin' | 'toss', height: number = 150): ChoreoFn {
return async (atk, def, dir, origAX, origDX, isCritical) => {
const cx = origDX - dir * 30
await k.tween(atk.pos.x, cx, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack'); sfxPunch(); await k.wait(0.08)
// Grab
def.play('hit'); sfxSpecial()
const origDY = def.pos.y
if (slamType === 'up') {
// Lift and slam down
await k.tween(origDY, origDY - height, 0.2, (v) => { def.pos.y = v; atk.pos.y = v + 20 }, k.easings.easeOutQuad)
await k.wait(0.1)
await k.tween(origDY - height, origDY, 0.15, (v) => { def.pos.y = v; atk.pos.y = GROUND_Y }, k.easings.easeInQuad)
sfxExplosion(); k.shake(isCritical ? 22 : 12); spawnShockwave(def.pos.x, GROUND_Y, theme.accent)
} else if (slamType === 'down') {
// Piledriver — both go up, come down head first
await Promise.all([
k.tween(origDY, origDY - height, 0.2, (v) => { def.pos.y = v }, k.easings.easeOutQuad),
k.tween(atk.pos.y, atk.pos.y - height, 0.2, (v) => { atk.pos.y = v }, k.easings.easeOutQuad),
])
await k.wait(0.1)
def.angle = 180
await Promise.all([
k.tween(origDY - height, origDY, 0.13, (v) => { def.pos.y = v }, k.easings.easeInQuad),
k.tween(atk.pos.y, GROUND_Y, 0.13, (v) => { atk.pos.y = v }, k.easings.easeInQuad),
])
def.angle = 0; sfxExplosion(); k.shake(isCritical ? 28 : 16); spawnShockwave(def.pos.x, GROUND_Y, '#ff2d2d')
} else if (slamType === 'spin') {
// Spin throw
await k.tween(0, 1, 0.35, (t) => {
def.pos.y = origDY - Math.sin(t * Math.PI) * height * 0.5
def.angle = t * 720
def.pos.x += dir * 2
})
def.angle = 0; sfxBonk(); k.shake(isCritical ? 20 : 10)
} else {
// Toss across screen
const tossX = origDX + dir * (isCritical ? 200 : 120)
await Promise.all([
k.tween(def.pos.x, tossX, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(origDY, origDY - height, 0.12, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(origDY - height, origDY, 0.13, (v) => { def.pos.y = v }, k.easings.easeInQuad)),
])
sfxBonk(); k.shake(isCritical ? 18 : 9); spawnShockwave(def.pos.x, GROUND_Y, theme.accent)
}
if (isCritical) { screenFlash(theme.accent); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 18 : 8, theme.accent)
def.play(isCritical ? 'knockback' : 'hit')
await k.wait(0.3); atk.play('idle')
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(atk.pos.y, GROUND_Y, 0.15, (v) => { atk.pos.y = v }, k.easings.easeOutQuad),
k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
}
return {
spawnWeaponProp, moveProp, destroyProp, PROPS,
spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
}
}
@@ -0,0 +1,122 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createFoodChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
const pizzaSlam = makeThrowProp('pizza', 2, 70, '#ffcc00')
const bananaFling = makeThrowProp('banana', 3, 55, '#ffff00')
const pieSmash = makeThrowProp('pie', 1, 80, '#ffffff')
const hotdogWhip = makeThrowProp('hotdog', 2, 45, '#ff6644')
const watermelonBomb = makeThrowProp('watermelon', 1, 90, '#ff4466')
const sushiBarrage = makeThrow('#ffffff', 10, 5, 40, false, '#ff6666')
const burgerToss = makeThrowProp('burger', 2, 65, '#ffaa00')
const iceCreamFling = makeThrow('#ffccdd', 12, 3, 50, true, '#ff88bb')
const tacoStorm = makeThrow('#ffcc44', 11, 4, 55, false, '#ff8800')
const donutBarrage = makeThrowProp('donut', 3, 60, '#ffaadd')
const popcornBlast = makeThrow('#ffffcc', 6, 12, 35, true, '#ffee88')
const cookieFling = makeThrow('#cc9944', 12, 3, 50, true, '#aa7722')
const eggBombard = makeThrow('#ffffdd', 10, 4, 65, true, '#ffee44')
const baguetteStrike = makeSwing(6, 55, '#dda855', '#cc9944', 10, 8)
const coffeeSplash = makeThrow('#443322', 10, 3, 45, true, '#886644')
const ramenWhip = makeThrow('#ffee88', 8, 6, 35, false, '#ffffff')
const nachoVolley = makeThrow('#ffcc22', 8, 8, 40, false, '#ff8800')
const pretzelFling = makeThrow('#aa7733', 14, 2, 60, false, '#cc9955')
const meatballStorm = makeThrow('#883322', 12, 5, 55, true, '#aa4433')
const popsicleJab = makeSwing(5, 45, '#ff66aa', '#ffffff', 12, 10)
const candyBarrage = makeThrow('#ff44ff', 8, 7, 40, true, '#ffaaff')
const waffleSlam = makeThrow('#dda844', 16, 2, 70, false, '#ffcc66')
const pancakeFrisbee = makeThrow('#eebb55', 14, 3, 30, true, '#ffdd88')
const drumstickSmack = makeSwing(7, 40, '#cc8844', '#aa6633', 14, 12)
const cornCobCannon = makeThrow('#ffee44', 10, 4, 50, false, '#44aa22')
const pickleJavelin = makeThrow('#669933', 8, 3, 45, false, '#88bb44')
const cabbagePunt = makeThrow('#44aa33', 18, 1, 85, true, '#66cc44')
const breadLoafBash = makeSwing(10, 35, '#dda855')
const cheeseWheel = makeThrow('#ffcc00', 20, 1, 80, true, '#ffee44')
const fishSlap = makeSwing(8, 40, '#8899aa', '#667788', 15, 8)
const sushiRoll = makeThrow('#ff8888', 10, 3, 55, true, '#ffffff')
const tacoSlam = makeDrop(18, 12, '#ffaa33', '#ffcc00', false)
const iceCreamFreeze = makeThrow('#aaddff', 8, 4, 45, true, '#88ccff')
const wokToss = makeThrow('#ff6600', 12, 2, 65, false, '#ffaa00')
const cakeSmash = makeDrop(22, 16, '#ffbbcc', '#ffffff', false)
const souffle = makeThrow('#ffeedd', 14, 1, 80, true, '#ffccaa')
const curryBlast = makeThrow('#ff8800', 6, 6, 40, true, '#ffaa00')
const gumballBarrage = makeThrow('#ff44ff', 5, 10, 35, true)
const frenchFryJavelin = makeThrow('#ffdd44', 3, 5, 30, false, '#ffbb00')
const coconutCannon = makeThrow('#884422', 12, 2, 70, true, '#66aa44')
const lobsterClaw = makeSwing(20, 8, '#ff3322', '#ff6644', 10, 10)
const chiliPepper = makeThrow('#ff2200', 6, 4, 45, false, '#ff4400')
const jellyBounce = makeThrow('#ff88ff', 10, 3, 90, true, '#ff44ff')
const steakSlap = makeSwing(18, 6, '#993322', '#ff4444', 8, 4)
const watermelonBowl = makeDrop(24, 18, '#44aa22', '#ff4444', true)
return {
pizzaSlam, bananaFling, pieSmash, hotdogWhip, watermelonBomb, sushiBarrage, burgerToss,
iceCreamFling, tacoStorm, donutBarrage, popcornBlast, cookieFling, eggBombard, baguetteStrike,
coffeeSplash, ramenWhip, nachoVolley, pretzelFling, meatballStorm, popsicleJab, candyBarrage,
waffleSlam, pancakeFrisbee, drumstickSmack, cornCobCannon, pickleJavelin, cabbagePunt,
breadLoafBash, cheeseWheel, fishSlap, sushiRoll, tacoSlam, iceCreamFreeze, wokToss, cakeSmash,
souffle, curryBlast, gumballBarrage, frenchFryJavelin, coconutCannon, lobsterClaw, chiliPepper,
jellyBounce, steakSlap, watermelonBowl,
}
}
@@ -0,0 +1,284 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createHeavyChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
async function jetpackDive(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const origAY = atk.pos.y
sfxJetpack()
// Jetpack blast off - fly way up off screen
const exhaustInterval = spawnExhaust(atk.pos.x, atk.pos.y, 0.8)
await k.tween(atk.pos.y, -50, 0.3, (v) => {
atk.pos.y = v
// Update exhaust position tracking would need the closure to capture, but exhaust spawns from initial pos
}, k.easings.easeInQuad)
clearInterval(exhaustInterval)
// Reposition above opponent
atk.pos.x = origDX
await k.wait(0.15)
// Dive bomb!
atk.play('kick')
sfxJetpack()
const exhaust2 = spawnExhaust(atk.pos.x, atk.pos.y, 0.3)
await k.tween(atk.pos.y, origAY, 0.2, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
clearInterval(exhaust2)
// IMPACT
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 25 : 12)
spawnSparks(def.pos.x, def.pos.y - 20, 20, '#ff6600')
spawnShockwave(def.pos.x, GROUND_Y, '#ff6600')
screenFlash(isCritical ? '#ff2d2d' : '#ff6600', 0.15)
sfxExplosion()
// Push defender far
const push = dir * (isCritical ? 150 : 80)
k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
// Return
await Promise.all([
k.tween(atk.pos.x, origAX, 0.3, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad),
k.tween(def.pos.x, origDX, 0.4, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function motorbikeCharge(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Back up to edge
const startX = dir === 1 ? -30 : W + 30
atk.opacity = 0
atk.pos.x = startX
// Build the bike: body + wheels
const bikeY = GROUND_Y - 15
const bike = k.add([k.rect(50, 18), k.pos(startX, bikeY), k.color(safeColor(isCritical ? '#ff2d2d' : '#4488ff')), k.z(16)])
const wheelF = k.add([k.circle(8), k.pos(startX + dir * 18, bikeY + 12), k.color(safeColor('#333333')), k.z(16)])
const wheelR = k.add([k.circle(8), k.pos(startX - dir * 16, bikeY + 12), k.color(safeColor('#333333')), k.z(16)])
sfxJetpack()
// Tire marks + exhaust as it zooms across
const exh = trackedInterval(() => {
const p = k.add([k.circle(3), k.pos(bike.pos.x - dir * 25, bikeY + 5), k.color(safeColor('#888888')), k.opacity(0.5), k.z(4)])
p.onUpdate(() => { p.opacity -= 2 * k.dt(); if (p.opacity <= 0) p.destroy() })
}, 30)
// ZOOM across screen
const targetX = origDX + dir * 30
await k.tween(startX, targetX, 0.3, (v) => {
bike.pos.x = v; wheelF.pos.x = v + dir * 18; wheelR.pos.x = v - dir * 16
}, k.easings.easeInQuad)
clearTracked(exh)
// IMPACT
sfxExplosion()
sfxCritical()
bike.destroy(); wheelF.destroy(); wheelR.destroy()
k.shake(isCritical ? 25 : 14)
screenFlash(isCritical ? '#ff2d2d' : '#4488ff', 0.2)
spawnSparks(def.pos.x, def.pos.y - 20, 20, '#ff6600')
spawnShockwave(def.pos.x, GROUND_Y, '#ff6600')
def.play('knockback')
const push = dir * (isCritical ? 160 : 80)
const origDY = def.pos.y
await Promise.all([
k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.y, origDY - 100, 0.15, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(def.pos.y, origDY, 0.2, (v) => { def.pos.y = v }, k.easings.easeInQuad)
),
])
// Attacker reappears at home
atk.pos.x = origAX
atk.opacity = 1
atk.play('idle')
await k.wait(0.2)
await k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function carSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.opacity = 0
const carY = GROUND_Y - 20
const startX = dir === 1 ? -60 : W + 60
// Car body
const car = k.add([k.rect(70, 25), k.pos(startX, carY), k.color(safeColor(isCritical ? '#ffd700' : '#ff6600')), k.z(16)])
// Roof
const roof = k.add([k.rect(35, 15), k.pos(startX + dir * 8, carY - 15), k.color(safeColor(isCritical ? '#ccaa00' : '#cc5500')), k.z(16)])
// Wheels
const w1 = k.add([k.circle(8), k.pos(startX + dir * 22, carY + 18), k.color(safeColor('#222222')), k.z(16)])
const w2 = k.add([k.circle(8), k.pos(startX - dir * 22, carY + 18), k.color(safeColor('#222222')), k.z(16)])
// Headlight
const hl = k.add([k.rect(4, 6), k.pos(startX + dir * 35, carY + 3), k.color(safeColor('#ffee00')), k.z(17)])
sfxJetpack()
sfxSpecial()
// DRIVE ACROSS
await k.tween(startX, def.pos.x, 0.35, (v) => {
car.pos.x = v; roof.pos.x = v + dir * 8; w1.pos.x = v + dir * 22; w2.pos.x = v - dir * 22; hl.pos.x = v + dir * 35
}, k.easings.easeInQuad)
// HIT!
car.destroy(); roof.destroy(); w1.destroy(); w2.destroy(); hl.destroy()
sfxExplosion()
k.shake(isCritical ? 30 : 18)
screenFlash('#ff6600', 0.2)
spawnSparks(def.pos.x, def.pos.y - 20, 25, '#ffcc00')
spawnShockwave(def.pos.x, GROUND_Y, '#ff6600')
def.play('knockback')
const push = dir * (isCritical ? 180 : 90)
const origDY = def.pos.y
await Promise.all([
k.tween(def.pos.x, origDX + push, 0.4, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.y, origDY - 150, 0.2, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(def.pos.y, origDY, 0.25, (v) => { def.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBoing()
spawnShockwave(def.pos.x, GROUND_Y, '#ffcc00')
atk.pos.x = origAX
atk.opacity = 1
atk.play('idle')
await k.wait(0.2)
await k.tween(def.pos.x, origDX, 0.4, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function boatCannon(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
// Build boat
const boatX = atk.pos.x
const boatY = GROUND_Y - 10
const hull = k.add([k.rect(60, 20), k.pos(boatX, boatY), k.color(safeColor('#8B4513')), k.z(8)])
const mast = k.add([k.rect(3, 40), k.pos(boatX + 10, boatY - 40), k.color(safeColor('#8B4513')), k.z(8)])
const sail = k.add([k.rect(25, 30), k.pos(boatX + 15, boatY - 45), k.color(safeColor('#ffffff')), k.opacity(0.8), k.z(8)])
// Slide boat forward
sfxJetpack()
const moveX = (origAX + origDX) / 2
await k.tween(boatX, moveX, 0.3, (v) => { hull.pos.x = v; mast.pos.x = v + 10; sail.pos.x = v + 15 }, k.easings.easeOutQuad)
// Fire cannons!
const shotCount = isCritical ? 5 : 3
for (let i = 0; i < shotCount; i++) {
sfxGunshot()
// Cannonball
const ballY = boatY - 5 + (Math.random() - 0.5) * 10
await spawnProjectile(moveX + dir * 30, ballY, def.pos.x, def.pos.y - 20 + (Math.random() - 0.5) * 30, '#333333', 7)
sfxBulletHit()
def.play('hit')
k.shake(5)
spawnSparks(def.pos.x, def.pos.y - 20, 5, '#ff6600')
await k.wait(0.08)
}
// Final broadside
if (isCritical) {
sfxExplosion()
screenFlash('#ff6600')
k.shake(20)
spawnSparks(def.pos.x, def.pos.y - 30, 20, '#ff6600')
}
def.play(isCritical ? 'knockback' : 'hit')
hull.destroy(); mast.destroy(); sail.destroy()
const push = dir * (isCritical ? 110 : 50)
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function anvilDrop(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
await k.wait(0.15)
// Shadow on ground growing
const shadow = k.add([k.circle(5), k.pos(def.pos.x, GROUND_Y - 2), k.color(safeColor('#000000')), k.opacity(0.3), k.z(4)])
k.tween(5, 25, 0.3, (v) => { shadow.radius = v })
// Anvil falls from sky
const anvil = k.add([k.rect(35, 25), k.pos(def.pos.x - 17, -30), k.color(safeColor('#555555')), k.opacity(0.9), k.z(18)])
const base = k.add([k.rect(45, 8), k.pos(def.pos.x - 22, -8), k.color(safeColor('#444444')), k.opacity(0.9), k.z(18)])
sfxSlideDown()
await k.tween(-30, def.pos.y - 50, 0.25, (v) => { anvil.pos.y = v; base.pos.y = v + 22 }, k.easings.easeInQuad)
// BONK!
sfxBonk()
sfxExplosion()
k.shake(isCritical ? 25 : 15)
screenFlash(isCritical ? '#ffffff' : '#888888', 0.15)
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#888888')
spawnShockwave(def.pos.x, GROUND_Y, '#888888')
shadow.destroy()
def.play(isCritical ? 'knockback' : 'hit')
// Anvil bounces
await k.tween(anvil.pos.y, anvil.pos.y - 40, 0.1, (v) => { anvil.pos.y = v; base.pos.y = v + 22 }, k.easings.easeOutQuad)
await k.tween(anvil.pos.y, anvil.pos.y + 40, 0.1, (v) => { anvil.pos.y = v; base.pos.y = v + 22 }, k.easings.easeInQuad)
sfxBonk()
await k.wait(0.15)
k.tween(0.9, 0, 0.3, (v) => { anvil.opacity = v; base.opacity = v }).then(() => { anvil.destroy(); base.destroy() })
const push = dir * (isCritical ? 100 : 45)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const pianoDrop = makeDrop(55, 40, '#222222', '#ffcc00')
const safeDrop = makeDrop(35, 35, '#555555', '#888888')
const fridgeDrop = makeDrop(30, 45, '#dddddd', '#88ccff')
const couchDrop = makeDrop(55, 25, '#886644', '#aa8866')
const tvDrop = makeDrop(35, 30, '#333333', '#4488ff')
const toiletDrop = makeDrop(25, 30, '#ffffff', '#88bbff')
const bathtubDrop = makeDrop(50, 25, '#dddddd', '#88ccff')
const washerDrop = makeDrop(32, 35, '#cccccc', '#4488ff')
const vendingDrop = makeDrop(30, 45, '#ff4444', '#ffcc00')
const chandelierDrop = makeDrop(40, 30, '#ffcc00', '#ffffff')
const boulderDrop = makeDrop(35, 35, '#888888', '#666666', true)
const satelliteDrop = makeDrop(28, 35, '#888888', '#4488ff')
const ufoDrop = makeDrop(45, 15, '#88ff88', '#44ff44', false)
const giantShoeDrop = makeDrop(40, 25, '#553322', '#886644')
const giantPhoneDrop = makeDrop(22, 40, '#333333', '#4488ff')
const shoppingCart = makeVehicle(45, 30, '#888888', 7, [{ w: 40, h: 3, color: '#aaaaaa', ox: 0, oy: -15 }])
const forkliftCharge = makeVehicle(50, 30, '#ffaa00', 8, [{ w: 8, h: 25, color: '#ffcc44', ox: 28, oy: -20 }])
const tankRoll = makeVehicle(65, 28, '#556633', 10, [{ w: 40, h: 6, color: '#445522', ox: 25, oy: -14 }])
const helicopterStrike = makeVehicle(50, 20, '#446688', 0, [{ w: 55, h: 3, color: '#557799', ox: 0, oy: -12 }])
const airplaneSwoop = makeVehicle(60, 15, '#dddddd', 0, [{ w: 50, h: 4, color: '#cccccc', ox: 0, oy: -3 }])
const rocketRide = makeVehicle(30, 50, '#ff4444', 0, [{ w: 20, h: 8, color: '#ffffff', ox: 0, oy: -28 }])
const unicycleRun = makeVehicle(12, 18, '#ff8800', 10)
const zamboniCrush = makeVehicle(60, 30, '#ffffff', 9, [{ w: 55, h: 5, color: '#88ccff', ox: 0, oy: 15 }])
const tractorPlow = makeVehicle(55, 32, '#44aa22', 10, [{ w: 30, h: 20, color: '#888888', ox: 30, oy: -8 }])
const golfCartDrive = makeVehicle(40, 25, '#ffffff', 6, [{ w: 15, h: 18, color: '#44aa44', ox: -8, oy: -16 }])
return {
jetpackDive, motorbikeCharge, carSmash, boatCannon, anvilDrop, pianoDrop, safeDrop, fridgeDrop,
couchDrop, tvDrop, toiletDrop, bathtubDrop, washerDrop, vendingDrop, chandelierDrop,
boulderDrop, satelliteDrop, ufoDrop, giantShoeDrop, giantPhoneDrop, shoppingCart,
forkliftCharge, tankRoll, helicopterStrike, airplaneSwoop, rocketRide, unicycleRun,
zamboniCrush, tractorPlow, golfCartDrive,
}
}
@@ -0,0 +1,28 @@
import type { ChoreoContext } from '../types'
import type { ChoreoFn } from './factories'
import { createBasicChoreographies } from './basic'
import { createRangedChoreographies } from './ranged'
import { createHeavyChoreographies } from './heavy'
import { createWeaponsChoreographies } from './weapons'
import { createFoodChoreographies } from './food'
import { createSillyChoreographies } from './silly'
import { createBitcoinChoreographies } from './bitcoin'
import { createCreatorChoreographies } from './creator'
import { createUltimatesChoreographies } from './ultimates'
export type { ChoreoFn }
export function createChoreographyMap(ctx: ChoreoContext): Record<string, ChoreoFn> {
return {
...createBasicChoreographies(ctx),
...createRangedChoreographies(ctx),
...createHeavyChoreographies(ctx),
...createWeaponsChoreographies(ctx),
...createFoodChoreographies(ctx),
...createSillyChoreographies(ctx),
...createBitcoinChoreographies(ctx),
...createCreatorChoreographies(ctx),
...createUltimatesChoreographies(ctx),
}
}
@@ -0,0 +1,570 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createRangedChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
spawnBullet,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
async function projectile(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
await k.wait(0.25)
// Fire projectile
const projY = atk.pos.y - 40
const color = isCritical ? '#ff2d7b' : theme.accent
const size = isCritical ? 12 : 8
await spawnProjectile(atk.pos.x + dir * 30, projY, def.pos.x, def.pos.y - 40, color, size)
// Impact
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 15 : 6)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 20 : 8, color)
if (isCritical) { screenFlash(color); sfxExplosion() }
const push = dir * (isCritical ? 80 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function gunBurst(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
// Spawn visible pixel gun prop
const gunX = atk.pos.x + dir * 15
const gunY = atk.pos.y - 40
const gunDef = PROPS.gun
const gunObjs = spawnWeaponProp('gun', gunX, gunY, dir < 0, 18)
await k.wait(0.15)
const bulletCount = isCritical ? 8 : 4
const muzzleX = gunX + dir * 30
// Fire bullets in rapid succession
for (let i = 0; i < bulletCount; i++) {
sfxGunshot()
// Muzzle flash
const flash = k.add([k.circle(5), k.pos(muzzleX, gunY), k.color(k.Color.fromHex('#ffee00')), k.opacity(0.9), k.z(19), k.anchor('center')])
setTimeout(() => { if (flash.exists()) flash.destroy() }, 35)
const targetY = def.pos.y - 20 - Math.random() * 50
spawnBullet(muzzleX, gunY, def.pos.x, targetY)
// Recoil: bounce gun back
if (gunDef) moveProp(gunObjs, gunDef, gunX - dir * 4, gunY, dir < 0)
atk.pos.x -= dir * 3
await k.wait(0.04)
if (gunDef) moveProp(gunObjs, gunDef, gunX, gunY, dir < 0)
atk.pos.x += dir * 3
sfxBulletHit()
def.opacity = 0.4
await k.wait(0.02)
def.opacity = 1
}
// Remove gun
destroyProp(gunObjs)
spawnBulletHoles(def.pos.x, def.pos.y - 30, isCritical ? 5 : 3)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 15 : 8)
if (isCritical) { screenFlash('#ffee00'); sfxExplosion() }
const push = dir * (isCritical ? 100 : 40)
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function laserBeam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
// Charge up (growing ball of energy)
const chargeX = atk.pos.x + dir * 25
const chargeY = atk.pos.y - 40
const charge = k.add([k.circle(3), k.pos(chargeX, chargeY), k.color(safeColor('#ff2d2d')), k.opacity(0.9), k.z(18)])
await k.tween(3, isCritical ? 18 : 12, 0.3, (v) => { charge.radius = v }, k.easings.easeOutQuad)
sfxZap()
// FIRE BEAM (long rect that stretches across)
charge.destroy()
const beamLen = Math.abs(def.pos.x - chargeX) + 30
const beam = k.add([k.rect(beamLen, isCritical ? 12 : 7), k.pos(chargeX, chargeY), k.color(safeColor(isCritical ? '#ff2d2d' : '#ff6600')), k.opacity(0.9), k.z(18)])
// Core beam (brighter, thinner)
const core = k.add([k.rect(beamLen, isCritical ? 5 : 3), k.pos(chargeX, chargeY + 2), k.color(safeColor('#ffffff')), k.opacity(0.8), k.z(19)])
k.shake(isCritical ? 12 : 6)
screenFlash(isCritical ? '#ff2d2d' : '#ff6600', 0.15)
def.play(isCritical ? 'knockback' : 'hit')
sfxCritical()
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ff2d2d')
await k.wait(0.25)
// Beam fades
k.tween(0.9, 0, 0.15, (v) => { beam.opacity = v; core.opacity = v * 0.8 }).then(() => { beam.destroy(); core.destroy() })
const push = dir * (isCritical ? 130 : 60)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function rocketLauncher(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
await k.wait(0.15)
// Rocket body
const rocketX = atk.pos.x + dir * 30
const rocketY = atk.pos.y - 35
const rocket = k.add([k.rect(20, 8), k.pos(rocketX, rocketY), k.color(safeColor('#888888')), k.z(17), k.rotate(dir === 1 ? 0 : 180)])
const nose = k.add([k.rect(6, 6), k.pos(rocketX + dir * 12, rocketY + 1), k.color(safeColor('#ff2d2d')), k.z(17)])
sfxJetpack()
// Trail exhaust as rocket flies
const exhaustInt = trackedInterval(() => {
const p = k.add([k.circle(3 + Math.random() * 4), k.pos(rocket.pos.x - dir * 12, rocket.pos.y + (Math.random() - 0.5) * 8), k.color(safeColor(Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.7), k.z(16)])
p.onUpdate(() => { p.pos.x -= dir * 100 * k.dt(); p.opacity -= 3 * k.dt(); if (p.opacity <= 0) p.destroy() })
}, 30)
// Fly rocket to target
const dur = 0.3
await k.tween(0, 1, dur, (t) => { rocket.pos.x = rocketX + (def.pos.x - rocketX) * t; nose.pos.x = rocket.pos.x + dir * 12; rocket.pos.y = rocketY + Math.sin(t * Math.PI * 3) * 8; nose.pos.y = rocket.pos.y + 1 }, k.easings.linear)
clearTracked(exhaustInt)
rocket.destroy(); nose.destroy()
// EXPLOSION!
sfxExplosion()
k.shake(isCritical ? 30 : 18)
screenFlash('#ff6600', 0.2)
// Explosion rings
for (let i = 0; i < 3; i++) {
setTimeout(() => spawnShockwave(def.pos.x, def.pos.y - 20, ['#ff6600', '#ff2d2d', '#ffcc00'][i]), i * 60)
}
spawnSparks(def.pos.x, def.pos.y - 30, 25, '#ff6600')
def.play(isCritical ? 'knockback' : 'hit')
const push = dir * (isCritical ? 160 : 80)
const origDY = def.pos.y
await Promise.all([
k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.y, origDY - (isCritical ? 120 : 60), 0.15, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(def.pos.y, origDY, 0.2, (v) => { def.pos.y = v }, k.easings.easeInQuad)
),
])
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function bombThrow(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack')
sfxSpecial()
const bombX = atk.pos.x + dir * 20
const bombY = atk.pos.y - 50
const bomb = k.add([k.circle(8), k.pos(bombX, bombY), k.color(safeColor('#333333')), k.z(17)])
const fuse = k.add([k.rect(2, 8), k.pos(bombX, bombY - 8), k.color(safeColor('#ff6600')), k.z(17)])
// Arc to opponent
const midX = (bombX + def.pos.x) / 2
await k.tween(0, 1, 0.35, (t) => {
bomb.pos.x = bombX + (def.pos.x - bombX) * t
bomb.pos.y = bombY + (GROUND_Y - 10 - bombY) * t - Math.sin(t * Math.PI) * 120
fuse.pos.x = bomb.pos.x; fuse.pos.y = bomb.pos.y - 8
}, k.easings.linear)
fuse.destroy()
// Bounce
sfxBonk()
await k.tween(bomb.pos.y, bomb.pos.y - 30, 0.08, (v) => { bomb.pos.y = v }, k.easings.easeOutQuad)
await k.tween(bomb.pos.y, GROUND_Y - 10, 0.08, (v) => { bomb.pos.y = v }, k.easings.easeInQuad)
// BOOM
bomb.destroy()
sfxExplosion()
k.shake(isCritical ? 28 : 15)
screenFlash(isCritical ? '#ff2d2d' : '#ff6600', 0.2)
spawnShockwave(def.pos.x, GROUND_Y, '#ff6600')
spawnSparks(def.pos.x, def.pos.y - 20, 20, '#ffcc00')
def.play(isCritical ? 'knockback' : 'hit')
const push = dir * (isCritical ? 120 : 55)
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function minigunSpray(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
// Spawn visible pixel minigun
const gpx = atk.pos.x + dir * 15
const gpy = atk.pos.y - 38
const mgDef = PROPS.minigun
const mgObjs = spawnWeaponProp('minigun', gpx, gpy, dir < 0, 18)
await k.wait(0.1)
sfxSpecial()
const bulletCount = isCritical ? 16 : 10
const muzzleX = gpx + dir * 30
for (let i = 0; i < bulletCount; i++) {
sfxGunshot()
// Muzzle flash
const flash = k.add([k.circle(4 + Math.random() * 4), k.pos(muzzleX, gpy + (Math.random() - 0.5) * 6), k.color(k.Color.fromHex('#ffee00')), k.opacity(0.9), k.z(19), k.anchor('center')])
setTimeout(() => { if (flash.exists()) flash.destroy() }, 30)
const spread = (Math.random() - 0.5) * 50
spawnBullet(muzzleX, gpy, def.pos.x + (Math.random() - 0.5) * 30, def.pos.y - 20 + spread)
// Shell casing
const casing = k.add([k.rect(3, 2), k.pos(gpx - dir * 5, gpy - 5), k.color(k.Color.fromHex('#ffd700')), k.opacity(0.8), k.z(16)])
const casVx = -dir * (100 + Math.random() * 100)
const casVy = -200 - Math.random() * 100
casing.onUpdate(() => { casing.pos.x += casVx * k.dt(); casing.pos.y += casVy * k.dt() + 500 * k.dt() * k.dt(); casing.opacity -= 1.5 * k.dt(); if (casing.opacity <= 0) casing.destroy() })
// Recoil gun + attacker
if (mgDef) moveProp(mgObjs, mgDef, gpx - dir * 3, gpy, dir < 0)
atk.pos.x -= dir * 2
await k.wait(0.01)
if (mgDef) moveProp(mgObjs, mgDef, gpx, gpy, dir < 0)
atk.pos.x += dir * 2
if (i % 2 === 0) { def.opacity = 0.3; await k.wait(0.01); def.opacity = 1 }
sfxBulletHit()
await k.wait(0.04)
}
destroyProp(mgObjs)
spawnBulletHoles(def.pos.x, def.pos.y - 30, isCritical ? 8 : 5)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 18 : 10)
if (isCritical) { screenFlash('#ffee00'); sfxExplosion() }
const push = dir * (isCritical ? 120 : 55)
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function sniperShot(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Back up to wall
const wallX = dir === 1 ? 30 : W - 30
await k.tween(atk.pos.x, wallX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('special')
// Laser sight line
const laserY = atk.pos.y - 38
const laser = k.add([k.rect(Math.abs(def.pos.x - wallX), 1), k.pos(Math.min(wallX, def.pos.x), laserY), k.color(safeColor('#ff0000')), k.opacity(0.5), k.z(15)])
// Wobble the laser for "aiming"
for (let i = 0; i < 8; i++) {
laser.pos.y = laserY + (Math.random() - 0.5) * 15
await k.wait(0.06)
}
laser.pos.y = def.pos.y - 35
await k.wait(0.15)
// FIRE
laser.destroy()
sfxGunshot()
sfxCritical()
screenFlash('#ffffff', 0.1)
// Tracer (bright line from attacker to defender)
const tracer = k.add([k.rect(Math.abs(def.pos.x - wallX), 3), k.pos(Math.min(wallX, def.pos.x), def.pos.y - 35), k.color(safeColor('#ffee00')), k.opacity(0.9), k.z(18)])
k.tween(0.9, 0, 0.1, (v) => { tracer.opacity = v }).then(() => tracer.destroy())
// Impact
def.play('knockback')
k.shake(isCritical ? 22 : 12)
spawnSparks(def.pos.x, def.pos.y - 35, 18, '#ffee00')
if (isCritical) { sfxExplosion(); screenFlash('#ff2d2d', 0.15) }
const push = dir * (isCritical ? 130 : 65)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.25, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad),
k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
const tennisBallVolley = makeThrow('#ccff00', 8, 5, 40, true, '#aadd00')
const baseballPitch = makeThrow('#ffffff', 8, 4, 50, true, '#dddddd')
const bowlingBallRoll = makeThrow('#222222', 18, 1, 20, true, '#444444')
const rubberDuckFlood = makeThrow('#ffdd00', 10, 6, 45, true, '#ffee44')
const shoeBarrage = makeThrow('#553322', 12, 4, 55, false, '#775544')
const bookStorm = makeThrow('#884422', 14, 4, 60, false, '#aa6644')
const phoneFling = makeThrow('#333333', 10, 3, 50, false, '#4488ff')
const keyboardSmash = makeThrow('#dddddd', 16, 2, 65, false, '#aaaaaa')
const diceBarrage = makeThrow('#ffffff', 8, 6, 45, false, '#ff0000')
const marbleShower = makeThrow('#44aaff', 5, 10, 30, true, '#88ccff')
const snowballFight = makeThrow('#ffffff', 12, 4, 55, true, '#ddeeff')
const paintCanBlast = makeThrow('#ff4488', 14, 3, 60, true, '#ff88aa')
const tomatoBarrage = makeThrow('#ff2222', 10, 5, 50, true, '#ff4444')
const waterBalloonRain = makeThrow('#4488ff', 14, 3, 70, true, '#88bbff')
const pillowFight = makeThrow('#ffffff', 16, 3, 45, false, '#eeeeff')
const vinylRecordFling = makeThrow('#111111', 14, 3, 35, true, '#222222')
const frisbeeToss = makeThrow('#ff6600', 14, 2, 30, true, '#ffaa44')
const beachBallBonk = makeThrow('#ff4488', 20, 1, 80, true, '#ffaacc')
const soccerKick = makeThrow('#ffffff', 14, 2, 75, true, '#111111')
const basketballDunk = makeThrow('#ff8800', 16, 1, 90, true, '#ff6600')
const kamehameha = makeBeam('#44bbff', 10, 1)
const freezeRay = makeBeam('#88eeff', 8, 2)
const heatVision = makeBeam('#ff2200', 4, 3)
const plasmaBeam = makeBeam('#ff44ff', 8, 1)
const rainbowBeam = makeBeam('#ff8800', 12, 1)
const shadowBeam = makeBeam('#440066', 10, 1)
const sonicWave = makeBeam('#88ff88', 14, 2)
const gravityBeam = makeBeam('#6600aa', 6, 2)
const mindBlast = makeBeam('#ff88ff', 8, 1)
const pixelBeam = makeBeam('#44ff44', 6, 3)
const glitchBeam = makeBeam('#ff00ff', 5, 4)
const bassDropBeam = makeBeam('#8844ff', 16, 1)
const wifiBlast = makeBeam('#4488ff', 8, 2)
const dataStream = makeBeam('#00ff88', 4, 5)
const chainLightning = makeBeam('#ffff00', 5, 3)
const fireball = makeThrow('#ff4400', 14, 2, 30, true, '#ff8800')
const iceLance = makeThrow('#88eeff', 10, 3, 35, false, '#44ccff')
const thunderStrike = makeBeam('#ffff00', 6, 2)
const darkVoid = makeBeam('#440066', 12, 1)
const solarFlare: ChoreoFn = async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
// Blinding flash
screenFlash('#ffffff', 0.25)
await k.wait(0.2)
// Then punch while blinded
const cx = origDX - dir * 40
await k.tween(atk.pos.x, cx, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack'); sfxPunch(); await k.wait(0.1)
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 18 : 8)
if (isCritical) { screenFlash('#ffff00'); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, 12, '#ffff00')
const push = dir * (isCritical ? 100 : 45)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
const spiritBomb = makeDrop(30, 30, '#44bbff', '#88ddff', true)
const windSlash = makeBeam('#aaffaa', 3, 4)
const earthquakeStrike: ChoreoFn = async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
atk.play('attack')
// Ground pound
const origAY = atk.pos.y
await k.tween(origAY, origAY + 15, 0.08, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(isCritical ? 30 : 18)
spawnShockwave(atk.pos.x, GROUND_Y, '#aa8844')
// Screen shakes violently, ground cracks spread toward opponent
for (let i = 0; i < (isCritical ? 4 : 2); i++) {
const cx2 = atk.pos.x + dir * (80 + i * 60)
spawnSparks(cx2, GROUND_Y - 5, 5, '#aa8844')
await k.wait(0.05)
}
def.play(isCritical ? 'knockback' : 'hit'); k.shake(10)
if (isCritical) { screenFlash('#aa8844'); sfxCritical() }
spawnSparks(def.pos.x, GROUND_Y - 5, 10, '#886644')
const origDY = def.pos.y
await k.tween(origDY, origDY - 80, 0.15, (v) => { def.pos.y = v }, k.easings.easeOutQuad)
await k.tween(origDY - 80, origDY, 0.2, (v) => { def.pos.y = v }, k.easings.easeInQuad)
spawnShockwave(def.pos.x, GROUND_Y, '#886644')
atk.pos.y = GROUND_Y
await k.wait(0.2); atk.play('idle')
}
const portalPunch: ChoreoFn = async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial()
// Open portal near attacker
const p1 = k.add([k.circle(20), k.pos(atk.pos.x + dir * 40, atk.pos.y - 30), k.color(safeColor('#8844ff')), k.opacity(0.7), k.z(15)])
// Open portal near defender
const p2 = k.add([k.circle(20), k.pos(def.pos.x - dir * 30, def.pos.y - 30), k.color(safeColor('#ff44ff')), k.opacity(0.7), k.z(15)])
await k.wait(0.2)
atk.play('attack'); sfxPunch()
// Fist appears from portal 2
await k.wait(0.1)
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 16 : 8)
spawnSparks(def.pos.x, def.pos.y - 25, 10, '#ff44ff')
if (isCritical) { screenFlash('#8844ff'); sfxCritical() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.15)
k.tween(0.7, 0, 0.2, (v) => { p1.opacity = v; p2.opacity = v }).then(() => { p1.destroy(); p2.destroy() })
await k.wait(0.2); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const gravityWell = makeSwarm('#6600aa', 1, 25, 300)
const plasmaOrb = makeThrow('#ff44ff', 16, 1, 40, true, '#ff88ff')
const voidRift = makeBeam('#220044', 14, 1)
const crystalShards = makeThrow('#88ffff', 8, 6, 45, false, '#44ddff')
const meteorStrike = makeDrop(25, 25, '#ff4400', '#ff8800', true)
const arcaneBarrage = makeThrow('#8844ff', 10, 5, 50, true, '#aa66ff')
const holySmite = makeBeam('#ffee88', 10, 1)
const hexCurse = makeSwarm('#44ff44', 6, 6, 400, true)
const bloodMoon = makeBeam('#ff2222', 12, 1)
const shadowClone = makeSwarm('#333333', 3, 16, 500)
const treeSmash = makeDrop(20, 60, '#44aa22', '#228811')
const icebergDrop = makeDrop(40, 35, '#88eeff', '#44ccff')
const lavaSplash = makeExplosion(15, 15, '#ff4400', 0.2, '#ff8800', true)
const tornadoFling = makeGrab('spin', 160)
const tsunamiWave = makeBeam('#2266cc', 20, 1)
const avalanche = makeSwarm('#ffffff', 12, 10, 400)
const vineWhip = makeSwing(3, 60, '#44aa22', '#33aa11', 6, 12)
const sandstorm = makeSwarm('#ccaa66', 15, 4, 350, true)
const thornBarrage = makeThrow('#44aa22', 6, 8, 35, false, '#228811')
const pollenCloud = makeSwarm('#ffee44', 12, 3, 250, true)
const blackHole = makeSwarm('#220044', 1, 30, 200)
const photonTorpedo = makeThrow('#44bbff', 12, 3, 40, true, '#88ddff')
const tractor_beam = makeBeam('#44ff88', 10, 1)
const warpDrive = makeSpin('corkscrew', 180, 1)
const alienAbduction = makeGrab('up', 250)
const laserSword = makeSwing(4, 55, '#44bbff', '#88ddff', 4, 50)
const ionCannon = makeBeam('#88ff44', 8, 2)
const plasmaSword = makeSwing(4, 55, '#ff44ff', '#ff88ff', 4, 50)
const cosmicRay = makeBeam('#ffffff', 3, 5)
const asteroidBelt = makeThrow('#888888', 14, 5, 60, true, '#666666')
const novaBlast = makeBeam('#ffdd00', 10, 3)
const plasmaWhip = makeSwing(40, 3, '#ff44ff', '#ff88ff', 4, 4)
async function sonicBoom(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const wave = k.add([k.circle(10), k.pos(atk.pos.x + dir * 20, atk.pos.y - 30), k.color(safeColor('#88ccff')), k.opacity(0.6), k.z(17)])
await Promise.all([
k.tween(wave.pos.x, def.pos.x, 0.15, (v: number) => { wave.pos.x = v }),
k.tween(10, isCritical ? 40 : 25, 0.15, (v: number) => { wave.radius = v }),
])
wave.destroy(); sfxExplosion(); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 12 : 6, '#88ccff')
spawnShockwave(def.pos.x, def.pos.y - 20, '#88ccff')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#88ccff'); sfxCritical() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const prismBeam = makeBeam('#ff88ff', 7, 2)
const photonCannon = makeBeam('#ffff44', 12, 1)
const vortexPull = makeBeam('#8844ff', 8, 3)
const thunderClap = makeDrop(40, 3, '#ffdd00', '#ffffff', false)
async function gravityCrush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial()
// Defender gets crushed down
const origY = def.pos.y
await k.tween(def.pos.y, GROUND_Y + 5, 0.15, (v: number) => { def.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(isCritical ? 16 : 8)
spawnSparks(def.pos.x, GROUND_Y, isCritical ? 14 : 7, '#8844ff')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#8844ff'); sfxCritical() }
await k.wait(0.15)
await k.tween(def.pos.y, origY, 0.2, (v: number) => { def.pos.y = v }, k.easings.easeOutQuad)
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function shockPulse(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
const pulse = k.add([k.circle(5), k.pos(atk.pos.x, atk.pos.y - 30), k.color(safeColor('#ffff00')), k.opacity(0.7), k.z(17)])
await k.tween(5, isCritical ? 60 : 40, 0.2, (v: number) => { pulse.radius = v; pulse.opacity = 0.7 - v / 100 })
pulse.destroy(); sfxExplosion(); k.shake(isCritical ? 14 : 7)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 12 : 6, '#ffff00')
def.play(isCritical ? 'knockback' : 'hit')
if (isCritical) { screenFlash('#ffff00'); sfxCritical() }
const push = dir * (isCritical ? 85 : 38)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const auraBlast = makeBeam('#44ff88', 9, 2)
const darkPulse = makeBeam('#660066', 8, 2)
const radiantBeam = makeBeam('#ffaa44', 10, 1)
const glacialRay = makeBeam('#88ddff', 6, 3)
return {
projectile, gunBurst, laserBeam, rocketLauncher, bombThrow, minigunSpray, sniperShot,
tennisBallVolley, baseballPitch, bowlingBallRoll, rubberDuckFlood, shoeBarrage, bookStorm,
phoneFling, keyboardSmash, diceBarrage, marbleShower, snowballFight, paintCanBlast,
tomatoBarrage, waterBalloonRain, pillowFight, vinylRecordFling, frisbeeToss, beachBallBonk,
soccerKick, basketballDunk, kamehameha, freezeRay, heatVision, plasmaBeam, rainbowBeam,
shadowBeam, sonicWave, gravityBeam, mindBlast, pixelBeam, glitchBeam, bassDropBeam, wifiBlast,
dataStream, chainLightning, fireball, iceLance, thunderStrike, darkVoid, solarFlare,
spiritBomb, windSlash, earthquakeStrike, portalPunch, gravityWell, plasmaOrb, voidRift,
crystalShards, meteorStrike, arcaneBarrage, holySmite, hexCurse, bloodMoon, shadowClone,
treeSmash, icebergDrop, lavaSplash, tornadoFling, tsunamiWave, avalanche, vineWhip, sandstorm,
thornBarrage, pollenCloud, blackHole, photonTorpedo, tractor_beam, warpDrive, alienAbduction,
laserSword, ionCannon, plasmaSword, cosmicRay, asteroidBelt, novaBlast, plasmaWhip, sonicBoom,
prismBeam, photonCannon, vortexPull, thunderClap, gravityCrush, shockPulse, auraBlast,
darkPulse, radiantBeam, glacialRay,
}
}
@@ -0,0 +1,301 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createSillyChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
spawnBullet,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
const rubberChicken = makeSwing(8, 40, '#ffcc00', '#ff8844', 12, 12)
const airHornBlast = makeBeam('#ffcc00', 18, 1)
const vuvuzelaBlast = makeBeam('#ff4444', 16, 2)
const selfieStrike: ChoreoFn = async (atk, def, dir, origAX, origDX, isCritical) => {
// Pose, flash, then punch
atk.play('idle'); await k.wait(0.2)
screenFlash('#ffffff', 0.08); sfxSpecial(); await k.wait(0.15)
const cx = origDX - dir * 40
await k.tween(atk.pos.x, cx, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack'); sfxPunch(); await k.wait(0.1)
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 14 : 6)
if (isCritical) { screenFlash('#ffffff'); sfxCritical() }
spawnSparks(def.pos.x, def.pos.y - 25, 8, '#ffcc00')
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
const dabAttack = makeSpin('horizontal', 30, 1)
const flossAttack = makeSpin('horizontal', 20, 3)
const yeetThrow = makeGrab('toss', 180)
const tPoseAssert = makeBeam('#ffffff', 20, 1)
const emojiBarrage = makeThrow('#ffcc00', 12, 6, 50, true, '#ff8800')
const memeBeam = makeBeam('#ff00ff', 10, 2)
const ratioAttack = makeThrow('#4488ff', 10, 5, 40, true, '#2266dd')
const capThrow = makeThrow('#ff0000', 14, 1, 30, true, '#0000ff')
const fingerGuns: ChoreoFn = async (atk, def, dir, origAX, origDX, isCritical) => {
atk.play('special'); sfxSpecial(); await k.wait(0.15)
const shots = isCritical ? 6 : 3
for (let i = 0; i < shots; i++) {
sfxGunshot()
await spawnBullet(atk.pos.x + dir * 25, atk.pos.y - 35, def.pos.x, def.pos.y - 25 + (Math.random() - 0.5) * 30)
sfxBulletHit(); def.play('hit'); k.shake(3)
spawnSparks(def.pos.x, def.pos.y - 25, 3, '#ffcc00'); await k.wait(0.06)
}
if (isCritical) { screenFlash('#ffcc00'); sfxCritical() }
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 6)
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const micDrop = makeDrop(12, 16, '#333333', '#ffcc00')
const dramaticEntrance = makeSpin('corkscrew', 200, 1)
const clapback = makeSwing(10, 5, '#ffaa88')
const touchGrass = makeSwarm('#44aa22', 15, 4, 300, true)
const noScope = makeBeam('#ff4400', 3, 1)
const sharkBite = makeSwarm('#4488aa', 1, 20, 600)
const bearSwipe = makeSwarm('#885533', 1, 22, 500)
const eagleDive = makeSwarm('#886644', 3, 12, 700, true)
const snakeLunge = makeSwarm('#44aa22', 2, 10, 550, true)
const bullCharge = makeSwarm('#883322', 1, 24, 450)
const gorillaSlam = makeSwarm('#555544', 1, 25, 400)
const scorpionSting = makeSwarm('#aa8822', 2, 8, 600, true)
const crabPinch = makeSwarm('#ff4422', 4, 10, 350)
const batSwarm = makeSwarm('#333333', 12, 5, 500, true)
const wolfPack = makeSwarm('#777766', 5, 12, 450)
const spiderWeb = makeSwarm('#dddddd', 8, 4, 300, true)
const beeSwarm = makeSwarm('#ffcc00', 10, 4, 550, true)
const catScratch = makeSwarm('#ff8844', 6, 6, 600, true)
const dogPile = makeSwarm('#aa8855', 4, 14, 400)
const dolphinFlip = makeSwarm('#6688cc', 3, 14, 500, true)
const dynamiteBlast = makeExplosion(8, 20, '#ff4444', 0.35, '#ff6600')
const c4Detonation = makeExplosion(14, 8, '#556633', 0.25, '#ff4400')
const fireworksBurst = makeExplosion(10, 10, '#ff0088', 0.2, '#ff44ff', true)
const nukeStrike = makeExplosion(12, 12, '#ffcc00', 0.4, '#ff2200', true)
const volcanoErupt = makeExplosion(20, 15, '#ff4400', 0.3, '#ff6600')
const grenadeBlast = makeExplosion(10, 12, '#556633', 0.3, '#ff6600', true)
const partyPopper = makeExplosion(8, 16, '#ff44ff', 0.15, '#ffcc00')
const pinataSmash = makeExplosion(20, 22, '#ff88cc', 0.2, '#ffaa44')
const balloonPop = makeExplosion(18, 18, '#ff4488', 0.1, '#ff88aa', true)
const glitterBomb = makeExplosion(12, 12, '#ffcc88', 0.15, '#ff88ff', true)
const smokeBomb = makeExplosion(14, 14, '#888888', 0.1, '#666666', true)
const flashBang = makeExplosion(8, 8, '#ffffff', 0.1, '#ffffff', true)
const cherryBomb = makeExplosion(8, 8, '#ff2222', 0.2, '#ff4444', true)
const confettiCannon = makeExplosion(16, 10, '#ffcc00', 0.1, '#ff88ff')
const stinkBomb = makeExplosion(12, 12, '#88aa33', 0.15, '#aacc44', true)
const droneStrike = makeSwarm('#888888', 3, 8, 600)
const satelliteLaser = makeBeam('#ff0000', 4, 1)
const hackerAttack = makeThrow('#00ff44', 8, 8, 30, false, '#44ff88')
const bugSwarm = makeSwarm('#ff4444', 10, 4, 500, true)
const stackOverflow = makeDrop(35, 35, '#ff8800', '#f48024')
const segfault = makeExplosion(20, 20, '#ff0000', 0.1, '#880000')
const blueScreen = makeBeam('#0000ff', 20, 1)
const fourOhFour = makeExplosion(15, 15, '#888888', 0.2, '#ff4444')
const ctrlAltDelete = makeSwing(8, 12, '#333333', '#ffffff', 20, 12)
const bitcoinCrash = makeDrop(20, 20, '#ff8800', '#ffcc00', true)
const nftRugPull = makeGrab('toss', 200)
const aiUprising = makeSwarm('#ff0044', 8, 8, 500, true)
const captchaTrap = makeExplosion(25, 20, '#4488ff', 0.3, '#2266cc')
const popupSpam = makeThrow('#ffffff', 14, 8, 30, false, '#ff4444')
const malwareInject = makeBeam('#00ff00', 5, 3)
const bassDrop = makeExplosion(10, 10, '#8844ff', 0.2, '#6622cc', true)
const guitarSolo = makeBeam('#ff8800', 8, 3)
const drumSolo = makeThrow('#aa8844', 10, 6, 45, true, '#886633')
const dubstepCannon = makeBeam('#ff00ff', 14, 2)
const airGuitar = makeBeam('#ffcc00', 6, 3)
const karaokeAttack = makeBeam('#ff44aa', 10, 2)
const beatboxBlast = makeSwarm('#4488ff', 8, 6, 400, true)
const vinylScratch = makeSwing(14, 2, '#111111', '#ff4444', 14, 14)
async function plushiePummel(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 45
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.12, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
const hits = isCritical ? 5 : 3
for (let i = 0; i < hits; i++) {
atk.play(i % 2 === 0 ? 'attack' : 'kick'); sfxBoing()
def.play('hit'); k.shake(2)
spawnSparks(def.pos.x, def.pos.y - 25, 3, '#ffbbcc')
await k.wait(0.08)
}
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 8 : 4)
if (isCritical) { screenFlash('#ffbbcc'); sfxCritical() }
const push = dir * (isCritical ? 60 : 25)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function selfieStun(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxBoing(); await k.wait(0.15)
// Camera flash
screenFlash('#ffffff', 0.08); sfxSpecial()
await k.wait(0.1)
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 5)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 10 : 5, '#ffff00')
if (isCritical) sfxCritical()
const push = dir * (isCritical ? 75 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function vibeCheck(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 40
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.1, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack'); sfxPunch()
// Vibe waves
for (let i = 0; i < 3; i++) {
const wave = k.add([k.circle(5 + i * 10), k.pos(def.pos.x, def.pos.y - 25), k.color(safeColor(i % 2 === 0 ? '#ff44ff' : '#44ffff')), k.opacity(0.4), k.z(16)])
k.tween(wave.opacity, 0, 0.2, (v: number) => { wave.opacity = v }).then(() => wave.destroy())
}
def.play(isCritical ? 'knockback' : 'hit'); k.shake(isCritical ? 12 : 5)
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 10 : 5, '#ff44ff')
if (isCritical) { screenFlash('#ff44ff'); sfxCritical() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
const bananaPeelSlip = makeDrop(14, 6, '#ffdd00', '#ffff00', false)
const rubberDucky = makeThrow('#ffdd00', 10, 2, 70, true, '#ffaa00')
const kazooBlast = makeBeam('#ff8844', 6, 2)
const confettiPop = makeThrow('#ff44ff', 4, 12, 40, true)
const airHornStun = makeBeam('#ff4444', 8, 1)
async function touchGrassSmack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special'); sfxSpecial(); await k.wait(0.1)
// Grass shoots up from ground
const blades: any[] = []
for (let i = 0; i < (isCritical ? 8 : 4); i++) {
const gx = def.pos.x + (i - (isCritical ? 3.5 : 1.5)) * 10
const blade = k.add([k.rect(2, 0), k.pos(gx, GROUND_Y), k.color(safeColor('#44cc22')), k.opacity(0.8), k.z(15)])
blades.push(blade)
k.tween(0, 40 + Math.random() * 20, 0.12, (v: number) => { blade.height = v; blade.pos.y = GROUND_Y - v })
await k.wait(0.02)
}
sfxBonk(); k.shake(isCritical ? 12 : 5)
def.play(isCritical ? 'knockback' : 'hit')
spawnSparks(def.pos.x, def.pos.y - 20, isCritical ? 10 : 5, '#44cc22')
if (isCritical) { screenFlash('#44cc22'); sfxCritical() }
await k.wait(0.2); blades.forEach(b => b.destroy())
const push = dir * (isCritical ? 75 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
return {
rubberChicken, airHornBlast, vuvuzelaBlast, selfieStrike, dabAttack, flossAttack, yeetThrow,
tPoseAssert, emojiBarrage, memeBeam, ratioAttack, capThrow, fingerGuns, micDrop,
dramaticEntrance, clapback, touchGrass, noScope, sharkBite, bearSwipe, eagleDive, snakeLunge,
bullCharge, gorillaSlam, scorpionSting, crabPinch, batSwarm, wolfPack, spiderWeb, beeSwarm,
catScratch, dogPile, dolphinFlip, dynamiteBlast, c4Detonation, fireworksBurst, nukeStrike,
volcanoErupt, grenadeBlast, partyPopper, pinataSmash, balloonPop, glitterBomb, smokeBomb,
flashBang, cherryBomb, confettiCannon, stinkBomb, droneStrike, satelliteLaser, hackerAttack,
bugSwarm, stackOverflow, segfault, blueScreen, fourOhFour, ctrlAltDelete, bitcoinCrash,
nftRugPull, aiUprising, captchaTrap, popupSpam, malwareInject, bassDrop, guitarSolo, drumSolo,
dubstepCannon, airGuitar, karaokeAttack, beatboxBlast, vinylScratch, plushiePummel, selfieStun,
vibeCheck, bananaPeelSlip, rubberDucky, kazooBlast, confettiPop, airHornStun, touchGrassSmack,
}
}
@@ -0,0 +1,741 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createUltimatesChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
async function ultimateRapidBarrage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Rush in and land 8 alternating punches + kicks with screen shake
const contactX = origDX - dir * 45
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
const hitColors = ['#ff2d7b', '#00f0ff', '#ffe14d', '#39ff14', '#ff6600', '#b83dff', '#ffffff', '#ff2d2d']
for (let i = 0; i < 8; i++) {
atk.play(i % 2 === 0 ? 'attack' : 'kick')
sfxPunch()
def.play('hit')
k.shake(3 + i)
spawnSparks(def.pos.x + (Math.random() - 0.5) * 20, def.pos.y - 15 - Math.random() * 30, 4, hitColors[i])
def.pos.x += dir * 3
await k.wait(0.04)
}
// Final uppercut launch
atk.play('special'); sfxCritical()
def.play('knockback'); k.shake(15); screenFlash('#ffe14d', 0.1)
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ffe14d')
await k.tween(def.pos.y, def.pos.y - 60, 0.15, (v) => { def.pos.y = v }, k.easings.easeOutQuad)
await k.tween(def.pos.y, GROUND_Y, 0.15, (v) => { def.pos.y = v }, k.easings.easeInQuad)
sfxBonk()
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
])
atk.play('idle')
}
async function ultimateOrbitalStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Call down 5 orbital beams on the defender's position
atk.play('special'); sfxSpecial()
for (let i = 0; i < 5; i++) {
const bx = def.pos.x + (Math.random() - 0.5) * 60
const beam = k.add([k.rect(4, H), k.pos(bx, 0), k.color(safeColor('#ff2d7b')), k.opacity(0.8), k.z(20)])
sfxZap(); k.shake(6)
spawnSparks(bx, def.pos.y - 20, 8, '#ff2d7b')
await k.wait(0.08)
beam.destroy()
}
def.play('knockback'); k.shake(12); screenFlash('#ff2d7b', 0.15)
spawnShockwave(def.pos.x, GROUND_Y, '#ff2d7b')
await k.wait(0.3); atk.play('idle')
}
async function ultimateCloneSurround(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Spawn 4 clones around the defender, all attack simultaneously
const clones: any[] = []
const positions = [
{ x: def.pos.x - 60, y: GROUND_Y }, { x: def.pos.x + 60, y: GROUND_Y },
{ x: def.pos.x - 30, y: GROUND_Y - 50 }, { x: def.pos.x + 30, y: GROUND_Y - 50 },
]
for (const p of positions) {
const cl = k.add([k.rect(20, 30), k.pos(p.x, p.y), k.anchor('center'), k.color(safeColor(theme.accent)), k.opacity(0.5), k.z(12)])
clones.push(cl)
}
sfxZap(); await k.wait(0.2)
// All converge
for (const cl of clones) {
k.tween(cl.pos.x, def.pos.x, 0.1, (v) => { cl.pos.x = v }, k.easings.easeInQuad)
k.tween(cl.pos.y, def.pos.y - 20, 0.1, (v) => { cl.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.12)
sfxExplosion(); k.shake(15); screenFlash('#ffffff', 0.15)
def.play('knockback')
spawnSparks(def.pos.x, def.pos.y - 20, 20, theme.accent)
clones.forEach(c => c.destroy())
await k.wait(0.3)
}
async function ultimateGatlingFury(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Pull out massive gatling gun, fire 12 shots with screen shake
atk.play('special')
const gunLen = 60
const gx = atk.pos.x + dir * 25
const gy = atk.pos.y - 25
const barrel = k.add([k.rect(gunLen, 16), k.pos(gx, gy), k.color(safeColor('#555555')), k.opacity(1), k.z(16)])
sfxBoing()
await k.wait(0.15)
for (let i = 0; i < 12; i++) {
sfxGunshot()
k.shake(3)
const flash = k.add([k.circle(8), k.pos(gx + dir * gunLen, gy + (Math.random() - 0.5) * 10), k.color(safeColor('#ffee00')), k.opacity(0.9), k.z(18)])
setTimeout(() => { if (flash.exists()) flash.destroy() }, 40)
if (i % 3 === 0) {
def.play('hit')
spawnSparks(def.pos.x, def.pos.y - 20 - Math.random() * 20, 4, '#ff6600')
}
await k.wait(0.04)
}
barrel.destroy()
def.play('knockback'); k.shake(10)
sfxExplosion()
spawnShockwave(def.pos.x, GROUND_Y, '#ff6600')
await k.wait(0.3); atk.play('idle')
}
async function ultimateEarthquake(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Jump high, slam ground, cracks + shockwaves across screen
sfxZoomWhoosh()
await k.tween(atk.pos.y, GROUND_Y - 150, 0.2, (v) => { atk.pos.y = v }, k.easings.easeOutQuad)
atk.play('special')
await k.wait(0.1)
await k.tween(atk.pos.y, GROUND_Y, 0.1, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(25); screenFlash('#ff8800', 0.2)
// 3 shockwaves radiating outward
for (let i = 0; i < 3; i++) {
setTimeout(() => { spawnShockwave(atk.pos.x + (i - 1) * 100, GROUND_Y, '#ff8800'); k.shake(8) }, i * 100)
}
// Ground cracks
for (let i = 0; i < 6; i++) {
const cx = Math.random() * W
k.add([k.rect(2, 15 + Math.random() * 10), k.pos(cx, GROUND_Y - 5), k.color(safeColor('#aa6600')), k.opacity(0.7), k.z(1), { lifetime: 0.8 }])
}
def.play('knockback')
spawnSparks(def.pos.x, def.pos.y - 20, 12, '#ff8800')
await k.wait(0.4)
atk.play('idle')
}
const ultimateBouncingFury = makeSpin('corkscrew', 160, 4)
const ultimateTripleBeam = makeBeam('#ff2d7b', 10, 3)
const ultimateMegaThrow = makeThrow('#ff4400', 22, 5, 80, true, '#ffcc00')
const ultimateTornadoGrab = makeGrab('spin', 200)
const ultimateMeteorShower = makeSwarm('#ff6600', 8, 10, 500, true)
async function ultimateScreenNuke(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Charge up glowing orb, launch it, entire screen whites out
atk.play('special'); sfxSpecial()
const orb = k.add([k.circle(5), k.pos(atk.pos.x + dir * 20, atk.pos.y - 30), k.color(safeColor('#ffffff')), k.opacity(0.8), k.z(18)])
// Charge: grow orb
for (let i = 0; i < 8; i++) {
await k.wait(0.05)
orb.radius = 5 + i * 3
spawnSparks(orb.pos.x + (Math.random() - 0.5) * 20, orb.pos.y + (Math.random() - 0.5) * 20, 2, '#ffe14d')
}
sfxZoomWhoosh()
// Launch
await k.tween(orb.pos.x, def.pos.x, 0.15, (v) => { orb.pos.x = v }, k.easings.easeInQuad)
orb.destroy()
// Full screen explosion
sfxExplosion(); sfxCritical()
screenFlash('#ffffff', 0.3); k.shake(30)
for (let i = 0; i < 4; i++) {
spawnShockwave(def.pos.x + (Math.random() - 0.5) * 80, GROUND_Y, ['#ff2d7b', '#ffe14d', '#00f0ff', '#b83dff'][i])
}
spawnSparks(def.pos.x, def.pos.y - 30, 30, '#ffffff')
def.play('knockback')
await k.wait(0.4); atk.play('idle')
}
async function ultimateBlackHoleVortex(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Summon black hole that pulls defender in, crushes, and ejects
atk.play('special'); sfxZap()
const bh = k.add([k.circle(8), k.pos(W / 2, GROUND_Y - 50), k.anchor('center'), k.color(safeColor('#220044')), k.opacity(0.9), k.z(20)])
const ring = k.add([k.circle(30), k.pos(W / 2, GROUND_Y - 50), k.anchor('center'), k.color(safeColor('#b83dff')), k.opacity(0.3), k.z(19)])
// Grow
await k.tween(8, 35, 0.3, (v) => { bh.radius = v; ring.radius = v + 20 }, k.easings.easeOutQuad)
// Pull defender in
sfxZoomWhoosh()
await k.tween(def.pos.x, W / 2, 0.2, (v) => { def.pos.x = v }, k.easings.easeInQuad)
await k.tween(def.pos.y, GROUND_Y - 50, 0.15, (v) => { def.pos.y = v }, k.easings.easeInQuad)
def.play('hit'); k.shake(10)
// Crush
for (let i = 0; i < 5; i++) {
k.shake(5); sfxPunch()
spawnSparks(W / 2 + (Math.random() - 0.5) * 30, GROUND_Y - 50 + (Math.random() - 0.5) * 20, 3, '#b83dff')
await k.wait(0.06)
}
// Eject
sfxExplosion(); k.shake(20); screenFlash('#b83dff', 0.15)
def.play('knockback')
await Promise.all([
k.tween(def.pos.x, origDX + dir * 40, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.y, GROUND_Y, 0.2, (v) => { def.pos.y = v }, k.easings.easeOutQuad),
])
bh.destroy(); ring.destroy()
spawnShockwave(def.pos.x, GROUND_Y, '#b83dff')
await k.tween(def.pos.x, origDX, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.2); atk.play('idle')
}
async function ultimateDimensionalRift(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Open rift, attacker disappears, attacks from multiple portals
sfxZap(); atk.opacity = 0
const colors = ['#ff2d7b', '#00f0ff', '#39ff14', '#b83dff']
for (let i = 0; i < 4; i++) {
const px = def.pos.x + (i % 2 === 0 ? -80 : 80)
const py = GROUND_Y - (i < 2 ? 0 : 60)
// Portal flash
const p = k.add([k.circle(15), k.pos(px, py), k.color(safeColor(colors[i])), k.opacity(0.7), k.z(15)])
await k.wait(0.06)
// Attack from portal
atk.pos.x = px; atk.pos.y = py; atk.opacity = 1
atk.play('attack'); sfxPunch()
def.play('hit'); k.shake(5)
spawnSparks(def.pos.x, def.pos.y - 20, 5, colors[i])
await k.wait(0.06)
atk.opacity = 0; p.destroy()
}
// Final slam from above
atk.pos.x = def.pos.x; atk.pos.y = GROUND_Y - 120; atk.opacity = 1
atk.play('special')
sfxZoomWhoosh()
await k.tween(atk.pos.y, GROUND_Y, 0.12, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(18); screenFlash('#ffffff', 0.15)
def.play('knockback')
spawnSparks(def.pos.x, GROUND_Y - 20, 20, '#ffffff')
spawnShockwave(def.pos.x, GROUND_Y, '#ffffff')
await k.tween(atk.pos.x, origAX, 0.25, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('idle')
}
async function ultimateTimeSlow(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Everything goes slow-mo, attacker lands 6 devastating hits in "frozen" time
scanlineGlitch(0.1)
// Dim screen
const dim = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor('#000022')), k.opacity(0.4), k.z(25)])
sfxZap()
const contactX = origDX - dir * 45
await k.tween(atk.pos.x, contactX, 0.3, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad)
for (let i = 0; i < 6; i++) {
atk.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxPunch()
def.play('hit')
k.shake(4)
spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 15 - Math.random() * 25, 5, '#00f0ff')
await k.wait(0.12) // Slow timing
}
dim.destroy()
// Time resumes — final hit with full impact
sfxCritical(); sfxExplosion(); k.shake(20); screenFlash('#00f0ff', 0.2)
def.play('knockback')
spawnShockwave(def.pos.x, GROUND_Y, '#00f0ff')
spawnSparks(def.pos.x, def.pos.y - 20, 20, '#00f0ff')
await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
atk.play('idle')
}
async function ultimateSummonArmy(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Summon an army of 8 mini clones that charge across the screen
atk.play('special'); sfxSpecial()
const army: any[] = []
for (let i = 0; i < 8; i++) {
const ax = atk.pos.x + (Math.random() - 0.5) * 30
const ay = GROUND_Y - Math.random() * 10
const m = k.add([k.rect(12, 18), k.pos(ax, ay), k.anchor('center'), k.color(safeColor(theme.accent)), k.opacity(0.6), k.z(11)])
army.push(m)
}
await k.wait(0.2)
sfxZoomWhoosh()
// All charge at defender
await Promise.all(army.map((m, i) =>
k.tween(m.pos.x, def.pos.x + (Math.random() - 0.5) * 20, 0.15 + i * 0.02, (v) => { m.pos.x = v }, k.easings.easeInQuad)
))
sfxExplosion(); k.shake(15)
def.play('knockback')
spawnSparks(def.pos.x, def.pos.y - 20, 20, theme.accent)
army.forEach(m => m.destroy())
await k.wait(0.3)
}
const ultimateHellfireRain = makeSwarm('#ff4400', 12, 12, 600, true)
const ultimateCrystalCage = makeSwarm('#88ccff', 10, 8, 350, false)
const ultimateGigaDrill = makeSpin('corkscrew', 200, 5)
const ultimateSuperSlam = makeGrab('up', 250)
const ultimateAvalancheDrop = makeExplosion(30, 30, '#aaeeff', 0.2, '#ffffff', true)
async function ultimateBulletTimeBarrage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Phase 1: World goes dark, heavy slow-mo overlay
const dim = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor('#000022')), k.opacity(0), k.z(25)])
await k.tween(0, 0.55, 0.2, (v) => { dim.opacity = v })
scanlineGlitch(0.15)
glitchRGB(0.1)
// Phase 2: Defender fires a barrage of slow-mo bullets
def.play('special')
for (let i = 0; i < 8; i++) {
sfxGunshot()
const bx = def.pos.x - dir * 20
const by = def.pos.y - 35 + (Math.random() - 0.5) * 30
const bullet = k.add([
k.rect(12, 3),
k.pos(bx, by),
k.color(safeColor('#ff4444')),
k.opacity(0.85),
k.z(26),
k.anchor('center'),
])
// Bullet trace
const trace = k.add([
k.rect(60, 1),
k.pos(bx + dir * 30, by),
k.color(safeColor('#ff6644')),
k.opacity(0.4),
k.z(25),
])
setTimeout(() => { if (trace.exists()) trace.destroy() }, 300)
// Bullet travels slowly
k.tween(bullet.pos.x, atk.pos.x + (Math.random() - 0.5) * 30, 0.3, (v) => { if (bullet.exists()) bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() })
await k.wait(0.05)
}
// Phase 3: Attacker dodges each bullet with slow-mo lean (afterimages cascade)
atk.play('idle')
for (let lean = 0; lean < 3; lean++) {
const leanDir = lean % 2 === 0 ? -1 : 1
const ghost = k.add([
k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.7, FRAME_SIZE * Math.abs(atk.scale.y)),
k.pos(atk.pos.x, atk.pos.y),
k.color(safeColor('#ffd700')),
k.opacity(0.3),
k.z(24),
k.anchor('bot'),
])
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 350)
await k.tween(atk.pos.y, GROUND_Y + leanDir * 15, 0.08, (v) => { atk.pos.y = v }, k.easings.easeOutQuad)
await k.tween(atk.pos.y, GROUND_Y, 0.08, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.1)
// Phase 4: Time RESUMES — golden flash, attacker blazes forward
dim.destroy()
screenFlash('#ffd700', 0.15)
sfxZoomWhoosh()
sfxCritical()
// Afterimage rush trail (8 ghosts across the screen)
const contactX = origDX - dir * 40
for (let i = 0; i < 8; i++) {
const gx = origAX + ((contactX - origAX) * i / 8)
const ghost = k.add([
k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.6, FRAME_SIZE * Math.abs(atk.scale.y) * 0.9),
k.pos(gx, GROUND_Y),
k.color(safeColor('#ffd700')),
k.opacity(0.35 - i * 0.04),
k.z(9),
k.anchor('bot'),
])
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 300)
}
await k.tween(atk.pos.x, contactX, 0.06, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
// Phase 5: 8-hit slow-mo combo — each hit with sparks and screen shake
for (let i = 0; i < 8; i++) {
atk.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxPunch()
def.play('hit')
k.shake(5 + i * 2)
spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 15 - Math.random() * 25, 5, i % 2 === 0 ? '#ffd700' : '#ffffff')
await k.wait(0.04)
}
// Phase 6: Final devastating blow — full screen effects
sfxExplosion()
k.shake(30)
screenFlash('#ffd700', 0.25)
def.play('knockback')
spawnShockwave(def.pos.x, GROUND_Y, '#ffd700')
spawnShockwave(def.pos.x + dir * 30, GROUND_Y, '#ffaa00')
spawnSparks(def.pos.x, def.pos.y - 25, 25, '#ffd700')
glitchRGB(0.3)
await k.wait(0.4)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.25, (v) => { atk.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
])
atk.play('idle')
}
async function ultimateFinaleRush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Full-screen dash through, reverse, dash again, then combo finish
sfxZoomWhoosh()
// First pass
await k.tween(atk.pos.x, origDX + dir * 60, 0.1, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
atk.play('attack'); sfxPunch(); def.play('hit'); k.shake(8)
spawnSparks(def.pos.x, def.pos.y - 25, 8, '#ff2d7b')
// Bounce off wall
await k.wait(0.05)
atk.scale.x = -atk.scale.x
await k.tween(atk.pos.x, origDX - dir * 60, 0.1, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
atk.play('kick'); sfxKick(); def.play('hit'); k.shake(8)
spawnSparks(def.pos.x, def.pos.y - 30, 8, '#00f0ff')
// Back to normal orientation
atk.scale.x = -atk.scale.x
// Final mega hit
await k.tween(atk.pos.x, origDX - dir * 40, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
atk.play('special'); sfxCritical(); sfxExplosion()
def.play('knockback'); k.shake(22); screenFlash('#ffe14d', 0.2)
spawnSparks(def.pos.x, def.pos.y - 25, 25, '#ffe14d')
spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d')
// Return
await Promise.all([
k.tween(atk.pos.x, origAX, 0.25, (v) => { atk.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
])
atk.play('idle')
}
async function ultimateMeteorCrash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Attacker launches into sky, becomes a meteor, crashes into arena
sfxZoomWhoosh(); atk.play('special')
await k.tween(atk.pos.y, -100, 0.2, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
atk.opacity = 0
await k.wait(0.3)
// Meteor warning
const warning = k.add([k.circle(15), k.pos(def.pos.x, GROUND_Y - 5), k.color(safeColor('#ff0000')), k.opacity(0.4), k.z(1)])
warning.onUpdate(() => { warning.opacity = 0.2 + Math.sin(k.time() * 20) * 0.2 })
sfxZap()
await k.wait(0.4)
warning.destroy()
// Impact
atk.pos.x = def.pos.x; atk.pos.y = -80; atk.opacity = 1
// Fire trail
const fireTrail: any[] = []
await k.tween(-80, GROUND_Y, 0.15, (v) => {
atk.pos.y = v
const f = k.add([k.circle(6), k.pos(atk.pos.x + (Math.random() - 0.5) * 15, v - 15), k.color(safeColor('#ff6600')), k.opacity(0.7), k.z(9)])
fireTrail.push(f)
k.tween(f.opacity, 0, 0.3, (op) => { f.opacity = op }).then(() => { if (f.exists()) f.destroy() })
}, k.easings.easeInQuad)
sfxExplosion(); sfxCritical(); k.shake(30); screenFlash('#ff6600', 0.3)
def.play('knockback')
for (let i = 0; i < 5; i++) {
spawnShockwave(def.pos.x + (Math.random() - 0.5) * 60, GROUND_Y, ['#ff6600', '#ff2d2d', '#ffe14d', '#ff8800', '#ffffff'][i])
}
spawnSparks(def.pos.x, GROUND_Y - 20, 30, '#ff6600')
fireTrail.forEach(f => { if (f.exists()) f.destroy() })
await k.tween(atk.pos.x, origAX, 0.3, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('idle')
}
async function ultimateScreenShatter(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Hit so hard the "screen" shatters with fragment overlay
const contactX = origDX - dir * 45
await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('special'); sfxCritical()
def.play('knockback'); k.shake(25)
// Create "shatter" fragments
const frags: any[] = []
for (let i = 0; i < 12; i++) {
const fx = W * Math.random()
const fy = H * Math.random() * 0.8
const fw = 30 + Math.random() * 60
const fh = 20 + Math.random() * 40
const frag = k.add([k.rect(fw, fh), k.pos(fx, fy), k.color(safeColor('#111111')), k.opacity(0.7), k.z(30), k.rotate(Math.random() * 30 - 15)])
frags.push(frag)
}
sfxExplosion(); screenFlash('#ffffff', 0.3)
spawnSparks(def.pos.x, def.pos.y - 20, 25, '#ffffff')
await k.wait(0.5)
// Fragments fall away
await Promise.all(frags.map(f =>
k.tween(f.pos.y, H + 50, 0.4, (v) => { f.pos.y = v; f.opacity *= 0.98 }, k.easings.easeInQuad).then(() => f.destroy())
))
await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('idle')
}
async function ultimateGravityReverse(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Flip gravity — defender floats up, gets juggled, slammed back down
atk.play('special'); sfxZap()
// Defender floats up
await k.tween(def.pos.y, GROUND_Y - 140, 0.3, (v) => { def.pos.y = v }, k.easings.easeOutQuad)
def.play('hit')
// Juggle with projectiles
for (let i = 0; i < 4; i++) {
await spawnProjectile(atk.pos.x + dir * 20, atk.pos.y - 30, def.pos.x, def.pos.y, '#b83dff', 8)
sfxBulletHit(); k.shake(4)
def.pos.y += (i % 2 === 0 ? -15 : 15)
spawnSparks(def.pos.x, def.pos.y, 4, '#b83dff')
await k.wait(0.06)
}
// Slam down
sfxZoomWhoosh()
await k.tween(def.pos.y, GROUND_Y, 0.12, (v) => { def.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(20); screenFlash('#b83dff', 0.15)
def.play('knockback')
spawnShockwave(def.pos.x, GROUND_Y, '#b83dff')
spawnSparks(def.pos.x, GROUND_Y - 10, 20, '#b83dff')
await k.wait(0.3); atk.play('idle')
}
async function ultimateInfiniteCombo(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// 15-hit combo with escalating screen effects
const contactX = origDX - dir * 40
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
const hitAnims = ['attack', 'kick', 'special', 'attack', 'kick', 'special', 'attack', 'kick', 'attack', 'kick', 'special', 'attack', 'kick', 'attack', 'special']
for (let i = 0; i < 15; i++) {
atk.play(hitAnims[i])
sfxPunch()
def.play('hit')
k.shake(2 + Math.floor(i / 3))
const hue = (i * 25) % 360
spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 10 - Math.random() * 30, 3, `hsl(${hue}, 100%, 60%)`)
if (i % 5 === 4) screenFlash(`hsl(${hue}, 100%, 50%)`, 0.05)
def.pos.x += dir * 2
await k.wait(0.03)
}
// Final blow
atk.play('special'); sfxCritical(); sfxExplosion()
def.play('knockback'); k.shake(25); screenFlash('#ffffff', 0.2)
spawnSparks(def.pos.x, def.pos.y - 25, 25, '#ffe14d')
spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d')
await Promise.all([
k.tween(atk.pos.x, origAX, 0.25, (v) => { atk.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
])
atk.play('idle')
}
const ultimatePlasmaStorm = makeSwarm('#ff00ff', 15, 10, 700, true)
const ultimateFrozenCoffin = makeExplosion(35, 50, '#88ccff', 0.4, '#aaeeff', false)
const ultimateLavaGeyser = makeBeam('#ff4400', 14, 4)
const ultimateChainGrab = makeGrab('spin', 280)
const ultimateRocketBarrage = makeSwarm('#ff6600', 10, 14, 550, false)
async function ultimateArmageddon(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Full screen goes dark, rain of fire, massive explosion, screen flash
const darkness = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor('#000000')), k.opacity(0), k.z(25)])
await k.tween(0, 0.7, 0.3, (v) => { darkness.opacity = v })
atk.play('special'); sfxSpecial()
// Rain of fire across entire screen
for (let wave = 0; wave < 3; wave++) {
for (let i = 0; i < 6; i++) {
const rx = Math.random() * W
const meteor = k.add([k.circle(6 + Math.random() * 8), k.pos(rx, -20), k.color(safeColor(wave === 2 ? '#ffffff' : '#ff6600')), k.opacity(0.9), k.z(26)])
k.tween(meteor.pos.y, GROUND_Y, 0.2 + Math.random() * 0.1, (v) => { meteor.pos.y = v }, k.easings.easeInQuad)
.then(() => { sfxBonk(); spawnSparks(rx, GROUND_Y - 5, 4, '#ff6600'); meteor.destroy() })
}
k.shake(10 + wave * 5)
await k.wait(0.15)
}
// Final mega explosion
darkness.destroy()
sfxExplosion(); sfxCritical(); k.shake(35); screenFlash('#ffffff', 0.4)
def.play('knockback')
for (let i = 0; i < 6; i++) {
spawnShockwave(W * (i / 5), GROUND_Y, ['#ff2d2d', '#ff6600', '#ffe14d', '#ffffff', '#ff2d7b', '#b83dff'][i])
}
spawnSparks(def.pos.x, def.pos.y - 30, 40, '#ffe14d')
glitchRGB(0.3)
await k.wait(0.5); atk.play('idle')
}
async function ultimateOmegaBeam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Charge up, fire a massive full-width beam that covers half the screen
atk.play('special'); sfxSpecial()
// Charge particles converging on attacker
for (let i = 0; i < 10; i++) {
const px = atk.pos.x + (Math.random() - 0.5) * 200
const py = atk.pos.y - 30 + (Math.random() - 0.5) * 100
const p = k.add([k.circle(3), k.pos(px, py), k.color(safeColor('#ffe14d')), k.opacity(0.8), k.z(15)])
k.tween(p.pos.x, atk.pos.x + dir * 15, 0.2, (v) => { p.pos.x = v })
k.tween(p.pos.y, atk.pos.y - 25, 0.2, (v) => { p.pos.y = v }).then(() => p.destroy())
}
await k.wait(0.3)
sfxZoomWhoosh()
// Massive beam
const beamW = W
const beamH = 40
const beam = k.add([k.rect(beamW, beamH), k.pos(atk.pos.x, atk.pos.y - 25 - beamH / 2), k.color(safeColor('#ffe14d')), k.opacity(0.9), k.z(22)])
const beamCore = k.add([k.rect(beamW, beamH / 2), k.pos(atk.pos.x, atk.pos.y - 25 - beamH / 4), k.color(safeColor('#ffffff')), k.opacity(0.7), k.z(23)])
sfxExplosion(); k.shake(20)
await k.wait(0.1)
def.play('knockback')
spawnSparks(def.pos.x, def.pos.y - 20, 20, '#ffe14d')
k.shake(25)
await k.wait(0.2)
// Beam fades
await k.tween(0.9, 0, 0.2, (v) => { beam.opacity = v; beamCore.opacity = v * 0.7 })
beam.destroy(); beamCore.destroy()
screenFlash('#ffe14d', 0.15)
spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d')
await k.wait(0.2); atk.play('idle')
}
async function ultimateBeyondTheScreen(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Attacker punches defender so hard they "break" off-screen, then gravity brings them back
const contactX = origDX - dir * 45
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('special'); sfxCritical()
k.shake(30); screenFlash('#ffffff', 0.2)
// Defender flies off screen
def.play('knockback')
await Promise.all([
k.tween(def.pos.x, def.pos.x + dir * W, 0.2, (v) => { def.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.y, -100, 0.2, (v) => { def.pos.y = v }, k.easings.easeOutQuad),
])
def.opacity = 0
sfxExplosion()
glitchRGB(0.2)
await k.wait(0.5)
// Defender falls back from top
def.pos.x = origDX + (Math.random() - 0.5) * 40
def.pos.y = -80
def.opacity = 1
sfxZoomWhoosh()
await k.tween(def.pos.y, GROUND_Y, 0.25, (v) => { def.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(12)
spawnShockwave(def.pos.x, GROUND_Y, '#ff2d2d')
spawnSparks(def.pos.x, GROUND_Y - 10, 15, '#ff2d2d')
await k.tween(def.pos.x, origDX, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('idle')
}
async function ultimateMatrixDodge(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Matrix-style slow-mo: attacker fires 3 projectiles, "dodges" in slo-mo, then counters
atk.play('special')
// Fire 3 slow projectiles
for (let i = 0; i < 3; i++) {
await spawnProjectile(atk.pos.x + dir * 20, atk.pos.y - 25 + i * 10, def.pos.x, def.pos.y - 20 + i * 10, '#ffe14d', 8)
sfxGunshot()
await k.wait(0.1)
}
// Pause
scanlineGlitch(0.08)
await k.wait(0.15)
// Teleport behind
sfxZap()
atk.pos.x = origDX + dir * 30
atk.scale.x = -atk.scale.x
atk.play('special')
// Counter attack combo
for (let i = 0; i < 4; i++) {
sfxPunch(); def.play('hit'); k.shake(5 + i * 2)
spawnSparks(def.pos.x, def.pos.y - 20 - i * 8, 5, '#00f0ff')
await k.wait(0.05)
}
sfxCritical(); sfxExplosion(); k.shake(22); screenFlash('#00f0ff', 0.2)
def.play('knockback')
spawnShockwave(def.pos.x, GROUND_Y, '#00f0ff')
atk.scale.x = -atk.scale.x
await k.tween(atk.pos.x, origAX, 0.25, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('idle')
}
async function ultimateWorldEnder(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// The ultimate ultimate: 3-phase attack — air combo, ground slam, finishing beam
// Phase 1: Launch defender into air
const contactX = origDX - dir * 45
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack'); sfxPunch(); def.play('hit')
await k.tween(def.pos.y, GROUND_Y - 120, 0.15, (v) => { def.pos.y = v }, k.easings.easeOutQuad)
// Phase 2: Air combo
await k.tween(atk.pos.y, GROUND_Y - 100, 0.1, (v) => { atk.pos.y = v }, k.easings.easeOutQuad)
for (let i = 0; i < 6; i++) {
atk.play(i % 2 === 0 ? 'attack' : 'kick')
sfxPunch(); def.play('hit'); k.shake(4 + i)
spawnSparks(def.pos.x, def.pos.y - 10, 4, `hsl(${i * 60}, 100%, 60%)`)
def.pos.y -= 5
await k.wait(0.04)
}
// Phase 3: Slam down together
atk.play('special'); sfxZoomWhoosh()
await Promise.all([
k.tween(atk.pos.y, GROUND_Y, 0.12, (v) => { atk.pos.y = v }, k.easings.easeInQuad),
k.tween(def.pos.y, GROUND_Y, 0.12, (v) => { def.pos.y = v }, k.easings.easeInQuad),
])
sfxExplosion(); sfxCritical(); k.shake(35); screenFlash('#ffffff', 0.3)
def.play('knockback')
// Massive aftermath
for (let i = 0; i < 8; i++) {
setTimeout(() => {
spawnShockwave(def.pos.x + (Math.random() - 0.5) * 100, GROUND_Y, ['#ff2d7b', '#ffe14d', '#00f0ff', '#b83dff', '#39ff14', '#ff6600', '#ffffff', '#ff2d2d'][i])
spawnSparks(def.pos.x + (Math.random() - 0.5) * 60, GROUND_Y - 10 - Math.random() * 40, 5, '#ffffff')
}, i * 50)
}
glitchRGB(0.4)
dimensionalShift(0.3)
await k.wait(0.5)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.3, (v) => { atk.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
])
atk.play('idle')
}
const ultimateAbsoluteZero = makeExplosion(40, 40, '#aaeeff', 0.5, '#ffffff', true)
const ultimateSolarCannon = makeBeam('#ffe14d', 16, 5)
const ultimatePhantomStrike = makeSpin('vertical', 250, 6)
const ultimateGodPunch = makeGrab('toss', 350)
const ultimateVoidCollapse = makeExplosion(50, 50, '#220044', 0.6, '#b83dff', true)
return {
ultimateRapidBarrage, ultimateOrbitalStrike, ultimateCloneSurround, ultimateGatlingFury,
ultimateEarthquake, ultimateBouncingFury, ultimateTripleBeam, ultimateMegaThrow,
ultimateTornadoGrab, ultimateMeteorShower, ultimateScreenNuke, ultimateBlackHoleVortex,
ultimateDimensionalRift, ultimateTimeSlow, ultimateSummonArmy, ultimateHellfireRain,
ultimateCrystalCage, ultimateGigaDrill, ultimateSuperSlam, ultimateAvalancheDrop,
ultimateBulletTimeBarrage, ultimateFinaleRush, ultimateMeteorCrash, ultimateScreenShatter,
ultimateGravityReverse, ultimateInfiniteCombo, ultimatePlasmaStorm, ultimateFrozenCoffin,
ultimateLavaGeyser, ultimateChainGrab, ultimateRocketBarrage, ultimateArmageddon,
ultimateOmegaBeam, ultimateBeyondTheScreen, ultimateMatrixDodge, ultimateWorldEnder,
ultimateAbsoluteZero, ultimateSolarCannon, ultimatePhantomStrike, ultimateGodPunch,
ultimateVoidCollapse,
}
}
@@ -0,0 +1,319 @@
import type { Fighter, ChoreoContext } from '../types'
import { createFactories } from './factories'
import type { ChoreoFn } from './factories'
export function createWeaponsChoreographies(ctx: ChoreoContext): Record<string, ChoreoFn> {
const {
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
trackedInterval, clearTracked,
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
glitchRGB, scanlineGlitch, dimensionalShift,
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
sfxZap, sfxZoomWhoosh, sfxBlock,
} = ctx
const {
spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage,
makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop,
makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab,
} = createFactories(ctx)
async function swordSlash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 50
await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('special')
sfxSpecial()
// Draw sword arc (series of rects forming a crescent)
const arcParts: any[] = []
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI - Math.PI / 4
const ax = contactX + dir * 20 + Math.cos(angle) * 60
const ay = atk.pos.y - 40 + Math.sin(angle) * 60
const part = k.add([
k.rect(25, 3), k.pos(ax, ay), k.color(safeColor(isCritical ? '#00f0ff' : '#aaddff')),
k.opacity(0.9), k.z(18), k.rotate(angle * 180 / Math.PI + 90),
])
arcParts.push(part)
}
await k.wait(0.08)
// Slash hits
arcParts.forEach(p => { k.tween(0.9, 0, 0.2, (v) => { if (p.exists()) p.opacity = v }).then(() => { if (p.exists()) p.destroy() }) })
sfxKick()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 18 : 8)
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#aaddff')
if (isCritical) { screenFlash('#00f0ff'); sfxCritical() }
const push = dir * (isCritical ? 110 : 50)
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function hammerSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 40
await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
// Giant hammer
const handle = k.add([k.rect(6, 70), k.pos(atk.pos.x + dir * 15, atk.pos.y - 90), k.color(safeColor('#8B4513')), k.anchor('bot'), k.z(18), k.rotate(dir === 1 ? -60 : 60)])
const head = k.add([k.rect(30, 20), k.pos(handle.pos.x + dir * 10, handle.pos.y - 60), k.color(safeColor('#888888')), k.z(18)])
atk.play('special')
sfxSpecial()
// Swing hammer down
await k.tween(dir === 1 ? -60 : 60, dir === 1 ? 70 : -70, 0.15, (v) => { handle.angle = v }, k.easings.easeInQuad)
handle.destroy(); head.destroy()
sfxExplosion()
// Ground crack effect
for (let i = 0; i < 6; i++) {
const crack = k.add([k.rect(2, 15 + Math.random() * 20), k.pos(def.pos.x + (i - 3) * 12, GROUND_Y - 5), k.color(safeColor('#ff6600')), k.opacity(0.8), k.z(4)])
setTimeout(() => { k.tween(0.8, 0, 0.6, (v) => { if (crack.exists()) crack.opacity = v }).then(() => { if (crack.exists()) crack.destroy() }) }, 400)
}
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 25 : 14)
spawnShockwave(def.pos.x, GROUND_Y, '#ff6600')
spawnSparks(def.pos.x, def.pos.y - 20, 18, '#ff6600')
if (isCritical) screenFlash('#ff6600')
const push = dir * (isCritical ? 120 : 55)
k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function whipCrack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack')
sfxSpecial()
await k.wait(0.1)
// Whip extends from attacker to defender (series of small segments)
const segments: any[] = []
const fromX = atk.pos.x + dir * 20
const fromY = atk.pos.y - 40
const toX = def.pos.x
const toY = def.pos.y - 30
const segCount = 12
for (let i = 0; i < segCount; i++) {
const t = i / segCount
const sx = fromX + (toX - fromX) * t
const sy = fromY + (toY - fromY) * t + Math.sin(t * Math.PI * 3) * 20
const seg = k.add([k.rect(6, 3), k.pos(sx, sy), k.color(safeColor('#8B4513')), k.opacity(0), k.z(17)])
segments.push(seg)
}
// Animate whip extending (each segment appears in sequence)
for (let i = 0; i < segments.length; i++) {
segments[i].opacity = 0.9
await k.wait(0.01)
}
// CRACK!
sfxPunch()
sfxBonk()
spawnSparks(toX, toY, 10, '#ffcc00')
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 14 : 7)
if (isCritical) screenFlash('#ffcc00')
// Whip retracts
for (let i = segments.length - 1; i >= 0; i--) {
if (segments[i].exists()) segments[i].destroy()
await k.wait(0.008)
}
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function katanaCombo(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 45
sfxSpecial()
await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
const slashCount = isCritical ? 5 : 3
for (let i = 0; i < slashCount; i++) {
atk.play(i % 2 === 0 ? 'attack' : 'kick')
sfxPunch()
// Slash trail (diagonal line)
const angle = [45, -45, 0, 135, -135][i % 5]
const slash = k.add([k.rect(50, 3), k.pos(def.pos.x, def.pos.y - 30), k.color(safeColor('#aaddff')), k.opacity(0.9), k.z(18), k.rotate(angle), k.anchor('center')])
k.tween(0.9, 0, 0.15, (v) => { if (slash.exists()) slash.opacity = v }).then(() => { if (slash.exists()) slash.destroy() })
def.play('hit')
k.shake(4 + i * 2)
spawnSparks(def.pos.x + (Math.random() - 0.5) * 30, def.pos.y - 20 - Math.random() * 30, 5, '#aaddff')
await k.wait(0.1)
}
// Final slash — big X mark
if (isCritical) {
const s1 = k.add([k.rect(80, 4), k.pos(def.pos.x, def.pos.y - 30), k.color(safeColor('#00f0ff')), k.opacity(0.9), k.z(18), k.rotate(45), k.anchor('center')])
const s2 = k.add([k.rect(80, 4), k.pos(def.pos.x, def.pos.y - 30), k.color(safeColor('#00f0ff')), k.opacity(0.9), k.z(18), k.rotate(-45), k.anchor('center')])
sfxCritical()
screenFlash('#00f0ff')
k.shake(20)
await k.wait(0.2)
k.tween(0.9, 0, 0.3, (v) => { if (s1.exists()) s1.opacity = v; if (s2.exists()) s2.opacity = v }).then(() => { if (s1.exists()) s1.destroy(); if (s2.exists()) s2.destroy() })
}
def.play('knockback')
const push = dir * (isCritical ? 110 : 50)
k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
async function chainsawRev(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 35
atk.play('special')
// Chainsaw buzzing (visual: vibrating rect)
const saw = k.add([k.rect(40, 8), k.pos(atk.pos.x + dir * 25, atk.pos.y - 35), k.color(safeColor('#888888')), k.z(18)])
const blade = k.add([k.rect(35, 3), k.pos(atk.pos.x + dir * 30, atk.pos.y - 32), k.color(safeColor('#ffcc00')), k.z(19)])
sfxSpecial()
// Rev up (shake the saw)
for (let i = 0; i < 8; i++) {
saw.pos.y += (Math.random() - 0.5) * 4
blade.pos.y = saw.pos.y + 3
await k.wait(0.03)
}
// Charge forward
await Promise.all([
k.tween(atk.pos.x, contactX, 0.15, (v) => { atk.pos.x = v; saw.pos.x = v + dir * 25; blade.pos.x = v + dir * 30 }, k.easings.easeInQuad),
])
// GRIND — multi-hit with sparks
const hits = isCritical ? 6 : 4
for (let i = 0; i < hits; i++) {
sfxPunch()
spawnSparks(def.pos.x - dir * 10, def.pos.y - 20 - Math.random() * 30, 5, '#ffcc00')
def.play('hit')
k.shake(3)
saw.pos.y += (Math.random() - 0.5) * 6
blade.pos.y = saw.pos.y + 3
await k.wait(0.05)
}
saw.destroy(); blade.destroy()
if (isCritical) { sfxCritical(); screenFlash('#ff6600') }
def.play('knockback')
k.shake(isCritical ? 18 : 10)
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ff6600')
const push = dir * (isCritical ? 110 : 50)
k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
}
const baseballBat = makeSwing(6, 55, '#aa7744', '#996633', 16, 10)
const fryingPan = makeSwing(5, 40, '#333333', '#444444', 22, 4)
const rollingPin = makeSwing(8, 45, '#dda855')
const umbrellaWhack = makeSwing(5, 55, '#222222', '#ff4444', 30, 3)
const mopSwipe = makeSwing(4, 60, '#aa9977', '#dddddd', 14, 12)
const broomStrike = makeSwing(4, 58, '#886633', '#558833', 12, 10)
const wrenchSmash = makeSwing(6, 38, '#888888', '#aaaaaa', 18, 8)
const crowbarSwing = makeSwing(5, 50, '#555555', '#444444', 8, 16)
const plungerSlam = makeSwing(5, 48, '#aa8844', '#ff4444', 12, 12)
const tennisRacket = makeSwing(3, 42, '#666666', '#44aa44', 20, 14)
const hockeyStick = makeSwing(4, 55, '#663300', '#222222', 12, 8)
const poolCue = makeSwing(3, 65, '#aa8855')
const guitarSmash = makeSwing(8, 50, '#884422', '#ffcc00', 18, 8)
const shovelBash = makeSwing(5, 52, '#666666', '#888888', 16, 12)
const pickaxeStrike = makeSwing(4, 50, '#886644', '#888888', 14, 8)
const oarSwing = makeSwing(5, 58, '#bb9966')
const flagpoleSmash = makeSwing(3, 65, '#888888', '#ff0000', 18, 12)
const caneWhack = makeSwing(4, 55, '#222222', '#ddaa00', 6, 6)
const rulerSlap = makeSwing(3, 50, '#ddcc88')
const newspaperRoll = makeSwing(6, 42, '#ccccbb')
const dragonBreath = makeBeam('#ff4400', 12, 2)
const maceSwing = makeSwing(5, 48, '#888888', '#666666', 16, 16)
const crossbowBolt = makeThrow('#886633', 6, 3, 25, false, '#aa8855')
const shieldBash = makeGrab('toss', 50)
const flailSwing = makeSwing(3, 55, '#888888', '#666666', 14, 14)
const battleAxe = makeSwing(6, 50, '#886633', '#888888', 20, 12)
const holyWater = makeThrow('#88ccff', 10, 3, 55, true, '#aaeeff')
const potionThrow = makeThrow('#44ff44', 10, 2, 60, true, '#88ff88')
const scrollBlast = makeBeam('#ffcc88', 8, 1)
const enchantedArrow = makeThrow('#ff88ff', 6, 3, 30, false, '#ffaaff')
const nunchakuSpin = makeSwing(25, 4, '#884422', '#ffaa44', 6, 6)
const tridentThrust = makeSwing(6, 40, '#44aaff', '#88ddff', 8, 4)
async function boomerangToss(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack'); sfxPunch(); await k.wait(0.08)
const boom = k.add([k.rect(14, 4), k.pos(atk.pos.x + dir * 20, atk.pos.y - 30), k.color(safeColor('#aa6633')), k.opacity(0.9), k.z(18), k.rotate(0)])
// Fly to defender
k.tween(boom.angle, 1080, 0.25, (v: number) => { boom.angle = v })
await k.tween(boom.pos.x, def.pos.x + dir * 30, 0.12, (v: number) => { boom.pos.x = v })
sfxBulletHit(); k.shake(5)
spawnSparks(def.pos.x, def.pos.y - 25, 4, '#aa6633')
def.play('hit')
// Return trip
await k.tween(boom.pos.x, atk.pos.x + dir * 15, 0.12, (v: number) => { boom.pos.x = v })
boom.destroy()
if (isCritical) {
// Double hit on return
def.play('knockback'); k.shake(14); screenFlash('#aa6633'); sfxCritical()
spawnSparks(def.pos.x, def.pos.y - 25, 10, '#aa6633')
}
const push = dir * (isCritical ? 85 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3); atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const morningstar = makeSwing(8, 30, '#666666', '#aaaaaa', 12, 12)
const scytheSlash = makeSwing(35, 4, '#888888', '#aaddff', 6, 6)
const javelinthrow = makeThrow('#886644', 4, 1, 30, false, '#aa8866')
const shuriken = makeThrow('#999999', 6, 4, 25, false, '#cccccc')
const warhammerCrush = makeDrop(16, 30, '#666666', '#ffaa00', false)
const chakramSlice = makeThrow('#ddaa44', 8, 2, 20, true, '#ffcc44')
const blowgunDart = makeThrow('#44aa22', 3, 3, 15, false, '#66cc44')
return {
swordSlash, hammerSmash, whipCrack, katanaCombo, chainsawRev, baseballBat, fryingPan,
rollingPin, umbrellaWhack, mopSwipe, broomStrike, wrenchSmash, crowbarSwing, plungerSlam,
tennisRacket, hockeyStick, poolCue, guitarSmash, shovelBash, pickaxeStrike, oarSwing,
flagpoleSmash, caneWhack, rulerSlap, newspaperRoll, dragonBreath, maceSwing, crossbowBolt,
shieldBash, flailSwing, battleAxe, holyWater, potionThrow, scrollBlast, enchantedArrow,
nunchakuSpin, tridentThrust, boomerangToss, morningstar, scytheSlash, javelinthrow, shuriken,
warhammerCrush, chakramSlice, blowgunDart,
}
}
+46
View File
@@ -17,6 +17,52 @@ export interface FightContext {
safeColor: (color: string) => ReturnType<KaplayInstance['Color']['fromHex']>
}
export interface ChoreoContext {
k: KaplayInstance
W: number
H: number
GROUND_Y: number
FRAME_SIZE: number
theme: { bg: string; ground: string; accent: string }
safeColor: (color: string) => any
trackedInterval: (fn: () => void, ms: number) => ReturnType<typeof setInterval>
clearTracked: (id: ReturnType<typeof setTimeout>) => void
// Particles & effects
spawnSparks: (x: number, y: number, count: number, color: string) => void
spawnBulletHoles: (x: number, y: number, count: number) => void
spawnExhaust: (x: number, y: number, duration: number) => ReturnType<typeof setInterval>
spawnProjectile: (fromX: number, fromY: number, toX: number, toY: number, color: string, size?: number) => Promise<void>
spawnShockwave: (x: number, y: number, color: string) => void
screenFlash: (color: string, duration?: number) => void
impactFreeze: (duration?: number) => Promise<void>
spawnAfterimages: (fighter: Fighter, count?: number) => void
glitchRGB: (duration?: number) => void
scanlineGlitch: (duration?: number) => void
dimensionalShift: (duration?: number) => void
// SFX
sfxPunch: () => void
sfxKick: () => void
sfxSpecial: () => void
sfxCritical: () => void
sfxExplosion: () => void
sfxBoing: () => void
sfxBonk: () => void
sfxBulletHit: () => void
sfxClash: () => void
sfxCoin: () => void
sfxGunshot: () => void
sfxJetpack: () => void
sfxRapidPunch: () => void
sfxSlideDown: () => void
sfxZap: () => void
sfxZoomWhoosh: () => void
sfxBlock: () => void
// Other effects
spawnBullet: (fromX: number, fromY: number, toX: number, toY: number) => Promise<void>
spawnGrotesqueDetails: (fighter: any, scaleFactor: number) => void
destroyGrotesqueDetails: () => void
}
export interface FightSceneConfig {
canvas: HTMLCanvasElement
botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }