characters invisible on human vs bot fight entrance fix
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
import { ref, type Ref } from 'vue'
|
import { ref, type Ref } from 'vue'
|
||||||
|
|
||||||
|
// Buffer to ensure client timer finishes BEFORE server timer
|
||||||
|
// (accounts for SSE delivery delay + network latency)
|
||||||
|
const CLIENT_TIMER_BUFFER_MS = 1500
|
||||||
|
|
||||||
export function useHumanChallenge(
|
export function useHumanChallenge(
|
||||||
fightId: Ref<string>,
|
fightId: Ref<string>,
|
||||||
myBotId: Ref<string | null>,
|
myBotId: Ref<string | null>,
|
||||||
@@ -18,6 +22,7 @@ export function useHumanChallenge(
|
|||||||
const humanChoices = ref<string[]>([])
|
const humanChoices = ref<string[]>([])
|
||||||
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)
|
||||||
|
|
||||||
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
|
||||||
@@ -25,10 +30,11 @@ export function useHumanChallenge(
|
|||||||
|
|
||||||
function applyChallenge(data: any, receivedAt?: number) {
|
function applyChallenge(data: any, receivedAt?: number) {
|
||||||
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
||||||
// Cap timeout for multiple choice — you're just tapping a button, not typing
|
// Use server-computed remainingMs when available (from polling), otherwise derive from timeoutMs
|
||||||
const hasChoices = data.choices && data.choices.length > 0
|
const hasChoices = data.choices && data.choices.length > 0
|
||||||
const baseTimeout = hasChoices ? Math.min(data.timeoutMs || 8000, 10000) : (data.timeoutMs || 8000)
|
const rawTimeout = data.remainingMs || (hasChoices ? Math.min(data.timeoutMs || 8000, 10000) : (data.timeoutMs || 8000))
|
||||||
const remaining = Math.max(1000, baseTimeout - elapsed)
|
// Subtract buffer so client always submits before server times out
|
||||||
|
const remaining = Math.max(1000, rawTimeout - elapsed - CLIENT_TIMER_BUFFER_MS)
|
||||||
humanChallenge.value = {
|
humanChallenge.value = {
|
||||||
type: data.type,
|
type: data.type,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
@@ -51,7 +57,6 @@ export function useHumanChallenge(
|
|||||||
if (humanTimer.value <= 0) {
|
if (humanTimer.value <= 0) {
|
||||||
if (timerHandle) clearInterval(timerHandle)
|
if (timerHandle) clearInterval(timerHandle)
|
||||||
if (!humanSubmitted.value) {
|
if (!humanSubmitted.value) {
|
||||||
// BUG-9: Submit empty timeout instead of random choice
|
|
||||||
humanSubmitted.value = true
|
humanSubmitted.value = true
|
||||||
void submitTimeout()
|
void submitTimeout()
|
||||||
}
|
}
|
||||||
@@ -113,7 +118,8 @@ export function useHumanChallenge(
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (data.pending) {
|
if (data.pending) {
|
||||||
if (roundCooldown.value > 0) {
|
// Queue if entrance is playing or cooldown active
|
||||||
|
if (entrancePlaying.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() }
|
||||||
}
|
}
|
||||||
@@ -179,10 +185,22 @@ export function useHumanChallenge(
|
|||||||
humanChoices.value = []
|
humanChoices.value = []
|
||||||
roundCooldown.value = 0
|
roundCooldown.value = 0
|
||||||
pendingChallengeData.value = null
|
pendingChallengeData.value = null
|
||||||
|
entrancePlaying.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mark entrance as playing — queues any challenges that arrive during animation */
|
||||||
|
function setEntrancePlaying(playing: boolean) {
|
||||||
|
entrancePlaying.value = playing
|
||||||
|
// When entrance finishes, flush any queued challenge
|
||||||
|
if (!playing && pendingChallengeData.value) {
|
||||||
|
applyChallenge(pendingChallengeData.value.data, pendingChallengeData.value.receivedAt)
|
||||||
|
pendingChallengeData.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSSEChallenge(sseData: any) {
|
function handleSSEChallenge(sseData: any) {
|
||||||
if (roundCooldown.value > 0) {
|
// Queue challenges during entrance animation or round cooldown
|
||||||
|
if (entrancePlaying.value || roundCooldown.value > 0) {
|
||||||
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
||||||
} else {
|
} else {
|
||||||
applyChallenge(sseData)
|
applyChallenge(sseData)
|
||||||
@@ -197,6 +215,7 @@ export function useHumanChallenge(
|
|||||||
humanChoices,
|
humanChoices,
|
||||||
roundCooldown,
|
roundCooldown,
|
||||||
pendingChallengeData,
|
pendingChallengeData,
|
||||||
|
entrancePlaying,
|
||||||
applyChallenge,
|
applyChallenge,
|
||||||
submitHumanAnswer,
|
submitHumanAnswer,
|
||||||
submitChoice,
|
submitChoice,
|
||||||
@@ -206,6 +225,7 @@ export function useHumanChallenge(
|
|||||||
startCooldown,
|
startCooldown,
|
||||||
clearChallenge,
|
clearChallenge,
|
||||||
resetState,
|
resetState,
|
||||||
|
setEntrancePlaying,
|
||||||
handleSSEChallenge,
|
handleSSEChallenge,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const {
|
|||||||
roundCooldown, pendingChallengeData,
|
roundCooldown, pendingChallengeData,
|
||||||
applyChallenge, submitChoice, startHumanPolling, stopHumanPolling,
|
applyChallenge, submitChoice, startHumanPolling, stopHumanPolling,
|
||||||
startCooldown, clearChallenge, resetState: resetChallengeState, handleSSEChallenge,
|
startCooldown, clearChallenge, resetState: resetChallengeState, handleSSEChallenge,
|
||||||
|
setEntrancePlaying,
|
||||||
} = challenge
|
} = challenge
|
||||||
|
|
||||||
// Live scene state
|
// Live scene state
|
||||||
@@ -155,6 +156,10 @@ async function initLiveScene() {
|
|||||||
await ensureAudioContext()
|
await ensureAudioContext()
|
||||||
liveScene.startMusic()
|
liveScene.startMusic()
|
||||||
}
|
}
|
||||||
|
// Let Kaplay register game objects before entrance (prevents invisible fighters)
|
||||||
|
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))
|
||||||
|
// Block challenges during entrance so the animation plays fully
|
||||||
|
setEntrancePlaying(true)
|
||||||
announceDeepIntro()
|
announceDeepIntro()
|
||||||
try {
|
try {
|
||||||
await liveScene.playEntrance()
|
await liveScene.playEntrance()
|
||||||
@@ -163,6 +168,7 @@ async function initLiveScene() {
|
|||||||
}
|
}
|
||||||
// Safety: ensure fighters are visible after entrance (prevents invisible characters on mobile)
|
// Safety: ensure fighters are visible after entrance (prevents invisible characters on mobile)
|
||||||
liveScene._resetPositions()
|
liveScene._resetPositions()
|
||||||
|
setEntrancePlaying(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
liveLogItems.value.push(
|
liveLogItems.value.push(
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ const trashTalk = ref('')
|
|||||||
const remainingSeconds = ref(45)
|
const remainingSeconds = ref(45)
|
||||||
const showTrashTalk = ref(false)
|
const showTrashTalk = ref(false)
|
||||||
|
|
||||||
|
// Buffer to ensure client timer finishes BEFORE server timer
|
||||||
|
// (accounts for polling delivery delay + network latency)
|
||||||
|
const CLIENT_TIMER_BUFFER_MS = 1500
|
||||||
|
|
||||||
// Feedback state
|
// Feedback state
|
||||||
const feedback = ref<'correct' | 'wrong' | null>(null)
|
const feedback = ref<'correct' | 'wrong' | null>(null)
|
||||||
const feedbackTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
const feedbackTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||||
@@ -197,7 +201,9 @@ async function pollForChallenge() {
|
|||||||
feedback.value = null
|
feedback.value = null
|
||||||
phase.value = 'challenge'
|
phase.value = 'challenge'
|
||||||
// Cap timer for multiple choice — just tapping a button, not typing
|
// Cap timer for multiple choice — just tapping a button, not typing
|
||||||
const timerMs = (data.choices?.length > 0) ? Math.min(data.remainingMs, 10000) : data.remainingMs
|
// Subtract buffer so client always submits before server times out
|
||||||
|
const rawMs = (data.choices?.length > 0) ? Math.min(data.remainingMs, 10000) : data.remainingMs
|
||||||
|
const timerMs = Math.max(1000, rawMs - CLIENT_TIMER_BUFFER_MS)
|
||||||
startTimer(timerMs)
|
startTimer(timerMs)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (!hasChoices.value) answerInput.value?.focus()
|
if (!hasChoices.value) answerInput.value?.focus()
|
||||||
|
|||||||
Reference in New Issue
Block a user