import type { Fighter, ChoreoContext } from '../types' import { createFactories } from './factories' import type { ChoreoFn } from './factories' export function createBitcoinChoreographies(ctx: ChoreoContext): Record { 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, } }