import kaplay from 'kaplay' import { generateSpriteSheet, generateJudgeSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS } from './sprites' import { sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit, sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect, sfxRoundStart, sfxBlock, sfxDodge, sfxClash, sfxRandomSilly, sfxBoing, sfxBonk, sfxZap, sfxSlideDown, sfxZoomWhoosh, sfxRapidPunch, fanfareRound, fanfareFight, fanfareDevastating, fanfareCritical, fanfareCombo, startMusic, stopMusic, announce, announceRandom, announceDramatic, announceHype, announceSilly, announceCool, announceFinishHim, announceFatality, announceFlawlessVictory, announceRandomHype, announceDeepIntro, announceRoundHype, announceCrowdReaction, } from './sounds' export interface FightSceneConfig { canvas: HTMLCanvasElement botA: { name: string; seed: string; tier: number; archetype?: string } botB: { name: string; seed: string; tier: number; archetype?: string } 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 } 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'], 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'], 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'], generic: ['dashPunch', 'flyingKick', 'uppercut', 'zoomRush', 'bombThrow', 'minigunSpray', 'pocketCannon'] }, creative_writing: { themed: ['penStab', 'scrollBlast', 'bookStorm', 'enchantedArrow'], generic: ['jetpackDive', 'aerialSlam', 'projectile', 'rapidFlurry', 'katanaCombo', 'arcaneBarrage', 'memeBeam'] }, math_blitz: { themed: ['mathAttack', 'dataStream', 'pixelBeam', 'hackerAttack'], 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'], generic: ['laserSword', 'plasmaSword', 'asteroidBelt', 'satelliteLaser', 'plasmaBeam', 'solarFlare'] }, hack_battle: { themed: ['hackerAttack', 'bugSwarm', 'stackOverflow', 'segfault', 'blueScreen', 'malwareInject'], 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'] }, } // 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'], 4: ['ultimateFinaleRush', 'ultimateMeteorCrash', 'ultimateScreenShatter', 'ultimateGravityReverse', 'ultimateInfiniteCombo', 'ultimatePlasmaStorm', 'ultimateFrozenCoffin', 'ultimateLavaGeyser', 'ultimateChainGrab', 'ultimateRocketBarrage'], 5: ['ultimateArmageddon', 'ultimateOmegaBeam', 'ultimateBeyondTheScreen', 'ultimateMatrixDodge', 'ultimateWorldEnder', 'ultimateAbsoluteZero', 'ultimateSolarCannon', 'ultimatePhantomStrike', 'ultimateGodPunch', 'ultimateVoidCollapse'], } function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0): string { // 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', ] 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 try { sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype) } catch (err) { console.error('[FightScene] Failed to generate sprite for botA:', err) sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary) } try { sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype) } catch (err) { console.error('[FightScene] Failed to generate sprite for botB:', err) sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary) } // Await sprite loading to prevent race condition where scene starts before sprites decode await Promise.all([ k.loadSprite('botA', sheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }), k.loadSprite('botB', sheetB, { 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 k.loadSprite('judge', judgeSheet, { sliceX: JUDGE_MAX_FRAMES, sliceY: JUDGE_ROWS, anims: judgeAnims }) const W = k.width() const H = k.height() 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) { for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2 const speed = 100 + Math.random() * 300 const size = 2 + Math.random() * 4 const spark = k.add([ k.rect(size, size), k.pos(x, y), k.color(safeColor(k,color)), k.opacity(1), k.z(20), k.rotate(Math.random() * 360), { vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 100 }, ]) spark.onUpdate(() => { spark.pos.x += (spark as any).vx * k.dt() spark.pos.y += (spark as any).vy * k.dt() ;(spark as any).vy += 400 * k.dt() // gravity 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: any[] = [] 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: any[] = [] 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] const fB = k.get('fighterB')[0] 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: any[] = [] 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()) } // Grotesque close-up stubs (disabled — looked bad) function spawnGrotesqueDetails(_fighter: any, _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') { // Server rack wall — multiple rows of racks with blinking LEDs 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 // Rack body with border 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)]) // Drive slots (horizontal lines) 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)]) } // Multiple blinking LEDs per rack 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 }) } // Activity LED (flashing amber) 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 running across ceiling for (let c = 0; c < 4; c++) { const cy = 5 + c * 3 k.add([k.rect(W, 2), k.pos(0, cy), k.color(safeColor(k,c % 2 === 0 ? '#111133' : '#0a0a22')), k.opacity(0.5), k.z(1)]) } } else if (a === 'gpu_graveyard') { // Floating circuit board fragments with traces and solder points 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), ]) // Trace lines on each chip 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)]) } // Solder point 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 fan silhouettes 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)]) // Fan blades (static, dead) for (let b = 0; b < 4; b++) { const ba = b * Math.PI / 2 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)]) } } } else if (a === 'prompt_dungeon') { // Stone wall texture 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 scattered on walls const runeSymbols = ['*', '+', 'o', '~', '^'] 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), ]) // Rune glow halo 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 on walls 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)]) 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 }) } // 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 }) } } 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 // Multi-blob cloud 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 streams (vertical lines falling like rain) for (let i = 0; i < 10; 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 }) } } else if (a === 'the_singularity') { // Central swirling vortex with multiple rings for (let ring = 0; ring < 3; ring++) { const ringDots = 12 + ring * 6 for (let i = 0; i < ringDots; i++) { const angle = (i / ringDots) * Math.PI * 2 const baseDist = 30 + ring * 35 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 ? '#ff44ff' : ring === 1 ? '#ff00ff' : '#aa00aa')), k.opacity(0.25 - ring * 0.05), k.z(1), ]) dot.onUpdate(() => { const a2 = angle + k.time() * (0.6 - 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 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)]) const coreHalo = 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)]) core.onUpdate(() => { core.opacity = 0.1 + Math.sin(k.time() * 2) * 0.08 }) } 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)]) // Segment node 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)]) } // Leaves at various heights for (let l = 0; l < 3; l++) { const ly = 20 + l * (GROUND_Y / 4) const dir = Math.random() > 0.5 ? 1 : -1 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)]) 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 on ground 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)]) } } else if (a === 'hacker_news' || a === 'stackoverflow_ruins') { // Code blocks / post fragments 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)]) // Indent markers 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) // Upvote triangle 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)]) } } else if (a === 'beach') { // Sun with rays const sunX = W * 0.8, sunY = 35 k.add([k.circle(30), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k,'#ffdd44')), k.opacity(0.12), 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 }) // Sun rays for (let r = 0; r < 8; r++) { const angle = r * Math.PI / 4 k.add([k.rect(40, 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)]) } // Ocean horizon with layered waves for (let layer = 0; layer < 3; layer++) { for (let i = 0; i < 4; i++) { const wave = k.add([ k.rect(W * 0.35, 3 + layer), k.pos(Math.random() * W, GROUND_Y - 18 - layer * 8 - i * 3), k.color(safeColor(k,layer === 0 ? '#3377bb' : '#2266aa')), k.opacity(0.2 - layer * 0.04), 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) }) } } // Palm trees with trunks and fronds for (const px of [W * 0.06, W * 0.93]) { const lean = px < W / 2 ? 8 : -8 // Trunk segments for (let s = 0; s < 8; s++) { const sy = GROUND_Y - 10 - s * 12 k.add([k.rect(7 - s * 0.5, 13), k.pos(px + s * lean / 8, sy), k.color(safeColor(k,'#3a2510')), k.opacity(0.5), k.z(1)]) } // Fronds const topX = px + lean, topY = GROUND_Y - 106 for (let f = 0; f < 6; f++) { const angle = -60 + f * 24 k.add([k.rect(35, 3), k.pos(topX, topY), k.anchor('left'), k.color(safeColor(k,'#1a6600')), k.opacity(0.35), k.z(1), k.rotate(angle)]) } } // Seagulls (v-shapes in sky) for (let i = 0; i < 3; i++) { const bx = W * 0.3 + Math.random() * W * 0.4 const by = 15 + Math.random() * 30 k.add([k.rect(6, 1), k.pos(bx, by), k.color(safeColor(k,'#333333')), k.opacity(0.3), k.z(1), k.rotate(-15)]) k.add([k.rect(6, 1), k.pos(bx + 5, by), k.color(safeColor(k,'#333333')), k.opacity(0.3), k.z(1), k.rotate(15)]) } } 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)]) } // Cacti with arms for (const cx of [W * 0.1, W * 0.5, W * 0.88]) { const ch = 35 + Math.random() * 20 // Main trunk k.add([k.rect(7, ch), k.pos(cx, GROUND_Y - ch), k.color(safeColor(k,'#226622')), k.opacity(0.45), k.z(1)]) // Arms 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)]) } } // Tumbleweed const tumble = k.add([k.circle(8), k.pos(-20, GROUND_Y - 10), k.color(safeColor(k,'#886644')), k.opacity(0.3), k.z(2)]) tumble.onUpdate(() => { tumble.pos.x = ((tumble.pos.x + 20 * k.dt()) % (W + 40)) - 20 }) // Heat shimmer bands for (let i = 0; i < 3; 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 }) } } else if (a === 'forest') { // Layered trees — back layer (small/faded), front layer (larger/darker) for (let layer = 0; layer < 2; layer++) { const treeCount = layer === 0 ? 12 : 6 const treeOp = layer === 0 ? 0.2 : 0.4 for (let i = 0; i < treeCount; i++) { const tx = 15 + (i / treeCount) * (W - 30) + (Math.random() - 0.5) * 20 const th = (30 + Math.random() * 30) * (layer === 0 ? 0.7 : 1) const trunkW = layer === 0 ? 5 : 8 // Trunk k.add([k.rect(trunkW, th), k.pos(tx, GROUND_Y - th), k.color(safeColor(k,layer === 0 ? '#0f1a08' : '#1a2a10')), k.opacity(treeOp), k.z(1 + layer)]) // Canopy — multiple overlapping circles for (let c = 0; c < 3; c++) { const cr = (12 + Math.random() * 12) * (layer === 0 ? 0.7 : 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' : '#1a3a10')), k.opacity(treeOp * 0.8), k.z(1 + layer), ]) } } } // Undergrowth (bushes at ground level) for (let i = 0; i < 8; i++) { k.add([k.circle(8 + Math.random() * 6), k.pos(Math.random() * W, GROUND_Y - 4), k.anchor('bot'), k.color(safeColor(k,'#1a3a10')), k.opacity(0.3), k.z(3)]) } // Fireflies for (let i = 0; i < 12; 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 < 4; i++) { const rx = W * (0.2 + i * 0.2) + (Math.random() - 0.5) * 30 k.add([k.rect(6, GROUND_Y * 0.6), k.pos(rx, 0), k.color(safeColor(k,'#aaff44')), k.opacity(0.03), k.z(2), k.rotate(5 - Math.random() * 10)]) } } else if (a === 'jungle') { // Dense vine canopy hanging from top for (let i = 0; i < 10; i++) { const vx = 20 + i * (W / 10) + (Math.random() - 0.5) * 15 const vineLen = 30 + Math.random() * 80 // Vine strand (wavy) for (let s = 0; s < vineLen; s += 4) { 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)]) } // Leaf clusters at bottom 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 in foreground edges for (const side of [0, W - 30]) { for (let l = 0; l < 3; l++) { const ly = 20 + l * (GROUND_Y / 4) k.add([k.rect(30, 8), k.pos(side, ly), k.color(safeColor(k,'#1a5510')), k.opacity(0.25), k.z(3), k.rotate(side === 0 ? 15 : -15)]) } } // Thick ground fog for (let i = 0; i < 8; 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 }) } } else if (a === 'outer_space') { // Dense star field with color variety for (let i = 0; i < 80; 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 — large, colorful, layered const nebulaColors = ['#4400aa', '#aa0066', '#0044aa', '#006644', '#660044'] for (let i = 0; i < 5; i++) { for (let blob = 0; blob < 3; blob++) { k.add([ k.circle(30 + Math.random() * 50), k.pos(W * (0.1 + i * 0.2) + blob * 20, H * 0.25 + Math.random() * 60 + blob * 15), k.color(safeColor(k,nebulaColors[i % nebulaColors.length])), k.opacity(0.05 + blob * 0.015), k.z(0), ]) } } // Floating asteroids with rotation for (let i = 0; i < 5; 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), ]) // Crater dot 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 const planetX = W * 0.75, planetY = GROUND_Y * 0.25 k.add([k.circle(20), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k,'#334466')), k.opacity(0.2), k.z(0)]) k.add([k.rect(44, 3), k.pos(planetX - 22, planetY - 1), k.color(safeColor(k,'#556688')), k.opacity(0.12), k.z(0), k.rotate(15)]) } 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 // Pillar left 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)]) // Pillar right 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)]) // Arch top 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)]) // Dark interior 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)]) } // Crumbled stone debris for (let i = 0; i < 6; 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), ]) } } else if (a === 'haunted_mansion') { // Gothic windows 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)]) // Window frame 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)]) // Moonlight through window 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 }) } // Cobwebs in corners for (const cx of [15, W - 35]) { for (let strand = 0; strand < 4; strand++) { k.add([k.rect(20 + strand * 5, 1), k.pos(cx, 5 + strand * 5), k.color(safeColor(k,'#666666')), k.opacity(0.12), k.z(1), k.rotate(cx < W / 2 ? 30 + strand * 5 : -30 - strand * 5)]) } } // Floating ghost particles for (let i = 0; i < 6; 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 }) } } else if (a === 'concert_hall') { // Stage lights const lightColors = ['#ff2288', '#22ff88', '#4488ff', '#ffaa22', '#ff44ff', '#44ffff'] for (let i = 0; i < 6; i++) { const lx = W * (0.1 + i * 0.15) // Light cone from ceiling 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), ]) // Light source dot 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)]) cone.onUpdate(() => { cone.opacity = 0.02 + Math.sin(k.time() * 1.5 + i * 1.2) * 0.025 }) } // Speaker stacks on sides for (const sx of [15, W - 35]) { for (let s = 0; s < 3; 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)]) } } } else if (a === 'colosseum' || a === 'sports_arena') { // Stadium seating tiers for (let tier = 0; tier < 4; tier++) { const ty = GROUND_Y * 0.15 + tier * (GROUND_Y * 0.15) k.add([k.rect(W, GROUND_Y * 0.12), k.pos(0, ty), k.color(safeColor(k,tier % 2 === 0 ? '#1a2a4a' : '#152040')), k.opacity(0.3), k.z(1)]) } } else if (a === 'kitchen_stadium') { // Kitchen equipment silhouettes // Stove k.add([k.rect(40, 30), k.pos(W * 0.08, GROUND_Y - 30), k.color(safeColor(k,'#1a1010')), k.opacity(0.4), k.z(1)]) for (let b = 0; b < 4; b++) { const flame = k.add([k.circle(4), k.pos(W * 0.08 + 6 + b * 9, GROUND_Y - 32), k.anchor('center'), k.color(safeColor(k,'#ff4400')), k.opacity(0.4), k.z(2)]) flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + b) * 0.15 }) } // Hanging pots for (let i = 0; i < 5; i++) { const px = W * (0.2 + i * 0.15) k.add([k.rect(1, 15 + Math.random() * 10), k.pos(px, 5), k.color(safeColor(k,'#333333')), k.opacity(0.3), k.z(1)]) k.add([k.circle(8), k.pos(px, 20 + Math.random() * 10), k.anchor('center'), k.color(safeColor(k,'#444444')), k.opacity(0.25), k.z(1)]) } } else if (a === 'junkyard') { // Piles of scrap for (let pile = 0; pile < 6; pile++) { const px = Math.random() * W const py = GROUND_Y - 10 for (let j = 0; j < 5; j++) { k.add([ k.rect(8 + Math.random() * 15, 5 + Math.random() * 10), k.pos(px + (Math.random() - 0.5) * 20, py - j * 8 - Math.random() * 5), k.color(safeColor(k,['#554433', '#665544', '#443322', '#776655', '#332211'][j % 5])), k.opacity(0.3), k.z(1), k.rotate(Math.random() * 30 - 15), ]) } } } else if (a === 'meme_dimension') { // Floating emoji-like shapes for (let i = 0; i < 8; i++) { const emoji = k.add([ k.circle(8 + Math.random() * 5), k.pos(Math.random() * W, Math.random() * GROUND_Y), k.color(safeColor(k,'#ffcc00')), k.opacity(0.15), k.z(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 }) } // Glitch bands for (let i = 0; i < 5; i++) { const band = k.add([ k.rect(W, 2 + Math.random() * 3), 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.15 : 0 }) } } else if (a === 'space_station') { // Structural beams for (let i = 0; i < 6; i++) { const bx = W * (0.15 + i * 0.15) k.add([k.rect(4, GROUND_Y), k.pos(bx, 0), k.color(safeColor(k,'#1a1a30')), k.opacity(0.3), k.z(1)]) // Cross beams k.add([k.rect(W * 0.15, 3), k.pos(bx, GROUND_Y * 0.3), k.color(safeColor(k,'#1a1a30')), k.opacity(0.25), k.z(1)]) } // Viewport (window to space) const vpX = W * 0.5, vpY = GROUND_Y * 0.3 k.add([k.circle(35), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k,'#000020')), k.opacity(0.5), k.z(0)]) k.add([k.circle(37), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k,'#2a2a50')), k.opacity(0.3), k.z(0)]) // Stars through viewport for (let s = 0; s < 8; s++) { k.add([ k.circle(1), k.pos(vpX + (Math.random() - 0.5) * 50, vpY + (Math.random() - 0.5) * 50), k.color(safeColor(k,'#ffffff')), k.opacity(0.3), k.z(0), ]) } } else if (a === 'savanna') { // Acacia tree silhouettes for (const tx of [W * 0.12, W * 0.7]) { k.add([k.rect(5, 50), k.pos(tx, GROUND_Y - 50), k.color(safeColor(k,'#2a1a08')), k.opacity(0.4), k.z(1)]) // Flat canopy k.add([k.rect(40, 5), k.pos(tx - 17, GROUND_Y - 52), k.color(safeColor(k,'#2a3a10')), k.opacity(0.3), k.z(1)]) k.add([k.rect(30, 4), k.pos(tx - 12, GROUND_Y - 57), k.color(safeColor(k,'#2a3a10')), k.opacity(0.25), k.z(1)]) } // Setting sun k.add([k.circle(25), k.pos(W * 0.85, GROUND_Y * 0.25), k.anchor('center'), k.color(safeColor(k,'#dd6622')), k.opacity(0.25), k.z(0)]) k.add([k.circle(35), k.pos(W * 0.85, GROUND_Y * 0.25), k.anchor('center'), k.color(safeColor(k,'#dd6622')), k.opacity(0.08), k.z(0)]) } else if (a === 'server_room') { // Server rack rows (perspective) for (let row = 0; row < 3; row++) { for (let col = 0; col < 6; col++) { const rx = 30 + col * (W / 6) const ry = 25 + row * 50 const rw = W / 6 - 12 k.add([k.rect(rw, 42), k.pos(rx, ry), k.color(safeColor(k,'#0a1a10')), k.opacity(0.4), k.z(1)]) // Status LEDs 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 }) } } } } } 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 k.add([k.rect(4, GROUND_Y - CHAIR_SEAT_Y + 15), k.pos(CHAIR_X - 22, CHAIR_SEAT_Y - 8), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)]) k.add([k.rect(4, GROUND_Y - CHAIR_SEAT_Y + 15), k.pos(CHAIR_X + 18, CHAIR_SEAT_Y - 8), 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 + i * ((GROUND_Y - CHAIR_SEAT_Y - 15) / 6) k.add([k.rect(44, 3), k.pos(CHAIR_X - 22, rungY), k.color(safeColor(k,'#443322')), k.opacity(0.6), k.z(3)]) } k.add([k.rect(54, 5), k.pos(CHAIR_X - 27, CHAIR_SEAT_Y - 2), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)]) k.add([k.rect(54, 1), k.pos(CHAIR_X - 27, CHAIR_SEAT_Y - 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3), k.z(3)]) k.add([k.rect(48, 4), k.pos(CHAIR_X - 24, CHAIR_SEAT_Y - 10), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)]) k.add([k.rect(3, 12), k.pos(CHAIR_X - 27, CHAIR_SEAT_Y - 10), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)]) k.add([k.rect(3, 12), k.pos(CHAIR_X + 24, CHAIR_SEAT_Y - 10), 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), k.z(4), k.opacity(0.9), 'judge', ]) const scaleA = 1.8 + botA.tier * 0.4 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 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: any, def: any, 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() } // 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: any, def: any, 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() } else { sfxPunch() } 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: any, def: any, 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') } else { sfxPunch() } 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: any, def: any, 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') if (isCritical) screenFlash('#00f0ff') // 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: any, def: any, 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() } 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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), ]) } // INVISIBLE SPEED: so fast you can barely see — just blur lines and impact async function lightningRush(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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 ]}, } // 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] 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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) // --- TIER 4+ ULTIMATES (Platinum) — full screen, multi-phase spectacles --- async function ultimateFinaleRush(atk: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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: any, def: any, 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) 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, // --- 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, // Tier 4+ ultimateFinaleRush, ultimateMeteorCrash, ultimateScreenShatter, ultimateGravityReverse, ultimateInfiniteCombo, ultimatePlasmaStorm, ultimateFrozenCoffin, ultimateLavaGeyser, ultimateChainGrab, ultimateRocketBarrage, // Tier 5+ ultimateArmageddon, ultimateOmegaBeam, ultimateBeyondTheScreen, ultimateMatrixDodge, ultimateWorldEnder, ultimateAbsoluteZero, ultimateSolarCannon, ultimatePhantomStrike, ultimateGodPunch, ultimateVoidCollapse, } // === 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'] // 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 // 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) // 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 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 = [ 'What a warrior!', 'They gave it everything!', 'Heart of a champion!', 'You can feel the respect!', 'That was beautiful!', 'Incredible spirit!', 'They left it all in the ring!', 'A true fighter!', 'Nothing but respect!', 'What courage!', 'The crowd is in tears!', 'What a moment!', 'This is what it\'s all about!', 'Pure heart!', 'Standing ovation!', ] const crowdSympathyLines = [ 'Aww...', 'So close!', 'Almost had it!', 'Tough break!', 'Next time!', 'Great effort though!', 'Don\'t give up!', 'The crowd feels that one...', 'Ohhh...', 'Heartbreaking!', ] const respectLines = [ 'Good fight.', 'You fought well.', 'Respect.', 'Well played.', 'That was fun!', 'Same time next week?', 'You\'re getting better!', 'No hard feelings!', 'Honor to fight you!', 'GG.', ] // Sanitize text for Kaplay (treats [ ] as styled text tags) function safeText(t: string): string { return t.replace(/[\[\]]/g, '') } // 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 stub (disabled — too small to look good) function spawnCrowdSigns(_count: number, _color: string, _text?: string) {} // 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') } sfxRandomSilly() 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 }) sfxRandomSilly() 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) } } return { k, async showAnnouncement(_text: string, _color: string = '#ffffff', _duration: number = 1200) {}, async playEntrance() { const fA = k.get('fighterA')[0] const fB = k.get('fighterB')[0] 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!') // 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("I can't believe you!", false) await k.wait(0.4) // Girlfriend storms off announceSilly('Fine! Good luck!') 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('Smooth!') // 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!') 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('Weeee!') 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!') 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() }, ] // 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 announceDeepIntro() await entrances[idxA](fA, HOME_A, true) await k.wait(0.3) 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') }, async playAttack(side: 'a' | 'b', choreographyName: string, isCritical: boolean) { const attacker = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] const defender = k.get(side === 'a' ? 'fighterB' : 'fighterA')[0] 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 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)) // Always 2-4 exchanges for variety — intensity controls winner dominance, not count const exchangeCount = 2 + Math.floor(Math.random() * 3) // Hyperdetail scales with intensity const hyperDetail = Math.random() < (0.05 + intensity * 0.45) const fA = k.get('fighterA')[0] const fB = k.get('fighterB')[0] 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) if (Math.random() < 0.3) { const challengeVoice: Record void> = { roast_battle: () => announceHype('Roast battle!'), food_fight: () => announceSilly('Food fight!'), wrestling_match: () => announceDramatic('Wrestling match!'), music_battle: () => announceCool('Music battle!'), magic_duel: () => announceDramatic('Magic duel!'), meme_war: () => announceSilly('Meme war!'), demolition: () => announceHype('Demolition derby!'), medieval_combat: () => announceDramatic('Medieval combat!'), space_war: () => announceCool('Space war!'), } const voiceFn = challengeVoice[event.challengeType] if (voiceFn) voiceFn() else if (Math.random() < 0.5) announceRoundHype() } // 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) } } 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 silly sound on non-decisive hits if (Math.random() < 0.15) sfxRandomSilly() } else { // Draw: alternate attackerSide = ex % 2 === 0 ? 'a' : 'b' } const attackerTier = attackerSide === 'a' ? botA.tier : botB.tier const choreo = pickChoreography(event.challengeType, exchangeCritical, event.round, attackerTier) // Morph system: transform during ultimates or devastating crits const isUltimate = choreo.startsWith('ultimate') const shouldMorph = 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] if (attacker) { 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) } } if (exchangeCritical && isLastExchange) { fanfareCritical() announceDramatic('Critical hit!') announceCrowdReaction('gasp') // RGB glitch + hyperspeed lines on critical final blow glitchRGB(0.3) const defPos = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0] 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 if (!aWon && !bWon && isLastExchange) { // Draw: clash in the middle const fA = k.get('fighterA')[0] const fB = k.get('fighterB')[0] if (fA && fB) { const midX = W / 2 sfxClash() await Promise.all([ k.tween(fA.pos.x, midX - 30, 0.15, (v) => { fA.pos.x = v }, k.easings.easeOutQuad), k.tween(fB.pos.x, midX + 30, 0.15, (v) => { fB.pos.x = v }, k.easings.easeOutQuad), ]) fA.play('attack'); fB.play('attack') await k.wait(0.1) fA.play('hit'); fB.play('hit') k.shake(8) spawnSparks(midX, GROUND_Y - 50, 12, '#ffffff') screenFlash('#ffffff', 0.08) sfxBonk() await k.wait(0.3) await Promise.all([ k.tween(fA.pos.x, HOME_A, 0.25, (v) => { fA.pos.x = v }, k.easings.easeOutQuad), k.tween(fB.pos.x, HOME_B, 0.25, (v) => { fB.pos.x = v }, k.easings.easeOutQuad), ]) fA.play('idle'); fB.play('idle') } } else { // Occasionally the defender dodges (non-final exchanges only) if (!isLastExchange && Math.random() < 0.15) { await this.playDodge(attackerSide === 'a' ? 'b' : 'a') sfxDodge() sfxBoing() } else { await this.playAttack(attackerSide, choreo, exchangeCritical) } } // 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.3 + Math.random() * 0.3) } // Clean up speed lines speedLines.forEach(l => { if (l.exists()) l.destroy() }) speedLines = [] // Clean up grotesque overlays and any leftover morphs before zooming out destroyGrotesqueDetails() destroyMorphOverlays() // Zoom back out from hyperdetail mode if (hyperDetail && fA && fB && savedScaleAX != null && savedScaleBX != null) { 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') }) } // Update combos if (aWon) { comboA++; comboB = 0 if (comboA >= 3) { fanfareCombo(comboA); announceHype(`${comboA} hit combo!`); announceCrowdReaction('cheer') } } else if (bWon) { comboB++; comboA = 0 if (comboB >= 3) { fanfareCombo(comboB); announceHype(`${comboB} hit combo!`); announceCrowdReaction('cheer') } } else { comboA = 0; comboB = 0 } if (isCritical && (aWon || bWon)) { fanfareDevastating() announceDramatic('Devastating!') announceCrowdReaction('ooh') // Dimensional shift on devastating rounds (60%) if (Math.random() < 0.6) dimensionalShift(0.5) } else if (aWon || bWon) { // Regular round win: crowd reacts (40%) if (Math.random() < 0.4) announceCrowdReaction(Math.random() < 0.5 ? 'cheer' : 'applause') } // Emotion: heartfelt announcer moment (8% chance on normal rounds) if (Math.random() < 0.08) { announceCool(heartfeltLines[Math.floor(Math.random() * heartfeltLines.length)]) } // Crowd sympathy for the loser on devastating rounds (25%) if (isCritical && (aWon || bWon) && Math.random() < 0.25) { const loser = k.get(aWon ? 'fighterB' : 'fighterA')[0] if (loser) { announceSilly(crowdSympathyLines[Math.floor(Math.random() * crowdSympathyLines.length)]) 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] const fB2 = k.get('fighterB')[0] 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() } }, async playTaunt(side: 'a' | 'b') { const taunter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] if (!taunter) return 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] 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] const winner = k.get(winningSide === 'a' ? 'fighterA' : 'fighterB')[0] 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() * 4) 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 { // 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) } // 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) } }) } // === Common ending: KO + celebration === announceFinishHim() 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.2) loser.play('ko') announceFatality() 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(); 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] const loser = k.get(winningSide === 'a' ? 'fighterB' : 'fighterA')[0] 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) announceFlawlessVictory() 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) sfxRandomSilly() }, i * 200) } await k.wait(0.7) }, startMusic() { startMusic() }, stopMusic() { stopMusic() }, destroy() { stopMusic() k.quit() }, } } export type FightSceneController = Awaited>