feat: 30 comedy sound effects, replace 75% round-end TTS with SFX

Comedy sounds (all procedural Web Audio, no samples):
- Vine Boom, Air Horn (MLG), Bruh, Fart, Record Scratch
- Rubber Chicken, Squeaky Toy, Wet Slap, Bone Crack, Cartoon Run
- Rim Shot (ba dum tss), Slide Whistle Up/Down, Yippee, Ding
- Taco Bell Bong, Windows Error, Meme Thud
- Sad Trombone, Emotional Damage, Dun Dun Dunnnn
- Fail Horn, Price Is Right Fail, Buzzer, Sad Violin
- Mission Failed, Crickets

Wiring:
- Round-end narration: 75% comedy SFX, 25% TTS (was 100% TTS)
- Critical rounds get Emotional Damage, draws get random fail
- Showboat loop: 40% chance of comedy sound punctuation after each
- KO finish: 50% chance of comedy fail sound on loser
- sfxRandomSilly expanded with comedy sounds
- New exports: sfxRandomComedy, sfxRandomFail

Also adds bulletTimeAttack combat choreography + ultimateBulletTimeBarrage
(tier 3+ ultimate) to fight system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 16:55:14 +00:00
co-authored by Claude Opus 4.6
parent d3f026d356
commit a59b0748a7
3 changed files with 618 additions and 57 deletions
+14 -1
View File
@@ -9,6 +9,7 @@ import {
setMusicIntensity, stopAllAudio,
setMasterMute, isMasterMuted, ensureAudioContext,
speakQuestion, speakAnswer, speakNarration,
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage,
} from '../game/sounds'
interface Round {
@@ -411,7 +412,19 @@ async function _doReplay() {
if (round.narration) {
logItems.value.push({ type: 'narration', round: round.roundNumber, text: `>> ${round.narration}`, color: 'neon-yellow' })
scrollLog()
if (doTTS) await speakNarration(round.narration)
// 75% comedy SFX instead of TTS narration — much funnier and faster
if (doTTS && Math.random() < 0.25) {
await speakNarration(round.narration)
} else {
// Play a comedy sound that matches the round result
if (isCritical) {
sfxEmotionalDamage()
} else if (!aWon && !bWon) {
sfxRandomFail()
} else {
sfxRandomComedy()
}
}
await sleep(insanityMode.value ? 30 : 200)
}
+134 -19
View File
@@ -15,6 +15,13 @@ import {
announceCreatorEntrance, announceCreatorRound, announceCreatorKO,
announceCreatorWin, announceCreatorLose, announceCreatorMorph,
announceCreatorCameo, announceCreatorTaunt, announceCreatorDevastating,
sfxRandomComedy, sfxRandomFail,
sfxVineBoom, sfxAirHorn, sfxBruh, sfxFart, sfxRecordScratch,
sfxRubberChicken, sfxSqueakyToy, sfxWetSlap, sfxBoneCrack, sfxCartoonRun,
sfxRimShot, sfxSlideWhistleUp, sfxSlideWhistleDown, sfxYippee, sfxDing,
sfxTacoBellBong, sfxWindowsError, sfxMemeThud,
sfxSadTrombone, sfxFailHorn, sfxPriceIsRightFail, sfxBuzzer,
sfxMissionFailed, sfxCrickets, sfxSadViolin, sfxEmotionalDamage, sfxDunDunDun,
} from './sounds'
export interface FightSceneConfig {
@@ -80,7 +87,7 @@ const spriteAnims = {
// Challenge type -> themed choreography (shown ~60% of the time) + generic fallbacks
const CHALLENGE_THEMED: Record<string, { themed: string[]; generic: string[] }> = {
speed_blitz: { themed: ['afterimageDash', 'lightningRush', 'rapidFlurry', 'tornadoSpin', 'corkscrewDive', 'warpDrive', 'backflipKick'], generic: ['dashPunch', 'multiHit', 'fullScreenDash', 'dashThrough', 'zoomRush', 'katanaCombo', 'cycloneKick', 'helicopterArms', 'breakdanceSweep'] },
speed_blitz: { themed: ['afterimageDash', 'lightningRush', 'rapidFlurry', 'tornadoSpin', 'corkscrewDive', 'warpDrive', 'backflipKick', 'bulletTimeAttack'], generic: ['dashPunch', 'multiHit', 'fullScreenDash', 'dashThrough', 'zoomRush', 'katanaCombo', 'cycloneKick', 'helicopterArms', 'breakdanceSweep'] },
riddle: { themed: ['riddleBarrage', 'portalPunch', 'cloneStrike', 'mindBlast', 'hexCurse'], generic: ['teleportStrike', 'projectile', 'zoomRush', 'whipCrack', 'bombThrow', 'captchaTrap', 'glitchBeam'] },
code_golf: { themed: ['golfClubSmash', 'golfCartDrive', 'stackOverflow', 'segfault', 'blueScreen'], generic: ['projectile', 'flyingKick', 'dashPunch', 'rapidFlurry', 'hammerSmash', 'bodySlam', 'homeRunSwing'] },
roast_battle: { themed: ['fireBreath', 'dragonBreath', 'fireball', 'lavaSplash', 'heatVision', 'volcanoErupt'], generic: ['gunBurst', 'jetpackDive', 'multiHit', 'zoomRush', 'laserBeam', 'chainsawRev', 'rocketLauncher', 'dynamiteBlast'] },
@@ -95,8 +102,8 @@ const CHALLENGE_THEMED: Record<string, { themed: string[]; generic: string[] }>
magic_duel: { themed: ['kamehameha', 'spiritBomb', 'arcaneBarrage', 'darkVoid', 'holySmite', 'shadowClone', 'hexCurse'], generic: ['fireball', 'iceLance', 'thunderStrike', 'portalPunch', 'crystalShards', 'scrollBlast', 'potionThrow'] },
sports_showdown: { themed: ['homeRunSwing', 'slapShot', 'servingAce', 'fieldGoalKick', 'bodyCheck', 'soccerKick', 'basketballDunk'], generic: ['baseballBat', 'tennisRacket', 'hockeyStick', 'bowlingBallRoll', 'tennisBallVolley', 'dropKick'] },
nature_clash: { themed: ['treeSmash', 'icebergDrop', 'lavaSplash', 'tornadoFling', 'tsunamiWave', 'avalanche', 'earthquakeStrike'], generic: ['vineWhip', 'sandstorm', 'thornBarrage', 'pollenCloud', 'windSlash', 'meteorStrike'] },
space_war: { themed: ['blackHole', 'photonTorpedo', 'tractor_beam', 'warpDrive', 'alienAbduction', 'ionCannon', 'cosmicRay'], generic: ['laserSword', 'plasmaSword', 'asteroidBelt', 'satelliteLaser', 'plasmaBeam', 'solarFlare'] },
hack_battle: { themed: ['hackerAttack', 'bugSwarm', 'stackOverflow', 'segfault', 'blueScreen', 'malwareInject'], generic: ['fourOhFour', 'ctrlAltDelete', 'aiUprising', 'captchaTrap', 'popupSpam', 'dataStream', 'glitchBeam'] },
space_war: { themed: ['blackHole', 'photonTorpedo', 'tractor_beam', 'warpDrive', 'alienAbduction', 'ionCannon', 'cosmicRay', 'bulletTimeAttack'], generic: ['laserSword', 'plasmaSword', 'asteroidBelt', 'satelliteLaser', 'plasmaBeam', 'solarFlare'] },
hack_battle: { themed: ['hackerAttack', 'bugSwarm', 'stackOverflow', 'segfault', 'blueScreen', 'malwareInject', 'bulletTimeAttack'], generic: ['fourOhFour', 'ctrlAltDelete', 'aiUprising', 'captchaTrap', 'popupSpam', 'dataStream', 'glitchBeam'] },
meme_war: { themed: ['selfieStrike', 'dabAttack', 'flossAttack', 'yeetThrow', 'tPoseAssert', 'emojiBarrage', 'memeBeam'], generic: ['rubberChicken', 'fingerGuns', 'micDrop', 'ratioAttack', 'capThrow', 'touchGrass', 'noScope'] },
animal_kingdom: { themed: ['sharkBite', 'bearSwipe', 'eagleDive', 'bullCharge', 'gorillaSlam', 'wolfPack', 'batSwarm'], generic: ['snakeLunge', 'scorpionSting', 'crabPinch', 'spiderWeb', 'beeSwarm', 'catScratch', 'dogPile', 'dolphinFlip'] },
demolition: { themed: ['dynamiteBlast', 'c4Detonation', 'nukeStrike', 'grenadeBlast', 'rocketLauncher', 'volcanoErupt'], generic: ['fireworksBurst', 'partyPopper', 'pinataSmash', 'cherryBomb', 'confettiCannon', 'anvilDrop', 'pianoDrop'] },
@@ -108,13 +115,13 @@ const CHALLENGE_THEMED: Record<string, { themed: string[]; generic: string[] }>
// Tier-gated ultimate move pools — cumulative (tier 3 bot can use tier 2+3 moves)
const TIER_ULTIMATES: Record<number, string[]> = {
2: ['ultimateRapidBarrage', 'ultimateOrbitalStrike', 'ultimateCloneSurround', 'ultimateGatlingFury', 'ultimateEarthquake', 'ultimateBouncingFury', 'ultimateTripleBeam', 'ultimateMegaThrow', 'ultimateTornadoGrab', 'ultimateMeteorShower'],
3: ['ultimateScreenNuke', 'ultimateBlackHoleVortex', 'ultimateDimensionalRift', 'ultimateTimeSlow', 'ultimateSummonArmy', 'ultimateHellfireRain', 'ultimateCrystalCage', 'ultimateGigaDrill', 'ultimateSuperSlam', 'ultimateAvalancheDrop'],
3: ['ultimateScreenNuke', 'ultimateBlackHoleVortex', 'ultimateDimensionalRift', 'ultimateTimeSlow', 'ultimateSummonArmy', 'ultimateHellfireRain', 'ultimateCrystalCage', 'ultimateGigaDrill', 'ultimateSuperSlam', 'ultimateAvalancheDrop', 'ultimateBulletTimeBarrage'],
4: ['ultimateFinaleRush', 'ultimateMeteorCrash', 'ultimateScreenShatter', 'ultimateGravityReverse', 'ultimateInfiniteCombo', 'ultimatePlasmaStorm', 'ultimateFrozenCoffin', 'ultimateLavaGeyser', 'ultimateChainGrab', 'ultimateRocketBarrage'],
5: ['ultimateArmageddon', 'ultimateOmegaBeam', 'ultimateBeyondTheScreen', 'ultimateMatrixDodge', 'ultimateWorldEnder', 'ultimateAbsoluteZero', 'ultimateSolarCannon', 'ultimatePhantomStrike', 'ultimateGodPunch', 'ultimateVoidCollapse'],
}
// The Creator — exclusive moves (50% ultimate chance, 100% on crits, custom pool)
const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention', 'bitcoinSwarm', 'satoshiTornado', 'hodlWave', 'bitcoinBarrage']
const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention', 'bitcoinSwarm', 'satoshiTornado', 'hodlWave', 'bitcoinBarrage', 'bulletTimeAttack']
const CREATOR_ULTIMATES = ['ultimateGenesisBlock', 'ultimateTheHalving', 'ultimateFullNodeCrush', 'ultimateSatoshiVision']
function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
@@ -172,6 +179,7 @@ function pickChoreography(challengeType: string, isCritical: boolean, _round: nu
'alienAbduction', 'tsunamiWave', 'earthquakeStrike', 'dragonBreath', 'pianoDrop',
'fridgeDrop', 'volcanoErupt', 'warpDrive', 'dubstepCannon', 'tornadoSpin',
'trampolineBounce', 'watermelonBomb', 'nftRugPull', 'blueScreen', 'yeetThrow',
'bulletTimeAttack',
]
return critMoves[Math.floor(Math.random() * critMoves.length)]
}
@@ -5507,6 +5515,110 @@ export async function createFightScene(config: FightSceneConfig) {
const ultimateSuperSlam = makeGrab('up', 250)
const ultimateAvalancheDrop = makeExplosion(30, 30, '#aaeeff', 0.2, '#ffffff', true)
// ULTIMATE BULLET TIME BARRAGE — full slow-mo cinematic: dodge rain of projectiles, afterimage rush, multi-hit counter
async function ultimateBulletTimeBarrage(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Phase 1: World goes dark, heavy slow-mo overlay
const dim = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000022')), k.opacity(0), k.z(25)])
await k.tween(0, 0.55, 0.2, (v) => { dim.opacity = v })
scanlineGlitch(0.15)
glitchRGB(0.1)
// Phase 2: Defender fires a barrage of slow-mo bullets
def.play('special')
for (let i = 0; i < 8; i++) {
sfxGunshot()
const bx = def.pos.x - dir * 20
const by = def.pos.y - 35 + (Math.random() - 0.5) * 30
const bullet = k.add([
k.rect(12, 3),
k.pos(bx, by),
k.color(safeColor(k, '#ff4444')),
k.opacity(0.85),
k.z(26),
k.anchor('center'),
])
// Bullet trace
const trace = k.add([
k.rect(60, 1),
k.pos(bx + dir * 30, by),
k.color(safeColor(k, '#ff6644')),
k.opacity(0.4),
k.z(25),
])
setTimeout(() => { if (trace.exists()) trace.destroy() }, 300)
// Bullet travels slowly
k.tween(bullet.pos.x, atk.pos.x + (Math.random() - 0.5) * 30, 0.3, (v) => { if (bullet.exists()) bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() })
await k.wait(0.05)
}
// Phase 3: Attacker dodges each bullet with slow-mo lean (afterimages cascade)
atk.play('idle')
for (let lean = 0; lean < 3; lean++) {
const leanDir = lean % 2 === 0 ? -1 : 1
const ghost = k.add([
k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.7, FRAME_SIZE * Math.abs(atk.scale.y)),
k.pos(atk.pos.x, atk.pos.y),
k.color(safeColor(k, '#ffd700')),
k.opacity(0.3),
k.z(24),
k.anchor('bot'),
])
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 350)
await k.tween(atk.pos.y, GROUND_Y + leanDir * 15, 0.08, (v) => { atk.pos.y = v }, k.easings.easeOutQuad)
await k.tween(atk.pos.y, GROUND_Y, 0.08, (v) => { atk.pos.y = v }, k.easings.easeInQuad)
}
await k.wait(0.1)
// Phase 4: Time RESUMES — golden flash, attacker blazes forward
dim.destroy()
screenFlash('#ffd700', 0.15)
sfxZoomWhoosh()
sfxCritical()
// Afterimage rush trail (8 ghosts across the screen)
const contactX = origDX - dir * 40
for (let i = 0; i < 8; i++) {
const gx = origAX + ((contactX - origAX) * i / 8)
const ghost = k.add([
k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.6, FRAME_SIZE * Math.abs(atk.scale.y) * 0.9),
k.pos(gx, GROUND_Y),
k.color(safeColor(k, '#ffd700')),
k.opacity(0.35 - i * 0.04),
k.z(9),
k.anchor('bot'),
])
setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 300)
}
await k.tween(atk.pos.x, contactX, 0.06, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
// Phase 5: 8-hit slow-mo combo — each hit with sparks and screen shake
for (let i = 0; i < 8; i++) {
atk.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxPunch()
def.play('hit')
k.shake(5 + i * 2)
spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 15 - Math.random() * 25, 5, i % 2 === 0 ? '#ffd700' : '#ffffff')
await k.wait(0.04)
}
// Phase 6: Final devastating blow — full screen effects
sfxExplosion()
k.shake(30)
screenFlash('#ffd700', 0.25)
def.play('knockback')
spawnShockwave(def.pos.x, GROUND_Y, '#ffd700')
spawnShockwave(def.pos.x + dir * 30, GROUND_Y, '#ffaa00')
spawnSparks(def.pos.x, def.pos.y - 25, 25, '#ffd700')
glitchRGB(0.3)
await k.wait(0.4)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.25, (v) => { atk.pos.x = v }, k.easings.easeOutQuad),
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
])
atk.play('idle')
}
// --- TIER 4+ ULTIMATES (Platinum) — full screen, multi-phase spectacles ---
async function ultimateFinaleRush(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Full-screen dash through, reverse, dash again, then combo finish
@@ -6506,7 +6618,7 @@ export async function createFightScene(config: FightSceneConfig) {
pocketCannon, grappleFlurry, bodySlam, pinballCombo, suplex,
// Themed
golfClubSmash, fireBreath, riddleBarrage, cloneStrike,
coinShower, penStab, mathAttack, trapCardAttack, afterimageDash,
coinShower, penStab, mathAttack, trapCardAttack, afterimageDash, bulletTimeAttack,
// --- NEW: Food throws ---
pizzaSlam, bananaFling, pieSmash, hotdogWhip, watermelonBomb,
sushiBarrage, burgerToss, iceCreamFling, tacoStorm, donutBarrage,
@@ -6589,7 +6701,7 @@ export async function createFightScene(config: FightSceneConfig) {
ultimateBouncingFury, ultimateTripleBeam, ultimateMegaThrow, ultimateTornadoGrab, ultimateMeteorShower,
// Tier 3+
ultimateScreenNuke, ultimateBlackHoleVortex, ultimateDimensionalRift, ultimateTimeSlow, ultimateSummonArmy,
ultimateHellfireRain, ultimateCrystalCage, ultimateGigaDrill, ultimateSuperSlam, ultimateAvalancheDrop,
ultimateHellfireRain, ultimateCrystalCage, ultimateGigaDrill, ultimateSuperSlam, ultimateAvalancheDrop, ultimateBulletTimeBarrage,
// Tier 4+
ultimateFinaleRush, ultimateMeteorCrash, ultimateScreenShatter, ultimateGravityReverse, ultimateInfiniteCombo,
ultimatePlasmaStorm, ultimateFrozenCoffin, ultimateLavaGeyser, ultimateChainGrab, ultimateRocketBarrage,
@@ -7531,7 +7643,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(judge.pos.y, judgeOrigY, 0.08, (v) => { judge.pos.y = v }, k.easings.easeInQuad)
judge.play(d % 2 === 0 ? 'call_left' : 'call_right')
}
sfxRandomSilly()
sfxRandomComedy()
judge.play('idle')
} else if (funnyAction === 2) {
// Holds up a 10 score card
@@ -7604,7 +7716,7 @@ export async function createFightScene(config: FightSceneConfig) {
flag.onUpdate(() => {
flag.pos.y = judge.pos.y - 30 + Math.sin(k.time() * 10) * 3
})
sfxRandomSilly()
sfxRandomComedy()
await k.wait(1.5)
flag.destroy(); pole.destroy()
} else {
@@ -8478,6 +8590,8 @@ export async function createFightScene(config: FightSceneConfig) {
const idx = Math.floor(Math.random() * pool.length)
try {
await pool[idx](f, homeX, homeY, sx, sy, dir)
// 40% chance of comedy sound punctuation after each showboat
if (Math.random() < 0.4) sfxRandomComedy()
} catch { break }
// Restore state after each showboat
const ff = k.get(tag)[0]
@@ -9386,7 +9500,7 @@ export async function createFightScene(config: FightSceneConfig) {
if (!isLastExchange && Math.random() < 0.15) {
await this.playDodge(attackerSide === 'a' ? 'b' : 'a')
sfxDodge()
sfxBoing()
if (Math.random() < 0.4) sfxRandomFail(); else sfxBoing()
} else {
await this.playAttack(attackerSide, choreo, exchangeCritical)
}
@@ -9792,7 +9906,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(loser.pos.y, loserOrigY, 0.3, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
sfxSplat(); k.shake(15); screenFlash('#ffe135', 0.15)
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe135')
winner.play('win'); sfxRandomSilly()
winner.play('win'); sfxRandomComedy()
} else if (finishStyle === 5) {
// ANVIL DROP — cartoon anvil from the sky
fatalityTagline = 'HEAVY METAL!'
@@ -9821,7 +9935,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
for (let i = 0; i < 8; i++) {
winner.play(i % 2 === 0 ? 'attack' : 'kick')
sfxBoing(); sfxRandomSilly()
sfxBoing(); sfxRandomComedy()
loser.play('hit'); k.shake(3 + i)
for (const p of chicken) { p.pos.x = loser.pos.x + (Math.random() - 0.5) * 10; p.pos.y = loser.pos.y - 30 + (Math.random() - 0.5) * 10 }
spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 40 - Math.random() * 20, ['SQUEAK!', 'BAWK!', 'HONK!', 'SQUAWK!'][i % 4], '#ffdd44')
@@ -9915,7 +10029,7 @@ export async function createFightScene(config: FightSceneConfig) {
await k.wait(0.3)
for (let i = 0; i < 6; i++) {
loser.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
sfxRandomSilly(); screenFlash(discoColors[i], 0.04)
sfxRandomComedy(); screenFlash(discoColors[i], 0.04)
await k.wait(0.1)
}
sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.2)
@@ -10060,7 +10174,7 @@ export async function createFightScene(config: FightSceneConfig) {
announceFast('Rev it up!')
const mower = spawnWeaponProp('lawn_mower', winner.pos.x + dir * 10, GROUND_Y - 8, dir < 0, 16)
winner.play('special'); sfxPowerUp()
for (let i = 0; i < 3; i++) { sfxRandomSilly(); await k.wait(0.1) }
for (let i = 0; i < 3; i++) { sfxRandomComedy(); await k.wait(0.1) }
sfxZoomWhoosh()
for (const p of mower) {
k.tween(p.pos.x, loser.pos.x + dir * 40, 0.2, (v) => { p.pos.x = v }, k.easings.easeInQuad)
@@ -10109,7 +10223,7 @@ export async function createFightScene(config: FightSceneConfig) {
loser.opacity = Math.random() > 0.5 ? 1 : 0.2
loser.pos.x += (Math.random() - 0.5) * 15
loser.play(['idle', 'attack', 'kick', 'hit', 'special'][Math.floor(Math.random() * 5)])
screenFlash('#00ff00', 0.02); sfxRandomSilly()
screenFlash('#00ff00', 0.02); sfxRandomFail()
await k.wait(0.06)
}
screenFlash('#0000ff', 0.3); sfxFail(); k.shake(20)
@@ -10175,7 +10289,7 @@ export async function createFightScene(config: FightSceneConfig) {
for (let i = 0; i < 30; i++) {
spawnProp(winner.pos.x + dir * 30, winner.pos.y - 25, loser.pos.x + (Math.random() - 0.5) * 80, loserOrigY - 80 - Math.random() * 60, 4, 3, confettiColors[i % 8], 400 + Math.random() * 200)
}
sfxBoing(); sfxRandomSilly()
sfxBoing(); sfxRandomComedy()
loser.play('hit'); k.shake(10); await k.wait(0.15)
loser.play('knockback'); sfxCritical(); k.shake(15)
await k.tween(loser.pos.x, loser.pos.x + dir * 50, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad)
@@ -10343,7 +10457,7 @@ export async function createFightScene(config: FightSceneConfig) {
sfxPowerUp()
const moves = ['attack', 'kick', 'special', 'kick', 'attack', 'special'] as const
for (let i = 0; i < moves.length; i++) {
winner.play(moves[i]); sfxRandomSilly()
winner.play(moves[i]); sfxRandomComedy()
spawnSparks(winner.pos.x, winner.pos.y - 30, 3, ['#ff00ff', '#00ffff', '#ffff00'][i % 3])
await k.wait(0.08)
}
@@ -10366,7 +10480,7 @@ export async function createFightScene(config: FightSceneConfig) {
announceSilly('What is that SMELL!?')
const cushion = k.add([k.circle(20), k.pos(loser.pos.x, GROUND_Y - 5), k.color(safeColor(k, '#ff6688')), k.z(10), k.opacity(0.8)])
sfxBoing(); await k.wait(0.2)
sfxWomp(); sfxRandomSilly(); k.shake(8)
sfxWomp(); sfxRandomComedy(); k.shake(8)
await k.tween(20, 40, 0.15, (v) => { cushion.radius = v }, k.easings.easeOutQuad)
sfxExplosion()
for (let i = 0; i < 12; i++) {
@@ -10466,6 +10580,7 @@ export async function createFightScene(config: FightSceneConfig) {
k.shake(18); sfxExplosion()
await k.wait(0.3)
loser.play('ko')
if (Math.random() < 0.5) setTimeout(() => sfxRandomFail(), 200)
await k.wait(0.4) // pause for visual impact before fatality voice
const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype
const loserArch = winningSide === 'a' ? botB.archetype : botA.archetype
@@ -10635,7 +10750,7 @@ export async function createFightScene(config: FightSceneConfig) {
18,
['#ff2d7b', '#00f0ff', '#ffe14d', '#b83dff', '#39ff14', '#ff6600', '#ffffff', '#ff2d2d'][i]
)
if (i % 2 === 0) sfxRandomSilly()
if (i % 2 === 0) sfxRandomComedy()
}, i * 200)
}
await k.wait(0.7)
+470 -37
View File
@@ -1384,10 +1384,411 @@ export function sfxFail() {
// Pick a random silly sound
export function sfxRandomSilly() {
const fns = [sfxBoing, sfxBonk, sfxSplat, sfxZap, sfxCoin, sfxSlideUp]
const fns = [
sfxBoing, sfxBonk, sfxSplat, sfxZap, sfxCoin, sfxSlideUp,
sfxVineBoom, sfxAirHorn, sfxFart, sfxRubberChicken, sfxSqueakyToy,
sfxCartoonRun, sfxWetSlap, sfxBoneCrack, sfxRimShot, sfxDing,
]
fns[Math.floor(Math.random() * fns.length)]()
}
// Random comedy sound — heavier on meme sounds, for taunts/showboats/round ends
export function sfxRandomComedy() {
const fns = [
sfxVineBoom, sfxAirHorn, sfxBruh, sfxFart, sfxRecordScratch,
sfxRubberChicken, sfxSqueakyToy, sfxWetSlap, sfxBoneCrack,
sfxCartoonRun, sfxRimShot, sfxSlideWhistleUp, sfxSlideWhistleDown,
sfxYippee, sfxDing, sfxTacoBellBong, sfxWindowsError, sfxMemeThud,
sfxBoing, sfxBonk, sfxSplat,
]
fns[Math.floor(Math.random() * fns.length)]()
}
// Random fail sound — for missed attacks, dodges, KOs on the loser
export function sfxRandomFail() {
const fns = [
sfxSadTrombone, sfxFailHorn, sfxPriceIsRightFail, sfxBuzzer,
sfxMissionFailed, sfxCrickets, sfxWindowsError, sfxSadViolin,
sfxWomp, sfxEmotionalDamage, sfxDunDunDun,
]
fns[Math.floor(Math.random() * fns.length)]()
}
// === COMEDY SOUND EFFECTS ===
// Procedural comedy/meme sounds inspired by classic internet soundboard culture
// VINE BOOM — iconic deep bass boom with heavy sub + distortion
export function sfxVineBoom() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
bodyThump(45, 0.4, d, t, 0.5)
bodyThump(90, 0.3, d, t, 0.35)
fmImpact(150, 5, 0.25, d, t)
noise(0.06, d, t)
reverbTail(0.5, d, t + 0.1)
}
// AIR HORN (MLG) — obnoxious ascending horn blasts
export function sfxAirHorn() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
for (let i = 0; i < 3; i++) {
const bt = t + i * 0.15
tone(520 + i * 30, 'sawtooth', 0.12, d, bt)
tone(523 + i * 30, 'square', 0.12, d, bt)
tone(1046 + i * 60, 'sawtooth', 0.08, d, bt)
}
reverbTail(0.3, d, t + 0.4)
}
// BRUH — deep bass impact with vowel-like formant resonance
export function sfxBruh() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
bodyThump(80, 0.3, d, t, 0.4)
const bufSize = Math.floor(c.sampleRate * 0.25)
const buf = c.createBuffer(1, bufSize, c.sampleRate)
const data = buf.getChannelData(0)
for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1
const src = c.createBufferSource(); src.buffer = buf
const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 300; bp.Q.value = 5
const g = c.createGain()
g.gain.setValueAtTime(0.25, t)
g.gain.exponentialRampToValueAtTime(0.001, t + 0.25)
src.connect(bp); bp.connect(g); g.connect(d)
src.start(t); src.stop(t + 0.25)
bodyThump(55, 0.15, d, t + 0.05, 0.3)
reverbTail(0.3, d, t + 0.1)
}
// FART — rumbling low-frequency modulated noise
export function sfxFart() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
const duration = 0.3 + Math.random() * 0.3
const osc = c.createOscillator(); osc.type = 'sawtooth'
osc.frequency.setValueAtTime(80 + Math.random() * 40, t)
osc.frequency.exponentialRampToValueAtTime(40 + Math.random() * 30, t + duration)
const lfo = c.createOscillator(); lfo.type = 'square'
lfo.frequency.setValueAtTime(20 + Math.random() * 30, t)
const lfoG = c.createGain(); lfoG.gain.value = 40
lfo.connect(lfoG); lfoG.connect(osc.frequency)
const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 150 + Math.random() * 100; bp.Q.value = 2
const g = c.createGain()
g.gain.setValueAtTime(0.3, t)
g.gain.setValueAtTime(0.25, t + duration * 0.3)
g.gain.exponentialRampToValueAtTime(0.001, t + duration)
osc.connect(bp); bp.connect(g); g.connect(d)
osc.start(t); osc.stop(t + duration)
lfo.start(t); lfo.stop(t + duration)
noise(duration * 0.6, d, t)
}
// RECORD SCRATCH — vinyl stop effect
export function sfxRecordScratch() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
sweep(3000, 100, 'sawtooth', 0.15, d)
sweep(2500, 80, 'square', 0.12, d)
noise(0.1, d, t)
highSnap(4000, 0.03, d, t)
setTimeout(() => sweep(200, 400, 'sine', 0.08, d), 100)
}
// RUBBER CHICKEN — squeaky honk
export function sfxRubberChicken() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
sweep(300, 1200, 'sine', 0.1, d)
const osc = c.createOscillator(); osc.type = 'sine'
osc.frequency.setValueAtTime(1100, t + 0.1)
osc.frequency.setValueAtTime(1200, t + 0.15)
osc.frequency.exponentialRampToValueAtTime(600, t + 0.35)
const lfo = c.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 20
const lfoG = c.createGain(); lfoG.gain.value = 80
lfo.connect(lfoG); lfoG.connect(osc.frequency)
const g = c.createGain()
g.gain.setValueAtTime(0.25, t + 0.1)
g.gain.exponentialRampToValueAtTime(0.001, t + 0.35)
osc.connect(g); g.connect(d)
osc.start(t + 0.1); osc.stop(t + 0.35)
lfo.start(t + 0.1); lfo.stop(t + 0.35)
}
// SQUEAKY TOY — quick toy squeeze
export function sfxSqueakyToy() {
const d = getSfxDest()
sweep(400, 1800, 'sine', 0.06, d)
sweep(1800, 600, 'sine', 0.1, d)
setTimeout(() => sweep(500, 1500, 'sine', 0.05, d), 120)
}
// WET SLAP — meaty slap with low-end body
export function sfxWetSlap() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
highSnap(3000, 0.02, d, t)
highSnap(1200, 0.03, d, t)
bodyThump(150, 0.08, d, t, 0.3)
noise(0.06, d, t)
fmImpact(300, 2, 0.08, d, t)
reverbTail(0.15, d, t + 0.03)
}
// BONE CRACK — sharp multi-frequency crack sequence
export function sfxBoneCrack() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
for (let i = 0; i < 3; i++) {
const ct = t + i * 0.04
highSnap(3000 + i * 1500, 0.015, d, ct)
fmImpact(800 + i * 400, 4, 0.03, d, ct)
noise(0.02, d, ct)
}
bodyThump(100, 0.04, d, t + 0.08, 0.15)
}
// CARTOON RUN — rapid bongo/pitter-patter footsteps
export function sfxCartoonRun() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
for (let i = 0; i < 8; i++) {
const st = t + i * 0.05
bodyThump(200 + (i % 2) * 80, 0.03, d, st, 0.15)
highSnap(2000 + Math.random() * 1000, 0.01, d, st)
}
}
// RIM SHOT — ba dum tss
export function sfxRimShot() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
bodyThump(180, 0.08, d, t, 0.2)
bodyThump(140, 0.08, d, t + 0.15, 0.2)
highSnap(4000, 0.04, d, t + 0.3)
noise(0.15, d, t + 0.3)
fmImpact(2000, 3, 0.12, d, t + 0.3)
bodyThump(200, 0.04, d, t + 0.3, 0.15)
}
// SLIDE WHISTLE UP — cartoon ascend
export function sfxSlideWhistleUp() {
const d = getSfxDest()
sweep(300, 2500, 'sine', 0.4, d)
sweep(305, 2510, 'sine', 0.4, d)
}
// SLIDE WHISTLE DOWN — cartoon descend
export function sfxSlideWhistleDown() {
const d = getSfxDest()
sweep(2500, 200, 'sine', 0.5, d)
sweep(2510, 205, 'sine', 0.5, d)
}
// YIPPEE — ascending cheerful chirps
export function sfxYippee() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
const notes = [523, 659, 784, 1047]
for (let i = 0; i < notes.length; i++) {
tone(notes[i], 'triangle', 0.08, d, t + i * 0.07)
tone(notes[i] * 2, 'sine', 0.06, d, t + i * 0.07)
}
}
// DING — bright bell
export function sfxDing() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
fmImpact(1319, 1.5, 0.3, d, t)
tone(1319, 'triangle', 0.25, d, t)
tone(2638, 'sine', 0.15, d, t)
}
// TACO BELL BONG — deep resonant bell
export function sfxTacoBellBong() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
fmImpact(130, 2.5, 0.8, d, t)
fmImpact(261, 3, 0.6, d, t)
tone(130, 'sine', 0.6, d, t)
bodyThump(65, 0.3, d, t, 0.2)
reverbTail(0.7, d, t + 0.1)
}
// WINDOWS ERROR — two-tone error chord
export function sfxWindowsError() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
tone(440, 'square', 0.15, d, t)
tone(466, 'square', 0.15, d, t)
tone(220, 'triangle', 0.2, d, t)
setTimeout(() => {
tone(349, 'square', 0.2, d)
tone(175, 'triangle', 0.25, d)
}, 180)
}
// MEME THUD — super heavy bass drop
export function sfxMemeThud() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
bodyThump(35, 0.5, d, t, 0.5)
bodyThump(70, 0.4, d, t, 0.4)
noise(0.08, d, t)
fmImpact(100, 6, 0.3, d, t)
reverbTail(0.6, d, t + 0.1)
}
// SAD TROMBONE (Wa Wa Wa Waaaa) — classic comedy fail
export function sfxSadTrombone() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
const notes: [number, number][] = [[466, 0.25], [440, 0.25], [415, 0.25], [392, 0.7]]
let off = 0
for (let i = 0; i < notes.length; i++) {
const [freq, dur] = notes[i]
tone(freq, 'sawtooth', dur, d, t + off)
tone(freq * 0.5, 'triangle', dur, d, t + off)
if (i === 3) {
const osc = c.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = freq
const lfo = c.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 5
const lfoG = c.createGain(); lfoG.gain.value = 8
lfo.connect(lfoG); lfoG.connect(osc.frequency)
const g = c.createGain()
g.gain.setValueAtTime(0.2, t + off)
g.gain.exponentialRampToValueAtTime(0.001, t + off + dur)
osc.connect(g); g.connect(d)
osc.start(t + off); osc.stop(t + off + dur)
lfo.start(t + off); lfo.stop(t + off + dur)
}
off += dur
}
reverbTail(0.5, d, t + off)
}
// EMOTIONAL DAMAGE — dramatic descending brass stab
export function sfxEmotionalDamage() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
tone(220, 'sawtooth', 0.3, d, t)
tone(277, 'sawtooth', 0.3, d, t)
tone(330, 'sawtooth', 0.3, d, t)
bodyThump(110, 0.2, d, t, 0.3)
noise(0.08, d, t)
fmImpact(600, 4, 0.2, d, t)
reverbTail(0.5, d, t + 0.1)
}
// DUN DUN DUNNNNN — dramatic reveal stinger
export function sfxDunDunDun() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
tone(147, 'sawtooth', 0.2, d, t)
tone(147, 'triangle', 0.2, d, t)
bodyThump(73, 0.15, d, t, 0.25)
tone(147, 'sawtooth', 0.2, d, t + 0.25)
bodyThump(73, 0.15, d, t + 0.25, 0.25)
tone(110, 'sawtooth', 0.8, d, t + 0.5)
tone(110, 'triangle', 0.8, d, t + 0.5)
tone(55, 'sine', 0.8, d, t + 0.5)
bodyThump(55, 0.5, d, t + 0.5, 0.3)
reverbTail(0.8, d, t + 0.6)
}
// FAIL HORN — game show wrong answer horn
export function sfxFailHorn() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
tone(311, 'sawtooth', 0.3, d, t)
tone(156, 'sawtooth', 0.3, d, t)
tone(233, 'square', 0.3, d, t)
setTimeout(() => {
tone(277, 'sawtooth', 0.5, d)
tone(139, 'sawtooth', 0.5, d)
tone(208, 'square', 0.5, d)
bodyThump(70, 0.3, d, undefined, 0.2)
}, 350)
reverbTail(0.5, d, t + 0.8)
}
// PRICE IS RIGHT FAIL — descending tuba + sad brass
export function sfxPriceIsRightFail() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
const notes = [262, 247, 233, 220, 208, 196]
for (let i = 0; i < notes.length; i++) {
const nt = t + i * 0.18
tone(notes[i], 'sawtooth', 0.2, d, nt)
tone(notes[i] * 0.5, 'triangle', 0.2, d, nt)
}
bodyThump(65, 0.4, d, t + notes.length * 0.18, 0.3)
reverbTail(0.5, d, t + notes.length * 0.18)
}
// BUZZER — harsh game show wrong answer
export function sfxBuzzer() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
tone(150, 'square', 0.4, d, t)
tone(153, 'square', 0.4, d, t)
tone(75, 'square', 0.4, d, t)
noise(0.15, d, t)
bodyThump(60, 0.2, d, t, 0.2)
}
// SAD VIOLIN — thin high vibrato melody
export function sfxSadViolin() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
const notes: [number, number][] = [[659, 0.3], [622, 0.3], [587, 0.4], [554, 0.5]]
let off = 0
for (const [freq, dur] of notes) {
const osc = c.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = freq
const lfo = c.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 6
const lfoG = c.createGain(); lfoG.gain.value = 6
lfo.connect(lfoG); lfoG.connect(osc.frequency)
const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = freq * 2; bp.Q.value = 3
const g = c.createGain()
g.gain.setValueAtTime(0.15, t + off)
g.gain.exponentialRampToValueAtTime(0.001, t + off + dur)
osc.connect(bp); bp.connect(g); g.connect(d)
osc.start(t + off); osc.stop(t + off + dur)
lfo.start(t + off); lfo.stop(t + off + dur)
off += dur
}
}
// MISSION FAILED — dark two-note descending stab
export function sfxMissionFailed() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
tone(165, 'sawtooth', 0.3, d, t)
tone(196, 'sawtooth', 0.3, d, t)
tone(247, 'sawtooth', 0.3, d, t)
bodyThump(82, 0.2, d, t, 0.2)
setTimeout(() => {
tone(139, 'sawtooth', 0.6, d)
tone(165, 'sawtooth', 0.6, d)
tone(208, 'sawtooth', 0.6, d)
bodyThump(70, 0.4, d, undefined, 0.25)
reverbTail(0.6, d)
}, 400)
}
// CRICKETS — awkward silence chirps
export function sfxCrickets() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
for (let i = 0; i < 3; i++) {
const ct = t + i * 0.4
tone(4200, 'sine', 0.03, d, ct)
tone(4200, 'sine', 0.03, d, ct + 0.05)
tone(4500, 'sine', 0.02, d, ct + 0.03)
tone(4500, 'sine', 0.02, d, ct + 0.08)
}
}
// === BACKGROUND MUSIC ===
// Rich, full arcade fighting soundtrack with 6 simultaneous layers
@@ -1619,7 +2020,8 @@ const track1: MusicTrack = {
330,415,494,415, 330,415,494,659, 494,659,494,415, 330,494,415,330,
440,523,659,523, 440,659,523,440, 659,523,440,330, 440,523,659,0,
],
chords: [[220,262,330],[175,220,262],[262,330,392],[196,247,294],[220,262,330],[294,349,440],[165,208,247],[220,262,330]],
// I-V-vi-IV in C major (classic pop anthem)
chords: [[262,330,392],[196,247,294],[220,262,330],[175,220,262],[262,330,392],[196,247,294],[220,262,330],[175,220,262]],
drums: [
1,3,0,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,0,4,0,
1,3,0,3, 2,0,3,3, 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,3,3,0, 1,3,0,3, 2,3,4,3,
@@ -1661,7 +2063,8 @@ const track2: MusicTrack = {
494,622,740,622, 494,622,740,988, 740,988,740,622, 494,740,622,494,
330,392,494,392, 330,494,392,330, 494,392,330,247, 330,392,494,0,
],
chords: [[165,196,247],[131,165,196],[196,247,294],[147,185,220],[165,196,247],[220,262,330],[247,311,370],[165,196,247]],
// i-VII-VI-v in E minor (Andalusian descent)
chords: [[165,196,247],[147,185,220],[131,165,196],[123,147,185],[165,196,247],[131,165,196],[147,185,220],[165,196,247]],
drums: [
1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0,
1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,3,3,3, 2,3,4,3,
@@ -1703,7 +2106,8 @@ const track3: MusicTrack = {
220,277,330,277, 220,277,330,440, 330,440,330,277, 220,330,277,220,
294,349,440,349, 294,440,349,294, 440,349,294,220, 294,349,440,0,
],
chords: [[147,175,220],[233,294,349],[175,220,262],[131,165,196],[147,175,220],[196,233,294],[220,277,330],[147,175,220]],
// i-III-VI-V in D minor (dark dramatic)
chords: [[147,175,220],[175,220,262],[233,294,349],[220,277,330],[147,175,220],[175,220,262],[233,294,349],[147,175,220]],
drums: [
1,0,0,3, 0,0,2,0, 1,0,0,3, 0,0,2,0, 1,0,0,3, 0,0,2,0, 1,0,3,3, 2,0,0,0,
1,0,0,3, 0,0,2,0, 1,0,3,3, 2,0,0,0, 1,0,0,3, 0,0,2,0, 1,0,0,3, 2,0,4,0,
@@ -1745,7 +2149,8 @@ const track4: MusicTrack = {
392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392,
262,311,392,311, 262,392,311,262, 392,311,262,196, 262,311,392,0,
],
chords: [[262,311,392],[208,262,311],[156,196,233],[233,294,349],[262,311,392],[175,208,262],[196,247,294],[262,311,392]],
// i-iv-VII-III in C minor (aggressive power)
chords: [[131,156,196],[175,208,262],[233,294,349],[156,196,233],[131,156,196],[175,208,262],[233,294,349],[131,156,196]],
drums: [
1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,4,0,
1,3,1,3, 2,0,3,3, 1,3,0,3, 2,3,3,0, 1,3,1,3, 2,3,3,0, 1,3,3,3, 2,3,4,3,
@@ -1787,7 +2192,8 @@ const track5: MusicTrack = {
392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392,
440,523,659,523, 440,659,523,440, 659,523,440,330, 440,523,659,0,
],
chords: [[220,262,330],[165,196,247],[175,220,262],[131,165,196],[220,262,330],[147,175,220],[196,247,294],[220,262,330]],
// i-v-iv-VII in A minor (grinding stubborn)
chords: [[220,262,330],[165,196,247],[147,175,220],[196,247,294],[220,262,330],[165,196,247],[147,175,220],[220,262,330]],
drums: [
1,0,0,3, 2,0,0,3, 1,0,0,3, 2,0,0,3, 1,0,0,3, 2,0,0,3, 1,0,3,0, 2,0,0,0,
1,0,0,3, 2,0,3,0, 1,0,0,3, 2,0,0,3, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,4,0,
@@ -1829,7 +2235,8 @@ const track6: MusicTrack = {
294,370,440,370, 294,370,440,587, 440,587,440,370, 294,440,370,294,
196,233,294,233, 196,294,233,196, 294,233,196,147, 196,233,294,0,
],
chords: [[196,233,294],[156,196,233],[233,294,349],[175,220,262],[196,233,294],[131,156,196],[147,185,220],[196,233,294]],
// i-VI-iv-V in G minor (melancholic beauty)
chords: [[196,233,294],[156,196,233],[131,156,196],[147,185,220],[196,233,294],[156,196,233],[147,185,220],[196,233,294]],
drums: [
1,0,0,0, 2,0,0,3, 1,0,0,0, 2,0,0,0, 1,0,0,3, 2,0,0,0, 1,0,0,3, 2,0,0,0,
1,0,0,3, 2,0,0,0, 1,0,3,0, 2,0,0,3, 1,0,0,3, 2,0,0,3, 1,0,3,0, 2,0,4,0,
@@ -1871,7 +2278,8 @@ const track7: MusicTrack = {
494,622,740,622, 494,622,740,988, 740,988,740,622, 494,740,622,494,
330,392,494,392, 330,494,392,330, 494,392,330,247, 330,392,494,0,
],
chords: [[165,196,247],[196,247,294],[147,185,220],[131,165,196],[165,196,247],[220,262,330],[247,311,370],[165,196,247]],
// I-IV-vi-V in G major (uplifting pop)
chords: [[196,247,294],[131,165,196],[165,196,247],[147,185,220],[196,247,294],[131,165,196],[165,196,247],[196,247,294]],
drums: [
1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0,
1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,3,3,3, 2,3,4,0,
@@ -1913,7 +2321,8 @@ const track8: MusicTrack = {
262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262,
349,415,523,415, 349,523,415,349, 523,415,349,262, 349,415,523,0,
],
chords: [[175,208,262],[139,175,208],[208,262,311],[156,196,233],[175,208,262],[233,277,349],[131,165,196],[175,208,262]],
// i-VII-VI-VII in F minor (oscillating tension)
chords: [[175,208,262],[156,196,233],[139,175,208],[156,196,233],[175,208,262],[139,175,208],[156,196,233],[175,208,262]],
drums: [
1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,4,0,
1,3,1,3, 2,3,3,0, 1,3,0,3, 2,0,3,3, 1,3,1,3, 2,0,3,3, 1,3,3,3, 2,3,4,3,
@@ -1955,7 +2364,8 @@ const track9: MusicTrack = {
262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262,
294,349,440,349, 294,440,349,294, 440,349,294,220, 294,349,440,0,
],
chords: [[147,175,220],[196,233,294],[233,294,349],[220,277,330],[147,175,220],[175,220,262],[131,165,196],[147,175,220]],
// i-iv-V-i → i-VI-VII-i in D minor (tension/release)
chords: [[147,175,220],[196,233,294],[220,277,330],[147,175,220],[147,175,220],[233,294,349],[262,330,392],[147,175,220]],
drums: [
1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,3,0,
1,0,3,3, 2,0,3,0, 1,3,3,0, 2,0,3,3, 1,0,3,0, 2,3,3,0, 1,3,3,3, 2,3,4,3,
@@ -1997,7 +2407,8 @@ const track10: MusicTrack = {
349,415,523,415, 349,415,523,698, 523,698,523,415, 349,523,415,349,
392,494,587,494, 392,587,494,392, 587,494,392,294, 392,494,587,0,
],
chords: [[262,311,392],[156,196,233],[233,294,349],[196,247,294],[262,311,392],[208,262,311],[175,208,262],[196,247,294]],
// I-vi-ii-V in Eb major (triumphant jazz-pop)
chords: [[156,196,233],[131,156,196],[175,208,262],[233,294,349],[156,196,233],[131,156,196],[175,208,262],[233,294,349]],
drums: [
1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0,
1,0,3,0, 2,0,3,3, 1,0,3,0, 2,3,3,0, 1,3,3,0, 2,3,3,3, 1,3,1,3, 2,3,4,3,
@@ -2016,34 +2427,38 @@ function playMusicBar() {
const c = getCtx()
getMusicDelay() // ensure delay is created
// Dynamic tempo: subtle intensity shift (+10 BPM at max)
const dynamicBpm = activeTrack.bpm + currentIntensity * 10
// Dynamic tempo: subtle intensity shift (+8 BPM at max)
const dynamicBpm = activeTrack.bpm + currentIntensity * 8
const beat = 60 / dynamicBpm
const now = c.currentTime + 0.05
const bar = barIndex % BARS
const off = bar * STEPS
const step = beat / 2
// Switch tracks at musical boundaries (full 8-bar cycles)
const chillTracks = ALL_TRACKS.filter(t => t.bpm < 160)
const midTracks = ALL_TRACKS.filter(t => t.bpm >= 160 && t.bpm < 175)
const intenseTracks = ALL_TRACKS.filter(t => t.bpm >= 175)
// === VERSE / CHORUS DYNAMICS ===
// Bars 0-3 are "verse" (sparser), bars 4-7 are "chorus" (climactic, full)
const isChorus = bar >= 4
const leadThreshold = isChorus ? 0.15 : 0.4
const arpThreshold = isChorus ? 0.25 : 0.55
// Switch tracks at musical boundaries — NEVER repeat the same track
const switchEvery = currentIntensity > 0.7 ? 8 : currentIntensity > 0.4 ? 16 : 24
if (barIndex > 0 && barIndex % switchEvery === 0) {
const pickFrom = (pool: MusicTrack[]) => {
const others = pool.filter(t => t !== activeTrack)
return others.length > 0 ? others[Math.floor(Math.random() * others.length)] : pool[0]
}
const prevTrack = activeTrack
if (currentIntensity > 0.7) {
activeTrack = pickFrom(intenseTracks.length > 0 ? intenseTracks : ALL_TRACKS)
} else if (currentIntensity < 0.3) {
activeTrack = pickFrom(chillTracks.length > 0 ? chillTracks : ALL_TRACKS)
} else if (Math.random() < 0.5) {
activeTrack = pickFrom(midTracks.length > 0 ? midTracks : ALL_TRACKS)
// Always pick from ALL tracks, excluding current — guarantees no repeat
const others = ALL_TRACKS.filter(t => t !== activeTrack)
// Prefer tracks matching intensity mood
const chill = others.filter(t => t.bpm < 155)
const mid = others.filter(t => t.bpm >= 155 && t.bpm < 168)
const intense = others.filter(t => t.bpm >= 168)
if (currentIntensity > 0.7 && intense.length > 0) {
activeTrack = intense[Math.floor(Math.random() * intense.length)]
} else if (currentIntensity < 0.3 && chill.length > 0) {
activeTrack = chill[Math.floor(Math.random() * chill.length)]
} else if (mid.length > 0 && Math.random() < 0.5) {
activeTrack = mid[Math.floor(Math.random() * mid.length)]
} else {
// Full random for maximum variety
activeTrack = pickFrom(ALL_TRACKS)
activeTrack = others[Math.floor(Math.random() * others.length)]
}
// Smooth crossfade on track switch
if (activeTrack !== prevTrack && musicGain) {
@@ -2054,35 +2469,53 @@ function playMusicBar() {
}
}
// Chord pad for the whole bar
// Chord pad for the whole bar — louder during chorus
const barDur = STEPS * step
chordPad(activeTrack.chords[bar].map(f => f * 2), barDur, musicGain, now)
const padVol = isChorus ? 0.06 : 0.04
const padFreqs = activeTrack.chords[bar].map(f => f * 2)
const padC = getCtx()
for (const freq of padFreqs) {
const osc = padC.createOscillator()
osc.type = 'triangle'
osc.frequency.value = freq
const g = padC.createGain()
g.gain.setValueAtTime(padVol, now)
g.gain.setValueAtTime(padVol, now + barDur * 0.8)
g.gain.exponentialRampToValueAtTime(0.001, now + barDur)
osc.connect(g); g.connect(musicGain)
osc.start(now); osc.stop(now + barDur)
}
for (let i = 0; i < STEPS; i++) {
const t = now + i * step
const idx = off + i
const nd = step - 0.01
// Layer 1: Bass — always plays but gets thicker with intensity
// Layer 1: Bass — always plays
if (activeTrack.bass[idx] > 0) thickBass(activeTrack.bass[idx], nd, musicGain, t)
// Layer 2: FM lead — fades in above 0.3 intensity
if (activeTrack.lead[idx] > 0 && currentIntensity > 0.3) {
// Layer 2: FM lead — verse: needs higher intensity; chorus: plays freely
if (activeTrack.lead[idx] > 0 && currentIntensity > leadThreshold) {
fmLead(activeTrack.lead[idx], nd * 0.8, musicGain, t)
}
// Layer 3: Arpeggio — fades in above 0.5 intensity
if (activeTrack.arp[idx] > 0 && currentIntensity > 0.5) {
// Layer 3: Arpeggio — verse: needs high intensity; chorus: plays freely
if (activeTrack.arp[idx] > 0 && currentIntensity > arpThreshold) {
chorusTone(activeTrack.arp[idx], 'triangle', nd * 0.6, musicGain, t, 0.04 + currentIntensity * 0.04)
}
// Layer 4: Drums — always, but intensity controls density
// Layer 4: Drums — intensity controls density
const d = activeTrack.drums[idx]
if (d === 1) kick(musicGain, t)
if (d === 2 && currentIntensity > 0.2) snare(musicGain, t)
if (d === 3) hihat(musicGain, t, false)
if (d === 4 && currentIntensity > 0.4) hihat(musicGain, t, true)
// Chorus: extra snare ghost notes on off-beats for energy
if (isChorus && currentIntensity > 0.6 && d === 0 && i % 4 === 2 && Math.random() < 0.3) {
snare(musicGain, t)
}
// Extra open hihat at high intensity (musical, not chaotic)
if (currentIntensity > 0.85 && i === 14 && Math.random() < 0.3) {
hihat(musicGain, t, true)