feat: THE CREATOR god-tier character, bitcoin choreographies, omni-morph, cameos

- Guy Fawkes mask archetype with golden outline, bitcoin chest symbol, laptop, tier-gated effects
- 12 custom bitcoin-themed choreographies (8 regular + 4 exclusive ultimates)
- Omni-morph system: creator morphs into any of 80+ archetypes instead of cycling 3
- 6% per-round cameo in non-creator fights — drops golden ₿ gifts to fighters
- Custom golden code rain entrance with persistent orbiting particles
- pickChoreography: 50% ultimate chance (100% on crits), all tier ultimates + 4 exclusive
- Server auto-assigns the_creator archetype on login/register for creator pubkey
- FightViewer, payments, queue, orchestrator improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 12:08:18 +00:00
co-authored by Claude Opus 4.6
parent 8a3480e5ab
commit 6d390f69b2
14 changed files with 1513 additions and 104 deletions
+91 -23
View File
@@ -8,6 +8,7 @@ import {
sfxCrowdCheer, sfxCrowdGasp, sfxCrowdOoh, sfxApplause, sfxDrumRoll,
setMusicIntensity, stopAllAudio,
setMasterMute, isMasterMuted, ensureAudioContext,
speakQuestion, speakAnswer, speakNarration,
} from '../game/sounds'
interface Round {
@@ -66,6 +67,7 @@ const hitTextX = ref(50)
const hitTextY = ref(30)
const glitching = ref(false)
const soundOn = ref(true)
const insanityMode = ref(false)
async function toggleSound() {
soundOn.value = !soundOn.value
@@ -73,6 +75,14 @@ async function toggleSound() {
setMasterMute(!soundOn.value)
}
function toggleInsanity() {
insanityMode.value = !insanityMode.value
// Insanity mode kills all TTS immediately
if (insanityMode.value && typeof speechSynthesis !== 'undefined') {
speechSynthesis.cancel()
}
}
// Staggered log
const logItems = ref<{ type: string; round: number; text: string; color: string }[]>([])
@@ -269,28 +279,72 @@ async function _doReplay() {
for (const round of props.fight.rounds) {
currentRound.value = round.roundNumber
fanfareRound(round.roundNumber)
await showOverlay(`ROUND ${round.roundNumber}`, '#00f0ff', 700)
await sleep(80)
await showOverlay(challengeLabel(round.challengeType), '#b83dff', 600)
await sleep(80)
fanfareFight()
await showOverlay('FIGHT!', '#ff2d7b', 400)
await sleep(80)
// Show speech bubbles BEFORE the fight animation so viewers see what bots said
if (scene) {
if (round.botAResponse) scene.showSpeechBubble('a', round.botAResponse.slice(0, 60), 3.5)
if (round.botBResponse) {
setTimeout(() => {
if (scene && round.botBResponse) scene.showSpeechBubble('b', round.botBResponse.slice(0, 60), 3.2)
}, 400)
}
if (insanityMode.value) {
// Insanity: minimal overlays, no fanfares
await showOverlay(`R${round.roundNumber}`, '#00f0ff', 150)
} else {
fanfareRound(round.roundNumber)
await showOverlay(`ROUND ${round.roundNumber}`, '#00f0ff', 700)
await sleep(80)
await showOverlay(challengeLabel(round.challengeType), '#b83dff', 600)
await sleep(80)
fanfareFight()
await showOverlay('FIGHT!', '#ff2d7b', 400)
await sleep(80)
}
await sleep(300)
// Log + fight animation in parallel
const logPromise = addRoundToLog(round, true)
// === TTS-synced question + answer flow ===
const challenge = JSON.parse(round.challengeData)
const doTTS = soundOn.value && !insanityMode.value
// 1. Show question in log AND speak it
logItems.value.push(
{ type: 'header', round: round.roundNumber, text: `ROUND ${round.roundNumber}: ${challengeLabel(round.challengeType)}`, color: 'neon-purple' },
)
scrollLog(); await sleep(insanityMode.value ? 30 : 100)
logItems.value.push(
{ type: 'prompt', round: round.roundNumber, text: challenge.prompt, color: 'text-muted' },
)
scrollLog()
if (doTTS && challenge.prompt) {
await speakQuestion(challenge.prompt)
await sleep(150)
} else {
await sleep(insanityMode.value ? 50 : 600)
}
// 2. Bot A: log + bubble + mouth + TTS (all synced)
if (round.botAResponse) {
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse.slice(0, 120) || '[NO RESPONSE]'}`, color: 'neon-cyan' })
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botATimeMs}ms | Score: ${round.botAScore}`, color: 'text-muted' })
scrollLog()
if (scene && !insanityMode.value) {
scene.showSpeechBubble('a', round.botAResponse.slice(0, 60), 5)
scene.startTalking('a')
}
if (doTTS) await speakAnswer(props.fight.botA!.name, round.botAResponse.slice(0, 60))
else await sleep(insanityMode.value ? 30 : 800)
scene?.stopTalking('a')
if (!insanityMode.value) await sleep(150)
}
// 3. Bot B: log + bubble + mouth + TTS
if (round.botBResponse) {
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse.slice(0, 120) || '[NO RESPONSE]'}`, color: 'neon-pink' })
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botBTimeMs}ms | Score: ${round.botBScore}`, color: 'text-muted' })
scrollLog()
if (scene && !insanityMode.value) {
scene.showSpeechBubble('b', round.botBResponse.slice(0, 60), 5)
scene.startTalking('b')
}
if (doTTS) await speakAnswer(props.fight.botB!.name, round.botBResponse.slice(0, 60))
else await sleep(insanityMode.value ? 30 : 800)
scene?.stopTalking('b')
if (!insanityMode.value) await sleep(150)
}
// Log is already populated above — no need for addRoundToLog stagger
const logPromise = Promise.resolve()
const isCritical = Math.abs((round.botAScore || 0) - (round.botBScore || 0)) > 4
const aWon = round.winnerId === props.fight.botA!.id
const bWon = round.winnerId === props.fight.botB!.id
@@ -328,6 +382,8 @@ async function _doReplay() {
if (round.narration) {
logItems.value.push({ type: 'narration', round: round.roundNumber, text: `>> ${round.narration}`, color: 'neon-yellow' })
scrollLog()
if (doTTS) await speakNarration(round.narration)
await sleep(insanityMode.value ? 30 : 200)
}
const winner = aWon ? props.fight.botA!.name : bWon ? props.fight.botB!.name : 'DRAW'
@@ -352,17 +408,18 @@ async function _doReplay() {
setMusicIntensity(1 - lowestHp / 100)
// Taunting between rounds — winner taunts, sometimes both
if (scene && (aWon || bWon)) {
if (!insanityMode.value && scene && (aWon || bWon)) {
const winnerSide = aWon ? 'a' : 'b'
await sleep(150)
await scene.playTaunt(winnerSide)
// Sometimes loser taunts back (30% chance)
if (Math.random() < 0.3) {
await scene.playTaunt(winnerSide === 'a' ? 'b' : 'a')
}
await sleep(200)
} else {
} else if (!insanityMode.value) {
await sleep(400)
} else {
await sleep(30)
}
}
@@ -593,6 +650,17 @@ async function _doReplay() {
<path d="M11 5L6 9H2v6h4l5 4V5z"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>
</svg>
</button>
<button
v-if="isReplaying"
class="h-8 px-2 flex items-center justify-center border text-[9px] font-pixel tracking-wider transition-all"
:class="insanityMode
? 'border-neon-yellow/70 text-neon-yellow bg-neon-yellow/10 animate-pulse'
: 'border-border/50 text-text-muted hover:text-neon-yellow hover:border-neon-yellow/50'"
title="INSANITY MODE: Skip TTS, max speed"
@click="toggleInsanity"
>
{{ insanityMode ? 'INSANITY' : 'SKIP' }}
</button>
<span class="font-pixel text-[10px] text-text-muted">{{ fight.status === 'finished' ? 'FINISHED' : fight.status.toUpperCase() }}</span>
</div>
</div>