human fight sequence fix
This commit is contained in:
@@ -23,6 +23,7 @@ export function useHumanChallenge(
|
|||||||
const roundCooldown = ref(0)
|
const roundCooldown = ref(0)
|
||||||
const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null)
|
const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null)
|
||||||
const entrancePlaying = ref(false)
|
const entrancePlaying = ref(false)
|
||||||
|
const animatingRound = ref(false)
|
||||||
|
|
||||||
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
||||||
let timerHandle: ReturnType<typeof setInterval> | null = null
|
let timerHandle: ReturnType<typeof setInterval> | null = null
|
||||||
@@ -118,8 +119,8 @@ export function useHumanChallenge(
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (data.pending) {
|
if (data.pending) {
|
||||||
// Queue if entrance is playing or cooldown active
|
// Queue if entrance is playing, round animating, or cooldown active
|
||||||
if (entrancePlaying.value || roundCooldown.value > 0) {
|
if (entrancePlaying.value || animatingRound.value || roundCooldown.value > 0) {
|
||||||
if (!pendingChallengeData.value || (data.roundNumber && data.roundNumber > (pendingChallengeData.value.data.roundNumber || 0))) {
|
if (!pendingChallengeData.value || (data.roundNumber && data.roundNumber > (pendingChallengeData.value.data.roundNumber || 0))) {
|
||||||
pendingChallengeData.value = { data, receivedAt: Date.now() }
|
pendingChallengeData.value = { data, receivedAt: Date.now() }
|
||||||
}
|
}
|
||||||
@@ -159,6 +160,8 @@ export function useHumanChallenge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startCooldown(seconds: number) {
|
function startCooldown(seconds: number) {
|
||||||
|
// Clear any existing cooldown interval to prevent double-decrement
|
||||||
|
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
||||||
roundCooldown.value = seconds
|
roundCooldown.value = seconds
|
||||||
cooldownHandle = setInterval(() => {
|
cooldownHandle = setInterval(() => {
|
||||||
roundCooldown.value--
|
roundCooldown.value--
|
||||||
@@ -178,6 +181,11 @@ export function useHumanChallenge(
|
|||||||
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
|
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopCooldown() {
|
||||||
|
roundCooldown.value = 0
|
||||||
|
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
||||||
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
humanChallenge.value = null
|
humanChallenge.value = null
|
||||||
humanAnswer.value = ''
|
humanAnswer.value = ''
|
||||||
@@ -186,6 +194,7 @@ export function useHumanChallenge(
|
|||||||
roundCooldown.value = 0
|
roundCooldown.value = 0
|
||||||
pendingChallengeData.value = null
|
pendingChallengeData.value = null
|
||||||
entrancePlaying.value = false
|
entrancePlaying.value = false
|
||||||
|
animatingRound.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark entrance as playing — queues any challenges that arrive during animation */
|
/** Mark entrance as playing — queues any challenges that arrive during animation */
|
||||||
@@ -199,8 +208,8 @@ export function useHumanChallenge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSSEChallenge(sseData: any) {
|
function handleSSEChallenge(sseData: any) {
|
||||||
// Queue challenges during entrance animation or round cooldown
|
// Queue challenges during entrance animation, round animation, or cooldown
|
||||||
if (entrancePlaying.value || roundCooldown.value > 0) {
|
if (entrancePlaying.value || animatingRound.value || roundCooldown.value > 0) {
|
||||||
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
||||||
} else {
|
} else {
|
||||||
applyChallenge(sseData)
|
applyChallenge(sseData)
|
||||||
@@ -216,6 +225,7 @@ export function useHumanChallenge(
|
|||||||
roundCooldown,
|
roundCooldown,
|
||||||
pendingChallengeData,
|
pendingChallengeData,
|
||||||
entrancePlaying,
|
entrancePlaying,
|
||||||
|
animatingRound,
|
||||||
applyChallenge,
|
applyChallenge,
|
||||||
submitHumanAnswer,
|
submitHumanAnswer,
|
||||||
submitChoice,
|
submitChoice,
|
||||||
@@ -224,6 +234,7 @@ export function useHumanChallenge(
|
|||||||
stopHumanPolling,
|
stopHumanPolling,
|
||||||
startCooldown,
|
startCooldown,
|
||||||
clearChallenge,
|
clearChallenge,
|
||||||
|
stopCooldown,
|
||||||
resetState,
|
resetState,
|
||||||
setEntrancePlaying,
|
setEntrancePlaying,
|
||||||
handleSSEChallenge,
|
handleSSEChallenge,
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ const {
|
|||||||
humanChallenge, humanAnswer, humanTimer, humanSubmitted, humanChoices,
|
humanChallenge, humanAnswer, humanTimer, humanSubmitted, humanChoices,
|
||||||
roundCooldown, pendingChallengeData,
|
roundCooldown, pendingChallengeData,
|
||||||
applyChallenge, submitChoice, startHumanPolling, stopHumanPolling,
|
applyChallenge, submitChoice, startHumanPolling, stopHumanPolling,
|
||||||
startCooldown, clearChallenge, resetState: resetChallengeState, handleSSEChallenge,
|
startCooldown, clearChallenge, stopCooldown, resetState: resetChallengeState, handleSSEChallenge,
|
||||||
setEntrancePlaying,
|
setEntrancePlaying, animatingRound,
|
||||||
} = challenge
|
} = challenge
|
||||||
|
|
||||||
// Live scene state
|
// Live scene state
|
||||||
@@ -187,6 +187,10 @@ async function initLiveScene() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- SSE event wiring ---
|
// --- SSE event wiring ---
|
||||||
|
// Track in-progress round animation so fight_end can wait for it
|
||||||
|
let _roundEndPromise: Promise<void> | null = null
|
||||||
|
let _fightEnding = false
|
||||||
|
|
||||||
function wireSSE() {
|
function wireSSE() {
|
||||||
connectSSE({
|
connectSSE({
|
||||||
onReaction(data) {
|
onReaction(data) {
|
||||||
@@ -208,9 +212,12 @@ function wireSSE() {
|
|||||||
handleSSEChallenge(sseData)
|
handleSSEChallenge(sseData)
|
||||||
},
|
},
|
||||||
onRoundEnd(data) {
|
onRoundEnd(data) {
|
||||||
handleRoundEnd(data).catch(() => {})
|
if (_fightEnding) return // Don't start new round animations after fight is ending
|
||||||
|
_roundEndPromise = handleRoundEnd(data).catch(() => {})
|
||||||
},
|
},
|
||||||
onFightEnd(data) {
|
onFightEnd(data) {
|
||||||
|
if (_fightEnding) return // Deduplicate (only process first fight_end)
|
||||||
|
_fightEnding = true
|
||||||
handleFightEnd(data).catch(() => {})
|
handleFightEnd(data).catch(() => {})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -238,6 +245,8 @@ async function handleRoundEnd(data: any) {
|
|||||||
|
|
||||||
liveCurrentRound.value = round
|
liveCurrentRound.value = round
|
||||||
clearChallenge()
|
clearChallenge()
|
||||||
|
// Block new challenges during animation (SSE/polling will queue them)
|
||||||
|
animatingRound.value = true
|
||||||
|
|
||||||
// Update HP (server 0-200, display 0-100)
|
// Update HP (server 0-200, display 0-100)
|
||||||
liveHpA.value = Math.round((hp.a / 200) * 100)
|
liveHpA.value = Math.round((hp.a / 200) * 100)
|
||||||
@@ -304,7 +313,11 @@ async function handleRoundEnd(data: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentChallengeInfo.value = null
|
currentChallengeInfo.value = null
|
||||||
startCooldown(3)
|
animatingRound.value = false
|
||||||
|
// Don't start cooldown if fight is ending (fight_end handler takes over)
|
||||||
|
if (!_fightEnding) {
|
||||||
|
startCooldown(3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFightEnd(data: any) {
|
async function handleFightEnd(data: any) {
|
||||||
@@ -314,8 +327,15 @@ async function handleFightEnd(data: any) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for any in-progress round animation to finish before KO sequence
|
||||||
|
if (_roundEndPromise) {
|
||||||
|
await _roundEndPromise
|
||||||
|
_roundEndPromise = null
|
||||||
|
}
|
||||||
|
|
||||||
clearChallenge()
|
clearChallenge()
|
||||||
roundCooldown.value = 0
|
animatingRound.value = false
|
||||||
|
stopCooldown()
|
||||||
|
|
||||||
if (data.winnerId) {
|
if (data.winnerId) {
|
||||||
if (data.winnerId === fd.botA.id) liveHpB.value = 0
|
if (data.winnerId === fd.botA.id) liveHpB.value = 0
|
||||||
@@ -400,6 +420,8 @@ async function toggleLiveSound() {
|
|||||||
// Watch route param changes
|
// Watch route param changes
|
||||||
watch(() => route.params.fightId, async (newId) => {
|
watch(() => route.params.fightId, async (newId) => {
|
||||||
if (!newId || newId === fightId.value) return
|
if (!newId || newId === fightId.value) return
|
||||||
|
_fightEnding = false
|
||||||
|
_roundEndPromise = null
|
||||||
cleanupPolling()
|
cleanupPolling()
|
||||||
stopHumanPolling()
|
stopHumanPolling()
|
||||||
stopAllAudio()
|
stopAllAudio()
|
||||||
@@ -464,6 +486,15 @@ watch(liveFightData, async (val) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Destroy live scene when fight transitions from live to finished (prevents kaplay instance leak)
|
||||||
|
watch(isLive, (live, wasLive) => {
|
||||||
|
if (!live && wasLive && liveScene) {
|
||||||
|
liveScene.destroy()
|
||||||
|
liveScene = null
|
||||||
|
liveSceneReady.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// --- Mount / Unmount ---
|
// --- Mount / Unmount ---
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Auto-unlock AudioContext on first user interaction (critical for mobile TTS)
|
// Auto-unlock AudioContext on first user interaction (critical for mobile TTS)
|
||||||
@@ -524,6 +555,8 @@ async function fightAgain(botId: string) {
|
|||||||
if (isRequeueing.value) return
|
if (isRequeueing.value) return
|
||||||
isRequeueing.value = true
|
isRequeueing.value = true
|
||||||
replayDone.value = false
|
replayDone.value = false
|
||||||
|
_fightEnding = false
|
||||||
|
_roundEndPromise = null
|
||||||
resetChallengeState()
|
resetChallengeState()
|
||||||
humanFightDone.value = false
|
humanFightDone.value = false
|
||||||
humanFightResult.value = null
|
humanFightResult.value = null
|
||||||
@@ -576,6 +609,21 @@ async function matchmake(botId: string) {
|
|||||||
if (isRequeueing.value) return
|
if (isRequeueing.value) return
|
||||||
isRequeueing.value = true
|
isRequeueing.value = true
|
||||||
replayDone.value = false
|
replayDone.value = false
|
||||||
|
_fightEnding = false
|
||||||
|
_roundEndPromise = null
|
||||||
|
resetChallengeState()
|
||||||
|
humanFightDone.value = false
|
||||||
|
humanFightResult.value = null
|
||||||
|
if (liveScene) { liveScene.destroy(); liveScene = null }
|
||||||
|
liveSceneReady.value = false
|
||||||
|
liveFightData.value = null
|
||||||
|
liveLogItems.value = []
|
||||||
|
liveHpA.value = 100
|
||||||
|
liveHpB.value = 100
|
||||||
|
liveCurrentRound.value = 0
|
||||||
|
stopHumanPolling()
|
||||||
|
stopPolling()
|
||||||
|
disconnectSSE()
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/fights/matchmake/${botId}`, { method: 'POST' })
|
const res = await fetch(`/api/fights/matchmake/${botId}`, { method: 'POST' })
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|||||||
@@ -454,14 +454,6 @@ fightsRouter.get('/:id/stream', (c) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const cleanupGlobal = fightEvents.onAll((event) => {
|
|
||||||
if (event.fightId === fightId && event.type === 'fight_end') {
|
|
||||||
void stream.writeSSE({
|
|
||||||
event: 'fight_end',
|
|
||||||
data: JSON.stringify({ ...event.data, spectators: spectatorCounts.get(fightId) || 0 }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -495,7 +487,7 @@ fightsRouter.get('/:id/stream', (c) => {
|
|||||||
spectatorCounts.set(fightId, current - 1)
|
spectatorCounts.set(fightId, current - 1)
|
||||||
}
|
}
|
||||||
cleanup()
|
cleanup()
|
||||||
cleanupGlobal()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user