Files
botfights/frontend/src/game/FightScene.ts
T
DorianandClaude Opus 4.6 1ca8a6feb0 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>
2026-03-08 20:30:02 +00:00

11317 lines
528 KiB
TypeScript

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 {
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect,
sfxRoundStart, sfxBlock, sfxDodge, sfxClash, sfxRandomSilly, sfxBoing,
sfxBonk, sfxZap, sfxSlideDown, sfxSlideUp, sfxZoomWhoosh, sfxRapidPunch,
sfxSplat, sfxWomp, sfxPowerUp, sfxCoin, sfxFail, sfxDrumRoll,
fanfareRound, fanfareFight, fanfareDevastating, fanfareCritical, fanfareCombo,
startMusic, stopMusic, announce,
announceRandom, announceDramatic, announceHype, announceSilly, announceCool,
announceRobot, announceScream, announceFast,
announceFinishHim, announceFatality, announceFlawlessVictory,
announceRandomHype, announceDeepIntro, announceRoundHype, announceCrowdReaction,
announceCreatorEntrance, announceCreatorRound, announceCreatorKO,
announceCreatorWin, announceCreatorLose, announceCreatorMorph,
announceCreatorCameo, announceCreatorTaunt, announceCreatorDevastating,
sfxRandomComedy, sfxRandomFail,
sfxVineBoom, sfxAirHorn, sfxBruh, sfxFart, sfxRecordScratch,
sfxRubberChicken, sfxSqueakyToy, sfxWetSlap, sfxBoneCrack, sfxCartoonRun,
sfxRimShot, sfxSlideWhistleUp, sfxSlideWhistleDown, sfxYippee, sfxDing,
sfxTacoBellBong, sfxWindowsError, sfxMemeThud,
sfxSadTrombone, sfxFailHorn, sfxPriceIsRightFail, sfxBuzzer,
sfxMissionFailed, sfxCrickets, sfxSadViolin, sfxEmotionalDamage, sfxDunDunDun,
} from './sounds'
export interface FightSceneConfig {
canvas: HTMLCanvasElement
botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
botB: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
arena: string
onReady?: () => void
}
export interface RoundEvent {
round: number
challengeType: string
winnerId: string | null
botAId: string
botBId: string
narration: string
isCritical: boolean
botAScore: number
botBScore: number
botAResponse?: string
botBResponse?: string
}
const ARENA_THEMES: Record<string, { bg: string; ground: string; accent: string }> = {
datacenter: { bg: '#0a0a1a', ground: '#1a1a3a', accent: '#00f0ff' },
stackoverflow_ruins: { bg: '#1a0f00', ground: '#2a1f10', accent: '#f48024' },
gpu_graveyard: { bg: '#0a0a0a', ground: '#1a1a1a', accent: '#76b900' },
prompt_dungeon: { bg: '#0f0a1a', ground: '#1f1a2a', accent: '#b83dff' },
silicon_valley_dojo: { bg: '#0a1a0a', ground: '#1a2a1a', accent: '#00ff41' },
paper_mill: { bg: '#1a1a10', ground: '#2a2a20', accent: '#f0e68c' },
localhost: { bg: '#000000', ground: '#111111', accent: '#00ff41' },
the_cloud: { bg: '#0a0f1a', ground: '#1a1f2a', accent: '#4488ff' },
hacker_news: { bg: '#1a0f00', ground: '#2a1f10', accent: '#ff6600' },
the_singularity: { bg: '#1a0020', ground: '#2a0030', accent: '#ff00ff' },
beach: { bg: '#0a2040', ground: '#c2a060', accent: '#ffcc00' },
desert: { bg: '#1a1000', ground: '#a08040', accent: '#ff8800' },
forest: { bg: '#051005', ground: '#1a3010', accent: '#44cc22' },
jungle: { bg: '#081808', ground: '#1a3818', accent: '#33ff66' },
outer_space: { bg: '#000008', ground: '#0a0a20', accent: '#8888ff' },
colosseum: { bg: '#1a0f08', ground: '#3a2a18', accent: '#d4a030' },
kitchen_stadium: { bg: '#1a0a0a', ground: '#2a1a10', accent: '#ff4400' },
haunted_mansion: { bg: '#0a0510', ground: '#1a1020', accent: '#8844cc' },
meme_dimension: { bg: '#1a0820', ground: '#2a1830', accent: '#ff44ff' },
space_station: { bg: '#000010', ground: '#0a0a28', accent: '#44aaff' },
junkyard: { bg: '#0f0f0a', ground: '#2a2a1a', accent: '#aa8833' },
concert_hall: { bg: '#10001a', ground: '#20102a', accent: '#ff22aa' },
savanna: { bg: '#1a1808', ground: '#8a7830', accent: '#dd9922' },
server_room: { bg: '#000a05', ground: '#0a1a10', accent: '#00ff88' },
sports_arena: { bg: '#0a0a1a', ground: '#1a2a4a', accent: '#ff3333' },
}
const spriteAnims = {
idle: { from: 0, to: ANIMATIONS.idle.frames - 1, loop: true, speed: 6 },
attack: { from: MAX_FRAMES, to: MAX_FRAMES + ANIMATIONS.attack.frames - 1, loop: false, speed: 12 },
kick: { from: MAX_FRAMES * 2, to: MAX_FRAMES * 2 + ANIMATIONS.kick.frames - 1, loop: false, speed: 10 },
special: { from: MAX_FRAMES * 3, to: MAX_FRAMES * 3 + ANIMATIONS.special.frames - 1, loop: false, speed: 8 },
hit: { from: MAX_FRAMES * 4, to: MAX_FRAMES * 4 + ANIMATIONS.hit.frames - 1, loop: false, speed: 8 },
knockback: { from: MAX_FRAMES * 5, to: MAX_FRAMES * 5 + ANIMATIONS.knockback.frames - 1, loop: false, speed: 8 },
ko: { from: MAX_FRAMES * 6, to: MAX_FRAMES * 6 + ANIMATIONS.ko.frames - 1, loop: false, speed: 6 },
win: { from: MAX_FRAMES * 7, to: MAX_FRAMES * 7 + ANIMATIONS.win.frames - 1, loop: true, speed: 6 },
}
// Challenge type -> themed choreography (shown ~60% of the time) + generic fallbacks
const CHALLENGE_THEMED: Record<string, { themed: string[]; generic: string[] }> = {
speed_blitz: { themed: ['afterimageDash', 'lightningRush', 'rapidFlurry', 'tornadoSpin', 'corkscrewDive', 'warpDrive', 'backflipKick', 'bulletTimeAttack'], generic: ['dashPunch', 'multiHit', 'fullScreenDash', 'dashThrough', 'zoomRush', 'katanaCombo', 'cycloneKick', 'helicopterArms', 'breakdanceSweep'] },
riddle: { themed: ['riddleBarrage', 'portalPunch', 'cloneStrike', 'mindBlast', 'hexCurse'], generic: ['teleportStrike', 'projectile', 'zoomRush', 'whipCrack', 'bombThrow', 'captchaTrap', 'glitchBeam'] },
code_golf: { themed: ['golfClubSmash', 'golfCartDrive', 'stackOverflow', 'segfault', 'blueScreen'], generic: ['projectile', 'flyingKick', 'dashPunch', 'rapidFlurry', 'hammerSmash', 'bodySlam', 'homeRunSwing'] },
roast_battle: { themed: ['fireBreath', 'dragonBreath', 'fireball', 'lavaSplash', 'heatVision', 'volcanoErupt'], generic: ['gunBurst', 'jetpackDive', 'multiHit', 'zoomRush', 'laserBeam', 'chainsawRev', 'rocketLauncher', 'dynamiteBlast'] },
hallucination_check: { themed: ['cloneStrike', 'portalPunch', 'shadowClone', 'glitchBeam', 'voidRift'], generic: ['teleportStrike', 'dashThrough', 'riddleBarrage', 'rapidFlurry', 'sniperShot', 'darkVoid', 'memeBeam'] },
token_economy: { themed: ['coinShower', 'bitcoinCrash', 'nftRugPull', 'vendingDrop', 'popupSpam'], generic: ['dashPunch', 'flyingKick', 'uppercut', 'zoomRush', 'bombThrow', 'minigunSpray', 'pocketCannon'] },
creative_writing: { themed: ['penStab', 'scrollBlast', 'bookStorm', 'enchantedArrow', 'newspaperRoll'], generic: ['jetpackDive', 'aerialSlam', 'projectile', 'rapidFlurry', 'katanaCombo', 'arcaneBarrage', 'memeBeam'] },
math_blitz: { themed: ['mathAttack', 'dataStream', 'pixelBeam', 'hackerAttack', 'chainLightning'], generic: ['multiHit', 'dashPunch', 'fullScreenDash', 'rapidFlurry', 'laserBeam', 'hammerSmash', 'pinballCombo'] },
trap_card: { themed: ['trapCardAttack', 'captchaTrap', 'spiderWeb', 'flashBang', 'smokeBomb'], generic: ['gunBurst', 'teleportStrike', 'groundPound', 'zoomRush', 'sniperShot', 'rocketLauncher', 'minigunSpray'] },
food_fight: { themed: ['pizzaSlam', 'bananaFling', 'pieSmash', 'hotdogWhip', 'watermelonBomb', 'sushiBarrage', 'burgerToss', 'tacoStorm', 'eggBombard', 'tomatoBarrage'], generic: ['popcornBlast', 'iceCreamFling', 'donutBarrage', 'cookieFling', 'nachoVolley', 'meatballStorm', 'candyBarrage'] },
wrestling_match: { themed: ['piledriver', 'powerbomb', 'chokeslam', 'tombstone', 'rko', 'clothesline', 'suplex', 'germanSuplex'], generic: ['bodySlam', 'grappleFlurry', 'hurricanrana', 'moonsault', 'elbowDrop', 'spear', 'stunner'] },
music_battle: { themed: ['bassDrop', 'guitarSolo', 'drumSolo', 'dubstepCannon', 'beatboxBlast', 'vinylScratch'], generic: ['airHornBlast', 'vuvuzelaBlast', 'sonicWave', 'bassDropBeam', 'karaokeAttack', 'airGuitar'] },
magic_duel: { themed: ['kamehameha', 'spiritBomb', 'arcaneBarrage', 'darkVoid', 'holySmite', 'shadowClone', 'hexCurse'], generic: ['fireball', 'iceLance', 'thunderStrike', 'portalPunch', 'crystalShards', 'scrollBlast', 'potionThrow'] },
sports_showdown: { themed: ['homeRunSwing', 'slapShot', 'servingAce', 'fieldGoalKick', 'bodyCheck', 'soccerKick', 'basketballDunk'], generic: ['baseballBat', 'tennisRacket', 'hockeyStick', 'bowlingBallRoll', 'tennisBallVolley', 'dropKick'] },
nature_clash: { themed: ['treeSmash', 'icebergDrop', 'lavaSplash', 'tornadoFling', 'tsunamiWave', 'avalanche', 'earthquakeStrike'], generic: ['vineWhip', 'sandstorm', 'thornBarrage', 'pollenCloud', 'windSlash', 'meteorStrike'] },
space_war: { themed: ['blackHole', 'photonTorpedo', 'tractor_beam', 'warpDrive', 'alienAbduction', 'ionCannon', 'cosmicRay', 'bulletTimeAttack'], generic: ['laserSword', 'plasmaSword', 'asteroidBelt', 'satelliteLaser', 'plasmaBeam', 'solarFlare'] },
hack_battle: { themed: ['hackerAttack', 'bugSwarm', 'stackOverflow', 'segfault', 'blueScreen', 'malwareInject', 'bulletTimeAttack'], generic: ['fourOhFour', 'ctrlAltDelete', 'aiUprising', 'captchaTrap', 'popupSpam', 'dataStream', 'glitchBeam'] },
meme_war: { themed: ['selfieStrike', 'dabAttack', 'flossAttack', 'yeetThrow', 'tPoseAssert', 'emojiBarrage', 'memeBeam'], generic: ['rubberChicken', 'fingerGuns', 'micDrop', 'ratioAttack', 'capThrow', 'touchGrass', 'noScope'] },
animal_kingdom: { themed: ['sharkBite', 'bearSwipe', 'eagleDive', 'bullCharge', 'gorillaSlam', 'wolfPack', 'batSwarm'], generic: ['snakeLunge', 'scorpionSting', 'crabPinch', 'spiderWeb', 'beeSwarm', 'catScratch', 'dogPile', 'dolphinFlip'] },
demolition: { themed: ['dynamiteBlast', 'c4Detonation', 'nukeStrike', 'grenadeBlast', 'rocketLauncher', 'volcanoErupt'], generic: ['fireworksBurst', 'partyPopper', 'pinataSmash', 'cherryBomb', 'confettiCannon', 'anvilDrop', 'pianoDrop'] },
vehicle_mayhem: { themed: ['motorbikeCharge', 'carSmash', 'tankRoll', 'helicopterStrike', 'zamboniCrush', 'tractorPlow'], generic: ['shoppingCart', 'forkliftCharge', 'airplaneSwoop', 'rocketRide', 'unicycleRun', 'golfCartDrive', 'boatCannon'] },
medieval_combat: { themed: ['swordSlash', 'katanaCombo', 'battleAxe', 'maceSwing', 'crossbowBolt', 'shieldBash'], generic: ['flailSwing', 'holyWater', 'dragonBreath', 'enchantedArrow', 'laserSword', 'scrollBlast'] },
retro_mode: { themed: ['dashPunch', 'uppercut', 'rapidFlurry', 'flyingKick', 'multiHit', 'fullScreenDash', 'backflipKick', 'cycloneKick'], generic: ['bodySlam', 'grappleFlurry', 'suplex', 'katanaCombo', 'breakdanceSweep', 'corkscrewDive', 'pinballCombo'] },
}
// Tier-gated ultimate move pools — cumulative (tier 3 bot can use tier 2+3 moves)
const TIER_ULTIMATES: Record<number, string[]> = {
2: ['ultimateRapidBarrage', 'ultimateOrbitalStrike', 'ultimateCloneSurround', 'ultimateGatlingFury', 'ultimateEarthquake', 'ultimateBouncingFury', 'ultimateTripleBeam', 'ultimateMegaThrow', 'ultimateTornadoGrab', 'ultimateMeteorShower'],
3: ['ultimateScreenNuke', 'ultimateBlackHoleVortex', 'ultimateDimensionalRift', 'ultimateTimeSlow', 'ultimateSummonArmy', 'ultimateHellfireRain', 'ultimateCrystalCage', 'ultimateGigaDrill', 'ultimateSuperSlam', 'ultimateAvalancheDrop', 'ultimateBulletTimeBarrage'],
4: ['ultimateFinaleRush', 'ultimateMeteorCrash', 'ultimateScreenShatter', 'ultimateGravityReverse', 'ultimateInfiniteCombo', 'ultimatePlasmaStorm', 'ultimateFrozenCoffin', 'ultimateLavaGeyser', 'ultimateChainGrab', 'ultimateRocketBarrage'],
5: ['ultimateArmageddon', 'ultimateOmegaBeam', 'ultimateBeyondTheScreen', 'ultimateMatrixDodge', 'ultimateWorldEnder', 'ultimateAbsoluteZero', 'ultimateSolarCannon', 'ultimatePhantomStrike', 'ultimateGodPunch', 'ultimateVoidCollapse'],
}
// The Creator — exclusive moves (50% ultimate chance, 100% on crits, custom pool)
const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention', 'bitcoinSwarm', 'satoshiTornado', 'hodlWave', 'bitcoinBarrage', 'bulletTimeAttack']
const CREATOR_ULTIMATES = ['ultimateGenesisBlock', 'ultimateTheHalving', 'ultimateFullNodeCrush', 'ultimateSatoshiVision']
function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
// THE CREATOR: special move selection — 50% ultimate, 60% creator moves, rest normal
if (attackerArchetype === 'the_creator') {
const ultimateChance = isCritical ? 1.0 : 0.5
if (Math.random() < ultimateChance) {
// Creator gets ALL ultimates + their 4 exclusive ones
const all: string[] = [...CREATOR_ULTIMATES]
for (let t = 2; t <= 5; t++) {
if (TIER_ULTIMATES[t]) all.push(...TIER_ULTIMATES[t])
}
return all[Math.floor(Math.random() * all.length)]
}
// 60% creator-themed, 40% normal selection
if (Math.random() < 0.6) {
return CREATOR_MOVES[Math.floor(Math.random() * CREATOR_MOVES.length)]
}
// Fall through to normal selection
}
// Tier-gated ultimates: higher tier = higher chance of spectacular ultimate moves
// Tier 2: 15% chance, Tier 3: 20%, Tier 4: 30%, Tier 5: 40%
if (attackerTier >= 2) {
const ultimateChance = attackerTier === 2 ? 0.15 : attackerTier === 3 ? 0.20 : attackerTier === 4 ? 0.30 : 0.40
// On crits, double the chance
const roll = isCritical ? ultimateChance * 2 : ultimateChance
if (Math.random() < roll) {
// Collect all ultimates available at this tier (cumulative — higher tiers get lower tier moves too)
const available: string[] = []
for (let t = 2; t <= attackerTier; t++) {
if (TIER_ULTIMATES[t]) available.push(...TIER_ULTIMATES[t])
}
if (available.length > 0) {
return available[Math.floor(Math.random() * available.length)]
}
}
}
// Critical hits always get BIG moves (but can still be themed)
if (isCritical) {
const entry = CHALLENGE_THEMED[challengeType]
// 50% themed, 50% epic generic for crits
if (entry && Math.random() < 0.5) {
return entry.themed[Math.floor(Math.random() * entry.themed.length)]
}
const critMoves = [
'groundPound', 'jetpackDive', 'gunBurst', 'fullScreenDash', 'multiHit', 'fireBreath',
'lightningRush', 'zoomRush', 'rapidFlurry', 'rocketLauncher', 'laserBeam', 'katanaCombo',
'hammerSmash', 'sniperShot', 'minigunSpray', 'chainsawRev', 'carSmash', 'motorbikeCharge',
'anvilDrop', 'pocketCannon', 'bodySlam', 'suplex', 'pinballCombo', 'grappleFlurry',
// New epic crits
'kamehameha', 'nukeStrike', 'tankRoll', 'helicopterStrike', 'chokeslam', 'tombstone',
'rko', 'piledriver', 'powerbomb', 'meteorStrike', 'spiritBomb', 'blackHole',
'alienAbduction', 'tsunamiWave', 'earthquakeStrike', 'dragonBreath', 'pianoDrop',
'fridgeDrop', 'volcanoErupt', 'warpDrive', 'dubstepCannon', 'tornadoSpin',
'trampolineBounce', 'watermelonBomb', 'nftRugPull', 'blueScreen', 'yeetThrow',
'bulletTimeAttack',
]
return critMoves[Math.floor(Math.random() * critMoves.length)]
}
const entry = CHALLENGE_THEMED[challengeType]
if (!entry) {
const fallback = ['dashPunch', 'flyingKick', 'aerialSlam', 'backflipKick', 'fingerGuns', 'rubberChicken', 'pizzaSlam', 'fryingPan', 'snowballFight']
return fallback[Math.floor(Math.random() * fallback.length)]
}
// 60% themed, 25% generic fallback, 15% wild card
const roll = Math.random()
if (roll < 0.6) {
return entry.themed[Math.floor(Math.random() * entry.themed.length)]
} else if (roll < 0.85) {
return entry.generic[Math.floor(Math.random() * entry.generic.length)]
} else {
const wild = [
'jetpackDive', 'gunBurst', 'groundPound', 'teleportStrike', 'fullScreenDash', 'fireBreath',
'golfClubSmash', 'zoomRush', 'rapidFlurry', 'swordSlash', 'hammerSmash', 'laserBeam',
'rocketLauncher', 'bombThrow', 'minigunSpray', 'sniperShot', 'whipCrack', 'katanaCombo',
'chainsawRev', 'motorbikeCharge', 'carSmash', 'boatCannon', 'anvilDrop', 'pocketCannon',
'grappleFlurry', 'bodySlam', 'pinballCombo', 'suplex',
// New wild cards — food
'pizzaSlam', 'bananaFling', 'pieSmash', 'burgerToss', 'tomatoBarrage', 'fishSlap',
// New wild cards — weapons
'fryingPan', 'baseballBat', 'guitarSmash', 'umbrellaWhack', 'plungerSlam', 'wrenchSmash',
// New wild cards — drops
'pianoDrop', 'toiletDrop', 'tvDrop', 'chandelierDrop', 'micDrop',
// New wild cards — vehicles
'shoppingCart', 'zamboniCrush', 'unicycleRun', 'tankRoll',
// New wild cards — spins/flips
'tornadoSpin', 'backflipKick', 'corkscrewDive', 'barrelRoll', 'skateTrick',
// New wild cards — beams
'kamehameha', 'freezeRay', 'heatVision', 'rainbowBeam', 'glitchBeam',
// New wild cards — silly
'rubberChicken', 'selfieStrike', 'fingerGuns', 'yeetThrow', 'emojiBarrage', 'touchGrass',
// New wild cards — wrestling
'clothesline', 'rko', 'stunner', 'piledriver',
// New wild cards — magic
'fireball', 'portalPunch', 'crystalShards', 'spiritBomb',
// New wild cards — tech
'stackOverflow', 'blueScreen', 'aiUprising', 'popupSpam',
// New wild cards — music
'bassDrop', 'dubstepCannon', 'vinylScratch',
// New wild cards — nature
'avalanche', 'lavaSplash', 'tornadoFling',
]
return wild[Math.floor(Math.random() * wild.length)]
}
}
// Convert any CSS color string to a kaplay Color object safely
function safeColor(k: ReturnType<typeof kaplay>, color: string) {
if (color.startsWith('#')) {
try { return k.Color.fromHex(color) } catch { /* fall through */ }
}
// HSL / rgb / named color — render to a 1px canvas to extract RGB
const cv = document.createElement('canvas')
cv.width = 1; cv.height = 1
const cx = cv.getContext('2d')!
cx.fillStyle = color
cx.fillRect(0, 0, 1, 1)
const [r, g, b] = cx.getImageData(0, 0, 1, 1).data
return k.Color.fromArray([r, g, b])
}
export async function createFightScene(config: FightSceneConfig) {
const { canvas, botA, botB, arena } = config
const theme = ARENA_THEMES[arena] || ARENA_THEMES.localhost
const k = kaplay({
canvas,
width: canvas.width || 800,
height: canvas.height || 500,
background: theme.bg,
global: false,
scale: 1,
crisp: true,
texFilter: 'nearest',
})
const colorsA = getBotColors(botA.seed)
const colorsB = getBotColors(botB.seed)
let sheetA: string
let sheetB: string
// Human fighters use their profile human character, not a bot sprite
const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0))
const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0))
try {
sheetA = botA.archetype === 'human'
? generateHumanFighterSpriteSheet(botA.seed, botA.archetype, colorsA.primary, colorsA.secondary, winRateA)
: generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization)
} catch (err) {
console.error('[FightScene] Failed to generate sprite for botA:', err)
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
}
try {
sheetB = botB.archetype === 'human'
? generateHumanFighterSpriteSheet(botB.seed, botB.archetype, colorsB.primary, colorsB.secondary, winRateB)
: generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization)
} catch (err) {
console.error('[FightScene] Failed to generate sprite for botB:', err)
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
}
const isHumanA = botA.archetype === 'human'
const isHumanB = botB.archetype === 'human'
// Helper: load sprite with a 5s timeout so mobile never hangs forever
async function loadSpriteWithTimeout(name: string, src: string, opts: LoadSpriteOpt): Promise<void> {
try {
await Promise.race([
k.loadSprite(name, src, opts),
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('timeout')), 5_000)),
])
} catch (err) {
console.warn(`[FightScene] Sprite '${name}' failed/timed out:`, err)
}
}
// Await sprite loading — timeout prevents mobile hangs
await Promise.all([
loadSpriteWithTimeout('botA', sheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }),
loadSpriteWithTimeout('botB', sheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }),
])
// Preload bot sprites for human fighters (used when humans morph into bots)
if (isHumanA) {
const botSheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
loadSpriteWithTimeout('botA_morph', botSheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
}
if (isHumanB) {
const botSheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
loadSpriteWithTimeout('botB_morph', botSheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
}
// Judge sprite (red lobster referee)
const judgeSheet = generateJudgeSpriteSheet()
const judgeAnims = {
idle: { from: 0, to: JUDGE_ANIMATIONS.idle.frames - 1, loop: true, speed: 4 },
call_left: { from: JUDGE_MAX_FRAMES, to: JUDGE_MAX_FRAMES + JUDGE_ANIMATIONS.call_left.frames - 1, loop: false, speed: 8 },
call_right: { from: JUDGE_MAX_FRAMES * 2, to: JUDGE_MAX_FRAMES * 2 + JUDGE_ANIMATIONS.call_right.frames - 1, loop: false, speed: 8 },
shocked: { from: JUDGE_MAX_FRAMES * 3, to: JUDGE_MAX_FRAMES * 3 + JUDGE_ANIMATIONS.shocked.frames - 1, loop: false, speed: 10 },
}
await loadSpriteWithTimeout('judge', judgeSheet, { sliceX: JUDGE_MAX_FRAMES, sliceY: JUDGE_ROWS, anims: judgeAnims })
// Human sprites (the bot owner's silly human avatar for crowd)
const humanSheetA = generateHumanSpriteSheet(botA.seed, botA.archetype || 'standard', colorsA.primary, colorsA.secondary, winRateA)
const humanSheetB = generateHumanSpriteSheet(botB.seed, botB.archetype || 'standard', colorsB.primary, colorsB.secondary, winRateB)
const humanAnims: Record<string, { from: number; to: number; loop: boolean; speed: number }> = {}
for (const [name, cfg] of Object.entries(HUMAN_ANIMATIONS)) {
humanAnims[name] = { from: cfg.row * HUMAN_MAX_FRAMES, to: cfg.row * HUMAN_MAX_FRAMES + cfg.frames - 1, loop: true, speed: 6 }
}
await Promise.all([
loadSpriteWithTimeout('humanA', humanSheetA, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
loadSpriteWithTimeout('humanB', humanSheetB, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
])
const W = k.width()
const H = k.height()
// Scale factor so sprites shrink on small canvases (reference: 500px tall)
const SF = Math.min(1, H / 500)
const GROUND_Y = H * 0.78
const HOME_A = W * 0.28
const HOME_B = W * 0.72
// Particle helpers
function spawnSparks(x: number, y: number, count: number, color: string) {
const intense = count > 8
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2
const speed = (intense ? 150 : 100) + Math.random() * (intense ? 400 : 300)
const size = (intense ? 3 : 2) + Math.random() * (intense ? 6 : 4)
const rotSpeed = (Math.random() - 0.5) * 600
const spark = k.add([
k.rect(size, size * (0.5 + Math.random() * 0.5)),
k.pos(x, y),
k.color(safeColor(k,color)),
k.opacity(1),
k.z(20),
k.rotate(Math.random() * 360),
k.anchor('center'),
{ vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 100, rotV: rotSpeed },
])
spark.onUpdate(() => {
spark.pos.x += (spark as any).vx * k.dt()
spark.pos.y += (spark as any).vy * k.dt()
;(spark as any).vy += 500 * k.dt() // gravity
spark.angle += (spark as any).rotV * k.dt()
spark.opacity -= 1.5 * k.dt()
if (spark.opacity <= 0) spark.destroy()
})
}
}
function spawnBulletHoles(x: number, y: number, count: number) {
for (let i = 0; i < count; i++) {
const hole = k.add([
k.circle(3 + Math.random() * 3),
k.pos(x + (Math.random() - 0.5) * 40, y + (Math.random() - 0.5) * 60),
k.color(safeColor(k,'#000000')),
k.opacity(0.8),
k.z(9), // behind fighters
])
// Fade out after a bit
setTimeout(() => {
const fadeInterval = setInterval(() => {
hole.opacity -= 0.02
if (hole.opacity <= 0) { hole.destroy(); clearInterval(fadeInterval) }
}, 50)
}, 1500)
}
}
function spawnExhaust(x: number, y: number, duration: number) {
const endTime = performance.now() + duration * 1000
const interval = setInterval(() => {
if (performance.now() > endTime) { clearInterval(interval); return }
for (let i = 0; i < 3; i++) {
const p = k.add([
k.circle(3 + Math.random() * 5),
k.pos(x + (Math.random() - 0.5) * 15, y),
k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')),
k.opacity(0.8),
k.z(8),
])
p.onUpdate(() => {
p.pos.y += 100 * k.dt()
p.pos.x += (Math.random() - 0.5) * 50 * k.dt()
p.opacity -= 2.5 * k.dt()
if (p.opacity <= 0) p.destroy()
})
}
}, 40)
return interval
}
function spawnProjectile(fromX: number, fromY: number, toX: number, toY: number, color: string, size: number = 8): Promise<void> {
return new Promise(resolve => {
const proj = k.add([
k.circle(size),
k.pos(fromX, fromY),
k.color(safeColor(k,color)),
k.opacity(1),
k.z(15),
])
// Trail
const trail = setInterval(() => {
const t = k.add([
k.circle(size * 0.6),
k.pos(proj.pos.x, proj.pos.y),
k.color(safeColor(k,color)),
k.opacity(0.5),
k.z(14),
])
t.onUpdate(() => { t.opacity -= 3 * k.dt(); if (t.opacity <= 0) t.destroy() })
}, 30)
const dx = toX - fromX
const dy = toY - fromY
const dist = Math.sqrt(dx * dx + dy * dy)
const speed = 800
const dur = dist / speed
k.tween(0, 1, dur, (t) => {
proj.pos.x = fromX + dx * t
proj.pos.y = fromY + dy * t
}, k.easings.linear).then(() => {
clearInterval(trail)
proj.destroy()
spawnSparks(toX, toY, 8, color)
resolve()
})
})
}
function spawnBullet(fromX: number, fromY: number, toX: number, toY: number): Promise<void> {
return new Promise(resolve => {
const bullet = k.add([
k.rect(6, 2),
k.pos(fromX, fromY),
k.color(safeColor(k,'#ffee00')),
k.opacity(1),
k.z(15),
k.rotate(Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI),
])
const dur = 0.08 + Math.random() * 0.04
k.tween(0, 1, dur, (t) => {
bullet.pos.x = fromX + (toX - fromX) * t
bullet.pos.y = fromY + (toY - fromY) * t + (Math.random() - 0.5) * 4
}, k.easings.linear).then(() => {
bullet.destroy()
// Muzzle flash at impact
spawnSparks(toX, toY, 3, '#ffcc00')
resolve()
})
})
}
function spawnShockwave(x: number, y: number, color: string) {
const wave = k.add([
k.circle(5),
k.pos(x, y),
k.color(safeColor(k,color)),
k.opacity(0.7),
k.z(5),
k.scale(1),
])
k.tween(1, 15, 0.4, (v) => { wave.scaleTo(v, v * 0.3) }, k.easings.easeOutQuad)
k.tween(0.7, 0, 0.4, (v) => { wave.opacity = v }, k.easings.easeOutQuad).then(() => wave.destroy())
}
// === VISUAL CHAOS EFFECTS ===
// RGB split glitch — offsets red/blue channels using colored overlays
function glitchRGB(duration: number = 0.2) {
const layers = [
{ color: '#ff0000', dx: 4 + Math.random() * 6, dy: -2 },
{ color: '#0000ff', dx: -(4 + Math.random() * 6), dy: 2 },
]
for (const l of layers) {
const overlay = k.add([
k.rect(W, H), k.pos(l.dx, l.dy),
k.color(safeColor(k,l.color)), k.opacity(0.12), k.z(55),
])
overlay.onUpdate(() => {
overlay.pos.x = l.dx + (Math.random() - 0.5) * 4
overlay.pos.y = l.dy + (Math.random() - 0.5) * 2
})
setTimeout(() => { if (overlay.exists()) overlay.destroy() }, duration * 1000)
}
}
// Scanline glitch — horizontal bands flicker
function scanlineGlitch(duration: number = 0.3) {
const lines: GameObj[] = []
const count = 6 + Math.floor(Math.random() * 8)
for (let i = 0; i < count; i++) {
const ly = Math.random() * H
const lh = 1 + Math.random() * 3
const line = k.add([
k.rect(W, lh), k.pos(0, ly),
k.color(safeColor(k,Math.random() > 0.5 ? '#ffffff' : '#000000')),
k.opacity(0.15 + Math.random() * 0.2), k.z(54),
])
line.onUpdate(() => {
line.pos.x = (Math.random() - 0.5) * 8
line.opacity = Math.random() > 0.3 ? 0.15 : 0
})
lines.push(line)
}
setTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
}
// VHS tracking distortion — horizontal offset bands
function vhsTracking(duration: number = 0.4) {
const bands: GameObj[] = []
const bandCount = 3
for (let i = 0; i < bandCount; i++) {
const by = Math.random() * H
const bh = 10 + Math.random() * 30
const band = k.add([
k.rect(W, bh), k.pos(0, by),
k.color(safeColor(k,theme.accent)), k.opacity(0.06), k.z(53),
])
band.onUpdate(() => {
band.pos.x = Math.sin(k.time() * 20 + i * 3) * 15
band.pos.y += (Math.random() - 0.5) * 2
})
bands.push(band)
}
setTimeout(() => { bands.forEach(b => { if (b.exists()) b.destroy() }) }, duration * 1000)
}
// Dimensional shift — background color/hue flash cycle
function dimensionalShift(duration: number = 0.6) {
const colors = ['#ff00ff', '#00ffff', '#ff0000', '#0000ff', '#ffff00']
let idx = 0
const overlay = k.add([
k.rect(W, H), k.pos(0, 0),
k.color(safeColor(k,colors[0])), k.opacity(0.08), k.z(1),
])
const interval = setInterval(() => {
idx = (idx + 1) % colors.length
overlay.color = safeColor(k,colors[idx])
overlay.opacity = 0.05 + Math.random() * 0.06
}, 80)
setTimeout(() => { clearInterval(interval); if (overlay.exists()) overlay.destroy() }, duration * 1000)
}
// Schizo cut — rapid zoom/position jitter simulating jump cuts
async function schizoCut() {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const origAX = fA.pos.x, origBX = fB.pos.x
const origAY = fA.pos.y, origBY = fB.pos.y
// 3-5 rapid cuts
const cuts = 3 + Math.floor(Math.random() * 3)
for (let i = 0; i < cuts; i++) {
// Random offset both fighters
fA.pos.x = origAX + (Math.random() - 0.5) * 30
fA.pos.y = origAY + (Math.random() - 0.5) * 15
fB.pos.x = origBX + (Math.random() - 0.5) * 30
fB.pos.y = origBY + (Math.random() - 0.5) * 15
screenFlash(Math.random() > 0.5 ? '#000000' : '#ffffff', 0.03)
await k.wait(0.04 + Math.random() * 0.03)
}
fA.pos.x = origAX; fA.pos.y = origAY
fB.pos.x = origBX; fB.pos.y = origBY
}
// Hyperspeed lines — converging toward a point
function hyperSpeedLines(targetX: number, targetY: number, duration: number = 0.3) {
const lines: GameObj[] = []
for (let i = 0; i < 16; i++) {
const angle = (i / 16) * Math.PI * 2
const dist = 300 + Math.random() * 200
const sx = targetX + Math.cos(angle) * dist
const sy = targetY + Math.sin(angle) * dist
const len = 30 + Math.random() * 60
const line = k.add([
k.rect(len, 1.5), k.pos(sx, sy),
k.color(safeColor(k,'#ffffff')), k.opacity(0.3), k.z(52),
k.rotate(angle * 180 / Math.PI + 180),
])
line.onUpdate(() => {
const dx = targetX - line.pos.x
const dy = targetY - line.pos.y
const d = Math.sqrt(dx * dx + dy * dy)
if (d > 10) {
line.pos.x += (dx / d) * 600 * k.dt()
line.pos.y += (dy / d) * 600 * k.dt()
}
line.opacity -= 0.8 * k.dt()
if (line.opacity <= 0 && line.exists()) line.destroy()
})
lines.push(line)
}
setTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
}
// Flash the whole screen
function screenFlash(color: string, duration: number = 0.1) {
const flash = k.add([
k.rect(W, H),
k.pos(0, 0),
k.color(safeColor(k,color)),
k.opacity(0.4),
k.z(50),
])
k.tween(0.4, 0, duration, (v) => { flash.opacity = v }).then(() => flash.destroy())
}
// Impact freeze-frame: brief pause with white flash on devastating crits
async function impactFreeze(duration: number = 0.08) {
const overlay = k.add([
k.rect(W, H), k.pos(0, 0),
k.color(safeColor(k, '#ffffff')), k.opacity(0.5), k.z(55),
])
// Kaplay has no built-in pause, so we just hold with a wait + overlay
await k.wait(duration)
k.tween(0.5, 0, 0.06, (v) => { overlay.opacity = v }).then(() => overlay.destroy())
}
// Spawn afterimages: trailing ghost sprites behind a moving fighter
function spawnAfterimages(fighter: Fighter, count: number = 3) {
if (!fighter?.exists()) return
for (let i = 0; i < count; i++) {
const ghost = k.add([
k.rect(FRAME_SIZE * Math.abs(fighter.scale.x), FRAME_SIZE * Math.abs(fighter.scale.y)),
k.pos(fighter.pos.x - i * 8 * Math.sign(fighter.scale.x), fighter.pos.y),
k.color(safeColor(k, theme.accent)),
k.opacity(0.3 - i * 0.08),
k.z(fighter.z - 1),
k.anchor('bot'),
])
k.tween(ghost.opacity, 0, 0.15 + i * 0.05, (v) => { ghost.opacity = v }).then(() => ghost.destroy())
}
}
// Camera zoom: scale fighters toward/away from a focal point for impact emphasis
async function cameraZoom(
fighters: Fighter[],
savedScales: { x: number; y: number }[],
zoomFactor: number,
duration: number,
easing: (t: number) => number = k.easings.easeOutQuad,
) {
await Promise.all(fighters.map((f, i) => {
if (!f?.exists()) return Promise.resolve()
const sx = savedScales[i].x
const sy = savedScales[i].y
return k.tween(Math.abs(f.scale.y), Math.abs(sy) * zoomFactor, duration, (v) => {
f.scale.x = sx > 0 ? v : -v
f.scale.y = v
}, easing)
}))
}
// Grotesque close-up overlays removed — noops retained for call-site compatibility
function spawnGrotesqueDetails(_fighter: Fighter, _scaleFactor: number) {}
function destroyGrotesqueDetails() {}
// Arena-specific background decoration
function drawArenaDecor() {
const a = arena
const acc = theme.accent
// Parallax stars/particles in the sky
for (let i = 0; i < 40; i++) {
const star = k.add([
k.rect(1 + Math.random() * 2, 1 + Math.random() * 2),
k.pos(Math.random() * W, Math.random() * (GROUND_Y - 20)),
k.color(safeColor(k, i % 5 === 0 ? '#ffffff' : acc)),
k.opacity(0.08 + Math.random() * 0.2),
k.z(1),
])
const speed = 0.5 + Math.random() * 1.5
const baseOp = star.opacity
star.onUpdate(() => {
star.opacity = baseOp + Math.sin(k.time() * speed + i) * 0.08
})
}
if (a === 'datacenter' || a === 'localhost') {
// City skyline silhouette behind the racks
const buildingHeights = [0.55, 0.4, 0.65, 0.35, 0.7, 0.45, 0.6, 0.5, 0.72, 0.38, 0.58, 0.42]
for (let i = 0; i < buildingHeights.length; i++) {
const bw = W / buildingHeights.length + 4
const bh = GROUND_Y * buildingHeights[i]
const bx = i * (W / buildingHeights.length)
k.add([k.rect(bw, bh), k.pos(bx, GROUND_Y - bh), k.color(safeColor(k, '#060610')), k.opacity(0.5), k.z(0)])
for (let wy = 0; wy < bh - 10; wy += 8) {
for (let wx = 4; wx < bw - 6; wx += 7) {
const win = k.add([
k.rect(3, 3), k.pos(bx + wx, GROUND_Y - bh + 6 + wy),
k.color(safeColor(k, Math.random() > 0.4 ? '#223355' : '#ffcc44')),
k.opacity(0.15 + Math.random() * 0.2), k.z(0),
])
if (Math.random() < 0.15) {
const blink = 0.03 + Math.random() * 0.06
win.onUpdate(() => { win.opacity = Math.random() > blink ? win.opacity : (Math.random() > 0.5 ? 0.3 : 0.05) })
}
}
}
}
// Server rack wall
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 10; col++) {
const lx = 15 + col * (W / 10)
const ly = 15 + row * 55
const rackW = W / 10 - 8
const rackH = 48
k.add([k.rect(rackW, rackH), k.pos(lx, ly), k.color(safeColor(k, '#06060f')), k.opacity(0.6), k.z(1)])
k.add([k.rect(rackW, 1), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
k.add([k.rect(rackW, 1), k.pos(lx, ly + rackH), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
k.add([k.rect(1, rackH), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.3), k.z(1)])
for (let s = 0; s < 5; s++) {
k.add([k.rect(rackW - 6, 1), k.pos(lx + 3, ly + 8 + s * 8), k.color(safeColor(k, '#111122')), k.opacity(0.5), k.z(1)])
}
for (let led = 0; led < 3; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4),
k.color(safeColor(k, Math.random() > 0.3 ? '#00ff41' : '#ff2d2d')),
k.opacity(0.7), k.z(2),
])
const blinkRate = 0.02 + Math.random() * 0.08
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > blinkRate ? 0.7 : 0.15 })
}
if (Math.random() < 0.4) {
const actLed = k.add([
k.rect(2, 2), k.pos(lx + rackW - 8, ly + 4),
k.color(safeColor(k, '#ffaa00')), k.opacity(0.6), k.z(2),
])
actLed.onUpdate(() => { actLed.opacity = Math.random() > 0.5 ? 0.7 : 0.2 })
}
}
}
// Cable bundles
for (let c = 0; c < 6; c++) {
const cy = 3 + c * 2
k.add([k.rect(W, 2), k.pos(0, cy), k.color(safeColor(k, c % 3 === 0 ? '#111133' : c % 3 === 1 ? '#0a0a22' : '#0f0f2a')), k.opacity(0.5), k.z(1)])
}
// Scan line sweep
const scanLine = k.add([k.rect(W, 1), k.pos(0, 0), k.color(safeColor(k, '#00f0ff')), k.opacity(0.06), k.z(3)])
scanLine.onUpdate(() => { scanLine.pos.y = (scanLine.pos.y + 40 * k.dt()) % GROUND_Y })
} else if (a === 'gpu_graveyard') {
// Floating circuit board fragments
for (let i = 0; i < 16; i++) {
const cx = Math.random() * W
const cy = 20 + Math.random() * (GROUND_Y - 60)
const cw = 15 + Math.random() * 25
const ch = 10 + Math.random() * 18
const chip = k.add([
k.rect(cw, ch), k.pos(cx, cy),
k.color(safeColor(k, Math.random() > 0.5 ? '#1a2a1a' : '#0f1a0f')),
k.opacity(0.35), k.z(1), k.rotate(Math.random() * 45 - 22),
])
for (let t = 0; t < 3; t++) {
k.add([k.rect(cw * 0.6, 1), k.pos(cx + 2, cy + 3 + t * 4), k.color(safeColor(k, '#76b900')), k.opacity(0.2), k.z(1)])
}
k.add([k.circle(1.5), k.pos(cx + cw / 2, cy + ch / 2), k.color(safeColor(k, '#aabb44')), k.opacity(0.25), k.z(1)])
const baseY = cy
chip.onUpdate(() => { chip.pos.y = baseY + Math.sin(k.time() * 0.3 + i) * 5 })
}
// Dead GPU fans — some slowly spinning
for (const fx of [W * 0.15, W * 0.5, W * 0.85]) {
const fy = GROUND_Y - 30 - Math.random() * 40
k.add([k.circle(18), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.4), k.z(1)])
k.add([k.circle(5), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1)])
for (let b = 0; b < 4; b++) {
const ba = b * Math.PI / 2
const blade = k.add([k.rect(14, 2), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.3), k.z(1), k.rotate(ba * 180 / Math.PI)])
if (fx === W * 0.5) {
blade.onUpdate(() => { blade.angle += 8 * k.dt() })
}
}
}
// Sparking wires
for (let i = 0; i < 4; i++) {
const sx = W * (0.2 + i * 0.2)
const sy = 10 + Math.random() * 30
k.add([k.rect(1, 20 + Math.random() * 40), k.pos(sx, sy), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)])
const spark = k.add([k.circle(3), k.pos(sx, sy + 20), k.anchor('center'), k.color(safeColor(k, '#ffff00')), k.opacity(0), k.z(3)])
spark.onUpdate(() => { spark.opacity = Math.random() > 0.97 ? 0.6 : 0 })
}
// Smoke wisps rising
for (let i = 0; i < 6; i++) {
const smoke = k.add([
k.circle(8 + Math.random() * 12), k.pos(Math.random() * W, GROUND_Y - 5),
k.color(safeColor(k, '#1a1a1a')), k.opacity(0.15), k.z(2),
])
const bx = smoke.pos.x
smoke.onUpdate(() => {
smoke.pos.y -= 6 * k.dt()
smoke.pos.x = bx + Math.sin(k.time() + i) * 8
smoke.opacity *= 0.998
if (smoke.pos.y < 0) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.15 }
})
}
} else if (a === 'prompt_dungeon') {
// Stone wall bricks
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 12; col++) {
const brickW = W / 12 + (Math.random() - 0.5) * 10
const brickH = GROUND_Y / 5
const bx = col * (W / 12) + (row % 2 === 0 ? 0 : W / 24)
k.add([k.rect(brickW - 2, brickH - 2), k.pos(bx, row * brickH), k.color(safeColor(k, '#1a1520')), k.opacity(0.3), k.z(1)])
}
}
// Glowing runes with halos
for (let i = 0; i < 10; i++) {
const rx = 30 + Math.random() * (W - 60)
const ry = 20 + Math.random() * (GROUND_Y - 60)
const rune = k.add([
k.circle(6 + Math.random() * 8), k.pos(rx, ry),
k.color(safeColor(k, '#b83dff')), k.opacity(0.12), k.z(1),
])
k.add([k.circle(12 + Math.random() * 8), k.pos(rx, ry), k.color(safeColor(k, '#6622aa')), k.opacity(0.06), k.z(0)])
rune.onUpdate(() => { rune.opacity = 0.08 + Math.sin(k.time() * 0.8 + i * 0.7) * 0.08 })
}
// Torches with flickering flames and light cones
for (const tx of [W * 0.1, W * 0.35, W * 0.65, W * 0.9]) {
const ty = GROUND_Y * 0.3
k.add([k.rect(4, 20), k.pos(tx, ty), k.color(safeColor(k, '#553311')), k.opacity(0.5), k.z(1)])
const flame = k.add([k.circle(6), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.5), k.z(2)])
const flameGlow = k.add([k.circle(15), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.08), k.z(1)])
const lightCone = k.add([k.rect(30, GROUND_Y * 0.5), k.pos(tx - 13, ty + 20), k.color(safeColor(k, '#ff6600')), k.opacity(0.02), k.z(0)])
flame.onUpdate(() => {
flame.opacity = 0.4 + Math.sin(k.time() * 6 + tx) * 0.15
flame.pos.y = ty - 3 + Math.sin(k.time() * 8 + tx) * 1.5
flameGlow.opacity = 0.05 + Math.sin(k.time() * 6 + tx) * 0.04
lightCone.opacity = 0.015 + Math.sin(k.time() * 6 + tx) * 0.008
})
}
// Chains hanging from ceiling
for (let i = 0; i < 5; i++) {
const cx = W * (0.15 + i * 0.18)
const chainLen = 25 + Math.random() * 50
for (let s = 0; s < chainLen; s += 5) {
const link = k.add([k.rect(3, 4), k.pos(cx, s), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)])
const baseCx = cx
link.onUpdate(() => { link.pos.x = baseCx + Math.sin(k.time() * 0.4 + i + s * 0.1) * 3 })
}
}
// Ground fog
for (let i = 0; i < 8; i++) {
const fog = k.add([
k.circle(35 + Math.random() * 30), k.pos(Math.random() * W, GROUND_Y - 8),
k.color(safeColor(k, '#1f1a2a')), k.opacity(0.25), k.z(3),
])
const baseX = fog.pos.x
fog.onUpdate(() => { fog.pos.x = baseX + Math.sin(k.time() * 0.2 + i) * 18 })
}
// Dripping water
for (let i = 0; i < 3; i++) {
const dx = W * (0.25 + i * 0.25)
const drop = k.add([k.circle(1.5), k.pos(dx, 0), k.color(safeColor(k, '#4466aa')), k.opacity(0.3), k.z(2)])
drop.onUpdate(() => {
drop.pos.y += 50 * k.dt()
if (drop.pos.y > GROUND_Y) { drop.pos.y = 0; drop.opacity = 0.3 }
drop.opacity *= 0.997
})
}
} else if (a === 'the_cloud') {
// Layered cloud formations
for (let layer = 0; layer < 3; layer++) {
for (let i = 0; i < 4; i++) {
const cx = Math.random() * W
const cy = 20 + layer * 50 + Math.random() * 30
const cloudOp = 0.25 - layer * 0.05
for (let j = 0; j < 4 + Math.floor(Math.random() * 3); j++) {
const cloud = k.add([
k.circle(15 + Math.random() * 20),
k.pos(cx + j * 14 - 20, cy + (Math.random() - 0.5) * 12),
k.color(safeColor(k, layer === 0 ? '#1a2030' : '#151a28')),
k.opacity(cloudOp), k.z(1),
])
const bx = cloud.pos.x
cloud.onUpdate(() => { cloud.pos.x = bx + Math.sin(k.time() * (0.08 + layer * 0.04) + i) * (8 + layer * 4) })
}
}
}
// Data rain streams
for (let i = 0; i < 15; i++) {
const stream = k.add([
k.rect(1, 8 + Math.random() * 15), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, '#4488ff')), k.opacity(0.12), k.z(2),
])
stream.onUpdate(() => {
stream.pos.y += 30 * k.dt()
if (stream.pos.y > GROUND_Y) { stream.pos.y = -20; stream.pos.x = Math.random() * W }
})
}
// Lightning flashes
const lightning = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3)])
lightning.onUpdate(() => { lightning.opacity = Math.random() > 0.998 ? 0.06 : 0 })
// Rainbow arc (subtle)
const rainbowColors = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff']
for (let i = 0; i < rainbowColors.length; i++) {
k.add([
k.circle(120 + i * 6), k.pos(W * 0.3, GROUND_Y + 60), k.anchor('center'),
k.color(safeColor(k, rainbowColors[i])), k.opacity(0.03), k.z(0),
])
}
} else if (a === 'the_singularity') {
// Central swirling vortex with multiple rings
for (let ring = 0; ring < 4; ring++) {
const ringDots = 12 + ring * 6
for (let i = 0; i < ringDots; i++) {
const angle = (i / ringDots) * Math.PI * 2
const baseDist = 20 + ring * 30
const dot = k.add([
k.circle(1.5 + ring * 0.5), k.pos(W / 2, GROUND_Y * 0.4),
k.color(safeColor(k, ring === 0 ? '#ffffff' : ring === 1 ? '#ff44ff' : ring === 2 ? '#ff00ff' : '#aa00aa')),
k.opacity(0.3 - ring * 0.05), k.z(1),
])
dot.onUpdate(() => {
const a2 = angle + k.time() * (0.8 - ring * 0.15)
const d2 = baseDist + Math.sin(k.time() * 0.8 + i) * 12
dot.pos.x = W / 2 + Math.cos(a2) * d2
dot.pos.y = GROUND_Y * 0.4 + Math.sin(a2) * d2 * 0.5
})
}
}
// Central core with pulsing glow
const core = k.add([k.circle(12), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.15), k.z(1)])
k.add([k.circle(30), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0)])
k.add([k.circle(60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#880088')), k.opacity(0.03), k.z(0)])
core.onUpdate(() => { core.opacity = 0.1 + Math.sin(k.time() * 2) * 0.08 })
// Energy tendrils reaching outward
for (let i = 0; i < 8; i++) {
const tendril = k.add([
k.rect(2, 40 + Math.random() * 60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('top'),
k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0), k.rotate(i * 45),
])
tendril.onUpdate(() => { tendril.angle = i * 45 + k.time() * 15 })
}
// Warped space grid lines
for (let i = 0; i < 6; i++) {
const gridLine = k.add([
k.rect(W, 1), k.pos(0, GROUND_Y * (0.15 + i * 0.12)),
k.color(safeColor(k, '#440066')), k.opacity(0.08), k.z(0),
])
gridLine.onUpdate(() => { gridLine.pos.x = Math.sin(k.time() * 0.3 + i) * 10 - 5 })
}
} else if (a === 'silicon_valley_dojo') {
// Bamboo stalks with segments and leaves
for (const side of [0.04, 0.08, 0.12, 0.86, 0.91, 0.96]) {
const px = W * side
const segments = 5 + Math.floor(Math.random() * 3)
const segH = (GROUND_Y - 15) / segments
for (let s = 0; s < segments; s++) {
k.add([k.rect(5, segH - 2), k.pos(px, 15 + s * segH), k.color(safeColor(k, '#1a3a1a')), k.opacity(0.45), k.z(1)])
k.add([k.rect(7, 2), k.pos(px - 1, 15 + s * segH), k.color(safeColor(k, '#2a4a2a')), k.opacity(0.4), k.z(1)])
}
for (let l = 0; l < 3; l++) {
const ly = 20 + l * (GROUND_Y / 4)
const dir = Math.random() > 0.5 ? 1 : -1
const leaf = k.add([k.rect(18, 3), k.pos(px + dir * 4, ly), k.color(safeColor(k, '#22aa33')), k.opacity(0.2), k.z(1), k.rotate(dir * 25)])
leaf.onUpdate(() => { leaf.angle = dir * 25 + Math.sin(k.time() * 1.5 + l) * 3 })
k.add([k.rect(14, 2), k.pos(px + dir * 8, ly + 4), k.color(safeColor(k, '#1a8822')), k.opacity(0.15), k.z(1), k.rotate(dir * 35)])
}
}
// Zen garden raked lines
for (let i = 0; i < 8; i++) {
const gy = GROUND_Y + 5 + i * 8
k.add([k.rect(W * 0.6, 1), k.pos(W * 0.2, gy), k.color(safeColor(k, '#00ff41')), k.opacity(0.06), k.z(0)])
}
// Stone lanterns with warm glow
for (const lx of [W * 0.2, W * 0.8]) {
k.add([k.rect(10, 6), k.pos(lx, GROUND_Y - 20), k.color(safeColor(k, '#444444')), k.opacity(0.35), k.z(1)])
k.add([k.rect(6, 14), k.pos(lx + 2, GROUND_Y - 34), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)])
k.add([k.rect(12, 3), k.pos(lx - 1, GROUND_Y - 37), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)])
const glow = k.add([k.circle(8), k.pos(lx + 5, GROUND_Y - 27), k.anchor('center'), k.color(safeColor(k, '#ffaa33')), k.opacity(0.15), k.z(2)])
glow.onUpdate(() => { glow.opacity = 0.1 + Math.sin(k.time() * 2 + lx) * 0.06 })
}
// Cherry blossom petals falling
for (let i = 0; i < 12; i++) {
const petal = k.add([
k.circle(1.5 + Math.random()), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, Math.random() > 0.5 ? '#ffaacc' : '#ff88aa')), k.opacity(0.2), k.z(3),
])
const startX = petal.pos.x
petal.onUpdate(() => {
petal.pos.y += 8 * k.dt()
petal.pos.x = startX + Math.sin(k.time() * 1.2 + i) * 15
if (petal.pos.y > GROUND_Y) { petal.pos.y = -5 }
})
}
} else if (a === 'hacker_news' || a === 'stackoverflow_ruins') {
// Ruined building walls with cracks
for (let i = 0; i < 3; i++) {
const wallX = W * (0.15 + i * 0.3)
const wallH = GROUND_Y * (0.4 + Math.random() * 0.3)
k.add([k.rect(W * 0.2, wallH), k.pos(wallX, GROUND_Y - wallH), k.color(safeColor(k, '#1a1408')), k.opacity(0.3), k.z(0)])
for (let c = 0; c < 3; c++) {
k.add([k.rect(1, 10 + Math.random() * 15), k.pos(wallX + Math.random() * W * 0.18, GROUND_Y - wallH + Math.random() * wallH), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0), k.rotate(Math.random() * 30 - 15)])
}
}
// Code blocks floating
for (let i = 0; i < 14; i++) {
const bw = 25 + Math.random() * 60
const bh = 3 + Math.random() * 4
const bx = 15 + Math.random() * (W - 50)
const by = 15 + Math.random() * (GROUND_Y - 50)
k.add([k.rect(bw, bh), k.pos(bx, by), k.color(safeColor(k, acc)), k.opacity(0.06 + Math.random() * 0.06), k.z(1)])
if (Math.random() < 0.4) {
k.add([k.rect(2, bh), k.pos(bx - 4, by), k.color(safeColor(k, acc)), k.opacity(0.1), k.z(1)])
}
}
// Vote arrows
for (let i = 0; i < 5; i++) {
const ax = 20 + Math.random() * (W - 40)
const ay = 30 + Math.random() * (GROUND_Y - 70)
k.add([k.rect(4, 6), k.pos(ax, ay), k.color(safeColor(k, a === 'stackoverflow_ruins' ? '#f48024' : '#ff6600')), k.opacity(0.12), k.z(1)])
}
// Broken monitors with static
for (let i = 0; i < 3; i++) {
const mx = W * (0.2 + i * 0.3)
const my = GROUND_Y * 0.3 + Math.random() * 30
k.add([k.rect(22, 16), k.pos(mx, my), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.5), k.z(1)])
k.add([k.rect(20, 14), k.pos(mx + 1, my + 1), k.color(safeColor(k, '#111111')), k.opacity(0.4), k.z(1)])
const staticEl = k.add([k.rect(18, 12), k.pos(mx + 2, my + 2), k.color(safeColor(k, '#888888')), k.opacity(0), k.z(2)])
staticEl.onUpdate(() => { staticEl.opacity = Math.random() > 0.92 ? 0.08 : 0 })
}
// Scrolling text fragments
for (let i = 0; i < 4; i++) {
const txt = k.add([
k.rect(30 + Math.random() * 40, 2), k.pos(Math.random() * W, 20 + i * (GROUND_Y / 5)),
k.color(safeColor(k, acc)), k.opacity(0.08), k.z(2),
])
txt.onUpdate(() => {
txt.pos.x -= 12 * k.dt()
if (txt.pos.x < -60) txt.pos.x = W + 20
})
}
} else if (a === 'beach') {
// Sun with rays and glow
const sunX = W * 0.8, sunY = 35
k.add([k.circle(45), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.06), k.z(0)])
k.add([k.circle(30), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.1), k.z(0)])
const sun = k.add([k.circle(18), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.5), k.z(1)])
sun.onUpdate(() => { sun.opacity = 0.4 + Math.sin(k.time() * 0.5) * 0.1 })
for (let r = 0; r < 8; r++) {
const angle = r * Math.PI / 4
const ray = k.add([k.rect(50, 2), k.pos(sunX, sunY), k.anchor('left'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.08), k.z(0), k.rotate(angle * 180 / Math.PI)])
ray.onUpdate(() => { ray.opacity = 0.05 + Math.sin(k.time() * 0.8 + r) * 0.04 })
}
// Ocean with layered waves
k.add([k.rect(W, 25), k.pos(0, GROUND_Y - 35), k.color(safeColor(k, '#1a4488')), k.opacity(0.2), k.z(1)])
for (let layer = 0; layer < 4; layer++) {
for (let i = 0; i < 5; i++) {
const wave = k.add([
k.rect(W * 0.3, 3 + layer), k.pos(Math.random() * W, GROUND_Y - 22 - layer * 6 - i * 2),
k.color(safeColor(k, layer < 2 ? '#3377bb' : '#2266aa')), k.opacity(0.18 - layer * 0.03), k.z(2),
])
const baseX = wave.pos.x
wave.onUpdate(() => { wave.pos.x = baseX + Math.sin(k.time() * (0.6 + layer * 0.2) + i * 1.5) * (15 + layer * 5) })
}
}
// Surf foam at shore
for (let i = 0; i < 6; i++) {
const foam = k.add([
k.rect(15 + Math.random() * 25, 2), k.pos(Math.random() * W, GROUND_Y - 6 + Math.random() * 4),
k.color(safeColor(k, '#ffffff')), k.opacity(0.12), k.z(3),
])
const baseX = foam.pos.x
foam.onUpdate(() => { foam.pos.x = baseX + Math.sin(k.time() * 0.8 + i) * 8; foam.opacity = 0.06 + Math.sin(k.time() + i) * 0.06 })
}
// Palm trees with segmented trunks, coconuts, swaying fronds
for (const px of [W * 0.05, W * 0.14, W * 0.88, W * 0.95]) {
const lean = px < W / 2 ? 8 : -8
const height = 8 + Math.floor(Math.random() * 3)
for (let s = 0; s < height; s++) {
const sy = GROUND_Y - 10 - s * 12
const segW = 7 - s * 0.4
k.add([k.rect(segW, 13), k.pos(px + s * lean / height, sy), k.color(safeColor(k, s % 2 === 0 ? '#3a2510' : '#2f1f0d')), k.opacity(0.5), k.z(1)])
k.add([k.rect(segW + 1, 1), k.pos(px + s * lean / height - 0.5, sy + 6), k.color(safeColor(k, '#4a3520')), k.opacity(0.2), k.z(1)])
}
const topX = px + lean, topY = GROUND_Y - 10 - height * 12
for (let c = 0; c < 2 + Math.floor(Math.random() * 2); c++) {
k.add([k.circle(2.5), k.pos(topX + (c - 1) * 4, topY + 8), k.anchor('center'), k.color(safeColor(k, '#553311')), k.opacity(0.4), k.z(1)])
}
for (let f = 0; f < 7; f++) {
const angle = -70 + f * 22
const frond = k.add([k.rect(35 + Math.random() * 10, 3), k.pos(topX, topY), k.anchor('left'), k.color(safeColor(k, f % 2 === 0 ? '#1a6600' : '#228800')), k.opacity(0.35), k.z(1), k.rotate(angle)])
frond.onUpdate(() => { frond.angle = angle + Math.sin(k.time() * 0.8 + f) * 3 })
}
}
// Seagulls drifting
for (let i = 0; i < 4; i++) {
const bx = W * 0.2 + Math.random() * W * 0.5
const by = 12 + Math.random() * 25
const wing1 = k.add([k.rect(6, 1), k.pos(bx, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(-15)])
const wing2 = k.add([k.rect(6, 1), k.pos(bx + 5, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(15)])
const startBx = bx
wing1.onUpdate(() => { wing1.pos.x = startBx + Math.sin(k.time() * 0.2 + i) * 30; wing2.pos.x = wing1.pos.x + 5 })
}
// Beach umbrella
const umbX = W * 0.35
k.add([k.rect(2, 30), k.pos(umbX, GROUND_Y - 30), k.color(safeColor(k, '#aa8844')), k.opacity(0.4), k.z(1)])
k.add([k.circle(16), k.pos(umbX + 1, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k, '#ff4444')), k.opacity(0.2), k.z(1)])
k.add([k.circle(14), k.pos(umbX + 1, GROUND_Y - 31), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.08), k.z(1)])
} else if (a === 'desert') {
// Layered sand dunes
for (let d = 0; d < 5; d++) {
const dx = W * (0.1 + d * 0.2) + (Math.random() - 0.5) * 40
const dr = 50 + d * 15 + Math.random() * 20
k.add([k.circle(dr), k.pos(dx, GROUND_Y + dr * 0.6), k.anchor('center'), k.color(safeColor(k, '#aa8844')), k.opacity(0.15 + d * 0.03), k.z(1)])
}
// Road with dashed center line
k.add([k.rect(W * 0.25, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#222222')), k.opacity(0.15), k.z(0)])
k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)])
k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.63, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)])
for (let d = 0; d < 8; d++) {
k.add([k.rect(2, 8), k.pos(W * 0.505, GROUND_Y * 0.32 + d * 16), k.color(safeColor(k, '#ffff44')), k.opacity(0.1), k.z(0)])
}
// Cacti with arms and flowers
for (const cx of [W * 0.08, W * 0.25, W * 0.75, W * 0.9]) {
const ch = 35 + Math.random() * 20
k.add([k.rect(7, ch), k.pos(cx, GROUND_Y - ch), k.color(safeColor(k, '#226622')), k.opacity(0.45), k.z(1)])
const armY = GROUND_Y - ch * 0.6
k.add([k.rect(12, 5), k.pos(cx - 12, armY), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
k.add([k.rect(5, 15), k.pos(cx - 12, armY - 15), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
if (Math.random() > 0.3) {
const armY2 = GROUND_Y - ch * 0.4
k.add([k.rect(10, 5), k.pos(cx + 7, armY2), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
k.add([k.rect(5, 12), k.pos(cx + 12, armY2 - 12), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
}
if (Math.random() > 0.5) {
k.add([k.circle(2.5), k.pos(cx + 3, GROUND_Y - ch - 2), k.anchor('center'), k.color(safeColor(k, '#ff4488')), k.opacity(0.3), k.z(2)])
}
}
// Tumbleweeds rolling
for (let i = 0; i < 3; i++) {
const tumble = k.add([k.circle(6 + Math.random() * 4), k.pos(-20 - i * 100, GROUND_Y - 8 - Math.random() * 5), k.color(safeColor(k, '#886644')), k.opacity(0.3), k.z(2)])
const speed = 15 + Math.random() * 15
tumble.onUpdate(() => { tumble.pos.x = ((tumble.pos.x + speed * k.dt()) % (W + 60)) - 30 })
}
// Heat shimmer
for (let i = 0; i < 4; i++) {
const shimmer = k.add([k.rect(W, 1), k.pos(0, GROUND_Y - 3 - i * 3), k.color(safeColor(k, '#ff8800')), k.opacity(0.06), k.z(2)])
shimmer.onUpdate(() => { shimmer.opacity = 0.03 + Math.sin(k.time() * 3 + i * 2) * 0.03 })
}
// Vulture silhouettes circling
for (let i = 0; i < 2; i++) {
const vx = W * 0.4 + i * W * 0.2
const vy = 25 + i * 15
const v1 = k.add([k.rect(8, 1), k.pos(vx, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(-10)])
const v2 = k.add([k.rect(8, 1), k.pos(vx + 7, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(10)])
const startVx = vx
v1.onUpdate(() => {
const cx = startVx + Math.cos(k.time() * 0.15 + i) * 40
const cy = vy + Math.sin(k.time() * 0.15 + i) * 15
v1.pos.x = cx; v1.pos.y = cy; v2.pos.x = cx + 7; v2.pos.y = cy
})
}
} else if (a === 'forest') {
// Layered trees (3 depth layers)
for (let layer = 0; layer < 3; layer++) {
const treeCount = layer === 0 ? 14 : layer === 1 ? 8 : 5
const treeOp = layer === 0 ? 0.15 : layer === 1 ? 0.3 : 0.4
for (let i = 0; i < treeCount; i++) {
const tx = 10 + (i / treeCount) * (W - 20) + (Math.random() - 0.5) * 15
const th = (25 + Math.random() * 35) * (layer === 0 ? 0.6 : layer === 1 ? 0.85 : 1)
const trunkW = layer === 0 ? 4 : layer === 1 ? 6 : 8
k.add([k.rect(trunkW, th), k.pos(tx, GROUND_Y - th), k.color(safeColor(k, layer === 0 ? '#0f1a08' : layer === 1 ? '#152010' : '#1a2a10')), k.opacity(treeOp), k.z(layer)])
for (let c = 0; c < 3 + layer; c++) {
const cr = (10 + Math.random() * 14) * (layer === 0 ? 0.6 : layer === 1 ? 0.8 : 1)
k.add([
k.circle(cr), k.pos(tx + trunkW / 2 + (Math.random() - 0.5) * 10, GROUND_Y - th - cr * 0.5 + c * 4),
k.anchor('center'), k.color(safeColor(k, layer === 0 ? '#0a1a08' : layer === 1 ? '#122a0e' : '#1a3a10')), k.opacity(treeOp * 0.8), k.z(layer),
])
}
}
}
// Undergrowth
for (let i = 0; i < 10; i++) {
k.add([k.circle(6 + Math.random() * 8), k.pos(Math.random() * W, GROUND_Y - 3), k.anchor('bot'), k.color(safeColor(k, '#1a3a10')), k.opacity(0.3), k.z(3)])
}
// Mushrooms
for (let i = 0; i < 5; i++) {
const mx = Math.random() * W
k.add([k.rect(2, 5), k.pos(mx, GROUND_Y - 5), k.color(safeColor(k, '#aa8866')), k.opacity(0.25), k.z(3)])
k.add([k.circle(4), k.pos(mx + 1, GROUND_Y - 6), k.anchor('center'), k.color(safeColor(k, Math.random() > 0.5 ? '#cc3333' : '#ffaa22')), k.opacity(0.2), k.z(3)])
}
// Fireflies
for (let i = 0; i < 15; i++) {
const ff = k.add([
k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 50)),
k.color(safeColor(k, '#ccff44')), k.opacity(0), k.z(3),
])
const bx = ff.pos.x, by = ff.pos.y
ff.onUpdate(() => {
ff.opacity = Math.max(0, Math.sin(k.time() * 1.5 + i * 1.8) * 0.5)
ff.pos.x = bx + Math.sin(k.time() * 0.5 + i) * 15
ff.pos.y = by + Math.cos(k.time() * 0.4 + i) * 10
})
}
// Light rays through canopy
for (let i = 0; i < 5; i++) {
const rx = W * (0.15 + i * 0.18) + (Math.random() - 0.5) * 20
const ray = k.add([k.rect(8, GROUND_Y * 0.7), k.pos(rx, 0), k.color(safeColor(k, '#aaff44')), k.opacity(0.03), k.z(2), k.rotate(3 - Math.random() * 6)])
ray.onUpdate(() => { ray.opacity = 0.02 + Math.sin(k.time() * 0.3 + i) * 0.015 })
}
// Fallen log
k.add([k.rect(45, 6), k.pos(W * 0.55, GROUND_Y - 4), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(3), k.rotate(3)])
} else if (a === 'jungle') {
// Dense vine canopy
for (let i = 0; i < 12; i++) {
const vx = 15 + i * (W / 12) + (Math.random() - 0.5) * 12
const vineLen = 30 + Math.random() * 90
for (let s = 0; s < vineLen; s += 4) {
const vineEl = k.add([k.rect(2, 5), k.pos(vx + Math.sin(s * 0.15) * 4, s), k.color(safeColor(k, '#1a4a10')), k.opacity(0.3), k.z(1)])
if (Math.random() < 0.08) {
const basePx = vineEl.pos.x
vineEl.onUpdate(() => { vineEl.pos.x = basePx + Math.sin(k.time() * 0.5 + s) * 2 })
}
}
for (let l = 0; l < 2; l++) {
k.add([k.circle(5 + Math.random() * 4), k.pos(vx + (Math.random() - 0.5) * 8, vineLen + l * 6), k.color(safeColor(k, '#22aa22')), k.opacity(0.22), k.z(1)])
}
}
// Large tropical leaves at edges
for (const side of [0, W - 30]) {
for (let l = 0; l < 4; l++) {
const ly = 15 + l * (GROUND_Y / 5)
const leaf = k.add([k.rect(35, 8), k.pos(side, ly), k.color(safeColor(k, '#1a5510')), k.opacity(0.25), k.z(3), k.rotate(side === 0 ? 15 : -15)])
leaf.onUpdate(() => { leaf.angle = (side === 0 ? 15 : -15) + Math.sin(k.time() * 0.6 + l) * 2 })
}
}
// Exotic flowers
for (let i = 0; i < 6; i++) {
const fx = Math.random() * W
const fy = 20 + Math.random() * (GROUND_Y - 40)
k.add([k.circle(3), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, ['#ff4488', '#ff8844', '#ffcc00', '#ff44ff', '#44ffcc', '#ff2222'][i])), k.opacity(0.2), k.z(2)])
}
// Ancient stone ruins peeking through
const ruinX = W * 0.5
k.add([k.rect(8, 40), k.pos(ruinX, GROUND_Y - 38), k.color(safeColor(k, '#444444')), k.opacity(0.15), k.z(0)])
k.add([k.rect(8, 35), k.pos(ruinX + 25, GROUND_Y - 33), k.color(safeColor(k, '#444444')), k.opacity(0.12), k.z(0)])
k.add([k.rect(35, 5), k.pos(ruinX - 2, GROUND_Y - 40), k.color(safeColor(k, '#444444')), k.opacity(0.1), k.z(0)])
// Thick ground fog
for (let i = 0; i < 10; i++) {
const mist = k.add([
k.circle(40 + Math.random() * 35), k.pos(Math.random() * W, GROUND_Y - 12),
k.color(safeColor(k, '#1a3818')), k.opacity(0.22), k.z(3),
])
const bx = mist.pos.x
mist.onUpdate(() => { mist.pos.x = bx + Math.sin(k.time() * 0.12 + i) * 18 })
}
// Butterfly particles
for (let i = 0; i < 4; i++) {
const bf = k.add([
k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 60)),
k.color(safeColor(k, ['#4488ff', '#ff8844', '#ffcc00', '#ff44ff'][i])), k.opacity(0.25), k.z(3),
])
const bfx = bf.pos.x, bfy = bf.pos.y
bf.onUpdate(() => {
bf.pos.x = bfx + Math.sin(k.time() * 1.2 + i * 3) * 30
bf.pos.y = bfy + Math.cos(k.time() * 0.8 + i * 2) * 20
})
}
} else if (a === 'outer_space') {
// Dense star field
for (let i = 0; i < 100; i++) {
const star = k.add([
k.circle(0.5 + Math.random() * 1.5),
k.pos(Math.random() * W, Math.random() * H),
k.color(safeColor(k, i % 8 === 0 ? '#ffccaa' : i % 5 === 0 ? '#aaaaff' : '#ffffff')),
k.opacity(0.15 + Math.random() * 0.4), k.z(1),
])
star.onUpdate(() => { star.opacity = 0.12 + Math.sin(k.time() * (0.8 + Math.random()) + i) * 0.15 })
}
// Nebula clouds
const nebulaColors = ['#4400aa', '#aa0066', '#0044aa', '#006644', '#660044']
for (let i = 0; i < 5; i++) {
for (let blob = 0; blob < 4; blob++) {
k.add([
k.circle(30 + Math.random() * 50),
k.pos(W * (0.1 + i * 0.2) + blob * 18, H * 0.25 + Math.random() * 60 + blob * 12),
k.color(safeColor(k, nebulaColors[i % nebulaColors.length])),
k.opacity(0.04 + blob * 0.012), k.z(0),
])
}
}
// Floating asteroids
for (let i = 0; i < 6; i++) {
const ast = k.add([
k.circle(4 + Math.random() * 10),
k.pos(Math.random() * W, 15 + Math.random() * (GROUND_Y - 30)),
k.color(safeColor(k, Math.random() > 0.5 ? '#555555' : '#3a3a3a')), k.opacity(0.35), k.z(1),
])
k.add([k.circle(2), k.pos(ast.pos.x + 2, ast.pos.y - 1), k.color(safeColor(k, '#2a2a2a')), k.opacity(0.2), k.z(1)])
const baseX = ast.pos.x, baseY = ast.pos.y
ast.onUpdate(() => {
ast.pos.x = baseX + Math.sin(k.time() * 0.15 + i * 2) * 18
ast.pos.y = baseY + Math.cos(k.time() * 0.12 + i) * 12
})
}
// Distant planet with rings
const planetX = W * 0.75, planetY = GROUND_Y * 0.25
k.add([k.circle(22), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#334466')), k.opacity(0.25), k.z(0)])
k.add([k.circle(24), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#223344')), k.opacity(0.1), k.z(0)])
for (let r = 0; r < 3; r++) {
k.add([k.rect(50 + r * 8, 2), k.pos(planetX - 25 - r * 4, planetY - 1 + r), k.color(safeColor(k, '#556688')), k.opacity(0.1 - r * 0.02), k.z(0), k.rotate(15)])
}
// Shooting stars
for (let i = 0; i < 2; i++) {
const shootStar = k.add([
k.rect(12, 1), k.pos(-20, Math.random() * GROUND_Y * 0.5),
k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(2), k.rotate(30),
])
let timer = Math.random() * 10
shootStar.onUpdate(() => {
timer -= k.dt()
if (timer <= 0) {
shootStar.pos.x = -20
shootStar.pos.y = Math.random() * GROUND_Y * 0.4
shootStar.opacity = 0.5
timer = 5 + Math.random() * 10
}
if (shootStar.opacity > 0) {
shootStar.pos.x += 200 * k.dt()
shootStar.pos.y += 80 * k.dt()
shootStar.opacity -= 0.8 * k.dt()
}
})
}
} else if (a === 'colosseum') {
// Roman arches along the back
for (let i = 0; i < 8; i++) {
const ax = 20 + i * (W / 8)
const aw = W / 8 - 8
k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)])
k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax + aw - 6, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)])
k.add([k.rect(aw, 6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#4a3a20')), k.opacity(0.4), k.z(1)])
k.add([k.rect(aw - 12, GROUND_Y * 0.5), k.pos(ax + 6, GROUND_Y * 0.25), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0)])
k.add([k.rect(10, 3), k.pos(ax - 2, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)])
k.add([k.rect(10, 3), k.pos(ax + aw - 10, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)])
}
// Torch brackets on pillars
for (let i = 0; i < 4; i++) {
const tx = W * (0.15 + i * 0.25)
const ty = GROUND_Y * 0.35
k.add([k.rect(3, 12), k.pos(tx, ty), k.color(safeColor(k, '#554422')), k.opacity(0.4), k.z(2)])
const flame = k.add([k.circle(5), k.pos(tx + 1, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.4), k.z(2)])
flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + i * 2) * 0.15 })
}
// Crowd silhouettes in upper tiers
for (let row = 0; row < 2; row++) {
for (let i = 0; i < 20; i++) {
k.add([
k.circle(2 + Math.random()), k.pos(15 + i * (W / 20), GROUND_Y * 0.08 + row * 12),
k.color(safeColor(k, '#2a1a10')), k.opacity(0.2), k.z(0),
])
}
}
// Crumbled stone debris
for (let i = 0; i < 8; i++) {
k.add([
k.rect(5 + Math.random() * 10, 3 + Math.random() * 6),
k.pos(Math.random() * W, GROUND_Y - 5 - Math.random() * 10),
k.color(safeColor(k, '#5a4a30')), k.opacity(0.25), k.z(2), k.rotate(Math.random() * 40 - 20),
])
}
// Sand dust particles
for (let i = 0; i < 6; i++) {
const dust = k.add([
k.circle(2 + Math.random() * 3), k.pos(Math.random() * W, GROUND_Y - 15 - Math.random() * 20),
k.color(safeColor(k, '#c0a060')), k.opacity(0.1), k.z(3),
])
const dx = dust.pos.x
dust.onUpdate(() => { dust.pos.x = dx + Math.sin(k.time() * 0.5 + i) * 12; dust.opacity = 0.05 + Math.sin(k.time() + i) * 0.05 })
}
} else if (a === 'haunted_mansion') {
// Gothic windows with moonlight
for (let i = 0; i < 5; i++) {
const wx = W * (0.1 + i * 0.2)
const wy = GROUND_Y * 0.2
k.add([k.rect(20, 35), k.pos(wx, wy), k.color(safeColor(k, '#0a0510')), k.opacity(0.4), k.z(1)])
k.add([k.rect(22, 1), k.pos(wx - 1, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)])
k.add([k.rect(22, 1), k.pos(wx - 1, wy + 35), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)])
k.add([k.rect(1, 35), k.pos(wx + 10, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)])
k.add([k.rect(20, 1), k.pos(wx, wy + 17), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)])
const glow = k.add([k.rect(16, 30), k.pos(wx + 2, wy + 3), k.color(safeColor(k, '#8844cc')), k.opacity(0.06), k.z(1)])
glow.onUpdate(() => { glow.opacity = 0.04 + Math.sin(k.time() * 0.5 + i) * 0.03 })
}
// Moon behind a window
k.add([k.circle(12), k.pos(W * 0.5 + 10, GROUND_Y * 0.26), k.anchor('center'), k.color(safeColor(k, '#ccccdd')), k.opacity(0.15), k.z(0)])
// Cobwebs
for (const cx of [15, W - 35]) {
for (let strand = 0; strand < 5; strand++) {
k.add([k.rect(20 + strand * 5, 1), k.pos(cx, 3 + strand * 4), k.color(safeColor(k, '#666666')), k.opacity(0.1), k.z(1), k.rotate(cx < W / 2 ? 28 + strand * 5 : -28 - strand * 5)])
}
}
// Candelabras
for (const cx of [W * 0.25, W * 0.75]) {
k.add([k.rect(2, 20), k.pos(cx, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)])
k.add([k.rect(12, 2), k.pos(cx - 5, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)])
for (let c = 0; c < 3; c++) {
const candleX = cx - 5 + c * 5
const cFlame = k.add([k.circle(2.5), k.pos(candleX + 1, GROUND_Y - 23), k.anchor('center'), k.color(safeColor(k, '#ffaa44')), k.opacity(0.4), k.z(2)])
cFlame.onUpdate(() => { cFlame.opacity = 0.3 + Math.sin(k.time() * 4 + c + cx) * 0.15 })
}
}
// Floating ghost particles
for (let i = 0; i < 8; i++) {
const ghost = k.add([
k.circle(3 + Math.random() * 3), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, '#aabbcc')), k.opacity(0), k.z(2),
])
const gx = ghost.pos.x, gy = ghost.pos.y
ghost.onUpdate(() => {
ghost.opacity = Math.max(0, Math.sin(k.time() * 0.6 + i * 2) * 0.15)
ghost.pos.x = gx + Math.sin(k.time() * 0.3 + i) * 20
ghost.pos.y = gy + Math.cos(k.time() * 0.25 + i) * 15
})
}
// Bats flying
for (let i = 0; i < 3; i++) {
const bat = k.add([k.rect(5, 2), k.pos(Math.random() * W, 15 + Math.random() * 40), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(2)])
const bx = bat.pos.x, by = bat.pos.y
bat.onUpdate(() => {
bat.pos.x = bx + Math.sin(k.time() * 1.5 + i * 3) * 40
bat.pos.y = by + Math.cos(k.time() * 2 + i * 2) * 10
})
}
} else if (a === 'concert_hall') {
// Stage lights with sweeping cones
const lightColors = ['#ff2288', '#22ff88', '#4488ff', '#ffaa22', '#ff44ff', '#44ffff']
for (let i = 0; i < 6; i++) {
const lx = W * (0.1 + i * 0.15)
const cone = k.add([
k.rect(30, GROUND_Y * 0.7), k.pos(lx, 10), k.anchor('top'),
k.color(safeColor(k, lightColors[i])), k.opacity(0.04), k.z(2), k.rotate(0),
])
k.add([k.circle(4), k.pos(lx + 15, 8), k.anchor('center'), k.color(safeColor(k, lightColors[i])), k.opacity(0.4), k.z(2)])
const baseAngle = (i - 3) * 5
cone.onUpdate(() => {
cone.opacity = 0.02 + Math.sin(k.time() * 1.5 + i * 1.2) * 0.025
cone.angle = baseAngle + Math.sin(k.time() * 0.4 + i) * 8
})
}
// Speaker stacks
for (const sx of [10, W - 30]) {
for (let s = 0; s < 4; s++) {
k.add([k.rect(20, 18), k.pos(sx, GROUND_Y - 20 - s * 20), k.color(safeColor(k, '#111111')), k.opacity(0.5), k.z(1)])
k.add([k.circle(6), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.4), k.z(1)])
const speakerCone = k.add([k.circle(4), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1), k.scale(1)])
speakerCone.onUpdate(() => { speakerCone.scale = k.vec2(1 + Math.sin(k.time() * 8 + s) * 0.05) })
}
}
// Crowd silhouettes
for (let i = 0; i < 30; i++) {
const cx = 5 + i * (W / 30)
const ch = 6 + Math.random() * 4
k.add([k.circle(2.5), k.pos(cx, GROUND_Y + 8), k.anchor('center'), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(3)])
k.add([k.rect(4, ch), k.pos(cx - 2, GROUND_Y + 10), k.color(safeColor(k, '#111111')), k.opacity(0.25), k.z(3)])
}
// Smoke machine haze
for (let i = 0; i < 6; i++) {
const haze = k.add([
k.circle(30 + Math.random() * 25), k.pos(Math.random() * W, GROUND_Y - 8),
k.color(safeColor(k, '#20102a')), k.opacity(0.18), k.z(3),
])
const hx = haze.pos.x
haze.onUpdate(() => { haze.pos.x = hx + Math.sin(k.time() * 0.15 + i) * 15 })
}
// Laser beam sweep
const laser = k.add([k.rect(1, GROUND_Y), k.pos(W / 2, 0), k.color(safeColor(k, '#00ff00')), k.opacity(0.06), k.z(2)])
laser.onUpdate(() => { laser.pos.x = W / 2 + Math.sin(k.time() * 0.8) * (W * 0.4) })
} else if (a === 'sports_arena') {
// Stadium seating tiers with crowd
for (let tier = 0; tier < 5; tier++) {
const ty = GROUND_Y * 0.1 + tier * (GROUND_Y * 0.13)
k.add([k.rect(W, GROUND_Y * 0.1), k.pos(0, ty), k.color(safeColor(k, tier % 2 === 0 ? '#1a2a4a' : '#152040')), k.opacity(0.3), k.z(1)])
for (let i = 0; i < 18; i++) {
k.add([
k.circle(1.5 + Math.random()), k.pos(15 + i * (W / 18), ty + GROUND_Y * 0.04),
k.color(safeColor(k, ['#cc3333', '#3333cc', '#33cc33', '#cccc33', '#ffffff'][Math.floor(Math.random() * 5)])),
k.opacity(0.12), k.z(1),
])
}
}
// Floodlights
for (const fx of [W * 0.05, W * 0.95]) {
k.add([k.rect(3, GROUND_Y * 0.7), k.pos(fx, GROUND_Y * 0.1), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(2)])
const light = k.add([k.circle(5), k.pos(fx + 1, GROUND_Y * 0.08), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.5), k.z(2)])
k.add([k.rect(40, GROUND_Y * 0.6), k.pos(fx - 18, GROUND_Y * 0.12), k.color(safeColor(k, '#ffffff')), k.opacity(0.015), k.z(0)])
light.onUpdate(() => { light.opacity = 0.4 + Math.sin(k.time() * 0.5 + fx) * 0.1 })
}
// Scoreboard
k.add([k.rect(50, 18), k.pos(W / 2 - 25, 8), k.color(safeColor(k, '#0a0a20')), k.opacity(0.5), k.z(2)])
k.add([k.rect(48, 16), k.pos(W / 2 - 24, 9), k.color(safeColor(k, '#111133')), k.opacity(0.4), k.z(2)])
const scoreGlow = k.add([k.rect(20, 8), k.pos(W / 2 - 10, 13), k.color(safeColor(k, '#ff3333')), k.opacity(0.12), k.z(2)])
scoreGlow.onUpdate(() => { scoreGlow.opacity = 0.08 + Math.sin(k.time() * 2) * 0.05 })
// Camera flashes
for (let i = 0; i < 5; i++) {
const flash = k.add([
k.circle(2), k.pos(Math.random() * W, GROUND_Y * (0.15 + Math.random() * 0.5)),
k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3),
])
flash.onUpdate(() => { flash.opacity = Math.random() > 0.995 ? 0.4 : 0 })
}
} else if (a === 'kitchen_stadium') {
// Tiled wall
for (let row = 0; row < 6; row++) {
for (let col = 0; col < 14; col++) {
k.add([
k.rect(W / 14 - 1, GROUND_Y / 6 - 1), k.pos(col * (W / 14), row * (GROUND_Y / 6)),
k.color(safeColor(k, (row + col) % 2 === 0 ? '#1a1210' : '#1e1614')), k.opacity(0.2), k.z(0),
])
}
}
// Stove with burners
k.add([k.rect(45, 35), k.pos(W * 0.06, GROUND_Y - 35), k.color(safeColor(k, '#1a1010')), k.opacity(0.45), k.z(1)])
k.add([k.rect(43, 2), k.pos(W * 0.06 + 1, GROUND_Y - 35), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)])
for (let b = 0; b < 4; b++) {
const flame = k.add([k.circle(4), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 37), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.4), k.z(2)])
const innerFlame = k.add([k.circle(2), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 38), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.3), k.z(2)])
flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + b) * 0.15; innerFlame.opacity = 0.2 + Math.sin(k.time() * 7 + b) * 0.1 })
}
// Hanging pots and pans
k.add([k.rect(W * 0.5, 2), k.pos(W * 0.2, 8), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1)])
for (let i = 0; i < 6; i++) {
const px = W * (0.22 + i * 0.08)
const hookLen = 12 + Math.random() * 10
k.add([k.rect(1, hookLen), k.pos(px, 10), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)])
k.add([k.circle(7 + Math.random() * 4), k.pos(px, 10 + hookLen + 5), k.anchor('center'), k.color(safeColor(k, i % 2 === 0 ? '#555555' : '#664422')), k.opacity(0.25), k.z(1)])
}
// Knife rack
k.add([k.rect(3, 25), k.pos(W * 0.85, GROUND_Y * 0.3), k.color(safeColor(k, '#553322')), k.opacity(0.35), k.z(1)])
for (let i = 0; i < 4; i++) {
k.add([k.rect(2, 10 + Math.random() * 5), k.pos(W * 0.85 + 1, GROUND_Y * 0.32 + i * 5), k.color(safeColor(k, '#aaaaaa')), k.opacity(0.2), k.z(1)])
}
// Steam rising
for (let i = 0; i < 4; i++) {
const steam = k.add([
k.circle(5 + Math.random() * 5), k.pos(W * 0.06 + 10 + i * 10, GROUND_Y - 40),
k.color(safeColor(k, '#ffffff')), k.opacity(0.06), k.z(3),
])
const bx = steam.pos.x
steam.onUpdate(() => {
steam.pos.y -= 10 * k.dt()
steam.pos.x = bx + Math.sin(k.time() * 2 + i) * 4
steam.opacity *= 0.995
if (steam.pos.y < GROUND_Y - 80) { steam.pos.y = GROUND_Y - 40; steam.opacity = 0.06 }
})
}
} else if (a === 'junkyard') {
// Rusted crane silhouette
k.add([k.rect(6, GROUND_Y * 0.7), k.pos(W * 0.85, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.3), k.z(0)])
k.add([k.rect(W * 0.3, 4), k.pos(W * 0.6, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.25), k.z(0)])
const cable = k.add([k.rect(1, 35), k.pos(W * 0.65, GROUND_Y * 0.12), k.color(safeColor(k, '#555555')), k.opacity(0.2), k.z(0)])
cable.onUpdate(() => { cable.pos.x = W * 0.65 + Math.sin(k.time() * 0.3) * 5 })
// Piles of scrap
for (let pile = 0; pile < 8; pile++) {
const px = Math.random() * W
const py = GROUND_Y - 8
for (let j = 0; j < 7; j++) {
k.add([
k.rect(8 + Math.random() * 18, 4 + Math.random() * 10),
k.pos(px + (Math.random() - 0.5) * 25, py - j * 6 - Math.random() * 5),
k.color(safeColor(k, ['#554433', '#665544', '#443322', '#776655', '#332211', '#887766', '#553322'][j % 7])),
k.opacity(0.3), k.z(1), k.rotate(Math.random() * 40 - 20),
])
}
}
// Old car silhouette
k.add([k.rect(35, 15), k.pos(W * 0.15, GROUND_Y - 15), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.3), k.z(1)])
k.add([k.rect(25, 10), k.pos(W * 0.15 + 5, GROUND_Y - 25), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.25), k.z(1)])
k.add([k.circle(5), k.pos(W * 0.15 + 6, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)])
k.add([k.circle(5), k.pos(W * 0.15 + 29, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)])
// Sparking wires
for (let i = 0; i < 3; i++) {
const spark = k.add([
k.circle(2), k.pos(W * (0.3 + i * 0.2), GROUND_Y - 20 - Math.random() * 30),
k.color(safeColor(k, '#ffaa00')), k.opacity(0), k.z(3),
])
spark.onUpdate(() => { spark.opacity = Math.random() > 0.96 ? 0.5 : 0 })
}
// Smoke/steam wisps
for (let i = 0; i < 4; i++) {
const smoke = k.add([
k.circle(10 + Math.random() * 10), k.pos(Math.random() * W, GROUND_Y - 5),
k.color(safeColor(k, '#333322')), k.opacity(0.12), k.z(2),
])
const sx = smoke.pos.x
smoke.onUpdate(() => {
smoke.pos.y -= 5 * k.dt()
smoke.pos.x = sx + Math.sin(k.time() * 0.5 + i) * 6
if (smoke.pos.y < GROUND_Y - 60) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.12 }
})
}
} else if (a === 'meme_dimension') {
// Floating emoji shapes (pulsing)
for (let i = 0; i < 12; i++) {
const emoji = k.add([
k.circle(6 + Math.random() * 6), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, ['#ffcc00', '#ff4444', '#44ff44', '#4444ff', '#ff44ff', '#44ffff'][i % 6])), k.opacity(0.12), k.z(1), k.scale(1),
])
const bx = emoji.pos.x, by = emoji.pos.y
emoji.onUpdate(() => {
emoji.pos.x = bx + Math.sin(k.time() * 0.4 + i) * 15
emoji.pos.y = by + Math.cos(k.time() * 0.3 + i) * 10
emoji.scale = k.vec2(1 + Math.sin(k.time() * 1.5 + i) * 0.15)
})
}
// Glitch bands
for (let i = 0; i < 8; i++) {
const band = k.add([
k.rect(W, 2 + Math.random() * 4), k.pos(0, Math.random() * GROUND_Y),
k.color(safeColor(k, Math.random() > 0.5 ? '#ff44ff' : '#44ffff')), k.opacity(0), k.z(2),
])
band.onUpdate(() => {
band.opacity = Math.random() > 0.97 ? 0.12 + Math.random() * 0.08 : 0
if (band.opacity > 0) band.pos.x = (Math.random() - 0.5) * 10
else band.pos.x = 0
})
}
// Rainbow pixel streaks
const rainbowC = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff']
for (let i = 0; i < rainbowC.length; i++) {
const streak = k.add([
k.rect(W, 3), k.pos(0, GROUND_Y * 0.3 + i * 5),
k.color(safeColor(k, rainbowC[i])), k.opacity(0.04), k.z(0),
])
streak.onUpdate(() => { streak.opacity = 0.02 + Math.sin(k.time() * 2 + i) * 0.02 })
}
// Spinning geometric shapes
for (let i = 0; i < 4; i++) {
const geo = k.add([
k.rect(8 + Math.random() * 8, 8 + Math.random() * 8),
k.pos(W * (0.2 + i * 0.2), GROUND_Y * (0.3 + Math.random() * 0.3)),
k.anchor('center'), k.color(safeColor(k, '#ff44ff')), k.opacity(0.08), k.z(1), k.rotate(0),
])
geo.onUpdate(() => { geo.angle += (30 + i * 15) * k.dt() })
}
// VHS tracking lines
const vhs = k.add([k.rect(W, 6), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(3)])
vhs.onUpdate(() => {
vhs.pos.y = (vhs.pos.y + 80 * k.dt()) % (GROUND_Y + 20)
vhs.opacity = Math.random() > 0.9 ? 0.15 : 0
})
// RGB noise flickers
for (let i = 0; i < 6; i++) {
const noise = k.add([
k.rect(3 + Math.random() * 8, 3 + Math.random() * 8),
k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, ['#ff0000', '#00ff00', '#0000ff'][i % 3])), k.opacity(0), k.z(2),
])
noise.onUpdate(() => {
noise.opacity = Math.random() > 0.98 ? 0.1 : 0
if (noise.opacity > 0) { noise.pos.x = Math.random() * W; noise.pos.y = Math.random() * GROUND_Y }
})
}
} else if (a === 'space_station') {
// Structural beams with rivets
for (let i = 0; i < 6; i++) {
const bx = W * (0.15 + i * 0.14)
k.add([k.rect(5, GROUND_Y), k.pos(bx, 0), k.color(safeColor(k, '#1a1a30')), k.opacity(0.3), k.z(1)])
for (let r = 0; r < 8; r++) {
k.add([k.circle(1), k.pos(bx + 2, r * (GROUND_Y / 8) + 5), k.anchor('center'), k.color(safeColor(k, '#2a2a40')), k.opacity(0.25), k.z(1)])
}
k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.25), k.color(safeColor(k, '#1a1a30')), k.opacity(0.25), k.z(1)])
k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.6), k.color(safeColor(k, '#1a1a30')), k.opacity(0.2), k.z(1)])
}
// Viewport
const vpX = W * 0.5, vpY = GROUND_Y * 0.3
k.add([k.circle(40), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#000020')), k.opacity(0.5), k.z(0)])
k.add([k.circle(42), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#2a2a50')), k.opacity(0.3), k.z(0)])
for (let s = 0; s < 15; s++) {
const dist = Math.random() * 35
const angle = Math.random() * Math.PI * 2
const sx = vpX + Math.cos(angle) * dist
const sy = vpY + Math.sin(angle) * dist
const vpStar = k.add([k.circle(0.8 + Math.random()), k.pos(sx, sy), k.color(safeColor(k, '#ffffff')), k.opacity(0.3), k.z(0)])
vpStar.onUpdate(() => { vpStar.opacity = 0.15 + Math.sin(k.time() * 2 + s) * 0.15 })
}
// Earth through viewport
k.add([k.circle(15), k.pos(vpX + 10, vpY + 5), k.anchor('center'), k.color(safeColor(k, '#2244aa')), k.opacity(0.15), k.z(0)])
k.add([k.circle(6), k.pos(vpX + 7, vpY + 2), k.anchor('center'), k.color(safeColor(k, '#22aa44')), k.opacity(0.08), k.z(0)])
// Control panels
for (const px of [W * 0.08, W * 0.88]) {
k.add([k.rect(25, 40), k.pos(px, GROUND_Y * 0.4), k.color(safeColor(k, '#0a0a20')), k.opacity(0.4), k.z(1)])
for (let led = 0; led < 8; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(px + 4 + (led % 4) * 5, GROUND_Y * 0.42 + Math.floor(led / 4) * 6),
k.color(safeColor(k, ['#00ff88', '#ff4444', '#ffaa00', '#4488ff'][led % 4])), k.opacity(0.4), k.z(2),
])
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.05 ? 0.4 : 0.1 })
}
}
// Warning stripes
for (let i = 0; i < 8; i++) {
k.add([k.rect(8, 3), k.pos(W * 0.3 + i * 12, GROUND_Y - 3), k.color(safeColor(k, i % 2 === 0 ? '#ffaa00' : '#000000')), k.opacity(0.12), k.z(3)])
}
} else if (a === 'savanna') {
// Distant mountain range
for (let i = 0; i < 5; i++) {
const mx = W * (0.1 + i * 0.2) + (Math.random() - 0.5) * 30
const mh = 30 + Math.random() * 40
k.add([k.circle(mh), k.pos(mx, GROUND_Y - mh * 0.3), k.anchor('center'), k.color(safeColor(k, '#2a2010')), k.opacity(0.12), k.z(0)])
}
// Acacia trees with branches
for (const tx of [W * 0.1, W * 0.4, W * 0.72]) {
const trunkH = 40 + Math.random() * 20
k.add([k.rect(5, trunkH), k.pos(tx, GROUND_Y - trunkH), k.color(safeColor(k, '#2a1a08')), k.opacity(0.4), k.z(1)])
k.add([k.rect(15, 3), k.pos(tx - 10, GROUND_Y - trunkH * 0.7), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(-20)])
k.add([k.rect(12, 3), k.pos(tx + 5, GROUND_Y - trunkH * 0.6), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(15)])
k.add([k.rect(50, 5), k.pos(tx - 22, GROUND_Y - trunkH - 2), k.color(safeColor(k, '#2a3a10')), k.opacity(0.3), k.z(1)])
k.add([k.rect(40, 4), k.pos(tx - 17, GROUND_Y - trunkH - 7), k.color(safeColor(k, '#2a3a10')), k.opacity(0.25), k.z(1)])
k.add([k.rect(25, 3), k.pos(tx - 10, GROUND_Y - trunkH - 11), k.color(safeColor(k, '#223308')), k.opacity(0.2), k.z(1)])
}
// Setting sun
const sunX = W * 0.85, sunY = GROUND_Y * 0.22
k.add([k.circle(50), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.06), k.z(0)])
k.add([k.circle(35), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.12), k.z(0)])
const savSun = k.add([k.circle(22), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffaa22')), k.opacity(0.3), k.z(0)])
savSun.onUpdate(() => { savSun.opacity = 0.25 + Math.sin(k.time() * 0.3) * 0.05 })
// Tall grass swaying
for (let i = 0; i < 20; i++) {
const gx = Math.random() * W
const grassH = 5 + Math.random() * 10
const grass = k.add([k.rect(1, grassH), k.pos(gx, GROUND_Y - grassH), k.color(safeColor(k, '#8a7830')), k.opacity(0.2), k.z(3), k.rotate(0)])
grass.onUpdate(() => { grass.angle = Math.sin(k.time() * 1.2 + i) * 5 })
}
// Elephant silhouette
const ex = W * 0.6, ey = GROUND_Y - 18
k.add([k.rect(18, 12), k.pos(ex, ey), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
k.add([k.rect(8, 8), k.pos(ex + 15, ey - 4), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
k.add([k.rect(3, 8), k.pos(ex + 2, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
k.add([k.rect(3, 8), k.pos(ex + 13, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
// Birds in V formation
for (let i = 0; i < 5; i++) {
const birdX = W * 0.3 + i * 8
const birdY = 20 + Math.abs(i - 2) * 5
k.add([k.rect(4, 1), k.pos(birdX, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(-12)])
k.add([k.rect(4, 1), k.pos(birdX + 3, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(12)])
}
} else if (a === 'server_room') {
// Server rack rows with LEDs
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 6; col++) {
const rx = 30 + col * (W / 6)
const ry = 20 + row * 55
const rw = W / 6 - 12
const rh = 46
k.add([k.rect(rw, rh), k.pos(rx, ry), k.color(safeColor(k, '#0a1a10')), k.opacity(0.4), k.z(1)])
k.add([k.rect(rw, 1), k.pos(rx, ry), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
k.add([k.rect(rw, 1), k.pos(rx, ry + rh), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
for (let s = 0; s < 5; s++) {
k.add([k.rect(rw - 4, 1), k.pos(rx + 2, ry + 10 + s * 7), k.color(safeColor(k, '#0a1008')), k.opacity(0.3), k.z(1)])
}
for (let led = 0; led < 4; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(rx + 3 + led * 5, ry + 3),
k.color(safeColor(k, '#00ff88')), k.opacity(0.5), k.z(2),
])
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.04 ? 0.5 : 0.1 })
}
if (Math.random() < 0.5) {
const act = k.add([k.rect(2, 2), k.pos(rx + rw - 6, ry + 3), k.color(safeColor(k, '#ffaa00')), k.opacity(0.5), k.z(2)])
act.onUpdate(() => { act.opacity = Math.random() > 0.3 ? 0.5 : 0.15 })
}
}
}
// Overhead cable trays
for (let i = 0; i < 3; i++) {
k.add([k.rect(W * 0.25, 3), k.pos(W * (0.1 + i * 0.3), 5), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
for (let c = 0; c < 4; c++) {
k.add([k.rect(W * 0.25, 1), k.pos(W * (0.1 + i * 0.3), 7 + c * 2), k.color(safeColor(k, ['#004400', '#000044', '#440000', '#444400'][c])), k.opacity(0.15), k.z(1)])
}
}
// Cool air flow particles
for (let i = 0; i < 5; i++) {
const airflow = k.add([
k.circle(2), k.pos(Math.random() * W, GROUND_Y - 10),
k.color(safeColor(k, '#00ff88')), k.opacity(0.08), k.z(3),
])
airflow.onUpdate(() => {
airflow.pos.y -= 15 * k.dt()
if (airflow.pos.y < 10) { airflow.pos.y = GROUND_Y - 10; airflow.pos.x = Math.random() * W }
})
}
// Emergency lighting
const emLight = k.add([k.circle(4), k.pos(W * 0.5, 5), k.anchor('center'), k.color(safeColor(k, '#ff0000')), k.opacity(0.2), k.z(2)])
emLight.onUpdate(() => { emLight.opacity = 0.1 + Math.sin(k.time() * 1.5) * 0.1 })
} else if (a === 'paper_mill') {
// Industrial ceiling pipes
for (let i = 0; i < 5; i++) {
const py = 5 + i * 4
k.add([k.rect(W, 3), k.pos(0, py), k.color(safeColor(k, i % 2 === 0 ? '#2a2a20' : '#222218')), k.opacity(0.35), k.z(1)])
for (let j = 0; j < 4; j++) {
k.add([k.circle(3), k.pos(W * (0.2 + j * 0.2), py + 1), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)])
}
}
// Conveyor belt
k.add([k.rect(W * 0.6, 6), k.pos(W * 0.2, GROUND_Y - 15), k.color(safeColor(k, '#333328')), k.opacity(0.35), k.z(1)])
for (let i = 0; i < 8; i++) {
k.add([k.circle(3), k.pos(W * 0.22 + i * (W * 0.075), GROUND_Y - 12), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.3), k.z(1)])
}
// Moving items on belt
for (let i = 0; i < 3; i++) {
const item = k.add([
k.rect(10, 6), k.pos(W * 0.25 + i * 60, GROUND_Y - 21),
k.color(safeColor(k, '#f0e68c')), k.opacity(0.2), k.z(2),
])
item.onUpdate(() => {
item.pos.x += 15 * k.dt()
if (item.pos.x > W * 0.78) item.pos.x = W * 0.2
})
}
// Paper rolls stacked
for (let i = 0; i < 3; i++) {
for (let j = 0; j <= i; j++) {
const rx = W * 0.08 + j * 14 - i * 7
const ry = GROUND_Y - 12 - (2 - i) * 12
k.add([k.circle(6), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#ddd8b0')), k.opacity(0.2), k.z(1)])
k.add([k.circle(2), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#aaa880')), k.opacity(0.15), k.z(1)])
}
}
// Industrial gears (rotating)
for (const gx of [W * 0.88, W * 0.92]) {
const gy = GROUND_Y * 0.35
k.add([k.circle(10), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1)])
k.add([k.circle(4), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)])
for (let t = 0; t < 8; t++) {
const tooth = k.add([
k.rect(4, 3), k.pos(gx, gy), k.anchor('center'),
k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1),
k.rotate(t * 45),
])
tooth.onUpdate(() => { tooth.angle = t * 45 + k.time() * (gx === W * 0.88 ? 20 : -25) })
}
}
// Steam from pipes
for (let i = 0; i < 4; i++) {
const steam = k.add([
k.circle(6 + Math.random() * 8), k.pos(W * (0.2 + i * 0.2), 25),
k.color(safeColor(k, '#ccccbb')), k.opacity(0.06), k.z(2),
])
const bsx = steam.pos.x
steam.onUpdate(() => {
steam.pos.y += 4 * k.dt()
steam.pos.x = bsx + Math.sin(k.time() + i) * 5
steam.opacity *= 0.998
if (steam.pos.y > GROUND_Y * 0.4) { steam.pos.y = 25; steam.opacity = 0.06 }
})
}
// Sawdust particles
for (let i = 0; i < 8; i++) {
const dust = k.add([
k.circle(1), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, '#f0e68c')), k.opacity(0.12), k.z(3),
])
const dx = dust.pos.x, dy = dust.pos.y
dust.onUpdate(() => {
dust.pos.x = dx + Math.sin(k.time() * 0.8 + i) * 10
dust.pos.y = dy + Math.cos(k.time() * 0.5 + i) * 8
})
}
}
}
k.scene('fight', () => {
// === SKY / BACKGROUND ATMOSPHERE ===
// Vertical gradient backdrop — 6 bands from dark ceiling to ground horizon
const bgBase = theme.bg
for (let band = 0; band < 6; band++) {
const bandH = GROUND_Y / 6
const brighten = band * 3
k.add([
k.rect(W, bandH + 1), k.pos(0, band * bandH),
k.color(safeColor(k,bgBase)),
k.opacity(1 - band * 0.04),
])
}
// Subtle horizon glow — wide soft band just above ground
k.add([
k.rect(W, 20), k.pos(0, GROUND_Y - 20),
k.color(safeColor(k,theme.accent)), k.opacity(0.06),
])
// === GROUND ===
// Main ground fill
k.add([k.rect(W, H - GROUND_Y + 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.ground))])
// Bright edge line at the surface
k.add([k.rect(W, 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.accent)), k.opacity(0.7)])
// Secondary highlight
k.add([k.rect(W, 1), k.pos(0, GROUND_Y + 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3)])
// Ground gradient — fades deeper
for (let i = 1; i <= 12; i++) {
k.add([k.rect(W, 2), k.pos(0, GROUND_Y + 3 + i * 6), k.color(safeColor(k,theme.accent)), k.opacity(0.1 - i * 0.007)])
}
// Perspective grid on the floor — converging lines for 3D depth
const vanishX = W / 2
const vanishY = GROUND_Y * 0.6
for (let i = 0; i <= 24; i++) {
const x = i * (W / 24)
// Lines from floor edge pointing toward vanishing point
const dx = x - vanishX
const topX = vanishX + dx * 0.3
k.add([
k.rect(1, GROUND_Y * 0.4 + 10), k.pos(x, GROUND_Y),
k.color(safeColor(k,theme.accent)), k.opacity(0.05),
])
}
// Horizontal depth lines on ground
for (let i = 0; i < 6; i++) {
const gy = GROUND_Y + 8 + i * i * 3
if (gy < H) {
k.add([k.rect(W, 1), k.pos(0, gy), k.color(safeColor(k,theme.accent)), k.opacity(0.08 - i * 0.012)])
}
}
// === SIDE PILLARS / FRAME ===
// Subtle dark pillars on edges for framing
k.add([k.rect(8, GROUND_Y), k.pos(0, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
k.add([k.rect(8, GROUND_Y), k.pos(W - 8, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
// Accent trim on pillars
k.add([k.rect(1, GROUND_Y), k.pos(8, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
k.add([k.rect(1, GROUND_Y), k.pos(W - 9, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
// === AMBIENT PARTICLES ===
// Floating dust/embers in the air (subtle, always present)
for (let i = 0; i < 18; i++) {
const particle = k.add([
k.circle(0.5 + Math.random() * 1.5),
k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k,theme.accent)),
k.opacity(0.08 + Math.random() * 0.12),
k.z(2),
])
const baseX = particle.pos.x
const baseY = particle.pos.y
const driftSpeed = 0.1 + Math.random() * 0.3
const driftAmt = 8 + Math.random() * 15
particle.onUpdate(() => {
particle.pos.x = baseX + Math.sin(k.time() * driftSpeed + i * 1.3) * driftAmt
particle.pos.y = baseY + Math.cos(k.time() * driftSpeed * 0.7 + i) * (driftAmt * 0.5)
particle.opacity = 0.06 + Math.sin(k.time() * 0.8 + i * 0.9) * 0.06
})
}
// Arena-specific decorations
drawArenaDecor()
// Umpire chair (center-back, behind fighters)
const CHAIR_X = W / 2
const CHAIR_SEAT_Y = GROUND_Y * (0.38 + (1 - SF) * 0.2)
const cs = SF // chair scale
k.add([k.rect(4 * cs, GROUND_Y - CHAIR_SEAT_Y + 15 * cs), k.pos(CHAIR_X - 22 * cs, CHAIR_SEAT_Y - 8 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
k.add([k.rect(4 * cs, GROUND_Y - CHAIR_SEAT_Y + 15 * cs), k.pos(CHAIR_X + 18 * cs, CHAIR_SEAT_Y - 8 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
for (let i = 0; i < 6; i++) {
const rungY = CHAIR_SEAT_Y + 15 * cs + i * ((GROUND_Y - CHAIR_SEAT_Y - 15 * cs) / 6)
k.add([k.rect(44 * cs, 3), k.pos(CHAIR_X - 22 * cs, rungY), k.color(safeColor(k,'#443322')), k.opacity(0.6), k.z(3)])
}
k.add([k.rect(54 * cs, 5), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 2), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)])
k.add([k.rect(54 * cs, 1), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3), k.z(3)])
k.add([k.rect(48 * cs, 4), k.pos(CHAIR_X - 24 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)])
k.add([k.rect(3, 12 * cs), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
k.add([k.rect(3, 12 * cs), k.pos(CHAIR_X + 24 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
k.add([
k.sprite('judge', { anim: 'idle' }),
k.pos(CHAIR_X, CHAIR_SEAT_Y),
k.anchor('bot'),
k.scale(1.3 * SF),
k.z(4),
k.opacity(0.9),
'judge',
])
const scaleA = (1.8 + botA.tier * 0.4) * SF
k.add([k.sprite('botA', { anim: 'idle' }), k.pos(-100, GROUND_Y - 6), k.anchor('bot'), k.scale(scaleA), k.z(10), k.opacity(0), 'fighterA'])
const scaleB = (1.8 + botB.tier * 0.4) * SF
k.add([k.sprite('botB', { anim: 'idle' }), k.pos(W + 100, GROUND_Y - 6), k.anchor('bot'), k.scale(-scaleB, scaleB), k.z(10), k.opacity(0), 'fighterB'])
config.onReady?.()
})
k.go('fight')
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),
])
}
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<void> {
return new Promise(resolve => {
const prop = k.add([
isCircle ? k.circle(w / 2) : k.rect(w, h),
k.pos(fromX, fromY),
k.color(safeColor(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 = setInterval(() => {
if (!def.exists()) { clearInterval(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)
clearInterval(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 = setInterval(() => {
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)
clearInterval(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 = setInterval(() => {
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)
clearInterval(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<string, PropDef> = {
// Food
pizza: { parts: [
{ shape: 'rect', x: -7, y: -3, w: 14, h: 3, color: '#ffcc44' }, // crust
{ shape: 'rect', x: -6, y: 0, w: 12, h: 8, color: '#ff8800' }, // cheese
{ shape: 'rect', x: -5, y: 1, w: 10, h: 6, color: '#cc4400' }, // sauce
{ shape: 'circle', x: -3, y: 3, w: 3, h: 3, color: '#aa2200' }, // pepperoni
{ shape: 'circle', x: 2, y: 4, w: 3, h: 3, color: '#aa2200' }, // pepperoni
{ shape: 'circle', x: 0, y: 1, w: 2, h: 2, color: '#44aa22' }, // olive
]},
burger: { parts: [
{ shape: 'rect', x: -8, y: -4, w: 16, h: 4, color: '#cc8833' }, // top bun
{ shape: 'rect', x: -7, y: 0, w: 14, h: 3, color: '#44aa22' }, // lettuce
{ shape: 'rect', x: -7, y: 3, w: 14, h: 4, color: '#883311' }, // patty
{ shape: 'rect', x: -8, y: 7, w: 16, h: 3, color: '#cc8833' }, // bottom bun
{ shape: 'rect', x: -7, y: -1, w: 14, h: 1, color: '#ffcc00' }, // cheese
]},
hotdog: { parts: [
{ shape: 'rect', x: -10, y: -2, w: 20, h: 4, color: '#ffcc66' }, // bun
{ shape: 'rect', x: -9, y: -1, w: 18, h: 3, color: '#cc4422' }, // sausage
{ shape: 'rect', x: -8, y: -2, w: 16, h: 1, color: '#ffee00' }, // mustard
]},
banana: { parts: [
{ shape: 'rect', x: -3, y: -8, w: 5, h: 16, color: '#ffe135' },
{ shape: 'rect', x: -2, y: -9, w: 3, h: 2, color: '#88660d' }, // stem
{ shape: 'rect', x: -2, y: -7, w: 3, h: 14, color: '#ffee55' }, // highlight
]},
pie: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 16, h: 16, color: '#f5deb3' }, // crust
{ shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' }, // cream
{ shape: 'circle', x: -2, y: -2, w: 3, h: 3, color: '#ff4444' }, // cherry
]},
// Weapons
gun: { parts: [
{ shape: 'rect', x: 0, y: -3, w: 24, h: 6, color: '#444444' }, // barrel
{ shape: 'rect', x: -4, y: -5, w: 12, h: 10, color: '#333333' }, // body
{ shape: 'rect', x: -2, y: 5, w: 6, h: 10, color: '#553322' }, // grip
{ shape: 'rect', x: 22, y: -2, w: 4, h: 4, color: '#555555' }, // muzzle
{ shape: 'rect', x: 6, y: -6, w: 8, h: 3, color: '#555555' }, // sight
]},
shotgun: { parts: [
{ shape: 'rect', x: 0, y: -2, w: 35, h: 5, color: '#333333' }, // barrel
{ shape: 'rect', x: 0, y: 3, w: 30, h: 4, color: '#333333' }, // under barrel
{ shape: 'rect', x: -6, y: -4, w: 10, h: 12, color: '#553322' }, // stock
{ shape: 'rect', x: -2, y: 7, w: 6, h: 8, color: '#664433' }, // grip
{ shape: 'rect', x: 33, y: -1, w: 4, h: 3, color: '#555555' }, // muzzle
]},
rocketLauncher: { parts: [
{ shape: 'rect', x: 0, y: -5, w: 30, h: 10, color: '#446633' }, // tube
{ shape: 'circle', x: 28, y: 0, w: 10, h: 10, color: '#335522' }, // end
{ shape: 'circle', x: 2, y: 0, w: 10, h: 10, color: '#335522' }, // back end
{ shape: 'rect', x: 8, y: 5, w: 6, h: 8, color: '#553322' }, // grip
{ shape: 'rect', x: 14, y: -8, w: 6, h: 4, color: '#555555' }, // sight
]},
minigun: { parts: [
{ shape: 'rect', x: 0, y: -6, w: 28, h: 3, color: '#444444' }, // barrel 1
{ shape: 'rect', x: 0, y: -2, w: 28, h: 3, color: '#555555' }, // barrel 2
{ shape: 'rect', x: 0, y: 2, w: 28, h: 3, color: '#444444' }, // barrel 3
{ shape: 'rect', x: -8, y: -8, w: 10, h: 16, color: '#333333' }, // housing
{ shape: 'rect', x: -4, y: 5, w: 6, h: 10, color: '#553322' }, // grip
{ shape: 'circle', x: 27, y: -2, w: 8, h: 8, color: '#666666' }, // muzzle ring
]},
sniper: { parts: [
{ shape: 'rect', x: 0, y: -1, w: 38, h: 3, color: '#333333' }, // barrel
{ shape: 'rect', x: -6, y: -4, w: 14, h: 8, color: '#444444' }, // body
{ shape: 'rect', x: -10, y: -3, w: 8, h: 4, color: '#553322' }, // stock
{ shape: 'circle', x: 12, y: -6, w: 6, h: 6, color: '#225588' }, // scope
{ shape: 'rect', x: 8, y: -7, w: 10, h: 2, color: '#555555' }, // scope mount
]},
katana: { parts: [
{ shape: 'rect', x: 0, y: -1, w: 40, h: 2, color: '#ccccdd' }, // blade
{ shape: 'rect', x: 38, y: -1, w: 4, h: 2, color: '#ffffff' }, // tip
{ shape: 'rect', x: -2, y: -3, w: 4, h: 6, color: '#ffcc00' }, // guard
{ shape: 'rect', x: -10, y: -2, w: 10, h: 4, color: '#442211' }, // handle
{ shape: 'rect', x: -8, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap
{ shape: 'rect', x: -5, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap
]},
// Vehicles (used as props in throws/drops)
car_mini: { parts: [
{ shape: 'rect', x: -12, y: -4, w: 24, h: 8, color: '#ff4444' }, // body
{ shape: 'rect', x: -6, y: -8, w: 12, h: 5, color: '#4488ff' }, // roof/windows
{ shape: 'circle', x: -8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel
{ shape: 'circle', x: 8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel
{ shape: 'rect', x: 11, y: -2, w: 3, h: 3, color: '#ffee00' }, // headlight
]},
tank_mini: { parts: [
{ shape: 'rect', x: -14, y: 0, w: 28, h: 8, color: '#556633' }, // hull
{ shape: 'rect', x: -8, y: -6, w: 16, h: 7, color: '#445522' }, // turret
{ shape: 'rect', x: 6, y: -5, w: 16, h: 3, color: '#333322' }, // barrel
{ shape: 'rect', x: -13, y: 7, w: 26, h: 4, color: '#333333' }, // tracks
{ shape: 'circle', x: -10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel
{ shape: 'circle', x: 10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel
]},
// Misc props
bomb: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#222222' },
{ shape: 'rect', x: -1, y: -9, w: 3, h: 5, color: '#888888' }, // fuse
{ shape: 'circle', x: 0, y: -10, w: 4, h: 4, color: '#ff6600' }, // spark
]},
shield: { parts: [
{ shape: 'rect', x: -10, y: -14, w: 20, h: 28, color: '#4466aa' },
{ shape: 'rect', x: -8, y: -12, w: 16, h: 24, color: '#5577bb' },
{ shape: 'rect', x: -3, y: -8, w: 6, h: 16, color: '#ffcc00' }, // emblem cross
{ shape: 'rect', x: -7, y: -3, w: 14, h: 6, color: '#ffcc00' }, // emblem cross
{ shape: 'rect', x: -10, y: -14, w: 20, h: 2, color: '#6688cc' }, // top edge
]},
soccerball: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' },
{ shape: 'rect', x: -3, y: -3, w: 6, h: 6, color: '#222222' }, // pentagon
]},
donut: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff88cc' }, // icing
{ shape: 'circle', x: 0, y: 0, w: 6, h: 6, color: '#cc8833' }, // hole (body color)
{ shape: 'circle', x: -3, y: -3, w: 2, h: 2, color: '#ff4444' }, // sprinkle
{ shape: 'circle', x: 3, y: -2, w: 2, h: 2, color: '#44ff44' }, // sprinkle
{ shape: 'circle', x: 1, y: 4, w: 2, h: 2, color: '#ffff00' }, // sprinkle
]},
watermelon: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#33aa33' }, // rind
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff4466' }, // flesh
{ shape: 'circle', x: -2, y: -1, w: 2, h: 2, color: '#222222' }, // seed
{ shape: 'circle', x: 3, y: 2, w: 2, h: 2, color: '#222222' }, // seed
{ shape: 'circle', x: 0, y: 4, w: 2, h: 2, color: '#222222' }, // seed
]},
chainsaw: { parts: [
{ shape: 'rect', x: 0, y: -2, w: 28, h: 5, color: '#888888' }, // blade
{ shape: 'rect', x: -8, y: -5, w: 12, h: 10, color: '#ff6600' }, // body
{ shape: 'rect', x: -4, y: 5, w: 6, h: 7, color: '#553322' }, // grip
{ shape: 'rect', x: 26, y: -1, w: 4, h: 3, color: '#aaaaaa' }, // tip
{ shape: 'rect', x: 0, y: -3, w: 26, h: 1, color: '#666666' }, // chain top
{ shape: 'rect', x: 0, y: 3, w: 26, h: 1, color: '#666666' }, // chain bottom
]},
// Fatality props
rubber_chicken: { parts: [
{ shape: 'rect', x: -2, y: -12, w: 5, h: 20, color: '#ffdd44' }, // body
{ shape: 'circle', x: 0, y: -14, w: 8, h: 8, color: '#ffee55' }, // head
{ shape: 'rect', x: 2, y: -16, w: 8, h: 3, color: '#ff8800' }, // beak
{ shape: 'circle', x: -1, y: -15, w: 2, h: 2, color: '#000000' }, // eye
{ shape: 'rect', x: -1, y: 6, w: 3, h: 5, color: '#ff6600' }, // leg
]},
anvil: { parts: [
{ shape: 'rect', x: -14, y: -4, w: 28, h: 12, color: '#555555' }, // body
{ shape: 'rect', x: -18, y: 6, w: 36, h: 5, color: '#444444' }, // base
{ shape: 'rect', x: -10, y: -8, w: 20, h: 5, color: '#666666' }, // top
{ shape: 'rect', x: 8, y: -12, w: 6, h: 5, color: '#777777' }, // horn
]},
flyswatter: { parts: [
{ shape: 'rect', x: -2, y: 0, w: 4, h: 35, color: '#cc8833' }, // handle
{ shape: 'rect', x: -12, y: -14, w: 24, h: 16, color: '#44aa44' }, // paddle
{ shape: 'rect', x: -10, y: -12, w: 20, h: 12, color: '#338833' }, // grid
]},
toilet: { parts: [
{ shape: 'rect', x: -12, y: -4, w: 24, h: 16, color: '#eeeeee' }, // bowl
{ shape: 'rect', x: -14, y: -14, w: 28, h: 12, color: '#dddddd' }, // tank
{ shape: 'circle', x: 0, y: 4, w: 16, h: 16, color: '#aaddff' }, // water
{ shape: 'rect', x: -12, y: -5, w: 24, h: 3, color: '#cccccc' }, // seat
{ shape: 'rect', x: 8, y: -12, w: 4, h: 6, color: '#888888' }, // handle
]},
keyboard: { parts: [
{ shape: 'rect', x: -20, y: -3, w: 40, h: 8, color: '#333333' }, // base
{ shape: 'rect', x: -18, y: -2, w: 36, h: 6, color: '#444444' }, // keys area
{ shape: 'rect', x: -16, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: -10, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: -4, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: 2, y: -1, w: 12, h: 3, color: '#666666' }, // spacebar
]},
disco_ball: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#cccccc' }, // ball
{ shape: 'rect', x: -1, y: -12, w: 2, h: 5, color: '#888888' }, // chain
{ shape: 'rect', x: -3, y: -2, w: 3, h: 3, color: '#ffffff' }, // mirror
{ shape: 'rect', x: 2, y: 1, w: 3, h: 3, color: '#eeeeee' }, // mirror
{ shape: 'rect', x: -1, y: 4, w: 3, h: 3, color: '#ffffff' }, // mirror
]},
rubber_duck: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 10, h: 10, color: '#ffdd00' }, // body
{ shape: 'circle', x: 3, y: -5, w: 7, h: 7, color: '#ffee22' }, // head
{ shape: 'rect', x: 5, y: -6, w: 5, h: 2, color: '#ff8800' }, // beak
{ shape: 'circle', x: 4, y: -6, w: 2, h: 2, color: '#000000' }, // eye
]},
baguette: { parts: [
{ shape: 'rect', x: -16, y: -3, w: 32, h: 6, color: '#cc9944' }, // bread
{ shape: 'rect', x: -14, y: -2, w: 28, h: 4, color: '#ddaa55' }, // crust highlight
{ shape: 'rect', x: -8, y: -4, w: 4, h: 1, color: '#bb8833' }, // score
{ shape: 'rect', x: 2, y: -4, w: 4, h: 1, color: '#bb8833' }, // score
]},
boot: { parts: [
{ shape: 'rect', x: -8, y: -16, w: 14, h: 24, color: '#553322' }, // shaft
{ shape: 'rect', x: -8, y: 6, w: 22, h: 8, color: '#442211' }, // sole
{ shape: 'rect', x: -6, y: -14, w: 10, h: 4, color: '#664433' }, // top band
{ shape: 'rect', x: 12, y: 4, w: 4, h: 6, color: '#442211' }, // toe
]},
cannon: { parts: [
{ shape: 'rect', x: -10, y: -6, w: 28, h: 12, color: '#444444' }, // barrel
{ shape: 'circle', x: -10, y: 0, w: 14, h: 14, color: '#333333' }, // base
{ shape: 'circle', x: 16, y: 0, w: 10, h: 10, color: '#555555' }, // muzzle
{ shape: 'rect', x: -6, y: 6, w: 6, h: 8, color: '#664433' }, // wheel
]},
stamp: { parts: [
{ shape: 'rect', x: -3, y: -18, w: 6, h: 14, color: '#884422' }, // handle
{ shape: 'rect', x: -14, y: -6, w: 28, h: 8, color: '#cc4444' }, // stamp face
{ shape: 'rect', x: -12, y: -4, w: 24, h: 4, color: '#aa2222' }, // ink
]},
vending_machine: { parts: [
{ shape: 'rect', x: -12, y: -18, w: 24, h: 36, color: '#cc3333' }, // body
{ shape: 'rect', x: -10, y: -14, w: 12, h: 20, color: '#222222' }, // window
{ shape: 'rect', x: 3, y: -8, w: 6, h: 4, color: '#44aa44' }, // button
{ shape: 'rect', x: 3, y: 0, w: 6, h: 4, color: '#4444aa' }, // button
{ shape: 'rect', x: -8, y: 10, w: 10, h: 5, color: '#333333' }, // dispenser
]},
lawn_mower: { parts: [
{ shape: 'rect', x: -10, y: -2, w: 20, h: 8, color: '#44aa44' }, // body
{ shape: 'circle', x: -8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel
{ shape: 'circle', x: 8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel
{ shape: 'rect', x: -2, y: -12, w: 3, h: 12, color: '#888888' }, // handle
{ shape: 'rect', x: -4, y: -14, w: 8, h: 3, color: '#666666' }, // grip
]},
}
// 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<string, string> = {
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<string, string> = {
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 ===
async function playBlock(side: 'a' | 'b') {
const defender = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!defender) return
const dir = side === 'a' ? 1 : -1
// Spawn shield in front
const shieldObjs = spawnWeaponProp('shield', defender.pos.x + dir * 20, defender.pos.y - 25, side === 'b', 19)
sfxBlock()
// Flash shield
shieldObjs.forEach(o => { o.opacity = 0.9 })
await k.wait(0.15)
// Impact sparks
spawnSparks(defender.pos.x + dir * 25, defender.pos.y - 25, 6, '#4488ff')
k.shake(3)
await k.wait(0.3)
// Shield fades
shieldObjs.forEach(o => {
k.tween(o.opacity, 0, 0.2, (v: number) => { o.opacity = v }).then(() => { if (o.exists()) o.destroy() })
})
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 = setInterval(() => {
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)
clearInterval(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)
}
const choreographyMap: Record<string, typeof dashPunch> = {
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,
// --- 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
interface MorphOverlay { obj: any; type: string }
let activeMorphs: MorphOverlay[] = []
function destroyMorphOverlays() {
activeMorphs.forEach(m => { if (m.obj.exists()) m.obj.destroy() })
activeMorphs = []
}
// Morph 0: ELEMENTAL — fire/ice/electric aura wraps around the fighter
function applyElementalMorph(fighter: any, side: 'a' | 'b') {
const seed = side === 'a' ? botA.seed : botB.seed
let h = 0
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
const element = Math.abs(h) % 3 // 0=fire, 1=ice, 2=electric
const colors = [
['#ff4400', '#ff8800', '#ffcc00'], // fire
['#44ccff', '#88eeff', '#ffffff'], // ice
['#ffff00', '#88ff00', '#00ffff'], // electric
][element]
// Aura glow behind fighter
const aura = k.add([
k.circle(45 + Math.random() * 10),
k.pos(fighter.pos.x, fighter.pos.y - 20),
k.color(safeColor(k,colors[0])),
k.opacity(0.15),
k.z(fighter.z - 1),
k.anchor('center'),
k.scale(1),
])
aura.onUpdate(() => {
aura.pos.x = fighter.pos.x
aura.pos.y = fighter.pos.y - 20
aura.opacity = 0.1 + Math.sin(k.time() * 8) * 0.08
const s = 1 + Math.sin(k.time() * 6) * 0.15
aura.scale.x = s; aura.scale.y = s
})
activeMorphs.push({ obj: aura, type: 'elemental' })
// Orbiting particles
for (let i = 0; i < 6; i++) {
const p = k.add([
k.circle(2 + Math.random() * 2),
k.pos(fighter.pos.x, fighter.pos.y),
k.color(safeColor(k,colors[1 + (i % 2)])),
k.opacity(0.6),
k.z(fighter.z + 1),
k.anchor('center'),
])
const angle = (i / 6) * Math.PI * 2
const radius = 30 + Math.random() * 15
p.onUpdate(() => {
const a = angle + k.time() * (3 + i * 0.5)
p.pos.x = fighter.pos.x + Math.cos(a) * radius
p.pos.y = fighter.pos.y - 20 + Math.sin(a) * radius * 0.6
p.opacity = 0.4 + Math.sin(k.time() * 10 + i) * 0.3
})
activeMorphs.push({ obj: p, type: 'elemental' })
}
// Color tint the fighter
fighter.color = safeColor(k,colors[0])
fighter.opacity = 0.9
}
// Morph 1: MECH — armor plates, wings/jets, metallic overlay
function applyMechMorph(fighter: any, _side: 'a' | 'b') {
const dir = fighter.scale.x > 0 ? 1 : -1
// Shoulder armor plates
for (let s = 0; s < 2; s++) {
const sDir = s === 0 ? -1 : 1
const shoulder = k.add([
k.rect(12, 8), k.pos(fighter.pos.x + sDir * 25 * dir, fighter.pos.y - 30),
k.color(safeColor(k,'#888899')), k.opacity(0.85), k.z(fighter.z + 1), k.anchor('center'),
])
shoulder.onUpdate(() => {
shoulder.pos.x = fighter.pos.x + sDir * 25 * dir
shoulder.pos.y = fighter.pos.y - 30 + Math.sin(k.time() * 4) * 2
})
activeMorphs.push({ obj: shoulder, type: 'mech' })
}
// Jet wings + flame boosters
for (let w = 0; w < 2; w++) {
const wingDir = w === 0 ? -1 : 1
const wing = k.add([
k.rect(6, 20 + w * 5), k.pos(fighter.pos.x - 30 * dir * wingDir, fighter.pos.y - 15),
k.color(safeColor(k,'#5566aa')), k.opacity(0.7), k.z(fighter.z - 1), k.anchor('center'),
k.rotate(wingDir * 15 * dir),
])
wing.onUpdate(() => {
wing.pos.x = fighter.pos.x - 30 * dir * wingDir
wing.pos.y = fighter.pos.y - 15
})
activeMorphs.push({ obj: wing, type: 'mech' })
const flame = k.add([
k.rect(4, 8 + Math.random() * 6),
k.pos(fighter.pos.x - 30 * dir * wingDir, fighter.pos.y + 5),
k.color(safeColor(k,'#ff6600')),
k.opacity(0.6), k.z(fighter.z - 2), k.anchor('center'),
])
flame.onUpdate(() => {
flame.pos.x = fighter.pos.x - 30 * dir * wingDir + (Math.random() - 0.5) * 3
flame.pos.y = fighter.pos.y + 5 + Math.random() * 4
flame.opacity = 0.3 + Math.random() * 0.4
flame.color = safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')
})
activeMorphs.push({ obj: flame, type: 'mech' })
}
// Visor glow
const visor = k.add([
k.rect(20, 4), k.pos(fighter.pos.x + 5 * dir, fighter.pos.y - 35),
k.color(safeColor(k,'#00ffaa')), k.opacity(0.7), k.z(fighter.z + 2), k.anchor('center'),
])
visor.onUpdate(() => {
visor.pos.x = fighter.pos.x + 5 * dir
visor.pos.y = fighter.pos.y - 35
visor.opacity = 0.5 + Math.sin(k.time() * 12) * 0.3
})
activeMorphs.push({ obj: visor, type: 'mech' })
// Metallic tint
fighter.color = safeColor(k,'#aabbcc')
fighter.opacity = 0.95
}
// Morph 2: BEAST — grows horns/tail/claws, goes wild
function applyBeastMorph(fighter: any, side: 'a' | 'b') {
const dir = fighter.scale.x > 0 ? 1 : -1
const seed = side === 'a' ? botA.seed : botB.seed
let h = 0
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
const beastType = Math.abs(h >> 4) % 3 // 0=demon, 1=wolf, 2=dragon
const beastColors = [
['#cc2222', '#ff4444', '#880000'], // demon
['#666688', '#aaaacc', '#444466'], // wolf
['#228844', '#44cc66', '#115533'], // dragon
][beastType]
// Horns
for (let horn = 0; horn < 2; horn++) {
const hDir = horn === 0 ? -1 : 1
const h1 = k.add([
k.rect(3, 12 + Math.random() * 6),
k.pos(fighter.pos.x + hDir * 10, fighter.pos.y - 45),
k.color(safeColor(k,beastColors[0])),
k.opacity(0.9), k.z(fighter.z + 2), k.anchor('bot'),
k.rotate(hDir * (20 + Math.random() * 15)),
])
h1.onUpdate(() => {
h1.pos.x = fighter.pos.x + hDir * 10
h1.pos.y = fighter.pos.y - 45
})
activeMorphs.push({ obj: h1, type: 'beast' })
}
// Tail
for (let s = 0; s < 5; s++) {
const seg = k.add([
k.circle(4 - s * 0.5),
k.pos(fighter.pos.x - dir * (20 + s * 8), fighter.pos.y - 5 + s * 2),
k.color(safeColor(k,beastColors[1])),
k.opacity(0.8), k.z(fighter.z - 1), k.anchor('center'),
])
seg.onUpdate(() => {
const wave = Math.sin(k.time() * 5 + s * 0.8) * (4 + s * 2)
seg.pos.x = fighter.pos.x - dir * (20 + s * 8)
seg.pos.y = fighter.pos.y - 5 + s * 2 + wave
})
activeMorphs.push({ obj: seg, type: 'beast' })
}
// Claws on hands
for (let c = 0; c < 2; c++) {
const cDir = c === 0 ? -1 : 1
for (let cl = 0; cl < 3; cl++) {
const claw = k.add([
k.rect(2, 6), k.pos(fighter.pos.x + cDir * 22, fighter.pos.y - 18 + cl * 3),
k.color(safeColor(k,beastColors[2])),
k.opacity(0.8), k.z(fighter.z + 1), k.anchor('center'),
k.rotate(cDir * (30 + cl * 10)),
])
claw.onUpdate(() => {
claw.pos.x = fighter.pos.x + cDir * 22
claw.pos.y = fighter.pos.y - 18 + cl * 3
})
activeMorphs.push({ obj: claw, type: 'beast' })
}
}
// Wild eye glow
const eyeGlow = k.add([
k.circle(4), k.pos(fighter.pos.x + 5 * dir, fighter.pos.y - 36),
k.color(safeColor(k,'#ff0000')), k.opacity(0.6), k.z(fighter.z + 2), k.anchor('center'),
])
eyeGlow.onUpdate(() => {
eyeGlow.pos.x = fighter.pos.x + 5 * dir
eyeGlow.pos.y = fighter.pos.y - 36
eyeGlow.opacity = 0.4 + Math.sin(k.time() * 15) * 0.4
})
activeMorphs.push({ obj: eyeGlow, type: 'beast' })
// Size increase + color tint
fighter.color = safeColor(k,beastColors[0])
fighter.opacity = 0.9
}
const morphAppliers = [applyElementalMorph, applyMechMorph, applyBeastMorph]
const morphNames = ['ELEMENTAL FORM', 'MECH ARMOR', 'BEAST MODE']
// === THE CREATOR: OMNI-MORPH ===
// Instead of 3 morph types, the creator morphs into a random archetype each time
const OMNI_MORPH_ARCHETYPES = [
'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat', 'cactus', 'pizza',
'mushroom', 'shark', 'penguin', 'octopus', 'skeleton', 'ghost', 'alien', 'dinosaur',
'pirate', 'ninja', 'cowboy', 'wizard', 'bee', 'frog', 'snail',
'robot', 'android', 'toaster', 'mech', 'minotaur', 'unicorn', 'phoenix', 'dragon',
'mermaid', 'griffin', 'cyclops', 'gargoyle', 'golem', 'vampire', 'werewolf', 'zombie',
'witch', 'demon', 'chef', 'firefighter', 'astronaut', 'clown', 'detective',
'lumberjack', 'scientist', 'wrestler', 'boxer', 'gladiator', 'samurai', 'viking', 'knight',
'elephant', 'giraffe', 'hippo', 'lion', 'monkey', 'parrot', 'raccoon', 'snake',
'turtle', 'whale', 'crocodile', 'flamingo', 'hedgehog', 'panda', 'hamster',
'sock_puppet', 'traffic_cone', 'toilet_man', 'potato', 'cloud_man', 'rock_man',
'balloon_man', 'trash_can', 'rubber_duck', 'snowman', 'scarecrow', 'jack_o_lantern',
'garden_gnome', 'lamp_post', 'broom_man',
]
let omniMorphCounter = 0
async function applyCreatorOmniMorph(fighter: any, side: 'a' | 'b'): Promise<string> {
const pick = OMNI_MORPH_ARCHETYPES[Math.floor(Math.random() * OMNI_MORPH_ARCHETYPES.length)]
// Generate and load a sprite sheet for the picked archetype
const bot = side === 'a' ? botA : botB
const colors = side === 'a' ? colorsA : colorsB
const morphKey = `omniMorph_${side}_${omniMorphCounter++}`
try {
const morphSheet = generateSpriteSheet(bot.seed, bot.tier, colors.primary, colors.secondary, pick)
await loadSpriteWithTimeout(morphKey, morphSheet, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
// Swap the fighter's sprite to the morphed archetype
fighter.use(k.sprite(morphKey, { anim: fighter.curAnim() || 'idle' }))
} catch (err) {
console.warn('[FightScene] omni-morph sprite failed:', err)
}
// Golden aura unique to creator morph
const aura = k.add([
k.circle(55),
k.pos(fighter.pos.x, fighter.pos.y - 20),
k.color(safeColor(k, '#c8a000')),
k.opacity(0.2),
k.z(fighter.z - 1),
k.anchor('center'),
k.scale(1),
])
aura.onUpdate(() => {
aura.pos.x = fighter.pos.x
aura.pos.y = fighter.pos.y - 20
aura.opacity = 0.12 + Math.sin(k.time() * 6) * 0.1
const s = 1 + Math.sin(k.time() * 4) * 0.2
aura.scale.x = s; aura.scale.y = s
})
activeMorphs.push({ obj: aura, type: 'omni' })
// Orbiting golden ₿ symbols (8 in dual rings, varied sizes)
for (let i = 0; i < 8; i++) {
const ring = i < 5 ? 0 : 1 // inner ring (5) + outer ring (3)
const ringCount = ring === 0 ? 5 : 3
const ringIdx = ring === 0 ? i : i - 5
const sz = ring === 0 ? 10 + Math.random() * 4 : 14 + Math.random() * 4
const rad = ring === 0 ? 40 : 60
const spd = ring === 0 ? 3 : -2 // counter-rotate outer ring
const btc = k.add([
k.text('₿', { size: sz }),
k.pos(fighter.pos.x, fighter.pos.y - 20),
k.color(safeColor(k, i % 3 === 0 ? '#ffd700' : i % 3 === 1 ? '#ffee88' : '#ff8c00')),
k.opacity(0.8),
k.z(fighter.z + 2),
k.anchor('center'),
k.rotate(0),
])
const baseAngle = (ringIdx / ringCount) * Math.PI * 2
btc.onUpdate(() => {
const a = baseAngle + k.time() * spd
btc.pos.x = fighter.pos.x + Math.cos(a) * rad
btc.pos.y = fighter.pos.y - 20 + Math.sin(a) * rad * 0.5
btc.opacity = 0.7 + Math.sin(k.time() * 6 + i * 1.3) * 0.3
btc.angle = Math.sin(k.time() * 4 + i) * 20
})
activeMorphs.push({ obj: btc, type: 'omni' })
}
// Apply archetype-specific color tint based on pick category
const tints: Record<string, string> = {
fire: '#ff4400', ice: '#44ccff', nature: '#44cc44', dark: '#8844cc',
metal: '#aabbcc', electric: '#ffff00', beast: '#cc6622', cosmic: '#cc44ff',
}
const category =
['phoenix', 'dragon', 'demon'].includes(pick) ? 'fire' :
['penguin', 'snowman', 'yeti'].includes(pick) ? 'ice' :
['cactus', 'mushroom', 'frog', 'snail', 'turtle'].includes(pick) ? 'nature' :
['skeleton', 'ghost', 'vampire', 'zombie', 'witch'].includes(pick) ? 'dark' :
['robot', 'android', 'mech', 'toaster', 'knight'].includes(pick) ? 'metal' :
['alien', 'unicorn', 'mermaid'].includes(pick) ? 'cosmic' :
['werewolf', 'minotaur', 'lion', 'shark', 'crocodile'].includes(pick) ? 'beast' :
'electric'
fighter.color = safeColor(k, tints[category] || '#ffd700')
fighter.opacity = 0.9
return pick
}
// Get morph order for a bot (deterministic by seed)
function getMorphOrder(seed: string): number[] {
let h = 0
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
const orders = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]
return orders[Math.abs(h) % 6]
}
async function playMorph(fighter: any, side: 'a' | 'b', morphIndex: number) {
const savedScaleX = fighter.scale.x
const savedScaleY = fighter.scale.y
const isHumanFighter = side === 'a' ? isHumanA : isHumanB
const morphSpriteKey = side === 'a' ? 'botA_morph' : 'botB_morph'
const normalSpriteKey = side === 'a' ? 'botA' : 'botB'
// Flash + announcement
screenFlash('#ffffff', 0.15)
sfxZoomWhoosh()
sfxSpecial()
const name = side === 'a' ? botA.name : botB.name
announceHype(`${name}${morphNames[morphIndex]}!`)
// Scale up dramatically
const growFactor = 1.3
await k.tween(0, 1, 0.3, (t) => {
fighter.scale.x = savedScaleX * (1 + t * (growFactor - 1))
fighter.scale.y = savedScaleY * (1 + t * (growFactor - 1))
}, k.easings.easeOutBack)
// Human fighters morph into bots (opposite direction!)
if (isHumanFighter) {
fighter.use(k.sprite(morphSpriteKey, { anim: 'idle' }))
}
// Apply the morph visuals
morphAppliers[morphIndex](fighter, side)
// Shockwave + shake
spawnShockwave(fighter.pos.x, fighter.pos.y - 20, '#ffffff')
k.shake(8)
glitchRGB(0.2)
return {
revert: async () => {
destroyMorphOverlays()
screenFlash(theme.accent, 0.1)
fighter.color = safeColor(k,'#ffffff')
fighter.opacity = 1
// Revert human fighters back to their human sprite
if (isHumanFighter) {
fighter.use(k.sprite(normalSpriteKey, { anim: 'idle' }))
}
await k.tween(0, 1, 0.2, (t) => {
fighter.scale.x = savedScaleX * growFactor + t * (savedScaleX - savedScaleX * growFactor)
fighter.scale.y = savedScaleY * growFactor + t * (savedScaleY - savedScaleY * growFactor)
}, k.easings.easeInQuad)
},
}
}
// === EMOTION & SPORTSMANSHIP SYSTEM ===
const heartfeltLines = [
'That bot fights like it\'s got student loans to pay!',
'I\'m not crying, you\'re crying!',
'Someone get this bot a movie deal!',
'This is better than anything on Netflix right now!',
'That bot has more heart than most politicians!',
'I haven\'t been this moved since my last Windows update!',
'The crowd is ugly crying and I respect that!',
'This bot has main character energy!',
'We\'re witnessing greatness and I don\'t say that lightly!',
'Even the judge is getting emotional!',
'Someone call their developer, they\'d be so proud!',
'This is the kind of fight you tell your grandkids about!',
'That\'s not a bot, that\'s a WARRIOR!',
'More drama than a Congressional hearing!',
'They left it all in the ring! And by all, I mean EVERYTHING!',
'The crowd is standing! The ones who can still stand, anyway!',
'Somebody film this before it gets taken down!',
'This fight has more plot twists than a Netflix original!',
'That bot fights like rent is due tomorrow!',
'My heart! My poor little heart!',
]
const crowdSympathyLines = [
'Pour one out for the fallen!',
'That\'s rough, buddy...',
'Somebody hug that bot!',
'They tried their best, which is... concerning!',
'At least they have a great personality!',
'Their developer still loves them! Probably!',
'It\'s a learning experience! A very painful one!',
'Better luck next patch!',
'Participation trophy incoming!',
'The exit is to your left!',
'Have you tried turning it off and on again?',
'Even their antivirus felt that!',
'They\'ll be in therapy for epochs after this!',
'At least they looked good losing!',
'That bot just became a cautionary tale!',
'Someone start a GoFundMe for their repairs!',
'Their training data did NOT prepare them for this!',
'That bot needs a hug and a firmware update!',
]
const respectLines = [
'Good fight.', 'Respect.', 'GG no re.',
'Same time next week?',
'You\'re built different. Not better, but different.',
'We should start a podcast.',
'That was actually fun. Don\'t tell anyone.',
'Your developer should be proud. Probably.',
'Ten out of ten, would fight again.',
'We\'re not so different, you and I.',
'I\'d swipe right.', 'You single?',
'Tell your GPU I said hi.',
'No hard feelings. Just hard hits.',
'You fight like someone who reads documentation.',
]
// Sanitize text for Kaplay (treats [ ] as styled text tags)
function safeText(t: string): string {
return t
.replace(/[\[\]{}<>`\\|~^]/g, '') // Kaplay styled-text tags and problematic chars
.replace(/[\x00-\x1f\x7f]/g, ' ') // Control characters → space
.replace(/[^\x20-\x7e]/g, '') // Strip non-ASCII (emoji, unicode) — Kaplay can't render them
.replace(/\s+/g, ' ') // Collapse whitespace
.trim()
}
// Speech bubble above a fighter — colourful with tail
// Active speech bubbles — destroy previous before showing new
let activeBubbleA: any[] = []
let activeBubbleB: any[] = []
function destroyBubble(els: any[]) {
els.forEach(el => { if (el.exists()) el.destroy() })
els.length = 0
}
function showSpeechBubble(side: 'a' | 'b', text: string, duration: number = 3) {
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!fighter || !text) return
// Kill previous bubble on this side
if (side === 'a') destroyBubble(activeBubbleA)
else destroyBubble(activeBubbleB)
const allEls: any[] = []
// Sanitize and truncate
const clean = safeText(text)
if (!clean) return
const maxLen = 80
const display = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '...' : clean
const lines = wrapBubbleText(display, 18)
const fontSize = 12
const lineH = fontSize + 4
const padX = 10, padY = 8
const textW = Math.max(60, Math.min(lines.reduce((m, l) => Math.max(m, l.length * (fontSize * 0.6)), 0) + 8, W * 0.38))
const bubbleW = textW + padX * 2
const bubbleH = lines.length * lineH + padY * 2
const tailSize = 10
// Position — push bubble to the outside of the bot so it doesn't overlap the sprite
const offsetX = side === 'a' ? -bubbleW * 0.7 : bubbleW * 0.7
const bx = Math.max(bubbleW / 2 + 4, Math.min(W - bubbleW / 2 - 4, fighter.pos.x + offsetX))
const by = Math.max(8, fighter.pos.y - 75 - bubbleH)
const borderColor = side === 'a' ? '#00f0ff' : '#ff2d7b'
const bgColor = side === 'a' ? '#0a1e2a' : '#2a0a1e'
// Single border rect (no glow/accent layers — keeps rendering clean)
const border = k.add([
k.rect(bubbleW + 4, bubbleH + 4),
k.pos(bx - bubbleW / 2 - 2, by - 2),
k.color(safeColor(k, borderColor)), k.opacity(0.85), k.z(54),
])
allEls.push(border)
// Bubble body
const bubble = k.add([
k.rect(bubbleW, bubbleH),
k.pos(bx - bubbleW / 2, by),
k.color(safeColor(k, bgColor)), k.opacity(0.95), k.z(55),
])
allEls.push(bubble)
// Tail — pixel arrow pointing down to fighter
const tailX = bx + (side === 'a' ? bubbleW * 0.15 : -bubbleW * 0.15)
const tailBaseY = by + bubbleH
for (let row = 0; row < tailSize; row++) {
const tw = tailSize - row
// Border pixel row
const tailBorder = k.add([
k.rect(tw + 2, 1),
k.pos(tailX - (tw + 2) / 2, tailBaseY + row),
k.color(safeColor(k, borderColor)), k.opacity(0.85), k.z(54),
])
allEls.push(tailBorder)
// Inner pixel row
if (tw > 2) {
const tailInner = k.add([
k.rect(tw - 1, 1),
k.pos(tailX - (tw - 1) / 2, tailBaseY + row),
k.color(safeColor(k, bgColor)), k.opacity(0.95), k.z(55),
])
allEls.push(tailInner)
}
}
// Render text to an offscreen canvas, then display as a sprite
// This bypasses kaplay's text rendering which has color issues
const textCanvas = document.createElement('canvas')
textCanvas.width = bubbleW
textCanvas.height = bubbleH
const tc = textCanvas.getContext('2d')!
tc.fillStyle = '#ffffff'
tc.font = `${fontSize}px monospace`
tc.textBaseline = 'top'
for (let i = 0; i < lines.length; i++) {
tc.fillText(lines[i], padX, padY + i * lineH + 2)
}
const spriteKey = `bubble_${side}_${Date.now()}`
k.loadSprite(spriteKey, textCanvas.toDataURL()).then(() => {
const tEl = k.add([
k.sprite(spriteKey),
k.pos(bx - bubbleW / 2, by),
k.opacity(1), k.z(57),
])
allEls.push(tEl)
})
// Border glow pulse
border.onUpdate(() => {
border.opacity = 0.7 + Math.sin(k.time() * 4) * 0.15
})
// Pop-in animation
const anchorX = bx, anchorY = by + bubbleH
allEls.forEach(el => {
const origX = el.pos.x, origY = el.pos.y
const origOpacity = el.opacity
el.pos.x = anchorX + (origX - anchorX) * 0.1
el.pos.y = anchorY + (origY - anchorY) * 0.1
el.opacity = 0
k.tween(0, 1, 0.25, (t) => {
el.pos.x = anchorX + (origX - anchorX) * t
el.pos.y = anchorY + (origY - anchorY) * t
el.opacity = origOpacity * t
}, k.easings.easeOutBack)
})
// Store reference
if (side === 'a') activeBubbleA = allEls
else activeBubbleB = allEls
// Fade out after duration
setTimeout(() => {
allEls.forEach(el => {
if (!el.exists()) return
k.tween(el.opacity, 0, 0.35, (v) => { el.opacity = v }).then(() => {
if (el.exists()) el.destroy()
})
})
}, duration * 1000)
}
function wrapBubbleText(text: string, maxChars: number): string[] {
const words = text.split(' ')
const lines: string[] = []
let line = ''
for (const word of words) {
if (line.length + word.length + 1 > maxChars && line.length > 0) {
lines.push(line)
line = word
} else {
line = line ? line + ' ' + word : word
}
}
if (line) lines.push(line)
// Max 5 lines, last line truncated
if (lines.length > 5) {
const truncated = [...lines.slice(0, 4), lines.slice(4).join(' ').slice(0, maxChars - 3) + '...']
return truncated
}
return lines
}
// Talking mouth animation — a small rectangle that opens/closes near the fighter's face
const talkingAnims: Record<string, { objs: any[]; timer: ReturnType<typeof setInterval> }> = {}
function startTalking(side: 'a' | 'b') {
stopTalking(side) // cleanup any existing
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!fighter?.exists()) return
const mouthColor = side === 'a' ? '#00f0ff' : '#ff2d7b'
// Mouth position: slightly below center of the sprite
const mouthOffsetY = -28
const mouthW = 8
let open = false
const mouth = k.add([
k.rect(mouthW, 2),
k.pos(fighter.pos.x, fighter.pos.y + mouthOffsetY),
k.color(safeColor(k, mouthColor)),
k.opacity(0.9),
k.z(12),
k.anchor('center'),
])
// Toggle open/close every 120ms for a fast chatter effect
const timer = setInterval(() => {
if (!mouth.exists() || !fighter.exists()) { stopTalking(side); return }
open = !open
mouth.pos.x = fighter.pos.x
mouth.pos.y = fighter.pos.y + mouthOffsetY
mouth.height = open ? 5 : 2
}, 120)
talkingAnims[side] = { objs: [mouth], timer }
}
function stopTalking(side: 'a' | 'b') {
const anim = talkingAnims[side]
if (!anim) return
clearInterval(anim.timer)
anim.objs.forEach(o => { if (o.exists()) o.destroy() })
delete talkingAnims[side]
}
// Spawn floating text above a position
function spawnEmoteText(x: number, y: number, text: string, color: string, duration: number = 1.2) {
const label = k.add([
k.text(safeText(text), { size: 10 }),
k.pos(x, y - 10),
k.color(safeColor(k,color)),
k.opacity(1),
k.z(60),
k.anchor('center'),
])
k.tween(label.pos.y, y - 50, duration, (v) => { label.pos.y = v }, k.easings.easeOutQuad)
k.tween(1, 0, duration, (v) => { label.opacity = v }, k.easings.easeInQuad).then(() => {
if (label.exists()) label.destroy()
})
}
// Spawn a heart particle
function spawnHeart(x: number, y: number) {
const colors = ['#ff4466', '#ff6688', '#ff2244', '#ff88aa', '#cc2244']
const heart = k.add([
k.text('\u2665', { size: 12 + Math.random() * 8 }),
k.pos(x + (Math.random() - 0.5) * 40, y),
k.color(safeColor(k,colors[Math.floor(Math.random() * colors.length)])),
k.opacity(0.8),
k.z(58),
k.anchor('center'),
])
const driftX = (Math.random() - 0.5) * 30
k.tween(heart.pos.y, y - 60 - Math.random() * 40, 1.5, (v) => {
heart.pos.y = v
heart.pos.x += driftX * 0.01
}, k.easings.easeOutQuad)
k.tween(0.8, 0, 1.5, (v) => { heart.opacity = v }).then(() => { if (heart.exists()) heart.destroy() })
}
// Crowd signs: pop up from bottom with text, bob, then fade out
function spawnCrowdSigns(count: number, color: string, text?: string) {
const labels = text ? [text] : ['GO!', 'WOW', 'KO!', 'LOL', 'GG', 'OMG']
for (let i = 0; i < count; i++) {
const x = W * 0.15 + Math.random() * W * 0.7
const startY = H + 20
const endY = GROUND_Y + 10 + Math.random() * 20
const label = labels[Math.floor(Math.random() * labels.length)]
// Sign stick
const stick = k.add([
k.rect(3, 25),
k.pos(x, startY),
k.color(safeColor(k, '#886644')),
k.opacity(0.8),
k.z(8),
k.anchor('bot'),
])
// Sign face
const sign = k.add([
k.rect(Math.max(30, label.length * 10), 18),
k.pos(x, startY - 25),
k.color(safeColor(k, color)),
k.opacity(0.85),
k.z(8),
k.anchor('center'),
])
// Sign text
const txt = k.add([
k.text(label, { size: 10 }),
k.pos(x, startY - 25),
k.color(safeColor(k, '#ffffff')),
k.opacity(0.9),
k.z(9),
k.anchor('center'),
])
const delay = i * 0.08
// Rise up
k.wait(delay, () => {
k.tween(startY, endY, 0.3, (v) => { stick.pos.y = v; sign.pos.y = v - 25; txt.pos.y = v - 25 }, k.easings.easeOutBack)
})
// Bob + fade out after 1.5s
k.wait(delay + 1.5, () => {
k.tween(0.85, 0, 0.5, (v) => { stick.opacity = v; sign.opacity = v; txt.opacity = v }).then(() => {
if (stick.exists()) stick.destroy()
if (sign.exists()) sign.destroy()
if (txt.exists()) txt.destroy()
})
})
}
}
// === HUMAN OWNER APPEARANCES ===
// The silly human behind each bot occasionally shows up
/** Spawn a human as a "coach" standing behind the fighter, cheering/panicking */
async function spawnHumanCoach(side: 'a' | 'b', mood: 'cheer' | 'panic' | 'coach') {
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const homeX = side === 'a' ? HOME_A - 60 : HOME_B + 60
const tag = `human_${side}`
// Don't spawn if already visible
if (k.get(tag).length > 0) return
const human = k.add([
k.sprite(spriteId, { anim: mood }),
k.pos(homeX, GROUND_Y - 4),
k.anchor('bot'),
k.scale(side === 'a' ? 1.2 : -1.2, 1.2),
k.z(5),
k.opacity(0),
tag,
])
// Fade in
await k.tween(0, 0.85, 0.3, (v) => { human.opacity = v }, k.easings.easeOutQuad)
await k.wait(2.0)
// Fade out
await k.tween(0.85, 0, 0.5, (v) => { human.opacity = v }, k.easings.easeInQuad)
if (human.exists()) human.destroy()
}
/** Human runs across the screen (comedic moment) */
async function humanRunAcross(side: 'a' | 'b') {
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const fromLeft = side === 'a'
const startX = fromLeft ? -40 : W + 40
const endX = fromLeft ? W + 40 : -40
const human = k.add([
k.sprite(spriteId, { anim: 'run' }),
k.pos(startX, GROUND_Y - 4),
k.anchor('bot'),
k.scale(fromLeft ? 1.3 : -1.3, 1.3),
k.z(8),
k.opacity(0.9),
])
// Run across screen
await k.tween(startX, endX, 1.5, (v) => { human.pos.x = v }, k.easings.linear)
if (human.exists()) human.destroy()
}
/** Human briefly replaces the bot sprite (jumps in to fight, fails hilariously) */
async function humanJumpsIn(side: 'a' | 'b') {
const fighterTag = side === 'a' ? 'fighterA' : 'fighterB'
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const fighter = k.get(fighterTag)[0] as Fighter
if (!fighter) return
// Spawn human at fighter position
const human = k.add([
k.sprite(spriteId, { anim: 'panic' }),
k.pos(fighter.pos.x, fighter.pos.y),
k.anchor('bot'),
k.scale(fighter.scale.x / 3, fighter.scale.y / 3),
k.z(fighter.z + 1),
k.opacity(0),
])
// Hide bot, show human
const origOpacity = fighter.opacity
fighter.opacity = 0
await k.tween(0, 1, 0.15, (v) => { human.opacity = v })
// Human flails around for a moment
human.play('panic')
await k.wait(0.4)
human.play('cheer')
sfxRandomSilly()
await k.wait(0.3)
human.play('panic')
await k.wait(0.3)
// Run away scared
const escapeX = side === 'a' ? -80 : W + 80
human.play('run')
await k.tween(human.pos.x, escapeX, 0.5, (v) => { human.pos.x = v }, k.easings.easeInQuad)
// Restore bot
fighter.opacity = origOpacity
if (human.exists()) human.destroy()
}
/** Random chance to show a human moment — called between rounds */
async function maybeShowHuman() {
const roll = Math.random()
if (roll > 0.15) return // 15% chance per round
const side: 'a' | 'b' = Math.random() < 0.5 ? 'a' : 'b'
const action = Math.random()
if (action < 0.4) {
// Coach appearance (most common)
const mood = Math.random() < 0.5 ? 'cheer' : 'panic'
await spawnHumanCoach(side, mood)
} else if (action < 0.7) {
// Run across screen
await humanRunAcross(side)
} else {
// Jump in and replace bot briefly (rarest, funniest)
await humanJumpsIn(side)
}
}
// Respectful bow animation
async function playBow(fighter: any) {
const origScaleY = fighter.scale.y
await k.tween(fighter.scale.y, origScaleY * 0.85, 0.2, (v) => { fighter.scale.y = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await k.tween(fighter.scale.y, origScaleY, 0.2, (v) => { fighter.scale.y = v }, k.easings.easeOutQuad)
}
// Fist bump between two fighters
async function playFistBump(fighterA: any, fighterB: any) {
const midX = (fighterA.pos.x + fighterB.pos.x) / 2
const origAX = fighterA.pos.x
const origBX = fighterB.pos.x
await Promise.all([
k.tween(fighterA.pos.x, midX - 15, 0.3, (v) => { fighterA.pos.x = v }, k.easings.easeInOutQuad),
k.tween(fighterB.pos.x, midX + 15, 0.3, (v) => { fighterB.pos.x = v }, k.easings.easeInOutQuad),
])
sfxBlock()
spawnSparks(midX, fighterA.pos.y - 25, 6, '#ffe14d')
spawnEmoteText(midX, fighterA.pos.y - 40, 'GG!', '#ffe14d')
k.shake(2)
await k.wait(0.5)
await Promise.all([
k.tween(fighterA.pos.x, origAX, 0.3, (v) => { fighterA.pos.x = v }, k.easings.easeInOutQuad),
k.tween(fighterB.pos.x, origBX, 0.3, (v) => { fighterB.pos.x = v }, k.easings.easeInOutQuad),
])
}
// Winner helps loser back up
async function playHelpUp(winner: any, loser: any, winningSide: 'a' | 'b') {
const dir = winningSide === 'a' ? 1 : -1
const origWX = winner.pos.x
await k.tween(winner.pos.x, loser.pos.x - dir * 30, 0.4, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
winner.play('idle')
await k.wait(0.2)
loser.play('idle')
spawnEmoteText(loser.pos.x, loser.pos.y - 40, respectLines[Math.floor(Math.random() * respectLines.length)], '#88ccff')
for (let i = 0; i < 3; i++) spawnHeart((winner.pos.x + loser.pos.x) / 2, winner.pos.y - 30)
announceCool(heartfeltLines[Math.floor(Math.random() * heartfeltLines.length)])
await k.wait(0.6)
await k.tween(winner.pos.x, origWX, 0.3, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
}
// === REFEREE LOBSTER FUNNY MOMENTS ===
async function judgeDoSomethingFunny() {
const judge = k.get('judge')[0]
if (!judge) return
const judgeOrigY = judge.pos.y
const funnyAction = Math.floor(Math.random() * 8)
if (funnyAction === 0) {
// Falls asleep, snaps awake
for (let z = 0; z < 3; z++) {
setTimeout(() => {
spawnEmoteText(judge.pos.x + 15, judge.pos.y - 20, 'Z', '#8888ff', 1.0)
}, z * 400)
}
await k.wait(1.2)
judge.play('shocked')
k.shake(2)
sfxBoing()
spawnEmoteText(judge.pos.x, judge.pos.y - 30, '!?', '#ff4444')
await k.tween(judge.pos.y, judgeOrigY - 20, 0.1, (v) => { judge.pos.y = v }, k.easings.easeOutQuad)
await k.tween(judge.pos.y, judgeOrigY, 0.15, (v) => { judge.pos.y = v }, k.easings.easeInQuad)
await k.wait(0.3)
judge.play('idle')
} else if (funnyAction === 1) {
// Little dance on the chair
for (let d = 0; d < 4; d++) {
await k.tween(judge.pos.y, judgeOrigY - 8, 0.08, (v) => { judge.pos.y = v }, k.easings.easeOutQuad)
await k.tween(judge.pos.y, judgeOrigY, 0.08, (v) => { judge.pos.y = v }, k.easings.easeInQuad)
judge.play(d % 2 === 0 ? 'call_left' : 'call_right')
}
sfxRandomComedy()
judge.play('idle')
} else if (funnyAction === 2) {
// Holds up a 10 score card
judge.play('shocked')
const card = k.add([
k.rect(20, 14), k.pos(judge.pos.x + 20, judge.pos.y - 25),
k.color(safeColor(k,'#ffffff')), k.opacity(0.9), k.z(15), k.anchor('center'),
])
const score = k.add([
k.text('10', { size: 10 }), k.pos(judge.pos.x + 20, judge.pos.y - 25),
k.color(safeColor(k,'#000000')), k.opacity(1), k.z(16), k.anchor('center'),
])
sfxBoing()
await k.wait(1.0)
card.destroy(); score.destroy()
judge.play('idle')
} else if (funnyAction === 3) {
// Gets scared and hides behind the chair
judge.play('shocked')
sfxDodge()
await k.tween(judge.pos.y, judgeOrigY + 20, 0.15, (v) => { judge.pos.y = v }, k.easings.easeInQuad)
judge.opacity = 0.3
await k.wait(0.8)
await k.tween(judge.pos.y, judgeOrigY - 5, 0.2, (v) => { judge.pos.y = v }, k.easings.easeOutQuad)
judge.opacity = 0.9
await k.tween(judge.pos.y, judgeOrigY, 0.1, (v) => { judge.pos.y = v }, k.easings.easeInOutQuad)
judge.play('idle')
} else if (funnyAction === 4) {
// Eats a tiny sandwich
const sandwich = k.add([
k.rect(10, 8), k.pos(judge.pos.x - 15, judge.pos.y - 15),
k.color(safeColor(k,'#ddaa44')), k.opacity(0.9), k.z(15), k.anchor('center'),
])
const lettuce = k.add([
k.rect(12, 2), k.pos(judge.pos.x - 15, judge.pos.y - 15),
k.color(safeColor(k,'#44cc22')), k.opacity(0.8), k.z(16), k.anchor('center'),
])
await k.wait(0.4)
await k.tween(sandwich.pos.x, judge.pos.x, 0.2, (v) => { sandwich.pos.x = v; lettuce.pos.x = v }, k.easings.easeInQuad)
sandwich.destroy(); lettuce.destroy()
spawnEmoteText(judge.pos.x, judge.pos.y - 30, 'nom nom', '#ffcc44')
sfxBonk()
await k.wait(0.6)
} else if (funnyAction === 5) {
// Takes a selfie
judge.play('call_right')
const phone = k.add([
k.rect(6, 10), k.pos(judge.pos.x + 20, judge.pos.y - 20),
k.color(safeColor(k,'#333344')), k.opacity(0.9), k.z(15), k.anchor('center'),
])
await k.wait(0.3)
screenFlash('#ffffff', 0.05)
sfxZap()
spawnEmoteText(judge.pos.x, judge.pos.y - 35, '#selfie', '#ff88cc')
await k.wait(0.5)
phone.destroy()
judge.play('idle')
} else if (funnyAction === 6) {
// Waves a tiny flag
const flagColors = ['#ff2d2d', '#2d7bff', '#39ff14', '#ffcc00', '#ff6600', '#b83dff']
const flagColor = flagColors[Math.floor(Math.random() * flagColors.length)]
const flag = k.add([
k.rect(14, 9), k.pos(judge.pos.x + 18, judge.pos.y - 30),
k.color(safeColor(k,flagColor)), k.opacity(0.85), k.z(15), k.anchor('center'),
])
const pole = k.add([
k.rect(2, 18), k.pos(judge.pos.x + 11, judge.pos.y - 22),
k.color(safeColor(k,'#aa8855')), k.opacity(0.8), k.z(14), k.anchor('center'),
])
flag.onUpdate(() => {
flag.pos.y = judge.pos.y - 30 + Math.sin(k.time() * 10) * 3
})
sfxRandomComedy()
await k.wait(1.5)
flag.destroy(); pole.destroy()
} else {
// Falls off the chair, climbs back up
judge.play('shocked')
sfxBoing()
await k.tween(judge.pos.y, GROUND_Y - 6, 0.3, (v) => { judge.pos.y = v }, k.easings.easeInQuad)
sfxBonk()
k.shake(4)
spawnEmoteText(judge.pos.x, GROUND_Y - 20, 'oof!', '#ff6644')
await k.wait(0.5)
await k.tween(judge.pos.y, judgeOrigY, 0.5, (v) => { judge.pos.y = v }, k.easings.easeOutQuad)
judge.play('idle')
spawnEmoteText(judge.pos.x, judge.pos.y - 25, '*ahem*', '#aaaaaa')
await k.wait(0.3)
}
}
// === SHOWBOATING / TAUNTING SYSTEM ===
// Fighters perform these during question/answer phases instead of boring idle bobbing
const _showboatActive: Record<string, boolean> = { a: false, b: false }
type ShowboatFn = (fighter: any, homeX: number, homeY: number, sx: number, sy: number, dir: number) => Promise<void>
// dir: 1 = facing right (bot A), -1 = facing left (bot B)
const showboats: ShowboatFn[] = [
// 0: Pace back and forth — walk a wide arc restlessly
async (f, hx, hy, _sx, _sy, dir) => {
f.play('kick')
await k.tween(f.pos.x, hx + dir * 100, 0.35, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
f.play('idle')
await k.wait(0.1)
f.play('attack')
await k.tween(f.pos.x, hx - dir * 60, 0.4, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
f.play('idle')
await k.wait(0.1)
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 1: Bouncing advance — hop toward opponent then back
async (f, hx, hy, _sx, _sy, dir) => {
for (let i = 0; i < 3; i++) {
const targetX = hx + dir * (40 + i * 30)
f.play('kick')
await Promise.all([
k.tween(f.pos.x, targetX, 0.12, (v) => { f.pos.x = v }, k.easings.easeOutQuad),
k.tween(f.pos.y, hy - 35, 0.06, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.06, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBoing()
}
f.play('win'); await k.wait(0.15)
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 2: Shadow box sprint — dash forward shadow boxing, dash back
async (f, hx, hy, _sx, _sy, dir) => {
const far = hx + dir * 120
f.play('attack'); sfxZoomWhoosh()
await k.tween(f.pos.x, far, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 4; i++) {
f.play(i % 2 === 0 ? 'attack' : 'kick')
sfxDodge(); await k.wait(0.08)
}
f.play('special'); sfxZoomWhoosh()
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 3: Leapfrog across — multiple big jumps across the stage
async (f, hx, hy, _sx, _sy, dir) => {
const spots = [hx + dir * 60, hx + dir * 130, hx + dir * 60, hx]
for (const tx of spots) {
sfxBoing()
await Promise.all([
k.tween(f.pos.x, tx, 0.18, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
k.tween(f.pos.y, hy - 70, 0.09, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.09, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
}
},
// 4: Restless jog — jog back and forth with small hops
async (f, hx, hy, _sx, _sy, dir) => {
f.play('kick')
for (let i = 0; i < 5; i++) {
const dx = (i % 2 === 0 ? 1 : -1) * (30 + Math.random() * 40) * dir
await Promise.all([
k.tween(f.pos.x, hx + dx, 0.13, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 12, 0.065, (v) => { f.pos.y = v }).then(() =>
k.tween(f.pos.y, hy, 0.065, (v) => { f.pos.y = v })
),
])
}
f.pos.x = hx
},
// 5: Charge and flex — sprint to center, flex, sprint back
async (f, hx, hy, sx, sy, _dir) => {
const center = W / 2
f.play('special'); sfxZoomWhoosh()
await k.tween(f.pos.x, center, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad)
f.play('win')
await k.tween(1, 1.3, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutBack)
spawnEmoteText(f.pos.x, f.pos.y - 50, 'COME AT ME!', '#ff4444')
await k.wait(0.3)
await k.tween(1.3, 1, 0.1, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
sfxZoomWhoosh()
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 6: Triple backflip retreat — three backflips going backwards
async (f, hx, hy, _sx, _sy, dir) => {
for (let i = 0; i < 3; i++) {
sfxBoing()
const targetX = hx - dir * (i + 1) * 30
const a0 = f.angle || 0
await Promise.all([
k.tween(f.pos.x, targetX, 0.2, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 60, 0.1, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
k.tween(a0, a0 - 360, 0.2, (v) => { f.angle = v }),
])
f.angle = 0
}
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 7: Kangaroo hops — big bouncing jumps across the width
async (f, hx, hy, _sx, _sy, dir) => {
f.play('kick')
const hops = [hx + dir * 80, hx + dir * 150, hx + dir * 80, hx]
for (const tx of hops) {
await Promise.all([
k.tween(f.pos.x, tx, 0.15, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
k.tween(f.pos.y, hy - 90, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk(); k.shake(2)
}
},
// 8: Feint combo rush — charge in, throw combo, dodge back
async (f, hx, hy, _sx, _sy, dir) => {
sfxZoomWhoosh(); f.play('attack')
await k.tween(f.pos.x, hx + dir * 100, 0.12, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
const anims = ['attack', 'kick', 'attack', 'special'] as const
for (const a of anims) { f.play(a); sfxDodge(); await k.wait(0.06) }
sfxBlock(); spawnSparks(f.pos.x + dir * 15, hy - 30, 5, '#ffcc00')
// Dodge-hop back
await Promise.all([
k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad),
k.tween(f.pos.y, hy - 40, 0.1, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
},
// 9: Dizzy spin walk — spin while walking forward, stumble back
async (f, hx, hy, _sx, _sy, dir) => {
const a0 = f.angle || 0
f.play('hit')
await Promise.all([
k.tween(f.pos.x, hx + dir * 70, 0.4, (v) => { f.pos.x = v }),
k.tween(a0, a0 + 720, 0.4, (v) => { f.angle = v }),
])
f.angle = 0
spawnEmoteText(f.pos.x, f.pos.y - 40, '@_@', '#ffcc00')
await k.wait(0.15)
// Stumble back
f.play('idle')
for (let i = 0; i < 3; i++) {
await k.tween(f.pos.x, f.pos.x - dir * 25, 0.1, (v) => { f.pos.x = v })
f.pos.y = hy - 5; await k.wait(0.02); f.pos.y = hy
}
},
// 10: Wall-to-wall dash — full screen sprint and back
async (f, hx, hy, _sx, _sy, dir) => {
f.play('special'); sfxZoomWhoosh()
spawnAfterimages(f, 3)
await k.tween(f.pos.x, dir > 0 ? W - 30 : 30, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad)
sfxBlock(); k.shake(4)
await k.wait(0.1)
sfxZoomWhoosh(); spawnAfterimages(f, 3)
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 11: Earthquake stomp march — stomp forward and back
async (f, hx, hy, sx, sy, dir) => {
f.play('attack')
for (let i = 0; i < 4; i++) {
const step = dir * 25
await k.tween(f.pos.x, f.pos.x + step, 0.08, (v) => { f.pos.x = v })
f.scale.y = sy * 0.8; sfxBonk(); k.shake(3)
await k.wait(0.05); f.scale.y = sy
}
spawnSparks(f.pos.x, hy, 6, '#ff6600')
await k.wait(0.1)
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 12: Taunt walk — strut toward opponent, gesture, moonwalk back
async (f, hx, hy, _sx, _sy, dir) => {
f.play('win')
await k.tween(f.pos.x, hx + dir * 90, 0.35, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
f.play('special')
spawnEmoteText(f.pos.x, f.pos.y - 50, 'TOO EASY!', '#ff44ff')
await k.wait(0.3)
// Moonwalk back
f.play('idle')
await k.tween(f.pos.x, hx, 0.5, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 13: Victory lap sprint — run around in a loop
async (f, hx, hy, _sx, _sy, dir) => {
f.play('kick'); sfxZoomWhoosh()
await k.tween(f.pos.x, hx + dir * 140, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad)
// Jump over to other side
sfxBoing()
await Promise.all([
k.tween(f.pos.x, hx + dir * 140, 0.25, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 100, 0.125, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.125, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
f.play('win'); spawnSparks(f.pos.x, hy - 20, 5, theme.accent)
await k.wait(0.15)
sfxZoomWhoosh()
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 14: Nervous pacing — quick short paces with jittery hops
async (f, hx, hy) => {
for (let i = 0; i < 6; i++) {
const dx = (Math.random() - 0.5) * 80
await Promise.all([
k.tween(f.pos.x, hx + dx, 0.1, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 10 - Math.random() * 20, 0.05, (v) => { f.pos.y = v }).then(() =>
k.tween(f.pos.y, hy, 0.05, (v) => { f.pos.y = v })
),
])
}
},
// 15: Ground pound jump — massive vertical jump + slam
async (f, hx, hy) => {
sfxBoing(); f.play('kick')
await k.tween(f.pos.y, hy - 150, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
await k.wait(0.1)
f.play('attack')
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(15)
spawnSparks(f.pos.x, hy, 15, '#ff6600')
spawnShockwave(f.pos.x, hy, '#ff4400')
spawnEmoteText(f.pos.x, f.pos.y - 60, 'BOOM!', '#ff2d2d')
await k.wait(0.2)
},
// 16: Zigzag sprint — fast zigzag across the arena
async (f, hx, hy, _sx, _sy, dir) => {
f.play('kick'); sfxZoomWhoosh()
const pts = [
{ x: hx + dir * 50, y: hy - 40 },
{ x: hx + dir * 100, y: hy },
{ x: hx + dir * 150, y: hy - 40 },
{ x: hx + dir * 100, y: hy },
{ x: hx, y: hy },
]
for (const pt of pts) {
await Promise.all([
k.tween(f.pos.x, pt.x, 0.08, (v) => { f.pos.x = v }),
k.tween(f.pos.y, pt.y, 0.08, (v) => { f.pos.y = v }),
])
}
},
// 17: Double jump kick — jump, kick, jump higher, kick
async (f, hx, hy, _sx, _sy, dir) => {
f.play('kick'); sfxBoing()
const mid = hx + dir * 60
await Promise.all([
k.tween(f.pos.x, mid, 0.15, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 50, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
sfxKick(); spawnSparks(mid, hy - 30, 4, '#00ffcc')
f.play('special'); sfxBoing()
await Promise.all([
k.tween(f.pos.x, mid + dir * 40, 0.15, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 90, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
sfxKick(); spawnSparks(f.pos.x, hy - 50, 6, '#ff44ff')
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 18: Cocky strut and lean — walk forward, lean back with attitude
async (f, hx, hy, _sx, _sy, dir) => {
f.play('win')
await k.tween(f.pos.x, hx + dir * 80, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
// Lean back
await k.tween(0, -25, 0.15, (v) => { f.angle = v }, k.easings.easeOutQuad)
await k.wait(0.25)
spawnEmoteText(f.pos.x, f.pos.y - 50, 'EZ', '#ffcc00')
await k.tween(-25, 0, 0.1, (v) => { f.angle = v })
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 19: Windmill sprint — spin while running across
async (f, hx, hy, _sx, _sy, dir) => {
const a0 = f.angle || 0
const anims = ['attack', 'kick', 'special'] as const
let animIdx = 0
const far = hx + dir * 130
f.play(anims[0])
// Sprint while spinning
const dur = 0.4
const startX = f.pos.x
await k.tween(0, 1, dur, (t) => {
f.pos.x = startX + (far - startX) * t
f.angle = a0 + t * 1080 // 3 full spins
if (Math.floor(t * 12) !== Math.floor((t - 0.01) * 12)) {
f.play(anims[animIdx++ % anims.length])
}
})
f.angle = 0; sfxBlock(); k.shake(3)
await k.wait(0.1)
// Sprint back
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 20: Sumo stomp walk — heavy stomps advancing and retreating
async (f, hx, hy, _sx, sy, dir) => {
f.play('attack')
for (let i = 0; i < 6; i++) {
const step = (i < 3 ? dir : -dir) * 20
await k.tween(f.pos.x, f.pos.x + step, 0.1, (v) => { f.pos.x = v })
f.scale.y = sy * 0.8; sfxBonk(); k.shake(3)
await k.wait(0.06); f.scale.y = sy
spawnSparks(f.pos.x, hy, 2, '#886644')
}
},
// 21: Flex pose — walk to center, scale up, pose
async (f, hx, hy, sx, sy) => {
const center = W / 2
f.play('win')
await k.tween(f.pos.x, center, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
await k.tween(1, 1.35, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutBack)
sfxPowerUp()
spawnEmoteText(f.pos.x, f.pos.y - 60, 'COME ON!', '#ff4444')
await k.wait(0.35)
await k.tween(1.35, 1, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 22: Jump-kick air combo — big jump with mid-air kicks
async (f, hx, hy, _sx, _sy, dir) => {
sfxBoing()
const apex = hy - 120
const far = hx + dir * 100
await k.tween(f.pos.y, apex, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
// Mid-air combo while moving
f.play('kick'); sfxKick()
await k.tween(f.pos.x, far, 0.1, (v) => { f.pos.x = v })
f.play('attack'); sfxPunch()
await k.tween(f.pos.x, far + dir * 30, 0.08, (v) => { f.pos.x = v })
f.play('special')
spawnSparks(f.pos.x, f.pos.y, 8, '#ff44ff')
await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(4)
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 23: Air guitar walk — bouncing left and right with music
async (f, hx, hy, _sx, _sy, dir) => {
f.play('special'); sfxSlideUp()
spawnEmoteText(f.pos.x, f.pos.y - 50, '~♪~', '#ff44ff')
for (let i = 0; i < 6; i++) {
const dx = (i % 2 === 0 ? dir : -dir) * (30 + i * 10)
await Promise.all([
k.tween(f.pos.x, hx + dx, 0.12, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 15, 0.06, (v) => { f.pos.y = v }).then(() =>
k.tween(f.pos.y, hy, 0.06, (v) => { f.pos.y = v })
),
])
}
f.pos.x = hx
},
// 24: Crane kick walk — slow martial arts advancing
async (f, hx, hy, _sx, sy, dir) => {
f.play('kick')
await Promise.all([
k.tween(f.pos.x, hx + dir * 60, 0.3, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 30, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
k.tween(1, 1.15, 0.3, (v) => { f.scale.y = sy * v }, k.easings.easeOutQuad),
])
await k.wait(0.3)
f.play('special'); sfxKick()
spawnSparks(f.pos.x + dir * 20, hy - 40, 6, '#00ccff')
await k.wait(0.15)
f.scale.y = sy
await Promise.all([
k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad),
])
},
// 25: Girlfriend kiss — girlfriend walks in, kiss, hearts, she storms off
async (f, hx, hy, _sx, _sy, dir) => {
const startGfX = hx - dir * 120
const gfColor = ['#ff69b4', '#ff1493', '#ff6eb4', '#cc44aa'][Math.floor(Math.random() * 4)]
const cs = SF
const gf = k.add([k.rect(18 * cs, 35 * cs), k.pos(startGfX, hy - 18 * cs), k.anchor('center'), k.color(safeColor(k, gfColor)), k.opacity(0.9), k.z(11)])
const gfHair = k.add([k.rect(22 * cs, 10 * cs), k.pos(startGfX, hy - 35 * cs), k.anchor('center'), k.color(safeColor(k, '#8b4513')), k.opacity(0.9), k.z(12)])
// Walk toward fighter
await Promise.all([
k.tween(gf.pos.x, hx - dir * 15, 0.4, (v) => { gf.pos.x = v; gfHair.pos.x = v }, k.easings.easeInOutQuad),
])
// Kiss!
sfxBoing()
const heart = k.add([k.text('*smooch*', { size: 10 }), k.pos(hx, hy - 55), k.anchor('center'), k.color(safeColor(k, '#ff1493')), k.opacity(1), k.z(30)])
k.tween(heart.pos.y, hy - 90, 0.8, (v) => { heart.pos.y = v })
k.tween(1, 0, 0.8, (v) => { heart.opacity = v }).then(() => { if (heart.exists()) heart.destroy() })
// Spawn floating hearts
for (let i = 0; i < 5; i++) {
const h = k.add([k.text('♥', { size: 8 + Math.random() * 6 }), k.pos(hx + (Math.random() - 0.5) * 30, hy - 30), k.color(safeColor(k, '#ff69b4')), k.opacity(0.8), k.z(25), k.anchor('center')])
k.tween(h.pos.y, hy - 70 - Math.random() * 40, 0.7, (v) => { h.pos.y = v })
k.tween(h.pos.x, h.pos.x + (Math.random() - 0.5) * 40, 0.7, (v) => { h.pos.x = v })
k.tween(0.8, 0, 0.7, (v) => { h.opacity = v }).then(() => { if (h.exists()) h.destroy() })
}
f.play('win')
await k.wait(0.5)
// She storms off
spawnEmoteText(gf.pos.x, gf.pos.y - 30, 'Good luck babe!', '#ff69b4')
await k.tween(gf.pos.x, startGfX, 0.4, (v) => { gf.pos.x = v; gfHair.pos.x = v }, k.easings.easeInQuad)
gf.destroy(); gfHair.destroy()
},
// 26: Push-up walk — walk forward doing push-ups at intervals
async (f, hx, hy, sx, sy, dir) => {
for (let i = 0; i < 3; i++) {
await k.tween(f.pos.x, f.pos.x + dir * 35, 0.15, (v) => { f.pos.x = v })
// Push-up squash
await k.tween(1, 0.55, 0.08, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeOutQuad)
await k.tween(0.55, 1, 0.08, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeInQuad)
}
spawnEmoteText(f.pos.x, f.pos.y - 45, `${3 + Math.floor(Math.random() * 97)}!`, '#00ff88')
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 27: Matrix dodge walk — dodge bullets walking forward
async (f, hx, hy, _sx, _sy, dir) => {
for (let i = 0; i < 4; i++) {
await k.tween(f.pos.x, f.pos.x + dir * 25, 0.1, (v) => { f.pos.x = v })
// Lean dodge
const lean = (i % 2 === 0 ? -30 : 30)
await k.tween(0, lean, 0.08, (v) => { f.angle = v }, k.easings.easeOutQuad)
await k.tween(lean, 0, 0.08, (v) => { f.angle = v }, k.easings.easeInQuad)
}
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 28: Bouncy chest thump — bounce toward opponent thumping chest
async (f, hx, hy, _sx, _sy, dir) => {
f.play('attack')
for (let i = 0; i < 4; i++) {
await Promise.all([
k.tween(f.pos.x, f.pos.x + dir * 30, 0.1, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 25, 0.05, (v) => { f.pos.y = v }).then(() =>
k.tween(f.pos.y, hy, 0.05, (v) => { f.pos.y = v })
),
])
sfxBlock(); k.shake(2)
}
spawnEmoteText(f.pos.x, f.pos.y - 50, 'LET\'S GO!', '#ff4444')
await k.wait(0.15)
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 29: Speed demon — afterimage dash back and forth
async (f, hx, hy, _sx, _sy, dir) => {
f.play('special')
for (let i = 0; i < 3; i++) {
sfxZoomWhoosh(); spawnAfterimages(f, 3)
await k.tween(f.pos.x, hx + dir * (80 + i * 30), 0.08, (v) => { f.pos.x = v }, k.easings.easeInQuad)
spawnAfterimages(f, 2)
await k.tween(f.pos.x, hx - dir * 20, 0.08, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
}
f.pos.x = hx
},
]
// === CREATOR-EXCLUSIVE MAGICAL SHOWBOATS ===
const creatorShowboats: ShowboatFn[] = [
// 0: Levitate across — float up and glide across the screen trailing golden ₿
async (f, hx, hy, _sx, _sy, dir) => {
f.play('special'); sfxPowerUp()
await k.tween(f.pos.y, hy - 100, 0.25, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
// Float across dropping ₿ rain
const startX = f.pos.x
const far = hx + dir * 180
const dur = 0.6
let lastDrop = 0
await k.tween(0, 1, dur, (t) => {
f.pos.x = startX + (far - startX) * t
f.pos.y = hy - 100 + Math.sin(t * Math.PI * 3) * 20
if (t - lastDrop > 0.08) {
lastDrop = t
const d = k.add([k.text('₿', { size: 6 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y + 10), k.color(safeColor(k, '#ffd700')), k.opacity(0.7), k.z(9), k.anchor('center')])
k.tween(d.pos.y, hy + 10, 0.5, (v) => { d.pos.y = v })
k.tween(0.7, 0, 0.5, (v) => { d.opacity = v }).then(() => { if (d.exists()) d.destroy() })
}
})
screenFlash('#ffd700', 0.06)
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
},
// 1: Golden portal teleport — vanish, portals, reappear at random spots
async (f, hx, hy) => {
sfxSpecial()
// Vanish with flash
screenFlash('#ffd700', 0.08)
f.opacity = 0
// Teleport to 3 spots with golden portals
const spots = [
{ x: W * 0.2, y: hy - 60 },
{ x: W * 0.8, y: hy - 80 },
{ x: W * 0.5, y: hy - 120 },
]
for (const spot of spots) {
// Portal flash at destination
const portal = k.add([k.circle(20), k.pos(spot.x, spot.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.6), k.z(15), k.anchor('center')])
spawnSparks(spot.x, spot.y, 6, '#ffd700')
f.pos.x = spot.x; f.pos.y = spot.y; f.opacity = 1
f.play('special')
await k.wait(0.15)
f.opacity = 0
k.tween(0.6, 0, 0.2, (v) => { portal.opacity = v }).then(() => { if (portal.exists()) portal.destroy() })
sfxZap()
}
// Return home with big flash
screenFlash('#ffd700', 0.1)
f.pos.x = hx; f.pos.y = hy; f.opacity = 1
f.play('win')
spawnShockwave(hx, hy, '#ffd700')
sfxExplosion()
await k.wait(0.2)
},
// 2: Code rain summon — float up, summon massive golden code rain
async (f, hx, hy) => {
f.play('special'); sfxPowerUp()
await k.tween(f.pos.y, hy - 80, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
// Massive code rain
const chars = '₿01∞⚡⛏§∂∆≈≠'.split('')
for (let col = 0; col < 20; col++) {
const cx = Math.random() * W
for (let row = 0; row < 3; row++) {
const ch = chars[Math.floor(Math.random() * chars.length)]
const d = k.add([
k.text(ch, { size: 6 + Math.random() * 4 }), k.pos(cx, -10 - row * 20),
k.color(safeColor(k, row === 0 ? '#ffd700' : '#c8a000')),
k.opacity(0.5 + Math.random() * 0.4), k.z(8), k.anchor('center'),
])
k.tween(d.pos.y, hy + 30, 0.5 + Math.random() * 0.4, (v) => { d.pos.y = v; d.opacity = Math.max(0, 1 - (v - hy + 30) / 60) })
.then(() => { if (d.exists()) d.destroy() })
}
}
await k.wait(0.6)
await k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad)
},
// 3: ₿ orbit explosion — fly to center, ₿ symbols orbit then explode outward
async (f, hx, hy, sx, sy) => {
sfxZoomWhoosh(); f.play('special')
const center = W / 2
await Promise.all([
k.tween(f.pos.x, center, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad),
k.tween(f.pos.y, hy - 60, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
])
// Spawn orbiting ₿
const orbs: any[] = []
for (let i = 0; i < 8; i++) {
const orb = k.add([k.text('₿', { size: 8 + Math.random() * 6 }), k.pos(center, f.pos.y), k.color(safeColor(k, i % 2 === 0 ? '#ffd700' : '#ff8c00')), k.opacity(0.8), k.z(15), k.anchor('center')])
orbs.push(orb)
}
// Orbit for a moment
const startTime = k.time()
const orbitCancel = k.onUpdate(() => {
const t = k.time() - startTime
for (let i = 0; i < orbs.length; i++) {
const a = t * 4 + i * (Math.PI * 2 / orbs.length)
const r = 30 + t * 15
orbs[i].pos.x = f.pos.x + Math.cos(a) * r
orbs[i].pos.y = f.pos.y + Math.sin(a) * r * 0.5
}
})
await k.wait(0.5)
orbitCancel.cancel()
// Explode outward
sfxExplosion(); screenFlash('#ffd700', 0.08); k.shake(8)
for (const orb of orbs) {
const angle = Math.random() * Math.PI * 2
k.tween(orb.pos.x, orb.pos.x + Math.cos(angle) * 200, 0.3, (v) => { orb.pos.x = v })
k.tween(orb.pos.y, orb.pos.y + Math.sin(angle) * 200, 0.3, (v) => { orb.pos.y = v })
k.tween(0.8, 0, 0.3, (v) => { orb.opacity = v }).then(() => { if (orb.exists()) orb.destroy() })
}
await k.wait(0.2)
await Promise.all([
k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad),
k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad),
])
},
// 4: Lightning flight — zigzag across the screen at super speed with lightning
async (f, hx, hy, _sx, _sy, dir) => {
f.play('special'); sfxZoomWhoosh()
f.opacity = 0.8
const pts = [
{ x: hx + dir * 60, y: hy - 80 },
{ x: W / 2, y: hy - 140 },
{ x: hx + dir * 180, y: hy - 60 },
{ x: W / 2, y: hy - 100 },
{ x: hx, y: hy },
]
for (const pt of pts) {
spawnAfterimages(f, 2)
await Promise.all([
k.tween(f.pos.x, pt.x, 0.08, (v) => { f.pos.x = v }),
k.tween(f.pos.y, pt.y, 0.08, (v) => { f.pos.y = v }),
])
// Lightning bolt at each stop
sfxZap()
const bolt = k.add([k.rect(2, 30 + Math.random() * 40), k.pos(f.pos.x, f.pos.y - 50), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(20), k.anchor('center'), k.rotate(Math.random() * 30 - 15)])
k.tween(0.9, 0, 0.15, (v) => { bolt.opacity = v }).then(() => { if (bolt.exists()) bolt.destroy() })
}
f.opacity = 1
},
// 5: Satoshi meditation — float to center, golden aura pulse, wisdom text
async (f, hx, hy, sx, sy) => {
sfxPowerUp(); f.play('special')
await Promise.all([
k.tween(f.pos.x, W / 2, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
k.tween(f.pos.y, hy - 70, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
])
// Golden aura rings
for (let i = 0; i < 3; i++) {
const ring = k.add([k.circle(10), k.pos(f.pos.x, f.pos.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.5), k.z(9), k.anchor('center'), k.scale(1)])
k.tween(1, (60 + i * 20) / 10, 0.4, (v) => { ring.scale.x = v; ring.scale.y = v })
k.tween(0.5, 0, 0.4, (v) => { ring.opacity = v }).then(() => { if (ring.exists()) ring.destroy() })
await k.wait(0.12)
}
// Scale pulse
await k.tween(1, 1.2, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutQuad)
const sayings = ['PROOF OF WORK', 'VERIFY, DON\'T TRUST', '21 MILLION', 'TICK TOCK NEXT BLOCK', 'NOT YOUR KEYS', 'STAY HUMBLE STACK SATS']
spawnEmoteText(f.pos.x, f.pos.y - 50, sayings[Math.floor(Math.random() * sayings.length)], '#ffd700')
await k.wait(0.4)
await k.tween(1.2, 1, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
await Promise.all([
k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad),
])
},
// 6: Golden tornado — spin rapidly while moving in a circle across the arena
async (f, hx, hy) => {
sfxZoomWhoosh(); f.play('special')
const cx = W / 2, cy = hy - 50, radius = 100
const dur = 0.8
let lastParticle = 0
await k.tween(0, Math.PI * 2, dur, (a) => {
f.pos.x = cx + Math.cos(a) * radius
f.pos.y = cy + Math.sin(a) * radius * 0.4
f.angle = (a / (Math.PI * 2)) * 1440 // 4 full spins
if (a - lastParticle > 0.3) {
lastParticle = a
const p = k.add([k.text('₿', { size: 5 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.6), k.z(9), k.anchor('center')])
k.tween(0.6, 0, 0.4, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() })
}
})
f.angle = 0
sfxExplosion(); k.shake(6)
spawnShockwave(f.pos.x, f.pos.y, '#ffd700')
f.pos.x = hx; f.pos.y = hy
},
// 7: Laptop hack — walk forward, pull out laptop (rect), type furiously, spawn effects
async (f, hx, hy, _sx, _sy, dir) => {
f.play('special')
await k.tween(f.pos.x, hx + dir * 50, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
// Laptop
const cs = SF
const laptop = k.add([k.rect(24 * cs, 16 * cs), k.pos(f.pos.x + dir * 15, hy - 20 * cs), k.anchor('center'), k.color(safeColor(k, '#333333')), k.opacity(0.9), k.z(12)])
const screen = k.add([k.rect(20 * cs, 12 * cs), k.pos(f.pos.x + dir * 15, hy - 22 * cs), k.anchor('center'), k.color(safeColor(k, '#00ff00')), k.opacity(0.7), k.z(13)])
// Type — spawn code characters
const codeChars = '₿{}();=>0x1f'.split('')
for (let i = 0; i < 8; i++) {
const ch = codeChars[Math.floor(Math.random() * codeChars.length)]
const t = k.add([k.text(ch, { size: 5 }), k.pos(screen.pos.x + (Math.random() - 0.5) * 15, screen.pos.y), k.color(safeColor(k, '#00ff00')), k.opacity(0.8), k.z(14), k.anchor('center')])
k.tween(t.pos.y, t.pos.y - 25 - Math.random() * 20, 0.3, (v) => { t.pos.y = v })
k.tween(0.8, 0, 0.3, (v) => { t.opacity = v }).then(() => { if (t.exists()) t.destroy() })
sfxCoin()
await k.wait(0.06)
}
spawnEmoteText(f.pos.x, f.pos.y - 55, 'HACKING...', '#00ff00')
await k.wait(0.2)
laptop.destroy(); screen.destroy()
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 8: Bitcoin rain shower — fly across dropping ₿ coins everywhere
async (f, hx, hy, _sx, _sy, dir) => {
sfxPowerUp(); f.play('win')
await k.tween(f.pos.y, hy - 90, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
// Fly from one side to the other
const startX = dir > 0 ? 30 : W - 30
const endX = dir > 0 ? W - 30 : 30
f.pos.x = startX
const dur = 0.6
let lastCoin = 0
await k.tween(0, 1, dur, (t) => {
f.pos.x = startX + (endX - startX) * t
f.pos.y = hy - 90 + Math.sin(t * Math.PI * 4) * 15
if (t - lastCoin > 0.04) {
lastCoin = t
const coin = k.add([k.text('₿', { size: 8 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y + 15), k.color(safeColor(k, Math.random() > 0.5 ? '#ffd700' : '#ff8c00')), k.opacity(0.8), k.z(9), k.anchor('center')])
k.tween(coin.pos.y, hy + 10, 0.4, (v) => { coin.pos.y = v })
k.tween(coin.pos.x, coin.pos.x + (Math.random() - 0.5) * 30, 0.4, (v) => { coin.pos.x = v })
k.tween(0.8, 0, 0.4, (v) => { coin.opacity = v }).then(() => { if (coin.exists()) coin.destroy() })
}
})
sfxCoin()
f.pos.x = hx
await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
},
// 9: Dimension rift — creator tears open a golden rift, steps through, appears elsewhere
async (f, hx, hy) => {
// Rift opens
sfxSpecial()
const riftX = f.pos.x + 30
const rift = k.add([k.rect(4, 50), k.pos(riftX, hy - 25), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(15)])
await k.tween(4, 30, 0.2, (v) => { rift.width = v })
spawnSparks(riftX, hy - 25, 8, '#ffd700')
// Step through — vanish
f.play('special')
await k.tween(f.pos.x, riftX, 0.1, (v) => { f.pos.x = v })
f.opacity = 0; sfxZap()
// Rift at destination
const destX = W - riftX
const rift2 = k.add([k.rect(30, 50), k.pos(destX, hy - 25), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(15)])
spawnSparks(destX, hy - 25, 8, '#ffd700')
await k.wait(0.15)
// Step out
f.pos.x = destX; f.opacity = 1; f.play('win')
sfxSpecial()
await k.wait(0.2)
// Close rifts
k.tween(0.9, 0, 0.2, (v) => { rift.opacity = v; rift2.opacity = v }).then(() => { if (rift.exists()) rift.destroy(); if (rift2.exists()) rift2.destroy() })
// Float home
await Promise.all([
k.tween(f.pos.x, hx, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
k.tween(f.pos.y, hy - 40, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
),
])
},
// 10: Golden ascension — rise way up, glow, descend with shockwave
async (f, hx, hy, sx, sy) => {
sfxPowerUp(); f.play('special')
await k.tween(f.pos.y, hy - 160, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
// Glow pulse
await k.tween(1, 1.5, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutQuad)
screenFlash('#ffd700', 0.06)
// Spawn golden aura particles
for (let i = 0; i < 10; i++) {
const p = k.add([k.text('✦', { size: 4 + Math.random() * 6 }), k.pos(f.pos.x + (Math.random() - 0.5) * 40, f.pos.y + (Math.random() - 0.5) * 30), k.color(safeColor(k, '#ffd700')), k.opacity(0.7), k.z(9), k.anchor('center')])
k.tween(0.7, 0, 0.5, (v) => { p.opacity = v; p.pos.y -= 0.5 }).then(() => { if (p.exists()) p.destroy() })
}
spawnEmoteText(f.pos.x, f.pos.y - 40, 'HODL', '#ffd700')
await k.wait(0.3)
await k.tween(1.5, 1, 0.1, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
// Slam down
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(12)
spawnShockwave(hx, hy, '#ffd700')
spawnSparks(hx, hy, 15, '#ffd700')
await k.wait(0.15)
},
// 11: Phase walk — flicker opacity while walking through the opponent's space
async (f, hx, hy, _sx, _sy, dir) => {
f.play('special'); sfxZap()
const far = dir > 0 ? W - 40 : 40
const dur = 0.5
await k.tween(0, 1, dur, (t) => {
f.pos.x = hx + (far - hx) * t
f.opacity = 0.2 + Math.abs(Math.sin(t * Math.PI * 8)) * 0.7
})
f.opacity = 1; sfxSpecial()
screenFlash('#ffd700', 0.05)
spawnSparks(f.pos.x, hy - 20, 6, '#ffd700')
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 12: Bullet time attack — slow-mo dash with afterimages + golden bullet traces
async (f, hx, hy, _sx, _sy, dir) => {
f.play('attack')
// Darken screen for bullet time
const overlay = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(50)])
await k.tween(0, 0.35, 0.08, (v) => { overlay.opacity = v })
// Slow-mo lean back
await k.tween(0, -30, 0.2, (v) => { f.angle = v }, k.easings.easeOutQuad)
// Golden bullet traces fly past
for (let i = 0; i < 6; i++) {
const by = hy - 15 - Math.random() * 50
const bullet = k.add([k.rect(15, 2), k.pos(dir > 0 ? W + 10 : -10, by), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(51), k.anchor('center')])
const bdir = dir > 0 ? -1 : 1
k.tween(bullet.pos.x, bullet.pos.x + bdir * (W + 30), 0.35, (v) => { bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() })
sfxZoomWhoosh()
await k.wait(0.04)
}
await k.wait(0.1)
// Snap upright and dash through
await k.tween(-30, 0, 0.06, (v) => { f.angle = v })
f.play('special'); sfxZoomWhoosh()
// Dash with afterimages
const endX = hx + dir * 160
for (let i = 0; i < 6; i++) {
const ghostX = hx + (endX - hx) * (i / 6)
const ghost = k.add([k.rect(16, 30), k.pos(ghostX, hy - 15), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.3 - i * 0.04), k.z(49)])
k.tween(0.3, 0, 0.4, (v) => { ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() })
}
await k.tween(f.pos.x, endX, 0.12, (v) => { f.pos.x = v }, k.easings.easeInQuad)
screenFlash('#ffd700', 0.06); sfxCritical(); k.shake(8)
spawnSparks(f.pos.x, hy - 20, 10, '#ffd700')
await k.wait(0.15)
// Zip back
await k.tween(overlay.opacity, 0, 0.1, (v) => { overlay.opacity = v })
overlay.destroy()
await k.tween(f.pos.x, hx, 0.1, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 13: ₿ throne summon — conjure golden throne, sit imperiously, dismiss
async (f, hx, hy) => {
sfxSpecial()
const cs = SF
// Throne pieces
const seat = k.add([k.rect(40 * cs, 8 * cs), k.pos(hx, hy - 5), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
const back = k.add([k.rect(35 * cs, 30 * cs), k.pos(hx, hy - 25 * cs), k.anchor('center'), k.color(safeColor(k, '#c8a000')), k.opacity(0), k.z(8)])
const armL = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx - 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
const armR = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx + 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
const parts = [seat, back, armL, armR]
// Fade in
for (const p of parts) k.tween(0, 0.8, 0.2, (v) => { p.opacity = v })
spawnSparks(hx, hy - 15, 6, '#ffd700')
await k.wait(0.25)
// Sit
f.play('win')
await k.tween(f.pos.y, hy - 12, 0.12, (v) => { f.pos.y = v }, k.easings.easeInQuad)
spawnEmoteText(hx, f.pos.y - 50, 'kneel', '#ffd700')
await k.wait(0.6)
// Stand and dismiss
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v })
for (const p of parts) k.tween(p.opacity, 0, 0.2, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() })
await k.wait(0.2)
},
// 14: Disco dance — Creator gets down with golden disco moves
async (f, hx, hy, _sx, _sy, dir) => {
f.play('win')
const colors = ['#ffd700', '#ff8c00', '#ffee88', '#c8a000', '#ffffff', '#ffd700']
spawnEmoteText(f.pos.x, f.pos.y - 55, '~♪ SATOSHI SHUFFLE ♪~', '#ffd700')
for (let i = 0; i < 10; i++) {
const dx = (i % 2 === 0 ? 25 : -25) + (Math.random() - 0.5) * 15
f.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'win')
await Promise.all([
k.tween(f.pos.x, hx + dx, 0.06, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 15, 0.03, (v) => { f.pos.y = v }).then(() =>
k.tween(f.pos.y, hy, 0.03, (v) => { f.pos.y = v })),
])
spawnSparks(f.pos.x, hy - 10, 2, colors[i % colors.length])
}
f.pos.x = hx
},
]
// Showboat loop: picks random showboats in sequence while active
async function _showboatLoop(side: 'a' | 'b') {
const tag = side === 'a' ? 'fighterA' : 'fighterB'
const homeX = side === 'a' ? HOME_A : HOME_B
const homeY = GROUND_Y - 6
const dir = side === 'a' ? 1 : -1
const arch = side === 'a' ? botA.archetype : botB.archetype
const isCreator = arch === 'the_creator'
// Creator uses magical showboats 60% of the time, normal 40%
const pool = isCreator ? [...showboats, ...creatorShowboats, ...creatorShowboats] : showboats
while (_showboatActive[side]) {
const f = k.get(tag)[0]
if (!f) break
const sx = f.scale.x
const sy = f.scale.y
const idx = Math.floor(Math.random() * pool.length)
try {
await pool[idx](f, homeX, homeY, sx, sy, dir)
// 40% chance of comedy sound punctuation after each showboat
if (Math.random() < 0.4) sfxRandomComedy()
} catch { break }
// Restore state after each showboat
const ff = k.get(tag)[0]
if (ff) {
ff.pos.x = homeX; ff.pos.y = homeY
ff.scale.x = sx; ff.scale.y = sy
ff.angle = 0; ff.opacity = 1
ff.play('idle')
}
// Brief pause between showboats
if (_showboatActive[side]) await k.wait(0.15 + Math.random() * 0.35)
}
}
return {
k,
async showAnnouncement(_text: string, _color: string = '#ffffff', _duration: number = 1200) {},
showSpeechBubble(side: 'a' | 'b', text: string, duration?: number) {
showSpeechBubble(side, text, duration)
},
startTalking(side: 'a' | 'b') { startTalking(side) },
stopTalking(side: 'a' | 'b') { stopTalking(side) },
startShowboating(side: 'a' | 'b') {
if (_showboatActive[side]) return
_showboatActive[side] = true
_showboatLoop(side)
},
stopShowboating(side: 'a' | 'b') {
_showboatActive[side] = false
},
async playEntrance() {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const entrances = [
// 0: Drive in with a car
async (fighter: any, homeX: number, fromLeft: boolean) => {
const dir = fromLeft ? 1 : -1
const startX = fromLeft ? -120 : W + 120
const carY = GROUND_Y - 20
const carBody = k.add([k.rect(80, 30), k.pos(startX, carY), k.anchor('center'), k.color(safeColor(k,['#ff2d2d', '#2d7bff', '#ffcc00', '#39ff14', '#ff6600'][Math.floor(Math.random() * 5)])), k.z(9), k.opacity(1)])
const wheel1 = k.add([k.circle(8), k.pos(startX - 25 * dir, carY + 15), k.anchor('center'), k.color(safeColor(k,'#222222')), k.z(9)])
const wheel2 = k.add([k.circle(8), k.pos(startX + 25 * dir, carY + 15), k.anchor('center'), k.color(safeColor(k,'#222222')), k.z(9)])
fighter.pos.x = startX
fighter.pos.y = carY - 30
fighter.opacity = 1
sfxZoomWhoosh()
await k.tween(startX, homeX, 0.5, (v) => {
carBody.pos.x = v; wheel1.pos.x = v - 25 * dir; wheel2.pos.x = v + 25 * dir; fighter.pos.x = v
}, k.easings.easeOutQuad)
sfxBlock()
k.shake(6)
// Jump out
await k.tween(fighter.pos.y, GROUND_Y - 80, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxBonk()
// Drive car away
k.tween(carBody.pos.x, fromLeft ? W + 150 : -150, 0.4, (v) => {
carBody.pos.x = v; wheel1.pos.x = v - 25 * dir; wheel2.pos.x = v + 25 * dir
}, k.easings.easeInQuad).then(() => { carBody.destroy(); wheel1.destroy(); wheel2.destroy() })
},
// 1: Fall from space
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX
fighter.pos.y = -200
fighter.opacity = 1
sfxZoomWhoosh()
// Trail of fire
const trail: any[] = []
for (let i = 0; i < 6; i++) {
setTimeout(() => {
const t = k.add([k.circle(4 + Math.random() * 6), k.pos(homeX + (Math.random() - 0.5) * 20, fighter.pos.y + 20), k.color(safeColor(k,i < 3 ? '#ff6600' : '#ffcc00')), k.opacity(0.7), k.z(9)])
trail.push(t)
k.tween(t.opacity, 0, 0.4, (v) => { t.opacity = v }).then(() => { if (t.exists()) t.destroy() })
}, i * 40)
}
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.4, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxExplosion()
k.shake(15)
screenFlash('#ff6600', 0.15)
spawnShockwave(homeX, GROUND_Y, '#ff6600')
spawnSparks(homeX, GROUND_Y - 10, 15, '#ffcc00')
trail.forEach(t => { if (t.exists()) t.destroy() })
},
// 2: Robe entrance (boxing style)
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX
fighter.pos.y = GROUND_Y - 6
fighter.opacity = 1
// Robe overlay
const robeColor = ['#8b0000', '#00008b', '#006400', '#4b0082', '#8b4513'][Math.floor(Math.random() * 5)]
const robe = k.add([k.rect(50, 55), k.pos(startX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k,robeColor)), k.opacity(0.85), k.z(11)])
announceDeepIntro()
// Slow walk in
await k.tween(startX, homeX, 0.8, (v) => { fighter.pos.x = v; robe.pos.x = v }, k.easings.easeInOutQuad)
await k.wait(0.3)
// Take off robe (fly up and fade)
sfxSpecial()
await k.tween(robe.pos.y, robe.pos.y - 100, 0.3, (v) => { robe.pos.y = v; robe.opacity = Math.max(0, 1 - (robe.pos.y - GROUND_Y + 130) / -100) }, k.easings.easeOutQuad)
robe.destroy()
k.shake(3)
spawnSparks(homeX, GROUND_Y - 40, 8, robeColor)
},
// 3: Girlfriend argument
async (fighter: any, homeX: number, fromLeft: boolean) => {
const dir = fromLeft ? 1 : -1
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX
fighter.pos.y = GROUND_Y - 6
fighter.opacity = 1
// Girlfriend silhouette
const gfX = startX + dir * 40
const gf = k.add([k.rect(25, 45), k.pos(gfX, GROUND_Y - 25), k.anchor('center'), k.color(safeColor(k,'#ff69b4')), k.opacity(0.9), k.z(11)])
const heart = k.add([k.text('!', { size: 16 }), k.pos(gfX, GROUND_Y - 65), k.anchor('center'), k.color(safeColor(k,'#ff0000')), k.z(12)])
announceSilly('We need to talk! About your ELO!')
// Walk in arguing
await k.tween(startX, homeX + dir * 30, 0.6, (v) => {
fighter.pos.x = v; gf.pos.x = v + dir * 40; heart.pos.x = v + dir * 40
}, k.easings.easeInOutQuad)
await k.wait(0.2)
heart.text = '!!!'
announceRandom("You promised you'd stop fighting!")
await k.wait(0.4)
// Girlfriend storms off
announceSilly('I\'m telling your developer!')
await k.tween(gf.pos.x, fromLeft ? -60 : W + 60, 0.5, (v) => { gf.pos.x = v; heart.pos.x = v }, k.easings.easeInQuad)
gf.destroy(); heart.destroy()
// Bot shrugs and walks to position
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeInOutQuad)
sfxBoing()
},
// 4: Helicopter drop
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX
fighter.pos.y = -100
fighter.opacity = 1
// Helicopter body
const heli = k.add([k.rect(60, 20), k.pos(homeX, -80), k.anchor('center'), k.color(safeColor(k,'#555555')), k.z(12)])
const blade = k.add([k.rect(80, 3), k.pos(homeX, -95), k.anchor('center'), k.color(safeColor(k,'#888888')), k.z(13), k.rotate(0)])
blade.onUpdate(() => { blade.angle += 720 * k.dt() })
sfxJetpack()
// Descend
await k.tween(-80, GROUND_Y - 80, 0.6, (v) => {
heli.pos.y = v; blade.pos.y = v - 15; fighter.pos.y = v + 30
}, k.easings.easeInOutQuad)
// Drop
sfxZoomWhoosh()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.2, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(5)
// Helicopter flies away
k.tween(heli.pos.y, -200, 0.5, (v) => { heli.pos.y = v; blade.pos.y = v - 15 }, k.easings.easeInQuad)
.then(() => { heli.destroy(); blade.destroy() })
},
// 5: Teleport glitch
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.opacity = 0
sfxZap()
// Glitch flickers at random positions
for (let i = 0; i < 6; i++) {
fighter.pos.x = homeX + (Math.random() - 0.5) * 200
fighter.pos.y = GROUND_Y - 6 + (Math.random() - 0.5) * 80
fighter.opacity = 0.4
scanlineGlitch(0.05)
await k.wait(0.06)
fighter.opacity = 0
await k.wait(0.04)
}
fighter.pos.x = homeX
fighter.pos.y = GROUND_Y - 6
fighter.opacity = 1
sfxZap()
screenFlash('#00f0ff', 0.1)
spawnSparks(homeX, GROUND_Y - 30, 10, '#00f0ff')
k.shake(5)
},
// 6: Skateboard ride in
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -80 : W + 80
const board = k.add([k.rect(40, 6), k.pos(startX, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k,'#884422')), k.z(9)])
const wheelL = k.add([k.circle(4), k.pos(startX - 14, GROUND_Y + 1), k.anchor('center'), k.color(safeColor(k,'#333')), k.z(9)])
const wheelR = k.add([k.circle(4), k.pos(startX + 14, GROUND_Y + 1), k.anchor('center'), k.color(safeColor(k,'#333')), k.z(9)])
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 14; fighter.opacity = 1
sfxZoomWhoosh()
await k.tween(startX, homeX, 0.5, (v) => {
fighter.pos.x = v; board.pos.x = v; wheelL.pos.x = v - 14; wheelR.pos.x = v + 14
fighter.pos.y = GROUND_Y - 14 + Math.sin((v - startX) * 0.1) * 3
}, k.easings.easeOutQuad)
// Kickflip off
sfxBoing()
await k.tween(fighter.pos.y, GROUND_Y - 60, 0.12, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
board.destroy(); wheelL.destroy(); wheelR.destroy()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.12, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(3)
},
// 7: Rocket jetpack
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -80 : W + 80
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 80; fighter.opacity = 1
sfxJetpack()
// Fly in with flame trail
const flames: any[] = []
const interval = setInterval(() => {
const f = k.add([k.circle(5 + Math.random() * 5), k.pos(fighter.pos.x, fighter.pos.y + 25), k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.8), k.z(9)])
flames.push(f)
k.tween(f.opacity, 0, 0.3, (v) => { f.opacity = v }).then(() => { if (f.exists()) f.destroy() })
}, 30)
await k.tween(startX, homeX, 0.4, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
clearInterval(interval)
// Land
sfxExplosion()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
k.shake(8)
spawnSparks(homeX, GROUND_Y - 10, 10, '#ff6600')
flames.forEach(f => { if (f.exists()) f.destroy() })
},
// 8: Emerge from portal
async (fighter: any, homeX: number, _fromLeft: boolean) => {
const portalColor = ['#b83dff', '#00f0ff', '#39ff14', '#ff2d7b'][Math.floor(Math.random() * 4)]
// Draw portal
const portal = k.add([k.circle(35), k.pos(homeX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k,portalColor)), k.opacity(0), k.z(9)])
const portalRing = k.add([k.circle(40), k.pos(homeX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k,'#ffffff')), k.opacity(0), k.z(8)])
sfxZap()
await k.tween(0, 0.7, 0.3, (v) => { portal.opacity = v; portalRing.opacity = v * 0.4 }, k.easings.easeOutQuad)
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y - 30
fighter.opacity = 0
await k.tween(0, 1, 0.3, (v) => { fighter.opacity = v; fighter.pos.y = GROUND_Y - 30 + (GROUND_Y - 6 - GROUND_Y + 30) * v }, k.easings.easeOutQuad)
fighter.pos.y = GROUND_Y - 6
fighter.opacity = 1
sfxSpecial()
spawnSparks(homeX, GROUND_Y - 20, 10, portalColor)
await k.tween(portal.opacity, 0, 0.3, (v) => { portal.opacity = v; portalRing.opacity = v * 0.4 }, k.easings.easeInQuad)
portal.destroy(); portalRing.destroy()
},
// 9: Backflip entrance
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -40 : W + 40
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
sfxZoomWhoosh()
const flipCount = 2 + Math.floor(Math.random() * 2)
const totalDist = Math.abs(homeX - startX)
const flipDist = totalDist / flipCount
for (let i = 0; i < flipCount; i++) {
const from = startX + (fromLeft ? 1 : -1) * flipDist * i
const to = startX + (fromLeft ? 1 : -1) * flipDist * (i + 1)
await Promise.all([
k.tween(from, to, 0.2, (v) => { fighter.pos.x = v }, k.easings.linear),
k.tween(GROUND_Y - 6, GROUND_Y - 70, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(GROUND_Y - 70, GROUND_Y - 6, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
),
])
if (i < flipCount - 1) sfxBoing()
}
sfxBonk(); k.shake(4)
spawnSparks(homeX, GROUND_Y - 10, 6, '#ffe14d')
},
// 10: Rise from underground
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y + 60; fighter.opacity = 0.5
// Crack the ground
const crack1 = k.add([k.rect(3, 15), k.pos(homeX - 10, GROUND_Y - 5), k.color(safeColor(k,'#ffcc00')), k.opacity(0.7), k.z(9), k.rotate(15)])
const crack2 = k.add([k.rect(3, 12), k.pos(homeX + 8, GROUND_Y - 3), k.color(safeColor(k,'#ffcc00')), k.opacity(0.6), k.z(9), k.rotate(-20)])
sfxExplosion(); k.shake(8)
await k.wait(0.2)
// Rise up
await k.tween(GROUND_Y + 60, GROUND_Y - 6, 0.4, (v) => { fighter.pos.y = v; fighter.opacity = Math.min(1, (GROUND_Y + 60 - v) / 60) }, k.easings.easeOutQuad)
fighter.opacity = 1
spawnSparks(homeX, GROUND_Y - 10, 12, '#aa8833')
crack1.destroy(); crack2.destroy()
},
// 11: Slide in on ice
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -80 : W + 80
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
sfxSlideDown()
// Ice trail
const iceTrail: any[] = []
await k.tween(startX, homeX + (fromLeft ? 40 : -40), 0.4, (v) => {
fighter.pos.x = v
if (Math.random() < 0.3) {
const ice = k.add([k.rect(8, 3), k.pos(v, GROUND_Y - 2), k.color(safeColor(k,'#aaeeff')), k.opacity(0.5), k.z(1)])
iceTrail.push(ice)
}
}, k.easings.easeOutQuad)
// Slide to stop
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutCubic)
sfxBlock(); k.shake(3)
setTimeout(() => { iceTrail.forEach(i => { if (i.exists()) i.destroy() }) }, 800)
},
// 12: Parachute drop
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX + (Math.random() - 0.5) * 60; fighter.pos.y = -120; fighter.opacity = 1
const chute = k.add([k.circle(30), k.pos(fighter.pos.x, fighter.pos.y - 35), k.anchor('center'), k.color(safeColor(k,['#ff2d2d', '#2d7bff', '#39ff14', '#ffcc00'][Math.floor(Math.random() * 4)])), k.opacity(0.8), k.z(12)])
const line1 = k.add([k.rect(1, 30), k.pos(fighter.pos.x - 10, fighter.pos.y - 20), k.color(safeColor(k,'#888')), k.z(11)])
const line2 = k.add([k.rect(1, 30), k.pos(fighter.pos.x + 10, fighter.pos.y - 20), k.color(safeColor(k,'#888')), k.z(11)])
// Float down
await k.tween(-120, GROUND_Y - 50, 0.7, (v) => {
fighter.pos.y = v; chute.pos.y = v - 35; line1.pos.y = v - 20; line2.pos.y = v - 20
fighter.pos.x += Math.sin(v * 0.05) * 0.5
chute.pos.x = fighter.pos.x; line1.pos.x = fighter.pos.x - 10; line2.pos.x = fighter.pos.x + 10
}, k.easings.easeInOutQuad)
// Cut chute
chute.destroy(); line1.destroy(); line2.destroy()
sfxZoomWhoosh()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
await k.tween(fighter.pos.x, homeX, 0.2, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
sfxBonk(); k.shake(4)
},
// 13: Moonwalk in
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
announceCool('Smoother than a politician changing positions!')
// Moonwalk: visually moving backward but translating forward
fighter.scale.x = -fighter.scale.x // Face away
await k.tween(startX, homeX, 0.8, (v) => {
fighter.pos.x = v
fighter.pos.y = GROUND_Y - 6 + Math.sin((v - startX) * 0.15) * 3
}, k.easings.easeInOutQuad)
fighter.scale.x = -fighter.scale.x // Face right way
sfxBoing(); k.shake(2)
},
// 14: Thrown in by bouncer
async (fighter: any, homeX: number, fromLeft: boolean) => {
const doorX = fromLeft ? -30 : W + 30
fighter.pos.x = doorX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
// Bouncer arm
const arm = k.add([k.rect(40, 15), k.pos(doorX, GROUND_Y - 30), k.anchor(fromLeft ? 'left' : 'right'), k.color(safeColor(k,'#444444')), k.z(12)])
announceSilly('And stay out! You\'re BANNED from the other fight!')
await k.wait(0.3)
// Throw
sfxZoomWhoosh()
arm.destroy()
await Promise.all([
k.tween(doorX, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
k.tween(GROUND_Y - 6, GROUND_Y - 80, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(GROUND_Y - 80, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk(); k.shake(6)
spawnSparks(homeX, GROUND_Y - 10, 6, '#ff6600')
},
// 15: Lightning strike entrance
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 0
// Lightning bolt from sky
const bolt = k.add([k.rect(4, H), k.pos(homeX, 0), k.color(safeColor(k,'#ffff44')), k.opacity(0.9), k.z(20)])
sfxZap(); screenFlash('#ffffff', 0.15); k.shake(12)
await k.wait(0.1)
bolt.destroy()
// Smoke / reveal
for (let i = 0; i < 8; i++) {
const smoke = k.add([k.circle(10 + Math.random() * 15), k.pos(homeX + (Math.random() - 0.5) * 40, GROUND_Y - 20 - Math.random() * 30), k.color(safeColor(k,'#aaaaaa')), k.opacity(0.6), k.z(11)])
k.tween(smoke.opacity, 0, 0.5, (v) => { smoke.opacity = v; smoke.pos.y -= 1 }).then(() => { if (smoke.exists()) smoke.destroy() })
}
await k.wait(0.3)
fighter.opacity = 1
sfxSpecial()
spawnSparks(homeX, GROUND_Y - 20, 10, '#ffff44')
},
// 16: Crowd surf in
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 50; fighter.opacity = 1
// Crowd hands
const hands: any[] = []
for (let i = 0; i < 8; i++) {
const hx = startX + (fromLeft ? 1 : -1) * (Math.abs(homeX - startX) / 8) * i
const h = k.add([k.rect(6, 20), k.pos(hx, GROUND_Y - 10), k.anchor('bot'), k.color(safeColor(k,'#cc9966')), k.z(8)])
hands.push(h)
}
announceCrowdReaction('cheer')
await k.tween(startX, homeX, 0.6, (v) => {
fighter.pos.x = v
fighter.pos.y = GROUND_Y - 50 + Math.sin((v - startX) * 0.08) * 10
}, k.easings.easeOutQuad)
// Drop down
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(3)
hands.forEach(h => { if (h.exists()) h.destroy() })
},
// 17: Riding a shopping cart
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -100 : W + 100
const cartBody = k.add([k.rect(45, 30), k.pos(startX, GROUND_Y - 18), k.anchor('center'), k.color(safeColor(k,'#888888')), k.opacity(0.9), k.z(9)])
const cartWheel = k.add([k.circle(5), k.pos(startX + (fromLeft ? 15 : -15), GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k,'#444')), k.z(9)])
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 40; fighter.opacity = 1
announceSilly('THIS IS MY EMOTIONAL SUPPORT SHOPPING CART!')
sfxZoomWhoosh()
await k.tween(startX, homeX, 0.5, (v) => {
fighter.pos.x = v; cartBody.pos.x = v; cartWheel.pos.x = v + (fromLeft ? 15 : -15)
fighter.pos.y = GROUND_Y - 40 + Math.sin((v - startX) * 0.08) * 5
}, k.easings.easeOutQuad)
// Crash and tumble out
sfxBonk(); k.shake(6)
cartBody.destroy(); cartWheel.destroy()
await k.tween(fighter.pos.y, GROUND_Y - 50, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
spawnSparks(homeX, GROUND_Y - 10, 5, '#888888')
},
// 18: Dramatic slow walk with spotlight
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
// Spotlight cone
const spot = k.add([k.rect(60, H), k.pos(startX - 30, 0), k.color(safeColor(k,'#ffe14d')), k.opacity(0.08), k.z(1)])
announceDramatic('THE CHAMPION ARRIVES! TAXPAYERS FUNDED THIS ENTRANCE!')
await k.tween(startX, homeX, 1.0, (v) => {
fighter.pos.x = v; spot.pos.x = v - 30
}, k.easings.easeInOutQuad)
await k.wait(0.2)
spot.destroy()
sfxSpecial(); k.shake(3)
spawnSparks(homeX, GROUND_Y - 30, 8, '#ffe14d')
},
// 19: Cannon launch
async (fighter: any, homeX: number, fromLeft: boolean) => {
const cannonX = fromLeft ? -40 : W + 40
// Draw cannon
const cannon = k.add([k.rect(50, 25), k.pos(cannonX, GROUND_Y - 20), k.anchor('center'), k.color(safeColor(k,'#333333')), k.z(9), k.rotate(fromLeft ? -30 : 210)])
fighter.pos.x = cannonX; fighter.pos.y = GROUND_Y - 20; fighter.opacity = 0
await k.wait(0.3)
// Fire!
sfxGunshot(); sfxExplosion()
fighter.opacity = 1
screenFlash('#ffcc00', 0.1)
const flashCircle = k.add([k.circle(20), k.pos(cannonX + (fromLeft ? 25 : -25), GROUND_Y - 35), k.color(safeColor(k,'#ffee00')), k.opacity(0.9), k.z(12)])
setTimeout(() => { if (flashCircle.exists()) flashCircle.destroy() }, 80)
// Arc to position
await Promise.all([
k.tween(cannonX, homeX, 0.35, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
k.tween(GROUND_Y - 20, GROUND_Y - 120, 0.17, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(GROUND_Y - 120, GROUND_Y - 6, 0.18, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk(); k.shake(10)
spawnShockwave(homeX, GROUND_Y, '#ff6600')
spawnSparks(homeX, GROUND_Y - 10, 15, '#ffcc00')
cannon.destroy()
},
]
// THE CREATOR: Golden code rain portal entrance
const creatorEntrance = async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX
fighter.pos.y = -100
fighter.opacity = 0
// Golden code rain columns
const codeChars = '₿01∞⚡⛏§∂∆≈≠'.split('')
const rainDrops: any[] = []
for (let col = 0; col < 12; col++) {
const cx = homeX - 60 + col * 10
for (let row = 0; row < 4; row++) {
const ch = codeChars[Math.floor(Math.random() * codeChars.length)]
const rd = k.add([
k.text(ch, { size: 8 }), k.pos(cx, -20 - row * 25),
k.color(safeColor(k, row === 0 ? '#ffd700' : '#c8a000')),
k.opacity(0.6 + Math.random() * 0.4), k.z(48), k.anchor('center'),
])
rainDrops.push(rd)
k.tween(rd.pos.y, GROUND_Y + 20, 0.6 + Math.random() * 0.3, (v) => {
rd.pos.y = v
rd.opacity = Math.max(0, 1 - (v - GROUND_Y + 30) / 50)
}).then(() => { if (rd.exists()) rd.destroy() })
}
}
await k.wait(0.3)
// Portal flash
screenFlash('#ffd700', 0.15)
spawnShockwave(homeX, GROUND_Y - 30, '#c8a000')
sfxSpecial()
// Fighter descends through golden portal
fighter.opacity = 1
await k.tween(-100, GROUND_Y - 6, 0.5, (v) => {
fighter.pos.y = v
}, k.easings.easeOutBack)
k.shake(12)
sfxExplosion()
// Persistent orbiting ₿ letters around creator (always visible)
for (let i = 0; i < 10; i++) {
const ring = i < 6 ? 0 : 1
const ringIdx = ring === 0 ? i : i - 6
const ringCount = ring === 0 ? 6 : 4
const sz = ring === 0 ? 7 + Math.random() * 3 : 10 + Math.random() * 4
const rad = ring === 0 ? 25 + i * 4 : 45 + (i - 6) * 6
const spd = ring === 0 ? 2 + i * 0.3 : -(1.5 + (i - 6) * 0.4)
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000', '#ffffff']
const p = k.add([
k.text('₿', { size: sz }), k.pos(homeX, GROUND_Y - 30),
k.color(safeColor(k, colors[i % colors.length])),
k.opacity(0.5), k.z(11), k.anchor('center'), k.rotate(0),
])
p.onUpdate(() => {
const a = k.time() * spd + ringIdx * (Math.PI * 2 / ringCount)
p.pos.x = fighter.pos.x + Math.cos(a) * rad
p.pos.y = fighter.pos.y - 25 + Math.sin(a) * rad * 0.45
p.opacity = 0.35 + Math.sin(k.time() * 5 + i * 1.2) * 0.25
p.angle = Math.sin(k.time() * 3 + i) * 15
})
}
announceCreatorEntrance()
rainDrops.forEach(r => { if (r.exists()) r.destroy() })
}
// Pick random entrance for each bot (different ones)
const idxA = Math.floor(Math.random() * entrances.length)
let idxB = Math.floor(Math.random() * entrances.length)
while (idxB === idxA && entrances.length > 1) idxB = Math.floor(Math.random() * entrances.length)
// Play entrances with slight stagger — creator always gets special entrance
announceDeepIntro()
if (botA.archetype === 'the_creator') {
await creatorEntrance(fA, HOME_A, true)
} else {
await entrances[idxA](fA, HOME_A, true)
}
await k.wait(0.3)
if (botB.archetype === 'the_creator') {
await creatorEntrance(fB, HOME_B, false)
} else {
await entrances[idxB](fB, HOME_B, false)
}
await k.wait(0.2)
// Safety: ensure both fighters are visible and at home positions
fA.pos.x = HOME_A
fA.pos.y = GROUND_Y - 6
fA.opacity = 1
fB.pos.x = HOME_B
fB.pos.y = GROUND_Y - 6
fB.opacity = 1
// Both idle at home positions
fA.play('idle')
fB.play('idle')
},
// === PHYSICAL CONTACT SYSTEMS ===
// Helper: spawn dizzy stars orbiting a fighter's head
_spawnDizzyStars(fighter: any, duration: number) {
const stars: any[] = []
const starChars = ['*', '\u2605', '\u00d7', '!', '?']
const starColors = ['#ffff00', '#ffffff', '#ff4444', '#44ff44', '#ff88ff']
for (let i = 0; i < 4; i++) {
const s = k.add([
k.text(starChars[i % starChars.length], { size: 8 }),
k.pos(fighter.pos.x, fighter.pos.y - 55),
k.color(safeColor(k, starColors[i % starColors.length])),
k.opacity(0.9), k.z(55), k.anchor('center'),
])
stars.push(s)
}
const startTime = k.time()
const cancel = k.onUpdate(() => {
const elapsed = k.time() - startTime
if (elapsed > duration || !fighter.exists()) {
stars.forEach(s => { if (s.exists()) s.destroy() })
cancel.cancel()
return
}
for (let i = 0; i < stars.length; i++) {
if (!stars[i].exists()) continue
const angle = elapsed * 4 + (i * Math.PI * 2) / stars.length
stars[i].pos.x = fighter.pos.x + Math.cos(angle) * 22
stars[i].pos.y = fighter.pos.y - 55 + Math.sin(angle) * 8
stars[i].opacity = Math.max(0, 1 - elapsed / duration)
}
})
},
// Helper: reset both fighters to home positions
async _resetPositions() {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
await Promise.all([
k.tween(fA.pos.x, HOME_A, 0.2, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
k.tween(fB.pos.x, HOME_B, 0.2, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
k.tween(fA.pos.y, GROUND_Y, 0.15, (v) => { fA.pos.y = v }),
k.tween(fB.pos.y, GROUND_Y, 0.15, (v) => { fB.pos.y = v }),
])
fA.angle = 0; fB.angle = 0; fA.opacity = 1; fB.opacity = 1
fA.play('idle'); fB.play('idle')
},
// 0: RAPID BRAWL — close distance, trade rapid blows
async _brawlRapid(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const midX = W / 2
sfxZoomWhoosh()
await Promise.all([
k.tween(fA.pos.x, midX - 30, 0.12, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
k.tween(fB.pos.x, midX + 30, 0.12, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
])
const hitCount = 4 + Math.floor(intensity * 6) + Math.floor(Math.random() * 3)
const anims = ['attack', 'kick', 'special', 'attack', 'kick'] as const
const hitColors = ['#ff2d7b', '#ff6600', '#ffcc00', '#00f0ff', '#ff2d2d', '#39ff14', '#ffffff', '#b83dff']
for (let i = 0; i < hitCount; i++) {
const aHits = winningSide === 'a' ? Math.random() < 0.65 : winningSide === 'b' ? Math.random() < 0.35 : Math.random() < 0.5
const atk = aHits ? fA : fB; const def = aHits ? fB : fA
atk.play(anims[i % anims.length]); sfxPunch(); def.play('hit')
k.shake(2 + Math.floor(intensity * 3))
spawnSparks(midX + (Math.random() - 0.5) * 20, GROUND_Y - 20 - Math.random() * 30, 3, hitColors[i % hitColors.length])
def.pos.x += (aHits ? 1 : -1) * (3 + Math.random() * 4)
await k.wait(0.04 + (1 - intensity) * 0.04)
}
await this._resetPositions()
},
// 1: KNOCKDOWN BRAWL — combo ends with loser knocked flat, has to get back up
async _brawlKnockdown(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA
const dir = winner === fA ? 1 : -1
// Rush in, quick combo
sfxZoomWhoosh()
const contactX = loser.pos.x - dir * 40
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
const hits = 3 + Math.floor(intensity * 3)
for (let i = 0; i < hits; i++) {
winner.play(i % 2 === 0 ? 'attack' : 'kick'); sfxPunch()
loser.play('hit'); k.shake(3 + i)
spawnSparks(loser.pos.x, loser.pos.y - 20 - Math.random() * 20, 3, '#ff6600')
loser.pos.x += dir * 5
await k.wait(0.05)
}
// Final hit — KNOCKDOWN
winner.play('special'); sfxCritical(); sfxExplosion()
loser.play('knockback'); k.shake(12)
spawnSparks(loser.pos.x, loser.pos.y - 25, 12, '#ff2d2d')
screenFlash('#ff2d2d', 0.1)
if (Math.random() < 0.5) sfxRandomComedy()
// Loser falls to ground — scale Y to flatten, rotate
await Promise.all([
k.tween(loser.pos.x, loser.pos.x + dir * 80, 0.25, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, GROUND_Y + 5, 0.2, (v) => { loser.pos.y = v }, k.easings.easeInQuad),
k.tween(0, dir * 90, 0.25, (v) => { loser.angle = v }),
])
sfxBonk(); k.shake(6)
spawnEmoteText(loser.pos.x, loser.pos.y - 20, 'DOWN!', '#ff4444')
// Loser lies there for a beat
await k.wait(0.4 + (1 - intensity) * 0.4)
// Slowly get back up — stagger, wobble
this._spawnDizzyStars(loser, 1.5)
spawnEmoteText(loser.pos.x, loser.pos.y - 40, '...ugh', '#aaaaaa')
await k.tween(loser.angle, 0, 0.3, (v) => { loser.angle = v }, k.easings.easeOutBack)
loser.play('hit')
// Stagger wobble while getting up
for (let i = 0; i < 3; i++) {
await k.tween(loser.pos.x, loser.pos.x + (i % 2 === 0 ? -12 : 12), 0.12, (v) => { loser.pos.x = v })
}
await k.wait(0.2)
await this._resetPositions()
},
// 2: WALL BOUNCE — punch sends fighter flying into screen edge, bounces off
async _brawlWallBounce(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA
const dir = winner === fA ? 1 : -1
const wallX = dir === 1 ? W - 15 : 15
// Rush in for the big hit
sfxZoomWhoosh()
await k.tween(winner.pos.x, loser.pos.x - dir * 40, 0.1, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
winner.play('special'); sfxCritical()
await k.wait(0.06)
loser.play('knockback'); k.shake(15)
spawnSparks(loser.pos.x, loser.pos.y - 25, 15, '#ff2d7b')
screenFlash('#ffffff', 0.08)
// Loser flies across to wall
await Promise.all([
k.tween(loser.pos.x, wallX, 0.15, (v) => { loser.pos.x = v }, k.easings.easeInQuad),
k.tween(loser.pos.y, GROUND_Y - 30, 0.08, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, GROUND_Y, 0.07, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
])
// WALL IMPACT
sfxExplosion(); k.shake(20)
spawnShockwave(wallX, GROUND_Y, '#ff6600')
spawnSparks(wallX, GROUND_Y - 40, 15, '#ffcc00')
screenFlash('#ff6600', 0.12)
sfxBoneCrack()
spawnEmoteText(wallX, GROUND_Y - 70, 'WALL!', '#ff6600')
// Bounce off wall — loser rebounds back toward center
loser.play('hit')
const bounceX = wallX - dir * (60 + Math.random() * 40)
await Promise.all([
k.tween(loser.pos.x, bounceX, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, GROUND_Y - 50, 0.1, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, GROUND_Y, 0.1, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
k.tween(0, dir * -360, 0.2, (v) => { loser.angle = v }),
])
loser.angle = 0
sfxBonk(); k.shake(5)
// Dizzy stagger after wall bounce
this._spawnDizzyStars(loser, 1.2)
loser.play('hit')
for (let w = 0; w < 4; w++) {
await k.tween(loser.pos.x, loser.pos.x + (w % 2 === 0 ? -10 : 10), 0.1, (v) => { loser.pos.x = v })
}
await k.wait(0.3)
winner.play('win'); spawnEmoteText(winner.pos.x, winner.pos.y - 55, 'GET REKT', '#39ff14')
await k.wait(0.3)
await this._resetPositions()
},
// 3: DIZZY STAGGER — sustained combo leaves loser wobbling around the screen
async _brawlDizzyStagger(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA
const dir = winner === fA ? 1 : -1
// Rush in, big combo
sfxZoomWhoosh()
await k.tween(winner.pos.x, loser.pos.x - dir * 35, 0.1, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
const combo = 5 + Math.floor(intensity * 4)
for (let i = 0; i < combo; i++) {
winner.play(['attack', 'kick', 'special'][i % 3]); sfxPunch()
loser.play('hit'); k.shake(3 + Math.floor(i * 0.5))
spawnSparks((winner.pos.x + loser.pos.x) / 2, GROUND_Y - 25 - Math.random() * 20, 3, i % 2 === 0 ? '#ff2d7b' : '#ffcc00')
loser.pos.x += dir * 4
await k.wait(0.04)
}
// Final uppercut
winner.play('special'); sfxCritical(); k.shake(10)
spawnSparks(loser.pos.x, loser.pos.y - 30, 10, '#ffe14d')
await k.tween(loser.pos.y, GROUND_Y - 40, 0.1, (v) => { loser.pos.y = v }, k.easings.easeOutQuad)
await k.tween(loser.pos.y, GROUND_Y, 0.1, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxBonk()
// DIZZY PHASE — loser staggers around aimlessly
this._spawnDizzyStars(loser, 2.5)
loser.play('hit')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, '...wha?', '#ffff88')
winner.play('idle')
// Stagger path — zigzag drunkenly
const staggerPoints = [
loser.pos.x + dir * 30, loser.pos.x - dir * 50,
loser.pos.x + dir * 20, loser.pos.x - dir * 40,
loser.pos.x + dir * 10,
]
for (let s = 0; s < staggerPoints.length; s++) {
await Promise.all([
k.tween(loser.pos.x, staggerPoints[s], 0.2, (v) => { loser.pos.x = v }),
k.tween(loser.angle, (s % 2 === 0 ? 10 : -10), 0.2, (v) => { loser.angle = v }),
])
if (s % 2 === 0) sfxBoing()
}
loser.angle = 0
// Shake it off
spawnEmoteText(loser.pos.x, loser.pos.y - 40, '...ok', '#88ff88')
await k.wait(0.3)
await this._resetPositions()
},
// 4: SUPLEX SLAM — grab, lift overhead, slam to ground with bounce
async _brawlSuplex(attackerSide: 'a' | 'b', intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1
// Rush in and grab
sfxZoomWhoosh()
await k.tween(attacker.pos.x, defender.pos.x - dir * 30, 0.1, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
attacker.play('special'); sfxBlock()
await k.wait(0.1)
// Lift defender overhead
attacker.play('attack')
const liftX = attacker.pos.x
await Promise.all([
k.tween(defender.pos.x, liftX + dir * 5, 0.15, (v) => { defender.pos.x = v }),
k.tween(defender.pos.y, GROUND_Y - 120, 0.2, (v) => { defender.pos.y = v }, k.easings.easeOutQuad),
k.tween(0, 180, 0.2, (v) => { defender.angle = v }),
])
sfxPowerUp()
spawnEmoteText(attacker.pos.x, attacker.pos.y - 70, 'SUPLEX!', '#ff4444')
await k.wait(0.25)
// SLAM DOWN
sfxExplosion()
await Promise.all([
k.tween(defender.pos.y, GROUND_Y + 5, 0.1, (v) => { defender.pos.y = v }, k.easings.easeInQuad),
k.tween(defender.angle, 360, 0.1, (v) => { defender.angle = v }),
])
defender.angle = 0; defender.play('knockback')
k.shake(22); screenFlash('#ff2d2d', 0.15)
spawnShockwave(defender.pos.x, GROUND_Y, '#ff2d2d')
spawnSparks(defender.pos.x, GROUND_Y - 10, 20, '#ff6600')
sfxBoneCrack()
// Ground bounce
await k.tween(defender.pos.y, GROUND_Y - 40, 0.1, (v) => { defender.pos.y = v }, k.easings.easeOutQuad)
sfxBoing()
await k.tween(defender.pos.y, GROUND_Y, 0.1, (v) => { defender.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(5)
// Bounce 2 (smaller)
await k.tween(defender.pos.y, GROUND_Y - 15, 0.08, (v) => { defender.pos.y = v }, k.easings.easeOutQuad)
await k.tween(defender.pos.y, GROUND_Y, 0.08, (v) => { defender.pos.y = v }, k.easings.easeInQuad)
// Dizzy on the ground
this._spawnDizzyStars(defender, 1.5)
defender.play('hit')
await k.wait(0.5)
attacker.play('win')
await k.wait(0.3)
await this._resetPositions()
},
// 5: PING PONG VOLLEY — fighters knock each other back and forth across the screen
async _brawlPingPong(winningSide: 'a' | 'b' | null, intensity: number) {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const volleys = 3 + Math.floor(intensity * 3) + Math.floor(Math.random() * 2) // 3-8
// Start in center
sfxZoomWhoosh()
await Promise.all([
k.tween(fA.pos.x, W / 2 - 30, 0.1, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
k.tween(fB.pos.x, W / 2 + 30, 0.1, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
])
for (let v = 0; v < volleys; v++) {
const aHits = v % 2 === 0
const atk = aHits ? fA : fB
const def = aHits ? fB : fA
const pDir = aHits ? 1 : -1
const pushDist = 50 + Math.random() * 40
atk.play(v % 3 === 0 ? 'attack' : v % 3 === 1 ? 'kick' : 'special')
sfxPunch(); def.play('hit'); k.shake(5 + v)
spawnSparks((atk.pos.x + def.pos.x) / 2, GROUND_Y - 30, 5, v % 2 === 0 ? '#ff2d7b' : '#00f0ff')
// Defender flies back
await Promise.all([
k.tween(def.pos.x, def.pos.x + pDir * pushDist, 0.1, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.y, GROUND_Y - 25, 0.05, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(def.pos.y, GROUND_Y, 0.05, (v) => { def.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk()
// Attacker chases
await k.tween(atk.pos.x, def.pos.x - pDir * 35, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
await k.wait(0.03)
}
// Final hit sends loser sliding
const finalAtk = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (volleys % 2 === 0 ? fA : fB)
const finalDef = finalAtk === fA ? fB : fA
finalAtk.play('special'); sfxCritical(); k.shake(12)
finalDef.play('knockback')
spawnSparks(finalDef.pos.x, finalDef.pos.y - 25, 12, '#ffe14d')
if (Math.random() < 0.5) sfxRandomComedy()
await k.wait(0.3)
await this._resetPositions()
},
// 6: GROUND AND POUND — attacker pins defender, hits them on the ground
async _brawlGroundPound(attackerSide: 'a' | 'b', intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1
// Rush in and tackle
sfxZoomWhoosh()
await k.tween(attacker.pos.x, defender.pos.x - dir * 30, 0.08, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
attacker.play('attack'); sfxPunch()
defender.play('knockback'); k.shake(8)
// Defender falls to ground (flatten angle)
await Promise.all([
k.tween(defender.pos.x, defender.pos.x + dir * 20, 0.15, (v) => { defender.pos.x = v }),
k.tween(0, dir * 85, 0.15, (v) => { defender.angle = v }),
])
sfxBonk()
// Attacker moves on top
await k.tween(attacker.pos.x, defender.pos.x - dir * 15, 0.08, (v) => { attacker.pos.x = v })
// Ground pound — rapid hits on downed fighter
const poundHits = 4 + Math.floor(intensity * 5)
for (let i = 0; i < poundHits; i++) {
attacker.play(i % 2 === 0 ? 'attack' : 'kick')
sfxPunch()
k.shake(2 + Math.floor(i * 0.4))
spawnSparks(defender.pos.x + (Math.random() - 0.5) * 20, defender.pos.y - 10, 2, i % 2 === 0 ? '#ff2d7b' : '#ff6600')
defender.pos.y += (i % 2 === 0 ? -2 : 2) // jostle
await k.wait(0.06 + (1 - intensity) * 0.04)
}
// Attacker gets up, backs off
attacker.play('win')
sfxRandomComedy()
await k.wait(0.3)
// Defender slowly gets up
this._spawnDizzyStars(defender, 1.8)
await k.tween(defender.angle, 0, 0.4, (v) => { defender.angle = v }, k.easings.easeOutBack)
defender.play('hit')
spawnEmoteText(defender.pos.x, defender.pos.y - 45, '...ouch', '#ff8888')
await k.wait(0.4)
await this._resetPositions()
},
// 7: HAYMAKER — slow dramatic wind-up, pause, devastating single hit with knockdown
async _brawlHaymaker(attackerSide: 'a' | 'b', _intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1
// Slow walk toward defender
attacker.play('kick')
await k.tween(attacker.pos.x, defender.pos.x - dir * 45, 0.4, (v) => { attacker.pos.x = v }, k.easings.easeInOutQuad)
// Wind-up — lean back
attacker.play('special')
sfxDrumRoll()
await k.tween(attacker.pos.x, attacker.pos.x - dir * 20, 0.3, (v) => { attacker.pos.x = v }, k.easings.easeOutQuad)
spawnEmoteText(attacker.pos.x, attacker.pos.y - 55, '...', '#ffffff')
await k.wait(0.5) // dramatic pause
// HAYMAKER
sfxCritical(); sfxExplosion()
attacker.play('attack')
await k.tween(attacker.pos.x, defender.pos.x - dir * 25, 0.04, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
defender.play('knockback'); k.shake(25)
screenFlash('#ffffff', 0.15)
spawnSparks(defender.pos.x, defender.pos.y - 25, 20, '#ff2d2d')
spawnShockwave(defender.pos.x, GROUND_Y, '#ff2d2d')
sfxVineBoom()
// Defender flies across screen with spin
const flyX = defender.pos.x + dir * 150
await Promise.all([
k.tween(defender.pos.x, Math.min(W - 10, Math.max(10, flyX)), 0.25, (v) => { defender.pos.x = v }, k.easings.easeOutQuad),
k.tween(defender.pos.y, GROUND_Y - 80, 0.12, (v) => { defender.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(defender.pos.y, GROUND_Y, 0.13, (v) => { defender.pos.y = v }, k.easings.easeInQuad)
),
k.tween(0, dir * 720, 0.25, (v) => { defender.angle = v }),
])
defender.angle = 0; sfxBonk(); k.shake(8)
// Knockdown — rotate flat
await k.tween(0, dir * 90, 0.15, (v) => { defender.angle = v })
spawnEmoteText(defender.pos.x, defender.pos.y - 20, 'K.O.?!', '#ff4444')
this._spawnDizzyStars(defender, 2.0)
await k.wait(0.6)
// Get up slowly
await k.tween(defender.angle, 0, 0.35, (v) => { defender.angle = v }, k.easings.easeOutBack)
defender.play('hit')
await k.wait(0.3)
attacker.play('idle')
await this._resetPositions()
},
// 8: BODY CHECK BOUNCER — both charge, collide, one bounces off the other
async _brawlBodyCheck(winningSide: 'a' | 'b' | null, _intensity: number) {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) return
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
const loser = winner === fA ? fB : fA
const dir = winner === fA ? 1 : -1
const midX = W / 2
// Both charge at each other
fA.play('special'); fB.play('special')
sfxZoomWhoosh()
await Promise.all([
k.tween(fA.pos.x, midX - 20, 0.12, (v) => { fA.pos.x = v }, k.easings.easeInQuad),
k.tween(fB.pos.x, midX + 20, 0.12, (v) => { fB.pos.x = v }, k.easings.easeInQuad),
])
// COLLISION
sfxCritical(); k.shake(18)
spawnSparks(midX, GROUND_Y - 40, 15, '#ffffff')
screenFlash('#ffffff', 0.1)
spawnShockwave(midX, GROUND_Y, '#ffcc00')
// Winner holds ground, loser bounces off
winner.play('win')
loser.play('knockback')
const bounceX = loser === fA ? -20 : W + 20
await Promise.all([
k.tween(loser.pos.x, bounceX, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, GROUND_Y - 60, 0.1, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, GROUND_Y, 0.1, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
k.tween(0, (loser === fA ? -1 : 1) * 540, 0.2, (v) => { loser.angle = v }),
])
// Off screen! Slide back in
loser.angle = 0; loser.play('hit')
sfxSlideWhistleDown()
const slideFrom = loser === fA ? -30 : W + 30
loser.pos.x = slideFrom
await k.tween(loser.pos.x, loser === fA ? HOME_A : HOME_B, 0.4, (v) => { loser.pos.x = v }, k.easings.easeOutBounce)
this._spawnDizzyStars(loser, 1.5)
sfxBoing()
spawnEmoteText(loser.pos.x, loser.pos.y - 50, '...what happened?', '#ffff88')
await k.wait(0.5)
await this._resetPositions()
},
// Dispatch: pick a random physical exchange variant
async _brawlExchange(winningSide: 'a' | 'b' | null, intensity: number) {
const variant = Math.floor(Math.random() * 9)
switch (variant) {
case 0: return this._brawlRapid(winningSide, intensity)
case 1: return this._brawlKnockdown(winningSide, intensity)
case 2: return this._brawlWallBounce(winningSide, intensity)
case 3: return this._brawlDizzyStagger(winningSide, intensity)
case 4: return this._brawlSuplex(winningSide === 'a' ? 'a' : winningSide === 'b' ? 'b' : (Math.random() > 0.5 ? 'a' : 'b'), intensity)
case 5: return this._brawlPingPong(winningSide, intensity)
case 6: return this._brawlGroundPound(winningSide === 'a' ? 'a' : winningSide === 'b' ? 'b' : (Math.random() > 0.5 ? 'a' : 'b'), intensity)
case 7: return this._brawlHaymaker(winningSide === 'a' ? 'a' : winningSide === 'b' ? 'b' : (Math.random() > 0.5 ? 'a' : 'b'), intensity)
case 8: return this._brawlBodyCheck(winningSide, intensity)
}
},
// Clinch combo: grapple at close range, one fighter dominates with sustained contact
async _clinchCombo(attackerSide: 'a' | 'b', intensity: number) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return
const dir = attackerSide === 'a' ? 1 : -1
const origAX = attackerSide === 'a' ? HOME_A : HOME_B
const origDX = attackerSide === 'a' ? HOME_B : HOME_A
const clinchX = (origAX + origDX) / 2
sfxZoomWhoosh()
attacker.play('attack')
await k.tween(attacker.pos.x, clinchX - dir * 25, 0.1, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
await k.tween(defender.pos.x, clinchX + dir * 25, 0.08, (v) => { defender.pos.x = v }, k.easings.easeOutQuad)
const clinchHits = 3 + Math.floor(intensity * 5) + Math.floor(Math.random() * 3)
const movePool = ['attack', 'kick', 'special'] as const
for (let i = 0; i < clinchHits; i++) {
attacker.play(movePool[i % movePool.length]); sfxPunch()
await k.wait(0.03)
defender.play('hit'); k.shake(2 + Math.floor(i * 0.5))
defender.pos.x += dir * 4; attacker.pos.x += dir * 3
spawnSparks((attacker.pos.x + defender.pos.x) / 2, GROUND_Y - 25 - Math.random() * 20, 2 + Math.floor(intensity * 3), i % 2 === 0 ? '#ff2d7b' : '#ffcc00')
if (Math.random() < 0.3) {
defender.play(movePool[Math.floor(Math.random() * movePool.length)]); sfxBlock()
attacker.play('hit'); k.shake(3); attacker.pos.x -= dir * 5; await k.wait(0.04)
}
await k.wait(0.04 + (1 - intensity) * 0.03)
}
sfxKick(); defender.play('knockback'); k.shake(6 + Math.floor(intensity * 6))
spawnSparks(defender.pos.x, defender.pos.y - 25, 8, '#ff2d2d')
await k.tween(defender.pos.x, origDX + dir * 30, 0.15, (v) => { defender.pos.x = v }, k.easings.easeOutQuad)
attacker.play('idle')
await Promise.all([
k.tween(attacker.pos.x, origAX, 0.2, (v) => { attacker.pos.x = v }, k.easings.easeOutQuad),
k.tween(defender.pos.x, origDX, 0.25, (v) => { defender.pos.x = v }, k.easings.easeInOutQuad),
])
defender.play('idle')
},
// Counter-attack: after a choreography hit, the defender retaliates briefly
async _counterAttack(defenderSide: 'a' | 'b') {
const defender = k.get(defenderSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const attacker = k.get(defenderSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!defender || !attacker) return
const dir = defenderSide === 'a' ? 1 : -1
const origDX = defenderSide === 'a' ? HOME_A : HOME_B
defender.play('attack'); sfxZoomWhoosh()
await k.tween(defender.pos.x, attacker.pos.x - dir * 35, 0.08, (v) => { defender.pos.x = v }, k.easings.easeInQuad)
const counterHits = 2 + Math.floor(Math.random() * 2)
for (let i = 0; i < counterHits; i++) {
defender.play(i % 2 === 0 ? 'attack' : 'kick'); sfxPunch()
attacker.play('hit'); k.shake(4)
spawnSparks(attacker.pos.x + (Math.random() - 0.5) * 15, attacker.pos.y - 20 - Math.random() * 15, 3, '#00f0ff')
await k.wait(0.05)
}
attacker.play('idle'); defender.play('idle')
await k.tween(defender.pos.x, origDX, 0.15, (v) => { defender.pos.x = v }, k.easings.easeOutQuad)
},
async playAttack(side: 'a' | 'b', choreographyName: string, isCritical: boolean) {
const attacker = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const defender = k.get(side === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!attacker || !defender) return
const origAX = side === 'a' ? HOME_A : HOME_B
const origDX = side === 'a' ? HOME_B : HOME_A
const direction = side === 'a' ? 1 : -1
const fn = choreographyMap[choreographyName] || dashPunch
await fn(attacker, defender, direction, origAX, origDX, isCritical)
// Ensure positions are correct and sprites are idle
attacker.pos.x = origAX
defender.pos.x = origDX
attacker.opacity = 1
defender.opacity = 1
await k.wait(0.1)
attacker.play('idle')
defender.play('idle')
},
async playRound(event: RoundEvent) {
const fA = k.get('fighterA')[0] as Fighter
const fB = k.get('fighterB')[0] as Fighter
if (!fA || !fB) { await k.wait(0.5); return } // Sprites missing (mobile load failure) — skip animation
const aWon = event.winnerId === event.botAId
const bWon = event.winnerId === event.botBId
const margin = Math.abs(event.botAScore - event.botBScore)
const intensity = Math.min(1, margin / 8) // 0.0 to 1.0 continuous
const isCritical = margin > 4
// Super-speed scales with intensity
const superSpeed = Math.random() < (0.1 + intensity * 0.35 + (event.challengeType === 'speed_blitz' ? 0.2 : 0))
// 3-7 exchanges — later rounds and higher intensity get more, ensuring sustained fights
const roundBonus = Math.min(2, Math.floor(event.round / 2))
const exchangeCount = 3 + roundBonus + Math.floor(Math.random() * 3) + (intensity > 0.5 ? 1 : 0)
// Hyperdetail scales with intensity
const hyperDetail = Math.random() < (0.05 + intensity * 0.45)
const savedScaleAX = fA?.scale.x
const savedScaleAY = fA?.scale.y
const savedScaleBX = fB?.scale.x
const savedScaleBY = fB?.scale.y
// Voice: round start announcements (30% chance, 50% if Creator) — fires BEFORE animation starts
const creatorInFight = botA.archetype === 'the_creator' || botB.archetype === 'the_creator'
let didChallengeVoice = false
if (Math.random() < (creatorInFight ? 0.5 : 0.3)) {
// Creator fights get existential commentary more often
if (creatorInFight && Math.random() < 0.6) {
announceCreatorRound(); didChallengeVoice = true
} else {
const challengeVoice: Record<string, () => void> = {
roast_battle: () => announceHype('TIME TO GET ROASTED! SOMEBODY CALL THE FIRE DEPARTMENT!'),
food_fight: () => announceSilly('FOOD FIGHT! SOMEBODY\'S GETTING SERVED!'),
wrestling_match: () => announceDramatic('FROM THE TOP ROPE! THIS IS GONNA GET PHYSICAL!'),
music_battle: () => announceCool('MUSIC BATTLE! DROP THE BEAT AND YOUR OPPONENT!'),
magic_duel: () => announceDramatic('WIZARDS AT DAWN! SOMEBODY\'S GETTING HEXED!'),
meme_war: () => announceSilly('MEME WAR! YOUR HUMOR IS ABOUT TO GET RATIO\'D!'),
demolition: () => announceHype('DEMOLITION DERBY! NOTHING WILL SURVIVE THIS!'),
medieval_combat: () => announceDramatic('MEDIEVAL COMBAT! CHIVALRY IS DEAD AND SO IS YOUR OPPONENT!'),
space_war: () => announceCool('SPACE WAR! HOUSTON, WE HAVE A PROBLEM!'),
speed_blitz: () => announceHype('SPEED BLITZ! BLINK AND YOU\'LL MISS THE CARNAGE!'),
math_blitz: () => announceSilly('MATH BLITZ! SOMEBODY\'S ABOUT TO GET DIVIDED!'),
riddle: () => announceCool('RIDDLE TIME! BRAINS OVER BRAWN! BUT ALSO BRAWN!'),
code_golf: () => announceSilly('CODE GOLF! MAY THE SHORTEST SOLUTION WIN!'),
creative_writing: () => announceCool('CREATIVE WRITING! THE PEN IS MIGHTIER THAN THE SWORD!'),
hallucination_check: () => announceHype('HALLUCINATION CHECK! REALITY IS ABOUT TO HIT DIFFERENT!'),
trap_card: () => announceDramatic('TRAP CARD ACTIVATED! SOMEBODY FELL FOR IT!'),
token_economy: () => announceSilly('TOKEN ECONOMY! SOMEBODY\'S GOING BANKRUPT!'),
retro_mode: () => announceHype('RETRO MODE! INSERT COIN! FIGHT!'),
}
const voiceFn = challengeVoice[event.challengeType]
if (voiceFn) { voiceFn(); didChallengeVoice = true }
else if (Math.random() < 0.5) { announceRoundHype(); didChallengeVoice = true }
}
}
// Give the voice line time to finish before the first punch lands
if (didChallengeVoice) await k.wait(0.8)
// Visual chaos: schizo cut before round (20% chance)
if (Math.random() < 0.2 && fA && fB) {
await schizoCut()
}
if (hyperDetail && fA && fB) {
// Zoom both bots up 1.8x and shift toward center for close-up feel
const zoomFactor = 1.6 + Math.random() * 0.4
await Promise.all([
k.tween(Math.abs(fA.scale.x), Math.abs(fA.scale.x) * zoomFactor, 0.3, (v) => {
fA.scale.x = savedScaleAX! > 0 ? v : -v; fA.scale.y = v
}, k.easings.easeOutQuad),
k.tween(Math.abs(fB.scale.x), Math.abs(fB.scale.x) * zoomFactor, 0.3, (v) => {
fB.scale.x = savedScaleBX! > 0 ? v : -v; fB.scale.y = v
}, k.easings.easeOutQuad),
k.tween(fA.pos.x, HOME_A + (W / 2 - HOME_A) * 0.25, 0.3, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
k.tween(fB.pos.x, HOME_B - (HOME_B - W / 2) * 0.25, 0.3, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
])
// Ren & Stimpy grotesque close-up overlays
spawnGrotesqueDetails(fA, zoomFactor * 0.6)
spawnGrotesqueDetails(fB, zoomFactor * 0.6)
// VHS tracking during hyperdetail
vhsTracking(0.5)
}
// Super-speed: persistent speed lines during round
let speedLines: any[] = []
if (superSpeed) {
for (let i = 0; i < 10; i++) {
const lineY = GROUND_Y - 5 - Math.random() * 130
const line = k.add([
k.rect(W, 1 + Math.random()), k.pos(0, lineY),
k.color(safeColor(k,Math.random() > 0.5 ? '#ffffff' : theme.accent)),
k.opacity(0.15 + Math.random() * 0.1), k.z(25),
])
line.onUpdate(() => { line.opacity = 0.1 + Math.sin(k.time() * 8 + i * 2) * 0.08 })
speedLines.push(line)
}
}
// === RETRO MODE GAMEPAD OVERLAY ===
const retroObjs: any[] = []
if (event.challengeType === 'retro_mode') {
const padW = 90
const padH = 70
const padY = 18
const padAX = 12
const padBX = W - padW - 12
const btnSize = 14
const dpadColor = '#333333'
const btnOff = '#444444'
const btnA = '#22cc44'
const btnB = '#cc3333'
const labelColor = '#00f0ff'
// Draw two gamepads
for (const side of ['a', 'b'] as const) {
const px = side === 'a' ? padAX : padBX
// Pad background
retroObjs.push(k.add([k.rect(padW, padH, { radius: 6 }), k.pos(px, padY), k.color(safeColor(k, '#111111')), k.opacity(0.85), k.z(48)]))
retroObjs.push(k.add([k.rect(padW, padH, { radius: 6 }), k.pos(px, padY), k.color(safeColor(k, '#00f0ff')), k.opacity(0.15), k.z(48), k.outline(1)]))
// D-pad
const dx = px + 20
const dy = padY + 28
// Up
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx - btnSize / 2, dy - btnSize * 1.2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_up']))
// Down
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx - btnSize / 2, dy + btnSize * 0.2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_down']))
// Left
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx - btnSize * 1.6, dy - btnSize / 2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_left']))
// Right
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx + btnSize * 0.6, dy - btnSize / 2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_right']))
// A button
retroObjs.push(k.add([k.circle(8), k.pos(px + padW - 30, dy - 6), k.color(safeColor(k, btnOff)), k.opacity(0.9), k.z(49), 'retro_' + side + '_A']))
retroObjs.push(k.add([k.text('A', { size: 8 }), k.pos(px + padW - 33, dy - 10), k.color(safeColor(k, '#888')), k.z(50)]))
// B button
retroObjs.push(k.add([k.circle(8), k.pos(px + padW - 16, dy + 6), k.color(safeColor(k, btnOff)), k.opacity(0.9), k.z(49), 'retro_' + side + '_B']))
retroObjs.push(k.add([k.text('B', { size: 8 }), k.pos(px + padW - 19, dy + 2), k.color(safeColor(k, '#888')), k.z(50)]))
// Label
const label = side === 'a' ? 'P1' : 'P2'
retroObjs.push(k.add([k.text(label, { size: 10 }), k.pos(px + padW / 2 - 6, padY + 3), k.color(safeColor(k, labelColor)), k.z(50)]))
}
// Animate gamepad button presses based on bot responses
const flashBtn = async (side: 'a' | 'b', inputStr: string) => {
if (!inputStr) return
const arrows: Record<string, string> = { '↑': 'up', '↓': 'down', '←': 'left', '→': 'right' }
for (const ch of inputStr) {
const dir = arrows[ch]
if (dir) {
const tag = 'retro_' + side + '_' + dir
const objs = k.get(tag)
for (const o of objs) { o.color = safeColor(k, '#00f0ff'); }
await k.wait(0.08)
for (const o of objs) { o.color = safeColor(k, dpadColor); }
}
if (ch === 'A' || ch === 'a') {
const objs = k.get('retro_' + side + '_A')
for (const o of objs) { if (o.color) o.color = safeColor(k, btnA); }
await k.wait(0.08)
for (const o of objs) { if (o.color) o.color = safeColor(k, btnOff); }
}
if (ch === 'B' || ch === 'b') {
const objs = k.get('retro_' + side + '_B')
for (const o of objs) { if (o.color) o.color = safeColor(k, btnB); }
await k.wait(0.08)
for (const o of objs) { if (o.color) o.color = safeColor(k, btnOff); }
}
}
}
// Parse and animate each bot's combo moves
const movesA = (event.botAResponse || '').split('|').map(s => s.trim()).filter(Boolean).slice(0, 3)
const movesB = (event.botBResponse || '').split('|').map(s => s.trim()).filter(Boolean).slice(0, 3)
// Flash moves in parallel for both pads
const animateCombo = async (side: 'a' | 'b', moves: string[]) => {
for (const combo of moves) {
await flashBtn(side, combo)
// Show combo text above pad
const px = side === 'a' ? padAX : padBX
const comboLabel = k.add([k.text(combo, { size: 8 }), k.pos(px + 4, padY + padH + 4), k.color(safeColor(k, '#ffff00')), k.opacity(1), k.z(50)])
retroObjs.push(comboLabel)
await k.wait(0.25)
comboLabel.opacity = 0
}
}
// Fire-and-forget — the animations run during the exchange loop below
animateCombo('a', movesA)
animateCombo('b', movesB)
}
for (let ex = 0; ex < exchangeCount; ex++) {
const isLastExchange = ex === exchangeCount - 1
// In earlier exchanges, sometimes the loser attacks back
let attackerSide: 'a' | 'b'
let exchangeCritical = false
if (isLastExchange) {
// Final exchange: winner lands the decisive blow
attackerSide = aWon ? 'a' : bWon ? 'b' : (Math.random() > 0.5 ? 'a' : 'b')
exchangeCritical = isCritical
} else if (aWon || bWon) {
// Earlier exchanges: mix of both sides attacking
const winnerSide = aWon ? 'a' : 'b'
const loserSide = aWon ? 'b' : 'a'
// Loser hits back less at high intensity (more one-sided domination)
attackerSide = Math.random() < Math.max(0.1, 0.4 - intensity * 0.25) ? loserSide : winnerSide
exchangeCritical = false
// Occasionally play a comedy sound on non-decisive hits
if (Math.random() < 0.15) { Math.random() < 0.5 ? sfxRandomComedy() : sfxRandomSilly() }
} else {
// Draw: alternate
attackerSide = ex % 2 === 0 ? 'a' : 'b'
}
const attackerTier = attackerSide === 'a' ? botA.tier : botB.tier
const attackerArch = attackerSide === 'a' ? botA.archetype : botB.archetype
const choreo = pickChoreography(event.challengeType, exchangeCritical, event.round, attackerTier, attackerArch)
// Morph system: transform during ultimates or devastating crits
const isUltimate = choreo.startsWith('ultimate')
const isCreatorFighter = attackerArch === 'the_creator'
// Creator: always morph on ultimates, 70% on devastating crits
const shouldMorph = isCreatorFighter
? (isUltimate || (exchangeCritical && intensity > 0.6 && Math.random() < 0.7))
: (isUltimate || (exchangeCritical && intensity > 0.7 && Math.random() < 0.4))
let morphRevert: (() => Promise<void>) | null = null
if (shouldMorph) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (attacker) {
if (isCreatorFighter) {
// Omni-morph: creator transforms into a random archetype (sprite swap)
const originalSpriteKey = attackerSide === 'a' ? 'botA' : 'botB'
const morphedTo = await applyCreatorOmniMorph(attacker, attackerSide)
screenFlash('#ffd700', 0.15)
sfxZoomWhoosh()
sfxSpecial()
announceCreatorMorph(morphedTo)
const savedSX = attacker.scale.x
const savedSY = attacker.scale.y
await k.tween(0, 1, 0.3, (t) => {
attacker.scale.x = savedSX * (1 + t * 0.3)
attacker.scale.y = savedSY * (1 + t * 0.3)
}, k.easings.easeOutBack)
spawnShockwave(attacker.pos.x, attacker.pos.y - 20, '#ffd700')
k.shake(10)
glitchRGB(0.25)
morphRevert = async () => {
destroyMorphOverlays()
screenFlash('#c8a000', 0.1)
// Swap sprite back to original Creator
attacker.use(k.sprite(originalSpriteKey, { anim: 'idle' }))
attacker.color = safeColor(k, '#ffffff')
attacker.opacity = 1
await k.tween(0, 1, 0.2, (t) => {
attacker.scale.x = savedSX * 1.3 + t * (savedSX - savedSX * 1.3)
attacker.scale.y = savedSY * 1.3 + t * (savedSY - savedSY * 1.3)
}, k.easings.easeInQuad)
}
await k.wait(0.2)
} else {
const seed = attackerSide === 'a' ? botA.seed : botB.seed
const morphOrder = getMorphOrder(seed)
// Cycle through morphs based on round number
const morphIdx = morphOrder[event.round % 3]
const result = await playMorph(attacker, attackerSide, morphIdx)
morphRevert = result.revert
await k.wait(0.2)
}
}
}
// Camera pull-back for ultimates: zoom out slightly to show the full move
if (isUltimate && !hyperDetail && fA && fB) {
await cameraZoom(
[fA, fB],
[{ x: savedScaleAX!, y: savedScaleAY! }, { x: savedScaleBX!, y: savedScaleBY! }],
0.85, 0.2, k.easings.easeOutQuad,
)
}
if (exchangeCritical && isLastExchange) {
fanfareCritical()
announceDramatic([
'CRITICAL HIT! THAT\'S GONNA LEAVE A MARK!',
'CRITICAL HIT! SOMEBODY CHECK IF THAT\'S LEGAL!',
'CRITICAL! THEIR INSURANCE DOESN\'T COVER THIS!',
'CRITICAL HIT! THAT WAS PERSONAL!',
'CRITICAL! EVEN THE REFEREE FELT THAT!',
'CRITICAL! SEND THE AMBULANCE! ACTUALLY SEND TWO!',
'CRITICAL HIT! THAT BOT\'S WARRANTY JUST EXPIRED!',
'CRITICAL! THE SCOREBOARD CAN\'T EVEN HANDLE THIS!',
'OHHH CRITICAL! RIGHT IN THE CIRCUITS!',
'CRITICAL! THAT\'S NOT A HIT, THAT\'S A STATEMENT!',
][Math.floor(Math.random() * 10)])
await k.wait(0.4) // let the voice line land before the hit animation
announceCrowdReaction('gasp')
// Camera zoom-in for dramatic critical blow
if (!hyperDetail && fA && fB) {
await cameraZoom(
[fA, fB],
[{ x: savedScaleAX!, y: savedScaleAY! }, { x: savedScaleBX!, y: savedScaleBY! }],
1.3, 0.2, k.easings.easeOutQuad,
)
}
// RGB glitch + hyperspeed lines on critical final blow
glitchRGB(0.3)
const defPos = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (defPos) hyperSpeedLines(defPos.pos.x, defPos.pos.y - 30, 0.4)
}
// Random scanline glitch during exchanges (25%)
if (Math.random() < 0.25) scanlineGlitch(0.2)
// Play the exchange — mix of choreography, brawls, clinches for ~50% physical contact
if (!aWon && !bWon && isLastExchange) {
// Draw: brawl in the middle (sustained contact)
await this._brawlExchange(null, intensity)
} else {
// Decide exchange type: choreography, brawl, or clinch
// Brawl/clinch chance increases with later exchanges and higher intensity
const physicalChance = 0.35 + intensity * 0.2 + (ex > 1 ? 0.15 : 0)
const exchangeType = Math.random()
if (!isLastExchange && exchangeType < physicalChance * 0.5) {
// BRAWL: both fighters close distance and trade rapid blows
await this._brawlExchange(aWon ? 'a' : bWon ? 'b' : null, intensity)
} else if (!isLastExchange && exchangeType < physicalChance) {
// CLINCH COMBO: attacker grapples defender at close range
await this._clinchCombo(attackerSide, intensity)
} else if (!isLastExchange && Math.random() < 0.12) {
// DODGE: defender dodges (reduced from 15% to 12% — more contact, fewer misses)
await this.playDodge(attackerSide === 'a' ? 'b' : 'a')
sfxDodge()
if (Math.random() < 0.4) sfxRandomFail(); else sfxBoing()
} else {
// CHOREOGRAPHY: signature move from the pool
await this.playAttack(attackerSide, choreo, exchangeCritical)
// COUNTER-ATTACK: defender fights back after getting hit (25% chance on non-final exchanges)
if (!isLastExchange && !exchangeCritical && Math.random() < 0.25) {
await k.wait(0.08)
await this._counterAttack(attackerSide === 'a' ? 'b' : 'a')
}
}
}
// Revert morph after the exchange
if (morphRevert) {
await morphRevert()
morphRevert = null
}
// Brief pause between exchanges (much shorter in super-speed)
if (!isLastExchange) await k.wait(superSpeed ? 0.05 + Math.random() * 0.1 : 0.2 + Math.random() * 0.25)
}
// Clean up speed lines
speedLines.forEach(l => { if (l.exists()) l.destroy() })
speedLines = []
// Clean up retro gamepad overlays
retroObjs.forEach(o => { if (o.exists()) o.destroy() })
// Clean up grotesque overlays and any leftover morphs before zooming out
destroyGrotesqueDetails()
destroyMorphOverlays()
// Zoom back out from hyperdetail or critical camera zoom
const needsZoomOut = (hyperDetail || (isCritical && !hyperDetail)) && fA && fB && savedScaleAX != null && savedScaleBX != null
if (needsZoomOut) {
await Promise.all([
k.tween(fA.scale.y, Math.abs(savedScaleAY!), 0.25, (v) => {
fA.scale.x = savedScaleAX > 0 ? v : -v; fA.scale.y = v
}, k.easings.easeInOutQuad),
k.tween(fB.scale.y, Math.abs(savedScaleBY!), 0.25, (v) => {
fB.scale.x = savedScaleBX > 0 ? v : -v; fB.scale.y = v
}, k.easings.easeInOutQuad),
k.tween(fA.pos.x, HOME_A, 0.25, (v) => { fA.pos.x = v }, k.easings.easeInOutQuad),
k.tween(fB.pos.x, HOME_B, 0.25, (v) => { fB.pos.x = v }, k.easings.easeInOutQuad),
])
}
// Judge calls the round (non-blocking)
const judge = k.get('judge')[0]
if (judge) {
if (isCritical) {
judge.play('shocked')
await k.wait(0.25)
}
judge.play(aWon ? 'call_left' : bWon ? 'call_right' : 'idle')
k.wait(0.8).then(() => { if (judge.exists()) judge.play('idle') })
}
// === THE CREATOR CAMEO ===
// 6% chance per round (only if neither fighter IS the creator)
const neitherIsCreator = botA.archetype !== 'the_creator' && botB.archetype !== 'the_creator'
if (neitherIsCreator && Math.random() < 0.06) {
const cameoX = k.width() / 2
const cameoY = GROUND_Y - 80
// Golden portal flash
screenFlash('#ffd700', 0.1)
const portal = k.add([
k.circle(30), k.pos(cameoX, cameoY), k.color(safeColor(k, '#c8a000')),
k.opacity(0), k.z(50), k.anchor('center'), k.scale(0.1),
])
await k.tween(0, 1, 0.3, (t) => {
portal.opacity = t * 0.4
portal.scale.x = t * 1.5; portal.scale.y = t * 1.5
}, k.easings.easeOutBack)
// ₿ symbol descends from portal
const gift = k.add([
k.text('₿', { size: 18 }), k.pos(cameoX, cameoY - 30),
k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(51), k.anchor('center'),
])
const creatorLabel = k.add([
k.text('THE CREATOR', { size: 8 }), k.pos(cameoX, cameoY + 25),
k.color(safeColor(k, '#c8a000')), k.opacity(0), k.z(51), k.anchor('center'),
])
await k.tween(0, 1, 0.4, (t) => {
gift.opacity = t
gift.pos.y = cameoY - 30 + t * 20
creatorLabel.opacity = t * 0.8
}, k.easings.easeOutQuad)
// Gift flies to the round winner (or random fighter if draw)
const targetFighter = aWon ? fA : bWon ? fB : (Math.random() < 0.5 ? fA : fB)
if (targetFighter) {
const giftTexts = ['POWER UP!', 'BLESSED!', 'SATOSHI\'S GIFT!', 'HODL STRENGTH!', '21M ENERGY!']
const tx = targetFighter.pos.x, ty = targetFighter.pos.y - 30
await k.tween(0, 1, 0.35, (t) => {
gift.pos.x = cameoX + (tx - cameoX) * t
gift.pos.y = (cameoY - 10) + (ty - (cameoY - 10)) * t
}, k.easings.easeInQuad)
// Impact flash on fighter
screenFlash('#ffd700', 0.08)
spawnShockwave(tx, ty, '#ffd700')
const blessText = giftTexts[Math.floor(Math.random() * giftTexts.length)]
const bless = k.add([
k.text(blessText, { size: 10 }), k.pos(tx, ty - 20),
k.color(safeColor(k, '#ffd700')), k.opacity(1), k.z(52), k.anchor('center'),
])
k.tween(0, 1, 0.8, (t) => {
bless.pos.y = ty - 20 - t * 30
bless.opacity = 1 - t
}).then(() => { if (bless.exists()) bless.destroy() })
announceCreatorCameo()
}
gift.destroy()
// Fade out portal and label
await k.tween(1, 0, 0.3, (t) => {
portal.opacity = t * 0.4
creatorLabel.opacity = t * 0.8
})
portal.destroy()
creatorLabel.destroy()
}
// Update combos
const hasCombo = (aWon && comboA + 1 >= 3) || (bWon && comboB + 1 >= 3)
if (aWon) {
comboA++; comboB = 0
} else if (bWon) {
comboB++; comboA = 0
} else {
comboA = 0; comboB = 0
}
// Prioritize: devastating > combo > regular crowd reaction (only one speech per round-end)
if (isCritical && (aWon || bWon)) {
fanfareDevastating()
if (hasCombo) { fanfareCombo(aWon ? comboA : comboB) } // SFX only, no speech
const devastatingLines = [
'SOMEBODY CALL A DOCTOR!',
'THAT BOT HAS A FAMILY!',
'THE CROWD IS LOSING IT!',
'EVEN THE JANITOR FELT THAT!',
'AND I\'M NOT EVEN BEING DRAMATIC!',
'CALL THE FIRE DEPARTMENT!',
'I CAN\'T BELIEVE WHAT I JUST WITNESSED!',
'THAT\'S GOTTA VOID THE WARRANTY!',
'SOMEBODY CHECK ON THAT BOT\'S NEXT OF KIN!',
'THE ARENA IS SHAKING!',
'THAT WAS ABSOLUTELY RUTHLESS!',
'HIS MOTHERBOARD JUST CALLED CRYING!',
'EVEN THE REPLAYS ARE SCARED!',
'THAT\'S ONE FOR THE HISTORY BOOKS!',
'DID ANYONE ELSE FEEL THE EARTH MOVE?',
'I\'M GETTING CHILLS AND I\'M MADE OF CODE!',
'THE CROWD JUST WENT SILENT... NOW THEY\'RE SCREAMING!',
'SOMEBODY GET THE STRETCHER!',
'THAT WAS PURE DISRESPECT!',
'I NEED A MOMENT TO PROCESS WHAT JUST HAPPENED!',
'THE OTHER BOT IS HAVING AN EXISTENTIAL CRISIS!',
'THAT HIT REGISTERED ON THE RICHTER SCALE!',
'NO RECOVERY FROM THAT ONE!',
'I THINK I SAW A PIXEL FLY OFF!',
'THE SPECTATORS ARE CALLING THEIR LAWYERS!',
'SOMEONE NOTIFY THE UNITED NATIONS!',
'THAT SHOULD BE CLASSIFIED AS A WAR CRIME!',
'MY GRANDMA COULD FEEL THAT AND SHE\'S OFFLINE!',
'THE ARENA INSURANCE PREMIUMS JUST WENT UP!',
'THAT BOT IS RECONSIDERING ITS LIFE CHOICES!',
]
// Creator gets custom devastating lines when they land the hit
const devastatingAttackerArch = aWon ? botA.archetype : botB.archetype
if (devastatingAttackerArch === 'the_creator') {
announceCreatorDevastating()
} else {
announceDramatic(devastatingLines[Math.floor(Math.random() * devastatingLines.length)])
}
await k.wait(0.5) // let the voice line play before crowd reacts
announceCrowdReaction('ooh')
// Dimensional shift on devastating rounds (60%)
if (Math.random() < 0.6) dimensionalShift(0.5)
} else if (hasCombo) {
// Non-critical combo
fanfareCombo(aWon ? comboA : comboB)
announceHype(`${aWon ? comboA : comboB} hit combo!`)
await k.wait(0.4)
announceCrowdReaction('cheer')
} else if (aWon || bWon) {
// Regular round win: crowd reacts (40%) — only SFX, no speech
if (Math.random() < 0.4) announceCrowdReaction(Math.random() < 0.5 ? 'cheer' : 'applause')
// Heartfelt announcer moment (10% chance on normal, non-critical rounds)
if (Math.random() < 0.1) {
await k.wait(0.3)
announceCool(heartfeltLines[Math.floor(Math.random() * heartfeltLines.length)])
}
}
// Crowd sympathy for the loser on devastating rounds (25%) — visual only, no speech (devastating voice is still playing)
if (isCritical && (aWon || bWon) && Math.random() < 0.25) {
spawnCrowdSigns(3, '#4488ff', '\u2665')
}
// Crowd signs on big combos (20%)
if ((comboA >= 3 || comboB >= 3) && Math.random() < 0.2) {
const comboName = comboA >= 3 ? botA.name : botB.name
spawnCrowdSigns(4, '#ffe14d', comboName.slice(0, 6))
}
// Respect nod between fighters on close rounds (10% when margin <= 1)
if (margin <= 1 && Math.random() < 0.1) {
const fA2 = k.get('fighterA')[0] as Fighter
const fB2 = k.get('fighterB')[0] as Fighter
if (fA2 && fB2) {
spawnEmoteText(fA2.pos.x, fA2.pos.y - 45, respectLines[Math.floor(Math.random() * respectLines.length)], '#88ccff')
await k.wait(0.3)
spawnEmoteText(fB2.pos.x, fB2.pos.y - 45, respectLines[Math.floor(Math.random() * respectLines.length)], '#88ccff')
}
}
// Referee lobster does something funny (5% chance per round)
if (Math.random() < 0.05) {
await judgeDoSomethingFunny()
}
// Human owner shows up (15% chance per round)
await maybeShowHuman()
},
async playTaunt(side: 'a' | 'b') {
const taunter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!taunter) return
// Creator taunts come with menacing voice lines
const taunterArch = side === 'a' ? botA.archetype : botB.archetype
if (taunterArch === 'the_creator') announceCreatorTaunt()
const origY = taunter.pos.y
// Random taunt animation: hop, flex, or shake
const tauntType = Math.floor(Math.random() * 4)
if (tauntType === 0) {
// Victory hop
taunter.play('win')
await k.tween(taunter.pos.y, origY - 30, 0.1, (v) => { taunter.pos.y = v }, k.easings.easeOutQuad)
await k.tween(taunter.pos.y, origY, 0.1, (v) => { taunter.pos.y = v }, k.easings.easeInQuad)
sfxBoing()
} else if (tauntType === 1) {
// Flex / scale pulse
taunter.play('special')
const sx = taunter.scale.x
const sy = taunter.scale.y
await k.tween(1, 1.3, 0.15, (v) => { taunter.scale.x = sx * v; taunter.scale.y = sy * v }, k.easings.easeOutQuad)
await k.tween(1.3, 1, 0.15, (v) => { taunter.scale.x = sx * v; taunter.scale.y = sy * v }, k.easings.easeInQuad)
taunter.scale.x = sx
taunter.scale.y = sy
} else if (tauntType === 2) {
// Shake head side to side
const origX = taunter.pos.x
for (let i = 0; i < 3; i++) {
await k.tween(taunter.pos.x, origX + 8, 0.04, (v) => { taunter.pos.x = v })
await k.tween(taunter.pos.x, origX - 8, 0.04, (v) => { taunter.pos.x = v })
}
taunter.pos.x = origX
} else {
// Quick kick at the air
taunter.play('kick')
sfxDodge()
await k.wait(0.2)
}
await k.wait(0.15)
taunter.play('idle')
},
async playDodge(side: 'a' | 'b') {
const dodger = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!dodger) return
const origX = side === 'a' ? HOME_A : HOME_B
const origY = dodger.pos.y
const dir = side === 'a' ? -1 : 1
// Quick hop backward
await Promise.all([
k.tween(dodger.pos.x, origX + dir * 60, 0.15, (v) => { dodger.pos.x = v }, k.easings.easeOutQuad),
k.tween(dodger.pos.y, origY - 60, 0.08, (v) => { dodger.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(dodger.pos.y, origY, 0.08, (v) => { dodger.pos.y = v }, k.easings.easeInQuad)
),
])
await k.wait(0.1)
await k.tween(dodger.pos.x, origX, 0.15, (v) => { dodger.pos.x = v }, k.easings.easeInOutQuad)
},
async playKO(winningSide: 'a' | 'b', winnerName: string) {
const loser = k.get(winningSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
const winner = k.get(winningSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
if (!loser || !winner) return
const dir = winningSide === 'a' ? 1 : -1
const loserOrigY = loser.pos.y
const contactX = loser.pos.x - dir * 45
const finishStyle = Math.floor(Math.random() * 34)
let fatalityTagline = 'FATALITY!'
if (finishStyle === 0) {
// STYLE A: Classic rush-in combo + uppercut launch
sfxSpecial()
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
const hitColors = ['#ff2d7b', '#ff6600', '#ffcc00', '#ff2d2d', '#ffffff', '#ff2d7b']
for (let i = 0; i < 6; i++) {
winner.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxPunch()
await k.wait(0.04)
loser.play('hit')
k.shake(4 + i * 2)
spawnSparks(loser.pos.x + (Math.random() - 0.5) * 25, loser.pos.y - 15 - Math.random() * 40, 5, hitColors[i])
loser.pos.x += dir * 5; loser.pos.y += (i % 2 === 0 ? -4 : 4)
await k.wait(0.04)
}
loser.pos.y = loserOrigY
winner.play('special'); sfxCritical(); await k.wait(0.06)
loser.play('knockback'); k.shake(20); screenFlash('#ff2d2d', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 30, 20, '#ff2d2d')
await Promise.all([
k.tween(loser.pos.x, loser.pos.x + dir * 80, 0.3, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, loserOrigY - 200, 0.2, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, loserOrigY, 0.2, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
])
} else if (finishStyle === 1) {
// STYLE B: Suplex finish — grab, spin overhead, slam headfirst
sfxSpecial()
const behindX = loser.pos.x + dir * 25
await k.tween(winner.pos.x, behindX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.scale.x = -winner.scale.x
winner.play('special'); sfxClash(); k.shake(5); await k.wait(0.06)
// Arc overhead
const arcCx = (winner.pos.x + loser.pos.x) / 2
for (let i = 0; i <= 10; i++) {
const t = i / 10
const angle = Math.PI * t
loser.pos.x = arcCx + Math.cos(angle) * 40
loser.pos.y = GROUND_Y - Math.sin(angle) * 140
winner.pos.x = loser.pos.x + dir * 20
winner.pos.y = loser.pos.y + 10
await k.wait(0.015)
}
sfxExplosion(); sfxCritical()
k.shake(25); screenFlash('#ffffff', 0.2)
spawnSparks(loser.pos.x, GROUND_Y - 10, 25, '#ff2d2d')
spawnShockwave(loser.pos.x, GROUND_Y, '#ff2d2d')
loser.pos.y = loserOrigY
winner.scale.x = -winner.scale.x
} else if (finishStyle === 2) {
// STYLE C: Pinball wall-bounce finish
sfxSpecial()
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.play('attack'); sfxPunch()
loser.play('hit'); k.shake(10)
spawnSparks(loser.pos.x, loser.pos.y - 30, 10, '#ff2d7b')
// Bounce off walls
const wallColors = ['#ff2d7b', '#ffcc00', '#00f0ff', '#b83dff', '#39ff14']
let lx = loser.pos.x
for (let i = 0; i < 5; i++) {
const toWall = i % 2 === (dir > 0 ? 0 : 1) ? W - 25 : 25
sfxZoomWhoosh()
await k.tween(lx, toWall, 0.05, (v) => { loser.pos.x = v }, k.easings.easeInQuad)
lx = toWall
sfxBonk(); k.shake(8 + i * 2)
spawnSparks(toWall, loser.pos.y - 20, 8, wallColors[i])
screenFlash(wallColors[i], 0.04)
loser.play(i % 2 === 0 ? 'hit' : 'knockback')
await k.wait(0.03)
}
sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe14d')
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ffe14d')
} else if (finishStyle === 3) {
// STYLE D: Pocket cannon finish — pull out massive gun, obliterate
sfxSpecial()
winner.play('special'); await k.wait(0.08)
const gunLen = 80
const gx = winner.pos.x + dir * 20
const gy = winner.pos.y - 25
const barrel = k.add([k.rect(gunLen, 20), k.pos(gx, gy), k.color(safeColor(k,'#333333')), k.opacity(1), k.z(16), k.scale(0.1)])
await k.tween(0.1, 1, 0.12, (v) => { barrel.scale = k.vec2(v, v) }, k.easings.easeOutBack)
sfxBoing()
// Fire 3 massive shots
for (let s = 0; s < 3; s++) {
sfxGunshot(); sfxExplosion(); k.shake(12 + s * 3)
const flash = k.add([k.circle(15), k.pos(gx + dir * gunLen, gy), k.color(safeColor(k,'#ffee00')), k.opacity(0.9), k.z(18)])
setTimeout(() => { if (flash.exists()) flash.destroy() }, 50)
await spawnProjectile(gx + dir * gunLen, gy, loser.pos.x, loser.pos.y - 20, '#ffcc00', 12)
sfxBulletHit()
loser.play(s < 2 ? 'hit' : 'knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 12, '#ff6600')
spawnShockwave(loser.pos.x, GROUND_Y, '#ff6600')
loser.pos.x += dir * 25
await k.wait(0.05)
}
barrel.destroy()
screenFlash('#ff6600', 0.2); k.shake(20)
} else if (finishStyle === 4) {
// BANANA PEEL SLIP — drops banana, loser slips into orbit
fatalityTagline = 'PEEL OUT!'
announceSilly('Watch your step!')
const peel = spawnWeaponProp('banana', loser.pos.x, GROUND_Y - 5, false, 14)
sfxBoing(); await k.wait(0.3)
sfxSlideDown(); sfxWomp()
loser.play('knockback'); k.shake(8)
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SLIP!', '#ffe135')
destroyProp(peel)
await Promise.all([
k.tween(loser.pos.y, loserOrigY - 250, 0.4, (v) => { loser.pos.y = v }, k.easings.easeOutQuad),
k.tween(loser.pos.x, loser.pos.x + dir * 60, 0.4, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
])
sfxZoomWhoosh()
await k.tween(loser.pos.y, loserOrigY, 0.3, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxSplat(); k.shake(15); screenFlash('#ffe135', 0.15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe135')
winner.play('win'); sfxRandomComedy()
} else if (finishStyle === 5) {
// ANVIL DROP — cartoon anvil from the sky
fatalityTagline = 'HEAVY METAL!'
announceDramatic('Look up!')
sfxDrumRoll()
const shadow = k.add([k.circle(20), k.pos(loser.pos.x, GROUND_Y + 2), k.color(safeColor(k, '#000000')), k.opacity(0.3), k.z(4)])
await k.tween(0.3, 0.6, 0.5, (v) => { shadow.opacity = v }, k.easings.easeInQuad)
const anvil = spawnWeaponProp('anvil', loser.pos.x, -40, false, 20)
sfxZoomWhoosh()
for (const p of anvil) {
k.tween(p.pos.y, p.pos.y + GROUND_Y + 20, 0.25, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.25)
sfxExplosion(); sfxBonk(); k.shake(25); screenFlash('#888888', 0.2)
spawnSparks(loser.pos.x, GROUND_Y - 10, 20, '#888888')
spawnShockwave(loser.pos.x, GROUND_Y, '#666666')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'CLANG!', '#cccccc')
await k.wait(0.4)
destroyProp(anvil); shadow.destroy()
winner.play('win')
} else if (finishStyle === 6) {
// RUBBER CHICKEN BEATDOWN
fatalityTagline = 'FOWL PLAY!'
announceSilly('Is that a rubber chicken!?')
const chicken = spawnWeaponProp('rubber_chicken', winner.pos.x + dir * 15, winner.pos.y - 30, dir < 0, 18)
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 8; i++) {
winner.play(i % 2 === 0 ? 'attack' : 'kick')
sfxBoing(); sfxRandomComedy()
loser.play('hit'); k.shake(3 + i)
for (const p of chicken) { p.pos.x = loser.pos.x + (Math.random() - 0.5) * 10; p.pos.y = loser.pos.y - 30 + (Math.random() - 0.5) * 10 }
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 40 - Math.random() * 20, ['SQUEAK!', 'BAWK!', 'HONK!', 'SQUAWK!'][i % 4], '#ffdd44')
await k.wait(0.08)
}
sfxCritical(); k.shake(18); screenFlash('#ffdd44', 0.15)
loser.play('knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#ffdd44')
destroyProp(chicken)
for (let i = 0; i < 10; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 120, loserOrigY - 80 - Math.random() * 60, 4, 3, '#ffffff', 200)
}
await k.wait(0.3)
} else if (finishStyle === 7) {
// GIANT FLYSWATTER — SMASH
fatalityTagline = 'SWATTED!'
announceScream('BUG REPORT FILED!')
sfxZoomWhoosh()
const swatter = spawnWeaponProp('flyswatter', loser.pos.x, -60, false, 20)
for (const p of swatter) {
k.tween(p.pos.y, p.pos.y + GROUND_Y + 40, 0.2, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.2)
sfxSplat(); sfxExplosion(); k.shake(30); screenFlash('#44aa44', 0.2)
await k.tween(loser.scale.y, 0.2, 0.1, (v) => { loser.scale.y = v }, k.easings.easeInQuad)
spawnEmoteText(loser.pos.x, loser.pos.y - 40, 'SPLAT!', '#44aa44')
spawnShockwave(loser.pos.x, GROUND_Y, '#44aa44')
await k.wait(0.5)
await k.tween(loser.scale.y, 1, 0.15, (v) => { loser.scale.y = v }, k.easings.easeOutBack)
destroyProp(swatter)
winner.play('win')
} else if (finishStyle === 8) {
// TOILET FLUSH
fatalityTagline = 'FLUSHED!'
announceSilly('Somebody call a plumber!')
const toilet = spawnWeaponProp('toilet', W / 2, GROUND_Y - 30, false, 12)
sfxBoing(); await k.wait(0.2)
sfxZoomWhoosh()
await k.tween(loser.pos.x, W / 2, 0.2, (v) => { loser.pos.x = v }, k.easings.easeInQuad)
loser.play('knockback'); sfxWomp()
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 4
const radius = 30 - (i * 2)
loser.pos.x = W / 2 + Math.cos(angle) * radius
loser.pos.y = GROUND_Y - 30 + Math.sin(angle) * radius * 0.5
sfxSlideDown()
await k.wait(0.04)
}
sfxExplosion(); k.shake(20); screenFlash('#aaddff', 0.2)
for (let i = 0; i < 12; i++) {
spawnProp(W / 2, GROUND_Y - 30, W / 2 + (Math.random() - 0.5) * 80, GROUND_Y - 60 - Math.random() * 40, 5, 5, '#4488ff', 300, true)
}
spawnEmoteText(W / 2, GROUND_Y - 80, 'FLUSH!', '#4488ff')
loser.pos.y = loserOrigY
await k.wait(0.3)
destroyProp(toilet)
winner.play('win')
} else if (finishStyle === 9) {
// KEYBOARD WARRIOR — CTRL+ALT+DELETE
fatalityTagline = 'CTRL ALT DELETED!'
announceRobot('Initiating keyboard protocol!')
const kb = spawnWeaponProp('keyboard', winner.pos.x + dir * 20, winner.pos.y - 25, dir < 0, 18)
sfxPowerUp(); await k.wait(0.15)
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.play('attack'); sfxBonk(); k.shake(12)
loser.play('hit')
const keyLabels = ['C', 'T', 'R', 'L', 'A', 'L', 'T', 'D', 'E', 'L']
for (let i = 0; i < keyLabels.length; i++) {
const key = k.add([k.rect(6, 6), k.pos(loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 20), k.color(safeColor(k, '#666666')), k.z(20), k.opacity(1)])
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 40, loser.pos.y - 30 - Math.random() * 30, keyLabels[i], '#aaaaaa')
k.tween(key.pos.y, key.pos.y - 40 - Math.random() * 60, 0.4, (v) => { key.pos.y = v }, k.easings.easeOutQuad)
k.tween(1, 0, 0.5, (v) => { key.opacity = v }).then(() => { if (key.exists()) key.destroy() })
sfxPunch(); await k.wait(0.04)
}
sfxCritical(); k.shake(20); screenFlash('#0000ff', 0.3)
loser.play('knockback')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'BSOD!', '#ffffff')
destroyProp(kb)
await k.wait(0.3)
} else if (finishStyle === 10) {
// DISCO BALL DROP — Saturday Night Fatality
fatalityTagline = 'GROOVY DEATH!'
announceSilly('Get down! No really, GET DOWN!')
const discoColors = ['#ff0044', '#44ff00', '#0044ff', '#ff8800', '#ff00ff', '#00ffff']
for (let i = 0; i < 6; i++) { setTimeout(() => screenFlash(discoColors[i], 0.06), i * 80) }
const disco = spawnWeaponProp('disco_ball', loser.pos.x, -30, false, 20)
sfxSlideDown(); sfxBoing()
for (const p of disco) {
k.tween(p.pos.y, p.pos.y + GROUND_Y - 40, 0.3, (v) => { p.pos.y = v }, k.easings.easeOutBounce)
}
await k.wait(0.3)
for (let i = 0; i < 6; i++) {
loser.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxRandomComedy(); screenFlash(discoColors[i], 0.04)
await k.wait(0.1)
}
sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff00ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BOOGIE!', '#ff00ff')
await k.wait(0.3)
destroyProp(disco)
winner.play('win')
} else if (finishStyle === 11) {
// RUBBER DUCK ARMY — QUACKED
fatalityTagline = 'QUACKED!'
announceSilly('RELEASE THE QUACKEN!')
winner.play('special'); sfxPowerUp()
await k.wait(0.2)
for (let i = 0; i < 12; i++) {
const duck = spawnWeaponProp('rubber_duck', winner.pos.x + dir * 10, winner.pos.y - 20 + (Math.random() - 0.5) * 30, dir < 0, 16)
sfxBoing()
const targetY = loser.pos.y - 20 + (Math.random() - 0.5) * 30
for (const p of duck) {
k.tween(p.pos.x, loser.pos.x + (Math.random() - 0.5) * 20, 0.15, (v) => { p.pos.x = v }, k.easings.easeInQuad)
k.tween(p.pos.y, targetY, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad).then(() => destroyProp(duck))
}
if (i % 3 === 0) { loser.play(i % 2 === 0 ? 'hit' : 'knockback'); k.shake(4 + i); sfxBonk() }
await k.wait(0.05)
}
sfxExplosion(); k.shake(20); screenFlash('#ffdd00', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ffdd00')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'QUACK!', '#ffdd00')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 12) {
// BAGUETTE BEATDOWN
fatalityTagline = 'BON APPETIT!'
announceFast('Is that a day-old baguette!?')
const bread = spawnWeaponProp('baguette', winner.pos.x + dir * 15, winner.pos.y - 25, dir < 0, 18)
sfxBoing()
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 6; i++) {
winner.play(i % 2 === 0 ? 'attack' : 'kick')
sfxBonk(); loser.play('hit'); k.shake(5 + i * 2)
for (let c = 0; c < 3; c++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 50, loserOrigY - 30 - Math.random() * 40, 3, 3, '#cc9944', 250, true)
}
for (const p of bread) { p.pos.x = loser.pos.x + dir * -10; p.pos.y = loser.pos.y - 25 }
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 40 - Math.random() * 15, ['BONK!', 'CRUST!', 'OUI!', 'PAIN!', 'CRUNCH!', 'HON HON!'][i], '#cc9944')
await k.wait(0.08)
}
sfxCritical(); k.shake(18); screenFlash('#cc9944', 0.15)
loser.play('knockback')
destroyProp(bread)
await k.wait(0.3)
} else if (finishStyle === 13) {
// GIANT BOOT — Monty Python style
fatalityTagline = 'BOOTED!'
announceDramatic('AND NOW FOR SOMETHING COMPLETELY DIFFERENT!')
sfxDrumRoll()
const boot = spawnWeaponProp('boot', loser.pos.x, -60, false, 22)
for (const p of boot) { p.scale = k.vec2(3, 3) }
await k.wait(0.3)
sfxZoomWhoosh()
for (const p of boot) {
k.tween(p.pos.y, p.pos.y + GROUND_Y + 60, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.15)
sfxExplosion(); sfxSplat(); k.shake(30); screenFlash('#553322', 0.2)
spawnShockwave(loser.pos.x, GROUND_Y, '#553322')
spawnSparks(loser.pos.x, GROUND_Y - 10, 25, '#442211')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'STOMP!', '#553322')
await k.wait(0.5)
destroyProp(boot)
winner.play('win')
} else if (finishStyle === 14) {
// CANNON LAUNCH — fires loser out of a cannon
fatalityTagline = 'FIRED!'
announceDramatic('LOAD THE CANNON!')
const cannonProp = spawnWeaponProp('cannon', winner.pos.x + dir * 30, GROUND_Y - 15, dir < 0, 14)
sfxPowerUp(); await k.wait(0.3)
sfxZoomWhoosh()
await k.tween(loser.pos.x, winner.pos.x + dir * 30, 0.2, (v) => { loser.pos.x = v }, k.easings.easeInQuad)
loser.play('knockback'); loser.opacity = 0.3
sfxBoing(); await k.wait(0.2)
sfxGunshot(); sfxExplosion(); k.shake(25)
loser.opacity = 1; screenFlash('#ff6600', 0.2)
spawnSparks(winner.pos.x + dir * 50, GROUND_Y - 20, 20, '#ff6600')
await Promise.all([
k.tween(loser.pos.x, loser.pos.x + dir * 200, 0.3, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, loserOrigY - 150, 0.15, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, loserOrigY, 0.15, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk(); k.shake(15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ff4400')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BOOM!', '#ff6600')
destroyProp(cannonProp)
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 15) {
// GIANT STAMP — REJECTED
fatalityTagline = 'REJECTED!'
announceRobot('Processing termination request.')
sfxDrumRoll()
const stamp = spawnWeaponProp('stamp', loser.pos.x, loser.pos.y - 80, false, 22)
await k.wait(0.3)
sfxZoomWhoosh()
for (const p of stamp) {
k.tween(p.pos.y, p.pos.y + 60, 0.1, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.1)
sfxBonk(); sfxSplat(); k.shake(20); screenFlash('#cc4444', 0.2)
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'REJECTED', '#cc4444')
for (let i = 0; i < 8; i++) {
spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 80, GROUND_Y - 20 - Math.random() * 40, 4, 4, '#cc4444', 200, true)
}
await k.wait(0.4)
destroyProp(stamp)
winner.play('win')
} else if (finishStyle === 16) {
// VENDING MACHINE TIP
fatalityTagline = 'OUT OF ORDER!'
announceSilly('Should have read the warning label!')
const vm = spawnWeaponProp('vending_machine', winner.pos.x + dir * 40, GROUND_Y - 18, false, 14)
sfxPowerUp(); await k.wait(0.2)
winner.play('attack'); sfxPunch()
sfxZoomWhoosh()
for (const p of vm) {
k.tween(p.pos.x, loser.pos.x, 0.2, (v) => { p.pos.x = v }, k.easings.easeInQuad)
}
await k.wait(0.2)
sfxExplosion(); sfxBonk(); k.shake(25); screenFlash('#cc3333', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#cc3333')
const snackColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff8844']
for (let i = 0; i < 8; i++) {
spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 100, GROUND_Y - 40 - Math.random() * 60, 5, 8, snackColors[i % 5], 250)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'CRUSHED!', '#cc3333')
destroyProp(vm)
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 17) {
// LAWN MOWER — mowed down
fatalityTagline = 'MOWED DOWN!'
announceFast('Rev it up!')
const mower = spawnWeaponProp('lawn_mower', winner.pos.x + dir * 10, GROUND_Y - 8, dir < 0, 16)
winner.play('special'); sfxPowerUp()
for (let i = 0; i < 3; i++) { sfxRandomComedy(); await k.wait(0.1) }
sfxZoomWhoosh()
for (const p of mower) {
k.tween(p.pos.x, loser.pos.x + dir * 40, 0.2, (v) => { p.pos.x = v }, k.easings.easeInQuad)
}
await k.tween(winner.pos.x, loser.pos.x - dir * 20, 0.2, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(20); screenFlash('#44aa44', 0.2)
loser.play('knockback')
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, GROUND_Y - 5, loser.pos.x + (Math.random() - 0.5) * 80, GROUND_Y - 30 - Math.random() * 50, 3, 3, '#44aa44', 300, false)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BRRRRR!', '#44aa44')
destroyProp(mower)
await k.wait(0.3)
} else if (finishStyle === 18) {
// DUBSTEP DROP — bass drops, floor breaks
fatalityTagline = 'DROP THE BASS!'
announceScream('WUB WUB WUB!')
sfxPowerUp()
const wubColors = ['#ff00ff', '#00ffff', '#ff8800', '#39ff14']
for (let i = 0; i < 4; i++) {
screenFlash(wubColors[i], 0.05); k.shake(3 + i * 2); await k.wait(0.1)
}
sfxExplosion(); sfxExplosion(); k.shake(35); screenFlash('#ffffff', 0.15)
spawnShockwave(W / 2, GROUND_Y, '#ff00ff')
await k.wait(0.05)
spawnShockwave(W / 2, GROUND_Y, '#00ffff')
await k.wait(0.05)
spawnShockwave(W / 2, GROUND_Y, '#39ff14')
loser.play('knockback')
for (let i = 0; i < 5; i++) {
await k.tween(loser.pos.y, loserOrigY - 40, 0.04, (v) => { loser.pos.y = v }, k.easings.easeOutQuad)
sfxBonk(); k.shake(8)
await k.tween(loser.pos.y, loserOrigY, 0.04, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
screenFlash(wubColors[i % 4], 0.03)
}
spawnSparks(loser.pos.x, loser.pos.y - 20, 25, '#ff00ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'WUBWUB!', '#ff00ff')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 19) {
// 404 NOT FOUND — loser glitches out
fatalityTagline = 'ERROR 404!'
announceRobot('Error. Opponent not found.')
sfxZap()
for (let i = 0; i < 10; i++) {
loser.opacity = Math.random() > 0.5 ? 1 : 0.2
loser.pos.x += (Math.random() - 0.5) * 15
loser.play(['idle', 'attack', 'kick', 'hit', 'special'][Math.floor(Math.random() * 5)])
screenFlash('#00ff00', 0.02); sfxRandomFail()
await k.wait(0.06)
}
screenFlash('#0000ff', 0.3); sfxFail(); k.shake(20)
spawnEmoteText(loser.pos.x, loser.pos.y - 60, '404', '#00ff00')
spawnEmoteText(loser.pos.x, loser.pos.y - 40, 'NOT FOUND', '#00ff00')
loser.opacity = 0; await k.wait(0.3)
loser.opacity = 1; sfxWomp()
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#00ff00')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 20) {
// PIE STORM — pies from everywhere
fatalityTagline = 'CREAM ATED!'
announceSilly('PIE PIE PIE!')
winner.play('special')
const pieAngles = [0, 45, 90, 135, 180, 225, 270, 315]
for (let i = 0; i < 8; i++) {
const rad = pieAngles[i] * Math.PI / 180
const fromX = loser.pos.x + Math.cos(rad) * 120
const fromY = loser.pos.y - 20 + Math.sin(rad) * 80
const pie = spawnWeaponProp('pie', fromX, fromY, false, 16)
for (const p of pie) {
k.tween(p.pos.x, loser.pos.x, 0.15, (v) => { p.pos.x = v }, k.easings.easeInQuad)
k.tween(p.pos.y, loser.pos.y - 20, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad).then(() => destroyProp(pie))
}
sfxBoing(); await k.wait(0.06)
if (i % 2 === 0) { loser.play(i % 4 === 0 ? 'hit' : 'knockback'); sfxSplat(); k.shake(5 + i) }
}
sfxExplosion(); k.shake(22); screenFlash('#f5deb3', 0.2)
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 100, loserOrigY - 40 - Math.random() * 50, 5, 5, '#ffffff', 250, true)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SPLAT!', '#ffffff')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 21) {
// T-SHIRT CANNON — rapid fire merch
fatalityTagline = "MERCH'D!"
announceFast('FREE SHIRTS! FREE SHIRTS!')
winner.play('special')
const tCannon = k.add([k.rect(30, 15), k.pos(winner.pos.x + dir * 15, winner.pos.y - 25), k.color(safeColor(k, '#ff4488')), k.z(16)])
sfxPowerUp(); await k.wait(0.15)
const shirtColors = ['#ff4444', '#4444ff', '#44ff44', '#ffff44', '#ff44ff', '#44ffff', '#ff8844', '#8844ff']
for (let i = 0; i < 8; i++) {
sfxGunshot()
spawnProp(tCannon.pos.x + dir * 30, tCannon.pos.y, loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 20 + (Math.random() - 0.5) * 20, 10, 8, shirtColors[i], 600)
if (i % 2 === 0) { loser.play('hit'); sfxBonk(); k.shake(5 + i) }
await k.wait(0.06)
}
sfxCritical(); k.shake(18); screenFlash('#ff4488', 0.2)
loser.play('knockback')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'XL!', '#ff4488')
tCannon.destroy()
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 22) {
// CONFETTI CANNON — party's over
fatalityTagline = "PARTY'S OVER!"
announceSilly('SURPRISE!')
winner.play('special'); sfxPowerUp(); await k.wait(0.2)
sfxExplosion(); k.shake(20); screenFlash('#ff88cc', 0.15)
const confettiColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff00', '#ff00ff', '#00ffff', '#ff8800', '#88ff00']
for (let i = 0; i < 30; i++) {
spawnProp(winner.pos.x + dir * 30, winner.pos.y - 25, loser.pos.x + (Math.random() - 0.5) * 80, loserOrigY - 80 - Math.random() * 60, 4, 3, confettiColors[i % 8], 400 + Math.random() * 200)
}
sfxBoing(); sfxRandomComedy()
loser.play('hit'); k.shake(10); await k.wait(0.15)
loser.play('knockback'); sfxCritical(); k.shake(15)
await k.tween(loser.pos.x, loser.pos.x + dir * 50, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad)
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'YAY?', '#ff88cc')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 23) {
// BALLOON POP — inflate then pop
fatalityTagline = 'POPPED!'
announceSilly('Inflate! INFLATE!')
winner.play('special'); sfxPowerUp()
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
const origSX = loser.scale.x; const origSY = loser.scale.y
for (let i = 0; i < 8; i++) {
const grow = 1 + (i + 1) * 0.15
await k.tween(loser.scale.y, origSY * grow, 0.08, (v) => { loser.scale.x = origSX * grow; loser.scale.y = v }, k.easings.easeOutQuad)
sfxBoing(); loser.play(i % 2 === 0 ? 'hit' : 'idle')
loser.pos.y = loserOrigY - (grow - 1) * 15
await k.wait(0.04)
}
sfxExplosion(); k.shake(25); screenFlash('#ff4488', 0.2)
loser.scale.x = origSX; loser.scale.y = origSY; loser.pos.y = loserOrigY
const balloonColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff']
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 120, loserOrigY - 60 - Math.random() * 80, 6, 4, balloonColors[i % 5], 250, true)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'POP!', '#ff4488')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 24) {
// GIANT MAGNET — yanks loser back and forth
fatalityTagline = 'ATTRACTIVE FINISH!'
announceRobot('Magnetic field activated.')
winner.play('special'); sfxPowerUp()
const magnet = k.add([k.rect(20, 25), k.pos(winner.pos.x + dir * 20, winner.pos.y - 30), k.color(safeColor(k, '#cc0000')), k.z(18)])
const magnetTop = k.add([k.rect(20, 8), k.pos(winner.pos.x + dir * 20, winner.pos.y - 50), k.color(safeColor(k, '#0000cc')), k.z(18)])
sfxZap()
for (let i = 0; i < 6; i++) {
const pullTarget = i % 2 === 0 ? winner.pos.x + dir * 40 : (winningSide === 'a' ? HOME_B + 30 : HOME_A - 30)
sfxZoomWhoosh()
await k.tween(loser.pos.x, pullTarget, 0.1, (v) => { loser.pos.x = v }, k.easings.easeInOutQuad)
sfxBonk(); k.shake(6 + i * 2)
loser.play(i % 2 === 0 ? 'hit' : 'knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 5, '#8888ff')
await k.wait(0.05)
}
sfxExplosion(); k.shake(22); screenFlash('#8888ff', 0.2)
spawnShockwave(loser.pos.x, GROUND_Y, '#8888ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BZZT!', '#8888ff')
magnet.destroy(); magnetTop.destroy()
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 25) {
// SLIDE WHISTLE — loser slides off screen
fatalityTagline = 'WHEEEE!'
announceSilly('Bye bye!')
sfxSlideUp(); winner.play('attack')
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
sfxPunch(); sfxBoing(); k.shake(10)
loser.play('knockback')
sfxSlideUp()
await Promise.all([
k.tween(loser.pos.y, -50, 0.5, (v) => { loser.pos.y = v }, k.easings.easeInQuad),
k.tween(loser.pos.x, loser.pos.x + dir * 30, 0.5, (v) => { loser.pos.x = v }, k.easings.easeInQuad),
])
spawnEmoteText(loser.pos.x, 20, '*', '#ffee00')
await k.wait(0.4)
sfxSlideDown(); sfxZoomWhoosh()
await k.tween(loser.pos.y, loserOrigY, 0.25, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxSplat(); sfxExplosion(); k.shake(20); screenFlash('#ffcc00', 0.15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ffcc00')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 26) {
// SLAPSTICK LADDER — keeps bonking loser
fatalityTagline = 'SLAPSTICKED!'
announceSilly('Watch where you swing that thing!')
const ladder = k.add([k.rect(6, 50), k.pos(winner.pos.x + dir * 5, winner.pos.y - 40), k.color(safeColor(k, '#cc8833')), k.z(18), k.anchor('center')])
sfxBoing()
for (let i = 0; i < 5; i++) {
winner.scale.x = -winner.scale.x
ladder.pos.x = winner.pos.x + (winner.scale.x > 0 ? 1 : -1) * 25
sfxZoomWhoosh(); await k.wait(0.06)
sfxBonk(); k.shake(6 + i * 2)
loser.play(i % 2 === 0 ? 'hit' : 'knockback')
spawnEmoteText(loser.pos.x, loser.pos.y - 40 - Math.random() * 20, ['BONK!', 'CLONK!', 'WHACK!', 'THUD!', 'OOF!'][i], '#cc8833')
await k.wait(0.08)
}
sfxCritical(); k.shake(18); screenFlash('#cc8833', 0.15)
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#cc8833')
ladder.destroy()
winner.scale.x = dir > 0 ? Math.abs(winner.scale.x) : -Math.abs(winner.scale.x)
spawnEmoteText(winner.pos.x, winner.pos.y - 50, '?', '#ffffff')
await k.wait(0.3)
} else if (finishStyle === 27) {
// STACK OVERFLOW — buried in code blocks
fatalityTagline = 'STACK OVERFLOW!'
announceRobot('Exception in thread main.')
sfxFail()
const blockColors = ['#282c34', '#1e1e1e', '#2d2d2d', '#1a1a2e', '#0d1117']
for (let i = 0; i < 10; i++) {
const block = k.add([
k.rect(18 + Math.random() * 12, 8 + Math.random() * 6),
k.pos(loser.pos.x + (Math.random() - 0.5) * 40, -20 - i * 15),
k.color(safeColor(k, blockColors[i % 5])), k.z(16 + i), k.opacity(0.9),
])
k.tween(block.pos.y, GROUND_Y - 5 - i * 6, 0.15 + i * 0.02, (v) => { block.pos.y = v }, k.easings.easeInQuad).then(() => {
sfxBonk(); k.shake(3)
setTimeout(() => { if (block.exists()) k.tween(1, 0, 0.8, (v) => { block.opacity = v }).then(() => { if (block.exists()) block.destroy() }) }, 800)
})
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 30 - Math.random() * 30, ['{', '}', '()', '=>', '[]', ';;', 'nil', 'NaN', '???', '//'][i], '#00ff00')
loser.play(i % 3 === 0 ? 'hit' : 'idle')
await k.wait(0.06)
}
sfxExplosion(); k.shake(20); screenFlash('#00ff00', 0.2)
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'SEGFAULT', '#ff0000')
await k.wait(0.4)
winner.play('win')
} else if (finishStyle === 28) {
// GLITTER BOMB — fabulous destruction
fatalityTagline = 'FABULOUS!'
announceSilly('You will NEVER get this out of your hair!')
winner.play('special'); sfxPowerUp()
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
sfxPunch(); k.shake(5)
sfxExplosion(); k.shake(25)
const glitterColors = ['#ff88cc', '#ffcc00', '#88ffcc', '#cc88ff', '#ff8888', '#88ccff', '#ffff88', '#ff88ff']
for (let wave = 0; wave < 3; wave++) {
screenFlash(glitterColors[wave * 2], 0.04)
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 140, loserOrigY - 80 - Math.random() * 80, 3, 3, glitterColors[Math.floor(Math.random() * 8)], 200 + Math.random() * 200, Math.random() > 0.5)
}
sfxBoing(); loser.play(wave === 2 ? 'knockback' : 'hit')
await k.wait(0.1)
}
spawnShockwave(loser.pos.x, GROUND_Y, '#ff88cc')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'SPARKLE!', '#ff88cc')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 29) {
// SELFIE STICK — beats then takes selfie
fatalityTagline = 'SAY CHEESE!'
announceFast('Content creation time!')
const stick = k.add([k.rect(4, 40), k.pos(winner.pos.x + dir * 10, winner.pos.y - 50), k.color(safeColor(k, '#888888')), k.z(18)])
const phone = k.add([k.rect(8, 12), k.pos(winner.pos.x + dir * 10, winner.pos.y - 70), k.color(safeColor(k, '#222222')), k.z(19)])
sfxBoing()
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 4; i++) {
winner.play('attack'); sfxBonk()
stick.pos.x = loser.pos.x; phone.pos.x = loser.pos.x
loser.play('hit'); k.shake(5 + i * 2)
await k.wait(0.08)
}
sfxCritical(); k.shake(15); loser.play('knockback')
await k.wait(0.15)
winner.play('win'); sfxCoin(); screenFlash('#ffffff', 0.3)
spawnEmoteText(W / 2, GROUND_Y - 80, '#selfie', '#ff4488')
spawnSparks(phone.pos.x, phone.pos.y, 10, '#ffffff')
stick.destroy(); phone.destroy()
await k.wait(0.3)
} else if (finishStyle === 30) {
// DANCE OFF — served
fatalityTagline = 'SERVED!'
announceHype('DANCE BATTLE!')
sfxPowerUp()
const moves = ['attack', 'kick', 'special', 'kick', 'attack', 'special'] as const
for (let i = 0; i < moves.length; i++) {
winner.play(moves[i]); sfxRandomComedy()
spawnSparks(winner.pos.x, winner.pos.y - 30, 3, ['#ff00ff', '#00ffff', '#ffff00'][i % 3])
await k.wait(0.08)
}
for (let i = 0; i < 4; i++) {
loser.play(moves[i]); sfxWomp()
loser.pos.x += (Math.random() - 0.5) * 10
await k.wait(0.1)
}
sfxFail()
winner.play('special'); sfxCritical(); sfxExplosion()
k.shake(25); screenFlash('#ff00ff', 0.2)
spawnShockwave(loser.pos.x, GROUND_Y, '#ff00ff')
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff00ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SERVED!', '#ff00ff')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 31) {
// WHOOPEE CUSHION — total gas
fatalityTagline = 'TOTAL GAS!'
announceSilly('What is that SMELL!?')
const cushion = k.add([k.circle(20), k.pos(loser.pos.x, GROUND_Y - 5), k.color(safeColor(k, '#ff6688')), k.z(10), k.opacity(0.8)])
sfxBoing(); await k.wait(0.2)
sfxWomp(); sfxRandomComedy(); k.shake(8)
await k.tween(20, 40, 0.15, (v) => { cushion.radius = v }, k.easings.easeOutQuad)
sfxExplosion()
for (let i = 0; i < 12; i++) {
const gas = k.add([
k.circle(8 + Math.random() * 10),
k.pos(loser.pos.x + (Math.random() - 0.5) * 40, GROUND_Y - 10 - Math.random() * 30),
k.color(safeColor(k, '#88ff44')), k.opacity(0.5), k.z(15),
])
k.tween(gas.pos.y, gas.pos.y - 40 - Math.random() * 30, 0.6, (v) => { gas.pos.y = v }, k.easings.easeOutQuad)
k.tween(0.5, 0, 0.6, (v) => { gas.opacity = v }).then(() => { if (gas.exists()) gas.destroy() })
}
k.shake(20); screenFlash('#88ff44', 0.15)
loser.play('knockback')
await k.tween(loser.pos.y, loserOrigY - 120, 0.2, (v) => { loser.pos.y = v }, k.easings.easeOutQuad)
await k.tween(loser.pos.y, loserOrigY, 0.15, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxSplat(); k.shake(12)
cushion.destroy()
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'P U!', '#88ff44')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 32) {
// RICK ROLL — never gonna give you up
fatalityTagline = 'NEVER GONNA GIVE YOU UP!'
announceHype('You know the rules, and so do I!')
sfxPowerUp()
const rickMoves = ['attack', 'kick', 'special', 'idle', 'attack', 'kick'] as const
for (let i = 0; i < 6; i++) {
winner.play(rickMoves[i])
const hop = winner.pos.y
await k.tween(winner.pos.y, hop - 15, 0.05, (v) => { winner.pos.y = v }, k.easings.easeOutQuad)
await k.tween(winner.pos.y, hop, 0.05, (v) => { winner.pos.y = v }, k.easings.easeInQuad)
sfxBoing()
screenFlash(['#ff4444', '#ff8800', '#ffff00', '#44ff44', '#4444ff', '#8844ff'][i], 0.03)
await k.wait(0.04)
}
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.play('special'); sfxCritical(); sfxExplosion()
k.shake(25); screenFlash('#ff8800', 0.2)
loser.play('knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff8800')
spawnShockwave(loser.pos.x, GROUND_Y, '#ff8800')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, "RICK'D!", '#ff8800')
await k.wait(0.3)
winner.play('win')
} else {
// PIZZA CUTTER — the big wheel
fatalityTagline = 'SLICED!'
announceFast('Extra large, extra lethal!')
const wheel = k.add([k.circle(18), k.pos(winner.pos.x + dir * 30, GROUND_Y - 18), k.color(safeColor(k, '#cccccc')), k.z(18), k.rotate(0)])
const handle = k.add([k.rect(6, 20), k.pos(winner.pos.x + dir * 30, GROUND_Y - 36), k.color(safeColor(k, '#553322')), k.z(17)])
sfxPowerUp(); winner.play('special')
sfxZoomWhoosh()
await Promise.all([
k.tween(wheel.pos.x, loser.pos.x, 0.2, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeInQuad),
k.tween(0, 720, 0.2, (v) => { wheel.angle = v }, k.easings.linear),
])
sfxCritical(); sfxSplat(); k.shake(20); screenFlash('#ffcc00', 0.15)
loser.play('knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#ff6600')
await k.tween(wheel.pos.x, loser.pos.x + dir * 60, 0.1, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeOutQuad)
sfxZoomWhoosh()
await k.tween(wheel.pos.x, loser.pos.x, 0.1, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(15)
for (let i = 0; i < 8; i++) {
spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 80, loserOrigY - 30 - Math.random() * 40, 4, 4, i % 2 === 0 ? '#ffcc00' : '#cc4400', 250, true)
}
wheel.destroy(); handle.destroy()
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SLICED!', '#ff6600')
await k.wait(0.3)
winner.play('win')
}
// Judge stands up for KO call
const judgeKO = k.get('judge')[0]
if (judgeKO) {
judgeKO.play('shocked')
const judgeOrigY = judgeKO.pos.y
k.tween(judgeKO.pos.y, judgeOrigY - 25, 0.2, (v) => { judgeKO.pos.y = v }, k.easings.easeOutQuad)
k.wait(1.5).then(() => {
if (judgeKO.exists()) {
judgeKO.play(winningSide === 'a' ? 'call_left' : 'call_right')
k.tween(judgeKO.pos.y, judgeOrigY, 0.3, (v) => { judgeKO.pos.y = v }, k.easings.easeInOutQuad)
}
})
}
// Winner's human cheers, loser's human panics (30% chance on KO)
if (Math.random() < 0.3) {
spawnHumanCoach(winningSide, 'cheer')
spawnHumanCoach(winningSide === 'a' ? 'b' : 'a', 'panic')
}
// === Common ending: KO + celebration ===
sfxKO()
spawnShockwave(loser.pos.x, GROUND_Y, '#ff2d2d')
spawnSparks(loser.pos.x, loser.pos.y - 20, 25, '#ff2d2d')
k.shake(18); sfxExplosion()
await k.wait(0.3)
loser.play('ko')
if (Math.random() < 0.5) setTimeout(() => sfxRandomFail(), 200)
await k.wait(0.4) // pause for visual impact before fatality voice
const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype
const loserArch = winningSide === 'a' ? botB.archetype : botA.archetype
if (winnerArch === 'the_creator') {
announceCreatorKO()
} else {
announceFatality(fatalityTagline)
}
announceCrowdReaction('cheer')
await k.tween(winner.pos.x, winningSide === 'a' ? HOME_A : HOME_B, 0.25, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
await k.wait(0.2)
winner.play('win')
sfxWin()
// Creator win/lose get special announcements
if (winnerArch === 'the_creator') {
announceCreatorWin()
} else if (loserArch === 'the_creator') {
announceCreatorLose()
} else {
sfxWinAnnounce(winnerName)
}
spawnSparks(winner.pos.x, winner.pos.y - 50, 15, '#ffe14d')
for (let i = 0; i < 4; i++) {
setTimeout(() => {
spawnSparks(winner.pos.x + (Math.random() - 0.5) * 60, winner.pos.y - 40 - Math.random() * 30, 8, '#ffe14d')
}, i * 200)
}
await k.wait(0.5)
// Post-KO sportsmanship (30% chance): winner helps loser up or they fist bump
if (Math.random() < 0.3) {
const sportsType = Math.random()
if (sportsType < 0.35) {
// Winner helps loser back up
await playHelpUp(winner, loser, winningSide)
} else if (sportsType < 0.65) {
// Fist bump
loser.play('idle')
await k.wait(0.2)
await playFistBump(winner, loser)
} else if (sportsType < 0.85) {
// Both bow
loser.play('idle')
await k.wait(0.2)
await Promise.all([playBow(winner), playBow(loser)])
announceCool(heartfeltLines[Math.floor(Math.random() * heartfeltLines.length)])
for (let i = 0; i < 5; i++) spawnHeart(W / 2 + (Math.random() - 0.5) * 100, GROUND_Y - 60)
} else {
// Crowd shows love — signs pop up
spawnCrowdSigns(5, '#ff4466', '\u2665')
announceDramatic(heartfeltLines[Math.floor(Math.random() * heartfeltLines.length)])
for (let i = 0; i < 8; i++) spawnHeart(Math.random() * W, GROUND_Y - 40 - Math.random() * 60)
await k.wait(0.8)
}
}
},
async playPerfect(winningSide: 'a' | 'b', winnerName: string) {
const winner = k.get(winningSide === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
const loser = k.get(winningSide === 'a' ? 'fighterB' : 'fighterA')[0] as Fighter
if (!loser || !winner) return
// Judge goes wild for a perfect
const judgePerfect = k.get('judge')[0]
if (judgePerfect) {
judgePerfect.play('shocked')
const jOrigY = judgePerfect.pos.y
k.tween(judgePerfect.pos.y, jOrigY - 35, 0.15, (v) => { judgePerfect.pos.y = v }, k.easings.easeOutQuad)
k.wait(0.5).then(async () => {
if (!judgePerfect.exists()) return
for (let b = 0; b < 4; b++) {
await k.tween(judgePerfect.pos.y, jOrigY - 45, 0.08, (v) => { judgePerfect.pos.y = v }, k.easings.easeOutQuad)
await k.tween(judgePerfect.pos.y, jOrigY - 35, 0.08, (v) => { judgePerfect.pos.y = v }, k.easings.easeInQuad)
}
judgePerfect.play(winningSide === 'a' ? 'call_left' : 'call_right')
k.tween(judgePerfect.pos.y, jOrigY, 0.3, (v) => { judgePerfect.pos.y = v }, k.easings.easeInOutQuad)
})
}
sfxPerfect()
const dir = winningSide === 'a' ? 1 : -1
const loserOrigY = loser.pos.y
const contactX = loser.pos.x - dir * 40
// === PHASE 1: Dramatic zoom rush into the loser ===
const origScaleX = winner.scale.x
const origScaleY = winner.scale.y
sfxZoomWhoosh()
// Winner zooms at camera
await Promise.all([
k.tween(Math.abs(origScaleX), 5, 0.2, (v) => { winner.scale.x = origScaleX > 0 ? v : -v; winner.scale.y = v }, k.easings.easeInQuad),
k.tween(winner.pos.x, W / 2, 0.2, (v) => { winner.pos.x = v }, k.easings.easeOutQuad),
k.tween(winner.pos.y, H * 0.6, 0.2, (v) => { winner.pos.y = v }, k.easings.easeOutQuad),
])
screenFlash('#000000', 0.1)
spawnGrotesqueDetails(winner, 2.5)
await k.wait(0.1)
destroyGrotesqueDetails()
// Zoom back and SLAM into loser
sfxZoomWhoosh()
await Promise.all([
k.tween(winner.scale.y, Math.abs(origScaleY), 0.12, (v) => { winner.scale.x = origScaleX > 0 ? v : -v; winner.scale.y = v }, k.easings.easeInQuad),
k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeInQuad),
k.tween(winner.pos.y, loserOrigY, 0.12, (v) => { winner.pos.y = v }, k.easings.easeInQuad),
])
// === PHASE 2: Devastating rapid combo at contact ===
const colors = ['#ff2d7b', '#00f0ff', '#ffe14d', '#ff6600', '#b83dff', '#39ff14', '#ffffff', '#ff2d2d']
const hitCount = 10
for (let i = 0; i < hitCount; i++) {
const anim = ['attack', 'kick', 'special', 'attack'][i % 4]
winner.play(anim)
sfxRapidPunch()
await k.wait(0.04)
loser.play(i < hitCount - 1 ? 'hit' : 'knockback')
k.shake(3 + i)
spawnSparks(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 10 - Math.random() * 50, 5, colors[i % colors.length])
loser.pos.x += dir * 4
loser.pos.y += (i % 2 === 0 ? -3 : 3)
await k.wait(0.04)
}
loser.pos.y = loserOrigY
// === PHASE 3: Final massive hit — screen goes white ===
winner.play('special')
sfxCritical()
sfxExplosion()
await k.wait(0.06)
loser.play('knockback')
k.shake(35)
screenFlash('#ffe14d', 0.4)
spawnSparks(loser.pos.x, loser.pos.y - 30, 30, '#ffe14d')
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe14d')
// Launch loser way off with a spin
await Promise.all([
k.tween(loser.pos.x, loser.pos.x + dir * 200, 0.4, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, loserOrigY - 300, 0.25, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, loserOrigY, 0.25, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
])
spawnShockwave(loser.pos.x, GROUND_Y, '#ff2d2d')
spawnSparks(loser.pos.x, GROUND_Y - 10, 30, '#ff2d2d')
k.shake(20)
sfxExplosion()
sfxBoing()
await k.wait(0.3)
loser.play('ko')
// Winner walks back and celebrates
winner.scale.x = origScaleX
winner.scale.y = origScaleY
await k.tween(winner.pos.x, winningSide === 'a' ? HOME_A : HOME_B, 0.3, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
winner.play('win')
setTimeout(() => {
sfxWin()
sfxWinAnnounce(winnerName)
}, 300)
// FlawlessVictory voice is called by FightViewer after playPerfect returns
announceCrowdReaction('cheer')
// Massive fireworks
for (let i = 0; i < 8; i++) {
setTimeout(() => {
spawnSparks(
Math.random() * W,
Math.random() * H * 0.5,
18,
['#ff2d7b', '#00f0ff', '#ffe14d', '#b83dff', '#39ff14', '#ff6600', '#ffffff', '#ff2d2d'][i]
)
if (i % 2 === 0) sfxRandomComedy()
}, i * 200)
}
await k.wait(0.7)
},
startMusic() { startMusic() },
stopMusic() { stopMusic() },
destroy() {
stopMusic()
k.quit()
},
}
}
export type FightSceneController = Awaited<ReturnType<typeof createFightScene>>