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:
co-authored by
Claude Opus 4.6
parent
1906821452
commit
4b46aaf643
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user