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