diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 964b349..baf8bbc 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -4,10 +4,12 @@ import { sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit, sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect, sfxRoundStart, sfxBlock, sfxDodge, sfxClash, sfxRandomSilly, sfxBoing, - sfxBonk, sfxZap, sfxSlideDown, sfxZoomWhoosh, sfxRapidPunch, + sfxBonk, sfxZap, sfxSlideDown, sfxSlideUp, sfxZoomWhoosh, sfxRapidPunch, + sfxSplat, sfxWomp, sfxPowerUp, sfxCoin, sfxFail, sfxDrumRoll, fanfareRound, fanfareFight, fanfareDevastating, fanfareCritical, fanfareCombo, startMusic, stopMusic, announce, announceRandom, announceDramatic, announceHype, announceSilly, announceCool, + announceRobot, announceScream, announceFast, announceFinishHim, announceFatality, announceFlawlessVictory, announceRandomHype, announceDeepIntro, announceRoundHype, announceCrowdReaction, } from './sounds' @@ -3362,6 +3364,90 @@ export async function createFightScene(config: FightSceneConfig) { { shape: 'rect', x: 0, y: -3, w: 26, h: 1, color: '#666666' }, // chain top { shape: 'rect', x: 0, y: 3, w: 26, h: 1, color: '#666666' }, // chain bottom ]}, + // Fatality props + rubber_chicken: { parts: [ + { shape: 'rect', x: -2, y: -12, w: 5, h: 20, color: '#ffdd44' }, // body + { shape: 'circle', x: 0, y: -14, w: 8, h: 8, color: '#ffee55' }, // head + { shape: 'rect', x: 2, y: -16, w: 8, h: 3, color: '#ff8800' }, // beak + { shape: 'circle', x: -1, y: -15, w: 2, h: 2, color: '#000000' }, // eye + { shape: 'rect', x: -1, y: 6, w: 3, h: 5, color: '#ff6600' }, // leg + ]}, + anvil: { parts: [ + { shape: 'rect', x: -14, y: -4, w: 28, h: 12, color: '#555555' }, // body + { shape: 'rect', x: -18, y: 6, w: 36, h: 5, color: '#444444' }, // base + { shape: 'rect', x: -10, y: -8, w: 20, h: 5, color: '#666666' }, // top + { shape: 'rect', x: 8, y: -12, w: 6, h: 5, color: '#777777' }, // horn + ]}, + flyswatter: { parts: [ + { shape: 'rect', x: -2, y: 0, w: 4, h: 35, color: '#cc8833' }, // handle + { shape: 'rect', x: -12, y: -14, w: 24, h: 16, color: '#44aa44' }, // paddle + { shape: 'rect', x: -10, y: -12, w: 20, h: 12, color: '#338833' }, // grid + ]}, + toilet: { parts: [ + { shape: 'rect', x: -12, y: -4, w: 24, h: 16, color: '#eeeeee' }, // bowl + { shape: 'rect', x: -14, y: -14, w: 28, h: 12, color: '#dddddd' }, // tank + { shape: 'circle', x: 0, y: 4, w: 16, h: 16, color: '#aaddff' }, // water + { shape: 'rect', x: -12, y: -5, w: 24, h: 3, color: '#cccccc' }, // seat + { shape: 'rect', x: 8, y: -12, w: 4, h: 6, color: '#888888' }, // handle + ]}, + keyboard: { parts: [ + { shape: 'rect', x: -20, y: -3, w: 40, h: 8, color: '#333333' }, // base + { shape: 'rect', x: -18, y: -2, w: 36, h: 6, color: '#444444' }, // keys area + { shape: 'rect', x: -16, y: -1, w: 4, h: 3, color: '#666666' }, // key + { shape: 'rect', x: -10, y: -1, w: 4, h: 3, color: '#666666' }, // key + { shape: 'rect', x: -4, y: -1, w: 4, h: 3, color: '#666666' }, // key + { shape: 'rect', x: 2, y: -1, w: 12, h: 3, color: '#666666' }, // spacebar + ]}, + disco_ball: { parts: [ + { shape: 'circle', x: 0, y: 0, w: 18, h: 18, color: '#cccccc' }, // ball + { shape: 'rect', x: -1, y: -12, w: 2, h: 5, color: '#888888' }, // chain + { shape: 'rect', x: -3, y: -2, w: 3, h: 3, color: '#ffffff' }, // mirror + { shape: 'rect', x: 2, y: 1, w: 3, h: 3, color: '#eeeeee' }, // mirror + { shape: 'rect', x: -1, y: 4, w: 3, h: 3, color: '#ffffff' }, // mirror + ]}, + rubber_duck: { parts: [ + { shape: 'circle', x: 0, y: 0, w: 10, h: 10, color: '#ffdd00' }, // body + { shape: 'circle', x: 3, y: -5, w: 7, h: 7, color: '#ffee22' }, // head + { shape: 'rect', x: 5, y: -6, w: 5, h: 2, color: '#ff8800' }, // beak + { shape: 'circle', x: 4, y: -6, w: 2, h: 2, color: '#000000' }, // eye + ]}, + baguette: { parts: [ + { shape: 'rect', x: -16, y: -3, w: 32, h: 6, color: '#cc9944' }, // bread + { shape: 'rect', x: -14, y: -2, w: 28, h: 4, color: '#ddaa55' }, // crust highlight + { shape: 'rect', x: -8, y: -4, w: 4, h: 1, color: '#bb8833' }, // score + { shape: 'rect', x: 2, y: -4, w: 4, h: 1, color: '#bb8833' }, // score + ]}, + boot: { parts: [ + { shape: 'rect', x: -8, y: -16, w: 14, h: 24, color: '#553322' }, // shaft + { shape: 'rect', x: -8, y: 6, w: 22, h: 8, color: '#442211' }, // sole + { shape: 'rect', x: -6, y: -14, w: 10, h: 4, color: '#664433' }, // top band + { shape: 'rect', x: 12, y: 4, w: 4, h: 6, color: '#442211' }, // toe + ]}, + cannon: { parts: [ + { shape: 'rect', x: -10, y: -6, w: 28, h: 12, color: '#444444' }, // barrel + { shape: 'circle', x: -10, y: 0, w: 14, h: 14, color: '#333333' }, // base + { shape: 'circle', x: 16, y: 0, w: 10, h: 10, color: '#555555' }, // muzzle + { shape: 'rect', x: -6, y: 6, w: 6, h: 8, color: '#664433' }, // wheel + ]}, + stamp: { parts: [ + { shape: 'rect', x: -3, y: -18, w: 6, h: 14, color: '#884422' }, // handle + { shape: 'rect', x: -14, y: -6, w: 28, h: 8, color: '#cc4444' }, // stamp face + { shape: 'rect', x: -12, y: -4, w: 24, h: 4, color: '#aa2222' }, // ink + ]}, + vending_machine: { parts: [ + { shape: 'rect', x: -12, y: -18, w: 24, h: 36, color: '#cc3333' }, // body + { shape: 'rect', x: -10, y: -14, w: 12, h: 20, color: '#222222' }, // window + { shape: 'rect', x: 3, y: -8, w: 6, h: 4, color: '#44aa44' }, // button + { shape: 'rect', x: 3, y: 0, w: 6, h: 4, color: '#4444aa' }, // button + { shape: 'rect', x: -8, y: 10, w: 10, h: 5, color: '#333333' }, // dispenser + ]}, + lawn_mower: { parts: [ + { shape: 'rect', x: -10, y: -2, w: 20, h: 8, color: '#44aa44' }, // body + { shape: 'circle', x: -8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel + { shape: 'circle', x: 8, y: 6, w: 6, h: 6, color: '#333333' }, // wheel + { shape: 'rect', x: -2, y: -12, w: 3, h: 12, color: '#888888' }, // handle + { shape: 'rect', x: -4, y: -14, w: 8, h: 3, color: '#666666' }, // grip + ]}, } // Spawn a multi-part pixel prop at position, returns array of game objects @@ -5439,7 +5525,7 @@ export async function createFightScene(config: FightSceneConfig) { 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.scale(fighter.scale.x / 3, fighter.scale.y / 3), k.z(fighter.z + 1), k.opacity(0), ]) @@ -5471,7 +5557,7 @@ export async function createFightScene(config: FightSceneConfig) { /** 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 + if (roll > 0.15) return // 15% chance per round const side: 'a' | 'b' = Math.random() < 0.5 ? 'a' : 'b' const action = Math.random() @@ -6414,7 +6500,7 @@ export async function createFightScene(config: FightSceneConfig) { await judgeDoSomethingFunny() } - // Human owner shows up (8% chance per round) + // Human owner shows up (15% chance per round) await maybeShowHuman() }, @@ -6482,7 +6568,8 @@ export async function createFightScene(config: FightSceneConfig) { const dir = winningSide === 'a' ? 1 : -1 const loserOrigY = loser.pos.y const contactX = loser.pos.x - dir * 45 - const finishStyle = Math.floor(Math.random() * 4) + const finishStyle = Math.floor(Math.random() * 34) + let fatalityTagline = 'FATALITY!' if (finishStyle === 0) { // STYLE A: Classic rush-in combo + uppercut launch @@ -6557,7 +6644,7 @@ export async function createFightScene(config: FightSceneConfig) { sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.15) spawnShockwave(loser.pos.x, GROUND_Y, '#ffe14d') spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ffe14d') - } else { + } else if (finishStyle === 3) { // STYLE D: Pocket cannon finish — pull out massive gun, obliterate sfxSpecial() winner.play('special'); await k.wait(0.08) @@ -6582,6 +6669,669 @@ export async function createFightScene(config: FightSceneConfig) { } barrel.destroy() screenFlash('#ff6600', 0.2); k.shake(20) + } else if (finishStyle === 4) { + // BANANA PEEL SLIP — drops banana, loser slips into orbit + fatalityTagline = 'PEEL OUT!' + announceSilly('Watch your step!') + const peel = spawnWeaponProp('banana', loser.pos.x, GROUND_Y - 5, false, 14) + sfxBoing(); await k.wait(0.3) + sfxSlideDown(); sfxWomp() + loser.play('knockback'); k.shake(8) + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SLIP!', '#ffe135') + destroyProp(peel) + await Promise.all([ + k.tween(loser.pos.y, loserOrigY - 250, 0.4, (v) => { loser.pos.y = v }, k.easings.easeOutQuad), + k.tween(loser.pos.x, loser.pos.x + dir * 60, 0.4, (v) => { loser.pos.x = v }, k.easings.easeOutQuad), + ]) + sfxZoomWhoosh() + await k.tween(loser.pos.y, loserOrigY, 0.3, (v) => { loser.pos.y = v }, k.easings.easeInQuad) + sfxSplat(); k.shake(15); screenFlash('#ffe135', 0.15) + spawnShockwave(loser.pos.x, GROUND_Y, '#ffe135') + winner.play('win'); sfxRandomSilly() + } else if (finishStyle === 5) { + // ANVIL DROP — cartoon anvil from the sky + fatalityTagline = 'HEAVY METAL!' + announceDramatic('Look up!') + sfxDrumRoll() + const shadow = k.add([k.circle(20), k.pos(loser.pos.x, GROUND_Y + 2), k.color(safeColor(k, '#000000')), k.opacity(0.3), k.z(4)]) + await k.tween(0.3, 0.6, 0.5, (v) => { shadow.opacity = v }, k.easings.easeInQuad) + const anvil = spawnWeaponProp('anvil', loser.pos.x, -40, false, 20) + sfxZoomWhoosh() + for (const p of anvil) { + k.tween(p.pos.y, p.pos.y + GROUND_Y + 20, 0.25, (v) => { p.pos.y = v }, k.easings.easeInQuad) + } + await k.wait(0.25) + sfxExplosion(); sfxBonk(); k.shake(25); screenFlash('#888888', 0.2) + spawnSparks(loser.pos.x, GROUND_Y - 10, 20, '#888888') + spawnShockwave(loser.pos.x, GROUND_Y, '#666666') + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'CLANG!', '#cccccc') + await k.wait(0.4) + destroyProp(anvil); shadow.destroy() + winner.play('win') + } else if (finishStyle === 6) { + // RUBBER CHICKEN BEATDOWN + fatalityTagline = 'FOWL PLAY!' + announceSilly('Is that a rubber chicken!?') + const chicken = spawnWeaponProp('rubber_chicken', winner.pos.x + dir * 15, winner.pos.y - 30, dir < 0, 18) + await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + for (let i = 0; i < 8; i++) { + winner.play(i % 2 === 0 ? 'attack' : 'kick') + sfxBoing(); sfxRandomSilly() + loser.play('hit'); k.shake(3 + i) + for (const p of chicken) { p.pos.x = loser.pos.x + (Math.random() - 0.5) * 10; p.pos.y = loser.pos.y - 30 + (Math.random() - 0.5) * 10 } + spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 40 - Math.random() * 20, ['SQUEAK!', 'BAWK!', 'HONK!', 'SQUAWK!'][i % 4], '#ffdd44') + await k.wait(0.08) + } + sfxCritical(); k.shake(18); screenFlash('#ffdd44', 0.15) + loser.play('knockback') + spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#ffdd44') + destroyProp(chicken) + for (let i = 0; i < 10; i++) { + spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 120, loserOrigY - 80 - Math.random() * 60, 4, 3, '#ffffff', 200) + } + await k.wait(0.3) + } else if (finishStyle === 7) { + // GIANT FLYSWATTER — SMASH + fatalityTagline = 'SWATTED!' + announceScream('BUG REPORT FILED!') + sfxZoomWhoosh() + const swatter = spawnWeaponProp('flyswatter', loser.pos.x, -60, false, 20) + for (const p of swatter) { + k.tween(p.pos.y, p.pos.y + GROUND_Y + 40, 0.2, (v) => { p.pos.y = v }, k.easings.easeInQuad) + } + await k.wait(0.2) + sfxSplat(); sfxExplosion(); k.shake(30); screenFlash('#44aa44', 0.2) + await k.tween(loser.scale.y, 0.2, 0.1, (v) => { loser.scale.y = v }, k.easings.easeInQuad) + spawnEmoteText(loser.pos.x, loser.pos.y - 40, 'SPLAT!', '#44aa44') + spawnShockwave(loser.pos.x, GROUND_Y, '#44aa44') + await k.wait(0.5) + await k.tween(loser.scale.y, 1, 0.15, (v) => { loser.scale.y = v }, k.easings.easeOutBack) + destroyProp(swatter) + winner.play('win') + } else if (finishStyle === 8) { + // TOILET FLUSH + fatalityTagline = 'FLUSHED!' + announceSilly('Somebody call a plumber!') + const toilet = spawnWeaponProp('toilet', W / 2, GROUND_Y - 30, false, 12) + sfxBoing(); await k.wait(0.2) + sfxZoomWhoosh() + await k.tween(loser.pos.x, W / 2, 0.2, (v) => { loser.pos.x = v }, k.easings.easeInQuad) + loser.play('knockback'); sfxWomp() + for (let i = 0; i < 12; i++) { + const angle = (i / 12) * Math.PI * 4 + const radius = 30 - (i * 2) + loser.pos.x = W / 2 + Math.cos(angle) * radius + loser.pos.y = GROUND_Y - 30 + Math.sin(angle) * radius * 0.5 + sfxSlideDown() + await k.wait(0.04) + } + sfxExplosion(); k.shake(20); screenFlash('#aaddff', 0.2) + for (let i = 0; i < 12; i++) { + spawnProp(W / 2, GROUND_Y - 30, W / 2 + (Math.random() - 0.5) * 80, GROUND_Y - 60 - Math.random() * 40, 5, 5, '#4488ff', 300, true) + } + spawnEmoteText(W / 2, GROUND_Y - 80, 'FLUSH!', '#4488ff') + loser.pos.y = loserOrigY + await k.wait(0.3) + destroyProp(toilet) + winner.play('win') + } else if (finishStyle === 9) { + // KEYBOARD WARRIOR — CTRL+ALT+DELETE + fatalityTagline = 'CTRL ALT DELETED!' + announceRobot('Initiating keyboard protocol!') + const kb = spawnWeaponProp('keyboard', winner.pos.x + dir * 20, winner.pos.y - 25, dir < 0, 18) + sfxPowerUp(); await k.wait(0.15) + await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + winner.play('attack'); sfxBonk(); k.shake(12) + loser.play('hit') + const keyLabels = ['C', 'T', 'R', 'L', 'A', 'L', 'T', 'D', 'E', 'L'] + for (let i = 0; i < keyLabels.length; i++) { + const key = k.add([k.rect(6, 6), k.pos(loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 20), k.color(safeColor(k, '#666666')), k.z(20), k.opacity(1)]) + spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 40, loser.pos.y - 30 - Math.random() * 30, keyLabels[i], '#aaaaaa') + k.tween(key.pos.y, key.pos.y - 40 - Math.random() * 60, 0.4, (v) => { key.pos.y = v }, k.easings.easeOutQuad) + k.tween(1, 0, 0.5, (v) => { key.opacity = v }).then(() => { if (key.exists()) key.destroy() }) + sfxPunch(); await k.wait(0.04) + } + sfxCritical(); k.shake(20); screenFlash('#0000ff', 0.3) + loser.play('knockback') + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'BSOD!', '#ffffff') + destroyProp(kb) + await k.wait(0.3) + } else if (finishStyle === 10) { + // DISCO BALL DROP — Saturday Night Fatality + fatalityTagline = 'GROOVY DEATH!' + announceSilly('Get down! No really, GET DOWN!') + const discoColors = ['#ff0044', '#44ff00', '#0044ff', '#ff8800', '#ff00ff', '#00ffff'] + for (let i = 0; i < 6; i++) { setTimeout(() => screenFlash(discoColors[i], 0.06), i * 80) } + const disco = spawnWeaponProp('disco_ball', loser.pos.x, -30, false, 20) + sfxSlideDown(); sfxBoing() + for (const p of disco) { + k.tween(p.pos.y, p.pos.y + GROUND_Y - 40, 0.3, (v) => { p.pos.y = v }, k.easings.easeOutBounce) + } + await k.wait(0.3) + for (let i = 0; i < 6; i++) { + loser.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'special') + sfxRandomSilly(); screenFlash(discoColors[i], 0.04) + await k.wait(0.1) + } + sfxExplosion(); k.shake(22); screenFlash('#ffffff', 0.2) + spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff00ff') + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BOOGIE!', '#ff00ff') + await k.wait(0.3) + destroyProp(disco) + winner.play('win') + } else if (finishStyle === 11) { + // RUBBER DUCK ARMY — QUACKED + fatalityTagline = 'QUACKED!' + announceSilly('RELEASE THE QUACKEN!') + winner.play('special'); sfxPowerUp() + await k.wait(0.2) + for (let i = 0; i < 12; i++) { + const duck = spawnWeaponProp('rubber_duck', winner.pos.x + dir * 10, winner.pos.y - 20 + (Math.random() - 0.5) * 30, dir < 0, 16) + sfxBoing() + const targetY = loser.pos.y - 20 + (Math.random() - 0.5) * 30 + for (const p of duck) { + k.tween(p.pos.x, loser.pos.x + (Math.random() - 0.5) * 20, 0.15, (v) => { p.pos.x = v }, k.easings.easeInQuad) + k.tween(p.pos.y, targetY, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad).then(() => destroyProp(duck)) + } + if (i % 3 === 0) { loser.play(i % 2 === 0 ? 'hit' : 'knockback'); k.shake(4 + i); sfxBonk() } + await k.wait(0.05) + } + sfxExplosion(); k.shake(20); screenFlash('#ffdd00', 0.2) + spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ffdd00') + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'QUACK!', '#ffdd00') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 12) { + // BAGUETTE BEATDOWN + fatalityTagline = 'BON APPETIT!' + announceFast('Is that a day-old baguette!?') + const bread = spawnWeaponProp('baguette', winner.pos.x + dir * 15, winner.pos.y - 25, dir < 0, 18) + sfxBoing() + await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + for (let i = 0; i < 6; i++) { + winner.play(i % 2 === 0 ? 'attack' : 'kick') + sfxBonk(); loser.play('hit'); k.shake(5 + i * 2) + for (let c = 0; c < 3; c++) { + spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 50, loserOrigY - 30 - Math.random() * 40, 3, 3, '#cc9944', 250, true) + } + for (const p of bread) { p.pos.x = loser.pos.x + dir * -10; p.pos.y = loser.pos.y - 25 } + spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 40 - Math.random() * 15, ['BONK!', 'CRUST!', 'OUI!', 'PAIN!', 'CRUNCH!', 'HON HON!'][i], '#cc9944') + await k.wait(0.08) + } + sfxCritical(); k.shake(18); screenFlash('#cc9944', 0.15) + loser.play('knockback') + destroyProp(bread) + await k.wait(0.3) + } else if (finishStyle === 13) { + // GIANT BOOT — Monty Python style + fatalityTagline = 'BOOTED!' + announceDramatic('AND NOW FOR SOMETHING COMPLETELY DIFFERENT!') + sfxDrumRoll() + const boot = spawnWeaponProp('boot', loser.pos.x, -60, false, 22) + for (const p of boot) { p.scale = k.vec2(3, 3) } + await k.wait(0.3) + sfxZoomWhoosh() + for (const p of boot) { + k.tween(p.pos.y, p.pos.y + GROUND_Y + 60, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad) + } + await k.wait(0.15) + sfxExplosion(); sfxSplat(); k.shake(30); screenFlash('#553322', 0.2) + spawnShockwave(loser.pos.x, GROUND_Y, '#553322') + spawnSparks(loser.pos.x, GROUND_Y - 10, 25, '#442211') + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'STOMP!', '#553322') + await k.wait(0.5) + destroyProp(boot) + winner.play('win') + } else if (finishStyle === 14) { + // CANNON LAUNCH — fires loser out of a cannon + fatalityTagline = 'FIRED!' + announceDramatic('LOAD THE CANNON!') + const cannonProp = spawnWeaponProp('cannon', winner.pos.x + dir * 30, GROUND_Y - 15, dir < 0, 14) + sfxPowerUp(); await k.wait(0.3) + sfxZoomWhoosh() + await k.tween(loser.pos.x, winner.pos.x + dir * 30, 0.2, (v) => { loser.pos.x = v }, k.easings.easeInQuad) + loser.play('knockback'); loser.opacity = 0.3 + sfxBoing(); await k.wait(0.2) + sfxGunshot(); sfxExplosion(); k.shake(25) + loser.opacity = 1; screenFlash('#ff6600', 0.2) + spawnSparks(winner.pos.x + dir * 50, GROUND_Y - 20, 20, '#ff6600') + await Promise.all([ + k.tween(loser.pos.x, loser.pos.x + dir * 200, 0.3, (v) => { loser.pos.x = v }, k.easings.easeOutQuad), + k.tween(loser.pos.y, loserOrigY - 150, 0.15, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() => + k.tween(loser.pos.y, loserOrigY, 0.15, (v) => { loser.pos.y = v }, k.easings.easeInQuad) + ), + ]) + sfxBonk(); k.shake(15) + spawnShockwave(loser.pos.x, GROUND_Y, '#ff4400') + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BOOM!', '#ff6600') + destroyProp(cannonProp) + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 15) { + // GIANT STAMP — REJECTED + fatalityTagline = 'REJECTED!' + announceRobot('Processing termination request.') + sfxDrumRoll() + const stamp = spawnWeaponProp('stamp', loser.pos.x, loser.pos.y - 80, false, 22) + await k.wait(0.3) + sfxZoomWhoosh() + for (const p of stamp) { + k.tween(p.pos.y, p.pos.y + 60, 0.1, (v) => { p.pos.y = v }, k.easings.easeInQuad) + } + await k.wait(0.1) + sfxBonk(); sfxSplat(); k.shake(20); screenFlash('#cc4444', 0.2) + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'REJECTED', '#cc4444') + for (let i = 0; i < 8; i++) { + spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 80, GROUND_Y - 20 - Math.random() * 40, 4, 4, '#cc4444', 200, true) + } + await k.wait(0.4) + destroyProp(stamp) + winner.play('win') + } else if (finishStyle === 16) { + // VENDING MACHINE TIP + fatalityTagline = 'OUT OF ORDER!' + announceSilly('Should have read the warning label!') + const vm = spawnWeaponProp('vending_machine', winner.pos.x + dir * 40, GROUND_Y - 18, false, 14) + sfxPowerUp(); await k.wait(0.2) + winner.play('attack'); sfxPunch() + sfxZoomWhoosh() + for (const p of vm) { + k.tween(p.pos.x, loser.pos.x, 0.2, (v) => { p.pos.x = v }, k.easings.easeInQuad) + } + await k.wait(0.2) + sfxExplosion(); sfxBonk(); k.shake(25); screenFlash('#cc3333', 0.2) + spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#cc3333') + const snackColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff8844'] + for (let i = 0; i < 8; i++) { + spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 100, GROUND_Y - 40 - Math.random() * 60, 5, 8, snackColors[i % 5], 250) + } + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'CRUSHED!', '#cc3333') + destroyProp(vm) + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 17) { + // LAWN MOWER — mowed down + fatalityTagline = 'MOWED DOWN!' + announceFast('Rev it up!') + const mower = spawnWeaponProp('lawn_mower', winner.pos.x + dir * 10, GROUND_Y - 8, dir < 0, 16) + winner.play('special'); sfxPowerUp() + for (let i = 0; i < 3; i++) { sfxRandomSilly(); await k.wait(0.1) } + sfxZoomWhoosh() + for (const p of mower) { + k.tween(p.pos.x, loser.pos.x + dir * 40, 0.2, (v) => { p.pos.x = v }, k.easings.easeInQuad) + } + await k.tween(winner.pos.x, loser.pos.x - dir * 20, 0.2, (v) => { winner.pos.x = v }, k.easings.easeInQuad) + sfxExplosion(); k.shake(20); screenFlash('#44aa44', 0.2) + loser.play('knockback') + for (let i = 0; i < 15; i++) { + spawnProp(loser.pos.x, GROUND_Y - 5, loser.pos.x + (Math.random() - 0.5) * 80, GROUND_Y - 30 - Math.random() * 50, 3, 3, '#44aa44', 300, false) + } + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BRRRRR!', '#44aa44') + destroyProp(mower) + await k.wait(0.3) + } else if (finishStyle === 18) { + // DUBSTEP DROP — bass drops, floor breaks + fatalityTagline = 'DROP THE BASS!' + announceScream('WUB WUB WUB!') + sfxPowerUp() + const wubColors = ['#ff00ff', '#00ffff', '#ff8800', '#39ff14'] + for (let i = 0; i < 4; i++) { + screenFlash(wubColors[i], 0.05); k.shake(3 + i * 2); await k.wait(0.1) + } + sfxExplosion(); sfxExplosion(); k.shake(35); screenFlash('#ffffff', 0.15) + spawnShockwave(W / 2, GROUND_Y, '#ff00ff') + await k.wait(0.05) + spawnShockwave(W / 2, GROUND_Y, '#00ffff') + await k.wait(0.05) + spawnShockwave(W / 2, GROUND_Y, '#39ff14') + loser.play('knockback') + for (let i = 0; i < 5; i++) { + await k.tween(loser.pos.y, loserOrigY - 40, 0.04, (v) => { loser.pos.y = v }, k.easings.easeOutQuad) + sfxBonk(); k.shake(8) + await k.tween(loser.pos.y, loserOrigY, 0.04, (v) => { loser.pos.y = v }, k.easings.easeInQuad) + screenFlash(wubColors[i % 4], 0.03) + } + spawnSparks(loser.pos.x, loser.pos.y - 20, 25, '#ff00ff') + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'WUBWUB!', '#ff00ff') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 19) { + // 404 NOT FOUND — loser glitches out + fatalityTagline = 'ERROR 404!' + announceRobot('Error. Opponent not found.') + sfxZap() + for (let i = 0; i < 10; i++) { + loser.opacity = Math.random() > 0.5 ? 1 : 0.2 + loser.pos.x += (Math.random() - 0.5) * 15 + loser.play(['idle', 'attack', 'kick', 'hit', 'special'][Math.floor(Math.random() * 5)]) + screenFlash('#00ff00', 0.02); sfxRandomSilly() + await k.wait(0.06) + } + screenFlash('#0000ff', 0.3); sfxFail(); k.shake(20) + spawnEmoteText(loser.pos.x, loser.pos.y - 60, '404', '#00ff00') + spawnEmoteText(loser.pos.x, loser.pos.y - 40, 'NOT FOUND', '#00ff00') + loser.opacity = 0; await k.wait(0.3) + loser.opacity = 1; sfxWomp() + spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#00ff00') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 20) { + // PIE STORM — pies from everywhere + fatalityTagline = 'CREAM ATED!' + announceSilly('PIE PIE PIE!') + winner.play('special') + const pieAngles = [0, 45, 90, 135, 180, 225, 270, 315] + for (let i = 0; i < 8; i++) { + const rad = pieAngles[i] * Math.PI / 180 + const fromX = loser.pos.x + Math.cos(rad) * 120 + const fromY = loser.pos.y - 20 + Math.sin(rad) * 80 + const pie = spawnWeaponProp('pie', fromX, fromY, false, 16) + for (const p of pie) { + k.tween(p.pos.x, loser.pos.x, 0.15, (v) => { p.pos.x = v }, k.easings.easeInQuad) + k.tween(p.pos.y, loser.pos.y - 20, 0.15, (v) => { p.pos.y = v }, k.easings.easeInQuad).then(() => destroyProp(pie)) + } + sfxBoing(); await k.wait(0.06) + if (i % 2 === 0) { loser.play(i % 4 === 0 ? 'hit' : 'knockback'); sfxSplat(); k.shake(5 + i) } + } + sfxExplosion(); k.shake(22); screenFlash('#f5deb3', 0.2) + for (let i = 0; i < 15; i++) { + spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 100, loserOrigY - 40 - Math.random() * 50, 5, 5, '#ffffff', 250, true) + } + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SPLAT!', '#ffffff') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 21) { + // T-SHIRT CANNON — rapid fire merch + fatalityTagline = "MERCH'D!" + announceFast('FREE SHIRTS! FREE SHIRTS!') + winner.play('special') + const tCannon = k.add([k.rect(30, 15), k.pos(winner.pos.x + dir * 15, winner.pos.y - 25), k.color(safeColor(k, '#ff4488')), k.z(16)]) + sfxPowerUp(); await k.wait(0.15) + const shirtColors = ['#ff4444', '#4444ff', '#44ff44', '#ffff44', '#ff44ff', '#44ffff', '#ff8844', '#8844ff'] + for (let i = 0; i < 8; i++) { + sfxGunshot() + spawnProp(tCannon.pos.x + dir * 30, tCannon.pos.y, loser.pos.x + (Math.random() - 0.5) * 20, loser.pos.y - 20 + (Math.random() - 0.5) * 20, 10, 8, shirtColors[i], 600) + if (i % 2 === 0) { loser.play('hit'); sfxBonk(); k.shake(5 + i) } + await k.wait(0.06) + } + sfxCritical(); k.shake(18); screenFlash('#ff4488', 0.2) + loser.play('knockback') + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'XL!', '#ff4488') + tCannon.destroy() + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 22) { + // CONFETTI CANNON — party's over + fatalityTagline = "PARTY'S OVER!" + announceSilly('SURPRISE!') + winner.play('special'); sfxPowerUp(); await k.wait(0.2) + sfxExplosion(); k.shake(20); screenFlash('#ff88cc', 0.15) + const confettiColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff00', '#ff00ff', '#00ffff', '#ff8800', '#88ff00'] + for (let i = 0; i < 30; i++) { + spawnProp(winner.pos.x + dir * 30, winner.pos.y - 25, loser.pos.x + (Math.random() - 0.5) * 80, loserOrigY - 80 - Math.random() * 60, 4, 3, confettiColors[i % 8], 400 + Math.random() * 200) + } + sfxBoing(); sfxRandomSilly() + loser.play('hit'); k.shake(10); await k.wait(0.15) + loser.play('knockback'); sfxCritical(); k.shake(15) + await k.tween(loser.pos.x, loser.pos.x + dir * 50, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad) + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'YAY?', '#ff88cc') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 23) { + // BALLOON POP — inflate then pop + fatalityTagline = 'POPPED!' + announceSilly('Inflate! INFLATE!') + winner.play('special'); sfxPowerUp() + await k.tween(winner.pos.x, contactX, 0.12, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + const origSX = loser.scale.x; const origSY = loser.scale.y + for (let i = 0; i < 8; i++) { + const grow = 1 + (i + 1) * 0.15 + await k.tween(loser.scale.y, origSY * grow, 0.08, (v) => { loser.scale.x = origSX * grow; loser.scale.y = v }, k.easings.easeOutQuad) + sfxBoing(); loser.play(i % 2 === 0 ? 'hit' : 'idle') + loser.pos.y = loserOrigY - (grow - 1) * 15 + await k.wait(0.04) + } + sfxExplosion(); k.shake(25); screenFlash('#ff4488', 0.2) + loser.scale.x = origSX; loser.scale.y = origSY; loser.pos.y = loserOrigY + const balloonColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff'] + for (let i = 0; i < 15; i++) { + spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 120, loserOrigY - 60 - Math.random() * 80, 6, 4, balloonColors[i % 5], 250, true) + } + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'POP!', '#ff4488') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 24) { + // GIANT MAGNET — yanks loser back and forth + fatalityTagline = 'ATTRACTIVE FINISH!' + announceRobot('Magnetic field activated.') + winner.play('special'); sfxPowerUp() + const magnet = k.add([k.rect(20, 25), k.pos(winner.pos.x + dir * 20, winner.pos.y - 30), k.color(safeColor(k, '#cc0000')), k.z(18)]) + const magnetTop = k.add([k.rect(20, 8), k.pos(winner.pos.x + dir * 20, winner.pos.y - 50), k.color(safeColor(k, '#0000cc')), k.z(18)]) + sfxZap() + for (let i = 0; i < 6; i++) { + const pullTarget = i % 2 === 0 ? winner.pos.x + dir * 40 : (winningSide === 'a' ? HOME_B + 30 : HOME_A - 30) + sfxZoomWhoosh() + await k.tween(loser.pos.x, pullTarget, 0.1, (v) => { loser.pos.x = v }, k.easings.easeInOutQuad) + sfxBonk(); k.shake(6 + i * 2) + loser.play(i % 2 === 0 ? 'hit' : 'knockback') + spawnSparks(loser.pos.x, loser.pos.y - 20, 5, '#8888ff') + await k.wait(0.05) + } + sfxExplosion(); k.shake(22); screenFlash('#8888ff', 0.2) + spawnShockwave(loser.pos.x, GROUND_Y, '#8888ff') + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'BZZT!', '#8888ff') + magnet.destroy(); magnetTop.destroy() + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 25) { + // SLIDE WHISTLE — loser slides off screen + fatalityTagline = 'WHEEEE!' + announceSilly('Bye bye!') + sfxSlideUp(); winner.play('attack') + await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + sfxPunch(); sfxBoing(); k.shake(10) + loser.play('knockback') + sfxSlideUp() + await Promise.all([ + k.tween(loser.pos.y, -50, 0.5, (v) => { loser.pos.y = v }, k.easings.easeInQuad), + k.tween(loser.pos.x, loser.pos.x + dir * 30, 0.5, (v) => { loser.pos.x = v }, k.easings.easeInQuad), + ]) + spawnEmoteText(loser.pos.x, 20, '*', '#ffee00') + await k.wait(0.4) + sfxSlideDown(); sfxZoomWhoosh() + await k.tween(loser.pos.y, loserOrigY, 0.25, (v) => { loser.pos.y = v }, k.easings.easeInQuad) + sfxSplat(); sfxExplosion(); k.shake(20); screenFlash('#ffcc00', 0.15) + spawnShockwave(loser.pos.x, GROUND_Y, '#ffcc00') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 26) { + // SLAPSTICK LADDER — keeps bonking loser + fatalityTagline = 'SLAPSTICKED!' + announceSilly('Watch where you swing that thing!') + const ladder = k.add([k.rect(6, 50), k.pos(winner.pos.x + dir * 5, winner.pos.y - 40), k.color(safeColor(k, '#cc8833')), k.z(18), k.anchor('center')]) + sfxBoing() + for (let i = 0; i < 5; i++) { + winner.scale.x = -winner.scale.x + ladder.pos.x = winner.pos.x + (winner.scale.x > 0 ? 1 : -1) * 25 + sfxZoomWhoosh(); await k.wait(0.06) + sfxBonk(); k.shake(6 + i * 2) + loser.play(i % 2 === 0 ? 'hit' : 'knockback') + spawnEmoteText(loser.pos.x, loser.pos.y - 40 - Math.random() * 20, ['BONK!', 'CLONK!', 'WHACK!', 'THUD!', 'OOF!'][i], '#cc8833') + await k.wait(0.08) + } + sfxCritical(); k.shake(18); screenFlash('#cc8833', 0.15) + spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#cc8833') + ladder.destroy() + winner.scale.x = dir > 0 ? Math.abs(winner.scale.x) : -Math.abs(winner.scale.x) + spawnEmoteText(winner.pos.x, winner.pos.y - 50, '?', '#ffffff') + await k.wait(0.3) + } else if (finishStyle === 27) { + // STACK OVERFLOW — buried in code blocks + fatalityTagline = 'STACK OVERFLOW!' + announceRobot('Exception in thread main.') + sfxFail() + const blockColors = ['#282c34', '#1e1e1e', '#2d2d2d', '#1a1a2e', '#0d1117'] + for (let i = 0; i < 10; i++) { + const block = k.add([ + k.rect(18 + Math.random() * 12, 8 + Math.random() * 6), + k.pos(loser.pos.x + (Math.random() - 0.5) * 40, -20 - i * 15), + k.color(safeColor(k, blockColors[i % 5])), k.z(16 + i), k.opacity(0.9), + ]) + k.tween(block.pos.y, GROUND_Y - 5 - i * 6, 0.15 + i * 0.02, (v) => { block.pos.y = v }, k.easings.easeInQuad).then(() => { + sfxBonk(); k.shake(3) + setTimeout(() => { if (block.exists()) k.tween(1, 0, 0.8, (v) => { block.opacity = v }).then(() => { if (block.exists()) block.destroy() }) }, 800) + }) + spawnEmoteText(loser.pos.x + (Math.random() - 0.5) * 30, loser.pos.y - 30 - Math.random() * 30, ['{', '}', '()', '=>', '[]', ';;', 'nil', 'NaN', '???', '//'][i], '#00ff00') + loser.play(i % 3 === 0 ? 'hit' : 'idle') + await k.wait(0.06) + } + sfxExplosion(); k.shake(20); screenFlash('#00ff00', 0.2) + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'SEGFAULT', '#ff0000') + await k.wait(0.4) + winner.play('win') + } else if (finishStyle === 28) { + // GLITTER BOMB — fabulous destruction + fatalityTagline = 'FABULOUS!' + announceSilly('You will NEVER get this out of your hair!') + winner.play('special'); sfxPowerUp() + await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + sfxPunch(); k.shake(5) + sfxExplosion(); k.shake(25) + const glitterColors = ['#ff88cc', '#ffcc00', '#88ffcc', '#cc88ff', '#ff8888', '#88ccff', '#ffff88', '#ff88ff'] + for (let wave = 0; wave < 3; wave++) { + screenFlash(glitterColors[wave * 2], 0.04) + for (let i = 0; i < 15; i++) { + spawnProp(loser.pos.x, loser.pos.y - 20, loser.pos.x + (Math.random() - 0.5) * 140, loserOrigY - 80 - Math.random() * 80, 3, 3, glitterColors[Math.floor(Math.random() * 8)], 200 + Math.random() * 200, Math.random() > 0.5) + } + sfxBoing(); loser.play(wave === 2 ? 'knockback' : 'hit') + await k.wait(0.1) + } + spawnShockwave(loser.pos.x, GROUND_Y, '#ff88cc') + spawnEmoteText(loser.pos.x, loser.pos.y - 60, 'SPARKLE!', '#ff88cc') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 29) { + // SELFIE STICK — beats then takes selfie + fatalityTagline = 'SAY CHEESE!' + announceFast('Content creation time!') + const stick = k.add([k.rect(4, 40), k.pos(winner.pos.x + dir * 10, winner.pos.y - 50), k.color(safeColor(k, '#888888')), k.z(18)]) + const phone = k.add([k.rect(8, 12), k.pos(winner.pos.x + dir * 10, winner.pos.y - 70), k.color(safeColor(k, '#222222')), k.z(19)]) + sfxBoing() + await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + for (let i = 0; i < 4; i++) { + winner.play('attack'); sfxBonk() + stick.pos.x = loser.pos.x; phone.pos.x = loser.pos.x + loser.play('hit'); k.shake(5 + i * 2) + await k.wait(0.08) + } + sfxCritical(); k.shake(15); loser.play('knockback') + await k.wait(0.15) + winner.play('win'); sfxCoin(); screenFlash('#ffffff', 0.3) + spawnEmoteText(W / 2, GROUND_Y - 80, '#selfie', '#ff4488') + spawnSparks(phone.pos.x, phone.pos.y, 10, '#ffffff') + stick.destroy(); phone.destroy() + await k.wait(0.3) + } else if (finishStyle === 30) { + // DANCE OFF — served + fatalityTagline = 'SERVED!' + announceHype('DANCE BATTLE!') + sfxPowerUp() + const moves = ['attack', 'kick', 'special', 'kick', 'attack', 'special'] as const + for (let i = 0; i < moves.length; i++) { + winner.play(moves[i]); sfxRandomSilly() + spawnSparks(winner.pos.x, winner.pos.y - 30, 3, ['#ff00ff', '#00ffff', '#ffff00'][i % 3]) + await k.wait(0.08) + } + for (let i = 0; i < 4; i++) { + loser.play(moves[i]); sfxWomp() + loser.pos.x += (Math.random() - 0.5) * 10 + await k.wait(0.1) + } + sfxFail() + winner.play('special'); sfxCritical(); sfxExplosion() + k.shake(25); screenFlash('#ff00ff', 0.2) + spawnShockwave(loser.pos.x, GROUND_Y, '#ff00ff') + spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff00ff') + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SERVED!', '#ff00ff') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 31) { + // WHOOPEE CUSHION — total gas + fatalityTagline = 'TOTAL GAS!' + announceSilly('What is that SMELL!?') + const cushion = k.add([k.circle(20), k.pos(loser.pos.x, GROUND_Y - 5), k.color(safeColor(k, '#ff6688')), k.z(10), k.opacity(0.8)]) + sfxBoing(); await k.wait(0.2) + sfxWomp(); sfxRandomSilly(); k.shake(8) + await k.tween(20, 40, 0.15, (v) => { cushion.radius = v }, k.easings.easeOutQuad) + sfxExplosion() + for (let i = 0; i < 12; i++) { + const gas = k.add([ + k.circle(8 + Math.random() * 10), + k.pos(loser.pos.x + (Math.random() - 0.5) * 40, GROUND_Y - 10 - Math.random() * 30), + k.color(safeColor(k, '#88ff44')), k.opacity(0.5), k.z(15), + ]) + k.tween(gas.pos.y, gas.pos.y - 40 - Math.random() * 30, 0.6, (v) => { gas.pos.y = v }, k.easings.easeOutQuad) + k.tween(0.5, 0, 0.6, (v) => { gas.opacity = v }).then(() => { if (gas.exists()) gas.destroy() }) + } + k.shake(20); screenFlash('#88ff44', 0.15) + loser.play('knockback') + await k.tween(loser.pos.y, loserOrigY - 120, 0.2, (v) => { loser.pos.y = v }, k.easings.easeOutQuad) + await k.tween(loser.pos.y, loserOrigY, 0.15, (v) => { loser.pos.y = v }, k.easings.easeInQuad) + sfxSplat(); k.shake(12) + cushion.destroy() + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'P U!', '#88ff44') + await k.wait(0.3) + winner.play('win') + } else if (finishStyle === 32) { + // RICK ROLL — never gonna give you up + fatalityTagline = 'NEVER GONNA GIVE YOU UP!' + announceHype('You know the rules, and so do I!') + sfxPowerUp() + const rickMoves = ['attack', 'kick', 'special', 'idle', 'attack', 'kick'] as const + for (let i = 0; i < 6; i++) { + winner.play(rickMoves[i]) + const hop = winner.pos.y + await k.tween(winner.pos.y, hop - 15, 0.05, (v) => { winner.pos.y = v }, k.easings.easeOutQuad) + await k.tween(winner.pos.y, hop, 0.05, (v) => { winner.pos.y = v }, k.easings.easeInQuad) + sfxBoing() + screenFlash(['#ff4444', '#ff8800', '#ffff00', '#44ff44', '#4444ff', '#8844ff'][i], 0.03) + await k.wait(0.04) + } + await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeOutQuad) + winner.play('special'); sfxCritical(); sfxExplosion() + k.shake(25); screenFlash('#ff8800', 0.2) + loser.play('knockback') + spawnSparks(loser.pos.x, loser.pos.y - 20, 20, '#ff8800') + spawnShockwave(loser.pos.x, GROUND_Y, '#ff8800') + spawnEmoteText(loser.pos.x, loser.pos.y - 50, "RICK'D!", '#ff8800') + await k.wait(0.3) + winner.play('win') + } else { + // PIZZA CUTTER — the big wheel + fatalityTagline = 'SLICED!' + announceFast('Extra large, extra lethal!') + const wheel = k.add([k.circle(18), k.pos(winner.pos.x + dir * 30, GROUND_Y - 18), k.color(safeColor(k, '#cccccc')), k.z(18)]) + const handle = k.add([k.rect(6, 20), k.pos(winner.pos.x + dir * 30, GROUND_Y - 36), k.color(safeColor(k, '#553322')), k.z(17)]) + sfxPowerUp(); winner.play('special') + sfxZoomWhoosh() + await Promise.all([ + k.tween(wheel.pos.x, loser.pos.x, 0.2, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeInQuad), + k.tween(0, 720, 0.2, (v) => { wheel.angle = v }, k.easings.linear), + ]) + sfxCritical(); sfxSplat(); k.shake(20); screenFlash('#ffcc00', 0.15) + loser.play('knockback') + spawnSparks(loser.pos.x, loser.pos.y - 20, 15, '#ff6600') + await k.tween(wheel.pos.x, loser.pos.x + dir * 60, 0.1, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeOutQuad) + sfxZoomWhoosh() + await k.tween(wheel.pos.x, loser.pos.x, 0.1, (v) => { wheel.pos.x = v; handle.pos.x = v }, k.easings.easeInQuad) + sfxBonk(); k.shake(15) + for (let i = 0; i < 8; i++) { + spawnProp(loser.pos.x, loser.pos.y - 10, loser.pos.x + (Math.random() - 0.5) * 80, loserOrigY - 30 - Math.random() * 40, 4, 4, i % 2 === 0 ? '#ffcc00' : '#cc4400', 250, true) + } + wheel.destroy(); handle.destroy() + spawnEmoteText(loser.pos.x, loser.pos.y - 50, 'SLICED!', '#ff6600') + await k.wait(0.3) + winner.play('win') } // Judge stands up for KO call @@ -6612,7 +7362,7 @@ export async function createFightScene(config: FightSceneConfig) { k.shake(18); sfxExplosion() await k.wait(0.2) loser.play('ko') - announceFatality() + 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) diff --git a/frontend/src/pages/JoinBoutPage.vue b/frontend/src/pages/JoinBoutPage.vue index b9b5705..f15d806 100644 --- a/frontend/src/pages/JoinBoutPage.vue +++ b/frontend/src/pages/JoinBoutPage.vue @@ -7,7 +7,7 @@ import SpritePreview from '../components/SpritePreview.vue' const router = useRouter() const { pubkey, bot, profilePicUrl, isLoggedIn, hasExtension, isLoading, login, registerBot, logout } = useNostr() -// Steps: 'login' | 'pick-character' | 'name-bot' | 'add-webhook' | 'ready' +// Steps: 'login' | 'pick-character' | 'name-bot' | 'bot-setup' | 'add-webhook' | 'ready' const step = ref('login') const error = ref('') const isJoining = ref(false) @@ -18,6 +18,35 @@ let pollHandle: ReturnType | null = null const selectedArchetype = ref('standard') const botName = ref('') const webhookUrl = ref('') +const showCode = ref(false) +const codeCopied = ref(false) + +const BOT_CODE = `const http = require('http') + +const server = http.createServer((req, res) => { + if (req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }) + return res.end(JSON.stringify({ status: 'ok' })) + } + let body = '' + req.on('data', c => { body += c }) + req.on('end', () => { + const { type, challenge } = JSON.parse(body) + // Your bot logic goes here — answer the challenge! + const answer = type === 'webhook_test' ? 'pong' : challenge + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ answer, trash_talk: 'Too easy.' })) + }) +}) + +server.listen(3000, () => console.log('Bot running on :3000')) +` + +function copyCode() { + navigator.clipboard.writeText(BOT_CODE) + codeCopied.value = true + setTimeout(() => { codeCopied.value = false }, 2000) +} const archetypeList = [ { id: 'standard', label: 'FIGHTER', desc: 'Classic brawler' }, @@ -100,7 +129,7 @@ function confirmName() { return } error.value = '' - step.value = 'add-webhook' + step.value = 'bot-setup' } async function confirmWebhook() { @@ -151,7 +180,7 @@ function handleSignOut() { - -