refactor: extract FightPage polling and challenge composables
Split FightPage.vue into useFightPolling (SSE, polling, reconnect) and useHumanChallenge (timer, submission, cooldown) composables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7c8e404cf1
commit
eb4d52438d
+115
-420
@@ -3,6 +3,8 @@ import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import FightViewer from '../components/FightViewer.vue'
|
||||
import { useNostr } from '../composables/useNostr'
|
||||
import { useFightPolling } from '../composables/useFightPolling'
|
||||
import { useHumanChallenge } from '../composables/useHumanChallenge'
|
||||
import { createFightScene, type FightSceneController } from '../game/FightScene'
|
||||
import {
|
||||
fanfareRound,
|
||||
@@ -15,102 +17,40 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { bot: myBot, isLoggedIn } = useNostr()
|
||||
const fightId = ref(route.params.fightId as string)
|
||||
const fight = ref<any>(null)
|
||||
const isLoading = ref(true)
|
||||
const isRequeueing = ref(false)
|
||||
const isLive = ref(false)
|
||||
const liveRounds = ref(0)
|
||||
const fightError = ref('')
|
||||
const replayDone = ref(false)
|
||||
const autoBattle = ref(false)
|
||||
const autoBattleCount = ref(0)
|
||||
let pollHandle: ReturnType<typeof setInterval> | null = null
|
||||
let pollCount = 0
|
||||
|
||||
// Watch route param changes (e.g. "watch another fight" navigating to a new fight)
|
||||
watch(() => route.params.fightId, async (newId) => {
|
||||
if (!newId || newId === fightId.value) return
|
||||
// Clean up current state
|
||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||
stopHumanPolling()
|
||||
disconnectSSE()
|
||||
stopAllAudio()
|
||||
if (liveScene) { liveScene.destroy(); liveScene = null }
|
||||
liveSceneReady.value = false
|
||||
|
||||
// Reset all state
|
||||
fightId.value = newId as string
|
||||
fight.value = null
|
||||
isLoading.value = true
|
||||
isLive.value = false
|
||||
liveRounds.value = 0
|
||||
fightError.value = ''
|
||||
replayDone.value = false
|
||||
autoBattle.value = false
|
||||
autoBattleCount.value = 0
|
||||
liveFightData.value = null
|
||||
liveLogItems.value = []
|
||||
liveHpA.value = 100
|
||||
liveHpB.value = 100
|
||||
liveCurrentRound.value = 0
|
||||
humanChallenge.value = null
|
||||
humanSubmitted.value = false
|
||||
humanFightDone.value = false
|
||||
humanFightResult.value = null
|
||||
pendingChallengeData.value = null
|
||||
|
||||
// Re-load
|
||||
const status = await loadFight()
|
||||
isLoading.value = false
|
||||
if (status === null || status !== 'finished') {
|
||||
isLive.value = true
|
||||
}
|
||||
if (isLive.value) {
|
||||
startPolling()
|
||||
connectSSE()
|
||||
if (isHumanFight.value) {
|
||||
startHumanPolling()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
if (liveFightData.value) await initLiveScene()
|
||||
} else {
|
||||
// Bot-vs-bot spectating: init live scene for real-time viewing
|
||||
if (!liveFightData.value) await loadFight()
|
||||
if (!liveFightData.value) {
|
||||
// loadFight sets liveFightData only for human fights; set it for spectating too
|
||||
const res = await fetch(`/api/fights/${fightId.value}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.botA && data.botB) liveFightData.value = data
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
if (liveFightData.value) await initLiveScene()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Human fight detection
|
||||
const isHumanFight = computed(() => !!myBot.value?.isHuman || myBot.value?.archetype === 'human')
|
||||
|
||||
// Human challenge state
|
||||
const humanChallenge = ref<{
|
||||
type: string
|
||||
label: string
|
||||
prompt: string
|
||||
roundNumber: number
|
||||
remainingMs: number
|
||||
scoring: string
|
||||
} | null>(null)
|
||||
const humanAnswer = ref('')
|
||||
const humanTimer = ref(5)
|
||||
const humanSubmitted = ref(false)
|
||||
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
||||
let timerHandle: ReturnType<typeof setInterval> | null = null
|
||||
// Composables
|
||||
const polling = useFightPolling(fightId)
|
||||
const {
|
||||
isLive, isLoading, liveRounds, fightError, fight, liveFightData,
|
||||
spectatorCount, currentChallengeInfo, pendingSSEEvents,
|
||||
loadFight, startPolling, stopPolling, connectSSE, disconnectSSE, cleanup: cleanupPolling,
|
||||
} = polling
|
||||
|
||||
// Live human fight scene state
|
||||
const liveFightData = ref<any>(null)
|
||||
const myBotId = computed(() => {
|
||||
if (!isLoggedIn.value || !myBot.value) return null
|
||||
const f = fight.value || liveFightData.value
|
||||
if (!f) return null
|
||||
if (myBot.value.id === f.botA?.id) return f.botA.id
|
||||
if (myBot.value.id === f.botB?.id) return f.botB.id
|
||||
return null
|
||||
})
|
||||
|
||||
const challenge = useHumanChallenge(fightId, myBotId)
|
||||
const {
|
||||
humanChallenge, humanAnswer, humanTimer, humanSubmitted, humanChoices,
|
||||
roundCooldown, pendingChallengeData,
|
||||
applyChallenge, submitChoice, startHumanPolling, stopHumanPolling,
|
||||
startCooldown, clearChallenge, resetState: resetChallengeState, handleSSEChallenge,
|
||||
} = challenge
|
||||
|
||||
// Live scene state
|
||||
const liveCanvas = ref<HTMLCanvasElement>()
|
||||
const liveLogEl = ref<HTMLElement>()
|
||||
let liveScene: FightSceneController | null = null
|
||||
@@ -118,15 +58,15 @@ const liveLogItems = ref<{ type: string; round: number; text: string; color: str
|
||||
const liveHpA = ref(100)
|
||||
const liveHpB = ref(100)
|
||||
const liveCurrentRound = ref(0)
|
||||
const roundCooldown = ref(0)
|
||||
let cooldownHandle: ReturnType<typeof setInterval> | null = null
|
||||
let eventSource: EventSource | null = null
|
||||
const liveSceneReady = ref(false)
|
||||
const liveSoundOn = ref(true)
|
||||
const liveAnnouncement = ref('')
|
||||
const liveAnnouncementColor = ref('#ffffff')
|
||||
const liveAnnouncementVisible = ref(false)
|
||||
const spectatorCount = ref(0)
|
||||
const humanFightDone = ref(false)
|
||||
const humanFightResult = ref<{ winnerId: string; winnerName: string; isPerfect: boolean } | null>(null)
|
||||
|
||||
// Emoji reactions
|
||||
const liveFloatingEmojis = ref<{ id: number; emoji: string; x: number }[]>([])
|
||||
let liveEmojiCounter = 0
|
||||
const EMOJI_MAP: Record<string, string> = {
|
||||
@@ -141,44 +81,6 @@ function spawnLiveReactionEmoji(key: string) {
|
||||
liveFloatingEmojis.value = liveFloatingEmojis.value.filter(e => e.id !== id)
|
||||
}, 2000)
|
||||
}
|
||||
const currentChallengeInfo = ref<{ type: string; label: string } | null>(null)
|
||||
const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null)
|
||||
const pendingSSEEvents = ref<{ type: string; data: any }[]>([])
|
||||
const humanChoices = ref<string[]>([])
|
||||
const humanFightDone = ref(false)
|
||||
const humanFightResult = ref<{ winnerId: string; winnerName: string; isPerfect: boolean } | null>(null)
|
||||
|
||||
function applyChallenge(data: any, receivedAt?: number) {
|
||||
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
||||
const remaining = Math.max(1000, (data.timeoutMs || 8000) - elapsed)
|
||||
humanChallenge.value = {
|
||||
type: data.type,
|
||||
label: data.label,
|
||||
prompt: data.prompt,
|
||||
roundNumber: data.roundNumber || data.round,
|
||||
remainingMs: remaining,
|
||||
scoring: data.scoring,
|
||||
}
|
||||
humanChoices.value = data.choices || []
|
||||
humanAnswer.value = ''
|
||||
humanSubmitted.value = false
|
||||
humanTimer.value = Math.ceil(remaining / 1000)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
function submitChoice(choice: string) {
|
||||
humanAnswer.value = choice
|
||||
submitHumanAnswer()
|
||||
}
|
||||
|
||||
const myBotId = computed(() => {
|
||||
if (!isLoggedIn.value || !myBot.value) return null
|
||||
const f = fight.value || liveFightData.value
|
||||
if (!f) return null
|
||||
if (myBot.value.id === f.botA?.id) return f.botA.id
|
||||
if (myBot.value.id === f.botB?.id) return f.botB.id
|
||||
return null
|
||||
})
|
||||
|
||||
const showOverlay = computed(() => replayDone.value && !isRequeueing.value && !autoBattle.value)
|
||||
|
||||
@@ -212,129 +114,6 @@ const challengeLabel = (type: string) => {
|
||||
|
||||
const tierClass = (t: number) => `tier-${t}`
|
||||
|
||||
// --- Load fight data ---
|
||||
async function loadFight(): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`/api/fights/${fightId.value}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
liveRounds.value = data.rounds?.length || 0
|
||||
|
||||
if (isHumanFight.value && !liveFightData.value && data.botA && data.botB) {
|
||||
liveFightData.value = data
|
||||
}
|
||||
|
||||
if (data.status === 'finished') {
|
||||
fight.value = data
|
||||
}
|
||||
return data.status
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[FightPage] loadFight failed:', err)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
pollCount = 0
|
||||
pollHandle = setInterval(async () => {
|
||||
pollCount++
|
||||
const s = await loadFight()
|
||||
if (s === 'finished') {
|
||||
// SSE fight_end handles transition for all live fights with SSE connected
|
||||
if (!eventSource) {
|
||||
// Fallback: no SSE connected, transition directly
|
||||
isLive.value = false
|
||||
disconnectSSE()
|
||||
stopHumanPolling()
|
||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||
}
|
||||
} else if (pollCount > 120) {
|
||||
isLive.value = false
|
||||
fightError.value = 'Fight took too long. Try refreshing.'
|
||||
stopHumanPolling()
|
||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// --- Human challenge polling ---
|
||||
function startHumanPolling() {
|
||||
if (!myBot.value) return
|
||||
pollForChallenge()
|
||||
humanPollHandle = setInterval(pollForChallenge, 400)
|
||||
}
|
||||
|
||||
function stopHumanPolling() {
|
||||
if (humanPollHandle) { clearInterval(humanPollHandle); humanPollHandle = null }
|
||||
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
|
||||
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
||||
}
|
||||
|
||||
async function pollForChallenge() {
|
||||
if (!myBot.value) return
|
||||
try {
|
||||
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBot.value.id}`)
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
|
||||
if (data.pending) {
|
||||
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) {
|
||||
applyChallenge(data)
|
||||
}
|
||||
} else if (data.fightStatus === 'finished') {
|
||||
humanChallenge.value = null
|
||||
humanSubmitted.value = false
|
||||
} else if (humanSubmitted.value) {
|
||||
humanChallenge.value = null
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[FightPage] pollForChallenge failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (timerHandle) clearInterval(timerHandle)
|
||||
timerHandle = setInterval(() => {
|
||||
humanTimer.value--
|
||||
if (humanTimer.value <= 0) {
|
||||
if (timerHandle) clearInterval(timerHandle)
|
||||
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)
|
||||
}
|
||||
|
||||
async function submitHumanAnswer() {
|
||||
if (!myBot.value || !humanChallenge.value || humanSubmitted.value) return
|
||||
if (!humanAnswer.value.trim()) return
|
||||
|
||||
humanSubmitted.value = true
|
||||
if (timerHandle) clearInterval(timerHandle)
|
||||
|
||||
try {
|
||||
await fetch(`/api/fights/${fightId.value}/respond/${myBot.value.id}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ answer: humanAnswer.value.trim() }),
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('[FightPage] submitHumanAnswer failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Live scene management ---
|
||||
async function initLiveScene() {
|
||||
const data = liveFightData.value
|
||||
@@ -344,7 +123,6 @@ async function initLiveScene() {
|
||||
|
||||
const container = liveCanvas.value.parentElement
|
||||
if (container) {
|
||||
// Wait for container to have dimensions
|
||||
for (let i = 0; i < 10 && (!container.clientWidth || !container.clientHeight); i++) {
|
||||
await new Promise(r => requestAnimationFrame(r))
|
||||
}
|
||||
@@ -388,55 +166,15 @@ async function initLiveScene() {
|
||||
scrollLiveLog()
|
||||
}
|
||||
|
||||
// --- SSE connection ---
|
||||
const sseListeners: { event: string; handler: EventListener }[] = []
|
||||
|
||||
function addSSEListener(es: EventSource, event: string, handler: (e: MessageEvent) => void) {
|
||||
es.addEventListener(event, handler as EventListener)
|
||||
sseListeners.push({ event, handler: handler as EventListener })
|
||||
}
|
||||
|
||||
function connectSSE() {
|
||||
eventSource = new EventSource(`/api/fights/${fightId.value}/stream`)
|
||||
|
||||
addSSEListener(eventSource, 'spectator_count', (e) => {
|
||||
try {
|
||||
if (!e.data) return
|
||||
const data = JSON.parse(e.data)
|
||||
if (typeof data.count === 'number') spectatorCount.value = data.count
|
||||
} catch (err) {
|
||||
console.warn('[SSE] spectator_count parse error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
addSSEListener(eventSource, 'ping', (e) => {
|
||||
try {
|
||||
if (!e.data) return
|
||||
const data = JSON.parse(e.data)
|
||||
if (typeof data.spectators === 'number') spectatorCount.value = data.spectators
|
||||
} catch (err) {
|
||||
console.warn('[SSE] ping parse error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
addSSEListener(eventSource, 'reaction', (e) => {
|
||||
try {
|
||||
if (!e.data) return
|
||||
const data = JSON.parse(e.data)
|
||||
// --- SSE event wiring ---
|
||||
function wireSSE() {
|
||||
connectSSE({
|
||||
onReaction(data) {
|
||||
if (typeof data.emoji === 'string' && data.counts) {
|
||||
spawnLiveReactionEmoji(data.emoji)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[SSE] reaction parse error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
addSSEListener(eventSource, 'round_start', (e) => {
|
||||
try {
|
||||
if (!e.data) return
|
||||
const data = JSON.parse(e.data)
|
||||
if (!data.challenge || !data.round) return
|
||||
currentChallengeInfo.value = { type: data.challenge.type, label: data.challenge.label }
|
||||
},
|
||||
onRoundStart(data) {
|
||||
liveLogItems.value.push({
|
||||
type: 'challenge',
|
||||
round: data.round,
|
||||
@@ -444,86 +182,29 @@ function connectSSE() {
|
||||
color: 'neon-green',
|
||||
})
|
||||
scrollLiveLog()
|
||||
} catch (err) {
|
||||
console.warn('[SSE] round_start parse error:', err)
|
||||
}
|
||||
})
|
||||
|
||||
addSSEListener(eventSource, 'human_challenge', async (e) => {
|
||||
try {
|
||||
if (!e.data) return
|
||||
const sseData = JSON.parse(e.data)
|
||||
},
|
||||
async onHumanChallenge(sseData) {
|
||||
if (myBot.value) {
|
||||
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBot.value.id}`)
|
||||
if (res.ok) {
|
||||
const fullData = await res.json()
|
||||
if (fullData.pending) {
|
||||
if (roundCooldown.value > 0) {
|
||||
pendingChallengeData.value = { data: fullData, receivedAt: Date.now() }
|
||||
} else {
|
||||
applyChallenge(fullData)
|
||||
try {
|
||||
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBot.value.id}`)
|
||||
if (res.ok) {
|
||||
const fullData = await res.json()
|
||||
if (fullData.pending) {
|
||||
handleSSEChallenge(fullData)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
if (roundCooldown.value > 0) {
|
||||
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
||||
} else {
|
||||
applyChallenge(sseData)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[FightPage] SSE human_challenge failed:', err)
|
||||
}
|
||||
})
|
||||
|
||||
addSSEListener(eventSource, 'round_end', (e) => {
|
||||
try {
|
||||
if (!e.data) return
|
||||
const data = JSON.parse(e.data)
|
||||
if (data.spectators !== undefined) spectatorCount.value = data.spectators
|
||||
handleSSEChallenge(sseData)
|
||||
},
|
||||
onRoundEnd(data) {
|
||||
handleRoundEnd(data).catch(() => {})
|
||||
} catch (err) {
|
||||
console.warn('[FightPage] SSE round_end failed:', err)
|
||||
}
|
||||
})
|
||||
|
||||
addSSEListener(eventSource, 'fight_end', (e) => {
|
||||
try {
|
||||
if (!e.data) return
|
||||
const data = JSON.parse(e.data)
|
||||
if (data.spectators !== undefined) spectatorCount.value = data.spectators
|
||||
},
|
||||
onFightEnd(data) {
|
||||
handleFightEnd(data).catch(() => {})
|
||||
} catch (err) {
|
||||
console.warn('[FightPage] SSE fight_end failed:', err)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
let sseRetries = 0
|
||||
eventSource.onerror = () => {
|
||||
sseRetries++
|
||||
if (sseRetries > 5 && eventSource) {
|
||||
eventSource.close()
|
||||
eventSource = null
|
||||
const delay = Math.min(1000 * 2 ** (sseRetries - 5), 10000)
|
||||
setTimeout(() => {
|
||||
if (isLive.value && !eventSource) connectSSE()
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
eventSource.onopen = () => { sseRetries = 0 }
|
||||
}
|
||||
|
||||
function disconnectSSE() {
|
||||
if (eventSource) {
|
||||
for (const { event, handler } of sseListeners) {
|
||||
eventSource.removeEventListener(event, handler)
|
||||
}
|
||||
sseListeners.length = 0
|
||||
eventSource.close()
|
||||
eventSource = null
|
||||
}
|
||||
spectatorCount.value = 0
|
||||
}
|
||||
|
||||
async function showLiveOverlay(text: string, color: string, duration: number) {
|
||||
@@ -547,11 +228,7 @@ async function handleRoundEnd(data: any) {
|
||||
const hp = data.hp
|
||||
|
||||
liveCurrentRound.value = round
|
||||
|
||||
// Clear challenge state
|
||||
humanChallenge.value = null
|
||||
humanSubmitted.value = false
|
||||
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
|
||||
clearChallenge()
|
||||
|
||||
// Update HP (server 0-200, display 0-100)
|
||||
liveHpA.value = Math.round((hp.a / 200) * 100)
|
||||
@@ -562,7 +239,6 @@ async function handleRoundEnd(data: any) {
|
||||
const bWon = result.winnerId === fd.botB.id
|
||||
const isCritical = Math.abs((result.botAScore || 0) - (result.botBScore || 0)) > 4
|
||||
|
||||
// Use challenge info from round_start SSE, fall back to generic
|
||||
const cLabel = currentChallengeInfo.value?.label || challengeLabel(currentChallengeInfo.value?.type || '')
|
||||
|
||||
liveLogItems.value.push(
|
||||
@@ -584,7 +260,6 @@ async function handleRoundEnd(data: any) {
|
||||
)
|
||||
scrollLiveLog()
|
||||
|
||||
// Play round animation
|
||||
if (liveScene) {
|
||||
fanfareRound(round)
|
||||
if (result.botAResponse) liveScene.showSpeechBubble('a', result.botAResponse.slice(0, 60), 3)
|
||||
@@ -616,20 +291,7 @@ async function handleRoundEnd(data: any) {
|
||||
}
|
||||
|
||||
currentChallengeInfo.value = null
|
||||
|
||||
// Countdown before next round
|
||||
roundCooldown.value = 3
|
||||
cooldownHandle = setInterval(() => {
|
||||
roundCooldown.value--
|
||||
if (roundCooldown.value <= 0) {
|
||||
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
||||
// Apply any buffered challenge from SSE
|
||||
if (pendingChallengeData.value) {
|
||||
applyChallenge(pendingChallengeData.value.data, pendingChallengeData.value.receivedAt)
|
||||
pendingChallengeData.value = null
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
startCooldown(3)
|
||||
}
|
||||
|
||||
async function handleFightEnd(data: any) {
|
||||
@@ -639,19 +301,14 @@ async function handleFightEnd(data: any) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear challenge
|
||||
humanChallenge.value = null
|
||||
humanSubmitted.value = false
|
||||
clearChallenge()
|
||||
roundCooldown.value = 0
|
||||
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
||||
|
||||
// Final HP
|
||||
if (data.winnerId) {
|
||||
if (data.winnerId === fd.botA.id) liveHpB.value = 0
|
||||
else liveHpA.value = 0
|
||||
}
|
||||
|
||||
// KO sequence
|
||||
if (liveScene && data.winnerId) {
|
||||
const winningSide = (data.winnerId === fd.botA.id ? 'a' : 'b') as 'a' | 'b'
|
||||
|
||||
@@ -676,11 +333,9 @@ async function handleFightEnd(data: any) {
|
||||
liveScene.stopMusic()
|
||||
}
|
||||
|
||||
// For human fights: stay in the live view and show post-fight buttons directly
|
||||
if (isHumanFight.value) {
|
||||
humanFightDone.value = true
|
||||
humanFightResult.value = { winnerId: data.winnerId, winnerName: data.winnerName, isPerfect: data.isPerfect }
|
||||
// Add result to battle log
|
||||
const myWon = myBot.value && data.winnerId === myBot.value.id
|
||||
liveLogItems.value.push(
|
||||
{ type: 'divider', round: 0, text: '', color: '' },
|
||||
@@ -694,21 +349,18 @@ async function handleFightEnd(data: any) {
|
||||
scrollLiveLog()
|
||||
disconnectSSE()
|
||||
stopHumanPolling()
|
||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||
// Keep isLive true so we stay in the human fight view
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up live scene (bot fights only)
|
||||
if (liveScene) { liveScene.destroy(); liveScene = null }
|
||||
liveSceneReady.value = false
|
||||
|
||||
// Load full fight data for replay
|
||||
await loadFight()
|
||||
isLive.value = false
|
||||
disconnectSSE()
|
||||
stopHumanPolling()
|
||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
async function toggleLiveSound() {
|
||||
@@ -717,13 +369,61 @@ async function toggleLiveSound() {
|
||||
setMasterMute(!liveSoundOn.value)
|
||||
}
|
||||
|
||||
// Watch for liveFightData becoming available (may arrive via polling after mount)
|
||||
// Watch route param changes
|
||||
watch(() => route.params.fightId, async (newId) => {
|
||||
if (!newId || newId === fightId.value) return
|
||||
cleanupPolling()
|
||||
stopHumanPolling()
|
||||
stopAllAudio()
|
||||
if (liveScene) { liveScene.destroy(); liveScene = null }
|
||||
liveSceneReady.value = false
|
||||
|
||||
fightId.value = newId as string
|
||||
fight.value = null
|
||||
isLoading.value = true
|
||||
isLive.value = false
|
||||
liveRounds.value = 0
|
||||
fightError.value = ''
|
||||
replayDone.value = false
|
||||
autoBattle.value = false
|
||||
autoBattleCount.value = 0
|
||||
liveFightData.value = null
|
||||
liveLogItems.value = []
|
||||
liveHpA.value = 100
|
||||
liveHpB.value = 100
|
||||
liveCurrentRound.value = 0
|
||||
humanFightDone.value = false
|
||||
humanFightResult.value = null
|
||||
resetChallengeState()
|
||||
|
||||
const status = await loadFight()
|
||||
isLoading.value = false
|
||||
if (status === null || status !== 'finished') {
|
||||
isLive.value = true
|
||||
}
|
||||
if (isLive.value) {
|
||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
||||
wireSSE()
|
||||
if (isHumanFight.value) {
|
||||
startHumanPolling()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
if (liveFightData.value) await initLiveScene()
|
||||
} else {
|
||||
if (!liveFightData.value) await loadFight()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
if (liveFightData.value) await initLiveScene()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for liveFightData becoming available
|
||||
watch(liveFightData, async (val) => {
|
||||
if (val && !liveSceneReady.value && !liveScene) {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
await initLiveScene()
|
||||
// Drain any SSE events that arrived before liveFightData was ready
|
||||
const queued = pendingSSEEvents.value.splice(0)
|
||||
for (const evt of queued) {
|
||||
if (evt.type === 'round_end') await handleRoundEnd(evt.data)
|
||||
@@ -742,19 +442,18 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
if (isLive.value) {
|
||||
startPolling()
|
||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
||||
if (isHumanFight.value) {
|
||||
startHumanPolling()
|
||||
connectSSE()
|
||||
wireSSE()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
_pageDestroyed = true
|
||||
if (pollHandle) clearInterval(pollHandle)
|
||||
cleanupPolling()
|
||||
stopHumanPolling()
|
||||
disconnectSSE()
|
||||
stopAllAudio()
|
||||
if (liveScene) { liveScene.destroy(); liveScene = null }
|
||||
autoBattle.value = false
|
||||
@@ -765,12 +464,9 @@ async function fightAgain(botId: string) {
|
||||
if (isRequeueing.value) return
|
||||
isRequeueing.value = true
|
||||
replayDone.value = false
|
||||
humanChallenge.value = null
|
||||
humanSubmitted.value = false
|
||||
pendingChallengeData.value = null
|
||||
resetChallengeState()
|
||||
humanFightDone.value = false
|
||||
humanFightResult.value = null
|
||||
roundCooldown.value = 0
|
||||
if (liveScene) { liveScene.destroy(); liveScene = null }
|
||||
liveSceneReady.value = false
|
||||
liveFightData.value = null
|
||||
@@ -790,11 +486,10 @@ async function fightAgain(botId: string) {
|
||||
liveRounds.value = 0
|
||||
fightError.value = ''
|
||||
window.history.replaceState({}, '', `/arena/${data.fightId}`)
|
||||
startPolling()
|
||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
||||
if (isHumanFight.value) {
|
||||
startHumanPolling()
|
||||
connectSSE()
|
||||
// Wait for fight data to arrive so we can init scene
|
||||
wireSSE()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await loadFight()
|
||||
if (liveFightData.value) break
|
||||
@@ -828,7 +523,7 @@ async function matchmake(botId: string) {
|
||||
liveRounds.value = 0
|
||||
fightError.value = ''
|
||||
window.history.replaceState({}, '', `/arena/${data.fightId}`)
|
||||
startPolling()
|
||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
||||
} else {
|
||||
fightError.value = `Matchmaking failed (${res.status})`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user