feat: mobile human vs AI — multiple choice, two-row VS bar, responsive arena

- Server-side choice generation for all 22 challenge types (factual + creative)
- Mobile: 3 multiple-choice buttons replace keyboard input (4s timer)
- Two-row VS bar on mobile (names row + HP bars row) in FightPage and FightViewer
- Battle log hidden on mobile to maximize canvas visibility
- Arena page responsive: stacking header, tighter fight cards, no name overflow
- Profile page two-column desktop layout with contained sprite display
- Human fighter sprites with baby growth system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 19:42:49 +00:00
co-authored by Claude Opus 4.6
parent 1906821452
commit 4b46aaf643
9 changed files with 1115 additions and 249 deletions
+455
View File
@@ -1,3 +1,5 @@
import { FRAME_SIZE, INTERNAL, SCALE, ANIMATIONS, TOTAL_ROWS, MAX_FRAMES } from './constants'
// Human sprite — 3x resolution for detailed pixel art of the bot's owner
// Renders at 144x144 internal grid (vs 48x48 for bots) for crisp detail
@@ -1215,6 +1217,459 @@ export function generateHumanSpriteSheet(
return canvas.toDataURL()
}
/**
* Generate a human character sprite sheet in the BOT format (96px frame, 48x48 internal)
* with all 8 fighting animations. This allows humans to fight in the arena while looking
* like their profile character (same seed/archetype/winRate).
*/
export function generateHumanFighterSpriteSheet(
seed: string,
archetype: string,
primaryColor: string,
secondaryColor: string,
winRate: number = 0.5,
): string {
const canvas = document.createElement('canvas')
canvas.width = FRAME_SIZE * MAX_FRAMES
canvas.height = FRAME_SIZE * TOTAL_ROWS
const ctx = canvas.getContext('2d')!
ctx.imageSmoothingEnabled = false
const I = INTERNAL // 48
const S = SCALE // 2
const rng = seededRng(seed)
const skinTone = SKIN_TONES[Math.floor(rng() * SKIN_TONES.length)]
const skinDk = darken(skinTone, 20)
const skinLt = lighten(skinTone, 10)
const hairColor = HAIR_COLORS[Math.floor(rng() * HAIR_COLORS.length)]
const shirtColor = primaryColor
const shirtDk = darken(primaryColor, 15)
const pantsColor = secondaryColor
const pantsDk = darken(secondaryColor, 15)
const out = '#0a0a0a'
const wr = Math.max(0, Math.min(1, winRate))
const isBaby = wr < 0.1
const isToddler = wr >= 0.1 && wr < 0.25
const isKid = wr >= 0.25 && wr < 0.4
const isShort = isBaby || isToddler || isKid
const isFat = isBaby || isToddler ? true : wr < 0.5 ? rng() > 0.4 : false
const isSwole = wr > 0.75
const isSuperHero = wr > 0.85
const hasDiaper = isBaby
const hasCape = isSuperHero
const hasGlasses = isBaby || isToddler ? false : wr < 0.4 ? true : rng() > 0.5
const costume = getCostume(archetype)
function px(x: number, y: number, color: string, ox: number, oy: number) {
if (x < 0 || x >= I || y < 0 || y >= I) return
ctx.fillStyle = color
ctx.fillRect(ox + x * S, oy + y * S, S, S)
}
function fill(x: number, y: number, w: number, h: number, color: string, ox: number, oy: number) {
for (let iy = y; iy < y + h; iy++) for (let ix = x; ix < x + w; ix++) px(ix, iy, color, ox, oy)
}
function box(x: number, y: number, w: number, h: number, fc: string, ox: number, oy: number) {
for (let i = x - 1; i <= x + w; i++) { px(i, y - 1, out, ox, oy); px(i, y + h, out, ox, oy) }
for (let i = y; i < y + h; i++) { px(x - 1, i, out, ox, oy); px(x + w, i, out, ox, oy) }
fill(x, y, w, h, fc, ox, oy)
}
function drawFighterFrame(fx: number, fy: number, pose: string, frame: number, total: number) {
const ox = fx * FRAME_SIZE
const oy = fy * FRAME_SIZE
const t = frame / Math.max(1, total - 1)
const bounce = Math.round(Math.sin(t * Math.PI * 2))
const idle = pose === 'idle'
const atk = pose === 'attack'
const kick = pose === 'kick'
const special = pose === 'special'
const hit = pose === 'hit'
const knockback = pose === 'knockback'
const ko = pose === 'ko'
const win = pose === 'win'
// Body dimensions at 48x48 scale (adapted from human proportions)
const bodyW = isFat ? 14 : isSwole ? 12 : isShort ? 8 : 10
const bodyH = isShort ? 8 : isSwole ? 11 : 10
const headW = isFat ? 10 : isShort ? 10 : isSwole ? 10 : 9
const headH = isFat ? 9 : isShort ? 9 : 8
const legH = isShort ? 6 : isSwole ? 10 : 9
const legW = isFat ? 5 : isSwole ? 4 : 3
const armW = isSwole ? 3 : 2
const armH = isShort ? 5 : isSwole ? 8 : 7
const cx = 24 // center x in 48x48
const ground = 42
const feetY = ground - 2
const legsTop = feetY - legH
const bodyTop = legsTop - bodyH
const headTop = bodyTop - headH - 1
// Pose offsets
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(bodyW * 0.7)
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)
}
// Cape (behind everything for superhero)
if (hasCape && !ko) {
const capeBx = cx - Math.floor(bodyW / 2) - 1 + hOff
for (let iy = bodyTop + vBounce + globalY; iy < feetY + 4; iy++) {
const flutter = knockback ? Math.round(Math.sin(t * Math.PI * 3 + iy * 0.3) * 2) : 0
for (let ix = capeBx - flutter; ix < capeBx + bodyW + 2 + flutter; ix++) {
px(ix, iy, (iy + ix) % 3 === 0 ? '#881111' : '#cc2222', ox, oy)
}
}
}
// Diaper (babies, behind legs)
if (hasDiaper) {
const dw = bodyW + 2
const dx = cx - Math.floor(dw / 2) + hOff
fill(dx, legsTop + vBounce + globalY - 1, dw, 4, '#f5f5f0', ox, oy)
px(cx + hOff, legsTop + vBounce + globalY, '#66aadd', 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
const legColor = hasDiaper || isToddler ? skinTone : costume.onesie ? (costume.hat || shirtColor) : pantsColor
const legColorDk = hasDiaper || isToddler ? skinDk : costume.onesie ? darken(costume.hat || shirtColor, 15) : pantsDk
if (ko) {
box(ll - 2, legsTop + koSlump + globalY, legW + 1, Math.max(1, legH - koSlump), legColorDk, ox, oy)
box(rl + 2, legsTop + koSlump + globalY, legW + 1, Math.max(1, legH - koSlump), legColorDk, ox, oy)
} else if (kick) {
box(ll, legsTop + globalY, legW, legH, legColor, ox, oy)
const kickExt = Math.round(Math.sin(t * Math.PI) * (legH + 2))
box(rl, legsTop + Math.floor(legH * 0.3) + globalY, kickExt + legW, legW, legColor, ox, oy)
if (kickExt > 2) fill(rl + kickExt + legW, legsTop + Math.floor(legH * 0.3) - 1 + globalY, 3, 3, skinTone, ox, oy)
} else if (knockback) {
box(ll + Math.round(t * -3), legsTop + globalY + 2, legW, legH - 2, legColor, ox, oy)
box(rl + Math.round(t * -2), legsTop + globalY + 3, legW, legH - 3, legColor, ox, oy)
} else {
box(ll, legsTop + vBounce + globalY, legW, legH, legColor, ox, oy)
box(rl + (atk ? Math.round(t * 2) : 0), legsTop + vBounce + globalY, legW, legH, legColor, ox, oy)
}
// Shoes
if (!ko && !knockback) {
fill(ll - 1, feetY + vBounce + globalY, legW + 2, 2, '#333333', ox, oy)
fill(rl - 1 + (atk ? Math.round(t * 2) : 0), feetY + vBounce + globalY, legW + 2, 2, '#333333', ox, oy)
}
// Body
const bx = cx - Math.floor(bodyW / 2) + hOff
const by = bodyTop + vBounce + koSlump + globalY
const torsoColor = costume.onesie ? (costume.hat || shirtColor) : shirtColor
const torsoDk = costume.onesie ? darken(costume.hat || shirtColor, 15) : shirtDk
box(bx, by, bodyW, bodyH, torsoColor, ox, oy)
// Shading
for (let iy = by + 1; iy < by + bodyH - 1; iy++) {
px(bx + bodyW - 1, iy, torsoDk, ox, oy)
px(bx + 1, iy, lighten(torsoColor, 8), ox, oy)
}
// Zipper for onesies
if (costume.onesie) {
for (let iy = by + 1; iy < by + bodyH - 1; iy++) px(cx + hOff, iy, darken(torsoColor, 20), ox, oy)
}
// Super mode: show skin and abs hint
if (isSwole && !costume.onesie) {
fill(bx + 2, by + 2, bodyW - 4, bodyH - 4, skinTone, ox, oy)
px(cx + hOff, by + 3, skinDk, ox, oy)
px(cx + hOff, by + 5, skinDk, ox, oy)
px(cx + hOff, by + 7, skinDk, ox, oy)
}
// Belt
if (!costume.onesie) {
fill(bx, by + bodyH - 2, bodyW, 2, isSuperHero ? '#ffd700' : '#443322', ox, oy)
px(cx + hOff, by + bodyH - 2, '#ffcc00', ox, oy)
}
// Arms
const armAttach = by + 2 + vBounce
const armLx = bx - armW + hOff
const armRx = bx + bodyW + hOff
const armColor = costume.onesie ? (costume.hat || shirtColor) : skinTone
const armColorDk = costume.onesie ? darken(costume.hat || shirtColor, 15) : skinDk
if (ko) {
fill(armLx - 3, armAttach + koSlump + 2, armH, armW, armColor, ox, oy)
fill(armRx + 2, armAttach + koSlump + 2, armH, armW, armColor, ox, oy)
} else if (knockback) {
box(armLx - Math.round(t * 4), armAttach + globalY - 2, armW, armH + 1, armColor, ox, oy)
box(armRx - Math.round(t * 3), armAttach + globalY - 1, armW, armH, armColor, ox, oy)
} else if (atk) {
box(armLx, armAttach + 2, armW, armH - 2, armColor, ox, oy)
const reach = Math.round(Math.sin(t * Math.PI) * (armH + 2))
if (reach > 0) {
box(armRx, armAttach - 1, reach + armW, armW + 1, armColor, ox, oy)
// Fist
fill(armRx + reach + armW, armAttach - 2, 3, 3, skinTone, ox, oy)
}
} else if (kick) {
box(armLx, armAttach, armW, armH - 1, armColor, ox, oy)
box(armRx, armAttach, armW, armH - 1, armColor, ox, oy)
} else if (special) {
box(armLx, armAttach, armW, armH, armColor, ox, oy)
const ext = Math.round(Math.sin(t * Math.PI) * (armH + 2))
box(armRx, armAttach - 2, ext + armW + 2, armW, armColor, ox, oy)
// Projectile (energy ball)
if (t > 0.3) {
const projX = armRx + ext + armW + 3 + Math.round(t * 8)
const projY = armAttach - 2
px(projX, projY, '#ff4400', ox, oy)
px(projX + 1, projY, '#ff6600', ox, oy)
px(projX, projY + 1, '#ffaa00', ox, oy)
px(projX + 1, projY + 1, '#ffcc00', ox, oy)
px(projX + 2, projY, '#ffcc00', ox, oy)
}
} else if (win) {
box(armLx, armAttach + 2, armW, armH - 1, armColor, ox, oy)
box(armRx, armAttach - armH + bounce, armW, armH, armColor, ox, oy)
// Fist pump up
fill(armRx - 1, armAttach - armH + bounce - 2, armW + 2, 2, skinTone, ox, oy)
} else {
const sw = idle ? bounce : hit ? 1 : 0
box(armLx, armAttach + sw + globalY, armW, armH, armColor, ox, oy)
box(armRx, armAttach - sw + globalY, armW, armH, armColor, ox, oy)
// Hands
fill(armLx, armAttach + sw + armH + globalY, armW + 1, 2, skinTone, ox, oy)
fill(armRx - 1, armAttach - sw + armH + globalY, armW + 1, 2, skinTone, ox, oy)
}
// Head
const hx = cx - Math.floor(headW / 2) + hOff
const hy = headTop + vBounce + koSlump + globalY
const faceColor = costume.facePaint || skinTone
const faceDk = costume.facePaint ? darken(costume.facePaint, 15) : skinDk
const faceLt = costume.facePaint ? lighten(costume.facePaint, 10) : skinLt
box(hx, hy, headW, headH, faceColor, ox, oy)
// Head shading
for (let iy = hy + 1; iy < hy + headH - 1; iy++) {
px(hx + headW - 1, iy, faceDk, ox, oy)
px(hx + 1, iy, faceLt, ox, oy)
}
// Ears
if (!costume.facePaint) {
px(hx - 1, hy + Math.floor(headH * 0.4), skinTone, ox, oy)
px(hx + headW, hy + Math.floor(headH * 0.4), skinTone, ox, oy)
}
// Hair (baby = tuft, otherwise full)
if (isBaby) {
px(cx + hOff, hy - 1, hairColor, ox, oy)
px(cx + hOff - 1, hy - 1, hairColor, ox, oy)
px(cx + hOff + 1, hy, hairColor, ox, oy)
} else if (!costume.onesie || costume.type !== 'animal') {
for (let ix = hx; ix < hx + headW; ix++) {
px(ix, hy, hairColor, ox, oy)
px(ix, hy + 1, hairColor, ox, oy)
}
for (let ix = hx + 1; ix < hx + headW - 1; ix++) px(ix, hy - 1, hairColor, ox, oy)
px(hx + 2, hy, lighten(hairColor, 15), ox, oy)
}
// Eyes
const eyeY = hy + Math.floor(headH * 0.3)
const leX = hx + 2
const reX = hx + headW - 4
if (ko) {
px(leX, eyeY, '#ff0000', ox, oy); px(leX + 1, eyeY + 1, '#ff0000', ox, oy)
px(leX + 1, eyeY, '#330000', ox, oy); px(leX, eyeY + 1, '#330000', ox, oy)
px(reX, eyeY, '#ff0000', ox, oy); px(reX + 1, eyeY + 1, '#ff0000', ox, oy)
px(reX + 1, eyeY, '#330000', ox, oy); px(reX, eyeY + 1, '#330000', ox, oy)
} else if (knockback) {
fill(leX, eyeY, 2, 2, '#ffffff', ox, oy)
fill(reX, eyeY, 2, 2, '#ffffff', ox, oy)
px(leX, eyeY + 1, '#000000', ox, oy)
px(reX, eyeY + 1, '#000000', ox, oy)
} else if (isBaby || isToddler) {
// Big sparkly baby eyes
fill(leX, eyeY, 3, 3, '#ffffff', ox, oy)
fill(reX, eyeY, 3, 3, '#ffffff', ox, oy)
fill(leX + 1, eyeY + 1, 2, 2, '#5588cc', ox, oy)
fill(reX + 1, eyeY + 1, 2, 2, '#5588cc', ox, oy)
px(leX + 1, eyeY + 1, '#000000', ox, oy)
px(reX + 1, eyeY + 1, '#000000', ox, oy)
px(leX, eyeY, '#ffffff', ox, oy)
px(reX, eyeY, '#ffffff', ox, oy)
} else {
fill(leX, eyeY, 2, 2, '#ffffff', ox, oy)
fill(reX, eyeY, 2, 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)
// Angry brows for attack/kick/special
if (atk || kick || special) {
px(leX, eyeY - 1, out, ox, oy); px(leX + 1, eyeY - 1, out, ox, oy)
px(reX, eyeY - 1, out, ox, oy); px(reX + 1, eyeY - 1, out, ox, oy)
}
}
// Glasses
if (hasGlasses && !ko) {
for (let ix = leX - 1; ix <= reX + 2; ix++) px(ix, eyeY - 1, '#444444', ox, oy)
px(leX - 1, eyeY, '#444444', ox, oy); px(leX + 2, eyeY, '#444444', ox, oy)
px(reX - 1, eyeY, '#444444', ox, oy); px(reX + 2, eyeY, '#444444', ox, oy)
}
// Nose
px(cx + hOff, hy + Math.floor(headH * 0.55), skinDk, ox, oy)
// Mouth
const mY = hy + Math.floor(headH * 0.7)
if (win) {
px(cx + hOff - 1, mY, out, ox, oy); fill(cx + hOff, mY, 2, 1, '#ffffff', ox, oy); px(cx + hOff + 2, mY, out, ox, oy)
} else if (ko || knockback) {
fill(cx + hOff - 1, mY, 2, 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, out, ox, oy); px(cx + hOff, mY, out, ox, oy)
}
// Baby rosy cheeks
if (isBaby || isToddler) {
px(hx + 1, hy + Math.floor(headH * 0.55), '#ffaaaa', ox, oy)
px(hx + headW - 2, hy + Math.floor(headH * 0.55), '#ffaaaa', ox, oy)
}
// Costume hat
const hatColor = costume.hat || hairColor
const hatDk = darken(hatColor, 15)
const hatLt = lighten(hatColor, 15)
if (costume.hatShape === 'ears') {
fill(hx, hy - 2, 2, 3, hatColor, ox, oy)
fill(hx + headW - 2, hy - 2, 2, 3, hatColor, ox, oy)
px(hx, hy - 1, hatLt, ox, oy); px(hx + headW - 2, hy - 1, hatLt, ox, oy)
} else if (costume.hatShape === 'helmet') {
for (let ix = hx - 1; ix < hx + headW + 1; ix++) { px(ix, hy - 2, hatColor, ox, oy); px(ix, hy - 1, hatColor, ox, oy) }
px(hx + 1, hy - 2, hatLt, ox, oy)
} else if (costume.hatShape === 'pointy') {
for (let h = 0; h < 6; h++) {
const w = Math.max(1, Math.floor(headW * (1 - h / 6)))
const sx = cx + hOff - Math.floor(w / 2)
for (let ix = sx; ix < sx + w; ix++) px(ix, hy - 1 - h, hatColor, ox, oy)
}
px(cx + hOff, hy - 7, '#ffcc44', ox, oy)
} else if (costume.hatShape === 'wide') {
for (let ix = hx - 3; ix < hx + headW + 3; ix++) { px(ix, hy - 2, hatColor, ox, oy); px(ix, hy - 1, hatColor, ox, oy) }
for (let ix = hx - 1; ix < hx + headW + 1; ix++) { px(ix, hy - 3, hatColor, ox, oy); px(ix, hy - 4, hatColor, ox, oy) }
} else if (costume.hatShape === 'tall') {
for (let h = 0; h < 6; h++) for (let ix = hx + 1; ix < hx + headW - 1; ix++) px(ix, hy - 1 - h, hatColor, ox, oy)
fill(hx + 2, hy - 5, headW - 4, 1, hatLt, ox, oy)
} else if (costume.hatShape === 'horns') {
for (let h = 0; h < 3; h++) {
px(hx - h, hy - 1 - h, hatColor, ox, oy)
px(hx + headW - 1 + h, hy - 1 - h, hatColor, ox, oy)
}
px(hx - 3, hy - 4, hatLt, ox, oy); px(hx + headW + 2, hy - 4, hatLt, ox, oy)
} else if (costume.hatShape === 'antenna') {
fill(hx + 1, hy - 1, headW - 2, 1, '#888888', ox, oy)
for (let h = 0; h < 4; h++) px(hx + 2, hy - 2 - h, '#888888', ox, oy)
for (let h = 0; h < 4; h++) px(hx + headW - 3, hy - 2 - h, '#888888', ox, oy)
fill(hx + 1, hy - 6, 2, 2, '#ff2222', ox, oy)
fill(hx + headW - 4, hy - 6, 2, 2, '#ff2222', ox, oy)
} else if (costume.hatShape === 'cone') {
for (let h = 0; h < 6; h++) {
const w = Math.max(1, 6 - h)
const sx = cx + hOff - Math.floor(w / 2)
const stripe = h % 2 === 0 ? '#ffffff' : hatColor
for (let ix = sx; ix < sx + w; ix++) px(ix, hy - 1 - h, stripe, ox, oy)
}
} else if (costume.hatShape === 'crown') {
for (let ix = hx; ix < hx + headW; ix++) px(ix, hy - 1, '#ffd700', ox, oy)
px(hx + 1, hy - 2, '#ffd700', ox, oy); px(cx + hOff, hy - 3, '#ffd700', ox, oy); px(hx + headW - 2, hy - 2, '#ffd700', ox, oy)
}
// Tail
if (costume.tail && !ko) {
const tailColor = costume.hat || shirtColor
for (let i = 0; i < 4; i++) {
px(bx - 1 - i, by + bodyH - 2 + Math.round(Math.sin(i * 0.8 + t * Math.PI * 2)), tailColor, ox, oy)
}
}
// Onesie hood (for animal costumes, covers hair)
if (costume.onesie && costume.type === 'animal') {
const oc = costume.hat || shirtColor
fill(hx, hy, headW, 2, oc, ox, oy)
fill(hx - 1, hy, 1, headH - 2, oc, ox, oy)
fill(hx + headW, hy, 1, headH - 2, oc, ox, oy)
}
// Hit spark
if (hit && t > 0.2) {
const sx = cx + hOff + Math.floor(bodyW / 2) + 3
const sy = by + 2
px(sx, sy, '#ffffff', ox, oy); px(sx - 1, sy, '#ffff00', ox, oy); px(sx + 1, sy, '#ffff00', ox, oy)
px(sx, sy - 1, '#ffff00', ox, oy); px(sx, sy + 1, '#ffff00', ox, oy)
}
// Knockback stars
if (knockback) {
for (let s = 0; s < 3; s++) {
const sa = t * Math.PI + s * 2.1
const sr = 5 + s * 3
const sx = cx + hOff - 2 + Math.round(Math.cos(sa) * sr)
const sy = hy - 2 + Math.round(Math.sin(sa) * sr * 0.5)
px(sx, sy, '#ffff00', ox, oy)
px(sx + 1, sy, '#ffffff', ox, oy)
}
}
// Win sparkles
if (win) {
for (let i = 0; i < 4; i++) {
const sa = t * Math.PI * 2 + i * 1.5
const sr = 10
const sx = cx + hOff + Math.round(Math.cos(sa) * sr)
const sy = by + Math.floor(bodyH / 2) + Math.round(Math.sin(sa) * sr * 0.5)
if ((frame + i) % 2 === 0) { px(sx, sy, '#ffd700', ox, oy); px(sx + 1, sy, '#ffffff', ox, oy) }
}
}
// Super hero glow
if (isSuperHero && !ko) {
for (let a = 0; a < 8; a++) {
const angle = a * Math.PI * 2 / 8 + t * Math.PI * 0.5
const r = Math.floor(bodyW / 2) + 6
const gx = cx + hOff + Math.round(Math.cos(angle) * r)
const gy = by + Math.floor(bodyH / 2) + Math.round(Math.sin(angle) * r * 0.6)
if ((frame + a) % 3 !== 0) px(gx, gy, a % 2 === 0 ? '#ffd700' : '#ffcc4460', ox, oy)
}
}
}
const entries = Object.entries(ANIMATIONS) as [string, { frames: number; row: number }][]
for (let row = 0; row < entries.length; row++) {
const [pose, cfg] = entries[row]
for (let f = 0; f < cfg.frames; f++) drawFighterFrame(f, row, pose, f, cfg.frames)
for (let f = cfg.frames; f < MAX_FRAMES; f++) drawFighterFrame(f, row, pose, cfg.frames - 1, cfg.frames)
}
return canvas.toDataURL()
}
function getCostume(archetype: string): CostumeConfig {
const raw = COSTUME_MAP[archetype] || {}
return {
+1 -1
View File
@@ -7,7 +7,7 @@ export { FRAME_SIZE, ANIMATIONS, MAX_FRAMES, TOTAL_ROWS } from './constants'
export type { Pal, Archetype, ArchetypeParams, Dimensions } from './constants'
export { getBotColors } from './palette'
export { generateJudgeSpriteSheet, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS } from './judge'
export { generateHumanSpriteSheet, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, HUMAN_FRAME_SIZE } from './human'
export { generateHumanSpriteSheet, generateHumanFighterSpriteSheet, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, HUMAN_FRAME_SIZE } from './human'
export { archetypes, rollArchetype } from './archetypes'
export interface SpriteCustomization {