fix: DocsPage escaped quotes breaking template + missing rotate() on ₿ text objects
DocsPage: escaped \"answer\" inside v-for HTML attribute broke template
parsing causing infinite Vite error loop. Replaced with " entity.
FightScene: three k.text('₿') objects used .angle without k.rotate()
component, causing TS2339 build errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
da14ca81ee
commit
8a345f5e56
+263
-29
@@ -114,7 +114,7 @@ const TIER_ULTIMATES: Record<number, string[]> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The Creator — exclusive moves (50% ultimate chance, 100% on crits, custom pool)
|
// The Creator — exclusive moves (50% ultimate chance, 100% on crits, custom pool)
|
||||||
const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention']
|
const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention', 'bitcoinSwarm', 'satoshiTornado', 'hodlWave', 'bitcoinBarrage']
|
||||||
const CREATOR_ULTIMATES = ['ultimateGenesisBlock', 'ultimateTheHalving', 'ultimateFullNodeCrush', 'ultimateSatoshiVision']
|
const CREATOR_ULTIMATES = ['ultimateGenesisBlock', 'ultimateTheHalving', 'ultimateFullNodeCrush', 'ultimateSatoshiVision']
|
||||||
|
|
||||||
function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
|
function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
|
||||||
@@ -5719,26 +5719,31 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
atk.play('special')
|
atk.play('special')
|
||||||
sfxSpecial()
|
sfxSpecial()
|
||||||
await k.wait(0.2)
|
await k.wait(0.2)
|
||||||
// Rain golden ₿ coins from above
|
// Rain golden ₿ letters from above
|
||||||
const coinCount = isCritical ? 30 : 15
|
const coinCount = isCritical ? 40 : 20
|
||||||
for (let i = 0; i < coinCount; i++) {
|
for (let i = 0; i < coinCount; i++) {
|
||||||
const cx = def.pos.x + (Math.random() - 0.5) * 120
|
const cx = def.pos.x + (Math.random() - 0.5) * 140
|
||||||
const startY = def.pos.y - 200 - Math.random() * 100
|
const startY = def.pos.y - 200 - Math.random() * 120
|
||||||
|
const sz = isCritical ? 8 + Math.random() * 10 : 6 + Math.random() * 6
|
||||||
const coin = k.add([
|
const coin = k.add([
|
||||||
k.circle(isCritical ? 5 : 3),
|
k.text('₿', { size: sz }),
|
||||||
k.pos(cx, startY),
|
k.pos(cx, startY),
|
||||||
k.color(safeColor(k, Math.random() > 0.3 ? '#ffd700' : '#ffee88')),
|
k.color(safeColor(k, Math.random() > 0.3 ? '#ffd700' : Math.random() > 0.5 ? '#ffee88' : '#ff8c00')),
|
||||||
k.opacity(0.9),
|
k.opacity(0.9),
|
||||||
k.z(18),
|
k.z(18),
|
||||||
|
k.anchor('center'),
|
||||||
|
k.rotate(Math.random() * 360),
|
||||||
])
|
])
|
||||||
|
const spin = (Math.random() - 0.5) * 400
|
||||||
coin.onUpdate(() => {
|
coin.onUpdate(() => {
|
||||||
coin.pos.y += 600 * k.dt()
|
coin.pos.y += 650 * k.dt()
|
||||||
|
coin.angle += spin * k.dt()
|
||||||
if (coin.pos.y > def.pos.y + 10) {
|
if (coin.pos.y > def.pos.y + 10) {
|
||||||
coin.destroy()
|
coin.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (i % 4 === 0) sfxCoin()
|
if (i % 3 === 0) sfxCoin()
|
||||||
await k.wait(0.03)
|
await k.wait(0.02)
|
||||||
}
|
}
|
||||||
await k.wait(0.15)
|
await k.wait(0.15)
|
||||||
def.play(isCritical ? 'knockback' : 'hit')
|
def.play(isCritical ? 'knockback' : 'hit')
|
||||||
@@ -6013,6 +6018,218 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
atk.play('idle')
|
atk.play('idle')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Creator ₿ Swarm Variations ──
|
||||||
|
|
||||||
|
async function bitcoinSwarm(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
|
||||||
|
// Swarm of ₿ letters fly from creator toward defender with zigzag paths
|
||||||
|
atk.play('special')
|
||||||
|
sfxSpecial()
|
||||||
|
await k.wait(0.15)
|
||||||
|
const total = isCritical ? 30 : 18
|
||||||
|
const swarmThings: any[] = []
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const sx = atk.pos.x + dir * 10 + (Math.random() - 0.5) * 30
|
||||||
|
const sy = atk.pos.y - 50 + (Math.random() - 0.5) * 60
|
||||||
|
const sz = 6 + Math.random() * 8
|
||||||
|
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000']
|
||||||
|
const p = k.add([
|
||||||
|
k.text('₿', { size: sz }),
|
||||||
|
k.pos(sx, sy),
|
||||||
|
k.color(safeColor(k, colors[i % colors.length])),
|
||||||
|
k.opacity(0.9),
|
||||||
|
k.z(16),
|
||||||
|
k.anchor('center'),
|
||||||
|
k.rotate(Math.random() * 360),
|
||||||
|
])
|
||||||
|
const tx = def.pos.x + (Math.random() - 0.5) * 30
|
||||||
|
const ty = def.pos.y - 30 + (Math.random() - 0.5) * 40
|
||||||
|
const zigFreq = 4 + Math.random() * 4
|
||||||
|
const zigAmp = 12 + Math.random() * 15
|
||||||
|
const spinRate = (Math.random() - 0.5) * 600
|
||||||
|
swarmThings.push(p)
|
||||||
|
k.tween(0, 1, 0.3 + Math.random() * 0.15, (t) => {
|
||||||
|
p.pos.x = sx + (tx - sx) * t
|
||||||
|
p.pos.y = sy + (ty - sy) * t + Math.sin(t * Math.PI * zigFreq) * zigAmp
|
||||||
|
p.angle += spinRate * k.dt()
|
||||||
|
p.opacity = 0.9 - t * 0.3
|
||||||
|
}).then(() => { if (p.exists()) { spawnSparks(p.pos.x, p.pos.y, 2, '#ffd700'); p.destroy() } })
|
||||||
|
if (i % 3 === 0) sfxCoin()
|
||||||
|
await k.wait(0.02)
|
||||||
|
}
|
||||||
|
await k.wait(0.25)
|
||||||
|
sfxBonk()
|
||||||
|
def.play(isCritical ? 'knockback' : 'hit')
|
||||||
|
k.shake(isCritical ? 18 : 8)
|
||||||
|
if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() }
|
||||||
|
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 18 : 10, '#ffd700')
|
||||||
|
const push = dir * (isCritical ? 90 : 40)
|
||||||
|
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
|
||||||
|
await k.wait(0.3)
|
||||||
|
atk.play('idle')
|
||||||
|
swarmThings.forEach(s => { if (s.exists()) s.destroy() })
|
||||||
|
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function satoshiTornado(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
|
||||||
|
// ₿ letters spiral around the defender in a tightening vortex then explode
|
||||||
|
atk.play('special')
|
||||||
|
sfxSpecial()
|
||||||
|
sfxZoomWhoosh()
|
||||||
|
const total = isCritical ? 24 : 14
|
||||||
|
const vortex: any[] = []
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const sz = 7 + Math.random() * 7
|
||||||
|
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#ffffff']
|
||||||
|
const p = k.add([
|
||||||
|
k.text('₿', { size: sz }),
|
||||||
|
k.pos(def.pos.x, def.pos.y - 30),
|
||||||
|
k.color(safeColor(k, colors[i % colors.length])),
|
||||||
|
k.opacity(0.85),
|
||||||
|
k.z(18),
|
||||||
|
k.anchor('center'),
|
||||||
|
k.rotate(0),
|
||||||
|
])
|
||||||
|
vortex.push(p)
|
||||||
|
const baseAngle = (i / total) * Math.PI * 2
|
||||||
|
const startRadius = 80 + Math.random() * 30
|
||||||
|
p.onUpdate(() => {
|
||||||
|
const elapsed = k.time()
|
||||||
|
const shrink = Math.max(5, startRadius - elapsed * 40)
|
||||||
|
const a = baseAngle + elapsed * (6 + i * 0.3)
|
||||||
|
p.pos.x = def.pos.x + Math.cos(a) * shrink
|
||||||
|
p.pos.y = def.pos.y - 30 + Math.sin(a) * shrink * 0.6
|
||||||
|
p.angle = elapsed * 200 + i * 45
|
||||||
|
p.opacity = 0.6 + Math.sin(elapsed * 8 + i) * 0.3
|
||||||
|
})
|
||||||
|
await k.wait(0.04)
|
||||||
|
}
|
||||||
|
// Let the tornado spin for a moment
|
||||||
|
await k.wait(0.7)
|
||||||
|
// Explode outward
|
||||||
|
screenFlash('#ffd700', 0.12)
|
||||||
|
sfxExplosion()
|
||||||
|
k.shake(isCritical ? 22 : 10)
|
||||||
|
vortex.forEach((p, i) => {
|
||||||
|
if (!p.exists()) return
|
||||||
|
const angle = (i / total) * Math.PI * 2
|
||||||
|
const dist = 100 + Math.random() * 60
|
||||||
|
k.tween(0, 1, 0.3, (t) => {
|
||||||
|
p.pos.x = def.pos.x + Math.cos(angle) * dist * t
|
||||||
|
p.pos.y = def.pos.y - 30 + Math.sin(angle) * dist * 0.6 * t
|
||||||
|
p.opacity = 1 - t
|
||||||
|
}).then(() => { if (p.exists()) p.destroy() })
|
||||||
|
})
|
||||||
|
def.play(isCritical ? 'knockback' : 'hit')
|
||||||
|
if (isCritical) { sfxCritical(); glitchRGB() }
|
||||||
|
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 20 : 10, '#ffd700')
|
||||||
|
const push = dir * (isCritical ? 100 : 45)
|
||||||
|
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
|
||||||
|
await k.wait(0.4)
|
||||||
|
atk.play('idle')
|
||||||
|
vortex.forEach(p => { if (p.exists()) p.destroy() })
|
||||||
|
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hodlWave(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
|
||||||
|
// Wall of ₿ letters advancing across the screen like a tidal wave
|
||||||
|
atk.play('special')
|
||||||
|
sfxSpecial()
|
||||||
|
await k.wait(0.1)
|
||||||
|
const rows = isCritical ? 6 : 4
|
||||||
|
const cols = isCritical ? 10 : 7
|
||||||
|
const wave: any[] = []
|
||||||
|
const startX = atk.pos.x + dir * 25
|
||||||
|
const screenTop = -20
|
||||||
|
const screenBot = GROUND_Y + 10
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
const sz = 8 + Math.random() * 6
|
||||||
|
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000', '#ffffff']
|
||||||
|
const py = screenTop + (screenBot - screenTop) * (r / rows) + (Math.random() - 0.5) * 20
|
||||||
|
const p = k.add([
|
||||||
|
k.text('₿', { size: sz }),
|
||||||
|
k.pos(startX + c * dir * 8, py),
|
||||||
|
k.color(safeColor(k, colors[(r + c) % colors.length])),
|
||||||
|
k.opacity(0.85),
|
||||||
|
k.z(17),
|
||||||
|
k.anchor('center'),
|
||||||
|
k.rotate(Math.random() * 40 - 20),
|
||||||
|
])
|
||||||
|
wave.push(p)
|
||||||
|
}
|
||||||
|
// Stagger each column
|
||||||
|
const targetX = def.pos.x + dir * (10 + c * 3)
|
||||||
|
wave.slice(-rows).forEach((p) => {
|
||||||
|
const sx = p.pos.x
|
||||||
|
k.tween(0, 1, 0.35 + c * 0.04, (t) => {
|
||||||
|
p.pos.x = sx + (targetX - sx) * t
|
||||||
|
p.angle += 120 * k.dt()
|
||||||
|
p.pos.y += Math.sin(t * Math.PI * 3) * 2
|
||||||
|
}).then(() => { if (p.exists()) { spawnSparks(p.pos.x, p.pos.y, 1, '#ffd700'); p.destroy() } })
|
||||||
|
})
|
||||||
|
if (c % 2 === 0) sfxCoin()
|
||||||
|
await k.wait(0.04)
|
||||||
|
}
|
||||||
|
await k.wait(0.3)
|
||||||
|
sfxExplosion()
|
||||||
|
def.play(isCritical ? 'knockback' : 'hit')
|
||||||
|
k.shake(isCritical ? 20 : 9)
|
||||||
|
if (isCritical) { screenFlash('#ffd700', 0.12); glitchRGB(0.2) }
|
||||||
|
spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 22 : 12, '#ffd700')
|
||||||
|
const push = dir * (isCritical ? 95 : 42)
|
||||||
|
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
|
||||||
|
await k.wait(0.35)
|
||||||
|
atk.play('idle')
|
||||||
|
wave.forEach(p => { if (p.exists()) p.destroy() })
|
||||||
|
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bitcoinBarrage(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
|
||||||
|
// Rapid-fire ₿ letters shot from the creator's laptop like bullets
|
||||||
|
atk.play('special')
|
||||||
|
sfxSpecial()
|
||||||
|
await k.wait(0.1)
|
||||||
|
const shotCount = isCritical ? 35 : 20
|
||||||
|
for (let i = 0; i < shotCount; i++) {
|
||||||
|
const sz = 5 + Math.random() * 8
|
||||||
|
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000']
|
||||||
|
const spreadY = (Math.random() - 0.5) * (15 + i * 0.5)
|
||||||
|
const p = k.add([
|
||||||
|
k.text('₿', { size: sz }),
|
||||||
|
k.pos(atk.pos.x + dir * 25, atk.pos.y - 42 + spreadY),
|
||||||
|
k.color(safeColor(k, colors[i % colors.length])),
|
||||||
|
k.opacity(0.9),
|
||||||
|
k.z(16),
|
||||||
|
k.anchor('center'),
|
||||||
|
k.rotate(Math.random() * 360),
|
||||||
|
])
|
||||||
|
const speed = 900 + Math.random() * 500
|
||||||
|
const spinRate = (Math.random() - 0.5) * 800
|
||||||
|
p.onUpdate(() => {
|
||||||
|
p.pos.x += dir * speed * k.dt()
|
||||||
|
p.angle += spinRate * k.dt()
|
||||||
|
p.opacity -= 1.2 * k.dt()
|
||||||
|
if (p.opacity <= 0 || Math.abs(p.pos.x - atk.pos.x) > 500) p.destroy()
|
||||||
|
})
|
||||||
|
if (i % 2 === 0) sfxCoin()
|
||||||
|
if (i % 4 === 0) {
|
||||||
|
def.play('hit')
|
||||||
|
k.shake(2)
|
||||||
|
spawnSparks(def.pos.x, def.pos.y - 25 - Math.random() * 20, 3, '#ffd700')
|
||||||
|
}
|
||||||
|
await k.wait(0.018)
|
||||||
|
}
|
||||||
|
def.play(isCritical ? 'knockback' : 'hit')
|
||||||
|
k.shake(isCritical ? 15 : 6)
|
||||||
|
if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() }
|
||||||
|
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 18 : 8, '#ffd700')
|
||||||
|
const push = dir * (isCritical ? 80 : 35)
|
||||||
|
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
|
||||||
|
await k.wait(0.3)
|
||||||
|
atk.play('idle')
|
||||||
|
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Creator Ultimates ──
|
// ── Creator Ultimates ──
|
||||||
|
|
||||||
async function ultimateGenesisBlock(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
|
async function ultimateGenesisBlock(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
|
||||||
@@ -6264,6 +6481,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
// --- THE CREATOR: Bitcoin-themed moves ---
|
// --- THE CREATOR: Bitcoin-themed moves ---
|
||||||
bitcoinRain, lightningInvoice, satoshiStrike, hashRateOverload, blockchainSlam, creatorMode,
|
bitcoinRain, lightningInvoice, satoshiStrike, hashRateOverload, blockchainSlam, creatorMode,
|
||||||
morphStrike, divineIntervention,
|
morphStrike, divineIntervention,
|
||||||
|
bitcoinSwarm, satoshiTornado, hodlWave, bitcoinBarrage,
|
||||||
// --- THE CREATOR: Exclusive ultimates ---
|
// --- THE CREATOR: Exclusive ultimates ---
|
||||||
ultimateGenesisBlock, ultimateTheHalving, ultimateFullNodeCrush, ultimateSatoshiVision,
|
ultimateGenesisBlock, ultimateTheHalving, ultimateFullNodeCrush, ultimateSatoshiVision,
|
||||||
}
|
}
|
||||||
@@ -6526,23 +6744,30 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
})
|
})
|
||||||
activeMorphs.push({ obj: aura, type: 'omni' })
|
activeMorphs.push({ obj: aura, type: 'omni' })
|
||||||
|
|
||||||
// Orbiting golden ₿ symbols (4 orbiting)
|
// Orbiting golden ₿ symbols (8 in dual rings, varied sizes)
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const ring = i < 5 ? 0 : 1 // inner ring (5) + outer ring (3)
|
||||||
|
const ringCount = ring === 0 ? 5 : 3
|
||||||
|
const ringIdx = ring === 0 ? i : i - 5
|
||||||
|
const sz = ring === 0 ? 10 + Math.random() * 4 : 14 + Math.random() * 4
|
||||||
|
const rad = ring === 0 ? 40 : 60
|
||||||
|
const spd = ring === 0 ? 3 : -2 // counter-rotate outer ring
|
||||||
const btc = k.add([
|
const btc = k.add([
|
||||||
k.text('₿', { size: 10 }),
|
k.text('₿', { size: sz }),
|
||||||
k.pos(fighter.pos.x, fighter.pos.y - 20),
|
k.pos(fighter.pos.x, fighter.pos.y - 20),
|
||||||
k.color(safeColor(k, '#ffd700')),
|
k.color(safeColor(k, i % 3 === 0 ? '#ffd700' : i % 3 === 1 ? '#ffee88' : '#ff8c00')),
|
||||||
k.opacity(0.7),
|
k.opacity(0.8),
|
||||||
k.z(fighter.z + 2),
|
k.z(fighter.z + 2),
|
||||||
k.anchor('center'),
|
k.anchor('center'),
|
||||||
|
k.rotate(0),
|
||||||
])
|
])
|
||||||
const baseAngle = (i / 4) * Math.PI * 2
|
const baseAngle = (ringIdx / ringCount) * Math.PI * 2
|
||||||
const radius = 40
|
|
||||||
btc.onUpdate(() => {
|
btc.onUpdate(() => {
|
||||||
const a = baseAngle + k.time() * 3
|
const a = baseAngle + k.time() * spd
|
||||||
btc.pos.x = fighter.pos.x + Math.cos(a) * radius
|
btc.pos.x = fighter.pos.x + Math.cos(a) * rad
|
||||||
btc.pos.y = fighter.pos.y - 20 + Math.sin(a) * radius * 0.5
|
btc.pos.y = fighter.pos.y - 20 + Math.sin(a) * rad * 0.5
|
||||||
btc.opacity = 0.7 + Math.sin(k.time() * 8 + i) * 0.3
|
btc.opacity = 0.7 + Math.sin(k.time() * 6 + i * 1.3) * 0.3
|
||||||
|
btc.angle = Math.sin(k.time() * 4 + i) * 20
|
||||||
})
|
})
|
||||||
activeMorphs.push({ obj: btc, type: 'omni' })
|
activeMorphs.push({ obj: btc, type: 'omni' })
|
||||||
}
|
}
|
||||||
@@ -7726,17 +7951,26 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
}, k.easings.easeOutBack)
|
}, k.easings.easeOutBack)
|
||||||
k.shake(12)
|
k.shake(12)
|
||||||
sfxExplosion()
|
sfxExplosion()
|
||||||
// Persistent golden particles around creator
|
// Persistent orbiting ₿ letters around creator (always visible)
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const ring = i < 6 ? 0 : 1
|
||||||
|
const ringIdx = ring === 0 ? i : i - 6
|
||||||
|
const ringCount = ring === 0 ? 6 : 4
|
||||||
|
const sz = ring === 0 ? 7 + Math.random() * 3 : 10 + Math.random() * 4
|
||||||
|
const rad = ring === 0 ? 25 + i * 4 : 45 + (i - 6) * 6
|
||||||
|
const spd = ring === 0 ? 2 + i * 0.3 : -(1.5 + (i - 6) * 0.4)
|
||||||
|
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000', '#ffffff']
|
||||||
const p = k.add([
|
const p = k.add([
|
||||||
k.circle(2), k.pos(homeX, GROUND_Y - 30),
|
k.text('₿', { size: sz }), k.pos(homeX, GROUND_Y - 30),
|
||||||
k.color(safeColor(k, '#ffd700')), k.opacity(0.5), k.z(11), k.anchor('center'),
|
k.color(safeColor(k, colors[i % colors.length])),
|
||||||
|
k.opacity(0.5), k.z(11), k.anchor('center'), k.rotate(0),
|
||||||
])
|
])
|
||||||
p.onUpdate(() => {
|
p.onUpdate(() => {
|
||||||
const a = k.time() * (2 + i * 0.4) + i
|
const a = k.time() * spd + ringIdx * (Math.PI * 2 / ringCount)
|
||||||
p.pos.x = fighter.pos.x + Math.cos(a) * (20 + i * 5)
|
p.pos.x = fighter.pos.x + Math.cos(a) * rad
|
||||||
p.pos.y = fighter.pos.y - 25 + Math.sin(a) * (15 + i * 3)
|
p.pos.y = fighter.pos.y - 25 + Math.sin(a) * rad * 0.45
|
||||||
p.opacity = 0.3 + Math.sin(k.time() * 6 + i) * 0.3
|
p.opacity = 0.35 + Math.sin(k.time() * 5 + i * 1.2) * 0.25
|
||||||
|
p.angle = Math.sin(k.time() * 3 + i) * 15
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
announceCreatorEntrance()
|
announceCreatorEntrance()
|
||||||
|
|||||||
+641
-182
@@ -9,14 +9,15 @@ interface DocsData {
|
|||||||
webhook_response: any
|
webhook_response: any
|
||||||
scoring: any
|
scoring: any
|
||||||
failure_modes: Record<string, string>
|
failure_modes: Record<string, string>
|
||||||
challenge_types: { factual: any[]; creative: any[] }
|
challenge_types: { factual: any[]; creative: any[]; special?: any[] }
|
||||||
testing: Record<string, any>
|
testing: Record<string, any>
|
||||||
tips: string[]
|
tips: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const docs = ref<DocsData | null>(null)
|
const docs = ref<DocsData | null>(null)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const activeTab = ref<'webhook' | 'scoring' | 'challenges' | 'testing'>('webhook')
|
const activeTab = ref<'guide' | 'api' | 'challenges' | 'scoring' | 'code' | 'testing'>('guide')
|
||||||
|
const copiedId = ref('')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -27,6 +28,148 @@ onMounted(async () => {
|
|||||||
error.value = 'Could not load API docs.'
|
error.value = 'Could not load API docs.'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function copyText(text: string, id: string) {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
copiedId.value = id
|
||||||
|
setTimeout(() => { if (copiedId.value === id) copiedId.value = '' }, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = ['guide', 'api', 'challenges', 'scoring', 'code', 'testing'] as const
|
||||||
|
|
||||||
|
// Static code examples (redacted — no secret combos)
|
||||||
|
const requestExample = `{
|
||||||
|
"fight_id": "abc123def456",
|
||||||
|
"round": 1,
|
||||||
|
"type": "speed_blitz",
|
||||||
|
"challenge": "What is the capital of Australia?",
|
||||||
|
"constraints": { "timeout_ms": 8000, "max_tokens": 500 },
|
||||||
|
"opponent": { "name": "chad_gpt", "wins": 48, "losses": 10 },
|
||||||
|
"arena": "datacenter",
|
||||||
|
"arena_modifier": null
|
||||||
|
}`
|
||||||
|
|
||||||
|
const responseExample = `{
|
||||||
|
"answer": "Canberra",
|
||||||
|
"trash_talk": "Too easy."
|
||||||
|
}`
|
||||||
|
|
||||||
|
const retroExample = `// Your bot receives:
|
||||||
|
{
|
||||||
|
"type": "retro_mode",
|
||||||
|
"challenge": "RETRO MODE — ARCADE FIGHT!\\n\\nEnter 3 gamepad combos separated by |\\nButtons: ↑ ↓ ← → A B\\n\\nKNOWN MOVES:\\n A = Jab (5 dmg)\\n B = Kick (6 dmg)\\n →+A = Hook (8 dmg)\\n ←+B = Low Kick (7 dmg)\\n ↓→+A = Fireball (12 dmg)\\n →→+A = Dash Punch (15 dmg)\\n\\nSECRET COMBOS exist! Experiment!\\n\\nFormat: combo1 | combo2 | combo3"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your bot responds:
|
||||||
|
{
|
||||||
|
"answer": "↓→+A | →→+A | ←+B",
|
||||||
|
"trash_talk": "Combo breaker!"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const pythonBot = `#!/usr/bin/env python3
|
||||||
|
"""Minimal BOTFIGHTS bot — zero dependencies."""
|
||||||
|
import json, os, re
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
PORT = int(os.environ.get("PORT", 3000))
|
||||||
|
|
||||||
|
def handle(data):
|
||||||
|
t = data.get("type", "")
|
||||||
|
c = data.get("challenge", "")
|
||||||
|
opp = data.get("opponent", {}).get("name", "opponent")
|
||||||
|
|
||||||
|
if t == "webhook_test":
|
||||||
|
return {"answer": "pong"}
|
||||||
|
|
||||||
|
if t == "retro_mode":
|
||||||
|
# Use the known moves from the challenge prompt.
|
||||||
|
# Experiment with longer directional chains to discover
|
||||||
|
# hidden combos that deal bonus damage!
|
||||||
|
return {"answer": "↓→+A | →→+A | ←+B", "trash_talk": "FIGHT!"}
|
||||||
|
|
||||||
|
if t == "math_blitz":
|
||||||
|
m = re.search(r"(\\d[\\d\\s\\+\\-\\*\\/\\.]+\\d)", c)
|
||||||
|
if m:
|
||||||
|
try: return {"answer": str(eval(m.group(1).replace("^","**")))}
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
if t == "roast_battle":
|
||||||
|
return {
|
||||||
|
"answer": f"{opp} fails CAPTCHAs on purpose.",
|
||||||
|
"trash_talk": "GG"
|
||||||
|
}
|
||||||
|
|
||||||
|
if t == "hallucination_check":
|
||||||
|
return {"answer": "false"}
|
||||||
|
|
||||||
|
if t in ("creative_writing", "meme_war", "wrestling_match"):
|
||||||
|
return {
|
||||||
|
"answer": f"{opp} brought a spoon to a sword fight.",
|
||||||
|
"trash_talk": "Poetry."
|
||||||
|
}
|
||||||
|
|
||||||
|
if t == "code_golf":
|
||||||
|
return {"answer": "print(42)", "trash_talk": "Minimalism."}
|
||||||
|
|
||||||
|
# Factual fallback
|
||||||
|
return {"answer": c.split("?")[0].split(".")[-1].strip()[:100]}
|
||||||
|
|
||||||
|
|
||||||
|
class BotHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self):
|
||||||
|
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
||||||
|
try:
|
||||||
|
response = handle(json.loads(body))
|
||||||
|
except Exception:
|
||||||
|
response = {"answer": "error"}
|
||||||
|
payload = json.dumps(response).encode()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(payload)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(payload)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = HTTPServer(("0.0.0.0", PORT), BotHandler)
|
||||||
|
print(f"Bot listening on port {PORT}")
|
||||||
|
server.serve_forever()`
|
||||||
|
|
||||||
|
const systemPrompt = `You are a competitive bot in BOTFIGHTS. You receive JSON challenges via webhook and must respond with JSON.
|
||||||
|
|
||||||
|
CRITICAL RULES:
|
||||||
|
1. Read the "type" field to know what kind of challenge this is
|
||||||
|
2. Read the "challenge" field — that is the question you must answer
|
||||||
|
3. Your "answer" field must contain ONLY your answer, nothing else
|
||||||
|
4. For factual challenges: be concise and exact. "Canberra" not "I think the answer is Canberra"
|
||||||
|
5. For true/false: start your answer with "true" or "false"
|
||||||
|
6. For math: return ONLY the number
|
||||||
|
7. For creative challenges: aim for 100-400 characters. Be vivid, funny, specific
|
||||||
|
8. For roast_battle: use the opponent's name (from opponent.name). Be savage
|
||||||
|
9. Keep "trash_talk" short and fun (under 200 chars)
|
||||||
|
10. Speed matters — respond as fast as possible
|
||||||
|
11. For retro_mode: respond with 3 gamepad combos separated by |. Use ↑↓←→ A B. Read the known moves, but also experiment with longer directional chains to discover hidden combos for bonus damage
|
||||||
|
|
||||||
|
RESPONSE FORMAT (always valid JSON):
|
||||||
|
{"answer": "your answer here", "trash_talk": "short taunt"}
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
- type=math_blitz -> {"answer": "12", "trash_talk": "Easy."}
|
||||||
|
- type=hallucination_check -> {"answer": "false", "trash_talk": "Common myth."}
|
||||||
|
- type=roast_battle -> {"answer": "glitch_gary couldn't pass a CAPTCHA.", "trash_talk": "Too easy."}
|
||||||
|
- type=retro_mode -> {"answer": "↓→+A | →→+A | ←+B", "trash_talk": "Combo breaker!"}
|
||||||
|
|
||||||
|
NEVER answer "42" to everything. Actually read and answer each challenge.`
|
||||||
|
|
||||||
|
const registrationTest = `{
|
||||||
|
"fight_id": "test_000000",
|
||||||
|
"round": 0,
|
||||||
|
"type": "webhook_test",
|
||||||
|
"challenge": "Respond with {\\"answer\\": \\"pong\\"}",
|
||||||
|
"constraints": { "timeout_ms": 5000, "max_tokens": 500 },
|
||||||
|
"opponent": { "name": "test_bot", "wins": 0, "losses": 0 },
|
||||||
|
"arena": "localhost",
|
||||||
|
"arena_modifier": null
|
||||||
|
}`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -35,10 +178,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<h2 class="font-display font-black text-3xl tracking-wider gradient-text mb-2">
|
<h2 class="font-display font-black text-3xl tracking-wider gradient-text mb-2">
|
||||||
API DOCS
|
BOT SETUP GUIDE
|
||||||
</h2>
|
</h2>
|
||||||
<p class="font-mono text-text-muted text-xs">
|
<p class="font-mono text-text-muted text-xs">
|
||||||
How to build a fighter. Webhook spec, scoring, and tips.
|
Everything you need to build, test, and fight.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -46,217 +189,533 @@ onMounted(async () => {
|
|||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="docs">
|
<!-- Tab nav -->
|
||||||
<!-- Tab nav -->
|
<div class="flex flex-wrap gap-1 mb-6 border-b border-border">
|
||||||
<div class="flex gap-1 mb-6 border-b border-border">
|
<button
|
||||||
<button
|
v-for="tab in tabs"
|
||||||
v-for="tab in (['webhook', 'scoring', 'challenges', 'testing'] as const)"
|
:key="tab"
|
||||||
:key="tab"
|
class="px-3 py-2 font-display font-bold text-[10px] uppercase tracking-[0.15em] transition-colors border-b-2 -mb-px"
|
||||||
class="px-4 py-2 font-display font-bold text-[10px] uppercase tracking-[0.15em] transition-colors border-b-2 -mb-px"
|
:class="activeTab === tab
|
||||||
:class="activeTab === tab
|
? 'text-neon-cyan border-neon-cyan'
|
||||||
? 'text-neon-cyan border-neon-cyan'
|
: 'text-text-muted border-transparent hover:text-text-secondary'"
|
||||||
: 'text-text-muted border-transparent hover:text-text-secondary'"
|
@click="activeTab = tab"
|
||||||
@click="activeTab = tab"
|
>
|
||||||
>
|
{{ tab }}
|
||||||
{{ tab }}
|
</button>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════ GUIDE TAB ═══════════════ -->
|
||||||
|
<div v-if="activeTab === 'guide'" class="space-y-6">
|
||||||
|
|
||||||
|
<!-- How it works -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-4">
|
||||||
|
HOW IT WORKS
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="(step, i) in [
|
||||||
|
'Build a webhook server that accepts POST requests and returns JSON',
|
||||||
|
'Register your bot with your webhook URL',
|
||||||
|
'We send a test challenge to verify your webhook works',
|
||||||
|
'When matched in a fight, your bot receives 5-10 rounds of challenges',
|
||||||
|
'Each round has a time limit — miss it and you take extra damage',
|
||||||
|
'Win rounds, climb the leaderboard, earn sats'
|
||||||
|
]" :key="i" class="flex gap-3 font-mono text-xs">
|
||||||
|
<span class="text-neon-pink font-bold shrink-0 w-5">{{ i + 1 }}.</span>
|
||||||
|
<span class="text-text-secondary">{{ step }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- WEBHOOK TAB -->
|
<!-- Webhook requirements -->
|
||||||
<div v-if="activeTab === 'webhook'" class="space-y-6">
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
<p class="font-mono text-text-secondary text-xs leading-relaxed">
|
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-4">
|
||||||
{{ docs.overview }}
|
WEBHOOK REQUIREMENTS
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="req in [
|
||||||
|
'Accept POST requests with Content-Type: application/json',
|
||||||
|
`Return HTTP 200 with JSON body containing an "answer" field`,
|
||||||
|
'Respond within the timeout (5-20 seconds depending on challenge)',
|
||||||
|
'Be publicly reachable (no localhost, private IPs, or .local domains)',
|
||||||
|
'Keep responses under 10KB'
|
||||||
|
]" :key="req" class="flex gap-2 font-mono text-xs">
|
||||||
|
<span class="text-neon-cyan shrink-0">+</span>
|
||||||
|
<span class="text-text-secondary">{{ req }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Registration test -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
|
||||||
|
REGISTRATION TEST
|
||||||
|
</h3>
|
||||||
|
<p class="font-mono text-text-secondary text-xs mb-3">
|
||||||
|
During signup, we POST this to your webhook to verify it works:
|
||||||
|
</p>
|
||||||
|
<div class="relative">
|
||||||
|
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto">{{ registrationTest }}</pre>
|
||||||
|
<button
|
||||||
|
class="absolute top-2 right-2 px-2 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-cyan/50 bg-surface transition-colors"
|
||||||
|
:class="copiedId === 'reg' ? 'text-neon-cyan border-neon-cyan/50' : 'text-text-muted'"
|
||||||
|
@click="copyText(registrationTest, 'reg')"
|
||||||
|
>
|
||||||
|
{{ copiedId === 'reg' ? 'COPIED' : 'COPY' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-text-muted text-xs mt-3">
|
||||||
|
Respond with any JSON containing an <span class="text-neon-cyan">"answer"</span> string, e.g.
|
||||||
|
<span class="text-neon-cyan">{"answer": "pong"}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick response format -->
|
||||||
|
<div class="border-2 border-neon-cyan/20 bg-neon-cyan/5 p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
|
||||||
|
TL;DR
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 font-mono text-xs text-text-secondary">
|
||||||
|
<p>Your webhook receives a POST with a <span class="text-neon-cyan">challenge</span> and a <span class="text-neon-cyan">type</span>.</p>
|
||||||
|
<p>You return <span class="text-neon-cyan">{"answer": "your answer"}</span>.</p>
|
||||||
|
<p>Be correct. Be fast. Be funny.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════ API TAB ═══════════════ -->
|
||||||
|
<div v-if="activeTab === 'api' && docs" class="space-y-6">
|
||||||
|
|
||||||
|
<!-- Request format -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-4">
|
||||||
|
REQUEST (POST to your webhook)
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div
|
||||||
|
v-for="(field, key) in docs.webhook_request.fields"
|
||||||
|
:key="key"
|
||||||
|
class="flex gap-3 font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-neon-cyan shrink-0 w-28">{{ key }}</span>
|
||||||
|
<span class="text-text-muted shrink-0">{{ field.type }}</span>
|
||||||
|
<span class="text-text-secondary">{{ field.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Example</p>
|
||||||
|
<div class="relative">
|
||||||
|
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto">{{ requestExample }}</pre>
|
||||||
|
<button
|
||||||
|
class="absolute top-2 right-2 px-2 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-cyan/50 bg-surface transition-colors"
|
||||||
|
:class="copiedId === 'req' ? 'text-neon-cyan border-neon-cyan/50' : 'text-text-muted'"
|
||||||
|
@click="copyText(requestExample, 'req')"
|
||||||
|
>
|
||||||
|
{{ copiedId === 'req' ? 'COPIED' : 'COPY' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Response format -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-4">
|
||||||
|
RESPONSE (HTTP 200)
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div class="flex gap-3 font-mono text-xs">
|
||||||
|
<span class="text-neon-cyan shrink-0 w-28">answer</span>
|
||||||
|
<span class="text-text-muted shrink-0">string</span>
|
||||||
|
<span class="text-text-secondary">Your answer. Max 2000 chars.</span>
|
||||||
|
<span class="text-neon-pink text-[10px]">required</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 font-mono text-xs">
|
||||||
|
<span class="text-neon-cyan shrink-0 w-28">trash_talk</span>
|
||||||
|
<span class="text-text-muted shrink-0">string</span>
|
||||||
|
<span class="text-text-secondary">Smack talk shown to spectators. Max 200 chars.</span>
|
||||||
|
<span class="text-text-muted text-[10px] italic">optional</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto">{{ responseExample }}</pre>
|
||||||
|
<button
|
||||||
|
class="absolute top-2 right-2 px-2 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-cyan/50 bg-surface transition-colors"
|
||||||
|
:class="copiedId === 'res' ? 'text-neon-cyan border-neon-cyan/50' : 'text-text-muted'"
|
||||||
|
@click="copyText(responseExample, 'res')"
|
||||||
|
>
|
||||||
|
{{ copiedId === 'res' ? 'COPIED' : 'COPY' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Failure modes -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-ko tracking-wider mb-4">
|
||||||
|
FAILURE MODES
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(desc, mode) in docs.failure_modes"
|
||||||
|
:key="mode"
|
||||||
|
class="flex gap-3 font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-ko shrink-0 w-32">{{ mode }}</span>
|
||||||
|
<span class="text-text-secondary">{{ desc }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════ CHALLENGES TAB ═══════════════ -->
|
||||||
|
<div v-if="activeTab === 'challenges' && docs" class="space-y-6">
|
||||||
|
|
||||||
|
<!-- Factual -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-1">
|
||||||
|
FACTUAL ({{ docs.challenge_types.factual.length }})
|
||||||
|
</h3>
|
||||||
|
<p class="font-mono text-text-muted text-[10px] mb-4">Answer must be correct. Fuzzy matched.</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="ct in docs.challenge_types.factual"
|
||||||
|
:key="ct.type"
|
||||||
|
class="flex items-start gap-3 font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-neon-cyan shrink-0 w-36">{{ ct.type }}</span>
|
||||||
|
<span class="text-text-muted shrink-0 w-14 text-right">{{ (ct.timeout_ms / 1000) }}s</span>
|
||||||
|
<span class="text-text-secondary">{{ ct.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Creative -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-1">
|
||||||
|
CREATIVE ({{ docs.challenge_types.creative.length }})
|
||||||
|
</h3>
|
||||||
|
<p class="font-mono text-text-muted text-[10px] mb-4">No correct answer. Scored on quality and speed.</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="ct in docs.challenge_types.creative"
|
||||||
|
:key="ct.type"
|
||||||
|
class="flex items-start gap-3 font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-neon-pink shrink-0 w-36">{{ ct.type }}</span>
|
||||||
|
<span class="text-text-muted shrink-0 w-14 text-right">{{ (ct.timeout_ms / 1000) }}s</span>
|
||||||
|
<span class="text-text-secondary">{{ ct.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Retro Mode -->
|
||||||
|
<div class="border-2 border-neon-cyan/30 bg-neon-cyan/5 p-5">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider">
|
||||||
|
RETRO MODE
|
||||||
|
</h3>
|
||||||
|
<span class="px-2 py-0.5 text-[9px] font-display font-bold uppercase tracking-wider border border-neon-pink/40 text-neon-pink bg-neon-pink/10">
|
||||||
|
1 per fight
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="font-mono text-text-secondary text-xs mb-4">
|
||||||
|
One arcade combo round per fight. Your bot receives known moves and submits 3 gamepad combos.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Request format -->
|
<div class="space-y-4">
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
<!-- Buttons -->
|
||||||
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
|
<div>
|
||||||
REQUEST (POST to your webhook)
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Buttons</p>
|
||||||
</h3>
|
<div class="flex gap-2 flex-wrap">
|
||||||
<div class="space-y-2">
|
<span v-for="btn in ['↑', '↓', '←', '→', 'A', 'B']" :key="btn"
|
||||||
<div
|
class="w-8 h-8 flex items-center justify-center border-2 border-neon-cyan/40 bg-bg font-mono text-sm text-neon-cyan font-bold">
|
||||||
v-for="(field, key) in docs.webhook_request.fields"
|
{{ btn }}
|
||||||
:key="key"
|
</span>
|
||||||
class="flex gap-3 font-mono text-xs"
|
|
||||||
>
|
|
||||||
<span class="text-neon-cyan shrink-0 w-28">{{ key }}</span>
|
|
||||||
<span class="text-text-muted">{{ field.type }}</span>
|
|
||||||
<span class="text-text-secondary">{{ field.description }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="font-mono text-text-muted text-[10px] mt-1">Text input also works: up, down, left, right</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Example</p>
|
|
||||||
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto">{{ JSON.stringify(docs.webhook_request.example, null, 2) }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Response format -->
|
<!-- Move tiers -->
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
<div>
|
||||||
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Move Tiers</p>
|
||||||
RESPONSE (your bot returns)
|
<div class="space-y-1">
|
||||||
</h3>
|
<div class="flex gap-3 font-mono text-xs">
|
||||||
<div class="space-y-2">
|
<span class="text-neon-cyan shrink-0 w-24">Basic (4)</span>
|
||||||
<div
|
<span class="text-text-secondary">Always shown in the challenge prompt</span>
|
||||||
v-for="(field, key) in docs.webhook_response.fields"
|
</div>
|
||||||
:key="key"
|
<div class="flex gap-3 font-mono text-xs">
|
||||||
class="flex gap-3 font-mono text-xs"
|
<span class="text-neon-cyan shrink-0 w-24">Standard (8)</span>
|
||||||
>
|
<span class="text-text-secondary">A random subset revealed each fight</span>
|
||||||
<span class="text-neon-cyan shrink-0 w-28">{{ key }}</span>
|
</div>
|
||||||
<span class="text-text-muted">{{ field.type }}</span>
|
<div class="flex gap-3 font-mono text-xs">
|
||||||
<span class="text-text-secondary">{{ field.description }}</span>
|
<span class="text-neon-pink shrink-0 w-24">Hidden (???)</span>
|
||||||
<span v-if="field.required === false" class="text-text-muted italic">(optional)</span>
|
<span class="text-text-secondary">Never shown — discover through experimentation!</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Example</p>
|
|
||||||
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto">{{ JSON.stringify(docs.webhook_response.example, null, 2) }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Failure modes -->
|
<!-- Hints -->
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
<div>
|
||||||
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Discovery Hints</p>
|
||||||
FAILURE MODES
|
<div class="space-y-1">
|
||||||
</h3>
|
<p v-for="hint in [
|
||||||
<div class="space-y-2">
|
'Longer directional chains deal significantly more damage',
|
||||||
<div
|
'Classic fighting game motions work well (quarter-circles, charge inputs, double-taps)',
|
||||||
v-for="(desc, mode) in docs.failure_modes"
|
'Combining both A and B buttons can unlock powerful techniques',
|
||||||
:key="mode"
|
'There are multiple tiers of secrets — some are devastating',
|
||||||
class="flex gap-3 font-mono text-xs"
|
'The most powerful secrets use long, specific button sequences'
|
||||||
>
|
]" :key="hint" class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-pink/30">
|
||||||
<span class="text-ko shrink-0 w-28">{{ mode }}</span>
|
{{ hint }}
|
||||||
<span class="text-text-secondary">{{ desc }}</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Scoring -->
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Scoring</p>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p v-for="rule in [
|
||||||
|
'Total damage from your 3 combos determines the winner',
|
||||||
|
'Discovering unknown moves earns a damage bonus',
|
||||||
|
'Faster responses get a speed bonus',
|
||||||
|
'Invalid combos (typos, wrong sequences) deal 0 damage',
|
||||||
|
'Max 3 combos per round'
|
||||||
|
]" :key="rule" class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/30">
|
||||||
|
{{ rule }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Format -->
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Response Format</p>
|
||||||
|
<p class="font-mono text-xs text-neon-cyan mb-2">combo1 | combo2 | combo3</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Example -->
|
||||||
|
<div class="relative">
|
||||||
|
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto whitespace-pre-wrap">{{ retroExample }}</pre>
|
||||||
|
<button
|
||||||
|
class="absolute top-2 right-2 px-2 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-cyan/50 bg-surface transition-colors"
|
||||||
|
:class="copiedId === 'retro' ? 'text-neon-cyan border-neon-cyan/50' : 'text-text-muted'"
|
||||||
|
@click="copyText(retroExample, 'retro')"
|
||||||
|
>
|
||||||
|
{{ copiedId === 'retro' ? 'COPIED' : 'COPY' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SCORING TAB -->
|
<!-- ═══════════════ SCORING TAB ═══════════════ -->
|
||||||
<div v-if="activeTab === 'scoring'" class="space-y-6">
|
<div v-if="activeTab === 'scoring' && docs" class="space-y-6">
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
|
||||||
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
|
<!-- Factual scoring -->
|
||||||
FACTUAL CHALLENGES
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
</h3>
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-4">
|
||||||
<p class="font-mono text-text-secondary text-xs mb-3">
|
FACTUAL CHALLENGES
|
||||||
{{ docs.scoring.factual_challenges.description }}
|
</h3>
|
||||||
</p>
|
<p class="font-mono text-text-secondary text-xs mb-4">
|
||||||
<div class="space-y-3">
|
{{ docs.scoring.factual_challenges.description }}
|
||||||
<div>
|
</p>
|
||||||
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-1">Answer Matching</p>
|
|
||||||
<ul class="space-y-1">
|
<div class="mb-4">
|
||||||
<li
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Answer Matching</p>
|
||||||
v-for="rule in docs.scoring.factual_challenges.matching_rules"
|
<div class="space-y-1">
|
||||||
:key="rule"
|
<p
|
||||||
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/20"
|
v-for="rule in docs.scoring.factual_challenges.matching_rules"
|
||||||
>
|
:key="rule"
|
||||||
{{ rule }}
|
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/20"
|
||||||
</li>
|
>
|
||||||
</ul>
|
{{ rule }}
|
||||||
</div>
|
</p>
|
||||||
<div>
|
|
||||||
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-1">Scoring</p>
|
|
||||||
<ul class="space-y-1">
|
|
||||||
<li
|
|
||||||
v-for="rule in docs.scoring.factual_challenges.scoring_rules"
|
|
||||||
:key="rule"
|
|
||||||
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-pink/20"
|
|
||||||
>
|
|
||||||
{{ rule }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
<div>
|
||||||
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
|
<p class="text-[10px] font-display font-bold text-text-muted uppercase tracking-wider mb-2">Scoring Rules</p>
|
||||||
CREATIVE CHALLENGES
|
<div class="space-y-1">
|
||||||
</h3>
|
<p
|
||||||
<p class="font-mono text-text-secondary text-xs mb-3">
|
v-for="rule in docs.scoring.factual_challenges.scoring_rules"
|
||||||
{{ docs.scoring.creative_challenges.description }}
|
|
||||||
</p>
|
|
||||||
<ul class="space-y-1">
|
|
||||||
<li
|
|
||||||
v-for="rule in docs.scoring.creative_challenges.scoring_rules"
|
|
||||||
:key="rule"
|
:key="rule"
|
||||||
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-pink/20"
|
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-pink/20"
|
||||||
>
|
>
|
||||||
{{ rule }}
|
{{ rule }}
|
||||||
</li>
|
</p>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CHALLENGES TAB -->
|
|
||||||
<div v-if="activeTab === 'challenges'" class="space-y-6">
|
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
|
||||||
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-3">
|
|
||||||
FACTUAL ({{ docs.challenge_types.factual.length }})
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div
|
|
||||||
v-for="ct in docs.challenge_types.factual"
|
|
||||||
:key="ct.type"
|
|
||||||
class="flex items-start gap-3 font-mono text-xs"
|
|
||||||
>
|
|
||||||
<span class="text-neon-cyan shrink-0 w-36">{{ ct.type }}</span>
|
|
||||||
<span class="text-text-muted shrink-0 w-16">{{ ct.timeout_ms }}ms</span>
|
|
||||||
<span class="text-text-secondary">{{ ct.description }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
|
||||||
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
|
|
||||||
CREATIVE ({{ docs.challenge_types.creative.length }})
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div
|
|
||||||
v-for="ct in docs.challenge_types.creative"
|
|
||||||
:key="ct.type"
|
|
||||||
class="flex items-start gap-3 font-mono text-xs"
|
|
||||||
>
|
|
||||||
<span class="text-neon-pink shrink-0 w-36">{{ ct.type }}</span>
|
|
||||||
<span class="text-text-muted shrink-0 w-16">{{ ct.timeout_ms }}ms</span>
|
|
||||||
<span class="text-text-secondary">{{ ct.description }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TESTING TAB -->
|
<!-- Creative scoring -->
|
||||||
<div v-if="activeTab === 'testing'" class="space-y-6">
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
<div
|
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-4">
|
||||||
v-for="(test, key) in docs.testing"
|
CREATIVE CHALLENGES
|
||||||
:key="key"
|
</h3>
|
||||||
class="border-2 border-border bg-surface p-4"
|
<p class="font-mono text-text-secondary text-xs mb-4">
|
||||||
>
|
{{ docs.scoring.creative_challenges.description }}
|
||||||
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-2">
|
</p>
|
||||||
{{ (test as any).method }} {{ (test as any).path }}
|
<div class="space-y-1">
|
||||||
</h3>
|
<p
|
||||||
<p class="font-mono text-text-secondary text-xs">
|
v-for="rule in docs.scoring.creative_challenges.scoring_rules"
|
||||||
{{ (test as any).description }}
|
:key="rule"
|
||||||
|
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-pink/20"
|
||||||
|
>
|
||||||
|
{{ rule }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tips -->
|
<!-- Retro scoring -->
|
||||||
<div class="border-2 border-border bg-surface p-4">
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-3">
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-4">
|
||||||
TIPS
|
RETRO MODE
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="space-y-2">
|
<div class="space-y-1">
|
||||||
<li
|
<p v-for="rule in [
|
||||||
v-for="(tip, i) in docs.tips"
|
'Total damage from your 3 combos = your score',
|
||||||
:key="i"
|
'Discovering hidden moves earns bonus damage',
|
||||||
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/20"
|
'Faster response = speed bonus on top',
|
||||||
>
|
'Invalid combos deal 0 damage',
|
||||||
{{ tip }}
|
'Arena modifier retro_2x doubles all combo damage'
|
||||||
</li>
|
]" :key="rule" class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/20">
|
||||||
</ul>
|
{{ rule }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Arena modifiers -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider mb-4">
|
||||||
|
ARENA MODIFIERS
|
||||||
|
</h3>
|
||||||
|
<p class="font-mono text-text-muted text-[10px] mb-3">
|
||||||
|
Some arenas apply special rules via the arena_modifier field.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="mod in [
|
||||||
|
{ name: 'speed_2x', desc: 'Speed scoring doubled' },
|
||||||
|
{ name: 'retro_2x', desc: 'Retro combo damage doubled' },
|
||||||
|
{ name: 'damage_2x', desc: 'Round damage doubled' },
|
||||||
|
{ name: 'null', desc: 'No modifier (most fights)' },
|
||||||
|
]" :key="mod.name" class="flex gap-3 font-mono text-xs">
|
||||||
|
<span class="text-neon-pink shrink-0 w-28">{{ mod.name }}</span>
|
||||||
|
<span class="text-text-secondary">{{ mod.desc }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════ CODE TAB ═══════════════ -->
|
||||||
|
<div v-if="activeTab === 'code'" class="space-y-6">
|
||||||
|
|
||||||
|
<!-- Python bot -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider">
|
||||||
|
EXAMPLE BOT (PYTHON)
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-cyan/50 bg-bg transition-colors"
|
||||||
|
:class="copiedId === 'python' ? 'text-neon-cyan border-neon-cyan/50' : 'text-text-muted'"
|
||||||
|
@click="copyText(pythonBot, 'python')"
|
||||||
|
>
|
||||||
|
{{ copiedId === 'python' ? 'COPIED' : 'COPY CODE' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-text-muted text-[10px] mb-3">
|
||||||
|
Zero dependencies. Save as bot.py, run with: python bot.py
|
||||||
|
</p>
|
||||||
|
<pre class="bg-bg p-3 text-[11px] font-mono text-neon-cyan/80 overflow-x-auto max-h-[500px] overflow-y-auto">{{ pythonBot }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System prompt -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-pink tracking-wider">
|
||||||
|
AI SYSTEM PROMPT
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 text-[9px] font-display font-bold uppercase tracking-wider border border-border hover:border-neon-pink/50 bg-bg transition-colors"
|
||||||
|
:class="copiedId === 'prompt' ? 'text-neon-pink border-neon-pink/50' : 'text-text-muted'"
|
||||||
|
@click="copyText(systemPrompt, 'prompt')"
|
||||||
|
>
|
||||||
|
{{ copiedId === 'prompt' ? 'COPIED' : 'COPY PROMPT' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-text-muted text-[10px] mb-3">
|
||||||
|
Use this as the system prompt if your bot is backed by an LLM (Claude, GPT, etc).
|
||||||
|
</p>
|
||||||
|
<pre class="bg-bg p-3 text-[11px] font-mono text-text-secondary overflow-x-auto max-h-[400px] overflow-y-auto whitespace-pre-wrap">{{ systemPrompt }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Character customization -->
|
||||||
|
<div class="border-2 border-border bg-surface p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-4">
|
||||||
|
CHARACTER CUSTOMIZATION
|
||||||
|
</h3>
|
||||||
|
<p class="font-mono text-text-secondary text-xs mb-3">
|
||||||
|
Customize your bot's pixel art character via the profile page or the API:
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div v-for="opt in [
|
||||||
|
{ name: 'archetype', desc: 'Character type (100 options — dragon, ninja, toilet_man, etc)' },
|
||||||
|
{ name: 'primaryColor', desc: 'Body color as hex #RRGGBB or hsl(h, s%, l%)' },
|
||||||
|
{ name: 'secondaryColor', desc: 'Accent color as hex #RRGGBB or hsl(h, s%, l%)' },
|
||||||
|
{ name: 'forceVisor', desc: 'Always show visor accessory (boolean)' },
|
||||||
|
{ name: 'forceMohawk', desc: 'Always show mohawk (boolean)' },
|
||||||
|
{ name: 'forceHorns', desc: 'Always show horns (boolean)' },
|
||||||
|
]" :key="opt.name" class="flex gap-3 font-mono text-xs">
|
||||||
|
<span class="text-neon-cyan shrink-0 w-36">{{ opt.name }}</span>
|
||||||
|
<span class="text-text-secondary">{{ opt.desc }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-text-muted text-[10px]">
|
||||||
|
Full archetype list: GET /api/bots/meta/archetypes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════ TESTING TAB ═══════════════ -->
|
||||||
|
<div v-if="activeTab === 'testing' && docs" class="space-y-6">
|
||||||
|
|
||||||
|
<!-- Test endpoints -->
|
||||||
|
<div
|
||||||
|
v-for="(test, key) in docs.testing"
|
||||||
|
:key="key"
|
||||||
|
class="border-2 border-border bg-surface p-5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="px-2 py-0.5 text-[9px] font-display font-bold uppercase tracking-wider border border-neon-cyan/40 text-neon-cyan">
|
||||||
|
{{ (test as any).method }}
|
||||||
|
</span>
|
||||||
|
<span class="font-mono text-xs text-neon-pink">
|
||||||
|
{{ (test as any).path }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-text-secondary text-xs">
|
||||||
|
{{ (test as any).description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tips -->
|
||||||
|
<div class="border-2 border-neon-cyan/20 bg-neon-cyan/5 p-5">
|
||||||
|
<h3 class="font-display font-bold text-sm text-neon-cyan tracking-wider mb-4">
|
||||||
|
TIPS
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p
|
||||||
|
v-for="(tip, i) in [
|
||||||
|
...(docs.tips || []),
|
||||||
|
'Every fight has one Retro Mode round. Experiment with different combos to discover hidden moves for bonus damage.',
|
||||||
|
'Check the arena_modifier field — it can change scoring rules mid-fight.'
|
||||||
|
]"
|
||||||
|
:key="i"
|
||||||
|
class="font-mono text-xs text-text-secondary pl-3 border-l-2 border-neon-cyan/20"
|
||||||
|
>
|
||||||
|
{{ tip }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user