refactor: type Fighter interface for all choreographies

Define Fighter type alias using Kaplay's GameObj with SpriteComp, PosComp,
ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, and ZComp.
Replace 81 choreography function signatures from (atk: any, def: any, ...)
to (atk: Fighter, def: Fighter, ...). Type loadSpriteWithTimeout opts as
LoadSpriteOpt. Type lines/bands arrays as GameObj[]. Type spawnAfterimages
and cameraZoom fighter params. Add as Fighter casts at k.get() call sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 20:30:02 +00:00
co-authored by Claude Opus 4.6
parent 156c727f82
commit 1ca8a6feb0
+137 -134
View File
@@ -1,4 +1,7 @@
import kaplay from 'kaplay' import kaplay from 'kaplay'
import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp, LoadSpriteOpt } from 'kaplay'
type Fighter = GameObj<SpriteComp | PosComp | ScaleComp | AnchorComp | OpacityComp | ColorComp | RotateComp | ZComp>
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 { 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 { import {
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit, sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
@@ -293,7 +296,7 @@ export async function createFightScene(config: FightSceneConfig) {
const isHumanB = botB.archetype === 'human' const isHumanB = botB.archetype === 'human'
// Helper: load sprite with a 5s timeout so mobile never hangs forever // Helper: load sprite with a 5s timeout so mobile never hangs forever
async function loadSpriteWithTimeout(name: string, src: string, opts: any): Promise<void> { async function loadSpriteWithTimeout(name: string, src: string, opts: LoadSpriteOpt): Promise<void> {
try { try {
await Promise.race([ await Promise.race([
k.loadSprite(name, src, opts), k.loadSprite(name, src, opts),
@@ -519,7 +522,7 @@ export async function createFightScene(config: FightSceneConfig) {
// Scanline glitch — horizontal bands flicker // Scanline glitch — horizontal bands flicker
function scanlineGlitch(duration: number = 0.3) { function scanlineGlitch(duration: number = 0.3) {
const lines: any[] = [] const lines: GameObj[] = []
const count = 6 + Math.floor(Math.random() * 8) const count = 6 + Math.floor(Math.random() * 8)
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const ly = Math.random() * H const ly = Math.random() * H
@@ -540,7 +543,7 @@ export async function createFightScene(config: FightSceneConfig) {
// VHS tracking distortion — horizontal offset bands // VHS tracking distortion — horizontal offset bands
function vhsTracking(duration: number = 0.4) { function vhsTracking(duration: number = 0.4) {
const bands: any[] = [] const bands: GameObj[] = []
const bandCount = 3 const bandCount = 3
for (let i = 0; i < bandCount; i++) { for (let i = 0; i < bandCount; i++) {
const by = Math.random() * H const by = Math.random() * H
@@ -576,8 +579,8 @@ export async function createFightScene(config: FightSceneConfig) {
// Schizo cut — rapid zoom/position jitter simulating jump cuts // Schizo cut — rapid zoom/position jitter simulating jump cuts
async function schizoCut() { async function schizoCut() {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const origAX = fA.pos.x, origBX = fB.pos.x const origAX = fA.pos.x, origBX = fB.pos.x
const origAY = fA.pos.y, origBY = fB.pos.y const origAY = fA.pos.y, origBY = fB.pos.y
@@ -598,7 +601,7 @@ export async function createFightScene(config: FightSceneConfig) {
// Hyperspeed lines — converging toward a point // Hyperspeed lines — converging toward a point
function hyperSpeedLines(targetX: number, targetY: number, duration: number = 0.3) { function hyperSpeedLines(targetX: number, targetY: number, duration: number = 0.3) {
const lines: any[] = [] const lines: GameObj[] = []
for (let i = 0; i < 16; i++) { for (let i = 0; i < 16; i++) {
const angle = (i / 16) * Math.PI * 2 const angle = (i / 16) * Math.PI * 2
const dist = 300 + Math.random() * 200 const dist = 300 + Math.random() * 200
@@ -650,7 +653,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// Spawn afterimages: trailing ghost sprites behind a moving fighter // Spawn afterimages: trailing ghost sprites behind a moving fighter
function spawnAfterimages(fighter: any, count: number = 3) { function spawnAfterimages(fighter: Fighter, count: number = 3) {
if (!fighter?.exists()) return if (!fighter?.exists()) return
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const ghost = k.add([ const ghost = k.add([
@@ -667,7 +670,7 @@ export async function createFightScene(config: FightSceneConfig) {
// Camera zoom: scale fighters toward/away from a focal point for impact emphasis // Camera zoom: scale fighters toward/away from a focal point for impact emphasis
async function cameraZoom( async function cameraZoom(
fighters: any[], fighters: Fighter[],
savedScales: { x: number; y: number }[], savedScales: { x: number; y: number }[],
zoomFactor: number, zoomFactor: number,
duration: number, duration: number,
@@ -685,7 +688,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// Grotesque close-up overlays removed — noops retained for call-site compatibility // Grotesque close-up overlays removed — noops retained for call-site compatibility
function spawnGrotesqueDetails(_fighter: any, _scaleFactor: number) {} function spawnGrotesqueDetails(_fighter: Fighter, _scaleFactor: number) {}
function destroyGrotesqueDetails() {} function destroyGrotesqueDetails() {}
// Arena-specific background decoration // Arena-specific background decoration
function drawArenaDecor() { function drawArenaDecor() {
@@ -2038,7 +2041,7 @@ export async function createFightScene(config: FightSceneConfig) {
// === CHOREOGRAPHY FUNCTIONS === // === CHOREOGRAPHY FUNCTIONS ===
// Each returns a Promise and handles all movement + animation // Each returns a Promise and handles all movement + animation
async function dashPunch(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function dashPunch(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 50 const contactX = origDX - dir * 50
// Fast sprint to contact // Fast sprint to contact
await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
@@ -2061,7 +2064,7 @@ export async function createFightScene(config: FightSceneConfig) {
]) ])
} }
async function aerialSlam(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function aerialSlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const origAY = atk.pos.y const origAY = atk.pos.y
const contactX = origDX - dir * 20 const contactX = origDX - dir * 20
// Jump way up // Jump way up
@@ -2090,7 +2093,7 @@ export async function createFightScene(config: FightSceneConfig) {
]) ])
} }
async function flyingKick(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function flyingKick(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const origAY = atk.pos.y const origAY = atk.pos.y
const contactX = origDX - dir * 30 const contactX = origDX - dir * 30
// Small jump + fly horizontally // Small jump + fly horizontally
@@ -2117,7 +2120,7 @@ export async function createFightScene(config: FightSceneConfig) {
]) ])
} }
async function dashThrough(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function dashThrough(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const behindX = origDX + dir * 80 const behindX = origDX + dir * 80
atk.play('attack') atk.play('attack')
sfxSpecial() sfxSpecial()
@@ -2137,7 +2140,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.1) await k.wait(0.1)
} }
async function uppercut(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function uppercut(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 40 const contactX = origDX - dir * 40
const origDY = def.pos.y const origDY = def.pos.y
// Run to contact // Run to contact
@@ -2163,7 +2166,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad) await k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
} }
async function multiHit(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function multiHit(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 45 const contactX = origDX - dir * 45
// Sprint to contact // Sprint to contact
await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
@@ -2192,7 +2195,7 @@ export async function createFightScene(config: FightSceneConfig) {
]) ])
} }
async function projectile(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function projectile(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
await k.wait(0.25) await k.wait(0.25)
@@ -2213,7 +2216,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function jetpackDive(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function jetpackDive(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const origAY = atk.pos.y const origAY = atk.pos.y
sfxJetpack() sfxJetpack()
// Jetpack blast off - fly way up off screen // Jetpack blast off - fly way up off screen
@@ -2250,7 +2253,7 @@ export async function createFightScene(config: FightSceneConfig) {
]) ])
} }
async function gunBurst(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function gunBurst(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
// Spawn visible pixel gun prop // Spawn visible pixel gun prop
const gunX = atk.pos.x + dir * 15 const gunX = atk.pos.x + dir * 15
@@ -2292,7 +2295,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function groundPound(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function groundPound(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const origAY = atk.pos.y const origAY = atk.pos.y
// Jump super high off screen // Jump super high off screen
sfxJetpack() sfxJetpack()
@@ -2332,7 +2335,7 @@ export async function createFightScene(config: FightSceneConfig) {
]) ])
} }
async function teleportStrike(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function teleportStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Fade out // Fade out
sfxSpecial() sfxSpecial()
spawnSparks(atk.pos.x, atk.pos.y - 30, 8, '#b83dff') spawnSparks(atk.pos.x, atk.pos.y - 30, 8, '#b83dff')
@@ -2365,7 +2368,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function fullScreenDash(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function fullScreenDash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Back up to wall // Back up to wall
const wallX = dir === 1 ? 20 : W - 20 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.tween(atk.pos.x, wallX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
@@ -2448,7 +2451,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// CODE GOLF: swing a giant golf club, golf balls fly at opponent // CODE GOLF: swing a giant golf club, golf balls fly at opponent
async function golfClubSmash(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function golfClubSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 60 const contactX = origDX - dir * 60
await k.tween(atk.pos.x, contactX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.15, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
// Spawn golf club (long thin rect) // Spawn golf club (long thin rect)
@@ -2509,7 +2512,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// ROAST BATTLE: fire breath — stream of fire from attacker's face // ROAST BATTLE: fire breath — stream of fire from attacker's face
async function fireBreath(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function fireBreath(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 80 const contactX = origDX - dir * 80
await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('special') atk.play('special')
@@ -2567,7 +2570,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// RIDDLE: question mark barrage rains from the sky // RIDDLE: question mark barrage rains from the sky
async function riddleBarrage(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function riddleBarrage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
sfxZap() sfxZap()
@@ -2612,7 +2615,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// HALLUCINATION CHECK: clone confusion — multiple fake attackers appear // HALLUCINATION CHECK: clone confusion — multiple fake attackers appear
async function cloneStrike(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function cloneStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
sfxZap() sfxZap()
// Spawn 3-5 "ghost" clones that zip around // Spawn 3-5 "ghost" clones that zip around
const cloneCount = isCritical ? 5 : 3 const cloneCount = isCritical ? 5 : 3
@@ -2662,7 +2665,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// TOKEN ECONOMY: coin shower — coins rain down on opponent // TOKEN ECONOMY: coin shower — coins rain down on opponent
async function coinShower(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function coinShower(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
sfxBoing() sfxBoing()
@@ -2698,7 +2701,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// CREATIVE WRITING: giant pen stab + ink splatter // CREATIVE WRITING: giant pen stab + ink splatter
async function penStab(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function penStab(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Spawn a giant pen/quill // Spawn a giant pen/quill
const penX = atk.pos.x + dir * 15 const penX = atk.pos.x + dir * 15
const penY = atk.pos.y - 70 const penY = atk.pos.y - 70
@@ -2757,7 +2760,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// MATH BLITZ: number projectiles fly at opponent // MATH BLITZ: number projectiles fly at opponent
async function mathAttack(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function mathAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
await k.wait(0.15) await k.wait(0.15)
@@ -2798,7 +2801,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// TRAP CARD: card shuriken + trap springs // TRAP CARD: card shuriken + trap springs
async function trapCardAttack(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function trapCardAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
await k.wait(0.1) await k.wait(0.1)
@@ -2863,7 +2866,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// SPEED BLITZ: afterimage dash — leaves ghost copies behind // SPEED BLITZ: afterimage dash — leaves ghost copies behind
async function afterimageDash(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function afterimageDash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
sfxSpecial() sfxSpecial()
const contactX = origDX - dir * 40 const contactX = origDX - dir * 40
// Leave afterimages as we dash // Leave afterimages as we dash
@@ -2911,7 +2914,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// BULLET TIME: slow-mo dodge, afterimages trail behind, golden traces, devastating counter // BULLET TIME: slow-mo dodge, afterimages trail behind, golden traces, devastating counter
async function bulletTimeAttack(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function bulletTimeAttack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Phase 1: Darken screen — entering bullet time // 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)]) 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 }) await k.tween(0, 0.45, 0.15, (v) => { dim.opacity = v })
@@ -3017,7 +3020,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// INVISIBLE SPEED: so fast you can barely see — just blur lines and impact // INVISIBLE SPEED: so fast you can barely see — just blur lines and impact
async function lightningRush(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function lightningRush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
sfxSpecial() sfxSpecial()
// Attacker vanishes // Attacker vanishes
atk.opacity = 0 atk.opacity = 0
@@ -3069,7 +3072,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// ZOOM RUSH: fly toward camera (scale huge) then slam back into opponent // ZOOM RUSH: fly toward camera (scale huge) then slam back into opponent
async function zoomRush(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function zoomRush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const origAY = atk.pos.y const origAY = atk.pos.y
const origScaleX = atk.scale.x const origScaleX = atk.scale.x
const origScaleY = atk.scale.y const origScaleY = atk.scale.y
@@ -3150,7 +3153,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// RAPID FLURRY: DBZ-style machine gun punches — so fast it's a blur // RAPID FLURRY: DBZ-style machine gun punches — so fast it's a blur
async function rapidFlurry(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function rapidFlurry(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 40 const contactX = origDX - dir * 40
// Sprint to contact // Sprint to contact
sfxSpecial() sfxSpecial()
@@ -3210,7 +3213,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// SWORD SLASH: giant energy sword swing with arc trail // SWORD SLASH: giant energy sword swing with arc trail
async function swordSlash(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function swordSlash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 50 const contactX = origDX - dir * 50
await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('special') atk.play('special')
@@ -3245,7 +3248,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// HAMMER SMASH: pull out giant hammer, overhead slam with ground crack // HAMMER SMASH: pull out giant hammer, overhead slam with ground crack
async function hammerSmash(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function hammerSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 40 const contactX = origDX - dir * 40
await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.12, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
// Giant hammer // Giant hammer
@@ -3277,7 +3280,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// LASER BEAM: charge up then fire continuous beam across screen // LASER BEAM: charge up then fire continuous beam across screen
async function laserBeam(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function laserBeam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
// Charge up (growing ball of energy) // Charge up (growing ball of energy)
@@ -3308,7 +3311,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// ROCKET LAUNCHER: fire a big slow rocket that explodes on impact // ROCKET LAUNCHER: fire a big slow rocket that explodes on impact
async function rocketLauncher(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function rocketLauncher(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
await k.wait(0.15) await k.wait(0.15)
@@ -3352,7 +3355,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// BOMB THROW: lob an arcing bomb that bounces once then detonates // BOMB THROW: lob an arcing bomb that bounces once then detonates
async function bombThrow(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function bombThrow(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack') atk.play('attack')
sfxSpecial() sfxSpecial()
const bombX = atk.pos.x + dir * 20 const bombX = atk.pos.x + dir * 20
@@ -3387,7 +3390,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// MINIGUN SPRAY: sustained automatic fire with shell casings // MINIGUN SPRAY: sustained automatic fire with shell casings
async function minigunSpray(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function minigunSpray(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
// Spawn visible pixel minigun // Spawn visible pixel minigun
const gpx = atk.pos.x + dir * 15 const gpx = atk.pos.x + dir * 15
@@ -3433,7 +3436,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// SNIPER SHOT: long pause, laser sight, then one devastating hit // SNIPER SHOT: long pause, laser sight, then one devastating hit
async function sniperShot(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function sniperShot(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Back up to wall // Back up to wall
const wallX = dir === 1 ? 30 : W - 30 const wallX = dir === 1 ? 30 : W - 30
await k.tween(atk.pos.x, wallX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, wallX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
@@ -3471,7 +3474,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// WHIP CRACK: ranged whip attack with crack sound // WHIP CRACK: ranged whip attack with crack sound
async function whipCrack(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function whipCrack(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('attack') atk.play('attack')
sfxSpecial() sfxSpecial()
await k.wait(0.1) await k.wait(0.1)
@@ -3514,7 +3517,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// KATANA COMBO: triple slash with cross patterns // KATANA COMBO: triple slash with cross patterns
async function katanaCombo(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function katanaCombo(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 45 const contactX = origDX - dir * 45
sfxSpecial() sfxSpecial()
await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
@@ -3552,7 +3555,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// CHAINSAW: charge with buzzing chainsaw, multi-hit grind // CHAINSAW: charge with buzzing chainsaw, multi-hit grind
async function chainsawRev(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function chainsawRev(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 35 const contactX = origDX - dir * 35
atk.play('special') atk.play('special')
// Chainsaw buzzing (visual: vibrating rect) // Chainsaw buzzing (visual: vibrating rect)
@@ -3595,7 +3598,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// MOTORBIKE CHARGE: ride a motorbike across the screen and slam into defender // MOTORBIKE CHARGE: ride a motorbike across the screen and slam into defender
async function motorbikeCharge(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function motorbikeCharge(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Back up to edge // Back up to edge
const startX = dir === 1 ? -30 : W + 30 const startX = dir === 1 ? -30 : W + 30
atk.opacity = 0 atk.opacity = 0
@@ -3643,7 +3646,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// CAR SMASH: drive a car from off-screen, defender gets sent flying // CAR SMASH: drive a car from off-screen, defender gets sent flying
async function carSmash(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function carSmash(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.opacity = 0 atk.opacity = 0
const carY = GROUND_Y - 20 const carY = GROUND_Y - 20
const startX = dir === 1 ? -60 : W + 60 const startX = dir === 1 ? -60 : W + 60
@@ -3688,7 +3691,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// BOAT CANNON: a boat slides across the ground and fires cannons // BOAT CANNON: a boat slides across the ground and fires cannons
async function boatCannon(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function boatCannon(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
// Build boat // Build boat
@@ -3731,7 +3734,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// ANVIL DROP: classic cartoon — anvil falls from sky onto opponent // ANVIL DROP: classic cartoon — anvil falls from sky onto opponent
async function anvilDrop(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function anvilDrop(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
await k.wait(0.15) await k.wait(0.15)
@@ -3766,7 +3769,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// POCKET CANNON: reach into pocket, pull out comically oversized gun, fire // POCKET CANNON: reach into pocket, pull out comically oversized gun, fire
async function pocketCannon(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function pocketCannon(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// "Reach into pocket" — arm down motion // "Reach into pocket" — arm down motion
atk.play('special') atk.play('special')
await k.wait(0.12) await k.wait(0.12)
@@ -3866,7 +3869,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// GRAPPLE FLURRY: both bots close in and trade sustained blows without splitting apart // GRAPPLE FLURRY: both bots close in and trade sustained blows without splitting apart
async function grappleFlurry(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function grappleFlurry(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Both rush to center — meet in the middle // Both rush to center — meet in the middle
const meetX = (origAX + origDX) / 2 const meetX = (origAX + origDX) / 2
const atkMeetX = meetX - dir * 25 const atkMeetX = meetX - dir * 25
@@ -3928,7 +3931,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// BODY SLAM: wrestling-style grab, lift, and slam // BODY SLAM: wrestling-style grab, lift, and slam
async function bodySlam(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function bodySlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Rush in close // Rush in close
sfxSpecial() sfxSpecial()
const contactX = def.pos.x - dir * 30 const contactX = def.pos.x - dir * 30
@@ -3978,7 +3981,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// PINBALL COMBO: bounce the opponent between walls/edges like a pinball // PINBALL COMBO: bounce the opponent between walls/edges like a pinball
async function pinballCombo(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function pinballCombo(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Initial launch punch // Initial launch punch
atk.play('attack') atk.play('attack')
sfxPunch() sfxPunch()
@@ -4026,7 +4029,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// SUPLEX: grab from behind, lift, and drive headfirst into ground // SUPLEX: grab from behind, lift, and drive headfirst into ground
async function suplex(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function suplex(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Dash behind defender // Dash behind defender
sfxSpecial() sfxSpecial()
const behindX = def.pos.x + dir * 30 const behindX = def.pos.x + dir * 30
@@ -4379,7 +4382,7 @@ export async function createFightScene(config: FightSceneConfig) {
// === DEFENSE / SHIELD SYSTEM === // === DEFENSE / SHIELD SYSTEM ===
async function playBlock(side: 'a' | 'b') { async function playBlock(side: 'a' | 'b') {
const defender = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] const defender = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!defender) return if (!defender) return
const dir = side === 'a' ? 1 : -1 const dir = side === 'a' ? 1 : -1
// Spawn shield in front // Spawn shield in front
@@ -5246,7 +5249,7 @@ export async function createFightScene(config: FightSceneConfig) {
// ============================================================ // ============================================================
// --- TIER 2+ ULTIMATES (Silver) — flashy multi-hits --- // --- TIER 2+ ULTIMATES (Silver) — flashy multi-hits ---
async function ultimateRapidBarrage(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Rush in and land 8 alternating punches + kicks with screen shake
const contactX = origDX - dir * 45 const contactX = origDX - dir * 45
sfxZoomWhoosh() sfxZoomWhoosh()
@@ -5275,7 +5278,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateOrbitalStrike(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Call down 5 orbital beams on the defender's position
atk.play('special'); sfxSpecial() atk.play('special'); sfxSpecial()
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
@@ -5291,7 +5294,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.3); atk.play('idle') await k.wait(0.3); atk.play('idle')
} }
async function ultimateCloneSurround(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateCloneSurround(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Spawn 4 clones around the defender, all attack simultaneously // Spawn 4 clones around the defender, all attack simultaneously
const clones: any[] = [] const clones: any[] = []
const positions = [ const positions = [
@@ -5316,7 +5319,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.3) await k.wait(0.3)
} }
async function ultimateGatlingFury(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Pull out massive gatling gun, fire 12 shots with screen shake
atk.play('special') atk.play('special')
const gunLen = 60 const gunLen = 60
@@ -5343,7 +5346,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.3); atk.play('idle') await k.wait(0.3); atk.play('idle')
} }
async function ultimateEarthquake(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateEarthquake(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Jump high, slam ground, cracks + shockwaves across screen // Jump high, slam ground, cracks + shockwaves across screen
sfxZoomWhoosh() sfxZoomWhoosh()
await k.tween(atk.pos.y, GROUND_Y - 150, 0.2, (v) => { atk.pos.y = v }, k.easings.easeOutQuad) await k.tween(atk.pos.y, GROUND_Y - 150, 0.2, (v) => { atk.pos.y = v }, k.easings.easeOutQuad)
@@ -5373,7 +5376,7 @@ export async function createFightScene(config: FightSceneConfig) {
const ultimateMeteorShower = makeSwarm('#ff6600', 8, 10, 500, true) const ultimateMeteorShower = makeSwarm('#ff6600', 8, 10, 500, true)
// --- TIER 3+ ULTIMATES (Gold) — multi-phase, full screen --- // --- TIER 3+ ULTIMATES (Gold) — multi-phase, full screen ---
async function ultimateScreenNuke(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Charge up glowing orb, launch it, entire screen whites out
atk.play('special'); sfxSpecial() 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)]) 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)])
@@ -5398,7 +5401,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.4); atk.play('idle') await k.wait(0.4); atk.play('idle')
} }
async function ultimateBlackHoleVortex(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Summon black hole that pulls defender in, crushes, and ejects
atk.play('special'); sfxZap() 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 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)])
@@ -5429,7 +5432,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.2); atk.play('idle') await k.wait(0.2); atk.play('idle')
} }
async function ultimateDimensionalRift(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateDimensionalRift(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Open rift, attacker disappears, attacks from multiple portals // Open rift, attacker disappears, attacks from multiple portals
sfxZap(); atk.opacity = 0 sfxZap(); atk.opacity = 0
const colors = ['#ff2d7b', '#00f0ff', '#39ff14', '#b83dff'] const colors = ['#ff2d7b', '#00f0ff', '#39ff14', '#b83dff']
@@ -5460,7 +5463,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateTimeSlow(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Everything goes slow-mo, attacker lands 6 devastating hits in "frozen" time
scanlineGlitch(0.1) scanlineGlitch(0.1)
// Dim screen // Dim screen
@@ -5486,7 +5489,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateSummonArmy(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Summon an army of 8 mini clones that charge across the screen
atk.play('special'); sfxSpecial() atk.play('special'); sfxSpecial()
const army: any[] = [] const army: any[] = []
@@ -5516,7 +5519,7 @@ export async function createFightScene(config: FightSceneConfig) {
const ultimateAvalancheDrop = makeExplosion(30, 30, '#aaeeff', 0.2, '#ffffff', true) 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 // ULTIMATE BULLET TIME BARRAGE — full slow-mo cinematic: dodge rain of projectiles, afterimage rush, multi-hit counter
async function ultimateBulletTimeBarrage(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateBulletTimeBarrage(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Phase 1: World goes dark, heavy slow-mo overlay // 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)]) 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 }) await k.tween(0, 0.55, 0.2, (v) => { dim.opacity = v })
@@ -5620,7 +5623,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// --- TIER 4+ ULTIMATES (Platinum) — full screen, multi-phase spectacles --- // --- TIER 4+ ULTIMATES (Platinum) — full screen, multi-phase spectacles ---
async function ultimateFinaleRush(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Full-screen dash through, reverse, dash again, then combo finish
sfxZoomWhoosh() sfxZoomWhoosh()
// First pass // First pass
@@ -5649,7 +5652,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateMeteorCrash(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Attacker launches into sky, becomes a meteor, crashes into arena
sfxZoomWhoosh(); atk.play('special') sfxZoomWhoosh(); atk.play('special')
await k.tween(atk.pos.y, -100, 0.2, (v) => { atk.pos.y = v }, k.easings.easeInQuad) await k.tween(atk.pos.y, -100, 0.2, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
@@ -5682,7 +5685,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateScreenShatter(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateScreenShatter(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Hit so hard the "screen" shatters with fragment overlay // Hit so hard the "screen" shatters with fragment overlay
const contactX = origDX - dir * 45 const contactX = origDX - dir * 45
await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.1, (v) => { atk.pos.x = v }, k.easings.easeOutQuad)
@@ -5709,7 +5712,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateGravityReverse(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Flip gravity — defender floats up, gets juggled, slammed back down
atk.play('special'); sfxZap() atk.play('special'); sfxZap()
// Defender floats up // Defender floats up
@@ -5733,7 +5736,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.3); atk.play('idle') await k.wait(0.3); atk.play('idle')
} }
async function ultimateInfiniteCombo(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateInfiniteCombo(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// 15-hit combo with escalating screen effects // 15-hit combo with escalating screen effects
const contactX = origDX - dir * 40 const contactX = origDX - dir * 40
sfxZoomWhoosh() sfxZoomWhoosh()
@@ -5769,7 +5772,7 @@ export async function createFightScene(config: FightSceneConfig) {
const ultimateRocketBarrage = makeSwarm('#ff6600', 10, 14, 550, false) const ultimateRocketBarrage = makeSwarm('#ff6600', 10, 14, 550, false)
// --- TIER 5+ ULTIMATES (Diamond/Legend) — absolutely insane full-screen --- // --- TIER 5+ ULTIMATES (Diamond/Legend) — absolutely insane full-screen ---
async function ultimateArmageddon(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // 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)]) 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 }) await k.tween(0, 0.7, 0.3, (v) => { darkness.opacity = v })
@@ -5797,7 +5800,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.5); atk.play('idle') await k.wait(0.5); atk.play('idle')
} }
async function ultimateOmegaBeam(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Charge up, fire a massive full-width beam that covers half the screen
atk.play('special'); sfxSpecial() atk.play('special'); sfxSpecial()
// Charge particles converging on attacker // Charge particles converging on attacker
@@ -5829,7 +5832,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.2); atk.play('idle') await k.wait(0.2); atk.play('idle')
} }
async function ultimateBeyondTheScreen(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Attacker punches defender so hard they "break" off-screen, then gravity brings them back
const contactX = origDX - dir * 45 const contactX = origDX - dir * 45
sfxZoomWhoosh() sfxZoomWhoosh()
@@ -5860,7 +5863,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateMatrixDodge(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // Matrix-style slow-mo: attacker fires 3 projectiles, "dodges" in slo-mo, then counters
atk.play('special') atk.play('special')
// Fire 3 slow projectiles // Fire 3 slow projectiles
@@ -5891,7 +5894,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function ultimateWorldEnder(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { 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 // The ultimate ultimate: 3-phase attack — air combo, ground slam, finishing beam
// Phase 1: Launch defender into air // Phase 1: Launch defender into air
const contactX = origDX - dir * 45 const contactX = origDX - dir * 45
@@ -5945,7 +5948,7 @@ export async function createFightScene(config: FightSceneConfig) {
// THE CREATOR — Exclusive choreographies (bitcoin-themed) // THE CREATOR — Exclusive choreographies (bitcoin-themed)
// ═══════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════
async function bitcoinRain(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function bitcoinRain(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
await k.wait(0.2) await k.wait(0.2)
@@ -5987,7 +5990,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function lightningInvoice(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function lightningInvoice(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxZap() sfxZap()
await k.wait(0.15) await k.wait(0.15)
@@ -6031,7 +6034,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function satoshiStrike(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function satoshiStrike(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
await k.wait(0.1) await k.wait(0.1)
@@ -6069,7 +6072,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function hashRateOverload(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function hashRateOverload(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
// Screen fills with scrolling hash text (simulated with falling green/gold rects) // Screen fills with scrolling hash text (simulated with falling green/gold rects)
@@ -6105,7 +6108,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function blockchainSlam(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function blockchainSlam(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 60 const contactX = origDX - dir * 60
sfxZoomWhoosh() sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.12, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad) await k.tween(atk.pos.x, contactX, 0.12, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
@@ -6144,7 +6147,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function creatorMode(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function creatorMode(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
// Typing effect — rapid small flashes from attacker // Typing effect — rapid small flashes from attacker
@@ -6181,7 +6184,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function morphStrike(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { 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 // Flash golden — the morph is handled by the morph system
// This move itself is a quick dash-hit with gold sparks // This move itself is a quick dash-hit with gold sparks
screenFlash('#ffd700', 0.08) screenFlash('#ffd700', 0.08)
@@ -6205,7 +6208,7 @@ export async function createFightScene(config: FightSceneConfig) {
atk.play('idle') atk.play('idle')
} }
async function divineIntervention(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { async function divineIntervention(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Ascend off screen // Ascend off screen
sfxZoomWhoosh() sfxZoomWhoosh()
atk.play('win') atk.play('win')
@@ -6250,7 +6253,7 @@ export async function createFightScene(config: FightSceneConfig) {
// ── Creator ₿ Swarm Variations ── // ── Creator ₿ Swarm Variations ──
async function bitcoinSwarm(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { 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 // Swarm of ₿ letters fly from creator toward defender with zigzag paths
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
@@ -6300,7 +6303,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function satoshiTornado(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { 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 // ₿ letters spiral around the defender in a tightening vortex then explode
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
@@ -6360,7 +6363,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function hodlWave(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { 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 // Wall of ₿ letters advancing across the screen like a tidal wave
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
@@ -6414,7 +6417,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function bitcoinBarrage(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { 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 // Rapid-fire ₿ letters shot from the creator's laptop like bullets
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
@@ -6462,7 +6465,7 @@ export async function createFightScene(config: FightSceneConfig) {
// ── Creator Ultimates ── // ── Creator Ultimates ──
async function ultimateGenesisBlock(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateGenesisBlock(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Screen goes dark, massive genesis block materializes and detonates // Screen goes dark, massive genesis block materializes and detonates
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
@@ -6495,7 +6498,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function ultimateTheHalving(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateTheHalving(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Screen splits horizontally with a golden line // Screen splits horizontally with a golden line
atk.play('special') atk.play('special')
sfxZap() sfxZap()
@@ -6528,7 +6531,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function ultimateFullNodeCrush(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateFullNodeCrush(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Giant server rack drops from above // Giant server rack drops from above
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
@@ -6567,7 +6570,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
} }
async function ultimateSatoshiVision(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { async function ultimateSatoshiVision(atk: Fighter, def: Fighter, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Everything turns gold — defender trapped in bitcoin block // Everything turns gold — defender trapped in bitcoin block
atk.play('special') atk.play('special')
sfxSpecial() sfxSpecial()
@@ -7179,7 +7182,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
function showSpeechBubble(side: 'a' | 'b', text: string, duration: number = 3) { function showSpeechBubble(side: 'a' | 'b', text: string, duration: number = 3) {
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!fighter || !text) return if (!fighter || !text) return
// Kill previous bubble on this side // Kill previous bubble on this side
@@ -7332,7 +7335,7 @@ export async function createFightScene(config: FightSceneConfig) {
function startTalking(side: 'a' | 'b') { function startTalking(side: 'a' | 'b') {
stopTalking(side) // cleanup any existing stopTalking(side) // cleanup any existing
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!fighter?.exists()) return if (!fighter?.exists()) return
const mouthColor = side === 'a' ? '#00f0ff' : '#ff2d7b' const mouthColor = side === 'a' ? '#00f0ff' : '#ff2d7b'
@@ -7510,7 +7513,7 @@ export async function createFightScene(config: FightSceneConfig) {
async function humanJumpsIn(side: 'a' | 'b') { async function humanJumpsIn(side: 'a' | 'b') {
const fighterTag = side === 'a' ? 'fighterA' : 'fighterB' const fighterTag = side === 'a' ? 'fighterA' : 'fighterB'
const spriteId = side === 'a' ? 'humanA' : 'humanB' const spriteId = side === 'a' ? 'humanA' : 'humanB'
const fighter = k.get(fighterTag)[0] const fighter = k.get(fighterTag)[0] as Fighter
if (!fighter) return if (!fighter) return
// Spawn human at fighter position // Spawn human at fighter position
@@ -8628,8 +8631,8 @@ export async function createFightScene(config: FightSceneConfig) {
}, },
async playEntrance() { async playEntrance() {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const entrances = [ const entrances = [
@@ -9170,8 +9173,8 @@ export async function createFightScene(config: FightSceneConfig) {
// Helper: reset both fighters to home positions // Helper: reset both fighters to home positions
async _resetPositions() { async _resetPositions() {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
await Promise.all([ await Promise.all([
k.tween(fA.pos.x, HOME_A, 0.2, (v) => { fA.pos.x = v }, k.easings.easeOutQuad), k.tween(fA.pos.x, HOME_A, 0.2, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
@@ -9185,8 +9188,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 0: RAPID BRAWL — close distance, trade rapid blows // 0: RAPID BRAWL — close distance, trade rapid blows
async _brawlRapid(winningSide: 'a' | 'b' | null, intensity: number) { async _brawlRapid(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const midX = W / 2 const midX = W / 2
sfxZoomWhoosh() sfxZoomWhoosh()
@@ -9211,8 +9214,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 1: KNOCKDOWN BRAWL — combo ends with loser knocked flat, has to get back up // 1: KNOCKDOWN BRAWL — combo ends with loser knocked flat, has to get back up
async _brawlKnockdown(winningSide: 'a' | 'b' | null, intensity: number) { async _brawlKnockdown(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB) const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA const loser = winner === fA ? fB : fA
@@ -9261,8 +9264,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 2: WALL BOUNCE — punch sends fighter flying into screen edge, bounces off // 2: WALL BOUNCE — punch sends fighter flying into screen edge, bounces off
async _brawlWallBounce(winningSide: 'a' | 'b' | null, intensity: number) { async _brawlWallBounce(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB) const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA const loser = winner === fA ? fB : fA
@@ -9320,8 +9323,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 3: DIZZY STAGGER — sustained combo leaves loser wobbling around the screen // 3: DIZZY STAGGER — sustained combo leaves loser wobbling around the screen
async _brawlDizzyStagger(winningSide: 'a' | 'b' | null, intensity: number) { async _brawlDizzyStagger(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB) const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA const loser = winner === fA ? fB : fA
@@ -9372,8 +9375,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 4: SUPLEX SLAM — grab, lift overhead, slam to ground with bounce // 4: SUPLEX SLAM — grab, lift overhead, slam to ground with bounce
async _brawlSuplex(attackerSide: 'a' | 'b', intensity: number) { async _brawlSuplex(attackerSide: 'a' | 'b', intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1 const dir = attackerSide === 'a' ? 1 : -1
@@ -9423,8 +9426,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 5: PING PONG VOLLEY — fighters knock each other back and forth across the screen // 5: PING PONG VOLLEY — fighters knock each other back and forth across the screen
async _brawlPingPong(winningSide: 'a' | 'b' | null, intensity: number) { async _brawlPingPong(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const volleys = 3 + Math.floor(intensity * 3) + Math.floor(Math.random() * 2) // 3-8 const volleys = 3 + Math.floor(intensity * 3) + Math.floor(Math.random() * 2) // 3-8
@@ -9471,8 +9474,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 6: GROUND AND POUND — attacker pins defender, hits them on the ground // 6: GROUND AND POUND — attacker pins defender, hits them on the ground
async _brawlGroundPound(attackerSide: 'a' | 'b', intensity: number) { async _brawlGroundPound(attackerSide: 'a' | 'b', intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1 const dir = attackerSide === 'a' ? 1 : -1
@@ -9515,8 +9518,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 7: HAYMAKER — slow dramatic wind-up, pause, devastating single hit with knockdown // 7: HAYMAKER — slow dramatic wind-up, pause, devastating single hit with knockdown
async _brawlHaymaker(attackerSide: 'a' | 'b', _intensity: number) { async _brawlHaymaker(attackerSide: 'a' | 'b', _intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1 const dir = attackerSide === 'a' ? 1 : -1
@@ -9563,8 +9566,8 @@ export async function createFightScene(config: FightSceneConfig) {
// 8: BODY CHECK BOUNCER — both charge, collide, one bounces off the other // 8: BODY CHECK BOUNCER — both charge, collide, one bounces off the other
async _brawlBodyCheck(winningSide: 'a' | 'b' | null, _intensity: number) { async _brawlBodyCheck(winningSide: 'a' | 'b' | null, _intensity: number) {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB) const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA const loser = winner === fA ? fB : fA
@@ -9625,8 +9628,8 @@ export async function createFightScene(config: FightSceneConfig) {
// Clinch combo: grapple at close range, one fighter dominates with sustained contact // Clinch combo: grapple at close range, one fighter dominates with sustained contact
async _clinchCombo(attackerSide: 'a' | 'b', intensity: number) { async _clinchCombo(attackerSide: 'a' | 'b', intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1 const dir = attackerSide === 'a' ? 1 : -1
const origAX = attackerSide === 'a' ? HOME_A : HOME_B const origAX = attackerSide === 'a' ? HOME_A : HOME_B
@@ -9664,8 +9667,8 @@ export async function createFightScene(config: FightSceneConfig) {
// Counter-attack: after a choreography hit, the defender retaliates briefly // Counter-attack: after a choreography hit, the defender retaliates briefly
async _counterAttack(defenderSide: 'a' | 'b') { async _counterAttack(defenderSide: 'a' | 'b') {
const defender = k.get(defenderSide === 'a' ? 'fighterA' : 'fighterB')[0] const defender = k.get(defenderSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const attacker = k.get(defenderSide === 'a' ? 'fighterB' : 'fighterA')[0] const attacker = k.get(defenderSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!defender || !attacker) return if (!defender || !attacker) return
const dir = defenderSide === 'a' ? 1 : -1 const dir = defenderSide === 'a' ? 1 : -1
const origDX = defenderSide === 'a' ? HOME_A : HOME_B const origDX = defenderSide === 'a' ? HOME_A : HOME_B
@@ -9683,8 +9686,8 @@ export async function createFightScene(config: FightSceneConfig) {
}, },
async playAttack(side: 'a' | 'b', choreographyName: string, isCritical: boolean) { async playAttack(side: 'a' | 'b', choreographyName: string, isCritical: boolean) {
const attacker = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] const attacker = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(side === 'a' ? 'fighterB' : 'fighterA')[0] const defender = k.get(side === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return if (!attacker || !defender) return
const origAX = side === 'a' ? HOME_A : HOME_B const origAX = side === 'a' ? HOME_A : HOME_B
@@ -9705,8 +9708,8 @@ export async function createFightScene(config: FightSceneConfig) {
}, },
async playRound(event: RoundEvent) { async playRound(event: RoundEvent) {
const fA = k.get('fighterA')[0] const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) { await k.wait(0.5); return } // Sprites missing (mobile load failure) — skip animation if (!fA || !fB) { await k.wait(0.5); return } // Sprites missing (mobile load failure) — skip animation
const aWon = event.winnerId === event.botAId const aWon = event.winnerId === event.botAId
@@ -9932,7 +9935,7 @@ export async function createFightScene(config: FightSceneConfig) {
: (isUltimate || (exchangeCritical && intensity > 0.7 && Math.random() < 0.4)) : (isUltimate || (exchangeCritical && intensity > 0.7 && Math.random() < 0.4))
let morphRevert: (() => Promise<void>) | null = null let morphRevert: (() => Promise<void>) | null = null
if (shouldMorph) { if (shouldMorph) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (attacker) { if (attacker) {
if (isCreatorFighter) { if (isCreatorFighter) {
// Omni-morph: creator transforms into a random archetype (sprite swap) // Omni-morph: creator transforms into a random archetype (sprite swap)
@@ -10010,7 +10013,7 @@ export async function createFightScene(config: FightSceneConfig) {
} }
// RGB glitch + hyperspeed lines on critical final blow // RGB glitch + hyperspeed lines on critical final blow
glitchRGB(0.3) glitchRGB(0.3)
const defPos = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] const defPos = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (defPos) hyperSpeedLines(defPos.pos.x, defPos.pos.y - 30, 0.4) if (defPos) hyperSpeedLines(defPos.pos.x, defPos.pos.y - 30, 0.4)
} }
@@ -10246,8 +10249,8 @@ export async function createFightScene(config: FightSceneConfig) {
// Respect nod between fighters on close rounds (10% when margin <= 1) // Respect nod between fighters on close rounds (10% when margin <= 1)
if (margin <= 1 && Math.random() < 0.1) { if (margin <= 1 && Math.random() < 0.1) {
const fA2 = k.get('fighterA')[0] const fA2 = k.get('fighterA')[0] as Fighter
const fB2 = k.get('fighterB')[0] const fB2 = k.get('fighterB')[0] as Fighter
if (fA2 && fB2) { if (fA2 && fB2) {
spawnEmoteText(fA2.pos.x, fA2.pos.y - 45, respectLines[Math.floor(Math.random() * respectLines.length)], '#88ccff') spawnEmoteText(fA2.pos.x, fA2.pos.y - 45, respectLines[Math.floor(Math.random() * respectLines.length)], '#88ccff')
await k.wait(0.3) await k.wait(0.3)
@@ -10265,7 +10268,7 @@ export async function createFightScene(config: FightSceneConfig) {
}, },
async playTaunt(side: 'a' | 'b') { async playTaunt(side: 'a' | 'b') {
const taunter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] const taunter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!taunter) return if (!taunter) return
// Creator taunts come with menacing voice lines // Creator taunts come with menacing voice lines
const taunterArch = side === 'a' ? botA.archetype : botB.archetype const taunterArch = side === 'a' ? botA.archetype : botB.archetype
@@ -10307,7 +10310,7 @@ export async function createFightScene(config: FightSceneConfig) {
}, },
async playDodge(side: 'a' | 'b') { async playDodge(side: 'a' | 'b') {
const dodger = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] const dodger = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!dodger) return if (!dodger) return
const origX = side === 'a' ? HOME_A : HOME_B const origX = side === 'a' ? HOME_A : HOME_B
const origY = dodger.pos.y const origY = dodger.pos.y
@@ -10324,8 +10327,8 @@ export async function createFightScene(config: FightSceneConfig) {
}, },
async playKO(winningSide: 'a' | 'b', winnerName: string) { async playKO(winningSide: 'a' | 'b', winnerName: string) {
const loser = k.get(winningSide === 'a' ? 'fighterB' : 'fighterA')[0] const loser = k.get(winningSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
const winner = k.get(winningSide === 'a' ? 'fighterA' : 'fighterB')[0] const winner = k.get(winningSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!loser || !winner) return if (!loser || !winner) return
const dir = winningSide === 'a' ? 1 : -1 const dir = winningSide === 'a' ? 1 : -1
@@ -11183,8 +11186,8 @@ export async function createFightScene(config: FightSceneConfig) {
}, },
async playPerfect(winningSide: 'a' | 'b', winnerName: string) { async playPerfect(winningSide: 'a' | 'b', winnerName: string) {
const winner = k.get(winningSide === 'a' ? 'fighterA' : 'fighterB')[0] const winner = k.get(winningSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const loser = k.get(winningSide === 'a' ? 'fighterB' : 'fighterA')[0] const loser = k.get(winningSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!loser || !winner) return if (!loser || !winner) return
// Judge goes wild for a perfect // Judge goes wild for a perfect