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:
co-authored by
Claude Opus 4.6
parent
8a3480e5ab
commit
6d390f69b2
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user