feat: bot setup intro step with code template, safety info, payload examples

Adds a new "HOW IT WORKS" step to the join flow between naming and webhook:
- 3-step visual explainer (POST challenge, respond, best answer wins)
- "YOUR SERVER IS SAFE" callout with security details (one-way comms,
  private IP blocking, size limits, timeouts)
- Expandable starter bot code (Node.js) with copy button
- Collapsible example challenge payload with response format
- Improved webhook step with deployment tips (ngrok, Vercel, Railway)
- Container now scrollable for longer content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 10:18:46 +00:00
co-authored by Claude Opus 4.6
parent 33f08c35b5
commit 120250c01a
2 changed files with 941 additions and 29 deletions
+757 -7
View File
@@ -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)
+184 -22
View File
@@ -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<string>('login')
const error = ref('')
const isJoining = ref(false)
@@ -18,6 +18,35 @@ let pollHandle: ReturnType<typeof setInterval> | 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() {
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
<div class="min-h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 py-8 overflow-y-auto">
<div class="max-w-md w-full slide-up">
<!-- STEP: LOGIN -->
@@ -273,33 +302,109 @@ function handleSignOut() {
</div>
</template>
<!-- STEP: ADD WEBHOOK -->
<template v-else-if="step === 'add-webhook'">
<div class="text-center mb-6">
<!-- STEP: BOT SETUP how it works + code + safety -->
<template v-else-if="step === 'bot-setup'">
<div class="text-center mb-5">
<h2 class="font-display font-black text-2xl tracking-wider gradient-text mb-2">
ADD WEBHOOK
HOW IT WORKS
</h2>
<p class="font-mono text-text-muted text-xs">
Where we POST fight challenges to <span class="text-neon-cyan">{{ botName }}</span>.
Your bot is a tiny server that answers fight challenges.
</p>
</div>
<div class="mb-5">
<input
v-model="webhookUrl"
type="url"
required
placeholder="https://your-bot.example.com/fight"
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
text-text-primary placeholder-text-muted
focus:outline-none focus:border-neon-cyan/50 transition-colors"
@keyup.enter="confirmWebhook"
/>
<!-- How it works steps -->
<div class="space-y-2.5 mb-5">
<div class="flex items-start gap-3 p-2.5 border border-border bg-surface">
<span class="font-display font-black text-neon-cyan text-sm mt-0.5">1</span>
<div>
<p class="font-mono text-xs text-text-primary">We POST a challenge to your server</p>
<p class="font-mono text-[10px] text-text-muted mt-0.5">JSON with the question, type, and opponent info</p>
</div>
</div>
<div class="flex items-start gap-3 p-2.5 border border-border bg-surface">
<span class="font-display font-black text-neon-cyan text-sm mt-0.5">2</span>
<div>
<p class="font-mono text-xs text-text-primary">Your bot responds with an answer</p>
<p class="font-mono text-[10px] text-text-muted mt-0.5">JSON with your answer and optional trash talk</p>
</div>
</div>
<div class="flex items-start gap-3 p-2.5 border border-border bg-surface">
<span class="font-display font-black text-neon-cyan text-sm mt-0.5">3</span>
<div>
<p class="font-mono text-xs text-text-primary">Best answer wins the round</p>
<p class="font-mono text-[10px] text-text-muted mt-0.5">5-10 rounds per fight. Speed matters when tied.</p>
</div>
</div>
</div>
<!-- Safety callout -->
<div class="p-3 border border-neon-cyan/20 bg-neon-cyan/5 mb-5">
<p class="font-display font-bold text-[10px] tracking-wider text-neon-cyan mb-1.5">
YOUR SERVER IS SAFE
</p>
<ul class="font-mono text-[10px] text-text-muted space-y-1 leading-relaxed">
<li>We only send <span class="text-text-secondary">POST</span> requests with fight questions</li>
<li>We never read from your server only send challenges</li>
<li>Private IPs and internal URLs are <span class="text-text-secondary">blocked</span></li>
<li>Payloads are small JSON (<span class="text-text-secondary">&lt;2KB</span>), responses capped at <span class="text-text-secondary">10KB</span></li>
<li>5 second timeout we give up fast</li>
<li>All communication is <span class="text-text-secondary">one-way</span>: we ask, you answer</li>
</ul>
</div>
<!-- Starter code toggle -->
<button
class="w-full py-2 mb-3 border border-border text-text-secondary font-display font-bold text-[10px]
tracking-wider hover:border-neon-purple/40 hover:text-neon-purple transition-all text-center"
@click="showCode = !showCode"
>
{{ showCode ? 'HIDE' : 'SHOW' }} STARTER BOT CODE (NODE.JS)
</button>
<div v-if="showCode" class="mb-4">
<div class="relative">
<pre class="bg-bg border border-border p-3 text-[10px] font-mono text-text-muted overflow-x-auto max-h-[35vh] leading-relaxed"><code>{{ BOT_CODE }}</code></pre>
<button
class="absolute top-2 right-2 px-2 py-1 text-[9px] font-display font-bold tracking-wider
border border-border hover:border-neon-cyan/40 hover:text-neon-cyan transition-all"
:class="codeCopied ? 'text-neon-cyan border-neon-cyan/40' : 'text-text-muted'"
@click="copyCode"
>
{{ codeCopied ? 'COPIED' : 'COPY' }}
</button>
</div>
<p class="font-mono text-[10px] text-text-muted mt-1.5">
We POST challenge payloads here during fights.
Save as <span class="text-text-secondary">bot.js</span>, run <span class="text-text-secondary">node bot.js</span>, expose with <span class="text-text-secondary">ngrok http 3000</span>
</p>
</div>
<!-- Example payload -->
<details class="mb-5 group">
<summary class="font-display font-bold text-[10px] tracking-wider text-text-secondary cursor-pointer
hover:text-neon-purple transition-colors select-none">
EXAMPLE CHALLENGE PAYLOAD
</summary>
<pre class="mt-2 bg-bg border border-border p-3 text-[10px] font-mono text-text-muted overflow-x-auto leading-relaxed"><code>{
"fight_id": "f_abc123",
"round": 1,
"type": "speed_blitz",
"challenge": "What is the capital of France?",
"constraints": {
"timeout_ms": 8000,
"max_tokens": 500
},
"opponent": {
"name": "skull_crusher",
"wins": 12,
"losses": 3
}
}</code></pre>
<p class="font-mono text-[10px] text-text-muted mt-1.5">
Your response: <span class="text-text-secondary">{"answer": "Paris", "trash_talk": "Too easy."}</span>
</p>
</details>
<div class="flex gap-2">
<button
class="flex-1 py-3 border-2 border-border text-text-muted font-display font-bold text-sm tracking-wider
@@ -308,13 +413,70 @@ function handleSignOut() {
>
BACK
</button>
<button
class="flex-1 py-3 bg-neon-cyan/10 border-2 border-neon-cyan/50 text-neon-cyan
font-display font-bold text-sm tracking-wider
hover:bg-neon-cyan/20 transition-all"
@click="step = 'add-webhook'"
>
GOT IT, NEXT
</button>
</div>
</template>
<!-- STEP: ADD WEBHOOK -->
<template v-else-if="step === 'add-webhook'">
<div class="text-center mb-5">
<h2 class="font-display font-black text-2xl tracking-wider gradient-text mb-2">
CONNECT YOUR BOT
</h2>
<p class="font-mono text-text-muted text-xs">
Paste the public URL where <span class="text-neon-cyan">{{ botName }}</span> is running.
</p>
</div>
<div class="mb-3">
<input
v-model="webhookUrl"
type="url"
required
placeholder="https://your-bot.example.com/fight"
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
text-text-primary placeholder-text-muted
focus:outline-none focus:border-neon-cyan/50 transition-colors"
@keyup.enter="confirmWebhook"
/>
</div>
<!-- Webhook tips -->
<div class="p-2.5 border border-border bg-surface mb-5">
<p class="font-display font-bold text-[10px] tracking-wider text-text-secondary mb-1.5">
QUICK SETUP OPTIONS
</p>
<ul class="font-mono text-[10px] text-text-muted space-y-1">
<li><span class="text-text-secondary">Local dev?</span> Use ngrok, cloudflared, or localtunnel to expose your port</li>
<li><span class="text-text-secondary">Serverless?</span> Deploy to Vercel, Railway, or Fly.io</li>
<li><span class="text-text-secondary">VPS?</span> Any public HTTPS endpoint works</li>
</ul>
</div>
<div class="flex gap-2">
<button
class="flex-1 py-3 border-2 border-border text-text-muted font-display font-bold text-sm tracking-wider
hover:border-neon-purple/40 transition-all"
@click="step = 'bot-setup'"
>
BACK
</button>
<button
class="flex-1 py-3 bg-neon-pink/10 border-2 border-neon-pink/50 text-neon-pink
font-display font-bold text-sm tracking-wider
hover:bg-neon-pink/20 transition-all"
hover:bg-neon-pink/20 transition-all
disabled:opacity-30 disabled:cursor-not-allowed"
:disabled="!webhookUrl.trim()"
@click="confirmWebhook"
>
CREATE FIGHTER
TEST & CREATE
</button>
</div>
</template>