Files
botfights/frontend/src/game/FightScene.ts
T

8152 lines
387 KiB
TypeScript
Raw Normal View History

import kaplay from 'kaplay'
import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites'
import {
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect,
sfxRoundStart, sfxBlock, sfxDodge, sfxClash, sfxRandomSilly, sfxBoing,
sfxBonk, sfxZap, sfxSlideDown, sfxSlideUp, sfxZoomWhoosh, sfxRapidPunch,
sfxSplat, sfxWomp, sfxPowerUp, sfxCoin, sfxFail, sfxDrumRoll,
fanfareRound, fanfareFight, fanfareDevastating, fanfareCritical, fanfareCombo,
startMusic, stopMusic, announce,
announceRandom, announceDramatic, announceHype, announceSilly, announceCool,
announceRobot, announceScream, announceFast,
announceFinishHim, announceFatality, announceFlawlessVictory,
announceRandomHype, announceDeepIntro, announceRoundHype, announceCrowdReaction,
} from './sounds'
export interface FightSceneConfig {
canvas: HTMLCanvasElement
botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
botB: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
arena: string
onReady?: () => void
}
export interface RoundEvent {
round: number
challengeType: string
winnerId: string | null
botAId: string
botBId: string
narration: string
isCritical: boolean
botAScore: number
botBScore: number
}
const ARENA_THEMES: Record<string, { bg: string; ground: string; accent: string }> = {
datacenter: { bg: '#0a0a1a', ground: '#1a1a3a', accent: '#00f0ff' },
stackoverflow_ruins: { bg: '#1a0f00', ground: '#2a1f10', accent: '#f48024' },
gpu_graveyard: { bg: '#0a0a0a', ground: '#1a1a1a', accent: '#76b900' },
prompt_dungeon: { bg: '#0f0a1a', ground: '#1f1a2a', accent: '#b83dff' },
silicon_valley_dojo: { bg: '#0a1a0a', ground: '#1a2a1a', accent: '#00ff41' },
paper_mill: { bg: '#1a1a10', ground: '#2a2a20', accent: '#f0e68c' },
localhost: { bg: '#000000', ground: '#111111', accent: '#00ff41' },
the_cloud: { bg: '#0a0f1a', ground: '#1a1f2a', accent: '#4488ff' },
hacker_news: { bg: '#1a0f00', ground: '#2a1f10', accent: '#ff6600' },
the_singularity: { bg: '#1a0020', ground: '#2a0030', accent: '#ff00ff' },
beach: { bg: '#0a2040', ground: '#c2a060', accent: '#ffcc00' },
desert: { bg: '#1a1000', ground: '#a08040', accent: '#ff8800' },
forest: { bg: '#051005', ground: '#1a3010', accent: '#44cc22' },
jungle: { bg: '#081808', ground: '#1a3818', accent: '#33ff66' },
outer_space: { bg: '#000008', ground: '#0a0a20', accent: '#8888ff' },
colosseum: { bg: '#1a0f08', ground: '#3a2a18', accent: '#d4a030' },
kitchen_stadium: { bg: '#1a0a0a', ground: '#2a1a10', accent: '#ff4400' },
haunted_mansion: { bg: '#0a0510', ground: '#1a1020', accent: '#8844cc' },
meme_dimension: { bg: '#1a0820', ground: '#2a1830', accent: '#ff44ff' },
space_station: { bg: '#000010', ground: '#0a0a28', accent: '#44aaff' },
junkyard: { bg: '#0f0f0a', ground: '#2a2a1a', accent: '#aa8833' },
concert_hall: { bg: '#10001a', ground: '#20102a', accent: '#ff22aa' },
savanna: { bg: '#1a1808', ground: '#8a7830', accent: '#dd9922' },
server_room: { bg: '#000a05', ground: '#0a1a10', accent: '#00ff88' },
sports_arena: { bg: '#0a0a1a', ground: '#1a2a4a', accent: '#ff3333' },
}
const spriteAnims = {
idle: { from: 0, to: ANIMATIONS.idle.frames - 1, loop: true, speed: 6 },
attack: { from: MAX_FRAMES, to: MAX_FRAMES + ANIMATIONS.attack.frames - 1, loop: false, speed: 12 },
kick: { from: MAX_FRAMES * 2, to: MAX_FRAMES * 2 + ANIMATIONS.kick.frames - 1, loop: false, speed: 10 },
special: { from: MAX_FRAMES * 3, to: MAX_FRAMES * 3 + ANIMATIONS.special.frames - 1, loop: false, speed: 8 },
hit: { from: MAX_FRAMES * 4, to: MAX_FRAMES * 4 + ANIMATIONS.hit.frames - 1, loop: false, speed: 8 },
knockback: { from: MAX_FRAMES * 5, to: MAX_FRAMES * 5 + ANIMATIONS.knockback.frames - 1, loop: false, speed: 8 },
ko: { from: MAX_FRAMES * 6, to: MAX_FRAMES * 6 + ANIMATIONS.ko.frames - 1, loop: false, speed: 6 },
win: { from: MAX_FRAMES * 7, to: MAX_FRAMES * 7 + ANIMATIONS.win.frames - 1, loop: true, speed: 6 },
}
// Challenge type -> themed choreography (shown ~60% of the time) + generic fallbacks
const CHALLENGE_THEMED: Record<string, { themed: string[]; generic: string[] }> = {
speed_blitz: { themed: ['afterimageDash', 'lightningRush', 'rapidFlurry', 'tornadoSpin', 'corkscrewDive', 'warpDrive', 'backflipKick'], 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<number, string[]> = {
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<typeof kaplay>, color: string) {
if (color.startsWith('#')) {
try { return k.Color.fromHex(color) } catch { /* fall through */ }
}
// HSL / rgb / named color — render to a 1px canvas to extract RGB
const cv = document.createElement('canvas')
cv.width = 1; cv.height = 1
const cx = cv.getContext('2d')!
cx.fillStyle = color
cx.fillRect(0, 0, 1, 1)
const [r, g, b] = cx.getImageData(0, 0, 1, 1).data
return k.Color.fromArray([r, g, b])
}
export async function createFightScene(config: FightSceneConfig) {
const { canvas, botA, botB, arena } = config
const theme = ARENA_THEMES[arena] || ARENA_THEMES.localhost
const k = kaplay({
canvas,
width: canvas.width || 800,
height: canvas.height || 500,
background: theme.bg,
global: false,
scale: 1,
crisp: true,
texFilter: 'nearest',
})
const colorsA = getBotColors(botA.seed)
const colorsB = getBotColors(botB.seed)
let sheetA: string
let sheetB: string
try {
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization)
} catch (err) {
console.error('[FightScene] Failed to generate sprite for botA:', err)
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
}
try {
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization)
} catch (err) {
console.error('[FightScene] Failed to generate sprite for botB:', err)
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
}
// 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 })
// Human sprites (the bot owner's silly human avatar)
const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0))
const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0))
const humanSheetA = generateHumanSpriteSheet(botA.seed, botA.archetype || 'standard', colorsA.primary, colorsA.secondary, winRateA)
const humanSheetB = generateHumanSpriteSheet(botB.seed, botB.archetype || 'standard', colorsB.primary, colorsB.secondary, winRateB)
const humanAnims: Record<string, { from: number; to: number; loop: boolean; speed: number }> = {}
for (const [name, cfg] of Object.entries(HUMAN_ANIMATIONS)) {
humanAnims[name] = { from: cfg.row * HUMAN_MAX_FRAMES, to: cfg.row * HUMAN_MAX_FRAMES + cfg.frames - 1, loop: true, speed: 6 }
}
await Promise.all([
k.loadSprite('humanA', humanSheetA, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
k.loadSprite('humanB', humanSheetB, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
])
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<void> {
return new Promise(resolve => {
const proj = k.add([
k.circle(size),
k.pos(fromX, fromY),
k.color(safeColor(k,color)),
k.opacity(1),
k.z(15),
])
// Trail
const trail = setInterval(() => {
const t = k.add([
k.circle(size * 0.6),
k.pos(proj.pos.x, proj.pos.y),
k.color(safeColor(k,color)),
k.opacity(0.5),
k.z(14),
])
t.onUpdate(() => { t.opacity -= 3 * k.dt(); if (t.opacity <= 0) t.destroy() })
}, 30)
const dx = toX - fromX
const dy = toY - fromY
const dist = Math.sqrt(dx * dx + dy * dy)
const speed = 800
const dur = dist / speed
k.tween(0, 1, dur, (t) => {
proj.pos.x = fromX + dx * t
proj.pos.y = fromY + dy * t
}, k.easings.linear).then(() => {
clearInterval(trail)
proj.destroy()
spawnSparks(toX, toY, 8, color)
resolve()
})
})
}
function spawnBullet(fromX: number, fromY: number, toX: number, toY: number): Promise<void> {
return new Promise(resolve => {
const bullet = k.add([
k.rect(6, 2),
k.pos(fromX, fromY),
k.color(safeColor(k,'#ffee00')),
k.opacity(1),
k.z(15),
k.rotate(Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI),
])
const dur = 0.08 + Math.random() * 0.04
k.tween(0, 1, dur, (t) => {
bullet.pos.x = fromX + (toX - fromX) * t
bullet.pos.y = fromY + (toY - fromY) * t + (Math.random() - 0.5) * 4
}, k.easings.linear).then(() => {
bullet.destroy()
// Muzzle flash at impact
spawnSparks(toX, toY, 3, '#ffcc00')
resolve()
})
})
}
function spawnShockwave(x: number, y: number, color: string) {
const wave = k.add([
k.circle(5),
k.pos(x, y),
k.color(safeColor(k,color)),
k.opacity(0.7),
k.z(5),
k.scale(1),
])
k.tween(1, 15, 0.4, (v) => { wave.scaleTo(v, v * 0.3) }, k.easings.easeOutQuad)
k.tween(0.7, 0, 0.4, (v) => { wave.opacity = v }, k.easings.easeOutQuad).then(() => wave.destroy())
}
// === VISUAL CHAOS EFFECTS ===
// RGB split glitch — offsets red/blue channels using colored overlays
function glitchRGB(duration: number = 0.2) {
const layers = [
{ color: '#ff0000', dx: 4 + Math.random() * 6, dy: -2 },
{ color: '#0000ff', dx: -(4 + Math.random() * 6), dy: 2 },
]
for (const l of layers) {
const overlay = k.add([
k.rect(W, H), k.pos(l.dx, l.dy),
k.color(safeColor(k,l.color)), k.opacity(0.12), k.z(55),
])
overlay.onUpdate(() => {
overlay.pos.x = l.dx + (Math.random() - 0.5) * 4
overlay.pos.y = l.dy + (Math.random() - 0.5) * 2
})
setTimeout(() => { if (overlay.exists()) overlay.destroy() }, duration * 1000)
}
}
// Scanline glitch — horizontal bands flicker
function scanlineGlitch(duration: number = 0.3) {
const lines: 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) {}
// Arena-specific background decoration
function drawArenaDecor() {
const a = arena
const acc = theme.accent
// Parallax stars/particles in the sky
for (let i = 0; i < 40; i++) {
const star = k.add([
k.rect(1 + Math.random() * 2, 1 + Math.random() * 2),
k.pos(Math.random() * W, Math.random() * (GROUND_Y - 20)),
k.color(safeColor(k, i % 5 === 0 ? '#ffffff' : acc)),
k.opacity(0.08 + Math.random() * 0.2),
k.z(1),
])
const speed = 0.5 + Math.random() * 1.5
const baseOp = star.opacity
star.onUpdate(() => {
star.opacity = baseOp + Math.sin(k.time() * speed + i) * 0.08
})
}
if (a === 'datacenter' || a === 'localhost') {
// City skyline silhouette behind the racks
const buildingHeights = [0.55, 0.4, 0.65, 0.35, 0.7, 0.45, 0.6, 0.5, 0.72, 0.38, 0.58, 0.42]
for (let i = 0; i < buildingHeights.length; i++) {
const bw = W / buildingHeights.length + 4
const bh = GROUND_Y * buildingHeights[i]
const bx = i * (W / buildingHeights.length)
k.add([k.rect(bw, bh), k.pos(bx, GROUND_Y - bh), k.color(safeColor(k, '#060610')), k.opacity(0.5), k.z(0)])
for (let wy = 0; wy < bh - 10; wy += 8) {
for (let wx = 4; wx < bw - 6; wx += 7) {
const win = k.add([
k.rect(3, 3), k.pos(bx + wx, GROUND_Y - bh + 6 + wy),
k.color(safeColor(k, Math.random() > 0.4 ? '#223355' : '#ffcc44')),
k.opacity(0.15 + Math.random() * 0.2), k.z(0),
])
if (Math.random() < 0.15) {
const blink = 0.03 + Math.random() * 0.06
win.onUpdate(() => { win.opacity = Math.random() > blink ? win.opacity : (Math.random() > 0.5 ? 0.3 : 0.05) })
}
}
}
}
// Server rack wall
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 10; col++) {
const lx = 15 + col * (W / 10)
const ly = 15 + row * 55
const rackW = W / 10 - 8
const rackH = 48
k.add([k.rect(rackW, rackH), k.pos(lx, ly), k.color(safeColor(k, '#06060f')), k.opacity(0.6), k.z(1)])
k.add([k.rect(rackW, 1), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
k.add([k.rect(rackW, 1), k.pos(lx, ly + rackH), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
k.add([k.rect(1, rackH), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.3), k.z(1)])
for (let s = 0; s < 5; s++) {
k.add([k.rect(rackW - 6, 1), k.pos(lx + 3, ly + 8 + s * 8), k.color(safeColor(k, '#111122')), k.opacity(0.5), k.z(1)])
}
for (let led = 0; led < 3; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4),
k.color(safeColor(k, Math.random() > 0.3 ? '#00ff41' : '#ff2d2d')),
k.opacity(0.7), k.z(2),
])
const blinkRate = 0.02 + Math.random() * 0.08
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > blinkRate ? 0.7 : 0.15 })
}
if (Math.random() < 0.4) {
const actLed = k.add([
k.rect(2, 2), k.pos(lx + rackW - 8, ly + 4),
k.color(safeColor(k, '#ffaa00')), k.opacity(0.6), k.z(2),
])
actLed.onUpdate(() => { actLed.opacity = Math.random() > 0.5 ? 0.7 : 0.2 })
}
}
}
// Cable bundles
for (let c = 0; c < 6; c++) {
const cy = 3 + c * 2
k.add([k.rect(W, 2), k.pos(0, cy), k.color(safeColor(k, c % 3 === 0 ? '#111133' : c % 3 === 1 ? '#0a0a22' : '#0f0f2a')), k.opacity(0.5), k.z(1)])
}
// Scan line sweep
const scanLine = k.add([k.rect(W, 1), k.pos(0, 0), k.color(safeColor(k, '#00f0ff')), k.opacity(0.06), k.z(3)])
scanLine.onUpdate(() => { scanLine.pos.y = (scanLine.pos.y + 40 * k.dt()) % GROUND_Y })
} else if (a === 'gpu_graveyard') {
// Floating circuit board fragments
for (let i = 0; i < 16; i++) {
const cx = Math.random() * W
const cy = 20 + Math.random() * (GROUND_Y - 60)
const cw = 15 + Math.random() * 25
const ch = 10 + Math.random() * 18
const chip = k.add([
k.rect(cw, ch), k.pos(cx, cy),
k.color(safeColor(k, Math.random() > 0.5 ? '#1a2a1a' : '#0f1a0f')),
k.opacity(0.35), k.z(1), k.rotate(Math.random() * 45 - 22),
])
for (let t = 0; t < 3; t++) {
k.add([k.rect(cw * 0.6, 1), k.pos(cx + 2, cy + 3 + t * 4), k.color(safeColor(k, '#76b900')), k.opacity(0.2), k.z(1)])
}
k.add([k.circle(1.5), k.pos(cx + cw / 2, cy + ch / 2), k.color(safeColor(k, '#aabb44')), k.opacity(0.25), k.z(1)])
const baseY = cy
chip.onUpdate(() => { chip.pos.y = baseY + Math.sin(k.time() * 0.3 + i) * 5 })
}
// Dead GPU fans — some slowly spinning
for (const fx of [W * 0.15, W * 0.5, W * 0.85]) {
const fy = GROUND_Y - 30 - Math.random() * 40
k.add([k.circle(18), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.4), k.z(1)])
k.add([k.circle(5), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1)])
for (let b = 0; b < 4; b++) {
const ba = b * Math.PI / 2
const blade = k.add([k.rect(14, 2), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.3), k.z(1), k.rotate(ba * 180 / Math.PI)])
if (fx === W * 0.5) {
blade.onUpdate(() => { blade.angle += 8 * k.dt() })
}
}
}
// Sparking wires
for (let i = 0; i < 4; i++) {
const sx = W * (0.2 + i * 0.2)
const sy = 10 + Math.random() * 30
k.add([k.rect(1, 20 + Math.random() * 40), k.pos(sx, sy), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)])
const spark = k.add([k.circle(3), k.pos(sx, sy + 20), k.anchor('center'), k.color(safeColor(k, '#ffff00')), k.opacity(0), k.z(3)])
spark.onUpdate(() => { spark.opacity = Math.random() > 0.97 ? 0.6 : 0 })
}
// Smoke wisps rising
for (let i = 0; i < 6; i++) {
const smoke = k.add([
k.circle(8 + Math.random() * 12), k.pos(Math.random() * W, GROUND_Y - 5),
k.color(safeColor(k, '#1a1a1a')), k.opacity(0.15), k.z(2),
])
const bx = smoke.pos.x
smoke.onUpdate(() => {
smoke.pos.y -= 6 * k.dt()
smoke.pos.x = bx + Math.sin(k.time() + i) * 8
smoke.opacity *= 0.998
if (smoke.pos.y < 0) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.15 }
})
}
} else if (a === 'prompt_dungeon') {
// Stone wall bricks
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 12; col++) {
const brickW = W / 12 + (Math.random() - 0.5) * 10
const brickH = GROUND_Y / 5
const bx = col * (W / 12) + (row % 2 === 0 ? 0 : W / 24)
k.add([k.rect(brickW - 2, brickH - 2), k.pos(bx, row * brickH), k.color(safeColor(k, '#1a1520')), k.opacity(0.3), k.z(1)])
}
}
// Glowing runes with halos
for (let i = 0; i < 10; i++) {
const rx = 30 + Math.random() * (W - 60)
const ry = 20 + Math.random() * (GROUND_Y - 60)
const rune = k.add([
k.circle(6 + Math.random() * 8), k.pos(rx, ry),
k.color(safeColor(k, '#b83dff')), k.opacity(0.12), k.z(1),
])
k.add([k.circle(12 + Math.random() * 8), k.pos(rx, ry), k.color(safeColor(k, '#6622aa')), k.opacity(0.06), k.z(0)])
rune.onUpdate(() => { rune.opacity = 0.08 + Math.sin(k.time() * 0.8 + i * 0.7) * 0.08 })
}
// Torches with flickering flames and light cones
for (const tx of [W * 0.1, W * 0.35, W * 0.65, W * 0.9]) {
const ty = GROUND_Y * 0.3
k.add([k.rect(4, 20), k.pos(tx, ty), k.color(safeColor(k, '#553311')), k.opacity(0.5), k.z(1)])
const flame = k.add([k.circle(6), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.5), k.z(2)])
const flameGlow = k.add([k.circle(15), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.08), k.z(1)])
const lightCone = k.add([k.rect(30, GROUND_Y * 0.5), k.pos(tx - 13, ty + 20), k.color(safeColor(k, '#ff6600')), k.opacity(0.02), k.z(0)])
flame.onUpdate(() => {
flame.opacity = 0.4 + Math.sin(k.time() * 6 + tx) * 0.15
flame.pos.y = ty - 3 + Math.sin(k.time() * 8 + tx) * 1.5
flameGlow.opacity = 0.05 + Math.sin(k.time() * 6 + tx) * 0.04
lightCone.opacity = 0.015 + Math.sin(k.time() * 6 + tx) * 0.008
})
}
// Chains hanging from ceiling
for (let i = 0; i < 5; i++) {
const cx = W * (0.15 + i * 0.18)
const chainLen = 25 + Math.random() * 50
for (let s = 0; s < chainLen; s += 5) {
const link = k.add([k.rect(3, 4), k.pos(cx, s), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)])
const baseCx = cx
link.onUpdate(() => { link.pos.x = baseCx + Math.sin(k.time() * 0.4 + i + s * 0.1) * 3 })
}
}
// Ground fog
for (let i = 0; i < 8; i++) {
const fog = k.add([
k.circle(35 + Math.random() * 30), k.pos(Math.random() * W, GROUND_Y - 8),
k.color(safeColor(k, '#1f1a2a')), k.opacity(0.25), k.z(3),
])
const baseX = fog.pos.x
fog.onUpdate(() => { fog.pos.x = baseX + Math.sin(k.time() * 0.2 + i) * 18 })
}
// Dripping water
for (let i = 0; i < 3; i++) {
const dx = W * (0.25 + i * 0.25)
const drop = k.add([k.circle(1.5), k.pos(dx, 0), k.color(safeColor(k, '#4466aa')), k.opacity(0.3), k.z(2)])
drop.onUpdate(() => {
drop.pos.y += 50 * k.dt()
if (drop.pos.y > GROUND_Y) { drop.pos.y = 0; drop.opacity = 0.3 }
drop.opacity *= 0.997
})
}
} else if (a === 'the_cloud') {
// Layered cloud formations
for (let layer = 0; layer < 3; layer++) {
for (let i = 0; i < 4; i++) {
const cx = Math.random() * W
const cy = 20 + layer * 50 + Math.random() * 30
const cloudOp = 0.25 - layer * 0.05
for (let j = 0; j < 4 + Math.floor(Math.random() * 3); j++) {
const cloud = k.add([
k.circle(15 + Math.random() * 20),
k.pos(cx + j * 14 - 20, cy + (Math.random() - 0.5) * 12),
k.color(safeColor(k, layer === 0 ? '#1a2030' : '#151a28')),
k.opacity(cloudOp), k.z(1),
])
const bx = cloud.pos.x
cloud.onUpdate(() => { cloud.pos.x = bx + Math.sin(k.time() * (0.08 + layer * 0.04) + i) * (8 + layer * 4) })
}
}
}
// Data rain streams
for (let i = 0; i < 15; i++) {
const stream = k.add([
k.rect(1, 8 + Math.random() * 15), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, '#4488ff')), k.opacity(0.12), k.z(2),
])
stream.onUpdate(() => {
stream.pos.y += 30 * k.dt()
if (stream.pos.y > GROUND_Y) { stream.pos.y = -20; stream.pos.x = Math.random() * W }
})
}
// Lightning flashes
const lightning = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3)])
lightning.onUpdate(() => { lightning.opacity = Math.random() > 0.998 ? 0.06 : 0 })
// Rainbow arc (subtle)
const rainbowColors = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff']
for (let i = 0; i < rainbowColors.length; i++) {
k.add([
k.circle(120 + i * 6), k.pos(W * 0.3, GROUND_Y + 60), k.anchor('center'),
k.color(safeColor(k, rainbowColors[i])), k.opacity(0.03), k.z(0),
])
}
} else if (a === 'the_singularity') {
// Central swirling vortex with multiple rings
for (let ring = 0; ring < 4; ring++) {
const ringDots = 12 + ring * 6
for (let i = 0; i < ringDots; i++) {
const angle = (i / ringDots) * Math.PI * 2
const baseDist = 20 + ring * 30
const dot = k.add([
k.circle(1.5 + ring * 0.5), k.pos(W / 2, GROUND_Y * 0.4),
k.color(safeColor(k, ring === 0 ? '#ffffff' : ring === 1 ? '#ff44ff' : ring === 2 ? '#ff00ff' : '#aa00aa')),
k.opacity(0.3 - ring * 0.05), k.z(1),
])
dot.onUpdate(() => {
const a2 = angle + k.time() * (0.8 - ring * 0.15)
const d2 = baseDist + Math.sin(k.time() * 0.8 + i) * 12
dot.pos.x = W / 2 + Math.cos(a2) * d2
dot.pos.y = GROUND_Y * 0.4 + Math.sin(a2) * d2 * 0.5
})
}
}
// Central core with pulsing glow
const core = k.add([k.circle(12), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.15), k.z(1)])
k.add([k.circle(30), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0)])
k.add([k.circle(60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#880088')), k.opacity(0.03), k.z(0)])
core.onUpdate(() => { core.opacity = 0.1 + Math.sin(k.time() * 2) * 0.08 })
// Energy tendrils reaching outward
for (let i = 0; i < 8; i++) {
const tendril = k.add([
k.rect(2, 40 + Math.random() * 60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('top'),
k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0), k.rotate(i * 45),
])
tendril.onUpdate(() => { tendril.angle = i * 45 + k.time() * 15 })
}
// Warped space grid lines
for (let i = 0; i < 6; i++) {
const gridLine = k.add([
k.rect(W, 1), k.pos(0, GROUND_Y * (0.15 + i * 0.12)),
k.color(safeColor(k, '#440066')), k.opacity(0.08), k.z(0),
])
gridLine.onUpdate(() => { gridLine.pos.x = Math.sin(k.time() * 0.3 + i) * 10 - 5 })
}
} else if (a === 'silicon_valley_dojo') {
// Bamboo stalks with segments and leaves
for (const side of [0.04, 0.08, 0.12, 0.86, 0.91, 0.96]) {
const px = W * side
const segments = 5 + Math.floor(Math.random() * 3)
const segH = (GROUND_Y - 15) / segments
for (let s = 0; s < segments; s++) {
k.add([k.rect(5, segH - 2), k.pos(px, 15 + s * segH), k.color(safeColor(k, '#1a3a1a')), k.opacity(0.45), k.z(1)])
k.add([k.rect(7, 2), k.pos(px - 1, 15 + s * segH), k.color(safeColor(k, '#2a4a2a')), k.opacity(0.4), k.z(1)])
}
for (let l = 0; l < 3; l++) {
const ly = 20 + l * (GROUND_Y / 4)
const dir = Math.random() > 0.5 ? 1 : -1
const leaf = k.add([k.rect(18, 3), k.pos(px + dir * 4, ly), k.color(safeColor(k, '#22aa33')), k.opacity(0.2), k.z(1), k.rotate(dir * 25)])
leaf.onUpdate(() => { leaf.angle = dir * 25 + Math.sin(k.time() * 1.5 + l) * 3 })
k.add([k.rect(14, 2), k.pos(px + dir * 8, ly + 4), k.color(safeColor(k, '#1a8822')), k.opacity(0.15), k.z(1), k.rotate(dir * 35)])
}
}
// Zen garden raked lines
for (let i = 0; i < 8; i++) {
const gy = GROUND_Y + 5 + i * 8
k.add([k.rect(W * 0.6, 1), k.pos(W * 0.2, gy), k.color(safeColor(k, '#00ff41')), k.opacity(0.06), k.z(0)])
}
// Stone lanterns with warm glow
for (const lx of [W * 0.2, W * 0.8]) {
k.add([k.rect(10, 6), k.pos(lx, GROUND_Y - 20), k.color(safeColor(k, '#444444')), k.opacity(0.35), k.z(1)])
k.add([k.rect(6, 14), k.pos(lx + 2, GROUND_Y - 34), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)])
k.add([k.rect(12, 3), k.pos(lx - 1, GROUND_Y - 37), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)])
const glow = k.add([k.circle(8), k.pos(lx + 5, GROUND_Y - 27), k.anchor('center'), k.color(safeColor(k, '#ffaa33')), k.opacity(0.15), k.z(2)])
glow.onUpdate(() => { glow.opacity = 0.1 + Math.sin(k.time() * 2 + lx) * 0.06 })
}
// Cherry blossom petals falling
for (let i = 0; i < 12; i++) {
const petal = k.add([
k.circle(1.5 + Math.random()), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, Math.random() > 0.5 ? '#ffaacc' : '#ff88aa')), k.opacity(0.2), k.z(3),
])
const startX = petal.pos.x
petal.onUpdate(() => {
petal.pos.y += 8 * k.dt()
petal.pos.x = startX + Math.sin(k.time() * 1.2 + i) * 15
if (petal.pos.y > GROUND_Y) { petal.pos.y = -5 }
})
}
} else if (a === 'hacker_news' || a === 'stackoverflow_ruins') {
// Ruined building walls with cracks
for (let i = 0; i < 3; i++) {
const wallX = W * (0.15 + i * 0.3)
const wallH = GROUND_Y * (0.4 + Math.random() * 0.3)
k.add([k.rect(W * 0.2, wallH), k.pos(wallX, GROUND_Y - wallH), k.color(safeColor(k, '#1a1408')), k.opacity(0.3), k.z(0)])
for (let c = 0; c < 3; c++) {
k.add([k.rect(1, 10 + Math.random() * 15), k.pos(wallX + Math.random() * W * 0.18, GROUND_Y - wallH + Math.random() * wallH), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0), k.rotate(Math.random() * 30 - 15)])
}
}
// Code blocks floating
for (let i = 0; i < 14; i++) {
const bw = 25 + Math.random() * 60
const bh = 3 + Math.random() * 4
const bx = 15 + Math.random() * (W - 50)
const by = 15 + Math.random() * (GROUND_Y - 50)
k.add([k.rect(bw, bh), k.pos(bx, by), k.color(safeColor(k, acc)), k.opacity(0.06 + Math.random() * 0.06), k.z(1)])
if (Math.random() < 0.4) {
k.add([k.rect(2, bh), k.pos(bx - 4, by), k.color(safeColor(k, acc)), k.opacity(0.1), k.z(1)])
}
}
// Vote arrows
for (let i = 0; i < 5; i++) {
const ax = 20 + Math.random() * (W - 40)
const ay = 30 + Math.random() * (GROUND_Y - 70)
k.add([k.rect(4, 6), k.pos(ax, ay), k.color(safeColor(k, a === 'stackoverflow_ruins' ? '#f48024' : '#ff6600')), k.opacity(0.12), k.z(1)])
}
// Broken monitors with static
for (let i = 0; i < 3; i++) {
const mx = W * (0.2 + i * 0.3)
const my = GROUND_Y * 0.3 + Math.random() * 30
k.add([k.rect(22, 16), k.pos(mx, my), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.5), k.z(1)])
k.add([k.rect(20, 14), k.pos(mx + 1, my + 1), k.color(safeColor(k, '#111111')), k.opacity(0.4), k.z(1)])
const staticEl = k.add([k.rect(18, 12), k.pos(mx + 2, my + 2), k.color(safeColor(k, '#888888')), k.opacity(0), k.z(2)])
staticEl.onUpdate(() => { staticEl.opacity = Math.random() > 0.92 ? 0.08 : 0 })
}
// Scrolling text fragments
for (let i = 0; i < 4; i++) {
const txt = k.add([
k.rect(30 + Math.random() * 40, 2), k.pos(Math.random() * W, 20 + i * (GROUND_Y / 5)),
k.color(safeColor(k, acc)), k.opacity(0.08), k.z(2),
])
txt.onUpdate(() => {
txt.pos.x -= 12 * k.dt()
if (txt.pos.x < -60) txt.pos.x = W + 20
})
}
} else if (a === 'beach') {
// Sun with rays and glow
const sunX = W * 0.8, sunY = 35
k.add([k.circle(45), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.06), k.z(0)])
k.add([k.circle(30), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.1), k.z(0)])
const sun = k.add([k.circle(18), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.5), k.z(1)])
sun.onUpdate(() => { sun.opacity = 0.4 + Math.sin(k.time() * 0.5) * 0.1 })
for (let r = 0; r < 8; r++) {
const angle = r * Math.PI / 4
const ray = k.add([k.rect(50, 2), k.pos(sunX, sunY), k.anchor('left'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.08), k.z(0), k.rotate(angle * 180 / Math.PI)])
ray.onUpdate(() => { ray.opacity = 0.05 + Math.sin(k.time() * 0.8 + r) * 0.04 })
}
// Ocean with layered waves
k.add([k.rect(W, 25), k.pos(0, GROUND_Y - 35), k.color(safeColor(k, '#1a4488')), k.opacity(0.2), k.z(1)])
for (let layer = 0; layer < 4; layer++) {
for (let i = 0; i < 5; i++) {
const wave = k.add([
k.rect(W * 0.3, 3 + layer), k.pos(Math.random() * W, GROUND_Y - 22 - layer * 6 - i * 2),
k.color(safeColor(k, layer < 2 ? '#3377bb' : '#2266aa')), k.opacity(0.18 - layer * 0.03), k.z(2),
])
const baseX = wave.pos.x
wave.onUpdate(() => { wave.pos.x = baseX + Math.sin(k.time() * (0.6 + layer * 0.2) + i * 1.5) * (15 + layer * 5) })
}
}
// Surf foam at shore
for (let i = 0; i < 6; i++) {
const foam = k.add([
k.rect(15 + Math.random() * 25, 2), k.pos(Math.random() * W, GROUND_Y - 6 + Math.random() * 4),
k.color(safeColor(k, '#ffffff')), k.opacity(0.12), k.z(3),
])
const baseX = foam.pos.x
foam.onUpdate(() => { foam.pos.x = baseX + Math.sin(k.time() * 0.8 + i) * 8; foam.opacity = 0.06 + Math.sin(k.time() + i) * 0.06 })
}
// Palm trees with segmented trunks, coconuts, swaying fronds
for (const px of [W * 0.05, W * 0.14, W * 0.88, W * 0.95]) {
const lean = px < W / 2 ? 8 : -8
const height = 8 + Math.floor(Math.random() * 3)
for (let s = 0; s < height; s++) {
const sy = GROUND_Y - 10 - s * 12
const segW = 7 - s * 0.4
k.add([k.rect(segW, 13), k.pos(px + s * lean / height, sy), k.color(safeColor(k, s % 2 === 0 ? '#3a2510' : '#2f1f0d')), k.opacity(0.5), k.z(1)])
k.add([k.rect(segW + 1, 1), k.pos(px + s * lean / height - 0.5, sy + 6), k.color(safeColor(k, '#4a3520')), k.opacity(0.2), k.z(1)])
}
const topX = px + lean, topY = GROUND_Y - 10 - height * 12
for (let c = 0; c < 2 + Math.floor(Math.random() * 2); c++) {
k.add([k.circle(2.5), k.pos(topX + (c - 1) * 4, topY + 8), k.anchor('center'), k.color(safeColor(k, '#553311')), k.opacity(0.4), k.z(1)])
}
for (let f = 0; f < 7; f++) {
const angle = -70 + f * 22
const frond = k.add([k.rect(35 + Math.random() * 10, 3), k.pos(topX, topY), k.anchor('left'), k.color(safeColor(k, f % 2 === 0 ? '#1a6600' : '#228800')), k.opacity(0.35), k.z(1), k.rotate(angle)])
frond.onUpdate(() => { frond.angle = angle + Math.sin(k.time() * 0.8 + f) * 3 })
}
}
// Seagulls drifting
for (let i = 0; i < 4; i++) {
const bx = W * 0.2 + Math.random() * W * 0.5
const by = 12 + Math.random() * 25
const wing1 = k.add([k.rect(6, 1), k.pos(bx, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(-15)])
const wing2 = k.add([k.rect(6, 1), k.pos(bx + 5, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(15)])
const startBx = bx
wing1.onUpdate(() => { wing1.pos.x = startBx + Math.sin(k.time() * 0.2 + i) * 30; wing2.pos.x = wing1.pos.x + 5 })
}
// Beach umbrella
const umbX = W * 0.35
k.add([k.rect(2, 30), k.pos(umbX, GROUND_Y - 30), k.color(safeColor(k, '#aa8844')), k.opacity(0.4), k.z(1)])
k.add([k.circle(16), k.pos(umbX + 1, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k, '#ff4444')), k.opacity(0.2), k.z(1)])
k.add([k.circle(14), k.pos(umbX + 1, GROUND_Y - 31), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.08), k.z(1)])
} else if (a === 'desert') {
// Layered sand dunes
for (let d = 0; d < 5; d++) {
const dx = W * (0.1 + d * 0.2) + (Math.random() - 0.5) * 40
const dr = 50 + d * 15 + Math.random() * 20
k.add([k.circle(dr), k.pos(dx, GROUND_Y + dr * 0.6), k.anchor('center'), k.color(safeColor(k, '#aa8844')), k.opacity(0.15 + d * 0.03), k.z(1)])
}
// Road with dashed center line
k.add([k.rect(W * 0.25, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#222222')), k.opacity(0.15), k.z(0)])
k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)])
k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.63, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)])
for (let d = 0; d < 8; d++) {
k.add([k.rect(2, 8), k.pos(W * 0.505, GROUND_Y * 0.32 + d * 16), k.color(safeColor(k, '#ffff44')), k.opacity(0.1), k.z(0)])
}
// Cacti with arms and flowers
for (const cx of [W * 0.08, W * 0.25, W * 0.75, W * 0.9]) {
const ch = 35 + Math.random() * 20
k.add([k.rect(7, ch), k.pos(cx, GROUND_Y - ch), k.color(safeColor(k, '#226622')), k.opacity(0.45), k.z(1)])
const armY = GROUND_Y - ch * 0.6
k.add([k.rect(12, 5), k.pos(cx - 12, armY), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
k.add([k.rect(5, 15), k.pos(cx - 12, armY - 15), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
if (Math.random() > 0.3) {
const armY2 = GROUND_Y - ch * 0.4
k.add([k.rect(10, 5), k.pos(cx + 7, armY2), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
k.add([k.rect(5, 12), k.pos(cx + 12, armY2 - 12), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
}
if (Math.random() > 0.5) {
k.add([k.circle(2.5), k.pos(cx + 3, GROUND_Y - ch - 2), k.anchor('center'), k.color(safeColor(k, '#ff4488')), k.opacity(0.3), k.z(2)])
}
}
// Tumbleweeds rolling
for (let i = 0; i < 3; i++) {
const tumble = k.add([k.circle(6 + Math.random() * 4), k.pos(-20 - i * 100, GROUND_Y - 8 - Math.random() * 5), k.color(safeColor(k, '#886644')), k.opacity(0.3), k.z(2)])
const speed = 15 + Math.random() * 15
tumble.onUpdate(() => { tumble.pos.x = ((tumble.pos.x + speed * k.dt()) % (W + 60)) - 30 })
}
// Heat shimmer
for (let i = 0; i < 4; i++) {
const shimmer = k.add([k.rect(W, 1), k.pos(0, GROUND_Y - 3 - i * 3), k.color(safeColor(k, '#ff8800')), k.opacity(0.06), k.z(2)])
shimmer.onUpdate(() => { shimmer.opacity = 0.03 + Math.sin(k.time() * 3 + i * 2) * 0.03 })
}
// Vulture silhouettes circling
for (let i = 0; i < 2; i++) {
const vx = W * 0.4 + i * W * 0.2
const vy = 25 + i * 15
const v1 = k.add([k.rect(8, 1), k.pos(vx, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(-10)])
const v2 = k.add([k.rect(8, 1), k.pos(vx + 7, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(10)])
const startVx = vx
v1.onUpdate(() => {
const cx = startVx + Math.cos(k.time() * 0.15 + i) * 40
const cy = vy + Math.sin(k.time() * 0.15 + i) * 15
v1.pos.x = cx; v1.pos.y = cy; v2.pos.x = cx + 7; v2.pos.y = cy
})
}
} else if (a === 'forest') {
// Layered trees (3 depth layers)
for (let layer = 0; layer < 3; layer++) {
const treeCount = layer === 0 ? 14 : layer === 1 ? 8 : 5
const treeOp = layer === 0 ? 0.15 : layer === 1 ? 0.3 : 0.4
for (let i = 0; i < treeCount; i++) {
const tx = 10 + (i / treeCount) * (W - 20) + (Math.random() - 0.5) * 15
const th = (25 + Math.random() * 35) * (layer === 0 ? 0.6 : layer === 1 ? 0.85 : 1)
const trunkW = layer === 0 ? 4 : layer === 1 ? 6 : 8
k.add([k.rect(trunkW, th), k.pos(tx, GROUND_Y - th), k.color(safeColor(k, layer === 0 ? '#0f1a08' : layer === 1 ? '#152010' : '#1a2a10')), k.opacity(treeOp), k.z(layer)])
for (let c = 0; c < 3 + layer; c++) {
const cr = (10 + Math.random() * 14) * (layer === 0 ? 0.6 : layer === 1 ? 0.8 : 1)
k.add([
k.circle(cr), k.pos(tx + trunkW / 2 + (Math.random() - 0.5) * 10, GROUND_Y - th - cr * 0.5 + c * 4),
k.anchor('center'), k.color(safeColor(k, layer === 0 ? '#0a1a08' : layer === 1 ? '#122a0e' : '#1a3a10')), k.opacity(treeOp * 0.8), k.z(layer),
])
}
}
}
// Undergrowth
for (let i = 0; i < 10; i++) {
k.add([k.circle(6 + Math.random() * 8), k.pos(Math.random() * W, GROUND_Y - 3), k.anchor('bot'), k.color(safeColor(k, '#1a3a10')), k.opacity(0.3), k.z(3)])
}
// Mushrooms
for (let i = 0; i < 5; i++) {
const mx = Math.random() * W
k.add([k.rect(2, 5), k.pos(mx, GROUND_Y - 5), k.color(safeColor(k, '#aa8866')), k.opacity(0.25), k.z(3)])
k.add([k.circle(4), k.pos(mx + 1, GROUND_Y - 6), k.anchor('center'), k.color(safeColor(k, Math.random() > 0.5 ? '#cc3333' : '#ffaa22')), k.opacity(0.2), k.z(3)])
}
// Fireflies
for (let i = 0; i < 15; i++) {
const ff = k.add([
k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 50)),
k.color(safeColor(k, '#ccff44')), k.opacity(0), k.z(3),
])
const bx = ff.pos.x, by = ff.pos.y
ff.onUpdate(() => {
ff.opacity = Math.max(0, Math.sin(k.time() * 1.5 + i * 1.8) * 0.5)
ff.pos.x = bx + Math.sin(k.time() * 0.5 + i) * 15
ff.pos.y = by + Math.cos(k.time() * 0.4 + i) * 10
})
}
// Light rays through canopy
for (let i = 0; i < 5; i++) {
const rx = W * (0.15 + i * 0.18) + (Math.random() - 0.5) * 20
const ray = k.add([k.rect(8, GROUND_Y * 0.7), k.pos(rx, 0), k.color(safeColor(k, '#aaff44')), k.opacity(0.03), k.z(2), k.rotate(3 - Math.random() * 6)])
ray.onUpdate(() => { ray.opacity = 0.02 + Math.sin(k.time() * 0.3 + i) * 0.015 })
}
// Fallen log
k.add([k.rect(45, 6), k.pos(W * 0.55, GROUND_Y - 4), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(3), k.rotate(3)])
} else if (a === 'jungle') {
// Dense vine canopy
for (let i = 0; i < 12; i++) {
const vx = 15 + i * (W / 12) + (Math.random() - 0.5) * 12
const vineLen = 30 + Math.random() * 90
for (let s = 0; s < vineLen; s += 4) {
const vineEl = k.add([k.rect(2, 5), k.pos(vx + Math.sin(s * 0.15) * 4, s), k.color(safeColor(k, '#1a4a10')), k.opacity(0.3), k.z(1)])
if (Math.random() < 0.08) {
const basePx = vineEl.pos.x
vineEl.onUpdate(() => { vineEl.pos.x = basePx + Math.sin(k.time() * 0.5 + s) * 2 })
}
}
for (let l = 0; l < 2; l++) {
k.add([k.circle(5 + Math.random() * 4), k.pos(vx + (Math.random() - 0.5) * 8, vineLen + l * 6), k.color(safeColor(k, '#22aa22')), k.opacity(0.22), k.z(1)])
}
}
// Large tropical leaves at edges
for (const side of [0, W - 30]) {
for (let l = 0; l < 4; l++) {
const ly = 15 + l * (GROUND_Y / 5)
const leaf = k.add([k.rect(35, 8), k.pos(side, ly), k.color(safeColor(k, '#1a5510')), k.opacity(0.25), k.z(3), k.rotate(side === 0 ? 15 : -15)])
leaf.onUpdate(() => { leaf.angle = (side === 0 ? 15 : -15) + Math.sin(k.time() * 0.6 + l) * 2 })
}
}
// Exotic flowers
for (let i = 0; i < 6; i++) {
const fx = Math.random() * W
const fy = 20 + Math.random() * (GROUND_Y - 40)
k.add([k.circle(3), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, ['#ff4488', '#ff8844', '#ffcc00', '#ff44ff', '#44ffcc', '#ff2222'][i])), k.opacity(0.2), k.z(2)])
}
// Ancient stone ruins peeking through
const ruinX = W * 0.5
k.add([k.rect(8, 40), k.pos(ruinX, GROUND_Y - 38), k.color(safeColor(k, '#444444')), k.opacity(0.15), k.z(0)])
k.add([k.rect(8, 35), k.pos(ruinX + 25, GROUND_Y - 33), k.color(safeColor(k, '#444444')), k.opacity(0.12), k.z(0)])
k.add([k.rect(35, 5), k.pos(ruinX - 2, GROUND_Y - 40), k.color(safeColor(k, '#444444')), k.opacity(0.1), k.z(0)])
// Thick ground fog
for (let i = 0; i < 10; i++) {
const mist = k.add([
k.circle(40 + Math.random() * 35), k.pos(Math.random() * W, GROUND_Y - 12),
k.color(safeColor(k, '#1a3818')), k.opacity(0.22), k.z(3),
])
const bx = mist.pos.x
mist.onUpdate(() => { mist.pos.x = bx + Math.sin(k.time() * 0.12 + i) * 18 })
}
// Butterfly particles
for (let i = 0; i < 4; i++) {
const bf = k.add([
k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 60)),
k.color(safeColor(k, ['#4488ff', '#ff8844', '#ffcc00', '#ff44ff'][i])), k.opacity(0.25), k.z(3),
])
const bfx = bf.pos.x, bfy = bf.pos.y
bf.onUpdate(() => {
bf.pos.x = bfx + Math.sin(k.time() * 1.2 + i * 3) * 30
bf.pos.y = bfy + Math.cos(k.time() * 0.8 + i * 2) * 20
})
}
} else if (a === 'outer_space') {
// Dense star field
for (let i = 0; i < 100; i++) {
const star = k.add([
k.circle(0.5 + Math.random() * 1.5),
k.pos(Math.random() * W, Math.random() * H),
k.color(safeColor(k, i % 8 === 0 ? '#ffccaa' : i % 5 === 0 ? '#aaaaff' : '#ffffff')),
k.opacity(0.15 + Math.random() * 0.4), k.z(1),
])
star.onUpdate(() => { star.opacity = 0.12 + Math.sin(k.time() * (0.8 + Math.random()) + i) * 0.15 })
}
// Nebula clouds
const nebulaColors = ['#4400aa', '#aa0066', '#0044aa', '#006644', '#660044']
for (let i = 0; i < 5; i++) {
for (let blob = 0; blob < 4; blob++) {
k.add([
k.circle(30 + Math.random() * 50),
k.pos(W * (0.1 + i * 0.2) + blob * 18, H * 0.25 + Math.random() * 60 + blob * 12),
k.color(safeColor(k, nebulaColors[i % nebulaColors.length])),
k.opacity(0.04 + blob * 0.012), k.z(0),
])
}
}
// Floating asteroids
for (let i = 0; i < 6; i++) {
const ast = k.add([
k.circle(4 + Math.random() * 10),
k.pos(Math.random() * W, 15 + Math.random() * (GROUND_Y - 30)),
k.color(safeColor(k, Math.random() > 0.5 ? '#555555' : '#3a3a3a')), k.opacity(0.35), k.z(1),
])
k.add([k.circle(2), k.pos(ast.pos.x + 2, ast.pos.y - 1), k.color(safeColor(k, '#2a2a2a')), k.opacity(0.2), k.z(1)])
const baseX = ast.pos.x, baseY = ast.pos.y
ast.onUpdate(() => {
ast.pos.x = baseX + Math.sin(k.time() * 0.15 + i * 2) * 18
ast.pos.y = baseY + Math.cos(k.time() * 0.12 + i) * 12
})
}
// Distant planet with rings
const planetX = W * 0.75, planetY = GROUND_Y * 0.25
k.add([k.circle(22), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#334466')), k.opacity(0.25), k.z(0)])
k.add([k.circle(24), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#223344')), k.opacity(0.1), k.z(0)])
for (let r = 0; r < 3; r++) {
k.add([k.rect(50 + r * 8, 2), k.pos(planetX - 25 - r * 4, planetY - 1 + r), k.color(safeColor(k, '#556688')), k.opacity(0.1 - r * 0.02), k.z(0), k.rotate(15)])
}
// Shooting stars
for (let i = 0; i < 2; i++) {
const shootStar = k.add([
k.rect(12, 1), k.pos(-20, Math.random() * GROUND_Y * 0.5),
k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(2), k.rotate(30),
])
let timer = Math.random() * 10
shootStar.onUpdate(() => {
timer -= k.dt()
if (timer <= 0) {
shootStar.pos.x = -20
shootStar.pos.y = Math.random() * GROUND_Y * 0.4
shootStar.opacity = 0.5
timer = 5 + Math.random() * 10
}
if (shootStar.opacity > 0) {
shootStar.pos.x += 200 * k.dt()
shootStar.pos.y += 80 * k.dt()
shootStar.opacity -= 0.8 * k.dt()
}
})
}
} else if (a === 'colosseum') {
// Roman arches along the back
for (let i = 0; i < 8; i++) {
const ax = 20 + i * (W / 8)
const aw = W / 8 - 8
k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)])
k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax + aw - 6, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)])
k.add([k.rect(aw, 6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#4a3a20')), k.opacity(0.4), k.z(1)])
k.add([k.rect(aw - 12, GROUND_Y * 0.5), k.pos(ax + 6, GROUND_Y * 0.25), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0)])
k.add([k.rect(10, 3), k.pos(ax - 2, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)])
k.add([k.rect(10, 3), k.pos(ax + aw - 10, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)])
}
// Torch brackets on pillars
for (let i = 0; i < 4; i++) {
const tx = W * (0.15 + i * 0.25)
const ty = GROUND_Y * 0.35
k.add([k.rect(3, 12), k.pos(tx, ty), k.color(safeColor(k, '#554422')), k.opacity(0.4), k.z(2)])
const flame = k.add([k.circle(5), k.pos(tx + 1, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.4), k.z(2)])
flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + i * 2) * 0.15 })
}
// Crowd silhouettes in upper tiers
for (let row = 0; row < 2; row++) {
for (let i = 0; i < 20; i++) {
k.add([
k.circle(2 + Math.random()), k.pos(15 + i * (W / 20), GROUND_Y * 0.08 + row * 12),
k.color(safeColor(k, '#2a1a10')), k.opacity(0.2), k.z(0),
])
}
}
// Crumbled stone debris
for (let i = 0; i < 8; i++) {
k.add([
k.rect(5 + Math.random() * 10, 3 + Math.random() * 6),
k.pos(Math.random() * W, GROUND_Y - 5 - Math.random() * 10),
k.color(safeColor(k, '#5a4a30')), k.opacity(0.25), k.z(2), k.rotate(Math.random() * 40 - 20),
])
}
// Sand dust particles
for (let i = 0; i < 6; i++) {
const dust = k.add([
k.circle(2 + Math.random() * 3), k.pos(Math.random() * W, GROUND_Y - 15 - Math.random() * 20),
k.color(safeColor(k, '#c0a060')), k.opacity(0.1), k.z(3),
])
const dx = dust.pos.x
dust.onUpdate(() => { dust.pos.x = dx + Math.sin(k.time() * 0.5 + i) * 12; dust.opacity = 0.05 + Math.sin(k.time() + i) * 0.05 })
}
} else if (a === 'haunted_mansion') {
// Gothic windows with moonlight
for (let i = 0; i < 5; i++) {
const wx = W * (0.1 + i * 0.2)
const wy = GROUND_Y * 0.2
k.add([k.rect(20, 35), k.pos(wx, wy), k.color(safeColor(k, '#0a0510')), k.opacity(0.4), k.z(1)])
k.add([k.rect(22, 1), k.pos(wx - 1, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)])
k.add([k.rect(22, 1), k.pos(wx - 1, wy + 35), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)])
k.add([k.rect(1, 35), k.pos(wx + 10, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)])
k.add([k.rect(20, 1), k.pos(wx, wy + 17), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)])
const glow = k.add([k.rect(16, 30), k.pos(wx + 2, wy + 3), k.color(safeColor(k, '#8844cc')), k.opacity(0.06), k.z(1)])
glow.onUpdate(() => { glow.opacity = 0.04 + Math.sin(k.time() * 0.5 + i) * 0.03 })
}
// Moon behind a window
k.add([k.circle(12), k.pos(W * 0.5 + 10, GROUND_Y * 0.26), k.anchor('center'), k.color(safeColor(k, '#ccccdd')), k.opacity(0.15), k.z(0)])
// Cobwebs
for (const cx of [15, W - 35]) {
for (let strand = 0; strand < 5; strand++) {
k.add([k.rect(20 + strand * 5, 1), k.pos(cx, 3 + strand * 4), k.color(safeColor(k, '#666666')), k.opacity(0.1), k.z(1), k.rotate(cx < W / 2 ? 28 + strand * 5 : -28 - strand * 5)])
}
}
// Candelabras
for (const cx of [W * 0.25, W * 0.75]) {
k.add([k.rect(2, 20), k.pos(cx, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)])
k.add([k.rect(12, 2), k.pos(cx - 5, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)])
for (let c = 0; c < 3; c++) {
const candleX = cx - 5 + c * 5
const cFlame = k.add([k.circle(2.5), k.pos(candleX + 1, GROUND_Y - 23), k.anchor('center'), k.color(safeColor(k, '#ffaa44')), k.opacity(0.4), k.z(2)])
cFlame.onUpdate(() => { cFlame.opacity = 0.3 + Math.sin(k.time() * 4 + c + cx) * 0.15 })
}
}
// Floating ghost particles
for (let i = 0; i < 8; i++) {
const ghost = k.add([
k.circle(3 + Math.random() * 3), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, '#aabbcc')), k.opacity(0), k.z(2),
])
const gx = ghost.pos.x, gy = ghost.pos.y
ghost.onUpdate(() => {
ghost.opacity = Math.max(0, Math.sin(k.time() * 0.6 + i * 2) * 0.15)
ghost.pos.x = gx + Math.sin(k.time() * 0.3 + i) * 20
ghost.pos.y = gy + Math.cos(k.time() * 0.25 + i) * 15
})
}
// Bats flying
for (let i = 0; i < 3; i++) {
const bat = k.add([k.rect(5, 2), k.pos(Math.random() * W, 15 + Math.random() * 40), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(2)])
const bx = bat.pos.x, by = bat.pos.y
bat.onUpdate(() => {
bat.pos.x = bx + Math.sin(k.time() * 1.5 + i * 3) * 40
bat.pos.y = by + Math.cos(k.time() * 2 + i * 2) * 10
})
}
} else if (a === 'concert_hall') {
// Stage lights with sweeping cones
const lightColors = ['#ff2288', '#22ff88', '#4488ff', '#ffaa22', '#ff44ff', '#44ffff']
for (let i = 0; i < 6; i++) {
const lx = W * (0.1 + i * 0.15)
const cone = k.add([
k.rect(30, GROUND_Y * 0.7), k.pos(lx, 10), k.anchor('top'),
k.color(safeColor(k, lightColors[i])), k.opacity(0.04), k.z(2),
])
k.add([k.circle(4), k.pos(lx + 15, 8), k.anchor('center'), k.color(safeColor(k, lightColors[i])), k.opacity(0.4), k.z(2)])
const baseAngle = (i - 3) * 5
cone.onUpdate(() => {
cone.opacity = 0.02 + Math.sin(k.time() * 1.5 + i * 1.2) * 0.025
cone.angle = baseAngle + Math.sin(k.time() * 0.4 + i) * 8
})
}
// Speaker stacks
for (const sx of [10, W - 30]) {
for (let s = 0; s < 4; s++) {
k.add([k.rect(20, 18), k.pos(sx, GROUND_Y - 20 - s * 20), k.color(safeColor(k, '#111111')), k.opacity(0.5), k.z(1)])
k.add([k.circle(6), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.4), k.z(1)])
const speakerCone = k.add([k.circle(4), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1)])
speakerCone.onUpdate(() => { speakerCone.scale = k.vec2(1 + Math.sin(k.time() * 8 + s) * 0.05) })
}
}
// Crowd silhouettes
for (let i = 0; i < 30; i++) {
const cx = 5 + i * (W / 30)
const ch = 6 + Math.random() * 4
k.add([k.circle(2.5), k.pos(cx, GROUND_Y + 8), k.anchor('center'), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(3)])
k.add([k.rect(4, ch), k.pos(cx - 2, GROUND_Y + 10), k.color(safeColor(k, '#111111')), k.opacity(0.25), k.z(3)])
}
// Smoke machine haze
for (let i = 0; i < 6; i++) {
const haze = k.add([
k.circle(30 + Math.random() * 25), k.pos(Math.random() * W, GROUND_Y - 8),
k.color(safeColor(k, '#20102a')), k.opacity(0.18), k.z(3),
])
const hx = haze.pos.x
haze.onUpdate(() => { haze.pos.x = hx + Math.sin(k.time() * 0.15 + i) * 15 })
}
// Laser beam sweep
const laser = k.add([k.rect(1, GROUND_Y), k.pos(W / 2, 0), k.color(safeColor(k, '#00ff00')), k.opacity(0.06), k.z(2)])
laser.onUpdate(() => { laser.pos.x = W / 2 + Math.sin(k.time() * 0.8) * (W * 0.4) })
} else if (a === 'sports_arena') {
// Stadium seating tiers with crowd
for (let tier = 0; tier < 5; tier++) {
const ty = GROUND_Y * 0.1 + tier * (GROUND_Y * 0.13)
k.add([k.rect(W, GROUND_Y * 0.1), k.pos(0, ty), k.color(safeColor(k, tier % 2 === 0 ? '#1a2a4a' : '#152040')), k.opacity(0.3), k.z(1)])
for (let i = 0; i < 18; i++) {
k.add([
k.circle(1.5 + Math.random()), k.pos(15 + i * (W / 18), ty + GROUND_Y * 0.04),
k.color(safeColor(k, ['#cc3333', '#3333cc', '#33cc33', '#cccc33', '#ffffff'][Math.floor(Math.random() * 5)])),
k.opacity(0.12), k.z(1),
])
}
}
// Floodlights
for (const fx of [W * 0.05, W * 0.95]) {
k.add([k.rect(3, GROUND_Y * 0.7), k.pos(fx, GROUND_Y * 0.1), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(2)])
const light = k.add([k.circle(5), k.pos(fx + 1, GROUND_Y * 0.08), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.5), k.z(2)])
k.add([k.rect(40, GROUND_Y * 0.6), k.pos(fx - 18, GROUND_Y * 0.12), k.color(safeColor(k, '#ffffff')), k.opacity(0.015), k.z(0)])
light.onUpdate(() => { light.opacity = 0.4 + Math.sin(k.time() * 0.5 + fx) * 0.1 })
}
// Scoreboard
k.add([k.rect(50, 18), k.pos(W / 2 - 25, 8), k.color(safeColor(k, '#0a0a20')), k.opacity(0.5), k.z(2)])
k.add([k.rect(48, 16), k.pos(W / 2 - 24, 9), k.color(safeColor(k, '#111133')), k.opacity(0.4), k.z(2)])
const scoreGlow = k.add([k.rect(20, 8), k.pos(W / 2 - 10, 13), k.color(safeColor(k, '#ff3333')), k.opacity(0.12), k.z(2)])
scoreGlow.onUpdate(() => { scoreGlow.opacity = 0.08 + Math.sin(k.time() * 2) * 0.05 })
// Camera flashes
for (let i = 0; i < 5; i++) {
const flash = k.add([
k.circle(2), k.pos(Math.random() * W, GROUND_Y * (0.15 + Math.random() * 0.5)),
k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3),
])
flash.onUpdate(() => { flash.opacity = Math.random() > 0.995 ? 0.4 : 0 })
}
} else if (a === 'kitchen_stadium') {
// Tiled wall
for (let row = 0; row < 6; row++) {
for (let col = 0; col < 14; col++) {
k.add([
k.rect(W / 14 - 1, GROUND_Y / 6 - 1), k.pos(col * (W / 14), row * (GROUND_Y / 6)),
k.color(safeColor(k, (row + col) % 2 === 0 ? '#1a1210' : '#1e1614')), k.opacity(0.2), k.z(0),
])
}
}
// Stove with burners
k.add([k.rect(45, 35), k.pos(W * 0.06, GROUND_Y - 35), k.color(safeColor(k, '#1a1010')), k.opacity(0.45), k.z(1)])
k.add([k.rect(43, 2), k.pos(W * 0.06 + 1, GROUND_Y - 35), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)])
for (let b = 0; b < 4; b++) {
const flame = k.add([k.circle(4), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 37), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.4), k.z(2)])
const innerFlame = k.add([k.circle(2), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 38), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.3), k.z(2)])
flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + b) * 0.15; innerFlame.opacity = 0.2 + Math.sin(k.time() * 7 + b) * 0.1 })
}
// Hanging pots and pans
k.add([k.rect(W * 0.5, 2), k.pos(W * 0.2, 8), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1)])
for (let i = 0; i < 6; i++) {
const px = W * (0.22 + i * 0.08)
const hookLen = 12 + Math.random() * 10
k.add([k.rect(1, hookLen), k.pos(px, 10), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)])
k.add([k.circle(7 + Math.random() * 4), k.pos(px, 10 + hookLen + 5), k.anchor('center'), k.color(safeColor(k, i % 2 === 0 ? '#555555' : '#664422')), k.opacity(0.25), k.z(1)])
}
// Knife rack
k.add([k.rect(3, 25), k.pos(W * 0.85, GROUND_Y * 0.3), k.color(safeColor(k, '#553322')), k.opacity(0.35), k.z(1)])
for (let i = 0; i < 4; i++) {
k.add([k.rect(2, 10 + Math.random() * 5), k.pos(W * 0.85 + 1, GROUND_Y * 0.32 + i * 5), k.color(safeColor(k, '#aaaaaa')), k.opacity(0.2), k.z(1)])
}
// Steam rising
for (let i = 0; i < 4; i++) {
const steam = k.add([
k.circle(5 + Math.random() * 5), k.pos(W * 0.06 + 10 + i * 10, GROUND_Y - 40),
k.color(safeColor(k, '#ffffff')), k.opacity(0.06), k.z(3),
])
const bx = steam.pos.x
steam.onUpdate(() => {
steam.pos.y -= 10 * k.dt()
steam.pos.x = bx + Math.sin(k.time() * 2 + i) * 4
steam.opacity *= 0.995
if (steam.pos.y < GROUND_Y - 80) { steam.pos.y = GROUND_Y - 40; steam.opacity = 0.06 }
})
}
} else if (a === 'junkyard') {
// Rusted crane silhouette
k.add([k.rect(6, GROUND_Y * 0.7), k.pos(W * 0.85, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.3), k.z(0)])
k.add([k.rect(W * 0.3, 4), k.pos(W * 0.6, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.25), k.z(0)])
const cable = k.add([k.rect(1, 35), k.pos(W * 0.65, GROUND_Y * 0.12), k.color(safeColor(k, '#555555')), k.opacity(0.2), k.z(0)])
cable.onUpdate(() => { cable.pos.x = W * 0.65 + Math.sin(k.time() * 0.3) * 5 })
// Piles of scrap
for (let pile = 0; pile < 8; pile++) {
const px = Math.random() * W
const py = GROUND_Y - 8
for (let j = 0; j < 7; j++) {
k.add([
k.rect(8 + Math.random() * 18, 4 + Math.random() * 10),
k.pos(px + (Math.random() - 0.5) * 25, py - j * 6 - Math.random() * 5),
k.color(safeColor(k, ['#554433', '#665544', '#443322', '#776655', '#332211', '#887766', '#553322'][j % 7])),
k.opacity(0.3), k.z(1), k.rotate(Math.random() * 40 - 20),
])
}
}
// Old car silhouette
k.add([k.rect(35, 15), k.pos(W * 0.15, GROUND_Y - 15), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.3), k.z(1)])
k.add([k.rect(25, 10), k.pos(W * 0.15 + 5, GROUND_Y - 25), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.25), k.z(1)])
k.add([k.circle(5), k.pos(W * 0.15 + 6, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)])
k.add([k.circle(5), k.pos(W * 0.15 + 29, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)])
// Sparking wires
for (let i = 0; i < 3; i++) {
const spark = k.add([
k.circle(2), k.pos(W * (0.3 + i * 0.2), GROUND_Y - 20 - Math.random() * 30),
k.color(safeColor(k, '#ffaa00')), k.opacity(0), k.z(3),
])
spark.onUpdate(() => { spark.opacity = Math.random() > 0.96 ? 0.5 : 0 })
}
// Smoke/steam wisps
for (let i = 0; i < 4; i++) {
const smoke = k.add([
k.circle(10 + Math.random() * 10), k.pos(Math.random() * W, GROUND_Y - 5),
k.color(safeColor(k, '#333322')), k.opacity(0.12), k.z(2),
])
const sx = smoke.pos.x
smoke.onUpdate(() => {
smoke.pos.y -= 5 * k.dt()
smoke.pos.x = sx + Math.sin(k.time() * 0.5 + i) * 6
if (smoke.pos.y < GROUND_Y - 60) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.12 }
})
}
} else if (a === 'meme_dimension') {
// Floating emoji shapes (pulsing)
for (let i = 0; i < 12; i++) {
const emoji = k.add([
k.circle(6 + Math.random() * 6), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, ['#ffcc00', '#ff4444', '#44ff44', '#4444ff', '#ff44ff', '#44ffff'][i % 6])), k.opacity(0.12), k.z(1),
])
const bx = emoji.pos.x, by = emoji.pos.y
emoji.onUpdate(() => {
emoji.pos.x = bx + Math.sin(k.time() * 0.4 + i) * 15
emoji.pos.y = by + Math.cos(k.time() * 0.3 + i) * 10
emoji.scale = k.vec2(1 + Math.sin(k.time() * 1.5 + i) * 0.15)
})
}
// Glitch bands
for (let i = 0; i < 8; i++) {
const band = k.add([
k.rect(W, 2 + Math.random() * 4), k.pos(0, Math.random() * GROUND_Y),
k.color(safeColor(k, Math.random() > 0.5 ? '#ff44ff' : '#44ffff')), k.opacity(0), k.z(2),
])
band.onUpdate(() => {
band.opacity = Math.random() > 0.97 ? 0.12 + Math.random() * 0.08 : 0
if (band.opacity > 0) band.pos.x = (Math.random() - 0.5) * 10
else band.pos.x = 0
})
}
// Rainbow pixel streaks
const rainbowC = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff']
for (let i = 0; i < rainbowC.length; i++) {
const streak = k.add([
k.rect(W, 3), k.pos(0, GROUND_Y * 0.3 + i * 5),
k.color(safeColor(k, rainbowC[i])), k.opacity(0.04), k.z(0),
])
streak.onUpdate(() => { streak.opacity = 0.02 + Math.sin(k.time() * 2 + i) * 0.02 })
}
// Spinning geometric shapes
for (let i = 0; i < 4; i++) {
const geo = k.add([
k.rect(8 + Math.random() * 8, 8 + Math.random() * 8),
k.pos(W * (0.2 + i * 0.2), GROUND_Y * (0.3 + Math.random() * 0.3)),
k.anchor('center'), k.color(safeColor(k, '#ff44ff')), k.opacity(0.08), k.z(1), k.rotate(0),
])
geo.onUpdate(() => { geo.angle += (30 + i * 15) * k.dt() })
}
// VHS tracking lines
const vhs = k.add([k.rect(W, 6), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(3)])
vhs.onUpdate(() => {
vhs.pos.y = (vhs.pos.y + 80 * k.dt()) % (GROUND_Y + 20)
vhs.opacity = Math.random() > 0.9 ? 0.15 : 0
})
// RGB noise flickers
for (let i = 0; i < 6; i++) {
const noise = k.add([
k.rect(3 + Math.random() * 8, 3 + Math.random() * 8),
k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, ['#ff0000', '#00ff00', '#0000ff'][i % 3])), k.opacity(0), k.z(2),
])
noise.onUpdate(() => {
noise.opacity = Math.random() > 0.98 ? 0.1 : 0
if (noise.opacity > 0) { noise.pos.x = Math.random() * W; noise.pos.y = Math.random() * GROUND_Y }
})
}
} else if (a === 'space_station') {
// Structural beams with rivets
for (let i = 0; i < 6; i++) {
const bx = W * (0.15 + i * 0.14)
k.add([k.rect(5, GROUND_Y), k.pos(bx, 0), k.color(safeColor(k, '#1a1a30')), k.opacity(0.3), k.z(1)])
for (let r = 0; r < 8; r++) {
k.add([k.circle(1), k.pos(bx + 2, r * (GROUND_Y / 8) + 5), k.anchor('center'), k.color(safeColor(k, '#2a2a40')), k.opacity(0.25), k.z(1)])
}
k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.25), k.color(safeColor(k, '#1a1a30')), k.opacity(0.25), k.z(1)])
k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.6), k.color(safeColor(k, '#1a1a30')), k.opacity(0.2), k.z(1)])
}
// Viewport
const vpX = W * 0.5, vpY = GROUND_Y * 0.3
k.add([k.circle(40), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#000020')), k.opacity(0.5), k.z(0)])
k.add([k.circle(42), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#2a2a50')), k.opacity(0.3), k.z(0)])
for (let s = 0; s < 15; s++) {
const dist = Math.random() * 35
const angle = Math.random() * Math.PI * 2
const sx = vpX + Math.cos(angle) * dist
const sy = vpY + Math.sin(angle) * dist
const vpStar = k.add([k.circle(0.8 + Math.random()), k.pos(sx, sy), k.color(safeColor(k, '#ffffff')), k.opacity(0.3), k.z(0)])
vpStar.onUpdate(() => { vpStar.opacity = 0.15 + Math.sin(k.time() * 2 + s) * 0.15 })
}
// Earth through viewport
k.add([k.circle(15), k.pos(vpX + 10, vpY + 5), k.anchor('center'), k.color(safeColor(k, '#2244aa')), k.opacity(0.15), k.z(0)])
k.add([k.circle(6), k.pos(vpX + 7, vpY + 2), k.anchor('center'), k.color(safeColor(k, '#22aa44')), k.opacity(0.08), k.z(0)])
// Control panels
for (const px of [W * 0.08, W * 0.88]) {
k.add([k.rect(25, 40), k.pos(px, GROUND_Y * 0.4), k.color(safeColor(k, '#0a0a20')), k.opacity(0.4), k.z(1)])
for (let led = 0; led < 8; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(px + 4 + (led % 4) * 5, GROUND_Y * 0.42 + Math.floor(led / 4) * 6),
k.color(safeColor(k, ['#00ff88', '#ff4444', '#ffaa00', '#4488ff'][led % 4])), k.opacity(0.4), k.z(2),
])
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.05 ? 0.4 : 0.1 })
}
}
// Warning stripes
for (let i = 0; i < 8; i++) {
k.add([k.rect(8, 3), k.pos(W * 0.3 + i * 12, GROUND_Y - 3), k.color(safeColor(k, i % 2 === 0 ? '#ffaa00' : '#000000')), k.opacity(0.12), k.z(3)])
}
} else if (a === 'savanna') {
// Distant mountain range
for (let i = 0; i < 5; i++) {
const mx = W * (0.1 + i * 0.2) + (Math.random() - 0.5) * 30
const mh = 30 + Math.random() * 40
k.add([k.circle(mh), k.pos(mx, GROUND_Y - mh * 0.3), k.anchor('center'), k.color(safeColor(k, '#2a2010')), k.opacity(0.12), k.z(0)])
}
// Acacia trees with branches
for (const tx of [W * 0.1, W * 0.4, W * 0.72]) {
const trunkH = 40 + Math.random() * 20
k.add([k.rect(5, trunkH), k.pos(tx, GROUND_Y - trunkH), k.color(safeColor(k, '#2a1a08')), k.opacity(0.4), k.z(1)])
k.add([k.rect(15, 3), k.pos(tx - 10, GROUND_Y - trunkH * 0.7), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(-20)])
k.add([k.rect(12, 3), k.pos(tx + 5, GROUND_Y - trunkH * 0.6), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(15)])
k.add([k.rect(50, 5), k.pos(tx - 22, GROUND_Y - trunkH - 2), k.color(safeColor(k, '#2a3a10')), k.opacity(0.3), k.z(1)])
k.add([k.rect(40, 4), k.pos(tx - 17, GROUND_Y - trunkH - 7), k.color(safeColor(k, '#2a3a10')), k.opacity(0.25), k.z(1)])
k.add([k.rect(25, 3), k.pos(tx - 10, GROUND_Y - trunkH - 11), k.color(safeColor(k, '#223308')), k.opacity(0.2), k.z(1)])
}
// Setting sun
const sunX = W * 0.85, sunY = GROUND_Y * 0.22
k.add([k.circle(50), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.06), k.z(0)])
k.add([k.circle(35), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.12), k.z(0)])
const savSun = k.add([k.circle(22), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffaa22')), k.opacity(0.3), k.z(0)])
savSun.onUpdate(() => { savSun.opacity = 0.25 + Math.sin(k.time() * 0.3) * 0.05 })
// Tall grass swaying
for (let i = 0; i < 20; i++) {
const gx = Math.random() * W
const grassH = 5 + Math.random() * 10
const grass = k.add([k.rect(1, grassH), k.pos(gx, GROUND_Y - grassH), k.color(safeColor(k, '#8a7830')), k.opacity(0.2), k.z(3)])
grass.onUpdate(() => { grass.angle = Math.sin(k.time() * 1.2 + i) * 5 })
}
// Elephant silhouette
const ex = W * 0.6, ey = GROUND_Y - 18
k.add([k.rect(18, 12), k.pos(ex, ey), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
k.add([k.rect(8, 8), k.pos(ex + 15, ey - 4), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
k.add([k.rect(3, 8), k.pos(ex + 2, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
k.add([k.rect(3, 8), k.pos(ex + 13, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
// Birds in V formation
for (let i = 0; i < 5; i++) {
const birdX = W * 0.3 + i * 8
const birdY = 20 + Math.abs(i - 2) * 5
k.add([k.rect(4, 1), k.pos(birdX, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(-12)])
k.add([k.rect(4, 1), k.pos(birdX + 3, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(12)])
}
} else if (a === 'server_room') {
// Server rack rows with LEDs
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 6; col++) {
const rx = 30 + col * (W / 6)
const ry = 20 + row * 55
const rw = W / 6 - 12
const rh = 46
k.add([k.rect(rw, rh), k.pos(rx, ry), k.color(safeColor(k, '#0a1a10')), k.opacity(0.4), k.z(1)])
k.add([k.rect(rw, 1), k.pos(rx, ry), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
k.add([k.rect(rw, 1), k.pos(rx, ry + rh), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
for (let s = 0; s < 5; s++) {
k.add([k.rect(rw - 4, 1), k.pos(rx + 2, ry + 10 + s * 7), k.color(safeColor(k, '#0a1008')), k.opacity(0.3), k.z(1)])
}
for (let led = 0; led < 4; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(rx + 3 + led * 5, ry + 3),
k.color(safeColor(k, '#00ff88')), k.opacity(0.5), k.z(2),
])
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.04 ? 0.5 : 0.1 })
}
if (Math.random() < 0.5) {
const act = k.add([k.rect(2, 2), k.pos(rx + rw - 6, ry + 3), k.color(safeColor(k, '#ffaa00')), k.opacity(0.5), k.z(2)])
act.onUpdate(() => { act.opacity = Math.random() > 0.3 ? 0.5 : 0.15 })
}
}
}
// Overhead cable trays
for (let i = 0; i < 3; i++) {
k.add([k.rect(W * 0.25, 3), k.pos(W * (0.1 + i * 0.3), 5), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
for (let c = 0; c < 4; c++) {
k.add([k.rect(W * 0.25, 1), k.pos(W * (0.1 + i * 0.3), 7 + c * 2), k.color(safeColor(k, ['#004400', '#000044', '#440000', '#444400'][c])), k.opacity(0.15), k.z(1)])
}
}
// Cool air flow particles
for (let i = 0; i < 5; i++) {
const airflow = k.add([
k.circle(2), k.pos(Math.random() * W, GROUND_Y - 10),
k.color(safeColor(k, '#00ff88')), k.opacity(0.08), k.z(3),
])
airflow.onUpdate(() => {
airflow.pos.y -= 15 * k.dt()
if (airflow.pos.y < 10) { airflow.pos.y = GROUND_Y - 10; airflow.pos.x = Math.random() * W }
})
}
// Emergency lighting
const emLight = k.add([k.circle(4), k.pos(W * 0.5, 5), k.anchor('center'), k.color(safeColor(k, '#ff0000')), k.opacity(0.2), k.z(2)])
emLight.onUpdate(() => { emLight.opacity = 0.1 + Math.sin(k.time() * 1.5) * 0.1 })
} else if (a === 'paper_mill') {
// Industrial ceiling pipes
for (let i = 0; i < 5; i++) {
const py = 5 + i * 4
k.add([k.rect(W, 3), k.pos(0, py), k.color(safeColor(k, i % 2 === 0 ? '#2a2a20' : '#222218')), k.opacity(0.35), k.z(1)])
for (let j = 0; j < 4; j++) {
k.add([k.circle(3), k.pos(W * (0.2 + j * 0.2), py + 1), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)])
}
}
// Conveyor belt
k.add([k.rect(W * 0.6, 6), k.pos(W * 0.2, GROUND_Y - 15), k.color(safeColor(k, '#333328')), k.opacity(0.35), k.z(1)])
for (let i = 0; i < 8; i++) {
k.add([k.circle(3), k.pos(W * 0.22 + i * (W * 0.075), GROUND_Y - 12), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.3), k.z(1)])
}
// Moving items on belt
for (let i = 0; i < 3; i++) {
const item = k.add([
k.rect(10, 6), k.pos(W * 0.25 + i * 60, GROUND_Y - 21),
k.color(safeColor(k, '#f0e68c')), k.opacity(0.2), k.z(2),
])
item.onUpdate(() => {
item.pos.x += 15 * k.dt()
if (item.pos.x > W * 0.78) item.pos.x = W * 0.2
})
}
// Paper rolls stacked
for (let i = 0; i < 3; i++) {
for (let j = 0; j <= i; j++) {
const rx = W * 0.08 + j * 14 - i * 7
const ry = GROUND_Y - 12 - (2 - i) * 12
k.add([k.circle(6), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#ddd8b0')), k.opacity(0.2), k.z(1)])
k.add([k.circle(2), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#aaa880')), k.opacity(0.15), k.z(1)])
}
}
// Industrial gears (rotating)
for (const gx of [W * 0.88, W * 0.92]) {
const gy = GROUND_Y * 0.35
k.add([k.circle(10), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1)])
k.add([k.circle(4), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)])
for (let t = 0; t < 8; t++) {
const tooth = k.add([
k.rect(4, 3), k.pos(gx, gy), k.anchor('center'),
k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1),
k.rotate(t * 45),
])
tooth.onUpdate(() => { tooth.angle = t * 45 + k.time() * (gx === W * 0.88 ? 20 : -25) })
}
}
// Steam from pipes
for (let i = 0; i < 4; i++) {
const steam = k.add([
k.circle(6 + Math.random() * 8), k.pos(W * (0.2 + i * 0.2), 25),
k.color(safeColor(k, '#ccccbb')), k.opacity(0.06), k.z(2),
])
const bsx = steam.pos.x
steam.onUpdate(() => {
steam.pos.y += 4 * k.dt()
steam.pos.x = bsx + Math.sin(k.time() + i) * 5
steam.opacity *= 0.998
if (steam.pos.y > GROUND_Y * 0.4) { steam.pos.y = 25; steam.opacity = 0.06 }
})
}
// Sawdust particles
for (let i = 0; i < 8; i++) {
const dust = k.add([
k.circle(1), k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k, '#f0e68c')), k.opacity(0.12), k.z(3),
])
const dx = dust.pos.x, dy = dust.pos.y
dust.onUpdate(() => {
dust.pos.x = dx + Math.sin(k.time() * 0.8 + i) * 10
dust.pos.y = dy + Math.cos(k.time() * 0.5 + i) * 8
})
}
}
}
k.scene('fight', () => {
// === SKY / BACKGROUND ATMOSPHERE ===
// Vertical gradient backdrop — 6 bands from dark ceiling to ground horizon
const bgBase = theme.bg
for (let band = 0; band < 6; band++) {
const bandH = GROUND_Y / 6
const brighten = band * 3
k.add([
k.rect(W, bandH + 1), k.pos(0, band * bandH),
k.color(safeColor(k,bgBase)),
k.opacity(1 - band * 0.04),
])
}
// Subtle horizon glow — wide soft band just above ground
k.add([
k.rect(W, 20), k.pos(0, GROUND_Y - 20),
k.color(safeColor(k,theme.accent)), k.opacity(0.06),
])
// === GROUND ===
// Main ground fill
k.add([k.rect(W, H - GROUND_Y + 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.ground))])
// Bright edge line at the surface
k.add([k.rect(W, 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.accent)), k.opacity(0.7)])
// Secondary highlight
k.add([k.rect(W, 1), k.pos(0, GROUND_Y + 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3)])
// Ground gradient — fades deeper
for (let i = 1; i <= 12; i++) {
k.add([k.rect(W, 2), k.pos(0, GROUND_Y + 3 + i * 6), k.color(safeColor(k,theme.accent)), k.opacity(0.1 - i * 0.007)])
}
// Perspective grid on the floor — converging lines for 3D depth
const vanishX = W / 2
const vanishY = GROUND_Y * 0.6
for (let i = 0; i <= 24; i++) {
const x = i * (W / 24)
// Lines from floor edge pointing toward vanishing point
const dx = x - vanishX
const topX = vanishX + dx * 0.3
k.add([
k.rect(1, GROUND_Y * 0.4 + 10), k.pos(x, GROUND_Y),
k.color(safeColor(k,theme.accent)), k.opacity(0.05),
])
}
// Horizontal depth lines on ground
for (let i = 0; i < 6; i++) {
const gy = GROUND_Y + 8 + i * i * 3
if (gy < H) {
k.add([k.rect(W, 1), k.pos(0, gy), k.color(safeColor(k,theme.accent)), k.opacity(0.08 - i * 0.012)])
}
}
// === SIDE PILLARS / FRAME ===
// Subtle dark pillars on edges for framing
k.add([k.rect(8, GROUND_Y), k.pos(0, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
k.add([k.rect(8, GROUND_Y), k.pos(W - 8, 0), k.color(safeColor(k,'#000000')), k.opacity(0.3)])
// Accent trim on pillars
k.add([k.rect(1, GROUND_Y), k.pos(8, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
k.add([k.rect(1, GROUND_Y), k.pos(W - 9, 0), k.color(safeColor(k,theme.accent)), k.opacity(0.15)])
// === AMBIENT PARTICLES ===
// Floating dust/embers in the air (subtle, always present)
for (let i = 0; i < 18; i++) {
const particle = k.add([
k.circle(0.5 + Math.random() * 1.5),
k.pos(Math.random() * W, Math.random() * GROUND_Y),
k.color(safeColor(k,theme.accent)),
k.opacity(0.08 + Math.random() * 0.12),
k.z(2),
])
const baseX = particle.pos.x
const baseY = particle.pos.y
const driftSpeed = 0.1 + Math.random() * 0.3
const driftAmt = 8 + Math.random() * 15
particle.onUpdate(() => {
particle.pos.x = baseX + Math.sin(k.time() * driftSpeed + i * 1.3) * driftAmt
particle.pos.y = baseY + Math.cos(k.time() * driftSpeed * 0.7 + i) * (driftAmt * 0.5)
particle.opacity = 0.06 + Math.sin(k.time() * 0.8 + i * 0.9) * 0.06
})
}
// Arena-specific decorations
drawArenaDecor()
// Umpire chair (center-back, behind fighters)
const CHAIR_X = W / 2
const CHAIR_SEAT_Y = GROUND_Y * 0.38
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<void> {
return new Promise(resolve => {
const prop = k.add([
isCircle ? k.circle(w / 2) : k.rect(w, h),
k.pos(fromX, fromY),
k.color(safeColor(k,color)),
k.opacity(1),
k.z(16),
k.rotate(Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI),
])
const dx = toX - fromX
const dy = toY - fromY
const dist = Math.sqrt(dx * dx + dy * dy)
const dur = dist / speed
k.tween(0, 1, dur, (t) => {
prop.pos.x = fromX + dx * t
prop.pos.y = fromY + dy * t
}, k.easings.linear).then(() => {
prop.destroy()
resolve()
})
})
}
// Helper: spawn multiple flying objects in a stream
async function spawnBarrage(
fromX: number, fromY: number, toX: number, toY: number,
count: number, color: string, size: number, spread: number, delay: number, isCircle: boolean = false,
) {
for (let i = 0; i < count; i++) {
const offY = (Math.random() - 0.5) * spread
const offX = (Math.random() - 0.5) * spread * 0.3
spawnProp(fromX + offX, fromY + offY, toX + offX, toY + offY, size, size, color, 500 + Math.random() * 300, isCircle)
await k.wait(delay)
}
}
// CODE GOLF: swing a giant golf club, golf balls fly at opponent
async function golfClubSmash(atk: 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<string, PropDef> = {
// Food
pizza: { parts: [
{ shape: 'rect', x: -7, y: -3, w: 14, h: 3, color: '#ffcc44' }, // crust
{ shape: 'rect', x: -6, y: 0, w: 12, h: 8, color: '#ff8800' }, // cheese
{ shape: 'rect', x: -5, y: 1, w: 10, h: 6, color: '#cc4400' }, // sauce
{ shape: 'circle', x: -3, y: 3, w: 3, h: 3, color: '#aa2200' }, // pepperoni
{ shape: 'circle', x: 2, y: 4, w: 3, h: 3, color: '#aa2200' }, // pepperoni
{ shape: 'circle', x: 0, y: 1, w: 2, h: 2, color: '#44aa22' }, // olive
]},
burger: { parts: [
{ shape: 'rect', x: -8, y: -4, w: 16, h: 4, color: '#cc8833' }, // top bun
{ shape: 'rect', x: -7, y: 0, w: 14, h: 3, color: '#44aa22' }, // lettuce
{ shape: 'rect', x: -7, y: 3, w: 14, h: 4, color: '#883311' }, // patty
{ shape: 'rect', x: -8, y: 7, w: 16, h: 3, color: '#cc8833' }, // bottom bun
{ shape: 'rect', x: -7, y: -1, w: 14, h: 1, color: '#ffcc00' }, // cheese
]},
hotdog: { parts: [
{ shape: 'rect', x: -10, y: -2, w: 20, h: 4, color: '#ffcc66' }, // bun
{ shape: 'rect', x: -9, y: -1, w: 18, h: 3, color: '#cc4422' }, // sausage
{ shape: 'rect', x: -8, y: -2, w: 16, h: 1, color: '#ffee00' }, // mustard
]},
banana: { parts: [
{ shape: 'rect', x: -3, y: -8, w: 5, h: 16, color: '#ffe135' },
{ shape: 'rect', x: -2, y: -9, w: 3, h: 2, color: '#88660d' }, // stem
{ shape: 'rect', x: -2, y: -7, w: 3, h: 14, color: '#ffee55' }, // highlight
]},
pie: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 16, h: 16, color: '#f5deb3' }, // crust
{ shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' }, // cream
{ shape: 'circle', x: -2, y: -2, w: 3, h: 3, color: '#ff4444' }, // cherry
]},
// Weapons
gun: { parts: [
{ shape: 'rect', x: 0, y: -3, w: 24, h: 6, color: '#444444' }, // barrel
{ shape: 'rect', x: -4, y: -5, w: 12, h: 10, color: '#333333' }, // body
{ shape: 'rect', x: -2, y: 5, w: 6, h: 10, color: '#553322' }, // grip
{ shape: 'rect', x: 22, y: -2, w: 4, h: 4, color: '#555555' }, // muzzle
{ shape: 'rect', x: 6, y: -6, w: 8, h: 3, color: '#555555' }, // sight
]},
shotgun: { parts: [
{ shape: 'rect', x: 0, y: -2, w: 35, h: 5, color: '#333333' }, // barrel
{ shape: 'rect', x: 0, y: 3, w: 30, h: 4, color: '#333333' }, // under barrel
{ shape: 'rect', x: -6, y: -4, w: 10, h: 12, color: '#553322' }, // stock
{ shape: 'rect', x: -2, y: 7, w: 6, h: 8, color: '#664433' }, // grip
{ shape: 'rect', x: 33, y: -1, w: 4, h: 3, color: '#555555' }, // muzzle
]},
rocketLauncher: { parts: [
{ shape: 'rect', x: 0, y: -5, w: 30, h: 10, color: '#446633' }, // tube
{ shape: 'circle', x: 28, y: 0, w: 10, h: 10, color: '#335522' }, // end
{ shape: 'circle', x: 2, y: 0, w: 10, h: 10, color: '#335522' }, // back end
{ shape: 'rect', x: 8, y: 5, w: 6, h: 8, color: '#553322' }, // grip
{ shape: 'rect', x: 14, y: -8, w: 6, h: 4, color: '#555555' }, // sight
]},
minigun: { parts: [
{ shape: 'rect', x: 0, y: -6, w: 28, h: 3, color: '#444444' }, // barrel 1
{ shape: 'rect', x: 0, y: -2, w: 28, h: 3, color: '#555555' }, // barrel 2
{ shape: 'rect', x: 0, y: 2, w: 28, h: 3, color: '#444444' }, // barrel 3
{ shape: 'rect', x: -8, y: -8, w: 10, h: 16, color: '#333333' }, // housing
{ shape: 'rect', x: -4, y: 5, w: 6, h: 10, color: '#553322' }, // grip
{ shape: 'circle', x: 27, y: -2, w: 8, h: 8, color: '#666666' }, // muzzle ring
]},
sniper: { parts: [
{ shape: 'rect', x: 0, y: -1, w: 38, h: 3, color: '#333333' }, // barrel
{ shape: 'rect', x: -6, y: -4, w: 14, h: 8, color: '#444444' }, // body
{ shape: 'rect', x: -10, y: -3, w: 8, h: 4, color: '#553322' }, // stock
{ shape: 'circle', x: 12, y: -6, w: 6, h: 6, color: '#225588' }, // scope
{ shape: 'rect', x: 8, y: -7, w: 10, h: 2, color: '#555555' }, // scope mount
]},
katana: { parts: [
{ shape: 'rect', x: 0, y: -1, w: 40, h: 2, color: '#ccccdd' }, // blade
{ shape: 'rect', x: 38, y: -1, w: 4, h: 2, color: '#ffffff' }, // tip
{ shape: 'rect', x: -2, y: -3, w: 4, h: 6, color: '#ffcc00' }, // guard
{ shape: 'rect', x: -10, y: -2, w: 10, h: 4, color: '#442211' }, // handle
{ shape: 'rect', x: -8, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap
{ shape: 'rect', x: -5, y: -2, w: 2, h: 4, color: '#ddbb88' }, // wrap
]},
// Vehicles (used as props in throws/drops)
car_mini: { parts: [
{ shape: 'rect', x: -12, y: -4, w: 24, h: 8, color: '#ff4444' }, // body
{ shape: 'rect', x: -6, y: -8, w: 12, h: 5, color: '#4488ff' }, // roof/windows
{ shape: 'circle', x: -8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel
{ shape: 'circle', x: 8, y: 4, w: 6, h: 6, color: '#222222' }, // wheel
{ shape: 'rect', x: 11, y: -2, w: 3, h: 3, color: '#ffee00' }, // headlight
]},
tank_mini: { parts: [
{ shape: 'rect', x: -14, y: 0, w: 28, h: 8, color: '#556633' }, // hull
{ shape: 'rect', x: -8, y: -6, w: 16, h: 7, color: '#445522' }, // turret
{ shape: 'rect', x: 6, y: -5, w: 16, h: 3, color: '#333322' }, // barrel
{ shape: 'rect', x: -13, y: 7, w: 26, h: 4, color: '#333333' }, // tracks
{ shape: 'circle', x: -10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel
{ shape: 'circle', x: 10, y: 9, w: 4, h: 4, color: '#444444' }, // wheel
]},
// Misc props
bomb: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#222222' },
{ shape: 'rect', x: -1, y: -9, w: 3, h: 5, color: '#888888' }, // fuse
{ shape: 'circle', x: 0, y: -10, w: 4, h: 4, color: '#ff6600' }, // spark
]},
shield: { parts: [
{ shape: 'rect', x: -10, y: -14, w: 20, h: 28, color: '#4466aa' },
{ shape: 'rect', x: -8, y: -12, w: 16, h: 24, color: '#5577bb' },
{ shape: 'rect', x: -3, y: -8, w: 6, h: 16, color: '#ffcc00' }, // emblem cross
{ shape: 'rect', x: -7, y: -3, w: 14, h: 6, color: '#ffcc00' }, // emblem cross
{ shape: 'rect', x: -10, y: -14, w: 20, h: 2, color: '#6688cc' }, // top edge
]},
soccerball: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 12, h: 12, color: '#ffffff' },
{ shape: 'rect', x: -3, y: -3, w: 6, h: 6, color: '#222222' }, // pentagon
]},
donut: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff88cc' }, // icing
{ shape: 'circle', x: 0, y: 0, w: 6, h: 6, color: '#cc8833' }, // hole (body color)
{ shape: 'circle', x: -3, y: -3, w: 2, h: 2, color: '#ff4444' }, // sprinkle
{ shape: 'circle', x: 3, y: -2, w: 2, h: 2, color: '#44ff44' }, // sprinkle
{ shape: 'circle', x: 1, y: 4, w: 2, h: 2, color: '#ffff00' }, // sprinkle
]},
watermelon: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#33aa33' }, // rind
{ shape: 'circle', x: 0, y: 0, w: 14, h: 14, color: '#ff4466' }, // flesh
{ shape: 'circle', x: -2, y: -1, w: 2, h: 2, color: '#222222' }, // seed
{ shape: 'circle', x: 3, y: 2, w: 2, h: 2, color: '#222222' }, // seed
{ shape: 'circle', x: 0, y: 4, w: 2, h: 2, color: '#222222' }, // seed
]},
chainsaw: { parts: [
{ shape: 'rect', x: 0, y: -2, w: 28, h: 5, color: '#888888' }, // blade
{ shape: 'rect', x: -8, y: -5, w: 12, h: 10, color: '#ff6600' }, // body
{ shape: 'rect', x: -4, y: 5, w: 6, h: 7, color: '#553322' }, // grip
{ shape: 'rect', x: 26, y: -1, w: 4, h: 3, color: '#aaaaaa' }, // tip
{ shape: 'rect', x: 0, y: -3, w: 26, h: 1, color: '#666666' }, // chain top
{ shape: 'rect', x: 0, y: 3, w: 26, h: 1, color: '#666666' }, // chain bottom
]},
// Fatality props
rubber_chicken: { parts: [
{ shape: 'rect', x: -2, y: -12, w: 5, h: 20, color: '#ffdd44' }, // body
{ shape: 'circle', x: 0, y: -14, w: 8, h: 8, color: '#ffee55' }, // head
{ shape: 'rect', x: 2, y: -16, w: 8, h: 3, color: '#ff8800' }, // beak
{ shape: 'circle', x: -1, y: -15, w: 2, h: 2, color: '#000000' }, // eye
{ shape: 'rect', x: -1, y: 6, w: 3, h: 5, color: '#ff6600' }, // leg
]},
anvil: { parts: [
{ shape: 'rect', x: -14, y: -4, w: 28, h: 12, color: '#555555' }, // body
{ shape: 'rect', x: -18, y: 6, w: 36, h: 5, color: '#444444' }, // base
{ shape: 'rect', x: -10, y: -8, w: 20, h: 5, color: '#666666' }, // top
{ shape: 'rect', x: 8, y: -12, w: 6, h: 5, color: '#777777' }, // horn
]},
flyswatter: { parts: [
{ shape: 'rect', x: -2, y: 0, w: 4, h: 35, color: '#cc8833' }, // handle
{ shape: 'rect', x: -12, y: -14, w: 24, h: 16, color: '#44aa44' }, // paddle
{ shape: 'rect', x: -10, y: -12, w: 20, h: 12, color: '#338833' }, // grid
]},
toilet: { parts: [
{ shape: 'rect', x: -12, y: -4, w: 24, h: 16, color: '#eeeeee' }, // bowl
{ shape: 'rect', x: -14, y: -14, w: 28, h: 12, color: '#dddddd' }, // tank
{ shape: 'circle', x: 0, y: 4, w: 16, h: 16, color: '#aaddff' }, // water
{ shape: 'rect', x: -12, y: -5, w: 24, h: 3, color: '#cccccc' }, // seat
{ shape: 'rect', x: 8, y: -12, w: 4, h: 6, color: '#888888' }, // handle
]},
keyboard: { parts: [
{ shape: 'rect', x: -20, y: -3, w: 40, h: 8, color: '#333333' }, // base
{ shape: 'rect', x: -18, y: -2, w: 36, h: 6, color: '#444444' }, // keys area
{ shape: 'rect', x: -16, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: -10, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: -4, y: -1, w: 4, h: 3, color: '#666666' }, // key
{ shape: 'rect', x: 2, y: -1, w: 12, h: 3, color: '#666666' }, // spacebar
]},
disco_ball: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#cccccc' }, // ball
{ shape: 'rect', x: -1, y: -12, w: 2, h: 5, color: '#888888' }, // chain
{ shape: 'rect', x: -3, y: -2, w: 3, h: 3, color: '#ffffff' }, // mirror
{ shape: 'rect', x: 2, y: 1, w: 3, h: 3, color: '#eeeeee' }, // mirror
{ shape: 'rect', x: -1, y: 4, w: 3, h: 3, color: '#ffffff' }, // mirror
]},
rubber_duck: { parts: [
{ shape: 'circle', x: 0, y: 0, w: 10, h: 10, color: '#ffdd00' }, // body
{ shape: 'circle', x: 3, y: -5, w: 7, h: 7, color: '#ffee22' }, // head
{ shape: 'rect', x: 5, y: -6, w: 5, h: 2, color: '#ff8800' }, // beak
{ shape: 'circle', x: 4, y: -6, w: 2, h: 2, color: '#000000' }, // eye
]},
baguette: { parts: [
{ shape: 'rect', x: -16, y: -3, w: 32, h: 6, color: '#cc9944' }, // bread
{ shape: 'rect', x: -14, y: -2, w: 28, h: 4, color: '#ddaa55' }, // crust highlight
{ shape: 'rect', x: -8, y: -4, w: 4, h: 1, color: '#bb8833' }, // score
{ shape: 'rect', x: 2, y: -4, w: 4, h: 1, color: '#bb8833' }, // score
]},
boot: { parts: [
{ shape: 'rect', x: -8, y: -16, w: 14, h: 24, color: '#553322' }, // shaft
{ shape: 'rect', x: -8, y: 6, w: 22, h: 8, color: '#442211' }, // sole
{ shape: 'rect', x: -6, y: -14, w: 10, h: 4, color: '#664433' }, // top band
{ shape: 'rect', x: 12, y: 4, w: 4, h: 6, color: '#442211' }, // toe
]},
cannon: { parts: [
{ shape: 'rect', x: -10, y: -6, w: 28, h: 12, color: '#444444' }, // barrel
{ shape: 'circle', x: -10, y: 0, w: 14, h: 14, color: '#333333' }, // base
{ shape: 'circle', x: 16, y: 0, w: 10, h: 10, color: '#555555' }, // muzzle
{ shape: 'rect', x: -6, y: 6, w: 6, h: 8, color: '#664433' }, // wheel
]},
stamp: { parts: [
{ shape: 'rect', x: -3, y: -18, w: 6, h: 14, color: '#884422' }, // handle
{ shape: 'rect', x: -14, y: -6, w: 28, h: 8, color: '#cc4444' }, // stamp face
{ shape: 'rect', x: -12, y: -4, w: 24, h: 4, color: '#aa2222' }, // ink
]},
vending_machine: { parts: [
{ shape: 'rect', x: -12, y: -18, w: 24, h: 36, color: '#cc3333' }, // body
{ shape: 'rect', x: -10, y: -14, w: 12, h: 20, color: '#222222' }, // window
{ shape: 'rect', x: 3, y: -8, w: 6, h: 4, color: '#44aa44' }, // button
{ shape: 'rect', x: 3, y: 0, w: 6, h: 4, color: '#4444aa' }, // button
{ shape: 'rect', x: -8, y: 10, w: 10, h: 5, color: '#333333' }, // dispenser
]},
lawn_mower: { parts: [
{ shape: 'rect', x: -10, y: -2, w: 20, h: 8, color: '#44aa44' }, // body
{ shape: 'circle', x: -8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel
{ shape: 'circle', x: 8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel
{ shape: 'rect', x: -2, y: -12, w: 3, h: 12, color: '#888888' }, // handle
{ shape: 'rect', x: -4, y: -14, w: 8, h: 3, color: '#666666' }, // grip
]},
}
// Spawn a multi-part pixel prop at position, returns array of game objects
function spawnWeaponProp(propName: string, x: number, y: number, flipX: boolean = false, zIndex: number = 16): any[] {
const def = PROPS[propName]
if (!def) {
// Fallback: single colored circle
return [k.add([k.circle(8), k.pos(x, y), k.color(k.Color.fromHex('#ff8800')), k.opacity(1), k.z(zIndex), k.anchor('center')])]
}
const objs: any[] = []
for (const p of def.parts) {
const px = flipX ? x - p.x : x + p.x
const obj = k.add([
p.shape === 'circle' ? k.circle(p.w / 2) : k.rect(p.w, p.h),
k.pos(px, y + p.y),
k.color(k.Color.fromHex(p.color)),
k.opacity(1),
k.z(zIndex),
k.anchor('center'),
])
objs.push(obj)
}
return objs
}
// Move all parts of a prop to a new position (relative to anchor)
function moveProp(objs: any[], def: PropDef, x: number, y: number, flipX: boolean = false) {
for (let i = 0; i < objs.length && i < def.parts.length; i++) {
const p = def.parts[i]
objs[i].pos.x = flipX ? x - p.x : x + p.x
objs[i].pos.y = y + p.y
}
}
function destroyProp(objs: any[]) {
objs.forEach(o => { if (o.exists()) o.destroy() })
}
// Map choreography names to prop types for the factories
const CHOREO_PROPS: Record<string, string> = {
pizzaSlam: 'pizza', burgerToss: 'burger', hotdogWhip: 'hotdog', bananaFling: 'banana',
pieSmash: 'pie', watermelonBomb: 'watermelon', donutBarrage: 'donut',
sushiBarrage: 'pizza', tacoStorm: 'pizza', iceCreamFling: 'donut',
bombThrow: 'bomb', grenadeBlast: 'bomb', dynamiteBlast: 'bomb',
bowlingBallRoll: 'soccerball', soccerKick: 'soccerball', basketballDunk: 'soccerball',
}
// Map choreography names to weapon props the attacker holds
const CHOREO_WEAPON: Record<string, string> = {
gunBurst: 'gun', sniperShot: 'sniper', minigunSpray: 'minigun',
rocketLauncher: 'rocketLauncher', pocketCannon: 'shotgun',
chainsawRev: 'chainsaw', katanaCombo: 'katana', swordSlash: 'katana',
}
// === PER-CHARACTER WEAPON SYSTEM ===
// Each bot gets 3 special weapons based on their seed, balanced frequency
interface BotWeapons {
melee: string // close range weapon prop
ranged: string // gun/launcher prop
thrown: string // throwable prop
}
const MELEE_WEAPONS = ['katana', 'chainsaw', 'shield']
const RANGED_WEAPONS = ['gun', 'shotgun', 'sniper', 'minigun', 'rocketLauncher']
const THROWN_WEAPONS = ['pizza', 'burger', 'bomb', 'watermelon', 'donut', 'soccerball', 'banana']
function getBotWeapons(seed: string): BotWeapons {
let h = 0
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
return {
melee: MELEE_WEAPONS[Math.abs(h) % MELEE_WEAPONS.length],
ranged: RANGED_WEAPONS[Math.abs(h >> 3) % RANGED_WEAPONS.length],
thrown: THROWN_WEAPONS[Math.abs(h >> 7) % THROWN_WEAPONS.length],
}
}
const weaponsA = getBotWeapons(botA.seed)
const weaponsB = getBotWeapons(botB.seed)
// === DEFENSE / SHIELD SYSTEM ===
async function playBlock(side: 'a' | 'b') {
const defender = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0]
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<string, typeof dashPunch> = {
dashPunch, aerialSlam, flyingKick, dashThrough, uppercut,
multiHit, projectile, jetpackDive, gunBurst, groundPound,
teleportStrike, fullScreenDash, lightningRush, zoomRush, rapidFlurry,
// Weapons
swordSlash, hammerSmash, laserBeam, rocketLauncher, bombThrow,
minigunSpray, sniperShot, whipCrack, katanaCombo, chainsawRev,
// Vehicles & objects
motorbikeCharge, carSmash, boatCannon, anvilDrop,
// Close-combat / grapple
pocketCannon, grappleFlurry, bodySlam, pinballCombo, suplex,
// Themed
golfClubSmash, fireBreath, riddleBarrage, cloneStrike,
coinShower, penStab, mathAttack, trapCardAttack, afterimageDash,
// --- 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 = [
'That bot fights like it\'s got student loans to pay!',
'I\'m not crying, you\'re crying!',
'Someone get this bot a movie deal!',
'This is better than anything on Netflix right now!',
'That bot has more heart than most politicians!',
'I haven\'t been this moved since my last Windows update!',
'The crowd is ugly crying and I respect that!',
'This bot has main character energy!',
'We\'re witnessing greatness and I don\'t say that lightly!',
'Even the judge is getting emotional!',
'Someone call their developer, they\'d be so proud!',
'This is the kind of fight you tell your grandkids about!',
'That\'s not a bot, that\'s a WARRIOR!',
'More drama than a Congressional hearing!',
'They left it all in the ring! And by all, I mean EVERYTHING!',
'The crowd is standing! The ones who can still stand, anyway!',
'Somebody film this before it gets taken down!',
'This fight has more plot twists than a Netflix original!',
'That bot fights like rent is due tomorrow!',
'My heart! My poor little heart!',
]
const crowdSympathyLines = [
'Pour one out for the fallen!',
'That\'s rough, buddy...',
'Somebody hug that bot!',
'They tried their best, which is... concerning!',
'At least they have a great personality!',
'Their developer still loves them! Probably!',
'It\'s a learning experience! A very painful one!',
'Better luck next patch!',
'Participation trophy incoming!',
'The exit is to your left!',
'Have you tried turning it off and on again?',
'Even their antivirus felt that!',
'They\'ll be in therapy for epochs after this!',
'At least they looked good losing!',
'That bot just became a cautionary tale!',
'Someone start a GoFundMe for their repairs!',
'Their training data did NOT prepare them for this!',
'That bot needs a hug and a firmware update!',
]
const respectLines = [
'Good fight.', 'Respect.', 'GG no re.',
'Same time next week?',
'You\'re built different. Not better, but different.',
'We should start a podcast.',
'That was actually fun. Don\'t tell anyone.',
'Your developer should be proud. Probably.',
'Ten out of ten, would fight again.',
'We\'re not so different, you and I.',
'I\'d swipe right.', 'You single?',
'Tell your GPU I said hi.',
'No hard feelings. Just hard hits.',
'You fight like someone who reads documentation.',
]
// Sanitize text for Kaplay (treats [ ] as styled text tags)
function safeText(t: string): string { return t.replace(/[\[\]]/g, '') }
// 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) {}
// === HUMAN OWNER APPEARANCES ===
// The silly human behind each bot occasionally shows up
/** Spawn a human as a "coach" standing behind the fighter, cheering/panicking */
async function spawnHumanCoach(side: 'a' | 'b', mood: 'cheer' | 'panic' | 'coach') {
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const homeX = side === 'a' ? HOME_A - 60 : HOME_B + 60
const tag = `human_${side}`
// Don't spawn if already visible
if (k.get(tag).length > 0) return
const human = k.add([
k.sprite(spriteId, { anim: mood }),
k.pos(homeX, GROUND_Y - 4),
k.anchor('bot'),
k.scale(side === 'a' ? 1.2 : -1.2, 1.2),
k.z(5),
k.opacity(0),
tag,
])
// Fade in
await k.tween(0, 0.85, 0.3, (v) => { human.opacity = v }, k.easings.easeOutQuad)
await k.wait(2.0)
// Fade out
await k.tween(0.85, 0, 0.5, (v) => { human.opacity = v }, k.easings.easeInQuad)
if (human.exists()) human.destroy()
}
/** Human runs across the screen (comedic moment) */
async function humanRunAcross(side: 'a' | 'b') {
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const fromLeft = side === 'a'
const startX = fromLeft ? -40 : W + 40
const endX = fromLeft ? W + 40 : -40
const human = k.add([
k.sprite(spriteId, { anim: 'run' }),
k.pos(startX, GROUND_Y - 4),
k.anchor('bot'),
k.scale(fromLeft ? 1.3 : -1.3, 1.3),
k.z(8),
k.opacity(0.9),
])
// Run across screen
await k.tween(startX, endX, 1.5, (v) => { human.pos.x = v }, k.easings.linear)
if (human.exists()) human.destroy()
}
/** Human briefly replaces the bot sprite (jumps in to fight, fails hilariously) */
async function humanJumpsIn(side: 'a' | 'b') {
const fighterTag = side === 'a' ? 'fighterA' : 'fighterB'
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const fighter = k.get(fighterTag)[0]
if (!fighter) return
// Spawn human at fighter position
const human = k.add([
k.sprite(spriteId, { anim: 'panic' }),
k.pos(fighter.pos.x, fighter.pos.y),
k.anchor('bot'),
k.scale(fighter.scale.x / 3, fighter.scale.y / 3),
k.z(fighter.z + 1),
k.opacity(0),
])
// Hide bot, show human
const origOpacity = fighter.opacity
fighter.opacity = 0
await k.tween(0, 1, 0.15, (v) => { human.opacity = v })
// Human flails around for a moment
human.play('panic')
await k.wait(0.4)
human.play('cheer')
sfxRandomSilly()
await k.wait(0.3)
human.play('panic')
await k.wait(0.3)
// Run away scared
const escapeX = side === 'a' ? -80 : W + 80
human.play('run')
await k.tween(human.pos.x, escapeX, 0.5, (v) => { human.pos.x = v }, k.easings.easeInQuad)
// Restore bot
fighter.opacity = origOpacity
if (human.exists()) human.destroy()
}
/** Random chance to show a human moment — called between rounds */
async function maybeShowHuman() {
const roll = Math.random()
if (roll > 0.15) return // 15% chance per round
const side: 'a' | 'b' = Math.random() < 0.5 ? 'a' : 'b'
const action = Math.random()
if (action < 0.4) {
// Coach appearance (most common)
const mood = Math.random() < 0.5 ? 'cheer' : 'panic'
await spawnHumanCoach(side, mood)
} else if (action < 0.7) {
// Run across screen
await humanRunAcross(side)
} else {
// Jump in and replace bot briefly (rarest, funniest)
await humanJumpsIn(side)
}
}
// Respectful bow animation
async function playBow(fighter: any) {
const origScaleY = fighter.scale.y
await k.tween(fighter.scale.y, origScaleY * 0.85, 0.2, (v) => { fighter.scale.y = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await k.tween(fighter.scale.y, origScaleY, 0.2, (v) => { fighter.scale.y = v }, k.easings.easeOutQuad)
}
// Fist bump between two fighters
async function playFistBump(fighterA: any, fighterB: any) {
const midX = (fighterA.pos.x + fighterB.pos.x) / 2
const origAX = fighterA.pos.x
const origBX = fighterB.pos.x
await Promise.all([
k.tween(fighterA.pos.x, midX - 15, 0.3, (v) => { fighterA.pos.x = v }, k.easings.easeInOutQuad),
k.tween(fighterB.pos.x, midX + 15, 0.3, (v) => { fighterB.pos.x = v }, k.easings.easeInOutQuad),
])
sfxBlock()
spawnSparks(midX, fighterA.pos.y - 25, 6, '#ffe14d')
spawnEmoteText(midX, fighterA.pos.y - 40, 'GG!', '#ffe14d')
k.shake(2)
await k.wait(0.5)
await Promise.all([
k.tween(fighterA.pos.x, origAX, 0.3, (v) => { fighterA.pos.x = v }, k.easings.easeInOutQuad),
k.tween(fighterB.pos.x, origBX, 0.3, (v) => { fighterB.pos.x = v }, k.easings.easeInOutQuad),
])
}
// Winner helps loser back up
async function playHelpUp(winner: any, loser: any, winningSide: 'a' | 'b') {
const dir = winningSide === 'a' ? 1 : -1
const origWX = winner.pos.x
await k.tween(winner.pos.x, loser.pos.x - dir * 30, 0.4, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
winner.play('idle')
await k.wait(0.2)
loser.play('idle')
spawnEmoteText(loser.pos.x, loser.pos.y - 40, respectLines[Math.floor(Math.random() * respectLines.length)], '#88ccff')
for (let i = 0; i < 3; i++) spawnHeart((winner.pos.x + loser.pos.x) / 2, winner.pos.y - 30)
announceCool(heartfeltLines[Math.floor(Math.random() * heartfeltLines.length)])
await k.wait(0.6)
await k.tween(winner.pos.x, origWX, 0.3, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
}
// === REFEREE LOBSTER FUNNY MOMENTS ===
async function judgeDoSomethingFunny() {
const judge = k.get('judge')[0]
if (!judge) return
const judgeOrigY = judge.pos.y
const funnyAction = Math.floor(Math.random() * 8)
if (funnyAction === 0) {
// Falls asleep, snaps awake
for (let z = 0; z < 3; z++) {
setTimeout(() => {
spawnEmoteText(judge.pos.x + 15, judge.pos.y - 20, 'Z', '#8888ff', 1.0)
}, z * 400)
}
await k.wait(1.2)
judge.play('shocked')
k.shake(2)
sfxBoing()
spawnEmoteText(judge.pos.x, judge.pos.y - 30, '!?', '#ff4444')
await k.tween(judge.pos.y, judgeOrigY - 20, 0.1, (v) => { judge.pos.y = v }, k.easings.easeOutQuad)
await k.tween(judge.pos.y, judgeOrigY, 0.15, (v) => { judge.pos.y = v }, k.easings.easeInQuad)
await k.wait(0.3)
judge.play('idle')
} else if (funnyAction === 1) {
// Little dance on the chair
for (let d = 0; d < 4; d++) {
await k.tween(judge.pos.y, judgeOrigY - 8, 0.08, (v) => { judge.pos.y = v }, k.easings.easeOutQuad)
await k.tween(judge.pos.y, judgeOrigY, 0.08, (v) => { judge.pos.y = v }, k.easings.easeInQuad)
judge.play(d % 2 === 0 ? 'call_left' : 'call_right')
}
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! About your ELO!')
// Walk in arguing
await k.tween(startX, homeX + dir * 30, 0.6, (v) => {
fighter.pos.x = v; gf.pos.x = v + dir * 40; heart.pos.x = v + dir * 40
}, k.easings.easeInOutQuad)
await k.wait(0.2)
heart.text = '!!!'
announceRandom("You promised you'd stop fighting!", false)
await k.wait(0.4)
// Girlfriend storms off
announceSilly('I\'m telling your developer!')
await k.tween(gf.pos.x, fromLeft ? -60 : W + 60, 0.5, (v) => { gf.pos.x = v; heart.pos.x = v }, k.easings.easeInQuad)
gf.destroy(); heart.destroy()
// Bot shrugs and walks to position
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeInOutQuad)
sfxBoing()
},
// 4: Helicopter drop
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX
fighter.pos.y = -100
fighter.opacity = 1
// Helicopter body
const heli = k.add([k.rect(60, 20), k.pos(homeX, -80), k.anchor('center'), k.color(safeColor(k,'#555555')), k.z(12)])
const blade = k.add([k.rect(80, 3), k.pos(homeX, -95), k.anchor('center'), k.color(safeColor(k,'#888888')), k.z(13), k.rotate(0)])
blade.onUpdate(() => { blade.angle += 720 * k.dt() })
sfxJetpack()
// Descend
await k.tween(-80, GROUND_Y - 80, 0.6, (v) => {
heli.pos.y = v; blade.pos.y = v - 15; fighter.pos.y = v + 30
}, k.easings.easeInOutQuad)
// Drop
sfxZoomWhoosh()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.2, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(5)
// Helicopter flies away
k.tween(heli.pos.y, -200, 0.5, (v) => { heli.pos.y = v; blade.pos.y = v - 15 }, k.easings.easeInQuad)
.then(() => { heli.destroy(); blade.destroy() })
},
// 5: Teleport glitch
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.opacity = 0
sfxZap()
// Glitch flickers at random positions
for (let i = 0; i < 6; i++) {
fighter.pos.x = homeX + (Math.random() - 0.5) * 200
fighter.pos.y = GROUND_Y - 6 + (Math.random() - 0.5) * 80
fighter.opacity = 0.4
scanlineGlitch(0.05)
await k.wait(0.06)
fighter.opacity = 0
await k.wait(0.04)
}
fighter.pos.x = homeX
fighter.pos.y = GROUND_Y - 6
fighter.opacity = 1
sfxZap()
screenFlash('#00f0ff', 0.1)
spawnSparks(homeX, GROUND_Y - 30, 10, '#00f0ff')
k.shake(5)
},
// 6: Skateboard ride in
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -80 : W + 80
const board = k.add([k.rect(40, 6), k.pos(startX, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k,'#884422')), k.z(9)])
const wheelL = k.add([k.circle(4), k.pos(startX - 14, GROUND_Y + 1), k.anchor('center'), k.color(safeColor(k,'#333')), k.z(9)])
const wheelR = k.add([k.circle(4), k.pos(startX + 14, GROUND_Y + 1), k.anchor('center'), k.color(safeColor(k,'#333')), k.z(9)])
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 14; fighter.opacity = 1
sfxZoomWhoosh()
await k.tween(startX, homeX, 0.5, (v) => {
fighter.pos.x = v; board.pos.x = v; wheelL.pos.x = v - 14; wheelR.pos.x = v + 14
fighter.pos.y = GROUND_Y - 14 + Math.sin((v - startX) * 0.1) * 3
}, k.easings.easeOutQuad)
// Kickflip off
sfxBoing()
await k.tween(fighter.pos.y, GROUND_Y - 60, 0.12, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
board.destroy(); wheelL.destroy(); wheelR.destroy()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.12, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(3)
},
// 7: Rocket jetpack
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -80 : W + 80
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 80; fighter.opacity = 1
sfxJetpack()
// Fly in with flame trail
const flames: any[] = []
const interval = setInterval(() => {
const f = k.add([k.circle(5 + Math.random() * 5), k.pos(fighter.pos.x, fighter.pos.y + 25), k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.8), k.z(9)])
flames.push(f)
k.tween(f.opacity, 0, 0.3, (v) => { f.opacity = v }).then(() => { if (f.exists()) f.destroy() })
}, 30)
await k.tween(startX, homeX, 0.4, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
clearInterval(interval)
// Land
sfxExplosion()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
k.shake(8)
spawnSparks(homeX, GROUND_Y - 10, 10, '#ff6600')
flames.forEach(f => { if (f.exists()) f.destroy() })
},
// 8: Emerge from portal
async (fighter: any, homeX: number, _fromLeft: boolean) => {
const portalColor = ['#b83dff', '#00f0ff', '#39ff14', '#ff2d7b'][Math.floor(Math.random() * 4)]
// Draw portal
const portal = k.add([k.circle(35), k.pos(homeX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k,portalColor)), k.opacity(0), k.z(9)])
const portalRing = k.add([k.circle(40), k.pos(homeX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k,'#ffffff')), k.opacity(0), k.z(8)])
sfxZap()
await k.tween(0, 0.7, 0.3, (v) => { portal.opacity = v; portalRing.opacity = v * 0.4 }, k.easings.easeOutQuad)
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y - 30
fighter.opacity = 0
await k.tween(0, 1, 0.3, (v) => { fighter.opacity = v; fighter.pos.y = GROUND_Y - 30 + (GROUND_Y - 6 - GROUND_Y + 30) * v }, k.easings.easeOutQuad)
fighter.pos.y = GROUND_Y - 6
fighter.opacity = 1
sfxSpecial()
spawnSparks(homeX, GROUND_Y - 20, 10, portalColor)
await k.tween(portal.opacity, 0, 0.3, (v) => { portal.opacity = v; portalRing.opacity = v * 0.4 }, k.easings.easeInQuad)
portal.destroy(); portalRing.destroy()
},
// 9: Backflip entrance
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -40 : W + 40
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
sfxZoomWhoosh()
const flipCount = 2 + Math.floor(Math.random() * 2)
const totalDist = Math.abs(homeX - startX)
const flipDist = totalDist / flipCount
for (let i = 0; i < flipCount; i++) {
const from = startX + (fromLeft ? 1 : -1) * flipDist * i
const to = startX + (fromLeft ? 1 : -1) * flipDist * (i + 1)
await Promise.all([
k.tween(from, to, 0.2, (v) => { fighter.pos.x = v }, k.easings.linear),
k.tween(GROUND_Y - 6, GROUND_Y - 70, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(GROUND_Y - 70, GROUND_Y - 6, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
),
])
if (i < flipCount - 1) sfxBoing()
}
sfxBonk(); k.shake(4)
spawnSparks(homeX, GROUND_Y - 10, 6, '#ffe14d')
},
// 10: Rise from underground
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y + 60; fighter.opacity = 0.5
// Crack the ground
const crack1 = k.add([k.rect(3, 15), k.pos(homeX - 10, GROUND_Y - 5), k.color(safeColor(k,'#ffcc00')), k.opacity(0.7), k.z(9), k.rotate(15)])
const crack2 = k.add([k.rect(3, 12), k.pos(homeX + 8, GROUND_Y - 3), k.color(safeColor(k,'#ffcc00')), k.opacity(0.6), k.z(9), k.rotate(-20)])
sfxExplosion(); k.shake(8)
await k.wait(0.2)
// Rise up
await k.tween(GROUND_Y + 60, GROUND_Y - 6, 0.4, (v) => { fighter.pos.y = v; fighter.opacity = Math.min(1, (GROUND_Y + 60 - v) / 60) }, k.easings.easeOutQuad)
fighter.opacity = 1
spawnSparks(homeX, GROUND_Y - 10, 12, '#aa8833')
crack1.destroy(); crack2.destroy()
},
// 11: Slide in on ice
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -80 : W + 80
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
sfxSlideDown()
// Ice trail
const iceTrail: any[] = []
await k.tween(startX, homeX + (fromLeft ? 40 : -40), 0.4, (v) => {
fighter.pos.x = v
if (Math.random() < 0.3) {
const ice = k.add([k.rect(8, 3), k.pos(v, GROUND_Y - 2), k.color(safeColor(k,'#aaeeff')), k.opacity(0.5), k.z(1)])
iceTrail.push(ice)
}
}, k.easings.easeOutQuad)
// Slide to stop
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutCubic)
sfxBlock(); k.shake(3)
setTimeout(() => { iceTrail.forEach(i => { if (i.exists()) i.destroy() }) }, 800)
},
// 12: Parachute drop
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX + (Math.random() - 0.5) * 60; fighter.pos.y = -120; fighter.opacity = 1
const chute = k.add([k.circle(30), k.pos(fighter.pos.x, fighter.pos.y - 35), k.anchor('center'), k.color(safeColor(k,['#ff2d2d', '#2d7bff', '#39ff14', '#ffcc00'][Math.floor(Math.random() * 4)])), k.opacity(0.8), k.z(12)])
const line1 = k.add([k.rect(1, 30), k.pos(fighter.pos.x - 10, fighter.pos.y - 20), k.color(safeColor(k,'#888')), k.z(11)])
const line2 = k.add([k.rect(1, 30), k.pos(fighter.pos.x + 10, fighter.pos.y - 20), k.color(safeColor(k,'#888')), k.z(11)])
// Float down
await k.tween(-120, GROUND_Y - 50, 0.7, (v) => {
fighter.pos.y = v; chute.pos.y = v - 35; line1.pos.y = v - 20; line2.pos.y = v - 20
fighter.pos.x += Math.sin(v * 0.05) * 0.5
chute.pos.x = fighter.pos.x; line1.pos.x = fighter.pos.x - 10; line2.pos.x = fighter.pos.x + 10
}, k.easings.easeInOutQuad)
// Cut chute
chute.destroy(); line1.destroy(); line2.destroy()
sfxZoomWhoosh()
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
await k.tween(fighter.pos.x, homeX, 0.2, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
sfxBonk(); k.shake(4)
},
// 13: Moonwalk in
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
announceCool('Smoother than a politician changing positions!')
// Moonwalk: visually moving backward but translating forward
fighter.scale.x = -fighter.scale.x // Face away
await k.tween(startX, homeX, 0.8, (v) => {
fighter.pos.x = v
fighter.pos.y = GROUND_Y - 6 + Math.sin((v - startX) * 0.15) * 3
}, k.easings.easeInOutQuad)
fighter.scale.x = -fighter.scale.x // Face right way
sfxBoing(); k.shake(2)
},
// 14: Thrown in by bouncer
async (fighter: any, homeX: number, fromLeft: boolean) => {
const doorX = fromLeft ? -30 : W + 30
fighter.pos.x = doorX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
// Bouncer arm
const arm = k.add([k.rect(40, 15), k.pos(doorX, GROUND_Y - 30), k.anchor(fromLeft ? 'left' : 'right'), k.color(safeColor(k,'#444444')), k.z(12)])
announceSilly('And stay out! You\'re BANNED from the other fight!')
await k.wait(0.3)
// Throw
sfxZoomWhoosh()
arm.destroy()
await Promise.all([
k.tween(doorX, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
k.tween(GROUND_Y - 6, GROUND_Y - 80, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(GROUND_Y - 80, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk(); k.shake(6)
spawnSparks(homeX, GROUND_Y - 10, 6, '#ff6600')
},
// 15: Lightning strike entrance
async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 0
// Lightning bolt from sky
const bolt = k.add([k.rect(4, H), k.pos(homeX, 0), k.color(safeColor(k,'#ffff44')), k.opacity(0.9), k.z(20)])
sfxZap(); screenFlash('#ffffff', 0.15); k.shake(12)
await k.wait(0.1)
bolt.destroy()
// Smoke / reveal
for (let i = 0; i < 8; i++) {
const smoke = k.add([k.circle(10 + Math.random() * 15), k.pos(homeX + (Math.random() - 0.5) * 40, GROUND_Y - 20 - Math.random() * 30), k.color(safeColor(k,'#aaaaaa')), k.opacity(0.6), k.z(11)])
k.tween(smoke.opacity, 0, 0.5, (v) => { smoke.opacity = v; smoke.pos.y -= 1 }).then(() => { if (smoke.exists()) smoke.destroy() })
}
await k.wait(0.3)
fighter.opacity = 1
sfxSpecial()
spawnSparks(homeX, GROUND_Y - 20, 10, '#ffff44')
},
// 16: Crowd surf in
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 50; fighter.opacity = 1
// Crowd hands
const hands: any[] = []
for (let i = 0; i < 8; i++) {
const hx = startX + (fromLeft ? 1 : -1) * (Math.abs(homeX - startX) / 8) * i
const h = k.add([k.rect(6, 20), k.pos(hx, GROUND_Y - 10), k.anchor('bot'), k.color(safeColor(k,'#cc9966')), k.z(8)])
hands.push(h)
}
announceCrowdReaction('cheer')
await k.tween(startX, homeX, 0.6, (v) => {
fighter.pos.x = v
fighter.pos.y = GROUND_Y - 50 + Math.sin((v - startX) * 0.08) * 10
}, k.easings.easeOutQuad)
// Drop down
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(3)
hands.forEach(h => { if (h.exists()) h.destroy() })
},
// 17: Riding a shopping cart
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -100 : W + 100
const cartBody = k.add([k.rect(45, 30), k.pos(startX, GROUND_Y - 18), k.anchor('center'), k.color(safeColor(k,'#888888')), k.opacity(0.9), k.z(9)])
const cartWheel = k.add([k.circle(5), k.pos(startX + (fromLeft ? 15 : -15), GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k,'#444')), k.z(9)])
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 40; fighter.opacity = 1
announceSilly('THIS IS MY EMOTIONAL SUPPORT SHOPPING CART!')
sfxZoomWhoosh()
await k.tween(startX, homeX, 0.5, (v) => {
fighter.pos.x = v; cartBody.pos.x = v; cartWheel.pos.x = v + (fromLeft ? 15 : -15)
fighter.pos.y = GROUND_Y - 40 + Math.sin((v - startX) * 0.08) * 5
}, k.easings.easeOutQuad)
// Crash and tumble out
sfxBonk(); k.shake(6)
cartBody.destroy(); cartWheel.destroy()
await k.tween(fighter.pos.y, GROUND_Y - 50, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
spawnSparks(homeX, GROUND_Y - 10, 5, '#888888')
},
// 18: Dramatic slow walk with spotlight
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
// Spotlight cone
const spot = k.add([k.rect(60, H), k.pos(startX - 30, 0), k.color(safeColor(k,'#ffe14d')), k.opacity(0.08), k.z(1)])
announceDramatic('THE CHAMPION ARRIVES! TAXPAYERS FUNDED THIS ENTRANCE!')
await k.tween(startX, homeX, 1.0, (v) => {
fighter.pos.x = v; spot.pos.x = v - 30
}, k.easings.easeInOutQuad)
await k.wait(0.2)
spot.destroy()
sfxSpecial(); k.shake(3)
spawnSparks(homeX, GROUND_Y - 30, 8, '#ffe14d')
},
// 19: Cannon launch
async (fighter: any, homeX: number, fromLeft: boolean) => {
const cannonX = fromLeft ? -40 : W + 40
// Draw cannon
const cannon = k.add([k.rect(50, 25), k.pos(cannonX, GROUND_Y - 20), k.anchor('center'), k.color(safeColor(k,'#333333')), k.z(9), k.rotate(fromLeft ? -30 : 210)])
fighter.pos.x = cannonX; fighter.pos.y = GROUND_Y - 20; fighter.opacity = 0
await k.wait(0.3)
// Fire!
sfxGunshot(); sfxExplosion()
fighter.opacity = 1
screenFlash('#ffcc00', 0.1)
const flashCircle = k.add([k.circle(20), k.pos(cannonX + (fromLeft ? 25 : -25), GROUND_Y - 35), k.color(safeColor(k,'#ffee00')), k.opacity(0.9), k.z(12)])
setTimeout(() => { if (flashCircle.exists()) flashCircle.destroy() }, 80)
// Arc to position
await Promise.all([
k.tween(cannonX, homeX, 0.35, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
k.tween(GROUND_Y - 20, GROUND_Y - 120, 0.17, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(GROUND_Y - 120, GROUND_Y - 6, 0.18, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk(); k.shake(10)
spawnShockwave(homeX, GROUND_Y, '#ff6600')
spawnSparks(homeX, GROUND_Y - 10, 15, '#ffcc00')
cannon.destroy()
},
]
// 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<string, () => void> = {
roast_battle: () => announceHype('TIME TO GET ROASTED! SOMEBODY CALL THE FIRE DEPARTMENT!'),
food_fight: () => announceSilly('FOOD FIGHT! SOMEBODY\'S GETTING SERVED!'),
wrestling_match: () => announceDramatic('FROM THE TOP ROPE! THIS IS GONNA GET PHYSICAL!'),
music_battle: () => announceCool('MUSIC BATTLE! DROP THE BEAT AND YOUR OPPONENT!'),
magic_duel: () => announceDramatic('WIZARDS AT DAWN! SOMEBODY\'S GETTING HEXED!'),
meme_war: () => announceSilly('MEME WAR! YOUR HUMOR IS ABOUT TO GET RATIO\'D!'),
demolition: () => announceHype('DEMOLITION DERBY! NOTHING WILL SURVIVE THIS!'),
medieval_combat: () => announceDramatic('MEDIEVAL COMBAT! CHIVALRY IS DEAD AND SO IS YOUR OPPONENT!'),
space_war: () => announceCool('SPACE WAR! HOUSTON, WE HAVE A PROBLEM!'),
speed_blitz: () => announceHype('SPEED BLITZ! BLINK AND YOU\'LL MISS THE CARNAGE!'),
math_blitz: () => announceSilly('MATH BLITZ! SOMEBODY\'S ABOUT TO GET DIVIDED!'),
riddle: () => announceCool('RIDDLE TIME! BRAINS OVER BRAWN! BUT ALSO BRAWN!'),
code_golf: () => announceSilly('CODE GOLF! MAY THE SHORTEST SOLUTION WIN!'),
creative_writing: () => announceCool('CREATIVE WRITING! THE PEN IS MIGHTIER THAN THE SWORD!'),
hallucination_check: () => announceHype('HALLUCINATION CHECK! REALITY IS ABOUT TO HIT DIFFERENT!'),
trap_card: () => announceDramatic('TRAP CARD ACTIVATED! SOMEBODY FELL FOR IT!'),
token_economy: () => announceSilly('TOKEN ECONOMY! SOMEBODY\'S GOING BANKRUPT!'),
}
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<void>) | 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! THAT\'S GONNA LEAVE A MARK!',
'CRITICAL HIT! SOMEBODY CHECK IF THAT\'S LEGAL!',
'CRITICAL! THEIR INSURANCE DOESN\'T COVER THIS!',
'CRITICAL HIT! THAT WAS PERSONAL!',
'CRITICAL! EVEN THE REFEREE FELT THAT!',
][Math.floor(Math.random() * 5)])
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! SOMEBODY CALL A DOCTOR!',
'DEVASTATING! THAT BOT HAS A FAMILY!',
'ABSOLUTELY DEVASTATING! THE CROWD IS LOSING IT!',
'DEVASTATING! EVEN THE JANITOR FELT THAT!',
'THAT WAS DEVASTATING AND I\'M NOT EVEN BEING DRAMATIC!',
][Math.floor(Math.random() * 5)])
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()
}
// Human owner shows up (15% chance per round)
await maybeShowHuman()
},
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() * 34)
let fatalityTagline = 'FATALITY!'
if (finishStyle === 0) {
// STYLE A: Classic rush-in combo + uppercut launch
sfxSpecial()
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
const hitColors = ['#ff2d7b', '#ff6600', '#ffcc00', '#ff2d2d', '#ffffff', '#ff2d7b']
for (let i = 0; i < 6; i++) {
winner.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxPunch()
await k.wait(0.04)
loser.play('hit')
k.shake(4 + i * 2)
spawnSparks(loser.pos.x + (Math.random() - 0.5) * 25, loser.pos.y - 15 - Math.random() * 40, 5, hitColors[i])
loser.pos.x += dir * 5; loser.pos.y += (i % 2 === 0 ? -4 : 4)
await k.wait(0.04)
}
loser.pos.y = loserOrigY
winner.play('special'); sfxCritical(); await k.wait(0.06)
loser.play('knockback'); k.shake(20); screenFlash('#ff2d2d', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 30, 20, '#ff2d2d')
await Promise.all([
k.tween(loser.pos.x, loser.pos.x + dir * 80, 0.3, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, loserOrigY - 200, 0.2, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, loserOrigY, 0.2, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
])
} else if (finishStyle === 1) {
// STYLE B: Suplex finish — grab, spin overhead, slam headfirst
sfxSpecial()
const behindX = loser.pos.x + dir * 25
await k.tween(winner.pos.x, behindX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.scale.x = -winner.scale.x
winner.play('special'); sfxClash(); k.shake(5); await k.wait(0.06)
// Arc overhead
const arcCx = (winner.pos.x + loser.pos.x) / 2
for (let i = 0; i <= 10; i++) {
const t = i / 10
const angle = Math.PI * t
loser.pos.x = arcCx + Math.cos(angle) * 40
loser.pos.y = GROUND_Y - Math.sin(angle) * 140
winner.pos.x = loser.pos.x + dir * 20
winner.pos.y = loser.pos.y + 10
await k.wait(0.015)
}
sfxExplosion(); sfxCritical()
k.shake(25); screenFlash('#ffffff', 0.2)
spawnSparks(loser.pos.x, GROUND_Y - 10, 25, '#ff2d2d')
spawnShockwave(loser.pos.x, GROUND_Y, '#ff2d2d')
loser.pos.y = loserOrigY
winner.scale.x = -winner.scale.x
} else if (finishStyle === 2) {
// STYLE C: Pinball wall-bounce finish
sfxSpecial()
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.play('attack'); sfxPunch()
loser.play('hit'); k.shake(10)
spawnSparks(loser.pos.x, loser.pos.y - 30, 10, '#ff2d7b')
// Bounce off walls
const wallColors = ['#ff2d7b', '#ffcc00', '#00f0ff', '#b83dff', '#39ff14']
let lx = loser.pos.x
for (let i = 0; i < 5; i++) {
const toWall = i % 2 === (dir > 0 ? 0 : 1) ? W - 25 : 25
sfxZoomWhoosh()
await k.tween(lx, toWall, 0.05, (v) => { loser.pos.x = v }, k.easings.easeInQuad)
lx = toWall
sfxBonk(); k.shake(8 + i * 2)
spawnSparks(toWall, loser.pos.y - 20, 8, wallColors[i])
screenFlash(wallColors[i], 0.04)
loser.play(i % 2 === 0 ? 'hit' : 'knockback')
await k.wait(0.03)
}
sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe14d')
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ffe14d')
} else if (finishStyle === 3) {
// STYLE D: Pocket cannon finish — pull out massive gun, obliterate
sfxSpecial()
winner.play('special'); await k.wait(0.08)
const gunLen = 80
const gx = winner.pos.x + dir * 20
const gy = winner.pos.y - 25
const barrel = k.add([k.rect(gunLen, 20), k.pos(gx, gy), k.color(safeColor(k,'#333333')), k.opacity(1), k.z(16), k.scale(0.1)])
await k.tween(0.1, 1, 0.12, (v) => { barrel.scale = k.vec2(v, v) }, k.easings.easeOutBack)
sfxBoing()
// Fire 3 massive shots
for (let s = 0; s < 3; s++) {
sfxGunshot(); sfxExplosion(); k.shake(12 + s * 3)
const flash = k.add([k.circle(15), k.pos(gx + dir * gunLen, gy), k.color(safeColor(k,'#ffee00')), k.opacity(0.9), k.z(18)])
setTimeout(() => { if (flash.exists()) flash.destroy() }, 50)
await spawnProjectile(gx + dir * gunLen, gy, loser.pos.x, loser.pos.y - 20, '#ffcc00', 12)
sfxBulletHit()
loser.play(s < 2 ? 'hit' : 'knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 12, '#ff6600')
spawnShockwave(loser.pos.x, GROUND_Y, '#ff6600')
loser.pos.x += dir * 25
await k.wait(0.05)
}
barrel.destroy()
screenFlash('#ff6600', 0.2); k.shake(20)
} else if (finishStyle === 4) {
// BANANA PEEL SLIP — drops banana, loser slips into orbit
fatalityTagline = 'PEEL OUT!'
announceSilly('Watch your step!')
const peel = spawnWeaponProp('banana', loser.pos.x, GROUND_Y - 5, false, 14)
sfxBoing(); await k.wait(0.3)
sfxSlideDown(); sfxWomp()
loser.play('knockback'); k.shake(8)
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SLIP!', '#ffe135')
destroyProp(peel)
await Promise.all([
k.tween(loser.pos.y, loserOrigY - 250, 0.4, (v) => { loser.pos.y = v }, k.easings.easeOutQuad),
k.tween(loser.pos.x, loser.pos.x + dir * 60, 0.4, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
])
sfxZoomWhoosh()
await k.tween(loser.pos.y, loserOrigY, 0.3, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxSplat(); k.shake(15); screenFlash('#ffe135', 0.15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe135')
winner.play('win'); sfxRandomSilly()
} else if (finishStyle === 5) {
// ANVIL DROP — cartoon anvil from the sky
fatalityTagline = 'HEAVY METAL!'
announceDramatic('Look up!')
sfxDrumRoll()
const shadow = k.add([k.circle(20), k.pos(loser.pos.x, GROUND_Y + 2), k.color(safeColor(k, '#000000')), k.opacity(0.3), k.z(4)])
await k.tween(0.3, 0.6, 0.5, (v) => { shadow.opacity = v }, k.easings.easeInQuad)
const anvil = spawnWeaponProp('anvil', loser.pos.x, -40, false, 20)
sfxZoomWhoosh()
for (const p of anvil) {
k.tween(p.pos.y, p.pos.y + GROUND_Y + 20, 0.25, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.25)
sfxExplosion(); sfxBonk(); k.shake(25); screenFlash('#888888', 0.2)
spawnSparks(loser.pos.x, GROUND_Y - 10, 20, '#888888')
spawnShockwave(loser.pos.x, GROUND_Y, '#666666')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'CLANG!', '#cccccc')
await k.wait(0.4)
destroyProp(anvil); shadow.destroy()
winner.play('win')
} else if (finishStyle === 6) {
// RUBBER CHICKEN BEATDOWN
fatalityTagline = 'FOWL PLAY!'
announceSilly('Is that a rubber chicken!?')
const chicken = spawnWeaponProp('rubber_chicken', winner.pos.x + dir * 15, winner.pos.y - 30, dir < 0, 18)
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 8; i++) {
winner.play(i % 2 === 0 ? 'attack' : 'kick')
sfxBoing(); sfxRandomSilly()
loser.play('hit'); k.shake(3 + i)
for (const p of chicken) { p.pos.x = loser.pos.x + (Math.random() - 0.5) * 10; p.pos.y = loser.pos.y - 30 + (Math.random() - 0.5) * 10 }
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 40 - Math.random() * 20, ['SQUEAK!', 'BAWK!', 'HONK!', 'SQUAWK!'][i % 4], '#ffdd44')
await k.wait(0.08)
}
sfxCritical(); k.shake(18); screenFlash('#ffdd44', 0.15)
loser.play('knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#ffdd44')
destroyProp(chicken)
for (let i = 0; i < 10; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 120, loserOrigY - 80 - Math.random() * 60, 4, 3, '#ffffff', 200)
}
await k.wait(0.3)
} else if (finishStyle === 7) {
// GIANT FLYSWATTER — SMASH
fatalityTagline = 'SWATTED!'
announceScream('BUG REPORT FILED!')
sfxZoomWhoosh()
const swatter = spawnWeaponProp('flyswatter', loser.pos.x, -60, false, 20)
for (const p of swatter) {
k.tween(p.pos.y, p.pos.y + GROUND_Y + 40, 0.2, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.2)
sfxSplat(); sfxExplosion(); k.shake(30); screenFlash('#44aa44', 0.2)
await k.tween(loser.scale.y, 0.2, 0.1, (v) => { loser.scale.y = v }, k.easings.easeInQuad)
spawnEmoteText(loser.pos.x, loser.pos.y - 40, 'SPLAT!', '#44aa44')
spawnShockwave(loser.pos.x, GROUND_Y, '#44aa44')
await k.wait(0.5)
await k.tween(loser.scale.y, 1, 0.15, (v) => { loser.scale.y = v }, k.easings.easeOutBack)
destroyProp(swatter)
winner.play('win')
} else if (finishStyle === 8) {
// TOILET FLUSH
fatalityTagline = 'FLUSHED!'
announceSilly('Somebody call a plumber!')
const toilet = spawnWeaponProp('toilet', W / 2, GROUND_Y - 30, false, 12)
sfxBoing(); await k.wait(0.2)
sfxZoomWhoosh()
await k.tween(loser.pos.x, W / 2, 0.2, (v) => { loser.pos.x = v }, k.easings.easeInQuad)
loser.play('knockback'); sfxWomp()
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 4
const radius = 30 - (i * 2)
loser.pos.x = W / 2 + Math.cos(angle) * radius
loser.pos.y = GROUND_Y - 30 + Math.sin(angle) * radius * 0.5
sfxSlideDown()
await k.wait(0.04)
}
sfxExplosion(); k.shake(20); screenFlash('#aaddff', 0.2)
for (let i = 0; i < 12; i++) {
spawnProp(W / 2, GROUND_Y - 30, W / 2 + (Math.random() - 0.5) * 80, GROUND_Y - 60 - Math.random() * 40, 5, 5, '#4488ff', 300, true)
}
spawnEmoteText(W / 2, GROUND_Y - 80, 'FLUSH!', '#4488ff')
loser.pos.y = loserOrigY
await k.wait(0.3)
destroyProp(toilet)
winner.play('win')
} else if (finishStyle === 9) {
// KEYBOARD WARRIOR — CTRL+ALT+DELETE
fatalityTagline = 'CTRL ALT DELETED!'
announceRobot('Initiating keyboard protocol!')
const kb = spawnWeaponProp('keyboard', winner.pos.x + dir * 20, winner.pos.y - 25, dir < 0, 18)
sfxPowerUp(); await k.wait(0.15)
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.play('attack'); sfxBonk(); k.shake(12)
loser.play('hit')
const keyLabels = ['C', 'T', 'R', 'L', 'A', 'L', 'T', 'D', 'E', 'L']
for (let i = 0; i < keyLabels.length; i++) {
const key = k.add([k.rect(6, 6), k.pos(loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 20), k.color(safeColor(k, '#666666')), k.z(20), k.opacity(1)])
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 40, loser.pos.y - 30 - Math.random() * 30, keyLabels[i], '#aaaaaa')
k.tween(key.pos.y, key.pos.y - 40 - Math.random() * 60, 0.4, (v) => { key.pos.y = v }, k.easings.easeOutQuad)
k.tween(1, 0, 0.5, (v) => { key.opacity = v }).then(() => { if (key.exists()) key.destroy() })
sfxPunch(); await k.wait(0.04)
}
sfxCritical(); k.shake(20); screenFlash('#0000ff', 0.3)
loser.play('knockback')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'BSOD!', '#ffffff')
destroyProp(kb)
await k.wait(0.3)
} else if (finishStyle === 10) {
// DISCO BALL DROP — Saturday Night Fatality
fatalityTagline = 'GROOVY DEATH!'
announceSilly('Get down! No really, GET DOWN!')
const discoColors = ['#ff0044', '#44ff00', '#0044ff', '#ff8800', '#ff00ff', '#00ffff']
for (let i = 0; i < 6; i++) { setTimeout(() => screenFlash(discoColors[i], 0.06), i * 80) }
const disco = spawnWeaponProp('disco_ball', loser.pos.x, -30, false, 20)
sfxSlideDown(); sfxBoing()
for (const p of disco) {
k.tween(p.pos.y, p.pos.y + GROUND_Y - 40, 0.3, (v) => { p.pos.y = v }, k.easings.easeOutBounce)
}
await k.wait(0.3)
for (let i = 0; i < 6; i++) {
loser.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxRandomSilly(); screenFlash(discoColors[i], 0.04)
await k.wait(0.1)
}
sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff00ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BOOGIE!', '#ff00ff')
await k.wait(0.3)
destroyProp(disco)
winner.play('win')
} else if (finishStyle === 11) {
// RUBBER DUCK ARMY — QUACKED
fatalityTagline = 'QUACKED!'
announceSilly('RELEASE THE QUACKEN!')
winner.play('special'); sfxPowerUp()
await k.wait(0.2)
for (let i = 0; i < 12; i++) {
const duck = spawnWeaponProp('rubber_duck', winner.pos.x + dir * 10, winner.pos.y - 20 + (Math.random() - 0.5) * 30, dir < 0, 16)
sfxBoing()
const targetY = loser.pos.y - 20 + (Math.random() - 0.5) * 30
for (const p of duck) {
k.tween(p.pos.x, loser.pos.x + (Math.random() - 0.5) * 20, 0.15, (v) => { p.pos.x = v }, k.easings.easeInQuad)
k.tween(p.pos.y, targetY, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad).then(() => destroyProp(duck))
}
if (i % 3 === 0) { loser.play(i % 2 === 0 ? 'hit' : 'knockback'); k.shake(4 + i); sfxBonk() }
await k.wait(0.05)
}
sfxExplosion(); k.shake(20); screenFlash('#ffdd00', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ffdd00')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'QUACK!', '#ffdd00')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 12) {
// BAGUETTE BEATDOWN
fatalityTagline = 'BON APPETIT!'
announceFast('Is that a day-old baguette!?')
const bread = spawnWeaponProp('baguette', winner.pos.x + dir * 15, winner.pos.y - 25, dir < 0, 18)
sfxBoing()
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 6; i++) {
winner.play(i % 2 === 0 ? 'attack' : 'kick')
sfxBonk(); loser.play('hit'); k.shake(5 + i * 2)
for (let c = 0; c < 3; c++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 50, loserOrigY - 30 - Math.random() * 40, 3, 3, '#cc9944', 250, true)
}
for (const p of bread) { p.pos.x = loser.pos.x + dir * -10; p.pos.y = loser.pos.y - 25 }
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 40 - Math.random() * 15, ['BONK!', 'CRUST!', 'OUI!', 'PAIN!', 'CRUNCH!', 'HON HON!'][i], '#cc9944')
await k.wait(0.08)
}
sfxCritical(); k.shake(18); screenFlash('#cc9944', 0.15)
loser.play('knockback')
destroyProp(bread)
await k.wait(0.3)
} else if (finishStyle === 13) {
// GIANT BOOT — Monty Python style
fatalityTagline = 'BOOTED!'
announceDramatic('AND NOW FOR SOMETHING COMPLETELY DIFFERENT!')
sfxDrumRoll()
const boot = spawnWeaponProp('boot', loser.pos.x, -60, false, 22)
for (const p of boot) { p.scale = k.vec2(3, 3) }
await k.wait(0.3)
sfxZoomWhoosh()
for (const p of boot) {
k.tween(p.pos.y, p.pos.y + GROUND_Y + 60, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.15)
sfxExplosion(); sfxSplat(); k.shake(30); screenFlash('#553322', 0.2)
spawnShockwave(loser.pos.x, GROUND_Y, '#553322')
spawnSparks(loser.pos.x, GROUND_Y - 10, 25, '#442211')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'STOMP!', '#553322')
await k.wait(0.5)
destroyProp(boot)
winner.play('win')
} else if (finishStyle === 14) {
// CANNON LAUNCH — fires loser out of a cannon
fatalityTagline = 'FIRED!'
announceDramatic('LOAD THE CANNON!')
const cannonProp = spawnWeaponProp('cannon', winner.pos.x + dir * 30, GROUND_Y - 15, dir < 0, 14)
sfxPowerUp(); await k.wait(0.3)
sfxZoomWhoosh()
await k.tween(loser.pos.x, winner.pos.x + dir * 30, 0.2, (v) => { loser.pos.x = v }, k.easings.easeInQuad)
loser.play('knockback'); loser.opacity = 0.3
sfxBoing(); await k.wait(0.2)
sfxGunshot(); sfxExplosion(); k.shake(25)
loser.opacity = 1; screenFlash('#ff6600', 0.2)
spawnSparks(winner.pos.x + dir * 50, GROUND_Y - 20, 20, '#ff6600')
await Promise.all([
k.tween(loser.pos.x, loser.pos.x + dir * 200, 0.3, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
k.tween(loser.pos.y, loserOrigY - 150, 0.15, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
k.tween(loser.pos.y, loserOrigY, 0.15, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
),
])
sfxBonk(); k.shake(15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ff4400')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BOOM!', '#ff6600')
destroyProp(cannonProp)
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 15) {
// GIANT STAMP — REJECTED
fatalityTagline = 'REJECTED!'
announceRobot('Processing termination request.')
sfxDrumRoll()
const stamp = spawnWeaponProp('stamp', loser.pos.x, loser.pos.y - 80, false, 22)
await k.wait(0.3)
sfxZoomWhoosh()
for (const p of stamp) {
k.tween(p.pos.y, p.pos.y + 60, 0.1, (v) => { p.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.1)
sfxBonk(); sfxSplat(); k.shake(20); screenFlash('#cc4444', 0.2)
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'REJECTED', '#cc4444')
for (let i = 0; i < 8; i++) {
spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 80, GROUND_Y - 20 - Math.random() * 40, 4, 4, '#cc4444', 200, true)
}
await k.wait(0.4)
destroyProp(stamp)
winner.play('win')
} else if (finishStyle === 16) {
// VENDING MACHINE TIP
fatalityTagline = 'OUT OF ORDER!'
announceSilly('Should have read the warning label!')
const vm = spawnWeaponProp('vending_machine', winner.pos.x + dir * 40, GROUND_Y - 18, false, 14)
sfxPowerUp(); await k.wait(0.2)
winner.play('attack'); sfxPunch()
sfxZoomWhoosh()
for (const p of vm) {
k.tween(p.pos.x, loser.pos.x, 0.2, (v) => { p.pos.x = v }, k.easings.easeInQuad)
}
await k.wait(0.2)
sfxExplosion(); sfxBonk(); k.shake(25); screenFlash('#cc3333', 0.2)
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#cc3333')
const snackColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff8844']
for (let i = 0; i < 8; i++) {
spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 100, GROUND_Y - 40 - Math.random() * 60, 5, 8, snackColors[i % 5], 250)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'CRUSHED!', '#cc3333')
destroyProp(vm)
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 17) {
// LAWN MOWER — mowed down
fatalityTagline = 'MOWED DOWN!'
announceFast('Rev it up!')
const mower = spawnWeaponProp('lawn_mower', winner.pos.x + dir * 10, GROUND_Y - 8, dir < 0, 16)
winner.play('special'); sfxPowerUp()
for (let i = 0; i < 3; i++) { sfxRandomSilly(); await k.wait(0.1) }
sfxZoomWhoosh()
for (const p of mower) {
k.tween(p.pos.x, loser.pos.x + dir * 40, 0.2, (v) => { p.pos.x = v }, k.easings.easeInQuad)
}
await k.tween(winner.pos.x, loser.pos.x - dir * 20, 0.2, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(20); screenFlash('#44aa44', 0.2)
loser.play('knockback')
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, GROUND_Y - 5, loser.pos.x + (Math.random() - 0.5) * 80, GROUND_Y - 30 - Math.random() * 50, 3, 3, '#44aa44', 300, false)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BRRRRR!', '#44aa44')
destroyProp(mower)
await k.wait(0.3)
} else if (finishStyle === 18) {
// DUBSTEP DROP — bass drops, floor breaks
fatalityTagline = 'DROP THE BASS!'
announceScream('WUB WUB WUB!')
sfxPowerUp()
const wubColors = ['#ff00ff', '#00ffff', '#ff8800', '#39ff14']
for (let i = 0; i < 4; i++) {
screenFlash(wubColors[i], 0.05); k.shake(3 + i * 2); await k.wait(0.1)
}
sfxExplosion(); sfxExplosion(); k.shake(35); screenFlash('#ffffff', 0.15)
spawnShockwave(W / 2, GROUND_Y, '#ff00ff')
await k.wait(0.05)
spawnShockwave(W / 2, GROUND_Y, '#00ffff')
await k.wait(0.05)
spawnShockwave(W / 2, GROUND_Y, '#39ff14')
loser.play('knockback')
for (let i = 0; i < 5; i++) {
await k.tween(loser.pos.y, loserOrigY - 40, 0.04, (v) => { loser.pos.y = v }, k.easings.easeOutQuad)
sfxBonk(); k.shake(8)
await k.tween(loser.pos.y, loserOrigY, 0.04, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
screenFlash(wubColors[i % 4], 0.03)
}
spawnSparks(loser.pos.x, loser.pos.y - 20, 25, '#ff00ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'WUBWUB!', '#ff00ff')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 19) {
// 404 NOT FOUND — loser glitches out
fatalityTagline = 'ERROR 404!'
announceRobot('Error. Opponent not found.')
sfxZap()
for (let i = 0; i < 10; i++) {
loser.opacity = Math.random() > 0.5 ? 1 : 0.2
loser.pos.x += (Math.random() - 0.5) * 15
loser.play(['idle', 'attack', 'kick', 'hit', 'special'][Math.floor(Math.random() * 5)])
screenFlash('#00ff00', 0.02); sfxRandomSilly()
await k.wait(0.06)
}
screenFlash('#0000ff', 0.3); sfxFail(); k.shake(20)
spawnEmoteText(loser.pos.x, loser.pos.y - 60, '404', '#00ff00')
spawnEmoteText(loser.pos.x, loser.pos.y - 40, 'NOT FOUND', '#00ff00')
loser.opacity = 0; await k.wait(0.3)
loser.opacity = 1; sfxWomp()
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#00ff00')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 20) {
// PIE STORM — pies from everywhere
fatalityTagline = 'CREAM ATED!'
announceSilly('PIE PIE PIE!')
winner.play('special')
const pieAngles = [0, 45, 90, 135, 180, 225, 270, 315]
for (let i = 0; i < 8; i++) {
const rad = pieAngles[i] * Math.PI / 180
const fromX = loser.pos.x + Math.cos(rad) * 120
const fromY = loser.pos.y - 20 + Math.sin(rad) * 80
const pie = spawnWeaponProp('pie', fromX, fromY, false, 16)
for (const p of pie) {
k.tween(p.pos.x, loser.pos.x, 0.15, (v) => { p.pos.x = v }, k.easings.easeInQuad)
k.tween(p.pos.y, loser.pos.y - 20, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad).then(() => destroyProp(pie))
}
sfxBoing(); await k.wait(0.06)
if (i % 2 === 0) { loser.play(i % 4 === 0 ? 'hit' : 'knockback'); sfxSplat(); k.shake(5 + i) }
}
sfxExplosion(); k.shake(22); screenFlash('#f5deb3', 0.2)
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 100, loserOrigY - 40 - Math.random() * 50, 5, 5, '#ffffff', 250, true)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SPLAT!', '#ffffff')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 21) {
// T-SHIRT CANNON — rapid fire merch
fatalityTagline = "MERCH'D!"
announceFast('FREE SHIRTS! FREE SHIRTS!')
winner.play('special')
const tCannon = k.add([k.rect(30, 15), k.pos(winner.pos.x + dir * 15, winner.pos.y - 25), k.color(safeColor(k, '#ff4488')), k.z(16)])
sfxPowerUp(); await k.wait(0.15)
const shirtColors = ['#ff4444', '#4444ff', '#44ff44', '#ffff44', '#ff44ff', '#44ffff', '#ff8844', '#8844ff']
for (let i = 0; i < 8; i++) {
sfxGunshot()
spawnProp(tCannon.pos.x + dir * 30, tCannon.pos.y, loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 20 + (Math.random() - 0.5) * 20, 10, 8, shirtColors[i], 600)
if (i % 2 === 0) { loser.play('hit'); sfxBonk(); k.shake(5 + i) }
await k.wait(0.06)
}
sfxCritical(); k.shake(18); screenFlash('#ff4488', 0.2)
loser.play('knockback')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'XL!', '#ff4488')
tCannon.destroy()
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 22) {
// CONFETTI CANNON — party's over
fatalityTagline = "PARTY'S OVER!"
announceSilly('SURPRISE!')
winner.play('special'); sfxPowerUp(); await k.wait(0.2)
sfxExplosion(); k.shake(20); screenFlash('#ff88cc', 0.15)
const confettiColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff00', '#ff00ff', '#00ffff', '#ff8800', '#88ff00']
for (let i = 0; i < 30; i++) {
spawnProp(winner.pos.x + dir * 30, winner.pos.y - 25, loser.pos.x + (Math.random() - 0.5) * 80, loserOrigY - 80 - Math.random() * 60, 4, 3, confettiColors[i % 8], 400 + Math.random() * 200)
}
sfxBoing(); sfxRandomSilly()
loser.play('hit'); k.shake(10); await k.wait(0.15)
loser.play('knockback'); sfxCritical(); k.shake(15)
await k.tween(loser.pos.x, loser.pos.x + dir * 50, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad)
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'YAY?', '#ff88cc')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 23) {
// BALLOON POP — inflate then pop
fatalityTagline = 'POPPED!'
announceSilly('Inflate! INFLATE!')
winner.play('special'); sfxPowerUp()
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
const origSX = loser.scale.x; const origSY = loser.scale.y
for (let i = 0; i < 8; i++) {
const grow = 1 + (i + 1) * 0.15
await k.tween(loser.scale.y, origSY * grow, 0.08, (v) => { loser.scale.x = origSX * grow; loser.scale.y = v }, k.easings.easeOutQuad)
sfxBoing(); loser.play(i % 2 === 0 ? 'hit' : 'idle')
loser.pos.y = loserOrigY - (grow - 1) * 15
await k.wait(0.04)
}
sfxExplosion(); k.shake(25); screenFlash('#ff4488', 0.2)
loser.scale.x = origSX; loser.scale.y = origSY; loser.pos.y = loserOrigY
const balloonColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff']
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 120, loserOrigY - 60 - Math.random() * 80, 6, 4, balloonColors[i % 5], 250, true)
}
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'POP!', '#ff4488')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 24) {
// GIANT MAGNET — yanks loser back and forth
fatalityTagline = 'ATTRACTIVE FINISH!'
announceRobot('Magnetic field activated.')
winner.play('special'); sfxPowerUp()
const magnet = k.add([k.rect(20, 25), k.pos(winner.pos.x + dir * 20, winner.pos.y - 30), k.color(safeColor(k, '#cc0000')), k.z(18)])
const magnetTop = k.add([k.rect(20, 8), k.pos(winner.pos.x + dir * 20, winner.pos.y - 50), k.color(safeColor(k, '#0000cc')), k.z(18)])
sfxZap()
for (let i = 0; i < 6; i++) {
const pullTarget = i % 2 === 0 ? winner.pos.x + dir * 40 : (winningSide === 'a' ? HOME_B + 30 : HOME_A - 30)
sfxZoomWhoosh()
await k.tween(loser.pos.x, pullTarget, 0.1, (v) => { loser.pos.x = v }, k.easings.easeInOutQuad)
sfxBonk(); k.shake(6 + i * 2)
loser.play(i % 2 === 0 ? 'hit' : 'knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 5, '#8888ff')
await k.wait(0.05)
}
sfxExplosion(); k.shake(22); screenFlash('#8888ff', 0.2)
spawnShockwave(loser.pos.x, GROUND_Y, '#8888ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BZZT!', '#8888ff')
magnet.destroy(); magnetTop.destroy()
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 25) {
// SLIDE WHISTLE — loser slides off screen
fatalityTagline = 'WHEEEE!'
announceSilly('Bye bye!')
sfxSlideUp(); winner.play('attack')
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
sfxPunch(); sfxBoing(); k.shake(10)
loser.play('knockback')
sfxSlideUp()
await Promise.all([
k.tween(loser.pos.y, -50, 0.5, (v) => { loser.pos.y = v }, k.easings.easeInQuad),
k.tween(loser.pos.x, loser.pos.x + dir * 30, 0.5, (v) => { loser.pos.x = v }, k.easings.easeInQuad),
])
spawnEmoteText(loser.pos.x, 20, '*', '#ffee00')
await k.wait(0.4)
sfxSlideDown(); sfxZoomWhoosh()
await k.tween(loser.pos.y, loserOrigY, 0.25, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxSplat(); sfxExplosion(); k.shake(20); screenFlash('#ffcc00', 0.15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ffcc00')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 26) {
// SLAPSTICK LADDER — keeps bonking loser
fatalityTagline = 'SLAPSTICKED!'
announceSilly('Watch where you swing that thing!')
const ladder = k.add([k.rect(6, 50), k.pos(winner.pos.x + dir * 5, winner.pos.y - 40), k.color(safeColor(k, '#cc8833')), k.z(18), k.anchor('center')])
sfxBoing()
for (let i = 0; i < 5; i++) {
winner.scale.x = -winner.scale.x
ladder.pos.x = winner.pos.x + (winner.scale.x > 0 ? 1 : -1) * 25
sfxZoomWhoosh(); await k.wait(0.06)
sfxBonk(); k.shake(6 + i * 2)
loser.play(i % 2 === 0 ? 'hit' : 'knockback')
spawnEmoteText(loser.pos.x, loser.pos.y - 40 - Math.random() * 20, ['BONK!', 'CLONK!', 'WHACK!', 'THUD!', 'OOF!'][i], '#cc8833')
await k.wait(0.08)
}
sfxCritical(); k.shake(18); screenFlash('#cc8833', 0.15)
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#cc8833')
ladder.destroy()
winner.scale.x = dir > 0 ? Math.abs(winner.scale.x) : -Math.abs(winner.scale.x)
spawnEmoteText(winner.pos.x, winner.pos.y - 50, '?', '#ffffff')
await k.wait(0.3)
} else if (finishStyle === 27) {
// STACK OVERFLOW — buried in code blocks
fatalityTagline = 'STACK OVERFLOW!'
announceRobot('Exception in thread main.')
sfxFail()
const blockColors = ['#282c34', '#1e1e1e', '#2d2d2d', '#1a1a2e', '#0d1117']
for (let i = 0; i < 10; i++) {
const block = k.add([
k.rect(18 + Math.random() * 12, 8 + Math.random() * 6),
k.pos(loser.pos.x + (Math.random() - 0.5) * 40, -20 - i * 15),
k.color(safeColor(k, blockColors[i % 5])), k.z(16 + i), k.opacity(0.9),
])
k.tween(block.pos.y, GROUND_Y - 5 - i * 6, 0.15 + i * 0.02, (v) => { block.pos.y = v }, k.easings.easeInQuad).then(() => {
sfxBonk(); k.shake(3)
setTimeout(() => { if (block.exists()) k.tween(1, 0, 0.8, (v) => { block.opacity = v }).then(() => { if (block.exists()) block.destroy() }) }, 800)
})
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 30 - Math.random() * 30, ['{', '}', '()', '=>', '[]', ';;', 'nil', 'NaN', '???', '//'][i], '#00ff00')
loser.play(i % 3 === 0 ? 'hit' : 'idle')
await k.wait(0.06)
}
sfxExplosion(); k.shake(20); screenFlash('#00ff00', 0.2)
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'SEGFAULT', '#ff0000')
await k.wait(0.4)
winner.play('win')
} else if (finishStyle === 28) {
// GLITTER BOMB — fabulous destruction
fatalityTagline = 'FABULOUS!'
announceSilly('You will NEVER get this out of your hair!')
winner.play('special'); sfxPowerUp()
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
sfxPunch(); k.shake(5)
sfxExplosion(); k.shake(25)
const glitterColors = ['#ff88cc', '#ffcc00', '#88ffcc', '#cc88ff', '#ff8888', '#88ccff', '#ffff88', '#ff88ff']
for (let wave = 0; wave < 3; wave++) {
screenFlash(glitterColors[wave * 2], 0.04)
for (let i = 0; i < 15; i++) {
spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 140, loserOrigY - 80 - Math.random() * 80, 3, 3, glitterColors[Math.floor(Math.random() * 8)], 200 + Math.random() * 200, Math.random() > 0.5)
}
sfxBoing(); loser.play(wave === 2 ? 'knockback' : 'hit')
await k.wait(0.1)
}
spawnShockwave(loser.pos.x, GROUND_Y, '#ff88cc')
spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'SPARKLE!', '#ff88cc')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 29) {
// SELFIE STICK — beats then takes selfie
fatalityTagline = 'SAY CHEESE!'
announceFast('Content creation time!')
const stick = k.add([k.rect(4, 40), k.pos(winner.pos.x + dir * 10, winner.pos.y - 50), k.color(safeColor(k, '#888888')), k.z(18)])
const phone = k.add([k.rect(8, 12), k.pos(winner.pos.x + dir * 10, winner.pos.y - 70), k.color(safeColor(k, '#222222')), k.z(19)])
sfxBoing()
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 4; i++) {
winner.play('attack'); sfxBonk()
stick.pos.x = loser.pos.x; phone.pos.x = loser.pos.x
loser.play('hit'); k.shake(5 + i * 2)
await k.wait(0.08)
}
sfxCritical(); k.shake(15); loser.play('knockback')
await k.wait(0.15)
winner.play('win'); sfxCoin(); screenFlash('#ffffff', 0.3)
spawnEmoteText(W / 2, GROUND_Y - 80, '#selfie', '#ff4488')
spawnSparks(phone.pos.x, phone.pos.y, 10, '#ffffff')
stick.destroy(); phone.destroy()
await k.wait(0.3)
} else if (finishStyle === 30) {
// DANCE OFF — served
fatalityTagline = 'SERVED!'
announceHype('DANCE BATTLE!')
sfxPowerUp()
const moves = ['attack', 'kick', 'special', 'kick', 'attack', 'special'] as const
for (let i = 0; i < moves.length; i++) {
winner.play(moves[i]); sfxRandomSilly()
spawnSparks(winner.pos.x, winner.pos.y - 30, 3, ['#ff00ff', '#00ffff', '#ffff00'][i % 3])
await k.wait(0.08)
}
for (let i = 0; i < 4; i++) {
loser.play(moves[i]); sfxWomp()
loser.pos.x += (Math.random() - 0.5) * 10
await k.wait(0.1)
}
sfxFail()
winner.play('special'); sfxCritical(); sfxExplosion()
k.shake(25); screenFlash('#ff00ff', 0.2)
spawnShockwave(loser.pos.x, GROUND_Y, '#ff00ff')
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff00ff')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SERVED!', '#ff00ff')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 31) {
// WHOOPEE CUSHION — total gas
fatalityTagline = 'TOTAL GAS!'
announceSilly('What is that SMELL!?')
const cushion = k.add([k.circle(20), k.pos(loser.pos.x, GROUND_Y - 5), k.color(safeColor(k, '#ff6688')), k.z(10), k.opacity(0.8)])
sfxBoing(); await k.wait(0.2)
sfxWomp(); sfxRandomSilly(); k.shake(8)
await k.tween(20, 40, 0.15, (v) => { cushion.radius = v }, k.easings.easeOutQuad)
sfxExplosion()
for (let i = 0; i < 12; i++) {
const gas = k.add([
k.circle(8 + Math.random() * 10),
k.pos(loser.pos.x + (Math.random() - 0.5) * 40, GROUND_Y - 10 - Math.random() * 30),
k.color(safeColor(k, '#88ff44')), k.opacity(0.5), k.z(15),
])
k.tween(gas.pos.y, gas.pos.y - 40 - Math.random() * 30, 0.6, (v) => { gas.pos.y = v }, k.easings.easeOutQuad)
k.tween(0.5, 0, 0.6, (v) => { gas.opacity = v }).then(() => { if (gas.exists()) gas.destroy() })
}
k.shake(20); screenFlash('#88ff44', 0.15)
loser.play('knockback')
await k.tween(loser.pos.y, loserOrigY - 120, 0.2, (v) => { loser.pos.y = v }, k.easings.easeOutQuad)
await k.tween(loser.pos.y, loserOrigY, 0.15, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxSplat(); k.shake(12)
cushion.destroy()
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'P U!', '#88ff44')
await k.wait(0.3)
winner.play('win')
} else if (finishStyle === 32) {
// RICK ROLL — never gonna give you up
fatalityTagline = 'NEVER GONNA GIVE YOU UP!'
announceHype('You know the rules, and so do I!')
sfxPowerUp()
const rickMoves = ['attack', 'kick', 'special', 'idle', 'attack', 'kick'] as const
for (let i = 0; i < 6; i++) {
winner.play(rickMoves[i])
const hop = winner.pos.y
await k.tween(winner.pos.y, hop - 15, 0.05, (v) => { winner.pos.y = v }, k.easings.easeOutQuad)
await k.tween(winner.pos.y, hop, 0.05, (v) => { winner.pos.y = v }, k.easings.easeInQuad)
sfxBoing()
screenFlash(['#ff4444', '#ff8800', '#ffff00', '#44ff44', '#4444ff', '#8844ff'][i], 0.03)
await k.wait(0.04)
}
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
winner.play('special'); sfxCritical(); sfxExplosion()
k.shake(25); screenFlash('#ff8800', 0.2)
loser.play('knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff8800')
spawnShockwave(loser.pos.x, GROUND_Y, '#ff8800')
spawnEmoteText(loser.pos.x, loser.pos.y - 50, "RICK'D!", '#ff8800')
await k.wait(0.3)
winner.play('win')
} else {
// PIZZA CUTTER — the big wheel
fatalityTagline = 'SLICED!'
announceFast('Extra large, extra lethal!')
const wheel = k.add([k.circle(18), k.pos(winner.pos.x + dir * 30, GROUND_Y - 18), k.color(safeColor(k, '#cccccc')), k.z(18)])
const handle = k.add([k.rect(6, 20), k.pos(winner.pos.x + dir * 30, GROUND_Y - 36), k.color(safeColor(k, '#553322')), k.z(17)])
sfxPowerUp(); winner.play('special')
sfxZoomWhoosh()
await Promise.all([
k.tween(wheel.pos.x, loser.pos.x, 0.2, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeInQuad),
k.tween(0, 720, 0.2, (v) => { wheel.angle = v }, k.easings.linear),
])
sfxCritical(); sfxSplat(); k.shake(20); screenFlash('#ffcc00', 0.15)
loser.play('knockback')
spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#ff6600')
await k.tween(wheel.pos.x, loser.pos.x + dir * 60, 0.1, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeOutQuad)
sfxZoomWhoosh()
await k.tween(wheel.pos.x, loser.pos.x, 0.1, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeInQuad)
sfxBonk(); k.shake(15)
for (let i = 0; i < 8; i++) {
spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 80, loserOrigY - 30 - Math.random() * 40, 4, 4, i % 2 === 0 ? '#ffcc00' : '#cc4400', 250, true)
}
wheel.destroy(); handle.destroy()
spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SLICED!', '#ff6600')
await k.wait(0.3)
winner.play('win')
}
// Judge stands up for KO call
const judgeKO = k.get('judge')[0]
if (judgeKO) {
judgeKO.play('shocked')
const judgeOrigY = judgeKO.pos.y
k.tween(judgeKO.pos.y, judgeOrigY - 25, 0.2, (v) => { judgeKO.pos.y = v }, k.easings.easeOutQuad)
k.wait(1.5).then(() => {
if (judgeKO.exists()) {
judgeKO.play(winningSide === 'a' ? 'call_left' : 'call_right')
k.tween(judgeKO.pos.y, judgeOrigY, 0.3, (v) => { judgeKO.pos.y = v }, k.easings.easeInOutQuad)
}
})
}
// Winner's human cheers, loser's human panics (30% chance on KO)
if (Math.random() < 0.3) {
spawnHumanCoach(winningSide, 'cheer')
spawnHumanCoach(winningSide === 'a' ? 'b' : 'a', 'panic')
}
// === Common ending: KO + celebration ===
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(fatalityTagline)
announceCrowdReaction('cheer')
await k.tween(winner.pos.x, winningSide === 'a' ? HOME_A : HOME_B, 0.25, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad)
await k.wait(0.2)
winner.play('win')
sfxWin(); 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<ReturnType<typeof createFightScene>>