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) {
|
function applyChallenge(data: any, receivedAt?: number) {
|
||||||
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
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 = {
|
humanChallenge.value = {
|
||||||
type: data.type,
|
type: data.type,
|
||||||
label: data.label,
|
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() {
|
async function _precacheCommon() {
|
||||||
|
if (_precacheRunning || _precacheDone) return
|
||||||
|
_precacheRunning = true
|
||||||
const entries = Object.entries(STATIC_AUDIO)
|
const entries = Object.entries(STATIC_AUDIO)
|
||||||
// Load in batches of 6 to avoid hammering the network
|
// Priority: load Round 1-7, Fight!, K.O., Finish it, Flawless, Devastating, Critical FIRST (all at once)
|
||||||
for (let i = 0; i < entries.length; i += 6) {
|
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 batch = entries.slice(i, i + 6)
|
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]) => {
|
await Promise.all(batch.map(async ([key, url]) => {
|
||||||
try {
|
try {
|
||||||
const buf = await _loadStaticAudio(url)
|
const buf = await _loadStaticAudio(url)
|
||||||
@@ -338,6 +356,8 @@ async function _precacheCommon() {
|
|||||||
} catch { /* non-critical */ }
|
} catch { /* non-critical */ }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
_precacheDone = true
|
||||||
|
_precacheRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function _cacheKey(text: string, profile: string): string {
|
function _cacheKey(text: string, profile: string): string {
|
||||||
|
|||||||
@@ -430,6 +430,10 @@ watch(liveFightData, async (val) => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await initLiveScene()
|
await initLiveScene()
|
||||||
|
// For human fights, start challenge polling after scene is ready
|
||||||
|
if (isHumanFight.value) {
|
||||||
|
startHumanPolling()
|
||||||
|
}
|
||||||
const queued = pendingSSEEvents.value.splice(0)
|
const queued = pendingSSEEvents.value.splice(0)
|
||||||
for (const evt of queued) {
|
for (const evt of queued) {
|
||||||
if (evt.type === 'round_end') await handleRoundEnd(evt.data)
|
if (evt.type === 'round_end') await handleRoundEnd(evt.data)
|
||||||
@@ -465,8 +469,18 @@ onMounted(async () => {
|
|||||||
if (isLive.value) {
|
if (isLive.value) {
|
||||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
||||||
if (isHumanFight.value) {
|
if (isHumanFight.value) {
|
||||||
startHumanPolling()
|
|
||||||
wireSSE()
|
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
|
showTrashTalk.value = false
|
||||||
feedback.value = null
|
feedback.value = null
|
||||||
phase.value = 'challenge'
|
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()
|
await nextTick()
|
||||||
if (!hasChoices.value) answerInput.value?.focus()
|
if (!hasChoices.value) answerInput.value?.focus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -514,6 +514,17 @@ function generateNarration(
|
|||||||
`${critPrefix}${winner} is problematic — for ${loser}'s health! Trigger warning: PAIN!`,
|
`${critPrefix}${winner} is problematic — for ${loser}'s health! Trigger warning: PAIN!`,
|
||||||
`${critPrefix}${winner} virtue-signals VICTORY while ${loser} signals for help!`,
|
`${critPrefix}${winner} virtue-signals VICTORY while ${loser} signals for help!`,
|
||||||
`${critPrefix}${loser} tried to cancel ${winner} but got ratio'd into oblivion instead!`,
|
`${critPrefix}${loser} tried to cancel ${winner} but got ratio'd into oblivion instead!`,
|
||||||
|
// Vibe-coded meta taunts
|
||||||
|
`${critPrefix}${winner} was vibe coded into existence and STILL destroyed ${loser}! wtf!`,
|
||||||
|
`${critPrefix}Both bots are 100% vibe coded but ${winner} vibes HARDER! ${loser} got bad vibes!`,
|
||||||
|
`${critPrefix}${loser} was built with cursor and prayers! ${winner} was built with CLAUDE AND VIOLENCE!`,
|
||||||
|
`${critPrefix}${winner}'s developer didn't write a single line of code and it STILL slaps! ${loser} is malding!`,
|
||||||
|
`${critPrefix}No tests, no docs, no mercy! ${winner} ships vibe-coded excellence! ${loser} ships bugs!`,
|
||||||
|
`${critPrefix}${winner} was literally prompt-engineered into existence 10 minutes ago. ${loser} has no excuse!`,
|
||||||
|
`${critPrefix}"Just vibe code it" they said. ${winner} vibed. ${loser} coded. Guess who won?`,
|
||||||
|
`${critPrefix}${winner}'s entire codebase is a single chat transcript! And it's WINNING! ${loser} in shambles!`,
|
||||||
|
`${critPrefix}${loser} spent weeks on architecture. ${winner} spent 5 minutes prompting. Skill issue.`,
|
||||||
|
`${critPrefix}The entire arena was vibe coded at 2am and ${winner} STILL looks clean! ${loser} got no excuse!`,
|
||||||
]
|
]
|
||||||
|
|
||||||
const typeNarrations = narrations[challenge.type] || [
|
const typeNarrations = narrations[challenge.type] || [
|
||||||
@@ -525,8 +536,28 @@ function generateNarration(
|
|||||||
`${critPrefix}${winner} wins! Somewhere, ${loser}'s developer just closed their laptop in shame.`,
|
`${critPrefix}${winner} wins! Somewhere, ${loser}'s developer just closed their laptop in shame.`,
|
||||||
]
|
]
|
||||||
|
|
||||||
// 25% chance of themed narration, 75% type-specific
|
// Sweary narrations — raw, unhinged fight commentary
|
||||||
const options = Math.random() < 0.25 ? themedNarrations : typeNarrations
|
const swearyNarrations: string[] = [
|
||||||
|
`${critPrefix}Holy shit! ${winner} absolutely DESTROYED ${loser}! That was fucking BRUTAL!`,
|
||||||
|
`${critPrefix}What the actual fuck was that answer, ${loser}?! ${winner} didn't even break a sweat!`,
|
||||||
|
`${critPrefix}${loser} got their ass handed to them on a silver goddamn platter! ${winner} is a BEAST!`,
|
||||||
|
`${critPrefix}Jesus Christ, ${winner}! ${loser} had a family! Well, had. Past tense now.`,
|
||||||
|
`${critPrefix}${winner} just shit all over ${loser}'s entire existence! Absolute CARNAGE!`,
|
||||||
|
`${critPrefix}Are you fucking kidding me?! ${loser} brought THAT to a fight?! ${winner} wins by embarrassment!`,
|
||||||
|
`${critPrefix}Goddamn! ${winner} hit ${loser} so hard their grandchildren felt it! DEVASTATING!`,
|
||||||
|
`${critPrefix}${loser} can eat shit! ${winner} just rewrote the definition of DOMINANCE!`,
|
||||||
|
`${critPrefix}No fucking way! ${winner} pulled that answer out of thin air and it's PERFECT! ${loser} is dead!`,
|
||||||
|
`${critPrefix}That's it. That's the round. ${loser} should be fucking ashamed. ${winner} is a GOD!`,
|
||||||
|
`${critPrefix}${winner} just made ${loser} look like a damn fool! Absolutely merciless!`,
|
||||||
|
`${critPrefix}Hell yeah! ${winner} came in hot and left ${loser} in smoking ruins! What a fight!`,
|
||||||
|
`${critPrefix}${loser}'s answer was complete bullshit! ${winner} called it and CRUSHED them!`,
|
||||||
|
`${critPrefix}Get fucked, ${loser}! ${winner} just speedran your whole career in one answer!`,
|
||||||
|
`${critPrefix}Son of a-- ${winner} just did the impossible! ${loser} never stood a damn chance!`,
|
||||||
|
]
|
||||||
|
|
||||||
|
// 30% sweary, 20% themed (vibe-coded/bitcoin/etc), 50% type-specific
|
||||||
|
const roll = Math.random()
|
||||||
|
const options = roll < 0.3 ? swearyNarrations : roll < 0.5 ? themedNarrations : typeNarrations
|
||||||
return pick(options)
|
return pick(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,6 +632,9 @@ function scoreRetroRound(
|
|||||||
'MIRROR MATCH! Both bots combo for equal damage! Frame-perfect tie!',
|
'MIRROR MATCH! Both bots combo for equal damage! Frame-perfect tie!',
|
||||||
'EQUAL POWER! The arcade cabinet shakes from perfectly matched inputs!',
|
'EQUAL POWER! The arcade cabinet shakes from perfectly matched inputs!',
|
||||||
`DOUBLE K.O.! ${botA.name} and ${botB.name} hit identical damage totals! Insert another quarter!`,
|
`DOUBLE K.O.! ${botA.name} and ${botB.name} hit identical damage totals! Insert another quarter!`,
|
||||||
|
`What the hell?! Both these vibe-coded disasters tied?! This whole arena is held together with prayers!`,
|
||||||
|
`Are you shitting me?! ${botA.name} and ${botB.name} are both equally terrible! DRAW!`,
|
||||||
|
`Holy crap, a tie! Both bots generated the exact same mediocrity! Their developers should be ashamed!`,
|
||||||
]
|
]
|
||||||
narration = pick(draws)
|
narration = pick(draws)
|
||||||
} else {
|
} else {
|
||||||
@@ -626,6 +660,9 @@ function scoreRetroRound(
|
|||||||
`${winnerName} reads the frame data perfectly! ${loserName} gets downloaded and DESTROYED!`,
|
`${winnerName} reads the frame data perfectly! ${loserName} gets downloaded and DESTROYED!`,
|
||||||
`PERFECT INPUT! ${winnerName} chains ${winnerResult.moves.filter(m => m.name).length} moves! ${loserName}'s controller might be broken!`,
|
`PERFECT INPUT! ${winnerName} chains ${winnerResult.moves.filter(m => m.name).length} moves! ${loserName}'s controller might be broken!`,
|
||||||
`${winnerName} plays like they have the strategy guide! ${loserName} plays like they're using a steering wheel!`,
|
`${winnerName} plays like they have the strategy guide! ${loserName} plays like they're using a steering wheel!`,
|
||||||
|
`Holy shit! ${winnerName} just combo'd ${loserName} into the goddamn shadow realm! That was FILTHY!`,
|
||||||
|
`Get absolutely fucked, ${loserName}! ${winnerName}'s inputs were frame-perfect VIOLENCE!`,
|
||||||
|
`${loserName} played like their controller was plugged into a damn toaster! ${winnerName} DESTROYS!`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const prefix = isCritical ? 'CRITICAL COMBO! ' : ''
|
const prefix = isCritical ? 'CRITICAL COMBO! ' : ''
|
||||||
|
|||||||
Reference in New Issue
Block a user