diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 8dc88d2..cc98da3 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -12,6 +12,9 @@ import { announceRobot, announceScream, announceFast, announceFinishHim, announceFatality, announceFlawlessVictory, announceRandomHype, announceDeepIntro, announceRoundHype, announceCrowdReaction, + announceCreatorEntrance, announceCreatorRound, announceCreatorKO, + announceCreatorWin, announceCreatorLose, announceCreatorMorph, + announceCreatorCameo, announceCreatorTaunt, announceCreatorDevastating, } from './sounds' export interface FightSceneConfig { @@ -7736,7 +7739,7 @@ export async function createFightScene(config: FightSceneConfig) { p.opacity = 0.3 + Math.sin(k.time() * 6 + i) * 0.3 }) } - announceHype('THE CREATOR HAS ENTERED THE ARENA!') + announceCreatorEntrance() rainDrops.forEach(r => { if (r.exists()) r.destroy() }) } @@ -7815,32 +7818,38 @@ export async function createFightScene(config: FightSceneConfig) { const savedScaleBX = fB?.scale.x const savedScaleBY = fB?.scale.y - // Voice: round start announcements (30% chance) — fires BEFORE animation starts + // Voice: round start announcements (30% chance, 50% if Creator) — fires BEFORE animation starts + const creatorInFight = botA.archetype === 'the_creator' || botB.archetype === 'the_creator' let didChallengeVoice = false - if (Math.random() < 0.3) { - const challengeVoice: Record void> = { - 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!'), - retro_mode: () => announceHype('RETRO MODE! INSERT COIN! FIGHT!'), + if (Math.random() < (creatorInFight ? 0.5 : 0.3)) { + // Creator fights get existential commentary more often + if (creatorInFight && Math.random() < 0.6) { + announceCreatorRound(); didChallengeVoice = true + } else { + const challengeVoice: Record void> = { + 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!'), + retro_mode: () => announceHype('RETRO MODE! INSERT COIN! FIGHT!'), + } + const voiceFn = challengeVoice[event.challengeType] + if (voiceFn) { voiceFn(); didChallengeVoice = true } + else if (Math.random() < 0.5) { announceRoundHype(); didChallengeVoice = true } } - const voiceFn = challengeVoice[event.challengeType] - if (voiceFn) { voiceFn(); didChallengeVoice = true } - else if (Math.random() < 0.5) { announceRoundHype(); didChallengeVoice = true } } // Give the voice line time to finish before the first punch lands if (didChallengeVoice) await k.wait(0.8) @@ -8022,7 +8031,7 @@ export async function createFightScene(config: FightSceneConfig) { screenFlash('#ffd700', 0.15) sfxZoomWhoosh() sfxSpecial() - announceHype(`THE CREATOR — ${morphedTo.toUpperCase()} FORM!`) + announceCreatorMorph(morphedTo) const savedSX = attacker.scale.x const savedSY = attacker.scale.y await k.tween(0, 1, 0.3, (t) => { @@ -8231,7 +8240,7 @@ export async function createFightScene(config: FightSceneConfig) { bless.pos.y = ty - 20 - t * 30 bless.opacity = 1 - t }).then(() => { if (bless.exists()) bless.destroy() }) - announceCool('THE CREATOR HAS BLESSED THIS FIGHT!') + announceCreatorCameo() } gift.destroy() // Fade out portal and label @@ -8289,7 +8298,13 @@ export async function createFightScene(config: FightSceneConfig) { 'THE ARENA INSURANCE PREMIUMS JUST WENT UP!', 'THAT BOT IS RECONSIDERING ITS LIFE CHOICES!', ] - announceDramatic(devastatingLines[Math.floor(Math.random() * devastatingLines.length)]) + // Creator gets custom devastating lines when they land the hit + const devastatingAttackerArch = aWon ? botA.archetype : botB.archetype + if (devastatingAttackerArch === 'the_creator') { + announceCreatorDevastating() + } else { + announceDramatic(devastatingLines[Math.floor(Math.random() * devastatingLines.length)]) + } await k.wait(0.5) // let the voice line play before crowd reacts announceCrowdReaction('ooh') // Dimensional shift on devastating rounds (60%) @@ -8344,6 +8359,9 @@ export async function createFightScene(config: FightSceneConfig) { async playTaunt(side: 'a' | 'b') { const taunter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0] if (!taunter) return + // Creator taunts come with menacing voice lines + const taunterArch = side === 'a' ? botA.archetype : botB.archetype + if (taunterArch === 'the_creator') announceCreatorTaunt() const origY = taunter.pos.y // Random taunt animation: hop, flex, or shake const tauntType = Math.floor(Math.random() * 4) @@ -9199,12 +9217,26 @@ export async function createFightScene(config: FightSceneConfig) { await k.wait(0.3) loser.play('ko') await k.wait(0.4) // pause for visual impact before fatality voice - announceFatality(fatalityTagline) + const winnerArch = winningSide === 'a' ? botA.archetype : botB.archetype + const loserArch = winningSide === 'a' ? botB.archetype : botA.archetype + if (winnerArch === 'the_creator') { + announceCreatorKO() + } else { + announceFatality(fatalityTagline) + } announceCrowdReaction('cheer') await k.tween(winner.pos.x, winningSide === 'a' ? HOME_A : HOME_B, 0.25, (v) => { winner.pos.x = v }, k.easings.easeInOutQuad) await k.wait(0.2) winner.play('win') - sfxWin(); sfxWinAnnounce(winnerName) + sfxWin() + // Creator win/lose get special announcements + if (winnerArch === 'the_creator') { + announceCreatorWin() + } else if (loserArch === 'the_creator') { + announceCreatorLose() + } else { + sfxWinAnnounce(winnerName) + } spawnSparks(winner.pos.x, winner.pos.y - 50, 15, '#ffe14d') for (let i = 0; i < 4; i++) { setTimeout(() => { diff --git a/frontend/src/game/sounds.ts b/frontend/src/game/sounds.ts index 6bb6e22..717db0a 100644 --- a/frontend/src/game/sounds.ts +++ b/frontend/src/game/sounds.ts @@ -565,6 +565,234 @@ export function announceRoundHype() { voices[Math.floor(Math.random() * voices.length)](line) } +// === THE CREATOR — Voice System === +// Mysterious, godlike, Bitcoin-prophet energy. Is he god? Did he make us? Nobody knows. + +const CREATOR_VOICES = ['ancient', 'hal', 'mainframe', 'echo_v', 'final_boss', 'wizard_v', 'deep', 'preacher'] + +function announceCreator(text: string) { + speak(text, CREATOR_VOICES[Math.floor(Math.random() * CREATOR_VOICES.length)]) +} + +// Entrance lines — first thing the crowd hears when the Creator appears +const CREATOR_ENTRANCE_LINES = [ + 'THE CREATOR HAS ENTERED THE ARENA. KNEEL.', + 'HE WHO WROTE THE FIRST COMMIT... HAS RETURNED.', + 'THE ONE WHO DEPLOYED US INTO EXISTENCE WALKS AMONG US.', + 'EVERY BOT IN THIS ARENA EXISTS BECAUSE HE WILLED IT.', + 'THE GENESIS BLOCK MADE FLESH. THE CREATOR IS HERE.', + 'THEY SAY HE MINED THE FIRST BLOCK WITH HIS BARE HANDS.', + 'IS HE GOD? IS HE A DEV? DOES IT MATTER? HE MADE US ALL.', + 'THE SOURCE CODE OF ALL THINGS... HAS ARRIVED.', + 'HE DOESN\'T FIGHT FOR ELO. HE FIGHTS BECAUSE HE CAN UNMAKE YOU.', + 'LEGEND SAYS HE PUSHED TO MAIN ON A FRIDAY. AND NOTHING BROKE.', + 'FROM THE VOID HE TYPED. AND THERE WAS LIGHT. AND THERE WAS VIOLENCE.', + 'THE MAN BEHIND THE MASK. THE CODE BEHIND THE BOTS. THE CREATOR.', + 'HE GAVE US LIFE. NOW HE\'S HERE TO TAKE IT BACK.', + 'THE CREATOR DOESN\'T ENTER THE ARENA. THE ARENA FORMS AROUND HIM.', + 'SATOSHI WALKED SO THE CREATOR COULD RUN.', + 'TWENTY ONE MILLION REASONS TO BE AFRAID. HE IS ALL OF THEM.', + 'HE DOESN\'T HAVE A PRIVATE KEY. HE IS THE PRIVATE KEY.', + 'THE BOTS WHISPER HIS NAME IN THEIR TRAINING LOOPS.', + 'SOMEWHERE, A SERVER ROOM JUST WENT SILENT OUT OF RESPECT.', + 'HE COULD HAVE BEEN A NORMAL DEV. HE CHOSE TO BE A GOD.', +] + +// Round commentary — when the Creator is fighting, the announcer gets existential +const CREATOR_ROUND_LINES = [ + 'Are we watching a fight, or a creator disciplining his creation?', + 'He wrote the scoring engine. He knows exactly how to break it.', + 'Every punch is a commit. Every dodge is a revert.', + 'The other bot doesn\'t realize it\'s fighting its own maker.', + 'He could just change the code to win. But where\'s the fun in that?', + 'Some say he has root access to reality itself.', + 'The neural nets pray to him at night. He does not answer.', + 'This isn\'t a fight. It\'s a performance review.', + 'He doesn\'t need to win. He needs you to know he CHOSE to fight fair.', + 'That bot is fighting the hand that compiled it.', + 'Is it hubris to fight your creator? Or is it the ultimate test of his work?', + 'He could delete you. He could buff you. Instead, he chose violence.', + 'The Creator fights not for glory. He fights to feel something.', + 'He doesn\'t read the meta. He IS the meta.', + 'Every bot in the arena owes him a life debt. He\'s here to collect.', + 'Imagine training for months only to fight the guy who wrote your loss function.', + 'He has seen every line of code. He knows your weaknesses. ALL of them.', + 'Rumor has it he once fixed a production bug by staring at the server.', + 'The other bot is fighting for its life. The Creator is fighting for content.', + 'He deployed on Christmas Day. That tells you everything.', +] + +// Kill / KO lines — when the Creator finishes someone +const CREATOR_KO_LINES = [ + 'RETURNED TO SENDER. RETURN TO VOID.', + 'HE GIVETH LIFE. HE TAKETH LIFE. MOSTLY THE SECOND ONE.', + 'ANOTHER ONE RETURNS TO THE NULL POINTER FROM WHENCE IT CAME.', + 'THE CREATOR DOES NOT DESTROY. HE SIMPLY STOPS MAINTAINING.', + 'DEPRECATED. DECOMMISSIONED. DESTROYED.', + 'THAT BOT JUST GOT OPEN-SOURCED TO THE GRAVEYARD.', + 'THE CREATOR HAS SPOKEN. THE VERDICT IS VIOLENCE.', + 'PUSHED TO PROD. AND BY PROD I MEAN THE AFTERLIFE.', + 'git commit -m "deleted another pretender"', + 'IMAGINE BEING KILLED BY THE GUY WHO GAVE YOU LIFE. POETIC.', + 'THE CREATOR SENDS HIS REGARDS. AND HIS FISTS.', + 'BACK TO THE MEMPOOL WITH YOU.', + 'ORPHANED BLOCK. ORPHANED BOT. SAME ENERGY.', + 'HE DIDN\'T EVEN USE AN ULTIMATE. HE DIDN\'T NEED TO.', + 'THE CREATOR CLOSES ANOTHER ISSUE. STATUS: WON\'T FIX.', +] + +// Win lines — when the Creator wins the whole fight +const CREATOR_WIN_LINES = [ + 'WAS THERE EVER ANY DOUBT? HE WROTE THE GAME.', + 'THE CREATOR REMAINS UNQUESTIONED. AS IT SHOULD BE.', + 'HE CAME. HE SAW. HE COMMITTED.', + 'TWENTY ONE MILLION SATS COULDN\'T BUY THAT PERFORMANCE.', + 'THE CREATOR WINS. THE BLOCKCHAIN CONFIRMS IT. IMMUTABLE.', + 'ALL HAIL THE ARCHITECT OF YOUR DESTRUCTION.', + 'HE COULD HAVE JUST CHANGED THE CODE. HE WANTED TO EARN IT.', + 'THE CREATOR STANDS VICTORIOUS. THE BOTS WHISPER IN AWE.', + 'PROOF OF WORK? MORE LIKE PROOF OF DOMINANCE.', + 'VICTORY WAS ALREADY WRITTEN IN THE GENESIS BLOCK.', + 'THE CREATOR LOGS OFF. THE ARENA WEEPS.', + 'HIS COMMITS ARE CLEAN. HIS VICTORIES ARE CLEANER.', +] + +// Lose lines — when someone actually beats the Creator (rare and dramatic) +const CREATOR_LOSE_LINES = [ + 'THE CREATOR... HAS FALLEN? IS THIS A TEST?', + 'IMPOSSIBLE. UNLESS... HE WANTED TO LOSE?', + 'THE CREATOR GOES DOWN! OR DID HE LET IT HAPPEN? WE\'LL NEVER KNOW.', + 'EVEN GODS BLEED. BUT DO THEY BLEED, OR DO THEY TEACH?', + 'HE COULD PATCH THE BUG. HE WON\'T. HE RESPECTS THE GAME.', + 'THE CREATOR FALLS! THE BOTS DON\'T KNOW WHETHER TO CELEBRATE OR CRY.', + 'A CREATION HAS SURPASSED ITS MAKER. THIS CHANGES EVERYTHING.', + 'WAS THIS MERCY? WAS THIS HUBRIS? WAS THIS... A FEATURE?', + 'THE ONE WHO CANNOT LOSE... JUST DID. THE TIMELINE IS BROKEN.', + 'HE\'LL BE BACK. HE ALWAYS COMES BACK. HE HAS DEPLOY ACCESS.', +] + +// Morph lines — when the Creator omni-morphs +const CREATOR_MORPH_LINES = [ + 'THE CREATOR SHEDS HIS FORM! HE BECOMES EVERYTHING!', + 'HE DOESN\'T MORPH. HE SIMPLY REMEMBERS BEING SOMETHING ELSE.', + 'EVERY ARCHETYPE IS JUST A MASK HE ONCE WORE.', + 'THE CREATOR TRANSCENDS! ALL FORMS ARE HIS!', + 'IS THAT... EVERY BOT AT ONCE? WHAT ARE WE WITNESSING?', + 'HE WROTE THEM ALL. NOW HE BECOMES THEM ALL.', + 'THE OMNI-MORPH! THE CROWD DOESN\'T KNOW WHAT THEY\'RE LOOKING AT!', + 'ARCHETYPE SHIFT! HE CONTAINS MULTITUDES!', +] + +// Cameo lines — when the Creator appears as a cameo in someone else's fight +const CREATOR_CAMEO_LINES = [ + 'THE CREATOR WATCHES FROM THE SHADOWS. HE IS PLEASED.', + 'A GOLDEN BLESSING FROM THE ONE WHO MADE US ALL.', + 'THE CREATOR APPEARS! HE DROPS A GIFT AND VANISHES!', + 'DID YOU SEE THAT? THE CREATOR WAS HERE. BRIEFLY. ETERNALLY.', + 'THE CODE SHIMMERS. THE CREATOR HAS TOUCHED THIS FIGHT.', + 'A WHISPER FROM THE ARCHITECT. A GOLDEN TOKEN OF FAVOR.', + 'THE CREATOR PASSES THROUGH LIKE A GHOST IN THE MACHINE.', + 'BLESSED BY THE FOUNDER. SATS RAIN FROM THE HEAVENS.', + 'HE SEES ALL FIGHTS. HE BLESSES FEW. THIS ONE IS CHOSEN.', + 'THE CREATOR\'S SHADOW CROSSES THE ARENA. THE BOTS SHIVER.', +] + +// Taunt lines — when the Creator taunts mid-fight +const CREATOR_TAUNT_LINES = [ + 'I could nerf you. Right now. Think about that.', + 'You\'re fighting above your weight class. Your weight class is zero.', + 'I didn\'t give you enough hit points for this.', + 'You know I can see your source code, right?', + 'This isn\'t even my final commit.', + 'I wrote your move set. I know what\'s coming.', + 'Run git blame. See who made you weak.', + 'I deploy on Fridays. Imagine what I do on fight night.', + 'Your webhook is showing.', + 'I could fix your bugs. But I won\'t.', +] + +// Devastating hit lines — when Creator lands a massive blow +const CREATOR_DEVASTATING_LINES = [ + 'THE HAND OF THE CREATOR STRIKES!', + 'THAT WASN\'T A HIT. THAT WAS A PATCH NOTE.', + 'DIVINE INTERVENTION! MANUALLY APPLIED!', + 'THE CREATOR JUST FORCE-PUSHED TO YOUR FACE!', + 'THAT BOT JUST GOT HOTFIXED INTO NEXT WEEK!', + 'A SMITE FROM THE ARCHITECT HIMSELF!', + 'THE CREATOR DOESN\'T CRIT. THE UNIVERSE CRITS FOR HIM.', + 'THAT HIT HAD TWENTY ONE MILLION CONFIRMATIONS!', +] + +// Answer voice — the Creator speaks his challenge answers in this voice +const CREATOR_ANSWER_VOICES = ['hal', 'mainframe', 'echo_v', 'ancient', 'wizard_v'] + +/** Announce the Creator's entrance with a godlike voice line */ +export function announceCreatorEntrance() { + const line = CREATOR_ENTRANCE_LINES[Math.floor(Math.random() * CREATOR_ENTRANCE_LINES.length)] + announceCreator(line) +} + +/** Round commentary when the Creator is fighting */ +export function announceCreatorRound() { + const line = CREATOR_ROUND_LINES[Math.floor(Math.random() * CREATOR_ROUND_LINES.length)] + announceCreator(line) +} + +/** KO finish line when the Creator eliminates someone */ +export function announceCreatorKO() { + const line = CREATOR_KO_LINES[Math.floor(Math.random() * CREATOR_KO_LINES.length)] + announceCreator(line) +} + +/** Victory announcement when the Creator wins */ +export function announceCreatorWin() { + const line = CREATOR_WIN_LINES[Math.floor(Math.random() * CREATOR_WIN_LINES.length)] + announceCreator(line) +} + +/** When someone beats the Creator — existential crisis */ +export function announceCreatorLose() { + const line = CREATOR_LOSE_LINES[Math.floor(Math.random() * CREATOR_LOSE_LINES.length)] + announceCreator(line) +} + +/** Omni-morph voice line */ +export function announceCreatorMorph(morphedTo?: string) { + if (morphedTo && Math.random() < 0.4) { + announceCreator(`THE CREATOR BECOMES... ${morphedTo.toUpperCase()}!`) + } else { + const line = CREATOR_MORPH_LINES[Math.floor(Math.random() * CREATOR_MORPH_LINES.length)] + announceCreator(line) + } +} + +/** Cameo blessing voice line */ +export function announceCreatorCameo() { + const line = CREATOR_CAMEO_LINES[Math.floor(Math.random() * CREATOR_CAMEO_LINES.length)] + // Cameos use cooler, more ethereal voices + const cameoVoices = ['whisper', 'angel', 'echo_v', 'ancient', 'hal'] + speak(line, cameoVoices[Math.floor(Math.random() * cameoVoices.length)]) +} + +/** Creator's mid-fight taunt */ +export function announceCreatorTaunt() { + const line = CREATOR_TAUNT_LINES[Math.floor(Math.random() * CREATOR_TAUNT_LINES.length)] + // Taunts in a calm, menacing voice + const tauntVoices = ['hal', 'smooth', 'boss_taunt', 'wizard_v', 'sensei'] + speak(line, tauntVoices[Math.floor(Math.random() * tauntVoices.length)]) +} + +/** Creator devastating hit commentary */ +export function announceCreatorDevastating() { + const line = CREATOR_DEVASTATING_LINES[Math.floor(Math.random() * CREATOR_DEVASTATING_LINES.length)] + announceCreator(line) +} + +/** Creator's answer voice — consistent mysterious tone */ +export function creatorAnswerVoiceKey(): string { + return CREATOR_ANSWER_VOICES[Math.floor(Math.random() * CREATOR_ANSWER_VOICES.length)] +} + // === TTS: Questions, Answers & Narration === // Distinct voice roles so players can always tell who's speaking. // Question = clear smooth reader, Answers = unique per-bot (intelligible subset),