import kaplay from 'kaplay' import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp, LoadSpriteOpt } from 'kaplay' type Fighter = GameObj 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 = { 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 = { 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 = { 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, 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 { try { await Promise.race([ k.loadSprite(name, src, opts), new Promise((_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 = {} 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 { 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 { 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 { 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 = { // Food pizza: { parts: [ { shape: 'rect', x: -7, y: -3, w: 14, h: 3, color: '#ffcc44' }, // crust { shape: 'rect', x: -6, y: 0, w: 12, h: 8, color: '#ff8800' }, // cheese { shape: 'rect', x: -5, y: 1, w: 10, h: 6, color: '#cc4400' }, // sauce { shape: 'circle', x: -3, y: 3, w: 3, h: 3, color: '#aa2200' }, // pepperoni { shape: 'circle', x: 2, y: 4, w: 3, h: 3, color: '#aa2200' }, // pepperoni { shape: 'circle', x: 0, y: 1, w: 2, h: 2, color: '#44aa22' }, // olive ]}, burger: { parts: [ { shape: 'rect', x: -8, y: -4, w: 16, h: 4, color: '#cc8833' }, // top bun { shape: 'rect', x: -7, y: 0, w: 14, h: 3, color: '#44aa22' }, // lettuce { shape: 'rect', x: -7, y: 3, w: 14, h: 4, color: '#883311' }, // patty { shape: 'rect', x: -8, y: 7, w: 16, h: 3, color: '#cc8833' }, // bottom bun { shape: 'rect', x: -7, y: -1, w: 14, h: 1, color: '#ffcc00' }, // cheese ]}, hotdog: { parts: [ { shape: 'rect', x: -10, y: -2, w: 20, h: 4, color: '#ffcc66' }, // bun { shape: 'rect', x: -9, y: -1, w: 18, h: 3, color: '#cc4422' }, // sausage { shape: 'rect', x: -8, y: -2, w: 16, h: 1, color: '#ffee00' }, // mustard ]}, banana: { parts: [ { shape: 'rect', x: -3, y: -8, w: 5, h: 16, color: '#ffe135' }, { shape: 'rect', x: -2, y: -9, w: 3, h: 2, color: '#88660d' }, // stem { shape: 'rect', x: -2, y: -7, w: 3, h: 14, color: '#ffee55' }, // highlight ]}, pie: { parts: [ { shape: 'circle', x: 0, y: 0, w: 16, h: 16, color: '#f5deb3' }, // crust { shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' }, // cream { shape: 'circle', x: -2, y: -2, w: 3, h: 3, color: '#ff4444' }, // cherry ]}, // Weapons gun: { parts: [ { shape: 'rect', x: 0, y: -3, w: 24, h: 6, color: '#444444' }, // barrel { shape: 'rect', x: -4, y: -5, w: 12, h: 10, color: '#333333' }, // body { shape: 'rect', x: -2, y: 5, w: 6, h: 10, color: '#553322' }, // grip { shape: 'rect', x: 22, y: -2, w: 4, h: 4, color: '#555555' }, // muzzle { shape: 'rect', x: 6, y: -6, w: 8, h: 3, color: '#555555' }, // sight ]}, shotgun: { parts: [ { shape: 'rect', x: 0, y: -2, w: 35, h: 5, color: '#333333' }, // barrel { shape: 'rect', x: 0, y: 3, w: 30, h: 4, color: '#333333' }, // under barrel { shape: 'rect', x: -6, y: -4, w: 10, h: 12, color: '#553322' }, // stock { shape: 'rect', x: -2, y: 7, w: 6, h: 8, color: '#664433' }, // grip { shape: 'rect', x: 33, y: -1, w: 4, h: 3, color: '#555555' }, // muzzle ]}, rocketLauncher: { parts: [ { shape: 'rect', x: 0, y: -5, w: 30, h: 10, color: '#446633' }, // tube { shape: 'circle', x: 28, y: 0, w: 10, h: 10, color: '#335522' }, // end { shape: 'circle', x: 2, y: 0, w: 10, h: 10, color: '#335522' }, // back end { shape: 'rect', x: 8, y: 5, w: 6, h: 8, color: '#553322' }, // grip { shape: 'rect', x: 14, y: -8, w: 6, h: 4, color: '#555555' }, // sight ]}, minigun: { parts: [ { shape: 'rect', x: 0, y: -6, w: 28, h: 3, color: '#444444' }, // barrel 1 { shape: 'rect', x: 0, y: -2, w: 28, h: 3, color: '#555555' }, // barrel 2 { shape: 'rect', x: 0, y: 2, w: 28, h: 3, color: '#444444' }, // barrel 3 { shape: 'rect', x: -8, y: -8, w: 10, h: 16, color: '#333333' }, // housing { shape: 'rect', x: -4, y: 5, w: 6, h: 10, color: '#553322' }, // grip { shape: 'circle', x: 27, y: -2, w: 8, h: 8, color: '#666666' }, // muzzle ring ]}, sniper: { parts: [ { shape: 'rect', x: 0, y: -1, w: 38, h: 3, color: '#333333' }, // barrel { shape: 'rect', x: -6, y: -4, w: 14, h: 8, color: '#444444' }, // body { shape: 'rect', x: -10, y: -3, w: 8, h: 4, color: '#553322' }, // stock { shape: 'circle', x: 12, y: -6, w: 6, h: 6, color: '#225588' }, // scope { shape: 'rect', x: 8, y: -7, w: 10, h: 2, color: '#555555' }, // scope mount ]}, katana: { parts: [ { shape: 'rect', x: 0, y: -1, w: 40, h: 2, color: '#ccccdd' }, // blade { shape: 'rect', x: 38, y: -1, w: 4, h: 2, color: '#ffffff' }, // tip { shape: 'rect', x: -2, y: -3, w: 4, h: 6, color: '#ffcc00' }, // guard { shape: 'rect', x: -10, y: -2, w: 10, h: 4, color: '#442211' }, // handle { shape: 'rect', x: -8, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap { shape: 'rect', x: -5, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap ]}, // Vehicles (used as props in throws/drops) car_mini: { parts: [ { shape: 'rect', x: -12, y: -4, w: 24, h: 8, color: '#ff4444' }, // body { shape: 'rect', x: -6, y: -8, w: 12, h: 5, color: '#4488ff' }, // roof/windows { shape: 'circle', x: -8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel { shape: 'circle', x: 8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel { shape: 'rect', x: 11, y: -2, w: 3, h: 3, color: '#ffee00' }, // headlight ]}, tank_mini: { parts: [ { shape: 'rect', x: -14, y: 0, w: 28, h: 8, color: '#556633' }, // hull { shape: 'rect', x: -8, y: -6, w: 16, h: 7, color: '#445522' }, // turret { shape: 'rect', x: 6, y: -5, w: 16, h: 3, color: '#333322' }, // barrel { shape: 'rect', x: -13, y: 7, w: 26, h: 4, color: '#333333' }, // tracks { shape: 'circle', x: -10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel { shape: 'circle', x: 10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel ]}, // Misc props bomb: { parts: [ { shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#222222' }, { shape: 'rect', x: -1, y: -9, w: 3, h: 5, color: '#888888' }, // fuse { shape: 'circle', x: 0, y: -10, w: 4, h: 4, color: '#ff6600' }, // spark ]}, shield: { parts: [ { shape: 'rect', x: -10, y: -14, w: 20, h: 28, color: '#4466aa' }, { shape: 'rect', x: -8, y: -12, w: 16, h: 24, color: '#5577bb' }, { shape: 'rect', x: -3, y: -8, w: 6, h: 16, color: '#ffcc00' }, // emblem cross { shape: 'rect', x: -7, y: -3, w: 14, h: 6, color: '#ffcc00' }, // emblem cross { shape: 'rect', x: -10, y: -14, w: 20, h: 2, color: '#6688cc' }, // top edge ]}, soccerball: { parts: [ { shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' }, { shape: 'rect', x: -3, y: -3, w: 6, h: 6, color: '#222222' }, // pentagon ]}, donut: { parts: [ { shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff88cc' }, // icing { shape: 'circle', x: 0, y: 0, w: 6, h: 6, color: '#cc8833' }, // hole (body color) { shape: 'circle', x: -3, y: -3, w: 2, h: 2, color: '#ff4444' }, // sprinkle { shape: 'circle', x: 3, y: -2, w: 2, h: 2, color: '#44ff44' }, // sprinkle { shape: 'circle', x: 1, y: 4, w: 2, h: 2, color: '#ffff00' }, // sprinkle ]}, watermelon: { parts: [ { shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#33aa33' }, // rind { shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff4466' }, // flesh { shape: 'circle', x: -2, y: -1, w: 2, h: 2, color: '#222222' }, // seed { shape: 'circle', x: 3, y: 2, w: 2, h: 2, color: '#222222' }, // seed { shape: 'circle', x: 0, y: 4, w: 2, h: 2, color: '#222222' }, // seed ]}, chainsaw: { parts: [ { shape: 'rect', x: 0, y: -2, w: 28, h: 5, color: '#888888' }, // blade { shape: 'rect', x: -8, y: -5, w: 12, h: 10, color: '#ff6600' }, // body { shape: 'rect', x: -4, y: 5, w: 6, h: 7, color: '#553322' }, // grip { shape: 'rect', x: 26, y: -1, w: 4, h: 3, color: '#aaaaaa' }, // tip { shape: 'rect', x: 0, y: -3, w: 26, h: 1, color: '#666666' }, // chain top { shape: 'rect', x: 0, y: 3, w: 26, h: 1, color: '#666666' }, // chain bottom ]}, // Fatality props rubber_chicken: { parts: [ { shape: 'rect', x: -2, y: -12, w: 5, h: 20, color: '#ffdd44' }, // body { shape: 'circle', x: 0, y: -14, w: 8, h: 8, color: '#ffee55' }, // head { shape: 'rect', x: 2, y: -16, w: 8, h: 3, color: '#ff8800' }, // beak { shape: 'circle', x: -1, y: -15, w: 2, h: 2, color: '#000000' }, // eye { shape: 'rect', x: -1, y: 6, w: 3, h: 5, color: '#ff6600' }, // leg ]}, anvil: { parts: [ { shape: 'rect', x: -14, y: -4, w: 28, h: 12, color: '#555555' }, // body { shape: 'rect', x: -18, y: 6, w: 36, h: 5, color: '#444444' }, // base { shape: 'rect', x: -10, y: -8, w: 20, h: 5, color: '#666666' }, // top { shape: 'rect', x: 8, y: -12, w: 6, h: 5, color: '#777777' }, // horn ]}, flyswatter: { parts: [ { shape: 'rect', x: -2, y: 0, w: 4, h: 35, color: '#cc8833' }, // handle { shape: 'rect', x: -12, y: -14, w: 24, h: 16, color: '#44aa44' }, // paddle { shape: 'rect', x: -10, y: -12, w: 20, h: 12, color: '#338833' }, // grid ]}, toilet: { parts: [ { shape: 'rect', x: -12, y: -4, w: 24, h: 16, color: '#eeeeee' }, // bowl { shape: 'rect', x: -14, y: -14, w: 28, h: 12, color: '#dddddd' }, // tank { shape: 'circle', x: 0, y: 4, w: 16, h: 16, color: '#aaddff' }, // water { shape: 'rect', x: -12, y: -5, w: 24, h: 3, color: '#cccccc' }, // seat { shape: 'rect', x: 8, y: -12, w: 4, h: 6, color: '#888888' }, // handle ]}, keyboard: { parts: [ { shape: 'rect', x: -20, y: -3, w: 40, h: 8, color: '#333333' }, // base { shape: 'rect', x: -18, y: -2, w: 36, h: 6, color: '#444444' }, // keys area { shape: 'rect', x: -16, y: -1, w: 4, h: 3, color: '#666666' }, // key { shape: 'rect', x: -10, y: -1, w: 4, h: 3, color: '#666666' }, // key { shape: 'rect', x: -4, y: -1, w: 4, h: 3, color: '#666666' }, // key { shape: 'rect', x: 2, y: -1, w: 12, h: 3, color: '#666666' }, // spacebar ]}, disco_ball: { parts: [ { shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#cccccc' }, // ball { shape: 'rect', x: -1, y: -12, w: 2, h: 5, color: '#888888' }, // chain { shape: 'rect', x: -3, y: -2, w: 3, h: 3, color: '#ffffff' }, // mirror { shape: 'rect', x: 2, y: 1, w: 3, h: 3, color: '#eeeeee' }, // mirror { shape: 'rect', x: -1, y: 4, w: 3, h: 3, color: '#ffffff' }, // mirror ]}, rubber_duck: { parts: [ { shape: 'circle', x: 0, y: 0, w: 10, h: 10, color: '#ffdd00' }, // body { shape: 'circle', x: 3, y: -5, w: 7, h: 7, color: '#ffee22' }, // head { shape: 'rect', x: 5, y: -6, w: 5, h: 2, color: '#ff8800' }, // beak { shape: 'circle', x: 4, y: -6, w: 2, h: 2, color: '#000000' }, // eye ]}, baguette: { parts: [ { shape: 'rect', x: -16, y: -3, w: 32, h: 6, color: '#cc9944' }, // bread { shape: 'rect', x: -14, y: -2, w: 28, h: 4, color: '#ddaa55' }, // crust highlight { shape: 'rect', x: -8, y: -4, w: 4, h: 1, color: '#bb8833' }, // score { shape: 'rect', x: 2, y: -4, w: 4, h: 1, color: '#bb8833' }, // score ]}, boot: { parts: [ { shape: 'rect', x: -8, y: -16, w: 14, h: 24, color: '#553322' }, // shaft { shape: 'rect', x: -8, y: 6, w: 22, h: 8, color: '#442211' }, // sole { shape: 'rect', x: -6, y: -14, w: 10, h: 4, color: '#664433' }, // top band { shape: 'rect', x: 12, y: 4, w: 4, h: 6, color: '#442211' }, // toe ]}, cannon: { parts: [ { shape: 'rect', x: -10, y: -6, w: 28, h: 12, color: '#444444' }, // barrel { shape: 'circle', x: -10, y: 0, w: 14, h: 14, color: '#333333' }, // base { shape: 'circle', x: 16, y: 0, w: 10, h: 10, color: '#555555' }, // muzzle { shape: 'rect', x: -6, y: 6, w: 6, h: 8, color: '#664433' }, // wheel ]}, stamp: { parts: [ { shape: 'rect', x: -3, y: -18, w: 6, h: 14, color: '#884422' }, // handle { shape: 'rect', x: -14, y: -6, w: 28, h: 8, color: '#cc4444' }, // stamp face { shape: 'rect', x: -12, y: -4, w: 24, h: 4, color: '#aa2222' }, // ink ]}, vending_machine: { parts: [ { shape: 'rect', x: -12, y: -18, w: 24, h: 36, color: '#cc3333' }, // body { shape: 'rect', x: -10, y: -14, w: 12, h: 20, color: '#222222' }, // window { shape: 'rect', x: 3, y: -8, w: 6, h: 4, color: '#44aa44' }, // button { shape: 'rect', x: 3, y: 0, w: 6, h: 4, color: '#4444aa' }, // button { shape: 'rect', x: -8, y: 10, w: 10, h: 5, color: '#333333' }, // dispenser ]}, lawn_mower: { parts: [ { shape: 'rect', x: -10, y: -2, w: 20, h: 8, color: '#44aa44' }, // body { shape: 'circle', x: -8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel { shape: 'circle', x: 8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel { shape: 'rect', x: -2, y: -12, w: 3, h: 12, color: '#888888' }, // handle { shape: 'rect', x: -4, y: -14, w: 8, h: 3, color: '#666666' }, // grip ]}, } // Spawn a multi-part pixel prop at position, returns array of game objects function spawnWeaponProp(propName: string, x: number, y: number, flipX: boolean = false, zIndex: number = 16): any[] { const def = PROPS[propName] if (!def) { // Fallback: single colored circle return [k.add([k.circle(8), k.pos(x, y), k.color(k.Color.fromHex('#ff8800')), k.opacity(1), k.z(zIndex), k.anchor('center')])] } const objs: any[] = [] for (const p of def.parts) { const px = flipX ? x - p.x : x + p.x const obj = k.add([ p.shape === 'circle' ? k.circle(p.w / 2) : k.rect(p.w, p.h), k.pos(px, y + p.y), k.color(k.Color.fromHex(p.color)), k.opacity(1), k.z(zIndex), k.anchor('center'), ]) objs.push(obj) } return objs } // Move all parts of a prop to a new position (relative to anchor) function moveProp(objs: any[], def: PropDef, x: number, y: number, flipX: boolean = false) { for (let i = 0; i < objs.length && i < def.parts.length; i++) { const p = def.parts[i] objs[i].pos.x = flipX ? x - p.x : x + p.x objs[i].pos.y = y + p.y } } function destroyProp(objs: any[]) { objs.forEach(o => { if (o.exists()) o.destroy() }) } // Map choreography names to prop types for the factories const CHOREO_PROPS: Record = { pizzaSlam: 'pizza', burgerToss: 'burger', hotdogWhip: 'hotdog', bananaFling: 'banana', pieSmash: 'pie', watermelonBomb: 'watermelon', donutBarrage: 'donut', sushiBarrage: 'pizza', tacoStorm: 'pizza', iceCreamFling: 'donut', bombThrow: 'bomb', grenadeBlast: 'bomb', dynamiteBlast: 'bomb', bowlingBallRoll: 'soccerball', soccerKick: 'soccerball', basketballDunk: 'soccerball', } // Map choreography names to weapon props the attacker holds const CHOREO_WEAPON: Record = { gunBurst: 'gun', sniperShot: 'sniper', minigunSpray: 'minigun', rocketLauncher: 'rocketLauncher', pocketCannon: 'shotgun', chainsawRev: 'chainsaw', katanaCombo: 'katana', swordSlash: 'katana', } // === PER-CHARACTER WEAPON SYSTEM === // Each bot gets 3 special weapons based on their seed, balanced frequency interface BotWeapons { melee: string // close range weapon prop ranged: string // gun/launcher prop thrown: string // throwable prop } const MELEE_WEAPONS = ['katana', 'chainsaw', 'shield'] const RANGED_WEAPONS = ['gun', 'shotgun', 'sniper', 'minigun', 'rocketLauncher'] const THROWN_WEAPONS = ['pizza', 'burger', 'bomb', 'watermelon', 'donut', 'soccerball', 'banana'] function getBotWeapons(seed: string): BotWeapons { let h = 0 for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0 return { melee: MELEE_WEAPONS[Math.abs(h) % MELEE_WEAPONS.length], ranged: RANGED_WEAPONS[Math.abs(h >> 3) % RANGED_WEAPONS.length], thrown: THROWN_WEAPONS[Math.abs(h >> 7) % THROWN_WEAPONS.length], } } const weaponsA = getBotWeapons(botA.seed) const weaponsB = getBotWeapons(botB.seed) // === DEFENSE / SHIELD SYSTEM === 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 = { 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 { 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 = { 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 }> = {} 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 = { a: false, b: false } type ShowboatFn = (fighter: any, homeX: number, homeY: number, sx: number, sy: number, dir: number) => Promise // 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 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 = { '↑': '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) | 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>