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:
co-authored by
Claude Opus 4.6
parent
08786a9b06
commit
cabe0adb31
+19
-5422
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,
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user