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:
co-authored by
Claude Opus 4.6
parent
33f08c35b5
commit
120250c01a
@@ -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)
|
||||
|
||||
@@ -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"><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>
|
||||
|
||||
Reference in New Issue
Block a user