fix: audio reliability — proper AudioContext resume, SFX destination, voice stability
- getSfxDest() helper centralizes SFX routing (replaces 38 inline calls) - Await ensureAudioContext() before playing audio (fixes suspended context on prod) - Chrome speechSynthesis pause bug workaround (periodic resume interval) - Respect masterMuted on initial gain node creation - Responsive overlay text sizes for small screens (text-2xl/5xl/7xl) - Speech queue depth management with safety flush at depth > 3 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
07fa2cf35c
commit
20e9f0f7a2
@@ -63,9 +63,9 @@ const hitTextY = ref(30)
|
|||||||
const glitching = ref(false)
|
const glitching = ref(false)
|
||||||
const soundOn = ref(true)
|
const soundOn = ref(true)
|
||||||
|
|
||||||
function toggleSound() {
|
async function toggleSound() {
|
||||||
soundOn.value = !soundOn.value
|
soundOn.value = !soundOn.value
|
||||||
ensureAudioContext()
|
await ensureAudioContext()
|
||||||
setMasterMute(!soundOn.value)
|
setMasterMute(!soundOn.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ async function replay() {
|
|||||||
|
|
||||||
await initScene()
|
await initScene()
|
||||||
if (soundOn.value) {
|
if (soundOn.value) {
|
||||||
ensureAudioContext()
|
await ensureAudioContext()
|
||||||
scene?.startMusic()
|
scene?.startMusic()
|
||||||
}
|
}
|
||||||
await sleep(300)
|
await sleep(300)
|
||||||
@@ -509,7 +509,7 @@ async function replay() {
|
|||||||
<div v-if="announcementVisible"
|
<div v-if="announcementVisible"
|
||||||
class="absolute inset-0 flex items-center justify-center pointer-events-none z-20">
|
class="absolute inset-0 flex items-center justify-center pointer-events-none z-20">
|
||||||
<div class="announce-text-wrapper">
|
<div class="announce-text-wrapper">
|
||||||
<p class="font-funky text-5xl sm:text-7xl tracking-widest announce-text uppercase announce-chromatic"
|
<p class="font-funky text-2xl sm:text-5xl lg:text-7xl tracking-widest announce-text uppercase announce-chromatic"
|
||||||
:data-text="announcement"
|
:data-text="announcement"
|
||||||
:style="{ color: announcementColor, textShadow: `0 0 20px ${announcementColor}, 0 0 40px ${announcementColor}, 0 0 80px ${announcementColor}40, 0 0 120px ${announcementColor}20` }">
|
:style="{ color: announcementColor, textShadow: `0 0 20px ${announcementColor}, 0 0 40px ${announcementColor}, 0 0 80px ${announcementColor}40, 0 0 120px ${announcementColor}20` }">
|
||||||
{{ announcement }}
|
{{ announcement }}
|
||||||
@@ -524,7 +524,7 @@ async function replay() {
|
|||||||
class="absolute pointer-events-none z-30"
|
class="absolute pointer-events-none z-30"
|
||||||
:style="{ left: `${hitTextX}%`, top: `${hitTextY}%`, transform: 'translate(-50%, -50%)' }">
|
:style="{ left: `${hitTextX}%`, top: `${hitTextY}%`, transform: 'translate(-50%, -50%)' }">
|
||||||
<div class="hit-text-wrapper">
|
<div class="hit-text-wrapper">
|
||||||
<p class="font-neon text-4xl sm:text-5xl tracking-wider hit-text hit-chromatic"
|
<p class="font-neon text-xl sm:text-4xl lg:text-5xl tracking-wider hit-text hit-chromatic"
|
||||||
:data-text="hitText"
|
:data-text="hitText"
|
||||||
:style="{ color: hitTextColor, textShadow: `0 0 15px ${hitTextColor}, 0 0 30px ${hitTextColor}, 0 0 60px ${hitTextColor}60` }">
|
:style="{ color: hitTextColor, textShadow: `0 0 15px ${hitTextColor}, 0 0 30px ${hitTextColor}, 0 0 60px ${hitTextColor}60` }">
|
||||||
{{ hitText }}
|
{{ hitText }}
|
||||||
|
|||||||
+70
-46
@@ -9,16 +9,24 @@ function getCtx(): AudioContext {
|
|||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
ctx = new AudioContext()
|
ctx = new AudioContext()
|
||||||
musicGain = ctx.createGain()
|
musicGain = ctx.createGain()
|
||||||
musicGain.gain.value = 0.12
|
musicGain.gain.value = masterMuted ? 0 : 0.12
|
||||||
musicGain.connect(ctx.destination)
|
musicGain.connect(ctx.destination)
|
||||||
sfxGain = ctx.createGain()
|
sfxGain = ctx.createGain()
|
||||||
sfxGain.gain.value = 0.25
|
sfxGain.gain.value = masterMuted ? 0 : 0.25
|
||||||
sfxGain.connect(ctx.destination)
|
sfxGain.connect(ctx.destination)
|
||||||
}
|
}
|
||||||
if (ctx.state === 'suspended') ctx.resume()
|
if (ctx.state === 'suspended') {
|
||||||
|
ctx.resume().catch(() => {})
|
||||||
|
}
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the SFX destination node (gain is 0 when muted, so audio is silent but context stays valid)
|
||||||
|
function getSfxDest(): AudioNode {
|
||||||
|
const c = getCtx()
|
||||||
|
return sfxGain ?? c.destination
|
||||||
|
}
|
||||||
|
|
||||||
function tone(freq: number, type: OscillatorType, duration: number, dest: AudioNode, startTime?: number) {
|
function tone(freq: number, type: OscillatorType, duration: number, dest: AudioNode, startTime?: number) {
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const osc = c.createOscillator()
|
const osc = c.createOscillator()
|
||||||
@@ -263,6 +271,12 @@ function loadVoices() {
|
|||||||
if (typeof speechSynthesis !== 'undefined') {
|
if (typeof speechSynthesis !== 'undefined') {
|
||||||
speechSynthesis.onvoiceschanged = loadVoices
|
speechSynthesis.onvoiceschanged = loadVoices
|
||||||
loadVoices()
|
loadVoices()
|
||||||
|
// Chrome bug workaround: speechSynthesis pauses after ~15s.
|
||||||
|
// Periodic resume() keeps it alive.
|
||||||
|
setInterval(() => {
|
||||||
|
if (speechSynthesis.speaking && !speechSynthesis.paused) return
|
||||||
|
if (speechSynthesis.paused) speechSynthesis.resume()
|
||||||
|
}, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _speechQueueDepth = 0
|
let _speechQueueDepth = 0
|
||||||
@@ -271,11 +285,14 @@ function speak(text: string, profileName: string, cancelPrevious: boolean = fals
|
|||||||
if (typeof speechSynthesis === 'undefined') return
|
if (typeof speechSynthesis === 'undefined') return
|
||||||
if (masterMuted) return
|
if (masterMuted) return
|
||||||
if (!voicesLoaded) loadVoices()
|
if (!voicesLoaded) loadVoices()
|
||||||
|
// Chrome bug: speechSynthesis can get stuck. Nudge it.
|
||||||
|
if (speechSynthesis.paused) speechSynthesis.resume()
|
||||||
// Only flush if queue is getting deep — allows voices to overlap naturally
|
// Only flush if queue is getting deep — allows voices to overlap naturally
|
||||||
if (cancelPrevious || speechSynthesis.pending && speechSynthesis.speaking) {
|
if (cancelPrevious || (speechSynthesis.pending && speechSynthesis.speaking)) {
|
||||||
// 2+ queued: drop the queue but let current utterance finish
|
if (_speechQueueDepth > 3) {
|
||||||
const queued = _speechQueueDepth
|
speechSynthesis.cancel()
|
||||||
if (queued > 3) speechSynthesis.cancel()
|
_speechQueueDepth = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const profile = voiceProfiles[profileName] || voiceProfiles.announcer
|
const profile = voiceProfiles[profileName] || voiceProfiles.announcer
|
||||||
const utter = new SpeechSynthesisUtterance(text)
|
const utter = new SpeechSynthesisUtterance(text)
|
||||||
@@ -448,7 +465,7 @@ export function announceRoundHype() {
|
|||||||
// === FANFARES (8-bit melodic announcements) ===
|
// === FANFARES (8-bit melodic announcements) ===
|
||||||
|
|
||||||
export function fanfareRound(roundNum: number) {
|
export function fanfareRound(roundNum: number) {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
// Ascending power chord
|
// Ascending power chord
|
||||||
@@ -462,7 +479,7 @@ export function fanfareRound(roundNum: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fanfareFight() {
|
export function fanfareFight() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
// Punchy staccato
|
// Punchy staccato
|
||||||
@@ -474,7 +491,7 @@ export function fanfareFight() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fanfareDevastating() {
|
export function fanfareDevastating() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
tone(220, 'sawtooth', 0.2, d, t)
|
tone(220, 'sawtooth', 0.2, d, t)
|
||||||
@@ -484,7 +501,7 @@ export function fanfareDevastating() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fanfareCritical() {
|
export function fanfareCritical() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
tone(440, 'square', 0.1, d, t)
|
tone(440, 'square', 0.1, d, t)
|
||||||
@@ -496,7 +513,7 @@ export function fanfareCritical() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fanfareCombo(count: number) {
|
export function fanfareCombo(count: number) {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
for (let i = 0; i < Math.min(count, 5); i++) {
|
for (let i = 0; i < Math.min(count, 5); i++) {
|
||||||
@@ -585,7 +602,7 @@ function fmImpact(carrier: number, modRatio: number, duration: number, dest: Aud
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxPunch() {
|
export function sfxPunch() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Heavy body thump
|
// Heavy body thump
|
||||||
bodyThump(120, 0.12, d, t, 0.3)
|
bodyThump(120, 0.12, d, t, 0.3)
|
||||||
@@ -598,7 +615,7 @@ export function sfxPunch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxKick() {
|
export function sfxKick() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Deep bass slam
|
// Deep bass slam
|
||||||
bodyThump(80, 0.18, d, t, 0.35)
|
bodyThump(80, 0.18, d, t, 0.35)
|
||||||
@@ -611,7 +628,7 @@ export function sfxKick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxSpecial() {
|
export function sfxSpecial() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Power charge sweep
|
// Power charge sweep
|
||||||
sweep(150, 800, 'sawtooth', 0.2, d)
|
sweep(150, 800, 'sawtooth', 0.2, d)
|
||||||
@@ -626,7 +643,7 @@ export function sfxSpecial() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxCritical() {
|
export function sfxCritical() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Massive bass hit
|
// Massive bass hit
|
||||||
bodyThump(60, 0.3, d, t, 0.4)
|
bodyThump(60, 0.3, d, t, 0.4)
|
||||||
@@ -648,7 +665,7 @@ export function sfxCritical() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxGunshot() {
|
export function sfxGunshot() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Sharp transient
|
// Sharp transient
|
||||||
highSnap(6000, 0.015, d, t)
|
highSnap(6000, 0.015, d, t)
|
||||||
@@ -662,7 +679,7 @@ export function sfxGunshot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxBulletHit() {
|
export function sfxBulletHit() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
bodyThump(150, 0.06, d, t, 0.2)
|
bodyThump(150, 0.06, d, t, 0.2)
|
||||||
highSnap(4000, 0.02, d, t)
|
highSnap(4000, 0.02, d, t)
|
||||||
@@ -671,7 +688,7 @@ export function sfxBulletHit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxJetpack() {
|
export function sfxJetpack() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Turbine rumble — layered oscillators with modulation
|
// Turbine rumble — layered oscillators with modulation
|
||||||
const osc1 = c.createOscillator(); osc1.type = 'sawtooth'
|
const osc1 = c.createOscillator(); osc1.type = 'sawtooth'
|
||||||
@@ -698,7 +715,7 @@ export function sfxJetpack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxExplosion() {
|
export function sfxExplosion() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Massive sub thump
|
// Massive sub thump
|
||||||
bodyThump(40, 0.5, d, t, 0.4)
|
bodyThump(40, 0.5, d, t, 0.4)
|
||||||
@@ -720,7 +737,7 @@ export function sfxExplosion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxBlock() {
|
export function sfxBlock() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Metallic shield ring
|
// Metallic shield ring
|
||||||
fmImpact(800, 1.5, 0.12, d, t)
|
fmImpact(800, 1.5, 0.12, d, t)
|
||||||
@@ -730,7 +747,7 @@ export function sfxBlock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxDodge() {
|
export function sfxDodge() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Quick air whoosh
|
// Quick air whoosh
|
||||||
sweep(300, 1200, 'sine', 0.1, d)
|
sweep(300, 1200, 'sine', 0.1, d)
|
||||||
@@ -740,7 +757,7 @@ export function sfxDodge() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxClash() {
|
export function sfxClash() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Two impacts overlapping
|
// Two impacts overlapping
|
||||||
bodyThump(100, 0.1, d, t, 0.25)
|
bodyThump(100, 0.1, d, t, 0.25)
|
||||||
@@ -754,7 +771,7 @@ export function sfxClash() {
|
|||||||
// === KO + WIN ===
|
// === KO + WIN ===
|
||||||
|
|
||||||
export function sfxKO() {
|
export function sfxKO() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Earth-shaking bass
|
// Earth-shaking bass
|
||||||
bodyThump(30, 0.6, d, t, 0.5)
|
bodyThump(30, 0.6, d, t, 0.5)
|
||||||
@@ -774,7 +791,7 @@ export function sfxKO() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxPerfect() {
|
export function sfxPerfect() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Massive descending power
|
// Massive descending power
|
||||||
bodyThump(30, 0.8, d, t, 0.45)
|
bodyThump(30, 0.8, d, t, 0.45)
|
||||||
@@ -793,7 +810,7 @@ export function sfxPerfect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxWin() {
|
export function sfxWin() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Victory jingle — richer with harmonics and FM shimmer
|
// Victory jingle — richer with harmonics and FM shimmer
|
||||||
const melody = [
|
const melody = [
|
||||||
@@ -821,7 +838,7 @@ export function sfxWinAnnounce(winnerName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxRoundStart() {
|
export function sfxRoundStart() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Bell-like tones with FM shimmer
|
// Bell-like tones with FM shimmer
|
||||||
fmImpact(440, 2, 0.2, d, t)
|
fmImpact(440, 2, 0.2, d, t)
|
||||||
@@ -836,7 +853,7 @@ export function sfxRoundStart() {
|
|||||||
// === SILLY / FUNNY SOUNDS ===
|
// === SILLY / FUNNY SOUNDS ===
|
||||||
|
|
||||||
export function sfxBoing() {
|
export function sfxBoing() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Spring with resonance
|
// Spring with resonance
|
||||||
sweep(200, 900, 'sine', 0.12, d)
|
sweep(200, 900, 'sine', 0.12, d)
|
||||||
@@ -846,7 +863,7 @@ export function sfxBoing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxWomp() {
|
export function sfxWomp() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Sad trombone — richer with vibrato
|
// Sad trombone — richer with vibrato
|
||||||
const notes = [[294, 0.25], [277, 0.25], [262, 0.25], [247, 0.5]] as [number, number][]
|
const notes = [[294, 0.25], [277, 0.25], [262, 0.25], [247, 0.5]] as [number, number][]
|
||||||
@@ -860,19 +877,19 @@ export function sfxWomp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxSlideUp() {
|
export function sfxSlideUp() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
sweep(200, 2000, 'sine', 0.25, d)
|
sweep(200, 2000, 'sine', 0.25, d)
|
||||||
sweep(210, 2100, 'sine', 0.25, d) // chorus
|
sweep(210, 2100, 'sine', 0.25, d) // chorus
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sfxSlideDown() {
|
export function sfxSlideDown() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
sweep(2000, 100, 'sine', 0.35, d)
|
sweep(2000, 100, 'sine', 0.35, d)
|
||||||
sweep(2020, 110, 'sine', 0.35, d)
|
sweep(2020, 110, 'sine', 0.35, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sfxBonk() {
|
export function sfxBonk() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
bodyThump(200, 0.06, d, t, 0.2)
|
bodyThump(200, 0.06, d, t, 0.2)
|
||||||
fmImpact(800, 3, 0.06, d, t)
|
fmImpact(800, 3, 0.06, d, t)
|
||||||
@@ -881,7 +898,7 @@ export function sfxBonk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxSplat() {
|
export function sfxSplat() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
bodyThump(100, 0.1, d, t, 0.2)
|
bodyThump(100, 0.1, d, t, 0.2)
|
||||||
noise(0.12, d, t)
|
noise(0.12, d, t)
|
||||||
@@ -891,7 +908,7 @@ export function sfxSplat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxZap() {
|
export function sfxZap() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Electric arc
|
// Electric arc
|
||||||
sweep(100, 4000, 'sawtooth', 0.06, d)
|
sweep(100, 4000, 'sawtooth', 0.06, d)
|
||||||
@@ -902,7 +919,7 @@ export function sfxZap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxZoomWhoosh() {
|
export function sfxZoomWhoosh() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
// Layered Doppler whoosh
|
// Layered Doppler whoosh
|
||||||
sweep(80, 1800, 'sawtooth', 0.2, d)
|
sweep(80, 1800, 'sawtooth', 0.2, d)
|
||||||
@@ -917,7 +934,7 @@ export function sfxZoomWhoosh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxRapidPunch() {
|
export function sfxRapidPunch() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
const ht = t + i * 0.04
|
const ht = t + i * 0.04
|
||||||
@@ -928,7 +945,7 @@ export function sfxRapidPunch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxPowerUp() {
|
export function sfxPowerUp() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const freq = 300 + i * 100
|
const freq = 300 + i * 100
|
||||||
@@ -939,7 +956,7 @@ export function sfxPowerUp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxCoin() {
|
export function sfxCoin() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
fmImpact(988, 1.5, 0.1, d, t)
|
fmImpact(988, 1.5, 0.1, d, t)
|
||||||
fmImpact(1319, 1.5, 0.15, d, t + 0.06)
|
fmImpact(1319, 1.5, 0.15, d, t + 0.06)
|
||||||
@@ -948,7 +965,7 @@ export function sfxCoin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sfxFail() {
|
export function sfxFail() {
|
||||||
const d = sfxGain ?? getCtx().destination
|
const d = getSfxDest()
|
||||||
const c = getCtx(); const t = c.currentTime
|
const c = getCtx(); const t = c.currentTime
|
||||||
sweep(600, 80, 'sawtooth', 0.25, d)
|
sweep(600, 80, 'sawtooth', 0.25, d)
|
||||||
bodyThump(80, 0.2, d, t + 0.15, 0.15)
|
bodyThump(80, 0.2, d, t + 0.15, 0.15)
|
||||||
@@ -1828,8 +1845,15 @@ export function isMasterMuted(): boolean {
|
|||||||
return masterMuted
|
return masterMuted
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureAudioContext() {
|
export async function ensureAudioContext() {
|
||||||
getCtx()
|
const c = getCtx()
|
||||||
|
if (c.state === 'suspended') {
|
||||||
|
try { await c.resume() } catch {}
|
||||||
|
}
|
||||||
|
// Prime speech synthesis on user gesture — some browsers need this
|
||||||
|
if (typeof speechSynthesis !== 'undefined' && !voicesLoaded) {
|
||||||
|
loadVoices()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === CROWD SOUNDS ===
|
// === CROWD SOUNDS ===
|
||||||
@@ -1837,7 +1861,7 @@ export function ensureAudioContext() {
|
|||||||
|
|
||||||
export function sfxCrowdOoh() {
|
export function sfxCrowdOoh() {
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const d = sfxGain ?? c.destination
|
const d = getSfxDest()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
// Rising "ooh" — filtered noise sweep
|
// Rising "ooh" — filtered noise sweep
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
@@ -1857,7 +1881,7 @@ export function sfxCrowdOoh() {
|
|||||||
|
|
||||||
export function sfxCrowdGasp() {
|
export function sfxCrowdGasp() {
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const d = sfxGain ?? c.destination
|
const d = getSfxDest()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
// Sharp intake — high noise burst + quick sine chirps
|
// Sharp intake — high noise burst + quick sine chirps
|
||||||
const bufSize = Math.max(1, Math.floor(c.sampleRate * 0.15))
|
const bufSize = Math.max(1, Math.floor(c.sampleRate * 0.15))
|
||||||
@@ -1886,7 +1910,7 @@ export function sfxCrowdGasp() {
|
|||||||
|
|
||||||
export function sfxCrowdCheer() {
|
export function sfxCrowdCheer() {
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const d = sfxGain ?? c.destination
|
const d = getSfxDest()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
// Layered noise + sine clusters = roaring crowd
|
// Layered noise + sine clusters = roaring crowd
|
||||||
for (let layer = 0; layer < 3; layer++) {
|
for (let layer = 0; layer < 3; layer++) {
|
||||||
@@ -1909,7 +1933,7 @@ export function sfxCrowdCheer() {
|
|||||||
|
|
||||||
export function sfxApplause() {
|
export function sfxApplause() {
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const d = sfxGain ?? c.destination
|
const d = getSfxDest()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
// Crackling filtered noise = many hands clapping
|
// Crackling filtered noise = many hands clapping
|
||||||
for (let burst = 0; burst < 6; burst++) {
|
for (let burst = 0; burst < 6; burst++) {
|
||||||
@@ -1930,7 +1954,7 @@ export function sfxApplause() {
|
|||||||
|
|
||||||
export function sfxDrumRoll() {
|
export function sfxDrumRoll() {
|
||||||
const c = getCtx()
|
const c = getCtx()
|
||||||
const d = sfxGain ?? c.destination
|
const d = getSfxDest()
|
||||||
const t = c.currentTime
|
const t = c.currentTime
|
||||||
// Rapid snare hits building in intensity
|
// Rapid snare hits building in intensity
|
||||||
const hits = 16
|
const hits = 16
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ async function initLiveScene() {
|
|||||||
|
|
||||||
if (liveScene) {
|
if (liveScene) {
|
||||||
if (liveSoundOn.value) {
|
if (liveSoundOn.value) {
|
||||||
ensureAudioContext()
|
await ensureAudioContext()
|
||||||
liveScene.startMusic()
|
liveScene.startMusic()
|
||||||
}
|
}
|
||||||
announceDeepIntro()
|
announceDeepIntro()
|
||||||
@@ -517,9 +517,9 @@ async function handleFightEnd(data: any) {
|
|||||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLiveSound() {
|
async function toggleLiveSound() {
|
||||||
liveSoundOn.value = !liveSoundOn.value
|
liveSoundOn.value = !liveSoundOn.value
|
||||||
ensureAudioContext()
|
await ensureAudioContext()
|
||||||
setMasterMute(!liveSoundOn.value)
|
setMasterMute(!liveSoundOn.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +829,7 @@ function stopAutoBattle() {
|
|||||||
<Transition name="announce">
|
<Transition name="announce">
|
||||||
<div v-if="liveAnnouncementVisible"
|
<div v-if="liveAnnouncementVisible"
|
||||||
class="absolute inset-0 flex items-center justify-center pointer-events-none z-20">
|
class="absolute inset-0 flex items-center justify-center pointer-events-none z-20">
|
||||||
<p class="font-funky text-5xl sm:text-7xl tracking-widest uppercase announce-text"
|
<p class="font-funky text-2xl sm:text-5xl lg:text-7xl tracking-widest uppercase announce-text"
|
||||||
:style="{ color: liveAnnouncementColor, textShadow: `0 0 20px ${liveAnnouncementColor}, 0 0 40px ${liveAnnouncementColor}` }">
|
:style="{ color: liveAnnouncementColor, textShadow: `0 0 20px ${liveAnnouncementColor}, 0 0 40px ${liveAnnouncementColor}` }">
|
||||||
{{ liveAnnouncement }}
|
{{ liveAnnouncement }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user