fix: mobile fight playback — prevent sprite loading hangs, TTS stalls, unlock audio from gesture
- Unified speakAsync/speakAsyncWithRate into _speakAsyncCore with: - 500ms startup check: bail immediately if speech won't start (mobile/no gesture) - 8s safety timeout (down from 15s) to prevent blocking - iOS-safe keepalive: only do Chrome pause/resume workaround on desktop Chrome - Immediate bail when no voices loaded - Sprite loading: 5s timeout per sprite prevents mobile hangs from stuck Image decodes - Scene creation: 10s timeout in FightViewer so overlay/voice flow continues even if canvas fails on mobile - playRound: bail gracefully if fighter sprites missing instead of crashing - Global audio unlock: first touch/click on site unlocks AudioContext + SpeechSynthesis - Practice button pre-unlocks audio while still in user gesture context Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9287fd1783
commit
6561ef5d15
@@ -142,13 +142,26 @@ async function initScene() {
|
||||
}
|
||||
canvasRef.value = newCanvas
|
||||
|
||||
scene = await createFightScene({
|
||||
canvas: newCanvas,
|
||||
botA: { name: props.fight.botA.name, seed: props.fight.botA.avatarSeed || props.fight.botA.name, tier: props.fight.botA.tier, archetype: props.fight.botA.archetype, customization: props.fight.botA.customization as any, wins: props.fight.botA.wins, losses: props.fight.botA.losses },
|
||||
botB: { name: props.fight.botB.name, seed: props.fight.botB.avatarSeed || props.fight.botB.name, tier: props.fight.botB.tier, archetype: props.fight.botB.archetype, customization: props.fight.botB.customization as any, wins: props.fight.botB.wins, losses: props.fight.botB.losses },
|
||||
arena: props.fight.arena,
|
||||
})
|
||||
sceneReady.value = true
|
||||
// Wrap scene creation in try/catch + timeout so a mobile sprite loading failure
|
||||
// or hang never blocks the overlay/voice/round flow
|
||||
try {
|
||||
const scenePromise = createFightScene({
|
||||
canvas: newCanvas,
|
||||
botA: { name: props.fight.botA.name, seed: props.fight.botA.avatarSeed || props.fight.botA.name, tier: props.fight.botA.tier, archetype: props.fight.botA.archetype, customization: props.fight.botA.customization as any, wins: props.fight.botA.wins, losses: props.fight.botA.losses },
|
||||
botB: { name: props.fight.botB.name, seed: props.fight.botB.avatarSeed || props.fight.botB.name, tier: props.fight.botB.tier, archetype: props.fight.botB.archetype, customization: props.fight.botB.customization as any, wins: props.fight.botB.wins, losses: props.fight.botB.losses },
|
||||
arena: props.fight.arena,
|
||||
})
|
||||
const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), 10_000))
|
||||
const result = await Promise.race([scenePromise, timeout])
|
||||
if (result) {
|
||||
scene = result
|
||||
sceneReady.value = true
|
||||
} else {
|
||||
console.warn('[FightViewer] Scene creation timed out (10s) — continuing without canvas')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[FightViewer] Scene creation failed — continuing without canvas:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const challengeLabel = (type: string) => {
|
||||
|
||||
Reference in New Issue
Block a user