Back to Outbreak: Soycoon City
SOURCE / PINNED RELEASE

Made of little things.

Outbreak: Soycoon City

Release
db7750b1d009…
Author-recorded commit
3ee3f5fd27a6…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b87c04571a9

Archive hash verified: dbebdc772b142a1c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/dressing.ts
import { BoxGeometry, CanvasTexture, CylinderGeometry, DoubleSide, Group, Mesh, MeshBasicMaterial, MeshLambertMaterial, NearestFilter, SRGBColorSpace, TorusGeometry, type Material } from 'three';
import type { Dressing, Room } from './world.js';
const matte = (color: number) => new MeshLambertMaterial({ color, flatShading: true });
const cube = (g: Group, size: [number, number, number], at: [number, number, number], material: Material) => { const m = new Mesh(new BoxGeometry(...size), material); m.position.set(...at); g.add(m); return m; };
function boardTexture(kind: Dressing['kind'], seed: number): CanvasTexture {
  const c = document.createElement('canvas'); c.width = 256; c.height = 160; const x = c.getContext('2d')!;
  const rand = () => { seed = (seed * 16807) % 2147483647; return seed / 2147483647; };
  x.fillStyle = kind === 'pinboard' ? '#8a704d' : kind === 'photo' ? '#bdb29a' : '#c2bda4'; x.fillRect(0, 0, 256, 160);
  if (kind === 'pinboard') {
    for (let i = 0; i < 1500; i++) { x.fillStyle = rand() > .5 ? '#b3996440' : '#423a2840'; x.fillRect(rand() * 256, rand() * 160, 2, 1); }
    for (let i = 0; i < 12; i++) {
      const px = 8 + i % 4 * 62 + rand() * 5, py = 8 + Math.floor(i / 4) * 48 + rand() * 5;
      x.fillStyle = '#d0c8a6'; x.fillRect(px, py, 43, 35); x.fillStyle = '#655f50';
      if (i % 3 === 0) { x.fillRect(px + 4, py + 4, 35, 25); x.fillStyle = '#a19b89'; x.fillRect(px + 8, py + 10, 14, 16); x.fillStyle = '#4a514b'; x.fillRect(px + 12, py + 6, 7, 9); }
      else { for (let j = 0; j < 5; j++) x.fillRect(px + 4, py + 5 + j * 5, 12 + rand() * 23, 1); }
      x.fillStyle = '#833a2a'; x.fillRect(px + 20, py - 1, 3, 4);
    }
    x.strokeStyle = '#8e3329'; x.lineWidth = 1; x.beginPath(); x.moveTo(28, 21); x.lineTo(154, 78); x.lineTo(92, 116); x.lineTo(218, 24); x.stroke();
  } else if (kind === 'photo') {
    x.fillStyle = '#4b4e43'; x.fillRect(12, 10, 232, 140); x.fillStyle = '#979783';
    for (let i = 0; i < 5; i++) { x.fillRect(24 + i * 44, 69, 31, 60); x.fillStyle = '#6c725e'; x.fillRect(32 + i * 44, 40, 18, 26); x.fillStyle = '#a0a18c'; }
    x.fillStyle = '#c8c5a4'; x.font = '9px monospace'; x.fillText('PRECINCT / 1996', 19, 145);
  } else {
    x.fillStyle = '#3f5d51'; x.fillRect(6, 6, 244, 29); x.fillStyle = '#ded6b9'; x.font = 'bold 17px monospace'; x.fillText('SAFETY FIRST', 20, 26);
    x.strokeStyle = '#586353'; x.lineWidth = 5; x.strokeRect(94, 47, 68, 69); x.beginPath(); x.moveTo(93, 116); x.lineTo(128, 54); x.lineTo(161, 116); x.stroke();
    x.fillStyle = '#74745b'; for (let i = 0; i < 4; i++) x.fillRect(24, 126 + i * 7, 170 - i * 21, 2);
  }
  const t = new CanvasTexture(c); t.colorSpace = SRGBColorSpace; t.minFilter = NearestFilter; t.magFilter = NearestFilter; return t;
}
export function addDressing(parent: Group, room: Room): void {
  for (const [index, d] of (room.dressing ?? []).entries()) {
    const g = new Group(); g.position.set(d.x, 0, d.z); g.name = `dress-${d.id}`; parent.add(g);
    if (d.side) g.rotation.y = { north: 0, east: -Math.PI / 2, south: Math.PI, west: Math.PI / 2 }[d.side];
    if (d.kind === 'pinboard' || d.kind === 'photo' || d.kind === 'poster') {
      const w = d.kind === 'pinboard' ? 1.65 : d.kind === 'photo' ? .75 : .58, h = d.kind === 'pinboard' ? .95 : d.kind === 'photo' ? .5 : .8;
      const y = d.kind === 'pinboard' && room.id === 'maintenance' ? 2.57 : d.kind === 'photo' ? 2.35 : 2.05;
      cube(g, [w + .1, h + .1, .07], [0, y, 0], matte(0x4b3c29));
      cube(g, [w, h, .015], [0, y, .044], new MeshBasicMaterial({ map: boardTexture(d.kind, 1237 + index * 111) }));
    } else if (d.kind === 'papers') {
      for (let i = 0; i < 6; i++) {
        const paper = cube(g, [.24 + i % 2 * .08, .008, .18], [(i % 3 - 1) * .26, .018 + i * .001, (Math.floor(i / 3) - .5) * .25], matte(i % 3 ? 0xbbb99a : 0x777967)); paper.rotation.y = i * 1.37;
        const line = cube(g, [.13, .003, .015], [0, .008, -.03], matte(0x747660)); paper.add(line); g.remove(line);
      }
    } else if (d.kind === 'cleaning') {
      const bucket = new Mesh(new CylinderGeometry(.23, .18, .34, 8, 1, true), new MeshLambertMaterial({ color: 0x697973, side: DoubleSide, flatShading: true })); bucket.position.set(.12, .18, 0); g.add(bucket);
      const rim = new Mesh(new TorusGeometry(.23, .015, 4, 8), matte(0x8a9488)); rim.rotation.x = Math.PI / 2; rim.position.set(.12, .35, 0); g.add(rim);
      const water = new Mesh(new CylinderGeometry(.19, .19, .01, 8), matte(0x263d39)); water.position.set(.12, .12, 0); g.add(water);
      const handle = new Mesh(new TorusGeometry(.22, .012, 4, 8), matte(0x394b43)); handle.position.set(.12, .37, 0); g.add(handle);
      for (const side of [-1, 1]) { const shaft = new Mesh(new CylinderGeometry(.017, .017, 1.55, 5), matte(0x856c42)); shaft.rotation.z = side * .12; shaft.position.set(side * .27, .87, -.08); g.add(shaft); cube(g, [.24, .12, .07], [side * .36, .12, -.08], matte(0xaaa27a)); }
    } else {
      const iron = matte(0x67674d);
      for (let i = 0; i < 3; i++) { const tube = new Mesh(new CylinderGeometry(.07, .07, 2.8, 6), iron); tube.position.set((i - 1) * .4, 1.5, .06); g.add(tube); }
      cube(g, [1.15, .09, .17], [0, .65, .06], iron); cube(g, [1.15, .09, .17], [0, 2.4, .06], iron);
      const valve = new Mesh(new TorusGeometry(.17, .026, 4, 8), matte(0x844734)); valve.position.set(0, 1.1, .2); g.add(valve);
    }
  }
}