feat: 30 showboat/taunt animations during question & answer phases

Fighters now perform random showboating micro-animations while idle
during question reading and opponent's answer — shadow boxing, backflips,
flex poses, moonwalks, ground pounds, air guitar, sumo stomps, and more.

Each showboat uses existing sprite anims (attack/kick/special/win) combined
with Kaplay tweens for position, scale, rotation, sparks, and emote text.
Showboating loops with random variety and pauses between moves.

Flow: both showboat during question → stop the answering bot during their
turn → stop all before fight animation begins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 16:07:56 +00:00
co-authored by Claude Opus 4.6
parent 6561ef5d15
commit 2534cbc4cf
2 changed files with 329 additions and 5 deletions
+19 -5
View File
@@ -221,7 +221,7 @@ function addRoundToLog(round: Round, stagger: boolean): Promise<void> {
const challenge = JSON.parse(round.challengeData)
logItems.value.push(
{ type: 'header', round: round.roundNumber, text: `ROUND ${round.roundNumber}: ${challengeLabel(round.challengeType)}`, color: 'neon-purple' },
{ type: 'prompt', round: round.roundNumber, text: challenge.prompt, color: 'text-muted' },
{ type: 'prompt', round: round.roundNumber, text: challenge.displayPrompt || challenge.prompt, color: 'text-muted' },
{ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse?.slice(0, 120) || '[NO RESPONSE]'} (${round.botATimeMs}ms)`, color: 'neon-cyan' },
{ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse?.slice(0, 120) || '[NO RESPONSE]'} (${round.botBTimeMs}ms)`, color: 'neon-pink' },
)
@@ -235,7 +235,7 @@ function addRoundToLog(round: Round, stagger: boolean): Promise<void> {
const challenge = JSON.parse(round.challengeData)
logItems.value.push({ type: 'header', round: round.roundNumber, text: `ROUND ${round.roundNumber}: ${challengeLabel(round.challengeType)}`, color: 'neon-purple' })
scrollLog(); await sleep(150)
logItems.value.push({ type: 'prompt', round: round.roundNumber, text: challenge.prompt, color: 'text-muted' })
logItems.value.push({ type: 'prompt', round: round.roundNumber, text: challenge.displayPrompt || challenge.prompt, color: 'text-muted' })
scrollLog(); await sleep(200)
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse?.slice(0, 120) || '[NO RESPONSE]'}`, color: 'neon-cyan' })
scrollLog(); await sleep(150)
@@ -310,24 +310,32 @@ async function _doReplay() {
const challenge = JSON.parse(round.challengeData)
const doTTS = soundOn.value && !insanityMode.value
// Both fighters showboat while the question is being asked
if (scene && !insanityMode.value) {
scene.startShowboating('a')
scene.startShowboating('b')
}
// 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' },
{ type: 'prompt', round: round.roundNumber, text: challenge.displayPrompt || challenge.prompt, color: 'text-muted' },
)
scrollLog()
if (doTTS && challenge.prompt) {
await speakQuestion(challenge.prompt)
if (doTTS && (challenge.displayPrompt || challenge.prompt)) {
await speakQuestion(challenge.displayPrompt || challenge.prompt)
await sleep(150)
} else {
await sleep(insanityMode.value ? 50 : 600)
}
// 2. Bot A: log + bubble + mouth + TTS (all synced)
// Stop A's showboat when it's their turn to talk, B keeps showboating
if (round.botAResponse) {
scene?.stopShowboating('a')
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()
@@ -342,7 +350,9 @@ async function _doReplay() {
}
// 3. Bot B: log + bubble + mouth + TTS
// Stop B's showboat when it's their turn to talk
if (round.botBResponse) {
scene?.stopShowboating('b')
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()
@@ -356,6 +366,10 @@ async function _doReplay() {
if (!insanityMode.value) await sleep(150)
}
// Stop all showboating before fight animation
scene?.stopShowboating('a')
scene?.stopShowboating('b')
// 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