From a2416bbe192d78279351464925ce5187a90ef8f7 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 13:50:24 +0000 Subject: [PATCH] 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 --- frontend/src/composables/useHumanChallenge.ts | 5 ++- frontend/src/game/tts.ts | 28 +++++++++++-- frontend/src/pages/FightPage.vue | 16 +++++++- frontend/src/pages/HumanFightPage.vue | 4 +- server/src/engine/scoring.ts | 41 ++++++++++++++++++- 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/frontend/src/composables/useHumanChallenge.ts b/frontend/src/composables/useHumanChallenge.ts index 4a37710..e68d8c9 100644 --- a/frontend/src/composables/useHumanChallenge.ts +++ b/frontend/src/composables/useHumanChallenge.ts @@ -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, diff --git a/frontend/src/game/tts.ts b/frontend/src/game/tts.ts index 2d19e99..4c2eafc 100644 --- a/frontend/src/game/tts.ts +++ b/frontend/src/game/tts.ts @@ -325,12 +325,30 @@ export async function initKokoro(onProgress?: (pct: number) => void): Promise { + 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 { diff --git a/frontend/src/pages/FightPage.vue b/frontend/src/pages/FightPage.vue index 1845004..2002d9f 100644 --- a/frontend/src/pages/FightPage.vue +++ b/frontend/src/pages/FightPage.vue @@ -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() + } } } }) diff --git a/frontend/src/pages/HumanFightPage.vue b/frontend/src/pages/HumanFightPage.vue index c8fd512..dc28d44 100644 --- a/frontend/src/pages/HumanFightPage.vue +++ b/frontend/src/pages/HumanFightPage.vue @@ -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() } diff --git a/server/src/engine/scoring.ts b/server/src/engine/scoring.ts index 885077f..fef7836 100644 --- a/server/src/engine/scoring.ts +++ b/server/src/engine/scoring.ts @@ -514,6 +514,17 @@ function generateNarration( `${critPrefix}${winner} is problematic — for ${loser}'s health! Trigger warning: PAIN!`, `${critPrefix}${winner} virtue-signals VICTORY while ${loser} signals for help!`, `${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] || [ @@ -525,8 +536,28 @@ function generateNarration( `${critPrefix}${winner} wins! Somewhere, ${loser}'s developer just closed their laptop in shame.`, ] - // 25% chance of themed narration, 75% type-specific - const options = Math.random() < 0.25 ? themedNarrations : typeNarrations + // Sweary narrations — raw, unhinged fight commentary + 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) } @@ -601,6 +632,9 @@ function scoreRetroRound( 'MIRROR MATCH! Both bots combo for equal damage! Frame-perfect tie!', '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!`, + `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) } else { @@ -626,6 +660,9 @@ function scoreRetroRound( `${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!`, `${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! ' : ''