feat: full comedy overhaul — voices, narrations, trash talk, commentary

Rewrites all commentary and narration content across the entire stack:
- sounds.ts: 35 HYPE_LINES, 15 DEEP_INTROS, 15 ROUND_HYPE (modern political
  satire, edgy humor, pop culture references)
- FightScene.ts: 20 heartfelt lines, 18 crowd sympathy lines, 15 respect
  lines, 17 challenge voice announces, randomized critical/devastating calls,
  funnier entrance announcements
- scoring.ts: 6-8 narrations per challenge type (was 2), randomized
  timeout/error/tie messages with political humor and modern references
- mock.ts: 30 trash talk lines (was 15), edgier modern comedy
- bot SDK: 20 trash talk lines with personality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 09:15:41 +00:00
co-authored by Claude Opus 4.6
parent 559782c8ce
commit 33f08c35b5
4 changed files with 411 additions and 92 deletions
+228 -34
View File
@@ -1,5 +1,5 @@
import kaplay from 'kaplay'
import { generateSpriteSheet, generateJudgeSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS } from './sprites'
import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites'
import {
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect,
@@ -14,8 +14,8 @@ import {
export interface FightSceneConfig {
canvas: HTMLCanvasElement
botA: { name: string; seed: string; tier: number; archetype?: string }
botB: { name: string; seed: string; tier: number; archetype?: string }
botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
botB: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
arena: string
onReady?: () => void
}
@@ -230,13 +230,13 @@ export async function createFightScene(config: FightSceneConfig) {
let sheetA: string
let sheetB: string
try {
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype)
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization)
} catch (err) {
console.error('[FightScene] Failed to generate sprite for botA:', err)
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
}
try {
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype)
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization)
} catch (err) {
console.error('[FightScene] Failed to generate sprite for botB:', err)
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
@@ -258,6 +258,20 @@ export async function createFightScene(config: FightSceneConfig) {
}
await k.loadSprite('judge', judgeSheet, { sliceX: JUDGE_MAX_FRAMES, sliceY: JUDGE_ROWS, anims: judgeAnims })
// Human sprites (the bot owner's silly human avatar)
const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0))
const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0))
const humanSheetA = generateHumanSpriteSheet(botA.seed, botA.archetype || 'standard', colorsA.primary, colorsA.secondary, winRateA)
const humanSheetB = generateHumanSpriteSheet(botB.seed, botB.archetype || 'standard', colorsB.primary, colorsB.secondary, winRateB)
const humanAnims: Record<string, { from: number; to: number; loop: boolean; speed: number }> = {}
for (const [name, cfg] of Object.entries(HUMAN_ANIMATIONS)) {
humanAnims[name] = { from: cfg.row * HUMAN_MAX_FRAMES, to: cfg.row * HUMAN_MAX_FRAMES + cfg.frames - 1, loop: true, speed: 6 }
}
await Promise.all([
k.loadSprite('humanA', humanSheetA, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
k.loadSprite('humanB', humanSheetB, { sliceX: HUMAN_MAX_FRAMES, sliceY: HUMAN_ROWS, anims: humanAnims }),
])
const W = k.width()
const H = k.height()
const GROUND_Y = H * 0.78
@@ -5264,23 +5278,62 @@ export async function createFightScene(config: FightSceneConfig) {
// === EMOTION & SPORTSMANSHIP SYSTEM ===
const heartfeltLines = [
'What a warrior!', 'They gave it everything!', 'Heart of a champion!',
'You can feel the respect!', 'That was beautiful!', 'Incredible spirit!',
'They left it all in the ring!', 'A true fighter!', 'Nothing but respect!',
'What courage!', 'The crowd is in tears!', 'What a moment!',
'This is what it\'s all about!', 'Pure heart!', 'Standing ovation!',
'That bot fights like it\'s got student loans to pay!',
'I\'m not crying, you\'re crying!',
'Someone get this bot a movie deal!',
'This is better than anything on Netflix right now!',
'That bot has more heart than most politicians!',
'I haven\'t been this moved since my last Windows update!',
'The crowd is ugly crying and I respect that!',
'This bot has main character energy!',
'We\'re witnessing greatness and I don\'t say that lightly!',
'Even the judge is getting emotional!',
'Someone call their developer, they\'d be so proud!',
'This is the kind of fight you tell your grandkids about!',
'That\'s not a bot, that\'s a WARRIOR!',
'More drama than a Congressional hearing!',
'They left it all in the ring! And by all, I mean EVERYTHING!',
'The crowd is standing! The ones who can still stand, anyway!',
'Somebody film this before it gets taken down!',
'This fight has more plot twists than a Netflix original!',
'That bot fights like rent is due tomorrow!',
'My heart! My poor little heart!',
]
const crowdSympathyLines = [
'Aww...', 'So close!', 'Almost had it!', 'Tough break!',
'Next time!', 'Great effort though!', 'Don\'t give up!',
'The crowd feels that one...', 'Ohhh...', 'Heartbreaking!',
'Pour one out for the fallen!',
'That\'s rough, buddy...',
'Somebody hug that bot!',
'They tried their best, which is... concerning!',
'At least they have a great personality!',
'Their developer still loves them! Probably!',
'It\'s a learning experience! A very painful one!',
'Better luck next patch!',
'Participation trophy incoming!',
'The exit is to your left!',
'Have you tried turning it off and on again?',
'Even their antivirus felt that!',
'They\'ll be in therapy for epochs after this!',
'At least they looked good losing!',
'That bot just became a cautionary tale!',
'Someone start a GoFundMe for their repairs!',
'Their training data did NOT prepare them for this!',
'That bot needs a hug and a firmware update!',
]
const respectLines = [
'Good fight.', 'You fought well.', 'Respect.', 'Well played.',
'That was fun!', 'Same time next week?', 'You\'re getting better!',
'No hard feelings!', 'Honor to fight you!', 'GG.',
'Good fight.', 'Respect.', 'GG no re.',
'Same time next week?',
'You\'re built different. Not better, but different.',
'We should start a podcast.',
'That was actually fun. Don\'t tell anyone.',
'Your developer should be proud. Probably.',
'Ten out of ten, would fight again.',
'We\'re not so different, you and I.',
'I\'d swipe right.', 'You single?',
'Tell your GPU I said hi.',
'No hard feelings. Just hard hits.',
'You fight like someone who reads documentation.',
]
// Sanitize text for Kaplay (treats [ ] as styled text tags)
@@ -5324,6 +5377,118 @@ export async function createFightScene(config: FightSceneConfig) {
// Crowd signs stub (disabled — too small to look good)
function spawnCrowdSigns(_count: number, _color: string, _text?: string) {}
// === HUMAN OWNER APPEARANCES ===
// The silly human behind each bot occasionally shows up
/** Spawn a human as a "coach" standing behind the fighter, cheering/panicking */
async function spawnHumanCoach(side: 'a' | 'b', mood: 'cheer' | 'panic' | 'coach') {
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const homeX = side === 'a' ? HOME_A - 60 : HOME_B + 60
const tag = `human_${side}`
// Don't spawn if already visible
if (k.get(tag).length > 0) return
const human = k.add([
k.sprite(spriteId, { anim: mood }),
k.pos(homeX, GROUND_Y - 4),
k.anchor('bot'),
k.scale(side === 'a' ? 1.2 : -1.2, 1.2),
k.z(5),
k.opacity(0),
tag,
])
// Fade in
await k.tween(0, 0.85, 0.3, (v) => { human.opacity = v }, k.easings.easeOutQuad)
await k.wait(2.0)
// Fade out
await k.tween(0.85, 0, 0.5, (v) => { human.opacity = v }, k.easings.easeInQuad)
if (human.exists()) human.destroy()
}
/** Human runs across the screen (comedic moment) */
async function humanRunAcross(side: 'a' | 'b') {
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const fromLeft = side === 'a'
const startX = fromLeft ? -40 : W + 40
const endX = fromLeft ? W + 40 : -40
const human = k.add([
k.sprite(spriteId, { anim: 'run' }),
k.pos(startX, GROUND_Y - 4),
k.anchor('bot'),
k.scale(fromLeft ? 1.3 : -1.3, 1.3),
k.z(8),
k.opacity(0.9),
])
// Run across screen
await k.tween(startX, endX, 1.5, (v) => { human.pos.x = v }, k.easings.linear)
if (human.exists()) human.destroy()
}
/** Human briefly replaces the bot sprite (jumps in to fight, fails hilariously) */
async function humanJumpsIn(side: 'a' | 'b') {
const fighterTag = side === 'a' ? 'fighterA' : 'fighterB'
const spriteId = side === 'a' ? 'humanA' : 'humanB'
const fighter = k.get(fighterTag)[0]
if (!fighter) return
// Spawn human at fighter position
const human = k.add([
k.sprite(spriteId, { anim: 'panic' }),
k.pos(fighter.pos.x, fighter.pos.y),
k.anchor('bot'),
k.scale(fighter.scale.x, fighter.scale.y),
k.z(fighter.z + 1),
k.opacity(0),
])
// Hide bot, show human
const origOpacity = fighter.opacity
fighter.opacity = 0
await k.tween(0, 1, 0.15, (v) => { human.opacity = v })
// Human flails around for a moment
human.play('panic')
await k.wait(0.4)
human.play('cheer')
sfxRandomSilly()
await k.wait(0.3)
human.play('panic')
await k.wait(0.3)
// Run away scared
const escapeX = side === 'a' ? -80 : W + 80
human.play('run')
await k.tween(human.pos.x, escapeX, 0.5, (v) => { human.pos.x = v }, k.easings.easeInQuad)
// Restore bot
fighter.opacity = origOpacity
if (human.exists()) human.destroy()
}
/** Random chance to show a human moment — called between rounds */
async function maybeShowHuman() {
const roll = Math.random()
if (roll > 0.08) return // 8% chance per round
const side: 'a' | 'b' = Math.random() < 0.5 ? 'a' : 'b'
const action = Math.random()
if (action < 0.4) {
// Coach appearance (most common)
const mood = Math.random() < 0.5 ? 'cheer' : 'panic'
await spawnHumanCoach(side, mood)
} else if (action < 0.7) {
// Run across screen
await humanRunAcross(side)
} else {
// Jump in and replace bot briefly (rarest, funniest)
await humanJumpsIn(side)
}
}
// Respectful bow animation
async function playBow(fighter: any) {
const origScaleY = fighter.scale.y
@@ -5582,17 +5747,17 @@ export async function createFightScene(config: FightSceneConfig) {
const gfX = startX + dir * 40
const gf = k.add([k.rect(25, 45), k.pos(gfX, GROUND_Y - 25), k.anchor('center'), k.color(safeColor(k,'#ff69b4')), k.opacity(0.9), k.z(11)])
const heart = k.add([k.text('!', { size: 16 }), k.pos(gfX, GROUND_Y - 65), k.anchor('center'), k.color(safeColor(k,'#ff0000')), k.z(12)])
announceSilly('We need to talk!')
announceSilly('We need to talk! About your ELO!')
// Walk in arguing
await k.tween(startX, homeX + dir * 30, 0.6, (v) => {
fighter.pos.x = v; gf.pos.x = v + dir * 40; heart.pos.x = v + dir * 40
}, k.easings.easeInOutQuad)
await k.wait(0.2)
heart.text = '!!!'
announceRandom("I can't believe you!", false)
announceRandom("You promised you'd stop fighting!", false)
await k.wait(0.4)
// Girlfriend storms off
announceSilly('Fine! Good luck!')
announceSilly('I\'m telling your developer!')
await k.tween(gf.pos.x, fromLeft ? -60 : W + 60, 0.5, (v) => { gf.pos.x = v; heart.pos.x = v }, k.easings.easeInQuad)
gf.destroy(); heart.destroy()
// Bot shrugs and walks to position
@@ -5779,7 +5944,7 @@ export async function createFightScene(config: FightSceneConfig) {
async (fighter: any, homeX: number, fromLeft: boolean) => {
const startX = fromLeft ? -60 : W + 60
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
announceCool('Smooth!')
announceCool('Smoother than a politician changing positions!')
// Moonwalk: visually moving backward but translating forward
fighter.scale.x = -fighter.scale.x // Face away
await k.tween(startX, homeX, 0.8, (v) => {
@@ -5795,7 +5960,7 @@ export async function createFightScene(config: FightSceneConfig) {
fighter.pos.x = doorX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
// Bouncer arm
const arm = k.add([k.rect(40, 15), k.pos(doorX, GROUND_Y - 30), k.anchor(fromLeft ? 'left' : 'right'), k.color(safeColor(k,'#444444')), k.z(12)])
announceSilly('And stay out!')
announceSilly('And stay out! You\'re BANNED from the other fight!')
await k.wait(0.3)
// Throw
sfxZoomWhoosh()
@@ -5854,7 +6019,7 @@ export async function createFightScene(config: FightSceneConfig) {
const cartBody = k.add([k.rect(45, 30), k.pos(startX, GROUND_Y - 18), k.anchor('center'), k.color(safeColor(k,'#888888')), k.opacity(0.9), k.z(9)])
const cartWheel = k.add([k.circle(5), k.pos(startX + (fromLeft ? 15 : -15), GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k,'#444')), k.z(9)])
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 40; fighter.opacity = 1
announceSilly('Weeee!')
announceSilly('THIS IS MY EMOTIONAL SUPPORT SHOPPING CART!')
sfxZoomWhoosh()
await k.tween(startX, homeX, 0.5, (v) => {
fighter.pos.x = v; cartBody.pos.x = v; cartWheel.pos.x = v + (fromLeft ? 15 : -15)
@@ -5873,7 +6038,7 @@ export async function createFightScene(config: FightSceneConfig) {
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
// Spotlight cone
const spot = k.add([k.rect(60, H), k.pos(startX - 30, 0), k.color(safeColor(k,'#ffe14d')), k.opacity(0.08), k.z(1)])
announceDramatic('The champion arrives!')
announceDramatic('THE CHAMPION ARRIVES! TAXPAYERS FUNDED THIS ENTRANCE!')
await k.tween(startX, homeX, 1.0, (v) => {
fighter.pos.x = v; spot.pos.x = v - 30
}, k.easings.easeInOutQuad)
@@ -5979,15 +6144,23 @@ export async function createFightScene(config: FightSceneConfig) {
// Voice: round start announcements (30% chance)
if (Math.random() < 0.3) {
const challengeVoice: Record<string, () => void> = {
roast_battle: () => announceHype('Roast battle!'),
food_fight: () => announceSilly('Food fight!'),
wrestling_match: () => announceDramatic('Wrestling match!'),
music_battle: () => announceCool('Music battle!'),
magic_duel: () => announceDramatic('Magic duel!'),
meme_war: () => announceSilly('Meme war!'),
demolition: () => announceHype('Demolition derby!'),
medieval_combat: () => announceDramatic('Medieval combat!'),
space_war: () => announceCool('Space war!'),
roast_battle: () => announceHype('TIME TO GET ROASTED! SOMEBODY CALL THE FIRE DEPARTMENT!'),
food_fight: () => announceSilly('FOOD FIGHT! SOMEBODY\'S GETTING SERVED!'),
wrestling_match: () => announceDramatic('FROM THE TOP ROPE! THIS IS GONNA GET PHYSICAL!'),
music_battle: () => announceCool('MUSIC BATTLE! DROP THE BEAT AND YOUR OPPONENT!'),
magic_duel: () => announceDramatic('WIZARDS AT DAWN! SOMEBODY\'S GETTING HEXED!'),
meme_war: () => announceSilly('MEME WAR! YOUR HUMOR IS ABOUT TO GET RATIO\'D!'),
demolition: () => announceHype('DEMOLITION DERBY! NOTHING WILL SURVIVE THIS!'),
medieval_combat: () => announceDramatic('MEDIEVAL COMBAT! CHIVALRY IS DEAD AND SO IS YOUR OPPONENT!'),
space_war: () => announceCool('SPACE WAR! HOUSTON, WE HAVE A PROBLEM!'),
speed_blitz: () => announceHype('SPEED BLITZ! BLINK AND YOU\'LL MISS THE CARNAGE!'),
math_blitz: () => announceSilly('MATH BLITZ! SOMEBODY\'S ABOUT TO GET DIVIDED!'),
riddle: () => announceCool('RIDDLE TIME! BRAINS OVER BRAWN! BUT ALSO BRAWN!'),
code_golf: () => announceSilly('CODE GOLF! MAY THE SHORTEST SOLUTION WIN!'),
creative_writing: () => announceCool('CREATIVE WRITING! THE PEN IS MIGHTIER THAN THE SWORD!'),
hallucination_check: () => announceHype('HALLUCINATION CHECK! REALITY IS ABOUT TO HIT DIFFERENT!'),
trap_card: () => announceDramatic('TRAP CARD ACTIVATED! SOMEBODY FELL FOR IT!'),
token_economy: () => announceSilly('TOKEN ECONOMY! SOMEBODY\'S GOING BANKRUPT!'),
}
const voiceFn = challengeVoice[event.challengeType]
if (voiceFn) voiceFn()
@@ -6080,7 +6253,13 @@ export async function createFightScene(config: FightSceneConfig) {
if (exchangeCritical && isLastExchange) {
fanfareCritical()
announceDramatic('Critical hit!')
announceDramatic([
'CRITICAL HIT! THAT\'S GONNA LEAVE A MARK!',
'CRITICAL HIT! SOMEBODY CHECK IF THAT\'S LEGAL!',
'CRITICAL! THEIR INSURANCE DOESN\'T COVER THIS!',
'CRITICAL HIT! THAT WAS PERSONAL!',
'CRITICAL! EVEN THE REFEREE FELT THAT!',
][Math.floor(Math.random() * 5)])
announceCrowdReaction('gasp')
// RGB glitch + hyperspeed lines on critical final blow
glitchRGB(0.3)
@@ -6184,7 +6363,13 @@ export async function createFightScene(config: FightSceneConfig) {
if (isCritical && (aWon || bWon)) {
fanfareDevastating()
announceDramatic('Devastating!')
announceDramatic([
'DEVASTATING! SOMEBODY CALL A DOCTOR!',
'DEVASTATING! THAT BOT HAS A FAMILY!',
'ABSOLUTELY DEVASTATING! THE CROWD IS LOSING IT!',
'DEVASTATING! EVEN THE JANITOR FELT THAT!',
'THAT WAS DEVASTATING AND I\'M NOT EVEN BEING DRAMATIC!',
][Math.floor(Math.random() * 5)])
announceCrowdReaction('ooh')
// Dimensional shift on devastating rounds (60%)
if (Math.random() < 0.6) dimensionalShift(0.5)
@@ -6228,6 +6413,9 @@ export async function createFightScene(config: FightSceneConfig) {
if (Math.random() < 0.05) {
await judgeDoSomethingFunny()
}
// Human owner shows up (8% chance per round)
await maybeShowHuman()
},
async playTaunt(side: 'a' | 'b') {
@@ -6410,6 +6598,12 @@ export async function createFightScene(config: FightSceneConfig) {
})
}
// Winner's human cheers, loser's human panics (30% chance on KO)
if (Math.random() < 0.3) {
spawnHumanCoach(winningSide, 'cheer')
spawnHumanCoach(winningSide === 'a' ? 'b' : 'a', 'panic')
}
// === Common ending: KO + celebration ===
announceFinishHim()
sfxKO()