feat: v2 — queue matchmaking, procedural audio, sprite archetypes, auth

- Add queue-based matchmaking with Elo-proximity and 10s timeout
- Procedural sound engine (SFX, voice announcer, 4-track music)
- Sprite system refactored into 6 archetypes (standard, lobster, sheep, cyborg, blob, tank)
- 42+ fight choreographies with themed/generic/wild card selection
- 4 KO finish styles, super-speed mode, hyperdetail close-ups
- Auth routes, JoinBout page, bot profile with stats
- 7-tier ranking system (Baby through Legend)
- Arena and challenge system expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 22:13:19 +00:00
co-authored by Claude Opus 4.6
parent 335c148866
commit 47d20fbe66
82 changed files with 14011 additions and 741 deletions
@@ -0,0 +1,71 @@
import type { Archetype } from '../constants'
export const alien: Archetype = {
name: 'alien',
weight: 0.03,
dimensionOverrides: (tier) => ({
hw: 13 + tier,
hh: 11 + tier,
bw: 8 + tier * 2,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko, knockback, frame,
hx, hy, hh, hw, cx, hOff, by } = p
// Big oval eyes (huge, almond-shaped)
if (!ko) {
const eyeY = hy + Math.floor(hh * 0.3)
const leX = hx + 1
const reX = hx + Math.floor(hw * 0.55)
const eyeW = Math.floor(hw * 0.35)
const eyeH = Math.floor(hh * 0.3)
// Large dark eyes
for (let ey = eyeY; ey < eyeY + eyeH; ey++) {
const progress = (ey - eyeY) / eyeH
const rowW = Math.floor(eyeW * (1 - Math.abs(progress - 0.5) * 1.5))
for (let ex = 0; ex < rowW; ex++) {
px(leX + ex + Math.floor((eyeW - rowW) / 2), ey, '#112211', ox, oy)
px(reX + ex + Math.floor((eyeW - rowW) / 2), ey, '#112211', ox, oy)
}
}
// Glowing pupil
const pupY = eyeY + Math.floor(eyeH / 2)
px(leX + Math.floor(eyeW / 2), pupY, '#44ff44', ox, oy)
px(reX + Math.floor(eyeW / 2), pupY, '#44ff44', ox, oy)
// Pupil flicker
if (frame % 3 === 0) {
px(leX + Math.floor(eyeW / 2), pupY, '#88ff88', ox, oy)
px(reX + Math.floor(eyeW / 2), pupY, '#88ff88', ox, oy)
}
}
// Antenna (single, glowing tip)
if (!ko) {
const antX = cx + hOff
const antBase = hy - 1
for (let a = 0; a < 4; a++) {
const wobble = idle ? Math.round(Math.sin(t * Math.PI * 3 + a * 0.5) * 1) : 0
px(antX + wobble, antBase - a, '#44aa44', ox, oy)
}
// Glowing tip
px(antX, antBase - 4, '#88ff88', ox, oy)
if (idle && Math.sin(t * Math.PI * 6) > 0) {
px(antX - 1, antBase - 4, '#44ff44', ox, oy)
px(antX + 1, antBase - 4, '#44ff44', ox, oy)
}
}
// Small slit mouth
if (!ko) {
const mY = hy + Math.floor(hh * 0.7)
px(cx + hOff - 1, mY, '#224422', ox, oy)
px(cx + hOff, mY, '#224422', ox, oy)
px(cx + hOff + 1, mY, '#224422', ox, oy)
}
// Green tinted body patches
for (let iy = by + 1; iy < by + p.bh; iy += 3) {
px(p.bx + 1, iy, '#33aa55', ox, oy)
}
},
}
@@ -0,0 +1,355 @@
import type { Archetype } from '../constants'
export const elephant: Archetype = {
name: 'elephant', weight: 0.02,
dimensionOverrides: (tier) => ({ legW: 6 + tier, legH: 6 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Big ears
for (let iy = hy; iy < hy + hh; iy++) {
px(hx - 2, iy, '#999999', ox, oy); px(hx - 3, iy, '#888888', ox, oy)
px(hx + hw + 1, iy, '#999999', ox, oy); px(hx + hw + 2, iy, '#888888', ox, oy)
}
// Trunk
const trunkWave = idle ? Math.sin(t * Math.PI * 2) * 2 : 0
for (let i = 0; i < 5; i++) {
px(hx + Math.floor(hw / 2) + Math.round(trunkWave * (i / 5)), hy + hh + i, '#999999', ox, oy)
}
// Tusks
px(hx + 1, hy + hh - 1, '#ffffcc', ox, oy); px(hx + 1, hy + hh, '#ffffcc', ox, oy)
px(hx + hw - 2, hy + hh - 1, '#ffffcc', ox, oy); px(hx + hw - 2, hy + hh, '#ffffcc', ox, oy)
},
}
export const giraffe: Archetype = {
name: 'giraffe', weight: 0.02,
dimensionOverrides: () => ({ legH: 10 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Spots on body
const spots = [[bx + 2, by + 1], [bx + bw - 3, by + 2], [bx + 1, by + bh - 3], [bx + bw - 2, by + bh - 2]]
for (const [sx, sy] of spots) {
px(sx, sy, '#aa7722', ox, oy); px(sx + 1, sy, '#aa7722', ox, oy)
px(sx, sy + 1, '#aa7722', ox, oy)
}
// Ossicones (little horns)
px(hx + 2, hy - 1, '#886633', ox, oy); px(hx + 2, hy - 2, '#ffcc88', ox, oy)
px(hx + hw - 3, hy - 1, '#886633', ox, oy); px(hx + hw - 3, hy - 2, '#ffcc88', ox, oy)
// Long eyelashes
px(hx + 1, hy + Math.floor(hh * 0.3), '#000000', ox, oy)
px(hx + hw - 2, hy + Math.floor(hh * 0.3), '#000000', ox, oy)
},
}
export const hippo: Archetype = {
name: 'hippo', weight: 0.02,
dimensionOverrides: (tier) => ({ legW: 6 + tier }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw } = p
if (ko) return
// Wide mouth (when idle, mouth opens)
const mouthOpen = idle ? Math.sin(t * Math.PI) > 0.5 : false
if (mouthOpen) {
for (let iy = hy + Math.floor(hh * 0.6); iy < hy + hh + 2; iy++) {
for (let ix = hx; ix < hx + hw; ix++) px(ix, iy, '#ff6688', ox, oy)
}
// Teeth
px(hx + 1, hy + Math.floor(hh * 0.6), '#ffffff', ox, oy)
px(hx + hw - 2, hy + Math.floor(hh * 0.6), '#ffffff', ox, oy)
}
// Small ears on top
px(hx + 1, hy - 1, '#998877', ox, oy); px(hx + hw - 2, hy - 1, '#998877', ox, oy)
// Nostrils
px(hx + Math.floor(hw / 2) - 1, hy + Math.floor(hh * 0.5), '#553344', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy + Math.floor(hh * 0.5), '#553344', ox, oy)
},
}
export const lion: Archetype = {
name: 'lion', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Mane (circle of fur around head)
for (let angle = 0; angle < 12; angle++) {
const a = angle * Math.PI * 2 / 12
const mx = hx + Math.floor(hw / 2) + Math.round(Math.cos(a) * (hw / 2 + 2))
const my = hy + Math.floor(hh / 2) + Math.round(Math.sin(a) * (hh / 2 + 2))
px(mx, my, '#cc8822', ox, oy)
}
// Nose
px(hx + Math.floor(hw / 2), hy + Math.floor(hh * 0.55), '#332211', ox, oy)
// Tail with tuft
px(bx + bw + 1, by + bh - 2, '#ccaa44', ox, oy)
px(bx + bw + 2, by + bh - 3, '#ccaa44', ox, oy)
px(bx + bw + 3, by + bh - 3, '#cc8822', ox, oy)
px(bx + bw + 3, by + bh - 4, '#cc8822', ox, oy)
},
}
export const monkey: Archetype = {
name: 'monkey', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Big round ears
for (let d = -1; d <= 1; d++) {
px(hx - 2, hy + Math.floor(hh / 2) + d, '#cc9966', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2) + d, '#cc9966', ox, oy)
}
px(hx - 2, hy + Math.floor(hh / 2), '#ffaa88', ox, oy) // inner
px(hx + hw + 1, hy + Math.floor(hh / 2), '#ffaa88', ox, oy)
// Curled tail
const curl = idle ? Math.sin(t * Math.PI * 2) * 2 : 0
px(bx + bw + 1, by + bh - 1, '#886644', ox, oy)
px(bx + bw + 2, by + bh - 2 + Math.round(curl), '#886644', ox, oy)
px(bx + bw + 3, by + bh - 1 + Math.round(curl), '#886644', ox, oy)
px(bx + bw + 3, by + bh + Math.round(curl), '#886644', ox, oy)
// Belly patch
px(bx + Math.floor(bw / 2), by + Math.floor(bh / 2), '#ffcc99', ox, oy)
px(bx + Math.floor(bw / 2) + 1, by + Math.floor(bh / 2), '#ffcc99', ox, oy)
},
}
export const parrot: Archetype = {
name: 'parrot', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Colorful feather crest
const feathers = ['#ff0000', '#ffaa00', '#ffff00', '#00ff00', '#0088ff']
for (let i = 0; i < feathers.length; i++) px(hx + Math.floor(hw / 2), hy - 1 - i, feathers[i], ox, oy)
// Curved beak
px(hx + hw, hy + Math.floor(hh / 2), '#ff8800', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2) + 1, '#ff6600', ox, oy)
// Colorful body stripes
px(bx + 1, by + 1, '#ff0000', ox, oy); px(bx + 2, by + 1, '#ff0000', ox, oy)
px(bx + 1, by + 3, '#00ff00', ox, oy); px(bx + 2, by + 3, '#00ff00', ox, oy)
px(bx + 1, by + 5, '#0088ff', ox, oy); px(bx + 2, by + 5, '#0088ff', ox, oy)
// Tail feathers
const wave = idle ? Math.sin(t * Math.PI * 2) * 2 : 0
px(bx + bw + 1, by + bh - 2, '#ff0000', ox, oy)
px(bx + bw + 2, by + bh - 1 + Math.round(wave), '#00ff00', ox, oy)
px(bx + bw + 3, by + bh + Math.round(wave), '#0088ff', ox, oy)
},
}
export const raccoon: Archetype = {
name: 'raccoon', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Mask (black around eyes)
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
px(ix, hy + Math.floor(hh * 0.3), '#222222', ox, oy)
px(ix, hy + Math.floor(hh * 0.4), '#222222', ox, oy)
}
// Pointy ears
px(hx + 1, hy - 1, '#888877', ox, oy); px(hx + hw - 2, hy - 1, '#888877', ox, oy)
// Striped tail
for (let i = 0; i < 6; i++) {
const color = i % 2 === 0 ? '#888877' : '#333322'
px(bx + bw + 1 + Math.floor(i / 2), by + bh - 2 + (i % 3), color, ox, oy)
}
// Tiny hands
px(bx - 1, by + bh, '#444444', ox, oy); px(bx + bw, by + bh, '#444444', ox, oy)
},
}
export const snakeArch: Archetype = {
name: 'snake', weight: 0.02,
dimensionOverrides: () => ({ legH: 2, legW: 4 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Forked tongue
if (idle && Math.sin(t * Math.PI * 4) > 0) {
px(hx + hw, hy + Math.floor(hh / 2), '#ff0044', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2) - 1, '#ff0044', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2) + 1, '#ff0044', ox, oy)
}
// Diamond pattern on body
for (let ix = bx + 1; ix < bx + bw - 1; ix += 3) {
px(ix, by + Math.floor(bh / 2), '#ffcc44', ox, oy)
}
// Slit eyes
px(hx + 2, hy + Math.floor(hh * 0.35), '#ffcc00', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.35), '#ffcc00', ox, oy)
// Coiled body underneath
const coil = Math.sin(t * Math.PI * 2) * 2
px(bx + 2, by + bh + 1 + Math.round(coil), '#448833', ox, oy)
px(bx + bw - 3, by + bh + 1 - Math.round(coil), '#448833', ox, oy)
},
}
export const turtle: Archetype = {
name: 'turtle', weight: 0.02,
dimensionOverrides: () => ({ legH: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, bx, by, bw, bh, hx, hy, hh, hw } = p
if (ko) return
// Shell (dome on back)
for (let iy = by - 1; iy < by + bh + 1; iy++) {
for (let ix = bx - 1; ix < bx + bw + 1; ix++) px(ix, iy, '#448833', ox, oy)
}
// Shell pattern
px(bx + Math.floor(bw / 2), by + Math.floor(bh / 2), '#336622', ox, oy)
px(bx + 2, by + 2, '#336622', ox, oy); px(bx + bw - 3, by + 2, '#336622', ox, oy)
px(bx + 2, by + bh - 3, '#336622', ox, oy); px(bx + bw - 3, by + bh - 3, '#336622', ox, oy)
// Beak
px(hx + hw, hy + Math.floor(hh / 2), '#aaaa44', ox, oy)
},
}
export const whale: Archetype = {
name: 'whale', weight: 0.02,
dimensionOverrides: () => ({ legH: 2 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Belly (lighter underbelly)
for (let ix = bx + 1; ix < bx + bw - 1; ix++) {
px(ix, by + bh - 2, '#aabbcc', ox, oy); px(ix, by + bh - 1, '#99aabb', ox, oy)
}
// Tail fluke
px(bx + bw + 1, by + Math.floor(bh / 2) - 2, '#6688aa', ox, oy)
px(bx + bw + 2, by + Math.floor(bh / 2) - 3, '#6688aa', ox, oy)
px(bx + bw + 1, by + Math.floor(bh / 2) + 2, '#6688aa', ox, oy)
px(bx + bw + 2, by + Math.floor(bh / 2) + 3, '#6688aa', ox, oy)
// Spout (when idle)
if (idle && Math.sin(t * Math.PI * 2) > 0.5) {
for (let h = 0; h < 4; h++) {
px(cx + hOff, hy - 2 - h, '#aaddff', ox, oy)
}
px(cx + hOff - 1, hy - 5, '#aaddff', ox, oy)
px(cx + hOff + 1, hy - 5, '#aaddff', ox, oy)
}
// Tiny eye
px(hx + 1, hy + Math.floor(hh * 0.4), '#222222', ox, oy)
},
}
export const crocodile: Archetype = {
name: 'crocodile', weight: 0.02,
dimensionOverrides: () => ({ legH: 4 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Long snout
px(hx + hw, hy + Math.floor(hh / 2), '#557744', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2), '#557744', ox, oy)
px(hx + hw + 2, hy + Math.floor(hh / 2), '#446633', ox, oy)
// Teeth
px(hx + hw, hy + Math.floor(hh / 2) + 1, '#ffffff', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2) + 1, '#ffffff', ox, oy)
// Scaly back ridge
for (let ix = bx + 1; ix < bx + bw; ix += 2) {
px(ix, by - 1, '#446633', ox, oy)
}
// Thick tail
px(bx + bw + 1, by + bh - 2, '#557744', ox, oy)
px(bx + bw + 2, by + bh - 1, '#557744', ox, oy)
px(bx + bw + 3, by + bh, '#557744', ox, oy)
px(bx + bw + 4, by + bh, '#446633', ox, oy)
},
}
export const flamingo: Archetype = {
name: 'flamingo', weight: 0.02,
dimensionOverrides: () => ({ legH: 10, legW: 2 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Curved beak
px(hx + hw, hy + Math.floor(hh / 2), '#ff8844', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2) + 1, '#222222', ox, oy)
// Wing (folded, pink gradient)
for (let iy = by + 1; iy < by + bh - 1; iy++) {
px(bx + bw, iy, '#ff88aa', ox, oy)
px(bx + bw + 1, iy, '#ff6699', ox, oy)
}
// Feather tuft on tail
px(bx + bw + 1, by + bh - 1, '#ff44aa', ox, oy)
px(bx + bw + 2, by + bh, '#ff44aa', ox, oy)
// Pink body tint
for (let ix = bx + 1; ix < bx + bw - 1; ix += 2) {
px(ix, by + 1, '#ffaacc', ox, oy)
}
},
}
export const hedgehog: Archetype = {
name: 'hedgehog', weight: 0.02,
dimensionOverrides: () => ({ legH: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, bx, by, bw, bh, hx, hy, hh, hw } = p
if (ko) return
// Spines on back
for (let ix = bx; ix < bx + bw; ix += 2) {
px(ix, by - 1, '#886644', ox, oy); px(ix, by - 2, '#aa8866', ox, oy)
}
for (let ix = bx + 1; ix < bx + bw - 1; ix += 2) {
px(ix, by - 1, '#775533', ox, oy)
}
// Cute nose
px(hx + hw, hy + Math.floor(hh * 0.6), '#222222', ox, oy)
// Small round ears
px(hx + 1, hy - 1, '#ccaa88', ox, oy); px(hx + hw - 2, hy - 1, '#ccaa88', ox, oy)
// Tiny feet visible
px(bx, by + bh + 1, '#ccaa88', ox, oy); px(bx + bw - 1, by + bh + 1, '#ccaa88', ox, oy)
},
}
export const panda: Archetype = {
name: 'panda', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Black eye patches
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
px(hx + 2 + dx, hy + Math.floor(hh * 0.35) + dy, '#000000', ox, oy)
px(hx + hw - 3 + dx, hy + Math.floor(hh * 0.35) + dy, '#000000', ox, oy)
}
}
// Round ears
px(hx, hy - 1, '#000000', ox, oy); px(hx + 1, hy - 1, '#000000', ox, oy)
px(hx + hw - 1, hy - 1, '#000000', ox, oy); px(hx + hw - 2, hy - 1, '#000000', ox, oy)
// White belly patch
for (let iy = by + 2; iy < by + bh - 1; iy++) {
px(bx + Math.floor(bw / 2), iy, '#ffffff', ox, oy)
px(bx + Math.floor(bw / 2) + 1, iy, '#ffffff', ox, oy)
}
// Black arms/legs coloring
px(bx, by, '#000000', ox, oy); px(bx + bw - 1, by, '#000000', ox, oy)
},
}
export const hamster: Archetype = {
name: 'hamster', weight: 0.02,
dimensionOverrides: () => ({ legH: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw } = p
if (ko) return
// Puffy cheeks
px(hx - 1, hy + Math.floor(hh * 0.5), '#ffcc99', ox, oy)
px(hx - 2, hy + Math.floor(hh * 0.5), '#ffbb88', ox, oy)
px(hx + hw, hy + Math.floor(hh * 0.5), '#ffcc99', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh * 0.5), '#ffbb88', ox, oy)
// Cheek stuffing animation
if (idle && Math.sin(t * Math.PI * 2) > 0.7) {
px(hx - 3, hy + Math.floor(hh * 0.5), '#ffaa77', ox, oy)
px(hx + hw + 2, hy + Math.floor(hh * 0.5), '#ffaa77', ox, oy)
}
// Tiny round ears
px(hx + 1, hy - 1, '#ffbb88', ox, oy)
px(hx + hw - 2, hy - 1, '#ffbb88', ox, oy)
// Buck teeth
px(hx + Math.floor(hw / 2), hy + hh, '#ffffff', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy + hh, '#ffffff', ox, oy)
// Tiny stub tail
px(p.bx + p.bw + 1, p.by + p.bh - 1, '#ffcc99', ox, oy)
},
}
@@ -0,0 +1,346 @@
import type { Archetype } from '../constants'
export const chef: Archetype = {
name: 'chef', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, cx, hOff, bx, by, bw, bh } = p
if (ko) return
// Chef hat (tall white)
for (let h = 0; h < 6; h++) {
const w = h < 3 ? hw + 2 : hw - 2
for (let ix = cx + hOff - Math.floor(w / 2); ix < cx + hOff + Math.ceil(w / 2); ix++) {
px(ix, hy - 1 - h, '#ffffff', ox, oy)
}
}
// Apron
for (let iy = by + 2; iy < by + bh + 2; iy++) {
for (let ix = bx + 1; ix < bx + bw - 1; ix++) px(ix, iy, '#ffffff', ox, oy)
}
// Apron string
px(bx + Math.floor(bw / 2), by + 2, '#cccccc', ox, oy)
// Mustache
px(hx + 1, hy + Math.floor(hh * 0.6), '#443322', ox, oy)
px(hx + 2, hy + Math.floor(hh * 0.6), '#443322', ox, oy)
px(hx + hw - 2, hy + Math.floor(hh * 0.6), '#443322', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.6), '#443322', ox, oy)
},
}
export const firefighter: Archetype = {
name: 'firefighter', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Helmet
for (let ix = hx - 1; ix < hx + hw + 1; ix++) px(ix, hy - 1, '#ff2222', ox, oy)
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 2, '#ff2222', ox, oy)
// Helmet shield
px(hx + Math.floor(hw / 2), hy - 1, '#ffcc00', ox, oy)
// Yellow stripes on body
for (let ix = bx; ix < bx + bw; ix++) {
px(ix, by + Math.floor(bh / 3), '#ffcc00', ox, oy)
px(ix, by + Math.floor(bh * 2 / 3), '#ffcc00', ox, oy)
}
// Oxygen tank on back
px(bx + bw + 1, by + 1, '#444444', ox, oy)
px(bx + bw + 1, by + 2, '#444444', ox, oy)
px(bx + bw + 1, by + 3, '#444444', ox, oy)
},
}
export const astronautArch: Archetype = {
name: 'astronaut', weight: 0.02, canHaveMohawk: false, canHaveVisor: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Helmet dome
for (let ix = hx - 1; ix < hx + hw + 1; ix++) {
px(ix, hy - 1, '#ffffff', ox, oy)
px(ix, hy + hh, '#ffffff', ox, oy)
}
px(hx - 1, hy, '#ffffff', ox, oy); px(hx + hw, hy, '#ffffff', ox, oy)
px(hx - 1, hy + hh - 1, '#ffffff', ox, oy); px(hx + hw, hy + hh - 1, '#ffffff', ox, oy)
// Visor (gold tint)
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
for (let iy = hy + 1; iy < hy + Math.floor(hh * 0.5); iy++) {
px(ix, iy, '#ffcc44', ox, oy)
}
}
// Backpack
px(bx + bw + 1, by, '#cccccc', ox, oy); px(bx + bw + 1, by + 1, '#cccccc', ox, oy)
px(bx + bw + 1, by + 2, '#cccccc', ox, oy); px(bx + bw + 2, by + 1, '#aaaaaa', ox, oy)
// Flag patch
px(bx + 1, by + 1, '#ff0000', ox, oy); px(bx + 2, by + 1, '#ffffff', ox, oy); px(bx + 3, by + 1, '#0000ff', ox, oy)
},
}
export const clown: Archetype = {
name: 'clown', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Red nose
px(cx + hOff, hy + Math.floor(hh * 0.55), '#ff0000', ox, oy)
px(cx + hOff + 1, hy + Math.floor(hh * 0.55), '#ff0000', ox, oy)
// Rainbow wig
const colors = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff']
for (let i = 0; i < colors.length; i++) {
px(hx - 1 + i, hy - 1, colors[i], ox, oy)
px(hx + hw - colors.length + i, hy - 1, colors[colors.length - 1 - i], ox, oy)
}
// Ruffle collar
for (let ix = bx - 1; ix < bx + bw + 1; ix++) {
px(ix, by - 1, (ix % 2 === 0) ? '#ffffff' : '#ff4444', ox, oy)
}
// Big shoes
px(bx - 2, by + bh + 5, '#ff0000', ox, oy); px(bx - 3, by + bh + 5, '#ff0000', ox, oy)
px(bx + bw + 1, by + bh + 5, '#ff0000', ox, oy); px(bx + bw + 2, by + bh + 5, '#ff0000', ox, oy)
// Flower on chest
const flowerBlink = Math.sin(t * Math.PI * 3) > 0
px(cx + hOff, by + 1, flowerBlink ? '#ff44ff' : '#ffff00', ox, oy)
},
}
export const detective: Archetype = {
name: 'detective', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Fedora
for (let ix = hx - 2; ix < hx + hw + 2; ix++) px(ix, hy - 1, '#443322', ox, oy)
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 2, '#554433', ox, oy)
for (let ix = hx + 1; ix < hx + hw - 1; ix++) px(ix, hy - 3, '#554433', ox, oy)
// Trench coat
for (let iy = by; iy < by + bh + 3; iy++) {
px(bx - 1, iy, '#aa9966', ox, oy); px(bx + bw, iy, '#aa9966', ox, oy)
}
// Belt
for (let ix = bx; ix < bx + bw; ix++) px(ix, by + Math.floor(bh * 0.7), '#554433', ox, oy)
// Magnifying glass
px(bx + bw + 2, by + Math.floor(bh / 2), '#888888', ox, oy)
px(bx + bw + 3, by + Math.floor(bh / 2) - 1, '#aaddff', ox, oy)
px(bx + bw + 3, by + Math.floor(bh / 2), '#aaddff', ox, oy)
},
}
export const nurse: Archetype = {
name: 'nurse', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Nurse cap
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 1, '#ffffff', ox, oy)
px(cx + hOff, hy - 1, '#ff0000', ox, oy) // red cross
px(cx + hOff - 1, hy - 2, '#ff0000', ox, oy)
px(cx + hOff + 1, hy - 2, '#ff0000', ox, oy)
px(cx + hOff, hy - 2, '#ff0000', ox, oy)
// White coat
for (let iy = by; iy < by + bh; iy++) {
px(bx, iy, '#ffffff', ox, oy); px(bx + bw - 1, iy, '#ffffff', ox, oy)
}
// Stethoscope
px(cx + hOff - 1, by + 1, '#444444', ox, oy)
px(cx + hOff - 2, by + 2, '#444444', ox, oy)
px(cx + hOff - 2, by + 3, '#888888', ox, oy)
},
}
export const lumberjack: Archetype = {
name: 'lumberjack', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Beanie
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 1, '#ff2222', ox, oy)
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 2, '#ff2222', ox, oy)
// Plaid pattern on body
for (let iy = by; iy < by + bh; iy += 2) {
for (let ix = bx; ix < bx + bw; ix += 2) px(ix, iy, '#cc2222', ox, oy)
}
for (let iy = by + 1; iy < by + bh; iy += 2) {
for (let ix = bx + 1; ix < bx + bw; ix += 2) px(ix, iy, '#222222', ox, oy)
}
// Big beard
for (let iy = hy + Math.floor(hh * 0.6); iy < hy + hh + 2; iy++) {
px(hx + 1, iy, '#884422', ox, oy); px(hx + 2, iy, '#884422', ox, oy)
px(hx + hw - 2, iy, '#884422', ox, oy); px(hx + hw - 3, iy, '#884422', ox, oy)
}
// Axe on back
px(bx + bw + 1, by - 2, '#886633', ox, oy)
px(bx + bw + 1, by - 1, '#886633', ox, oy)
px(bx + bw + 1, by, '#886633', ox, oy)
px(bx + bw + 2, by - 2, '#888888', ox, oy)
px(bx + bw + 2, by - 3, '#888888', ox, oy)
},
}
export const scientist: Archetype = {
name: 'scientist', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Lab coat
for (let iy = by; iy < by + bh + 2; iy++) {
px(bx - 1, iy, '#ffffff', ox, oy); px(bx + bw, iy, '#ffffff', ox, oy)
}
// Safety goggles
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
px(ix, hy + Math.floor(hh * 0.3), '#88ccff', ox, oy)
}
px(hx, hy + Math.floor(hh * 0.3), '#888888', ox, oy)
px(hx + hw - 1, hy + Math.floor(hh * 0.3), '#888888', ox, oy)
// Beaker in hand (bubbling)
const bubble = Math.sin(t * Math.PI * 4) > 0
px(bx + bw + 2, by + bh - 4, '#88ffcc', ox, oy)
px(bx + bw + 2, by + bh - 3, '#88ffcc', ox, oy)
px(bx + bw + 2, by + bh - 2, '#44cc88', ox, oy)
if (bubble) px(bx + bw + 2, by + bh - 5, '#aaffdd', ox, oy)
// Wild hair
px(hx - 1, hy, '#ffffff', ox, oy); px(hx + hw, hy, '#ffffff', ox, oy)
px(hx, hy - 1, '#ffffff', ox, oy); px(hx + hw - 1, hy - 1, '#ffffff', ox, oy)
},
}
export const wrestler: Archetype = {
name: 'wrestler', weight: 0.02, canHaveMohawk: false,
dimensionOverrides: (tier) => ({ legW: 5 + tier }),
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Luchador mask
for (let ix = hx; ix < hx + hw; ix++) {
for (let iy = hy; iy < hy + hh; iy++) px(ix, iy, '#ff0044', ox, oy)
}
// Eye holes
px(hx + 2, hy + Math.floor(hh * 0.35), '#000000', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.35), '#000000', ox, oy)
// Mouth hole
px(hx + Math.floor(hw / 2), hy + Math.floor(hh * 0.7), '#000000', ox, oy)
// Championship belt
for (let ix = bx; ix < bx + bw; ix++) px(ix, by + bh - 2, '#ffcc00', ox, oy)
px(bx + Math.floor(bw / 2), by + bh - 2, '#ffffff', ox, oy)
// Wrist bands
px(bx - 1, by + bh - 1, '#ff0044', ox, oy); px(bx + bw, by + bh - 1, '#ff0044', ox, oy)
},
}
export const boxer: Archetype = {
name: 'boxer', weight: 0.02,
drawFeatures: (p) => {
const { px, pal, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, armLx, armRx, armAttach, armH } = p
if (ko) return
// Boxing gloves (big circles at arm ends)
const gloveY = armAttach + armH - 2
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
if (dx * dx + dy * dy <= 5) {
px(armLx + dx + p.hOff, gloveY + dy, '#ff0000', p.ox, p.oy)
px(armRx + dx + p.hOff, gloveY + dy, '#ff0000', p.ox, p.oy)
}
}
}
// Shorts
for (let ix = bx; ix < bx + bw; ix++) {
px(ix, by + bh - 1, '#ffcc00', ox, oy)
px(ix, by + bh, '#ffcc00', ox, oy)
}
// Headband
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy + 1, '#ff0000', ox, oy)
},
}
export const gladiator: Archetype = {
name: 'gladiator', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Helmet with plume
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 1, '#ccaa44', ox, oy)
for (let h = 0; h < 4; h++) px(hx + Math.floor(hw / 2), hy - 2 - h, '#ff2222', ox, oy)
// Chest plate
for (let iy = by; iy < by + 3; iy++) {
for (let ix = bx + 1; ix < bx + bw - 1; ix++) px(ix, iy, '#ccaa44', ox, oy)
}
// Shield (on left arm)
for (let iy = by + 1; iy < by + 5; iy++) {
for (let ix = bx - 4; ix < bx - 1; ix++) px(ix, iy, '#886633', ox, oy)
}
px(bx - 3, by + 3, '#ccaa44', ox, oy) // boss on shield
// Sandals
px(bx - 1, by + bh + 5, '#886633', ox, oy); px(bx + bw, by + bh + 5, '#886633', ox, oy)
},
}
export const samuraiArch: Archetype = {
name: 'samurai', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Kabuto helmet
for (let ix = hx - 2; ix < hx + hw + 2; ix++) px(ix, hy - 1, '#886633', ox, oy)
px(cx + hOff, hy - 2, '#886633', ox, oy)
// Crescent moon ornament
px(cx + hOff - 2, hy - 3, '#ffcc00', ox, oy)
px(cx + hOff, hy - 4, '#ffcc00', ox, oy)
px(cx + hOff + 2, hy - 3, '#ffcc00', ox, oy)
// Armor plates
for (let iy = by; iy < by + bh; iy += 2) {
for (let ix = bx; ix < bx + bw; ix++) px(ix, iy, '#445566', ox, oy)
}
// Katana on back
px(bx + bw + 1, by - 3, '#888888', ox, oy)
for (let h = 0; h < 6; h++) px(bx + bw + 1, by - 2 + h, '#886633', ox, oy)
},
}
export const vikingArch: Archetype = {
name: 'viking', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Horned helmet
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 1, '#888888', ox, oy)
px(hx - 1, hy - 1, '#ccaa44', ox, oy); px(hx - 2, hy - 2, '#ccaa44', ox, oy); px(hx - 3, hy - 3, '#ccaa44', ox, oy)
px(hx + hw, hy - 1, '#ccaa44', ox, oy); px(hx + hw + 1, hy - 2, '#ccaa44', ox, oy); px(hx + hw + 2, hy - 3, '#ccaa44', ox, oy)
// Big beard
for (let iy = hy + Math.floor(hh * 0.5); iy < hy + hh + 3; iy++) {
const bw2 = Math.max(1, 3 - (iy - hy - Math.floor(hh * 0.5)))
for (let ix = hx + Math.floor(hw / 2) - bw2; ix <= hx + Math.floor(hw / 2) + bw2; ix++) {
px(ix, iy, '#cc8833', ox, oy)
}
}
// Fur vest
for (let ix = bx; ix < bx + bw; ix += 2) {
px(ix, by, '#886644', ox, oy); px(ix, by + 1, '#776633', ox, oy)
}
// Shield on back
px(bx + bw + 1, by + 2, '#886633', ox, oy); px(bx + bw + 2, by + 2, '#886633', ox, oy)
px(bx + bw + 1, by + 3, '#886633', ox, oy); px(bx + bw + 2, by + 3, '#886633', ox, oy)
},
}
export const knight: Archetype = {
name: 'knight', weight: 0.02, canHaveMohawk: false, canHaveVisor: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Full helmet
for (let ix = hx; ix < hx + hw; ix++) {
for (let iy = hy; iy < hy + hh; iy++) px(ix, iy, '#aaaaaa', ox, oy)
}
// Visor slit
for (let ix = hx + 1; ix < hx + hw - 1; ix++) px(ix, hy + Math.floor(hh * 0.4), '#333333', ox, oy)
// Plume
for (let h = 0; h < 4; h++) px(cx + hOff, hy - 1 - h, '#ff0000', ox, oy)
// Full body armor
for (let ix = bx; ix < bx + bw; ix++) {
for (let iy = by; iy < by + bh; iy++) px(ix, iy, '#999999', ox, oy)
}
// Cross on chest
px(cx + hOff, by + 2, '#ff0000', ox, oy)
px(cx + hOff - 1, by + 3, '#ff0000', ox, oy)
px(cx + hOff, by + 3, '#ff0000', ox, oy)
px(cx + hOff + 1, by + 3, '#ff0000', ox, oy)
px(cx + hOff, by + 4, '#ff0000', ox, oy)
},
}
@@ -0,0 +1,352 @@
import type { Archetype } from '../constants'
export const minotaur: Archetype = {
name: 'minotaur', weight: 0.02, canHaveHorns: false,
dimensionOverrides: (tier) => ({ legW: 5 + tier, legH: 7 + tier }),
drawFeatures: (p) => {
const { px, pal, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Big curved horns
for (let h = 0; h < 5; h++) {
px(hx - 1 - h, hy - h, '#aa8844', ox, oy)
px(hx + hw + h, hy - h, '#aa8844', ox, oy)
}
px(hx - 6, hy - 4, '#ffcc88', ox, oy); px(hx + hw + 5, hy - 4, '#ffcc88', ox, oy)
// Nose ring
px(hx + Math.floor(hw / 2), hy + hh - 1, '#ffcc00', ox, oy)
px(hx + Math.floor(hw / 2) - 1, hy + hh, '#ffcc00', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy + hh, '#ffcc00', ox, oy)
// Furry chest
for (let ix = bx + 1; ix < bx + bw - 1; ix += 2) px(ix, by + 1, '#886644', ox, oy)
},
}
export const unicorn: Archetype = {
name: 'unicorn', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, cx, hOff, bx, by, bw, bh, feetY, globalY, vBounce } = p
if (ko) return
// Spiral horn
for (let h = 0; h < 7; h++) {
const color = h % 2 === 0 ? '#ffaaff' : '#ffffff'
px(cx + hOff, hy - 1 - h, color, ox, oy)
}
// Rainbow mane
const rainbow = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff']
for (let i = 0; i < rainbow.length; i++) {
px(hx - 1, hy + 1 + i, rainbow[i], ox, oy)
}
// Sparkle trail
if (idle) {
const sparkX = bx - 3 - Math.floor(t * 5) % 8
const sparkY = by + Math.floor(bh / 2) + Math.round(Math.sin(t * Math.PI * 3 + 1) * 3)
px(sparkX, sparkY, '#ffff88', ox, oy)
px(sparkX - 3, sparkY + 2, '#ffaaff', ox, oy)
}
},
}
export const phoenix: Archetype = {
name: 'phoenix', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Flame wings
const wingFlap = idle ? Math.sin(t * Math.PI * 3) * 3 : 0
const flames = ['#ff4400', '#ff8800', '#ffcc00', '#ffee88']
for (let f = 0; f < 4; f++) {
px(bx - 2 - f, by + 1 + Math.round(wingFlap) + f, flames[f], ox, oy)
px(bx + bw + 1 + f, by + 1 - Math.round(wingFlap) + f, flames[f], ox, oy)
}
// Flame tail
for (let i = 0; i < 5; i++) {
px(bx + bw + 1 + i, by + bh - 1 + Math.round(Math.sin(t * Math.PI * 4 + i) * 2), flames[i % 4], ox, oy)
}
// Crown feathers
px(cx + hOff - 1, hy - 2, '#ff4400', ox, oy)
px(cx + hOff, hy - 3, '#ff8800', ox, oy)
px(cx + hOff + 1, hy - 2, '#ff4400', ox, oy)
// Beak
px(hx + hw, hy + Math.floor(hh / 2), '#ffaa00', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2), '#ff8800', ox, oy)
},
}
export const dragonArch: Archetype = {
name: 'dragon', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, atk, bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Horns
px(hx, hy - 1, '#886644', ox, oy); px(hx - 1, hy - 2, '#886644', ox, oy)
px(hx + hw - 1, hy - 1, '#886644', ox, oy); px(hx + hw, hy - 2, '#886644', ox, oy)
// Spiky back ridge
for (let i = 0; i < 5; i++) {
px(bx + bw + 1 + i, by + i * 2, '#44aa22', ox, oy)
px(bx + bw + 1 + i, by + i * 2 + 1, '#338811', ox, oy)
}
// Wings (folded)
for (let w = 0; w < 3; w++) {
px(bx - 1 - w, by + w, '#44aa44', ox, oy)
px(bx - 1 - w, by + w + 1, '#338833', ox, oy)
}
// Fire breath on attack
if (atk) {
const colors = ['#ff4400', '#ff8800', '#ffcc00']
for (let f = 0; f < 6; f++) {
px(hx + hw + f, hy + Math.floor(hh / 2) + Math.round(Math.sin(t * 10 + f) * 2), colors[f % 3], ox, oy)
}
}
},
}
export const mermaid: Archetype = {
name: 'mermaid', weight: 0.02,
dimensionOverrides: () => ({ legH: 4, legW: 6 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, hx, hy, hh, hw, feetY, globalY, vBounce } = p
if (ko) return
// Tail fin at feet (replaces legs visually)
const tailY = feetY + vBounce + globalY
px(bx + Math.floor(bw / 2) - 4, tailY + 2, '#44aacc', ox, oy)
px(bx + Math.floor(bw / 2) + 3, tailY + 2, '#44aacc', ox, oy)
px(bx + Math.floor(bw / 2) - 5, tailY + 3, '#228899', ox, oy)
px(bx + Math.floor(bw / 2) + 4, tailY + 3, '#228899', ox, oy)
// Scale pattern on body
for (let iy = by + 2; iy < by + bh; iy += 2) {
for (let ix = bx + 1; ix < bx + bw - 1; ix += 2) {
px(ix, iy, '#55ccaa', ox, oy)
}
}
// Shell crown
px(hx + Math.floor(hw / 2), hy - 1, '#ffaacc', ox, oy)
px(hx + Math.floor(hw / 2) - 1, hy - 1, '#ff88aa', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy - 1, '#ff88aa', ox, oy)
// Flowing hair
if (idle) {
const wave = Math.sin(t * Math.PI * 2) * 2
for (let h = 0; h < 4; h++) {
px(hx - 1, hy + h + Math.round(wave), '#44ccff', ox, oy)
}
}
},
}
export const griffin: Archetype = {
name: 'griffin', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, hx, hy, hh, hw } = p
if (ko) return
// Eagle head features: beak
px(hx + hw, hy + Math.floor(hh / 2), '#ffaa00', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2) + 1, '#ff8800', ox, oy)
// Wings
const flap = idle ? Math.sin(t * Math.PI * 2) * 4 : 0
for (let w = 0; w < 5; w++) {
px(bx - 1 - w, by + Math.round(flap) + w, '#886644', ox, oy)
px(bx + bw + w, by - Math.round(flap) + w, '#886644', ox, oy)
}
// Feathered chest
for (let iy = by; iy < by + 3; iy++) {
for (let ix = bx + 1; ix < bx + bw - 1; ix += 2) px(ix, iy, '#ddbb88', ox, oy)
}
// Lion tail
px(bx + bw + 1, by + bh - 2, '#aa8844', ox, oy)
px(bx + bw + 2, by + bh - 3, '#aa8844', ox, oy)
px(bx + bw + 3, by + bh - 3, '#cc9955', ox, oy)
},
}
export const cyclops: Archetype = {
name: 'cyclops', weight: 0.02, canHaveVisor: false,
dimensionOverrides: (tier) => ({ legW: 5 + tier }),
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// One big eye (covers normal eyes)
const eyeX = cx + hOff, eyeY = hy + Math.floor(hh * 0.35)
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
if (dx * dx + dy * dy <= 5) px(eyeX + dx, eyeY + dy, '#ffffff', ox, oy)
}
}
px(eyeX, eyeY, '#ff0000', ox, oy)
px(eyeX + 1, eyeY, '#880000', ox, oy)
// Brow ridge
for (let i = -3; i <= 3; i++) px(eyeX + i, eyeY - 3, '#886644', ox, oy)
},
}
export const gargoyle: Archetype = {
name: 'gargoyle', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, hx, hy, hh, hw } = p
if (ko) return
// Small horns
px(hx, hy - 1, '#666666', ox, oy); px(hx - 1, hy - 1, '#555555', ox, oy)
px(hx + hw - 1, hy - 1, '#666666', ox, oy); px(hx + hw, hy - 1, '#555555', ox, oy)
// Bat wings
const flap = idle ? Math.sin(t * Math.PI * 1.5) * 3 : 0
for (let w = 0; w < 6; w++) {
px(bx - 1 - w, by + 2 + Math.round(flap) + Math.floor(w / 2), '#555555', ox, oy)
px(bx + bw + w, by + 2 - Math.round(flap) + Math.floor(w / 2), '#555555', ox, oy)
}
// Stone texture
for (let ix = bx + 1; ix < bx + bw - 1; ix += 3) {
for (let iy = by + 1; iy < by + bh; iy += 3) px(ix, iy, '#777777', ox, oy)
}
// Glowing eyes
px(hx + 2, hy + Math.floor(hh * 0.35), '#ffaa00', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.35), '#ffaa00', ox, oy)
},
}
export const golem: Archetype = {
name: 'golem', weight: 0.02,
dimensionOverrides: (tier) => ({ legW: 6 + tier, legH: 7 }),
drawFeatures: (p) => {
const { px, ox, oy, t, ko, bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Cracks/rune patterns
const rune = '#44aaff'
px(cx + hOff, by + 2, rune, ox, oy); px(cx + hOff, by + 4, rune, ox, oy)
px(cx + hOff - 2, by + 3, rune, ox, oy); px(cx + hOff + 2, by + 3, rune, ox, oy)
// Glowing core
const pulse = Math.sin(t * Math.PI * 2) > 0 ? '#44aaff' : '#2266aa'
px(cx + hOff, by + Math.floor(bh / 2), pulse, ox, oy)
px(cx + hOff - 1, by + Math.floor(bh / 2), pulse, ox, oy)
px(cx + hOff + 1, by + Math.floor(bh / 2), pulse, ox, oy)
// Rocky texture
for (let ix = bx; ix < bx + bw; ix += 4) px(ix, by + bh - 1, '#999999', ox, oy)
// Forehead rune
px(cx + hOff, hy + 1, rune, ox, oy)
},
}
export const vampire: Archetype = {
name: 'vampire', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Cape
for (let iy = by; iy < by + bh + 3; iy++) {
px(bx - 1, iy, '#440000', ox, oy); px(bx - 2, iy + 1, '#330000', ox, oy)
px(bx + bw, iy, '#440000', ox, oy); px(bx + bw + 1, iy + 1, '#330000', ox, oy)
}
// Widow's peak hair
px(hx + Math.floor(hw / 2), hy - 1, '#111111', ox, oy)
px(hx + Math.floor(hw / 2) - 1, hy, '#111111', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy, '#111111', ox, oy)
// Fangs
px(hx + 2, hy + hh, '#ffffff', ox, oy)
px(hx + hw - 3, hy + hh, '#ffffff', ox, oy)
// Red eyes
px(hx + 2, hy + Math.floor(hh * 0.35), '#ff0000', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.35), '#ff0000', ox, oy)
},
}
export const werewolf: Archetype = {
name: 'werewolf', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Fur tufts everywhere
for (let ix = bx; ix < bx + bw; ix += 2) px(ix, by - 1, '#665544', ox, oy)
for (let iy = by; iy < by + bh; iy += 3) {
px(bx - 1, iy, '#665544', ox, oy); px(bx + bw, iy, '#665544', ox, oy)
}
// Pointy ears
px(hx, hy - 1, '#665544', ox, oy); px(hx - 1, hy - 2, '#665544', ox, oy)
px(hx + hw - 1, hy - 1, '#665544', ox, oy); px(hx + hw, hy - 2, '#665544', ox, oy)
// Snout
px(hx + Math.floor(hw / 2), hy + hh - 1, '#553322', ox, oy)
px(hx + Math.floor(hw / 2), hy + hh, '#553322', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy + hh, '#222222', ox, oy)
// Claws
px(bx - 1, by + bh, '#cccccc', ox, oy); px(bx + bw, by + bh, '#cccccc', ox, oy)
},
}
export const zombie: Archetype = {
name: 'zombie', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Green skin patches
px(hx + 2, hy + 1, '#44aa44', ox, oy); px(hx + hw - 3, hy + 2, '#44aa44', ox, oy)
px(bx + 2, by + 2, '#44aa44', ox, oy); px(bx + bw - 3, by + bh - 2, '#44aa44', ox, oy)
// Exposed ribs
for (let r = 0; r < 3; r++) {
px(bx + 1, by + 2 + r * 2, '#ddddcc', ox, oy)
px(bx + 2, by + 2 + r * 2, '#ddddcc', ox, oy)
}
// Droopy eye
px(hx + 2, hy + Math.floor(hh * 0.5), '#ff4444', ox, oy)
// Torn clothes
px(bx + bw - 1, by + bh - 1, '#554433', ox, oy)
px(bx + bw - 2, by + bh, '#554433', ox, oy)
// Brain showing
px(hx + Math.floor(hw / 2), hy, '#ff88aa', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy, '#ff88aa', ox, oy)
},
}
export const witch: Archetype = {
name: 'witch', weight: 0.02, canHaveMohawk: false, canHaveVisor: false,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, idle, ko, hx, hy, hh, hw, cx, hOff, bx, by, bw, bh } = p
if (ko) return
// Pointed hat
for (let h = 0; h < 7; h++) {
const w = Math.max(1, 4 - h)
for (let ix = cx + hOff - Math.floor(w / 2); ix < cx + hOff + Math.ceil(w / 2); ix++) {
px(ix, hy - 1 - h, '#220044', ox, oy)
}
}
// Hat brim
for (let ix = hx - 2; ix < hx + hw + 2; ix++) px(ix, hy - 1, '#330055', ox, oy)
// Wart on nose
px(hx + Math.floor(hw / 2) + 1, hy + Math.floor(hh * 0.6), '#448833', ox, oy)
// Broom (floating next to body)
if (idle) {
px(bx - 3, by + bh - 2, '#886633', ox, oy)
px(bx - 3, by + bh - 1, '#886633', ox, oy)
px(bx - 3, by + bh, '#886633', ox, oy)
px(bx - 4, by + bh + 1, '#aa9944', ox, oy)
px(bx - 3, by + bh + 1, '#aa9944', ox, oy)
px(bx - 2, by + bh + 1, '#aa9944', ox, oy)
}
// Cat familiar (tiny, near feet)
px(bx - 2, by + bh + 3, '#111111', ox, oy)
px(bx - 1, by + bh + 3, '#111111', ox, oy)
px(bx - 2, by + bh + 2, '#111111', ox, oy)
},
}
export const demon: Archetype = {
name: 'demon', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Demon horns (curved forward)
px(hx, hy - 1, '#cc2222', ox, oy); px(hx - 1, hy - 2, '#cc2222', ox, oy); px(hx, hy - 3, '#cc2222', ox, oy)
px(hx + hw - 1, hy - 1, '#cc2222', ox, oy); px(hx + hw, hy - 2, '#cc2222', ox, oy); px(hx + hw - 1, hy - 3, '#cc2222', ox, oy)
// Pointed tail
px(bx + bw + 1, by + bh - 1, '#cc2222', ox, oy)
px(bx + bw + 2, by + bh - 2, '#cc2222', ox, oy)
px(bx + bw + 3, by + bh - 3, '#cc2222', ox, oy)
px(bx + bw + 4, by + bh - 3, '#ff4444', ox, oy) // arrow tip
px(bx + bw + 3, by + bh - 4, '#ff4444', ox, oy)
// Glowing eyes
px(hx + 2, hy + Math.floor(hh * 0.35), '#ffaa00', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.35), '#ffaa00', ox, oy)
// Dark aura
if (p.idle) {
const flicker = Math.sin(t * Math.PI * 4) > 0
if (flicker) {
px(bx - 1, by - 1, '#440000', ox, oy); px(bx + bw, by - 1, '#440000', ox, oy)
}
}
},
}
@@ -0,0 +1,335 @@
import type { Archetype } from '../constants'
export const robot: Archetype = {
name: 'robot', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, idle, ko, hx, hy, hh, hw, cx, hOff, bx, by, bw, bh } = p
if (ko) return
// Antenna on head
px(cx + hOff, hy - 3, '#888888', ox, oy)
px(cx + hOff, hy - 4, '#888888', ox, oy)
px(cx + hOff, hy - 5, idle ? '#ff0000' : '#00ff00', ox, oy)
// Panel lines on body
for (let ix = bx + 2; ix < bx + bw - 2; ix += 3) {
px(ix, by + Math.floor(bh / 2), '#666666', ox, oy)
}
// Chest light
const blink = Math.sin(t * Math.PI * 4) > 0
px(cx + hOff, by + 2, blink ? '#00ff44' : '#004411', ox, oy)
px(cx + hOff + 1, by + 2, blink ? '#00ff44' : '#004411', ox, oy)
// Bolts on joints
px(bx, by, '#aaaaaa', ox, oy); px(bx + bw - 1, by, '#aaaaaa', ox, oy)
px(bx, by + bh - 1, '#aaaaaa', ox, oy); px(bx + bw - 1, by + bh - 1, '#aaaaaa', ox, oy)
},
}
export const android: Archetype = {
name: 'android', weight: 0.02,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, idle, ko, hx, hy, hh, hw, cx, hOff, bx, by, bw, bh } = p
if (ko) return
// Glowing circuit lines on body
const glow = Math.sin(t * Math.PI * 2) * 0.5 + 0.5 > 0.5 ? '#44ffff' : '#228888'
for (let iy = by + 1; iy < by + bh; iy += 2) {
px(cx + hOff, iy, glow, ox, oy)
}
// Ear sensors
px(hx - 1, hy + Math.floor(hh / 2), '#44ffff', ox, oy)
px(hx + hw, hy + Math.floor(hh / 2), '#44ffff', ox, oy)
// Visor line across eyes
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
px(ix, hy + Math.floor(hh * 0.35), '#44ffff', ox, oy)
}
},
}
export const droneBug: Archetype = {
name: 'drone', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Propellers on top (spinning)
const spin = Math.floor(t * 20) % 2
if (spin === 0) {
for (let i = -8; i <= 8; i++) px(cx + hOff + i, by - 5, '#666666', ox, oy)
} else {
for (let i = -2; i <= 2; i++) px(cx + hOff + i, by - 5, '#666666', ox, oy)
}
// Camera lens on front
px(bx + bw - 1, by + Math.floor(bh / 2), '#ff0000', ox, oy)
px(bx + bw, by + Math.floor(bh / 2), '#ff0000', ox, oy)
// LED strip on bottom
const led = Math.floor(t * 6) % 3
px(bx + 2 + led * 3, by + bh, '#00ff00', ox, oy)
},
}
export const toaster: Archetype = {
name: 'toaster', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
dimensionOverrides: () => ({ legH: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, cx, hOff, hy } = p
if (ko) return
// Toast popping out of top
const pop = idle ? Math.sin(t * Math.PI * 2) * 3 : 0
px(cx + hOff - 2, hy - 3 + pop, '#dda855', ox, oy)
px(cx + hOff - 1, hy - 3 + pop, '#dda855', ox, oy)
px(cx + hOff, hy - 3 + pop, '#cc9944', ox, oy)
px(cx + hOff + 1, hy - 3 + pop, '#dda855', ox, oy)
px(cx + hOff - 2, hy - 4 + pop, '#cc9944', ox, oy)
px(cx + hOff + 1, hy - 4 + pop, '#cc9944', ox, oy)
// Dial on side
px(bx, by + Math.floor(bh / 2), '#888888', ox, oy)
// Lever
px(bx + bw, by + 2, '#666666', ox, oy)
px(bx + bw, by + 3, '#666666', ox, oy)
// Chrome stripe
for (let ix = bx + 1; ix < bx + bw - 1; ix++) {
px(ix, by + bh - 2, '#cccccc', ox, oy)
}
},
}
export const tvHead: Archetype = {
name: 'tv_head', weight: 0.02, canHaveMohawk: false, canHaveVisor: false,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Static/scan lines on face
for (let iy = hy + 1; iy < hy + hh - 1; iy += 2) {
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
const flicker = Math.random() > 0.5 ? '#224488' : '#113366'
px(ix, iy, flicker, ox, oy)
}
}
// Antenna ears
px(hx, hy - 1, '#888888', ox, oy)
px(hx - 1, hy - 2, '#888888', ox, oy)
px(hx + hw - 1, hy - 1, '#888888', ox, oy)
px(hx + hw, hy - 2, '#888888', ox, oy)
// Power button
px(hx + hw, hy + hh - 2, '#ff0000', ox, oy)
},
}
export const calculator: Archetype = {
name: 'calculator', weight: 0.02, canHaveMohawk: false,
dimensionOverrides: () => ({ legH: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Number display on head
const num = Math.floor(t * 3) % 10
const digits = ['1','3','3','7','4','2','0','6','9','5']
// Just draw a green rect for display
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
px(ix, hy + 1, '#003300', ox, oy)
px(ix, hy + 2, '#003300', ox, oy)
}
px(hx + 2, hy + 1, '#00ff00', ox, oy)
px(hx + hw - 3, hy + 1, '#00ff00', ox, oy)
// Button grid on body
for (let gx = 0; gx < 3; gx++) {
for (let gy = 0; gy < 3; gy++) {
px(bx + 2 + gx * 3, by + 1 + gy * 3, '#888888', ox, oy)
}
}
},
}
export const satellite: Archetype = {
name: 'satellite', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Solar panels (wide rectangles extending from body)
for (let iy = by + 1; iy < by + 4; iy++) {
for (let ix = bx - 8; ix < bx - 1; ix++) px(ix, iy, '#2244aa', ox, oy)
for (let ix = bx + bw + 1; ix < bx + bw + 8; ix++) px(ix, iy, '#2244aa', ox, oy)
}
// Dish on top
for (let i = -3; i <= 3; i++) px(cx + hOff + i, by - 2, '#cccccc', ox, oy)
px(cx + hOff, by - 3, '#cccccc', ox, oy)
// Blinking light
const blink = Math.sin(t * Math.PI * 3) > 0
px(cx + hOff, by - 4, blink ? '#ff0000' : '#440000', ox, oy)
},
}
export const mech: Archetype = {
name: 'mech', weight: 0.02,
dimensionOverrides: (tier) => ({ legW: 6 + tier, legH: 8 + tier }),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, ko, bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Shoulder pads
for (let ix = bx - 3; ix < bx; ix++) { px(ix, by, '#888888', ox, oy); px(ix, by + 1, '#666666', ox, oy) }
for (let ix = bx + bw; ix < bx + bw + 3; ix++) { px(ix, by, '#888888', ox, oy); px(ix, by + 1, '#666666', ox, oy) }
// Cockpit window on chest
for (let iy = by + 2; iy < by + 5; iy++) {
px(cx + hOff - 1, iy, '#44aaff', ox, oy)
px(cx + hOff, iy, '#88ccff', ox, oy)
px(cx + hOff + 1, iy, '#44aaff', ox, oy)
}
// Exhaust pipes on back
px(bx + bw + 1, by + bh - 3, '#555555', ox, oy)
px(bx + bw + 1, by + bh - 2, '#555555', ox, oy)
px(bx + bw + 1, by + bh - 1, '#555555', ox, oy)
},
}
export const ledCube: Archetype = {
name: 'led_cube', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, bx, by, bw, bh, hx, hy, hh, hw } = p
if (ko) return
// Pulsing colored LEDs all over body
const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff']
for (let ix = bx + 1; ix < bx + bw - 1; ix += 2) {
for (let iy = by + 1; iy < by + bh - 1; iy += 2) {
const ci = (ix + iy + Math.floor(t * 4)) % colors.length
px(ix, iy, colors[ci], ox, oy)
}
}
// Face LEDs
const faceColor = colors[Math.floor(t * 2) % colors.length]
px(hx + 2, hy + 2, faceColor, ox, oy)
px(hx + hw - 3, hy + 2, faceColor, ox, oy)
},
}
export const circuit: Archetype = {
name: 'circuit', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, bx, by, bw, bh, cx, hOff, hy, hh } = p
if (ko) return
// PCB traces on body
const trace = '#44aa22'
for (let ix = bx + 1; ix < bx + bw; ix += 3) {
for (let iy = by; iy < by + bh; iy++) px(ix, iy, trace, ox, oy)
}
for (let iy = by + 2; iy < by + bh; iy += 3) {
for (let ix = bx; ix < bx + bw; ix++) px(ix, iy, trace, ox, oy)
}
// Chip on body center
px(cx + hOff - 1, by + Math.floor(bh / 2), '#222222', ox, oy)
px(cx + hOff, by + Math.floor(bh / 2), '#222222', ox, oy)
px(cx + hOff + 1, by + Math.floor(bh / 2), '#222222', ox, oy)
// Solder points
px(bx + 2, by + 1, '#cccccc', ox, oy)
px(bx + bw - 3, by + bh - 2, '#cccccc', ox, oy)
},
}
export const antennaBug: Archetype = {
name: 'antenna_bot', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hw, cx, hOff } = p
if (ko) return
// Multiple antennae
const wobble = idle ? Math.sin(t * Math.PI * 3) * 2 : 0
for (let a = 0; a < 3; a++) {
const ax = hx + 1 + a * Math.floor((hw - 2) / 2)
for (let h = 0; h < 4 + a; h++) {
px(ax, hy - 1 - h + Math.round(wobble * (a === 1 ? -1 : 1)), '#888888', ox, oy)
}
const tipColor = ['#ff0000', '#00ff00', '#0000ff'][a]
px(ax, hy - 5 - a + Math.round(wobble * (a === 1 ? -1 : 1)), tipColor, ox, oy)
}
},
}
export const microwave: Archetype = {
name: 'microwave', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
dimensionOverrides: () => ({ legH: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Door window on face
for (let ix = hx + 1; ix < hx + hw - 2; ix++) {
for (let iy = hy + 1; iy < hy + hh - 1; iy++) {
px(ix, iy, '#223344', ox, oy)
}
}
// Spinning plate inside (when idle)
const spin = Math.floor(t * 4) % 4
const plateX = hx + Math.floor(hw / 2) + (spin < 2 ? -1 : 1)
px(plateX, hy + Math.floor(hh / 2), '#ffee88', ox, oy)
// Buttons on right side
px(hx + hw - 1, hy + 1, '#ff0000', ox, oy)
px(hx + hw - 1, hy + 3, '#00ff00', ox, oy)
// Handle
px(bx + bw, by + Math.floor(bh / 2), '#aaaaaa', ox, oy)
px(bx + bw, by + Math.floor(bh / 2) + 1, '#aaaaaa', ox, oy)
},
}
export const cyberdog: Archetype = {
name: 'cyberdog', weight: 0.02,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff, feetY, globalY, vBounce } = p
if (ko) return
// Floppy robo-ears with LEDs
px(hx - 1, hy + 1, '#888888', ox, oy); px(hx - 1, hy + 2, '#888888', ox, oy)
px(hx - 1, hy + 3, '#00ff00', ox, oy)
px(hx + hw, hy + 1, '#888888', ox, oy); px(hx + hw, hy + 2, '#888888', ox, oy)
px(hx + hw, hy + 3, '#00ff00', ox, oy)
// Robo-tail (wagging)
const wag = idle ? Math.sin(t * Math.PI * 4) * 4 : 0
px(bx + bw + 1, by + 2, '#888888', ox, oy)
px(bx + bw + 2, by + 1 + Math.round(wag), '#888888', ox, oy)
px(bx + bw + 3, by + Math.round(wag), '#ff4444', ox, oy)
// Snout
px(hx + Math.floor(hw / 2), hy + hh - 1, '#444444', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy + hh - 1, '#444444', ox, oy)
px(hx + Math.floor(hw / 2), hy + hh, '#222222', ox, oy)
},
}
export const robocat: Archetype = {
name: 'robocat', weight: 0.02,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Pointy metal ears
px(hx + 1, hy - 1, '#aaaaaa', ox, oy); px(hx, hy - 2, '#aaaaaa', ox, oy)
px(hx + hw - 2, hy - 1, '#aaaaaa', ox, oy); px(hx + hw - 1, hy - 2, '#aaaaaa', ox, oy)
// Whisker sensors
for (let w = 1; w <= 3; w++) {
px(hx - w, hy + Math.floor(hh * 0.6) + (w === 2 ? -1 : w === 3 ? 1 : 0), '#cccccc', ox, oy)
px(hx + hw + w - 1, hy + Math.floor(hh * 0.6) + (w === 2 ? -1 : w === 3 ? 1 : 0), '#cccccc', ox, oy)
}
// Curled tail with LED tip
const curl = Math.sin(t * Math.PI * 2) * 2
px(bx + bw + 1, by + bh - 2, '#888888', ox, oy)
px(bx + bw + 2, by + bh - 3, '#888888', ox, oy)
px(bx + bw + 2, by + bh - 4 + Math.round(curl), '#ff00ff', ox, oy)
},
}
export const ufoBot: Archetype = {
name: 'ufo_bot', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, bx, by, bw, bh, cx, hOff, hy } = p
if (ko) return
// Dome on top
for (let i = -2; i <= 2; i++) px(cx + hOff + i, hy - 2, '#88ffcc', ox, oy)
for (let i = -1; i <= 1; i++) px(cx + hOff + i, hy - 3, '#aaffdd', ox, oy)
px(cx + hOff, hy - 4, '#ccffee', ox, oy)
// Rotating lights around body
const phase = Math.floor(t * 6) % 6
const colors = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff']
for (let i = 0; i < 6; i++) {
const angle = (i + phase) * Math.PI * 2 / 6
const lx = cx + hOff + Math.round(Math.cos(angle) * (bw / 2 + 2))
const ly = by + Math.floor(bh / 2) + Math.round(Math.sin(angle) * 2)
px(lx, ly, colors[i], ox, oy)
}
// Tractor beam below (when idle)
if (p.idle) {
for (let iy = by + bh + 1; iy < by + bh + 4; iy++) {
px(cx + hOff, iy, '#44ff88', ox, oy)
}
}
},
}
@@ -0,0 +1,373 @@
import type { Archetype } from '../constants'
export const sockPuppet: Archetype = {
name: 'sock_puppet', weight: 0.02, canHaveMohawk: false,
dimensionOverrides: () => ({ legH: 2 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Googly eyes (one bigger)
px(hx + 2, hy + 2, '#ffffff', ox, oy); px(hx + 3, hy + 2, '#ffffff', ox, oy)
px(hx + 2, hy + 3, '#ffffff', ox, oy); px(hx + 3, hy + 3, '#000000', ox, oy)
px(hx + hw - 3, hy + 1, '#ffffff', ox, oy); px(hx + hw - 2, hy + 1, '#ffffff', ox, oy)
px(hx + hw - 3, hy + 2, '#ffffff', ox, oy); px(hx + hw - 2, hy + 2, '#000000', ox, oy)
px(hx + hw - 3, hy + 3, '#ffffff', ox, oy); px(hx + hw - 2, hy + 3, '#ffffff', ox, oy)
// Mouth (flapping)
const flap = idle ? Math.sin(t * Math.PI * 3) > 0 : false
if (flap) {
for (let ix = hx + 1; ix < hx + hw - 1; ix++) px(ix, hy + hh, '#ff4466', ox, oy)
}
// Yarn hair
px(cx + hOff - 1, hy - 1, '#ff8844', ox, oy)
px(cx + hOff, hy - 2, '#ff8844', ox, oy)
px(cx + hOff + 1, hy - 1, '#ff8844', ox, oy)
},
}
export const trafficCone: Archetype = {
name: 'traffic_cone', weight: 0.02, canHaveMohawk: false, canHaveHorns: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Cone shape above head (orange + white stripes)
for (let h = 0; h < 8; h++) {
const w = Math.max(1, 8 - h)
const color = (h % 3 === 0) ? '#ffffff' : '#ff6600'
for (let ix = cx + hOff - Math.floor(w / 2); ix < cx + hOff + Math.ceil(w / 2); ix++) {
px(ix, hy - 1 - h, color, ox, oy)
}
}
// Tip
px(cx + hOff, hy - 9, '#ff4400', ox, oy)
// Base (wider at head level)
for (let ix = hx - 2; ix < hx + hw + 2; ix++) px(ix, hy - 1, '#ff6600', ox, oy)
},
}
export const toiletMan: Archetype = {
name: 'toilet_man', weight: 0.02, canHaveMohawk: false,
dimensionOverrides: () => ({ legH: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Toilet seat around head
for (let ix = hx - 1; ix < hx + hw + 1; ix++) {
px(ix, hy - 1, '#ffffff', ox, oy); px(ix, hy + hh, '#ffffff', ox, oy)
}
px(hx - 1, hy, '#ffffff', ox, oy); px(hx + hw, hy, '#ffffff', ox, oy)
px(hx - 1, hy + hh - 1, '#ffffff', ox, oy); px(hx + hw, hy + hh - 1, '#ffffff', ox, oy)
// Tank on back
px(bx + bw + 1, by, '#ffffff', ox, oy); px(bx + bw + 1, by + 1, '#ffffff', ox, oy)
px(bx + bw + 1, by + 2, '#ffffff', ox, oy)
// Handle
px(bx + bw + 2, by, '#cccccc', ox, oy)
// Water splash eyes
px(hx + 2, hy + Math.floor(hh * 0.35), '#4488ff', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.35), '#4488ff', ox, oy)
},
}
export const potato: Archetype = {
name: 'potato', weight: 0.02,
dimensionOverrides: () => ({ legH: 3, legW: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, bx, by, bw, bh, hx, hy, hh, hw } = p
if (ko) return
// Eyes (spots) on body
px(bx + 2, by + 2, '#443322', ox, oy)
px(bx + bw - 3, by + 3, '#443322', ox, oy)
px(bx + 1, by + bh - 3, '#443322', ox, oy)
// Sprout on top
px(hx + Math.floor(hw / 2), hy - 1, '#44aa22', ox, oy)
px(hx + Math.floor(hw / 2), hy - 2, '#44aa22', ox, oy)
px(hx + Math.floor(hw / 2) - 1, hy - 3, '#66cc44', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy - 3, '#66cc44', ox, oy)
// Dirt spots
px(bx + Math.floor(bw / 2), by + bh - 1, '#553311', ox, oy)
},
}
export const cloudMan: Archetype = {
name: 'cloud_man', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Fluffy cloud outline
for (let ix = bx - 2; ix < bx + bw + 2; ix++) px(ix, by - 1, '#ffffff', ox, oy)
for (let ix = bx - 1; ix < bx + bw + 1; ix++) px(ix, by - 2, '#ffffff', ox, oy)
// Rain drops below (when idle)
if (idle) {
const phase = Math.floor(t * 6) % 4
for (let d = 0; d < 3; d++) {
const dx = bx + 1 + d * Math.floor(bw / 3)
const dy = by + bh + 1 + (phase + d) % 4
px(dx, dy, '#4488ff', ox, oy)
}
}
// Poofy top
px(cx + hOff - 2, hy - 1, '#ffffff', ox, oy)
px(cx + hOff, hy - 2, '#ffffff', ox, oy)
px(cx + hOff + 2, hy - 1, '#ffffff', ox, oy)
},
}
export const rockMan: Archetype = {
name: 'rock_man', weight: 0.02,
dimensionOverrides: () => ({ legH: 4 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, bx, by, bw, bh, hx, hy, hh, hw } = p
if (ko) return
// Cracks on body
px(bx + 2, by + 1, '#555555', ox, oy); px(bx + 3, by + 2, '#555555', ox, oy)
px(bx + 4, by + 2, '#555555', ox, oy); px(bx + 5, by + 3, '#555555', ox, oy)
px(bx + bw - 3, by + bh - 3, '#555555', ox, oy); px(bx + bw - 4, by + bh - 2, '#555555', ox, oy)
// Mossy patches
px(bx + 1, by, '#448833', ox, oy); px(bx + 2, by, '#558844', ox, oy)
px(bx + bw - 2, by + bh - 1, '#448833', ox, oy)
// Crystal embedded
px(bx + Math.floor(bw / 2), by + Math.floor(bh / 2), '#88aaff', ox, oy)
px(bx + Math.floor(bw / 2) + 1, by + Math.floor(bh / 2), '#aaccff', ox, oy)
// Stone face texture
px(hx + 1, hy + hh - 1, '#777777', ox, oy); px(hx + hw - 2, hy + hh - 1, '#777777', ox, oy)
},
}
export const balloonMan: Archetype = {
name: 'balloon_man', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Balloon on string above head
const bob = idle ? Math.sin(t * Math.PI * 2) * 3 : 0
// String
for (let h = 0; h < 4; h++) px(cx + hOff, hy - 2 - h, '#888888', ox, oy)
// Balloon
const bY = hy - 6 + Math.round(bob)
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
if (dx * dx + dy * dy <= 5) px(cx + hOff + dx, bY + dy, '#ff4488', ox, oy)
}
}
// Knot
px(cx + hOff, bY + 3, '#cc2266', ox, oy)
// Highlight
px(cx + hOff - 1, bY - 1, '#ffaacc', ox, oy)
},
}
export const trashCan: Archetype = {
name: 'trash_can', weight: 0.02, canHaveMohawk: false,
dimensionOverrides: () => ({ legH: 2 }),
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Lid (on top of head, slightly ajar when idle)
const lidAngle = idle ? Math.sin(t * Math.PI * 2) * 2 : 0
for (let ix = hx - 1; ix < hx + hw + 1; ix++) {
px(ix, hy - 1 + Math.round(lidAngle > 1 ? -1 : 0), '#888888', ox, oy)
}
// Handle on lid
px(hx + Math.floor(hw / 2), hy - 2, '#aaaaaa', ox, oy)
// Garbage sticking out when lid is open
if (lidAngle > 1) {
px(hx + 2, hy - 2, '#ff4444', ox, oy) // apple core
px(hx + hw - 3, hy - 2, '#ffcc44', ox, oy) // banana peel
}
// Dented texture
px(bx + 2, by + 3, '#666666', ox, oy); px(bx + bw - 3, by + bh - 3, '#666666', ox, oy)
// Flies (tiny dots)
if (idle) {
const flyX = bx + Math.floor(bw / 2) + Math.round(Math.sin(t * Math.PI * 5) * 4)
const flyY = hy - 3 + Math.round(Math.cos(t * Math.PI * 7) * 2)
px(flyX, flyY, '#222222', ox, oy)
}
},
}
export const rubberDuckArch: Archetype = {
name: 'rubber_duck', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Beak
px(hx + hw, hy + Math.floor(hh / 2), '#ff8800', ox, oy)
px(hx + hw + 1, hy + Math.floor(hh / 2), '#ff6600', ox, oy)
// Highlight on body (shiny rubber)
px(bx + 2, by + 1, '#ffee88', ox, oy)
px(bx + 3, by + 1, '#ffee88', ox, oy)
px(bx + 2, by + 2, '#ffee88', ox, oy)
// Bobbing motion water ripples
if (idle) {
const ripple = Math.sin(t * Math.PI * 2)
px(bx - 2, by + bh + 1, ripple > 0 ? '#88ccff' : '#4488cc', ox, oy)
px(bx + bw + 1, by + bh + 1, ripple < 0 ? '#88ccff' : '#4488cc', ox, oy)
}
// Crown (bath time king!)
px(hx + Math.floor(hw / 2) - 1, hy - 1, '#ffcc00', ox, oy)
px(hx + Math.floor(hw / 2), hy - 2, '#ffcc00', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy - 1, '#ffcc00', ox, oy)
},
}
export const snowman: Archetype = {
name: 'snowman', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Top hat
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
px(ix, hy - 1, '#222222', ox, oy); px(ix, hy - 2, '#222222', ox, oy)
px(ix, hy - 3, '#222222', ox, oy)
}
for (let ix = hx - 1; ix < hx + hw + 1; ix++) px(ix, hy - 1, '#333333', ox, oy)
// Carrot nose
px(hx + Math.floor(hw / 2), hy + Math.floor(hh * 0.5), '#ff8800', ox, oy)
px(hx + Math.floor(hw / 2) + 1, hy + Math.floor(hh * 0.5), '#ff6600', ox, oy)
// Coal buttons
px(cx + hOff, by + 2, '#222222', ox, oy)
px(cx + hOff, by + Math.floor(bh / 2), '#222222', ox, oy)
px(cx + hOff, by + bh - 2, '#222222', ox, oy)
// Scarf
for (let ix = bx - 1; ix < bx + bw + 1; ix++) px(ix, by - 1, '#ff0000', ox, oy)
px(bx - 1, by, '#ff0000', ox, oy); px(bx - 1, by + 1, '#ff0000', ox, oy)
// Stick arms
px(bx - 2, by + Math.floor(bh / 2), '#886633', ox, oy)
px(bx - 3, by + Math.floor(bh / 2) - 1, '#886633', ox, oy)
px(bx + bw + 1, by + Math.floor(bh / 2), '#886633', ox, oy)
px(bx + bw + 2, by + Math.floor(bh / 2) - 1, '#886633', ox, oy)
},
}
export const scarecrow: Archetype = {
name: 'scarecrow', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, t, idle, ko, hx, hy, hh, hw, bx, by, bw, bh } = p
if (ko) return
// Straw hat
for (let ix = hx - 3; ix < hx + hw + 3; ix++) px(ix, hy - 1, '#ddbb66', ox, oy)
for (let ix = hx; ix < hx + hw; ix++) px(ix, hy - 2, '#ccaa55', ox, oy)
// Button eyes
px(hx + 2, hy + Math.floor(hh * 0.35), '#444444', ox, oy)
px(hx + hw - 3, hy + Math.floor(hh * 0.35), '#444444', ox, oy)
// Stitched mouth
for (let ix = hx + 1; ix < hx + hw - 1; ix += 2) {
px(ix, hy + Math.floor(hh * 0.7), '#444444', ox, oy)
}
// Straw poking out
px(bx - 1, by + bh - 1, '#ddbb66', ox, oy)
px(bx + bw, by + bh - 1, '#ddbb66', ox, oy)
px(hx - 1, hy + Math.floor(hh / 2), '#ddbb66', ox, oy)
// Patched clothes
px(bx + 2, by + 2, '#886644', ox, oy); px(bx + 3, by + 2, '#886644', ox, oy)
px(bx + 2, by + 3, '#886644', ox, oy); px(bx + 3, by + 3, '#886644', ox, oy)
// Crow on shoulder
px(bx + bw + 1, by - 1, '#222222', ox, oy); px(bx + bw + 2, by - 1, '#222222', ox, oy)
px(bx + bw + 1, by - 2, '#222222', ox, oy)
},
}
export const jackOLantern: Archetype = {
name: 'jack_o_lantern', weight: 0.02, canHaveMohawk: false, canHaveVisor: false,
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, cx, hOff } = p
if (ko) return
// Stem
px(cx + hOff, hy - 1, '#44aa22', ox, oy); px(cx + hOff, hy - 2, '#44aa22', ox, oy)
// Carved face (replaces normal face)
const glow = Math.sin(t * Math.PI * 3) > 0 ? '#ffcc00' : '#ff8800'
// Triangle eyes
px(hx + 2, hy + 2, glow, ox, oy)
px(hx + 1, hy + 3, glow, ox, oy); px(hx + 2, hy + 3, glow, ox, oy); px(hx + 3, hy + 3, glow, ox, oy)
px(hx + hw - 3, hy + 2, glow, ox, oy)
px(hx + hw - 4, hy + 3, glow, ox, oy); px(hx + hw - 3, hy + 3, glow, ox, oy); px(hx + hw - 2, hy + 3, glow, ox, oy)
// Jagged mouth
for (let ix = hx + 1; ix < hx + hw - 1; ix++) {
px(ix, hy + Math.floor(hh * 0.7), glow, ox, oy)
if (ix % 2 === 0) px(ix, hy + Math.floor(hh * 0.7) - 1, glow, ox, oy)
}
// Ridges
for (let iy = hy; iy < hy + hh; iy += 2) {
px(hx, iy, '#cc6600', ox, oy); px(hx + hw - 1, iy, '#cc6600', ox, oy)
}
},
}
export const gardenGnome: Archetype = {
name: 'garden_gnome', weight: 0.02, canHaveMohawk: false,
dimensionOverrides: () => ({ legH: 4 }),
drawFeatures: (p) => {
const { px, ox, oy, ko, hx, hy, hh, hw, bx, by, bw, bh, cx, hOff } = p
if (ko) return
// Pointy red hat
for (let h = 0; h < 6; h++) {
const w = Math.max(1, 4 - h)
for (let ix = cx + hOff - Math.floor(w / 2); ix < cx + hOff + Math.ceil(w / 2); ix++) {
px(ix, hy - 1 - h, '#ff0000', ox, oy)
}
}
// Big white beard
for (let iy = hy + Math.floor(hh * 0.5); iy < hy + hh + 4; iy++) {
const bw2 = Math.max(1, 4 - (iy - hy - Math.floor(hh * 0.5)))
for (let ix = cx + hOff - bw2; ix <= cx + hOff + bw2; ix++) {
px(ix, iy, '#ffffff', ox, oy)
}
}
// Rosy cheeks
px(hx + 1, hy + Math.floor(hh * 0.5), '#ff8888', ox, oy)
px(hx + hw - 2, hy + Math.floor(hh * 0.5), '#ff8888', ox, oy)
// Belt with buckle
for (let ix = bx; ix < bx + bw; ix++) px(ix, by + Math.floor(bh * 0.7), '#886633', ox, oy)
px(cx + hOff, by + Math.floor(bh * 0.7), '#ffcc00', ox, oy)
// Fishing rod or lantern
px(bx + bw + 1, by + 2, '#886633', ox, oy)
px(bx + bw + 1, by + 3, '#886633', ox, oy)
px(bx + bw + 2, by + 3, '#ffcc00', ox, oy)
},
}
export const lampPost: Archetype = {
name: 'lamp_post', weight: 0.02, canHaveMohawk: false,
dimensionOverrides: () => ({ legH: 2, legW: 3 }),
drawFeatures: (p) => {
const { px, ox, oy, t, ko, hx, hy, hh, hw, cx, hOff, bx, by } = p
if (ko) return
// Lamp shade on top
for (let ix = hx - 2; ix < hx + hw + 2; ix++) px(ix, hy - 1, '#445566', ox, oy)
// Light glow
const glow = Math.sin(t * Math.PI * 2) > 0 ? '#ffee88' : '#ffcc44'
for (let ix = hx - 1; ix < hx + hw + 1; ix++) px(ix, hy - 2, glow, ox, oy)
px(hx + Math.floor(hw / 2), hy - 3, glow, ox, oy)
// Moths circling (tiny dots)
const moth1X = cx + hOff + Math.round(Math.sin(t * Math.PI * 4) * 5)
const moth1Y = hy - 3 + Math.round(Math.cos(t * Math.PI * 4) * 3)
px(moth1X, moth1Y, '#ccccaa', ox, oy)
const moth2X = cx + hOff + Math.round(Math.cos(t * Math.PI * 3) * 4)
const moth2Y = hy - 2 + Math.round(Math.sin(t * Math.PI * 3) * 2)
px(moth2X, moth2Y, '#ccccaa', ox, oy)
// Base plate
for (let ix = bx - 1; ix < bx + p.bw + 1; ix++) px(ix, by + p.bh + 1, '#445566', ox, oy)
},
}
export const broomMan: Archetype = {
name: 'broom_man', weight: 0.02, canHaveMohawk: false,
drawFeatures: (p) => {
const { px, ox, oy, ko, bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY } = p
if (ko) return
// Bristles at feet
const footY = feetY + globalY + p.vBounce
for (let ix = bx - 2; ix < bx + bw + 2; ix++) {
px(ix, footY + 1, '#ccaa55', ox, oy)
px(ix, footY + 2, '#bbaa44', ox, oy)
}
// Handle extends up through head
for (let iy = hy - 4; iy < hy; iy++) {
px(cx + hOff, iy, '#886633', ox, oy)
}
// Dust cloud when moving
if (!p.idle && !p.knockback) {
px(bx - 3, footY + 2, '#ccccaa', ox, oy)
px(bx - 4, footY + 1, '#ccccaa', ox, oy)
}
},
}
@@ -0,0 +1,60 @@
import type { Archetype } from '../constants'
export const bee: Archetype = {
name: 'bee',
weight: 0.03,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko, knockback, win,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY, bounce } = p
// Yellow/black stripes on body
for (let iy = by; iy < by + bh; iy++) {
if ((iy - by) % 3 === 0) {
for (let ix = bx + 1; ix < bx + bw - 1; ix++) {
px(ix, iy, '#ffcc00', ox, oy)
}
}
}
// Wings (transparent, buzzing)
if (!ko && !knockback) {
const wingPhase = idle || win ? Math.sin(t * Math.PI * 8) * 3 : 0
// Left wing
const lwx = bx - 3
const lwy = by - 2 + Math.round(wingPhase)
px(lwx, lwy, '#aaddff', ox, oy)
px(lwx - 1, lwy, '#88bbee', ox, oy)
px(lwx - 2, lwy - 1, '#88bbee', ox, oy)
px(lwx, lwy - 1, '#aaddff', ox, oy)
px(lwx - 1, lwy - 1, '#aaddff', ox, oy)
// Right wing
const rwx = bx + bw + 2
const rwy = by - 2 - Math.round(wingPhase)
px(rwx, rwy, '#aaddff', ox, oy)
px(rwx + 1, rwy, '#88bbee', ox, oy)
px(rwx + 2, rwy - 1, '#88bbee', ox, oy)
px(rwx, rwy - 1, '#aaddff', ox, oy)
px(rwx + 1, rwy - 1, '#aaddff', ox, oy)
}
// Stinger at bottom
if (!ko) {
const stingX = cx + hOff
const stingY = feetY + globalY + 2
px(stingX, stingY, '#222222', ox, oy)
px(stingX, stingY + 1, '#111111', ox, oy)
px(stingX, stingY + 2, '#ffcc00', ox, oy)
}
// Antennae (short, bobbing)
if (!ko) {
const antWobble = idle ? Math.round(Math.sin(t * Math.PI * 3) * 1) : 0
px(hx + 2, hy - 1, '#222222', ox, oy)
px(hx + 1, hy - 2 + antWobble, '#222222', ox, oy)
px(hx + 0, hy - 3 + antWobble, '#ffcc00', ox, oy)
px(hx + hw - 3, hy - 1, '#222222', ox, oy)
px(hx + hw - 2, hy - 2 - antWobble, '#222222', ox, oy)
px(hx + hw - 1, hy - 3 - antWobble, '#ffcc00', ox, oy)
}
},
}
@@ -0,0 +1,53 @@
import type { Archetype } from '../constants'
export const blob: Archetype = {
name: 'blob',
weight: 0.14,
canHaveMohawk: false,
dimensionOverrides: (tier) => ({
bw: 12 + tier * 2,
bh: 10 + tier,
hw: 12 + tier,
hh: 11 + tier,
legH: 4,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, hit, ko,
cx, hOff, by, bw, bh, hx, hw, hy, hh, globalY } = p
// Amorphous body -- wobbly outline
const wobAmp = idle ? 1 : hit ? 2 : 0
for (let s = 0; s < 8; s++) {
const sa = s * Math.PI * 2 / 8 + t * 0.3
const sr = Math.floor(bw / 2) + 2 + Math.round(Math.sin(sa * 3 + t * Math.PI * 4) * wobAmp)
const sx = cx + hOff + Math.round(Math.cos(sa) * sr)
const sy = by + Math.floor(bh / 2) + Math.round(Math.sin(sa) * (bh / 2)) + globalY
px(sx, sy, pal.light, ox, oy)
px(sx, sy + 1, pal.light, ox, oy)
}
// Googly eyes -- oversized, bouncy
if (!ko) {
const geS = 3 + Math.floor(tier * 0.3)
const geLx = hx + 1
const geRx = hx + hw - geS - 1
const geY = hy + Math.floor(hh * 0.2)
// Big white circles
for (let ey = geY; ey < geY + geS; ey++) {
for (let ex = geLx; ex < geLx + geS; ex++) px(ex, ey, '#ffffff', ox, oy)
for (let ex = geRx; ex < geRx + geS; ex++) px(ex, ey, '#ffffff', ox, oy)
}
// Bouncing pupils
const pupOff = idle ? Math.round(Math.sin(t * Math.PI * 2) * 1) : 0
px(geLx + Math.floor(geS / 2) + pupOff, geY + geS - 2, '#000000', ox, oy)
px(geLx + Math.floor(geS / 2) + pupOff + 1, geY + geS - 2, '#000000', ox, oy)
px(geRx + Math.floor(geS / 2) - pupOff, geY + geS - 2, '#000000', ox, oy)
px(geRx + Math.floor(geS / 2) - pupOff + 1, geY + geS - 2, '#000000', ox, oy)
}
// Drool / slime drip
if (idle || hit) {
const drY = hy + Math.floor(hh * 0.8) + Math.round(t * 2)
px(cx + hOff, drY, pal.accLight, ox, oy)
px(cx + hOff, drY + 1, pal.accLight, ox, oy)
}
},
}
@@ -0,0 +1,47 @@
import type { Archetype } from '../constants'
export const cactus: Archetype = {
name: 'cactus',
weight: 0.03,
canHaveMohawk: false,
dimensionOverrides: () => ({
bw: 14,
armW: 2,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, tier, ko,
bx, by, bw, bh, hx, hy, hh } = p
// Green body override tint
const green = '#2d8a4e'
const darkGreen = '#1a5e33'
// Spikes all over body
if (!ko) {
for (let s = 0; s < 8 + tier; s++) {
const sx = bx + Math.floor(Math.random() * bw)
const sy = by + Math.floor(Math.random() * bh)
const sDir = sx < bx + bw / 2 ? -1 : 1
px(sx + sDir * 1, sy, '#cccc44', ox, oy)
px(sx + sDir * 2, sy, '#aaaa33', ox, oy)
}
}
// Spikes on head
for (let s = 0; s < 5; s++) {
const sx = hx + 1 + Math.floor(s * (p.hw - 2) / 4)
px(sx, hy - 1, '#cccc44', ox, oy)
px(sx, hy - 2, '#aaaa33', ox, oy)
}
// Small flower on top
const flowerY = hy - 3
const flowerX = hx + Math.floor(p.hw / 2) + 2
px(flowerX, flowerY, '#ff66aa', ox, oy)
px(flowerX - 1, flowerY, '#ff88cc', ox, oy)
px(flowerX + 1, flowerY, '#ff88cc', ox, oy)
px(flowerX, flowerY - 1, '#ff88cc', ox, oy)
px(flowerX, flowerY + 1, '#ff88cc', ox, oy)
px(flowerX, flowerY, '#ffff00', ox, oy) // center
},
}
@@ -0,0 +1,54 @@
import type { Archetype } from '../constants'
export const cat: Archetype = {
name: 'cat',
weight: 0.04,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, idle, ko, knockback, atk, special,
cx, hOff, hx, hw, hy, hh, bw, feetY, globalY, bounce } = p
if (ko) return
// Pointed ears (triangles above head)
px(hx, hy - 1, pal.body, ox, oy)
px(hx - 1, hy - 2, pal.body, ox, oy)
px(hx - 2, hy - 3, pal.dark, ox, oy)
px(hx, hy - 2, '#ffaaaa', ox, oy) // inner ear pink
px(hx + hw - 1, hy - 1, pal.body, ox, oy)
px(hx + hw, hy - 2, pal.body, ox, oy)
px(hx + hw + 1, hy - 3, pal.dark, ox, oy)
px(hx + hw - 1, hy - 2, '#ffaaaa', ox, oy)
// Whiskers (3 per side)
const wY = hy + Math.floor(hh * 0.5)
for (let w = 0; w < 3; w++) {
const wd = w - 1
px(hx - 2 - w, wY + wd, pal.out, ox, oy)
px(hx - 3 - w, wY + wd, pal.out, ox, oy)
px(hx + hw + 1 + w, wY + wd, pal.out, ox, oy)
px(hx + hw + 2 + w, wY + wd, pal.out, ox, oy)
}
// Slit eyes override (narrow pupils)
const eyeY = hy + Math.floor(hh * 0.35)
const leX = hx + Math.floor(hw * 0.25)
const reX = hx + Math.floor(hw * 0.65)
if (atk || special) {
px(leX, eyeY, '#ffcc00', ox, oy)
px(reX, eyeY, '#ffcc00', ox, oy)
}
// Curled tail
if (!knockback) {
const tailDir = -1
const curlPhase = idle ? t * Math.PI * 2 : 0
for (let tt = 1; tt <= 5; tt++) {
const curlX = Math.round(Math.sin(curlPhase + tt * 0.5) * 2)
px(cx + hOff + tailDir * (Math.floor(bw / 2) + tt) + curlX, feetY - tt + globalY, pal.body, ox, oy)
}
// Curl at tip
px(cx + hOff + tailDir * (Math.floor(bw / 2) + 5) + 2, feetY - 6 + globalY, pal.dark, ox, oy)
px(cx + hOff + tailDir * (Math.floor(bw / 2) + 5) + 3, feetY - 5 + globalY, pal.dark, ox, oy)
}
},
}
@@ -0,0 +1,67 @@
import type { Archetype } from '../constants'
export const cowboy: Archetype = {
name: 'cowboy',
weight: 0.03,
canHaveVisor: false,
canHaveMohawk: false,
drawFeatures: (p) => {
const { px, pal, ox, oy, tier, ko, idle, t, bounce,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY, vBounce, ll, rl, legW } = p
// Cowboy hat (wide brim, tall crown)
if (!ko) {
const hatY = hy - 1
// Wide brim
for (let hbx = hx - 4; hbx < hx + hw + 4; hbx++) {
px(hbx, hatY, '#8B6914', ox, oy)
px(hbx, hatY + 1, '#7a5c12', ox, oy)
}
// Crown (tall rectangle)
for (let hcy = hatY - 4; hcy < hatY; hcy++) {
for (let hcx = hx + 1; hcx < hx + hw - 1; hcx++) {
px(hcx, hcy, '#8B6914', ox, oy)
}
}
// Hat band
for (let hcx = hx + 1; hcx < hx + hw - 1; hcx++) {
px(hcx, hatY - 1, '#cc8833', ox, oy)
}
// Dent in top
px(cx + hOff, hatY - 4, '#7a5c12', ox, oy)
}
// Bandana (around neck)
if (!ko) {
const bandY = by
for (let bx2 = bx; bx2 < bx + bw; bx2++) {
px(bx2, bandY, '#cc3333', ox, oy)
}
// Hanging triangle
px(cx + hOff, bandY + 1, '#cc3333', ox, oy)
px(cx + hOff - 1, bandY + 1, '#aa2222', ox, oy)
px(cx + hOff + 1, bandY + 1, '#aa2222', ox, oy)
px(cx + hOff, bandY + 2, '#882222', ox, oy)
}
// Boots with spurs
if (!ko && !p.knockback) {
const bootY = feetY + vBounce + globalY
// Left boot
px(ll - 1, bootY, '#663311', ox, oy)
px(ll + legW, bootY, '#663311', ox, oy)
px(ll + legW + 1, bootY + 1, '#ffd700', ox, oy) // spur
// Right boot
px(rl - 1, bootY, '#663311', ox, oy)
px(rl + legW, bootY, '#663311', ox, oy)
px(rl + legW + 1, bootY + 1, '#ffd700', ox, oy) // spur
}
// Belt buckle (big oval)
const beltY = by + bh - 2
px(cx + hOff - 1, beltY, '#ffd700', ox, oy)
px(cx + hOff, beltY, '#ffee44', ox, oy)
px(cx + hOff + 1, beltY, '#ffd700', ox, oy)
px(cx + hOff, beltY - 1, '#ffd700', ox, oy)
},
}
@@ -0,0 +1,43 @@
import type { Archetype } from '../constants'
export const cyborg: Archetype = {
name: 'cyborg',
weight: 0.14,
drawFeatures: (p) => {
const { px, fill, pal, ox, oy, t, atk, special, ko, knockback,
bx, by, bw, bh, hx, hw, hy, hh } = p
// Half-human half-robot: left side robot, right side organic
// Robot half -- metal plating on left arm/body
for (let iy = by; iy < by + bh; iy++) {
px(bx, iy, '#888888', ox, oy)
px(bx + 1, iy, '#666666', ox, oy)
}
// Exposed wiring
if (!ko) {
px(bx + 2, by + 2, '#ff2200', ox, oy)
px(bx + 2, by + 4, '#00ff44', ox, oy)
px(bx + 2, by + 6, '#0088ff', ox, oy)
}
// Robot eye (left eye is mechanical)
const cyEyeY = hy + Math.floor(hh * 0.35)
if (!ko && !knockback) {
const cyEyeX = hx + Math.floor(hw * 0.2)
px(cyEyeX - 1, cyEyeY - 1, '#444444', ox, oy)
px(cyEyeX + 1, cyEyeY - 1, '#444444', ox, oy)
px(cyEyeX, cyEyeY, '#ff0000', ox, oy)
// Glowing scan line
if (p.frame % 2 === 0) px(cyEyeX - 1, cyEyeY, '#ff0000', ox, oy)
}
// Metal jaw plate
const jawY = hy + Math.floor(hh * 0.6)
for (let jx = hx; jx < hx + Math.floor(hw / 2); jx++) {
px(jx, jawY, '#777777', ox, oy)
}
// Sparking joint
if ((atk || special) && t > 0.3) {
px(bx + Math.floor(bw / 2), by - 1, '#ffff00', ox, oy)
px(bx + Math.floor(bw / 2) + 1, by - 2, '#ffffff', ox, oy)
}
},
}
@@ -0,0 +1,65 @@
import type { Archetype } from '../constants'
export const dinosaur: Archetype = {
name: 'dinosaur',
weight: 0.03,
dimensionOverrides: (tier) => ({
bw: 13 + tier * 2,
bh: 9 + tier,
armW: 2,
armH: 3 + Math.floor(tier * 0.5),
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko, knockback,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY } = p
// Spiky back (row of spines along top of body)
if (!ko) {
const spineCount = 4 + tier
for (let s = 0; s < spineCount; s++) {
const sx = bx + 1 + Math.floor(s * (bw - 2) / (spineCount - 1))
px(sx, by - 1, pal.acc, ox, oy)
px(sx, by - 2, pal.accDark, ox, oy)
if (s % 2 === 0) px(sx, by - 3, pal.accDark, ox, oy)
}
}
// Big jaw on head (wider lower face)
const jawY = hy + Math.floor(hh * 0.55)
for (let jx = hx; jx < hx + hw; jx++) {
px(jx, jawY, pal.dark, ox, oy)
}
// Teeth (jagged)
for (let tx = hx + 1; tx < hx + hw - 1; tx += 2) {
px(tx, jawY + 1, '#eeeeee', ox, oy)
}
// Tiny arms (T-rex style — already small from dimension override)
// Just add claws at tips
if (!ko && !knockback) {
px(p.armLx - 1, p.armAttach + p.armH, '#cccc88', ox, oy)
px(p.armRx + p.armW, p.armAttach + p.armH, '#cccc88', ox, oy)
}
// Thick tail
if (!knockback) {
const tailDir = -1
for (let tt = 1; tt <= 5 + tier; tt++) {
const tailW = Math.max(1, 3 - Math.floor(tt / 3))
for (let tw = 0; tw < tailW; tw++) {
px(cx + hOff + tailDir * (Math.floor(bw / 2) + tt),
feetY - Math.floor(tt * 0.7) + tw + globalY, pal.body, ox, oy)
}
}
// Tail tip
px(cx + hOff + tailDir * (Math.floor(bw / 2) + 6 + tier),
feetY - Math.floor((5 + tier) * 0.7) + globalY, pal.dark, ox, oy)
}
// Nostril on snout
if (!ko) {
const nY = hy + Math.floor(hh * 0.45)
px(hx + hw - 2, nY, '#222222', ox, oy)
}
},
}
@@ -0,0 +1,56 @@
import type { Archetype } from '../constants'
export const dog: Archetype = {
name: 'dog',
weight: 0.04,
drawFeatures: (p) => {
const { px, box, pal, ox, oy, t, tier, idle, ko, knockback, hit, atk, win,
cx, hOff, hx, hw, hy, hh, by, bh, bw, globalY, vBounce, feetY, bounce } = p
if (ko) return
// Floppy ears
const earDrop = idle ? Math.abs(bounce) + 2 : hit ? 3 : 1
px(hx - 1, hy + 1, pal.body, ox, oy)
px(hx - 2, hy + 2, pal.body, ox, oy)
px(hx - 2, hy + 2 + earDrop, pal.dark, ox, oy)
px(hx - 3, hy + 3 + earDrop, pal.dark, ox, oy)
px(hx + hw, hy + 1, pal.body, ox, oy)
px(hx + hw + 1, hy + 2, pal.body, ox, oy)
px(hx + hw + 1, hy + 2 + earDrop, pal.dark, ox, oy)
px(hx + hw + 2, hy + 3 + earDrop, pal.dark, ox, oy)
// Snout with nose
const mY = hy + Math.floor(hh * 0.55)
px(cx + hOff + 2, mY, pal.skin, ox, oy)
px(cx + hOff + 3, mY, pal.skin, ox, oy)
px(cx + hOff + 3, mY + 1, pal.skinDark, ox, oy)
px(cx + hOff + 4, mY, '#222222', ox, oy) // nose
// Tongue (sticks out when idle or winning)
if (idle || win) {
const tongueY = mY + 2 + Math.round(Math.sin(t * Math.PI * 2) * 0.5)
px(cx + hOff + 2, tongueY, '#ff6688', ox, oy)
px(cx + hOff + 3, tongueY, '#ff6688', ox, oy)
px(cx + hOff + 2, tongueY + 1, '#ee5577', ox, oy)
}
// Wagging tail
if (!knockback) {
const wagOffset = idle || win ? Math.round(Math.sin(t * Math.PI * 4) * 3) : 0
const tailDir = -1
for (let tt = 1; tt <= 4; tt++) {
px(cx + hOff + tailDir * (Math.floor(bw / 2) + tt), feetY - tt + globalY + wagOffset, pal.body, ox, oy)
}
px(cx + hOff + tailDir * (Math.floor(bw / 2) + 5), feetY - 5 + globalY + wagOffset, pal.dark, ox, oy)
}
// Collar
const collarY = by + bh - 3
for (let cx2 = cx + hOff - Math.floor(bw / 2); cx2 < cx + hOff + Math.floor(bw / 2); cx2++) {
px(cx2, collarY, '#cc2222', ox, oy)
}
// Tag
px(cx + hOff, collarY + 1, '#ffd700', ox, oy)
},
}
@@ -0,0 +1,80 @@
import type { Archetype } from '../constants'
export const frog: Archetype = {
name: 'frog',
weight: 0.03,
dimensionOverrides: (tier) => ({
legH: 4 + Math.floor(tier * 0.5),
legW: 4 + Math.floor(tier * 0.5),
}),
drawFeatures: (p) => {
const { px, fill, pal, ox, oy, t, tier, idle, ko, atk, special, knockback,
hx, hy, hh, hw, cx, hOff, bx, by, bw, bh, feetY, globalY, vBounce, ll, rl, legW } = p
// Bulging eyes (extend above head)
if (!ko) {
const eyeY = hy - 2
const leX = hx + 1
const reX = hx + hw - 4
// Eye bulges (circles above head)
for (let ey = eyeY - 2; ey <= eyeY; ey++) {
px(leX, ey, '#88cc44', ox, oy)
px(leX + 1, ey, '#88cc44', ox, oy)
px(leX + 2, ey, '#88cc44', ox, oy)
px(reX, ey, '#88cc44', ox, oy)
px(reX + 1, ey, '#88cc44', ox, oy)
px(reX + 2, ey, '#88cc44', ox, oy)
}
// Pupils
px(leX + 1, eyeY, '#000000', ox, oy)
px(reX + 1, eyeY, '#000000', ox, oy)
// Highlight
px(leX, eyeY - 2, '#aaffaa', ox, oy)
px(reX + 2, eyeY - 2, '#aaffaa', ox, oy)
}
// Wide mouth (extends beyond head width)
if (!ko) {
const mY = hy + Math.floor(hh * 0.7)
for (let mx = hx - 1; mx <= hx + hw; mx++) {
px(mx, mY, '#226622', ox, oy)
}
}
// Long tongue (on attack)
if (atk || special) {
const tongueY = hy + Math.floor(hh * 0.7)
const tongueLen = Math.round(Math.sin(t * Math.PI) * (15 + tier * 3))
const dir = p.armRx > cx ? 1 : -1
for (let tl = 0; tl < tongueLen; tl++) {
px(hx + (dir > 0 ? hw : 0) + dir * tl, tongueY, '#ff4466', ox, oy)
}
// Tongue tip (wider)
if (tongueLen > 3) {
px(hx + (dir > 0 ? hw : 0) + dir * tongueLen, tongueY - 1, '#ff4466', ox, oy)
px(hx + (dir > 0 ? hw : 0) + dir * tongueLen, tongueY + 1, '#ff4466', ox, oy)
}
}
// Webbed feet
if (!ko && !knockback) {
const footY = feetY + vBounce + globalY
// Extra-wide toe spread
px(ll - 2, footY + 1, '#44aa22', ox, oy)
px(ll + legW + 1, footY + 1, '#44aa22', ox, oy)
px(rl - 2, footY + 1, '#44aa22', ox, oy)
px(rl + legW + 1, footY + 1, '#44aa22', ox, oy)
}
// Spotted belly
const spots = [
[bx + 2, by + 2],
[bx + bw - 3, by + 3],
[bx + Math.floor(bw / 2), by + bh - 3],
]
for (const [sx, sy] of spots) {
px(sx, sy, '#aaee66', ox, oy)
px(sx + 1, sy, '#aaee66', ox, oy)
}
},
}
@@ -0,0 +1,64 @@
import type { Archetype } from '../constants'
export const ghost: Archetype = {
name: 'ghost',
weight: 0.03,
canHaveMohawk: false,
canHaveHorns: false,
dimensionOverrides: () => ({
legH: 2,
legW: 2,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, idle, ko,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY } = p
// Wavy bottom (no real legs -- ghostly wisp)
if (!ko) {
const waveY = by + bh
for (let wx = bx; wx < bx + bw; wx++) {
const wave = Math.round(Math.sin(t * Math.PI * 3 + wx * 0.4) * 2)
px(wx, waveY + wave, pal.light, ox, oy)
px(wx, waveY + wave + 1, pal.light, ox, oy)
px(wx, waveY + wave + 2, pal.body, ox, oy)
}
}
// Semi-transparent overlay effect (lighter body patches)
for (let iy = by + 1; iy < by + bh; iy += 2) {
for (let ix = bx + 1; ix < bx + bw - 1; ix += 3) {
px(ix, iy, pal.light, ox, oy)
}
}
// Glowing eyes (large, circular)
if (!ko) {
const eyeY = hy + Math.floor(hh * 0.35)
const leX = hx + Math.floor(hw * 0.2)
const reX = hx + Math.floor(hw * 0.6)
// Large glowing circles
px(leX, eyeY, '#44ffff', ox, oy)
px(leX + 1, eyeY, '#44ffff', ox, oy)
px(leX, eyeY + 1, '#22cccc', ox, oy)
px(leX + 1, eyeY + 1, '#22cccc', ox, oy)
px(reX, eyeY, '#44ffff', ox, oy)
px(reX + 1, eyeY, '#44ffff', ox, oy)
px(reX, eyeY + 1, '#22cccc', ox, oy)
px(reX + 1, eyeY + 1, '#22cccc', ox, oy)
// Glow halo
const glowPulse = idle ? Math.sin(t * Math.PI * 4) * 0.5 : 0
if (glowPulse > 0) {
px(leX - 1, eyeY, '#22ffff', ox, oy)
px(reX + 2, eyeY, '#22ffff', ox, oy)
}
}
// Open mouth (always slightly open, spooky)
if (!ko) {
const mY = hy + Math.floor(hh * 0.65)
px(cx + hOff, mY, '#111122', ox, oy)
px(cx + hOff - 1, mY, '#111122', ox, oy)
px(cx + hOff + 1, mY, '#111122', ox, oy)
}
},
}
@@ -0,0 +1,68 @@
import type { Archetype } from '../constants'
import { standard } from './standard'
import { lobster } from './lobster'
import { sheep } from './sheep'
import { cyborg } from './cyborg'
import { blob } from './blob'
import { tank } from './tank'
import { dog } from './dog'
import { cat } from './cat'
import { cactus } from './cactus'
import { pizza } from './pizza'
import { mushroom } from './mushroom'
import { shark } from './shark'
import { penguin } from './penguin'
import { octopus } from './octopus'
import { skeleton } from './skeleton'
import { ghost } from './ghost'
import { alien } from './alien'
import { dinosaur } from './dinosaur'
import { pirate } from './pirate'
import { ninja } from './ninja'
import { cowboy } from './cowboy'
import { wizard } from './wizard'
import { bee } from './bee'
import { frog } from './frog'
import { snail } from './snail'
// Batch 4: Robots & Tech
import { robot, android, droneBug, toaster, tvHead, calculator, satellite, mech, ledCube, circuit, antennaBug, microwave, cyberdog, robocat, ufoBot } from './batch_robots'
// Batch 5: Mythology & Fantasy
import { minotaur, unicorn, phoenix, dragonArch, mermaid, griffin, cyclops, gargoyle, golem, vampire, werewolf, zombie, witch, demon } from './batch_mythology'
// Batch 6: Jobs & Warriors
import { chef, firefighter, astronautArch, clown, detective, nurse, lumberjack, scientist, wrestler, boxer, gladiator, samuraiArch, vikingArch, knight } from './batch_jobs'
// Batch 7: More Animals
import { elephant, giraffe, hippo, lion, monkey, parrot, raccoon, snakeArch, turtle, whale, crocodile, flamingo, hedgehog, panda, hamster } from './batch_animals2'
// Batch 8: Silly & Objects
import { sockPuppet, trafficCone, toiletMan, potato, cloudMan, rockMan, balloonMan, trashCan, rubberDuckArch, snowman, scarecrow, jackOLantern, gardenGnome, lampPost, broomMan } from './batch_silly'
export const archetypes: Archetype[] = [
// Original 6
standard, lobster, sheep, cyborg, blob, tank,
// Batch 1: animals & food
dog, cat, cactus, pizza, mushroom, shark, penguin, octopus,
// Batch 2: fantasy & themed
skeleton, ghost, alien, dinosaur, pirate, ninja, cowboy, wizard,
// Batch 3: critters
bee, frog, snail,
// Batch 4: robots & tech
robot, android, droneBug, toaster, tvHead, calculator, satellite, mech, ledCube, circuit, antennaBug, microwave, cyberdog, robocat, ufoBot,
// Batch 5: mythology & fantasy
minotaur, unicorn, phoenix, dragonArch, mermaid, griffin, cyclops, gargoyle, golem, vampire, werewolf, zombie, witch, demon,
// Batch 6: jobs & warriors
chef, firefighter, astronautArch, clown, detective, nurse, lumberjack, scientist, wrestler, boxer, gladiator, samuraiArch, vikingArch, knight,
// Batch 7: more animals
elephant, giraffe, hippo, lion, monkey, parrot, raccoon, snakeArch, turtle, whale, crocodile, flamingo, hedgehog, panda, hamster,
// Batch 8: silly & objects
sockPuppet, trafficCone, toiletMan, potato, cloudMan, rockMan, balloonMan, trashCan, rubberDuckArch, snowman, scarecrow, jackOLantern, gardenGnome, lampPost, broomMan,
]
export function rollArchetype(roll: number): Archetype {
// Normalize: sum all weights, then compare proportionally
const totalWeight = archetypes.reduce((sum, a) => sum + a.weight, 0)
let cumulative = 0
for (const arch of archetypes) {
cumulative += arch.weight / totalWeight
if (roll < cumulative) return arch
}
return archetypes[0]
}
@@ -0,0 +1,58 @@
import type { Archetype } from '../constants'
export const lobster: Archetype = {
name: 'lobster',
weight: 0.15,
canHaveHorns: false,
dimensionOverrides: (tier) => ({
hw: 8 + tier,
hh: 7 + tier,
armW: 4,
armH: 4 + tier,
}),
drawFeatures: (p) => {
const { px, fill, pal, ox, oy, t, tier, idle, atk, kick, special, knockback, ko,
cx, hOff, armLx, armRx, armAttach, armH, armW, globalY, vBounce,
hx, hw, hy, bx, bw, by, bh, feetY } = p
if (ko) return
// Lobster claws replace fists -- big pincers on each arm
const clawSize = 4 + Math.floor(tier * 0.5)
const clawOpen = (atk || kick || special) ? 2 : 0
// Left claw
const lcx = armLx - 2 + hOff
const lcy = armAttach + armH + globalY + vBounce
px(lcx, lcy, pal.acc, ox, oy); px(lcx + 1, lcy, pal.acc, ox, oy)
px(lcx - 1, lcy - clawOpen, pal.acc, ox, oy); px(lcx + 2, lcy - clawOpen, pal.acc, ox, oy)
px(lcx - 1, lcy + 1 + clawOpen, pal.acc, ox, oy); px(lcx + 2, lcy + 1 + clawOpen, pal.acc, ox, oy)
for (let c = 0; c < clawSize; c++) { px(lcx - 2 - c, lcy - clawOpen, pal.accDark, ox, oy); px(lcx - 2 - c, lcy + 1 + clawOpen, pal.accDark, ox, oy) }
// Right claw
const rcx = armRx + armW + hOff
const rcy = armAttach + armH + globalY + vBounce - (atk ? Math.round(Math.sin(t * Math.PI) * 6) : 0)
px(rcx, rcy, pal.acc, ox, oy); px(rcx + 1, rcy, pal.acc, ox, oy)
px(rcx - 1, rcy - clawOpen, pal.acc, ox, oy); px(rcx + 2, rcy - clawOpen, pal.acc, ox, oy)
px(rcx - 1, rcy + 1 + clawOpen, pal.acc, ox, oy); px(rcx + 2, rcy + 1 + clawOpen, pal.acc, ox, oy)
for (let c = 0; c < clawSize; c++) { px(rcx + 3 + c, rcy - clawOpen, pal.accDark, ox, oy); px(rcx + 3 + c, rcy + 1 + clawOpen, pal.accDark, ox, oy) }
// Antennae -- long feelers from head
const antL = 5 + tier
for (let a = 1; a <= antL; a++) {
const wobble = idle ? Math.round(Math.sin(t * Math.PI * 2 + a * 0.5) * 1) : 0
px(hx + 2 + hOff, hy - a + wobble, pal.acc, ox, oy)
px(hx + hw - 3 + hOff, hy - a - wobble, pal.acc, ox, oy)
}
px(hx + 2 + hOff, hy - antL - 1, pal.accLight, ox, oy)
px(hx + hw - 3 + hOff, hy - antL - 1, pal.accLight, ox, oy)
// Segmented body lines
for (let s = by + 2; s < by + bh; s += 2) {
for (let sx = bx; sx < bx + bw; sx++) px(sx, s, pal.accDark, ox, oy)
}
// Tail
if (!knockback) {
const tailDir = -1 // behind
for (let tt = 1; tt <= 3; tt++) {
px(cx + hOff + tailDir * (Math.floor(bw / 2) + tt), feetY - tt + globalY, pal.accDark, ox, oy)
px(cx + hOff + tailDir * (Math.floor(bw / 2) + tt), feetY - tt + 1 + globalY, pal.accDark, ox, oy)
}
}
},
}
@@ -0,0 +1,52 @@
import type { Archetype } from '../constants'
export const mushroom: Archetype = {
name: 'mushroom',
weight: 0.03,
canHaveMohawk: false,
canHaveHorns: false,
dimensionOverrides: (tier) => ({
hh: 12 + tier,
legH: 4,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko,
hx, hy, hw, hh, cx, hOff, by, bh, globalY } = p
// Dome cap on head (wider than head, rounded)
const capW = hw + 6
const capH = Math.floor(hh * 0.6)
const capX = hx - 3
const capY = hy - capH + 2
for (let iy = 0; iy < capH; iy++) {
const rowW = capW - Math.floor(iy * iy / capH)
const rx = capX + Math.floor((capW - rowW) / 2)
for (let ix = rx; ix < rx + rowW; ix++) {
px(ix, capY + iy, pal.acc, ox, oy)
}
}
// Spots on cap
if (!ko) {
const spotPositions = [
[capX + 2, capY + 2],
[capX + capW - 3, capY + 3],
[capX + Math.floor(capW / 2), capY + 1],
[capX + Math.floor(capW / 3), capY + capH - 2],
]
for (const [sx, sy] of spotPositions) {
px(sx, sy, pal.accLight, ox, oy)
px(sx + 1, sy, pal.accLight, ox, oy)
}
}
// Spore particles (floating up when idle)
if (idle) {
for (let s = 0; s < 3; s++) {
const sporeX = cx + hOff + Math.round(Math.sin(t * Math.PI * 2 + s * 2) * 8)
const sporeY = by + bh + 2 - Math.round(t * 4 + s * 3) % 10
px(sporeX, sporeY + globalY, pal.accLight, ox, oy)
}
}
},
}
@@ -0,0 +1,68 @@
import type { Archetype } from '../constants'
export const ninja: Archetype = {
name: 'ninja',
weight: 0.03,
canHaveVisor: false,
drawFeatures: (p) => {
const { px, fill, pal, ox, oy, tier, ko, idle, atk, special, t,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
// Mask covering lower face
if (!ko) {
const maskY = hy + Math.floor(hh * 0.5)
for (let my = maskY; my < hy + hh; my++) {
for (let mx = hx + 1; mx < hx + hw - 1; mx++) {
px(mx, my, '#222222', ox, oy)
}
}
}
// Headband (dark with knot trailing)
if (!ko) {
const bandY = hy + 2
for (let bx2 = hx; bx2 < hx + hw; bx2++) {
px(bx2, bandY, '#cc2222', ox, oy)
}
// Trailing tails
const trail = idle ? Math.round(Math.sin(t * Math.PI * 2) * 1) : 0
px(hx - 1, bandY + 1 + trail, '#cc2222', ox, oy)
px(hx - 2, bandY + 2 + trail, '#aa1111', ox, oy)
px(hx - 3, bandY + 3 + trail, '#881111', ox, oy)
}
// Throwing star on back
if (!ko) {
const starX = bx + bw - 2
const starY = by + 2
px(starX, starY, '#aaaaaa', ox, oy)
px(starX - 1, starY - 1, '#888888', ox, oy)
px(starX + 1, starY - 1, '#888888', ox, oy)
px(starX - 1, starY + 1, '#888888', ox, oy)
px(starX + 1, starY + 1, '#888888', ox, oy)
}
// Dark body wraps
for (let wy = by + 1; wy < by + bh; wy += 3) {
for (let wx = bx; wx < bx + bw; wx++) {
px(wx, wy, '#1a1a2a', ox, oy)
}
}
// Visible eyes only (narrow, intense)
if (!ko) {
const eyeY = hy + Math.floor(hh * 0.35)
const leX = hx + Math.floor(hw * 0.2)
const reX = hx + Math.floor(hw * 0.6)
// Narrow slits
px(leX, eyeY + 1, '#ffffff', ox, oy)
px(leX + 1, eyeY + 1, '#ffffff', ox, oy)
px(reX, eyeY + 1, '#ffffff', ox, oy)
px(reX + 1, eyeY + 1, '#ffffff', ox, oy)
if (atk || special) {
px(leX, eyeY + 1, '#ff4444', ox, oy)
px(reX + 1, eyeY + 1, '#ff4444', ox, oy)
}
}
},
}
@@ -0,0 +1,59 @@
import type { Archetype } from '../constants'
export const octopus: Archetype = {
name: 'octopus',
weight: 0.03,
canHaveMohawk: false,
dimensionOverrides: (tier) => ({
hw: 13 + tier,
hh: 10 + tier,
legH: 3,
armW: 2,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko, knockback,
cx, hOff, bx, by, bw, bh, feetY, globalY, vBounce } = p
// Tentacle legs (4 visible, wavy)
if (!ko) {
for (let leg = 0; leg < 4; leg++) {
const baseX = bx + Math.floor(leg * bw / 3) + hOff
const baseY = feetY + globalY
for (let seg = 0; seg < 6; seg++) {
const wave = idle ? Math.round(Math.sin(t * Math.PI * 3 + leg * 1.5 + seg * 0.8) * 2) : 0
const tx = baseX + wave
const ty = baseY + seg
const color = seg < 4 ? pal.body : pal.dark
px(tx, ty, color, ox, oy)
px(tx + 1, ty, color, ox, oy)
}
// Suction cups
if (idle) {
px(bx + Math.floor(leg * bw / 3) + hOff + 1, feetY + 3 + globalY, pal.accLight, ox, oy)
}
}
}
// Tentacle arms (replace normal arms with wavy ones)
if (!ko && !knockback) {
for (let arm = 0; arm < 2; arm++) {
const armDir = arm === 0 ? -1 : 1
const armBase = cx + hOff + armDir * Math.floor(bw / 2 + 2)
for (let seg = 0; seg < 5 + tier; seg++) {
const wave = idle ? Math.round(Math.sin(t * Math.PI * 2 + arm * Math.PI + seg * 0.6) * 2) : 0
px(armBase + armDir * seg, by + 3 + wave + globalY, pal.body, ox, oy)
// Suction cup
if (seg % 2 === 1) px(armBase + armDir * seg, by + 4 + wave + globalY, pal.accLight, ox, oy)
}
}
}
// Large head already provided by base, but add dome bump
const domeX = cx + hOff
const domeY = p.hy - 2
px(domeX, domeY, pal.body, ox, oy)
px(domeX - 1, domeY, pal.body, ox, oy)
px(domeX + 1, domeY, pal.body, ox, oy)
px(domeX, domeY - 1, pal.dark, ox, oy)
},
}
@@ -0,0 +1,58 @@
import type { Archetype } from '../constants'
export const penguin: Archetype = {
name: 'penguin',
weight: 0.03,
dimensionOverrides: (tier) => ({
armW: 4,
armH: 6 + tier,
}),
drawFeatures: (p) => {
const { px, fill, pal, ox, oy, tier, ko, idle,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, bounce } = p
// White belly (tuxedo front)
const bellyX = bx + 2
const bellyW = bw - 4
const bellyY = by + 2
const bellyH = bh - 3
for (let iy = bellyY; iy < bellyY + bellyH; iy++) {
for (let ix = bellyX; ix < bellyX + bellyW; ix++) {
px(ix, iy, '#eeeeee', ox, oy)
}
}
// Orange beak
if (!ko) {
const beakY = hy + Math.floor(hh * 0.5)
const beakX = cx + hOff
px(beakX, beakY, '#ff8800', ox, oy)
px(beakX + 1, beakY, '#ff8800', ox, oy)
px(beakX + 2, beakY, '#ff6600', ox, oy)
px(beakX, beakY + 1, '#ff6600', ox, oy)
}
// Flipper-like arms (wider, more rounded at tips)
// Flippers already drawn by base as arms, just add tips
if (!ko) {
const armAttach = by + 2 + (idle ? bounce : 0)
// Flipper tips on left
px(bx - 5 + hOff, armAttach + p.armH, pal.dark, ox, oy)
px(bx - 5 + hOff, armAttach + p.armH + 1, pal.dark, ox, oy)
// Flipper tips on right
px(bx + bw + 4 + hOff, armAttach + p.armH, pal.dark, ox, oy)
px(bx + bw + 4 + hOff, armAttach + p.armH + 1, pal.dark, ox, oy)
}
// Orange feet
if (!ko) {
const feetY = p.feetY + p.vBounce + p.globalY
px(p.ll, feetY, '#ff8800', ox, oy)
px(p.ll + 1, feetY, '#ff8800', ox, oy)
px(p.ll - 1, feetY, '#ff6600', ox, oy)
px(p.rl, feetY, '#ff8800', ox, oy)
px(p.rl + 1, feetY, '#ff8800', ox, oy)
px(p.rl + 2, feetY, '#ff6600', ox, oy)
}
},
}
@@ -0,0 +1,68 @@
import type { Archetype } from '../constants'
export const pirate: Archetype = {
name: 'pirate',
weight: 0.03,
canHaveVisor: false,
drawFeatures: (p) => {
const { px, fill, pal, ox, oy, tier, ko, knockback,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY, vBounce, ll, rl, legW } = p
// Pirate hat (wide brim)
if (!ko) {
const hatY = hy - 2
// Brim
for (let hbx = hx - 2; hbx < hx + hw + 2; hbx++) {
px(hbx, hatY, '#222222', ox, oy)
}
// Crown of hat
for (let hcy = hatY - 3; hcy < hatY; hcy++) {
for (let hcx = hx + 1; hcx < hx + hw - 1; hcx++) {
px(hcx, hcy, '#222222', ox, oy)
}
}
// Skull emblem
px(cx + hOff, hatY - 2, '#ffffff', ox, oy)
px(cx + hOff - 1, hatY - 1, '#ffffff', ox, oy)
px(cx + hOff + 1, hatY - 1, '#ffffff', ox, oy)
}
// Eye patch (covers one eye)
if (!ko) {
const eyeY = hy + Math.floor(hh * 0.35)
const patchX = hx + Math.floor(hw * 0.2)
fill(patchX - 1, eyeY - 1, 4, 4, '#111111', ox, oy)
// Strap
for (let sx = patchX + 3; sx < hx + hw; sx++) {
px(sx, eyeY - 1, '#222222', ox, oy)
}
}
// Peg leg (replaces one leg)
if (!ko && !knockback) {
const pegX = rl
const pegY = p.legsTop + p.vBounce + p.globalY
for (let py = pegY; py < feetY + vBounce + globalY + 2; py++) {
px(pegX + 1, py, '#aa8844', ox, oy)
}
}
// Hook hand (replaces one arm end)
if (!ko && !knockback) {
const hookX = p.armRx + p.armW + hOff
const hookY = p.armAttach + p.armH + p.globalY
px(hookX, hookY, '#aaaaaa', ox, oy)
px(hookX + 1, hookY + 1, '#888888', ox, oy)
px(hookX, hookY + 1, '#888888', ox, oy)
px(hookX - 1, hookY + 1, '#888888', ox, oy)
}
// Belt with buckle
const beltY = by + bh - 2
for (let beltX = bx; beltX < bx + bw; beltX++) {
px(beltX, beltY, '#8B4513', ox, oy)
}
px(cx + hOff, beltY, '#ffd700', ox, oy)
px(cx + hOff + 1, beltY, '#ffd700', ox, oy)
},
}
@@ -0,0 +1,52 @@
import type { Archetype } from '../constants'
export const pizza: Archetype = {
name: 'pizza',
weight: 0.03,
canHaveMohawk: false,
canHaveHorns: false,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff } = p
// Triangular body shape hint -- darker edges converging up
for (let iy = by; iy < by + bh; iy++) {
const progress = (iy - by) / bh
const indent = Math.floor(progress * 3)
px(bx + indent, iy, '#e8a030', ox, oy) // crust color
px(bx + bw - 1 - indent, iy, '#e8a030', ox, oy)
}
// Cheese drip effect
if (!ko) {
const dripCount = 3 + Math.floor(tier * 0.5)
for (let d = 0; d < dripCount; d++) {
const dx = bx + 2 + Math.floor(d * (bw - 4) / (dripCount - 1))
const dLen = 2 + Math.floor(Math.sin(t * Math.PI * 2 + d) * 1.5)
for (let dy = 0; dy < Math.max(1, dLen); dy++) {
px(dx, by + bh + dy, '#ffdd44', ox, oy)
}
}
}
// Pepperoni spots
const spots = [
[bx + 3, by + 2],
[bx + bw - 4, by + 3],
[bx + Math.floor(bw / 2), by + Math.floor(bh / 2)],
[bx + 2, by + bh - 3],
[bx + bw - 3, by + bh - 2],
]
for (const [sx, sy] of spots) {
px(sx, sy, '#cc3322', ox, oy)
px(sx + 1, sy, '#cc3322', ox, oy)
px(sx, sy + 1, '#aa2211', ox, oy)
}
// Crusty edges on head
const crustY = hy + hh - 1
for (let cx2 = hx; cx2 < hx + hw; cx2++) {
px(cx2, crustY, '#c88020', ox, oy)
}
},
}
@@ -0,0 +1,62 @@
import type { Archetype } from '../constants'
export const shark: Archetype = {
name: 'shark',
weight: 0.03,
dimensionOverrides: (tier) => ({
bw: 12 + tier * 2,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, tier, ko, knockback, atk, idle,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY, t } = p
// Dorsal fin on back
if (!ko && !knockback) {
const finX = cx + hOff
const finY = by - 2
px(finX, finY, pal.dark, ox, oy)
px(finX, finY - 1, pal.dark, ox, oy)
px(finX, finY - 2, pal.body, ox, oy)
px(finX - 1, finY, pal.dark, ox, oy)
px(finX + 1, finY, pal.dark, ox, oy)
px(finX, finY - 3, pal.body, ox, oy)
}
// Teeth row on head (visible grin)
if (!ko) {
const teethY = hy + Math.floor(hh * 0.7)
for (let tx = hx + 1; tx < hx + hw - 1; tx += 2) {
px(tx, teethY, '#ffffff', ox, oy)
px(tx, teethY + 1, '#eeeeee', ox, oy)
}
}
// Tail fin
if (!knockback) {
const tailDir = -1
const tailX = cx + hOff + tailDir * (Math.floor(bw / 2) + 1)
const tailY = by + Math.floor(bh / 2) + globalY
px(tailX, tailY, pal.dark, ox, oy)
px(tailX + tailDir, tailY - 2, pal.body, ox, oy)
px(tailX + tailDir, tailY + 2, pal.body, ox, oy)
px(tailX + tailDir * 2, tailY - 3, pal.dark, ox, oy)
px(tailX + tailDir * 2, tailY + 3, pal.dark, ox, oy)
}
// Gills on body (3 slashes)
for (let g = 0; g < 3; g++) {
px(bx + 2, by + 2 + g * 2, pal.dark, ox, oy)
px(bx + 3, by + 2 + g * 2, pal.dark, ox, oy)
}
// Beady black eyes
if (!ko) {
const eyeY = hy + Math.floor(hh * 0.3)
px(hx + 2, eyeY, '#000000', ox, oy)
px(hx + hw - 3, eyeY, '#000000', ox, oy)
// Tiny white reflection
px(hx + 2, eyeY, '#111111', ox, oy)
px(hx + hw - 3, eyeY, '#111111', ox, oy)
}
},
}
@@ -0,0 +1,45 @@
import type { Archetype } from '../constants'
export const sheep: Archetype = {
name: 'sheep',
weight: 0.13,
canHaveMohawk: false,
drawFeatures: (p) => {
const { px, box, pal, ox, oy, t, tier, idle, hit, ko, knockback, kick,
cx, hOff, by, bw, bh, hx, hw, hy, hh, globalY, vBounce, feetY, legW, ll, rl } = p
// Woolly body -- fluffy circles around torso
const woolColors = [pal.light, '#eeeeee', '#dddddd', pal.light]
for (let w = 0; w < 6 + tier; w++) {
const angle = w * Math.PI * 2 / (6 + tier) + (idle ? t * 0.2 : 0)
const wr = Math.floor(bw / 2) + 2
const wx = cx + hOff + Math.round(Math.cos(angle) * wr)
const wy = by + Math.floor(bh / 2) + Math.round(Math.sin(angle) * (bh / 2 - 1)) + globalY
const wc = woolColors[w % woolColors.length]
px(wx, wy, wc, ox, oy); px(wx + 1, wy, wc, ox, oy)
px(wx, wy + 1, wc, ox, oy)
}
// Fluffy head wool
for (let w = 0; w < 5; w++) {
const wa = w * Math.PI * 2 / 5
const wrx = Math.round(Math.cos(wa) * (hw / 2 + 1))
const wry = Math.round(Math.sin(wa) * (hh / 2))
px(hx + Math.floor(hw / 2) + wrx, hy + Math.floor(hh / 2) + wry - 2, '#eeeeee', ox, oy)
}
// Floppy ears
if (!ko) {
const earDrop = idle ? Math.abs(p.bounce) : hit ? 2 : 0
px(hx - 1, hy + 2 + earDrop, pal.skin, ox, oy)
px(hx - 2, hy + 3 + earDrop, pal.skin, ox, oy)
px(hx - 2, hy + 4 + earDrop, pal.skinDark, ox, oy)
px(hx + hw, hy + 2 + earDrop, pal.skin, ox, oy)
px(hx + hw + 1, hy + 3 + earDrop, pal.skin, ox, oy)
px(hx + hw + 1, hy + 4 + earDrop, pal.skinDark, ox, oy)
}
// Stubby hooves instead of feet
if (!ko && !knockback && !kick) {
box(ll - 1, feetY + vBounce + globalY, legW + 2, 2, '#333333', ox, oy)
box(rl - 1, feetY + vBounce + globalY, legW + 2, 2, '#333333', ox, oy)
}
},
}
@@ -0,0 +1,58 @@
import type { Archetype } from '../constants'
export const skeleton: Archetype = {
name: 'skeleton',
weight: 0.03,
drawFeatures: (p) => {
const { px, pal, ox, oy, ko,
bx, by, bw, bh, hx, hy, hh, hw } = p
// Visible ribs on body
for (let r = 0; r < 4; r++) {
const ribY = by + 2 + r * 2
const ribW = bw - 4 - r
const ribX = bx + 2 + Math.floor(r / 2)
for (let rx = ribX; rx < ribX + ribW; rx++) {
px(rx, ribY, '#ddddcc', ox, oy)
}
}
// Spine down center
for (let sy = by + 1; sy < by + bh - 1; sy++) {
px(bx + Math.floor(bw / 2), sy, '#ccccbb', ox, oy)
}
// Skull head shape (hollow eyes, jaw)
if (!ko) {
// Dark eye sockets
const eyeY = hy + Math.floor(hh * 0.3)
const leX = hx + Math.floor(hw * 0.2)
const reX = hx + Math.floor(hw * 0.6)
px(leX, eyeY, '#111111', ox, oy)
px(leX + 1, eyeY, '#111111', ox, oy)
px(leX, eyeY + 1, '#111111', ox, oy)
px(leX + 1, eyeY + 1, '#111111', ox, oy)
px(reX, eyeY, '#111111', ox, oy)
px(reX + 1, eyeY, '#111111', ox, oy)
px(reX, eyeY + 1, '#111111', ox, oy)
px(reX + 1, eyeY + 1, '#111111', ox, oy)
// Glowing dots in sockets
px(leX + 1, eyeY + 1, '#ff2222', ox, oy)
px(reX, eyeY + 1, '#ff2222', ox, oy)
}
// Nose hole
const noseY = hy + Math.floor(hh * 0.5)
px(hx + Math.floor(hw / 2), noseY, '#111111', ox, oy)
// Teeth on lower face
const jawY = hy + Math.floor(hh * 0.65)
for (let tx = hx + 2; tx < hx + hw - 2; tx += 2) {
px(tx, jawY, '#eeeeee', ox, oy)
}
// Bony limbs (lighter color on arms/legs)
px(p.armLx + 1, p.armAttach + 2, '#ddddcc', ox, oy)
px(p.armRx + 1, p.armAttach + 2, '#ddddcc', ox, oy)
},
}
@@ -0,0 +1,76 @@
import type { Archetype } from '../constants'
export const snail: Archetype = {
name: 'snail',
weight: 0.03,
canHaveMohawk: false,
canHaveHorns: false,
dimensionOverrides: () => ({
legH: 3,
legW: 5,
}),
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko, knockback,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, feetY, globalY, vBounce } = p
// Shell on back (spiral circle)
if (!ko) {
const shellX = bx + bw + 1
const shellY = by + Math.floor(bh / 2)
const shellR = Math.floor(bh / 2) + 2
// Outer shell
for (let a = 0; a < 16; a++) {
const angle = a * Math.PI * 2 / 16
const sx = shellX + Math.round(Math.cos(angle) * shellR)
const sy = shellY + Math.round(Math.sin(angle) * shellR)
px(sx, sy, pal.acc, ox, oy)
}
// Inner spiral
for (let a = 0; a < 12; a++) {
const angle = a * Math.PI * 2 / 12
const r2 = shellR * 0.6
const sx = shellX + Math.round(Math.cos(angle) * r2)
const sy = shellY + Math.round(Math.sin(angle) * r2)
px(sx, sy, pal.accDark, ox, oy)
}
// Center
px(shellX, shellY, pal.accLight, ox, oy)
px(shellX + 1, shellY, pal.accLight, ox, oy)
}
// Eye stalks (extend from top of head)
if (!ko) {
const stalkH = 4 + Math.floor(tier * 0.5)
const wobble = idle ? Math.round(Math.sin(t * Math.PI * 2) * 1) : 0
// Left stalk
for (let s = 0; s < stalkH; s++) {
px(hx + 2 + hOff, hy - 1 - s + wobble, pal.body, ox, oy)
}
px(hx + 2 + hOff, hy - 1 - stalkH + wobble, '#ffffff', ox, oy)
px(hx + 2 + hOff, hy - stalkH + wobble, '#000000', ox, oy)
// Right stalk
for (let s = 0; s < stalkH; s++) {
px(hx + hw - 3 + hOff, hy - 1 - s - wobble, pal.body, ox, oy)
}
px(hx + hw - 3 + hOff, hy - 1 - stalkH - wobble, '#ffffff', ox, oy)
px(hx + hw - 3 + hOff, hy - stalkH - wobble, '#000000', ox, oy)
}
// Slime trail (behind body, on ground)
if (!ko && !knockback) {
const slimeY = feetY + vBounce + globalY + 2
for (let sx = bx - 8; sx < bx; sx++) {
px(sx + hOff, slimeY, '#88cc88', ox, oy)
}
}
// Gooey body texture
if (idle) {
for (let g = 0; g < 3; g++) {
const gx = bx + 1 + g * Math.floor(bw / 3)
const gy = by + bh + Math.round(Math.sin(t * Math.PI * 2 + g) * 1)
px(gx, gy, pal.light, ox, oy)
}
}
},
}
@@ -0,0 +1,8 @@
import type { Archetype } from '../constants'
export const standard: Archetype = {
name: 'standard',
weight: 0.30,
canHaveVisor: true,
drawFeatures: () => {},
}
@@ -0,0 +1,48 @@
import type { Archetype } from '../constants'
export const tank: Archetype = {
name: 'tank',
weight: 0.14,
dimensionOverrides: (tier) => ({
bw: 14 + tier * 2,
bh: 6 + tier,
legH: 4 + tier,
legW: 4 + Math.floor(tier * 0.5),
}),
drawFeatures: (p) => {
const { px, fill, pal, ox, oy, tier, ko, knockback,
cx, hOff, bx, by, bw, bh, hx, hw, hy, hh, feetY, globalY, vBounce } = p
// Heavy armor plating -- double outline
for (let iy = by; iy < by + bh; iy++) {
px(bx - 1, iy, pal.dark, ox, oy)
px(bx + bw, iy, pal.dark, ox, oy)
}
// Rivets
px(bx, by + 1, '#888888', ox, oy); px(bx + bw - 1, by + 1, '#888888', ox, oy)
px(bx, by + bh - 2, '#888888', ox, oy); px(bx + bw - 1, by + bh - 2, '#888888', ox, oy)
// Thick neck (connects head to body more solidly)
const neckW = Math.floor(hw * 0.4)
for (let nx = cx - Math.floor(neckW / 2); nx < cx + Math.floor(neckW / 2); nx++) {
px(nx + hOff, by - 1 + globalY + vBounce, pal.body, ox, oy)
px(nx + hOff, by - 2 + globalY + vBounce, pal.dark, ox, oy)
}
// Treads instead of feet
if (!ko && !knockback) {
const treadY = feetY + vBounce + globalY
for (let tx = cx - Math.floor(bw / 2) - 1; tx <= cx + Math.floor(bw / 2) + 1; tx++) {
px(tx + hOff, treadY, '#444444', ox, oy)
px(tx + hOff, treadY + 1, '#333333', ox, oy)
if (tx % 2 === 0) px(tx + hOff, treadY, '#555555', ox, oy)
}
}
// Helmet visor
if (!ko) {
const vizY = hy + 1
for (let vx = hx + 1; vx < hx + hw - 1; vx++) {
px(vx, vizY, pal.accDark, ox, oy)
}
px(hx + 1, vizY, pal.accLight, ox, oy)
}
},
}
@@ -0,0 +1,78 @@
import type { Archetype } from '../constants'
export const wizard: Archetype = {
name: 'wizard',
weight: 0.03,
canHaveMohawk: false,
canHaveVisor: false,
drawFeatures: (p) => {
const { px, pal, ox, oy, t, tier, idle, ko, special, atk, frame,
bx, by, bw, bh, hx, hy, hh, hw, cx, hOff, armLx, armAttach, armH, globalY } = p
// Pointed wizard hat (tall triangle)
if (!ko) {
const hatBase = hy - 1
const hatHeight = 8 + tier
for (let h = 0; h < hatHeight; h++) {
const rowW = Math.max(1, Math.floor((hatHeight - h) / hatHeight * (hw - 2)))
const rowX = cx + hOff - Math.floor(rowW / 2)
for (let hx2 = rowX; hx2 < rowX + rowW; hx2++) {
px(hx2, hatBase - h, pal.acc, ox, oy)
}
}
// Hat brim
for (let hbx = hx - 2; hbx < hx + hw + 2; hbx++) {
px(hbx, hatBase, pal.accDark, ox, oy)
}
// Star on hat
px(cx + hOff, hatBase - Math.floor(hatHeight * 0.6), '#ffd700', ox, oy)
px(cx + hOff - 1, hatBase - Math.floor(hatHeight * 0.6), '#ffee88', ox, oy)
px(cx + hOff + 1, hatBase - Math.floor(hatHeight * 0.6), '#ffee88', ox, oy)
}
// Long robe (extends body down, covers legs partially)
if (!ko) {
const robeY = by + bh
for (let ry = 0; ry < 3; ry++) {
const robeW = bw + 2 + ry
const robeX = bx - 1 - Math.floor(ry / 2)
for (let rx = robeX; rx < robeX + robeW; rx++) {
px(rx, robeY + ry, pal.acc, ox, oy)
}
}
}
// Staff replaces left arm
if (!ko && !p.knockback) {
const staffX = armLx - 1 + hOff
const staffTop = armAttach - 10 + globalY
const staffBot = p.feetY + globalY + 2
for (let sy = staffTop; sy < staffBot; sy++) {
px(staffX, sy, '#8B6914', ox, oy)
}
// Orb at top
const orbY = staffTop - 1
px(staffX, orbY, pal.accLight, ox, oy)
px(staffX - 1, orbY, pal.acc, ox, oy)
px(staffX + 1, orbY, pal.acc, ox, oy)
px(staffX, orbY - 1, pal.acc, ox, oy)
// Sparkle when attacking/special
if ((special || atk) && frame % 2 === 0) {
px(staffX - 1, orbY - 1, '#ffffff', ox, oy)
px(staffX + 1, orbY - 1, '#ffff00', ox, oy)
px(staffX, orbY - 2, '#ffffff', ox, oy)
}
}
// Beard
if (!ko) {
const beardY = hy + Math.floor(hh * 0.65)
for (let by2 = beardY; by2 < beardY + 3 + Math.floor(tier * 0.5); by2++) {
const bWidth = Math.max(1, 3 - (by2 - beardY))
for (let bbx = cx + hOff - Math.floor(bWidth / 2); bbx <= cx + hOff + Math.floor(bWidth / 2); bbx++) {
px(bbx, by2, '#cccccc', ox, oy)
}
}
}
},
}
+70
View File
@@ -0,0 +1,70 @@
export const FRAME_SIZE = 96
export const INTERNAL = 48
export const SCALE = FRAME_SIZE / INTERNAL
export const ANIMATIONS = {
idle: { frames: 4, row: 0 },
attack: { frames: 6, row: 1 },
kick: { frames: 5, row: 2 },
special: { frames: 6, row: 3 },
hit: { frames: 3, row: 4 },
knockback: { frames: 5, row: 5 },
ko: { frames: 5, row: 6 },
win: { frames: 4, row: 7 },
}
export const TOTAL_ROWS = Object.keys(ANIMATIONS).length
export const MAX_FRAMES = 6
export interface Pal {
body: string; dark: string; light: string
acc: string; accDark: string; accLight: string
out: string; skin: string; skinDark: string
}
export interface Dimensions {
bw: number; bh: number
hw: number; hh: number
legH: number; legW: number
armW: number; armH: number
}
export function baseDimensions(tier: number): Dimensions {
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,
}
}
export interface ArchetypeParams {
px: (x: number, y: number, color: string, ox: number, oy: number) => void
box: (x: number, y: number, w: number, h: number, fillColor: string, ox: number, oy: number) => void
fill: (x: number, y: number, w: number, h: number, color: string, ox: number, oy: number) => void
pal: Pal
ox: number; oy: number
t: number; frame: number; bounce: number
tier: number
specialType: 'fire' | 'electric'
idle: boolean; atk: boolean; kick: boolean; special: boolean
hit: boolean; knockback: boolean; ko: boolean; win: boolean
cx: number; ground: number
hOff: number; vBounce: number; koSlump: number; globalY: number
bx: number; by: number; bw: number; bh: number
hx: number; hy: number; hw: number; hh: number
armLx: number; armRx: number; armAttach: number; armW: number; armH: number
feetY: number; legsTop: number; legH: number; legW: number; ll: number; rl: number
}
export interface Archetype {
name: string
weight: number
canHaveVisor?: boolean
canHaveMohawk?: boolean
canHaveHorns?: boolean
dimensionOverrides?: (tier: number) => Partial<Dimensions>
drawFeatures: (p: ArchetypeParams) => void
}
+528
View File
@@ -0,0 +1,528 @@
import { FRAME_SIZE, INTERNAL, SCALE, ANIMATIONS, TOTAL_ROWS, MAX_FRAMES, baseDimensions } from './constants'
import type { Pal, ArchetypeParams } from './constants'
import { makePal } from './palette'
import { rollArchetype, archetypes } from './archetypes'
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 { archetypes, rollArchetype } from './archetypes'
export function generateSpriteSheet(
seed: string, tier: number, primaryColor: string, secondaryColor: string,
archetypeOverride?: string,
): 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 pal = makePal(primaryColor, secondaryColor, tier)
let sh = 0
for (let i = 0; i < seed.length; i++) sh = ((sh << 5) - sh + seed.charCodeAt(i)) | 0
const rng = () => { sh = (sh * 16807) % 2147483647; return (sh & 0x7fffffff) / 2147483647 }
rng(); rng(); rng()
// Character archetype -- determined by seed for consistent variety, or forced by override
const archetypeRoll = rng()
const arch = archetypeOverride
? (archetypes.find(a => a.name === archetypeOverride) || rollArchetype(archetypeRoll))
: rollArchetype(archetypeRoll)
// Consume rng in same order as original for determinism
const visorRoll = rng()
const mohawkRoll = rng()
const hornsRoll = rng()
const specialRoll = rng()
const hasVisor = visorRoll > 0.5 && tier >= 2 && (arch.canHaveVisor ?? false)
const hasMohawk = mohawkRoll > 0.5 && tier >= 3 && (arch.canHaveMohawk ?? true)
const hasHorns = hornsRoll > 0.6 && tier >= 4 && !hasMohawk && (arch.canHaveHorns ?? true)
const specialType: 'fire' | 'electric' = specialRoll > 0.5 ? 'fire' : 'electric'
// Dimensions: base + archetype overrides
const dims = { ...baseDimensions(tier), ...(arch.dimensionOverrides?.(tier) ?? {}) }
function px(x: number, y: number, color: string, ox: number, oy: number) {
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: number, y: number, w: number, h: number, fillColor: string, ox: number, oy: number) {
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, fillColor, ox, oy)
}
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 drawFrame(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'
const { bw, bh, hw, hh, legH, legW, armW, armH } = dims
// Anchor: center bottom at (24, 42) in 48x48
const cx = 24
const ground = 42
// Positions bottom-up
const feetY = ground - 2
const legsTop = feetY - legH
const bodyTop = legsTop - bh
const headTop = bodyTop - hh
// 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(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)
if (kickExt > 2) {
box(rl + kickExt + legW, legsTop + Math.floor(legH * 0.3) - 1 + globalY, 3 + tier, 3, pal.acc, 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 (tier 2+)
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)
// Shading
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)
}
// Horizontal stripes (tier detail)
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)
}
}
}
// Belt (tier 2+)
if (tier >= 2) {
const beltY = by + bh - 2
fill(bx, beltY, bw, 1, pal.acc, ox, oy)
fill(bx, beltY + 1, bw, 1, pal.accDark, ox, oy)
if (tier >= 3) { px(cx + hOff, beltY, '#ffd700', ox, oy); px(cx + hOff + 1, beltY, '#ffd700', ox, oy) }
}
// Chest emblem (tier 4+)
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)
}
// Shoulder pads (tier 3+)
if (tier >= 3 && !ko && !knockback) {
const sy = by
const 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)
px(bx - pw, sy, pal.accLight, ox, oy)
px(bx + bw + 1, sy, pal.accLight, ox, oy)
}
// ---- ARMS ----
const armAttach = by + 2 + vBounce
const armLx = bx - armW + hOff
const 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)
const fC = tier >= 5 ? '#ffd700' : tier >= 3 ? '#ff3333' : pal.body
box(armRx + reach + armW, armAttach - 2, fS, fS + 1, fC, ox, oy)
if (tier >= 2 && t > 0.3 && t < 0.7) {
const ix = armRx + reach + armW + fS + 1
px(ix, armAttach - 2, '#ffff00', ox, oy)
px(ix + 1, armAttach, '#ffffff', ox, oy)
px(ix, armAttach + 2, '#ffff00', ox, oy)
px(ix + 2, armAttach - 1, '#ffaa00', ox, oy)
px(ix + 2, armAttach + 1, '#ffaa00', ox, oy)
}
}
if (tier >= 3) {
const gs = 3 + Math.floor(tier * 0.3)
box(armLx - 1, armAttach + armH - 1, gs, gs, tier >= 5 ? '#ffd700' : '#ff3333', 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)
if (tier >= 3) {
const gs = 2 + Math.floor(tier * 0.3)
const gc = tier >= 5 ? '#ffd700' : '#ff3333'
box(armLx, armAttach + armH - 1, gs, gs, gc, ox, oy)
box(armRx, armAttach + armH - 1, gs, gs, gc, 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)
const projY = armAttach - 2
if (specialType === 'fire') {
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)
px(projX - 1, projY, '#ff2200', ox, oy)
px(projX - 2, projY + 1, '#ff440066', ox, oy)
px(projX - 3, projY, '#ff220044', ox, oy)
} else {
px(projX, projY, '#00eeff', ox, oy)
px(projX + 1, projY - 1, '#44ffff', ox, oy)
px(projX + 2, projY + 1, '#00eeff', ox, oy)
px(projX + 3, projY, '#88ffff', ox, oy)
px(projX + 1, projY + 1, '#0088ff', ox, oy)
px(projX - 1, projY - 1, '#44ffff', ox, oy)
px(projX + 4, projY - 1, '#ffffff', 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)
if (tier >= 3) {
const gs = 3 + Math.floor(tier * 0.3)
box(armRx - 1, armAttach - armH + bounce - gs, gs + 1, gs, tier >= 5 ? '#ffd700' : '#ff3333', 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)
if (tier >= 3) {
const gs = 3 + Math.floor(tier * 0.3)
const gc = tier >= 5 ? '#ffd700' : '#ff3333'
box(armLx - 1, armAttach + sw + armH + globalY, gs, gs, gc, ox, oy)
box(armRx, armAttach - sw + armH + globalY, gs, gs, gc, ox, oy)
}
}
// ---- HEAD ----
const hx = cx - Math.floor(hw / 2) + hOff
const hy = headTop + vBounce + koSlump + globalY
if (tier <= 1) {
// BOXY ROBOT
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(hx + 1, hy + 1, pal.light, ox, oy)
// Antenna
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)
// Eyes
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)
if (frame % 2 === 0) {
px(hx + 2, eyeY, '#00cc33', ox, oy)
px(hx + hw - 4, eyeY, '#00cc33', ox, oy)
}
}
// Mouth grille
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)
}
// Bolts
px(hx, hy + Math.floor(hh / 2), pal.accDark, ox, oy)
px(hx + hw - 1, hy + Math.floor(hh / 2), pal.accDark, ox, oy)
// Claw pincers (tier 0)
if (tier === 0) {
const cy2 = hy + Math.floor(hh / 2)
px(hx - 2, cy2, pal.acc, ox, oy); px(hx - 3, cy2 - 1, pal.acc, ox, oy); px(hx - 3, cy2 + 1, pal.acc, ox, oy)
px(hx + hw + 1, cy2, pal.acc, ox, oy); px(hx + hw + 2, cy2 - 1, pal.acc, ox, oy); px(hx + hw + 2, cy2 + 1, pal.acc, ox, oy)
}
} else {
// ROUNDED HEAD (tier 2+)
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)
}
px(hx + 2, hy + 1, pal.light, ox, oy); px(hx + 3, hy + 1, pal.light, ox, oy)
// Face area
if (tier >= 2) {
const faceTop = hy + Math.floor(hh * 0.25)
const 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)
}
}
// Eyes
const eyeY = hy + Math.floor(hh * 0.35)
const leX = hx + Math.floor(hw * 0.2)
const reX = hx + Math.floor(hw * 0.6)
const 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 if (knockback) {
fill(leX - 1, eyeY, ew + 2, 3, '#ffffff', ox, oy)
fill(reX - 1, eyeY, ew + 2, 3, '#ffffff', ox, oy)
px(leX, eyeY + 1, '#000000', ox, oy)
px(reX, eyeY + 1, '#000000', ox, oy)
} else {
fill(leX, eyeY, ew, 2, '#ffffff', ox, oy)
fill(reX, eyeY, ew, 2, '#ffffff', ox, oy)
const ps = atk || kick || special ? 1 : 0
px(leX + ps, eyeY + 1, '#000000', ox, oy)
px(reX + ps, eyeY + 1, '#000000', ox, oy)
if (tier >= 4) {
px(leX, eyeY, pal.acc, ox, oy)
px(reX + ew - 1, eyeY, pal.acc, ox, oy)
if (special) {
px(leX - 1, eyeY, specialType === 'fire' ? '#ff4400' : '#00eeff', ox, oy)
px(reX + ew, eyeY, specialType === 'fire' ? '#ff4400' : '#00eeff', 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)
}
}
// Mouth
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)
px(cx + hOff - 1, mY + 1, pal.out, ox, oy)
px(cx + hOff, mY + 1, pal.out, ox, oy)
px(cx + hOff + 1, mY + 1, pal.out, ox, oy)
} else if (ko || knockback) {
box(cx + hOff - 1, mY, 3, 2, '#000000', ox, oy)
} else if (hit) {
px(cx + hOff, mY, pal.out, ox, oy)
px(cx + hOff + 1, mY, pal.out, ox, oy)
} else if (atk || kick || special) {
fill(cx + hOff - 1, mY, 3, 2, '#000000', ox, oy)
px(cx + hOff - 1, mY, pal.out, ox, oy)
px(cx + hOff + 1, mY, pal.out, ox, oy)
} else {
px(cx + hOff - 1, mY, pal.out, ox, oy)
px(cx + hOff, mY, pal.out, ox, oy)
}
// Visor
if (hasVisor) {
const vY = eyeY - 1
for (let vx = hx + 1; vx < hx + hw - 1; vx++) px(vx, vY, pal.accDark, ox, oy)
px(hx + 1, vY, pal.accLight, ox, oy)
}
// Headband (tier 4+)
if (tier >= 4) {
const bY = hy + 2
for (let bx2 = hx; bx2 < hx + hw; bx2++) px(bx2, bY, pal.acc, ox, oy)
px(hx - 1, bY + 1, pal.acc, ox, oy)
px(hx - 2, bY + 1 + (idle ? Math.abs(bounce) : 0), pal.acc, ox, oy)
px(hx - 3, bY + 2 + (idle ? Math.abs(bounce) : 0), pal.acc, ox, oy)
px(hx - 4, bY + 2, pal.accDark, ox, oy)
}
// Mohawk
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) }
}
}
// Horns (tier 4+, alt to mohawk)
if (hasHorns) {
px(hx + 1, hy - 1, pal.acc, ox, oy); px(hx, hy - 2, pal.acc, ox, oy); px(hx - 1, hy - 3, pal.accLight, ox, oy)
px(hx + hw - 2, hy - 1, pal.acc, ox, oy); px(hx + hw - 1, hy - 2, pal.acc, ox, oy); px(hx + hw, hy - 3, pal.accLight, ox, oy)
}
// Crown (tier 5)
if (tier >= 5) {
const cY = hy - 1 - (hasMohawk ? 5 : hasHorns ? 3 : 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)
px(hx + 2, cY, '#00f0ff', ox, oy)
px(hx + hw - 3, cY, '#00f0ff', ox, oy)
}
}
// ---- ARCHETYPE-SPECIFIC FEATURES ----
const archParams: ArchetypeParams = {
px, box, fill, pal, ox, oy, t, frame, bounce, tier, specialType,
idle, atk, kick, special, hit, knockback, ko, win,
cx, ground, hOff, vBounce, koSlump, globalY,
bx, by, bw, bh, hx, hy, hw, hh,
armLx, armRx, armAttach, armW, armH,
feetY, legsTop, legH, legW, ll, rl,
}
arch.drawFeatures(archParams)
// ---- AURA (tier 4+) ----
if (tier >= 4 && !ko) {
const aCx = cx + hOff
const aCy = by + Math.floor(bh / 2)
const aR = Math.floor(bw / 2) + tier + 3
const dots = 6 + tier * 2
for (let i = 0; i < dots; i++) {
const ang = t * Math.PI * 2 + i * Math.PI * 2 / dots
const ax = aCx + Math.round(Math.cos(ang) * aR)
const ay = aCy + Math.round(Math.sin(ang) * (aR * 0.6))
if ((frame + i) % 3 !== 0) px(ax, ay, i % 2 === 0 ? pal.acc : pal.light, ox, oy)
}
if (tier >= 5) {
for (let p = 0; p < 4; p++) {
const pt = (t + p * 0.25) % 1
const py2 = ground - Math.round(pt * (ground - hy + 4))
const ppx = aCx + Math.round(Math.sin(py2 * 0.4 + p) * 3)
px(ppx, py2, p % 2 === 0 ? pal.acc : '#ffd700', ox, oy)
}
}
}
// ---- HIT SPARK ----
if (hit && t > 0.2) {
const sx = cx + hOff + Math.floor(bw / 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)
px(sx + 2, sy - 1, '#ff8800', ox, oy); px(sx + 2, sy + 1, '#ff8800', ox, oy)
px(sx - 1, sy - 1, '#ff4400', 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 (tier >= 2 && win) {
for (let i = 0; i < tier + 2; i++) {
const sa = t * Math.PI * 2 + i * 1.5
const sr = 10 + tier * 2
const sx = cx + hOff + Math.round(Math.cos(sa) * sr)
const sy = by + Math.floor(bh / 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) }
}
}
}
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++) drawFrame(f, row, pose, f, cfg.frames)
for (let f = cfg.frames; f < MAX_FRAMES; f++) drawFrame(f, row, pose, cfg.frames - 1, cfg.frames)
}
return canvas.toDataURL()
}
+174
View File
@@ -0,0 +1,174 @@
import { FRAME_SIZE, INTERNAL, SCALE } from './constants'
export const JUDGE_ANIMATIONS = {
idle: { frames: 4, row: 0 },
call_left: { frames: 4, row: 1 },
call_right: { frames: 4, row: 2 },
shocked: { frames: 4, row: 3 },
}
export const JUDGE_ROWS = Object.keys(JUDGE_ANIMATIONS).length
export const JUDGE_MAX_FRAMES = 6
export function generateJudgeSpriteSheet(): string {
const canvas = document.createElement('canvas')
canvas.width = FRAME_SIZE * JUDGE_MAX_FRAMES
canvas.height = FRAME_SIZE * JUDGE_ROWS
const ctx = canvas.getContext('2d')!
ctx.imageSmoothingEnabled = false
const red = '#cc2222'
const dkRed = '#881111'
const ltRed = '#ee4444'
const out = '#0a0a0a'
const stripeB = '#111111'
const stripeW = '#eeeeee'
const cardGreen = '#22cc44'
function px(x: number, y: number, color: string, ox: number, oy: number) {
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: 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) }
for (let iy = y; iy < y + h; iy++) for (let ix = x; ix < x + w; ix++) px(ix, iy, fc, ox, oy)
}
function fill(x: number, y: number, w: number, h: number, c: string, ox: number, oy: number) {
for (let iy = y; iy < y + h; iy++) for (let ix = x; ix < x + w; ix++) px(ix, iy, c, ox, oy)
}
function drawJudge(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 cx = 24
const baseY = 40
const bodyW = 14
const bodyH = 10
const headW = 12
const headH = 7
const idle = pose === 'idle'
const callL = pose === 'call_left'
const callR = pose === 'call_right'
const shocked = pose === 'shocked'
const yOff = idle ? bounce : shocked ? -Math.round(Math.abs(Math.sin(t * Math.PI)) * 3) : 0
const bodyTop = baseY - 16 + yOff
const bx = cx - Math.floor(bodyW / 2)
const headTop = bodyTop - headH - 1
const hx = cx - Math.floor(headW / 2)
// === TAIL SEGMENTS ===
for (let s = 0; s < 3; s++) {
const sw = bodyW - 2 - s * 2
const sx = cx - Math.floor(sw / 2)
const sy = bodyTop + bodyH + s * 2
fill(sx, sy, sw, 2, s % 2 === 0 ? red : dkRed, ox, oy)
px(sx - 1, sy, out, ox, oy); px(sx + sw, sy, out, ox, oy)
px(sx - 1, sy + 1, out, ox, oy); px(sx + sw, sy + 1, out, ox, oy)
}
// Tail fan
const fanY = bodyTop + bodyH + 6
for (let f = -3; f <= 3; f++) px(cx + f, fanY, Math.abs(f) > 2 ? dkRed : red, ox, oy)
px(cx - 3, fanY + 1, dkRed, ox, oy); px(cx + 3, fanY + 1, dkRed, ox, oy)
// === BODY (referee striped shirt) ===
for (let i = bx - 1; i <= bx + bodyW; i++) { px(i, bodyTop - 1, out, ox, oy); px(i, bodyTop + bodyH, out, ox, oy) }
for (let i = bodyTop; i < bodyTop + bodyH; i++) { px(bx - 1, i, out, ox, oy); px(bx + bodyW, i, out, ox, oy) }
for (let iy = bodyTop; iy < bodyTop + bodyH; iy++) {
for (let ix = bx; ix < bx + bodyW; ix++) {
px(ix, iy, Math.floor((ix - bx) / 2) % 2 === 0 ? stripeB : stripeW, ox, oy)
}
}
// V-neck showing red body
px(cx - 1, bodyTop, red, ox, oy); px(cx, bodyTop, red, ox, oy); px(cx + 1, bodyTop, red, ox, oy)
px(cx, bodyTop + 1, red, ox, oy)
// Whistle
px(cx + 2, bodyTop + 2, '#aaaaaa', ox, oy)
px(cx + 3, bodyTop + 3, '#888888', ox, oy)
// === HEAD ===
box(hx, headTop, headW, headH, red, ox, oy)
for (let iy = headTop + 1; iy < headTop + headH - 1; iy++) {
px(hx + headW - 1, iy, dkRed, ox, oy)
px(hx + 1, iy, ltRed, ox, oy)
}
// Mouth
if (shocked) {
fill(cx - 1, headTop + headH - 2, 3, 2, '#000000', ox, oy)
} else {
px(cx - 1, headTop + headH - 2, out, ox, oy); px(cx, headTop + headH - 2, out, ox, oy)
}
// === EYE STALKS ===
const eyeExt = shocked ? 3 : 1
// Left stalk + eye
px(hx + 2, headTop - 1, dkRed, ox, oy); px(hx + 1, headTop - 2, dkRed, ox, oy)
for (let e = 0; e < eyeExt; e++) px(hx, headTop - 3 - e, dkRed, ox, oy)
fill(hx - 2, headTop - 3 - eyeExt, 3, 2, '#ffffff', ox, oy)
px(hx - 1, headTop - 2 - eyeExt, '#000000', ox, oy)
// Right stalk + eye
px(hx + headW - 3, headTop - 1, dkRed, ox, oy); px(hx + headW - 2, headTop - 2, dkRed, ox, oy)
for (let e = 0; e < eyeExt; e++) px(hx + headW - 1, headTop - 3 - e, dkRed, ox, oy)
fill(hx + headW - 1, headTop - 3 - eyeExt, 3, 2, '#ffffff', ox, oy)
px(hx + headW, headTop - 2 - eyeExt, '#000000', ox, oy)
// === ANTENNAE ===
const antBase = headTop - 3 - eyeExt
for (let a = 1; a <= 5; a++) {
const wobble = idle ? Math.round(Math.sin(t * Math.PI * 2 + a * 0.8)) : 0
px(hx - 1, antBase - a + wobble, red, ox, oy)
px(hx + headW, antBase - a - wobble, red, ox, oy)
}
px(hx - 2, antBase - 5, ltRed, ox, oy); px(hx + headW + 1, antBase - 5, ltRed, ox, oy)
// === LEFT CLAW ===
const leftRaised = callL || shocked
if (leftRaised) {
const armTopY = bodyTop - 8 - Math.round(Math.abs(Math.sin(t * Math.PI)) * 2)
fill(bx - 3, armTopY, 2, bodyTop + 3 - armTopY, red, ox, oy)
for (let c = 0; c < 5; c++) px(bx - 5 - c, armTopY, red, ox, oy)
for (let c = 0; c < 5; c++) px(bx - 5 - c, armTopY + 2, red, ox, oy)
px(bx - 5, armTopY + 1, dkRed, ox, oy)
if (callL) box(bx - 12, armTopY - 4, 4, 6, cardGreen, ox, oy)
} else {
const cly = bodyTop + 5 + (idle ? bounce : 0)
fill(bx - 3, bodyTop + 2, 2, cly - bodyTop - 2, red, ox, oy)
for (let c = 0; c < 4; c++) px(bx - 5 - c, cly, red, ox, oy)
for (let c = 0; c < 4; c++) px(bx - 5 - c, cly + 2, red, ox, oy)
px(bx - 5, cly + 1, dkRed, ox, oy)
}
// === RIGHT CLAW (mirror) ===
const rightRaised = callR || shocked
if (rightRaised) {
const armTopY = bodyTop - 8 - Math.round(Math.abs(Math.sin(t * Math.PI)) * 2)
fill(bx + bodyW + 1, armTopY, 2, bodyTop + 3 - armTopY, red, ox, oy)
for (let c = 0; c < 5; c++) px(bx + bodyW + 4 + c, armTopY, red, ox, oy)
for (let c = 0; c < 5; c++) px(bx + bodyW + 4 + c, armTopY + 2, red, ox, oy)
px(bx + bodyW + 4, armTopY + 1, dkRed, ox, oy)
if (callR) box(bx + bodyW + 7, armTopY - 4, 4, 6, cardGreen, ox, oy)
} else {
const cry = bodyTop + 5 + (idle ? -bounce : 0)
fill(bx + bodyW + 1, bodyTop + 2, 2, cry - bodyTop - 2, red, ox, oy)
for (let c = 0; c < 4; c++) px(bx + bodyW + 4 + c, cry, red, ox, oy)
for (let c = 0; c < 4; c++) px(bx + bodyW + 4 + c, cry + 2, red, ox, oy)
px(bx + bodyW + 4, cry + 1, dkRed, ox, oy)
}
}
const entries = Object.entries(JUDGE_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++) drawJudge(f, row, pose, f, cfg.frames)
for (let f = cfg.frames; f < JUDGE_MAX_FRAMES; f++) drawJudge(f, row, pose, cfg.frames - 1, cfg.frames)
}
return canvas.toDataURL()
}
+32
View File
@@ -0,0 +1,32 @@
import type { Pal } from './constants'
export function parseHSL(c: string): [number, number, number] {
const m = c.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
return m ? [+m[1], +m[2], +m[3]] : [200, 70, 50]
}
export function makePal(primary: string, secondary: string, tier: number): Pal {
const [h, s, l] = parseHSL(primary)
const [h2, s2, l2] = parseHSL(secondary)
return {
body: primary,
dark: `hsl(${h}, ${s}%, ${Math.max(0, l - 20)}%)`,
light: `hsl(${h}, ${Math.min(100, s + 5)}%, ${Math.min(95, l + 15)}%)`,
acc: secondary,
accDark: `hsl(${h2}, ${s2}%, ${Math.max(0, l2 - 20)}%)`,
accLight: `hsl(${h2}, ${Math.min(100, s2)}%, ${Math.min(95, l2 + 15)}%)`,
out: '#0a0a0a',
skin: tier <= 1 ? primary : `hsl(${h}, ${Math.max(20, s - 30)}%, ${Math.min(85, l + 25)}%)`,
skinDark: tier <= 1 ? `hsl(${h}, ${s}%, ${Math.max(0, l - 10)}%)` : `hsl(${h}, ${Math.max(15, s - 35)}%, ${Math.min(75, l + 15)}%)`,
}
}
export function getBotColors(seed: string): { primary: string; secondary: string } {
let h = 0
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
const hue = Math.abs(h % 360)
return {
primary: `hsl(${hue}, 70%, 50%)`,
secondary: `hsl(${(hue + 140) % 360}, 80%, 60%)`,
}
}