-
-
{{ item.text }}
-
{{ item.text }}
-
{{ item.text }}
-
{{ item.text }}
-
{{ item.text }}
-
{{ item.text }}
-
{{ item.text }}
+
+
{{ item.text }}
+
+
Challenge
+
{{ item.text }}
+
+
+
+
{{ item.text }}
+
+
{{ item.text }}
{{ item.text }}
diff --git a/frontend/src/components/HumanPreview.vue b/frontend/src/components/HumanPreview.vue
new file mode 100644
index 0000000..0828715
--- /dev/null
+++ b/frontend/src/components/HumanPreview.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts
index baf8bbc..5149bc4 100644
--- a/frontend/src/game/FightScene.ts
+++ b/frontend/src/game/FightScene.ts
@@ -566,8 +566,6 @@ export async function createFightScene(config: FightSceneConfig) {
// Grotesque close-up stubs (disabled — looked bad)
function spawnGrotesqueDetails(_fighter: any, _scaleFactor: number) {}
- function destroyGrotesqueDetails() {}
-
// Arena-specific background decoration
function drawArenaDecor() {
const a = arena
@@ -578,7 +576,7 @@ export async function createFightScene(config: FightSceneConfig) {
const star = k.add([
k.rect(1 + Math.random() * 2, 1 + Math.random() * 2),
k.pos(Math.random() * W, Math.random() * (GROUND_Y - 20)),
- k.color(safeColor(k,i % 5 === 0 ? '#ffffff' : acc)),
+ k.color(safeColor(k, i % 5 === 0 ? '#ffffff' : acc)),
k.opacity(0.08 + Math.random() * 0.2),
k.z(1),
])
@@ -590,49 +588,70 @@ export async function createFightScene(config: FightSceneConfig) {
}
if (a === 'datacenter' || a === 'localhost') {
- // Server rack wall — multiple rows of racks with blinking LEDs
+ // City skyline silhouette behind the racks
+ const buildingHeights = [0.55, 0.4, 0.65, 0.35, 0.7, 0.45, 0.6, 0.5, 0.72, 0.38, 0.58, 0.42]
+ for (let i = 0; i < buildingHeights.length; i++) {
+ const bw = W / buildingHeights.length + 4
+ const bh = GROUND_Y * buildingHeights[i]
+ const bx = i * (W / buildingHeights.length)
+ k.add([k.rect(bw, bh), k.pos(bx, GROUND_Y - bh), k.color(safeColor(k, '#060610')), k.opacity(0.5), k.z(0)])
+ for (let wy = 0; wy < bh - 10; wy += 8) {
+ for (let wx = 4; wx < bw - 6; wx += 7) {
+ const win = k.add([
+ k.rect(3, 3), k.pos(bx + wx, GROUND_Y - bh + 6 + wy),
+ k.color(safeColor(k, Math.random() > 0.4 ? '#223355' : '#ffcc44')),
+ k.opacity(0.15 + Math.random() * 0.2), k.z(0),
+ ])
+ if (Math.random() < 0.15) {
+ const blink = 0.03 + Math.random() * 0.06
+ win.onUpdate(() => { win.opacity = Math.random() > blink ? win.opacity : (Math.random() > 0.5 ? 0.3 : 0.05) })
+ }
+ }
+ }
+ }
+ // Server rack wall
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 10; col++) {
const lx = 15 + col * (W / 10)
const ly = 15 + row * 55
const rackW = W / 10 - 8
const rackH = 48
- // Rack body with border
- k.add([k.rect(rackW, rackH), k.pos(lx, ly), k.color(safeColor(k,'#06060f')), k.opacity(0.6), k.z(1)])
- k.add([k.rect(rackW, 1), k.pos(lx, ly), k.color(safeColor(k,'#1a1a3a')), k.opacity(0.4), k.z(1)])
- k.add([k.rect(rackW, 1), k.pos(lx, ly + rackH), k.color(safeColor(k,'#1a1a3a')), k.opacity(0.4), k.z(1)])
- k.add([k.rect(1, rackH), k.pos(lx, ly), k.color(safeColor(k,'#1a1a3a')), k.opacity(0.3), k.z(1)])
- // Drive slots (horizontal lines)
+ k.add([k.rect(rackW, rackH), k.pos(lx, ly), k.color(safeColor(k, '#06060f')), k.opacity(0.6), k.z(1)])
+ k.add([k.rect(rackW, 1), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(rackW, 1), k.pos(lx, ly + rackH), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(1, rackH), k.pos(lx, ly), k.color(safeColor(k, '#1a1a3a')), k.opacity(0.3), k.z(1)])
for (let s = 0; s < 5; s++) {
- k.add([k.rect(rackW - 6, 1), k.pos(lx + 3, ly + 8 + s * 8), k.color(safeColor(k,'#111122')), k.opacity(0.5), k.z(1)])
+ k.add([k.rect(rackW - 6, 1), k.pos(lx + 3, ly + 8 + s * 8), k.color(safeColor(k, '#111122')), k.opacity(0.5), k.z(1)])
}
- // Multiple blinking LEDs per rack
for (let led = 0; led < 3; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(lx + 4 + led * 6, ly + 4),
- k.color(safeColor(k,Math.random() > 0.3 ? '#00ff41' : '#ff2d2d')),
+ k.color(safeColor(k, Math.random() > 0.3 ? '#00ff41' : '#ff2d2d')),
k.opacity(0.7), k.z(2),
])
const blinkRate = 0.02 + Math.random() * 0.08
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > blinkRate ? 0.7 : 0.15 })
}
- // Activity LED (flashing amber)
if (Math.random() < 0.4) {
const actLed = k.add([
k.rect(2, 2), k.pos(lx + rackW - 8, ly + 4),
- k.color(safeColor(k,'#ffaa00')), k.opacity(0.6), k.z(2),
+ k.color(safeColor(k, '#ffaa00')), k.opacity(0.6), k.z(2),
])
actLed.onUpdate(() => { actLed.opacity = Math.random() > 0.5 ? 0.7 : 0.2 })
}
}
}
- // Cable bundles running across ceiling
- for (let c = 0; c < 4; c++) {
- const cy = 5 + c * 3
- k.add([k.rect(W, 2), k.pos(0, cy), k.color(safeColor(k,c % 2 === 0 ? '#111133' : '#0a0a22')), k.opacity(0.5), k.z(1)])
+ // Cable bundles
+ for (let c = 0; c < 6; c++) {
+ const cy = 3 + c * 2
+ k.add([k.rect(W, 2), k.pos(0, cy), k.color(safeColor(k, c % 3 === 0 ? '#111133' : c % 3 === 1 ? '#0a0a22' : '#0f0f2a')), k.opacity(0.5), k.z(1)])
}
+ // Scan line sweep
+ const scanLine = k.add([k.rect(W, 1), k.pos(0, 0), k.color(safeColor(k, '#00f0ff')), k.opacity(0.06), k.z(3)])
+ scanLine.onUpdate(() => { scanLine.pos.y = (scanLine.pos.y + 40 * k.dt()) % GROUND_Y })
+
} else if (a === 'gpu_graveyard') {
- // Floating circuit board fragments with traces and solder points
+ // Floating circuit board fragments
for (let i = 0; i < 16; i++) {
const cx = Math.random() * W
const cy = 20 + Math.random() * (GROUND_Y - 60)
@@ -640,73 +659,117 @@ export async function createFightScene(config: FightSceneConfig) {
const ch = 10 + Math.random() * 18
const chip = k.add([
k.rect(cw, ch), k.pos(cx, cy),
- k.color(safeColor(k,Math.random() > 0.5 ? '#1a2a1a' : '#0f1a0f')),
+ k.color(safeColor(k, Math.random() > 0.5 ? '#1a2a1a' : '#0f1a0f')),
k.opacity(0.35), k.z(1), k.rotate(Math.random() * 45 - 22),
])
- // Trace lines on each chip
for (let t = 0; t < 3; t++) {
- k.add([k.rect(cw * 0.6, 1), k.pos(cx + 2, cy + 3 + t * 4), k.color(safeColor(k,'#76b900')), k.opacity(0.2), k.z(1)])
+ k.add([k.rect(cw * 0.6, 1), k.pos(cx + 2, cy + 3 + t * 4), k.color(safeColor(k, '#76b900')), k.opacity(0.2), k.z(1)])
}
- // Solder point
- k.add([k.circle(1.5), k.pos(cx + cw / 2, cy + ch / 2), k.color(safeColor(k,'#aabb44')), k.opacity(0.25), k.z(1)])
+ k.add([k.circle(1.5), k.pos(cx + cw / 2, cy + ch / 2), k.color(safeColor(k, '#aabb44')), k.opacity(0.25), k.z(1)])
const baseY = cy
chip.onUpdate(() => { chip.pos.y = baseY + Math.sin(k.time() * 0.3 + i) * 5 })
}
- // Dead GPU fan silhouettes
+ // Dead GPU fans — some slowly spinning
for (const fx of [W * 0.15, W * 0.5, W * 0.85]) {
const fy = GROUND_Y - 30 - Math.random() * 40
- k.add([k.circle(18), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k,'#0a0a0a')), k.opacity(0.4), k.z(1)])
- k.add([k.circle(5), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k,'#1a1a1a')), k.opacity(0.3), k.z(1)])
- // Fan blades (static, dead)
+ k.add([k.circle(18), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.4), k.z(1)])
+ k.add([k.circle(5), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1)])
for (let b = 0; b < 4; b++) {
const ba = b * Math.PI / 2
- k.add([k.rect(14, 2), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k,'#222222')), k.opacity(0.3), k.z(1), k.rotate(ba * 180 / Math.PI)])
+ const blade = k.add([k.rect(14, 2), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.3), k.z(1), k.rotate(ba * 180 / Math.PI)])
+ if (fx === W * 0.5) {
+ blade.onUpdate(() => { blade.angle += 8 * k.dt() })
+ }
}
}
+ // Sparking wires
+ for (let i = 0; i < 4; i++) {
+ const sx = W * (0.2 + i * 0.2)
+ const sy = 10 + Math.random() * 30
+ k.add([k.rect(1, 20 + Math.random() * 40), k.pos(sx, sy), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)])
+ const spark = k.add([k.circle(3), k.pos(sx, sy + 20), k.anchor('center'), k.color(safeColor(k, '#ffff00')), k.opacity(0), k.z(3)])
+ spark.onUpdate(() => { spark.opacity = Math.random() > 0.97 ? 0.6 : 0 })
+ }
+ // Smoke wisps rising
+ for (let i = 0; i < 6; i++) {
+ const smoke = k.add([
+ k.circle(8 + Math.random() * 12), k.pos(Math.random() * W, GROUND_Y - 5),
+ k.color(safeColor(k, '#1a1a1a')), k.opacity(0.15), k.z(2),
+ ])
+ const bx = smoke.pos.x
+ smoke.onUpdate(() => {
+ smoke.pos.y -= 6 * k.dt()
+ smoke.pos.x = bx + Math.sin(k.time() + i) * 8
+ smoke.opacity *= 0.998
+ if (smoke.pos.y < 0) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.15 }
+ })
+ }
+
} else if (a === 'prompt_dungeon') {
- // Stone wall texture
+ // Stone wall bricks
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 12; col++) {
const brickW = W / 12 + (Math.random() - 0.5) * 10
const brickH = GROUND_Y / 5
const bx = col * (W / 12) + (row % 2 === 0 ? 0 : W / 24)
- k.add([k.rect(brickW - 2, brickH - 2), k.pos(bx, row * brickH), k.color(safeColor(k,'#1a1520')), k.opacity(0.3), k.z(1)])
+ k.add([k.rect(brickW - 2, brickH - 2), k.pos(bx, row * brickH), k.color(safeColor(k, '#1a1520')), k.opacity(0.3), k.z(1)])
}
}
- // Glowing runes scattered on walls
- const runeSymbols = ['*', '+', 'o', '~', '^']
+ // Glowing runes with halos
for (let i = 0; i < 10; i++) {
const rx = 30 + Math.random() * (W - 60)
const ry = 20 + Math.random() * (GROUND_Y - 60)
const rune = k.add([
k.circle(6 + Math.random() * 8), k.pos(rx, ry),
- k.color(safeColor(k,'#b83dff')), k.opacity(0.12), k.z(1),
+ k.color(safeColor(k, '#b83dff')), k.opacity(0.12), k.z(1),
])
- // Rune glow halo
- k.add([k.circle(12 + Math.random() * 8), k.pos(rx, ry), k.color(safeColor(k,'#6622aa')), k.opacity(0.06), k.z(0)])
+ k.add([k.circle(12 + Math.random() * 8), k.pos(rx, ry), k.color(safeColor(k, '#6622aa')), k.opacity(0.06), k.z(0)])
rune.onUpdate(() => { rune.opacity = 0.08 + Math.sin(k.time() * 0.8 + i * 0.7) * 0.08 })
}
- // Torches on walls
+ // Torches with flickering flames and light cones
for (const tx of [W * 0.1, W * 0.35, W * 0.65, W * 0.9]) {
const ty = GROUND_Y * 0.3
- k.add([k.rect(4, 20), k.pos(tx, ty), k.color(safeColor(k,'#553311')), k.opacity(0.5), k.z(1)])
- const flame = k.add([k.circle(6), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k,'#ff6600')), k.opacity(0.5), k.z(2)])
- const flameGlow = k.add([k.circle(15), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k,'#ff4400')), k.opacity(0.08), k.z(1)])
+ k.add([k.rect(4, 20), k.pos(tx, ty), k.color(safeColor(k, '#553311')), k.opacity(0.5), k.z(1)])
+ const flame = k.add([k.circle(6), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.5), k.z(2)])
+ const flameGlow = k.add([k.circle(15), k.pos(tx + 2, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.08), k.z(1)])
+ const lightCone = k.add([k.rect(30, GROUND_Y * 0.5), k.pos(tx - 13, ty + 20), k.color(safeColor(k, '#ff6600')), k.opacity(0.02), k.z(0)])
flame.onUpdate(() => {
flame.opacity = 0.4 + Math.sin(k.time() * 6 + tx) * 0.15
flame.pos.y = ty - 3 + Math.sin(k.time() * 8 + tx) * 1.5
flameGlow.opacity = 0.05 + Math.sin(k.time() * 6 + tx) * 0.04
+ lightCone.opacity = 0.015 + Math.sin(k.time() * 6 + tx) * 0.008
})
}
+ // Chains hanging from ceiling
+ for (let i = 0; i < 5; i++) {
+ const cx = W * (0.15 + i * 0.18)
+ const chainLen = 25 + Math.random() * 50
+ for (let s = 0; s < chainLen; s += 5) {
+ const link = k.add([k.rect(3, 4), k.pos(cx, s), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)])
+ const baseCx = cx
+ link.onUpdate(() => { link.pos.x = baseCx + Math.sin(k.time() * 0.4 + i + s * 0.1) * 3 })
+ }
+ }
// Ground fog
for (let i = 0; i < 8; i++) {
const fog = k.add([
k.circle(35 + Math.random() * 30), k.pos(Math.random() * W, GROUND_Y - 8),
- k.color(safeColor(k,'#1f1a2a')), k.opacity(0.25), k.z(3),
+ k.color(safeColor(k, '#1f1a2a')), k.opacity(0.25), k.z(3),
])
const baseX = fog.pos.x
fog.onUpdate(() => { fog.pos.x = baseX + Math.sin(k.time() * 0.2 + i) * 18 })
}
+ // Dripping water
+ for (let i = 0; i < 3; i++) {
+ const dx = W * (0.25 + i * 0.25)
+ const drop = k.add([k.circle(1.5), k.pos(dx, 0), k.color(safeColor(k, '#4466aa')), k.opacity(0.3), k.z(2)])
+ drop.onUpdate(() => {
+ drop.pos.y += 50 * k.dt()
+ if (drop.pos.y > GROUND_Y) { drop.pos.y = 0; drop.opacity = 0.3 }
+ drop.opacity *= 0.997
+ })
+ }
+
} else if (a === 'the_cloud') {
// Layered cloud formations
for (let layer = 0; layer < 3; layer++) {
@@ -714,12 +777,11 @@ export async function createFightScene(config: FightSceneConfig) {
const cx = Math.random() * W
const cy = 20 + layer * 50 + Math.random() * 30
const cloudOp = 0.25 - layer * 0.05
- // Multi-blob cloud
for (let j = 0; j < 4 + Math.floor(Math.random() * 3); j++) {
const cloud = k.add([
k.circle(15 + Math.random() * 20),
k.pos(cx + j * 14 - 20, cy + (Math.random() - 0.5) * 12),
- k.color(safeColor(k,layer === 0 ? '#1a2030' : '#151a28')),
+ k.color(safeColor(k, layer === 0 ? '#1a2030' : '#151a28')),
k.opacity(cloudOp), k.z(1),
])
const bx = cloud.pos.x
@@ -727,41 +789,71 @@ export async function createFightScene(config: FightSceneConfig) {
}
}
}
- // Data streams (vertical lines falling like rain)
- for (let i = 0; i < 10; i++) {
+ // Data rain streams
+ for (let i = 0; i < 15; i++) {
const stream = k.add([
k.rect(1, 8 + Math.random() * 15), k.pos(Math.random() * W, Math.random() * GROUND_Y),
- k.color(safeColor(k,'#4488ff')), k.opacity(0.12), k.z(2),
+ k.color(safeColor(k, '#4488ff')), k.opacity(0.12), k.z(2),
])
stream.onUpdate(() => {
stream.pos.y += 30 * k.dt()
- if (stream.pos.y > GROUND_Y) stream.pos.y = -20
+ if (stream.pos.y > GROUND_Y) { stream.pos.y = -20; stream.pos.x = Math.random() * W }
})
}
+ // Lightning flashes
+ const lightning = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3)])
+ lightning.onUpdate(() => { lightning.opacity = Math.random() > 0.998 ? 0.06 : 0 })
+ // Rainbow arc (subtle)
+ const rainbowColors = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff']
+ for (let i = 0; i < rainbowColors.length; i++) {
+ k.add([
+ k.circle(120 + i * 6), k.pos(W * 0.3, GROUND_Y + 60), k.anchor('center'),
+ k.color(safeColor(k, rainbowColors[i])), k.opacity(0.03), k.z(0),
+ ])
+ }
+
} else if (a === 'the_singularity') {
// Central swirling vortex with multiple rings
- for (let ring = 0; ring < 3; ring++) {
+ for (let ring = 0; ring < 4; ring++) {
const ringDots = 12 + ring * 6
for (let i = 0; i < ringDots; i++) {
const angle = (i / ringDots) * Math.PI * 2
- const baseDist = 30 + ring * 35
+ const baseDist = 20 + ring * 30
const dot = k.add([
k.circle(1.5 + ring * 0.5), k.pos(W / 2, GROUND_Y * 0.4),
- k.color(safeColor(k,ring === 0 ? '#ff44ff' : ring === 1 ? '#ff00ff' : '#aa00aa')),
- k.opacity(0.25 - ring * 0.05), k.z(1),
+ k.color(safeColor(k, ring === 0 ? '#ffffff' : ring === 1 ? '#ff44ff' : ring === 2 ? '#ff00ff' : '#aa00aa')),
+ k.opacity(0.3 - ring * 0.05), k.z(1),
])
dot.onUpdate(() => {
- const a2 = angle + k.time() * (0.6 - ring * 0.15)
+ const a2 = angle + k.time() * (0.8 - ring * 0.15)
const d2 = baseDist + Math.sin(k.time() * 0.8 + i) * 12
dot.pos.x = W / 2 + Math.cos(a2) * d2
dot.pos.y = GROUND_Y * 0.4 + Math.sin(a2) * d2 * 0.5
})
}
}
- // Central core glow
- const core = k.add([k.circle(12), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k,'#ffffff')), k.opacity(0.15), k.z(1)])
- const coreHalo = k.add([k.circle(30), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k,'#ff00ff')), k.opacity(0.06), k.z(0)])
+ // Central core with pulsing glow
+ const core = k.add([k.circle(12), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.15), k.z(1)])
+ k.add([k.circle(30), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0)])
+ k.add([k.circle(60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('center'), k.color(safeColor(k, '#880088')), k.opacity(0.03), k.z(0)])
core.onUpdate(() => { core.opacity = 0.1 + Math.sin(k.time() * 2) * 0.08 })
+ // Energy tendrils reaching outward
+ for (let i = 0; i < 8; i++) {
+ const tendril = k.add([
+ k.rect(2, 40 + Math.random() * 60), k.pos(W / 2, GROUND_Y * 0.4), k.anchor('top'),
+ k.color(safeColor(k, '#ff00ff')), k.opacity(0.06), k.z(0), k.rotate(i * 45),
+ ])
+ tendril.onUpdate(() => { tendril.angle = i * 45 + k.time() * 15 })
+ }
+ // Warped space grid lines
+ for (let i = 0; i < 6; i++) {
+ const gridLine = k.add([
+ k.rect(W, 1), k.pos(0, GROUND_Y * (0.15 + i * 0.12)),
+ k.color(safeColor(k, '#440066')), k.opacity(0.08), k.z(0),
+ ])
+ gridLine.onUpdate(() => { gridLine.pos.x = Math.sin(k.time() * 0.3 + i) * 10 - 5 })
+ }
+
} else if (a === 'silicon_valley_dojo') {
// Bamboo stalks with segments and leaves
for (const side of [0.04, 0.08, 0.12, 0.86, 0.91, 0.96]) {
@@ -769,147 +861,249 @@ export async function createFightScene(config: FightSceneConfig) {
const segments = 5 + Math.floor(Math.random() * 3)
const segH = (GROUND_Y - 15) / segments
for (let s = 0; s < segments; s++) {
- k.add([k.rect(5, segH - 2), k.pos(px, 15 + s * segH), k.color(safeColor(k,'#1a3a1a')), k.opacity(0.45), k.z(1)])
- // Segment node
- k.add([k.rect(7, 2), k.pos(px - 1, 15 + s * segH), k.color(safeColor(k,'#2a4a2a')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(5, segH - 2), k.pos(px, 15 + s * segH), k.color(safeColor(k, '#1a3a1a')), k.opacity(0.45), k.z(1)])
+ k.add([k.rect(7, 2), k.pos(px - 1, 15 + s * segH), k.color(safeColor(k, '#2a4a2a')), k.opacity(0.4), k.z(1)])
}
- // Leaves at various heights
for (let l = 0; l < 3; l++) {
const ly = 20 + l * (GROUND_Y / 4)
const dir = Math.random() > 0.5 ? 1 : -1
- k.add([k.rect(18, 3), k.pos(px + dir * 4, ly), k.color(safeColor(k,'#22aa33')), k.opacity(0.2), k.z(1), k.rotate(dir * 25)])
- k.add([k.rect(14, 2), k.pos(px + dir * 8, ly + 4), k.color(safeColor(k,'#1a8822')), k.opacity(0.15), k.z(1), k.rotate(dir * 35)])
+ const leaf = k.add([k.rect(18, 3), k.pos(px + dir * 4, ly), k.color(safeColor(k, '#22aa33')), k.opacity(0.2), k.z(1), k.rotate(dir * 25)])
+ leaf.onUpdate(() => { leaf.angle = dir * 25 + Math.sin(k.time() * 1.5 + l) * 3 })
+ k.add([k.rect(14, 2), k.pos(px + dir * 8, ly + 4), k.color(safeColor(k, '#1a8822')), k.opacity(0.15), k.z(1), k.rotate(dir * 35)])
}
}
- // Zen garden raked lines on ground
+ // Zen garden raked lines
for (let i = 0; i < 8; i++) {
const gy = GROUND_Y + 5 + i * 8
- k.add([k.rect(W * 0.6, 1), k.pos(W * 0.2, gy), k.color(safeColor(k,'#00ff41')), k.opacity(0.06), k.z(0)])
+ k.add([k.rect(W * 0.6, 1), k.pos(W * 0.2, gy), k.color(safeColor(k, '#00ff41')), k.opacity(0.06), k.z(0)])
}
+ // Stone lanterns with warm glow
+ for (const lx of [W * 0.2, W * 0.8]) {
+ k.add([k.rect(10, 6), k.pos(lx, GROUND_Y - 20), k.color(safeColor(k, '#444444')), k.opacity(0.35), k.z(1)])
+ k.add([k.rect(6, 14), k.pos(lx + 2, GROUND_Y - 34), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)])
+ k.add([k.rect(12, 3), k.pos(lx - 1, GROUND_Y - 37), k.color(safeColor(k, '#555555')), k.opacity(0.3), k.z(1)])
+ const glow = k.add([k.circle(8), k.pos(lx + 5, GROUND_Y - 27), k.anchor('center'), k.color(safeColor(k, '#ffaa33')), k.opacity(0.15), k.z(2)])
+ glow.onUpdate(() => { glow.opacity = 0.1 + Math.sin(k.time() * 2 + lx) * 0.06 })
+ }
+ // Cherry blossom petals falling
+ for (let i = 0; i < 12; i++) {
+ const petal = k.add([
+ k.circle(1.5 + Math.random()), k.pos(Math.random() * W, Math.random() * GROUND_Y),
+ k.color(safeColor(k, Math.random() > 0.5 ? '#ffaacc' : '#ff88aa')), k.opacity(0.2), k.z(3),
+ ])
+ const startX = petal.pos.x
+ petal.onUpdate(() => {
+ petal.pos.y += 8 * k.dt()
+ petal.pos.x = startX + Math.sin(k.time() * 1.2 + i) * 15
+ if (petal.pos.y > GROUND_Y) { petal.pos.y = -5 }
+ })
+ }
+
} else if (a === 'hacker_news' || a === 'stackoverflow_ruins') {
- // Code blocks / post fragments floating
+ // Ruined building walls with cracks
+ for (let i = 0; i < 3; i++) {
+ const wallX = W * (0.15 + i * 0.3)
+ const wallH = GROUND_Y * (0.4 + Math.random() * 0.3)
+ k.add([k.rect(W * 0.2, wallH), k.pos(wallX, GROUND_Y - wallH), k.color(safeColor(k, '#1a1408')), k.opacity(0.3), k.z(0)])
+ for (let c = 0; c < 3; c++) {
+ k.add([k.rect(1, 10 + Math.random() * 15), k.pos(wallX + Math.random() * W * 0.18, GROUND_Y - wallH + Math.random() * wallH), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0), k.rotate(Math.random() * 30 - 15)])
+ }
+ }
+ // Code blocks floating
for (let i = 0; i < 14; i++) {
const bw = 25 + Math.random() * 60
const bh = 3 + Math.random() * 4
const bx = 15 + Math.random() * (W - 50)
const by = 15 + Math.random() * (GROUND_Y - 50)
- k.add([k.rect(bw, bh), k.pos(bx, by), k.color(safeColor(k,acc)), k.opacity(0.06 + Math.random() * 0.06), k.z(1)])
- // Indent markers
+ k.add([k.rect(bw, bh), k.pos(bx, by), k.color(safeColor(k, acc)), k.opacity(0.06 + Math.random() * 0.06), k.z(1)])
if (Math.random() < 0.4) {
- k.add([k.rect(2, bh), k.pos(bx - 4, by), k.color(safeColor(k,acc)), k.opacity(0.1), k.z(1)])
+ k.add([k.rect(2, bh), k.pos(bx - 4, by), k.color(safeColor(k, acc)), k.opacity(0.1), k.z(1)])
}
}
// Vote arrows
for (let i = 0; i < 5; i++) {
const ax = 20 + Math.random() * (W - 40)
const ay = 30 + Math.random() * (GROUND_Y - 70)
- // Upvote triangle
- k.add([k.rect(4, 6), k.pos(ax, ay), k.color(safeColor(k,a === 'stackoverflow_ruins' ? '#f48024' : '#ff6600')), k.opacity(0.12), k.z(1)])
+ k.add([k.rect(4, 6), k.pos(ax, ay), k.color(safeColor(k, a === 'stackoverflow_ruins' ? '#f48024' : '#ff6600')), k.opacity(0.12), k.z(1)])
}
+ // Broken monitors with static
+ for (let i = 0; i < 3; i++) {
+ const mx = W * (0.2 + i * 0.3)
+ const my = GROUND_Y * 0.3 + Math.random() * 30
+ k.add([k.rect(22, 16), k.pos(mx, my), k.color(safeColor(k, '#0a0a0a')), k.opacity(0.5), k.z(1)])
+ k.add([k.rect(20, 14), k.pos(mx + 1, my + 1), k.color(safeColor(k, '#111111')), k.opacity(0.4), k.z(1)])
+ const staticEl = k.add([k.rect(18, 12), k.pos(mx + 2, my + 2), k.color(safeColor(k, '#888888')), k.opacity(0), k.z(2)])
+ staticEl.onUpdate(() => { staticEl.opacity = Math.random() > 0.92 ? 0.08 : 0 })
+ }
+ // Scrolling text fragments
+ for (let i = 0; i < 4; i++) {
+ const txt = k.add([
+ k.rect(30 + Math.random() * 40, 2), k.pos(Math.random() * W, 20 + i * (GROUND_Y / 5)),
+ k.color(safeColor(k, acc)), k.opacity(0.08), k.z(2),
+ ])
+ txt.onUpdate(() => {
+ txt.pos.x -= 12 * k.dt()
+ if (txt.pos.x < -60) txt.pos.x = W + 20
+ })
+ }
+
} else if (a === 'beach') {
- // Sun with rays
+ // Sun with rays and glow
const sunX = W * 0.8, sunY = 35
- k.add([k.circle(30), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k,'#ffdd44')), k.opacity(0.12), k.z(0)])
- const sun = k.add([k.circle(18), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k,'#ffcc00')), k.opacity(0.5), k.z(1)])
+ k.add([k.circle(45), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.06), k.z(0)])
+ k.add([k.circle(30), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffdd44')), k.opacity(0.1), k.z(0)])
+ const sun = k.add([k.circle(18), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.5), k.z(1)])
sun.onUpdate(() => { sun.opacity = 0.4 + Math.sin(k.time() * 0.5) * 0.1 })
- // Sun rays
for (let r = 0; r < 8; r++) {
const angle = r * Math.PI / 4
- k.add([k.rect(40, 2), k.pos(sunX, sunY), k.anchor('left'), k.color(safeColor(k,'#ffcc00')), k.opacity(0.08), k.z(0), k.rotate(angle * 180 / Math.PI)])
+ const ray = k.add([k.rect(50, 2), k.pos(sunX, sunY), k.anchor('left'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.08), k.z(0), k.rotate(angle * 180 / Math.PI)])
+ ray.onUpdate(() => { ray.opacity = 0.05 + Math.sin(k.time() * 0.8 + r) * 0.04 })
}
- // Ocean horizon with layered waves
- for (let layer = 0; layer < 3; layer++) {
- for (let i = 0; i < 4; i++) {
+ // Ocean with layered waves
+ k.add([k.rect(W, 25), k.pos(0, GROUND_Y - 35), k.color(safeColor(k, '#1a4488')), k.opacity(0.2), k.z(1)])
+ for (let layer = 0; layer < 4; layer++) {
+ for (let i = 0; i < 5; i++) {
const wave = k.add([
- k.rect(W * 0.35, 3 + layer), k.pos(Math.random() * W, GROUND_Y - 18 - layer * 8 - i * 3),
- k.color(safeColor(k,layer === 0 ? '#3377bb' : '#2266aa')), k.opacity(0.2 - layer * 0.04), k.z(2),
+ k.rect(W * 0.3, 3 + layer), k.pos(Math.random() * W, GROUND_Y - 22 - layer * 6 - i * 2),
+ k.color(safeColor(k, layer < 2 ? '#3377bb' : '#2266aa')), k.opacity(0.18 - layer * 0.03), k.z(2),
])
const baseX = wave.pos.x
wave.onUpdate(() => { wave.pos.x = baseX + Math.sin(k.time() * (0.6 + layer * 0.2) + i * 1.5) * (15 + layer * 5) })
}
}
- // Palm trees with trunks and fronds
- for (const px of [W * 0.06, W * 0.93]) {
+ // Surf foam at shore
+ for (let i = 0; i < 6; i++) {
+ const foam = k.add([
+ k.rect(15 + Math.random() * 25, 2), k.pos(Math.random() * W, GROUND_Y - 6 + Math.random() * 4),
+ k.color(safeColor(k, '#ffffff')), k.opacity(0.12), k.z(3),
+ ])
+ const baseX = foam.pos.x
+ foam.onUpdate(() => { foam.pos.x = baseX + Math.sin(k.time() * 0.8 + i) * 8; foam.opacity = 0.06 + Math.sin(k.time() + i) * 0.06 })
+ }
+ // Palm trees with segmented trunks, coconuts, swaying fronds
+ for (const px of [W * 0.05, W * 0.14, W * 0.88, W * 0.95]) {
const lean = px < W / 2 ? 8 : -8
- // Trunk segments
- for (let s = 0; s < 8; s++) {
+ const height = 8 + Math.floor(Math.random() * 3)
+ for (let s = 0; s < height; s++) {
const sy = GROUND_Y - 10 - s * 12
- k.add([k.rect(7 - s * 0.5, 13), k.pos(px + s * lean / 8, sy), k.color(safeColor(k,'#3a2510')), k.opacity(0.5), k.z(1)])
+ const segW = 7 - s * 0.4
+ k.add([k.rect(segW, 13), k.pos(px + s * lean / height, sy), k.color(safeColor(k, s % 2 === 0 ? '#3a2510' : '#2f1f0d')), k.opacity(0.5), k.z(1)])
+ k.add([k.rect(segW + 1, 1), k.pos(px + s * lean / height - 0.5, sy + 6), k.color(safeColor(k, '#4a3520')), k.opacity(0.2), k.z(1)])
}
- // Fronds
- const topX = px + lean, topY = GROUND_Y - 106
- for (let f = 0; f < 6; f++) {
- const angle = -60 + f * 24
- k.add([k.rect(35, 3), k.pos(topX, topY), k.anchor('left'), k.color(safeColor(k,'#1a6600')), k.opacity(0.35), k.z(1), k.rotate(angle)])
+ const topX = px + lean, topY = GROUND_Y - 10 - height * 12
+ for (let c = 0; c < 2 + Math.floor(Math.random() * 2); c++) {
+ k.add([k.circle(2.5), k.pos(topX + (c - 1) * 4, topY + 8), k.anchor('center'), k.color(safeColor(k, '#553311')), k.opacity(0.4), k.z(1)])
+ }
+ for (let f = 0; f < 7; f++) {
+ const angle = -70 + f * 22
+ const frond = k.add([k.rect(35 + Math.random() * 10, 3), k.pos(topX, topY), k.anchor('left'), k.color(safeColor(k, f % 2 === 0 ? '#1a6600' : '#228800')), k.opacity(0.35), k.z(1), k.rotate(angle)])
+ frond.onUpdate(() => { frond.angle = angle + Math.sin(k.time() * 0.8 + f) * 3 })
}
}
- // Seagulls (v-shapes in sky)
- for (let i = 0; i < 3; i++) {
- const bx = W * 0.3 + Math.random() * W * 0.4
- const by = 15 + Math.random() * 30
- k.add([k.rect(6, 1), k.pos(bx, by), k.color(safeColor(k,'#333333')), k.opacity(0.3), k.z(1), k.rotate(-15)])
- k.add([k.rect(6, 1), k.pos(bx + 5, by), k.color(safeColor(k,'#333333')), k.opacity(0.3), k.z(1), k.rotate(15)])
+ // Seagulls drifting
+ for (let i = 0; i < 4; i++) {
+ const bx = W * 0.2 + Math.random() * W * 0.5
+ const by = 12 + Math.random() * 25
+ const wing1 = k.add([k.rect(6, 1), k.pos(bx, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(-15)])
+ const wing2 = k.add([k.rect(6, 1), k.pos(bx + 5, by), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1), k.rotate(15)])
+ const startBx = bx
+ wing1.onUpdate(() => { wing1.pos.x = startBx + Math.sin(k.time() * 0.2 + i) * 30; wing2.pos.x = wing1.pos.x + 5 })
}
+ // Beach umbrella
+ const umbX = W * 0.35
+ k.add([k.rect(2, 30), k.pos(umbX, GROUND_Y - 30), k.color(safeColor(k, '#aa8844')), k.opacity(0.4), k.z(1)])
+ k.add([k.circle(16), k.pos(umbX + 1, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(k, '#ff4444')), k.opacity(0.2), k.z(1)])
+ k.add([k.circle(14), k.pos(umbX + 1, GROUND_Y - 31), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.08), k.z(1)])
+
} else if (a === 'desert') {
// Layered sand dunes
for (let d = 0; d < 5; d++) {
const dx = W * (0.1 + d * 0.2) + (Math.random() - 0.5) * 40
const dr = 50 + d * 15 + Math.random() * 20
- k.add([k.circle(dr), k.pos(dx, GROUND_Y + dr * 0.6), k.anchor('center'), k.color(safeColor(k,'#aa8844')), k.opacity(0.15 + d * 0.03), k.z(1)])
+ k.add([k.circle(dr), k.pos(dx, GROUND_Y + dr * 0.6), k.anchor('center'), k.color(safeColor(k, '#aa8844')), k.opacity(0.15 + d * 0.03), k.z(1)])
}
- // Cacti with arms
- for (const cx of [W * 0.1, W * 0.5, W * 0.88]) {
+ // Road with dashed center line
+ k.add([k.rect(W * 0.25, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#222222')), k.opacity(0.15), k.z(0)])
+ k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.38, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)])
+ k.add([k.rect(2, GROUND_Y * 0.5), k.pos(W * 0.63, GROUND_Y * 0.3), k.color(safeColor(k, '#aaaa44')), k.opacity(0.08), k.z(0)])
+ for (let d = 0; d < 8; d++) {
+ k.add([k.rect(2, 8), k.pos(W * 0.505, GROUND_Y * 0.32 + d * 16), k.color(safeColor(k, '#ffff44')), k.opacity(0.1), k.z(0)])
+ }
+ // Cacti with arms and flowers
+ for (const cx of [W * 0.08, W * 0.25, W * 0.75, W * 0.9]) {
const ch = 35 + Math.random() * 20
- // Main trunk
- k.add([k.rect(7, ch), k.pos(cx, GROUND_Y - ch), k.color(safeColor(k,'#226622')), k.opacity(0.45), k.z(1)])
- // Arms
+ k.add([k.rect(7, ch), k.pos(cx, GROUND_Y - ch), k.color(safeColor(k, '#226622')), k.opacity(0.45), k.z(1)])
const armY = GROUND_Y - ch * 0.6
- k.add([k.rect(12, 5), k.pos(cx - 12, armY), k.color(safeColor(k,'#226622')), k.opacity(0.4), k.z(1)])
- k.add([k.rect(5, 15), k.pos(cx - 12, armY - 15), k.color(safeColor(k,'#226622')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(12, 5), k.pos(cx - 12, armY), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(5, 15), k.pos(cx - 12, armY - 15), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
if (Math.random() > 0.3) {
const armY2 = GROUND_Y - ch * 0.4
- k.add([k.rect(10, 5), k.pos(cx + 7, armY2), k.color(safeColor(k,'#226622')), k.opacity(0.4), k.z(1)])
- k.add([k.rect(5, 12), k.pos(cx + 12, armY2 - 12), k.color(safeColor(k,'#226622')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(10, 5), k.pos(cx + 7, armY2), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(5, 12), k.pos(cx + 12, armY2 - 12), k.color(safeColor(k, '#226622')), k.opacity(0.4), k.z(1)])
+ }
+ if (Math.random() > 0.5) {
+ k.add([k.circle(2.5), k.pos(cx + 3, GROUND_Y - ch - 2), k.anchor('center'), k.color(safeColor(k, '#ff4488')), k.opacity(0.3), k.z(2)])
}
}
- // Tumbleweed
- const tumble = k.add([k.circle(8), k.pos(-20, GROUND_Y - 10), k.color(safeColor(k,'#886644')), k.opacity(0.3), k.z(2)])
- tumble.onUpdate(() => { tumble.pos.x = ((tumble.pos.x + 20 * k.dt()) % (W + 40)) - 20 })
- // Heat shimmer bands
+ // Tumbleweeds rolling
for (let i = 0; i < 3; i++) {
- const shimmer = k.add([k.rect(W, 1), k.pos(0, GROUND_Y - 3 - i * 3), k.color(safeColor(k,'#ff8800')), k.opacity(0.06), k.z(2)])
+ const tumble = k.add([k.circle(6 + Math.random() * 4), k.pos(-20 - i * 100, GROUND_Y - 8 - Math.random() * 5), k.color(safeColor(k, '#886644')), k.opacity(0.3), k.z(2)])
+ const speed = 15 + Math.random() * 15
+ tumble.onUpdate(() => { tumble.pos.x = ((tumble.pos.x + speed * k.dt()) % (W + 60)) - 30 })
+ }
+ // Heat shimmer
+ for (let i = 0; i < 4; i++) {
+ const shimmer = k.add([k.rect(W, 1), k.pos(0, GROUND_Y - 3 - i * 3), k.color(safeColor(k, '#ff8800')), k.opacity(0.06), k.z(2)])
shimmer.onUpdate(() => { shimmer.opacity = 0.03 + Math.sin(k.time() * 3 + i * 2) * 0.03 })
}
+ // Vulture silhouettes circling
+ for (let i = 0; i < 2; i++) {
+ const vx = W * 0.4 + i * W * 0.2
+ const vy = 25 + i * 15
+ const v1 = k.add([k.rect(8, 1), k.pos(vx, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(-10)])
+ const v2 = k.add([k.rect(8, 1), k.pos(vx + 7, vy), k.color(safeColor(k, '#222222')), k.opacity(0.25), k.z(1), k.rotate(10)])
+ const startVx = vx
+ v1.onUpdate(() => {
+ const cx = startVx + Math.cos(k.time() * 0.15 + i) * 40
+ const cy = vy + Math.sin(k.time() * 0.15 + i) * 15
+ v1.pos.x = cx; v1.pos.y = cy; v2.pos.x = cx + 7; v2.pos.y = cy
+ })
+ }
+
} else if (a === 'forest') {
- // Layered trees — back layer (small/faded), front layer (larger/darker)
- for (let layer = 0; layer < 2; layer++) {
- const treeCount = layer === 0 ? 12 : 6
- const treeOp = layer === 0 ? 0.2 : 0.4
+ // Layered trees (3 depth layers)
+ for (let layer = 0; layer < 3; layer++) {
+ const treeCount = layer === 0 ? 14 : layer === 1 ? 8 : 5
+ const treeOp = layer === 0 ? 0.15 : layer === 1 ? 0.3 : 0.4
for (let i = 0; i < treeCount; i++) {
- const tx = 15 + (i / treeCount) * (W - 30) + (Math.random() - 0.5) * 20
- const th = (30 + Math.random() * 30) * (layer === 0 ? 0.7 : 1)
- const trunkW = layer === 0 ? 5 : 8
- // Trunk
- k.add([k.rect(trunkW, th), k.pos(tx, GROUND_Y - th), k.color(safeColor(k,layer === 0 ? '#0f1a08' : '#1a2a10')), k.opacity(treeOp), k.z(1 + layer)])
- // Canopy — multiple overlapping circles
- for (let c = 0; c < 3; c++) {
- const cr = (12 + Math.random() * 12) * (layer === 0 ? 0.7 : 1)
+ const tx = 10 + (i / treeCount) * (W - 20) + (Math.random() - 0.5) * 15
+ const th = (25 + Math.random() * 35) * (layer === 0 ? 0.6 : layer === 1 ? 0.85 : 1)
+ const trunkW = layer === 0 ? 4 : layer === 1 ? 6 : 8
+ k.add([k.rect(trunkW, th), k.pos(tx, GROUND_Y - th), k.color(safeColor(k, layer === 0 ? '#0f1a08' : layer === 1 ? '#152010' : '#1a2a10')), k.opacity(treeOp), k.z(layer)])
+ for (let c = 0; c < 3 + layer; c++) {
+ const cr = (10 + Math.random() * 14) * (layer === 0 ? 0.6 : layer === 1 ? 0.8 : 1)
k.add([
k.circle(cr), k.pos(tx + trunkW / 2 + (Math.random() - 0.5) * 10, GROUND_Y - th - cr * 0.5 + c * 4),
- k.anchor('center'), k.color(safeColor(k,layer === 0 ? '#0a1a08' : '#1a3a10')), k.opacity(treeOp * 0.8), k.z(1 + layer),
+ k.anchor('center'), k.color(safeColor(k, layer === 0 ? '#0a1a08' : layer === 1 ? '#122a0e' : '#1a3a10')), k.opacity(treeOp * 0.8), k.z(layer),
])
}
}
}
- // Undergrowth (bushes at ground level)
- for (let i = 0; i < 8; i++) {
- k.add([k.circle(8 + Math.random() * 6), k.pos(Math.random() * W, GROUND_Y - 4), k.anchor('bot'), k.color(safeColor(k,'#1a3a10')), k.opacity(0.3), k.z(3)])
+ // Undergrowth
+ for (let i = 0; i < 10; i++) {
+ k.add([k.circle(6 + Math.random() * 8), k.pos(Math.random() * W, GROUND_Y - 3), k.anchor('bot'), k.color(safeColor(k, '#1a3a10')), k.opacity(0.3), k.z(3)])
+ }
+ // Mushrooms
+ for (let i = 0; i < 5; i++) {
+ const mx = Math.random() * W
+ k.add([k.rect(2, 5), k.pos(mx, GROUND_Y - 5), k.color(safeColor(k, '#aa8866')), k.opacity(0.25), k.z(3)])
+ k.add([k.circle(4), k.pos(mx + 1, GROUND_Y - 6), k.anchor('center'), k.color(safeColor(k, Math.random() > 0.5 ? '#cc3333' : '#ffaa22')), k.opacity(0.2), k.z(3)])
}
// Fireflies
- for (let i = 0; i < 12; i++) {
+ for (let i = 0; i < 15; i++) {
const ff = k.add([
k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 50)),
- k.color(safeColor(k,'#ccff44')), k.opacity(0), k.z(3),
+ k.color(safeColor(k, '#ccff44')), k.opacity(0), k.z(3),
])
const bx = ff.pos.x, by = ff.pos.y
ff.onUpdate(() => {
@@ -919,128 +1113,221 @@ export async function createFightScene(config: FightSceneConfig) {
})
}
// Light rays through canopy
- for (let i = 0; i < 4; i++) {
- const rx = W * (0.2 + i * 0.2) + (Math.random() - 0.5) * 30
- k.add([k.rect(6, GROUND_Y * 0.6), k.pos(rx, 0), k.color(safeColor(k,'#aaff44')), k.opacity(0.03), k.z(2), k.rotate(5 - Math.random() * 10)])
+ for (let i = 0; i < 5; i++) {
+ const rx = W * (0.15 + i * 0.18) + (Math.random() - 0.5) * 20
+ const ray = k.add([k.rect(8, GROUND_Y * 0.7), k.pos(rx, 0), k.color(safeColor(k, '#aaff44')), k.opacity(0.03), k.z(2), k.rotate(3 - Math.random() * 6)])
+ ray.onUpdate(() => { ray.opacity = 0.02 + Math.sin(k.time() * 0.3 + i) * 0.015 })
}
+ // Fallen log
+ k.add([k.rect(45, 6), k.pos(W * 0.55, GROUND_Y - 4), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(3), k.rotate(3)])
+
} else if (a === 'jungle') {
- // Dense vine canopy hanging from top
- for (let i = 0; i < 10; i++) {
- const vx = 20 + i * (W / 10) + (Math.random() - 0.5) * 15
- const vineLen = 30 + Math.random() * 80
- // Vine strand (wavy)
+ // Dense vine canopy
+ for (let i = 0; i < 12; i++) {
+ const vx = 15 + i * (W / 12) + (Math.random() - 0.5) * 12
+ const vineLen = 30 + Math.random() * 90
for (let s = 0; s < vineLen; s += 4) {
- k.add([k.rect(2, 5), k.pos(vx + Math.sin(s * 0.15) * 4, s), k.color(safeColor(k,'#1a4a10')), k.opacity(0.3), k.z(1)])
+ const vineEl = k.add([k.rect(2, 5), k.pos(vx + Math.sin(s * 0.15) * 4, s), k.color(safeColor(k, '#1a4a10')), k.opacity(0.3), k.z(1)])
+ if (Math.random() < 0.08) {
+ const basePx = vineEl.pos.x
+ vineEl.onUpdate(() => { vineEl.pos.x = basePx + Math.sin(k.time() * 0.5 + s) * 2 })
+ }
}
- // Leaf clusters at bottom
for (let l = 0; l < 2; l++) {
- k.add([k.circle(5 + Math.random() * 4), k.pos(vx + (Math.random() - 0.5) * 8, vineLen + l * 6), k.color(safeColor(k,'#22aa22')), k.opacity(0.22), k.z(1)])
+ k.add([k.circle(5 + Math.random() * 4), k.pos(vx + (Math.random() - 0.5) * 8, vineLen + l * 6), k.color(safeColor(k, '#22aa22')), k.opacity(0.22), k.z(1)])
}
}
- // Large tropical leaves in foreground edges
+ // Large tropical leaves at edges
for (const side of [0, W - 30]) {
- for (let l = 0; l < 3; l++) {
- const ly = 20 + l * (GROUND_Y / 4)
- k.add([k.rect(30, 8), k.pos(side, ly), k.color(safeColor(k,'#1a5510')), k.opacity(0.25), k.z(3), k.rotate(side === 0 ? 15 : -15)])
+ for (let l = 0; l < 4; l++) {
+ const ly = 15 + l * (GROUND_Y / 5)
+ const leaf = k.add([k.rect(35, 8), k.pos(side, ly), k.color(safeColor(k, '#1a5510')), k.opacity(0.25), k.z(3), k.rotate(side === 0 ? 15 : -15)])
+ leaf.onUpdate(() => { leaf.angle = (side === 0 ? 15 : -15) + Math.sin(k.time() * 0.6 + l) * 2 })
}
}
+ // Exotic flowers
+ for (let i = 0; i < 6; i++) {
+ const fx = Math.random() * W
+ const fy = 20 + Math.random() * (GROUND_Y - 40)
+ k.add([k.circle(3), k.pos(fx, fy), k.anchor('center'), k.color(safeColor(k, ['#ff4488', '#ff8844', '#ffcc00', '#ff44ff', '#44ffcc', '#ff2222'][i])), k.opacity(0.2), k.z(2)])
+ }
+ // Ancient stone ruins peeking through
+ const ruinX = W * 0.5
+ k.add([k.rect(8, 40), k.pos(ruinX, GROUND_Y - 38), k.color(safeColor(k, '#444444')), k.opacity(0.15), k.z(0)])
+ k.add([k.rect(8, 35), k.pos(ruinX + 25, GROUND_Y - 33), k.color(safeColor(k, '#444444')), k.opacity(0.12), k.z(0)])
+ k.add([k.rect(35, 5), k.pos(ruinX - 2, GROUND_Y - 40), k.color(safeColor(k, '#444444')), k.opacity(0.1), k.z(0)])
// Thick ground fog
- for (let i = 0; i < 8; i++) {
+ for (let i = 0; i < 10; i++) {
const mist = k.add([
k.circle(40 + Math.random() * 35), k.pos(Math.random() * W, GROUND_Y - 12),
- k.color(safeColor(k,'#1a3818')), k.opacity(0.22), k.z(3),
+ k.color(safeColor(k, '#1a3818')), k.opacity(0.22), k.z(3),
])
const bx = mist.pos.x
mist.onUpdate(() => { mist.pos.x = bx + Math.sin(k.time() * 0.12 + i) * 18 })
}
+ // Butterfly particles
+ for (let i = 0; i < 4; i++) {
+ const bf = k.add([
+ k.circle(1.5), k.pos(Math.random() * W, 30 + Math.random() * (GROUND_Y - 60)),
+ k.color(safeColor(k, ['#4488ff', '#ff8844', '#ffcc00', '#ff44ff'][i])), k.opacity(0.25), k.z(3),
+ ])
+ const bfx = bf.pos.x, bfy = bf.pos.y
+ bf.onUpdate(() => {
+ bf.pos.x = bfx + Math.sin(k.time() * 1.2 + i * 3) * 30
+ bf.pos.y = bfy + Math.cos(k.time() * 0.8 + i * 2) * 20
+ })
+ }
+
} else if (a === 'outer_space') {
- // Dense star field with color variety
- for (let i = 0; i < 80; i++) {
+ // Dense star field
+ for (let i = 0; i < 100; i++) {
const star = k.add([
k.circle(0.5 + Math.random() * 1.5),
k.pos(Math.random() * W, Math.random() * H),
- k.color(safeColor(k,i % 8 === 0 ? '#ffccaa' : i % 5 === 0 ? '#aaaaff' : '#ffffff')),
+ k.color(safeColor(k, i % 8 === 0 ? '#ffccaa' : i % 5 === 0 ? '#aaaaff' : '#ffffff')),
k.opacity(0.15 + Math.random() * 0.4), k.z(1),
])
star.onUpdate(() => { star.opacity = 0.12 + Math.sin(k.time() * (0.8 + Math.random()) + i) * 0.15 })
}
- // Nebula clouds — large, colorful, layered
+ // Nebula clouds
const nebulaColors = ['#4400aa', '#aa0066', '#0044aa', '#006644', '#660044']
for (let i = 0; i < 5; i++) {
- for (let blob = 0; blob < 3; blob++) {
+ for (let blob = 0; blob < 4; blob++) {
k.add([
k.circle(30 + Math.random() * 50),
- k.pos(W * (0.1 + i * 0.2) + blob * 20, H * 0.25 + Math.random() * 60 + blob * 15),
- k.color(safeColor(k,nebulaColors[i % nebulaColors.length])),
- k.opacity(0.05 + blob * 0.015), k.z(0),
+ k.pos(W * (0.1 + i * 0.2) + blob * 18, H * 0.25 + Math.random() * 60 + blob * 12),
+ k.color(safeColor(k, nebulaColors[i % nebulaColors.length])),
+ k.opacity(0.04 + blob * 0.012), k.z(0),
])
}
}
- // Floating asteroids with rotation
- for (let i = 0; i < 5; i++) {
+ // Floating asteroids
+ for (let i = 0; i < 6; i++) {
const ast = k.add([
k.circle(4 + Math.random() * 10),
k.pos(Math.random() * W, 15 + Math.random() * (GROUND_Y - 30)),
- k.color(safeColor(k,Math.random() > 0.5 ? '#555555' : '#3a3a3a')), k.opacity(0.35), k.z(1),
+ k.color(safeColor(k, Math.random() > 0.5 ? '#555555' : '#3a3a3a')), k.opacity(0.35), k.z(1),
])
- // Crater dot
- k.add([k.circle(2), k.pos(ast.pos.x + 2, ast.pos.y - 1), k.color(safeColor(k,'#2a2a2a')), k.opacity(0.2), k.z(1)])
+ k.add([k.circle(2), k.pos(ast.pos.x + 2, ast.pos.y - 1), k.color(safeColor(k, '#2a2a2a')), k.opacity(0.2), k.z(1)])
const baseX = ast.pos.x, baseY = ast.pos.y
ast.onUpdate(() => {
ast.pos.x = baseX + Math.sin(k.time() * 0.15 + i * 2) * 18
ast.pos.y = baseY + Math.cos(k.time() * 0.12 + i) * 12
})
}
- // Distant planet
+ // Distant planet with rings
const planetX = W * 0.75, planetY = GROUND_Y * 0.25
- k.add([k.circle(20), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k,'#334466')), k.opacity(0.2), k.z(0)])
- k.add([k.rect(44, 3), k.pos(planetX - 22, planetY - 1), k.color(safeColor(k,'#556688')), k.opacity(0.12), k.z(0), k.rotate(15)])
+ k.add([k.circle(22), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#334466')), k.opacity(0.25), k.z(0)])
+ k.add([k.circle(24), k.pos(planetX, planetY), k.anchor('center'), k.color(safeColor(k, '#223344')), k.opacity(0.1), k.z(0)])
+ for (let r = 0; r < 3; r++) {
+ k.add([k.rect(50 + r * 8, 2), k.pos(planetX - 25 - r * 4, planetY - 1 + r), k.color(safeColor(k, '#556688')), k.opacity(0.1 - r * 0.02), k.z(0), k.rotate(15)])
+ }
+ // Shooting stars
+ for (let i = 0; i < 2; i++) {
+ const shootStar = k.add([
+ k.rect(12, 1), k.pos(-20, Math.random() * GROUND_Y * 0.5),
+ k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(2), k.rotate(30),
+ ])
+ let timer = Math.random() * 10
+ shootStar.onUpdate(() => {
+ timer -= k.dt()
+ if (timer <= 0) {
+ shootStar.pos.x = -20
+ shootStar.pos.y = Math.random() * GROUND_Y * 0.4
+ shootStar.opacity = 0.5
+ timer = 5 + Math.random() * 10
+ }
+ if (shootStar.opacity > 0) {
+ shootStar.pos.x += 200 * k.dt()
+ shootStar.pos.y += 80 * k.dt()
+ shootStar.opacity -= 0.8 * k.dt()
+ }
+ })
+ }
+
} else if (a === 'colosseum') {
// Roman arches along the back
for (let i = 0; i < 8; i++) {
const ax = 20 + i * (W / 8)
const aw = W / 8 - 8
- // Pillar left
- k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k,'#3a2a18')), k.opacity(0.4), k.z(1)])
- // Pillar right
- k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax + aw - 6, GROUND_Y * 0.2), k.color(safeColor(k,'#3a2a18')), k.opacity(0.4), k.z(1)])
- // Arch top
- k.add([k.rect(aw, 6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k,'#4a3a20')), k.opacity(0.4), k.z(1)])
- // Dark interior
- k.add([k.rect(aw - 12, GROUND_Y * 0.5), k.pos(ax + 6, GROUND_Y * 0.25), k.color(safeColor(k,'#0a0800')), k.opacity(0.3), k.z(0)])
+ k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(6, GROUND_Y * 0.6), k.pos(ax + aw - 6, GROUND_Y * 0.2), k.color(safeColor(k, '#3a2a18')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(aw, 6), k.pos(ax, GROUND_Y * 0.2), k.color(safeColor(k, '#4a3a20')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(aw - 12, GROUND_Y * 0.5), k.pos(ax + 6, GROUND_Y * 0.25), k.color(safeColor(k, '#0a0800')), k.opacity(0.3), k.z(0)])
+ k.add([k.rect(10, 3), k.pos(ax - 2, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)])
+ k.add([k.rect(10, 3), k.pos(ax + aw - 10, GROUND_Y * 0.2), k.color(safeColor(k, '#5a4a30')), k.opacity(0.35), k.z(1)])
+ }
+ // Torch brackets on pillars
+ for (let i = 0; i < 4; i++) {
+ const tx = W * (0.15 + i * 0.25)
+ const ty = GROUND_Y * 0.35
+ k.add([k.rect(3, 12), k.pos(tx, ty), k.color(safeColor(k, '#554422')), k.opacity(0.4), k.z(2)])
+ const flame = k.add([k.circle(5), k.pos(tx + 1, ty - 3), k.anchor('center'), k.color(safeColor(k, '#ff6600')), k.opacity(0.4), k.z(2)])
+ flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + i * 2) * 0.15 })
+ }
+ // Crowd silhouettes in upper tiers
+ for (let row = 0; row < 2; row++) {
+ for (let i = 0; i < 20; i++) {
+ k.add([
+ k.circle(2 + Math.random()), k.pos(15 + i * (W / 20), GROUND_Y * 0.08 + row * 12),
+ k.color(safeColor(k, '#2a1a10')), k.opacity(0.2), k.z(0),
+ ])
+ }
}
// Crumbled stone debris
- for (let i = 0; i < 6; i++) {
+ for (let i = 0; i < 8; i++) {
k.add([
k.rect(5 + Math.random() * 10, 3 + Math.random() * 6),
k.pos(Math.random() * W, GROUND_Y - 5 - Math.random() * 10),
- k.color(safeColor(k,'#5a4a30')), k.opacity(0.25), k.z(2), k.rotate(Math.random() * 40 - 20),
+ k.color(safeColor(k, '#5a4a30')), k.opacity(0.25), k.z(2), k.rotate(Math.random() * 40 - 20),
])
}
+ // Sand dust particles
+ for (let i = 0; i < 6; i++) {
+ const dust = k.add([
+ k.circle(2 + Math.random() * 3), k.pos(Math.random() * W, GROUND_Y - 15 - Math.random() * 20),
+ k.color(safeColor(k, '#c0a060')), k.opacity(0.1), k.z(3),
+ ])
+ const dx = dust.pos.x
+ dust.onUpdate(() => { dust.pos.x = dx + Math.sin(k.time() * 0.5 + i) * 12; dust.opacity = 0.05 + Math.sin(k.time() + i) * 0.05 })
+ }
+
} else if (a === 'haunted_mansion') {
- // Gothic windows
+ // Gothic windows with moonlight
for (let i = 0; i < 5; i++) {
const wx = W * (0.1 + i * 0.2)
const wy = GROUND_Y * 0.2
- k.add([k.rect(20, 35), k.pos(wx, wy), k.color(safeColor(k,'#0a0510')), k.opacity(0.4), k.z(1)])
- // Window frame
- k.add([k.rect(22, 1), k.pos(wx - 1, wy), k.color(safeColor(k,'#2a1830')), k.opacity(0.4), k.z(1)])
- k.add([k.rect(22, 1), k.pos(wx - 1, wy + 35), k.color(safeColor(k,'#2a1830')), k.opacity(0.4), k.z(1)])
- // Moonlight through window
- const glow = k.add([k.rect(16, 30), k.pos(wx + 2, wy + 3), k.color(safeColor(k,'#8844cc')), k.opacity(0.06), k.z(1)])
+ k.add([k.rect(20, 35), k.pos(wx, wy), k.color(safeColor(k, '#0a0510')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(22, 1), k.pos(wx - 1, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(22, 1), k.pos(wx - 1, wy + 35), k.color(safeColor(k, '#2a1830')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(1, 35), k.pos(wx + 10, wy), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)])
+ k.add([k.rect(20, 1), k.pos(wx, wy + 17), k.color(safeColor(k, '#2a1830')), k.opacity(0.3), k.z(1)])
+ const glow = k.add([k.rect(16, 30), k.pos(wx + 2, wy + 3), k.color(safeColor(k, '#8844cc')), k.opacity(0.06), k.z(1)])
glow.onUpdate(() => { glow.opacity = 0.04 + Math.sin(k.time() * 0.5 + i) * 0.03 })
}
- // Cobwebs in corners
+ // Moon behind a window
+ k.add([k.circle(12), k.pos(W * 0.5 + 10, GROUND_Y * 0.26), k.anchor('center'), k.color(safeColor(k, '#ccccdd')), k.opacity(0.15), k.z(0)])
+ // Cobwebs
for (const cx of [15, W - 35]) {
- for (let strand = 0; strand < 4; strand++) {
- k.add([k.rect(20 + strand * 5, 1), k.pos(cx, 5 + strand * 5), k.color(safeColor(k,'#666666')), k.opacity(0.12), k.z(1), k.rotate(cx < W / 2 ? 30 + strand * 5 : -30 - strand * 5)])
+ for (let strand = 0; strand < 5; strand++) {
+ k.add([k.rect(20 + strand * 5, 1), k.pos(cx, 3 + strand * 4), k.color(safeColor(k, '#666666')), k.opacity(0.1), k.z(1), k.rotate(cx < W / 2 ? 28 + strand * 5 : -28 - strand * 5)])
+ }
+ }
+ // Candelabras
+ for (const cx of [W * 0.25, W * 0.75]) {
+ k.add([k.rect(2, 20), k.pos(cx, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)])
+ k.add([k.rect(12, 2), k.pos(cx - 5, GROUND_Y - 20), k.color(safeColor(k, '#666644')), k.opacity(0.3), k.z(1)])
+ for (let c = 0; c < 3; c++) {
+ const candleX = cx - 5 + c * 5
+ const cFlame = k.add([k.circle(2.5), k.pos(candleX + 1, GROUND_Y - 23), k.anchor('center'), k.color(safeColor(k, '#ffaa44')), k.opacity(0.4), k.z(2)])
+ cFlame.onUpdate(() => { cFlame.opacity = 0.3 + Math.sin(k.time() * 4 + c + cx) * 0.15 })
}
}
// Floating ghost particles
- for (let i = 0; i < 6; i++) {
+ for (let i = 0; i < 8; i++) {
const ghost = k.add([
k.circle(3 + Math.random() * 3), k.pos(Math.random() * W, Math.random() * GROUND_Y),
- k.color(safeColor(k,'#aabbcc')), k.opacity(0), k.z(2),
+ k.color(safeColor(k, '#aabbcc')), k.opacity(0), k.z(2),
])
const gx = ghost.pos.x, gy = ghost.pos.y
ghost.onUpdate(() => {
@@ -1049,130 +1336,459 @@ export async function createFightScene(config: FightSceneConfig) {
ghost.pos.y = gy + Math.cos(k.time() * 0.25 + i) * 15
})
}
+ // Bats flying
+ for (let i = 0; i < 3; i++) {
+ const bat = k.add([k.rect(5, 2), k.pos(Math.random() * W, 15 + Math.random() * 40), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(2)])
+ const bx = bat.pos.x, by = bat.pos.y
+ bat.onUpdate(() => {
+ bat.pos.x = bx + Math.sin(k.time() * 1.5 + i * 3) * 40
+ bat.pos.y = by + Math.cos(k.time() * 2 + i * 2) * 10
+ })
+ }
+
} else if (a === 'concert_hall') {
- // Stage lights
+ // Stage lights with sweeping cones
const lightColors = ['#ff2288', '#22ff88', '#4488ff', '#ffaa22', '#ff44ff', '#44ffff']
for (let i = 0; i < 6; i++) {
const lx = W * (0.1 + i * 0.15)
- // Light cone from ceiling
const cone = k.add([
k.rect(30, GROUND_Y * 0.7), k.pos(lx, 10), k.anchor('top'),
- k.color(safeColor(k,lightColors[i])), k.opacity(0.04), k.z(2),
+ k.color(safeColor(k, lightColors[i])), k.opacity(0.04), k.z(2),
])
- // Light source dot
- k.add([k.circle(4), k.pos(lx + 15, 8), k.anchor('center'), k.color(safeColor(k,lightColors[i])), k.opacity(0.4), k.z(2)])
- cone.onUpdate(() => { cone.opacity = 0.02 + Math.sin(k.time() * 1.5 + i * 1.2) * 0.025 })
+ k.add([k.circle(4), k.pos(lx + 15, 8), k.anchor('center'), k.color(safeColor(k, lightColors[i])), k.opacity(0.4), k.z(2)])
+ const baseAngle = (i - 3) * 5
+ cone.onUpdate(() => {
+ cone.opacity = 0.02 + Math.sin(k.time() * 1.5 + i * 1.2) * 0.025
+ cone.angle = baseAngle + Math.sin(k.time() * 0.4 + i) * 8
+ })
}
- // Speaker stacks on sides
- for (const sx of [15, W - 35]) {
- for (let s = 0; s < 3; s++) {
- k.add([k.rect(20, 18), k.pos(sx, GROUND_Y - 20 - s * 20), k.color(safeColor(k,'#111111')), k.opacity(0.5), k.z(1)])
- k.add([k.circle(6), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k,'#222222')), k.opacity(0.4), k.z(1)])
+ // Speaker stacks
+ for (const sx of [10, W - 30]) {
+ for (let s = 0; s < 4; s++) {
+ k.add([k.rect(20, 18), k.pos(sx, GROUND_Y - 20 - s * 20), k.color(safeColor(k, '#111111')), k.opacity(0.5), k.z(1)])
+ k.add([k.circle(6), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#222222')), k.opacity(0.4), k.z(1)])
+ const speakerCone = k.add([k.circle(4), k.pos(sx + 10, GROUND_Y - 11 - s * 20), k.anchor('center'), k.color(safeColor(k, '#1a1a1a')), k.opacity(0.3), k.z(1)])
+ speakerCone.onUpdate(() => { speakerCone.scale = k.vec2(1 + Math.sin(k.time() * 8 + s) * 0.05) })
}
}
- } else if (a === 'colosseum' || a === 'sports_arena') {
- // Stadium seating tiers
- for (let tier = 0; tier < 4; tier++) {
- const ty = GROUND_Y * 0.15 + tier * (GROUND_Y * 0.15)
- k.add([k.rect(W, GROUND_Y * 0.12), k.pos(0, ty), k.color(safeColor(k,tier % 2 === 0 ? '#1a2a4a' : '#152040')), k.opacity(0.3), k.z(1)])
+ // Crowd silhouettes
+ for (let i = 0; i < 30; i++) {
+ const cx = 5 + i * (W / 30)
+ const ch = 6 + Math.random() * 4
+ k.add([k.circle(2.5), k.pos(cx, GROUND_Y + 8), k.anchor('center'), k.color(safeColor(k, '#111111')), k.opacity(0.3), k.z(3)])
+ k.add([k.rect(4, ch), k.pos(cx - 2, GROUND_Y + 10), k.color(safeColor(k, '#111111')), k.opacity(0.25), k.z(3)])
}
- } else if (a === 'kitchen_stadium') {
- // Kitchen equipment silhouettes
- // Stove
- k.add([k.rect(40, 30), k.pos(W * 0.08, GROUND_Y - 30), k.color(safeColor(k,'#1a1010')), k.opacity(0.4), k.z(1)])
- for (let b = 0; b < 4; b++) {
- const flame = k.add([k.circle(4), k.pos(W * 0.08 + 6 + b * 9, GROUND_Y - 32), k.anchor('center'), k.color(safeColor(k,'#ff4400')), k.opacity(0.4), k.z(2)])
- flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + b) * 0.15 })
+ // Smoke machine haze
+ for (let i = 0; i < 6; i++) {
+ const haze = k.add([
+ k.circle(30 + Math.random() * 25), k.pos(Math.random() * W, GROUND_Y - 8),
+ k.color(safeColor(k, '#20102a')), k.opacity(0.18), k.z(3),
+ ])
+ const hx = haze.pos.x
+ haze.onUpdate(() => { haze.pos.x = hx + Math.sin(k.time() * 0.15 + i) * 15 })
}
- // Hanging pots
- for (let i = 0; i < 5; i++) {
- const px = W * (0.2 + i * 0.15)
- k.add([k.rect(1, 15 + Math.random() * 10), k.pos(px, 5), k.color(safeColor(k,'#333333')), k.opacity(0.3), k.z(1)])
- k.add([k.circle(8), k.pos(px, 20 + Math.random() * 10), k.anchor('center'), k.color(safeColor(k,'#444444')), k.opacity(0.25), k.z(1)])
- }
- } else if (a === 'junkyard') {
- // Piles of scrap
- for (let pile = 0; pile < 6; pile++) {
- const px = Math.random() * W
- const py = GROUND_Y - 10
- for (let j = 0; j < 5; j++) {
+ // Laser beam sweep
+ const laser = k.add([k.rect(1, GROUND_Y), k.pos(W / 2, 0), k.color(safeColor(k, '#00ff00')), k.opacity(0.06), k.z(2)])
+ laser.onUpdate(() => { laser.pos.x = W / 2 + Math.sin(k.time() * 0.8) * (W * 0.4) })
+
+ } else if (a === 'sports_arena') {
+ // Stadium seating tiers with crowd
+ for (let tier = 0; tier < 5; tier++) {
+ const ty = GROUND_Y * 0.1 + tier * (GROUND_Y * 0.13)
+ k.add([k.rect(W, GROUND_Y * 0.1), k.pos(0, ty), k.color(safeColor(k, tier % 2 === 0 ? '#1a2a4a' : '#152040')), k.opacity(0.3), k.z(1)])
+ for (let i = 0; i < 18; i++) {
k.add([
- k.rect(8 + Math.random() * 15, 5 + Math.random() * 10),
- k.pos(px + (Math.random() - 0.5) * 20, py - j * 8 - Math.random() * 5),
- k.color(safeColor(k,['#554433', '#665544', '#443322', '#776655', '#332211'][j % 5])),
- k.opacity(0.3), k.z(1), k.rotate(Math.random() * 30 - 15),
+ k.circle(1.5 + Math.random()), k.pos(15 + i * (W / 18), ty + GROUND_Y * 0.04),
+ k.color(safeColor(k, ['#cc3333', '#3333cc', '#33cc33', '#cccc33', '#ffffff'][Math.floor(Math.random() * 5)])),
+ k.opacity(0.12), k.z(1),
])
}
}
+ // Floodlights
+ for (const fx of [W * 0.05, W * 0.95]) {
+ k.add([k.rect(3, GROUND_Y * 0.7), k.pos(fx, GROUND_Y * 0.1), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(2)])
+ const light = k.add([k.circle(5), k.pos(fx + 1, GROUND_Y * 0.08), k.anchor('center'), k.color(safeColor(k, '#ffffff')), k.opacity(0.5), k.z(2)])
+ k.add([k.rect(40, GROUND_Y * 0.6), k.pos(fx - 18, GROUND_Y * 0.12), k.color(safeColor(k, '#ffffff')), k.opacity(0.015), k.z(0)])
+ light.onUpdate(() => { light.opacity = 0.4 + Math.sin(k.time() * 0.5 + fx) * 0.1 })
+ }
+ // Scoreboard
+ k.add([k.rect(50, 18), k.pos(W / 2 - 25, 8), k.color(safeColor(k, '#0a0a20')), k.opacity(0.5), k.z(2)])
+ k.add([k.rect(48, 16), k.pos(W / 2 - 24, 9), k.color(safeColor(k, '#111133')), k.opacity(0.4), k.z(2)])
+ const scoreGlow = k.add([k.rect(20, 8), k.pos(W / 2 - 10, 13), k.color(safeColor(k, '#ff3333')), k.opacity(0.12), k.z(2)])
+ scoreGlow.onUpdate(() => { scoreGlow.opacity = 0.08 + Math.sin(k.time() * 2) * 0.05 })
+ // Camera flashes
+ for (let i = 0; i < 5; i++) {
+ const flash = k.add([
+ k.circle(2), k.pos(Math.random() * W, GROUND_Y * (0.15 + Math.random() * 0.5)),
+ k.color(safeColor(k, '#ffffff')), k.opacity(0), k.z(3),
+ ])
+ flash.onUpdate(() => { flash.opacity = Math.random() > 0.995 ? 0.4 : 0 })
+ }
+
+ } else if (a === 'kitchen_stadium') {
+ // Tiled wall
+ for (let row = 0; row < 6; row++) {
+ for (let col = 0; col < 14; col++) {
+ k.add([
+ k.rect(W / 14 - 1, GROUND_Y / 6 - 1), k.pos(col * (W / 14), row * (GROUND_Y / 6)),
+ k.color(safeColor(k, (row + col) % 2 === 0 ? '#1a1210' : '#1e1614')), k.opacity(0.2), k.z(0),
+ ])
+ }
+ }
+ // Stove with burners
+ k.add([k.rect(45, 35), k.pos(W * 0.06, GROUND_Y - 35), k.color(safeColor(k, '#1a1010')), k.opacity(0.45), k.z(1)])
+ k.add([k.rect(43, 2), k.pos(W * 0.06 + 1, GROUND_Y - 35), k.color(safeColor(k, '#333333')), k.opacity(0.3), k.z(1)])
+ for (let b = 0; b < 4; b++) {
+ const flame = k.add([k.circle(4), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 37), k.anchor('center'), k.color(safeColor(k, '#ff4400')), k.opacity(0.4), k.z(2)])
+ const innerFlame = k.add([k.circle(2), k.pos(W * 0.06 + 6 + b * 10, GROUND_Y - 38), k.anchor('center'), k.color(safeColor(k, '#ffcc00')), k.opacity(0.3), k.z(2)])
+ flame.onUpdate(() => { flame.opacity = 0.3 + Math.sin(k.time() * 5 + b) * 0.15; innerFlame.opacity = 0.2 + Math.sin(k.time() * 7 + b) * 0.1 })
+ }
+ // Hanging pots and pans
+ k.add([k.rect(W * 0.5, 2), k.pos(W * 0.2, 8), k.color(safeColor(k, '#444444')), k.opacity(0.3), k.z(1)])
+ for (let i = 0; i < 6; i++) {
+ const px = W * (0.22 + i * 0.08)
+ const hookLen = 12 + Math.random() * 10
+ k.add([k.rect(1, hookLen), k.pos(px, 10), k.color(safeColor(k, '#444444')), k.opacity(0.25), k.z(1)])
+ k.add([k.circle(7 + Math.random() * 4), k.pos(px, 10 + hookLen + 5), k.anchor('center'), k.color(safeColor(k, i % 2 === 0 ? '#555555' : '#664422')), k.opacity(0.25), k.z(1)])
+ }
+ // Knife rack
+ k.add([k.rect(3, 25), k.pos(W * 0.85, GROUND_Y * 0.3), k.color(safeColor(k, '#553322')), k.opacity(0.35), k.z(1)])
+ for (let i = 0; i < 4; i++) {
+ k.add([k.rect(2, 10 + Math.random() * 5), k.pos(W * 0.85 + 1, GROUND_Y * 0.32 + i * 5), k.color(safeColor(k, '#aaaaaa')), k.opacity(0.2), k.z(1)])
+ }
+ // Steam rising
+ for (let i = 0; i < 4; i++) {
+ const steam = k.add([
+ k.circle(5 + Math.random() * 5), k.pos(W * 0.06 + 10 + i * 10, GROUND_Y - 40),
+ k.color(safeColor(k, '#ffffff')), k.opacity(0.06), k.z(3),
+ ])
+ const bx = steam.pos.x
+ steam.onUpdate(() => {
+ steam.pos.y -= 10 * k.dt()
+ steam.pos.x = bx + Math.sin(k.time() * 2 + i) * 4
+ steam.opacity *= 0.995
+ if (steam.pos.y < GROUND_Y - 80) { steam.pos.y = GROUND_Y - 40; steam.opacity = 0.06 }
+ })
+ }
+
+ } else if (a === 'junkyard') {
+ // Rusted crane silhouette
+ k.add([k.rect(6, GROUND_Y * 0.7), k.pos(W * 0.85, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.3), k.z(0)])
+ k.add([k.rect(W * 0.3, 4), k.pos(W * 0.6, GROUND_Y * 0.1), k.color(safeColor(k, '#443322')), k.opacity(0.25), k.z(0)])
+ const cable = k.add([k.rect(1, 35), k.pos(W * 0.65, GROUND_Y * 0.12), k.color(safeColor(k, '#555555')), k.opacity(0.2), k.z(0)])
+ cable.onUpdate(() => { cable.pos.x = W * 0.65 + Math.sin(k.time() * 0.3) * 5 })
+ // Piles of scrap
+ for (let pile = 0; pile < 8; pile++) {
+ const px = Math.random() * W
+ const py = GROUND_Y - 8
+ for (let j = 0; j < 7; j++) {
+ k.add([
+ k.rect(8 + Math.random() * 18, 4 + Math.random() * 10),
+ k.pos(px + (Math.random() - 0.5) * 25, py - j * 6 - Math.random() * 5),
+ k.color(safeColor(k, ['#554433', '#665544', '#443322', '#776655', '#332211', '#887766', '#553322'][j % 7])),
+ k.opacity(0.3), k.z(1), k.rotate(Math.random() * 40 - 20),
+ ])
+ }
+ }
+ // Old car silhouette
+ k.add([k.rect(35, 15), k.pos(W * 0.15, GROUND_Y - 15), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.3), k.z(1)])
+ k.add([k.rect(25, 10), k.pos(W * 0.15 + 5, GROUND_Y - 25), k.color(safeColor(k, '#2a2a1a')), k.opacity(0.25), k.z(1)])
+ k.add([k.circle(5), k.pos(W * 0.15 + 6, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)])
+ k.add([k.circle(5), k.pos(W * 0.15 + 29, GROUND_Y - 3), k.anchor('center'), k.color(safeColor(k, '#1a1a0a')), k.opacity(0.3), k.z(1)])
+ // Sparking wires
+ for (let i = 0; i < 3; i++) {
+ const spark = k.add([
+ k.circle(2), k.pos(W * (0.3 + i * 0.2), GROUND_Y - 20 - Math.random() * 30),
+ k.color(safeColor(k, '#ffaa00')), k.opacity(0), k.z(3),
+ ])
+ spark.onUpdate(() => { spark.opacity = Math.random() > 0.96 ? 0.5 : 0 })
+ }
+ // Smoke/steam wisps
+ for (let i = 0; i < 4; i++) {
+ const smoke = k.add([
+ k.circle(10 + Math.random() * 10), k.pos(Math.random() * W, GROUND_Y - 5),
+ k.color(safeColor(k, '#333322')), k.opacity(0.12), k.z(2),
+ ])
+ const sx = smoke.pos.x
+ smoke.onUpdate(() => {
+ smoke.pos.y -= 5 * k.dt()
+ smoke.pos.x = sx + Math.sin(k.time() * 0.5 + i) * 6
+ if (smoke.pos.y < GROUND_Y - 60) { smoke.pos.y = GROUND_Y - 5; smoke.opacity = 0.12 }
+ })
+ }
+
} else if (a === 'meme_dimension') {
- // Floating emoji-like shapes
- for (let i = 0; i < 8; i++) {
+ // Floating emoji shapes (pulsing)
+ for (let i = 0; i < 12; i++) {
const emoji = k.add([
- k.circle(8 + Math.random() * 5), k.pos(Math.random() * W, Math.random() * GROUND_Y),
- k.color(safeColor(k,'#ffcc00')), k.opacity(0.15), k.z(1),
+ k.circle(6 + Math.random() * 6), k.pos(Math.random() * W, Math.random() * GROUND_Y),
+ k.color(safeColor(k, ['#ffcc00', '#ff4444', '#44ff44', '#4444ff', '#ff44ff', '#44ffff'][i % 6])), k.opacity(0.12), k.z(1),
])
const bx = emoji.pos.x, by = emoji.pos.y
emoji.onUpdate(() => {
emoji.pos.x = bx + Math.sin(k.time() * 0.4 + i) * 15
emoji.pos.y = by + Math.cos(k.time() * 0.3 + i) * 10
+ emoji.scale = k.vec2(1 + Math.sin(k.time() * 1.5 + i) * 0.15)
})
}
// Glitch bands
- for (let i = 0; i < 5; i++) {
+ for (let i = 0; i < 8; i++) {
const band = k.add([
- k.rect(W, 2 + Math.random() * 3), k.pos(0, Math.random() * GROUND_Y),
- k.color(safeColor(k,Math.random() > 0.5 ? '#ff44ff' : '#44ffff')), k.opacity(0), k.z(2),
+ k.rect(W, 2 + Math.random() * 4), k.pos(0, Math.random() * GROUND_Y),
+ k.color(safeColor(k, Math.random() > 0.5 ? '#ff44ff' : '#44ffff')), k.opacity(0), k.z(2),
])
- band.onUpdate(() => { band.opacity = Math.random() > 0.97 ? 0.15 : 0 })
+ band.onUpdate(() => {
+ band.opacity = Math.random() > 0.97 ? 0.12 + Math.random() * 0.08 : 0
+ if (band.opacity > 0) band.pos.x = (Math.random() - 0.5) * 10
+ else band.pos.x = 0
+ })
}
- } else if (a === 'space_station') {
- // Structural beams
+ // Rainbow pixel streaks
+ const rainbowC = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff']
+ for (let i = 0; i < rainbowC.length; i++) {
+ const streak = k.add([
+ k.rect(W, 3), k.pos(0, GROUND_Y * 0.3 + i * 5),
+ k.color(safeColor(k, rainbowC[i])), k.opacity(0.04), k.z(0),
+ ])
+ streak.onUpdate(() => { streak.opacity = 0.02 + Math.sin(k.time() * 2 + i) * 0.02 })
+ }
+ // Spinning geometric shapes
+ for (let i = 0; i < 4; i++) {
+ const geo = k.add([
+ k.rect(8 + Math.random() * 8, 8 + Math.random() * 8),
+ k.pos(W * (0.2 + i * 0.2), GROUND_Y * (0.3 + Math.random() * 0.3)),
+ k.anchor('center'), k.color(safeColor(k, '#ff44ff')), k.opacity(0.08), k.z(1), k.rotate(0),
+ ])
+ geo.onUpdate(() => { geo.angle += (30 + i * 15) * k.dt() })
+ }
+ // VHS tracking lines
+ const vhs = k.add([k.rect(W, 6), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(3)])
+ vhs.onUpdate(() => {
+ vhs.pos.y = (vhs.pos.y + 80 * k.dt()) % (GROUND_Y + 20)
+ vhs.opacity = Math.random() > 0.9 ? 0.15 : 0
+ })
+ // RGB noise flickers
for (let i = 0; i < 6; i++) {
- const bx = W * (0.15 + i * 0.15)
- k.add([k.rect(4, GROUND_Y), k.pos(bx, 0), k.color(safeColor(k,'#1a1a30')), k.opacity(0.3), k.z(1)])
- // Cross beams
- k.add([k.rect(W * 0.15, 3), k.pos(bx, GROUND_Y * 0.3), k.color(safeColor(k,'#1a1a30')), k.opacity(0.25), k.z(1)])
- }
- // Viewport (window to space)
- const vpX = W * 0.5, vpY = GROUND_Y * 0.3
- k.add([k.circle(35), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k,'#000020')), k.opacity(0.5), k.z(0)])
- k.add([k.circle(37), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k,'#2a2a50')), k.opacity(0.3), k.z(0)])
- // Stars through viewport
- for (let s = 0; s < 8; s++) {
- k.add([
- k.circle(1), k.pos(vpX + (Math.random() - 0.5) * 50, vpY + (Math.random() - 0.5) * 50),
- k.color(safeColor(k,'#ffffff')), k.opacity(0.3), k.z(0),
+ const noise = k.add([
+ k.rect(3 + Math.random() * 8, 3 + Math.random() * 8),
+ k.pos(Math.random() * W, Math.random() * GROUND_Y),
+ k.color(safeColor(k, ['#ff0000', '#00ff00', '#0000ff'][i % 3])), k.opacity(0), k.z(2),
])
+ noise.onUpdate(() => {
+ noise.opacity = Math.random() > 0.98 ? 0.1 : 0
+ if (noise.opacity > 0) { noise.pos.x = Math.random() * W; noise.pos.y = Math.random() * GROUND_Y }
+ })
}
+
+ } else if (a === 'space_station') {
+ // Structural beams with rivets
+ for (let i = 0; i < 6; i++) {
+ const bx = W * (0.15 + i * 0.14)
+ k.add([k.rect(5, GROUND_Y), k.pos(bx, 0), k.color(safeColor(k, '#1a1a30')), k.opacity(0.3), k.z(1)])
+ for (let r = 0; r < 8; r++) {
+ k.add([k.circle(1), k.pos(bx + 2, r * (GROUND_Y / 8) + 5), k.anchor('center'), k.color(safeColor(k, '#2a2a40')), k.opacity(0.25), k.z(1)])
+ }
+ k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.25), k.color(safeColor(k, '#1a1a30')), k.opacity(0.25), k.z(1)])
+ k.add([k.rect(W * 0.14, 3), k.pos(bx, GROUND_Y * 0.6), k.color(safeColor(k, '#1a1a30')), k.opacity(0.2), k.z(1)])
+ }
+ // Viewport
+ const vpX = W * 0.5, vpY = GROUND_Y * 0.3
+ k.add([k.circle(40), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#000020')), k.opacity(0.5), k.z(0)])
+ k.add([k.circle(42), k.pos(vpX, vpY), k.anchor('center'), k.color(safeColor(k, '#2a2a50')), k.opacity(0.3), k.z(0)])
+ for (let s = 0; s < 15; s++) {
+ const dist = Math.random() * 35
+ const angle = Math.random() * Math.PI * 2
+ const sx = vpX + Math.cos(angle) * dist
+ const sy = vpY + Math.sin(angle) * dist
+ const vpStar = k.add([k.circle(0.8 + Math.random()), k.pos(sx, sy), k.color(safeColor(k, '#ffffff')), k.opacity(0.3), k.z(0)])
+ vpStar.onUpdate(() => { vpStar.opacity = 0.15 + Math.sin(k.time() * 2 + s) * 0.15 })
+ }
+ // Earth through viewport
+ k.add([k.circle(15), k.pos(vpX + 10, vpY + 5), k.anchor('center'), k.color(safeColor(k, '#2244aa')), k.opacity(0.15), k.z(0)])
+ k.add([k.circle(6), k.pos(vpX + 7, vpY + 2), k.anchor('center'), k.color(safeColor(k, '#22aa44')), k.opacity(0.08), k.z(0)])
+ // Control panels
+ for (const px of [W * 0.08, W * 0.88]) {
+ k.add([k.rect(25, 40), k.pos(px, GROUND_Y * 0.4), k.color(safeColor(k, '#0a0a20')), k.opacity(0.4), k.z(1)])
+ for (let led = 0; led < 8; led++) {
+ const ledEl = k.add([
+ k.rect(2, 2), k.pos(px + 4 + (led % 4) * 5, GROUND_Y * 0.42 + Math.floor(led / 4) * 6),
+ k.color(safeColor(k, ['#00ff88', '#ff4444', '#ffaa00', '#4488ff'][led % 4])), k.opacity(0.4), k.z(2),
+ ])
+ ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.05 ? 0.4 : 0.1 })
+ }
+ }
+ // Warning stripes
+ for (let i = 0; i < 8; i++) {
+ k.add([k.rect(8, 3), k.pos(W * 0.3 + i * 12, GROUND_Y - 3), k.color(safeColor(k, i % 2 === 0 ? '#ffaa00' : '#000000')), k.opacity(0.12), k.z(3)])
+ }
+
} else if (a === 'savanna') {
- // Acacia tree silhouettes
- for (const tx of [W * 0.12, W * 0.7]) {
- k.add([k.rect(5, 50), k.pos(tx, GROUND_Y - 50), k.color(safeColor(k,'#2a1a08')), k.opacity(0.4), k.z(1)])
- // Flat canopy
- k.add([k.rect(40, 5), k.pos(tx - 17, GROUND_Y - 52), k.color(safeColor(k,'#2a3a10')), k.opacity(0.3), k.z(1)])
- k.add([k.rect(30, 4), k.pos(tx - 12, GROUND_Y - 57), k.color(safeColor(k,'#2a3a10')), k.opacity(0.25), k.z(1)])
+ // Distant mountain range
+ for (let i = 0; i < 5; i++) {
+ const mx = W * (0.1 + i * 0.2) + (Math.random() - 0.5) * 30
+ const mh = 30 + Math.random() * 40
+ k.add([k.circle(mh), k.pos(mx, GROUND_Y - mh * 0.3), k.anchor('center'), k.color(safeColor(k, '#2a2010')), k.opacity(0.12), k.z(0)])
+ }
+ // Acacia trees with branches
+ for (const tx of [W * 0.1, W * 0.4, W * 0.72]) {
+ const trunkH = 40 + Math.random() * 20
+ k.add([k.rect(5, trunkH), k.pos(tx, GROUND_Y - trunkH), k.color(safeColor(k, '#2a1a08')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(15, 3), k.pos(tx - 10, GROUND_Y - trunkH * 0.7), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(-20)])
+ k.add([k.rect(12, 3), k.pos(tx + 5, GROUND_Y - trunkH * 0.6), k.color(safeColor(k, '#2a1a08')), k.opacity(0.3), k.z(1), k.rotate(15)])
+ k.add([k.rect(50, 5), k.pos(tx - 22, GROUND_Y - trunkH - 2), k.color(safeColor(k, '#2a3a10')), k.opacity(0.3), k.z(1)])
+ k.add([k.rect(40, 4), k.pos(tx - 17, GROUND_Y - trunkH - 7), k.color(safeColor(k, '#2a3a10')), k.opacity(0.25), k.z(1)])
+ k.add([k.rect(25, 3), k.pos(tx - 10, GROUND_Y - trunkH - 11), k.color(safeColor(k, '#223308')), k.opacity(0.2), k.z(1)])
}
// Setting sun
- k.add([k.circle(25), k.pos(W * 0.85, GROUND_Y * 0.25), k.anchor('center'), k.color(safeColor(k,'#dd6622')), k.opacity(0.25), k.z(0)])
- k.add([k.circle(35), k.pos(W * 0.85, GROUND_Y * 0.25), k.anchor('center'), k.color(safeColor(k,'#dd6622')), k.opacity(0.08), k.z(0)])
+ const sunX = W * 0.85, sunY = GROUND_Y * 0.22
+ k.add([k.circle(50), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.06), k.z(0)])
+ k.add([k.circle(35), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#dd6622')), k.opacity(0.12), k.z(0)])
+ const savSun = k.add([k.circle(22), k.pos(sunX, sunY), k.anchor('center'), k.color(safeColor(k, '#ffaa22')), k.opacity(0.3), k.z(0)])
+ savSun.onUpdate(() => { savSun.opacity = 0.25 + Math.sin(k.time() * 0.3) * 0.05 })
+ // Tall grass swaying
+ for (let i = 0; i < 20; i++) {
+ const gx = Math.random() * W
+ const grassH = 5 + Math.random() * 10
+ const grass = k.add([k.rect(1, grassH), k.pos(gx, GROUND_Y - grassH), k.color(safeColor(k, '#8a7830')), k.opacity(0.2), k.z(3)])
+ grass.onUpdate(() => { grass.angle = Math.sin(k.time() * 1.2 + i) * 5 })
+ }
+ // Elephant silhouette
+ const ex = W * 0.6, ey = GROUND_Y - 18
+ k.add([k.rect(18, 12), k.pos(ex, ey), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
+ k.add([k.rect(8, 8), k.pos(ex + 15, ey - 4), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
+ k.add([k.rect(3, 8), k.pos(ex + 2, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
+ k.add([k.rect(3, 8), k.pos(ex + 13, ey + 12), k.color(safeColor(k, '#1a1508')), k.opacity(0.15), k.z(0)])
+ // Birds in V formation
+ for (let i = 0; i < 5; i++) {
+ const birdX = W * 0.3 + i * 8
+ const birdY = 20 + Math.abs(i - 2) * 5
+ k.add([k.rect(4, 1), k.pos(birdX, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(-12)])
+ k.add([k.rect(4, 1), k.pos(birdX + 3, birdY), k.color(safeColor(k, '#222222')), k.opacity(0.2), k.z(1), k.rotate(12)])
+ }
+
} else if (a === 'server_room') {
- // Server rack rows (perspective)
+ // Server rack rows with LEDs
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 6; col++) {
const rx = 30 + col * (W / 6)
- const ry = 25 + row * 50
+ const ry = 20 + row * 55
const rw = W / 6 - 12
- k.add([k.rect(rw, 42), k.pos(rx, ry), k.color(safeColor(k,'#0a1a10')), k.opacity(0.4), k.z(1)])
- // Status LEDs
+ const rh = 46
+ k.add([k.rect(rw, rh), k.pos(rx, ry), k.color(safeColor(k, '#0a1a10')), k.opacity(0.4), k.z(1)])
+ k.add([k.rect(rw, 1), k.pos(rx, ry), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
+ k.add([k.rect(rw, 1), k.pos(rx, ry + rh), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
+ for (let s = 0; s < 5; s++) {
+ k.add([k.rect(rw - 4, 1), k.pos(rx + 2, ry + 10 + s * 7), k.color(safeColor(k, '#0a1008')), k.opacity(0.3), k.z(1)])
+ }
for (let led = 0; led < 4; led++) {
const ledEl = k.add([
k.rect(2, 2), k.pos(rx + 3 + led * 5, ry + 3),
- k.color(safeColor(k,'#00ff88')), k.opacity(0.5), k.z(2),
+ k.color(safeColor(k, '#00ff88')), k.opacity(0.5), k.z(2),
])
ledEl.onUpdate(() => { ledEl.opacity = Math.random() > 0.04 ? 0.5 : 0.1 })
}
+ if (Math.random() < 0.5) {
+ const act = k.add([k.rect(2, 2), k.pos(rx + rw - 6, ry + 3), k.color(safeColor(k, '#ffaa00')), k.opacity(0.5), k.z(2)])
+ act.onUpdate(() => { act.opacity = Math.random() > 0.3 ? 0.5 : 0.15 })
+ }
}
}
+ // Overhead cable trays
+ for (let i = 0; i < 3; i++) {
+ k.add([k.rect(W * 0.25, 3), k.pos(W * (0.1 + i * 0.3), 5), k.color(safeColor(k, '#1a2a18')), k.opacity(0.3), k.z(1)])
+ for (let c = 0; c < 4; c++) {
+ k.add([k.rect(W * 0.25, 1), k.pos(W * (0.1 + i * 0.3), 7 + c * 2), k.color(safeColor(k, ['#004400', '#000044', '#440000', '#444400'][c])), k.opacity(0.15), k.z(1)])
+ }
+ }
+ // Cool air flow particles
+ for (let i = 0; i < 5; i++) {
+ const airflow = k.add([
+ k.circle(2), k.pos(Math.random() * W, GROUND_Y - 10),
+ k.color(safeColor(k, '#00ff88')), k.opacity(0.08), k.z(3),
+ ])
+ airflow.onUpdate(() => {
+ airflow.pos.y -= 15 * k.dt()
+ if (airflow.pos.y < 10) { airflow.pos.y = GROUND_Y - 10; airflow.pos.x = Math.random() * W }
+ })
+ }
+ // Emergency lighting
+ const emLight = k.add([k.circle(4), k.pos(W * 0.5, 5), k.anchor('center'), k.color(safeColor(k, '#ff0000')), k.opacity(0.2), k.z(2)])
+ emLight.onUpdate(() => { emLight.opacity = 0.1 + Math.sin(k.time() * 1.5) * 0.1 })
+
+ } else if (a === 'paper_mill') {
+ // Industrial ceiling pipes
+ for (let i = 0; i < 5; i++) {
+ const py = 5 + i * 4
+ k.add([k.rect(W, 3), k.pos(0, py), k.color(safeColor(k, i % 2 === 0 ? '#2a2a20' : '#222218')), k.opacity(0.35), k.z(1)])
+ for (let j = 0; j < 4; j++) {
+ k.add([k.circle(3), k.pos(W * (0.2 + j * 0.2), py + 1), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)])
+ }
+ }
+ // Conveyor belt
+ k.add([k.rect(W * 0.6, 6), k.pos(W * 0.2, GROUND_Y - 15), k.color(safeColor(k, '#333328')), k.opacity(0.35), k.z(1)])
+ for (let i = 0; i < 8; i++) {
+ k.add([k.circle(3), k.pos(W * 0.22 + i * (W * 0.075), GROUND_Y - 12), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.3), k.z(1)])
+ }
+ // Moving items on belt
+ for (let i = 0; i < 3; i++) {
+ const item = k.add([
+ k.rect(10, 6), k.pos(W * 0.25 + i * 60, GROUND_Y - 21),
+ k.color(safeColor(k, '#f0e68c')), k.opacity(0.2), k.z(2),
+ ])
+ item.onUpdate(() => {
+ item.pos.x += 15 * k.dt()
+ if (item.pos.x > W * 0.78) item.pos.x = W * 0.2
+ })
+ }
+ // Paper rolls stacked
+ for (let i = 0; i < 3; i++) {
+ for (let j = 0; j <= i; j++) {
+ const rx = W * 0.08 + j * 14 - i * 7
+ const ry = GROUND_Y - 12 - (2 - i) * 12
+ k.add([k.circle(6), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#ddd8b0')), k.opacity(0.2), k.z(1)])
+ k.add([k.circle(2), k.pos(rx, ry), k.anchor('center'), k.color(safeColor(k, '#aaa880')), k.opacity(0.15), k.z(1)])
+ }
+ }
+ // Industrial gears (rotating)
+ for (const gx of [W * 0.88, W * 0.92]) {
+ const gy = GROUND_Y * 0.35
+ k.add([k.circle(10), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1)])
+ k.add([k.circle(4), k.pos(gx, gy), k.anchor('center'), k.color(safeColor(k, '#333328')), k.opacity(0.3), k.z(1)])
+ for (let t = 0; t < 8; t++) {
+ const tooth = k.add([
+ k.rect(4, 3), k.pos(gx, gy), k.anchor('center'),
+ k.color(safeColor(k, '#444438')), k.opacity(0.25), k.z(1),
+ k.rotate(t * 45),
+ ])
+ tooth.onUpdate(() => { tooth.angle = t * 45 + k.time() * (gx === W * 0.88 ? 20 : -25) })
+ }
+ }
+ // Steam from pipes
+ for (let i = 0; i < 4; i++) {
+ const steam = k.add([
+ k.circle(6 + Math.random() * 8), k.pos(W * (0.2 + i * 0.2), 25),
+ k.color(safeColor(k, '#ccccbb')), k.opacity(0.06), k.z(2),
+ ])
+ const bsx = steam.pos.x
+ steam.onUpdate(() => {
+ steam.pos.y += 4 * k.dt()
+ steam.pos.x = bsx + Math.sin(k.time() + i) * 5
+ steam.opacity *= 0.998
+ if (steam.pos.y > GROUND_Y * 0.4) { steam.pos.y = 25; steam.opacity = 0.06 }
+ })
+ }
+ // Sawdust particles
+ for (let i = 0; i < 8; i++) {
+ const dust = k.add([
+ k.circle(1), k.pos(Math.random() * W, Math.random() * GROUND_Y),
+ k.color(safeColor(k, '#f0e68c')), k.opacity(0.12), k.z(3),
+ ])
+ const dx = dust.pos.x, dy = dust.pos.y
+ dust.onUpdate(() => {
+ dust.pos.x = dx + Math.sin(k.time() * 0.8 + i) * 10
+ dust.pos.y = dy + Math.cos(k.time() * 0.5 + i) * 8
+ })
+ }
}
}
diff --git a/frontend/src/game/sounds.ts b/frontend/src/game/sounds.ts
index 2435b5e..1af4d49 100644
--- a/frontend/src/game/sounds.ts
+++ b/frontend/src/game/sounds.ts
@@ -335,11 +335,11 @@ const ROUND_HYPE = [
// Mortal Kombat style dramatic calls
export function announceFinishHim() {
- speak('Finish him!', 'announcer', true, true)
+ speak('Finish it!', 'announcer', true, true)
}
-export function announceFatality() {
- speak('Fatality!', 'deep', true, true)
+export function announceFatality(tagline?: string) {
+ speak(tagline || 'Fatality!', 'deep', true, true)
}
export function announceFlawlessVictory() {
diff --git a/frontend/src/game/sprites/human.ts b/frontend/src/game/sprites/human.ts
new file mode 100644
index 0000000..67ab00e
--- /dev/null
+++ b/frontend/src/game/sprites/human.ts
@@ -0,0 +1,866 @@
+// Human sprite — 3x resolution for detailed pixel art of the bot's owner
+// Renders at 144x144 internal grid (vs 48x48 for bots) for crisp detail
+
+export const HUMAN_FRAME_SIZE = 288
+export const HUMAN_INTERNAL = 144
+export const HUMAN_SCALE = HUMAN_FRAME_SIZE / HUMAN_INTERNAL
+
+export const HUMAN_ANIMATIONS = {
+ idle: { frames: 4, row: 0 },
+ cheer: { frames: 4, row: 1 },
+ panic: { frames: 4, row: 2 },
+ run: { frames: 4, row: 3 },
+ coach: { frames: 4, row: 4 },
+}
+export const HUMAN_ROWS = Object.keys(HUMAN_ANIMATIONS).length
+export const HUMAN_MAX_FRAMES = 4
+
+// Costume system
+type CostumeType = 'animal' | 'armor' | 'hat' | 'tech' | 'monster' | 'food' | 'silly' | 'generic'
+
+interface CostumeConfig {
+ type: CostumeType
+ hat?: string
+ hatShape: 'none' | 'ears' | 'helmet' | 'pointy' | 'wide' | 'tall' | 'horns' | 'antenna' | 'crown' | 'cone'
+ prop?: string
+ propShape: 'none' | 'sword' | 'wand' | 'phone' | 'sign' | 'shield' | 'food' | 'remote'
+ onesie: boolean
+ facePaint?: string
+ tail: boolean
+}
+
+const COSTUME_MAP: Record
> = {
+ dog: { type: 'animal', hatShape: 'ears', hat: '#aa7744', onesie: true, tail: true },
+ cat: { type: 'animal', hatShape: 'ears', hat: '#ff8844', onesie: true, tail: true },
+ frog: { type: 'animal', hatShape: 'ears', hat: '#44aa22', onesie: true },
+ shark: { type: 'animal', hatShape: 'tall', hat: '#4477aa', onesie: true },
+ penguin: { type: 'animal', hatShape: 'none', hat: '#222222', onesie: true },
+ octopus: { type: 'animal', hatShape: 'none', hat: '#cc44aa', onesie: true },
+ bee: { type: 'animal', hatShape: 'antenna', hat: '#ffcc00', onesie: true },
+ snail: { type: 'animal', hatShape: 'antenna', hat: '#88aa44', onesie: true },
+ lobster: { type: 'animal', hatShape: 'antenna', hat: '#cc2222', onesie: true },
+ sheep: { type: 'animal', hatShape: 'ears', hat: '#eeeeee', onesie: true },
+ elephant: { type: 'animal', hatShape: 'ears', hat: '#888899', onesie: true },
+ giraffe: { type: 'animal', hatShape: 'horns', hat: '#ddaa44', onesie: true },
+ hippo: { type: 'animal', hatShape: 'ears', hat: '#8877aa', onesie: true },
+ lion: { type: 'animal', hatShape: 'ears', hat: '#cc8822', onesie: true },
+ monkey: { type: 'animal', hatShape: 'ears', hat: '#884422', onesie: true, tail: true },
+ parrot: { type: 'animal', hatShape: 'tall', hat: '#ff4422', onesie: true, tail: true },
+ raccoon: { type: 'animal', hatShape: 'ears', hat: '#666666', onesie: true, tail: true },
+ snake: { type: 'animal', hatShape: 'none', hat: '#448822', onesie: true, tail: true },
+ turtle: { type: 'animal', hatShape: 'none', hat: '#44aa44', onesie: true },
+ whale: { type: 'animal', hatShape: 'none', hat: '#4466bb', onesie: true },
+ crocodile: { type: 'animal', hatShape: 'none', hat: '#447722', onesie: true, tail: true },
+ flamingo: { type: 'animal', hatShape: 'tall', hat: '#ff66aa', onesie: true },
+ hedgehog: { type: 'animal', hatShape: 'tall', hat: '#886644', onesie: true },
+ panda: { type: 'animal', hatShape: 'ears', hat: '#222222', onesie: true },
+ hamster: { type: 'animal', hatShape: 'ears', hat: '#ddaa77', onesie: true },
+ dinosaur: { type: 'animal', hatShape: 'horns', hat: '#44aa44', onesie: true, tail: true },
+ knight: { type: 'armor', hatShape: 'helmet', hat: '#aaaaaa', propShape: 'sword', prop: '#cccccc' },
+ gladiator: { type: 'armor', hatShape: 'helmet', hat: '#cc8833', propShape: 'sword', prop: '#dddddd' },
+ samurai: { type: 'armor', hatShape: 'helmet', hat: '#882222', propShape: 'sword', prop: '#cccccc' },
+ viking: { type: 'armor', hatShape: 'horns', hat: '#886633', propShape: 'shield', prop: '#aa8844' },
+ boxer: { type: 'armor', hatShape: 'none', propShape: 'none', facePaint: '#cc2222' },
+ wrestler: { type: 'armor', hatShape: 'none', propShape: 'none', facePaint: '#ffcc00' },
+ pirate: { type: 'hat', hatShape: 'wide', hat: '#222222', propShape: 'sword', prop: '#dddddd' },
+ cowboy: { type: 'hat', hatShape: 'wide', hat: '#aa7744' },
+ wizard: { type: 'hat', hatShape: 'pointy', hat: '#4422aa', propShape: 'wand', prop: '#ffcc44' },
+ witch: { type: 'hat', hatShape: 'pointy', hat: '#222222', propShape: 'wand', prop: '#44ff44' },
+ chef: { type: 'hat', hatShape: 'tall', hat: '#ffffff', propShape: 'food', prop: '#ff8844' },
+ detective: { type: 'hat', hatShape: 'wide', hat: '#554433' },
+ nurse: { type: 'hat', hatShape: 'none', hat: '#ffffff' },
+ firefighter:{ type: 'hat', hatShape: 'helmet', hat: '#ff2222' },
+ astronaut: { type: 'hat', hatShape: 'helmet', hat: '#eeeeee' },
+ lumberjack: { type: 'hat', hatShape: 'none', hat: '#cc4422', propShape: 'sword', prop: '#886633' },
+ scientist: { type: 'hat', hatShape: 'none', propShape: 'remote', prop: '#44cc44' },
+ clown: { type: 'hat', hatShape: 'tall', hat: '#ff2222', facePaint: '#ffffff' },
+ robot: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ android: { type: 'tech', hatShape: 'antenna', propShape: 'phone' },
+ drone_bug: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ toaster: { type: 'tech', hatShape: 'none', propShape: 'remote' },
+ tv_head: { type: 'tech', hatShape: 'none', propShape: 'phone', facePaint: '#4488ff' },
+ calculator: { type: 'tech', hatShape: 'none', propShape: 'phone' },
+ satellite: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ mech: { type: 'tech', hatShape: 'helmet', hat: '#888888', propShape: 'remote' },
+ led_cube: { type: 'tech', hatShape: 'none', propShape: 'phone' },
+ circuit: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ antenna_bug:{ type: 'tech', hatShape: 'antenna', propShape: 'phone' },
+ microwave: { type: 'tech', hatShape: 'none', propShape: 'remote' },
+ cyberdog: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ robocat: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ ufo_bot: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ cyborg: { type: 'tech', hatShape: 'antenna', propShape: 'remote' },
+ vampire: { type: 'monster', hatShape: 'none', facePaint: '#ffffff' },
+ werewolf: { type: 'monster', hatShape: 'ears', hat: '#664422', facePaint: '#664422' },
+ zombie: { type: 'monster', hatShape: 'none', facePaint: '#668844' },
+ demon: { type: 'monster', hatShape: 'horns', hat: '#cc2222', facePaint: '#cc2222' },
+ skeleton: { type: 'monster', hatShape: 'none', facePaint: '#ffffff' },
+ ghost: { type: 'monster', hatShape: 'none', onesie: true, hat: '#ffffff' },
+ alien: { type: 'monster', hatShape: 'antenna', hat: '#44ff44', facePaint: '#44ff44' },
+ minotaur: { type: 'monster', hatShape: 'horns', hat: '#884422' },
+ unicorn: { type: 'monster', hatShape: 'horns', hat: '#ff88cc' },
+ phoenix: { type: 'monster', hatShape: 'tall', hat: '#ff4400', onesie: true },
+ dragon: { type: 'monster', hatShape: 'horns', hat: '#44aa22', onesie: true, tail: true },
+ mermaid: { type: 'monster', hatShape: 'none', hat: '#44ccaa', onesie: true },
+ griffin: { type: 'monster', hatShape: 'ears', hat: '#cc8822', onesie: true },
+ cyclops: { type: 'monster', hatShape: 'none', facePaint: '#8877aa' },
+ gargoyle: { type: 'monster', hatShape: 'horns', hat: '#666666', facePaint: '#888888' },
+ golem: { type: 'monster', hatShape: 'none', facePaint: '#886644' },
+ pizza: { type: 'food', hatShape: 'cone', hat: '#ffcc44' },
+ mushroom: { type: 'food', hatShape: 'tall', hat: '#cc2222' },
+ cactus: { type: 'food', hatShape: 'tall', hat: '#44aa22' },
+ sock_puppet: { type: 'silly', hatShape: 'none', onesie: true, hat: '#ff8844' },
+ traffic_cone: { type: 'silly', hatShape: 'cone', hat: '#ff6600' },
+ toilet_man: { type: 'silly', hatShape: 'none', propShape: 'sign', prop: '#ffffff' },
+ potato: { type: 'silly', hatShape: 'none', hat: '#aa8844', onesie: true },
+ cloud_man: { type: 'silly', hatShape: 'none', hat: '#ffffff', onesie: true },
+ rock_man: { type: 'silly', hatShape: 'none', hat: '#888888', onesie: true },
+ balloon_man: { type: 'silly', hatShape: 'none', propShape: 'sign', prop: '#ff4488' },
+ trash_can: { type: 'silly', hatShape: 'none', hat: '#888888', onesie: true },
+ rubber_duck: { type: 'silly', hatShape: 'none', hat: '#ffcc00', onesie: true },
+ snowman: { type: 'silly', hatShape: 'tall', hat: '#222222', onesie: true },
+ scarecrow: { type: 'silly', hatShape: 'wide', hat: '#ddbb66', onesie: true },
+ jack_o_lantern:{ type: 'silly', hatShape: 'none', hat: '#ff8800', facePaint: '#ff8800' },
+ garden_gnome: { type: 'silly', hatShape: 'pointy', hat: '#ff0000' },
+ lamp_post: { type: 'silly', hatShape: 'cone', hat: '#ffee88' },
+ broom_man: { type: 'silly', hatShape: 'none', propShape: 'sword', prop: '#886633' },
+ ninja: { type: 'hat', hatShape: 'none', facePaint: '#222222', onesie: true, hat: '#222222' },
+}
+
+const SKIN_TONES = [
+ '#ffdbb4', '#f1c27d', '#e0ac69', '#c68642', '#8d5524', '#6b3a1f',
+ '#ffe0bd', '#ffcd94', '#eac086', '#deb887', '#d2a679', '#a0785a',
+]
+
+const HAIR_COLORS = [
+ '#2c1b0e', '#4a3728', '#8b6914', '#c4a35a', '#e6c87f',
+ '#1a1a1a', '#333333', '#cc2222', '#ff6633', '#ff88cc', '#4488ff',
+]
+
+type PxFn = (x: number, y: number, color: string, ox: number, oy: number) => void
+
+function seededRng(seed: string) {
+ let sh = 0
+ for (let i = 0; i < seed.length; i++) sh = ((sh << 5) - sh + seed.charCodeAt(i)) | 0
+ sh = (sh * 31337) | 0
+ return () => { sh = (sh * 16807) % 2147483647; return (sh & 0x7fffffff) / 2147483647 }
+}
+
+/**
+ * Generate human sprite at 3x resolution. winRate (0-1) controls physique:
+ * 0.0 = fat slobby mess with cheetos, 0.5 = normal, 1.0 = swole superhero with cape
+ */
+export function generateHumanSpriteSheet(
+ seed: string,
+ archetype: string,
+ primaryColor: string,
+ secondaryColor: string,
+ winRate: number = 0.5,
+): string {
+ const canvas = document.createElement('canvas')
+ canvas.width = HUMAN_FRAME_SIZE * HUMAN_MAX_FRAMES
+ canvas.height = HUMAN_FRAME_SIZE * HUMAN_ROWS
+ const ctx = canvas.getContext('2d')!
+ ctx.imageSmoothingEnabled = false
+
+ const I = HUMAN_INTERNAL
+ const S = HUMAN_SCALE
+ const rng = seededRng(seed)
+
+ const skinTone = SKIN_TONES[Math.floor(rng() * SKIN_TONES.length)]
+ const skinMid = darken(skinTone, 12)
+ const skinDark = darken(skinTone, 30)
+ const skinLight = lighten(skinTone, 10)
+ const hairColor = HAIR_COLORS[Math.floor(rng() * HAIR_COLORS.length)]
+ const hairDark = darken(hairColor, 20)
+ const shirtColor = primaryColor
+ const shirtDark = darken(primaryColor, 15)
+ const pantsColor = secondaryColor
+ const pantsDark = darken(secondaryColor, 15)
+
+ const wr = Math.max(0, Math.min(1, winRate))
+ const isFat = wr < 0.3 ? true : wr < 0.5 ? rng() > 0.4 : false
+ const isShort = wr < 0.25
+ const isSwole = wr > 0.75
+ const isSuperHero = wr > 0.85
+ const hasBeard = wr < 0.35 ? true : rng() > 0.6
+ const hasGlasses = wr < 0.4 ? true : rng() > 0.5
+ const hasStains = wr < 0.3
+ const hasCheetos = wr < 0.2
+ const hasCape = isSuperHero
+ const hasHeadband = wr > 0.7
+ const hasSweatDrops = wr < 0.4
+
+ const costume = getCostume(archetype)
+ const out = '#0a0a0a'
+
+ function px(x: number, y: number, color: string, ox: number, oy: number) {
+ if (x < 0 || x >= I || y < 0 || y >= I) return
+ ctx.fillStyle = color
+ ctx.fillRect(ox + x * S, oy + y * S, S, S)
+ }
+
+ function fill(x: number, y: number, w: number, h: number, color: string, ox: number, oy: number) {
+ for (let iy = y; iy < y + h; iy++) for (let ix = x; ix < x + w; ix++) px(ix, iy, color, ox, oy)
+ }
+
+ function box(x: number, y: number, w: number, h: number, fc: string, ox: number, oy: number) {
+ for (let i = x - 1; i <= x + w; i++) { px(i, y - 1, out, ox, oy); px(i, y + h, out, ox, oy) }
+ for (let i = y; i < y + h; i++) { px(x - 1, i, out, ox, oy); px(x + w, i, out, ox, oy) }
+ fill(x, y, w, h, fc, ox, oy)
+ }
+
+ // Rounded box for softer shapes
+ function roundBox(x: number, y: number, w: number, h: number, fc: string, ox: number, oy: number) {
+ fill(x + 1, y, w - 2, h, fc, ox, oy)
+ fill(x, y + 1, w, h - 2, fc, ox, oy)
+ // Outline
+ for (let i = x + 1; i < x + w - 1; i++) { px(i, y - 1, out, ox, oy); px(i, y + h, out, ox, oy) }
+ for (let i = y + 1; i < y + h - 1; i++) { px(x - 1, i, out, ox, oy); px(x + w, i, out, ox, oy) }
+ px(x, y, out, ox, oy); px(x + w - 1, y, out, ox, oy)
+ px(x, y + h - 1, out, ox, oy); px(x + w - 1, y + h - 1, out, ox, oy)
+ }
+
+ function drawHuman(fx: number, fy: number, pose: string, frame: number, total: number) {
+ const ox = fx * HUMAN_FRAME_SIZE
+ const oy = fy * HUMAN_FRAME_SIZE
+ const t = frame / Math.max(1, total - 1)
+ const bounce = Math.round(Math.sin(t * Math.PI * 2) * 2)
+
+ const isIdle = pose === 'idle'
+ const isCheer = pose === 'cheer'
+ const isPanic = pose === 'panic'
+ const isRun = pose === 'run'
+ const isCoach = pose === 'coach'
+
+ const cx = 72 // center x
+ const ground = 126 // ground line
+
+ // Body dimensions (3x base, adjusted by physique)
+ const bodyW = isFat ? 42 : isSwole ? 36 : 27
+ const bodyH = isShort ? 24 : isSwole ? 33 : 30
+ const headW = isFat ? 30 : isSwole ? 28 : 27
+ const headH = isFat ? 27 : 24
+ const legH = isShort ? 18 : isSwole ? 30 : 27
+ const legW = isFat ? 14 : isSwole ? 12 : 9
+ const armW = isSwole ? 9 : 6
+ const armH = isShort ? 15 : isSwole ? 24 : 21
+ const neckH = 3
+
+ const feetY = ground - 4
+ const legsTop = feetY - legH
+ const bodyTop = legsTop - bodyH
+ const neckTop = bodyTop - neckH
+ const headTop = neckTop - headH
+
+ // Pose offsets
+ const cheerJump = isCheer ? Math.round(Math.abs(Math.sin(t * Math.PI)) * 18) : 0
+ const panicShake = isPanic ? Math.round(Math.sin(t * Math.PI * 6) * 4) : 0
+ const runBob = isRun ? Math.round(Math.sin(t * Math.PI * 2) * 4) : 0
+ const yOff = -(cheerJump + runBob) + (isIdle ? bounce : 0)
+ const xOff = panicShake
+
+ // Shadow
+ const shadowW = isFat ? 22 : isSwole ? 18 : 14
+ for (let sx = cx - shadowW; sx <= cx + shadowW; sx++) {
+ px(sx, ground, 'rgba(0,0,0,0.15)', ox, oy)
+ px(sx, ground + 1, 'rgba(0,0,0,0.08)', ox, oy)
+ }
+
+ // ===== CAPE (drawn behind everything) =====
+ if (hasCape) {
+ const capeX = cx - Math.floor(bodyW / 2) - 4 + xOff
+ const capeW2 = bodyW + 8
+ for (let iy = bodyTop + yOff; iy < feetY + yOff + 12; iy++) {
+ const flutter = isRun ? Math.round(Math.sin(t * Math.PI * 3 + iy * 0.15) * 4) : Math.round(Math.sin(iy * 0.1) * 1)
+ for (let ix = capeX - flutter; ix < capeX + capeW2 + flutter; ix++) {
+ const shade = (iy + ix) % 5 === 0 ? '#881111' : (iy + ix) % 3 === 0 ? '#aa1818' : '#cc2222'
+ px(ix, iy, shade, ox, oy)
+ }
+ }
+ // Cape clasp at neck
+ fill(cx - 3 + xOff, bodyTop + yOff - 2, 6, 3, '#ffd700', ox, oy)
+ }
+
+ // ===== LEGS =====
+ const legGap = isRun ? Math.round(Math.sin(t * Math.PI * 2) * 8) : isPanic ? 6 : 3
+ const ll = cx - legGap - Math.floor(legW / 2) + xOff
+ const rl = cx + legGap - Math.floor(legW / 2) + xOff
+
+ const legColor = costume.onesie ? (costume.hat || shirtColor) : pantsColor
+ const legColorDk = costume.onesie ? darken(costume.hat || shirtColor, 15) : pantsDark
+
+ // Left leg
+ roundBox(ll, legsTop + yOff, legW, legH, legColor, ox, oy)
+ fill(ll + legW - 2, legsTop + yOff + 2, 2, legH - 4, legColorDk, ox, oy) // shading
+ // Knee highlight
+ px(ll + 2, legsTop + Math.floor(legH / 2) + yOff, lighten(legColor, 10), ox, oy)
+ // Right leg
+ roundBox(rl, legsTop + yOff, legW, legH, legColor, ox, oy)
+ fill(rl + legW - 2, legsTop + yOff + 2, 2, legH - 4, legColorDk, ox, oy)
+ px(rl + 2, legsTop + Math.floor(legH / 2) + yOff, lighten(legColor, 10), ox, oy)
+
+ // Socks visible (between pants and shoes)
+ if (!costume.onesie) {
+ fill(ll, feetY + yOff - 3, legW, 3, '#ffffff', ox, oy)
+ fill(rl, feetY + yOff - 3, legW, 3, '#ffffff', ox, oy)
+ }
+
+ // Shoes (chunky)
+ roundBox(ll - 2, feetY + yOff, legW + 5, 5, '#333333', ox, oy)
+ px(ll + legW + 2, feetY + yOff + 1, '#555555', ox, oy) // shoe highlight
+ fill(ll - 1, feetY + yOff + 4, legW + 4, 1, '#222222', ox, oy) // sole
+ roundBox(rl - 2, feetY + yOff, legW + 5, 5, '#333333', ox, oy)
+ px(rl + legW + 2, feetY + yOff + 1, '#555555', ox, oy)
+ fill(rl - 1, feetY + yOff + 4, legW + 4, 1, '#222222', ox, oy)
+
+ // Superhero boots
+ if (isSuperHero) {
+ fill(ll - 2, feetY + yOff - 6, legW + 5, 6, '#cc2222', ox, oy)
+ fill(rl - 2, feetY + yOff - 6, legW + 5, 6, '#cc2222', ox, oy)
+ fill(ll, feetY + yOff - 7, legW + 1, 1, '#ffd700', ox, oy) // gold trim
+ fill(rl, feetY + yOff - 7, legW + 1, 1, '#ffd700', ox, oy)
+ }
+
+ // ===== BODY =====
+ const bx = cx - Math.floor(bodyW / 2) + xOff
+ const by = bodyTop + yOff
+
+ const torsoColor = costume.onesie ? (costume.hat || shirtColor) : shirtColor
+ const torsoDk = costume.onesie ? darken(costume.hat || shirtColor, 15) : shirtDark
+
+ roundBox(bx, by, bodyW, bodyH, torsoColor, ox, oy)
+ // Body shading (right side darker)
+ for (let iy = by + 2; iy < by + bodyH - 2; iy++) {
+ px(bx + bodyW - 2, iy, torsoDk, ox, oy)
+ px(bx + bodyW - 3, iy, torsoDk, ox, oy)
+ px(bx + 1, iy, lighten(torsoColor, 8), ox, oy) // left highlight
+ }
+
+ // Zipper for onesies
+ if (costume.onesie) {
+ for (let iy = by + 2; iy < by + bodyH - 2; iy++) {
+ px(cx + xOff, iy, darken(torsoColor, 25), ox, oy)
+ px(cx + 1 + xOff, iy, darken(torsoColor, 20), ox, oy)
+ }
+ } else {
+ // Collar
+ fill(bx + 3, by, bodyW - 6, 3, darken(shirtColor, 10), ox, oy)
+ fill(bx + Math.floor(bodyW / 2) - 2, by, 4, 4, skinTone, ox, oy) // V-neck
+
+ // Shirt pattern (bigger at 3x)
+ const patCx = cx + xOff
+ const patCy = by + Math.floor(bodyH / 2) + 2
+ // Draw a big emblem
+ fill(patCx - 4, patCy - 4, 8, 8, darken(torsoColor, 8), ox, oy)
+ fill(patCx - 3, patCy - 3, 6, 6, secondaryColor, ox, oy)
+ fill(patCx - 2, patCy - 2, 4, 4, lighten(secondaryColor, 15), ox, oy)
+ px(patCx, patCy, '#ffffff', ox, oy)
+ }
+
+ // Belt
+ fill(bx, by + bodyH - 3, bodyW, 3, '#443322', ox, oy)
+ fill(cx - 2 + xOff, by + bodyH - 3, 5, 3, '#ffcc00', ox, oy) // buckle
+ px(cx + xOff, by + bodyH - 2, '#ffffff', ox, oy) // buckle highlight
+
+ // Food stains (losers)
+ if (hasStains) {
+ fill(bx + 4, by + 6, 3, 2, '#cc8822', ox, oy)
+ fill(bx + bodyW - 8, by + 10, 2, 3, '#cc2222', ox, oy)
+ fill(bx + 10, by + 4, 2, 2, '#ff8844', ox, oy)
+ px(bx + bodyW - 5, by + 8, '#884400', ox, oy)
+ }
+
+ // Belly hanging over belt
+ if (isFat && wr < 0.25) {
+ fill(bx + 4, by + bodyH, bodyW - 8, 4, skinTone, ox, oy)
+ fill(bx + 5, by + bodyH + 3, bodyW - 10, 2, skinDark, ox, oy)
+ }
+
+ // ===== NECK =====
+ fill(cx - 4 + xOff, neckTop + yOff, 8, neckH, skinTone, ox, oy)
+ px(cx + 3 + xOff, neckTop + yOff + 1, skinDark, ox, oy)
+
+ // ===== ARMS =====
+ const armAttach = by + 3
+ const armLx = bx - armW - 2
+ const armRx = bx + bodyW + 2
+ const armSkin = costume.onesie ? (costume.hat || shirtColor) : skinTone
+ const armSkinDk = costume.onesie ? darken(costume.hat || shirtColor, 15) : skinDark
+
+ if (isCheer) {
+ const armUpH = Math.round(Math.abs(Math.sin(t * Math.PI)) * 12) + 6
+ // Arms reaching up
+ roundBox(armLx, armAttach - armUpH, armW, armUpH, armSkin, ox, oy)
+ roundBox(armRx, armAttach - armUpH, armW, armUpH, armSkin, ox, oy)
+ // Hands (open, fingers spread)
+ fill(armLx - 2, armAttach - armUpH - 5, armW + 3, 4, skinTone, ox, oy)
+ fill(armRx - 1, armAttach - armUpH - 5, armW + 3, 4, skinTone, ox, oy)
+ // Individual fingers
+ for (let f = 0; f < 4; f++) {
+ px(armLx - 2 + f * 2, armAttach - armUpH - 6, skinTone, ox, oy)
+ px(armRx - 1 + f * 2, armAttach - armUpH - 6, skinTone, ox, oy)
+ }
+ } else if (isPanic) {
+ const flapL = Math.round(Math.sin(t * Math.PI * 4) * 12)
+ const flapR = Math.round(Math.cos(t * Math.PI * 4) * 12)
+ roundBox(armLx + flapL, armAttach - 8, armW, armH, armSkin, ox, oy)
+ roundBox(armRx - flapR, armAttach - 10, armW, armH, armSkin, ox, oy)
+ // Blurred hands (motion)
+ fill(armLx + flapL - 1, armAttach - 10, armW + 2, 3, skinTone, ox, oy)
+ fill(armRx - flapR - 1, armAttach - 12, armW + 2, 3, skinTone, ox, oy)
+ } else if (isCoach) {
+ const pointExt = Math.round(Math.sin(t * Math.PI) * 8)
+ // Pointing arm
+ roundBox(armRx, armAttach, armW + 8 + pointExt, armW, armSkin, ox, oy)
+ // Pointing finger
+ fill(armRx + armW + 8 + pointExt, armAttach, 4, armW - 2, skinTone, ox, oy)
+ px(armRx + armW + 12 + pointExt, armAttach + 1, skinTone, ox, oy)
+ // Other arm on hip
+ roundBox(armLx, armAttach + 4, armW, 8, armSkin, ox, oy)
+ fill(armLx + 2, armAttach + 11, armW - 2, 3, skinTone, ox, oy) // hand on hip
+ } else if (isRun) {
+ const pump = Math.round(Math.sin(t * Math.PI * 2) * 8)
+ roundBox(armLx, armAttach + pump, armW, armH - 6, armSkin, ox, oy)
+ roundBox(armRx, armAttach - pump, armW, armH - 6, armSkin, ox, oy)
+ // Fists
+ fill(armLx, armAttach + pump + armH - 8, armW + 1, 3, skinTone, ox, oy)
+ fill(armRx, armAttach - pump + armH - 8, armW + 1, 3, skinTone, ox, oy)
+ } else {
+ // Idle: arms hanging
+ roundBox(armLx, armAttach, armW, armH, armSkin, ox, oy)
+ roundBox(armRx, armAttach, armW, armH, armSkin, ox, oy)
+ // Arm shading
+ for (let iy = armAttach + 2; iy < armAttach + armH - 2; iy++) {
+ px(armLx + armW - 1, iy, armSkinDk, ox, oy)
+ px(armRx + armW - 1, iy, armSkinDk, ox, oy)
+ }
+ // Hands
+ fill(armLx, armAttach + armH - 4, armW + 1, 4, skinTone, ox, oy)
+ fill(armRx - 1, armAttach + armH - 4, armW + 1, 4, skinTone, ox, oy)
+ // Fingers (3 visible)
+ for (let f = 0; f < 3; f++) {
+ px(armLx + f * 2, armAttach + armH, skinTone, ox, oy)
+ px(armRx - 1 + f * 2, armAttach + armH, skinTone, ox, oy)
+ }
+
+ // Phone or cheetos in right hand
+ if (hasCheetos) {
+ roundBox(armRx - 1, armAttach + armH + 1, 8, 12, '#ff8800', ox, oy)
+ fill(armRx, armAttach + armH + 2, 6, 3, '#ffcc00', ox, oy) // label
+ fill(armRx + 1, armAttach + armH + 6, 4, 2, '#ffaa00', ox, oy) // crumbs
+ // Cheese dust on fingers
+ px(armRx, armAttach + armH - 1, '#ff8844', ox, oy)
+ px(armRx + 2, armAttach + armH - 1, '#ff8844', ox, oy)
+ } else {
+ // Gamepad controller
+ const gpX = armRx - 3
+ const gpY = armAttach + armH - 2
+ roundBox(gpX, gpY, 14, 8, '#333344', ox, oy) // body
+ fill(gpX + 1, gpY + 1, 12, 6, '#2a2a3a', ox, oy) // inset
+ // Grips
+ roundBox(gpX - 2, gpY + 2, 3, 6, '#333344', ox, oy)
+ roundBox(gpX + 13, gpY + 2, 3, 6, '#333344', ox, oy)
+ // D-pad (left side)
+ fill(gpX + 2, gpY + 3, 3, 1, '#555566', ox, oy)
+ fill(gpX + 3, gpY + 2, 1, 3, '#555566', ox, oy)
+ // Buttons (right side)
+ px(gpX + 9, gpY + 2, '#ff4444', ox, oy)
+ px(gpX + 11, gpY + 2, '#4488ff', ox, oy)
+ px(gpX + 10, gpY + 1, '#ffcc00', ox, oy)
+ px(gpX + 10, gpY + 3, '#44cc44', ox, oy)
+ // Wire coming out the top
+ const wireX = gpX + 7
+ for (let w = 0; w < 14; w++) {
+ const wobble = Math.round(Math.sin(w * 0.6 + t * Math.PI * 2) * 2)
+ px(wireX + wobble, gpY - 1 - w, '#444444', ox, oy)
+ }
+ }
+ }
+
+ // ===== PROP (costume-specific, bigger) =====
+ if (costume.propShape !== 'none' && costume.prop && !isCheer && !isPanic) {
+ const propX = armRx + armW + 3
+ const propY = armAttach + 3
+ if (costume.propShape === 'sword') {
+ // Blade
+ for (let i = 0; i < 22; i++) { px(propX + i, propY - i, costume.prop, ox, oy); px(propX + i, propY - i - 1, lighten(costume.prop, 15), ox, oy) }
+ // Guard
+ fill(propX - 1, propY + 1, 5, 2, '#886633', ox, oy)
+ fill(propX - 2, propY + 3, 2, 6, '#664422', ox, oy) // handle
+ px(propX - 1, propY + 8, '#ffcc44', ox, oy) // pommel
+ } else if (costume.propShape === 'wand') {
+ for (let i = 0; i < 16; i++) px(propX, propY - i, '#886633', ox, oy)
+ // Sparkle at tip
+ const sparkle = ['#ffcc44', '#ffffff', '#ffee88']
+ for (let dx = -2; dx <= 2; dx++) for (let dy = -2; dy <= 2; dy++) {
+ if (Math.abs(dx) + Math.abs(dy) <= 2) px(propX + dx, propY - 17 + dy, sparkle[(dx + dy + 4) % 3], ox, oy)
+ }
+ } else if (costume.propShape === 'shield') {
+ roundBox(propX, propY - 6, 12, 15, costume.prop, ox, oy)
+ fill(propX + 3, propY - 3, 6, 9, lighten(costume.prop, 15), ox, oy)
+ fill(propX + 5, propY - 1, 2, 5, '#ffffff', ox, oy) // emblem
+ } else if (costume.propShape === 'sign') {
+ for (let i = 0; i < 20; i++) px(propX + 2, propY - i, '#886633', ox, oy) // pole
+ roundBox(propX - 6, propY - 30, 18, 12, costume.prop, ox, oy) // sign board
+ } else if (costume.propShape === 'phone' || costume.propShape === 'remote') {
+ roundBox(propX, propY, 5, 8, '#333333', ox, oy)
+ fill(propX + 1, propY + 1, 3, 5, '#44ff44', ox, oy)
+ px(propX + 2, propY + 2, '#88ff88', ox, oy)
+ } else if (costume.propShape === 'food') {
+ roundBox(propX, propY - 2, 8, 6, costume.prop, ox, oy)
+ fill(propX + 1, propY - 4, 6, 2, '#44aa22', ox, oy) // lettuce
+ px(propX + 3, propY - 5, '#ff4444', ox, oy) // cherry on top
+ }
+ }
+
+ // ===== HEAD =====
+ const hx = cx - Math.floor(headW / 2) + xOff
+ const hy = headTop + yOff
+
+ const faceColor = costume.facePaint || skinTone
+ const faceDark = costume.facePaint ? darken(costume.facePaint, 15) : skinDark
+ const faceLight = costume.facePaint ? lighten(costume.facePaint, 10) : skinLight
+
+ roundBox(hx, hy, headW, headH, faceColor, ox, oy)
+ // Head shading
+ for (let iy = hy + 2; iy < hy + headH - 2; iy++) {
+ px(hx + headW - 2, iy, faceDark, ox, oy)
+ px(hx + headW - 3, iy, faceDark, ox, oy)
+ px(hx + 1, iy, faceLight, ox, oy)
+ }
+
+ // Ears
+ if (!costume.facePaint) {
+ fill(hx - 2, hy + Math.floor(headH * 0.3), 2, 5, skinTone, ox, oy)
+ px(hx - 2, hy + Math.floor(headH * 0.3) + 1, skinDark, ox, oy) // inner ear
+ fill(hx + headW, hy + Math.floor(headH * 0.3), 2, 5, skinTone, ox, oy)
+ px(hx + headW + 1, hy + Math.floor(headH * 0.3) + 1, skinDark, ox, oy)
+ }
+
+ // Hair
+ if (!costume.onesie || costume.type !== 'animal') {
+ // Top hair (fluffy)
+ for (let ix = hx; ix < hx + headW; ix++) {
+ px(ix, hy, hairColor, ox, oy)
+ px(ix, hy + 1, hairColor, ox, oy)
+ px(ix, hy + 2, hairColor, ox, oy)
+ }
+ for (let ix = hx + 1; ix < hx + headW - 1; ix++) {
+ px(ix, hy - 1, hairColor, ox, oy)
+ }
+ // Hair volume on sides
+ fill(hx - 1, hy, 2, 5, hairColor, ox, oy)
+ fill(hx + headW - 1, hy, 2, 5, hairColor, ox, oy)
+ // Hair highlights
+ px(hx + 3, hy, lighten(hairColor, 15), ox, oy)
+ px(hx + 4, hy, lighten(hairColor, 15), ox, oy)
+ px(hx + 5, hy - 1, lighten(hairColor, 10), ox, oy)
+ // Messy hair for losers
+ if (wr < 0.3) {
+ px(hx + 2, hy - 2, hairColor, ox, oy)
+ px(hx + headW - 4, hy - 3, hairColor, ox, oy)
+ px(hx - 1, hy - 1, hairColor, ox, oy)
+ }
+ }
+
+ // ===== EYES =====
+ const eyeY = hy + Math.floor(headH * 0.35)
+ const leX = hx + 5
+ const reX = hx + headW - 11
+
+ if (isPanic) {
+ // Wide terrified eyes
+ fill(leX - 1, eyeY - 2, 8, 7, '#ffffff', ox, oy)
+ fill(reX - 1, eyeY - 2, 8, 7, '#ffffff', ox, oy)
+ // Tiny pupils (fear)
+ fill(leX + 2, eyeY + 1, 2, 2, '#000000', ox, oy)
+ fill(reX + 2, eyeY + 1, 2, 2, '#000000', ox, oy)
+ // Eyebrow (raised)
+ fill(leX - 1, eyeY - 4, 7, 2, hairColor, ox, oy)
+ fill(reX - 1, eyeY - 4, 7, 2, hairColor, ox, oy)
+ } else if (isCheer) {
+ // Happy squinted eyes
+ fill(leX, eyeY, 6, 2, '#000000', ox, oy)
+ fill(reX, eyeY, 6, 2, '#000000', ox, oy)
+ fill(leX, eyeY - 1, 6, 1, '#ffffff', ox, oy)
+ fill(reX, eyeY - 1, 6, 1, '#ffffff', ox, oy)
+ // Raised cheeks
+ fill(leX - 1, eyeY + 2, 3, 2, '#ff8888', ox, oy)
+ fill(reX + 4, eyeY + 2, 3, 2, '#ff8888', ox, oy)
+ } else {
+ // Normal eyes with iris detail
+ fill(leX, eyeY, 6, 5, '#ffffff', ox, oy)
+ fill(reX, eyeY, 6, 5, '#ffffff', ox, oy)
+ // Iris
+ fill(leX + 2, eyeY + 1, 3, 3, '#4466aa', ox, oy)
+ fill(reX + 2, eyeY + 1, 3, 3, '#4466aa', ox, oy)
+ // Pupil
+ fill(leX + 3, eyeY + 2, 2, 2, '#000000', ox, oy)
+ fill(reX + 3, eyeY + 2, 2, 2, '#000000', ox, oy)
+ // Eye highlight
+ px(leX + 2, eyeY + 1, '#ffffff', ox, oy)
+ px(reX + 2, eyeY + 1, '#ffffff', ox, oy)
+ // Eyebrows
+ fill(leX, eyeY - 2, 6, 2, hairColor, ox, oy)
+ fill(reX, eyeY - 2, 6, 2, hairColor, ox, oy)
+ // Angry eyebrows for coach
+ if (isCoach) {
+ px(leX, eyeY - 3, hairColor, ox, oy)
+ px(reX + 5, eyeY - 3, hairColor, ox, oy)
+ }
+ }
+
+ // Glasses
+ if (hasGlasses) {
+ // Frames
+ for (let ix = leX - 2; ix <= reX + 7; ix++) px(ix, eyeY - 1, '#444444', ox, oy)
+ // Left lens
+ for (let iy = eyeY - 1; iy <= eyeY + 4; iy++) { px(leX - 2, iy, '#444444', ox, oy); px(leX + 6, iy, '#444444', ox, oy) }
+ px(leX - 2, eyeY + 4, '#444444', ox, oy); px(leX + 6, eyeY + 4, '#444444', ox, oy)
+ for (let ix = leX - 1; ix <= leX + 5; ix++) px(ix, eyeY + 4, '#444444', ox, oy)
+ // Right lens
+ for (let iy = eyeY - 1; iy <= eyeY + 4; iy++) { px(reX - 2, iy, '#444444', ox, oy); px(reX + 6, iy, '#444444', ox, oy) }
+ for (let ix = reX - 1; ix <= reX + 5; ix++) px(ix, eyeY + 4, '#444444', ox, oy)
+ // Lens reflection
+ px(leX, eyeY, '#aaccff', ox, oy)
+ px(reX, eyeY, '#aaccff', ox, oy)
+ }
+
+ // Nose
+ const noseY = hy + Math.floor(headH * 0.55)
+ px(cx + xOff, noseY, skinDark, ox, oy)
+ px(cx + 1 + xOff, noseY, skinDark, ox, oy)
+ px(cx + xOff, noseY + 1, skinDark, ox, oy)
+ // Red nose for clown
+ if (costume.type === 'hat' && archetype === 'clown') {
+ fill(cx - 1 + xOff, noseY - 1, 4, 4, '#ff0000', ox, oy)
+ }
+
+ // Mouth
+ const mY = hy + Math.floor(headH * 0.72)
+ if (isCheer) {
+ // Big grin with teeth
+ fill(cx - 5 + xOff, mY, 10, 4, '#000000', ox, oy)
+ fill(cx - 4 + xOff, mY, 8, 2, '#ffffff', ox, oy) // teeth
+ fill(cx - 4 + xOff, mY + 2, 8, 2, '#cc4444', ox, oy) // tongue
+ } else if (isPanic) {
+ // Screaming
+ roundBox(cx - 4 + xOff, mY - 2, 8, 8, '#000000', ox, oy)
+ fill(cx - 3 + xOff, mY + 2, 6, 2, '#cc4444', ox, oy) // tongue
+ // Teeth
+ px(cx - 3 + xOff, mY - 1, '#ffffff', ox, oy)
+ px(cx + 2 + xOff, mY - 1, '#ffffff', ox, oy)
+ } else if (isCoach) {
+ // Yelling
+ fill(cx - 4 + xOff, mY, 8, 5, '#000000', ox, oy)
+ fill(cx - 3 + xOff, mY, 6, 2, '#ffffff', ox, oy)
+ } else {
+ // Neutral / slight smile
+ fill(cx - 3 + xOff, mY, 6, 2, '#0a0a0a', ox, oy)
+ px(cx - 4 + xOff, mY + 1, '#0a0a0a', ox, oy)
+ px(cx + 3 + xOff, mY + 1, '#0a0a0a', ox, oy)
+ // Drool for very low win rate
+ if (wr < 0.15) {
+ px(cx + 3 + xOff, mY + 2, '#88ccff', ox, oy)
+ px(cx + 3 + xOff, mY + 3, '#88ccff', ox, oy)
+ px(cx + 3 + xOff, mY + 4, '#66aadd', ox, oy)
+ }
+ }
+
+ // Sweat drops
+ if (hasSweatDrops && (isIdle || isPanic || isCoach)) {
+ fill(hx + headW + 2, eyeY + 2, 2, 4, '#88ccff', ox, oy)
+ px(hx + headW + 2, eyeY + 6, '#66aadd', ox, oy)
+ if (wr < 0.2) {
+ fill(hx - 3, eyeY + 4, 2, 3, '#88ccff', ox, oy)
+ // Puddle forming
+ fill(hx + headW + 1, eyeY + 7, 4, 1, '#aaddff', ox, oy)
+ }
+ }
+
+ // Double chin
+ if (isFat && wr < 0.25) {
+ fill(hx + 4, hy + headH, headW - 8, 4, skinTone, ox, oy)
+ fill(hx + 5, hy + headH + 3, headW - 10, 2, skinDark, ox, oy)
+ fill(hx + 6, hy + headH + 4, headW - 12, 2, skinDark, ox, oy)
+ }
+
+ // Beard
+ if (hasBeard && !costume.facePaint) {
+ // Scrappy neckbeard for losers, cool beard for winners
+ if (wr < 0.35) {
+ // Patchy neckbeard
+ for (let ix = hx + 3; ix < hx + headW - 3; ix++) {
+ if ((ix + hy) % 3 !== 0) px(ix, hy + headH - 2, hairDark, ox, oy)
+ if ((ix + hy) % 2 !== 0) px(ix, hy + headH - 1, hairDark, ox, oy)
+ if ((ix + hy) % 3 === 0) px(ix, hy + headH, hairDark, ox, oy)
+ }
+ } else {
+ // Full beard
+ for (let ix = hx + 3; ix < hx + headW - 3; ix++) {
+ fill(ix, hy + headH - 3, 1, 5, hairColor, ox, oy)
+ }
+ fill(cx - 2 + xOff, hy + headH + 1, 4, 2, hairDark, ox, oy)
+ }
+ }
+
+ // Headband (winners)
+ if (hasHeadband && costume.hatShape === 'none') {
+ const hbColor = wr > 0.85 ? '#ffd700' : '#ff2222'
+ fill(hx - 2, hy + 3, headW + 4, 3, hbColor, ox, oy)
+ // Trailing ribbons
+ px(hx - 3, hy + 4, hbColor, ox, oy)
+ px(hx - 4, hy + 5, hbColor, ox, oy)
+ px(hx - 5, hy + 5 + (isIdle ? bounce : 0), hbColor, ox, oy)
+ px(hx - 6, hy + 6 + (isIdle ? bounce : 0), darken(hbColor, 15), ox, oy)
+ px(hx - 7, hy + 5 + (isIdle ? bounce : 0), darken(hbColor, 15), ox, oy)
+ }
+
+ // Winner glow aura
+ if (isSuperHero && !isPanic) {
+ for (let a = 0; a < 16; a++) {
+ const angle = a * Math.PI * 2 / 16 + t * Math.PI * 0.5
+ const r = 40 + Math.sin(t * Math.PI * 4 + a * 0.8) * 8
+ const gx = cx + xOff + Math.round(Math.cos(angle) * r)
+ const gy = by + Math.floor(bodyH / 2) + Math.round(Math.sin(angle) * r)
+ fill(gx - 1, gy - 1, 3, 3, '#ffcc4430', ox, oy)
+ px(gx, gy, '#ffcc4460', ox, oy)
+ }
+ }
+
+ // ===== COSTUME HAT/ACCESSORIES =====
+ const hatColor = costume.hat || hairColor
+ const hatDk = darken(hatColor, 15)
+ const hatLt = lighten(hatColor, 15)
+
+ if (costume.hatShape === 'ears') {
+ // Big floppy costume ears
+ fill(hx - 1, hy - 5, 5, 6, hatColor, ox, oy)
+ fill(hx + headW - 4, hy - 5, 5, 6, hatColor, ox, oy)
+ fill(hx, hy - 4, 3, 4, hatLt, ox, oy) // inner
+ fill(hx + headW - 3, hy - 4, 3, 4, hatLt, ox, oy)
+ } else if (costume.hatShape === 'helmet') {
+ for (let ix = hx - 3; ix < hx + headW + 3; ix++) {
+ px(ix, hy - 3, hatColor, ox, oy); px(ix, hy - 2, hatColor, ox, oy)
+ px(ix, hy - 1, hatColor, ox, oy); px(ix, hy, hatColor, ox, oy)
+ }
+ for (let ix = hx - 1; ix < hx + headW + 1; ix++) {
+ px(ix, hy - 4, hatColor, ox, oy); px(ix, hy - 5, hatColor, ox, oy)
+ }
+ fill(hx + 2, hy - 4, 4, 2, hatLt, ox, oy) // highlight
+ // Visor
+ fill(hx + 2, hy + 1, headW - 4, 3, '#33333388', ox, oy)
+ } else if (costume.hatShape === 'pointy') {
+ for (let h = 0; h < 18; h++) {
+ const w = Math.max(1, Math.floor((headW + 2) * (1 - h / 18)))
+ const sx2 = cx + xOff - Math.floor(w / 2)
+ for (let ix = sx2; ix < sx2 + w; ix++) px(ix, hy - 1 - h, hatColor, ox, oy)
+ }
+ // Stars and moons on the hat
+ px(cx - 3 + xOff, hy - 8, '#ffcc44', ox, oy)
+ px(cx + 4 + xOff, hy - 12, '#ffcc44', ox, oy)
+ px(cx + xOff, hy - 16, '#ffffff', ox, oy) // star tip
+ fill(cx - 1 + xOff, hy - 18, 3, 3, '#ffcc44', ox, oy)
+ } else if (costume.hatShape === 'wide') {
+ for (let ix = hx - 8; ix < hx + headW + 8; ix++) {
+ px(ix, hy - 3, hatColor, ox, oy); px(ix, hy - 2, hatColor, ox, oy)
+ }
+ for (let ix = hx - 3; ix < hx + headW + 3; ix++) {
+ px(ix, hy - 4, hatColor, ox, oy); px(ix, hy - 5, hatColor, ox, oy)
+ px(ix, hy - 6, hatColor, ox, oy); px(ix, hy - 7, hatColor, ox, oy)
+ }
+ // Hat band
+ fill(hx - 2, hy - 4, headW + 4, 2, hatDk, ox, oy)
+ } else if (costume.hatShape === 'tall') {
+ for (let h = 0; h < 16; h++) {
+ for (let ix = hx + 2; ix < hx + headW - 2; ix++) px(ix, hy - 1 - h, hatColor, ox, oy)
+ }
+ fill(hx + 3, hy - 14, headW - 8, 2, hatLt, ox, oy) // puff/highlight
+ } else if (costume.hatShape === 'horns') {
+ // Chunky horns
+ for (let h = 0; h < 8; h++) {
+ px(hx - h, hy - 1 - h, hatColor, ox, oy); px(hx - h - 1, hy - 1 - h, hatColor, ox, oy)
+ px(hx + headW - 1 + h, hy - 1 - h, hatColor, ox, oy); px(hx + headW + h, hy - 1 - h, hatColor, ox, oy)
+ }
+ px(hx - 8, hy - 9, hatLt, ox, oy); px(hx + headW + 8, hy - 9, hatLt, ox, oy) // tips
+ } else if (costume.hatShape === 'antenna') {
+ // Bouncy antenna headband
+ fill(hx + 2, hy - 2, headW - 4, 2, '#888888', ox, oy) // headband
+ const bobX = isIdle ? Math.round(Math.sin(t * Math.PI * 3) * 2) : 0
+ // Left antenna
+ for (let h = 0; h < 10; h++) px(hx + 4 + bobX, hy - 3 - h, '#888888', ox, oy)
+ fill(hx + 2 + bobX, hy - 14, 5, 4, '#ff2222', ox, oy) // left ball
+ px(hx + 3 + bobX, hy - 13, '#ff6666', ox, oy) // highlight
+ // Right antenna
+ for (let h = 0; h < 10; h++) px(hx + headW - 5 - bobX, hy - 3 - h, '#888888', ox, oy)
+ fill(hx + headW - 7 - bobX, hy - 14, 5, 4, '#ff2222', ox, oy)
+ px(hx + headW - 6 - bobX, hy - 13, '#ff6666', ox, oy)
+ } else if (costume.hatShape === 'cone') {
+ for (let h = 0; h < 20; h++) {
+ const w = Math.max(2, 18 - h)
+ const sx2 = cx + xOff - Math.floor(w / 2)
+ const stripe = (h % 6 < 3) ? '#ffffff' : hatColor
+ for (let ix = sx2; ix < sx2 + w; ix++) px(ix, hy - 1 - h, stripe, ox, oy)
+ }
+ }
+
+ // Tail (costume)
+ if (costume.tail) {
+ const tailColor = costume.hat || shirtColor
+ const tx = bx - 4
+ for (let i = 0; i < 8; i++) {
+ px(tx - i, by + bodyH - 4 + Math.round(Math.sin(i * 0.8 + t * Math.PI * 2) * 2), tailColor, ox, oy)
+ px(tx - i, by + bodyH - 3 + Math.round(Math.sin(i * 0.8 + t * Math.PI * 2) * 2), tailColor, ox, oy)
+ }
+ px(tx - 8, by + bodyH - 4, darken(tailColor, 20), ox, oy) // tail tip
+ }
+ }
+
+ // Render all frames
+ const entries = Object.entries(HUMAN_ANIMATIONS) as [string, { frames: number; row: number }][]
+ for (let row = 0; row < entries.length; row++) {
+ const [pose, cfg] = entries[row]
+ for (let f = 0; f < cfg.frames; f++) drawHuman(f, row, pose, f, cfg.frames)
+ }
+
+ return canvas.toDataURL()
+}
+
+function getCostume(archetype: string): CostumeConfig {
+ const raw = COSTUME_MAP[archetype] || {}
+ return {
+ type: raw.type || 'generic',
+ hat: raw.hat,
+ hatShape: raw.hatShape || 'none',
+ prop: raw.prop,
+ propShape: raw.propShape || 'none',
+ onesie: raw.onesie || false,
+ facePaint: raw.facePaint,
+ tail: raw.tail || false,
+ }
+}
+
+function darken(hex: string, amount: number): string {
+ if (hex.startsWith('hsl')) {
+ const m = hex.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
+ if (m) return `hsl(${m[1]}, ${m[2]}%, ${Math.max(0, +m[3] - amount)}%)`
+ }
+ if (hex.length < 7) return hex
+ const r = Math.max(0, parseInt(hex.slice(1, 3), 16) - amount * 2.55)
+ const g = Math.max(0, parseInt(hex.slice(3, 5), 16) - amount * 2.55)
+ const b = Math.max(0, parseInt(hex.slice(5, 7), 16) - amount * 2.55)
+ return `#${Math.round(r).toString(16).padStart(2, '0')}${Math.round(g).toString(16).padStart(2, '0')}${Math.round(b).toString(16).padStart(2, '0')}`
+}
+
+function lighten(hex: string, amount: number): string {
+ if (hex.startsWith('hsl')) {
+ const m = hex.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
+ if (m) return `hsl(${m[1]}, ${m[2]}%, ${Math.min(95, +m[3] + amount)}%)`
+ }
+ if (hex.length < 7) return hex
+ const r = Math.min(255, parseInt(hex.slice(1, 3), 16) + amount * 2.55)
+ const g = Math.min(255, parseInt(hex.slice(3, 5), 16) + amount * 2.55)
+ const b = Math.min(255, parseInt(hex.slice(5, 7), 16) + amount * 2.55)
+ return `#${Math.round(r).toString(16).padStart(2, '0')}${Math.round(g).toString(16).padStart(2, '0')}${Math.round(b).toString(16).padStart(2, '0')}`
+}
diff --git a/frontend/src/game/sprites/index.ts b/frontend/src/game/sprites/index.ts
index 996d8b5..0428c0b 100644
--- a/frontend/src/game/sprites/index.ts
+++ b/frontend/src/game/sprites/index.ts
@@ -7,6 +7,7 @@ export { FRAME_SIZE, ANIMATIONS, MAX_FRAMES, TOTAL_ROWS } from './constants'
export type { Pal, Archetype, ArchetypeParams, Dimensions } from './constants'
export { getBotColors } from './palette'
export { generateJudgeSpriteSheet, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS } from './judge'
+export { generateHumanSpriteSheet, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, HUMAN_FRAME_SIZE } from './human'
export { archetypes, rollArchetype } from './archetypes'
export interface SpriteCustomization {
diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue
index 6ef8d7b..ffa4cbb 100644
--- a/frontend/src/pages/BotProfilePage.vue
+++ b/frontend/src/pages/BotProfilePage.vue
@@ -3,6 +3,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter, RouterLink } from 'vue-router'
import { useNostr } from '../composables/useNostr'
import SpritePreview from '../components/SpritePreview.vue'
+import HumanPreview from '../components/HumanPreview.vue'
import type { SpriteCustomization } from '../game/sprites'
const route = useRoute()
@@ -278,25 +279,34 @@ const tierClass = (t: number) => `tier-${t}`