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
+310
View File
@@ -7502,6 +7502,307 @@ export async function createFightScene(config: FightSceneConfig) {
}
}
// === SHOWBOATING / TAUNTING SYSTEM ===
// 30 idle animations fighters perform during question/answer phases
const _showboatActive: Record<string, boolean> = { a: false, b: false }
type ShowboatFn = (fighter: any, homeX: number, homeY: number, sx: number, sy: number) => Promise<void>
const showboats: ShowboatFn[] = [
// 0: Shadow boxing — quick attack/kick combo in place
async (f, _hx, _hy, _sx, _sy) => {
f.play('attack'); sfxDodge(); await k.wait(0.18)
f.play('kick'); await k.wait(0.18)
f.play('attack'); await k.wait(0.18)
},
// 1: Flex pose — scale up, hold, shrink back
async (f, _hx, _hy, sx, sy) => {
f.play('win')
await k.tween(1, 1.25, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutBack)
await k.wait(0.4)
await k.tween(1.25, 1, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeInQuad)
},
// 2: Bouncy taunt — rapid small hops
async (f, _hx, hy) => {
for (let i = 0; i < 4; i++) {
await k.tween(f.pos.y, hy - 15, 0.06, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
await k.tween(f.pos.y, hy, 0.06, (v) => { f.pos.y = v }, k.easings.easeInQuad)
}
},
// 3: Spin move — full 360 rotation
async (f) => {
f.play('special')
const startAngle = f.angle || 0
await k.tween(startAngle, startAngle + 360, 0.4, (v) => { f.angle = v }, k.easings.easeInOutQuad)
f.angle = startAngle
},
// 4: Moonwalk — slide backward while idle
async (f, hx) => {
const dir = f.pos.x < W / 2 ? -1 : 1
await k.tween(f.pos.x, hx + dir * 40, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
await k.tween(f.pos.x, hx, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 5: Jump rope — rapid tiny bounces with kick anim
async (f, _hx, hy) => {
f.play('kick')
for (let i = 0; i < 6; i++) {
await k.tween(f.pos.y, hy - 8, 0.05, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
await k.tween(f.pos.y, hy, 0.05, (v) => { f.pos.y = v }, k.easings.easeInQuad)
}
},
// 6: Intimidate stare — zoom in slightly, hold, zoom out
async (f, _hx, _hy, sx, sy) => {
f.play('special')
await k.tween(1, 1.35, 0.25, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeOutQuad)
await k.wait(0.5)
await k.tween(1.35, 1, 0.2, (v) => { f.scale.x = sx * v; f.scale.y = sy * v }, k.easings.easeInQuad)
},
// 7: Cocky lean — tilt to one side
async (f) => {
const start = f.angle || 0
await k.tween(start, start + 12, 0.2, (v) => { f.angle = v }, k.easings.easeOutQuad)
await k.wait(0.4)
await k.tween(start + 12, start, 0.2, (v) => { f.angle = v }, k.easings.easeInQuad)
},
// 8: Fist pump — jump + attack anim
async (f, _hx, hy) => {
f.play('attack'); sfxPunch()
await k.tween(f.pos.y, hy - 40, 0.12, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
await k.tween(f.pos.y, hy, 0.12, (v) => { f.pos.y = v }, k.easings.easeInQuad)
},
// 9: Shuffle dance — rapid left-right shimmy
async (f, hx) => {
f.play('win')
for (let i = 0; i < 4; i++) {
await k.tween(f.pos.x, hx + 12, 0.06, (v) => { f.pos.x = v })
await k.tween(f.pos.x, hx - 12, 0.06, (v) => { f.pos.x = v })
}
f.pos.x = hx
},
// 10: Push-ups — squish vertically
async (f, _hx, _hy, sx, sy) => {
for (let i = 0; i < 3; i++) {
await k.tween(1, 0.6, 0.12, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeOutQuad)
await k.tween(0.6, 1, 0.12, (v) => { f.scale.y = sy * v; f.scale.x = sx * (2 - v) }, k.easings.easeInQuad)
}
},
// 11: Victory lap — dash to center and back
async (f, hx, hy) => {
f.play('kick'); sfxZoomWhoosh()
const center = W / 2
await k.tween(f.pos.x, center, 0.2, (v) => { f.pos.x = v }, k.easings.easeInQuad)
f.play('win')
spawnSparks(center, hy - 20, 5, theme.accent)
await k.wait(0.2)
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 12: Yawn stretch — slow scale up, hold, shrink
async (f, _hx, hy, sx, sy) => {
await k.tween(1, 1.15, 0.4, (v) => { f.scale.x = sx * v; f.scale.y = sy * v; f.pos.y = hy - (v - 1) * 30 }, k.easings.easeInOutQuad)
spawnEmoteText(f.pos.x, f.pos.y - 40, '*yawn*', '#aaaaaa')
await k.wait(0.3)
await k.tween(1.15, 1, 0.3, (v) => { f.scale.x = sx * v; f.scale.y = sy * v; f.pos.y = hy - (v - 1) * 30 }, k.easings.easeInOutQuad)
},
// 13: Backflip — jump high + rotate 360
async (f, _hx, hy) => {
sfxBoing()
const startAngle = f.angle || 0
await Promise.all([
k.tween(f.pos.y, hy - 80, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
k.tween(startAngle, startAngle - 360, 0.4, (v) => { f.angle = v }, k.easings.easeInOutQuad),
])
await k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad)
f.angle = startAngle
},
// 14: Chest thump — attack anim + shake
async (f) => {
f.play('attack'); sfxBlock()
k.shake(3)
spawnEmoteText(f.pos.x, f.pos.y - 40, 'COME ON!', '#ff4444')
await k.wait(0.3)
f.play('attack')
k.shake(3)
await k.wait(0.2)
},
// 15: Hover float — rise slowly, hold, descend
async (f, _hx, hy) => {
f.play('special')
await k.tween(f.pos.y, hy - 50, 0.4, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await k.tween(f.pos.y, hy, 0.3, (v) => { f.pos.y = v }, k.easings.easeInQuad)
},
// 16: Dizzy spin — multiple fast spins
async (f) => {
f.play('hit')
const startAngle = f.angle || 0
await k.tween(startAngle, startAngle + 720, 0.5, (v) => { f.angle = v }, k.easings.easeOutQuad)
spawnEmoteText(f.pos.x, f.pos.y - 40, '@_@', '#ffcc00')
f.angle = startAngle
await k.wait(0.2)
},
// 17: Ground pound — jump high, slam down with shake
async (f, _hx, hy) => {
f.play('kick'); sfxBoing()
await k.tween(f.pos.y, hy - 70, 0.15, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
await k.wait(0.05)
f.play('attack')
await k.tween(f.pos.y, hy, 0.08, (v) => { f.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(8)
spawnSparks(f.pos.x, hy, 8, '#ff6600')
},
// 18: Feint charge — dash toward opponent, brake, dash back
async (f, hx) => {
const dir = f.pos.x < W / 2 ? 1 : -1
f.play('attack'); sfxZoomWhoosh()
await k.tween(f.pos.x, hx + dir * 80, 0.12, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
sfxBlock()
await k.wait(0.1)
f.play('idle')
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 19: Butt wiggle — oscillating rotation
async (f) => {
f.play('win')
const start = f.angle || 0
for (let i = 0; i < 5; i++) {
await k.tween(f.angle, start + 8, 0.05, (v) => { f.angle = v })
await k.tween(f.angle, start - 8, 0.05, (v) => { f.angle = v })
}
f.angle = start
},
// 20: Jumping jacks — alternating stretch X/Y
async (f, _hx, hy, sx, sy) => {
for (let i = 0; i < 3; i++) {
f.play(i % 2 === 0 ? 'kick' : 'attack')
await k.tween(0, 1, 0.12, (t) => {
f.scale.x = sx * (1 + 0.2 * Math.sin(t * Math.PI))
f.scale.y = sy * (1 - 0.1 * Math.sin(t * Math.PI))
f.pos.y = hy - 10 * Math.sin(t * Math.PI)
})
}
f.scale.x = sx; f.scale.y = sy; f.pos.y = hy
},
// 21: Sumo stomp — stomp left then right with mini shakes
async (f, hx, hy, sx, sy) => {
f.play('attack')
// Left stomp
await k.tween(f.pos.x, hx - 15, 0.08, (v) => { f.pos.x = v })
f.scale.y = sy * 0.85; k.shake(3); sfxBonk()
await k.wait(0.1); f.scale.y = sy
// Right stomp
await k.tween(f.pos.x, hx + 15, 0.08, (v) => { f.pos.x = v })
f.scale.y = sy * 0.85; k.shake(3); sfxBonk()
await k.wait(0.1); f.scale.y = sy
f.pos.x = hx
},
// 22: Crane stance — rise on one leg, arms out
async (f, _hx, hy, sx, sy) => {
f.play('kick')
await Promise.all([
k.tween(f.pos.y, hy - 30, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad),
k.tween(1, 1.15, 0.3, (v) => { f.scale.y = sy * v }, k.easings.easeOutQuad),
])
await k.wait(0.5)
await Promise.all([
k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad),
k.tween(1.15, 1, 0.2, (v) => { f.scale.y = sy * v }, k.easings.easeInQuad),
])
},
// 23: Matrix dodge — lean way back
async (f) => {
const start = f.angle || 0
f.play('special')
await k.tween(start, start - 35, 0.2, (v) => { f.angle = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await k.tween(start - 35, start, 0.15, (v) => { f.angle = v }, k.easings.easeInQuad)
},
// 24: Air guitar — special anim + rapid bounces
async (f, _hx, hy) => {
f.play('special'); sfxSlideUp()
for (let i = 0; i < 5; i++) {
await k.tween(f.pos.y, hy - 6, 0.04, (v) => { f.pos.y = v })
await k.tween(f.pos.y, hy, 0.04, (v) => { f.pos.y = v })
}
spawnEmoteText(f.pos.x, f.pos.y - 45, '~♪~', '#ff44ff')
},
// 25: Cracking knuckles — shake + attack burst
async (f, hx) => {
const origX = f.pos.x
for (let i = 0; i < 4; i++) {
await k.tween(f.pos.x, hx + 4, 0.025, (v) => { f.pos.x = v })
await k.tween(f.pos.x, hx - 4, 0.025, (v) => { f.pos.x = v })
}
f.pos.x = origX
f.play('attack'); sfxBlock()
await k.wait(0.15)
},
// 26: Windmill arms — rapid attack/kick cycling
async (f) => {
const anims = ['attack', 'kick', 'special', 'attack', 'kick', 'special'] as const
for (const a of anims) {
f.play(a)
await k.wait(0.07)
}
},
// 27: Confident strut — walk forward, pose, walk back
async (f, hx) => {
const dir = f.pos.x < W / 2 ? 1 : -1
f.play('win')
await k.tween(f.pos.x, hx + dir * 35, 0.3, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
spawnEmoteText(f.pos.x, f.pos.y - 45, '😎', '#ffcc00')
await k.wait(0.2)
await k.tween(f.pos.x, hx, 0.25, (v) => { f.pos.x = v }, k.easings.easeInOutQuad)
},
// 28: Earthquake stomp — big jump, land with major shake
async (f, _hx, hy) => {
sfxBoing()
await k.tween(f.pos.y, hy - 90, 0.2, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
f.play('attack')
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }, k.easings.easeInQuad)
sfxExplosion(); k.shake(12)
spawnSparks(f.pos.x, hy, 12, '#ffcc00')
spawnEmoteText(f.pos.x, f.pos.y - 50, 'BOOM!', '#ff2d2d')
await k.wait(0.15)
},
// 29: Meditation float — rise, pulse glow, descend
async (f, _hx, hy, sx, sy) => {
f.play('special')
await k.tween(f.pos.y, hy - 35, 0.3, (v) => { f.pos.y = v }, k.easings.easeOutQuad)
for (let i = 0; i < 3; i++) {
await k.tween(1, 1.08, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
await k.tween(1.08, 1, 0.15, (v) => { f.scale.x = sx * v; f.scale.y = sy * v })
}
await k.tween(f.pos.y, hy, 0.2, (v) => { f.pos.y = v }, k.easings.easeInQuad)
},
]
// Showboat loop: picks random showboats in sequence while active
async function _showboatLoop(side: 'a' | 'b') {
const tag = side === 'a' ? 'fighterA' : 'fighterB'
const homeX = side === 'a' ? HOME_A : HOME_B
const homeY = GROUND_Y - 6
while (_showboatActive[side]) {
const f = k.get(tag)[0]
if (!f) break
const sx = f.scale.x
const sy = f.scale.y
const idx = Math.floor(Math.random() * showboats.length)
try {
await showboats[idx](f, homeX, homeY, sx, sy)
} catch { break }
// Restore state after each showboat
const ff = k.get(tag)[0]
if (ff) {
ff.pos.x = homeX; ff.pos.y = homeY
ff.scale.x = sx; ff.scale.y = sy
ff.angle = 0
ff.play('idle')
}
// Brief pause between showboats
if (_showboatActive[side]) await k.wait(0.3 + Math.random() * 0.6)
}
}
return {
k,
@@ -7514,6 +7815,15 @@ export async function createFightScene(config: FightSceneConfig) {
startTalking(side: 'a' | 'b') { startTalking(side) },
stopTalking(side: 'a' | 'b') { stopTalking(side) },
startShowboating(side: 'a' | 'b') {
if (_showboatActive[side]) return
_showboatActive[side] = true
_showboatLoop(side)
},
stopShowboating(side: 'a' | 'b') {
_showboatActive[side] = false
},
async playEntrance() {
const fA = k.get('fighterA')[0]
const fB = k.get('fighterB')[0]