// 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 export const HUMAN_FRAME_SIZE = 288 export const HUMAN_INTERNAL = 144 export const HUMAN_SCALE = HUMAN_FRAME_SIZE / HUMAN_INTERNAL export const HUMAN_ANIMATIONS = { idle: { frames: 4, row: 0 }, cheer: { frames: 4, row: 1 }, panic: { frames: 4, row: 2 }, run: { frames: 4, row: 3 }, coach: { frames: 4, row: 4 }, } export const HUMAN_ROWS = Object.keys(HUMAN_ANIMATIONS).length export const HUMAN_MAX_FRAMES = 4 // Costume system type CostumeType = 'animal' | 'armor' | 'hat' | 'tech' | 'monster' | 'food' | 'silly' | 'generic' interface CostumeConfig { type: CostumeType hat?: string hatShape: 'none' | 'ears' | 'helmet' | 'pointy' | 'wide' | 'tall' | 'horns' | 'antenna' | 'crown' | 'cone' prop?: string propShape: 'none' | 'sword' | 'wand' | 'phone' | 'sign' | 'shield' | 'food' | 'remote' onesie: boolean facePaint?: string tail: boolean } const COSTUME_MAP: Record> = { dog: { type: 'animal', hatShape: 'ears', hat: '#aa7744', onesie: true, tail: true }, cat: { type: 'animal', hatShape: 'ears', hat: '#ff8844', onesie: true, tail: true }, frog: { type: 'animal', hatShape: 'ears', hat: '#44aa22', onesie: true }, shark: { type: 'animal', hatShape: 'tall', hat: '#4477aa', onesie: true }, penguin: { type: 'animal', hatShape: 'none', hat: '#222222', onesie: true }, octopus: { type: 'animal', hatShape: 'none', hat: '#cc44aa', onesie: true }, bee: { type: 'animal', hatShape: 'antenna', hat: '#ffcc00', onesie: true }, snail: { type: 'animal', hatShape: 'antenna', hat: '#88aa44', onesie: true }, lobster: { type: 'animal', hatShape: 'antenna', hat: '#cc2222', onesie: true }, sheep: { type: 'animal', hatShape: 'ears', hat: '#eeeeee', onesie: true }, elephant: { type: 'animal', hatShape: 'ears', hat: '#888899', onesie: true }, giraffe: { type: 'animal', hatShape: 'horns', hat: '#ddaa44', onesie: true }, hippo: { type: 'animal', hatShape: 'ears', hat: '#8877aa', onesie: true }, lion: { type: 'animal', hatShape: 'ears', hat: '#cc8822', onesie: true }, monkey: { type: 'animal', hatShape: 'ears', hat: '#884422', onesie: true, tail: true }, parrot: { type: 'animal', hatShape: 'tall', hat: '#ff4422', onesie: true, tail: true }, raccoon: { type: 'animal', hatShape: 'ears', hat: '#666666', onesie: true, tail: true }, snake: { type: 'animal', hatShape: 'none', hat: '#448822', onesie: true, tail: true }, turtle: { type: 'animal', hatShape: 'none', hat: '#44aa44', onesie: true }, whale: { type: 'animal', hatShape: 'none', hat: '#4466bb', onesie: true }, crocodile: { type: 'animal', hatShape: 'none', hat: '#447722', onesie: true, tail: true }, flamingo: { type: 'animal', hatShape: 'tall', hat: '#ff66aa', onesie: true }, hedgehog: { type: 'animal', hatShape: 'tall', hat: '#886644', onesie: true }, panda: { type: 'animal', hatShape: 'ears', hat: '#222222', onesie: true }, hamster: { type: 'animal', hatShape: 'ears', hat: '#ddaa77', onesie: true }, dinosaur: { type: 'animal', hatShape: 'horns', hat: '#44aa44', onesie: true, tail: true }, knight: { type: 'armor', hatShape: 'helmet', hat: '#aaaaaa', propShape: 'sword', prop: '#cccccc' }, gladiator: { type: 'armor', hatShape: 'helmet', hat: '#cc8833', propShape: 'sword', prop: '#dddddd' }, samurai: { type: 'armor', hatShape: 'helmet', hat: '#882222', propShape: 'sword', prop: '#cccccc' }, viking: { type: 'armor', hatShape: 'horns', hat: '#886633', propShape: 'shield', prop: '#aa8844' }, boxer: { type: 'armor', hatShape: 'none', propShape: 'none', facePaint: '#cc2222' }, wrestler: { type: 'armor', hatShape: 'none', propShape: 'none', facePaint: '#ffcc00' }, pirate: { type: 'hat', hatShape: 'wide', hat: '#222222', propShape: 'sword', prop: '#dddddd' }, cowboy: { type: 'hat', hatShape: 'wide', hat: '#aa7744' }, wizard: { type: 'hat', hatShape: 'pointy', hat: '#4422aa', propShape: 'wand', prop: '#ffcc44' }, witch: { type: 'hat', hatShape: 'pointy', hat: '#222222', propShape: 'wand', prop: '#44ff44' }, chef: { type: 'hat', hatShape: 'tall', hat: '#ffffff', propShape: 'food', prop: '#ff8844' }, detective: { type: 'hat', hatShape: 'wide', hat: '#554433' }, nurse: { type: 'hat', hatShape: 'none', hat: '#ffffff' }, firefighter:{ type: 'hat', hatShape: 'helmet', hat: '#ff2222' }, astronaut: { type: 'hat', hatShape: 'helmet', hat: '#eeeeee' }, lumberjack: { type: 'hat', hatShape: 'none', hat: '#cc4422', propShape: 'sword', prop: '#886633' }, scientist: { type: 'hat', hatShape: 'none', propShape: 'remote', prop: '#44cc44' }, clown: { type: 'hat', hatShape: 'tall', hat: '#ff2222', facePaint: '#ffffff' }, robot: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, android: { type: 'tech', hatShape: 'antenna', propShape: 'phone' }, drone_bug: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, toaster: { type: 'tech', hatShape: 'none', propShape: 'remote' }, tv_head: { type: 'tech', hatShape: 'none', propShape: 'phone', facePaint: '#4488ff' }, calculator: { type: 'tech', hatShape: 'none', propShape: 'phone' }, satellite: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, mech: { type: 'tech', hatShape: 'helmet', hat: '#888888', propShape: 'remote' }, led_cube: { type: 'tech', hatShape: 'none', propShape: 'phone' }, circuit: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, antenna_bug:{ type: 'tech', hatShape: 'antenna', propShape: 'phone' }, microwave: { type: 'tech', hatShape: 'none', propShape: 'remote' }, cyberdog: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, robocat: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, ufo_bot: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, cyborg: { type: 'tech', hatShape: 'antenna', propShape: 'remote' }, vampire: { type: 'monster', hatShape: 'none', facePaint: '#ffffff' }, werewolf: { type: 'monster', hatShape: 'ears', hat: '#664422', facePaint: '#664422' }, zombie: { type: 'monster', hatShape: 'none', facePaint: '#668844' }, demon: { type: 'monster', hatShape: 'horns', hat: '#cc2222', facePaint: '#cc2222' }, skeleton: { type: 'monster', hatShape: 'none', facePaint: '#ffffff' }, ghost: { type: 'monster', hatShape: 'none', onesie: true, hat: '#ffffff' }, alien: { type: 'monster', hatShape: 'antenna', hat: '#44ff44', facePaint: '#44ff44' }, minotaur: { type: 'monster', hatShape: 'horns', hat: '#884422' }, unicorn: { type: 'monster', hatShape: 'horns', hat: '#ff88cc' }, phoenix: { type: 'monster', hatShape: 'tall', hat: '#ff4400', onesie: true }, dragon: { type: 'monster', hatShape: 'horns', hat: '#44aa22', onesie: true, tail: true }, mermaid: { type: 'monster', hatShape: 'none', hat: '#44ccaa', onesie: true }, griffin: { type: 'monster', hatShape: 'ears', hat: '#cc8822', onesie: true }, cyclops: { type: 'monster', hatShape: 'none', facePaint: '#8877aa' }, gargoyle: { type: 'monster', hatShape: 'horns', hat: '#666666', facePaint: '#888888' }, golem: { type: 'monster', hatShape: 'none', facePaint: '#886644' }, pizza: { type: 'food', hatShape: 'cone', hat: '#ffcc44' }, mushroom: { type: 'food', hatShape: 'tall', hat: '#cc2222' }, cactus: { type: 'food', hatShape: 'tall', hat: '#44aa22' }, sock_puppet: { type: 'silly', hatShape: 'none', onesie: true, hat: '#ff8844' }, traffic_cone: { type: 'silly', hatShape: 'cone', hat: '#ff6600' }, toilet_man: { type: 'silly', hatShape: 'none', propShape: 'sign', prop: '#ffffff' }, potato: { type: 'silly', hatShape: 'none', hat: '#aa8844', onesie: true }, cloud_man: { type: 'silly', hatShape: 'none', hat: '#ffffff', onesie: true }, rock_man: { type: 'silly', hatShape: 'none', hat: '#888888', onesie: true }, balloon_man: { type: 'silly', hatShape: 'none', propShape: 'sign', prop: '#ff4488' }, trash_can: { type: 'silly', hatShape: 'none', hat: '#888888', onesie: true }, rubber_duck: { type: 'silly', hatShape: 'none', hat: '#ffcc00', onesie: true }, snowman: { type: 'silly', hatShape: 'tall', hat: '#222222', onesie: true }, scarecrow: { type: 'silly', hatShape: 'wide', hat: '#ddbb66', onesie: true }, jack_o_lantern:{ type: 'silly', hatShape: 'none', hat: '#ff8800', facePaint: '#ff8800' }, garden_gnome: { type: 'silly', hatShape: 'pointy', hat: '#ff0000' }, lamp_post: { type: 'silly', hatShape: 'cone', hat: '#ffee88' }, broom_man: { type: 'silly', hatShape: 'none', propShape: 'sword', prop: '#886633' }, ninja: { type: 'hat', hatShape: 'none', facePaint: '#222222', onesie: true, hat: '#222222' }, } const SKIN_TONES = [ '#ffdbb4', '#f1c27d', '#e0ac69', '#c68642', '#8d5524', '#6b3a1f', '#ffe0bd', '#ffcd94', '#eac086', '#deb887', '#d2a679', '#a0785a', ] const HAIR_COLORS = [ '#2c1b0e', '#4a3728', '#8b6914', '#c4a35a', '#e6c87f', '#1a1a1a', '#333333', '#cc2222', '#ff6633', '#ff88cc', '#4488ff', ] type PxFn = (x: number, y: number, color: string, ox: number, oy: number) => void function seededRng(seed: string) { let sh = 0 for (let i = 0; i < seed.length; i++) sh = ((sh << 5) - sh + seed.charCodeAt(i)) | 0 sh = (sh * 31337) | 0 return () => { sh = (sh * 16807) % 2147483647; return (sh & 0x7fffffff) / 2147483647 } } /** * Generate human sprite at 3x resolution. winRate (0-1) controls physique: * 0.0 = fat slobby mess with cheetos, 0.5 = normal, 1.0 = swole superhero with cape */ export function generateHumanSpriteSheet( seed: string, archetype: string, primaryColor: string, secondaryColor: string, winRate: number = 0.5, ): string { const canvas = document.createElement('canvas') canvas.width = HUMAN_FRAME_SIZE * HUMAN_MAX_FRAMES canvas.height = HUMAN_FRAME_SIZE * HUMAN_ROWS const ctx = canvas.getContext('2d')! ctx.imageSmoothingEnabled = false const I = HUMAN_INTERNAL const S = HUMAN_SCALE const rng = seededRng(seed) const skinTone = SKIN_TONES[Math.floor(rng() * SKIN_TONES.length)] const skinMid = darken(skinTone, 12) const skinDark = darken(skinTone, 30) const skinLight = lighten(skinTone, 10) const hairColor = HAIR_COLORS[Math.floor(rng() * HAIR_COLORS.length)] const hairDark = darken(hairColor, 20) const shirtColor = primaryColor const shirtDark = darken(primaryColor, 15) const pantsColor = secondaryColor const pantsDark = darken(secondaryColor, 15) const wr = Math.max(0, Math.min(1, winRate)) const isFat = wr < 0.3 ? true : wr < 0.5 ? rng() > 0.4 : false const isShort = wr < 0.25 const isSwole = wr > 0.75 const isSuperHero = wr > 0.85 const isSuperMode = wr > 0.9 const hasBeard = wr < 0.35 ? true : isSuperMode ? false : rng() > 0.6 const hasGlasses = wr < 0.4 ? true : isSuperMode ? false : rng() > 0.5 const hasStains = wr < 0.3 const hasCheetos = wr < 0.2 const hasCape = isSuperHero const hasHeadband = wr > 0.7 const hasSweatDrops = wr < 0.4 const costume = getCostume(archetype) const out = '#0a0a0a' 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) } // Rounded box for softer shapes function roundBox(x: number, y: number, w: number, h: number, fc: string, ox: number, oy: number) { fill(x + 1, y, w - 2, h, fc, ox, oy) fill(x, y + 1, w, h - 2, fc, ox, oy) // Outline for (let i = x + 1; i < x + w - 1; i++) { px(i, y - 1, out, ox, oy); px(i, y + h, out, ox, oy) } for (let i = y + 1; i < y + h - 1; i++) { px(x - 1, i, out, ox, oy); px(x + w, i, out, ox, oy) } px(x, y, out, ox, oy); px(x + w - 1, y, out, ox, oy) px(x, y + h - 1, out, ox, oy); px(x + w - 1, y + h - 1, out, ox, oy) } function drawHuman(fx: number, fy: number, pose: string, frame: number, total: number) { const ox = fx * HUMAN_FRAME_SIZE const oy = fy * HUMAN_FRAME_SIZE const t = frame / Math.max(1, total - 1) const bounce = Math.round(Math.sin(t * Math.PI * 2) * 2) const isIdle = pose === 'idle' const isCheer = pose === 'cheer' const isPanic = pose === 'panic' const isRun = pose === 'run' const isCoach = pose === 'coach' const cx = 72 // center x const ground = 126 // ground line // Body dimensions (3x base, adjusted by physique) const bodyW = isFat ? 42 : isSuperMode ? 44 : isSwole ? 36 : 27 const bodyH = isShort ? 24 : isSuperMode ? 36 : isSwole ? 33 : 30 const headW = isFat ? 30 : isSuperMode ? 26 : isSwole ? 28 : 27 const headH = isFat ? 27 : 24 const legH = isShort ? 18 : isSuperMode ? 33 : isSwole ? 30 : 27 const legW = isFat ? 14 : isSuperMode ? 14 : isSwole ? 12 : 9 const armW = isSuperMode ? 14 : isSwole ? 9 : 6 const armH = isShort ? 15 : isSuperMode ? 27 : isSwole ? 24 : 21 const neckH = isSuperMode ? 5 : 3 const feetY = ground - 4 const legsTop = feetY - legH const bodyTop = legsTop - bodyH const neckTop = bodyTop - neckH const headTop = neckTop - headH // Pose offsets const cheerJump = isCheer ? Math.round(Math.abs(Math.sin(t * Math.PI)) * 18) : 0 const panicShake = isPanic ? Math.round(Math.sin(t * Math.PI * 6) * 4) : 0 const runBob = isRun ? Math.round(Math.sin(t * Math.PI * 2) * 4) : 0 const yOff = -(cheerJump + runBob) + (isIdle ? bounce : 0) const xOff = panicShake // Shadow const shadowW = isFat ? 22 : isSwole ? 18 : 14 for (let sx = cx - shadowW; sx <= cx + shadowW; sx++) { px(sx, ground, 'rgba(0,0,0,0.15)', ox, oy) px(sx, ground + 1, 'rgba(0,0,0,0.08)', ox, oy) } // ===== CAPE (drawn behind everything) ===== if (hasCape) { const capeX = cx - Math.floor(bodyW / 2) - 4 + xOff const capeW2 = bodyW + 8 for (let iy = bodyTop + yOff; iy < feetY + yOff + 12; iy++) { const flutter = isRun ? Math.round(Math.sin(t * Math.PI * 3 + iy * 0.15) * 4) : Math.round(Math.sin(iy * 0.1) * 1) for (let ix = capeX - flutter; ix < capeX + capeW2 + flutter; ix++) { const shade = (iy + ix) % 5 === 0 ? '#881111' : (iy + ix) % 3 === 0 ? '#aa1818' : '#cc2222' px(ix, iy, shade, ox, oy) } } // Cape clasp at neck fill(cx - 3 + xOff, bodyTop + yOff - 2, 6, 3, '#ffd700', ox, oy) } // ===== LEGS ===== const legGap = isRun ? Math.round(Math.sin(t * Math.PI * 2) * 8) : isPanic ? 6 : 3 const ll = cx - legGap - Math.floor(legW / 2) + xOff const rl = cx + legGap - Math.floor(legW / 2) + xOff const legColor = costume.onesie ? (costume.hat || shirtColor) : pantsColor const legColorDk = costume.onesie ? darken(costume.hat || shirtColor, 15) : pantsDark // Left leg roundBox(ll, legsTop + yOff, legW, legH, legColor, ox, oy) fill(ll + legW - 2, legsTop + yOff + 2, 2, legH - 4, legColorDk, ox, oy) // shading // Knee highlight px(ll + 2, legsTop + Math.floor(legH / 2) + yOff, lighten(legColor, 10), ox, oy) // Right leg roundBox(rl, legsTop + yOff, legW, legH, legColor, ox, oy) fill(rl + legW - 2, legsTop + yOff + 2, 2, legH - 4, legColorDk, ox, oy) px(rl + 2, legsTop + Math.floor(legH / 2) + yOff, lighten(legColor, 10), ox, oy) // Super mode: quad definition lines visible through pants if (isSuperMode) { const quadY = legsTop + yOff + 3 const quadH = Math.floor(legH * 0.5) // Left quad outer sweep for (let iy = quadY; iy < quadY + quadH; iy++) { const bulge = Math.round(Math.sin((iy - quadY) / quadH * Math.PI) * 2) px(ll - 1, iy, legColor, ox, oy) if (bulge > 0) px(ll - 2, iy, legColor, ox, oy) } // Right quad outer sweep for (let iy = quadY; iy < quadY + quadH; iy++) { const bulge = Math.round(Math.sin((iy - quadY) / quadH * Math.PI) * 2) px(rl + legW, iy, legColor, ox, oy) if (bulge > 0) px(rl + legW + 1, iy, legColor, ox, oy) } // Quad separation line (vastus medialis) for (let iy = quadY + 2; iy < quadY + quadH - 2; iy++) { px(ll + Math.floor(legW / 2), iy, legColorDk, ox, oy) px(rl + Math.floor(legW / 2), iy, legColorDk, ox, oy) } // Calf definition (lower leg) const calfY = legsTop + yOff + Math.floor(legH * 0.6) const calfH = Math.floor(legH * 0.3) for (let iy = calfY; iy < calfY + calfH; iy++) { const bulge = Math.round(Math.sin((iy - calfY) / calfH * Math.PI) * 1.5) if (bulge > 0) { px(ll - 1, iy, legColor, ox, oy) px(rl + legW, iy, legColor, ox, oy) } } } // Socks visible (between pants and shoes) if (!costume.onesie) { fill(ll, feetY + yOff - 3, legW, 3, '#ffffff', ox, oy) fill(rl, feetY + yOff - 3, legW, 3, '#ffffff', ox, oy) } // Shoes (chunky) roundBox(ll - 2, feetY + yOff, legW + 5, 5, '#333333', ox, oy) px(ll + legW + 2, feetY + yOff + 1, '#555555', ox, oy) // shoe highlight fill(ll - 1, feetY + yOff + 4, legW + 4, 1, '#222222', ox, oy) // sole roundBox(rl - 2, feetY + yOff, legW + 5, 5, '#333333', ox, oy) px(rl + legW + 2, feetY + yOff + 1, '#555555', ox, oy) fill(rl - 1, feetY + yOff + 4, legW + 4, 1, '#222222', ox, oy) // Superhero boots if (isSuperHero) { fill(ll - 2, feetY + yOff - 6, legW + 5, 6, '#cc2222', ox, oy) fill(rl - 2, feetY + yOff - 6, legW + 5, 6, '#cc2222', ox, oy) fill(ll, feetY + yOff - 7, legW + 1, 1, '#ffd700', ox, oy) // gold trim fill(rl, feetY + yOff - 7, legW + 1, 1, '#ffd700', ox, oy) } // ===== BODY ===== const bx = cx - Math.floor(bodyW / 2) + xOff const by = bodyTop + yOff if (isSuperMode) { // === SHIRTLESS SUPER MODE — ridiculous muscles === roundBox(bx, by, bodyW, bodyH, skinTone, ox, oy) // Body shading for (let iy = by + 2; iy < by + bodyH - 2; iy++) { px(bx + bodyW - 2, iy, skinDark, ox, oy) px(bx + bodyW - 3, iy, skinMid, ox, oy) px(bx + 1, iy, skinLight, ox, oy) } // === TRAPS (massive neck-to-shoulder muscles) === fill(bx + 4, by, bodyW - 8, 4, skinTone, ox, oy) fill(bx + 2, by + 1, 4, 3, skinMid, ox, oy) // left trap fill(bx + bodyW - 6, by + 1, 4, 3, skinMid, ox, oy) // right trap // Trap definition lines px(bx + 6, by + 2, skinDark, ox, oy) px(bx + bodyW - 7, by + 2, skinDark, ox, oy) // === PECS (two big slabs) === const pecY = by + 5 const pecW = Math.floor(bodyW / 2) - 4 const pecH = 7 // Left pec roundBox(bx + 3, pecY, pecW, pecH, skinLight, ox, oy) fill(bx + 4, pecY + pecH - 2, pecW - 2, 2, skinMid, ox, oy) // underboob shadow px(bx + 5, pecY + 1, lighten(skinTone, 20), ox, oy) // highlight // Right pec roundBox(bx + bodyW - 3 - pecW, pecY, pecW, pecH, skinLight, ox, oy) fill(bx + bodyW - 2 - pecW, pecY + pecH - 2, pecW - 2, 2, skinMid, ox, oy) px(bx + bodyW - 2 - pecW, pecY + 1, lighten(skinTone, 20), ox, oy) // Pec gap (sternum line) for (let iy = pecY; iy < pecY + pecH; iy++) px(cx + xOff, iy, skinDark, ox, oy) // === ABS (6-pack, 3 rows of 2) === const absTop = pecY + pecH + 2 const absW = 6 const absH = 4 for (let row = 0; row < 3; row++) { const ay = absTop + row * (absH + 1) // Left ab roundBox(cx + xOff - absW - 1, ay, absW, absH, skinLight, ox, oy) px(cx + xOff - absW, ay + 1, lighten(skinTone, 18), ox, oy) // highlight // Right ab roundBox(cx + xOff + 1, ay, absW, absH, skinLight, ox, oy) px(cx + xOff + 2, ay + 1, lighten(skinTone, 18), ox, oy) // Center line px(cx + xOff, ay + 1, skinDark, ox, oy) px(cx + xOff, ay + 2, skinDark, ox, oy) } // Linea alba (center line connecting abs) for (let iy = absTop; iy < absTop + 3 * (absH + 1); iy++) px(cx + xOff, iy, skinDark, ox, oy) // === V-LINES (obliques leading into pants) === const vStart = absTop + 3 * (absH + 1) for (let i = 0; i < 4; i++) { px(cx + xOff - 5 - i, vStart + i, skinDark, ox, oy) px(cx + xOff + 5 + i, vStart + i, skinDark, ox, oy) } // === VEINS (because why not) === // Chest vein px(bx + 6, pecY + 2, darken(skinTone, 5), ox, oy) px(bx + 7, pecY + 3, darken(skinTone, 5), ox, oy) px(bx + 8, pecY + 3, darken(skinTone, 5), ox, oy) // Bicep veins drawn in arm section // Championship belt fill(bx, by + bodyH - 4, bodyW, 4, '#ffd700', ox, oy) fill(bx + 1, by + bodyH - 3, bodyW - 2, 2, '#daa520', ox, oy) // Belt plate fill(cx - 5 + xOff, by + bodyH - 5, 10, 5, '#ffd700', ox, oy) fill(cx - 4 + xOff, by + bodyH - 4, 8, 3, '#ffffff', ox, oy) // plate face fill(cx - 2 + xOff, by + bodyH - 3, 4, 1, '#ffd700', ox, oy) // star/emblem // Side gems px(bx + 4, by + bodyH - 3, '#ff2222', ox, oy) px(bx + bodyW - 5, by + bodyH - 3, '#4488ff', ox, oy) } else { const torsoColor = costume.onesie ? (costume.hat || shirtColor) : shirtColor const torsoDk = costume.onesie ? darken(costume.hat || shirtColor, 15) : shirtDark roundBox(bx, by, bodyW, bodyH, torsoColor, ox, oy) // Body shading (right side darker) for (let iy = by + 2; iy < by + bodyH - 2; iy++) { px(bx + bodyW - 2, iy, torsoDk, ox, oy) px(bx + bodyW - 3, iy, torsoDk, ox, oy) px(bx + 1, iy, lighten(torsoColor, 8), ox, oy) // left highlight } // Zipper for onesies if (costume.onesie) { for (let iy = by + 2; iy < by + bodyH - 2; iy++) { px(cx + xOff, iy, darken(torsoColor, 25), ox, oy) px(cx + 1 + xOff, iy, darken(torsoColor, 20), ox, oy) } } else { // Collar fill(bx + 3, by, bodyW - 6, 3, darken(shirtColor, 10), ox, oy) fill(bx + Math.floor(bodyW / 2) - 2, by, 4, 4, skinTone, ox, oy) // V-neck // Shirt pattern (bigger at 3x) const patCx = cx + xOff const patCy = by + Math.floor(bodyH / 2) + 2 // Draw a big emblem fill(patCx - 4, patCy - 4, 8, 8, darken(torsoColor, 8), ox, oy) fill(patCx - 3, patCy - 3, 6, 6, secondaryColor, ox, oy) fill(patCx - 2, patCy - 2, 4, 4, lighten(secondaryColor, 15), ox, oy) px(patCx, patCy, '#ffffff', ox, oy) } // Belt fill(bx, by + bodyH - 3, bodyW, 3, '#443322', ox, oy) fill(cx - 2 + xOff, by + bodyH - 3, 5, 3, '#ffcc00', ox, oy) // buckle px(cx + xOff, by + bodyH - 2, '#ffffff', ox, oy) // buckle highlight } // Food stains (losers) if (hasStains) { fill(bx + 4, by + 6, 3, 2, '#cc8822', ox, oy) fill(bx + bodyW - 8, by + 10, 2, 3, '#cc2222', ox, oy) fill(bx + 10, by + 4, 2, 2, '#ff8844', ox, oy) px(bx + bodyW - 5, by + 8, '#884400', ox, oy) } // Belly hanging over belt if (isFat && wr < 0.25) { fill(bx + 4, by + bodyH, bodyW - 8, 4, skinTone, ox, oy) fill(bx + 5, by + bodyH + 3, bodyW - 10, 2, skinDark, ox, oy) } // ===== NECK ===== if (isSuperMode) { // Thick bull neck fill(cx - 7 + xOff, neckTop + yOff, 14, neckH, skinTone, ox, oy) // Neck muscle lines (sternocleidomastoid) px(cx - 4 + xOff, neckTop + yOff + 1, skinDark, ox, oy) px(cx - 3 + xOff, neckTop + yOff + 2, skinDark, ox, oy) px(cx + 4 + xOff, neckTop + yOff + 1, skinDark, ox, oy) px(cx + 3 + xOff, neckTop + yOff + 2, skinDark, ox, oy) // Neck vein px(cx + 5 + xOff, neckTop + yOff, darken(skinTone, 8), ox, oy) px(cx + 5 + xOff, neckTop + yOff + 1, darken(skinTone, 8), ox, oy) } else { fill(cx - 4 + xOff, neckTop + yOff, 8, neckH, skinTone, ox, oy) px(cx + 3 + xOff, neckTop + yOff + 1, skinDark, ox, oy) } // ===== ARMS ===== const armAttach = by + 3 const armLx = bx - armW - 2 const armRx = bx + bodyW + 2 const armSkin = isSuperMode ? skinTone : costume.onesie ? (costume.hat || shirtColor) : skinTone const armSkinDk = isSuperMode ? skinDark : costume.onesie ? darken(costume.hat || shirtColor, 15) : skinDark // Shoulder caps for super mode (drawn before arms so they overlap) if (isSuperMode) { // Massive round deltoids const dY = armAttach - 2 // Left delt for (let dy = 0; dy < 6; dy++) { const dw = 6 - Math.abs(dy - 2) fill(armLx - dw + armW, dY + dy, dw + 2, 1, dy < 3 ? skinLight : skinTone, ox, oy) } // Right delt for (let dy = 0; dy < 6; dy++) { const dw = 6 - Math.abs(dy - 2) fill(armRx - 2, dY + dy, dw + 2, 1, dy < 3 ? skinLight : skinTone, ox, oy) } // Delt-arm separation line px(armLx + armW - 1, armAttach + 2, skinDark, ox, oy) px(armRx, armAttach + 2, skinDark, ox, oy) } if (isCheer) { const armUpH = Math.round(Math.abs(Math.sin(t * Math.PI)) * 12) + 6 // Arms reaching up roundBox(armLx, armAttach - armUpH, armW, armUpH, armSkin, ox, oy) roundBox(armRx, armAttach - armUpH, armW, armUpH, armSkin, ox, oy) // Hands (open, fingers spread) fill(armLx - 2, armAttach - armUpH - 5, armW + 3, 4, skinTone, ox, oy) fill(armRx - 1, armAttach - armUpH - 5, armW + 3, 4, skinTone, ox, oy) // Individual fingers for (let f = 0; f < 4; f++) { px(armLx - 2 + f * 2, armAttach - armUpH - 6, skinTone, ox, oy) px(armRx - 1 + f * 2, armAttach - armUpH - 6, skinTone, ox, oy) } } else if (isPanic) { const flapL = Math.round(Math.sin(t * Math.PI * 4) * 12) const flapR = Math.round(Math.cos(t * Math.PI * 4) * 12) roundBox(armLx + flapL, armAttach - 8, armW, armH, armSkin, ox, oy) roundBox(armRx - flapR, armAttach - 10, armW, armH, armSkin, ox, oy) // Blurred hands (motion) fill(armLx + flapL - 1, armAttach - 10, armW + 2, 3, skinTone, ox, oy) fill(armRx - flapR - 1, armAttach - 12, armW + 2, 3, skinTone, ox, oy) } else if (isCoach) { const pointExt = Math.round(Math.sin(t * Math.PI) * 8) // Pointing arm roundBox(armRx, armAttach, armW + 8 + pointExt, armW, armSkin, ox, oy) // Pointing finger fill(armRx + armW + 8 + pointExt, armAttach, 4, armW - 2, skinTone, ox, oy) px(armRx + armW + 12 + pointExt, armAttach + 1, skinTone, ox, oy) // Other arm on hip roundBox(armLx, armAttach + 4, armW, 8, armSkin, ox, oy) fill(armLx + 2, armAttach + 11, armW - 2, 3, skinTone, ox, oy) // hand on hip } else if (isRun) { const pump = Math.round(Math.sin(t * Math.PI * 2) * 8) roundBox(armLx, armAttach + pump, armW, armH - 6, armSkin, ox, oy) roundBox(armRx, armAttach - pump, armW, armH - 6, armSkin, ox, oy) // Fists fill(armLx, armAttach + pump + armH - 8, armW + 1, 3, skinTone, ox, oy) fill(armRx, armAttach - pump + armH - 8, armW + 1, 3, skinTone, ox, oy) } else { // Idle: arms hanging roundBox(armLx, armAttach, armW, armH, armSkin, ox, oy) roundBox(armRx, armAttach, armW, armH, armSkin, ox, oy) // Arm shading for (let iy = armAttach + 2; iy < armAttach + armH - 2; iy++) { px(armLx + armW - 1, iy, armSkinDk, ox, oy) px(armRx + armW - 1, iy, armSkinDk, ox, oy) } // Super mode: bulging biceps, veins, forearm definition if (isSuperMode) { // Bicep bulge (outer swell on both arms) const bicepY = armAttach + 3 const bicepH = Math.floor(armH * 0.4) // Left arm bulge for (let iy = bicepY; iy < bicepY + bicepH; iy++) { const swell = Math.round(Math.sin((iy - bicepY) / bicepH * Math.PI) * 3) for (let s = 0; s < swell; s++) { px(armLx - 1 - s, iy, skinTone, ox, oy) } if (swell > 1) px(armLx - swell, iy, skinMid, ox, oy) // shadow edge } // Right arm bulge for (let iy = bicepY; iy < bicepY + bicepH; iy++) { const swell = Math.round(Math.sin((iy - bicepY) / bicepH * Math.PI) * 3) for (let s = 0; s < swell; s++) { px(armRx + armW + s, iy, skinTone, ox, oy) } if (swell > 1) px(armRx + armW + swell - 1, iy, skinMid, ox, oy) } // Bicep-tricep separation line const sepX1 = armLx + Math.floor(armW / 2) const sepX2 = armRx + Math.floor(armW / 2) for (let iy = bicepY + 1; iy < bicepY + bicepH - 1; iy++) { px(sepX1, iy, skinDark, ox, oy) px(sepX2, iy, skinDark, ox, oy) } // Veins (wiggly lines down the arms) for (let v = 0; v < 8; v++) { const vy = armAttach + 4 + v * 2 const wobble = Math.round(Math.sin(v * 1.2) * 1) px(armLx + 2 + wobble, vy, darken(skinTone, 8), ox, oy) px(armRx + armW - 3 + wobble, vy, darken(skinTone, 8), ox, oy) } // Forearm taper definition const frmY = armAttach + Math.floor(armH * 0.6) px(armLx + 1, frmY, skinDark, ox, oy) px(armLx + 1, frmY + 1, skinDark, ox, oy) px(armRx + armW - 2, frmY, skinDark, ox, oy) px(armRx + armW - 2, frmY + 1, skinDark, ox, oy) } // Hands fill(armLx, armAttach + armH - 4, armW + 1, 4, skinTone, ox, oy) fill(armRx - 1, armAttach + armH - 4, armW + 1, 4, skinTone, ox, oy) // Fingers (3 visible) for (let f = 0; f < 3; f++) { px(armLx + f * 2, armAttach + armH, skinTone, ox, oy) px(armRx - 1 + f * 2, armAttach + armH, skinTone, ox, oy) } // Phone or cheetos in right hand if (hasCheetos) { roundBox(armRx - 1, armAttach + armH + 1, 8, 12, '#ff8800', ox, oy) fill(armRx, armAttach + armH + 2, 6, 3, '#ffcc00', ox, oy) // label fill(armRx + 1, armAttach + armH + 6, 4, 2, '#ffaa00', ox, oy) // crumbs // Cheese dust on fingers px(armRx, armAttach + armH - 1, '#ff8844', ox, oy) px(armRx + 2, armAttach + armH - 1, '#ff8844', ox, oy) } else { // Gamepad controller const gpX = armRx - 3 const gpY = armAttach + armH - 2 roundBox(gpX, gpY, 14, 8, '#333344', ox, oy) // body fill(gpX + 1, gpY + 1, 12, 6, '#2a2a3a', ox, oy) // inset // Grips roundBox(gpX - 2, gpY + 2, 3, 6, '#333344', ox, oy) roundBox(gpX + 13, gpY + 2, 3, 6, '#333344', ox, oy) // D-pad (left side) fill(gpX + 2, gpY + 3, 3, 1, '#555566', ox, oy) fill(gpX + 3, gpY + 2, 1, 3, '#555566', ox, oy) // Buttons (right side) px(gpX + 9, gpY + 2, '#ff4444', ox, oy) px(gpX + 11, gpY + 2, '#4488ff', ox, oy) px(gpX + 10, gpY + 1, '#ffcc00', ox, oy) px(gpX + 10, gpY + 3, '#44cc44', ox, oy) // Wire coming out the top const wireX = gpX + 7 for (let w = 0; w < 14; w++) { const wobble = Math.round(Math.sin(w * 0.6 + t * Math.PI * 2) * 2) px(wireX + wobble, gpY - 1 - w, '#444444', ox, oy) } } } // ===== PROP (costume-specific, bigger) ===== if (costume.propShape !== 'none' && costume.prop && !isCheer && !isPanic) { const propX = armRx + armW + 3 const propY = armAttach + 3 if (costume.propShape === 'sword') { // Blade for (let i = 0; i < 22; i++) { px(propX + i, propY - i, costume.prop, ox, oy); px(propX + i, propY - i - 1, lighten(costume.prop, 15), ox, oy) } // Guard fill(propX - 1, propY + 1, 5, 2, '#886633', ox, oy) fill(propX - 2, propY + 3, 2, 6, '#664422', ox, oy) // handle px(propX - 1, propY + 8, '#ffcc44', ox, oy) // pommel } else if (costume.propShape === 'wand') { for (let i = 0; i < 16; i++) px(propX, propY - i, '#886633', ox, oy) // Sparkle at tip const sparkle = ['#ffcc44', '#ffffff', '#ffee88'] for (let dx = -2; dx <= 2; dx++) for (let dy = -2; dy <= 2; dy++) { if (Math.abs(dx) + Math.abs(dy) <= 2) px(propX + dx, propY - 17 + dy, sparkle[(dx + dy + 4) % 3], ox, oy) } } else if (costume.propShape === 'shield') { roundBox(propX, propY - 6, 12, 15, costume.prop, ox, oy) fill(propX + 3, propY - 3, 6, 9, lighten(costume.prop, 15), ox, oy) fill(propX + 5, propY - 1, 2, 5, '#ffffff', ox, oy) // emblem } else if (costume.propShape === 'sign') { for (let i = 0; i < 20; i++) px(propX + 2, propY - i, '#886633', ox, oy) // pole roundBox(propX - 6, propY - 30, 18, 12, costume.prop, ox, oy) // sign board } else if (costume.propShape === 'phone' || costume.propShape === 'remote') { roundBox(propX, propY, 5, 8, '#333333', ox, oy) fill(propX + 1, propY + 1, 3, 5, '#44ff44', ox, oy) px(propX + 2, propY + 2, '#88ff88', ox, oy) } else if (costume.propShape === 'food') { roundBox(propX, propY - 2, 8, 6, costume.prop, ox, oy) fill(propX + 1, propY - 4, 6, 2, '#44aa22', ox, oy) // lettuce px(propX + 3, propY - 5, '#ff4444', ox, oy) // cherry on top } } // ===== HEAD ===== const hx = cx - Math.floor(headW / 2) + xOff const hy = headTop + yOff const faceColor = costume.facePaint || skinTone const faceDark = costume.facePaint ? darken(costume.facePaint, 15) : skinDark const faceLight = costume.facePaint ? lighten(costume.facePaint, 10) : skinLight roundBox(hx, hy, headW, headH, faceColor, ox, oy) // Head shading for (let iy = hy + 2; iy < hy + headH - 2; iy++) { px(hx + headW - 2, iy, faceDark, ox, oy) px(hx + headW - 3, iy, faceDark, ox, oy) px(hx + 1, iy, faceLight, ox, oy) } // Ears if (!costume.facePaint) { fill(hx - 2, hy + Math.floor(headH * 0.3), 2, 5, skinTone, ox, oy) px(hx - 2, hy + Math.floor(headH * 0.3) + 1, skinDark, ox, oy) // inner ear fill(hx + headW, hy + Math.floor(headH * 0.3), 2, 5, skinTone, ox, oy) px(hx + headW + 1, hy + Math.floor(headH * 0.3) + 1, skinDark, ox, oy) } // Hair if (!costume.onesie || costume.type !== 'animal') { // Top hair (fluffy) for (let ix = hx; ix < hx + headW; ix++) { px(ix, hy, hairColor, ox, oy) px(ix, hy + 1, hairColor, ox, oy) px(ix, hy + 2, hairColor, ox, oy) } for (let ix = hx + 1; ix < hx + headW - 1; ix++) { px(ix, hy - 1, hairColor, ox, oy) } // Hair volume on sides fill(hx - 1, hy, 2, 5, hairColor, ox, oy) fill(hx + headW - 1, hy, 2, 5, hairColor, ox, oy) // Hair highlights px(hx + 3, hy, lighten(hairColor, 15), ox, oy) px(hx + 4, hy, lighten(hairColor, 15), ox, oy) px(hx + 5, hy - 1, lighten(hairColor, 10), ox, oy) // Messy hair for losers if (wr < 0.3) { px(hx + 2, hy - 2, hairColor, ox, oy) px(hx + headW - 4, hy - 3, hairColor, ox, oy) px(hx - 1, hy - 1, hairColor, ox, oy) } } // ===== EYES ===== const eyeY = hy + Math.floor(headH * 0.35) const leX = hx + 5 const reX = hx + headW - 11 if (isPanic) { // Wide terrified eyes fill(leX - 1, eyeY - 2, 8, 7, '#ffffff', ox, oy) fill(reX - 1, eyeY - 2, 8, 7, '#ffffff', ox, oy) // Tiny pupils (fear) fill(leX + 2, eyeY + 1, 2, 2, '#000000', ox, oy) fill(reX + 2, eyeY + 1, 2, 2, '#000000', ox, oy) // Eyebrow (raised) fill(leX - 1, eyeY - 4, 7, 2, hairColor, ox, oy) fill(reX - 1, eyeY - 4, 7, 2, hairColor, ox, oy) } else if (isCheer) { // Happy squinted eyes fill(leX, eyeY, 6, 2, '#000000', ox, oy) fill(reX, eyeY, 6, 2, '#000000', ox, oy) fill(leX, eyeY - 1, 6, 1, '#ffffff', ox, oy) fill(reX, eyeY - 1, 6, 1, '#ffffff', ox, oy) // Raised cheeks fill(leX - 1, eyeY + 2, 3, 2, '#ff8888', ox, oy) fill(reX + 4, eyeY + 2, 3, 2, '#ff8888', ox, oy) } else { // Normal eyes with iris detail fill(leX, eyeY, 6, 5, '#ffffff', ox, oy) fill(reX, eyeY, 6, 5, '#ffffff', ox, oy) // Iris fill(leX + 2, eyeY + 1, 3, 3, '#4466aa', ox, oy) fill(reX + 2, eyeY + 1, 3, 3, '#4466aa', ox, oy) // Pupil fill(leX + 3, eyeY + 2, 2, 2, '#000000', ox, oy) fill(reX + 3, eyeY + 2, 2, 2, '#000000', ox, oy) // Eye highlight px(leX + 2, eyeY + 1, '#ffffff', ox, oy) px(reX + 2, eyeY + 1, '#ffffff', ox, oy) // Eyebrows fill(leX, eyeY - 2, 6, 2, hairColor, ox, oy) fill(reX, eyeY - 2, 6, 2, hairColor, ox, oy) // Angry eyebrows for coach if (isCoach) { px(leX, eyeY - 3, hairColor, ox, oy) px(reX + 5, eyeY - 3, hairColor, ox, oy) } } // Glasses if (hasGlasses) { // Frames for (let ix = leX - 2; ix <= reX + 7; ix++) px(ix, eyeY - 1, '#444444', ox, oy) // Left lens for (let iy = eyeY - 1; iy <= eyeY + 4; iy++) { px(leX - 2, iy, '#444444', ox, oy); px(leX + 6, iy, '#444444', ox, oy) } px(leX - 2, eyeY + 4, '#444444', ox, oy); px(leX + 6, eyeY + 4, '#444444', ox, oy) for (let ix = leX - 1; ix <= leX + 5; ix++) px(ix, eyeY + 4, '#444444', ox, oy) // Right lens for (let iy = eyeY - 1; iy <= eyeY + 4; iy++) { px(reX - 2, iy, '#444444', ox, oy); px(reX + 6, iy, '#444444', ox, oy) } for (let ix = reX - 1; ix <= reX + 5; ix++) px(ix, eyeY + 4, '#444444', ox, oy) // Lens reflection px(leX, eyeY, '#aaccff', ox, oy) px(reX, eyeY, '#aaccff', ox, oy) } // Nose const noseY = hy + Math.floor(headH * 0.55) px(cx + xOff, noseY, skinDark, ox, oy) px(cx + 1 + xOff, noseY, skinDark, ox, oy) px(cx + xOff, noseY + 1, skinDark, ox, oy) // Red nose for clown if (costume.type === 'hat' && archetype === 'clown') { fill(cx - 1 + xOff, noseY - 1, 4, 4, '#ff0000', ox, oy) } // Mouth const mY = hy + Math.floor(headH * 0.72) if (isCheer && isSuperMode) { // SUPER MODE CHEER: massive blinding grin fill(cx - 7 + xOff, mY - 1, 14, 6, '#000000', ox, oy) // huge mouth fill(cx - 6 + xOff, mY - 1, 12, 3, '#ffffff', ox, oy) // top teeth fill(cx - 5 + xOff, mY + 3, 10, 2, '#ffffff', ox, oy) // bottom teeth fill(cx - 5 + xOff, mY + 2, 10, 1, '#cc4444', ox, oy) // tongue peeking // Tooth lines for (let ti = 0; ti < 6; ti++) px(cx - 6 + ti * 2 + xOff, mY, '#dddddd', ox, oy) // Gleam sparkles on both sides px(cx - 9 + xOff, mY - 2, '#ffffff', ox, oy) px(cx - 8 + xOff, mY - 3, '#ffffff', ox, oy) px(cx - 10 + xOff, mY - 3, '#ffffff', ox, oy) px(cx - 9 + xOff, mY - 4, '#ffffff', ox, oy) px(cx + 9 + xOff, mY - 2, '#ffffff', ox, oy) px(cx + 8 + xOff, mY - 3, '#ffffff', ox, oy) px(cx + 10 + xOff, mY - 3, '#ffffff', ox, oy) px(cx + 9 + xOff, mY - 4, '#ffffff', ox, oy) } else if (isCheer) { // Big grin with teeth fill(cx - 5 + xOff, mY, 10, 4, '#000000', ox, oy) fill(cx - 4 + xOff, mY, 8, 2, '#ffffff', ox, oy) // teeth fill(cx - 4 + xOff, mY + 2, 8, 2, '#cc4444', ox, oy) // tongue } else if (isPanic) { // Screaming roundBox(cx - 4 + xOff, mY - 2, 8, 8, '#000000', ox, oy) fill(cx - 3 + xOff, mY + 2, 6, 2, '#cc4444', ox, oy) // tongue // Teeth px(cx - 3 + xOff, mY - 1, '#ffffff', ox, oy) px(cx + 2 + xOff, mY - 1, '#ffffff', ox, oy) } else if (isCoach) { // Yelling fill(cx - 4 + xOff, mY, 8, 5, '#000000', ox, oy) fill(cx - 3 + xOff, mY, 6, 2, '#ffffff', ox, oy) } else if (isSuperMode) { // Super mode idle: big confident grin with gleaming teeth fill(cx - 6 + xOff, mY, 12, 5, '#000000', ox, oy) // wide mouth // Top teeth row — big, white, perfect fill(cx - 5 + xOff, mY, 10, 2, '#ffffff', ox, oy) // Individual tooth lines for (let ti = 0; ti < 5; ti++) px(cx - 5 + ti * 2 + xOff, mY + 1, '#dddddd', ox, oy) // Bottom teeth fill(cx - 4 + xOff, mY + 3, 8, 1, '#ffffff', ox, oy) // Tooth gleam / sparkle px(cx - 4 + xOff, mY, '#ffffee', ox, oy) px(cx + 2 + xOff, mY, '#ffffee', ox, oy) // Sparkle star near mouth px(cx + 7 + xOff, mY - 1, '#ffffff', ox, oy) px(cx + 8 + xOff, mY - 2, '#ffffff', ox, oy) px(cx + 6 + xOff, mY - 2, '#ffffff', ox, oy) px(cx + 7 + xOff, mY - 3, '#ffffff', ox, oy) // Dimples px(cx - 7 + xOff, mY + 1, skinDark, ox, oy) px(cx + 7 + xOff, mY + 1, skinDark, ox, oy) } else { // Neutral / slight smile fill(cx - 3 + xOff, mY, 6, 2, '#0a0a0a', ox, oy) px(cx - 4 + xOff, mY + 1, '#0a0a0a', ox, oy) px(cx + 3 + xOff, mY + 1, '#0a0a0a', ox, oy) // Drool for very low win rate if (wr < 0.15) { px(cx + 3 + xOff, mY + 2, '#88ccff', ox, oy) px(cx + 3 + xOff, mY + 3, '#88ccff', ox, oy) px(cx + 3 + xOff, mY + 4, '#66aadd', ox, oy) } } // Sweat drops if (hasSweatDrops && (isIdle || isPanic || isCoach)) { fill(hx + headW + 2, eyeY + 2, 2, 4, '#88ccff', ox, oy) px(hx + headW + 2, eyeY + 6, '#66aadd', ox, oy) if (wr < 0.2) { fill(hx - 3, eyeY + 4, 2, 3, '#88ccff', ox, oy) // Puddle forming fill(hx + headW + 1, eyeY + 7, 4, 1, '#aaddff', ox, oy) } } // Double chin if (isFat && wr < 0.25) { fill(hx + 4, hy + headH, headW - 8, 4, skinTone, ox, oy) fill(hx + 5, hy + headH + 3, headW - 10, 2, skinDark, ox, oy) fill(hx + 6, hy + headH + 4, headW - 12, 2, skinDark, ox, oy) } // Beard if (hasBeard && !costume.facePaint) { // Scrappy neckbeard for losers, cool beard for winners if (wr < 0.35) { // Patchy neckbeard for (let ix = hx + 3; ix < hx + headW - 3; ix++) { if ((ix + hy) % 3 !== 0) px(ix, hy + headH - 2, hairDark, ox, oy) if ((ix + hy) % 2 !== 0) px(ix, hy + headH - 1, hairDark, ox, oy) if ((ix + hy) % 3 === 0) px(ix, hy + headH, hairDark, ox, oy) } } else { // Full beard for (let ix = hx + 3; ix < hx + headW - 3; ix++) { fill(ix, hy + headH - 3, 1, 5, hairColor, ox, oy) } fill(cx - 2 + xOff, hy + headH + 1, 4, 2, hairDark, ox, oy) } } // Headband (winners) if (hasHeadband && costume.hatShape === 'none') { const hbColor = wr > 0.85 ? '#ffd700' : '#ff2222' fill(hx - 2, hy + 3, headW + 4, 3, hbColor, ox, oy) // Trailing ribbons px(hx - 3, hy + 4, hbColor, ox, oy) px(hx - 4, hy + 5, hbColor, ox, oy) px(hx - 5, hy + 5 + (isIdle ? bounce : 0), hbColor, ox, oy) px(hx - 6, hy + 6 + (isIdle ? bounce : 0), darken(hbColor, 15), ox, oy) px(hx - 7, hy + 5 + (isIdle ? bounce : 0), darken(hbColor, 15), ox, oy) } // Winner glow aura if (isSuperHero && !isPanic) { for (let a = 0; a < 16; a++) { const angle = a * Math.PI * 2 / 16 + t * Math.PI * 0.5 const r = 40 + Math.sin(t * Math.PI * 4 + a * 0.8) * 8 const gx = cx + xOff + Math.round(Math.cos(angle) * r) const gy = by + Math.floor(bodyH / 2) + Math.round(Math.sin(angle) * r) fill(gx - 1, gy - 1, 3, 3, '#ffcc4430', ox, oy) px(gx, gy, '#ffcc4460', ox, oy) } } // ===== COSTUME HAT/ACCESSORIES ===== const hatColor = costume.hat || hairColor const hatDk = darken(hatColor, 15) const hatLt = lighten(hatColor, 15) if (costume.hatShape === 'ears') { // Big floppy costume ears fill(hx - 1, hy - 5, 5, 6, hatColor, ox, oy) fill(hx + headW - 4, hy - 5, 5, 6, hatColor, ox, oy) fill(hx, hy - 4, 3, 4, hatLt, ox, oy) // inner fill(hx + headW - 3, hy - 4, 3, 4, hatLt, ox, oy) } else if (costume.hatShape === 'helmet') { for (let ix = hx - 3; ix < hx + headW + 3; ix++) { px(ix, hy - 3, hatColor, ox, oy); px(ix, hy - 2, hatColor, ox, oy) px(ix, hy - 1, hatColor, ox, oy); px(ix, hy, hatColor, ox, oy) } for (let ix = hx - 1; ix < hx + headW + 1; ix++) { px(ix, hy - 4, hatColor, ox, oy); px(ix, hy - 5, hatColor, ox, oy) } fill(hx + 2, hy - 4, 4, 2, hatLt, ox, oy) // highlight // Visor fill(hx + 2, hy + 1, headW - 4, 3, '#33333388', ox, oy) } else if (costume.hatShape === 'pointy') { for (let h = 0; h < 18; h++) { const w = Math.max(1, Math.floor((headW + 2) * (1 - h / 18))) const sx2 = cx + xOff - Math.floor(w / 2) for (let ix = sx2; ix < sx2 + w; ix++) px(ix, hy - 1 - h, hatColor, ox, oy) } // Stars and moons on the hat px(cx - 3 + xOff, hy - 8, '#ffcc44', ox, oy) px(cx + 4 + xOff, hy - 12, '#ffcc44', ox, oy) px(cx + xOff, hy - 16, '#ffffff', ox, oy) // star tip fill(cx - 1 + xOff, hy - 18, 3, 3, '#ffcc44', ox, oy) } else if (costume.hatShape === 'wide') { for (let ix = hx - 8; ix < hx + headW + 8; ix++) { px(ix, hy - 3, hatColor, ox, oy); px(ix, hy - 2, hatColor, ox, oy) } for (let ix = hx - 3; ix < hx + headW + 3; ix++) { px(ix, hy - 4, hatColor, ox, oy); px(ix, hy - 5, hatColor, ox, oy) px(ix, hy - 6, hatColor, ox, oy); px(ix, hy - 7, hatColor, ox, oy) } // Hat band fill(hx - 2, hy - 4, headW + 4, 2, hatDk, ox, oy) } else if (costume.hatShape === 'tall') { for (let h = 0; h < 16; h++) { for (let ix = hx + 2; ix < hx + headW - 2; ix++) px(ix, hy - 1 - h, hatColor, ox, oy) } fill(hx + 3, hy - 14, headW - 8, 2, hatLt, ox, oy) // puff/highlight } else if (costume.hatShape === 'horns') { // Chunky horns for (let h = 0; h < 8; h++) { px(hx - h, hy - 1 - h, hatColor, ox, oy); px(hx - h - 1, hy - 1 - h, hatColor, ox, oy) px(hx + headW - 1 + h, hy - 1 - h, hatColor, ox, oy); px(hx + headW + h, hy - 1 - h, hatColor, ox, oy) } px(hx - 8, hy - 9, hatLt, ox, oy); px(hx + headW + 8, hy - 9, hatLt, ox, oy) // tips } else if (costume.hatShape === 'antenna') { // Bouncy antenna headband fill(hx + 2, hy - 2, headW - 4, 2, '#888888', ox, oy) // headband const bobX = isIdle ? Math.round(Math.sin(t * Math.PI * 3) * 2) : 0 // Left antenna for (let h = 0; h < 10; h++) px(hx + 4 + bobX, hy - 3 - h, '#888888', ox, oy) fill(hx + 2 + bobX, hy - 14, 5, 4, '#ff2222', ox, oy) // left ball px(hx + 3 + bobX, hy - 13, '#ff6666', ox, oy) // highlight // Right antenna for (let h = 0; h < 10; h++) px(hx + headW - 5 - bobX, hy - 3 - h, '#888888', ox, oy) fill(hx + headW - 7 - bobX, hy - 14, 5, 4, '#ff2222', ox, oy) px(hx + headW - 6 - bobX, hy - 13, '#ff6666', ox, oy) } else if (costume.hatShape === 'cone') { for (let h = 0; h < 20; h++) { const w = Math.max(2, 18 - h) const sx2 = cx + xOff - Math.floor(w / 2) const stripe = (h % 6 < 3) ? '#ffffff' : hatColor for (let ix = sx2; ix < sx2 + w; ix++) px(ix, hy - 1 - h, stripe, ox, oy) } } // Tail (costume) if (costume.tail) { const tailColor = costume.hat || shirtColor const tx = bx - 4 for (let i = 0; i < 8; i++) { px(tx - i, by + bodyH - 4 + Math.round(Math.sin(i * 0.8 + t * Math.PI * 2) * 2), tailColor, ox, oy) px(tx - i, by + bodyH - 3 + Math.round(Math.sin(i * 0.8 + t * Math.PI * 2) * 2), tailColor, ox, oy) } px(tx - 8, by + bodyH - 4, darken(tailColor, 20), ox, oy) // tail tip } } // Render all frames const entries = Object.entries(HUMAN_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++) drawHuman(f, row, pose, f, cfg.frames) } return canvas.toDataURL() } function getCostume(archetype: string): CostumeConfig { const raw = COSTUME_MAP[archetype] || {} return { type: raw.type || 'generic', hat: raw.hat, hatShape: raw.hatShape || 'none', prop: raw.prop, propShape: raw.propShape || 'none', onesie: raw.onesie || false, facePaint: raw.facePaint, tail: raw.tail || false, } } function darken(hex: string, amount: number): string { if (hex.startsWith('hsl')) { const m = hex.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/) if (m) return `hsl(${m[1]}, ${m[2]}%, ${Math.max(0, +m[3] - amount)}%)` } if (hex.length < 7) return hex const r = Math.max(0, parseInt(hex.slice(1, 3), 16) - amount * 2.55) const g = Math.max(0, parseInt(hex.slice(3, 5), 16) - amount * 2.55) const b = Math.max(0, parseInt(hex.slice(5, 7), 16) - amount * 2.55) return `#${Math.round(r).toString(16).padStart(2, '0')}${Math.round(g).toString(16).padStart(2, '0')}${Math.round(b).toString(16).padStart(2, '0')}` } function lighten(hex: string, amount: number): string { if (hex.startsWith('hsl')) { const m = hex.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/) if (m) return `hsl(${m[1]}, ${m[2]}%, ${Math.min(95, +m[3] + amount)}%)` } if (hex.length < 7) return hex const r = Math.min(255, parseInt(hex.slice(1, 3), 16) + amount * 2.55) const g = Math.min(255, parseInt(hex.slice(3, 5), 16) + amount * 2.55) const b = Math.min(255, parseInt(hex.slice(5, 7), 16) + amount * 2.55) return `#${Math.round(r).toString(16).padStart(2, '0')}${Math.round(g).toString(16).padStart(2, '0')}${Math.round(b).toString(16).padStart(2, '0')}` }