diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue
index deb165c..86cc9c8 100644
--- a/frontend/src/components/FightViewer.vue
+++ b/frontend/src/components/FightViewer.vue
@@ -451,9 +451,13 @@ async function _doReplay() {
await sleep(insanityMode.value ? 50 : 300)
}
+ // Hide human player's typed answers from replay
+ const replayIsAHuman = props.fight.botA?.archetype === 'human'
+ const replayIsBHuman = props.fight.botB?.archetype === 'human'
+
// 2. Bot A: ensure audio ready, then show log + bubble + mouth + voice all at once
const isCodeRound = isCodeAnswer(round.challengeType, round.botAResponse || '')
- if (round.botAResponse) {
+ if (round.botAResponse && !replayIsAHuman) {
if (doTTS && !isCodeRound) await awaitAnswerReady(props.fight.botA!.name, round.botAResponse)
scene?.stopShowboating('a')
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse || '[NO RESPONSE]'}`, color: 'neon-cyan' })
@@ -473,10 +477,13 @@ async function _doReplay() {
if (!doTTS && !insanityMode.value) await sleep(400)
scene?.stopTalking('a')
scene?.hideSpeechBubble('a')
+ } else if (replayIsAHuman) {
+ scene?.stopShowboating('a')
+ await sleep(insanityMode.value ? 50 : 200)
}
// 3. Bot B: ensure audio ready, then show log + bubble + mouth + voice all at once
- if (round.botBResponse) {
+ if (round.botBResponse && !replayIsBHuman) {
if (doTTS && !isCodeRound) await awaitAnswerReady(props.fight.botB!.name, round.botBResponse)
scene?.stopShowboating('b')
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse || '[NO RESPONSE]'}`, color: 'neon-pink' })
@@ -497,6 +504,9 @@ async function _doReplay() {
if (!doTTS && !insanityMode.value) await sleep(400)
scene?.stopTalking('b')
scene?.hideSpeechBubble('b')
+ } else if (replayIsBHuman) {
+ scene?.stopShowboating('b')
+ await sleep(insanityMode.value ? 50 : 200)
}
// Stop all showboating before fight animation
diff --git a/frontend/src/components/NavBar.vue b/frontend/src/components/NavBar.vue
index de13968..c5fb11d 100644
--- a/frontend/src/components/NavBar.vue
+++ b/frontend/src/components/NavBar.vue
@@ -39,12 +39,12 @@ const bottomNav = [
BOTFIGHTS
- BETA
+ BETA
diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts
index 06ed023..42a1192 100644
--- a/frontend/src/game/FightScene.ts
+++ b/frontend/src/game/FightScene.ts
@@ -1222,7 +1222,11 @@ export async function createFightScene(config: FightSceneConfig) {
stopShowboating(side: 'a' | 'b') { showboatSystem.stopShowboating(side) },
async playEntrance() {
- return entranceSystem.playEntrance()
+ // Timeout entrance to prevent hanging on mobile (max 8s)
+ await Promise.race([
+ entranceSystem.playEntrance(),
+ new Promise(r => setTimeout(r, 8000)),
+ ])
},
// Physical contact delegates
diff --git a/frontend/src/game/fight/entrances.ts b/frontend/src/game/fight/entrances.ts
index fcec22c..b0b94c6 100644
--- a/frontend/src/game/fight/entrances.ts
+++ b/frontend/src/game/fight/entrances.ts
@@ -524,21 +524,30 @@ async function playEntrance() {
while (idxB === idxA && attempts < 5) { idxB = pickTierEntrance(botB.tier); attempts++ }
// Play entrances with slight stagger — creator always gets special entrance
+ // Wrap each in try/catch so one failing doesn't prevent the other
announceDeepIntro()
- if (botA.archetype === 'the_creator') {
- await creatorEntrance(fA, HOME_A, true)
- } else {
- await entrances[idxA](fA, HOME_A, true)
+ try {
+ if (botA.archetype === 'the_creator') {
+ await creatorEntrance(fA, HOME_A, true)
+ } else {
+ await entrances[idxA](fA, HOME_A, true)
+ }
+ } catch (e) {
+ console.warn('[Entrance] Fighter A entrance failed:', e)
}
await k.wait(0.3)
- if (botB.archetype === 'the_creator') {
- await creatorEntrance(fB, HOME_B, false)
- } else {
- await entrances[idxB](fB, HOME_B, false)
+ try {
+ if (botB.archetype === 'the_creator') {
+ await creatorEntrance(fB, HOME_B, false)
+ } else {
+ await entrances[idxB](fB, HOME_B, false)
+ }
+ } catch (e) {
+ console.warn('[Entrance] Fighter B entrance failed:', e)
}
await k.wait(0.2)
- // Safety: ensure both fighters are visible and at home positions
+ // Safety: ALWAYS ensure both fighters are visible and at home positions
fA.pos.x = HOME_A
fA.pos.y = GROUND_Y - 6
fA.opacity = 1
@@ -547,8 +556,8 @@ async function playEntrance() {
fB.opacity = 1
// Both idle at home positions
- fA.play('idle')
- fB.play('idle')
+ try { fA.play('idle') } catch { /* sprite may not have anim */ }
+ try { fB.play('idle') } catch { /* sprite may not have anim */ }
}
return { playEntrance }
diff --git a/frontend/src/pages/FightPage.vue b/frontend/src/pages/FightPage.vue
index 931ae2a..7da2e76 100644
--- a/frontend/src/pages/FightPage.vue
+++ b/frontend/src/pages/FightPage.vue
@@ -248,13 +248,17 @@ async function handleRoundEnd(data: any) {
const cLabel = currentChallengeInfo.value?.label || challengeLabel(currentChallengeInfo.value?.type || '')
+ // Don't show the human player's typed answer in the battle log
+ const isAHuman = myBotId.value === fd.botA.id && isHumanFight.value
+ const isBHuman = myBotId.value === fd.botB.id && isHumanFight.value
+
liveLogItems.value.push(
{ type: 'header', round, text: `ROUND ${round}: ${cLabel}`, color: 'neon-purple' },
)
- if (result.botAResponse) {
+ if (result.botAResponse && !isAHuman) {
liveLogItems.value.push({ type: 'responseA', round, text: `${fd.botA.name}: ${result.botAResponse}`, color: 'neon-cyan' })
}
- if (result.botBResponse) {
+ if (result.botBResponse && !isBHuman) {
liveLogItems.value.push({ type: 'responseB', round, text: `${fd.botB.name}: ${result.botBResponse}`, color: 'neon-pink' })
}
if (result.narration) {
@@ -269,8 +273,8 @@ async function handleRoundEnd(data: any) {
if (liveScene) {
fanfareRound(round)
- if (result.botAResponse) liveScene.showSpeechBubble('a', result.botAResponse, 3)
- if (result.botBResponse) {
+ if (result.botAResponse && !isAHuman) liveScene.showSpeechBubble('a', result.botAResponse, 3)
+ if (result.botBResponse && !isBHuman) {
setTimeout(() => {
if (liveScene && result.botBResponse) liveScene.showSpeechBubble('b', result.botBResponse, 2.5)
}, 300)