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:
co-authored by
Claude Opus 4.6
parent
d3f026d356
commit
a59b0748a7
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user