2026-03-06 16:27:54 +00:00
|
|
|
import kaplay from 'kaplay'
|
2026-03-08 20:30:02 +00:00
|
|
|
import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp, LoadSpriteOpt } from 'kaplay'
|
|
|
|
|
|
2026-03-07 19:42:49 +00:00
|
|
|
import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, generateHumanFighterSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites'
|
2026-03-08 23:00:57 +00:00
|
|
|
import type { Fighter, FightSceneConfig, RoundEvent, FightContext, ChoreoContext } from './fight/types'
|
2026-03-08 22:16:50 +00:00
|
|
|
export type { FightSceneConfig, RoundEvent } from './fight/types'
|
|
|
|
|
import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MOVES, CREATOR_ULTIMATES } from './fight/constants'
|
2026-03-08 23:34:05 +00:00
|
|
|
import {
|
|
|
|
|
CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO,
|
|
|
|
|
CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF,
|
2026-03-09 00:23:39 +00:00
|
|
|
TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles, isReducedMotion, haptic,
|
2026-03-08 23:34:05 +00:00
|
|
|
} from './fight/config'
|
2026-03-08 22:23:50 +00:00
|
|
|
import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles'
|
|
|
|
|
import { glitchRGB as _glitchRGB, scanlineGlitch as _scanlineGlitch, vhsTracking as _vhsTracking, dimensionalShift as _dimensionalShift } from './fight/effects'
|
|
|
|
|
import { spawnProjectile as _spawnProjectile } from './fight/projectiles'
|
2026-03-08 23:00:57 +00:00
|
|
|
import { createChoreographyMap } from './fight/choreography'
|
|
|
|
|
import { createFactories } from './fight/choreography/factories'
|
2026-03-08 23:24:20 +00:00
|
|
|
import { createMorphSystem } from './fight/morphs'
|
|
|
|
|
import { createEntranceSystem } from './fight/entrances'
|
|
|
|
|
import { createRoundSystem } from './fight/rounds'
|
|
|
|
|
import { createFinisherSystem } from './fight/finishers'
|
2026-03-06 22:13:19 +00:00
|
|
|
import {
|
|
|
|
|
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
|
2026-03-08 23:46:30 +00:00
|
|
|
sfxJetpack, sfxExplosion, sfxBlock, sfxDodge, sfxClash, sfxRandomSilly,
|
|
|
|
|
sfxBoing, sfxBonk, sfxZap, sfxSlideDown, sfxSlideUp, sfxZoomWhoosh,
|
|
|
|
|
sfxRapidPunch, sfxPowerUp, sfxCoin, sfxRandomComedy,
|
|
|
|
|
startMusic, stopMusic, announceCool,
|
2026-03-08 22:11:58 +00:00
|
|
|
} from './audio'
|
2026-03-06 16:27:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-06 23:44:40 +00:00
|
|
|
|
2026-03-08 12:08:18 +00:00
|
|
|
|
2026-03-08 23:24:20 +00:00
|
|
|
export function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
|
2026-03-08 23:34:05 +00:00
|
|
|
// THE CREATOR: special move selection
|
2026-03-08 12:08:18 +00:00
|
|
|
if (attackerArchetype === 'the_creator') {
|
2026-03-08 23:34:05 +00:00
|
|
|
const ultimateChance = isCritical ? 1.0 : CREATOR_ULTIMATE_CHANCE
|
2026-03-08 12:08:18 +00:00
|
|
|
if (Math.random() < ultimateChance) {
|
|
|
|
|
// Creator gets ALL ultimates + their 4 exclusive ones
|
|
|
|
|
const all: string[] = [...CREATOR_ULTIMATES]
|
|
|
|
|
for (let t = 2; t <= 5; t++) {
|
|
|
|
|
if (TIER_ULTIMATES[t]) all.push(...TIER_ULTIMATES[t])
|
|
|
|
|
}
|
|
|
|
|
return all[Math.floor(Math.random() * all.length)]
|
|
|
|
|
}
|
2026-03-08 23:34:05 +00:00
|
|
|
if (Math.random() < CREATOR_THEMED_MOVE_RATIO) {
|
2026-03-08 12:08:18 +00:00
|
|
|
return CREATOR_MOVES[Math.floor(Math.random() * CREATOR_MOVES.length)]
|
|
|
|
|
}
|
|
|
|
|
// Fall through to normal selection
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 23:44:40 +00:00
|
|
|
// Tier-gated ultimates: higher tier = higher chance of spectacular ultimate moves
|
|
|
|
|
if (attackerTier >= 2) {
|
2026-03-08 23:34:05 +00:00
|
|
|
const ultimateChance = TIER_ULTIMATE_CHANCES[attackerTier] ?? TIER_ULTIMATE_CHANCES[5]
|
2026-03-06 23:44:40 +00:00
|
|
|
// 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)]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
// Critical hits always get BIG moves (but can still be themed)
|
|
|
|
|
if (isCritical) {
|
|
|
|
|
const entry = CHALLENGE_THEMED[challengeType]
|
2026-03-08 23:34:05 +00:00
|
|
|
if (entry && Math.random() < CRIT_THEMED_RATIO) {
|
2026-03-06 22:13:19 +00:00
|
|
|
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',
|
2026-03-08 16:55:14 +00:00
|
|
|
'bulletTimeAttack',
|
2026-03-06 22:13:19 +00:00
|
|
|
]
|
|
|
|
|
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)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const roll = Math.random()
|
2026-03-08 23:34:05 +00:00
|
|
|
if (roll < CHOREOGRAPHY_THEMED_RATIO) {
|
2026-03-06 22:13:19 +00:00
|
|
|
return entry.themed[Math.floor(Math.random() * entry.themed.length)]
|
2026-03-08 23:34:05 +00:00
|
|
|
} else if (roll < CHOREOGRAPHY_GENERIC_CUTOFF) {
|
2026-03-06 22:13:19 +00:00
|
|
|
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)]
|
|
|
|
|
}
|
2026-03-06 16:27:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
// 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])
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 23:44:40 +00:00
|
|
|
export async function createFightScene(config: FightSceneConfig) {
|
2026-03-06 16:27:54 +00:00
|
|
|
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',
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-09 00:23:39 +00:00
|
|
|
// Wrap k.shake to support reduced motion + haptic feedback
|
|
|
|
|
const _origShake = k.shake.bind(k)
|
|
|
|
|
k.shake = ((intensity: number) => {
|
|
|
|
|
if (intensity >= 3) haptic(Math.min(100, intensity * 8))
|
|
|
|
|
if (!isReducedMotion()) _origShake(intensity)
|
|
|
|
|
}) as typeof k.shake
|
|
|
|
|
|
2026-03-06 16:27:54 +00:00
|
|
|
const colorsA = getBotColors(botA.seed)
|
|
|
|
|
const colorsB = getBotColors(botB.seed)
|
|
|
|
|
|
2026-03-06 23:44:40 +00:00
|
|
|
let sheetA: string
|
|
|
|
|
let sheetB: string
|
2026-03-07 19:42:49 +00:00
|
|
|
|
|
|
|
|
// Human fighters use their profile human character, not a bot sprite
|
|
|
|
|
const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0))
|
|
|
|
|
const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0))
|
|
|
|
|
|
2026-03-06 23:44:40 +00:00
|
|
|
try {
|
2026-03-07 19:42:49 +00:00
|
|
|
sheetA = botA.archetype === 'human'
|
|
|
|
|
? generateHumanFighterSpriteSheet(botA.seed, botA.archetype, colorsA.primary, colorsA.secondary, winRateA)
|
|
|
|
|
: generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization)
|
2026-03-06 23:44:40 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[FightScene] Failed to generate sprite for botA:', err)
|
|
|
|
|
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
|
|
|
|
|
}
|
|
|
|
|
try {
|
2026-03-07 19:42:49 +00:00
|
|
|
sheetB = botB.archetype === 'human'
|
|
|
|
|
? generateHumanFighterSpriteSheet(botB.seed, botB.archetype, colorsB.primary, colorsB.secondary, winRateB)
|
|
|
|
|
: generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization)
|
2026-03-06 23:44:40 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[FightScene] Failed to generate sprite for botB:', err)
|
|
|
|
|
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 19:42:49 +00:00
|
|
|
const isHumanA = botA.archetype === 'human'
|
|
|
|
|
const isHumanB = botB.archetype === 'human'
|
|
|
|
|
|
2026-03-08 15:54:58 +00:00
|
|
|
// Helper: load sprite with a 5s timeout so mobile never hangs forever
|
2026-03-08 20:30:02 +00:00
|
|
|
async function loadSpriteWithTimeout(name: string, src: string, opts: LoadSpriteOpt): Promise<void> {
|
2026-03-08 15:54:58 +00:00
|
|
|
try {
|
|
|
|
|
await Promise.race([
|
|
|
|
|
k.loadSprite(name, src, opts),
|
|
|
|
|
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('timeout')), 5_000)),
|
|
|
|
|
])
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn(`[FightScene] Sprite '${name}' failed/timed out:`, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Await sprite loading — timeout prevents mobile hangs
|
2026-03-06 23:44:40 +00:00
|
|
|
await Promise.all([
|
2026-03-08 15:54:58 +00:00
|
|
|
loadSpriteWithTimeout('botA', sheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }),
|
|
|
|
|
loadSpriteWithTimeout('botB', sheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }),
|
2026-03-06 23:44:40 +00:00
|
|
|
])
|
2026-03-06 16:27:54 +00:00
|
|
|
|
2026-03-07 19:42:49 +00:00
|
|
|
// Preload bot sprites for human fighters (used when humans morph into bots)
|
|
|
|
|
if (isHumanA) {
|
|
|
|
|
const botSheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
|
2026-03-08 15:54:58 +00:00
|
|
|
loadSpriteWithTimeout('botA_morph', botSheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
|
2026-03-07 19:42:49 +00:00
|
|
|
}
|
|
|
|
|
if (isHumanB) {
|
|
|
|
|
const botSheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
|
2026-03-08 15:54:58 +00:00
|
|
|
loadSpriteWithTimeout('botB_morph', botSheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
|
2026-03-07 19:42:49 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
// 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 },
|
|
|
|
|
}
|
2026-03-08 15:54:58 +00:00
|
|
|
await loadSpriteWithTimeout('judge', judgeSheet, { sliceX: JUDGE_MAX_FRAMES, sliceY: JUDGE_ROWS, anims: judgeAnims })
|
2026-03-06 22:13:19 +00:00
|
|
|
|
2026-03-07 19:42:49 +00:00
|
|
|
// Human sprites (the bot owner's silly human avatar for crowd)
|
2026-03-07 09:15:41 +00:00
|
|
|
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([
|
2026-03-08 15:54:58 +00:00
|
|
|
loadSpriteWithTimeout('humanA', humanSheetA, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
|
|
|
|
|
loadSpriteWithTimeout('humanB', humanSheetB, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
|
2026-03-07 09:15:41 +00:00
|
|
|
])
|
|
|
|
|
|
2026-03-06 16:27:54 +00:00
|
|
|
const W = k.width()
|
|
|
|
|
const H = k.height()
|
2026-03-08 20:39:11 +00:00
|
|
|
// Track all intervals/timeouts for cleanup on scene destroy
|
|
|
|
|
const cleanupTimers = new Set<ReturnType<typeof setTimeout>>()
|
|
|
|
|
function trackedTimeout(fn: () => void, ms: number): ReturnType<typeof setTimeout> {
|
|
|
|
|
const id = setTimeout(() => { cleanupTimers.delete(id); fn() }, ms)
|
|
|
|
|
cleanupTimers.add(id)
|
|
|
|
|
return id
|
|
|
|
|
}
|
|
|
|
|
function trackedInterval(fn: () => void, ms: number): ReturnType<typeof setInterval> {
|
|
|
|
|
const id = setInterval(fn, ms)
|
|
|
|
|
cleanupTimers.add(id)
|
|
|
|
|
return id
|
|
|
|
|
}
|
|
|
|
|
function clearTracked(id: ReturnType<typeof setTimeout>) {
|
|
|
|
|
clearInterval(id)
|
|
|
|
|
clearTimeout(id)
|
|
|
|
|
cleanupTimers.delete(id)
|
|
|
|
|
}
|
2026-03-07 22:07:52 +00:00
|
|
|
// Scale factor so sprites shrink on small canvases (reference: 500px tall)
|
2026-03-08 22:23:50 +00:00
|
|
|
|
|
|
|
|
// Fight context for extracted modules
|
|
|
|
|
const fightCtx: FightContext = {
|
|
|
|
|
k, W, H, theme,
|
|
|
|
|
trackedTimeout, trackedInterval, clearTracked,
|
|
|
|
|
safeColor: (color: string) => safeColor(k, color),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wrappers for extracted particle/effect/projectile functions
|
|
|
|
|
const spawnSparks = (x: number, y: number, count: number, color: string) => _spawnSparks(fightCtx, x, y, count, color)
|
|
|
|
|
const spawnBulletHoles = (x: number, y: number, count: number) => _spawnBulletHoles(fightCtx, x, y, count)
|
|
|
|
|
const spawnExhaust = (x: number, y: number, duration: number) => _spawnExhaust(fightCtx, x, y, duration)
|
|
|
|
|
const spawnProjectile = (fromX: number, fromY: number, toX: number, toY: number, color: string, size: number = 8) => _spawnProjectile(fightCtx, fromX, fromY, toX, toY, color, size)
|
|
|
|
|
const glitchRGB = (duration: number = 0.2) => _glitchRGB(fightCtx, duration)
|
|
|
|
|
const scanlineGlitch = (duration: number = 0.3) => _scanlineGlitch(fightCtx, duration)
|
|
|
|
|
const vhsTracking = (duration: number = 0.4) => _vhsTracking(fightCtx, duration)
|
|
|
|
|
const dimensionalShift = (duration: number = 0.6) => _dimensionalShift(fightCtx, duration)
|
|
|
|
|
|
2026-03-07 22:07:52 +00:00
|
|
|
const SF = Math.min(1, H / 500)
|
2026-03-06 16:27:54 +00:00
|
|
|
const GROUND_Y = H * 0.78
|
2026-03-06 22:13:19 +00:00
|
|
|
const HOME_A = W * 0.28
|
|
|
|
|
const HOME_B = W * 0.72
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,'#ffee00')),
|
2026-03-06 22:13:19 +00:00
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,color)),
|
2026-03-06 22:13:19 +00:00
|
|
|
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 ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Schizo cut — rapid zoom/position jitter simulating jump cuts
|
|
|
|
|
async function schizoCut() {
|
2026-03-08 20:30:02 +00:00
|
|
|
const fA = k.get('fighterA')[0] as Fighter
|
|
|
|
|
const fB = k.get('fighterB')[0] as Fighter
|
2026-03-06 22:13:19 +00:00
|
|
|
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) {
|
2026-03-09 00:00:48 +00:00
|
|
|
const lineCount = isPerfMode() ? 6 : 16
|
2026-03-08 20:30:02 +00:00
|
|
|
const lines: GameObj[] = []
|
2026-03-09 00:00:48 +00:00
|
|
|
for (let i = 0; i < lineCount; i++) {
|
|
|
|
|
const angle = (i / lineCount) * Math.PI * 2
|
2026-03-06 22:13:19 +00:00
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,'#ffffff')), k.opacity(0.3), k.z(52),
|
2026-03-06 22:13:19 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2026-03-08 23:55:58 +00:00
|
|
|
trackedTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flash the whole screen
|
|
|
|
|
function screenFlash(color: string, duration: number = 0.1) {
|
|
|
|
|
const flash = k.add([
|
|
|
|
|
k.rect(W, H),
|
|
|
|
|
k.pos(0, 0),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,color)),
|
2026-03-06 22:13:19 +00:00
|
|
|
k.opacity(0.4),
|
|
|
|
|
k.z(50),
|
|
|
|
|
])
|
|
|
|
|
k.tween(0.4, 0, duration, (v) => { flash.opacity = v }).then(() => flash.destroy())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 00:07:21 +00:00
|
|
|
// Impact freeze-frame: brief pause with white flash on devastating crits
|
|
|
|
|
async function impactFreeze(duration: number = 0.08) {
|
|
|
|
|
const overlay = k.add([
|
|
|
|
|
k.rect(W, H), k.pos(0, 0),
|
|
|
|
|
k.color(safeColor(k, '#ffffff')), k.opacity(0.5), k.z(55),
|
|
|
|
|
])
|
|
|
|
|
// Kaplay has no built-in pause, so we just hold with a wait + overlay
|
|
|
|
|
await k.wait(duration)
|
|
|
|
|
k.tween(0.5, 0, 0.06, (v) => { overlay.opacity = v }).then(() => overlay.destroy())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spawn afterimages: trailing ghost sprites behind a moving fighter
|
2026-03-08 20:30:02 +00:00
|
|
|
function spawnAfterimages(fighter: Fighter, count: number = 3) {
|
2026-03-08 00:07:21 +00:00
|
|
|
if (!fighter?.exists()) return
|
2026-03-09 00:00:48 +00:00
|
|
|
const n = isPerfMode() ? Math.max(1, Math.round(count * 0.5)) : count
|
|
|
|
|
for (let i = 0; i < n; i++) {
|
2026-03-08 00:07:21 +00:00
|
|
|
const ghost = k.add([
|
|
|
|
|
k.rect(FRAME_SIZE * Math.abs(fighter.scale.x), FRAME_SIZE * Math.abs(fighter.scale.y)),
|
|
|
|
|
k.pos(fighter.pos.x - i * 8 * Math.sign(fighter.scale.x), fighter.pos.y),
|
|
|
|
|
k.color(safeColor(k, theme.accent)),
|
|
|
|
|
k.opacity(0.3 - i * 0.08),
|
|
|
|
|
k.z(fighter.z - 1),
|
|
|
|
|
k.anchor('bot'),
|
|
|
|
|
])
|
|
|
|
|
k.tween(ghost.opacity, 0, 0.15 + i * 0.05, (v) => { ghost.opacity = v }).then(() => ghost.destroy())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Camera zoom: scale fighters toward/away from a focal point for impact emphasis
|
|
|
|
|
async function cameraZoom(
|
2026-03-08 20:30:02 +00:00
|
|
|
fighters: Fighter[],
|
2026-03-08 00:07:21 +00:00
|
|
|
savedScales: { x: number; y: number }[],
|
|
|
|
|
zoomFactor: number,
|
|
|
|
|
duration: number,
|
|
|
|
|
easing: (t: number) => number = k.easings.easeOutQuad,
|
|
|
|
|
) {
|
|
|
|
|
await Promise.all(fighters.map((f, i) => {
|
|
|
|
|
if (!f?.exists()) return Promise.resolve()
|
|
|
|
|
const sx = savedScales[i].x
|
|
|
|
|
const sy = savedScales[i].y
|
|
|
|
|
return k.tween(Math.abs(f.scale.y), Math.abs(sy) * zoomFactor, duration, (v) => {
|
|
|
|
|
f.scale.x = sx > 0 ? v : -v
|
|
|
|
|
f.scale.y = v
|
|
|
|
|
}, easing)
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 12:08:18 +00:00
|
|
|
// Grotesque close-up overlays removed — noops retained for call-site compatibility
|
2026-03-08 20:30:02 +00:00
|
|
|
function spawnGrotesqueDetails(_fighter: Fighter, _scaleFactor: number) {}
|
2026-03-08 12:08:18 +00:00
|
|
|
function destroyGrotesqueDetails() {}
|
2026-03-06 22:13:19 +00:00
|
|
|
// Arena-specific background decoration
|
|
|
|
|
function drawArenaDecor() {
|
|
|
|
|
const a = arena
|
|
|
|
|
const acc = theme.accent
|
|
|
|
|
|
|
|
|
|
// Parallax stars/particles in the sky
|
2026-03-09 00:00:48 +00:00
|
|
|
const starCount = isPerfMode() ? 15 : 40
|
|
|
|
|
for (let i = 0; i < starCount; i++) {
|
2026-03-06 22:13:19 +00:00
|
|
|
const star = k.add([
|
|
|
|
|
k.rect(1 + Math.random() * 2, 1 + Math.random() * 2),
|
|
|
|
|
k.pos(Math.random() * W, Math.random() * (GROUND_Y - 20)),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, i % 5 === 0 ? '#ffffff' : acc)),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.opacity(0.08 + Math.random() * 0.2),
|
2026-03-06 22:13:19 +00:00
|
|
|
k.z(1),
|
|
|
|
|
])
|
|
|
|
|
const speed = 0.5 + Math.random() * 1.5
|
|
|
|
|
const baseOp = star.opacity
|
|
|
|
|
star.onUpdate(() => {
|
2026-03-07 00:14:46 +00:00
|
|
|
star.opacity = baseOp + Math.sin(k.time() * speed + i) * 0.08
|
2026-03-06 22:13:19 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (a === 'datacenter' || a === 'localhost') {
|
2026-03-09 00:00:48 +00:00
|
|
|
const perf = isPerfMode()
|
2026-03-07 10:42:18 +00:00
|
|
|
// City skyline silhouette behind the racks
|
2026-03-09 00:00:48 +00:00
|
|
|
const buildingHeights = perf
|
|
|
|
|
? [0.55, 0.65, 0.7, 0.6, 0.72, 0.58]
|
|
|
|
|
: [0.55, 0.4, 0.65, 0.35, 0.7, 0.45, 0.6, 0.5, 0.72, 0.38, 0.58, 0.42]
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-09 00:00:48 +00:00
|
|
|
const winStep = perf ? 14 : 7
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let wy = 0; wy < bh - 10; wy += 8) {
|
2026-03-09 00:00:48 +00:00
|
|
|
for (let wx = 4; wx < bw - 6; wx += winStep) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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),
|
|
|
|
|
])
|
2026-03-09 00:00:48 +00:00
|
|
|
if (!perf && Math.random() < 0.15) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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
|
2026-03-09 00:00:48 +00:00
|
|
|
const rackRows = perf ? 2 : 4
|
|
|
|
|
const rackCols = perf ? 5 : 10
|
|
|
|
|
for (let row = 0; row < rackRows; row++) {
|
|
|
|
|
for (let col = 0; col < rackCols; col++) {
|
|
|
|
|
const lx = 15 + col * (W / rackCols)
|
2026-03-07 00:14:46 +00:00
|
|
|
const ly = 15 + row * 55
|
2026-03-09 00:00:48 +00:00
|
|
|
const rackW = W / rackCols - 8
|
2026-03-07 00:14:46 +00:00
|
|
|
const rackH = 48
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let s = 0; s < 5; s++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-09 00:00:48 +00:00
|
|
|
if (!perf) {
|
|
|
|
|
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 })
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 })
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
} else if (a === 'gpu_graveyard') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Floating circuit board fragments
|
2026-03-09 00:00:48 +00:00
|
|
|
const chipCount = isPerfMode() ? 6 : 16
|
|
|
|
|
for (let i = 0; i < chipCount; i++) {
|
2026-03-06 22:13:19 +00:00
|
|
|
const cx = Math.random() * W
|
|
|
|
|
const cy = 20 + Math.random() * (GROUND_Y - 60)
|
2026-03-07 00:14:46 +00:00
|
|
|
const cw = 15 + Math.random() * 25
|
|
|
|
|
const ch = 10 + Math.random() * 18
|
2026-03-06 22:13:19 +00:00
|
|
|
const chip = k.add([
|
2026-03-07 00:14:46 +00:00
|
|
|
k.rect(cw, ch), k.pos(cx, cy),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, Math.random() > 0.5 ? '#1a2a1a' : '#0f1a0f')),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.opacity(0.35), k.z(1), k.rotate(Math.random() * 45 - 22),
|
2026-03-06 22:13:19 +00:00
|
|
|
])
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let t = 0; t < 3; t++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-06 22:13:19 +00:00
|
|
|
const baseY = cy
|
|
|
|
|
chip.onUpdate(() => { chip.pos.y = baseY + Math.sin(k.time() * 0.3 + i) * 5 })
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Dead GPU fans — some slowly spinning
|
2026-03-07 00:14:46 +00:00
|
|
|
for (const fx of [W * 0.15, W * 0.5, W * 0.85]) {
|
|
|
|
|
const fy = GROUND_Y - 30 - Math.random() * 40
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let b = 0; b < 4; b++) {
|
|
|
|
|
const ba = b * Math.PI / 2
|
2026-03-07 10:42:18 +00:00
|
|
|
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() })
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'prompt_dungeon') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Stone wall bricks
|
2026-03-07 00:14:46 +00:00
|
|
|
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)
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Glowing runes with halos
|
2026-03-07 00:14:46 +00:00
|
|
|
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),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#b83dff')), k.opacity(0.12), k.z(1),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
2026-03-07 10:42:18 +00:00
|
|
|
k.add([k.circle(12 + Math.random() * 8), k.pos(rx, ry), k.color(safeColor(k, '#6622aa')), k.opacity(0.06), k.z(0)])
|
2026-03-07 00:14:46 +00:00
|
|
|
rune.onUpdate(() => { rune.opacity = 0.08 + Math.sin(k.time() * 0.8 + i * 0.7) * 0.08 })
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Torches with flickering flames and light cones
|
2026-03-07 00:14:46 +00:00
|
|
|
for (const tx of [W * 0.1, W * 0.35, W * 0.65, W * 0.9]) {
|
|
|
|
|
const ty = GROUND_Y * 0.3
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
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
|
2026-03-07 10:42:18 +00:00
|
|
|
lightCone.opacity = 0.015 + Math.sin(k.time() * 6 + tx) * 0.008
|
2026-03-07 00:14:46 +00:00
|
|
|
})
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 })
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
// Ground fog
|
|
|
|
|
for (let i = 0; i < 8; i++) {
|
2026-03-06 22:13:19 +00:00
|
|
|
const fog = k.add([
|
2026-03-07 00:14:46 +00:00
|
|
|
k.circle(35 + Math.random() * 30), k.pos(Math.random() * W, GROUND_Y - 8),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#1f1a2a')), k.opacity(0.25), k.z(3),
|
2026-03-06 22:13:19 +00:00
|
|
|
])
|
|
|
|
|
const baseX = fog.pos.x
|
2026-03-07 00:14:46 +00:00
|
|
|
fog.onUpdate(() => { fog.pos.x = baseX + Math.sin(k.time() * 0.2 + i) * 18 })
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
} else if (a === 'the_cloud') {
|
2026-03-07 00:14:46 +00:00
|
|
|
// 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),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, layer === 0 ? '#1a2030' : '#151a28')),
|
2026-03-07 00:14:46 +00:00
|
|
|
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) })
|
|
|
|
|
}
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Data rain streams
|
|
|
|
|
for (let i = 0; i < 15; i++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const stream = k.add([
|
|
|
|
|
k.rect(1, 8 + Math.random() * 15), k.pos(Math.random() * W, Math.random() * GROUND_Y),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#4488ff')), k.opacity(0.12), k.z(2),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
stream.onUpdate(() => {
|
|
|
|
|
stream.pos.y += 30 * k.dt()
|
2026-03-07 10:42:18 +00:00
|
|
|
if (stream.pos.y > GROUND_Y) { stream.pos.y = -20; stream.pos.x = Math.random() * W }
|
2026-03-07 00:14:46 +00:00
|
|
|
})
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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),
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
} else if (a === 'the_singularity') {
|
2026-03-07 00:14:46 +00:00
|
|
|
// Central swirling vortex with multiple rings
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let ring = 0; ring < 4; ring++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const ringDots = 12 + ring * 6
|
|
|
|
|
for (let i = 0; i < ringDots; i++) {
|
|
|
|
|
const angle = (i / ringDots) * Math.PI * 2
|
2026-03-07 10:42:18 +00:00
|
|
|
const baseDist = 20 + ring * 30
|
2026-03-07 00:14:46 +00:00
|
|
|
const dot = k.add([
|
|
|
|
|
k.circle(1.5 + ring * 0.5), k.pos(W / 2, GROUND_Y * 0.4),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, ring === 0 ? '#ffffff' : ring === 1 ? '#ff44ff' : ring === 2 ? '#ff00ff' : '#aa00aa')),
|
|
|
|
|
k.opacity(0.3 - ring * 0.05), k.z(1),
|
2026-03-06 22:13:19 +00:00
|
|
|
])
|
2026-03-07 00:14:46 +00:00
|
|
|
dot.onUpdate(() => {
|
2026-03-07 10:42:18 +00:00
|
|
|
const a2 = angle + k.time() * (0.8 - ring * 0.15)
|
2026-03-07 00:14:46 +00:00
|
|
|
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
|
|
|
|
|
})
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
core.onUpdate(() => { core.opacity = 0.1 + Math.sin(k.time() * 2) * 0.08 })
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 })
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} 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++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
for (let l = 0; l < 3; l++) {
|
|
|
|
|
const ly = 20 + l * (GROUND_Y / 4)
|
|
|
|
|
const dir = Math.random() > 0.5 ? 1 : -1
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Zen garden raked lines
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
|
const gy = GROUND_Y + 5 + i * 8
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
} else if (a === 'hacker_news' || a === 'stackoverflow_ruins') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
2026-03-07 00:14:46 +00:00
|
|
|
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)
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
if (Math.random() < 0.4) {
|
2026-03-07 10:42:18 +00:00
|
|
|
k.add([k.rect(2, bh), k.pos(bx - 4, by), k.color(safeColor(k, acc)), k.opacity(0.1), k.z(1)])
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
// Vote arrows
|
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
|
|
|
const ax = 20 + Math.random() * (W - 40)
|
|
|
|
|
const ay = 30 + Math.random() * (GROUND_Y - 70)
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'beach') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Sun with rays and glow
|
2026-03-07 00:14:46 +00:00
|
|
|
const sunX = W * 0.8, sunY = 35
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
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
|
2026-03-07 10:42:18 +00:00
|
|
|
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 })
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const wave = k.add([
|
2026-03-07 10:42:18 +00:00
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
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) })
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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]) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const lean = px < W / 2 ? 8 : -8
|
2026-03-07 10:42:18 +00:00
|
|
|
const height = 8 + Math.floor(Math.random() * 3)
|
|
|
|
|
for (let s = 0; s < height; s++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const sy = GROUND_Y - 10 - s * 12
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
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 })
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 })
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} 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
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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]) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const ch = 35 + Math.random() * 20
|
2026-03-07 10:42:18 +00:00
|
|
|
k.add([k.rect(7, ch), k.pos(cx, GROUND_Y - ch), k.color(safeColor(k, '#226622')), k.opacity(0.45), k.z(1)])
|
2026-03-07 00:14:46 +00:00
|
|
|
const armY = GROUND_Y - ch * 0.6
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
if (Math.random() > 0.3) {
|
|
|
|
|
const armY2 = GROUND_Y - ch * 0.4
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Tumbleweeds rolling
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let i = 0; i < 3; i++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
shimmer.onUpdate(() => { shimmer.opacity = 0.03 + Math.sin(k.time() * 3 + i * 2) * 0.03 })
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
} else if (a === 'forest') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let i = 0; i < treeCount; i++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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)
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([
|
|
|
|
|
k.circle(cr), k.pos(tx + trunkW / 2 + (Math.random() - 0.5) * 10, GROUND_Y - th - cr * 0.5 + c * 4),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.anchor('center'), k.color(safeColor(k, layer === 0 ? '#0a1a08' : layer === 1 ? '#122a0e' : '#1a3a10')), k.opacity(treeOp * 0.8), k.z(layer),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
|
|
|
|
// Fireflies
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let i = 0; i < 15; i++) {
|
2026-03-06 22:13:19 +00:00
|
|
|
const ff = k.add([
|
2026-03-07 00:14:46 +00:00
|
|
|
k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 50)),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#ccff44')), k.opacity(0), k.z(3),
|
2026-03-06 22:13:19 +00:00
|
|
|
])
|
2026-03-07 00:14:46 +00:00
|
|
|
const bx = ff.pos.x, by = ff.pos.y
|
2026-03-06 22:13:19 +00:00
|
|
|
ff.onUpdate(() => {
|
2026-03-07 00:14:46 +00:00
|
|
|
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
|
2026-03-06 22:13:19 +00:00
|
|
|
})
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
// Light rays through canopy
|
2026-03-07 10:42:18 +00:00
|
|
|
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 })
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'jungle') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let s = 0; s < vineLen; s += 4) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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 })
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
for (let l = 0; l < 2; l++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Large tropical leaves at edges
|
2026-03-07 00:14:46 +00:00
|
|
|
for (const side of [0, W - 30]) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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 })
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
// Thick ground fog
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let i = 0; i < 10; i++) {
|
2026-03-06 22:13:19 +00:00
|
|
|
const mist = k.add([
|
2026-03-07 00:14:46 +00:00
|
|
|
k.circle(40 + Math.random() * 35), k.pos(Math.random() * W, GROUND_Y - 12),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#1a3818')), k.opacity(0.22), k.z(3),
|
2026-03-06 22:13:19 +00:00
|
|
|
])
|
|
|
|
|
const bx = mist.pos.x
|
2026-03-07 00:14:46 +00:00
|
|
|
mist.onUpdate(() => { mist.pos.x = bx + Math.sin(k.time() * 0.12 + i) * 18 })
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
} else if (a === 'outer_space') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Dense star field
|
|
|
|
|
for (let i = 0; i < 100; i++) {
|
2026-03-06 22:13:19 +00:00
|
|
|
const star = k.add([
|
|
|
|
|
k.circle(0.5 + Math.random() * 1.5),
|
|
|
|
|
k.pos(Math.random() * W, Math.random() * H),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, i % 8 === 0 ? '#ffccaa' : i % 5 === 0 ? '#aaaaff' : '#ffffff')),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.opacity(0.15 + Math.random() * 0.4), k.z(1),
|
2026-03-06 22:13:19 +00:00
|
|
|
])
|
2026-03-07 00:14:46 +00:00
|
|
|
star.onUpdate(() => { star.opacity = 0.12 + Math.sin(k.time() * (0.8 + Math.random()) + i) * 0.15 })
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Nebula clouds
|
2026-03-07 00:14:46 +00:00
|
|
|
const nebulaColors = ['#4400aa', '#aa0066', '#0044aa', '#006644', '#660044']
|
|
|
|
|
for (let i = 0; i < 5; i++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let blob = 0; blob < 4; blob++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([
|
|
|
|
|
k.circle(30 + Math.random() * 50),
|
2026-03-07 10:42:18 +00:00
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
}
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Floating asteroids
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
2026-03-06 22:13:19 +00:00
|
|
|
const ast = k.add([
|
2026-03-07 00:14:46 +00:00
|
|
|
k.circle(4 + Math.random() * 10),
|
|
|
|
|
k.pos(Math.random() * W, 15 + Math.random() * (GROUND_Y - 30)),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, Math.random() > 0.5 ? '#555555' : '#3a3a3a')), k.opacity(0.35), k.z(1),
|
2026-03-06 22:13:19 +00:00
|
|
|
])
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-06 22:13:19 +00:00
|
|
|
const baseX = ast.pos.x, baseY = ast.pos.y
|
|
|
|
|
ast.onUpdate(() => {
|
2026-03-07 00:14:46 +00:00
|
|
|
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
|
2026-03-06 22:13:19 +00:00
|
|
|
})
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// Distant planet with rings
|
2026-03-07 00:14:46 +00:00
|
|
|
const planetX = W * 0.75, planetY = GROUND_Y * 0.25
|
2026-03-07 10:42:18 +00:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} 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
|
2026-03-07 10:42:18 +00:00
|
|
|
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),
|
|
|
|
|
])
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
// Crumbled stone debris
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let i = 0; i < 8; i++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([
|
|
|
|
|
k.rect(5 + Math.random() * 10, 3 + Math.random() * 6),
|
|
|
|
|
k.pos(Math.random() * W, GROUND_Y - 5 - Math.random() * 10),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#5a4a30')), k.opacity(0.25), k.z(2), k.rotate(Math.random() * 40 - 20),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 })
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'haunted_mansion') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Gothic windows with moonlight
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let i = 0; i < 5; i++) {
|
|
|
|
|
const wx = W * (0.1 + i * 0.2)
|
|
|
|
|
const wy = GROUND_Y * 0.2
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
glow.onUpdate(() => { glow.opacity = 0.04 + Math.sin(k.time() * 0.5 + i) * 0.03 })
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
2026-03-07 00:14:46 +00:00
|
|
|
for (const cx of [15, W - 35]) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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 })
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Floating ghost particles
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let i = 0; i < 8; i++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const ghost = k.add([
|
|
|
|
|
k.circle(3 + Math.random() * 3), k.pos(Math.random() * W, Math.random() * GROUND_Y),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#aabbcc')), k.opacity(0), k.z(2),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'concert_hall') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Stage lights with sweeping cones
|
2026-03-07 00:14:46 +00:00
|
|
|
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'),
|
2026-03-07 11:44:09 +00:00
|
|
|
k.color(safeColor(k, lightColors[i])), k.opacity(0.04), k.z(2), k.rotate(0),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
2026-03-07 10:42:18 +00:00
|
|
|
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
|
|
|
|
|
})
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
2026-03-07 11:44:09 +00:00
|
|
|
const speakerCone = k.add([k.circle(4), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1), k.scale(1)])
|
2026-03-07 10:42:18 +00:00
|
|
|
speakerCone.onUpdate(() => { speakerCone.scale = k.vec2(1 + Math.sin(k.time() * 8 + s) * 0.05) })
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 })
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([
|
2026-03-07 10:42:18 +00:00
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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 }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'meme_dimension') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Floating emoji shapes (pulsing)
|
|
|
|
|
for (let i = 0; i < 12; i++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const emoji = k.add([
|
2026-03-07 10:42:18 +00:00
|
|
|
k.circle(6 + Math.random() * 6), k.pos(Math.random() * W, Math.random() * GROUND_Y),
|
2026-03-07 11:44:09 +00:00
|
|
|
k.color(safeColor(k, ['#ffcc00', '#ff4444', '#44ff44', '#4444ff', '#ff44ff', '#44ffff'][i % 6])), k.opacity(0.12), k.z(1), k.scale(1),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
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
|
2026-03-07 10:42:18 +00:00
|
|
|
emoji.scale = k.vec2(1 + Math.sin(k.time() * 1.5 + i) * 0.15)
|
2026-03-07 00:14:46 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
// Glitch bands
|
2026-03-07 10:42:18 +00:00
|
|
|
for (let i = 0; i < 8; i++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
const band = k.add([
|
2026-03-07 10:42:18 +00:00
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
2026-03-07 10:42:18 +00:00
|
|
|
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
|
|
|
|
|
})
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let i = 0; i < 6; i++) {
|
2026-03-07 10:42:18 +00:00
|
|
|
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),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
2026-03-07 10:42:18 +00:00
|
|
|
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 }
|
|
|
|
|
})
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
|
|
|
|
|
} 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)])
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'savanna') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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)])
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
// Setting sun
|
2026-03-07 10:42:18 +00:00
|
|
|
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
|
2026-03-07 11:44:09 +00:00
|
|
|
const grass = k.add([k.rect(1, grassH), k.pos(gx, GROUND_Y - grassH), k.color(safeColor(k, '#8a7830')), k.opacity(0.2), k.z(3), k.rotate(0)])
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
} else if (a === 'server_room') {
|
2026-03-07 10:42:18 +00:00
|
|
|
// Server rack rows with LEDs
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let row = 0; row < 3; row++) {
|
|
|
|
|
for (let col = 0; col < 6; col++) {
|
|
|
|
|
const rx = 30 + col * (W / 6)
|
2026-03-07 10:42:18 +00:00
|
|
|
const ry = 20 + row * 55
|
2026-03-07 00:14:46 +00:00
|
|
|
const rw = W / 6 - 12
|
2026-03-07 10:42:18 +00:00
|
|
|
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)])
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
for (let led = 0; led < 4; led++) {
|
|
|
|
|
const ledEl = k.add([
|
|
|
|
|
k.rect(2, 2), k.pos(rx + 3 + led * 5, ry + 3),
|
2026-03-07 10:42:18 +00:00
|
|
|
k.color(safeColor(k, '#00ff88')), k.opacity(0.5), k.z(2),
|
2026-03-07 00:14:46 +00:00
|
|
|
])
|
|
|
|
|
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.04 ? 0.5 : 0.1 })
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
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 })
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 10:42:18 +00:00
|
|
|
// 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
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-06 16:27:54 +00:00
|
|
|
|
|
|
|
|
k.scene('fight', () => {
|
2026-03-06 23:44:40 +00:00
|
|
|
// === 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),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,bgBase)),
|
2026-03-06 23:44:40 +00:00
|
|
|
k.opacity(1 - band * 0.04),
|
|
|
|
|
])
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-06 23:44:40 +00:00
|
|
|
// Subtle horizon glow — wide soft band just above ground
|
|
|
|
|
k.add([
|
|
|
|
|
k.rect(W, 20), k.pos(0, GROUND_Y - 20),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,theme.accent)), k.opacity(0.06),
|
2026-03-06 23:44:40 +00:00
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// === GROUND ===
|
|
|
|
|
// Main ground fill
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([k.rect(W, H - GROUND_Y + 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.ground))])
|
2026-03-06 23:44:40 +00:00
|
|
|
// Bright edge line at the surface
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([k.rect(W, 2), k.pos(0, GROUND_Y), k.color(safeColor(k,theme.accent)), k.opacity(0.7)])
|
2026-03-06 23:44:40 +00:00
|
|
|
// Secondary highlight
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([k.rect(W, 1), k.pos(0, GROUND_Y + 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3)])
|
2026-03-06 23:44:40 +00:00
|
|
|
// Ground gradient — fades deeper
|
|
|
|
|
for (let i = 1; i <= 12; i++) {
|
2026-03-07 00:14:46 +00:00
|
|
|
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)])
|
2026-03-06 23:44:40 +00:00
|
|
|
}
|
|
|
|
|
// 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),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,theme.accent)), k.opacity(0.05),
|
2026-03-06 23:44:40 +00:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
// Horizontal depth lines on ground
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
const gy = GROUND_Y + 8 + i * i * 3
|
|
|
|
|
if (gy < H) {
|
2026-03-07 00:14:46 +00:00
|
|
|
k.add([k.rect(W, 1), k.pos(0, gy), k.color(safeColor(k,theme.accent)), k.opacity(0.08 - i * 0.012)])
|
2026-03-06 23:44:40 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === SIDE PILLARS / FRAME ===
|
|
|
|
|
// Subtle dark pillars on edges for framing
|
2026-03-07 00:14:46 +00:00
|
|
|
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)])
|
2026-03-06 23:44:40 +00:00
|
|
|
// Accent trim on pillars
|
2026-03-07 00:14:46 +00:00
|
|
|
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)])
|
2026-03-06 23:44:40 +00:00
|
|
|
|
|
|
|
|
// === 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),
|
2026-03-07 00:14:46 +00:00
|
|
|
k.color(safeColor(k,theme.accent)),
|
2026-03-06 23:44:40 +00:00
|
|
|
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
|
|
|
|
|
})
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-06 16:27:54 +00:00
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
// Arena-specific decorations
|
|
|
|
|
drawArenaDecor()
|
|
|
|
|
|
|
|
|
|
// Umpire chair (center-back, behind fighters)
|
|
|
|
|
const CHAIR_X = W / 2
|
2026-03-07 22:07:52 +00:00
|
|
|
const CHAIR_SEAT_Y = GROUND_Y * (0.38 + (1 - SF) * 0.2)
|
|
|
|
|
const cs = SF // chair scale
|
|
|
|
|
k.add([k.rect(4 * cs, GROUND_Y - CHAIR_SEAT_Y + 15 * cs), k.pos(CHAIR_X - 22 * cs, CHAIR_SEAT_Y - 8 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
|
|
|
|
|
k.add([k.rect(4 * cs, GROUND_Y - CHAIR_SEAT_Y + 15 * cs), k.pos(CHAIR_X + 18 * cs, CHAIR_SEAT_Y - 8 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
|
2026-03-06 22:13:19 +00:00
|
|
|
for (let i = 0; i < 6; i++) {
|
2026-03-07 22:07:52 +00:00
|
|
|
const rungY = CHAIR_SEAT_Y + 15 * cs + i * ((GROUND_Y - CHAIR_SEAT_Y - 15 * cs) / 6)
|
|
|
|
|
k.add([k.rect(44 * cs, 3), k.pos(CHAIR_X - 22 * cs, rungY), k.color(safeColor(k,'#443322')), k.opacity(0.6), k.z(3)])
|
2026-03-06 16:27:54 +00:00
|
|
|
}
|
2026-03-07 22:07:52 +00:00
|
|
|
k.add([k.rect(54 * cs, 5), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 2), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)])
|
|
|
|
|
k.add([k.rect(54 * cs, 1), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 2), k.color(safeColor(k,theme.accent)), k.opacity(0.3), k.z(3)])
|
|
|
|
|
k.add([k.rect(48 * cs, 4), k.pos(CHAIR_X - 24 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#665544')), k.opacity(0.8), k.z(3)])
|
|
|
|
|
k.add([k.rect(3, 12 * cs), k.pos(CHAIR_X - 27 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
|
|
|
|
|
k.add([k.rect(3, 12 * cs), k.pos(CHAIR_X + 24 * cs, CHAIR_SEAT_Y - 10 * cs), k.color(safeColor(k,'#554433')), k.opacity(0.7), k.z(3)])
|
2026-03-06 22:13:19 +00:00
|
|
|
k.add([
|
|
|
|
|
k.sprite('judge', { anim: 'idle' }),
|
|
|
|
|
k.pos(CHAIR_X, CHAIR_SEAT_Y),
|
|
|
|
|
k.anchor('bot'),
|
2026-03-07 22:07:52 +00:00
|
|
|
k.scale(1.3 * SF),
|
2026-03-06 22:13:19 +00:00
|
|
|
k.z(4),
|
|
|
|
|
k.opacity(0.9),
|
|
|
|
|
'judge',
|
|
|
|
|
])
|
2026-03-06 16:27:54 +00:00
|
|
|
|
2026-03-07 22:07:52 +00:00
|
|
|
const scaleA = (1.8 + botA.tier * 0.4) * SF
|
2026-03-06 23:44:40 +00:00
|
|
|
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'])
|
2026-03-06 16:27:54 +00:00
|
|
|
|
2026-03-07 22:07:52 +00:00
|
|
|
const scaleB = (1.8 + botB.tier * 0.4) * SF
|
2026-03-06 23:44:40 +00:00
|
|
|
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'])
|
2026-03-06 16:27:54 +00:00
|
|
|
|
|
|
|
|
config.onReady?.()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
k.go('fight')
|
|
|
|
|
|
|
|
|
|
let comboA = 0
|
|
|
|
|
let comboB = 0
|
|
|
|
|
|
2026-03-08 23:00:57 +00:00
|
|
|
// === CHOREOGRAPHY SYSTEM (extracted to fight/choreography/) ===
|
|
|
|
|
const choreoCtx: ChoreoContext = {
|
|
|
|
|
k, W, H, GROUND_Y, FRAME_SIZE, theme,
|
|
|
|
|
safeColor: (color: string) => safeColor(k, color),
|
2026-03-08 23:55:58 +00:00
|
|
|
trackedTimeout, trackedInterval, clearTracked,
|
2026-03-08 23:00:57 +00:00
|
|
|
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
|
|
|
|
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
|
|
|
|
glitchRGB, scanlineGlitch, dimensionalShift,
|
|
|
|
|
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
|
|
|
|
|
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
|
|
|
|
|
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
|
|
|
|
|
sfxZap, sfxZoomWhoosh, sfxBlock,
|
|
|
|
|
spawnBullet, spawnGrotesqueDetails, destroyGrotesqueDetails,
|
2026-03-06 22:13:19 +00:00
|
|
|
}
|
2026-03-08 23:00:57 +00:00
|
|
|
const choreographyMap = createChoreographyMap(choreoCtx)
|
|
|
|
|
const { spawnWeaponProp, destroyProp, spawnProp, moveProp } = createFactories(choreoCtx)
|
2026-03-07 00:14:46 +00:00
|
|
|
|
|
|
|
|
async function playBlock(side: 'a' | 'b') {
|
2026-03-08 20:30:02 +00:00
|
|
|
const defender = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
|
2026-03-07 00:14:46 +00:00
|
|
|
if (!defender) return
|
|
|
|
|
const dir = side === 'a' ? 1 : -1
|
|
|
|
|
// Spawn shield in front
|
2026-03-07 00:53:15 +00:00
|
|
|
const shieldObjs = spawnWeaponProp('shield', defender.pos.x + dir * 20, defender.pos.y - 25, side === 'b', 19)
|
2026-03-07 00:14:46 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 23:24:20 +00:00
|
|
|
// === MORPH SYSTEM (extracted to fight/morphs.ts) ===
|
|
|
|
|
const morphSystem = createMorphSystem(choreoCtx, {
|
|
|
|
|
botA, botB, colorsA, colorsB, isHumanA, isHumanB, loadSpriteWithTimeout,
|
|
|
|
|
})
|
|
|
|
|
const { destroyMorphOverlays, playMorph, applyCreatorOmniMorph, getMorphOrder } = morphSystem
|
2026-03-06 22:13:19 +00:00
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
// === EMOTION & SPORTSMANSHIP SYSTEM ===
|
|
|
|
|
|
|
|
|
|
const heartfeltLines = [
|
2026-03-07 09:15:41 +00:00
|
|
|
'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!',
|
2026-03-07 00:14:46 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const crowdSympathyLines = [
|
2026-03-07 09:15:41 +00:00
|
|
|
'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!',
|
2026-03-07 00:14:46 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const respectLines = [
|
2026-03-07 09:15:41 +00:00
|
|
|
'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.',
|
2026-03-07 00:14:46 +00:00
|
|
|
]
|
|
|
|
|
|
2026-03-07 00:53:15 +00:00
|
|
|
// Sanitize text for Kaplay (treats [ ] as styled text tags)
|
2026-03-07 11:42:32 +00:00
|
|
|
function safeText(t: string): string {
|
|
|
|
|
return t
|
|
|
|
|
.replace(/[\[\]{}<>`\\|~^]/g, '') // Kaplay styled-text tags and problematic chars
|
|
|
|
|
.replace(/[\x00-\x1f\x7f]/g, ' ') // Control characters → space
|
|
|
|
|
.replace(/[^\x20-\x7e]/g, '') // Strip non-ASCII (emoji, unicode) — Kaplay can't render them
|
|
|
|
|
.replace(/\s+/g, ' ') // Collapse whitespace
|
|
|
|
|
.trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Speech bubble above a fighter — colourful with tail
|
|
|
|
|
// Active speech bubbles — destroy previous before showing new
|
|
|
|
|
let activeBubbleA: any[] = []
|
|
|
|
|
let activeBubbleB: any[] = []
|
|
|
|
|
|
|
|
|
|
function destroyBubble(els: any[]) {
|
|
|
|
|
els.forEach(el => { if (el.exists()) el.destroy() })
|
|
|
|
|
els.length = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showSpeechBubble(side: 'a' | 'b', text: string, duration: number = 3) {
|
2026-03-08 20:30:02 +00:00
|
|
|
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
|
2026-03-07 11:42:32 +00:00
|
|
|
if (!fighter || !text) return
|
|
|
|
|
|
|
|
|
|
// Kill previous bubble on this side
|
|
|
|
|
if (side === 'a') destroyBubble(activeBubbleA)
|
|
|
|
|
else destroyBubble(activeBubbleB)
|
|
|
|
|
|
|
|
|
|
const allEls: any[] = []
|
|
|
|
|
|
|
|
|
|
// Sanitize and truncate
|
|
|
|
|
const clean = safeText(text)
|
|
|
|
|
if (!clean) return
|
|
|
|
|
const maxLen = 80
|
|
|
|
|
const display = clean.length > maxLen ? clean.slice(0, maxLen - 1) + '...' : clean
|
|
|
|
|
const lines = wrapBubbleText(display, 18)
|
|
|
|
|
const fontSize = 12
|
|
|
|
|
const lineH = fontSize + 4
|
|
|
|
|
const padX = 10, padY = 8
|
|
|
|
|
const textW = Math.max(60, Math.min(lines.reduce((m, l) => Math.max(m, l.length * (fontSize * 0.6)), 0) + 8, W * 0.38))
|
|
|
|
|
const bubbleW = textW + padX * 2
|
|
|
|
|
const bubbleH = lines.length * lineH + padY * 2
|
|
|
|
|
const tailSize = 10
|
|
|
|
|
|
2026-03-07 15:20:14 +00:00
|
|
|
// Position — push bubble to the outside of the bot so it doesn't overlap the sprite
|
|
|
|
|
const offsetX = side === 'a' ? -bubbleW * 0.7 : bubbleW * 0.7
|
|
|
|
|
const bx = Math.max(bubbleW / 2 + 4, Math.min(W - bubbleW / 2 - 4, fighter.pos.x + offsetX))
|
|
|
|
|
const by = Math.max(8, fighter.pos.y - 75 - bubbleH)
|
2026-03-07 11:42:32 +00:00
|
|
|
|
|
|
|
|
const borderColor = side === 'a' ? '#00f0ff' : '#ff2d7b'
|
2026-03-07 11:59:10 +00:00
|
|
|
const bgColor = side === 'a' ? '#0a1e2a' : '#2a0a1e'
|
2026-03-07 11:42:32 +00:00
|
|
|
|
2026-03-07 11:59:10 +00:00
|
|
|
// Single border rect (no glow/accent layers — keeps rendering clean)
|
2026-03-07 11:42:32 +00:00
|
|
|
const border = k.add([
|
2026-03-07 11:59:10 +00:00
|
|
|
k.rect(bubbleW + 4, bubbleH + 4),
|
|
|
|
|
k.pos(bx - bubbleW / 2 - 2, by - 2),
|
2026-03-07 11:42:32 +00:00
|
|
|
k.color(safeColor(k, borderColor)), k.opacity(0.85), k.z(54),
|
|
|
|
|
])
|
|
|
|
|
allEls.push(border)
|
|
|
|
|
|
|
|
|
|
// Bubble body
|
|
|
|
|
const bubble = k.add([
|
|
|
|
|
k.rect(bubbleW, bubbleH),
|
|
|
|
|
k.pos(bx - bubbleW / 2, by),
|
|
|
|
|
k.color(safeColor(k, bgColor)), k.opacity(0.95), k.z(55),
|
|
|
|
|
])
|
|
|
|
|
allEls.push(bubble)
|
|
|
|
|
|
2026-03-07 11:59:10 +00:00
|
|
|
// Tail — pixel arrow pointing down to fighter
|
2026-03-07 11:42:32 +00:00
|
|
|
const tailX = bx + (side === 'a' ? bubbleW * 0.15 : -bubbleW * 0.15)
|
2026-03-07 11:59:10 +00:00
|
|
|
const tailBaseY = by + bubbleH
|
|
|
|
|
for (let row = 0; row < tailSize; row++) {
|
|
|
|
|
const tw = tailSize - row
|
|
|
|
|
// Border pixel row
|
|
|
|
|
const tailBorder = k.add([
|
|
|
|
|
k.rect(tw + 2, 1),
|
|
|
|
|
k.pos(tailX - (tw + 2) / 2, tailBaseY + row),
|
|
|
|
|
k.color(safeColor(k, borderColor)), k.opacity(0.85), k.z(54),
|
|
|
|
|
])
|
|
|
|
|
allEls.push(tailBorder)
|
|
|
|
|
// Inner pixel row
|
|
|
|
|
if (tw > 2) {
|
|
|
|
|
const tailInner = k.add([
|
|
|
|
|
k.rect(tw - 1, 1),
|
|
|
|
|
k.pos(tailX - (tw - 1) / 2, tailBaseY + row),
|
|
|
|
|
k.color(safeColor(k, bgColor)), k.opacity(0.95), k.z(55),
|
|
|
|
|
])
|
|
|
|
|
allEls.push(tailInner)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 11:42:32 +00:00
|
|
|
|
2026-03-07 15:20:14 +00:00
|
|
|
// Render text to an offscreen canvas, then display as a sprite
|
|
|
|
|
// This bypasses kaplay's text rendering which has color issues
|
|
|
|
|
const textCanvas = document.createElement('canvas')
|
|
|
|
|
textCanvas.width = bubbleW
|
|
|
|
|
textCanvas.height = bubbleH
|
|
|
|
|
const tc = textCanvas.getContext('2d')!
|
|
|
|
|
tc.fillStyle = '#ffffff'
|
|
|
|
|
tc.font = `${fontSize}px monospace`
|
|
|
|
|
tc.textBaseline = 'top'
|
2026-03-07 11:42:32 +00:00
|
|
|
for (let i = 0; i < lines.length; i++) {
|
2026-03-07 15:20:14 +00:00
|
|
|
tc.fillText(lines[i], padX, padY + i * lineH + 2)
|
|
|
|
|
}
|
|
|
|
|
const spriteKey = `bubble_${side}_${Date.now()}`
|
|
|
|
|
k.loadSprite(spriteKey, textCanvas.toDataURL()).then(() => {
|
2026-03-07 11:42:32 +00:00
|
|
|
const tEl = k.add([
|
2026-03-07 15:20:14 +00:00
|
|
|
k.sprite(spriteKey),
|
|
|
|
|
k.pos(bx - bubbleW / 2, by),
|
|
|
|
|
k.opacity(1), k.z(57),
|
2026-03-07 11:42:32 +00:00
|
|
|
])
|
|
|
|
|
allEls.push(tEl)
|
2026-03-07 15:20:14 +00:00
|
|
|
})
|
2026-03-07 11:42:32 +00:00
|
|
|
|
2026-03-07 11:59:10 +00:00
|
|
|
// Border glow pulse
|
2026-03-07 11:42:32 +00:00
|
|
|
border.onUpdate(() => {
|
|
|
|
|
border.opacity = 0.7 + Math.sin(k.time() * 4) * 0.15
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-07 11:59:10 +00:00
|
|
|
// Pop-in animation
|
2026-03-07 11:42:32 +00:00
|
|
|
const anchorX = bx, anchorY = by + bubbleH
|
|
|
|
|
allEls.forEach(el => {
|
|
|
|
|
const origX = el.pos.x, origY = el.pos.y
|
2026-03-07 11:59:10 +00:00
|
|
|
const origOpacity = el.opacity
|
2026-03-07 11:42:32 +00:00
|
|
|
el.pos.x = anchorX + (origX - anchorX) * 0.1
|
|
|
|
|
el.pos.y = anchorY + (origY - anchorY) * 0.1
|
2026-03-07 11:59:10 +00:00
|
|
|
el.opacity = 0
|
2026-03-07 11:42:32 +00:00
|
|
|
k.tween(0, 1, 0.25, (t) => {
|
|
|
|
|
el.pos.x = anchorX + (origX - anchorX) * t
|
|
|
|
|
el.pos.y = anchorY + (origY - anchorY) * t
|
2026-03-07 11:59:10 +00:00
|
|
|
el.opacity = origOpacity * t
|
2026-03-07 11:42:32 +00:00
|
|
|
}, k.easings.easeOutBack)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Store reference
|
|
|
|
|
if (side === 'a') activeBubbleA = allEls
|
|
|
|
|
else activeBubbleB = allEls
|
|
|
|
|
|
|
|
|
|
// Fade out after duration
|
2026-03-08 23:55:58 +00:00
|
|
|
trackedTimeout(() => {
|
2026-03-07 11:42:32 +00:00
|
|
|
allEls.forEach(el => {
|
|
|
|
|
if (!el.exists()) return
|
|
|
|
|
k.tween(el.opacity, 0, 0.35, (v) => { el.opacity = v }).then(() => {
|
|
|
|
|
if (el.exists()) el.destroy()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}, duration * 1000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function wrapBubbleText(text: string, maxChars: number): string[] {
|
|
|
|
|
const words = text.split(' ')
|
|
|
|
|
const lines: string[] = []
|
|
|
|
|
let line = ''
|
|
|
|
|
for (const word of words) {
|
|
|
|
|
if (line.length + word.length + 1 > maxChars && line.length > 0) {
|
|
|
|
|
lines.push(line)
|
|
|
|
|
line = word
|
|
|
|
|
} else {
|
|
|
|
|
line = line ? line + ' ' + word : word
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (line) lines.push(line)
|
|
|
|
|
// Max 5 lines, last line truncated
|
|
|
|
|
if (lines.length > 5) {
|
|
|
|
|
const truncated = [...lines.slice(0, 4), lines.slice(4).join(' ').slice(0, maxChars - 3) + '...']
|
|
|
|
|
return truncated
|
|
|
|
|
}
|
|
|
|
|
return lines
|
|
|
|
|
}
|
2026-03-07 00:53:15 +00:00
|
|
|
|
2026-03-08 12:08:18 +00:00
|
|
|
// Talking mouth animation — a small rectangle that opens/closes near the fighter's face
|
|
|
|
|
const talkingAnims: Record<string, { objs: any[]; timer: ReturnType<typeof setInterval> }> = {}
|
|
|
|
|
|
|
|
|
|
function startTalking(side: 'a' | 'b') {
|
|
|
|
|
stopTalking(side) // cleanup any existing
|
2026-03-08 20:30:02 +00:00
|
|
|
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] as Fighter
|
2026-03-08 12:08:18 +00:00
|
|
|
if (!fighter?.exists()) return
|
|
|
|
|
|
|
|
|
|
const mouthColor = side === 'a' ? '#00f0ff' : '#ff2d7b'
|
|
|
|
|
// Mouth position: slightly below center of the sprite
|
|
|
|
|
const mouthOffsetY = -28
|
|
|
|
|
const mouthW = 8
|
|
|
|
|
let open = false
|
|
|
|
|
|
|
|
|
|
const mouth = k.add([
|
|
|
|
|
k.rect(mouthW, 2),
|
|
|
|
|
k.pos(fighter.pos.x, fighter.pos.y + mouthOffsetY),
|
|
|
|
|
k.color(safeColor(k, mouthColor)),
|
|
|
|
|
k.opacity(0.9),
|
|
|
|
|
k.z(12),
|
|
|
|
|
k.anchor('center'),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Toggle open/close every 120ms for a fast chatter effect
|
2026-03-08 20:39:11 +00:00
|
|
|
const timer = trackedInterval(() => {
|
2026-03-08 12:08:18 +00:00
|
|
|
if (!mouth.exists() || !fighter.exists()) { stopTalking(side); return }
|
|
|
|
|
open = !open
|
|
|
|
|
mouth.pos.x = fighter.pos.x
|
|
|
|
|
mouth.pos.y = fighter.pos.y + mouthOffsetY
|
|
|
|
|
mouth.height = open ? 5 : 2
|
|
|
|
|
}, 120)
|
|
|
|
|
|
|
|
|
|
talkingAnims[side] = { objs: [mouth], timer }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopTalking(side: 'a' | 'b') {
|
|
|
|
|
const anim = talkingAnims[side]
|
|
|
|
|
if (!anim) return
|
2026-03-08 20:39:11 +00:00
|
|
|
clearTracked(anim.timer)
|
2026-03-08 12:08:18 +00:00
|
|
|
anim.objs.forEach(o => { if (o.exists()) o.destroy() })
|
|
|
|
|
delete talkingAnims[side]
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
// Spawn floating text above a position
|
|
|
|
|
function spawnEmoteText(x: number, y: number, text: string, color: string, duration: number = 1.2) {
|
|
|
|
|
const label = k.add([
|
2026-03-07 00:53:15 +00:00
|
|
|
k.text(safeText(text), { size: 10 }),
|
2026-03-07 00:14:46 +00:00
|
|
|
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() })
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 00:00:16 +00:00
|
|
|
// Crowd signs: pop up from bottom with text, bob, then fade out
|
|
|
|
|
function spawnCrowdSigns(count: number, color: string, text?: string) {
|
|
|
|
|
const labels = text ? [text] : ['GO!', 'WOW', 'KO!', 'LOL', 'GG', 'OMG']
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
const x = W * 0.15 + Math.random() * W * 0.7
|
|
|
|
|
const startY = H + 20
|
|
|
|
|
const endY = GROUND_Y + 10 + Math.random() * 20
|
|
|
|
|
const label = labels[Math.floor(Math.random() * labels.length)]
|
|
|
|
|
// Sign stick
|
|
|
|
|
const stick = k.add([
|
|
|
|
|
k.rect(3, 25),
|
|
|
|
|
k.pos(x, startY),
|
|
|
|
|
k.color(safeColor(k, '#886644')),
|
|
|
|
|
k.opacity(0.8),
|
|
|
|
|
k.z(8),
|
|
|
|
|
k.anchor('bot'),
|
|
|
|
|
])
|
|
|
|
|
// Sign face
|
|
|
|
|
const sign = k.add([
|
|
|
|
|
k.rect(Math.max(30, label.length * 10), 18),
|
|
|
|
|
k.pos(x, startY - 25),
|
|
|
|
|
k.color(safeColor(k, color)),
|
|
|
|
|
k.opacity(0.85),
|
|
|
|
|
k.z(8),
|
|
|
|
|
k.anchor('center'),
|
|
|
|
|
])
|
|
|
|
|
// Sign text
|
|
|
|
|
const txt = k.add([
|
|
|
|
|
k.text(label, { size: 10 }),
|
|
|
|
|
k.pos(x, startY - 25),
|
|
|
|
|
k.color(safeColor(k, '#ffffff')),
|
|
|
|
|
k.opacity(0.9),
|
|
|
|
|
k.z(9),
|
|
|
|
|
k.anchor('center'),
|
|
|
|
|
])
|
|
|
|
|
const delay = i * 0.08
|
|
|
|
|
// Rise up
|
|
|
|
|
k.wait(delay, () => {
|
|
|
|
|
k.tween(startY, endY, 0.3, (v) => { stick.pos.y = v; sign.pos.y = v - 25; txt.pos.y = v - 25 }, k.easings.easeOutBack)
|
|
|
|
|
})
|
|
|
|
|
// Bob + fade out after 1.5s
|
|
|
|
|
k.wait(delay + 1.5, () => {
|
|
|
|
|
k.tween(0.85, 0, 0.5, (v) => { stick.opacity = v; sign.opacity = v; txt.opacity = v }).then(() => {
|
|
|
|
|
if (stick.exists()) stick.destroy()
|
|
|
|
|
if (sign.exists()) sign.destroy()
|
|
|
|
|
if (txt.exists()) txt.destroy()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 00:14:46 +00:00
|
|
|
|
2026-03-07 09:15:41 +00:00
|
|
|
// === 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'
|
2026-03-08 20:30:02 +00:00
|
|
|
const fighter = k.get(fighterTag)[0] as Fighter
|
2026-03-07 09:15:41 +00:00
|
|
|
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'),
|
2026-03-07 10:18:46 +00:00
|
|
|
k.scale(fighter.scale.x / 3, fighter.scale.y / 3),
|
2026-03-07 09:15:41 +00:00
|
|
|
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()
|
2026-03-07 10:18:46 +00:00
|
|
|
if (roll > 0.15) return // 15% chance per round
|
2026-03-07 09:15:41 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:14:46 +00:00
|
|
|
// 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++) {
|
2026-03-08 23:55:58 +00:00
|
|
|
trackedTimeout(() => {
|
2026-03-07 00:14:46 +00:00
|
|
|
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')
|
|
|
|
|
}
|
2026-03-08 16:55:14 +00:00
|
|
|
sfxRandomComedy()
|
2026-03-07 00:14:46 +00:00
|
|
|
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
|
|
|
|
|
})
|
2026-03-08 16:55:14 +00:00
|
|
|
sfxRandomComedy()
|
2026-03-07 00:14:46 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 16:07:56 +00:00
|
|
|
// === SHOWBOATING / TAUNTING SYSTEM ===
|
2026-03-08 16:16:41 +00:00
|
|
|
// Fighters perform these during question/answer phases instead of boring idle bobbing
|
2026-03-08 16:07:56 +00:00
|
|
|
const _showboatActive: Record<string, boolean> = { a: false, b: false }
|
|
|
|
|
|
2026-03-08 16:16:41 +00:00
|
|
|
type ShowboatFn = (fighter: any, homeX: number, homeY: number, sx: number, sy: number, dir: number) => Promise<void>
|
2026-03-08 16:07:56 +00:00
|
|
|
|
2026-03-08 16:16:41 +00:00
|
|
|
// dir: 1 = facing right (bot A), -1 = facing left (bot B)
|
2026-03-08 16:07:56 +00:00
|
|
|
const showboats: ShowboatFn[] = [
|
2026-03-08 16:16:41 +00:00
|
|
|
// 0: Pace back and forth — walk a wide arc restlessly
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
2026-03-08 16:07:56 +00:00
|
|
|
f.play('kick')
|
2026-03-08 16:16:41 +00:00
|
|
|
await k.tween(f.pos.x, hx + dir * 100, 0.35, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
2026-03-08 16:07:56 +00:00
|
|
|
f.play('idle')
|
2026-03-08 16:16:41 +00:00
|
|
|
await k.wait(0.1)
|
|
|
|
|
f.play('attack')
|
|
|
|
|
await k.tween(f.pos.x, hx - dir * 60, 0.4, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
f.play('idle')
|
|
|
|
|
await k.wait(0.1)
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 1: Bouncing advance — hop toward opponent then back
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
const targetX = hx + dir * (40 + i * 30)
|
|
|
|
|
f.play('kick')
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, targetX, 0.12, (v) => { f.pos.x = v }, k.easings.easeOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy - 35, 0.06, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.06, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
sfxBoing()
|
|
|
|
|
}
|
|
|
|
|
f.play('win'); await k.wait(0.15)
|
2026-03-08 16:07:56 +00:00
|
|
|
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
2026-03-08 16:16:41 +00:00
|
|
|
// 2: Shadow box sprint — dash forward shadow boxing, dash back
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
const far = hx + dir * 120
|
|
|
|
|
f.play('attack'); sfxZoomWhoosh()
|
|
|
|
|
await k.tween(f.pos.x, far, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
|
f.play(i % 2 === 0 ? 'attack' : 'kick')
|
|
|
|
|
sfxDodge(); await k.wait(0.08)
|
|
|
|
|
}
|
|
|
|
|
f.play('special'); sfxZoomWhoosh()
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 3: Leapfrog across — multiple big jumps across the stage
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
const spots = [hx + dir * 60, hx + dir * 130, hx + dir * 60, hx]
|
|
|
|
|
for (const tx of spots) {
|
|
|
|
|
sfxBoing()
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, tx, 0.18, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy - 70, 0.09, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.09, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 4: Restless jog — jog back and forth with small hops
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('kick')
|
2026-03-08 16:07:56 +00:00
|
|
|
for (let i = 0; i < 5; i++) {
|
2026-03-08 16:16:41 +00:00
|
|
|
const dx = (i % 2 === 0 ? 1 : -1) * (30 + Math.random() * 40) * dir
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx + dx, 0.13, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 12, 0.065, (v) => { f.pos.y = v }).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.065, (v) => { f.pos.y = v })
|
|
|
|
|
),
|
|
|
|
|
])
|
2026-03-08 16:07:56 +00:00
|
|
|
}
|
|
|
|
|
f.pos.x = hx
|
|
|
|
|
},
|
2026-03-08 16:16:41 +00:00
|
|
|
// 5: Charge and flex — sprint to center, flex, sprint back
|
|
|
|
|
async (f, hx, hy, sx, sy, _dir) => {
|
|
|
|
|
const center = W / 2
|
|
|
|
|
f.play('special'); sfxZoomWhoosh()
|
|
|
|
|
await k.tween(f.pos.x, center, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad)
|
|
|
|
|
f.play('win')
|
|
|
|
|
await k.tween(1, 1.3, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutBack)
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 50, 'COME AT ME!', '#ff4444')
|
|
|
|
|
await k.wait(0.3)
|
|
|
|
|
await k.tween(1.3, 1, 0.1, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
|
|
|
|
|
sfxZoomWhoosh()
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 6: Triple backflip retreat — three backflips going backwards
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
sfxBoing()
|
|
|
|
|
const targetX = hx - dir * (i + 1) * 30
|
|
|
|
|
const a0 = f.angle || 0
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, targetX, 0.2, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 60, 0.1, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
k.tween(a0, a0 - 360, 0.2, (v) => { f.angle = v }),
|
|
|
|
|
])
|
|
|
|
|
f.angle = 0
|
|
|
|
|
}
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 7: Kangaroo hops — big bouncing jumps across the width
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('kick')
|
|
|
|
|
const hops = [hx + dir * 80, hx + dir * 150, hx + dir * 80, hx]
|
|
|
|
|
for (const tx of hops) {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, tx, 0.15, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy - 90, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
sfxBonk(); k.shake(2)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 8: Feint combo rush — charge in, throw combo, dodge back
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
sfxZoomWhoosh(); f.play('attack')
|
|
|
|
|
await k.tween(f.pos.x, hx + dir * 100, 0.12, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
const anims = ['attack', 'kick', 'attack', 'special'] as const
|
|
|
|
|
for (const a of anims) { f.play(a); sfxDodge(); await k.wait(0.06) }
|
|
|
|
|
sfxBlock(); spawnSparks(f.pos.x + dir * 15, hy - 30, 5, '#ffcc00')
|
|
|
|
|
// Dodge-hop back
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy - 40, 0.1, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
// 9: Dizzy spin walk — spin while walking forward, stumble back
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
const a0 = f.angle || 0
|
|
|
|
|
f.play('hit')
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx + dir * 70, 0.4, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(a0, a0 + 720, 0.4, (v) => { f.angle = v }),
|
|
|
|
|
])
|
|
|
|
|
f.angle = 0
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 40, '@_@', '#ffcc00')
|
|
|
|
|
await k.wait(0.15)
|
|
|
|
|
// Stumble back
|
|
|
|
|
f.play('idle')
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
await k.tween(f.pos.x, f.pos.x - dir * 25, 0.1, (v) => { f.pos.x = v })
|
|
|
|
|
f.pos.y = hy - 5; await k.wait(0.02); f.pos.y = hy
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 10: Wall-to-wall dash — full screen sprint and back
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('special'); sfxZoomWhoosh()
|
|
|
|
|
spawnAfterimages(f, 3)
|
|
|
|
|
await k.tween(f.pos.x, dir > 0 ? W - 30 : 30, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad)
|
|
|
|
|
sfxBlock(); k.shake(4)
|
|
|
|
|
await k.wait(0.1)
|
|
|
|
|
sfxZoomWhoosh(); spawnAfterimages(f, 3)
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 11: Earthquake stomp march — stomp forward and back
|
|
|
|
|
async (f, hx, hy, sx, sy, dir) => {
|
|
|
|
|
f.play('attack')
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
|
const step = dir * 25
|
|
|
|
|
await k.tween(f.pos.x, f.pos.x + step, 0.08, (v) => { f.pos.x = v })
|
|
|
|
|
f.scale.y = sy * 0.8; sfxBonk(); k.shake(3)
|
|
|
|
|
await k.wait(0.05); f.scale.y = sy
|
|
|
|
|
}
|
|
|
|
|
spawnSparks(f.pos.x, hy, 6, '#ff6600')
|
|
|
|
|
await k.wait(0.1)
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 12: Taunt walk — strut toward opponent, gesture, moonwalk back
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('win')
|
|
|
|
|
await k.tween(f.pos.x, hx + dir * 90, 0.35, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
f.play('special')
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 50, 'TOO EASY!', '#ff44ff')
|
|
|
|
|
await k.wait(0.3)
|
|
|
|
|
// Moonwalk back
|
|
|
|
|
f.play('idle')
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.5, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 13: Victory lap sprint — run around in a loop
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('kick'); sfxZoomWhoosh()
|
|
|
|
|
await k.tween(f.pos.x, hx + dir * 140, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad)
|
|
|
|
|
// Jump over to other side
|
|
|
|
|
sfxBoing()
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx + dir * 140, 0.25, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 100, 0.125, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.125, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
f.play('win'); spawnSparks(f.pos.x, hy - 20, 5, theme.accent)
|
|
|
|
|
await k.wait(0.15)
|
|
|
|
|
sfxZoomWhoosh()
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 14: Nervous pacing — quick short paces with jittery hops
|
|
|
|
|
async (f, hx, hy) => {
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
const dx = (Math.random() - 0.5) * 80
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx + dx, 0.1, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 10 - Math.random() * 20, 0.05, (v) => { f.pos.y = v }).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.05, (v) => { f.pos.y = v })
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 15: Ground pound jump — massive vertical jump + slam
|
|
|
|
|
async (f, hx, hy) => {
|
|
|
|
|
sfxBoing(); f.play('kick')
|
|
|
|
|
await k.tween(f.pos.y, hy - 150, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
|
|
|
|
|
await k.wait(0.1)
|
|
|
|
|
f.play('attack')
|
|
|
|
|
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
sfxExplosion(); k.shake(15)
|
|
|
|
|
spawnSparks(f.pos.x, hy, 15, '#ff6600')
|
|
|
|
|
spawnShockwave(f.pos.x, hy, '#ff4400')
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 60, 'BOOM!', '#ff2d2d')
|
|
|
|
|
await k.wait(0.2)
|
|
|
|
|
},
|
|
|
|
|
// 16: Zigzag sprint — fast zigzag across the arena
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('kick'); sfxZoomWhoosh()
|
|
|
|
|
const pts = [
|
|
|
|
|
{ x: hx + dir * 50, y: hy - 40 },
|
|
|
|
|
{ x: hx + dir * 100, y: hy },
|
|
|
|
|
{ x: hx + dir * 150, y: hy - 40 },
|
|
|
|
|
{ x: hx + dir * 100, y: hy },
|
|
|
|
|
{ x: hx, y: hy },
|
|
|
|
|
]
|
|
|
|
|
for (const pt of pts) {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, pt.x, 0.08, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, pt.y, 0.08, (v) => { f.pos.y = v }),
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 17: Double jump kick — jump, kick, jump higher, kick
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('kick'); sfxBoing()
|
|
|
|
|
const mid = hx + dir * 60
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, mid, 0.15, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 50, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
sfxKick(); spawnSparks(mid, hy - 30, 4, '#00ffcc')
|
|
|
|
|
f.play('special'); sfxBoing()
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, mid + dir * 40, 0.15, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 90, 0.075, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.075, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
sfxKick(); spawnSparks(f.pos.x, hy - 50, 6, '#ff44ff')
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 18: Cocky strut and lean — walk forward, lean back with attitude
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('win')
|
|
|
|
|
await k.tween(f.pos.x, hx + dir * 80, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
// Lean back
|
|
|
|
|
await k.tween(0, -25, 0.15, (v) => { f.angle = v }, k.easings.easeOutQuad)
|
|
|
|
|
await k.wait(0.25)
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 50, 'EZ', '#ffcc00')
|
|
|
|
|
await k.tween(-25, 0, 0.1, (v) => { f.angle = v })
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 19: Windmill sprint — spin while running across
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
const a0 = f.angle || 0
|
|
|
|
|
const anims = ['attack', 'kick', 'special'] as const
|
|
|
|
|
let animIdx = 0
|
|
|
|
|
const far = hx + dir * 130
|
|
|
|
|
f.play(anims[0])
|
|
|
|
|
// Sprint while spinning
|
|
|
|
|
const dur = 0.4
|
|
|
|
|
const startX = f.pos.x
|
|
|
|
|
await k.tween(0, 1, dur, (t) => {
|
|
|
|
|
f.pos.x = startX + (far - startX) * t
|
|
|
|
|
f.angle = a0 + t * 1080 // 3 full spins
|
|
|
|
|
if (Math.floor(t * 12) !== Math.floor((t - 0.01) * 12)) {
|
|
|
|
|
f.play(anims[animIdx++ % anims.length])
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
f.angle = 0; sfxBlock(); k.shake(3)
|
|
|
|
|
await k.wait(0.1)
|
|
|
|
|
// Sprint back
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 20: Sumo stomp walk — heavy stomps advancing and retreating
|
|
|
|
|
async (f, hx, hy, _sx, sy, dir) => {
|
|
|
|
|
f.play('attack')
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
const step = (i < 3 ? dir : -dir) * 20
|
|
|
|
|
await k.tween(f.pos.x, f.pos.x + step, 0.1, (v) => { f.pos.x = v })
|
|
|
|
|
f.scale.y = sy * 0.8; sfxBonk(); k.shake(3)
|
|
|
|
|
await k.wait(0.06); f.scale.y = sy
|
|
|
|
|
spawnSparks(f.pos.x, hy, 2, '#886644')
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 21: Flex pose — walk to center, scale up, pose
|
|
|
|
|
async (f, hx, hy, sx, sy) => {
|
|
|
|
|
const center = W / 2
|
|
|
|
|
f.play('win')
|
|
|
|
|
await k.tween(f.pos.x, center, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
await k.tween(1, 1.35, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutBack)
|
|
|
|
|
sfxPowerUp()
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 60, 'COME ON!', '#ff4444')
|
|
|
|
|
await k.wait(0.35)
|
|
|
|
|
await k.tween(1.35, 1, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 22: Jump-kick air combo — big jump with mid-air kicks
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
sfxBoing()
|
|
|
|
|
const apex = hy - 120
|
|
|
|
|
const far = hx + dir * 100
|
|
|
|
|
await k.tween(f.pos.y, apex, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
|
|
|
|
|
// Mid-air combo while moving
|
|
|
|
|
f.play('kick'); sfxKick()
|
|
|
|
|
await k.tween(f.pos.x, far, 0.1, (v) => { f.pos.x = v })
|
|
|
|
|
f.play('attack'); sfxPunch()
|
|
|
|
|
await k.tween(f.pos.x, far + dir * 30, 0.08, (v) => { f.pos.x = v })
|
|
|
|
|
f.play('special')
|
|
|
|
|
spawnSparks(f.pos.x, f.pos.y, 8, '#ff44ff')
|
|
|
|
|
await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
sfxBonk(); k.shake(4)
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 23: Air guitar walk — bouncing left and right with music
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('special'); sfxSlideUp()
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 50, '~♪~', '#ff44ff')
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
const dx = (i % 2 === 0 ? dir : -dir) * (30 + i * 10)
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx + dx, 0.12, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 15, 0.06, (v) => { f.pos.y = v }).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.06, (v) => { f.pos.y = v })
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
f.pos.x = hx
|
|
|
|
|
},
|
|
|
|
|
// 24: Crane kick walk — slow martial arts advancing
|
|
|
|
|
async (f, hx, hy, _sx, sy, dir) => {
|
2026-03-08 16:07:56 +00:00
|
|
|
f.play('kick')
|
|
|
|
|
await Promise.all([
|
2026-03-08 16:16:41 +00:00
|
|
|
k.tween(f.pos.x, hx + dir * 60, 0.3, (v) => { f.pos.x = v }),
|
2026-03-08 16:07:56 +00:00
|
|
|
k.tween(f.pos.y, hy - 30, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
|
|
|
|
|
k.tween(1, 1.15, 0.3, (v) => { f.scale.y = sy * v }, k.easings.easeOutQuad),
|
|
|
|
|
])
|
2026-03-08 16:16:41 +00:00
|
|
|
await k.wait(0.3)
|
|
|
|
|
f.play('special'); sfxKick()
|
|
|
|
|
spawnSparks(f.pos.x + dir * 20, hy - 40, 6, '#00ccff')
|
|
|
|
|
await k.wait(0.15)
|
|
|
|
|
f.scale.y = sy
|
2026-03-08 16:07:56 +00:00
|
|
|
await Promise.all([
|
2026-03-08 16:16:41 +00:00
|
|
|
k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad),
|
2026-03-08 16:07:56 +00:00
|
|
|
])
|
|
|
|
|
},
|
2026-03-08 16:16:41 +00:00
|
|
|
// 25: Girlfriend kiss — girlfriend walks in, kiss, hearts, she storms off
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
const startGfX = hx - dir * 120
|
|
|
|
|
const gfColor = ['#ff69b4', '#ff1493', '#ff6eb4', '#cc44aa'][Math.floor(Math.random() * 4)]
|
|
|
|
|
const cs = SF
|
|
|
|
|
const gf = k.add([k.rect(18 * cs, 35 * cs), k.pos(startGfX, hy - 18 * cs), k.anchor('center'), k.color(safeColor(k, gfColor)), k.opacity(0.9), k.z(11)])
|
|
|
|
|
const gfHair = k.add([k.rect(22 * cs, 10 * cs), k.pos(startGfX, hy - 35 * cs), k.anchor('center'), k.color(safeColor(k, '#8b4513')), k.opacity(0.9), k.z(12)])
|
|
|
|
|
// Walk toward fighter
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(gf.pos.x, hx - dir * 15, 0.4, (v) => { gf.pos.x = v; gfHair.pos.x = v }, k.easings.easeInOutQuad),
|
|
|
|
|
])
|
|
|
|
|
// Kiss!
|
|
|
|
|
sfxBoing()
|
|
|
|
|
const heart = k.add([k.text('*smooch*', { size: 10 }), k.pos(hx, hy - 55), k.anchor('center'), k.color(safeColor(k, '#ff1493')), k.opacity(1), k.z(30)])
|
|
|
|
|
k.tween(heart.pos.y, hy - 90, 0.8, (v) => { heart.pos.y = v })
|
|
|
|
|
k.tween(1, 0, 0.8, (v) => { heart.opacity = v }).then(() => { if (heart.exists()) heart.destroy() })
|
|
|
|
|
// Spawn floating hearts
|
2026-03-08 16:07:56 +00:00
|
|
|
for (let i = 0; i < 5; i++) {
|
2026-03-08 16:16:41 +00:00
|
|
|
const h = k.add([k.text('♥', { size: 8 + Math.random() * 6 }), k.pos(hx + (Math.random() - 0.5) * 30, hy - 30), k.color(safeColor(k, '#ff69b4')), k.opacity(0.8), k.z(25), k.anchor('center')])
|
|
|
|
|
k.tween(h.pos.y, hy - 70 - Math.random() * 40, 0.7, (v) => { h.pos.y = v })
|
|
|
|
|
k.tween(h.pos.x, h.pos.x + (Math.random() - 0.5) * 40, 0.7, (v) => { h.pos.x = v })
|
|
|
|
|
k.tween(0.8, 0, 0.7, (v) => { h.opacity = v }).then(() => { if (h.exists()) h.destroy() })
|
2026-03-08 16:07:56 +00:00
|
|
|
}
|
|
|
|
|
f.play('win')
|
2026-03-08 16:16:41 +00:00
|
|
|
await k.wait(0.5)
|
|
|
|
|
// She storms off
|
|
|
|
|
spawnEmoteText(gf.pos.x, gf.pos.y - 30, 'Good luck babe!', '#ff69b4')
|
|
|
|
|
await k.tween(gf.pos.x, startGfX, 0.4, (v) => { gf.pos.x = v; gfHair.pos.x = v }, k.easings.easeInQuad)
|
|
|
|
|
gf.destroy(); gfHair.destroy()
|
|
|
|
|
},
|
|
|
|
|
// 26: Push-up walk — walk forward doing push-ups at intervals
|
|
|
|
|
async (f, hx, hy, sx, sy, dir) => {
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
await k.tween(f.pos.x, f.pos.x + dir * 35, 0.15, (v) => { f.pos.x = v })
|
|
|
|
|
// Push-up squash
|
|
|
|
|
await k.tween(1, 0.55, 0.08, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeOutQuad)
|
|
|
|
|
await k.tween(0.55, 1, 0.08, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeInQuad)
|
|
|
|
|
}
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 45, `${3 + Math.floor(Math.random() * 97)}!`, '#00ff88')
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 27: Matrix dodge walk — dodge bullets walking forward
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
|
await k.tween(f.pos.x, f.pos.x + dir * 25, 0.1, (v) => { f.pos.x = v })
|
|
|
|
|
// Lean dodge
|
|
|
|
|
const lean = (i % 2 === 0 ? -30 : 30)
|
|
|
|
|
await k.tween(0, lean, 0.08, (v) => { f.angle = v }, k.easings.easeOutQuad)
|
|
|
|
|
await k.tween(lean, 0, 0.08, (v) => { f.angle = v }, k.easings.easeInQuad)
|
|
|
|
|
}
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 28: Bouncy chest thump — bounce toward opponent thumping chest
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('attack')
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, f.pos.x + dir * 30, 0.1, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 25, 0.05, (v) => { f.pos.y = v }).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.05, (v) => { f.pos.y = v })
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
sfxBlock(); k.shake(2)
|
|
|
|
|
}
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 50, 'LET\'S GO!', '#ff4444')
|
|
|
|
|
await k.wait(0.15)
|
2026-03-08 16:07:56 +00:00
|
|
|
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
2026-03-08 16:16:41 +00:00
|
|
|
// 29: Speed demon — afterimage dash back and forth
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('special')
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
sfxZoomWhoosh(); spawnAfterimages(f, 3)
|
|
|
|
|
await k.tween(f.pos.x, hx + dir * (80 + i * 30), 0.08, (v) => { f.pos.x = v }, k.easings.easeInQuad)
|
|
|
|
|
spawnAfterimages(f, 2)
|
|
|
|
|
await k.tween(f.pos.x, hx - dir * 20, 0.08, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
}
|
|
|
|
|
f.pos.x = hx
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// === CREATOR-EXCLUSIVE MAGICAL SHOWBOATS ===
|
|
|
|
|
const creatorShowboats: ShowboatFn[] = [
|
|
|
|
|
// 0: Levitate across — float up and glide across the screen trailing golden ₿
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('special'); sfxPowerUp()
|
|
|
|
|
await k.tween(f.pos.y, hy - 100, 0.25, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
|
|
|
|
|
// Float across dropping ₿ rain
|
|
|
|
|
const startX = f.pos.x
|
|
|
|
|
const far = hx + dir * 180
|
|
|
|
|
const dur = 0.6
|
|
|
|
|
let lastDrop = 0
|
|
|
|
|
await k.tween(0, 1, dur, (t) => {
|
|
|
|
|
f.pos.x = startX + (far - startX) * t
|
|
|
|
|
f.pos.y = hy - 100 + Math.sin(t * Math.PI * 3) * 20
|
|
|
|
|
if (t - lastDrop > 0.08) {
|
|
|
|
|
lastDrop = t
|
|
|
|
|
const d = k.add([k.text('₿', { size: 6 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y + 10), k.color(safeColor(k, '#ffd700')), k.opacity(0.7), k.z(9), k.anchor('center')])
|
|
|
|
|
k.tween(d.pos.y, hy + 10, 0.5, (v) => { d.pos.y = v })
|
|
|
|
|
k.tween(0.7, 0, 0.5, (v) => { d.opacity = v }).then(() => { if (d.exists()) d.destroy() })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
screenFlash('#ffd700', 0.06)
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
},
|
|
|
|
|
// 1: Golden portal teleport — vanish, portals, reappear at random spots
|
|
|
|
|
async (f, hx, hy) => {
|
|
|
|
|
sfxSpecial()
|
|
|
|
|
// Vanish with flash
|
|
|
|
|
screenFlash('#ffd700', 0.08)
|
|
|
|
|
f.opacity = 0
|
|
|
|
|
// Teleport to 3 spots with golden portals
|
|
|
|
|
const spots = [
|
|
|
|
|
{ x: W * 0.2, y: hy - 60 },
|
|
|
|
|
{ x: W * 0.8, y: hy - 80 },
|
|
|
|
|
{ x: W * 0.5, y: hy - 120 },
|
|
|
|
|
]
|
|
|
|
|
for (const spot of spots) {
|
|
|
|
|
// Portal flash at destination
|
|
|
|
|
const portal = k.add([k.circle(20), k.pos(spot.x, spot.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.6), k.z(15), k.anchor('center')])
|
|
|
|
|
spawnSparks(spot.x, spot.y, 6, '#ffd700')
|
|
|
|
|
f.pos.x = spot.x; f.pos.y = spot.y; f.opacity = 1
|
|
|
|
|
f.play('special')
|
|
|
|
|
await k.wait(0.15)
|
|
|
|
|
f.opacity = 0
|
|
|
|
|
k.tween(0.6, 0, 0.2, (v) => { portal.opacity = v }).then(() => { if (portal.exists()) portal.destroy() })
|
|
|
|
|
sfxZap()
|
|
|
|
|
}
|
|
|
|
|
// Return home with big flash
|
|
|
|
|
screenFlash('#ffd700', 0.1)
|
|
|
|
|
f.pos.x = hx; f.pos.y = hy; f.opacity = 1
|
|
|
|
|
f.play('win')
|
|
|
|
|
spawnShockwave(hx, hy, '#ffd700')
|
|
|
|
|
sfxExplosion()
|
|
|
|
|
await k.wait(0.2)
|
|
|
|
|
},
|
|
|
|
|
// 2: Code rain summon — float up, summon massive golden code rain
|
|
|
|
|
async (f, hx, hy) => {
|
|
|
|
|
f.play('special'); sfxPowerUp()
|
|
|
|
|
await k.tween(f.pos.y, hy - 80, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
|
|
|
|
|
// Massive code rain
|
|
|
|
|
const chars = '₿01∞⚡⛏§∂∆≈≠'.split('')
|
|
|
|
|
for (let col = 0; col < 20; col++) {
|
|
|
|
|
const cx = Math.random() * W
|
|
|
|
|
for (let row = 0; row < 3; row++) {
|
|
|
|
|
const ch = chars[Math.floor(Math.random() * chars.length)]
|
|
|
|
|
const d = k.add([
|
|
|
|
|
k.text(ch, { size: 6 + Math.random() * 4 }), k.pos(cx, -10 - row * 20),
|
|
|
|
|
k.color(safeColor(k, row === 0 ? '#ffd700' : '#c8a000')),
|
|
|
|
|
k.opacity(0.5 + Math.random() * 0.4), k.z(8), k.anchor('center'),
|
|
|
|
|
])
|
|
|
|
|
k.tween(d.pos.y, hy + 30, 0.5 + Math.random() * 0.4, (v) => { d.pos.y = v; d.opacity = Math.max(0, 1 - (v - hy + 30) / 60) })
|
|
|
|
|
.then(() => { if (d.exists()) d.destroy() })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await k.wait(0.6)
|
|
|
|
|
await k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
},
|
|
|
|
|
// 3: ₿ orbit explosion — fly to center, ₿ symbols orbit then explode outward
|
|
|
|
|
async (f, hx, hy, sx, sy) => {
|
|
|
|
|
sfxZoomWhoosh(); f.play('special')
|
|
|
|
|
const center = W / 2
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, center, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad),
|
|
|
|
|
k.tween(f.pos.y, hy - 60, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
|
|
|
|
|
])
|
|
|
|
|
// Spawn orbiting ₿
|
|
|
|
|
const orbs: any[] = []
|
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
|
const orb = k.add([k.text('₿', { size: 8 + Math.random() * 6 }), k.pos(center, f.pos.y), k.color(safeColor(k, i % 2 === 0 ? '#ffd700' : '#ff8c00')), k.opacity(0.8), k.z(15), k.anchor('center')])
|
|
|
|
|
orbs.push(orb)
|
|
|
|
|
}
|
|
|
|
|
// Orbit for a moment
|
|
|
|
|
const startTime = k.time()
|
|
|
|
|
const orbitCancel = k.onUpdate(() => {
|
|
|
|
|
const t = k.time() - startTime
|
|
|
|
|
for (let i = 0; i < orbs.length; i++) {
|
|
|
|
|
const a = t * 4 + i * (Math.PI * 2 / orbs.length)
|
|
|
|
|
const r = 30 + t * 15
|
|
|
|
|
orbs[i].pos.x = f.pos.x + Math.cos(a) * r
|
|
|
|
|
orbs[i].pos.y = f.pos.y + Math.sin(a) * r * 0.5
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
await k.wait(0.5)
|
|
|
|
|
orbitCancel.cancel()
|
|
|
|
|
// Explode outward
|
|
|
|
|
sfxExplosion(); screenFlash('#ffd700', 0.08); k.shake(8)
|
|
|
|
|
for (const orb of orbs) {
|
|
|
|
|
const angle = Math.random() * Math.PI * 2
|
|
|
|
|
k.tween(orb.pos.x, orb.pos.x + Math.cos(angle) * 200, 0.3, (v) => { orb.pos.x = v })
|
|
|
|
|
k.tween(orb.pos.y, orb.pos.y + Math.sin(angle) * 200, 0.3, (v) => { orb.pos.y = v })
|
|
|
|
|
k.tween(0.8, 0, 0.3, (v) => { orb.opacity = v }).then(() => { if (orb.exists()) orb.destroy() })
|
|
|
|
|
}
|
|
|
|
|
await k.wait(0.2)
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad),
|
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
// 4: Lightning flight — zigzag across the screen at super speed with lightning
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('special'); sfxZoomWhoosh()
|
|
|
|
|
f.opacity = 0.8
|
|
|
|
|
const pts = [
|
|
|
|
|
{ x: hx + dir * 60, y: hy - 80 },
|
|
|
|
|
{ x: W / 2, y: hy - 140 },
|
|
|
|
|
{ x: hx + dir * 180, y: hy - 60 },
|
|
|
|
|
{ x: W / 2, y: hy - 100 },
|
|
|
|
|
{ x: hx, y: hy },
|
|
|
|
|
]
|
|
|
|
|
for (const pt of pts) {
|
|
|
|
|
spawnAfterimages(f, 2)
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, pt.x, 0.08, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, pt.y, 0.08, (v) => { f.pos.y = v }),
|
|
|
|
|
])
|
|
|
|
|
// Lightning bolt at each stop
|
|
|
|
|
sfxZap()
|
|
|
|
|
const bolt = k.add([k.rect(2, 30 + Math.random() * 40), k.pos(f.pos.x, f.pos.y - 50), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(20), k.anchor('center'), k.rotate(Math.random() * 30 - 15)])
|
|
|
|
|
k.tween(0.9, 0, 0.15, (v) => { bolt.opacity = v }).then(() => { if (bolt.exists()) bolt.destroy() })
|
|
|
|
|
}
|
|
|
|
|
f.opacity = 1
|
|
|
|
|
},
|
|
|
|
|
// 5: Satoshi meditation — float to center, golden aura pulse, wisdom text
|
|
|
|
|
async (f, hx, hy, sx, sy) => {
|
|
|
|
|
sfxPowerUp(); f.play('special')
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, W / 2, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy - 70, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
|
|
|
|
|
])
|
|
|
|
|
// Golden aura rings
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
const ring = k.add([k.circle(10), k.pos(f.pos.x, f.pos.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.5), k.z(9), k.anchor('center'), k.scale(1)])
|
|
|
|
|
k.tween(1, (60 + i * 20) / 10, 0.4, (v) => { ring.scale.x = v; ring.scale.y = v })
|
|
|
|
|
k.tween(0.5, 0, 0.4, (v) => { ring.opacity = v }).then(() => { if (ring.exists()) ring.destroy() })
|
|
|
|
|
await k.wait(0.12)
|
|
|
|
|
}
|
|
|
|
|
// Scale pulse
|
|
|
|
|
await k.tween(1, 1.2, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutQuad)
|
|
|
|
|
const sayings = ['PROOF OF WORK', 'VERIFY, DON\'T TRUST', '21 MILLION', 'TICK TOCK NEXT BLOCK', 'NOT YOUR KEYS', 'STAY HUMBLE STACK SATS']
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 50, sayings[Math.floor(Math.random() * sayings.length)], '#ffd700')
|
|
|
|
|
await k.wait(0.4)
|
|
|
|
|
await k.tween(1.2, 1, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad),
|
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
// 6: Golden tornado — spin rapidly while moving in a circle across the arena
|
|
|
|
|
async (f, hx, hy) => {
|
|
|
|
|
sfxZoomWhoosh(); f.play('special')
|
|
|
|
|
const cx = W / 2, cy = hy - 50, radius = 100
|
|
|
|
|
const dur = 0.8
|
|
|
|
|
let lastParticle = 0
|
|
|
|
|
await k.tween(0, Math.PI * 2, dur, (a) => {
|
|
|
|
|
f.pos.x = cx + Math.cos(a) * radius
|
|
|
|
|
f.pos.y = cy + Math.sin(a) * radius * 0.4
|
|
|
|
|
f.angle = (a / (Math.PI * 2)) * 1440 // 4 full spins
|
|
|
|
|
if (a - lastParticle > 0.3) {
|
|
|
|
|
lastParticle = a
|
|
|
|
|
const p = k.add([k.text('₿', { size: 5 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y), k.color(safeColor(k, '#ffd700')), k.opacity(0.6), k.z(9), k.anchor('center')])
|
|
|
|
|
k.tween(0.6, 0, 0.4, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
f.angle = 0
|
|
|
|
|
sfxExplosion(); k.shake(6)
|
|
|
|
|
spawnShockwave(f.pos.x, f.pos.y, '#ffd700')
|
|
|
|
|
f.pos.x = hx; f.pos.y = hy
|
|
|
|
|
},
|
|
|
|
|
// 7: Laptop hack — walk forward, pull out laptop (rect), type furiously, spawn effects
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('special')
|
|
|
|
|
await k.tween(f.pos.x, hx + dir * 50, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
// Laptop
|
|
|
|
|
const cs = SF
|
|
|
|
|
const laptop = k.add([k.rect(24 * cs, 16 * cs), k.pos(f.pos.x + dir * 15, hy - 20 * cs), k.anchor('center'), k.color(safeColor(k, '#333333')), k.opacity(0.9), k.z(12)])
|
|
|
|
|
const screen = k.add([k.rect(20 * cs, 12 * cs), k.pos(f.pos.x + dir * 15, hy - 22 * cs), k.anchor('center'), k.color(safeColor(k, '#00ff00')), k.opacity(0.7), k.z(13)])
|
|
|
|
|
// Type — spawn code characters
|
|
|
|
|
const codeChars = '₿{}();=>0x1f'.split('')
|
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
|
const ch = codeChars[Math.floor(Math.random() * codeChars.length)]
|
|
|
|
|
const t = k.add([k.text(ch, { size: 5 }), k.pos(screen.pos.x + (Math.random() - 0.5) * 15, screen.pos.y), k.color(safeColor(k, '#00ff00')), k.opacity(0.8), k.z(14), k.anchor('center')])
|
|
|
|
|
k.tween(t.pos.y, t.pos.y - 25 - Math.random() * 20, 0.3, (v) => { t.pos.y = v })
|
|
|
|
|
k.tween(0.8, 0, 0.3, (v) => { t.opacity = v }).then(() => { if (t.exists()) t.destroy() })
|
|
|
|
|
sfxCoin()
|
|
|
|
|
await k.wait(0.06)
|
|
|
|
|
}
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 55, 'HACKING...', '#00ff00')
|
|
|
|
|
await k.wait(0.2)
|
|
|
|
|
laptop.destroy(); screen.destroy()
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 8: Bitcoin rain shower — fly across dropping ₿ coins everywhere
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
sfxPowerUp(); f.play('win')
|
|
|
|
|
await k.tween(f.pos.y, hy - 90, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
|
|
|
|
|
// Fly from one side to the other
|
|
|
|
|
const startX = dir > 0 ? 30 : W - 30
|
|
|
|
|
const endX = dir > 0 ? W - 30 : 30
|
|
|
|
|
f.pos.x = startX
|
|
|
|
|
const dur = 0.6
|
|
|
|
|
let lastCoin = 0
|
|
|
|
|
await k.tween(0, 1, dur, (t) => {
|
|
|
|
|
f.pos.x = startX + (endX - startX) * t
|
|
|
|
|
f.pos.y = hy - 90 + Math.sin(t * Math.PI * 4) * 15
|
|
|
|
|
if (t - lastCoin > 0.04) {
|
|
|
|
|
lastCoin = t
|
|
|
|
|
const coin = k.add([k.text('₿', { size: 8 + Math.random() * 4 }), k.pos(f.pos.x, f.pos.y + 15), k.color(safeColor(k, Math.random() > 0.5 ? '#ffd700' : '#ff8c00')), k.opacity(0.8), k.z(9), k.anchor('center')])
|
|
|
|
|
k.tween(coin.pos.y, hy + 10, 0.4, (v) => { coin.pos.y = v })
|
|
|
|
|
k.tween(coin.pos.x, coin.pos.x + (Math.random() - 0.5) * 30, 0.4, (v) => { coin.pos.x = v })
|
|
|
|
|
k.tween(0.8, 0, 0.4, (v) => { coin.opacity = v }).then(() => { if (coin.exists()) coin.destroy() })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
sfxCoin()
|
|
|
|
|
f.pos.x = hx
|
|
|
|
|
await k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
},
|
|
|
|
|
// 9: Dimension rift — creator tears open a golden rift, steps through, appears elsewhere
|
|
|
|
|
async (f, hx, hy) => {
|
|
|
|
|
// Rift opens
|
|
|
|
|
sfxSpecial()
|
|
|
|
|
const riftX = f.pos.x + 30
|
|
|
|
|
const rift = k.add([k.rect(4, 50), k.pos(riftX, hy - 25), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(15)])
|
|
|
|
|
await k.tween(4, 30, 0.2, (v) => { rift.width = v })
|
|
|
|
|
spawnSparks(riftX, hy - 25, 8, '#ffd700')
|
|
|
|
|
// Step through — vanish
|
|
|
|
|
f.play('special')
|
|
|
|
|
await k.tween(f.pos.x, riftX, 0.1, (v) => { f.pos.x = v })
|
|
|
|
|
f.opacity = 0; sfxZap()
|
|
|
|
|
// Rift at destination
|
|
|
|
|
const destX = W - riftX
|
|
|
|
|
const rift2 = k.add([k.rect(30, 50), k.pos(destX, hy - 25), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(15)])
|
|
|
|
|
spawnSparks(destX, hy - 25, 8, '#ffd700')
|
|
|
|
|
await k.wait(0.15)
|
|
|
|
|
// Step out
|
|
|
|
|
f.pos.x = destX; f.opacity = 1; f.play('win')
|
|
|
|
|
sfxSpecial()
|
|
|
|
|
await k.wait(0.2)
|
|
|
|
|
// Close rifts
|
|
|
|
|
k.tween(0.9, 0, 0.2, (v) => { rift.opacity = v; rift2.opacity = v }).then(() => { if (rift.exists()) rift.destroy(); if (rift2.exists()) rift2.destroy() })
|
|
|
|
|
// Float home
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad),
|
|
|
|
|
k.tween(f.pos.y, hy - 40, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.15, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
// 10: Golden ascension — rise way up, glow, descend with shockwave
|
|
|
|
|
async (f, hx, hy, sx, sy) => {
|
|
|
|
|
sfxPowerUp(); f.play('special')
|
|
|
|
|
await k.tween(f.pos.y, hy - 160, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
|
|
|
|
|
// Glow pulse
|
|
|
|
|
await k.tween(1, 1.5, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutQuad)
|
|
|
|
|
screenFlash('#ffd700', 0.06)
|
|
|
|
|
// Spawn golden aura particles
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
const p = k.add([k.text('✦', { size: 4 + Math.random() * 6 }), k.pos(f.pos.x + (Math.random() - 0.5) * 40, f.pos.y + (Math.random() - 0.5) * 30), k.color(safeColor(k, '#ffd700')), k.opacity(0.7), k.z(9), k.anchor('center')])
|
|
|
|
|
k.tween(0.7, 0, 0.5, (v) => { p.opacity = v; p.pos.y -= 0.5 }).then(() => { if (p.exists()) p.destroy() })
|
|
|
|
|
}
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 40, 'HODL', '#ffd700')
|
|
|
|
|
await k.wait(0.3)
|
|
|
|
|
await k.tween(1.5, 1, 0.1, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
|
|
|
|
|
// Slam down
|
2026-03-08 16:07:56 +00:00
|
|
|
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
sfxExplosion(); k.shake(12)
|
2026-03-08 16:16:41 +00:00
|
|
|
spawnShockwave(hx, hy, '#ffd700')
|
|
|
|
|
spawnSparks(hx, hy, 15, '#ffd700')
|
2026-03-08 16:07:56 +00:00
|
|
|
await k.wait(0.15)
|
|
|
|
|
},
|
2026-03-08 16:16:41 +00:00
|
|
|
// 11: Phase walk — flicker opacity while walking through the opponent's space
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('special'); sfxZap()
|
|
|
|
|
const far = dir > 0 ? W - 40 : 40
|
|
|
|
|
const dur = 0.5
|
|
|
|
|
await k.tween(0, 1, dur, (t) => {
|
|
|
|
|
f.pos.x = hx + (far - hx) * t
|
|
|
|
|
f.opacity = 0.2 + Math.abs(Math.sin(t * Math.PI * 8)) * 0.7
|
|
|
|
|
})
|
|
|
|
|
f.opacity = 1; sfxSpecial()
|
|
|
|
|
screenFlash('#ffd700', 0.05)
|
|
|
|
|
spawnSparks(f.pos.x, hy - 20, 6, '#ffd700')
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
2026-03-08 16:07:56 +00:00
|
|
|
},
|
2026-03-08 16:23:47 +00:00
|
|
|
// 12: Bullet time attack — slow-mo dash with afterimages + golden bullet traces
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('attack')
|
|
|
|
|
// Darken screen for bullet time
|
|
|
|
|
const overlay = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(50)])
|
|
|
|
|
await k.tween(0, 0.35, 0.08, (v) => { overlay.opacity = v })
|
|
|
|
|
// Slow-mo lean back
|
|
|
|
|
await k.tween(0, -30, 0.2, (v) => { f.angle = v }, k.easings.easeOutQuad)
|
|
|
|
|
// Golden bullet traces fly past
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
const by = hy - 15 - Math.random() * 50
|
|
|
|
|
const bullet = k.add([k.rect(15, 2), k.pos(dir > 0 ? W + 10 : -10, by), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(51), k.anchor('center')])
|
|
|
|
|
const bdir = dir > 0 ? -1 : 1
|
|
|
|
|
k.tween(bullet.pos.x, bullet.pos.x + bdir * (W + 30), 0.35, (v) => { bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() })
|
|
|
|
|
sfxZoomWhoosh()
|
|
|
|
|
await k.wait(0.04)
|
|
|
|
|
}
|
|
|
|
|
await k.wait(0.1)
|
|
|
|
|
// Snap upright and dash through
|
|
|
|
|
await k.tween(-30, 0, 0.06, (v) => { f.angle = v })
|
|
|
|
|
f.play('special'); sfxZoomWhoosh()
|
|
|
|
|
// Dash with afterimages
|
|
|
|
|
const endX = hx + dir * 160
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
const ghostX = hx + (endX - hx) * (i / 6)
|
|
|
|
|
const ghost = k.add([k.rect(16, 30), k.pos(ghostX, hy - 15), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.3 - i * 0.04), k.z(49)])
|
|
|
|
|
k.tween(0.3, 0, 0.4, (v) => { ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() })
|
|
|
|
|
}
|
|
|
|
|
await k.tween(f.pos.x, endX, 0.12, (v) => { f.pos.x = v }, k.easings.easeInQuad)
|
|
|
|
|
screenFlash('#ffd700', 0.06); sfxCritical(); k.shake(8)
|
|
|
|
|
spawnSparks(f.pos.x, hy - 20, 10, '#ffd700')
|
|
|
|
|
await k.wait(0.15)
|
|
|
|
|
// Zip back
|
|
|
|
|
await k.tween(overlay.opacity, 0, 0.1, (v) => { overlay.opacity = v })
|
|
|
|
|
overlay.destroy()
|
|
|
|
|
await k.tween(f.pos.x, hx, 0.1, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
|
|
|
|
|
},
|
|
|
|
|
// 13: ₿ throne summon — conjure golden throne, sit imperiously, dismiss
|
|
|
|
|
async (f, hx, hy) => {
|
|
|
|
|
sfxSpecial()
|
|
|
|
|
const cs = SF
|
|
|
|
|
// Throne pieces
|
|
|
|
|
const seat = k.add([k.rect(40 * cs, 8 * cs), k.pos(hx, hy - 5), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
|
|
|
|
|
const back = k.add([k.rect(35 * cs, 30 * cs), k.pos(hx, hy - 25 * cs), k.anchor('center'), k.color(safeColor(k, '#c8a000')), k.opacity(0), k.z(8)])
|
|
|
|
|
const armL = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx - 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
|
|
|
|
|
const armR = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx + 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
|
|
|
|
|
const parts = [seat, back, armL, armR]
|
|
|
|
|
// Fade in
|
|
|
|
|
for (const p of parts) k.tween(0, 0.8, 0.2, (v) => { p.opacity = v })
|
|
|
|
|
spawnSparks(hx, hy - 15, 6, '#ffd700')
|
|
|
|
|
await k.wait(0.25)
|
|
|
|
|
// Sit
|
|
|
|
|
f.play('win')
|
|
|
|
|
await k.tween(f.pos.y, hy - 12, 0.12, (v) => { f.pos.y = v }, k.easings.easeInQuad)
|
|
|
|
|
spawnEmoteText(hx, f.pos.y - 50, 'kneel', '#ffd700')
|
|
|
|
|
await k.wait(0.6)
|
|
|
|
|
// Stand and dismiss
|
|
|
|
|
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v })
|
|
|
|
|
for (const p of parts) k.tween(p.opacity, 0, 0.2, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() })
|
|
|
|
|
await k.wait(0.2)
|
|
|
|
|
},
|
|
|
|
|
// 14: Disco dance — Creator gets down with golden disco moves
|
|
|
|
|
async (f, hx, hy, _sx, _sy, dir) => {
|
|
|
|
|
f.play('win')
|
|
|
|
|
const colors = ['#ffd700', '#ff8c00', '#ffee88', '#c8a000', '#ffffff', '#ffd700']
|
|
|
|
|
spawnEmoteText(f.pos.x, f.pos.y - 55, '~♪ SATOSHI SHUFFLE ♪~', '#ffd700')
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
const dx = (i % 2 === 0 ? 25 : -25) + (Math.random() - 0.5) * 15
|
|
|
|
|
f.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'win')
|
|
|
|
|
await Promise.all([
|
|
|
|
|
k.tween(f.pos.x, hx + dx, 0.06, (v) => { f.pos.x = v }),
|
|
|
|
|
k.tween(f.pos.y, hy - 15, 0.03, (v) => { f.pos.y = v }).then(() =>
|
|
|
|
|
k.tween(f.pos.y, hy, 0.03, (v) => { f.pos.y = v })),
|
|
|
|
|
])
|
|
|
|
|
spawnSparks(f.pos.x, hy - 10, 2, colors[i % colors.length])
|
|
|
|
|
}
|
|
|
|
|
f.pos.x = hx
|
|
|
|
|
},
|
2026-03-08 16:07:56 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// Showboat loop: picks random showboats in sequence while active
|
|
|
|
|
async function _showboatLoop(side: 'a' | 'b') {
|
|
|
|
|
const tag = side === 'a' ? 'fighterA' : 'fighterB'
|
|
|
|
|
const homeX = side === 'a' ? HOME_A : HOME_B
|
|
|
|
|
const homeY = GROUND_Y - 6
|
2026-03-08 16:16:41 +00:00
|
|
|
const dir = side === 'a' ? 1 : -1
|
|
|
|
|
const arch = side === 'a' ? botA.archetype : botB.archetype
|
|
|
|
|
const isCreator = arch === 'the_creator'
|
|
|
|
|
// Creator uses magical showboats 60% of the time, normal 40%
|
|
|
|
|
const pool = isCreator ? [...showboats, ...creatorShowboats, ...creatorShowboats] : showboats
|
2026-03-08 16:07:56 +00:00
|
|
|
while (_showboatActive[side]) {
|
|
|
|
|
const f = k.get(tag)[0]
|
|
|
|
|
if (!f) break
|
|
|
|
|
const sx = f.scale.x
|
|
|
|
|
const sy = f.scale.y
|
2026-03-08 16:16:41 +00:00
|
|
|
const idx = Math.floor(Math.random() * pool.length)
|
2026-03-08 16:07:56 +00:00
|
|
|
try {
|
2026-03-08 16:16:41 +00:00
|
|
|
await pool[idx](f, homeX, homeY, sx, sy, dir)
|
2026-03-08 16:55:14 +00:00
|
|
|
// 40% chance of comedy sound punctuation after each showboat
|
|
|
|
|
if (Math.random() < 0.4) sfxRandomComedy()
|
2026-03-08 16:07:56 +00:00
|
|
|
} catch { break }
|
|
|
|
|
// Restore state after each showboat
|
|
|
|
|
const ff = k.get(tag)[0]
|
|
|
|
|
if (ff) {
|
|
|
|
|
ff.pos.x = homeX; ff.pos.y = homeY
|
|
|
|
|
ff.scale.x = sx; ff.scale.y = sy
|
2026-03-08 16:16:41 +00:00
|
|
|
ff.angle = 0; ff.opacity = 1
|
2026-03-08 16:07:56 +00:00
|
|
|
ff.play('idle')
|
|
|
|
|
}
|
|
|
|
|
// Brief pause between showboats
|
2026-03-08 16:16:41 +00:00
|
|
|
if (_showboatActive[side]) await k.wait(0.15 + Math.random() * 0.35)
|
2026-03-08 16:07:56 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 23:24:20 +00:00
|
|
|
// === EXTRACTED SYSTEMS ===
|
|
|
|
|
const entranceSystem = createEntranceSystem(choreoCtx, { HOME_A, HOME_B, botA, botB })
|
|
|
|
|
const roundSystem = createRoundSystem(choreoCtx, {
|
|
|
|
|
HOME_A, HOME_B, botA, botB,
|
|
|
|
|
choreographyMap, playBlock,
|
|
|
|
|
destroyMorphOverlays, playMorph, getMorphOrder, applyCreatorOmniMorph,
|
|
|
|
|
spawnEmoteText, spawnCrowdSigns,
|
|
|
|
|
judgeDoSomethingFunny, maybeShowHuman,
|
|
|
|
|
cameraZoom, hyperSpeedLines, schizoCut,
|
|
|
|
|
showSpeechBubble,
|
|
|
|
|
})
|
|
|
|
|
const finisherSystem = createFinisherSystem(choreoCtx, {
|
|
|
|
|
HOME_A, HOME_B, botA, botB,
|
|
|
|
|
spawnEmoteText, spawnCrowdSigns, spawnHeart,
|
|
|
|
|
spawnWeaponProp, destroyProp, spawnProp,
|
|
|
|
|
playBow, playFistBump, playHelpUp,
|
|
|
|
|
cameraZoom, hyperSpeedLines, showSpeechBubble,
|
|
|
|
|
spawnHumanCoach,
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-06 16:27:54 +00:00
|
|
|
return {
|
|
|
|
|
k,
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
async showAnnouncement(_text: string, _color: string = '#ffffff', _duration: number = 1200) {},
|
2026-03-06 16:27:54 +00:00
|
|
|
|
2026-03-07 11:42:32 +00:00
|
|
|
showSpeechBubble(side: 'a' | 'b', text: string, duration?: number) {
|
|
|
|
|
showSpeechBubble(side, text, duration)
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-08 12:08:18 +00:00
|
|
|
startTalking(side: 'a' | 'b') { startTalking(side) },
|
|
|
|
|
stopTalking(side: 'a' | 'b') { stopTalking(side) },
|
|
|
|
|
|
2026-03-08 16:07:56 +00:00
|
|
|
startShowboating(side: 'a' | 'b') {
|
|
|
|
|
if (_showboatActive[side]) return
|
|
|
|
|
_showboatActive[side] = true
|
|
|
|
|
_showboatLoop(side)
|
|
|
|
|
},
|
|
|
|
|
stopShowboating(side: 'a' | 'b') {
|
|
|
|
|
_showboatActive[side] = false
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-06 23:44:40 +00:00
|
|
|
async playEntrance() {
|
2026-03-08 23:24:20 +00:00
|
|
|
return entranceSystem.playEntrance()
|
2026-03-06 23:44:40 +00:00
|
|
|
},
|
|
|
|
|
|
2026-03-08 23:24:20 +00:00
|
|
|
// Physical contact delegates
|
|
|
|
|
_spawnDizzyStars(fighter: any, duration: number) { return roundSystem._spawnDizzyStars(fighter, duration) },
|
|
|
|
|
_resetPositions() { return roundSystem._resetPositions() },
|
|
|
|
|
_brawlRapid(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlRapid(winningSide, intensity) },
|
|
|
|
|
_brawlKnockdown(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlKnockdown(winningSide, intensity) },
|
|
|
|
|
_brawlWallBounce(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlWallBounce(winningSide, intensity) },
|
|
|
|
|
_brawlDizzyStagger(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlDizzyStagger(winningSide, intensity) },
|
|
|
|
|
_brawlSuplex(attackerSide: 'a' | 'b', intensity: number) { return roundSystem._brawlSuplex(attackerSide, intensity) },
|
|
|
|
|
_brawlPingPong(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlPingPong(winningSide, intensity) },
|
|
|
|
|
_brawlGroundPound(attackerSide: 'a' | 'b', intensity: number) { return roundSystem._brawlGroundPound(attackerSide, intensity) },
|
|
|
|
|
_brawlHaymaker(attackerSide: 'a' | 'b', _intensity: number) { return roundSystem._brawlHaymaker(attackerSide, _intensity) },
|
|
|
|
|
_brawlBodyCheck(winningSide: 'a' | 'b' | null, _intensity: number) { return roundSystem._brawlBodyCheck(winningSide, _intensity) },
|
|
|
|
|
_brawlExchange(winningSide: 'a' | 'b' | null, intensity: number) { return roundSystem._brawlExchange(winningSide, intensity) },
|
|
|
|
|
_clinchCombo(attackerSide: 'a' | 'b', intensity: number) { return roundSystem._clinchCombo(attackerSide, intensity) },
|
|
|
|
|
_counterAttack(defenderSide: 'a' | 'b') { return roundSystem._counterAttack(defenderSide) },
|
2026-03-08 17:01:06 +00:00
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
async playAttack(side: 'a' | 'b', choreographyName: string, isCritical: boolean) {
|
2026-03-08 23:24:20 +00:00
|
|
|
return roundSystem.playAttack(side, choreographyName, isCritical)
|
2026-03-06 16:27:54 +00:00
|
|
|
},
|
|
|
|
|
async playRound(event: RoundEvent) {
|
2026-03-08 23:24:20 +00:00
|
|
|
return roundSystem.playRound(event)
|
2026-03-06 16:27:54 +00:00
|
|
|
},
|
2026-03-06 22:13:19 +00:00
|
|
|
async playTaunt(side: 'a' | 'b') {
|
2026-03-08 23:24:20 +00:00
|
|
|
return roundSystem.playTaunt(side)
|
2026-03-06 22:13:19 +00:00
|
|
|
},
|
|
|
|
|
async playDodge(side: 'a' | 'b') {
|
2026-03-08 23:24:20 +00:00
|
|
|
return roundSystem.playDodge(side)
|
2026-03-06 22:13:19 +00:00
|
|
|
},
|
|
|
|
|
async playKO(winningSide: 'a' | 'b', winnerName: string) {
|
2026-03-08 23:24:20 +00:00
|
|
|
return finisherSystem.playKO(winningSide, winnerName)
|
2026-03-06 22:13:19 +00:00
|
|
|
},
|
|
|
|
|
async playPerfect(winningSide: 'a' | 'b', winnerName: string) {
|
2026-03-08 23:24:20 +00:00
|
|
|
return finisherSystem.playPerfect(winningSide, winnerName)
|
2026-03-06 16:27:54 +00:00
|
|
|
},
|
2026-03-08 21:35:04 +00:00
|
|
|
async playVictoryCelebration(winningSide: 'a' | 'b', isUpset: boolean) {
|
2026-03-08 23:24:20 +00:00
|
|
|
return finisherSystem.playVictoryCelebration(winningSide, isUpset)
|
2026-03-08 21:35:04 +00:00
|
|
|
},
|
|
|
|
|
|
2026-03-06 22:13:19 +00:00
|
|
|
startMusic() { startMusic() },
|
|
|
|
|
stopMusic() { stopMusic() },
|
2026-03-06 16:27:54 +00:00
|
|
|
|
|
|
|
|
destroy() {
|
2026-03-06 22:13:19 +00:00
|
|
|
stopMusic()
|
2026-03-08 20:39:11 +00:00
|
|
|
stopTalking('a')
|
|
|
|
|
stopTalking('b')
|
2026-03-08 23:55:58 +00:00
|
|
|
// Destroy lingering speech bubbles
|
|
|
|
|
activeBubbleA.forEach(o => { if (o.exists()) o.destroy() })
|
|
|
|
|
activeBubbleB.forEach(o => { if (o.exists()) o.destroy() })
|
|
|
|
|
activeBubbleA.length = 0
|
|
|
|
|
activeBubbleB.length = 0
|
|
|
|
|
// Clear all tracked timers
|
2026-03-08 20:39:11 +00:00
|
|
|
for (const id of cleanupTimers) {
|
|
|
|
|
clearInterval(id)
|
|
|
|
|
clearTimeout(id)
|
|
|
|
|
}
|
|
|
|
|
cleanupTimers.clear()
|
2026-03-08 23:55:58 +00:00
|
|
|
// Destroy all remaining game objects before quitting
|
|
|
|
|
try { k.get('*').forEach(o => { if (o.exists()) o.destroy() }) } catch {}
|
2026-03-06 16:27:54 +00:00
|
|
|
k.quit()
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 23:44:40 +00:00
|
|
|
export type FightSceneController = Awaited<ReturnType<typeof createFightScene>>
|