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:
co-authored by
Claude Opus 4.6
parent
c5f6c6bbf9
commit
4307a02d25
@@ -265,25 +265,34 @@ if (typeof speechSynthesis !== 'undefined') {
|
|||||||
loadVoices()
|
loadVoices()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _speechQueueDepth = 0
|
||||||
|
|
||||||
function speak(text: string, profileName: string, cancelPrevious: boolean = false, _echo: boolean = false) {
|
function speak(text: string, profileName: string, cancelPrevious: boolean = false, _echo: boolean = false) {
|
||||||
if (typeof speechSynthesis === 'undefined') return
|
if (typeof speechSynthesis === 'undefined') return
|
||||||
if (masterMuted) return
|
if (masterMuted) return
|
||||||
if (!voicesLoaded) loadVoices()
|
if (!voicesLoaded) loadVoices()
|
||||||
// Always cancel queued speech — only the most recent line matters
|
// Only flush if queue is getting deep — allows voices to overlap naturally
|
||||||
// This prevents stale voiceovers from playing seconds after their visual moment
|
if (cancelPrevious || speechSynthesis.pending && speechSynthesis.speaking) {
|
||||||
speechSynthesis.cancel()
|
// 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 profile = voiceProfiles[profileName] || voiceProfiles.announcer
|
||||||
const utter = new SpeechSynthesisUtterance(text)
|
const utter = new SpeechSynthesisUtterance(text)
|
||||||
if (profile.voice) utter.voice = profile.voice
|
if (profile.voice) utter.voice = profile.voice
|
||||||
utter.pitch = profile.pitch
|
utter.pitch = profile.pitch
|
||||||
utter.rate = profile.rate
|
utter.rate = profile.rate
|
||||||
utter.volume = profile.volume
|
utter.volume = profile.volume
|
||||||
|
_speechQueueDepth++
|
||||||
|
utter.onend = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
|
||||||
|
utter.onerror = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
|
||||||
speechSynthesis.speak(utter)
|
speechSynthesis.speak(utter)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopAllAudio() {
|
export function stopAllAudio() {
|
||||||
stopMusic()
|
stopMusic()
|
||||||
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
|
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
|
||||||
|
_speechQueueDepth = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public voice functions
|
// Public voice functions
|
||||||
@@ -292,13 +301,15 @@ export function announce(text: string, pitch?: number, rate?: number) {
|
|||||||
if (pitch !== undefined || rate !== undefined) {
|
if (pitch !== undefined || rate !== undefined) {
|
||||||
if (typeof speechSynthesis === 'undefined') return
|
if (typeof speechSynthesis === 'undefined') return
|
||||||
if (!voicesLoaded) loadVoices()
|
if (!voicesLoaded) loadVoices()
|
||||||
speechSynthesis.cancel() // cancel previous to stay in sync with visuals
|
|
||||||
const utter = new SpeechSynthesisUtterance(text)
|
const utter = new SpeechSynthesisUtterance(text)
|
||||||
const profile = voiceProfiles.announcer
|
const profile = voiceProfiles.announcer
|
||||||
if (profile.voice) utter.voice = profile.voice
|
if (profile.voice) utter.voice = profile.voice
|
||||||
utter.pitch = pitch ?? 1.0
|
utter.pitch = pitch ?? 1.0
|
||||||
utter.rate = rate ?? 0.8
|
utter.rate = rate ?? 0.8
|
||||||
utter.volume = 1.0
|
utter.volume = 1.0
|
||||||
|
_speechQueueDepth++
|
||||||
|
utter.onend = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
|
||||||
|
utter.onerror = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) }
|
||||||
speechSynthesis.speak(utter)
|
speechSynthesis.speak(utter)
|
||||||
} else {
|
} else {
|
||||||
speak(text, 'announcer')
|
speak(text, 'announcer')
|
||||||
|
|||||||
@@ -187,7 +187,13 @@ async function pollForChallenge() {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (data.pending) {
|
if (data.pending) {
|
||||||
if (roundCooldown.value > 0) return
|
if (roundCooldown.value > 0) {
|
||||||
|
// Buffer for when cooldown ends
|
||||||
|
if (!pendingChallengeData.value || (data.roundNumber && data.roundNumber > (pendingChallengeData.value.data.roundNumber || 0))) {
|
||||||
|
pendingChallengeData.value = { data, receivedAt: Date.now() }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!humanChallenge.value || humanChallenge.value.roundNumber !== data.roundNumber) {
|
if (!humanChallenge.value || humanChallenge.value.roundNumber !== data.roundNumber) {
|
||||||
applyChallenge(data)
|
applyChallenge(data)
|
||||||
}
|
}
|
||||||
@@ -206,7 +212,13 @@ function startTimer() {
|
|||||||
humanTimer.value--
|
humanTimer.value--
|
||||||
if (humanTimer.value <= 0) {
|
if (humanTimer.value <= 0) {
|
||||||
if (timerHandle) clearInterval(timerHandle)
|
if (timerHandle) clearInterval(timerHandle)
|
||||||
if (!humanSubmitted.value) humanSubmitted.value = true
|
if (!humanSubmitted.value) {
|
||||||
|
// Auto-submit a random choice on timeout
|
||||||
|
if (humanChoices.value.length > 0 && !humanAnswer.value.trim()) {
|
||||||
|
humanAnswer.value = humanChoices.value[Math.floor(Math.random() * humanChoices.value.length)]
|
||||||
|
}
|
||||||
|
submitHumanAnswer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@@ -618,14 +630,14 @@ async function matchmake(botId: string) {
|
|||||||
|
|
||||||
function onReplayDone() {
|
function onReplayDone() {
|
||||||
replayDone.value = true
|
replayDone.value = true
|
||||||
if (autoBattle.value && myBotId.value) {
|
if (autoBattle.value && myBotId.value && !isHumanFight.value) {
|
||||||
autoBattleCount.value++
|
autoBattleCount.value++
|
||||||
fightAgain(myBotId.value)
|
fightAgain(myBotId.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAutoBattle() {
|
function startAutoBattle() {
|
||||||
if (!myBotId.value) return
|
if (!myBotId.value || isHumanFight.value) return
|
||||||
autoBattle.value = true
|
autoBattle.value = true
|
||||||
autoBattleCount.value = 0
|
autoBattleCount.value = 0
|
||||||
fightAgain(myBotId.value)
|
fightAgain(myBotId.value)
|
||||||
|
|||||||
+736
-205
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user