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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
}
@@ -1,52 +1,17 @@
// Pixel-art sprite sheet generator
// 48x48 internal resolution scaled to 96x96 frames
// Many animation states for rich fighting
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'
const FRAME_SIZE = 96
const INTERNAL = 48
const SCALE = FRAME_SIZE / INTERNAL
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 },
}
const TOTAL_ROWS = Object.keys(ANIMATIONS).length
const MAX_FRAMES = 6
interface Pal {
body: string; dark: string; light: string
acc: string; accDark: string; accLight: string
out: string; skin: string; skinDark: string
}
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)}%)`,
}
}
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 { 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
@@ -61,10 +26,25 @@ export function generateSpriteSheet(
const rng = () => { sh = (sh * 16807) % 2147483647; return (sh & 0x7fffffff) / 2147483647 }
rng(); rng(); rng()
const hasVisor = rng() > 0.5 && tier >= 2
const hasMohawk = rng() > 0.5 && tier >= 3
const hasHorns = rng() > 0.6 && tier >= 4 && !hasMohawk
const specialType = rng() > 0.5 ? 'fire' : 'electric' // determines special attack visuals
// 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
@@ -97,15 +77,7 @@ export function generateSpriteSheet(
const ko = pose === 'ko'
const win = pose === 'win'
// Dimensions scale with tier
const bw = 10 + tier * 2 // body width
const bh = 8 + tier // body height
const hw = 10 + tier // head width
const hh = 9 + tier // head height
const legH = 6 + tier // leg height
const legW = 3 + Math.floor(tier * 0.5)
const armW = 3
const armH = 5 + tier
const { bw, bh, hw, hh, legH, legW, armW, armH } = dims
// Anchor: center bottom at (24, 42) in 48x48
const cx = 24
@@ -121,7 +93,7 @@ export function generateSpriteSheet(
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 // arc in the air
const kbLift = knockback ? Math.round(Math.sin(t * Math.PI) * 6) : 0
const globalY = -kbLift
// ---- SHADOW ----
@@ -140,17 +112,13 @@ export function generateSpriteSheet(
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) {
// Standing leg
box(ll, legsTop + globalY, legW, legH, pal.dark, ox, oy)
// Kicking leg — extends horizontally
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)
// Foot on kick
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) {
// Legs trailing behind in arc
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 {
@@ -210,7 +178,6 @@ export function generateSpriteSheet(
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)
// Highlight
px(bx - pw, sy, pal.accLight, ox, oy)
px(bx + bw + 1, sy, pal.accLight, ox, oy)
}
@@ -224,20 +191,16 @@ export function generateSpriteSheet(
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) {
// Arms flailing behind
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) {
// Guard left arm
box(armLx, armAttach + 2, armW, armH - 2, pal.body, ox, oy)
// Punch right arm
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)
// Impact
if (tier >= 2 && t > 0.3 && t < 0.7) {
const ix = armRx + reach + armW + fS + 1
px(ix, armAttach - 2, '#ffff00', ox, oy)
@@ -247,13 +210,11 @@ export function generateSpriteSheet(
px(ix + 2, armAttach + 1, '#ffaa00', ox, oy)
}
}
// Left glove
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) {
// Both arms in guard
box(armLx, armAttach, armW, armH - 1, pal.body, ox, oy)
box(armRx, armAttach, armW, armH - 1, pal.body, ox, oy)
if (tier >= 3) {
@@ -263,49 +224,40 @@ export function generateSpriteSheet(
box(armRx, armAttach + armH - 1, gs, gs, gc, ox, oy)
}
} else if (special) {
// Left arm forward, channeling
box(armLx, armAttach, armW, armH, pal.body, ox, oy)
// Right arm extended, casting
const ext = Math.round(Math.sin(t * Math.PI) * (armH + 2))
box(armRx, armAttach - 2, ext + armW + 2, armW, pal.body, ox, oy)
// Projectile effect
if (t > 0.3) {
const projX = armRx + ext + armW + 3 + Math.round(t * 8)
const projY = armAttach - 2
if (specialType === 'fire') {
// Fireball
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)
// Trail
px(projX - 2, projY + 1, '#ff440066', ox, oy)
px(projX - 3, projY, '#ff220044', ox, oy)
} else {
// Electric bolt
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)
// Sparks
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)
// Raised arm
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 {
// Idle
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)
@@ -324,7 +276,6 @@ export function generateSpriteSheet(
if (tier <= 1) {
// BOXY ROBOT
box(hx, hy, hw, hh, pal.body, ox, oy)
// Shading
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)
@@ -345,7 +296,6 @@ export function generateSpriteSheet(
} else {
fill(hx + 2, eyeY, 2, 2, '#00ff41', ox, oy)
fill(hx + hw - 4, eyeY, 2, 2, '#00ff41', ox, oy)
// Scanline flicker
if (frame % 2 === 0) {
px(hx + 2, eyeY, '#00cc33', ox, oy)
px(hx + hw - 4, eyeY, '#00cc33', ox, oy)
@@ -365,9 +315,9 @@ export function generateSpriteSheet(
// Claw pincers (tier 0)
if (tier === 0) {
const cy = hy + Math.floor(hh / 2)
px(hx - 2, cy, pal.acc, ox, oy); px(hx - 3, cy - 1, pal.acc, ox, oy); px(hx - 3, cy + 1, pal.acc, ox, oy)
px(hx + hw + 1, cy, pal.acc, ox, oy); px(hx + hw + 2, cy - 1, pal.acc, ox, oy); px(hx + hw + 2, cy + 1, pal.acc, ox, oy)
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+)
@@ -376,14 +326,13 @@ export function generateSpriteSheet(
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)
}
// Shading
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 (lighter "skin" for tier 2+)
// Face area
if (tier >= 2) {
const faceTop = hy + Math.floor(hh * 0.25)
const faceBot = hy + Math.floor(hh * 0.75)
@@ -407,7 +356,6 @@ export function generateSpriteSheet(
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) {
// Wide shock eyes
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)
@@ -419,7 +367,6 @@ export function generateSpriteSheet(
px(leX + ps, eyeY + 1, '#000000', ox, oy)
px(reX + ps, eyeY + 1, '#000000', ox, oy)
// Eye glow (tier 4+)
if (tier >= 4) {
px(leX, eyeY, pal.acc, ox, oy)
px(reX + ew - 1, eyeY, pal.acc, ox, oy)
@@ -429,7 +376,6 @@ export function generateSpriteSheet(
}
}
// Angry brows when attacking
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)
@@ -439,7 +385,6 @@ export function generateSpriteSheet(
// Mouth
const mY = hy + Math.floor(hh * 0.65)
if (win) {
// Big grin
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)
@@ -447,13 +392,11 @@ export function generateSpriteSheet(
px(cx + hOff, mY + 1, pal.out, ox, oy)
px(cx + hOff + 1, mY + 1, pal.out, ox, oy)
} else if (ko || knockback) {
// Open mouth shock
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) {
// Battle yell
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)
@@ -466,7 +409,7 @@ export function generateSpriteSheet(
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) // highlight
px(hx + 1, vY, pal.accLight, ox, oy)
}
// Headband (tier 4+)
@@ -507,6 +450,17 @@ export function generateSpriteSheet(
}
}
// ---- 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
@@ -522,9 +476,9 @@ export function generateSpriteSheet(
if (tier >= 5) {
for (let p = 0; p < 4; p++) {
const pt = (t + p * 0.25) % 1
const py = ground - Math.round(pt * (ground - hy + 4))
const ppx = aCx + Math.round(Math.sin(py * 0.4 + p) * 3)
px(ppx, py, p % 2 === 0 ? pal.acc : '#ffd700', ox, oy)
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)
}
}
}
@@ -572,15 +526,3 @@ export function generateSpriteSheet(
return canvas.toDataURL()
}
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%)`,
}
}
export { FRAME_SIZE, ANIMATIONS, MAX_FRAMES, TOTAL_ROWS }
+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%)`,
}
}