Files
botfights/docs/sprite-reference.html
T
DorianandClaude Opus 4.6 f1fe798442 docs: add retro mode docs, bot guide, sprite guide
Update BOT_SETUP.md with full retro mode combo reference, scoring,
and strategy tips. Add docs/bot-guide.md as a standalone developer
guide covering all challenge types including retro mode. Add
docs/sprite-guide.md and sprite-reference.html for custom sprite
creation. Update docs.ts API endpoint and Python example bot with
retro_mode handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:43:34 +00:00

362 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Botfights Sprite Sheet Reference</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a1a; color: #e0e0e0; font-family: 'Courier New', monospace; padding: 20px; }
h1 { color: #00f0ff; margin-bottom: 10px; font-size: 20px; }
.info { color: #888; margin-bottom: 20px; font-size: 13px; }
.controls { margin-bottom: 20px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.controls label { color: #aaa; font-size: 13px; }
.controls select, .controls input { background: #1a1a2e; color: #00f0ff; border: 1px solid #333; padding: 4px 8px; font-family: inherit; font-size: 13px; }
.controls button { background: #00f0ff; color: #000; border: none; padding: 6px 16px; cursor: pointer; font-family: inherit; font-weight: bold; font-size: 13px; }
.controls button:hover { background: #44ffff; }
.canvas-wrap { position: relative; display: inline-block; border: 2px solid #333; }
canvas { display: block; image-rendering: pixelated; }
.legend { margin-top: 20px; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; }
.legend-item { background: #111; padding: 8px 12px; border-left: 3px solid #00f0ff; font-size: 12px; }
.legend-item .row-num { color: #00f0ff; font-weight: bold; }
.legend-item .frames { color: #888; }
</style>
</head>
<body>
<h1>BOTFIGHTS SPRITE SHEET REFERENCE</h1>
<p class="info">576 x 768 PNG &mdash; 6 columns &times; 8 rows &mdash; each frame 96 &times; 96px (48&times;48 internal, 2x scaled)</p>
<div class="controls">
<label>Tier: <select id="tier">
<option value="0">0 - Baby</option>
<option value="1">1 - Bronze</option>
<option value="2" selected>2 - Silver</option>
<option value="3">3 - Gold</option>
<option value="4">4 - Platinum</option>
<option value="5">5 - Diamond/Legend</option>
</select></label>
<label>Hue: <input type="range" id="hue" min="0" max="359" value="200" style="width:120px"> <span id="hueVal">200</span></label>
<button onclick="generate()">Generate</button>
<button onclick="download()">Download PNG</button>
</div>
<div class="canvas-wrap">
<canvas id="sheet" width="576" height="768"></canvas>
</div>
<div class="canvas-wrap" style="margin-left: 20px; vertical-align: top; display: inline-block;">
<canvas id="annotated" width="720" height="900"></canvas>
</div>
<div class="legend">
<div class="legend-item"><span class="row-num">Row 0</span> idle <span class="frames">(4 frames, loop, speed 6)</span></div>
<div class="legend-item"><span class="row-num">Row 1</span> attack <span class="frames">(6 frames, once, speed 12)</span></div>
<div class="legend-item"><span class="row-num">Row 2</span> kick <span class="frames">(5 frames, once, speed 10)</span></div>
<div class="legend-item"><span class="row-num">Row 3</span> special <span class="frames">(6 frames, once, speed 8)</span></div>
<div class="legend-item"><span class="row-num">Row 4</span> hit <span class="frames">(3 frames, once, speed 8)</span></div>
<div class="legend-item"><span class="row-num">Row 5</span> knockback <span class="frames">(5 frames, once, speed 8)</span></div>
<div class="legend-item"><span class="row-num">Row 6</span> ko <span class="frames">(5 frames, once, speed 6)</span></div>
<div class="legend-item"><span class="row-num">Row 7</span> win <span class="frames">(4 frames, loop, speed 6)</span></div>
</div>
<script>
const FRAME = 96, INTERNAL = 48, SCALE = 2, MAX_FRAMES = 6, TOTAL_ROWS = 8
const ANIMS = [
{ name: 'idle', frames: 4, loop: true },
{ name: 'attack', frames: 6, loop: false },
{ name: 'kick', frames: 5, loop: false },
{ name: 'special', frames: 6, loop: false },
{ name: 'hit', frames: 3, loop: false },
{ name: 'knockback', frames: 5, loop: false },
{ name: 'ko', frames: 5, loop: false },
{ name: 'win', frames: 4, loop: true },
]
function hsl(h, s, l) { return `hsl(${h},${s}%,${l}%)` }
function makePal(h, tier) {
const h2 = (h + 140) % 360
const primary = hsl(h, 70, 50), secondary = hsl(h2, 80, 60)
return {
body: primary,
dark: hsl(h, 70, 30),
light: hsl(h, 75, 65),
acc: secondary,
accDark: hsl(h2, 80, 40),
accLight: hsl(h2, 80, 75),
out: '#0a0a0a',
skin: tier <= 1 ? primary : hsl(h, 40, 75),
skinDark: tier <= 1 ? hsl(h, 70, 40) : hsl(h, 35, 65),
}
}
function baseDims(tier) {
return {
bw: 10 + tier * 2, bh: 8 + tier,
hw: 10 + tier, hh: 9 + tier,
legH: 6 + tier, legW: 3 + Math.floor(tier * 0.5),
armW: 3, armH: 5 + tier,
}
}
function drawSheet(canvas, tier, hue) {
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.imageSmoothingEnabled = false
const pal = makePal(hue, tier)
const dims = baseDims(tier)
const specialType = 'fire'
const hasVisor = tier >= 2
const hasMohawk = tier >= 3
const hasHorns = false
function px(x, y, color, ox, oy) {
if (x < 0 || x >= INTERNAL || y < 0 || y >= INTERNAL) return
ctx.fillStyle = color
ctx.fillRect(ox + x * SCALE, oy + y * SCALE, SCALE, SCALE)
}
function box(x, y, w, h, fill, ox, oy) {
for (let i = x - 1; i <= x + w; i++) { px(i, y - 1, pal.out, ox, oy); px(i, y + h, pal.out, ox, oy) }
for (let i = y; i < y + h; i++) { px(x - 1, i, pal.out, ox, oy); px(x + w, i, pal.out, ox, oy) }
for (let iy = y; iy < y + h; iy++) for (let ix = x; ix < x + w; ix++) px(ix, iy, fill, ox, oy)
}
function fill(x, y, w, h, color, ox, oy) {
for (let iy = y; iy < y + h; iy++) for (let ix = x; ix < x + w; ix++) px(ix, iy, color, ox, oy)
}
const poses = ['idle', 'attack', 'kick', 'special', 'hit', 'knockback', 'ko', 'win']
const { bw, bh, hw, hh, legH, legW, armW, armH } = dims
for (let row = 0; row < TOTAL_ROWS; row++) {
const anim = ANIMS[row]
const pose = poses[row]
for (let frame = 0; frame < MAX_FRAMES; frame++) {
const ox = frame * FRAME, oy = row * FRAME
const t = frame / Math.max(1, anim.frames - 1)
const bounce = Math.round(Math.sin(t * Math.PI * 2))
const idle = pose === 'idle', atk = pose === 'attack', kick = pose === 'kick'
const special = pose === 'special', hit = pose === 'hit'
const knockback = pose === 'knockback', ko = pose === 'ko', win = pose === 'win'
// Dim frames beyond anim count
if (frame >= anim.frames) {
ctx.fillStyle = 'rgba(255,0,0,0.03)'
ctx.fillRect(ox, oy, FRAME, FRAME)
continue
}
const cx = 24, ground = 42
const feetY = ground - 2
const legsTop = feetY - legH
const bodyTop = legsTop - bh
const headTop = bodyTop - hh
const hOff = hit ? Math.round(t * 3) : knockback ? Math.round(t * 8) : ko ? 2 : 0
const vBounce = idle ? bounce : 0
const koSlump = ko ? Math.round(t * 5) : 0
const kbLift = knockback ? Math.round(Math.sin(t * Math.PI) * 6) : 0
const globalY = -kbLift
// Shadow
const shadowW = Math.floor(bw * 0.7) + (knockback ? 2 : 0)
for (let sx = cx - shadowW; sx <= cx + shadowW; sx++) {
px(sx, ground, 'rgba(0,0,0,0.25)', ox, oy)
px(sx, ground + 1, 'rgba(0,0,0,0.1)', ox, oy)
}
// Legs
const legGap = atk || kick ? Math.round(1 + t * 3) : ko ? 4 : knockback ? 3 : 1
const ll = cx - legGap - Math.floor(legW / 2) + hOff
const rl = cx + legGap - Math.floor(legW / 2) + hOff
if (ko) {
box(ll - 2, legsTop + koSlump + globalY, legW + 1, Math.max(1, legH - koSlump), pal.dark, ox, oy)
box(rl + 2, legsTop + koSlump + globalY, legW + 1, Math.max(1, legH - koSlump), pal.dark, ox, oy)
} else if (kick) {
box(ll, legsTop + globalY, legW, legH, pal.dark, ox, oy)
const kickExt = Math.round(Math.sin(t * Math.PI) * (legH + tier * 2))
box(rl, legsTop + Math.floor(legH * 0.3) + globalY, kickExt + legW, legW, pal.dark, ox, oy)
} else if (knockback) {
box(ll + Math.round(t * -3), legsTop + globalY + 2, legW, legH - 2, pal.dark, ox, oy)
box(rl + Math.round(t * -2), legsTop + globalY + 3, legW, legH - 3, pal.dark, ox, oy)
} else {
box(ll, legsTop + vBounce + globalY, legW, legH, pal.dark, ox, oy)
box(rl + (atk ? Math.round(t * 2) : 0), legsTop + vBounce + globalY, legW, legH, pal.dark, ox, oy)
}
// Feet
if (tier >= 2 && !ko && !knockback && !kick) {
box(ll - 1, feetY + vBounce + globalY, legW + 2, 2, pal.accDark, ox, oy)
box(rl - 1 + (atk ? Math.round(t * 2) : 0), feetY + vBounce + globalY, legW + 2, 2, pal.accDark, ox, oy)
}
// Body
const bx = cx - Math.floor(bw / 2) + hOff
const by = bodyTop + vBounce + koSlump + globalY
box(bx, by, bw, bh, pal.body, ox, oy)
for (let iy = by + 1; iy < by + bh - 1; iy++) {
px(bx + bw - 1, iy, pal.dark, ox, oy)
px(bx + bw - 2, iy, pal.dark, ox, oy)
px(bx + 1, iy, pal.light, ox, oy)
}
if (tier >= 1) for (let iy = by + 2; iy < by + bh - 1; iy += 2) for (let ix = bx + 2; ix < bx + bw - 2; ix++) px(ix, iy, pal.dark, ox, oy)
if (tier >= 2) { fill(bx, by + bh - 2, bw, 1, pal.acc, ox, oy); fill(bx, by + bh - 1, bw, 1, pal.accDark, ox, oy) }
if (tier >= 4) { const ey = by + Math.round(bh * 0.3); px(cx+hOff-1,ey,pal.acc,ox,oy); px(cx+hOff,ey,pal.accLight,ox,oy); px(cx+hOff+1,ey,pal.acc,ox,oy); px(cx+hOff,ey-1,pal.acc,ox,oy); px(cx+hOff,ey+1,pal.acc,ox,oy) }
if (tier >= 3 && !ko && !knockback) { const sy=by,pw=2+Math.floor(tier*0.5); box(bx-pw-1,sy,pw+1,3,pal.acc,ox,oy); box(bx+bw,sy,pw+1,3,pal.acc,ox,oy) }
// Arms
const armAttach = by + 2 + vBounce
const armLx = bx - armW + hOff, armRx = bx + bw + hOff
if (ko) { fill(armLx-3,armAttach+koSlump+2,armH,armW,pal.body,ox,oy); fill(armRx+2,armAttach+koSlump+2,armH,armW,pal.body,ox,oy) }
else if (knockback) { box(armLx-Math.round(t*4),armAttach+globalY-2,armW,armH+1,pal.body,ox,oy); box(armRx-Math.round(t*3),armAttach+globalY-1,armW,armH,pal.body,ox,oy) }
else if (atk) {
box(armLx,armAttach+2,armW,armH-2,pal.body,ox,oy)
const reach = Math.round(Math.sin(t*Math.PI)*(armH+tier*2))
if (reach>0) { box(armRx,armAttach-1,reach+armW,armW+1,pal.body,ox,oy); const fS=3+Math.floor(tier*0.5); box(armRx+reach+armW,armAttach-2,fS,fS+1,tier>=3?'#ff3333':pal.body,ox,oy) }
} else if (kick) { box(armLx,armAttach,armW,armH-1,pal.body,ox,oy); box(armRx,armAttach,armW,armH-1,pal.body,ox,oy) }
else if (special) {
box(armLx,armAttach,armW,armH,pal.body,ox,oy)
const ext = Math.round(Math.sin(t*Math.PI)*(armH+2))
box(armRx,armAttach-2,ext+armW+2,armW,pal.body,ox,oy)
if (t>0.3) { const projX=armRx+ext+armW+3+Math.round(t*8),projY=armAttach-2; px(projX,projY,'#ff4400',ox,oy); px(projX+1,projY,'#ff6600',ox,oy); px(projX,projY+1,'#ff8800',ox,oy); px(projX+1,projY+1,'#ffaa00',ox,oy); px(projX+2,projY,'#ffcc00',ox,oy) }
} else if (win) { box(armLx,armAttach+2,armW,armH-1,pal.body,ox,oy); box(armRx,armAttach-armH+bounce,armW,armH,pal.body,ox,oy) }
else { const sw=idle?bounce:hit?1:0; box(armLx,armAttach+sw+globalY,armW,armH,pal.body,ox,oy); box(armRx,armAttach-sw+globalY,armW,armH,pal.body,ox,oy) }
// Head
const hx = cx - Math.floor(hw / 2) + hOff
const hy = headTop + vBounce + koSlump + globalY
if (tier <= 1) {
box(hx,hy,hw,hh,pal.body,ox,oy)
for (let iy=hy+1;iy<hy+hh-1;iy++) px(hx+hw-1,iy,pal.dark,ox,oy)
px(cx+hOff,hy-1,pal.accDark,ox,oy); px(cx+hOff,hy-2-(idle?Math.abs(bounce):0),pal.acc,ox,oy); px(cx+hOff,hy-3-(idle?Math.abs(bounce):0),pal.acc,ox,oy)
px(cx+hOff-1,hy-3-(idle?Math.abs(bounce):0),pal.accDark,ox,oy); px(cx+hOff+1,hy-3-(idle?Math.abs(bounce):0),pal.accDark,ox,oy)
const eyeY = hy+Math.floor(hh*0.3)
if (ko) { px(hx+2,eyeY,'#ff0000',ox,oy); px(hx+3,eyeY+1,'#ff0000',ox,oy); px(hx+3,eyeY,'#330000',ox,oy); px(hx+2,eyeY+1,'#330000',ox,oy); px(hx+hw-3,eyeY,'#ff0000',ox,oy); px(hx+hw-4,eyeY+1,'#ff0000',ox,oy); px(hx+hw-4,eyeY,'#330000',ox,oy); px(hx+hw-3,eyeY+1,'#330000',ox,oy) }
else { fill(hx+2,eyeY,2,2,'#00ff41',ox,oy); fill(hx+hw-4,eyeY,2,2,'#00ff41',ox,oy) }
const mY = hy+Math.floor(hh*0.65); for (let mx=hx+2;mx<hx+hw-2;mx+=2) { px(mx,mY,pal.out,ox,oy); px(mx,mY+1,pal.out,ox,oy) }
} else {
box(hx+1,hy,hw-2,hh,pal.body,ox,oy)
for (let iy=hy+2;iy<hy+hh-2;iy++) { px(hx,iy,pal.body,ox,oy); px(hx+hw-1,iy,pal.body,ox,oy); px(hx-1,iy,pal.out,ox,oy); px(hx+hw,iy,pal.out,ox,oy) }
for (let iy=hy+2;iy<hy+hh-2;iy++) { px(hx+hw-1,iy,pal.dark,ox,oy); px(hx+hw-2,iy,pal.dark,ox,oy) }
const faceTop=hy+Math.floor(hh*0.25),faceBot=hy+Math.floor(hh*0.75)
for (let iy=faceTop;iy<faceBot;iy++) { for (let ix=hx+2;ix<hx+hw-2;ix++) px(ix,iy,pal.skin,ox,oy); px(hx+hw-3,iy,pal.skinDark,ox,oy) }
const eyeY=hy+Math.floor(hh*0.35),leX=hx+Math.floor(hw*0.2),reX=hx+Math.floor(hw*0.6),ew=Math.max(2,Math.floor(tier*0.5)+1)
if (ko) { px(leX,eyeY,'#ff0000',ox,oy);px(leX+1,eyeY+1,'#ff0000',ox,oy);px(leX+1,eyeY,'#880000',ox,oy);px(leX,eyeY+1,'#880000',ox,oy); px(reX,eyeY,'#ff0000',ox,oy);px(reX+1,eyeY+1,'#ff0000',ox,oy);px(reX+1,eyeY,'#880000',ox,oy);px(reX,eyeY+1,'#880000',ox,oy) }
else { fill(leX,eyeY,ew,2,'#ffffff',ox,oy); fill(reX,eyeY,ew,2,'#ffffff',ox,oy); px(leX+(atk||kick||special?1:0),eyeY+1,'#000000',ox,oy); px(reX+(atk||kick||special?1:0),eyeY+1,'#000000',ox,oy) }
if (atk||kick||special) { px(leX,eyeY-1,pal.out,ox,oy);px(leX+1,eyeY-1,pal.out,ox,oy);px(reX,eyeY-1,pal.out,ox,oy);px(reX+1,eyeY-1,pal.out,ox,oy) }
const mY=hy+Math.floor(hh*0.65)
if (win) { px(cx+hOff-2,mY,pal.out,ox,oy); fill(cx+hOff-1,mY,3,1,'#ffffff',ox,oy); px(cx+hOff+2,mY,pal.out,ox,oy) }
else if (ko||knockback) { box(cx+hOff-1,mY,3,2,'#000000',ox,oy) }
else if (atk||kick||special) { fill(cx+hOff-1,mY,3,2,'#000000',ox,oy) }
else { px(cx+hOff-1,mY,pal.out,ox,oy); px(cx+hOff,mY,pal.out,ox,oy) }
if (hasVisor) { const vY=eyeY-1; for (let vx=hx+1;vx<hx+hw-1;vx++) px(vx,vY,pal.accDark,ox,oy) }
if (hasMohawk) { for (let m=1;m<=Math.min(tier+1,5);m++) { px(cx+hOff,hy-m,pal.acc,ox,oy); if(m<=3) px(cx+hOff+1,hy-m,pal.accDark,ox,oy) } }
if (tier>=4) { const bY2=hy+2; for(let bx2=hx;bx2<hx+hw;bx2++) px(bx2,bY2,pal.acc,ox,oy); px(hx-1,bY2+1,pal.acc,ox,oy); px(hx-2,bY2+1,pal.acc,ox,oy); px(hx-3,bY2+2,pal.acc,ox,oy) }
if (tier>=5) { const cY=hy-1-(hasMohawk?5:0); for(let cx2=hx+1;cx2<hx+hw-1;cx2++) px(cx2,cY,'#ffd700',ox,oy); for(let cx2=hx+2;cx2<hx+hw-2;cx2++) px(cx2,cY+1,'#ffd700',ox,oy); px(hx+2,cY-1,'#ffd700',ox,oy); px(cx+hOff,cY-2,'#ffd700',ox,oy); px(hx+hw-3,cY-1,'#ffd700',ox,oy); px(cx+hOff,cY-1,'#ff2d7b',ox,oy) }
}
}
}
}
function drawAnnotated(srcCanvas, destCanvas) {
const ctx = destCanvas.getContext('2d')
ctx.clearRect(0, 0, destCanvas.width, destCanvas.height)
ctx.fillStyle = '#0a0a1a'
ctx.fillRect(0, 0, destCanvas.width, destCanvas.height)
const offsetX = 110, offsetY = 30
// Draw the sprite sheet
ctx.drawImage(srcCanvas, offsetX, offsetY)
// Grid lines
ctx.strokeStyle = '#333'
ctx.lineWidth = 0.5
for (let col = 0; col <= MAX_FRAMES; col++) {
ctx.beginPath(); ctx.moveTo(offsetX + col * FRAME, offsetY); ctx.lineTo(offsetX + col * FRAME, offsetY + TOTAL_ROWS * FRAME); ctx.stroke()
}
for (let row = 0; row <= TOTAL_ROWS; row++) {
ctx.beginPath(); ctx.moveTo(offsetX, offsetY + row * FRAME); ctx.lineTo(offsetX + MAX_FRAMES * FRAME, offsetY + row * FRAME); ctx.stroke()
}
// Row labels
ctx.font = '11px Courier New'
ctx.textAlign = 'right'
const labels = [
['Row 0: idle', '4f loop'],
['Row 1: attack', '6f once'],
['Row 2: kick', '5f once'],
['Row 3: special', '6f once'],
['Row 4: hit', '3f once'],
['Row 5: knockback', '5f once'],
['Row 6: ko', '5f once'],
['Row 7: win', '4f loop'],
]
for (let row = 0; row < TOTAL_ROWS; row++) {
const y = offsetY + row * FRAME + FRAME / 2
ctx.fillStyle = '#00f0ff'
ctx.fillText(labels[row][0], offsetX - 6, y - 6)
ctx.fillStyle = '#666'
ctx.fillText(labels[row][1], offsetX - 6, y + 8)
}
// Column numbers
ctx.textAlign = 'center'
for (let col = 0; col < MAX_FRAMES; col++) {
ctx.fillStyle = '#00f0ff'
ctx.fillText(`F${col}`, offsetX + col * FRAME + FRAME / 2, offsetY - 8)
}
// Frame count markers (red lines after last valid frame)
ctx.strokeStyle = 'rgba(255, 60, 60, 0.4)'
ctx.lineWidth = 2
const frameCounts = [4, 6, 5, 6, 3, 5, 5, 4]
for (let row = 0; row < TOTAL_ROWS; row++) {
const fc = frameCounts[row]
if (fc < MAX_FRAMES) {
const x = offsetX + fc * FRAME
const y1 = offsetY + row * FRAME + 2
const y2 = offsetY + (row + 1) * FRAME - 2
ctx.beginPath(); ctx.moveTo(x, y1); ctx.lineTo(x, y2); ctx.stroke()
}
}
// Title
ctx.font = 'bold 14px Courier New'
ctx.textAlign = 'left'
ctx.fillStyle = '#00f0ff'
ctx.fillText('BOTFIGHTS SPRITE SHEET', offsetX, offsetY - 18)
ctx.font = '10px Courier New'
ctx.fillStyle = '#666'
ctx.fillText(`576x768 | 96x96 frames | 48x48 internal (2x)`, offsetX + 200, offsetY - 18)
// Dimension callout
ctx.font = '10px Courier New'
ctx.fillStyle = '#888'
ctx.textAlign = 'center'
ctx.fillText('96px', offsetX + FRAME / 2, offsetY + TOTAL_ROWS * FRAME + 18)
ctx.fillText('576px total', offsetX + MAX_FRAMES * FRAME / 2, offsetY + TOTAL_ROWS * FRAME + 32)
ctx.save()
ctx.translate(offsetX + MAX_FRAMES * FRAME + 18, offsetY + TOTAL_ROWS * FRAME / 2)
ctx.rotate(Math.PI / 2)
ctx.fillText('768px total', 0, 0)
ctx.restore()
}
function generate() {
const tier = parseInt(document.getElementById('tier').value)
const hue = parseInt(document.getElementById('hue').value)
const sheet = document.getElementById('sheet')
const annotated = document.getElementById('annotated')
drawSheet(sheet, tier, hue)
drawAnnotated(sheet, annotated)
}
function download() {
const link = document.createElement('a')
link.download = 'botfights-spritesheet.png'
link.href = document.getElementById('sheet').toDataURL('image/png')
link.click()
}
document.getElementById('hue').oninput = function() { document.getElementById('hueVal').textContent = this.value }
generate()
</script>
</body>
</html>