fix: human fight timing, creative timer, TTS reliability, add sweary/vibe narrations
- Fix invisible characters in human mode: init live scene BEFORE starting challenge polling so entrance plays before first question appears - Cap creative writing timer to 10s for multiple choice (just tapping buttons) - Fix TTS reliability: precache priority phrases (Round 1-7, Fight!, K.O.) all at once instead of in slow batches; prevent duplicate precache runs - Add 10 vibe-coded narrations (~20% chance): "I was vibe coded into existence" - Add 15 sweary narrations (~30% chance): raw unhinged fight commentary - Add sweary draw and retro narrations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3ba05a66b4
commit
a2416bbe19
@@ -25,7 +25,10 @@ export function useHumanChallenge(
|
||||
|
||||
function applyChallenge(data: any, receivedAt?: number) {
|
||||
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
||||
const remaining = Math.max(1000, (data.timeoutMs || 8000) - elapsed)
|
||||
// Cap timeout for multiple choice — you're just tapping a button, not typing
|
||||
const hasChoices = data.choices && data.choices.length > 0
|
||||
const baseTimeout = hasChoices ? Math.min(data.timeoutMs || 8000, 10000) : (data.timeoutMs || 8000)
|
||||
const remaining = Math.max(1000, baseTimeout - elapsed)
|
||||
humanChallenge.value = {
|
||||
type: data.type,
|
||||
label: data.label,
|
||||
|
||||
@@ -325,12 +325,30 @@ export async function initKokoro(onProgress?: (pct: number) => void): Promise<vo
|
||||
}
|
||||
}
|
||||
|
||||
// Preload all static audio files into cache on startup (parallel fetch, no worker needed)
|
||||
// Preload all static audio files into cache on startup (no worker needed)
|
||||
let _precacheRunning = false
|
||||
let _precacheDone = false
|
||||
async function _precacheCommon() {
|
||||
if (_precacheRunning || _precacheDone) return
|
||||
_precacheRunning = true
|
||||
const entries = Object.entries(STATIC_AUDIO)
|
||||
// Load in batches of 6 to avoid hammering the network
|
||||
for (let i = 0; i < entries.length; i += 6) {
|
||||
const batch = entries.slice(i, i + 6)
|
||||
// Priority: load Round 1-7, Fight!, K.O., Finish it, Flawless, Devastating, Critical FIRST (all at once)
|
||||
const priorityIds = new Set(['round_1','round_2','round_3','round_4','round_5','round_6','round_7','fight','ko','finish_it','flawless_victory','devastating','critical_hit'])
|
||||
const priority = entries.filter(([, url]) => {
|
||||
const id = url.split('/').pop()?.replace('.wav', '') || ''
|
||||
return priorityIds.has(id)
|
||||
})
|
||||
const rest = entries.filter(e => !priority.includes(e))
|
||||
// Load priority batch all at once (13 small wav files)
|
||||
await Promise.all(priority.map(async ([key, url]) => {
|
||||
try {
|
||||
const buf = await _loadStaticAudio(url)
|
||||
if (buf) audioCache.set(key, { buf, lastAccess: Date.now() })
|
||||
} catch { /* non-critical */ }
|
||||
}))
|
||||
// Load the rest in batches of 8
|
||||
for (let i = 0; i < rest.length; i += 8) {
|
||||
const batch = rest.slice(i, i + 8)
|
||||
await Promise.all(batch.map(async ([key, url]) => {
|
||||
try {
|
||||
const buf = await _loadStaticAudio(url)
|
||||
@@ -338,6 +356,8 @@ async function _precacheCommon() {
|
||||
} catch { /* non-critical */ }
|
||||
}))
|
||||
}
|
||||
_precacheDone = true
|
||||
_precacheRunning = false
|
||||
}
|
||||
|
||||
function _cacheKey(text: string, profile: string): string {
|
||||
|
||||
@@ -430,6 +430,10 @@ watch(liveFightData, async (val) => {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
await initLiveScene()
|
||||
// For human fights, start challenge polling after scene is ready
|
||||
if (isHumanFight.value) {
|
||||
startHumanPolling()
|
||||
}
|
||||
const queued = pendingSSEEvents.value.splice(0)
|
||||
for (const evt of queued) {
|
||||
if (evt.type === 'round_end') await handleRoundEnd(evt.data)
|
||||
@@ -465,8 +469,18 @@ onMounted(async () => {
|
||||
if (isLive.value) {
|
||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
||||
if (isHumanFight.value) {
|
||||
startHumanPolling()
|
||||
wireSSE()
|
||||
// Init scene first, then start challenge polling — ensures entrance
|
||||
// plays before the first question appears (characters invisible otherwise)
|
||||
if (liveFightData.value) {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
await initLiveScene()
|
||||
}
|
||||
// Only start polling after scene is ready (or watcher will start it when liveFightData arrives)
|
||||
if (liveSceneReady.value) {
|
||||
startHumanPolling()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -188,7 +188,9 @@ async function pollForChallenge() {
|
||||
showTrashTalk.value = false
|
||||
feedback.value = null
|
||||
phase.value = 'challenge'
|
||||
startTimer(data.remainingMs)
|
||||
// Cap timer for multiple choice — just tapping a button, not typing
|
||||
const timerMs = (data.choices?.length > 0) ? Math.min(data.remainingMs, 10000) : data.remainingMs
|
||||
startTimer(timerMs)
|
||||
await nextTick()
|
||||
if (!hasChoices.value) answerInput.value?.focus()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user