feat: speech overflow fix, 60+ voice profiles, post-fight audio cleanup

- Change speak() default to cancelPrevious=true so voices don't queue endlessly
- Add stopAllAudio() export and wire into FightViewer unmount + post-fight
- Flush speech queue at fight end with delayed cancel for clean cutoff
- Expand from 30 to 60+ voice profiles (robots, accents, game, characters)
- Add speech bubbles showing bot responses during rounds
- Wire announceFinishHim/Fatality/FlawlessVictory/Devastating to not cancel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 11:42:32 +00:00
co-authored by Claude Opus 4.6
parent d7e2ed8b9d
commit c87bff01a8
6 changed files with 819 additions and 202 deletions
+37 -15
View File
@@ -4,9 +4,9 @@ import { createFightScene, type FightSceneController } from '../game/FightScene'
import {
fanfareRound, fanfareFight, announce, announceDeep, announceFast,
announceDeepIntro, announceRandomHype, announceRoundHype,
announceFinishHim, announceFatality, announceFlawlessVictory,
announceFinishHim, announceFlawlessVictory,
sfxCrowdCheer, sfxCrowdGasp, sfxCrowdOoh, sfxApplause, sfxDrumRoll,
setMusicIntensity,
setMusicIntensity, stopAllAudio,
} from '../game/sounds'
interface Round {
@@ -38,6 +38,7 @@ interface FightData {
}
const props = defineProps<{ fight: FightData; autoplay?: boolean }>()
const emit = defineEmits<{ 'replay-done': [] }>()
const canvasRef = ref<HTMLCanvasElement>()
const logEl = ref<HTMLElement>()
@@ -65,8 +66,7 @@ const logItems = ref<{ type: string; round: number; text: string; color: string
onMounted(async () => {
if (props.autoplay) {
// Fresh fight — start replay immediately instead of showing static result
await initScene()
// Fresh fight — replay() will call initScene(), no need to double-init
await nextTick()
replay()
} else {
@@ -85,6 +85,7 @@ function mapHp(hp: number, winnerId: string | null, botId: string | undefined):
}
onUnmounted(() => {
stopAllAudio()
if (scene) { scene.destroy(); scene = null }
})
@@ -240,17 +241,30 @@ async function replay() {
const aWon = round.winnerId === props.fight.botA!.id
const bWon = round.winnerId === props.fight.botB!.id
await scene!.playRound({
round: round.roundNumber,
challengeType: round.challengeType,
winnerId: round.winnerId,
botAId: props.fight.botA!.id,
botBId: props.fight.botB!.id,
narration: round.narration || '',
isCritical,
botAScore: round.botAScore || 0,
botBScore: round.botBScore || 0,
})
try {
await scene!.playRound({
round: round.roundNumber,
challengeType: round.challengeType,
winnerId: round.winnerId,
botAId: props.fight.botA!.id,
botBId: props.fight.botB!.id,
narration: round.narration || '',
isCritical,
botAScore: round.botAScore || 0,
botBScore: round.botBScore || 0,
})
} catch (err) {
console.error(`[FightViewer] playRound ${round.roundNumber} error:`, err)
}
// Speech bubbles showing bot responses
if (scene) {
if (round.botAResponse) scene.showSpeechBubble('a', round.botAResponse.slice(0, 100), 2.8)
// Stagger bot B slightly so they don't pop in at the exact same time
setTimeout(() => {
if (scene && round.botBResponse) scene.showSpeechBubble('b', round.botBResponse.slice(0, 100), 2.5)
}, 300)
}
// Hit text overlay
const hitWords = isCritical
@@ -323,6 +337,8 @@ async function replay() {
displayHpB.value = mapHp(props.fight.botBHp, props.fight.winnerId, props.fight.botB?.id)
scene?.stopMusic()
// Flush any queued speech from mid-fight so only final lines play
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
// Always end with a dramatic death/KO sequence
if (scene) {
@@ -370,7 +386,13 @@ async function replay() {
}
}
// Kill any remaining queued speech — fight is over
setTimeout(() => {
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
}, 3000)
isReplaying.value = false
emit('replay-done')
}
</script>