diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 201ab5e..3c4ec0d 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -2,12 +2,14 @@ import kaplay from 'kaplay' import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp, LoadSpriteOpt } from 'kaplay' import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, generateHumanFighterSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites' -import type { Fighter, FightSceneConfig, RoundEvent, FightContext } from './fight/types' +import type { Fighter, FightSceneConfig, RoundEvent, FightContext, ChoreoContext } from './fight/types' export type { FightSceneConfig, RoundEvent } from './fight/types' import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MOVES, CREATOR_ULTIMATES } from './fight/constants' import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles' import { glitchRGB as _glitchRGB, scanlineGlitch as _scanlineGlitch, vhsTracking as _vhsTracking, dimensionalShift as _dimensionalShift } from './fight/effects' import { spawnProjectile as _spawnProjectile } from './fight/projectiles' +import { createChoreographyMap } from './fight/choreography' +import { createFactories } from './fight/choreography/factories' import { sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit, sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect, @@ -1807,2348 +1809,22 @@ export async function createFightScene(config: FightSceneConfig) { let comboA = 0 let comboB = 0 - // === CHOREOGRAPHY FUNCTIONS === - // Each returns a Promise and handles all movement + animation - - async function dashPunch(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const contactX = origDX - dir * 50 - // Fast sprint to contact - await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - atk.play('attack') - sfxPunch() - await k.wait(0.12) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 12 : 5) - spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 12 : 5, '#ffcc00') - if (isCritical) { screenFlash('#ffffff'); sfxCritical(); await impactFreeze() } - spawnAfterimages(atk, 2) - // Push defender - const push = dir * (isCritical ? 100 : 35) - k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad) - await k.wait(0.35) - // Return - 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), - ]) + // === CHOREOGRAPHY SYSTEM (extracted to fight/choreography/) === + const choreoCtx: ChoreoContext = { + k, W, H, GROUND_Y, FRAME_SIZE, theme, + safeColor: (color: string) => safeColor(k, color), + 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, spawnGrotesqueDetails, destroyGrotesqueDetails, } - - async function aerialSlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const origAY = atk.pos.y - const contactX = origDX - dir * 20 - // Jump way up - await k.tween(atk.pos.y, origAY - 220, 0.18, (v) => { atk.pos.y = v }, k.easings.easeOutQuad) - atk.play('kick') - sfxKick() - // Arc down onto opponent - await Promise.all([ - k.tween(atk.pos.x, contactX, 0.22, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(atk.pos.y, origAY, 0.22, (v) => { atk.pos.y = v }, k.easings.easeInQuad), - ]) - // SLAM - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 20 : 10) - spawnSparks(def.pos.x, def.pos.y - 20, 15, theme.accent) - spawnShockwave(def.pos.x, GROUND_Y, theme.accent) - if (isCritical) { screenFlash('#ff2d7b'); sfxExplosion(); await impactFreeze(0.1) } else { sfxPunch() } - spawnAfterimages(atk, 3) - const push = dir * (isCritical ? 120 : 50) - k.tween(def.pos.x, origDX + push, 0.25, (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(atk.pos.y, origAY, 0.15, (v) => { atk.pos.y = v }, k.easings.easeOutBounce), - k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - async function flyingKick(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const origAY = atk.pos.y - const contactX = origDX - dir * 30 - // Small jump + fly horizontally - atk.play('kick') - sfxKick() - await Promise.all([ - k.tween(atk.pos.x, contactX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad), - k.tween(atk.pos.y, origAY - 80, 0.1, (v) => { atk.pos.y = v }, k.easings.easeOutQuad).then(() => - k.tween(atk.pos.y, origAY, 0.1, (v) => { atk.pos.y = v }, k.easings.easeInQuad) - ), - ]) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 14 : 6) - spawnSparks(def.pos.x, def.pos.y - 40, 8, '#ff6600') - if (isCritical) { sfxCritical(); screenFlash('#ff6600'); await impactFreeze() } else { sfxPunch() } - spawnAfterimages(atk, 3) - 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) - // Bounce back - 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.easeInOutQuad), - ]) - } - - async function dashThrough(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const behindX = origDX + dir * 80 - atk.play('attack') - sfxSpecial() - // Blazing fast dash through opponent - await k.tween(atk.pos.x, behindX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeInQuad) - // Hit happens mid-pass - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 15 : 7) - spawnSparks(origDX, def.pos.y - 30, 10, '#00f0ff') - spawnAfterimages(atk, 3) - if (isCritical) { screenFlash('#00f0ff'); await impactFreeze() } - // Dramatic pause behind opponent - await k.wait(0.3) - // Zip back to start - atk.play('idle') - await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - await k.wait(0.1) - } - - async function uppercut(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const contactX = origDX - dir * 40 - const origDY = def.pos.y - // Run to contact - await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - atk.play('special') - sfxSpecial() - await k.wait(0.1) - // Launch defender into the air! - def.play('knockback') - k.shake(isCritical ? 18 : 8) - spawnSparks(def.pos.x, def.pos.y - 30, 12, '#ffe14d') - if (isCritical) { screenFlash('#ffe14d'); sfxExplosion(); await impactFreeze(0.1) } - const launchHeight = isCritical ? 250 : 160 - await k.tween(def.pos.y, origDY - launchHeight, 0.25, (v) => { def.pos.y = v }, k.easings.easeOutQuad) - await k.wait(0.15) - // Defender crashes back down - await k.tween(def.pos.y, origDY, 0.3, (v) => { def.pos.y = v }, k.easings.easeInQuad) - spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d') - k.shake(5) - sfxPunch() - await k.wait(0.2) - // Return - await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad) - } - - async function multiHit(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const contactX = origDX - dir * 45 - // Sprint to contact - await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - const hitCount = isCritical ? 5 : 3 - for (let i = 0; i < hitCount; i++) { - atk.play(i % 2 === 0 ? 'attack' : 'kick') - sfxPunch() - await k.wait(0.08) - def.play('hit') - k.shake(3 + i) - spawnSparks(def.pos.x + (Math.random() - 0.5) * 30, def.pos.y - 20 - Math.random() * 40, 4, '#ff2d7b') - // Jitter defender - def.pos.x += dir * 8 - await k.wait(0.08) - } - if (isCritical) { screenFlash('#ff2d7b'); sfxCritical() } - // Final big push - def.play('knockback') - k.shake(12) - const push = dir * (isCritical ? 130 : 60) - 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - 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 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 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 groundPound(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const origAY = atk.pos.y - // Jump super high off screen - sfxJetpack() - await k.tween(atk.pos.y, -100, 0.25, (v) => { atk.pos.y = v }, k.easings.easeOutQuad) - // Move to center of stage - const slamX = (origAX + origDX) / 2 - atk.pos.x = slamX - await k.wait(0.2) - // SLAM DOWN - atk.play('special') - sfxSpecial() - await k.tween(atk.pos.y, origAY, 0.12, (v) => { atk.pos.y = v }, k.easings.easeInQuad) - // Massive shockwave - k.shake(isCritical ? 30 : 18) - screenFlash(isCritical ? '#ff2d2d' : '#ffe14d', 0.2) - sfxExplosion() - spawnShockwave(slamX, GROUND_Y, theme.accent) - spawnSparks(slamX, GROUND_Y - 10, 25, theme.accent) - // Defender gets launched - const origDY = def.pos.y - def.play('knockback') - const push = dir * (isCritical ? 130 : 70) - const launchH = isCritical ? 180 : 100 - 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 - launchH, 0.2, (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) - ), - ]) - k.shake(5) - spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d') - await k.wait(0.3) - // 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 teleportStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Fade out - sfxSpecial() - spawnSparks(atk.pos.x, atk.pos.y - 30, 8, '#b83dff') - await k.tween(1, 0, 0.15, (v) => { atk.opacity = v }) - // Teleport behind opponent - atk.pos.x = origDX + dir * 60 - await k.wait(0.1) - // Fade in behind - spawnSparks(atk.pos.x, atk.pos.y - 30, 8, '#b83dff') - await k.tween(0, 1, 0.1, (v) => { atk.opacity = v }) - // Strike from behind! - atk.play('attack') - sfxPunch() - await k.wait(0.1) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 15 : 8) - spawnSparks(def.pos.x, def.pos.y - 30, 12, '#b83dff') - if (isCritical) { screenFlash('#b83dff'); sfxCritical() } - // Push defender forward (reversed since we're behind) - const push = -dir * (isCritical ? 80 : 40) - k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad) - await k.wait(0.3) - // Teleport back home - spawnSparks(atk.pos.x, atk.pos.y - 30, 6, '#b83dff') - await k.tween(1, 0, 0.1, (v) => { atk.opacity = v }) - atk.pos.x = origAX - await k.tween(0, 1, 0.1, (v) => { atk.opacity = v }) - spawnSparks(origAX, atk.pos.y - 30, 6, '#b83dff') - atk.play('idle') - await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad) - } - - async function fullScreenDash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Back up to wall - const wallX = dir === 1 ? 20 : W - 20 - await k.tween(atk.pos.x, wallX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - await k.wait(0.1) - // FULL SPEED DASH across entire screen! - atk.play('kick') - sfxSpecial() - const targetX = dir === 1 ? W - 20 : 20 - // Speed lines - for (let i = 0; i < 8; i++) { - const lineY = Math.random() * H * 0.7 - const line = k.add([ - k.rect(W, 2), - k.pos(0, lineY), - k.color(safeColor(k,theme.accent)), - k.opacity(0.3), - k.z(12), - ]) - setTimeout(() => { if (line.exists()) line.destroy() }, 300) - } - await k.tween(atk.pos.x, targetX, 0.18, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad) - // Hit happens when passing through defender - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 22 : 10) - spawnSparks(origDX, def.pos.y - 30, 15, '#00f0ff') - screenFlash(isCritical ? '#ffffff' : '#00f0ff', 0.12) - if (isCritical) sfxExplosion() - else sfxKick() - const push = dir * (isCritical ? 100 : 50) - k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad) - await k.wait(0.35) - // Return from far side - 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // === THEMED CHOREOGRAPHIES (challenge-specific props) === - - // Helper: spawn a flying prop (rect or circle) from A to B - function spawnProp( - fromX: number, fromY: number, toX: number, toY: number, - w: number, h: number, color: string, speed: number = 600, isCircle: boolean = false, - ): Promise { - return new Promise(resolve => { - const prop = k.add([ - isCircle ? k.circle(w / 2) : k.rect(w, h), - k.pos(fromX, fromY), - k.color(safeColor(k,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() - }) - }) - } - - // Helper: spawn multiple flying objects in a stream - 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) - } - } - - // CODE GOLF: swing a giant golf club, golf balls fly at opponent - async function golfClubSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const contactX = origDX - dir * 60 - await k.tween(atk.pos.x, contactX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - // Spawn golf club (long thin rect) - const club = k.add([ - k.rect(8, 60), - k.pos(atk.pos.x + dir * 20, atk.pos.y - 60), - k.color(safeColor(k,'#888888')), - k.anchor('bot'), - k.z(18), - k.rotate(dir === 1 ? -45 : 45), - ]) - // Club head (wider rect at end) - const clubHead = k.add([ - k.rect(18, 12), - k.pos(club.pos.x + dir * 15, club.pos.y - 55), - k.color(safeColor(k,'#cccccc')), - k.z(18), - ]) - atk.play('attack') - sfxSpecial() - // Swing the club - await k.tween(dir === 1 ? -45 : 45, dir === 1 ? 90 : -90, 0.15, (v) => { club.angle = v }, k.easings.easeInQuad) - sfxBonk() - // Club hits - launch golf balls! - club.destroy() - clubHead.destroy() - const ballCount = isCritical ? 6 : 3 - for (let i = 0; i < ballCount; i++) { - const ballY = def.pos.y - 20 - Math.random() * 40 - const ball = k.add([ - k.circle(5), - k.pos(atk.pos.x + dir * 30, atk.pos.y - 40), - k.color(safeColor(k,'#ffffff')), - k.z(16), - ]) - sfxPunch() - const targetX = def.pos.x + (Math.random() - 0.5) * 30 - k.tween(0, 1, 0.12, (t) => { - ball.pos.x = (atk.pos.x + dir * 30) + (targetX - atk.pos.x - dir * 30) * t - ball.pos.y = (atk.pos.y - 40) + (ballY - atk.pos.y + 40) * t - Math.sin(t * Math.PI) * 60 - }).then(() => { - ball.destroy() - spawnSparks(targetX, ballY, 4, '#ffffff') - }) - await k.wait(0.06) - } - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 18 : 8) - if (isCritical) screenFlash('#39ff14') - spawnSparks(def.pos.x, def.pos.y - 30, 10, '#39ff14') - 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) - 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), - ]) - } - - // ROAST BATTLE: fire breath — stream of fire from attacker's face - async function fireBreath(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const contactX = origDX - dir * 80 - await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - atk.play('special') - sfxSpecial() - // Fire stream! - const fireX = atk.pos.x + dir * 25 - const fireY = atk.pos.y - 45 - const fireCount = isCritical ? 40 : 20 - for (let i = 0; i < fireCount; i++) { - const p = k.add([ - k.circle(4 + Math.random() * 6), - k.pos(fireX, fireY + (Math.random() - 0.5) * 15), - k.color(safeColor(k,Math.random() > 0.3 ? '#ff6600' : Math.random() > 0.5 ? '#ffcc00' : '#ff2d2d')), - k.opacity(0.9), - k.z(16), - ]) - const speed = 400 + Math.random() * 400 - const vy = (Math.random() - 0.5) * 100 - p.onUpdate(() => { - p.pos.x += dir * speed * k.dt() - p.pos.y += vy * k.dt() - p.opacity -= 2 * k.dt() - if (p.opacity <= 0 || Math.abs(p.pos.x - fireX) > Math.abs(def.pos.x - fireX) + 30) p.destroy() - }) - if (i % 5 === 0) { - def.opacity = 0.5 - setTimeout(() => { if (def.exists()) def.opacity = 1 }, 50) - } - await k.wait(0.02) - } - await k.wait(0.1) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 20 : 8) - spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ff6600') - if (isCritical) { screenFlash('#ff6600'); sfxExplosion() } - // Defender on fire briefly - const fireTimer = trackedInterval(() => { - if (!def.exists()) { clearTracked(fireTimer); return } - k.add([ - k.circle(3), - k.pos(def.pos.x + (Math.random() - 0.5) * 25, def.pos.y - Math.random() * 60), - k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), - k.opacity(0.7), - k.z(11), - ]).onUpdate(function(this: any) { this.pos.y -= 80 * k.dt(); this.opacity -= 2 * k.dt(); if (this.opacity <= 0) this.destroy() }) - }, 40) - const push = dir * (isCritical ? 110 : 50) - k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad) - await k.wait(0.6) - clearTracked(fireTimer) - 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), - ]) - } - - // RIDDLE: question mark barrage rains from the sky - async function riddleBarrage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - atk.play('special') - sfxSpecial() - sfxZap() - await k.wait(0.2) - // Rain question marks from above - const qCount = isCritical ? 12 : 6 - for (let i = 0; i < qCount; i++) { - const qx = def.pos.x + (Math.random() - 0.5) * 100 - const qy = -20 - const q = k.add([ - k.circle(8), - k.pos(qx, qy), - k.color(safeColor(k,'#b83dff')), - k.opacity(0.9), - k.z(16), - ]) - // Inner dot to make it look like "?" - const dot = k.add([ - k.circle(3), - k.pos(qx, qy + 12), - k.color(safeColor(k,'#ffffff')), - k.opacity(0.9), - k.z(17), - ]) - k.tween(qy, def.pos.y - 20, 0.2 + Math.random() * 0.1, (v) => { q.pos.y = v; dot.pos.y = v + 12 }, k.easings.easeInQuad).then(() => { - q.destroy(); dot.destroy() - spawnSparks(qx, def.pos.y - 20, 3, '#b83dff') - sfxBonk() - k.shake(2) - }) - await k.wait(0.06) - } - await k.wait(0.15) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 15 : 7) - if (isCritical) { screenFlash('#b83dff'); sfxExplosion() } - const push = dir * (isCritical ? 90 : 40) - k.tween(def.pos.x, origDX + push, 0.25, (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) - } - - // HALLUCINATION CHECK: clone confusion — multiple fake attackers appear - async function cloneStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - sfxZap() - // Spawn 3-5 "ghost" clones that zip around - const cloneCount = isCritical ? 5 : 3 - const clones: any[] = [] - for (let i = 0; i < cloneCount; i++) { - const clone = k.add([ - k.rect(20, 35), - k.pos(atk.pos.x, atk.pos.y - 25), - k.color(safeColor(k,theme.accent)), - k.opacity(0.3), - k.z(9), - ]) - clones.push(clone) - // Each clone zips to a random position - const rx = origDX + (Math.random() - 0.5) * 200 - const ry = GROUND_Y - 6 - Math.random() * 80 - k.tween(0, 1, 0.2, (t) => { - clone.pos.x = atk.pos.x + (rx - atk.pos.x) * t - clone.pos.y = (atk.pos.y - 25) + (ry - atk.pos.y + 25) * t - }) - } - await k.wait(0.25) - // Defender is confused — screen warps - screenFlash('#b83dff', 0.1) - await k.wait(0.15) - // Real attacker teleports and strikes - spawnSparks(atk.pos.x, atk.pos.y - 30, 6, theme.accent) - await k.tween(1, 0, 0.08, (v) => { atk.opacity = v }) - atk.pos.x = origDX - dir * 40 - await k.tween(0, 1, 0.08, (v) => { atk.opacity = v }) - atk.play('attack') - sfxPunch() - await k.wait(0.1) - // Destroy clones - clones.forEach(c => { if (c.exists()) { spawnSparks(c.pos.x, c.pos.y, 3, theme.accent); c.destroy() } }) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 16 : 7) - if (isCritical) { screenFlash('#ffffff'); sfxCritical() } - spawnSparks(def.pos.x, def.pos.y - 30, 10, theme.accent) - 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) - await Promise.all([ - k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // TOKEN ECONOMY: coin shower — coins rain down on opponent - async function coinShower(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - atk.play('special') - sfxSpecial() - sfxBoing() - await k.wait(0.15) - // Coins rain from above - const coinCount = isCritical ? 20 : 10 - for (let i = 0; i < coinCount; i++) { - const cx = def.pos.x + (Math.random() - 0.5) * 80 - const coin = k.add([ - k.circle(5), - k.pos(cx, -10), - k.color(safeColor(k,Math.random() > 0.3 ? '#ffd700' : '#ffb000')), - k.opacity(1), - k.z(16), - ]) - const targetY = def.pos.y - 10 - Math.random() * 50 - k.tween(coin.pos.y, targetY, 0.15 + Math.random() * 0.1, (v) => { coin.pos.y = v }, k.easings.easeInQuad).then(() => { - coin.destroy() - spawnSparks(cx, targetY, 2, '#ffd700') - sfxBulletHit() - }) - await k.wait(0.03) - } - await k.wait(0.1) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 14 : 6) - if (isCritical) { screenFlash('#ffd700'); sfxExplosion() } - const push = dir * (isCritical ? 90 : 40) - k.tween(def.pos.x, origDX + push, 0.25, (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) - } - - // CREATIVE WRITING: giant pen stab + ink splatter - async function penStab(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Spawn a giant pen/quill - const penX = atk.pos.x + dir * 15 - const penY = atk.pos.y - 70 - const pen = k.add([ - k.rect(6, 80), - k.pos(penX, penY), - k.color(safeColor(k,'#4488ff')), - k.anchor('bot'), - k.z(18), - k.rotate(dir === 1 ? -30 : 30), - ]) - // Pen tip - const tip = k.add([ - k.rect(4, 12), - k.pos(penX + dir * 5, penY - 68), - k.color(safeColor(k,'#333333')), - k.z(18), - ]) - atk.play('special') - sfxSpecial() - await k.wait(0.15) - // Thrust pen forward at opponent - const targetX = origDX - dir * 20 - await Promise.all([ - k.tween(pen.pos.x, targetX, 0.12, (v) => { pen.pos.x = v; tip.pos.x = v + dir * 5 }, k.easings.easeInQuad), - k.tween(pen.angle, dir === 1 ? 80 : -80, 0.12, (v) => { pen.angle = v }, k.easings.easeInQuad), - k.tween(atk.pos.x, targetX - dir * 30, 0.12, (v) => { atk.pos.x = v }, k.easings.easeInQuad), - ]) - sfxPunch() - pen.destroy() - tip.destroy() - // INK SPLATTER! - const inkColors = ['#1a1a3a', '#2a2a5a', '#0a0a2a', '#4488ff'] - for (let i = 0; i < (isCritical ? 20 : 10); i++) { - const splat = k.add([ - k.circle(4 + Math.random() * 10), - k.pos(def.pos.x + (Math.random() - 0.5) * 60, def.pos.y - Math.random() * 70), - k.color(safeColor(k,inkColors[Math.floor(Math.random() * inkColors.length)])), - k.opacity(0.8), - k.z(9), - ]) - setTimeout(() => { - k.tween(0.8, 0, 1.5, (v) => { if (splat.exists()) splat.opacity = v }).then(() => { if (splat.exists()) splat.destroy() }) - }, 800) - } - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 16 : 7) - if (isCritical) screenFlash('#4488ff') - 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.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.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // MATH BLITZ: number projectiles fly at opponent - async function mathAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - atk.play('special') - sfxSpecial() - await k.wait(0.15) - // Fire number "projectiles" (colored circles representing digits) - const numCount = isCritical ? 10 : 5 - const colors = ['#ff2d7b', '#00f0ff', '#ffe14d', '#39ff14', '#b83dff'] - for (let i = 0; i < numCount; i++) { - const size = 6 + Math.random() * 8 - const fromY = atk.pos.y - 30 - Math.random() * 30 - const toY = def.pos.y - 10 - Math.random() * 50 - const num = k.add([ - k.circle(size), - k.pos(atk.pos.x + dir * 25, fromY), - k.color(safeColor(k,colors[i % colors.length])), - k.opacity(1), - k.z(16), - ]) - const dur = 0.1 + Math.random() * 0.05 - sfxGunshot() - k.tween(0, 1, dur, (t) => { - num.pos.x = (atk.pos.x + dir * 25) + (def.pos.x - atk.pos.x - dir * 25) * t - num.pos.y = fromY + (toY - fromY) * t - }).then(() => { - num.destroy() - spawnSparks(def.pos.x + (Math.random() - 0.5) * 20, toY, 3, colors[i % colors.length]) - }) - await k.wait(0.04) - } - await k.wait(0.1) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 15 : 7) - if (isCritical) { screenFlash('#ffe14d'); sfxExplosion() } - const push = dir * (isCritical ? 95 : 40) - k.tween(def.pos.x, origDX + push, 0.25, (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) - } - - // TRAP CARD: card shuriken + trap springs - async function trapCardAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - atk.play('special') - sfxSpecial() - await k.wait(0.1) - // Throw spinning cards - const cardCount = isCritical ? 5 : 3 - for (let i = 0; i < cardCount; i++) { - const card = k.add([ - k.rect(12, 18), - k.pos(atk.pos.x + dir * 20, atk.pos.y - 40 + (Math.random() - 0.5) * 20), - k.color(safeColor(k,i === 0 ? '#ff2d7b' : '#ffffff')), - k.anchor('center'), - k.z(16), - k.rotate(0), - ]) - const fromX = card.pos.x - const fromY = card.pos.y - const toX = def.pos.x - const toY = def.pos.y - 30 + (Math.random() - 0.5) * 30 - sfxGunshot() - k.tween(0, 1, 0.12, (t) => { - card.pos.x = fromX + (toX - fromX) * t - card.pos.y = fromY + (toY - fromY) * t - card.angle = t * 720 // Spin - }).then(() => { - card.destroy() - spawnSparks(toX, toY, 5, '#ff2d7b') - sfxBulletHit() - }) - await k.wait(0.08) - } - await k.wait(0.1) - // TRAP SPRINGS! A bear trap appears under defender - const trap = k.add([ - k.rect(30, 8), - k.pos(def.pos.x - 15, GROUND_Y - 8), - k.color(safeColor(k,'#888888')), - k.z(8), - ]) - const jaw1 = k.add([ - k.rect(30, 4), - k.pos(def.pos.x - 15, GROUND_Y - 16), - k.color(safeColor(k,'#aaaaaa')), - k.z(8), - ]) - sfxBonk() - k.shake(3) - await k.wait(0.05) - // Snap! Jaws close - k.tween(jaw1.pos.y, GROUND_Y - 10, 0.05, (v) => { jaw1.pos.y = v }) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 18 : 8) - if (isCritical) { screenFlash('#ff2d7b'); sfxExplosion() } - spawnSparks(def.pos.x, GROUND_Y - 10, 8, '#ff2d7b') - await k.wait(0.3) - trap.destroy() - jaw1.destroy() - const push = dir * (isCritical ? 90 : 40) - k.tween(def.pos.x, origDX + push, 0.25, (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) - } - - // SPEED BLITZ: afterimage dash — leaves ghost copies behind - async function afterimageDash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - sfxSpecial() - const contactX = origDX - dir * 40 - // Leave afterimages as we dash - const ghostCount = isCritical ? 6 : 4 - const startX = atk.pos.x - for (let i = 0; i < ghostCount; i++) { - const ghost = k.add([ - k.rect(20, 35), - k.pos(atk.pos.x, atk.pos.y - 25), - k.color(safeColor(k,theme.accent)), - k.opacity(0.4), - k.z(9), - ]) - setTimeout(() => { k.tween(0.4, 0, 0.3, (v) => { if (ghost.exists()) ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() }) }, 50) - // Move attacker a step forward - const stepX = startX + ((contactX - startX) / ghostCount) * (i + 1) - await k.tween(atk.pos.x, stepX, 0.03, (v) => { atk.pos.x = v }) - } - atk.play('attack') - sfxPunch() - await k.wait(0.08) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 16 : 7) - // Speed lines - for (let i = 0; i < 5; i++) { - const lineY = GROUND_Y - 20 - Math.random() * 100 - const line = k.add([ - k.rect(W * 0.4, 2), - k.pos(dir === 1 ? origAX : origDX, lineY), - k.color(safeColor(k,theme.accent)), - k.opacity(0.5), - k.z(12), - ]) - k.tween(0.5, 0, 0.2, (v) => { if (line.exists()) line.opacity = v }).then(() => { if (line.exists()) line.destroy() }) - } - if (isCritical) { screenFlash('#ffffff'); sfxCritical() } - spawnSparks(def.pos.x, def.pos.y - 30, 12, theme.accent) - 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) - 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), - ]) - } - - // BULLET TIME: slow-mo dodge, afterimages trail behind, golden traces, devastating counter - async function bulletTimeAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Phase 1: Darken screen — entering bullet time - const dim = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000011')), k.opacity(0), k.z(25)]) - await k.tween(0, 0.45, 0.15, (v) => { dim.opacity = v }) - scanlineGlitch(0.1) - - // Phase 2: Attacker fires slow golden projectiles (bullets visible in slow-mo) - atk.play('special') - sfxGunshot() - const bulletCount = isCritical ? 5 : 3 - const bullets: any[] = [] - for (let i = 0; i < bulletCount; i++) { - const bx = atk.pos.x + dir * 25 - const by = atk.pos.y - 30 + (i - 1) * 12 - const bullet = k.add([ - k.rect(isCritical ? 14 : 10, 3), - k.pos(bx, by), - k.color(safeColor(k, '#ffd700')), - k.opacity(0.9), - k.z(26), - k.anchor('center'), - k.rotate(dir === 1 ? 0 : 180), - ]) - bullets.push(bullet) - // Golden trace trail behind each bullet - const trace = k.add([ - k.rect(40, 1), - k.pos(bx - dir * 20, by), - k.color(safeColor(k, '#ffaa00')), - k.opacity(0.5), - k.z(25), - ]) - k.tween(trace.opacity, 0, 0.4, (v) => { if (trace.exists()) trace.opacity = v }).then(() => { if (trace.exists()) trace.destroy() }) - await k.wait(0.08) // Slow-mo pacing between shots - } - - // Phase 3: Bullets travel slowly toward defender - await Promise.all(bullets.map((b, i) => - k.tween(b.pos.x, def.pos.x + (Math.random() - 0.5) * 15, 0.25 + i * 0.03, (v) => { b.pos.x = v }, k.easings.easeInQuad) - )) - bullets.forEach(b => { if (b.exists()) b.destroy() }) - - // Phase 4: Attacker dodges (lean back in slow-mo with afterimages) - const leanBack = dir * -30 - for (let i = 0; i < 4; i++) { - const ghost = k.add([ - k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.8, FRAME_SIZE * Math.abs(atk.scale.y)), - k.pos(atk.pos.x + i * leanBack * 0.15, atk.pos.y), - k.color(safeColor(k, '#ffd700')), - k.opacity(0.25 - i * 0.05), - k.z(24), - k.anchor('bot'), - ]) - setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 400) - } - await k.tween(atk.pos.x, atk.pos.x + leanBack, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - - // Phase 5: Time resumes — attacker dashes in for devastating counter - dim.destroy() - sfxZoomWhoosh() - const contactX = origDX - dir * 40 - // Afterimage trail during rush - const rushStart = atk.pos.x - for (let i = 0; i < 5; i++) { - const ghost = k.add([ - k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.7, FRAME_SIZE * Math.abs(atk.scale.y) * 0.9), - k.pos(rushStart + ((contactX - rushStart) * i / 5), atk.pos.y), - k.color(safeColor(k, theme.accent)), - k.opacity(0.3 - i * 0.05), - k.z(9), - k.anchor('bot'), - ]) - setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 250) - } - await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad) - atk.play('attack') - - // Phase 6: Impact - const hits = isCritical ? 4 : 2 - for (let i = 0; i < hits; i++) { - sfxPunch() - def.play('hit') - k.shake(6 + i * 3) - spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 20 - Math.random() * 20, 6, '#ffd700') - if (i < hits - 1) await k.wait(0.06) - } - // Final devastating blow - sfxCritical() - if (isCritical) { sfxExplosion(); screenFlash('#ffd700', 0.2) } - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 22 : 12) - spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 20 : 12, '#ffd700') - if (isCritical) spawnShockwave(def.pos.x, GROUND_Y, '#ffd700') - - // Return to positions - const push = dir * (isCritical ? 70 : 35) - k.tween(def.pos.x, origDX + push, 0.25, (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) => { atk.pos.x = v }, k.easings.easeInQuad), - k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - atk.play('idle') - } - - // INVISIBLE SPEED: so fast you can barely see — just blur lines and impact - async function lightningRush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - sfxSpecial() - // Attacker vanishes - atk.opacity = 0 - // Blur lines everywhere - for (let i = 0; i < 12; i++) { - const lineY = GROUND_Y - 10 - Math.random() * 120 - const line = k.add([ - k.rect(W, 2 + Math.random() * 2), - k.pos(0, lineY), - k.color(safeColor(k,Math.random() > 0.5 ? '#ffffff' : theme.accent)), - k.opacity(0.6), - k.z(25), - ]) - k.tween(0.6, 0, 0.15, (v) => { if (line.exists()) line.opacity = v }).then(() => { if (line.exists()) line.destroy() }) - } - // Multiple instant impacts across the screen (so fast, hits happen everywhere) - const hitCount = isCritical ? 8 : 5 - for (let i = 0; i < hitCount; i++) { - const hx = origDX + (Math.random() - 0.5) * 100 - const hy = GROUND_Y - 20 - Math.random() * 80 - spawnSparks(hx, hy, 5, '#ffffff') - sfxPunch() - k.shake(3) - // Brief flash of attacker at hit position - atk.pos.x = hx - dir * 30 - atk.opacity = 0.3 - await k.wait(0.02) - atk.opacity = 0 - await k.wait(0.01) - } - // Final massive hit - screenFlash('#ffffff', 0.15) - atk.pos.x = origDX - dir * 40 - atk.opacity = 1 - atk.play('attack') - sfxCritical() - await k.wait(0.05) - def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 25 : 12) - spawnSparks(def.pos.x, def.pos.y - 30, 20, '#ffffff') - if (isCritical) sfxExplosion() - const push = dir * (isCritical ? 140 : 70) - 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.15, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // ZOOM RUSH: fly toward camera (scale huge) then slam back into opponent - async function zoomRush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const origAY = atk.pos.y - const origScaleX = atk.scale.x - const origScaleY = atk.scale.y - const origDefScaleX = def.scale.x - const origDefScaleY = def.scale.y - sfxZoomWhoosh() - // Randomize zoom level for variety (3x to 7x) - const zoomScale = isCritical ? 5 + Math.random() * 2 : 3 + Math.random() * 2 - const zoomDur = 0.2 + Math.random() * 0.1 - // Attacker rushes toward camera — scale up dramatically - // Defender shrinks to background (perspective effect) - await Promise.all([ - k.tween(Math.abs(origScaleX), zoomScale, zoomDur, (v) => { - atk.scale.x = origScaleX > 0 ? v : -v - atk.scale.y = v - }, k.easings.easeInQuad), - k.tween(Math.abs(origDefScaleX), Math.abs(origDefScaleX) * 0.4, zoomDur, (v) => { - def.scale.x = origDefScaleX > 0 ? v : -v - def.scale.y = v - }, k.easings.easeInQuad), - k.tween(atk.pos.x, W / 2, zoomDur, (v) => { atk.pos.x = v }, k.easings.easeOutQuad), - k.tween(atk.pos.y, H * 0.6, zoomDur, (v) => { atk.pos.y = v }, k.easings.easeOutQuad), - k.tween(1, 0.3, zoomDur, (v) => { atk.opacity = Math.max(0.3, v) }), - ]) - // Brief dramatic pause at huge size — screen darkens + grotesque close-up - screenFlash('#000000', 0.15) - if (zoomScale >= 4) spawnGrotesqueDetails(atk, zoomScale * 0.4) - k.shake(5) - await k.wait(0.1 + Math.random() * 0.08) - destroyGrotesqueDetails() - // Zoom back and SLAM into opponent (defender grows back) - sfxZoomWhoosh() - sfxSpecial() - await Promise.all([ - k.tween(atk.scale.y, Math.abs(origScaleY), 0.15, (v) => { - atk.scale.x = origScaleX > 0 ? v : -v - atk.scale.y = v - }, k.easings.easeInQuad), - k.tween(Math.abs(def.scale.y), Math.abs(origDefScaleY), 0.15, (v) => { - def.scale.x = origDefScaleX > 0 ? v : -v - def.scale.y = v - }, k.easings.easeOutQuad), - k.tween(atk.pos.x, origDX - dir * 40, 0.15, (v) => { atk.pos.x = v }, k.easings.easeInQuad), - k.tween(atk.pos.y, origAY, 0.15, (v) => { atk.pos.y = v }, k.easings.easeInQuad), - k.tween(0.3, 1, 0.15, (v) => { atk.opacity = v }), - ]) - // IMPACT — rapid punches on contact - atk.play('attack') - const punchCount = isCritical ? 8 : 5 - for (let i = 0; i < punchCount; i++) { - sfxRapidPunch() - def.play(i % 2 === 0 ? 'hit' : 'knockback') - spawnSparks(def.pos.x + (Math.random() - 0.5) * 40, def.pos.y - 10 - Math.random() * 50, 4, i % 2 === 0 ? '#ff2d7b' : '#ffcc00') - k.shake(2 + i) - def.pos.x += dir * 4 - atk.play(i % 2 === 0 ? 'attack' : 'kick') - await k.wait(0.04) - } - // Final haymaker - sfxCritical() - k.shake(isCritical ? 25 : 15) - screenFlash(isCritical ? '#ffffff' : '#ff2d7b', 0.2) - spawnSparks(def.pos.x, def.pos.y - 30, 20, '#ffffff') - spawnShockwave(def.pos.x, GROUND_Y, '#ff2d7b') - if (isCritical) sfxExplosion() - 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 — restore both scales - atk.scale.x = origScaleX - atk.scale.y = origScaleY - def.scale.x = origDefScaleX - def.scale.y = origDefScaleY - await Promise.all([ - k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // RAPID FLURRY: DBZ-style machine gun punches — so fast it's a blur - async function rapidFlurry(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - const contactX = origDX - dir * 40 - // Sprint to contact - sfxSpecial() - await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - // UNLEASH — rapid alternating punches - const hitCount = isCritical ? 14 : 8 - const colors = ['#ff2d7b', '#ffcc00', '#00f0ff', '#ff6600', '#ffffff'] - // Speed lines during flurry - for (let i = 0; i < 6; i++) { - const lineY = GROUND_Y - 10 - Math.random() * 100 - const line = k.add([ - k.rect(W * 0.5, 2), - k.pos(dir === 1 ? origAX - 50 : origDX + 50, lineY), - k.color(safeColor(k,colors[i % colors.length])), - k.opacity(0.4), - k.z(12), - ]) - k.tween(0.4, 0, 0.5, (v) => { if (line.exists()) line.opacity = v }).then(() => { if (line.exists()) line.destroy() }) - } - for (let i = 0; i < hitCount; i++) { - const anim = ['attack', 'kick', 'attack', 'special'][i % 4] - atk.play(anim) - sfxRapidPunch() - // Sparks spray in alternating directions - const sparkX = def.pos.x + (i % 2 === 0 ? -1 : 1) * (10 + Math.random() * 20) - const sparkY = def.pos.y - 10 - Math.random() * 50 - spawnSparks(sparkX, sparkY, 3, colors[i % colors.length]) - def.play('hit') - k.shake(2) - // Jitter defender rapidly side to side - def.pos.x += (i % 2 === 0 ? dir : -dir) * 6 - def.pos.y += (i % 2 === 0 ? -3 : 3) - await k.wait(0.035) - } - // Pause — "Is it over?" - await k.wait(0.08) - // ONE MORE — the big finish - atk.play('special') - sfxCritical() - sfxPunch() - await k.wait(0.06) - def.play('knockback') - k.shake(isCritical ? 22 : 12) - screenFlash(isCritical ? '#ffffff' : '#ffcc00', 0.15) - spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 25 : 12, '#ffcc00') - spawnShockwave(def.pos.x, GROUND_Y, '#ffcc00') - if (isCritical) sfxExplosion() - const push = dir * (isCritical ? 140 : 70) - k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad) - // Reset defender Y - k.tween(def.pos.y, GROUND_Y - 6, 0.2, (v) => { def.pos.y = v }) - 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // SWORD SLASH: giant energy sword swing with arc trail - 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(k,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), - ]) - } - - // HAMMER SMASH: pull out giant hammer, overhead slam with ground crack - 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(k,'#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(k,'#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(k,'#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), - ]) - } - - // LASER BEAM: charge up then fire continuous beam across screen - 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(k,'#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(k,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(k,'#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) - } - - // ROCKET LAUNCHER: fire a big slow rocket that explodes on impact - 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(k,'#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(k,'#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(k,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) - } - - // BOMB THROW: lob an arcing bomb that bounces once then detonates - 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(k,'#333333')), k.z(17)]) - const fuse = k.add([k.rect(2, 8), k.pos(bombX, bombY - 8), k.color(safeColor(k,'#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) - } - - // MINIGUN SPRAY: sustained automatic fire with shell casings - 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) - } - - // SNIPER SHOT: long pause, laser sight, then one devastating hit - 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(k,'#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(k,'#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), - ]) - } - - // WHIP CRACK: ranged whip attack with crack sound - 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(k,'#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) - } - - // KATANA COMBO: triple slash with cross patterns - 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(k,'#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(k,'#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(k,'#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), - ]) - } - - // CHAINSAW: charge with buzzing chainsaw, multi-hit grind - 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(k,'#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(k,'#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), - ]) - } - - // MOTORBIKE CHARGE: ride a motorbike across the screen and slam into defender - 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(k,isCritical ? '#ff2d2d' : '#4488ff')), k.z(16)]) - const wheelF = k.add([k.circle(8), k.pos(startX + dir * 18, bikeY + 12), k.color(safeColor(k,'#333333')), k.z(16)]) - const wheelR = k.add([k.circle(8), k.pos(startX - dir * 16, bikeY + 12), k.color(safeColor(k,'#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(k,'#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) - } - - // CAR SMASH: drive a car from off-screen, defender gets sent flying - 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(k,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(k,isCritical ? '#ccaa00' : '#cc5500')), k.z(16)]) - // Wheels - const w1 = k.add([k.circle(8), k.pos(startX + dir * 22, carY + 18), k.color(safeColor(k,'#222222')), k.z(16)]) - const w2 = k.add([k.circle(8), k.pos(startX - dir * 22, carY + 18), k.color(safeColor(k,'#222222')), k.z(16)]) - // Headlight - const hl = k.add([k.rect(4, 6), k.pos(startX + dir * 35, carY + 3), k.color(safeColor(k,'#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) - } - - // BOAT CANNON: a boat slides across the ground and fires cannons - 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(k,'#8B4513')), k.z(8)]) - const mast = k.add([k.rect(3, 40), k.pos(boatX + 10, boatY - 40), k.color(safeColor(k,'#8B4513')), k.z(8)]) - const sail = k.add([k.rect(25, 30), k.pos(boatX + 15, boatY - 45), k.color(safeColor(k,'#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) - } - - // ANVIL DROP: classic cartoon — anvil falls from sky onto opponent - 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(k,'#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(k,'#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(k,'#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) - } - - // POCKET CANNON: reach into pocket, pull out comically oversized gun, fire - async function pocketCannon(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // "Reach into pocket" — arm down motion - atk.play('special') - await k.wait(0.12) - - // Gun appears — comically oversized - const gunLen = isCritical ? 90 : 65 - const gunH = isCritical ? 22 : 16 - const gx = atk.pos.x + dir * 20 - const gy = atk.pos.y - 25 - // Barrel - const barrel = k.add([ - k.rect(gunLen, gunH), k.pos(gx, gy), - k.color(safeColor(k,'#333333')), k.opacity(1), k.z(16), k.scale(1), - ]) - // Handle - const handle = k.add([ - k.rect(12, 20), k.pos(gx - dir * 5, gy + gunH / 2), - k.color(safeColor(k,'#555555')), k.opacity(1), k.z(15), k.scale(1), - ]) - // Scope on top - const scope = k.add([ - k.circle(5), k.pos(gx + dir * gunLen * 0.6, gy - 6), - k.color(safeColor(k,'#880000')), k.opacity(0.9), k.z(17), k.scale(1), - ]) - - // Gun grows in from small — comedy "pulling from tiny pocket" effect - barrel.scale = k.vec2(0.1, 0.1) - handle.scale = k.vec2(0.1, 0.1) - scope.scale = k.vec2(0.1, 0.1) - await Promise.all([ - k.tween(0.1, 1, 0.15, (v) => { barrel.scale = k.vec2(v, v) }, k.easings.easeOutBack), - k.tween(0.1, 1, 0.15, (v) => { handle.scale = k.vec2(v, v) }, k.easings.easeOutBack), - k.tween(0.1, 1, 0.15, (v) => { scope.scale = k.vec2(v, v) }, k.easings.easeOutBack), - ]) - sfxBoing() - k.shake(3) - await k.wait(0.08) - - // AIM — brief wobble - const wobbleTime = 0.12 - await k.tween(0, wobbleTime, wobbleTime, (t) => { - const w = Math.sin(t * 40) * 3 - barrel.pos.y = gy + w - handle.pos.y = gy + gunH / 2 + w - scope.pos.y = gy - 6 + w - }) - - // FIRE! massive shot - const shotCount = isCritical ? 4 : 2 - for (let s = 0; s < shotCount; s++) { - sfxGunshot() - sfxExplosion() - k.shake(isCritical ? 15 : 10) - // Muzzle flash - const flash = k.add([ - k.circle(12 + Math.random() * 8), - k.pos(gx + dir * gunLen, gy), - k.color(safeColor(k,'#ffee00')), - k.opacity(0.9), k.z(18), - ]) - setTimeout(() => { if (flash.exists()) flash.destroy() }, 60) - // Recoil - const recoilX = atk.pos.x - dir * 15 - k.tween(atk.pos.x, recoilX, 0.04, (v) => { - atk.pos.x = v - barrel.pos.x = v + dir * 20 - handle.pos.x = v + dir * 20 - dir * 5 - scope.pos.x = v + dir * 20 + dir * gunLen * 0.6 - }).then(() => k.tween(recoilX, origAX, 0.06, (v) => { - atk.pos.x = v - barrel.pos.x = v + dir * 20 - handle.pos.x = v + dir * 20 - dir * 5 - scope.pos.x = v + dir * 20 + dir * gunLen * 0.6 - })) - // Massive projectile - await spawnProjectile(gx + dir * gunLen, gy, def.pos.x, def.pos.y - 20 + (Math.random() - 0.5) * 20, '#ffcc00', 10) - sfxBulletHit() - def.play(s === shotCount - 1 ? 'knockback' : 'hit') - spawnSparks(def.pos.x, def.pos.y - 20, 10, '#ff6600') - spawnShockwave(def.pos.x, GROUND_Y, '#ff6600') - def.pos.x += dir * (isCritical ? 20 : 12) - if (s < shotCount - 1) await k.wait(0.06) - } - - // Gun disappears — shrinks back into pocket - await Promise.all([ - k.tween(1, 0, 0.12, (v) => { barrel.scale = k.vec2(v, v) }), - k.tween(1, 0, 0.12, (v) => { handle.scale = k.vec2(v, v) }), - k.tween(1, 0, 0.12, (v) => { scope.scale = k.vec2(v, v) }), - ]) - barrel.destroy(); handle.destroy(); scope.destroy() - sfxBoing() - - if (isCritical) screenFlash('#ff6600', 0.15) - atk.play('idle') - await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad) - } - - // GRAPPLE FLURRY: both bots close in and trade sustained blows without splitting apart - async function grappleFlurry(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Both rush to center — meet in the middle - const meetX = (origAX + origDX) / 2 - const atkMeetX = meetX - dir * 25 - const defMeetX = meetX + dir * 25 - sfxSpecial() - await Promise.all([ - k.tween(atk.pos.x, atkMeetX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad), - k.tween(def.pos.x, defMeetX, 0.12, (v) => { def.pos.x = v }, k.easings.easeOutQuad), - ]) - - // Sustained trading blows — attacker dominates but defender fights back - const hitCount = isCritical ? 10 : 7 - for (let i = 0; i < hitCount; i++) { - const atkHits = i % 3 !== 2 // attacker hits 2/3 of the time - if (atkHits) { - atk.play(i % 2 === 0 ? 'attack' : 'kick') - sfxPunch() - await k.wait(0.03) - def.play('hit') - spawnSparks(def.pos.x - dir * 5, def.pos.y - 25 - Math.random() * 20, 3, i % 2 === 0 ? '#ff2d7b' : '#ffcc00') - k.shake(2 + Math.floor(i / 2)) - // Push defender slightly but attacker follows - def.pos.x += dir * 3 - atk.pos.x += dir * 2 - } else { - // Defender strikes back! - def.play('attack') - sfxKick() - await k.wait(0.03) - atk.play('hit') - spawnSparks(atk.pos.x + dir * 5, atk.pos.y - 25 - Math.random() * 20, 3, '#00f0ff') - k.shake(2) - atk.pos.x -= dir * 3 - def.pos.x -= dir * 2 - } - await k.wait(0.05) - } - - // Attacker finishes with a shove - atk.play('special') - sfxCritical() - k.shake(isCritical ? 20 : 12) - spawnSparks(def.pos.x, def.pos.y - 30, 12, '#ff2d7b') - if (isCritical) { - screenFlash('#ff2d7b', 0.15) - spawnShockwave(def.pos.x, GROUND_Y, '#ff2d7b') - } - def.play('knockback') - const pushDist = dir * (isCritical ? 120 : 60) - k.tween(def.pos.x, origDX + pushDist * 0.5, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad) - await k.wait(0.25) - - // Both return - atk.play('idle') - await Promise.all([ - k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(def.pos.x, origDX, 0.25, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // BODY SLAM: wrestling-style grab, lift, and slam - async function bodySlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Rush in close - sfxSpecial() - const contactX = def.pos.x - dir * 30 - await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - - // Grab — both freeze - atk.play('special') - def.play('hit') - sfxClash() - k.shake(5) - await k.wait(0.1) - - // Lift opponent overhead - const liftY = atk.pos.y - 80 - sfxBoing() - await k.tween(def.pos.y, liftY, 0.2, (v) => { def.pos.y = v }, k.easings.easeOutQuad) - await k.tween(def.pos.x, atk.pos.x + dir * 5, 0.1, (v) => { def.pos.x = v }) - // Spin overhead - const defOrigY = GROUND_Y - for (let i = 0; i < (isCritical ? 3 : 2); i++) { - await k.tween(def.pos.x, atk.pos.x - dir * 20, 0.06, (v) => { def.pos.x = v }) - await k.tween(def.pos.x, atk.pos.x + dir * 20, 0.06, (v) => { def.pos.x = v }) - } - - // SLAM DOWN - sfxExplosion() - def.play('knockback') - const slamX = atk.pos.x + dir * 50 - await Promise.all([ - k.tween(def.pos.y, defOrigY, 0.08, (v) => { def.pos.y = v }, k.easings.easeInQuad), - k.tween(def.pos.x, slamX, 0.08, (v) => { def.pos.x = v }), - ]) - sfxCritical() - k.shake(isCritical ? 25 : 16) - screenFlash(isCritical ? '#ffffff' : '#ff6600', 0.15) - spawnSparks(slamX, defOrigY - 10, 18, '#ff6600') - spawnShockwave(slamX, GROUND_Y, '#ff6600') - spawnBulletHoles(slamX, GROUND_Y, 3) - await k.wait(0.2) - - // Return - atk.play('idle') - await Promise.all([ - k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // PINBALL COMBO: bounce the opponent between walls/edges like a pinball - async function pinballCombo(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Initial launch punch - atk.play('attack') - sfxPunch() - const contactX = def.pos.x - dir * 35 - await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - def.play('hit') - k.shake(8) - spawnSparks(def.pos.x, def.pos.y - 30, 8, '#ff2d7b') - - // Bounce the opponent back and forth across the screen - const bounces = isCritical ? 5 : 3 - const leftWall = 30 - const rightWall = W - 30 - let defX = def.pos.x - for (let i = 0; i < bounces; i++) { - const toWall = i % 2 === (dir > 0 ? 0 : 1) ? rightWall : leftWall - sfxZoomWhoosh() - await k.tween(defX, toWall, 0.06, (v) => { def.pos.x = v }, k.easings.easeInQuad) - defX = toWall - // Wall hit - sfxBonk() - k.shake(6 + i * 2) - spawnSparks(toWall, def.pos.y - 20, 6, ['#ff2d7b', '#ffcc00', '#00f0ff'][i % 3]) - screenFlash(['#ff2d7b', '#ffcc00', '#00f0ff'][i % 3], 0.05) - def.play(i % 2 === 0 ? 'hit' : 'knockback') - await k.wait(0.04) - } - - // Final slam to ground - sfxCritical() - sfxExplosion() - k.shake(isCritical ? 22 : 14) - screenFlash('#ffffff', 0.12) - spawnSparks(def.pos.x, def.pos.y - 20, 15, '#ffe14d') - spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d') - def.play('knockback') - await k.wait(0.2) - - // Return - atk.play('idle') - await Promise.all([ - k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // SUPLEX: grab from behind, lift, and drive headfirst into ground - async function suplex(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { - // Dash behind defender - sfxSpecial() - const behindX = def.pos.x + dir * 30 - await k.tween(atk.pos.x, behindX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) - // Flip attacker to face back - atk.scale.x = -atk.scale.x - atk.play('special') - sfxClash() - k.shake(4) - await k.wait(0.06) - - // Grab and arc overhead - const arcCx = (atk.pos.x + def.pos.x) / 2 - const arcTop = atk.pos.y - 120 - // Both rise in an arc - const steps = 8 - for (let i = 0; i <= steps; i++) { - const t = i / steps - const angle = Math.PI * t - def.pos.x = arcCx + Math.cos(angle) * 40 - def.pos.y = GROUND_Y - Math.sin(angle) * 120 - atk.pos.x = def.pos.x + dir * 20 - atk.pos.y = def.pos.y + 10 - await k.wait(0.02) - } - - // IMPACT — headfirst into ground - sfxExplosion() - sfxCritical() - k.shake(isCritical ? 28 : 18) - screenFlash(isCritical ? '#ffffff' : '#ff2d2d', 0.2) - spawnSparks(def.pos.x, GROUND_Y - 10, 20, '#ff2d2d') - spawnShockwave(def.pos.x, GROUND_Y, '#ff2d2d') - def.play('ko') - await k.wait(0.15) - def.play(isCritical ? 'knockback' : 'hit') - - // Flip attacker back - atk.scale.x = -atk.scale.x - atk.play('idle') - def.pos.y = GROUND_Y - await Promise.all([ - k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), - k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - - // ============================================================ - // PIXEL PROP SYSTEM — draw recognizable objects from shapes - // ============================================================ - - type PropDef = { parts: { shape: 'rect' | 'circle'; x: number; y: number; w: number; h: number; color: string }[] } - - const PROPS: Record = { - // 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 - ]}, - } - - // Spawn a multi-part pixel prop at position, returns array of game objects - 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 - } - - // Move all parts of a prop to a new position (relative to anchor) - 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() }) - } - - // Map choreography names to prop types for the factories - const CHOREO_PROPS: Record = { - pizzaSlam: 'pizza', burgerToss: 'burger', hotdogWhip: 'hotdog', bananaFling: 'banana', - pieSmash: 'pie', watermelonBomb: 'watermelon', donutBarrage: 'donut', - sushiBarrage: 'pizza', tacoStorm: 'pizza', iceCreamFling: 'donut', - bombThrow: 'bomb', grenadeBlast: 'bomb', dynamiteBlast: 'bomb', - bowlingBallRoll: 'soccerball', soccerKick: 'soccerball', basketballDunk: 'soccerball', - } - - // Map choreography names to weapon props the attacker holds - const CHOREO_WEAPON: Record = { - gunBurst: 'gun', sniperShot: 'sniper', minigunSpray: 'minigun', - rocketLauncher: 'rocketLauncher', pocketCannon: 'shotgun', - chainsawRev: 'chainsaw', katanaCombo: 'katana', swordSlash: 'katana', - } - - // === PER-CHARACTER WEAPON SYSTEM === - // Each bot gets 3 special weapons based on their seed, balanced frequency - - interface BotWeapons { - melee: string // close range weapon prop - ranged: string // gun/launcher prop - thrown: string // throwable prop - } - - const MELEE_WEAPONS = ['katana', 'chainsaw', 'shield'] - const RANGED_WEAPONS = ['gun', 'shotgun', 'sniper', 'minigun', 'rocketLauncher'] - const THROWN_WEAPONS = ['pizza', 'burger', 'bomb', 'watermelon', 'donut', 'soccerball', 'banana'] - - 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], - } - } - - const weaponsA = getBotWeapons(botA.seed) - const weaponsB = getBotWeapons(botB.seed) - - // === DEFENSE / SHIELD SYSTEM === + const choreographyMap = createChoreographyMap(choreoCtx) + const { spawnWeaponProp, destroyProp, spawnProp, moveProp } = createFactories(choreoCtx) async function playBlock(side: 'a' | 'b') { const defender = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter @@ -4171,3085 +1847,6 @@ export async function createFightScene(config: FightSceneConfig) { await k.wait(0.1) } - // Spawn attacker's signature weapon as a visual during choreographies - function getAttackerWeapon(side: 'a' | 'b', type: 'melee' | 'ranged' | 'thrown'): string { - const weapons = side === 'a' ? weaponsA : weaponsB - return weapons[type] - } - - // ============================================================ - // CHOREOGRAPHY FACTORIES — generate moves from templates - // ============================================================ - type ChoreoFn = typeof dashPunch - - // FACTORY 1: Throw object(s) in an arc at the opponent — NOW WITH PIXEL PROPS - 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) - } - } - - // FACTORY 1b: Throw with detailed pixel prop - 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) - } - } - - // FACTORY 1c: Ranged attack with visible pixel gun prop - 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) - } - } - - // FACTORY 2: Swing weapon — run up, swing rect, hit - 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(k,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(k,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), - ]) - } - } - - // FACTORY 3: Drop something from the sky onto opponent - 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(k,'#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(k,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) - } - } - - // FACTORY 4: Vehicle drives across screen (like motorbikeCharge/carSmash) - 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(k,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(k,'#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(k,'#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(k,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(k,'#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) - } - } - - // FACTORY 5: Beam/ray attack — attacker fires continuous beam - 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(k,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(k,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(k,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) - } - } - - // FACTORY 6: Spin/flip — attacker does acrobatic motion then strikes - 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), - ]) - } - } - - // FACTORY 7: Summon/swarm — creatures fly from behind attacker toward defender - 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(k,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) - } - } - - // FACTORY 8: Explosion — thing appears near opponent and explodes - 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(k,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(k,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) - } - } - - // FACTORY 9: Grab/wrestling — run up, grab, slam - 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), - ]) - } - } - - // ============================================================ - // GENERATED CHOREOGRAPHIES — 258 new moves from factories - // ============================================================ - - // --- FOOD THROWS (30) — key items use detailed pixel props --- - 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) - - // --- WEAPON SWINGS (20) --- - 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') - - // --- PROJECTILE STREAMS (20) --- - 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') - - // --- DROP ATTACKS (15) --- - 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') - - // --- VEHICLES (10) --- - 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 }]) - - // --- SPIN ATTACKS (10) --- - const tornadoSpin = makeSpin('horizontal', 80, 3) - const backflipKick = makeSpin('vertical', 140, 1) - const corkscrewDive = makeSpin('corkscrew', 100, 2) - const helicopterArms = makeSpin('horizontal', 50, 4) - const breakdanceSweep = makeSpin('horizontal', 30, 3) - const cartwheelStrike = makeSpin('vertical', 90, 1) - const frontflipSlam = makeSpin('vertical', 160, 2) - const drillSpin = makeSpin('corkscrew', 60, 3) - const cycloneKick = makeSpin('horizontal', 100, 2) - const pirouetteStrike = makeSpin('horizontal', 40, 5) - - // --- FLIP ATTACKS (10) --- - const doubleBackflip = makeSpin('vertical', 180, 2) - const wallBounce = makeSpin('vertical', 200, 1) - const springboardLaunch = makeSpin('vertical', 220, 1) - const barrelRoll = makeSpin('corkscrew', 80, 1) - const trampolineBounce = makeSpin('vertical', 250, 2) - const poleVault = makeSpin('vertical', 230, 1) - const skateTrick = makeSpin('corkscrew', 120, 1) - const surfSlam = makeSpin('horizontal', 60, 2) - const rolloutBall = makeSpin('horizontal', 20, 4) - const aerialTwist = makeSpin('corkscrew', 150, 2) - - // --- BEAM ATTACKS (15) --- - 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) - - // --- ANIMAL SUMMONS (15) --- - 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) - - // --- EXPLOSION ATTACKS (15) --- - 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) - - // --- SILLY/MEME (20) --- - 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) - - // --- GRAPPLE/WRESTLING (15) --- - const piledriver = makeGrab('down', 180) - const powerbomb = makeGrab('up', 200) - const ddt = makeGrab('down', 120) - const germanSuplex = makeGrab('up', 160) - const chokeslam = makeGrab('up', 220) - const tombstone = makeGrab('down', 200) - const stunner = makeGrab('down', 80) - const rko = makeGrab('down', 140) - const spear = makeGrab('toss', 80) - const clothesline: ChoreoFn = async (atk, def, dir, origAX, origDX, isCritical) => { - const cx = origDX - dir * 20 - atk.play('attack'); sfxSpecial() - await k.tween(atk.pos.x, cx, 0.12, (v) => { atk.pos.x = v }, k.easings.easeInQuad) - sfxPunch(); def.play(isCritical ? 'knockback' : 'hit') - k.shake(isCritical ? 18 : 9); spawnSparks(def.pos.x, def.pos.y - 30, 10, '#ff4444') - if (isCritical) { screenFlash('#ff4444'); sfxCritical() } - // Defender spins backwards - const origDY = def.pos.y - await Promise.all([ - k.tween(def.pos.x, origDX + dir * (isCritical ? 120 : 60), 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad), - k.tween(0, 360, 0.25, (v) => { def.angle = v }).then(() => { def.angle = 0 }), - k.tween(origDY, origDY - 50, 0.12, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() => - k.tween(origDY - 50, origDY, 0.13, (v) => { def.pos.y = v }, k.easings.easeInQuad)), - ]) - spawnShockwave(def.pos.x, GROUND_Y, '#ff4444') - await k.wait(0.2); 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), - ]) - } - const atomicDrop = makeGrab('up', 140) - const hurricanrana = makeGrab('spin', 120) - const moonsault = makeSpin('vertical', 200, 2) - const elbowDrop = makeDrop(10, 8, '#ffaa88', '#ff4444') - const frogSplash = makeSpin('vertical', 180, 1) - - // --- ENERGY/MAGIC (20) --- - 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(k,'#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(k,'#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) - - // --- TECH/MODERN (15) --- - 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) - - // --- MUSIC (8) --- - 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) - - // --- SPORTS (10) --- - const homeRunSwing = makeSwing(5, 55, '#886633', '#ffffff', 10, 10) - const slapShot = makeSwing(4, 55, '#333333', '#444444', 8, 8) - const servingAce = makeThrow('#ccff00', 10, 1, 80, true, '#aadd00') - const fieldGoalKick = makeSpin('vertical', 100, 1) - const bodyCheck = makeGrab('toss', 60) - const dropKick = makeSpin('vertical', 120, 1) - const elbowStrike = makeGrab('down', 60) - const kneeStrike = makeGrab('up', 50) - const headbutt = makeGrab('down', 30) - const shoulderCharge = makeGrab('toss', 40) - - // --- NATURE/ELEMENTAL (10) --- - 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) - - // --- SPACE/SCI-FI (10) --- - 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') - - // --- MEDIEVAL/FANTASY (10) --- - 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') - - // ============================================================ - // TIER-GATED ULTIMATE MOVES — unlock with higher tier/wins - // Tier 2+: 10 moves, Tier 3+: 10 more, Tier 4+: 10 more, Tier 5+: 10 more - // ============================================================ - - // --- TIER 2+ ULTIMATES (Silver) — flashy multi-hits --- - 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(k,'#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(k,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(k,'#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(k,'#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(k,'#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) - - // --- TIER 3+ ULTIMATES (Gold) — multi-phase, full screen --- - 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(k,'#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(k,'#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(k,'#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(k,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(k,'#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(k,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) - - // ULTIMATE BULLET TIME BARRAGE — full slow-mo cinematic: dodge rain of projectiles, afterimage rush, multi-hit counter - 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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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') - } - - // --- TIER 4+ ULTIMATES (Platinum) — full screen, multi-phase spectacles --- - 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(k,'#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(k,'#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(k,'#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) - - // --- TIER 5+ ULTIMATES (Diamond/Legend) — absolutely insane full-screen --- - 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(k,'#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(k,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(k,'#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(k,'#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(k,'#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) - - // Tier-gated pools (mapping is at module level for pickChoreography access) - - // ═══════════════════════════════════════════════════════ - // THE CREATOR — Exclusive choreographies (bitcoin-themed) - // ═══════════════════════════════════════════════════════ - - 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(k, 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(k, 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(k, '#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(k, 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(k, 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(k, 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(k, 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(k, '#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') - } - - // ── Creator ₿ Swarm Variations ── - - 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(k, 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(k, 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(k, 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(k, 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) - } - - // ── Creator Ultimates ── - - 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(k, '#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(k, '#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(k, '#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(k, '#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(k, 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(k, '#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(k, '#ffd700')), k.opacity(0.9), k.z(33)]) - const bot = k.add([k.rect(bw, 2), k.pos(bx, by2 + bh2), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(33)]) - const left = k.add([k.rect(2, bh2), k.pos(bx, by2), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(33)]) - const right = k.add([k.rect(2, bh2), k.pos(bx + bw, by2), k.color(safeColor(k, '#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) - } - - // --- BITCOIN-THEMED CHOREOGRAPHIES (30) --- - - 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(k, '#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(k, '#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(k, 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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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(k, '#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) - - // --- 50 NEW GENERAL CHOREOGRAPHIES --- - // -- Food (15) -- - 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) - - // -- Energy/Beam (15) -- - 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(k, '#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(k, '#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) - - // -- Silly (10) -- - 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(k, 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(k, '#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) - } - - // -- Weapons (10) -- - 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(k, '#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') - - const choreographyMap: Record = { - dashPunch, aerialSlam, flyingKick, dashThrough, uppercut, - multiHit, projectile, jetpackDive, gunBurst, groundPound, - teleportStrike, fullScreenDash, lightningRush, zoomRush, rapidFlurry, - // Weapons - swordSlash, hammerSmash, laserBeam, rocketLauncher, bombThrow, - minigunSpray, sniperShot, whipCrack, katanaCombo, chainsawRev, - // Vehicles & objects - motorbikeCharge, carSmash, boatCannon, anvilDrop, - // Close-combat / grapple - pocketCannon, grappleFlurry, bodySlam, pinballCombo, suplex, - // Themed - golfClubSmash, fireBreath, riddleBarrage, cloneStrike, - coinShower, penStab, mathAttack, trapCardAttack, afterimageDash, bulletTimeAttack, - // --- NEW: Food throws --- - 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, - // --- NEW: Weapon swings --- - baseballBat, fryingPan, rollingPin, umbrellaWhack, mopSwipe, - broomStrike, wrenchSmash, crowbarSwing, plungerSlam, tennisRacket, - hockeyStick, poolCue, guitarSmash, shovelBash, pickaxeStrike, - oarSwing, flagpoleSmash, caneWhack, rulerSlap, newspaperRoll, - // --- NEW: Projectile streams --- - tennisBallVolley, baseballPitch, bowlingBallRoll, rubberDuckFlood, - shoeBarrage, bookStorm, phoneFling, keyboardSmash, diceBarrage, - marbleShower, snowballFight, paintCanBlast, tomatoBarrage, - waterBalloonRain, pillowFight, vinylRecordFling, frisbeeToss, - beachBallBonk, soccerKick, basketballDunk, - // --- NEW: Drop attacks --- - pianoDrop, safeDrop, fridgeDrop, couchDrop, tvDrop, toiletDrop, - bathtubDrop, washerDrop, vendingDrop, chandelierDrop, boulderDrop, - satelliteDrop, ufoDrop, giantShoeDrop, giantPhoneDrop, - // --- NEW: Vehicles --- - shoppingCart, forkliftCharge, tankRoll, helicopterStrike, - airplaneSwoop, rocketRide, unicycleRun, zamboniCrush, tractorPlow, golfCartDrive, - // --- NEW: Spin attacks --- - tornadoSpin, backflipKick, corkscrewDive, helicopterArms, breakdanceSweep, - cartwheelStrike, frontflipSlam, drillSpin, cycloneKick, pirouetteStrike, - // --- NEW: Flip attacks --- - doubleBackflip, wallBounce, springboardLaunch, barrelRoll, - trampolineBounce, poleVault, skateTrick, surfSlam, rolloutBall, aerialTwist, - // --- NEW: Beam attacks --- - kamehameha, freezeRay, heatVision, plasmaBeam, rainbowBeam, - shadowBeam, sonicWave, gravityBeam, mindBlast, pixelBeam, - glitchBeam, bassDropBeam, wifiBlast, dataStream, chainLightning, - // --- NEW: Animal summons --- - sharkBite, bearSwipe, eagleDive, snakeLunge, bullCharge, - gorillaSlam, scorpionSting, crabPinch, batSwarm, wolfPack, - spiderWeb, beeSwarm, catScratch, dogPile, dolphinFlip, - // --- NEW: Explosion attacks --- - dynamiteBlast, c4Detonation, fireworksBurst, nukeStrike, volcanoErupt, - grenadeBlast, partyPopper, pinataSmash, balloonPop, glitterBomb, - smokeBomb, flashBang, cherryBomb, confettiCannon, stinkBomb, - // --- NEW: Silly/Meme --- - rubberChicken, airHornBlast, vuvuzelaBlast, selfieStrike, - dabAttack, flossAttack, yeetThrow, tPoseAssert, emojiBarrage, - memeBeam, ratioAttack, capThrow, fingerGuns, micDrop, - dramaticEntrance, clapback, touchGrass, noScope, - // --- NEW: Grapple/Wrestling --- - piledriver, powerbomb, ddt, germanSuplex, chokeslam, - tombstone, stunner, rko, spear, clothesline, - atomicDrop, hurricanrana, moonsault, elbowDrop, frogSplash, - // --- NEW: Energy/Magic --- - fireball, iceLance, thunderStrike, darkVoid, solarFlare, - spiritBomb, windSlash, earthquakeStrike, portalPunch, - gravityWell, plasmaOrb, voidRift, crystalShards, meteorStrike, - arcaneBarrage, holySmite, hexCurse, bloodMoon, shadowClone, - // --- NEW: Tech/Modern --- - droneStrike, satelliteLaser, hackerAttack, bugSwarm, - stackOverflow, segfault, blueScreen, fourOhFour, ctrlAltDelete, - bitcoinCrash, nftRugPull, aiUprising, captchaTrap, popupSpam, malwareInject, - // --- NEW: Music --- - bassDrop, guitarSolo, drumSolo, dubstepCannon, airGuitar, - karaokeAttack, beatboxBlast, vinylScratch, - // --- NEW: Sports --- - homeRunSwing, slapShot, servingAce, fieldGoalKick, bodyCheck, - dropKick, elbowStrike, kneeStrike, headbutt, shoulderCharge, - // --- NEW: Nature/Elemental --- - treeSmash, icebergDrop, lavaSplash, tornadoFling, tsunamiWave, - avalanche, vineWhip, sandstorm, thornBarrage, pollenCloud, - // --- NEW: Space/Sci-fi --- - blackHole, photonTorpedo, tractor_beam, warpDrive, alienAbduction, - laserSword, ionCannon, plasmaSword, cosmicRay, asteroidBelt, - // --- NEW: Medieval/Fantasy --- - dragonBreath, maceSwing, crossbowBolt, shieldBash, flailSwing, - battleAxe, holyWater, potionThrow, scrollBlast, enchantedArrow, - // --- TIER-GATED ULTIMATES --- - // Tier 2+ - ultimateRapidBarrage, ultimateOrbitalStrike, ultimateCloneSurround, ultimateGatlingFury, ultimateEarthquake, - ultimateBouncingFury, ultimateTripleBeam, ultimateMegaThrow, ultimateTornadoGrab, ultimateMeteorShower, - // Tier 3+ - ultimateScreenNuke, ultimateBlackHoleVortex, ultimateDimensionalRift, ultimateTimeSlow, ultimateSummonArmy, - ultimateHellfireRain, ultimateCrystalCage, ultimateGigaDrill, ultimateSuperSlam, ultimateAvalancheDrop, ultimateBulletTimeBarrage, - // Tier 4+ - ultimateFinaleRush, ultimateMeteorCrash, ultimateScreenShatter, ultimateGravityReverse, ultimateInfiniteCombo, - ultimatePlasmaStorm, ultimateFrozenCoffin, ultimateLavaGeyser, ultimateChainGrab, ultimateRocketBarrage, - // Tier 5+ - ultimateArmageddon, ultimateOmegaBeam, ultimateBeyondTheScreen, ultimateMatrixDodge, ultimateWorldEnder, - ultimateAbsoluteZero, ultimateSolarCannon, ultimatePhantomStrike, ultimateGodPunch, ultimateVoidCollapse, - // --- 50 NEW GENERAL --- - // Food - sushiRoll, tacoSlam, iceCreamFreeze, wokToss, cakeSmash, - souffle, curryBlast, gumballBarrage, frenchFryJavelin, coconutCannon, - lobsterClaw, chiliPepper, jellyBounce, steakSlap, watermelonBowl, - // Energy/Beam - novaBlast, plasmaWhip, sonicBoom, prismBeam, - photonCannon, vortexPull, thunderClap, gravityCrush, shockPulse, - auraBlast, darkPulse, radiantBeam, glacialRay, - // Silly - plushiePummel, selfieStun, vibeCheck, bananaPeelSlip, rubberDucky, - kazooBlast, confettiPop, airHornStun, touchGrassSmack, - // Weapons - nunchakuSpin, tridentThrust, boomerangToss, morningstar, scytheSlash, - javelinthrow, shuriken, warhammerCrush, chakramSlice, blowgunDart, - // --- BITCOIN-THEMED (30 new) --- - 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, - // --- THE CREATOR: Bitcoin-themed moves --- - bitcoinRain, lightningInvoice, satoshiStrike, hashRateOverload, blockchainSlam, creatorMode, - morphStrike, divineIntervention, - bitcoinSwarm, satoshiTornado, hodlWave, bitcoinBarrage, - // --- THE CREATOR: Exclusive ultimates --- - ultimateGenesisBlock, ultimateTheHalving, ultimateFullNodeCrush, ultimateSatoshiVision, - } - // === MORPH / COSTUME SYSTEM === // 3 morph types per character: elemental, mech, and beast // Each morph temporarily transforms the fighter with visual overlays, aura, and color shift @@ -10246,7 +4843,7 @@ export async function createFightScene(config: FightSceneConfig) { const origDX = side === 'a' ? HOME_B : HOME_A const direction = side === 'a' ? 1 : -1 - const fn = choreographyMap[choreographyName] || dashPunch + const fn = choreographyMap[choreographyName] || choreographyMap['dashPunch'] await fn(attacker, defender, direction, origAX, origDX, isCritical) // Ensure positions are correct and sprites are idle diff --git a/frontend/src/game/fight/choreography/basic.ts b/frontend/src/game/fight/choreography/basic.ts new file mode 100644 index 0000000..020a2cf --- /dev/null +++ b/frontend/src/game/fight/choreography/basic.ts @@ -0,0 +1,1467 @@ +import type { Fighter, ChoreoContext } from '../types' +import { createFactories } from './factories' +import type { ChoreoFn } from './factories' + +export function createBasicChoreographies(ctx: ChoreoContext): Record { + const { + k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor, + trackedInterval, clearTracked, + spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile, + spawnShockwave, screenFlash, impactFreeze, spawnAfterimages, + glitchRGB, scanlineGlitch, dimensionalShift, + sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion, + sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin, + sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown, + sfxZap, sfxZoomWhoosh, sfxBlock, + spawnGrotesqueDetails, destroyGrotesqueDetails, + } = ctx + const { + spawnWeaponProp, moveProp, destroyProp, PROPS, spawnProp, spawnBarrage, + makeThrow, makeThrowProp, makeGunProp, makeSwing, makeDrop, + makeVehicle, makeBeam, makeSpin, makeSwarm, makeExplosion, makeGrab, + } = createFactories(ctx) + +async function dashPunch(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const contactX = origDX - dir * 50 + // Fast sprint to contact + await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + atk.play('attack') + sfxPunch() + await k.wait(0.12) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 12 : 5) + spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 12 : 5, '#ffcc00') + if (isCritical) { screenFlash('#ffffff'); sfxCritical(); await impactFreeze() } + spawnAfterimages(atk, 2) + // Push defender + const push = dir * (isCritical ? 100 : 35) + k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.35) + // Return + 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 aerialSlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const origAY = atk.pos.y + const contactX = origDX - dir * 20 + // Jump way up + await k.tween(atk.pos.y, origAY - 220, 0.18, (v) => { atk.pos.y = v }, k.easings.easeOutQuad) + atk.play('kick') + sfxKick() + // Arc down onto opponent + await Promise.all([ + k.tween(atk.pos.x, contactX, 0.22, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(atk.pos.y, origAY, 0.22, (v) => { atk.pos.y = v }, k.easings.easeInQuad), + ]) + // SLAM + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 20 : 10) + spawnSparks(def.pos.x, def.pos.y - 20, 15, theme.accent) + spawnShockwave(def.pos.x, GROUND_Y, theme.accent) + if (isCritical) { screenFlash('#ff2d7b'); sfxExplosion(); await impactFreeze(0.1) } else { sfxPunch() } + spawnAfterimages(atk, 3) + const push = dir * (isCritical ? 120 : 50) + k.tween(def.pos.x, origDX + push, 0.25, (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(atk.pos.y, origAY, 0.15, (v) => { atk.pos.y = v }, k.easings.easeOutBounce), + k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function flyingKick(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const origAY = atk.pos.y + const contactX = origDX - dir * 30 + // Small jump + fly horizontally + atk.play('kick') + sfxKick() + await Promise.all([ + k.tween(atk.pos.x, contactX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad), + k.tween(atk.pos.y, origAY - 80, 0.1, (v) => { atk.pos.y = v }, k.easings.easeOutQuad).then(() => + k.tween(atk.pos.y, origAY, 0.1, (v) => { atk.pos.y = v }, k.easings.easeInQuad) + ), + ]) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 14 : 6) + spawnSparks(def.pos.x, def.pos.y - 40, 8, '#ff6600') + if (isCritical) { sfxCritical(); screenFlash('#ff6600'); await impactFreeze() } else { sfxPunch() } + spawnAfterimages(atk, 3) + 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) + // Bounce back + 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.easeInOutQuad), + ]) +} + +async function dashThrough(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const behindX = origDX + dir * 80 + atk.play('attack') + sfxSpecial() + // Blazing fast dash through opponent + await k.tween(atk.pos.x, behindX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeInQuad) + // Hit happens mid-pass + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 15 : 7) + spawnSparks(origDX, def.pos.y - 30, 10, '#00f0ff') + spawnAfterimages(atk, 3) + if (isCritical) { screenFlash('#00f0ff'); await impactFreeze() } + // Dramatic pause behind opponent + await k.wait(0.3) + // Zip back to start + atk.play('idle') + await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.1) +} + +async function uppercut(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const contactX = origDX - dir * 40 + const origDY = def.pos.y + // Run to contact + await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + atk.play('special') + sfxSpecial() + await k.wait(0.1) + // Launch defender into the air! + def.play('knockback') + k.shake(isCritical ? 18 : 8) + spawnSparks(def.pos.x, def.pos.y - 30, 12, '#ffe14d') + if (isCritical) { screenFlash('#ffe14d'); sfxExplosion(); await impactFreeze(0.1) } + const launchHeight = isCritical ? 250 : 160 + await k.tween(def.pos.y, origDY - launchHeight, 0.25, (v) => { def.pos.y = v }, k.easings.easeOutQuad) + await k.wait(0.15) + // Defender crashes back down + await k.tween(def.pos.y, origDY, 0.3, (v) => { def.pos.y = v }, k.easings.easeInQuad) + spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d') + k.shake(5) + sfxPunch() + await k.wait(0.2) + // Return + await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad) +} + +async function multiHit(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const contactX = origDX - dir * 45 + // Sprint to contact + await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + const hitCount = isCritical ? 5 : 3 + for (let i = 0; i < hitCount; i++) { + atk.play(i % 2 === 0 ? 'attack' : 'kick') + sfxPunch() + await k.wait(0.08) + def.play('hit') + k.shake(3 + i) + spawnSparks(def.pos.x + (Math.random() - 0.5) * 30, def.pos.y - 20 - Math.random() * 40, 4, '#ff2d7b') + // Jitter defender + def.pos.x += dir * 8 + await k.wait(0.08) + } + if (isCritical) { screenFlash('#ff2d7b'); sfxCritical() } + // Final big push + def.play('knockback') + k.shake(12) + const push = dir * (isCritical ? 130 : 60) + 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function groundPound(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const origAY = atk.pos.y + // Jump super high off screen + sfxJetpack() + await k.tween(atk.pos.y, -100, 0.25, (v) => { atk.pos.y = v }, k.easings.easeOutQuad) + // Move to center of stage + const slamX = (origAX + origDX) / 2 + atk.pos.x = slamX + await k.wait(0.2) + // SLAM DOWN + atk.play('special') + sfxSpecial() + await k.tween(atk.pos.y, origAY, 0.12, (v) => { atk.pos.y = v }, k.easings.easeInQuad) + // Massive shockwave + k.shake(isCritical ? 30 : 18) + screenFlash(isCritical ? '#ff2d2d' : '#ffe14d', 0.2) + sfxExplosion() + spawnShockwave(slamX, GROUND_Y, theme.accent) + spawnSparks(slamX, GROUND_Y - 10, 25, theme.accent) + // Defender gets launched + const origDY = def.pos.y + def.play('knockback') + const push = dir * (isCritical ? 130 : 70) + const launchH = isCritical ? 180 : 100 + 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 - launchH, 0.2, (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) + ), + ]) + k.shake(5) + spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d') + await k.wait(0.3) + // 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 teleportStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Fade out + sfxSpecial() + spawnSparks(atk.pos.x, atk.pos.y - 30, 8, '#b83dff') + await k.tween(1, 0, 0.15, (v) => { atk.opacity = v }) + // Teleport behind opponent + atk.pos.x = origDX + dir * 60 + await k.wait(0.1) + // Fade in behind + spawnSparks(atk.pos.x, atk.pos.y - 30, 8, '#b83dff') + await k.tween(0, 1, 0.1, (v) => { atk.opacity = v }) + // Strike from behind! + atk.play('attack') + sfxPunch() + await k.wait(0.1) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 15 : 8) + spawnSparks(def.pos.x, def.pos.y - 30, 12, '#b83dff') + if (isCritical) { screenFlash('#b83dff'); sfxCritical() } + // Push defender forward (reversed since we're behind) + const push = -dir * (isCritical ? 80 : 40) + k.tween(def.pos.x, origDX + push, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.3) + // Teleport back home + spawnSparks(atk.pos.x, atk.pos.y - 30, 6, '#b83dff') + await k.tween(1, 0, 0.1, (v) => { atk.opacity = v }) + atk.pos.x = origAX + await k.tween(0, 1, 0.1, (v) => { atk.opacity = v }) + spawnSparks(origAX, atk.pos.y - 30, 6, '#b83dff') + atk.play('idle') + await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad) +} + +async function fullScreenDash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Back up to wall + const wallX = dir === 1 ? 20 : W - 20 + await k.tween(atk.pos.x, wallX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.1) + // FULL SPEED DASH across entire screen! + atk.play('kick') + sfxSpecial() + const targetX = dir === 1 ? W - 20 : 20 + // Speed lines + for (let i = 0; i < 8; i++) { + const lineY = Math.random() * H * 0.7 + const line = k.add([ + k.rect(W, 2), + k.pos(0, lineY), + k.color(safeColor(theme.accent)), + k.opacity(0.3), + k.z(12), + ]) + setTimeout(() => { if (line.exists()) line.destroy() }, 300) + } + await k.tween(atk.pos.x, targetX, 0.18, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad) + // Hit happens when passing through defender + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 22 : 10) + spawnSparks(origDX, def.pos.y - 30, 15, '#00f0ff') + screenFlash(isCritical ? '#ffffff' : '#00f0ff', 0.12) + if (isCritical) sfxExplosion() + else sfxKick() + const push = dir * (isCritical ? 100 : 50) + k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.35) + // Return from far side + 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function lightningRush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + sfxSpecial() + // Attacker vanishes + atk.opacity = 0 + // Blur lines everywhere + for (let i = 0; i < 12; i++) { + const lineY = GROUND_Y - 10 - Math.random() * 120 + const line = k.add([ + k.rect(W, 2 + Math.random() * 2), + k.pos(0, lineY), + k.color(safeColor(Math.random() > 0.5 ? '#ffffff' : theme.accent)), + k.opacity(0.6), + k.z(25), + ]) + k.tween(0.6, 0, 0.15, (v) => { if (line.exists()) line.opacity = v }).then(() => { if (line.exists()) line.destroy() }) + } + // Multiple instant impacts across the screen (so fast, hits happen everywhere) + const hitCount = isCritical ? 8 : 5 + for (let i = 0; i < hitCount; i++) { + const hx = origDX + (Math.random() - 0.5) * 100 + const hy = GROUND_Y - 20 - Math.random() * 80 + spawnSparks(hx, hy, 5, '#ffffff') + sfxPunch() + k.shake(3) + // Brief flash of attacker at hit position + atk.pos.x = hx - dir * 30 + atk.opacity = 0.3 + await k.wait(0.02) + atk.opacity = 0 + await k.wait(0.01) + } + // Final massive hit + screenFlash('#ffffff', 0.15) + atk.pos.x = origDX - dir * 40 + atk.opacity = 1 + atk.play('attack') + sfxCritical() + await k.wait(0.05) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 25 : 12) + spawnSparks(def.pos.x, def.pos.y - 30, 20, '#ffffff') + if (isCritical) sfxExplosion() + const push = dir * (isCritical ? 140 : 70) + 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.15, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function zoomRush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const origAY = atk.pos.y + const origScaleX = atk.scale.x + const origScaleY = atk.scale.y + const origDefScaleX = def.scale.x + const origDefScaleY = def.scale.y + sfxZoomWhoosh() + // Randomize zoom level for variety (3x to 7x) + const zoomScale = isCritical ? 5 + Math.random() * 2 : 3 + Math.random() * 2 + const zoomDur = 0.2 + Math.random() * 0.1 + // Attacker rushes toward camera — scale up dramatically + // Defender shrinks to background (perspective effect) + await Promise.all([ + k.tween(Math.abs(origScaleX), zoomScale, zoomDur, (v) => { + atk.scale.x = origScaleX > 0 ? v : -v + atk.scale.y = v + }, k.easings.easeInQuad), + k.tween(Math.abs(origDefScaleX), Math.abs(origDefScaleX) * 0.4, zoomDur, (v) => { + def.scale.x = origDefScaleX > 0 ? v : -v + def.scale.y = v + }, k.easings.easeInQuad), + k.tween(atk.pos.x, W / 2, zoomDur, (v) => { atk.pos.x = v }, k.easings.easeOutQuad), + k.tween(atk.pos.y, H * 0.6, zoomDur, (v) => { atk.pos.y = v }, k.easings.easeOutQuad), + k.tween(1, 0.3, zoomDur, (v) => { atk.opacity = Math.max(0.3, v) }), + ]) + // Brief dramatic pause at huge size — screen darkens + grotesque close-up + screenFlash('#000000', 0.15) + if (zoomScale >= 4) spawnGrotesqueDetails(atk, zoomScale * 0.4) + k.shake(5) + await k.wait(0.1 + Math.random() * 0.08) + destroyGrotesqueDetails() + // Zoom back and SLAM into opponent (defender grows back) + sfxZoomWhoosh() + sfxSpecial() + await Promise.all([ + k.tween(atk.scale.y, Math.abs(origScaleY), 0.15, (v) => { + atk.scale.x = origScaleX > 0 ? v : -v + atk.scale.y = v + }, k.easings.easeInQuad), + k.tween(Math.abs(def.scale.y), Math.abs(origDefScaleY), 0.15, (v) => { + def.scale.x = origDefScaleX > 0 ? v : -v + def.scale.y = v + }, k.easings.easeOutQuad), + k.tween(atk.pos.x, origDX - dir * 40, 0.15, (v) => { atk.pos.x = v }, k.easings.easeInQuad), + k.tween(atk.pos.y, origAY, 0.15, (v) => { atk.pos.y = v }, k.easings.easeInQuad), + k.tween(0.3, 1, 0.15, (v) => { atk.opacity = v }), + ]) + // IMPACT — rapid punches on contact + atk.play('attack') + const punchCount = isCritical ? 8 : 5 + for (let i = 0; i < punchCount; i++) { + sfxRapidPunch() + def.play(i % 2 === 0 ? 'hit' : 'knockback') + spawnSparks(def.pos.x + (Math.random() - 0.5) * 40, def.pos.y - 10 - Math.random() * 50, 4, i % 2 === 0 ? '#ff2d7b' : '#ffcc00') + k.shake(2 + i) + def.pos.x += dir * 4 + atk.play(i % 2 === 0 ? 'attack' : 'kick') + await k.wait(0.04) + } + // Final haymaker + sfxCritical() + k.shake(isCritical ? 25 : 15) + screenFlash(isCritical ? '#ffffff' : '#ff2d7b', 0.2) + spawnSparks(def.pos.x, def.pos.y - 30, 20, '#ffffff') + spawnShockwave(def.pos.x, GROUND_Y, '#ff2d7b') + if (isCritical) sfxExplosion() + 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 — restore both scales + atk.scale.x = origScaleX + atk.scale.y = origScaleY + def.scale.x = origDefScaleX + def.scale.y = origDefScaleY + await Promise.all([ + k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(def.pos.x, origDX, 0.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function rapidFlurry(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const contactX = origDX - dir * 40 + // Sprint to contact + sfxSpecial() + await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + // UNLEASH — rapid alternating punches + const hitCount = isCritical ? 14 : 8 + const colors = ['#ff2d7b', '#ffcc00', '#00f0ff', '#ff6600', '#ffffff'] + // Speed lines during flurry + for (let i = 0; i < 6; i++) { + const lineY = GROUND_Y - 10 - Math.random() * 100 + const line = k.add([ + k.rect(W * 0.5, 2), + k.pos(dir === 1 ? origAX - 50 : origDX + 50, lineY), + k.color(safeColor(colors[i % colors.length])), + k.opacity(0.4), + k.z(12), + ]) + k.tween(0.4, 0, 0.5, (v) => { if (line.exists()) line.opacity = v }).then(() => { if (line.exists()) line.destroy() }) + } + for (let i = 0; i < hitCount; i++) { + const anim = ['attack', 'kick', 'attack', 'special'][i % 4] + atk.play(anim) + sfxRapidPunch() + // Sparks spray in alternating directions + const sparkX = def.pos.x + (i % 2 === 0 ? -1 : 1) * (10 + Math.random() * 20) + const sparkY = def.pos.y - 10 - Math.random() * 50 + spawnSparks(sparkX, sparkY, 3, colors[i % colors.length]) + def.play('hit') + k.shake(2) + // Jitter defender rapidly side to side + def.pos.x += (i % 2 === 0 ? dir : -dir) * 6 + def.pos.y += (i % 2 === 0 ? -3 : 3) + await k.wait(0.035) + } + // Pause — "Is it over?" + await k.wait(0.08) + // ONE MORE — the big finish + atk.play('special') + sfxCritical() + sfxPunch() + await k.wait(0.06) + def.play('knockback') + k.shake(isCritical ? 22 : 12) + screenFlash(isCritical ? '#ffffff' : '#ffcc00', 0.15) + spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 25 : 12, '#ffcc00') + spawnShockwave(def.pos.x, GROUND_Y, '#ffcc00') + if (isCritical) sfxExplosion() + const push = dir * (isCritical ? 140 : 70) + k.tween(def.pos.x, origDX + push, 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad) + // Reset defender Y + k.tween(def.pos.y, GROUND_Y - 6, 0.2, (v) => { def.pos.y = v }) + 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function afterimageDash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + sfxSpecial() + const contactX = origDX - dir * 40 + // Leave afterimages as we dash + const ghostCount = isCritical ? 6 : 4 + const startX = atk.pos.x + for (let i = 0; i < ghostCount; i++) { + const ghost = k.add([ + k.rect(20, 35), + k.pos(atk.pos.x, atk.pos.y - 25), + k.color(safeColor(theme.accent)), + k.opacity(0.4), + k.z(9), + ]) + setTimeout(() => { k.tween(0.4, 0, 0.3, (v) => { if (ghost.exists()) ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() }) }, 50) + // Move attacker a step forward + const stepX = startX + ((contactX - startX) / ghostCount) * (i + 1) + await k.tween(atk.pos.x, stepX, 0.03, (v) => { atk.pos.x = v }) + } + atk.play('attack') + sfxPunch() + await k.wait(0.08) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 16 : 7) + // Speed lines + for (let i = 0; i < 5; i++) { + const lineY = GROUND_Y - 20 - Math.random() * 100 + const line = k.add([ + k.rect(W * 0.4, 2), + k.pos(dir === 1 ? origAX : origDX, lineY), + k.color(safeColor(theme.accent)), + k.opacity(0.5), + k.z(12), + ]) + k.tween(0.5, 0, 0.2, (v) => { if (line.exists()) line.opacity = v }).then(() => { if (line.exists()) line.destroy() }) + } + if (isCritical) { screenFlash('#ffffff'); sfxCritical() } + spawnSparks(def.pos.x, def.pos.y - 30, 12, theme.accent) + 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) + 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 bulletTimeAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Phase 1: Darken screen — entering bullet time + const dim = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor('#000011')), k.opacity(0), k.z(25)]) + await k.tween(0, 0.45, 0.15, (v) => { dim.opacity = v }) + scanlineGlitch(0.1) + + // Phase 2: Attacker fires slow golden projectiles (bullets visible in slow-mo) + atk.play('special') + sfxGunshot() + const bulletCount = isCritical ? 5 : 3 + const bullets: any[] = [] + for (let i = 0; i < bulletCount; i++) { + const bx = atk.pos.x + dir * 25 + const by = atk.pos.y - 30 + (i - 1) * 12 + const bullet = k.add([ + k.rect(isCritical ? 14 : 10, 3), + k.pos(bx, by), + k.color(safeColor('#ffd700')), + k.opacity(0.9), + k.z(26), + k.anchor('center'), + k.rotate(dir === 1 ? 0 : 180), + ]) + bullets.push(bullet) + // Golden trace trail behind each bullet + const trace = k.add([ + k.rect(40, 1), + k.pos(bx - dir * 20, by), + k.color(safeColor('#ffaa00')), + k.opacity(0.5), + k.z(25), + ]) + k.tween(trace.opacity, 0, 0.4, (v) => { if (trace.exists()) trace.opacity = v }).then(() => { if (trace.exists()) trace.destroy() }) + await k.wait(0.08) // Slow-mo pacing between shots + } + + // Phase 3: Bullets travel slowly toward defender + await Promise.all(bullets.map((b, i) => + k.tween(b.pos.x, def.pos.x + (Math.random() - 0.5) * 15, 0.25 + i * 0.03, (v) => { b.pos.x = v }, k.easings.easeInQuad) + )) + bullets.forEach(b => { if (b.exists()) b.destroy() }) + + // Phase 4: Attacker dodges (lean back in slow-mo with afterimages) + const leanBack = dir * -30 + for (let i = 0; i < 4; i++) { + const ghost = k.add([ + k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.8, FRAME_SIZE * Math.abs(atk.scale.y)), + k.pos(atk.pos.x + i * leanBack * 0.15, atk.pos.y), + k.color(safeColor('#ffd700')), + k.opacity(0.25 - i * 0.05), + k.z(24), + k.anchor('bot'), + ]) + setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 400) + } + await k.tween(atk.pos.x, atk.pos.x + leanBack, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + + // Phase 5: Time resumes — attacker dashes in for devastating counter + dim.destroy() + sfxZoomWhoosh() + const contactX = origDX - dir * 40 + // Afterimage trail during rush + const rushStart = atk.pos.x + for (let i = 0; i < 5; i++) { + const ghost = k.add([ + k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.7, FRAME_SIZE * Math.abs(atk.scale.y) * 0.9), + k.pos(rushStart + ((contactX - rushStart) * i / 5), atk.pos.y), + k.color(safeColor(theme.accent)), + k.opacity(0.3 - i * 0.05), + k.z(9), + k.anchor('bot'), + ]) + setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 250) + } + await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad) + atk.play('attack') + + // Phase 6: Impact + const hits = isCritical ? 4 : 2 + for (let i = 0; i < hits; i++) { + sfxPunch() + def.play('hit') + k.shake(6 + i * 3) + spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 20 - Math.random() * 20, 6, '#ffd700') + if (i < hits - 1) await k.wait(0.06) + } + // Final devastating blow + sfxCritical() + if (isCritical) { sfxExplosion(); screenFlash('#ffd700', 0.2) } + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 22 : 12) + spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 20 : 12, '#ffd700') + if (isCritical) spawnShockwave(def.pos.x, GROUND_Y, '#ffd700') + + // Return to positions + const push = dir * (isCritical ? 70 : 35) + k.tween(def.pos.x, origDX + push, 0.25, (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) => { atk.pos.x = v }, k.easings.easeInQuad), + k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) + atk.play('idle') +} + +async function golfClubSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const contactX = origDX - dir * 60 + await k.tween(atk.pos.x, contactX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + // Spawn golf club (long thin rect) + const club = k.add([ + k.rect(8, 60), + k.pos(atk.pos.x + dir * 20, atk.pos.y - 60), + k.color(safeColor('#888888')), + k.anchor('bot'), + k.z(18), + k.rotate(dir === 1 ? -45 : 45), + ]) + // Club head (wider rect at end) + const clubHead = k.add([ + k.rect(18, 12), + k.pos(club.pos.x + dir * 15, club.pos.y - 55), + k.color(safeColor('#cccccc')), + k.z(18), + ]) + atk.play('attack') + sfxSpecial() + // Swing the club + await k.tween(dir === 1 ? -45 : 45, dir === 1 ? 90 : -90, 0.15, (v) => { club.angle = v }, k.easings.easeInQuad) + sfxBonk() + // Club hits - launch golf balls! + club.destroy() + clubHead.destroy() + const ballCount = isCritical ? 6 : 3 + for (let i = 0; i < ballCount; i++) { + const ballY = def.pos.y - 20 - Math.random() * 40 + const ball = k.add([ + k.circle(5), + k.pos(atk.pos.x + dir * 30, atk.pos.y - 40), + k.color(safeColor('#ffffff')), + k.z(16), + ]) + sfxPunch() + const targetX = def.pos.x + (Math.random() - 0.5) * 30 + k.tween(0, 1, 0.12, (t) => { + ball.pos.x = (atk.pos.x + dir * 30) + (targetX - atk.pos.x - dir * 30) * t + ball.pos.y = (atk.pos.y - 40) + (ballY - atk.pos.y + 40) * t - Math.sin(t * Math.PI) * 60 + }).then(() => { + ball.destroy() + spawnSparks(targetX, ballY, 4, '#ffffff') + }) + await k.wait(0.06) + } + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 18 : 8) + if (isCritical) screenFlash('#39ff14') + spawnSparks(def.pos.x, def.pos.y - 30, 10, '#39ff14') + 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) + 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 fireBreath(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + const contactX = origDX - dir * 80 + await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + atk.play('special') + sfxSpecial() + // Fire stream! + const fireX = atk.pos.x + dir * 25 + const fireY = atk.pos.y - 45 + const fireCount = isCritical ? 40 : 20 + for (let i = 0; i < fireCount; i++) { + const p = k.add([ + k.circle(4 + Math.random() * 6), + k.pos(fireX, fireY + (Math.random() - 0.5) * 15), + k.color(safeColor(Math.random() > 0.3 ? '#ff6600' : Math.random() > 0.5 ? '#ffcc00' : '#ff2d2d')), + k.opacity(0.9), + k.z(16), + ]) + const speed = 400 + Math.random() * 400 + const vy = (Math.random() - 0.5) * 100 + p.onUpdate(() => { + p.pos.x += dir * speed * k.dt() + p.pos.y += vy * k.dt() + p.opacity -= 2 * k.dt() + if (p.opacity <= 0 || Math.abs(p.pos.x - fireX) > Math.abs(def.pos.x - fireX) + 30) p.destroy() + }) + if (i % 5 === 0) { + def.opacity = 0.5 + setTimeout(() => { if (def.exists()) def.opacity = 1 }, 50) + } + await k.wait(0.02) + } + await k.wait(0.1) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 20 : 8) + spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ff6600') + if (isCritical) { screenFlash('#ff6600'); sfxExplosion() } + // Defender on fire briefly + const fireTimer = trackedInterval(() => { + if (!def.exists()) { clearTracked(fireTimer); return } + k.add([ + k.circle(3), + k.pos(def.pos.x + (Math.random() - 0.5) * 25, def.pos.y - Math.random() * 60), + k.color(safeColor(Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), + k.opacity(0.7), + k.z(11), + ]).onUpdate(function(this: any) { this.pos.y -= 80 * k.dt(); this.opacity -= 2 * k.dt(); if (this.opacity <= 0) this.destroy() }) + }, 40) + const push = dir * (isCritical ? 110 : 50) + k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.6) + clearTracked(fireTimer) + 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 riddleBarrage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + atk.play('special') + sfxSpecial() + sfxZap() + await k.wait(0.2) + // Rain question marks from above + const qCount = isCritical ? 12 : 6 + for (let i = 0; i < qCount; i++) { + const qx = def.pos.x + (Math.random() - 0.5) * 100 + const qy = -20 + const q = k.add([ + k.circle(8), + k.pos(qx, qy), + k.color(safeColor('#b83dff')), + k.opacity(0.9), + k.z(16), + ]) + // Inner dot to make it look like "?" + const dot = k.add([ + k.circle(3), + k.pos(qx, qy + 12), + k.color(safeColor('#ffffff')), + k.opacity(0.9), + k.z(17), + ]) + k.tween(qy, def.pos.y - 20, 0.2 + Math.random() * 0.1, (v) => { q.pos.y = v; dot.pos.y = v + 12 }, k.easings.easeInQuad).then(() => { + q.destroy(); dot.destroy() + spawnSparks(qx, def.pos.y - 20, 3, '#b83dff') + sfxBonk() + k.shake(2) + }) + await k.wait(0.06) + } + await k.wait(0.15) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 15 : 7) + if (isCritical) { screenFlash('#b83dff'); sfxExplosion() } + const push = dir * (isCritical ? 90 : 40) + k.tween(def.pos.x, origDX + push, 0.25, (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 cloneStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + sfxZap() + // Spawn 3-5 "ghost" clones that zip around + const cloneCount = isCritical ? 5 : 3 + const clones: any[] = [] + for (let i = 0; i < cloneCount; i++) { + const clone = k.add([ + k.rect(20, 35), + k.pos(atk.pos.x, atk.pos.y - 25), + k.color(safeColor(theme.accent)), + k.opacity(0.3), + k.z(9), + ]) + clones.push(clone) + // Each clone zips to a random position + const rx = origDX + (Math.random() - 0.5) * 200 + const ry = GROUND_Y - 6 - Math.random() * 80 + k.tween(0, 1, 0.2, (t) => { + clone.pos.x = atk.pos.x + (rx - atk.pos.x) * t + clone.pos.y = (atk.pos.y - 25) + (ry - atk.pos.y + 25) * t + }) + } + await k.wait(0.25) + // Defender is confused — screen warps + screenFlash('#b83dff', 0.1) + await k.wait(0.15) + // Real attacker teleports and strikes + spawnSparks(atk.pos.x, atk.pos.y - 30, 6, theme.accent) + await k.tween(1, 0, 0.08, (v) => { atk.opacity = v }) + atk.pos.x = origDX - dir * 40 + await k.tween(0, 1, 0.08, (v) => { atk.opacity = v }) + atk.play('attack') + sfxPunch() + await k.wait(0.1) + // Destroy clones + clones.forEach(c => { if (c.exists()) { spawnSparks(c.pos.x, c.pos.y, 3, theme.accent); c.destroy() } }) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 16 : 7) + if (isCritical) { screenFlash('#ffffff'); sfxCritical() } + spawnSparks(def.pos.x, def.pos.y - 30, 10, theme.accent) + 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) + await Promise.all([ + k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function coinShower(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + atk.play('special') + sfxSpecial() + sfxBoing() + await k.wait(0.15) + // Coins rain from above + const coinCount = isCritical ? 20 : 10 + for (let i = 0; i < coinCount; i++) { + const cx = def.pos.x + (Math.random() - 0.5) * 80 + const coin = k.add([ + k.circle(5), + k.pos(cx, -10), + k.color(safeColor(Math.random() > 0.3 ? '#ffd700' : '#ffb000')), + k.opacity(1), + k.z(16), + ]) + const targetY = def.pos.y - 10 - Math.random() * 50 + k.tween(coin.pos.y, targetY, 0.15 + Math.random() * 0.1, (v) => { coin.pos.y = v }, k.easings.easeInQuad).then(() => { + coin.destroy() + spawnSparks(cx, targetY, 2, '#ffd700') + sfxBulletHit() + }) + await k.wait(0.03) + } + await k.wait(0.1) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 14 : 6) + if (isCritical) { screenFlash('#ffd700'); sfxExplosion() } + const push = dir * (isCritical ? 90 : 40) + k.tween(def.pos.x, origDX + push, 0.25, (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 penStab(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Spawn a giant pen/quill + const penX = atk.pos.x + dir * 15 + const penY = atk.pos.y - 70 + const pen = k.add([ + k.rect(6, 80), + k.pos(penX, penY), + k.color(safeColor('#4488ff')), + k.anchor('bot'), + k.z(18), + k.rotate(dir === 1 ? -30 : 30), + ]) + // Pen tip + const tip = k.add([ + k.rect(4, 12), + k.pos(penX + dir * 5, penY - 68), + k.color(safeColor('#333333')), + k.z(18), + ]) + atk.play('special') + sfxSpecial() + await k.wait(0.15) + // Thrust pen forward at opponent + const targetX = origDX - dir * 20 + await Promise.all([ + k.tween(pen.pos.x, targetX, 0.12, (v) => { pen.pos.x = v; tip.pos.x = v + dir * 5 }, k.easings.easeInQuad), + k.tween(pen.angle, dir === 1 ? 80 : -80, 0.12, (v) => { pen.angle = v }, k.easings.easeInQuad), + k.tween(atk.pos.x, targetX - dir * 30, 0.12, (v) => { atk.pos.x = v }, k.easings.easeInQuad), + ]) + sfxPunch() + pen.destroy() + tip.destroy() + // INK SPLATTER! + const inkColors = ['#1a1a3a', '#2a2a5a', '#0a0a2a', '#4488ff'] + for (let i = 0; i < (isCritical ? 20 : 10); i++) { + const splat = k.add([ + k.circle(4 + Math.random() * 10), + k.pos(def.pos.x + (Math.random() - 0.5) * 60, def.pos.y - Math.random() * 70), + k.color(safeColor(inkColors[Math.floor(Math.random() * inkColors.length)])), + k.opacity(0.8), + k.z(9), + ]) + setTimeout(() => { + k.tween(0.8, 0, 1.5, (v) => { if (splat.exists()) splat.opacity = v }).then(() => { if (splat.exists()) splat.destroy() }) + }, 800) + } + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 16 : 7) + if (isCritical) screenFlash('#4488ff') + 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.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.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function mathAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + atk.play('special') + sfxSpecial() + await k.wait(0.15) + // Fire number "projectiles" (colored circles representing digits) + const numCount = isCritical ? 10 : 5 + const colors = ['#ff2d7b', '#00f0ff', '#ffe14d', '#39ff14', '#b83dff'] + for (let i = 0; i < numCount; i++) { + const size = 6 + Math.random() * 8 + const fromY = atk.pos.y - 30 - Math.random() * 30 + const toY = def.pos.y - 10 - Math.random() * 50 + const num = k.add([ + k.circle(size), + k.pos(atk.pos.x + dir * 25, fromY), + k.color(safeColor(colors[i % colors.length])), + k.opacity(1), + k.z(16), + ]) + const dur = 0.1 + Math.random() * 0.05 + sfxGunshot() + k.tween(0, 1, dur, (t) => { + num.pos.x = (atk.pos.x + dir * 25) + (def.pos.x - atk.pos.x - dir * 25) * t + num.pos.y = fromY + (toY - fromY) * t + }).then(() => { + num.destroy() + spawnSparks(def.pos.x + (Math.random() - 0.5) * 20, toY, 3, colors[i % colors.length]) + }) + await k.wait(0.04) + } + await k.wait(0.1) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 15 : 7) + if (isCritical) { screenFlash('#ffe14d'); sfxExplosion() } + const push = dir * (isCritical ? 95 : 40) + k.tween(def.pos.x, origDX + push, 0.25, (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 trapCardAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + atk.play('special') + sfxSpecial() + await k.wait(0.1) + // Throw spinning cards + const cardCount = isCritical ? 5 : 3 + for (let i = 0; i < cardCount; i++) { + const card = k.add([ + k.rect(12, 18), + k.pos(atk.pos.x + dir * 20, atk.pos.y - 40 + (Math.random() - 0.5) * 20), + k.color(safeColor(i === 0 ? '#ff2d7b' : '#ffffff')), + k.anchor('center'), + k.z(16), + k.rotate(0), + ]) + const fromX = card.pos.x + const fromY = card.pos.y + const toX = def.pos.x + const toY = def.pos.y - 30 + (Math.random() - 0.5) * 30 + sfxGunshot() + k.tween(0, 1, 0.12, (t) => { + card.pos.x = fromX + (toX - fromX) * t + card.pos.y = fromY + (toY - fromY) * t + card.angle = t * 720 // Spin + }).then(() => { + card.destroy() + spawnSparks(toX, toY, 5, '#ff2d7b') + sfxBulletHit() + }) + await k.wait(0.08) + } + await k.wait(0.1) + // TRAP SPRINGS! A bear trap appears under defender + const trap = k.add([ + k.rect(30, 8), + k.pos(def.pos.x - 15, GROUND_Y - 8), + k.color(safeColor('#888888')), + k.z(8), + ]) + const jaw1 = k.add([ + k.rect(30, 4), + k.pos(def.pos.x - 15, GROUND_Y - 16), + k.color(safeColor('#aaaaaa')), + k.z(8), + ]) + sfxBonk() + k.shake(3) + await k.wait(0.05) + // Snap! Jaws close + k.tween(jaw1.pos.y, GROUND_Y - 10, 0.05, (v) => { jaw1.pos.y = v }) + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 18 : 8) + if (isCritical) { screenFlash('#ff2d7b'); sfxExplosion() } + spawnSparks(def.pos.x, GROUND_Y - 10, 8, '#ff2d7b') + await k.wait(0.3) + trap.destroy() + jaw1.destroy() + const push = dir * (isCritical ? 90 : 40) + k.tween(def.pos.x, origDX + push, 0.25, (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 pocketCannon(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // "Reach into pocket" — arm down motion + atk.play('special') + await k.wait(0.12) + + // Gun appears — comically oversized + const gunLen = isCritical ? 90 : 65 + const gunH = isCritical ? 22 : 16 + const gx = atk.pos.x + dir * 20 + const gy = atk.pos.y - 25 + // Barrel + const barrel = k.add([ + k.rect(gunLen, gunH), k.pos(gx, gy), + k.color(safeColor('#333333')), k.opacity(1), k.z(16), k.scale(1), + ]) + // Handle + const handle = k.add([ + k.rect(12, 20), k.pos(gx - dir * 5, gy + gunH / 2), + k.color(safeColor('#555555')), k.opacity(1), k.z(15), k.scale(1), + ]) + // Scope on top + const scope = k.add([ + k.circle(5), k.pos(gx + dir * gunLen * 0.6, gy - 6), + k.color(safeColor('#880000')), k.opacity(0.9), k.z(17), k.scale(1), + ]) + + // Gun grows in from small — comedy "pulling from tiny pocket" effect + barrel.scale = k.vec2(0.1, 0.1) + handle.scale = k.vec2(0.1, 0.1) + scope.scale = k.vec2(0.1, 0.1) + await Promise.all([ + k.tween(0.1, 1, 0.15, (v) => { barrel.scale = k.vec2(v, v) }, k.easings.easeOutBack), + k.tween(0.1, 1, 0.15, (v) => { handle.scale = k.vec2(v, v) }, k.easings.easeOutBack), + k.tween(0.1, 1, 0.15, (v) => { scope.scale = k.vec2(v, v) }, k.easings.easeOutBack), + ]) + sfxBoing() + k.shake(3) + await k.wait(0.08) + + // AIM — brief wobble + const wobbleTime = 0.12 + await k.tween(0, wobbleTime, wobbleTime, (t) => { + const w = Math.sin(t * 40) * 3 + barrel.pos.y = gy + w + handle.pos.y = gy + gunH / 2 + w + scope.pos.y = gy - 6 + w + }) + + // FIRE! massive shot + const shotCount = isCritical ? 4 : 2 + for (let s = 0; s < shotCount; s++) { + sfxGunshot() + sfxExplosion() + k.shake(isCritical ? 15 : 10) + // Muzzle flash + const flash = k.add([ + k.circle(12 + Math.random() * 8), + k.pos(gx + dir * gunLen, gy), + k.color(safeColor('#ffee00')), + k.opacity(0.9), k.z(18), + ]) + setTimeout(() => { if (flash.exists()) flash.destroy() }, 60) + // Recoil + const recoilX = atk.pos.x - dir * 15 + k.tween(atk.pos.x, recoilX, 0.04, (v) => { + atk.pos.x = v + barrel.pos.x = v + dir * 20 + handle.pos.x = v + dir * 20 - dir * 5 + scope.pos.x = v + dir * 20 + dir * gunLen * 0.6 + }).then(() => k.tween(recoilX, origAX, 0.06, (v) => { + atk.pos.x = v + barrel.pos.x = v + dir * 20 + handle.pos.x = v + dir * 20 - dir * 5 + scope.pos.x = v + dir * 20 + dir * gunLen * 0.6 + })) + // Massive projectile + await spawnProjectile(gx + dir * gunLen, gy, def.pos.x, def.pos.y - 20 + (Math.random() - 0.5) * 20, '#ffcc00', 10) + sfxBulletHit() + def.play(s === shotCount - 1 ? 'knockback' : 'hit') + spawnSparks(def.pos.x, def.pos.y - 20, 10, '#ff6600') + spawnShockwave(def.pos.x, GROUND_Y, '#ff6600') + def.pos.x += dir * (isCritical ? 20 : 12) + if (s < shotCount - 1) await k.wait(0.06) + } + + // Gun disappears — shrinks back into pocket + await Promise.all([ + k.tween(1, 0, 0.12, (v) => { barrel.scale = k.vec2(v, v) }), + k.tween(1, 0, 0.12, (v) => { handle.scale = k.vec2(v, v) }), + k.tween(1, 0, 0.12, (v) => { scope.scale = k.vec2(v, v) }), + ]) + barrel.destroy(); handle.destroy(); scope.destroy() + sfxBoing() + + if (isCritical) screenFlash('#ff6600', 0.15) + atk.play('idle') + await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad) +} + +async function grappleFlurry(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Both rush to center — meet in the middle + const meetX = (origAX + origDX) / 2 + const atkMeetX = meetX - dir * 25 + const defMeetX = meetX + dir * 25 + sfxSpecial() + await Promise.all([ + k.tween(atk.pos.x, atkMeetX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad), + k.tween(def.pos.x, defMeetX, 0.12, (v) => { def.pos.x = v }, k.easings.easeOutQuad), + ]) + + // Sustained trading blows — attacker dominates but defender fights back + const hitCount = isCritical ? 10 : 7 + for (let i = 0; i < hitCount; i++) { + const atkHits = i % 3 !== 2 // attacker hits 2/3 of the time + if (atkHits) { + atk.play(i % 2 === 0 ? 'attack' : 'kick') + sfxPunch() + await k.wait(0.03) + def.play('hit') + spawnSparks(def.pos.x - dir * 5, def.pos.y - 25 - Math.random() * 20, 3, i % 2 === 0 ? '#ff2d7b' : '#ffcc00') + k.shake(2 + Math.floor(i / 2)) + // Push defender slightly but attacker follows + def.pos.x += dir * 3 + atk.pos.x += dir * 2 + } else { + // Defender strikes back! + def.play('attack') + sfxKick() + await k.wait(0.03) + atk.play('hit') + spawnSparks(atk.pos.x + dir * 5, atk.pos.y - 25 - Math.random() * 20, 3, '#00f0ff') + k.shake(2) + atk.pos.x -= dir * 3 + def.pos.x -= dir * 2 + } + await k.wait(0.05) + } + + // Attacker finishes with a shove + atk.play('special') + sfxCritical() + k.shake(isCritical ? 20 : 12) + spawnSparks(def.pos.x, def.pos.y - 30, 12, '#ff2d7b') + if (isCritical) { + screenFlash('#ff2d7b', 0.15) + spawnShockwave(def.pos.x, GROUND_Y, '#ff2d7b') + } + def.play('knockback') + const pushDist = dir * (isCritical ? 120 : 60) + k.tween(def.pos.x, origDX + pushDist * 0.5, 0.2, (v) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.25) + + // Both return + atk.play('idle') + await Promise.all([ + k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(def.pos.x, origDX, 0.25, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function bodySlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Rush in close + sfxSpecial() + const contactX = def.pos.x - dir * 30 + await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + + // Grab — both freeze + atk.play('special') + def.play('hit') + sfxClash() + k.shake(5) + await k.wait(0.1) + + // Lift opponent overhead + const liftY = atk.pos.y - 80 + sfxBoing() + await k.tween(def.pos.y, liftY, 0.2, (v) => { def.pos.y = v }, k.easings.easeOutQuad) + await k.tween(def.pos.x, atk.pos.x + dir * 5, 0.1, (v) => { def.pos.x = v }) + // Spin overhead + const defOrigY = GROUND_Y + for (let i = 0; i < (isCritical ? 3 : 2); i++) { + await k.tween(def.pos.x, atk.pos.x - dir * 20, 0.06, (v) => { def.pos.x = v }) + await k.tween(def.pos.x, atk.pos.x + dir * 20, 0.06, (v) => { def.pos.x = v }) + } + + // SLAM DOWN + sfxExplosion() + def.play('knockback') + const slamX = atk.pos.x + dir * 50 + await Promise.all([ + k.tween(def.pos.y, defOrigY, 0.08, (v) => { def.pos.y = v }, k.easings.easeInQuad), + k.tween(def.pos.x, slamX, 0.08, (v) => { def.pos.x = v }), + ]) + sfxCritical() + k.shake(isCritical ? 25 : 16) + screenFlash(isCritical ? '#ffffff' : '#ff6600', 0.15) + spawnSparks(slamX, defOrigY - 10, 18, '#ff6600') + spawnShockwave(slamX, GROUND_Y, '#ff6600') + spawnBulletHoles(slamX, GROUND_Y, 3) + await k.wait(0.2) + + // Return + atk.play('idle') + await Promise.all([ + k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function pinballCombo(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Initial launch punch + atk.play('attack') + sfxPunch() + const contactX = def.pos.x - dir * 35 + await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + def.play('hit') + k.shake(8) + spawnSparks(def.pos.x, def.pos.y - 30, 8, '#ff2d7b') + + // Bounce the opponent back and forth across the screen + const bounces = isCritical ? 5 : 3 + const leftWall = 30 + const rightWall = W - 30 + let defX = def.pos.x + for (let i = 0; i < bounces; i++) { + const toWall = i % 2 === (dir > 0 ? 0 : 1) ? rightWall : leftWall + sfxZoomWhoosh() + await k.tween(defX, toWall, 0.06, (v) => { def.pos.x = v }, k.easings.easeInQuad) + defX = toWall + // Wall hit + sfxBonk() + k.shake(6 + i * 2) + spawnSparks(toWall, def.pos.y - 20, 6, ['#ff2d7b', '#ffcc00', '#00f0ff'][i % 3]) + screenFlash(['#ff2d7b', '#ffcc00', '#00f0ff'][i % 3], 0.05) + def.play(i % 2 === 0 ? 'hit' : 'knockback') + await k.wait(0.04) + } + + // Final slam to ground + sfxCritical() + sfxExplosion() + k.shake(isCritical ? 22 : 14) + screenFlash('#ffffff', 0.12) + spawnSparks(def.pos.x, def.pos.y - 20, 15, '#ffe14d') + spawnShockwave(def.pos.x, GROUND_Y, '#ffe14d') + def.play('knockback') + await k.wait(0.2) + + // Return + atk.play('idle') + await Promise.all([ + k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +async function suplex(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Dash behind defender + sfxSpecial() + const behindX = def.pos.x + dir * 30 + await k.tween(atk.pos.x, behindX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + // Flip attacker to face back + atk.scale.x = -atk.scale.x + atk.play('special') + sfxClash() + k.shake(4) + await k.wait(0.06) + + // Grab and arc overhead + const arcCx = (atk.pos.x + def.pos.x) / 2 + const arcTop = atk.pos.y - 120 + // Both rise in an arc + const steps = 8 + for (let i = 0; i <= steps; i++) { + const t = i / steps + const angle = Math.PI * t + def.pos.x = arcCx + Math.cos(angle) * 40 + def.pos.y = GROUND_Y - Math.sin(angle) * 120 + atk.pos.x = def.pos.x + dir * 20 + atk.pos.y = def.pos.y + 10 + await k.wait(0.02) + } + + // IMPACT — headfirst into ground + sfxExplosion() + sfxCritical() + k.shake(isCritical ? 28 : 18) + screenFlash(isCritical ? '#ffffff' : '#ff2d2d', 0.2) + spawnSparks(def.pos.x, GROUND_Y - 10, 20, '#ff2d2d') + spawnShockwave(def.pos.x, GROUND_Y, '#ff2d2d') + def.play('ko') + await k.wait(0.15) + def.play(isCritical ? 'knockback' : 'hit') + + // Flip attacker back + atk.scale.x = -atk.scale.x + atk.play('idle') + def.pos.y = GROUND_Y + await Promise.all([ + k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInOutQuad), + k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +const tornadoSpin = makeSpin('horizontal', 80, 3) + +const backflipKick = makeSpin('vertical', 140, 1) + +const corkscrewDive = makeSpin('corkscrew', 100, 2) + +const helicopterArms = makeSpin('horizontal', 50, 4) + +const breakdanceSweep = makeSpin('horizontal', 30, 3) + +const cartwheelStrike = makeSpin('vertical', 90, 1) + +const frontflipSlam = makeSpin('vertical', 160, 2) + +const drillSpin = makeSpin('corkscrew', 60, 3) + +const cycloneKick = makeSpin('horizontal', 100, 2) + +const pirouetteStrike = makeSpin('horizontal', 40, 5) + +const doubleBackflip = makeSpin('vertical', 180, 2) + +const wallBounce = makeSpin('vertical', 200, 1) + +const springboardLaunch = makeSpin('vertical', 220, 1) + +const barrelRoll = makeSpin('corkscrew', 80, 1) + +const trampolineBounce = makeSpin('vertical', 250, 2) + +const poleVault = makeSpin('vertical', 230, 1) + +const skateTrick = makeSpin('corkscrew', 120, 1) + +const surfSlam = makeSpin('horizontal', 60, 2) + +const rolloutBall = makeSpin('horizontal', 20, 4) + +const aerialTwist = makeSpin('corkscrew', 150, 2) + +const piledriver = makeGrab('down', 180) + +const powerbomb = makeGrab('up', 200) + +const ddt = makeGrab('down', 120) + +const germanSuplex = makeGrab('up', 160) + +const chokeslam = makeGrab('up', 220) + +const tombstone = makeGrab('down', 200) + +const stunner = makeGrab('down', 80) + +const rko = makeGrab('down', 140) + +const spear = makeGrab('toss', 80) + +const clothesline: ChoreoFn = async (atk, def, dir, origAX, origDX, isCritical) => { + const cx = origDX - dir * 20 + atk.play('attack'); sfxSpecial() + await k.tween(atk.pos.x, cx, 0.12, (v) => { atk.pos.x = v }, k.easings.easeInQuad) + sfxPunch(); def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 18 : 9); spawnSparks(def.pos.x, def.pos.y - 30, 10, '#ff4444') + if (isCritical) { screenFlash('#ff4444'); sfxCritical() } + // Defender spins backwards + const origDY = def.pos.y + await Promise.all([ + k.tween(def.pos.x, origDX + dir * (isCritical ? 120 : 60), 0.25, (v) => { def.pos.x = v }, k.easings.easeOutQuad), + k.tween(0, 360, 0.25, (v) => { def.angle = v }).then(() => { def.angle = 0 }), + k.tween(origDY, origDY - 50, 0.12, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() => + k.tween(origDY - 50, origDY, 0.13, (v) => { def.pos.y = v }, k.easings.easeInQuad)), + ]) + spawnShockwave(def.pos.x, GROUND_Y, '#ff4444') + await k.wait(0.2); 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.35, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) +} + +const atomicDrop = makeGrab('up', 140) + +const hurricanrana = makeGrab('spin', 120) + +const moonsault = makeSpin('vertical', 200, 2) + +const elbowDrop = makeDrop(10, 8, '#ffaa88', '#ff4444') + +const frogSplash = makeSpin('vertical', 180, 1) + +const homeRunSwing = makeSwing(5, 55, '#886633', '#ffffff', 10, 10) + +const slapShot = makeSwing(4, 55, '#333333', '#444444', 8, 8) + +const servingAce = makeThrow('#ccff00', 10, 1, 80, true, '#aadd00') + +const fieldGoalKick = makeSpin('vertical', 100, 1) + +const bodyCheck = makeGrab('toss', 60) + +const dropKick = makeSpin('vertical', 120, 1) + +const elbowStrike = makeGrab('down', 60) + +const kneeStrike = makeGrab('up', 50) + +const headbutt = makeGrab('down', 30) + +const shoulderCharge = makeGrab('toss', 40) + + return { + dashPunch, aerialSlam, flyingKick, dashThrough, uppercut, multiHit, groundPound, + teleportStrike, fullScreenDash, lightningRush, zoomRush, rapidFlurry, afterimageDash, + bulletTimeAttack, golfClubSmash, fireBreath, riddleBarrage, cloneStrike, coinShower, penStab, + mathAttack, trapCardAttack, pocketCannon, grappleFlurry, bodySlam, pinballCombo, suplex, + tornadoSpin, backflipKick, corkscrewDive, helicopterArms, breakdanceSweep, cartwheelStrike, + frontflipSlam, drillSpin, cycloneKick, pirouetteStrike, doubleBackflip, wallBounce, + springboardLaunch, barrelRoll, trampolineBounce, poleVault, skateTrick, surfSlam, rolloutBall, + aerialTwist, piledriver, powerbomb, ddt, germanSuplex, chokeslam, tombstone, stunner, rko, + spear, clothesline, atomicDrop, hurricanrana, moonsault, elbowDrop, frogSplash, homeRunSwing, + slapShot, servingAce, fieldGoalKick, bodyCheck, dropKick, elbowStrike, kneeStrike, headbutt, + shoulderCharge, + } +} diff --git a/frontend/src/game/fight/choreography/bitcoin.ts b/frontend/src/game/fight/choreography/bitcoin.ts new file mode 100644 index 0000000..15a10c5 --- /dev/null +++ b/frontend/src/game/fight/choreography/bitcoin.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/creator.ts b/frontend/src/game/fight/choreography/creator.ts new file mode 100644 index 0000000..8b68a05 --- /dev/null +++ b/frontend/src/game/fight/choreography/creator.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/factories.ts b/frontend/src/game/fight/choreography/factories.ts new file mode 100644 index 0000000..4a8d172 --- /dev/null +++ b/frontend/src/game/fight/choreography/factories.ts @@ -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 = { + // 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 + +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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/food.ts b/frontend/src/game/fight/choreography/food.ts new file mode 100644 index 0000000..3076165 --- /dev/null +++ b/frontend/src/game/fight/choreography/food.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/heavy.ts b/frontend/src/game/fight/choreography/heavy.ts new file mode 100644 index 0000000..4afe29f --- /dev/null +++ b/frontend/src/game/fight/choreography/heavy.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/index.ts b/frontend/src/game/fight/choreography/index.ts new file mode 100644 index 0000000..2b31812 --- /dev/null +++ b/frontend/src/game/fight/choreography/index.ts @@ -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 { + return { + ...createBasicChoreographies(ctx), + ...createRangedChoreographies(ctx), + ...createHeavyChoreographies(ctx), + ...createWeaponsChoreographies(ctx), + ...createFoodChoreographies(ctx), + ...createSillyChoreographies(ctx), + ...createBitcoinChoreographies(ctx), + ...createCreatorChoreographies(ctx), + ...createUltimatesChoreographies(ctx), + } +} diff --git a/frontend/src/game/fight/choreography/ranged.ts b/frontend/src/game/fight/choreography/ranged.ts new file mode 100644 index 0000000..1e5af44 --- /dev/null +++ b/frontend/src/game/fight/choreography/ranged.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/silly.ts b/frontend/src/game/fight/choreography/silly.ts new file mode 100644 index 0000000..e38f8f2 --- /dev/null +++ b/frontend/src/game/fight/choreography/silly.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/ultimates.ts b/frontend/src/game/fight/choreography/ultimates.ts new file mode 100644 index 0000000..f24ebb7 --- /dev/null +++ b/frontend/src/game/fight/choreography/ultimates.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/choreography/weapons.ts b/frontend/src/game/fight/choreography/weapons.ts new file mode 100644 index 0000000..035a8b3 --- /dev/null +++ b/frontend/src/game/fight/choreography/weapons.ts @@ -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 { + 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, + } +} diff --git a/frontend/src/game/fight/types.ts b/frontend/src/game/fight/types.ts index 7513be0..29b4593 100644 --- a/frontend/src/game/fight/types.ts +++ b/frontend/src/game/fight/types.ts @@ -17,6 +17,52 @@ export interface FightContext { safeColor: (color: string) => ReturnType } +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 + clearTracked: (id: ReturnType) => 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 + spawnProjectile: (fromX: number, fromY: number, toX: number, toY: number, color: string, size?: number) => Promise + spawnShockwave: (x: number, y: number, color: string) => void + screenFlash: (color: string, duration?: number) => void + impactFreeze: (duration?: number) => Promise + 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 + 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 }