fix: server crash from syntax error in challenges.ts, voice overlap, human fight UX

- Remove stray duplicate template block outside TEMPLATES array that caused parse error preventing server startup (fixes /api/queue/status 500)
- Add choices field to Challenge/PromptEntry interfaces for multiple choice support
- Allow voice lines to overlap naturally instead of cancelling previous speech on every call, with safety flush at depth > 3
- Buffer challenges during cooldown in polling path, auto-submit random choice on timeout
- Block auto-battle for human fights, guard replay-done auto-transition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 21:15:42 +00:00
co-authored by Claude Opus 4.6
parent c5f6c6bbf9
commit 4307a02d25
3 changed files with 767 additions and 213 deletions
+15 -4
View File
@@ -265,25 +265,34 @@ if (typeof speechSynthesis !== 'undefined') {
loadVoices()
}
let _speechQueueDepth = 0
function speak(text: string, profileName: string, cancelPrevious: boolean = false, _echo: boolean = false) {
if (typeof speechSynthesis === 'undefined') return
if (masterMuted) return
if (!voicesLoaded) loadVoices()
// Always cancel queued speech — only the most recent line matters
// This prevents stale voiceovers from playing seconds after their visual moment
speechSynthesis.cancel()
// Only flush if queue is getting deep — allows voices to overlap naturally
if (cancelPrevious || speechSynthesis.pending && speechSynthesis.speaking) {
// 2+ queued: drop the queue but let current utterance finish
const queued = _speechQueueDepth
if (queued > 3) speechSynthesis.cancel()
}
const profile = voiceProfiles[profileName] || voiceProfiles.announcer
const utter = new SpeechSynthesisUtterance(text)
if (profile.voice) utter.voice = profile.voice
utter.pitch = profile.pitch
utter.rate = profile.rate
utter.volume = profile.volume
_speechQueueDepth++
utter.onend = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
utter.onerror = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
speechSynthesis.speak(utter)
}
export function stopAllAudio() {
stopMusic()
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
_speechQueueDepth = 0
}
// Public voice functions
@@ -292,13 +301,15 @@ export function announce(text: string, pitch?: number, rate?: number) {
if (pitch !== undefined || rate !== undefined) {
if (typeof speechSynthesis === 'undefined') return
if (!voicesLoaded) loadVoices()
speechSynthesis.cancel() // cancel previous to stay in sync with visuals
const utter = new SpeechSynthesisUtterance(text)
const profile = voiceProfiles.announcer
if (profile.voice) utter.voice = profile.voice
utter.pitch = pitch ?? 1.0
utter.rate = rate ?? 0.8
utter.volume = 1.0
_speechQueueDepth++
utter.onend = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
utter.onerror = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
speechSynthesis.speak(utter)
} else {
speak(text, 'announcer')