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,
|
setMusicIntensity, stopAllAudio,
|
||||||
setMasterMute, isMasterMuted, ensureAudioContext,
|
setMasterMute, isMasterMuted, ensureAudioContext,
|
||||||
speakQuestion, speakAnswer, speakNarration,
|
speakQuestion, speakAnswer, speakNarration,
|
||||||
|
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage,
|
||||||
} from '../game/sounds'
|
} from '../game/sounds'
|
||||||
|
|
||||||
interface Round {
|
interface Round {
|
||||||
@@ -411,7 +412,19 @@ async function _doReplay() {
|
|||||||
if (round.narration) {
|
if (round.narration) {
|
||||||
logItems.value.push({ type: 'narration', round: round.roundNumber, text: `>> ${round.narration}`, color: 'neon-yellow' })
|
logItems.value.push({ type: 'narration', round: round.roundNumber, text: `>> ${round.narration}`, color: 'neon-yellow' })
|
||||||
scrollLog()
|
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)
|
await sleep(insanityMode.value ? 30 : 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+134
-19
@@ -15,6 +15,13 @@ import {
|
|||||||
announceCreatorEntrance, announceCreatorRound, announceCreatorKO,
|
announceCreatorEntrance, announceCreatorRound, announceCreatorKO,
|
||||||
announceCreatorWin, announceCreatorLose, announceCreatorMorph,
|
announceCreatorWin, announceCreatorLose, announceCreatorMorph,
|
||||||
announceCreatorCameo, announceCreatorTaunt, announceCreatorDevastating,
|
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'
|
} from './sounds'
|
||||||
|
|
||||||
export interface FightSceneConfig {
|
export interface FightSceneConfig {
|
||||||
@@ -80,7 +87,7 @@ const spriteAnims = {
|
|||||||
|
|
||||||
// Challenge type -> themed choreography (shown ~60% of the time) + generic fallbacks
|
// Challenge type -> themed choreography (shown ~60% of the time) + generic fallbacks
|
||||||
const CHALLENGE_THEMED: Record<string, { themed: string[]; generic: string[] }> = {
|
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'] },
|
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'] },
|
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'] },
|
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'] },
|
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'] },
|
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'] },
|
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'] },
|
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'], generic: ['fourOhFour', 'ctrlAltDelete', 'aiUprising', 'captchaTrap', 'popupSpam', 'dataStream', 'glitchBeam'] },
|
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'] },
|
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'] },
|
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'] },
|
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)
|
// Tier-gated ultimate move pools — cumulative (tier 3 bot can use tier 2+3 moves)
|
||||||
const TIER_ULTIMATES: Record<number, string[]> = {
|
const TIER_ULTIMATES: Record<number, string[]> = {
|
||||||
2: ['ultimateRapidBarrage', 'ultimateOrbitalStrike', 'ultimateCloneSurround', 'ultimateGatlingFury', 'ultimateEarthquake', 'ultimateBouncingFury', 'ultimateTripleBeam', 'ultimateMegaThrow', 'ultimateTornadoGrab', 'ultimateMeteorShower'],
|
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'],
|
4: ['ultimateFinaleRush', 'ultimateMeteorCrash', 'ultimateScreenShatter', 'ultimateGravityReverse', 'ultimateInfiniteCombo', 'ultimatePlasmaStorm', 'ultimateFrozenCoffin', 'ultimateLavaGeyser', 'ultimateChainGrab', 'ultimateRocketBarrage'],
|
||||||
5: ['ultimateArmageddon', 'ultimateOmegaBeam', 'ultimateBeyondTheScreen', 'ultimateMatrixDodge', 'ultimateWorldEnder', 'ultimateAbsoluteZero', 'ultimateSolarCannon', 'ultimatePhantomStrike', 'ultimateGodPunch', 'ultimateVoidCollapse'],
|
5: ['ultimateArmageddon', 'ultimateOmegaBeam', 'ultimateBeyondTheScreen', 'ultimateMatrixDodge', 'ultimateWorldEnder', 'ultimateAbsoluteZero', 'ultimateSolarCannon', 'ultimatePhantomStrike', 'ultimateGodPunch', 'ultimateVoidCollapse'],
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Creator — exclusive moves (50% ultimate chance, 100% on crits, custom pool)
|
// 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']
|
const CREATOR_ULTIMATES = ['ultimateGenesisBlock', 'ultimateTheHalving', 'ultimateFullNodeCrush', 'ultimateSatoshiVision']
|
||||||
|
|
||||||
function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
|
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',
|
'alienAbduction', 'tsunamiWave', 'earthquakeStrike', 'dragonBreath', 'pianoDrop',
|
||||||
'fridgeDrop', 'volcanoErupt', 'warpDrive', 'dubstepCannon', 'tornadoSpin',
|
'fridgeDrop', 'volcanoErupt', 'warpDrive', 'dubstepCannon', 'tornadoSpin',
|
||||||
'trampolineBounce', 'watermelonBomb', 'nftRugPull', 'blueScreen', 'yeetThrow',
|
'trampolineBounce', 'watermelonBomb', 'nftRugPull', 'blueScreen', 'yeetThrow',
|
||||||
|
'bulletTimeAttack',
|
||||||
]
|
]
|
||||||
return critMoves[Math.floor(Math.random() * critMoves.length)]
|
return critMoves[Math.floor(Math.random() * critMoves.length)]
|
||||||
}
|
}
|
||||||
@@ -5507,6 +5515,110 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
const ultimateSuperSlam = makeGrab('up', 250)
|
const ultimateSuperSlam = makeGrab('up', 250)
|
||||||
const ultimateAvalancheDrop = makeExplosion(30, 30, '#aaeeff', 0.2, '#ffffff', true)
|
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 ---
|
// --- TIER 4+ ULTIMATES (Platinum) — full screen, multi-phase spectacles ---
|
||||||
async function ultimateFinaleRush(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
|
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
|
// 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,
|
pocketCannon, grappleFlurry, bodySlam, pinballCombo, suplex,
|
||||||
// Themed
|
// Themed
|
||||||
golfClubSmash, fireBreath, riddleBarrage, cloneStrike,
|
golfClubSmash, fireBreath, riddleBarrage, cloneStrike,
|
||||||
coinShower, penStab, mathAttack, trapCardAttack, afterimageDash,
|
coinShower, penStab, mathAttack, trapCardAttack, afterimageDash, bulletTimeAttack,
|
||||||
// --- NEW: Food throws ---
|
// --- NEW: Food throws ---
|
||||||
pizzaSlam, bananaFling, pieSmash, hotdogWhip, watermelonBomb,
|
pizzaSlam, bananaFling, pieSmash, hotdogWhip, watermelonBomb,
|
||||||
sushiBarrage, burgerToss, iceCreamFling, tacoStorm, donutBarrage,
|
sushiBarrage, burgerToss, iceCreamFling, tacoStorm, donutBarrage,
|
||||||
@@ -6589,7 +6701,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
ultimateBouncingFury, ultimateTripleBeam, ultimateMegaThrow, ultimateTornadoGrab, ultimateMeteorShower,
|
ultimateBouncingFury, ultimateTripleBeam, ultimateMegaThrow, ultimateTornadoGrab, ultimateMeteorShower,
|
||||||
// Tier 3+
|
// Tier 3+
|
||||||
ultimateScreenNuke, ultimateBlackHoleVortex, ultimateDimensionalRift, ultimateTimeSlow, ultimateSummonArmy,
|
ultimateScreenNuke, ultimateBlackHoleVortex, ultimateDimensionalRift, ultimateTimeSlow, ultimateSummonArmy,
|
||||||
ultimateHellfireRain, ultimateCrystalCage, ultimateGigaDrill, ultimateSuperSlam, ultimateAvalancheDrop,
|
ultimateHellfireRain, ultimateCrystalCage, ultimateGigaDrill, ultimateSuperSlam, ultimateAvalancheDrop, ultimateBulletTimeBarrage,
|
||||||
// Tier 4+
|
// Tier 4+
|
||||||
ultimateFinaleRush, ultimateMeteorCrash, ultimateScreenShatter, ultimateGravityReverse, ultimateInfiniteCombo,
|
ultimateFinaleRush, ultimateMeteorCrash, ultimateScreenShatter, ultimateGravityReverse, ultimateInfiniteCombo,
|
||||||
ultimatePlasmaStorm, ultimateFrozenCoffin, ultimateLavaGeyser, ultimateChainGrab, ultimateRocketBarrage,
|
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)
|
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')
|
judge.play(d % 2 === 0 ? 'call_left' : 'call_right')
|
||||||
}
|
}
|
||||||
sfxRandomSilly()
|
sfxRandomComedy()
|
||||||
judge.play('idle')
|
judge.play('idle')
|
||||||
} else if (funnyAction === 2) {
|
} else if (funnyAction === 2) {
|
||||||
// Holds up a 10 score card
|
// Holds up a 10 score card
|
||||||
@@ -7604,7 +7716,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
flag.onUpdate(() => {
|
flag.onUpdate(() => {
|
||||||
flag.pos.y = judge.pos.y - 30 + Math.sin(k.time() * 10) * 3
|
flag.pos.y = judge.pos.y - 30 + Math.sin(k.time() * 10) * 3
|
||||||
})
|
})
|
||||||
sfxRandomSilly()
|
sfxRandomComedy()
|
||||||
await k.wait(1.5)
|
await k.wait(1.5)
|
||||||
flag.destroy(); pole.destroy()
|
flag.destroy(); pole.destroy()
|
||||||
} else {
|
} else {
|
||||||
@@ -8478,6 +8590,8 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
const idx = Math.floor(Math.random() * pool.length)
|
const idx = Math.floor(Math.random() * pool.length)
|
||||||
try {
|
try {
|
||||||
await pool[idx](f, homeX, homeY, sx, sy, dir)
|
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 }
|
} catch { break }
|
||||||
// Restore state after each showboat
|
// Restore state after each showboat
|
||||||
const ff = k.get(tag)[0]
|
const ff = k.get(tag)[0]
|
||||||
@@ -9386,7 +9500,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
if (!isLastExchange && Math.random() < 0.15) {
|
if (!isLastExchange && Math.random() < 0.15) {
|
||||||
await this.playDodge(attackerSide === 'a' ? 'b' : 'a')
|
await this.playDodge(attackerSide === 'a' ? 'b' : 'a')
|
||||||
sfxDodge()
|
sfxDodge()
|
||||||
sfxBoing()
|
if (Math.random() < 0.4) sfxRandomFail(); else sfxBoing()
|
||||||
} else {
|
} else {
|
||||||
await this.playAttack(attackerSide, choreo, exchangeCritical)
|
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)
|
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)
|
sfxSplat(); k.shake(15); screenFlash('#ffe135', 0.15)
|
||||||
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe135')
|
spawnShockwave(loser.pos.x, GROUND_Y, '#ffe135')
|
||||||
winner.play('win'); sfxRandomSilly()
|
winner.play('win'); sfxRandomComedy()
|
||||||
} else if (finishStyle === 5) {
|
} else if (finishStyle === 5) {
|
||||||
// ANVIL DROP — cartoon anvil from the sky
|
// ANVIL DROP — cartoon anvil from the sky
|
||||||
fatalityTagline = 'HEAVY METAL!'
|
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)
|
await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad)
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
winner.play(i % 2 === 0 ? 'attack' : 'kick')
|
winner.play(i % 2 === 0 ? 'attack' : 'kick')
|
||||||
sfxBoing(); sfxRandomSilly()
|
sfxBoing(); sfxRandomComedy()
|
||||||
loser.play('hit'); k.shake(3 + i)
|
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 }
|
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')
|
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)
|
await k.wait(0.3)
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
loser.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special')
|
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)
|
await k.wait(0.1)
|
||||||
}
|
}
|
||||||
sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.2)
|
sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.2)
|
||||||
@@ -10060,7 +10174,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
announceFast('Rev it up!')
|
announceFast('Rev it up!')
|
||||||
const mower = spawnWeaponProp('lawn_mower', winner.pos.x + dir * 10, GROUND_Y - 8, dir < 0, 16)
|
const mower = spawnWeaponProp('lawn_mower', winner.pos.x + dir * 10, GROUND_Y - 8, dir < 0, 16)
|
||||||
winner.play('special'); sfxPowerUp()
|
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()
|
sfxZoomWhoosh()
|
||||||
for (const p of mower) {
|
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)
|
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.opacity = Math.random() > 0.5 ? 1 : 0.2
|
||||||
loser.pos.x += (Math.random() - 0.5) * 15
|
loser.pos.x += (Math.random() - 0.5) * 15
|
||||||
loser.play(['idle', 'attack', 'kick', 'hit', 'special'][Math.floor(Math.random() * 5)])
|
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)
|
await k.wait(0.06)
|
||||||
}
|
}
|
||||||
screenFlash('#0000ff', 0.3); sfxFail(); k.shake(20)
|
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++) {
|
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)
|
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('hit'); k.shake(10); await k.wait(0.15)
|
||||||
loser.play('knockback'); sfxCritical(); k.shake(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)
|
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()
|
sfxPowerUp()
|
||||||
const moves = ['attack', 'kick', 'special', 'kick', 'attack', 'special'] as const
|
const moves = ['attack', 'kick', 'special', 'kick', 'attack', 'special'] as const
|
||||||
for (let i = 0; i < moves.length; i++) {
|
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])
|
spawnSparks(winner.pos.x, winner.pos.y - 30, 3, ['#ff00ff', '#00ffff', '#ffff00'][i % 3])
|
||||||
await k.wait(0.08)
|
await k.wait(0.08)
|
||||||
}
|
}
|
||||||
@@ -10366,7 +10480,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
announceSilly('What is that SMELL!?')
|
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)])
|
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)
|
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)
|
await k.tween(20, 40, 0.15, (v) => { cushion.radius = v }, k.easings.easeOutQuad)
|
||||||
sfxExplosion()
|
sfxExplosion()
|
||||||
for (let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
@@ -10466,6 +10580,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
k.shake(18); sfxExplosion()
|
k.shake(18); sfxExplosion()
|
||||||
await k.wait(0.3)
|
await k.wait(0.3)
|
||||||
loser.play('ko')
|
loser.play('ko')
|
||||||
|
if (Math.random() < 0.5) setTimeout(() => sfxRandomFail(), 200)
|
||||||
await k.wait(0.4) // pause for visual impact before fatality voice
|
await k.wait(0.4) // pause for visual impact before fatality voice
|
||||||
const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype
|
const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype
|
||||||
const loserArch = winningSide === 'a' ? botB.archetype : botA.archetype
|
const loserArch = winningSide === 'a' ? botB.archetype : botA.archetype
|
||||||
@@ -10635,7 +10750,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
18,
|
18,
|
||||||
['#ff2d7b', '#00f0ff', '#ffe14d', '#b83dff', '#39ff14', '#ff6600', '#ffffff', '#ff2d2d'][i]
|
['#ff2d7b', '#00f0ff', '#ffe14d', '#b83dff', '#39ff14', '#ff6600', '#ffffff', '#ff2d2d'][i]
|
||||||
)
|
)
|
||||||
if (i % 2 === 0) sfxRandomSilly()
|
if (i % 2 === 0) sfxRandomComedy()
|
||||||
}, i * 200)
|
}, i * 200)
|
||||||
}
|
}
|
||||||
await k.wait(0.7)
|
await k.wait(0.7)
|
||||||
|
|||||||
+470
-37
@@ -1384,10 +1384,411 @@ export function sfxFail() {
|
|||||||
|
|
||||||
// Pick a random silly sound
|
// Pick a random silly sound
|
||||||
export function sfxRandomSilly() {
|
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)]()
|
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 ===
|
// === BACKGROUND MUSIC ===
|
||||||
// Rich, full arcade fighting soundtrack with 6 simultaneous layers
|
// 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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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,
|
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,
|
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: [
|
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,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,
|
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()
|
const c = getCtx()
|
||||||
getMusicDelay() // ensure delay is created
|
getMusicDelay() // ensure delay is created
|
||||||
|
|
||||||
// Dynamic tempo: subtle intensity shift (+10 BPM at max)
|
// Dynamic tempo: subtle intensity shift (+8 BPM at max)
|
||||||
const dynamicBpm = activeTrack.bpm + currentIntensity * 10
|
const dynamicBpm = activeTrack.bpm + currentIntensity * 8
|
||||||
const beat = 60 / dynamicBpm
|
const beat = 60 / dynamicBpm
|
||||||
const now = c.currentTime + 0.05
|
const now = c.currentTime + 0.05
|
||||||
const bar = barIndex % BARS
|
const bar = barIndex % BARS
|
||||||
const off = bar * STEPS
|
const off = bar * STEPS
|
||||||
const step = beat / 2
|
const step = beat / 2
|
||||||
|
|
||||||
// Switch tracks at musical boundaries (full 8-bar cycles)
|
// === VERSE / CHORUS DYNAMICS ===
|
||||||
const chillTracks = ALL_TRACKS.filter(t => t.bpm < 160)
|
// Bars 0-3 are "verse" (sparser), bars 4-7 are "chorus" (climactic, full)
|
||||||
const midTracks = ALL_TRACKS.filter(t => t.bpm >= 160 && t.bpm < 175)
|
const isChorus = bar >= 4
|
||||||
const intenseTracks = ALL_TRACKS.filter(t => t.bpm >= 175)
|
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
|
const switchEvery = currentIntensity > 0.7 ? 8 : currentIntensity > 0.4 ? 16 : 24
|
||||||
if (barIndex > 0 && barIndex % switchEvery === 0) {
|
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
|
const prevTrack = activeTrack
|
||||||
if (currentIntensity > 0.7) {
|
// Always pick from ALL tracks, excluding current — guarantees no repeat
|
||||||
activeTrack = pickFrom(intenseTracks.length > 0 ? intenseTracks : ALL_TRACKS)
|
const others = ALL_TRACKS.filter(t => t !== activeTrack)
|
||||||
} else if (currentIntensity < 0.3) {
|
// Prefer tracks matching intensity mood
|
||||||
activeTrack = pickFrom(chillTracks.length > 0 ? chillTracks : ALL_TRACKS)
|
const chill = others.filter(t => t.bpm < 155)
|
||||||
} else if (Math.random() < 0.5) {
|
const mid = others.filter(t => t.bpm >= 155 && t.bpm < 168)
|
||||||
activeTrack = pickFrom(midTracks.length > 0 ? midTracks : ALL_TRACKS)
|
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 {
|
} else {
|
||||||
// Full random for maximum variety
|
activeTrack = others[Math.floor(Math.random() * others.length)]
|
||||||
activeTrack = pickFrom(ALL_TRACKS)
|
|
||||||
}
|
}
|
||||||
// Smooth crossfade on track switch
|
// Smooth crossfade on track switch
|
||||||
if (activeTrack !== prevTrack && musicGain) {
|
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
|
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++) {
|
for (let i = 0; i < STEPS; i++) {
|
||||||
const t = now + i * step
|
const t = now + i * step
|
||||||
const idx = off + i
|
const idx = off + i
|
||||||
const nd = step - 0.01
|
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)
|
if (activeTrack.bass[idx] > 0) thickBass(activeTrack.bass[idx], nd, musicGain, t)
|
||||||
|
|
||||||
// Layer 2: FM lead — fades in above 0.3 intensity
|
// Layer 2: FM lead — verse: needs higher intensity; chorus: plays freely
|
||||||
if (activeTrack.lead[idx] > 0 && currentIntensity > 0.3) {
|
if (activeTrack.lead[idx] > 0 && currentIntensity > leadThreshold) {
|
||||||
fmLead(activeTrack.lead[idx], nd * 0.8, musicGain, t)
|
fmLead(activeTrack.lead[idx], nd * 0.8, musicGain, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer 3: Arpeggio — fades in above 0.5 intensity
|
// Layer 3: Arpeggio — verse: needs high intensity; chorus: plays freely
|
||||||
if (activeTrack.arp[idx] > 0 && currentIntensity > 0.5) {
|
if (activeTrack.arp[idx] > 0 && currentIntensity > arpThreshold) {
|
||||||
chorusTone(activeTrack.arp[idx], 'triangle', nd * 0.6, musicGain, t, 0.04 + currentIntensity * 0.04)
|
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]
|
const d = activeTrack.drums[idx]
|
||||||
if (d === 1) kick(musicGain, t)
|
if (d === 1) kick(musicGain, t)
|
||||||
if (d === 2 && currentIntensity > 0.2) snare(musicGain, t)
|
if (d === 2 && currentIntensity > 0.2) snare(musicGain, t)
|
||||||
if (d === 3) hihat(musicGain, t, false)
|
if (d === 3) hihat(musicGain, t, false)
|
||||||
if (d === 4 && currentIntensity > 0.4) hihat(musicGain, t, true)
|
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)
|
// Extra open hihat at high intensity (musical, not chaotic)
|
||||||
if (currentIntensity > 0.85 && i === 14 && Math.random() < 0.3) {
|
if (currentIntensity > 0.85 && i === 14 && Math.random() < 0.3) {
|
||||||
hihat(musicGain, t, true)
|
hihat(musicGain, t, true)
|
||||||
|
|||||||
Reference in New Issue
Block a user