@blkc-code/render-canvas 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/draw.d.ts +9 -9
  2. package/dist/draw.d.ts.map +1 -1
  3. package/dist/draw.js +73 -305
  4. package/dist/draw.js.map +1 -1
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/links.d.ts +16 -0
  10. package/dist/links.d.ts.map +1 -0
  11. package/dist/links.js +75 -0
  12. package/dist/links.js.map +1 -0
  13. package/dist/office.d.ts +2 -0
  14. package/dist/office.d.ts.map +1 -1
  15. package/dist/office.js +2 -0
  16. package/dist/office.js.map +1 -1
  17. package/dist/pack.d.ts +31 -0
  18. package/dist/pack.d.ts.map +1 -0
  19. package/dist/pack.js +10 -0
  20. package/dist/pack.js.map +1 -0
  21. package/dist/paint.d.ts +15 -0
  22. package/dist/paint.d.ts.map +1 -0
  23. package/dist/paint.js +18 -0
  24. package/dist/paint.js.map +1 -0
  25. package/dist/runtime.js +1 -1
  26. package/dist/runtime.txt +1 -1
  27. package/dist/types.d.ts +13 -2
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/world.d.ts +15 -11
  30. package/dist/world.d.ts.map +1 -1
  31. package/dist/world.js +31 -59
  32. package/dist/world.js.map +1 -1
  33. package/dist/worlds/office-data.d.ts +14 -0
  34. package/dist/worlds/office-data.d.ts.map +1 -0
  35. package/dist/worlds/office-data.js +52 -0
  36. package/dist/worlds/office-data.js.map +1 -0
  37. package/dist/worlds/office-paint.d.ts +13 -0
  38. package/dist/worlds/office-paint.d.ts.map +1 -0
  39. package/dist/worlds/office-paint.js +292 -0
  40. package/dist/worlds/office-paint.js.map +1 -0
  41. package/package.json +7 -2
  42. package/src/draw.ts +73 -289
  43. package/src/index.ts +3 -0
  44. package/src/links.ts +76 -0
  45. package/src/office.ts +3 -0
  46. package/src/pack.ts +36 -0
  47. package/src/paint.ts +24 -0
  48. package/src/types.ts +12 -2
  49. package/src/world.ts +41 -60
  50. package/src/worlds/office-data.ts +57 -0
  51. package/src/worlds/office-paint.ts +276 -0
package/src/world.ts CHANGED
@@ -3,6 +3,9 @@
3
3
  * heading. Nothing in here touches a canvas, so all of it can be tested.
4
4
  */
5
5
 
6
+ import { registerWorld, worldData, type ItemSpec, type WorldData } from "./pack.js";
7
+ import { OFFICE, OFFICE_ITEMS, OFFICE_LAYOUT } from "./worlds/office-data.js";
8
+ import type { LinkRoute } from "./links.js";
6
9
  import type { AgentState, FloorStyle, FurnitureType, OfficeAgent, OfficeDoor, OfficeFurniture, OfficeLayout, OfficeRoom, OfficeScene, OfficeTask, TaskState } from "./types.js";
7
10
 
8
11
  export const MAX_COLS = 64;
@@ -12,54 +15,14 @@ const MAX_FURNITURE = 300;
12
15
  const MAX_TASKS = 60;
13
16
  export const WALK_SPEED = 4.5; // tiles a second
14
17
 
15
- /** Footprint in tiles, and where someone stands to use it, relative to its corner. */
16
- export const FURNITURE: Record<FurnitureType, { w: number; h: number; solid?: boolean; wall?: boolean; spot?: { x: number; y: number } }> = {
17
- desk: { w: 3, h: 2, spot: { x: 1, y: 2 } },
18
- coffee: { w: 2, h: 1, spot: { x: 0, y: 1 } },
19
- water: { w: 1, h: 1, spot: { x: 0, y: 1 } },
20
- sofa: { w: 3, h: 1, spot: { x: 1, y: 1 } },
21
- plant: { w: 1, h: 1 },
22
- fern: { w: 1, h: 1 },
23
- board: { w: 4, h: 1, spot: { x: 1, y: 1 } },
24
- whiteboard: { w: 3, h: 1, spot: { x: 1, y: 1 } },
25
- shelf: { w: 2, h: 1, spot: { x: 0, y: 1 } },
26
- server: { w: 1, h: 1 },
27
- table: { w: 3, h: 2, spot: { x: 1, y: 2 } },
28
- rug: { w: 4, h: 3, solid: false },
29
- arcade: { w: 1, h: 1, spot: { x: 0, y: 1 } },
30
- fridge: { w: 1, h: 1, spot: { x: 0, y: 1 } },
31
- printer: { w: 1, h: 1, spot: { x: 0, y: 1 } },
32
- lamp: { w: 1, h: 1 },
33
- beanbag: { w: 1, h: 1, spot: { x: 0, y: 1 } },
34
- tv: { w: 3, h: 1, spot: { x: 1, y: 2 } },
35
- vending: { w: 1, h: 1, spot: { x: 0, y: 1 } },
36
- trophy: { w: 1, h: 1 },
37
- clock: { w: 1, h: 1, solid: false, wall: true },
38
- poster: { w: 1, h: 1, solid: false, wall: true },
39
- };
18
+ /** Footprints and uses, for the office. Other worlds bring their own. */
19
+ export const FURNITURE = OFFICE_ITEMS;
40
20
 
41
21
  const AGENT_STATES: readonly string[] = ["working", "thinking", "blocked", "reviewing", "idle", "done", "away"];
42
22
  const TASK_STATES: readonly string[] = ["todo", "doing", "done"];
43
- const FLOORS: readonly string[] = ["wood", "carpet", "tile", "stone", "concrete", "grass"];
44
-
45
- export const DEFAULT_LAYOUT = {
46
- cols: 26,
47
- rows: 17,
48
- rooms: [
49
- { id: "studio", name: "Studio", x: 1, y: 3, w: 15, h: 13, floor: "wood" },
50
- { id: "lounge", name: "Lounge", x: 17, y: 3, w: 8, h: 6, floor: "carpet" },
51
- { id: "quests", name: "Quests", x: 17, y: 10, w: 8, h: 6, floor: "tile" },
52
- ] as OfficeRoom[],
53
- doors: [{ x: 16, y: 6 }, { x: 16, y: 13 }, { x: 23, y: 9 }] as OfficeDoor[],
54
- furniture: [
55
- { type: "desk", x: 2, y: 4 }, { type: "desk", x: 7, y: 4 }, { type: "desk", x: 12, y: 4 },
56
- { type: "desk", x: 2, y: 10 }, { type: "desk", x: 7, y: 10 }, { type: "desk", x: 12, y: 10 },
57
- { type: "plant", x: 1, y: 8 }, { type: "plant", x: 15, y: 8 }, { type: "server", x: 15, y: 15 }, { type: "server", x: 14, y: 15 },
58
- { type: "shelf", x: 1, y: 15 },
59
- { type: "coffee", x: 19, y: 3 }, { type: "water", x: 21, y: 3 }, { type: "sofa", x: 22, y: 3 }, { type: "rug", x: 20, y: 5 }, { type: "plant", x: 24, y: 8 },
60
- { type: "board", x: 19, y: 10 }, { type: "table", x: 19, y: 13 }, { type: "plant", x: 24, y: 15 },
61
- ] as OfficeFurniture[],
62
- };
23
+ const FLOORS: readonly string[] = ["wood", "carpet", "tile", "stone", "concrete", "grass", "hall"];
24
+
25
+ export const DEFAULT_LAYOUT = OFFICE_LAYOUT;
63
26
 
64
27
  const int = (value: unknown, fallback: number): number => (typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback);
65
28
  const str = (value: unknown, max = 120): string => (typeof value === "string" ? value.slice(0, max) : "");
@@ -94,6 +57,12 @@ export interface Agent {
94
57
  }
95
58
 
96
59
  export interface World {
60
+ /** What this world is made of. */
61
+ pack: WorldData;
62
+ links: { from: string; to: string }[];
63
+ /** Worked out when first drawn, and again when the building changes. */
64
+ routes?: LinkRoute[];
65
+ showLinks: boolean;
97
66
  cols: number;
98
67
  rows: number;
99
68
  rooms: Room[];
@@ -187,13 +156,18 @@ function toTask(raw: OfficeTask): Task {
187
156
  return { title: str(raw?.title, 80), state: TASK_STATES.includes(raw?.state as string) ? (raw.state as TaskState) : "todo", agent: str(raw?.agent, 60) };
188
157
  }
189
158
 
190
- function buildLayout(scene: OfficeLayout): Pick<World, "cols" | "rows" | "rooms" | "doors" | "furniture" | "walkable" | "spots" | "seats"> {
159
+ registerWorld(OFFICE);
160
+
161
+ /** A piece of furniture's spec. Only items in the world's list get into a World. */
162
+ export const specOf = (world: Pick<World, "pack">, item: Item): ItemSpec => world.pack.items[item.type] as ItemSpec;
163
+
164
+ function buildLayout(scene: OfficeLayout, pack: WorldData): Pick<World, "links" | "cols" | "rows" | "rooms" | "doors" | "furniture" | "walkable" | "spots" | "seats"> {
191
165
  const custom = Array.isArray(scene?.rooms) && scene.rooms.length > 0;
192
- const cols = Math.min(Math.max(int(scene?.cols, custom ? 20 : DEFAULT_LAYOUT.cols), 6), MAX_COLS);
193
- const rows = Math.min(Math.max(int(scene?.rows, custom ? 14 : DEFAULT_LAYOUT.rows), 6), MAX_ROWS);
194
- const rawRooms = custom ? list<OfficeRoom>(scene.rooms, 40) : DEFAULT_LAYOUT.rooms;
195
- const rawDoors = custom ? list<OfficeDoor>(scene.doors, 60) : DEFAULT_LAYOUT.doors;
196
- const rawItems = Array.isArray(scene?.furniture) ? list<OfficeFurniture>(scene.furniture, MAX_FURNITURE) : custom ? [] : DEFAULT_LAYOUT.furniture;
166
+ const cols = Math.min(Math.max(int(scene?.cols, custom ? 20 : pack.layout.cols), 6), MAX_COLS);
167
+ const rows = Math.min(Math.max(int(scene?.rows, custom ? 14 : pack.layout.rows), 6), MAX_ROWS);
168
+ const rawRooms = custom ? list<OfficeRoom>(scene.rooms, 40) : pack.layout.rooms;
169
+ const rawDoors = custom ? list<OfficeDoor>(scene.doors, 60) : pack.layout.doors;
170
+ const rawItems = Array.isArray(scene?.furniture) ? list<OfficeFurniture>(scene.furniture, MAX_FURNITURE) : custom ? [] : pack.layout.furniture;
197
171
 
198
172
  // Clamped to the grid: everything later loops over these, so a huge room must not hang the page.
199
173
  const rooms: Room[] = rawRooms.map((room, index) => {
@@ -220,36 +194,43 @@ function buildLayout(scene: OfficeLayout): Pick<World, "cols" | "rows" | "rooms"
220
194
  for (const door of doors) mark(door.x, door.y, true);
221
195
 
222
196
  const furniture: Item[] = rawItems
223
- .filter((item) => item && Object.hasOwn(FURNITURE, item.type))
197
+ .filter((item) => item && Object.hasOwn(pack.items, item.type))
224
198
  .map((item) => ({ type: item.type, x: int(item.x, -99), y: int(item.y, -99) }));
225
199
  for (const item of furniture) {
226
- const spec = FURNITURE[item.type];
200
+ const spec = pack.items[item.type] as ItemSpec;
227
201
  if (spec.solid === false) continue;
228
202
  for (let y = item.y; y < item.y + spec.h; y++) for (let x = item.x; x < item.x + spec.w; x++) mark(x, y, false);
229
203
  }
230
204
 
231
205
  const open = (x: number, y: number): boolean => x >= 0 && y >= 0 && x < cols && y < rows && walkable[y * cols + x] === true;
232
206
  const spot = (item: Item): { x: number; y: number } | undefined => {
233
- const offset = FURNITURE[item.type].spot;
207
+ const offset = (pack.items[item.type] as ItemSpec).spot;
234
208
  if (!offset) return undefined;
235
209
  const point = { x: item.x + offset.x, y: item.y + offset.y };
236
210
  return open(point.x, point.y) ? point : undefined;
237
211
  };
238
- const seats = furniture.filter((item) => item.type === "desk").map(spot).filter((p): p is { x: number; y: number } => Boolean(p));
212
+ const seats = furniture.filter((item) => (pack.items[item.type] as ItemSpec).use === "seat").map(spot).filter((p): p is { x: number; y: number } => Boolean(p));
239
213
  const spots: World["spots"] = { lounge: [], board: [] };
240
214
  for (const item of furniture) {
241
215
  const point = spot(item);
242
216
  if (!point) continue;
243
- if (["coffee", "sofa", "water", "shelf", "arcade", "fridge", "beanbag", "vending", "tv"].includes(item.type)) spots.lounge.push(point);
244
- if (["board", "table", "whiteboard"].includes(item.type)) spots.board.push(point);
217
+ const use = (pack.items[item.type] as ItemSpec).use;
218
+ if (use === "lounge") spots.lounge.push(point);
219
+ if (use === "board") spots.board.push(point);
245
220
  }
246
- return { cols, rows, rooms, doors, furniture, walkable, seats, spots };
221
+ const links = list<{ from: string; to: string }>(scene?.links, 60)
222
+ .map((link) => ({ from: str(link?.from, 60), to: str(link?.to, 60) }))
223
+ .filter((link) => link.from && link.to);
224
+ return { cols, rows, rooms, doors, furniture, walkable, seats, spots, links };
247
225
  }
248
226
 
249
227
  /** Build a world from a scene. Bad values are dropped or clamped, so it never throws. */
250
228
  export function createWorld(scene: OfficeScene = {}): World {
229
+ const pack = worldData(scene?.world, OFFICE);
251
230
  const world: World = {
252
- ...buildLayout(scene),
231
+ pack,
232
+ showLinks: true,
233
+ ...buildLayout(scene, pack),
253
234
  agents: list<OfficeAgent>(scene?.agents, MAX_AGENTS).map(toAgent),
254
235
  tasks: list<OfficeTask>(scene?.tasks, MAX_TASKS).map(toTask),
255
236
  };
@@ -259,7 +240,7 @@ export function createWorld(scene: OfficeScene = {}): World {
259
240
 
260
241
  /** Change the building while everyone is in it. Anyone left inside a wall or a desk moves. */
261
242
  export function applyLayout(world: World, layout: OfficeLayout): void {
262
- Object.assign(world, buildLayout(layout));
243
+ Object.assign(world, buildLayout(layout, world.pack), { routes: undefined });
263
244
  for (const agent of world.agents) {
264
245
  agent.seat = undefined;
265
246
  agent.path = [];
@@ -0,0 +1,57 @@
1
+ /** The office as data: what furniture exists, how big it is, what it's for, and what things are called. */
2
+
3
+ import type { WorldData } from "../pack.js";
4
+ import type { OfficeDoor, OfficeFurniture, OfficeRoom } from "../types.js";
5
+
6
+ /** Footprint in tiles, and where someone stands to use it, relative to its corner. */
7
+ export const OFFICE_ITEMS: WorldData["items"] = {
8
+ desk: { w: 3, h: 2, spot: { x: 1, y: 2 }, use: "seat" },
9
+ coffee: { w: 2, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
10
+ water: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
11
+ sofa: { w: 3, h: 1, spot: { x: 1, y: 1 }, use: "lounge" },
12
+ plant: { w: 1, h: 1 },
13
+ fern: { w: 1, h: 1 },
14
+ board: { w: 4, h: 1, spot: { x: 1, y: 1 }, use: "board" },
15
+ whiteboard: { w: 3, h: 1, spot: { x: 1, y: 1 }, use: "board" },
16
+ shelf: { w: 2, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
17
+ server: { w: 1, h: 1 },
18
+ table: { w: 3, h: 2, spot: { x: 1, y: 2 }, use: "board" },
19
+ rug: { w: 4, h: 3, solid: false },
20
+ arcade: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
21
+ fridge: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
22
+ printer: { w: 1, h: 1, spot: { x: 0, y: 1 } },
23
+ lamp: { w: 1, h: 1 },
24
+ beanbag: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
25
+ tv: { w: 3, h: 1, spot: { x: 1, y: 2 }, use: "lounge" },
26
+ vending: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
27
+ trophy: { w: 1, h: 1 },
28
+ clock: { w: 1, h: 1, solid: false, wall: true },
29
+ poster: { w: 1, h: 1, solid: false, wall: true },
30
+ };
31
+
32
+ export const OFFICE_LAYOUT = {
33
+ cols: 26,
34
+ rows: 17,
35
+ rooms: [
36
+ { id: "studio", name: "Studio", x: 1, y: 3, w: 15, h: 13, floor: "wood" },
37
+ { id: "lounge", name: "Lounge", x: 17, y: 3, w: 8, h: 6, floor: "carpet" },
38
+ { id: "quests", name: "Quests", x: 17, y: 10, w: 8, h: 6, floor: "tile" },
39
+ ] as OfficeRoom[],
40
+ doors: [{ x: 16, y: 6 }, { x: 16, y: 13 }, { x: 23, y: 9 }] as OfficeDoor[],
41
+ furniture: [
42
+ { type: "desk", x: 2, y: 4 }, { type: "desk", x: 7, y: 4 }, { type: "desk", x: 12, y: 4 },
43
+ { type: "desk", x: 2, y: 10 }, { type: "desk", x: 7, y: 10 }, { type: "desk", x: 12, y: 10 },
44
+ { type: "plant", x: 1, y: 8 }, { type: "plant", x: 15, y: 8 }, { type: "server", x: 15, y: 15 }, { type: "server", x: 14, y: 15 },
45
+ { type: "shelf", x: 1, y: 15 },
46
+ { type: "coffee", x: 19, y: 3 }, { type: "water", x: 21, y: 3 }, { type: "sofa", x: 22, y: 3 }, { type: "rug", x: 20, y: 5 }, { type: "plant", x: 24, y: 8 },
47
+ { type: "board", x: 19, y: 10 }, { type: "table", x: 19, y: 13 }, { type: "plant", x: 24, y: 15 },
48
+ ] as OfficeFurniture[],
49
+ };
50
+
51
+ export const OFFICE: WorldData = {
52
+ id: "office",
53
+ name: "Pixel Office",
54
+ words: { quests: "QUESTS" },
55
+ items: OFFICE_ITEMS,
56
+ layout: OFFICE_LAYOUT,
57
+ };
@@ -0,0 +1,276 @@
1
+ /**
2
+ * How the office looks. Every piece of art here is original to this project, made in code at
3
+ * 16 pixels a tile.
4
+ */
5
+
6
+ import { OUTLINE, TILE, rect, shade, type Ctx, type Palette } from "../paint.js";
7
+ import { isSeated, type Item, type World } from "../world.js";
8
+
9
+ function drawDesk(c: Ctx, x: number, y: number, on: boolean, time: number, seed: number, palette: Palette, blocked: boolean): void {
10
+ rect(c, "#55351d", x + 1, y + 22, 46, 8);
11
+ rect(c, "#6b4427", x + 1, y + 13, 46, 10);
12
+ rect(c, "#8a5a33", x + 1, y + 10, 46, 12);
13
+ rect(c, "#a87545", x + 1, y + 10, 46, 2);
14
+ rect(c, "#3b2413", x + 3, y + 22, 3, 9);
15
+ rect(c, "#3b2413", x + 42, y + 22, 3, 9);
16
+ rect(c, "#55351d", x + 30, y + 23, 12, 7);
17
+ rect(c, "#7c5637", x + 31, y + 24, 10, 2);
18
+ rect(c, "#d4a24e", x + 35, y + 24, 2, 1);
19
+ rect(c, "#7c5637", x + 31, y + 27, 10, 2);
20
+ rect(c, "#d4a24e", x + 35, y + 27, 2, 1);
21
+ // Monitor
22
+ rect(c, OUTLINE, x + 14, y - 3, 20, 15);
23
+ rect(c, "#374151", x + 22, y + 12, 4, 2);
24
+ rect(c, "#4b5563", x + 19, y + 14, 10, 1);
25
+ if (blocked) {
26
+ rect(c, "#3f0f14", x + 15, y - 2, 18, 13);
27
+ if (Math.floor(time * 2) % 2 === 0) { rect(c, "#ef4444", x + 23, y, 2, 6); rect(c, "#ef4444", x + 23, y + 7, 2, 2); }
28
+ } else if (on) {
29
+ rect(c, "#0f2a2e", x + 15, y - 2, 18, 13);
30
+ const scroll = Math.floor(time * 3 + (seed % 13));
31
+ for (let line = 0; line < 5; line++) {
32
+ const width = 3 + (((seed >> line) & 7) + (scroll + line) * 7) % 12;
33
+ rect(c, line % 3 === 0 ? palette.primary : line % 3 === 1 ? "#7dd3fc" : "#fcd34d", x + 16 + (line % 2) * 2, y - 1 + line * 2.2, width, 1);
34
+ }
35
+ c.fillStyle = "rgba(125,211,252,0.12)";
36
+ c.fillRect(x + 10, y + 11, 28, 8);
37
+ } else {
38
+ rect(c, "#1f2937", x + 15, y - 2, 18, 13);
39
+ rect(c, "#374151", x + 17, y, 3, 1);
40
+ }
41
+ rect(c, "#d1d5db", x + 16, y + 15, 16, 5);
42
+ for (let row = 0; row < 2; row++) for (let key = 0; key < 7; key++) rect(c, "#9ca3af", x + 17 + key * 2, y + 16 + row * 2, 1, 1);
43
+ rect(c, "#e5e7eb", x + 34, y + 16, 3, 4);
44
+ rect(c, "#f3f4f6", x + 40, y + 13, 5, 5);
45
+ rect(c, palette.primary, x + 40, y + 15, 5, 1);
46
+ rect(c, "#fde68a", x + 4, y + 13, 7, 6);
47
+ rect(c, "#fbbf24", x + 5, y + 15, 5, 1);
48
+ }
49
+
50
+ function drawBoard(c: Ctx, x: number, y: number, world: World): void {
51
+ rect(c, "#3a2c1e", x, y - 12, 64, 27);
52
+ rect(c, "#d9c29c", x + 2, y - 10, 60, 23);
53
+ for (let i = 0; i < 40; i++) rect(c, "#c9b08a", x + 3 + (i * 37) % 58, y - 9 + (i * 13) % 21, 1, 1);
54
+ const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
55
+ const per = 8;
56
+ world.tasks.slice(0, per * 2).forEach((task, index) => {
57
+ const col = index % per;
58
+ const row = Math.floor(index / per);
59
+ rect(c, "#00000033", x + 5 + col * 7, y - 7 + row * 10, 6, 8);
60
+ rect(c, colors[task.state], x + 4 + col * 7, y - 8 + row * 10, 6, 8);
61
+ rect(c, "#ef4444", x + 6 + col * 7, y - 8 + row * 10, 2, 1);
62
+ rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 10, 4, 1);
63
+ rect(c, "#00000040", x + 5 + col * 7, y - 3 + row * 10, 3, 1);
64
+ });
65
+ }
66
+
67
+ function drawFurniture(c: Ctx, item: Item, world: World, time: number, palette: Palette): void {
68
+ const x = item.x * TILE;
69
+ const y = item.y * TILE;
70
+ switch (item.type) {
71
+ case "desk": {
72
+ const seatX = item.x + 1;
73
+ const seatY = item.y + 2;
74
+ const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a));
75
+ drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette, user?.state === "blocked");
76
+ break;
77
+ }
78
+ case "coffee":
79
+ rect(c, "#374151", x, y + 2, 32, 14);
80
+ rect(c, "#6b7280", x, y + 2, 32, 2);
81
+ rect(c, OUTLINE, x + 4, y - 9, 15, 15);
82
+ rect(c, "#27272a", x + 5, y - 8, 13, 13);
83
+ rect(c, Math.floor(time * 2) % 2 ? "#ef4444" : "#7f1d1d", x + 15, y - 6, 2, 2);
84
+ rect(c, "#52525b", x + 7, y - 6, 6, 4);
85
+ rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
86
+ rect(c, "#f3f4f6", x + 23, y - 1, 5, 5);
87
+ rect(c, "#92400e", x + 24, y, 3, 1);
88
+ for (let i = 0; i < 3; i++) {
89
+ c.fillStyle = "rgba(255,255,255,0.5)";
90
+ c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
91
+ }
92
+ break;
93
+ case "water":
94
+ rect(c, "#d1d5db", x + 3, y + 3, 10, 13);
95
+ rect(c, "#9ca3af", x + 3, y + 14, 10, 2);
96
+ rect(c, "#60a5fa", x + 4, y - 8, 8, 11);
97
+ rect(c, "#bae6fd", x + 5, y - 7, 2, 9);
98
+ rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
99
+ rect(c, "#ef4444", x + 9, y + 6, 2, 2);
100
+ if (Math.floor(time * 0.4 + item.x) % 5 === 0) rect(c, "#e0f2fe", x + 8, y - 2 - ((time * 6) % 7), 1, 1);
101
+ break;
102
+ case "sofa":
103
+ rect(c, shade(palette.primary, -60), x + 1, y - 7, 46, 11);
104
+ rect(c, shade(palette.primary, -30), x + 2, y - 6, 44, 3);
105
+ rect(c, "#4b5563", x, y + 3, 48, 11);
106
+ rect(c, "#6b7280", x + 3, y + 3, 20, 7);
107
+ rect(c, "#6b7280", x + 25, y + 3, 20, 7);
108
+ rect(c, "#374151", x, y + 12, 48, 3);
109
+ rect(c, "#374151", x - 1, y - 2, 4, 16);
110
+ rect(c, "#374151", x + 45, y - 2, 4, 16);
111
+ break;
112
+ case "plant":
113
+ rect(c, "#92400e", x + 4, y + 9, 8, 7);
114
+ rect(c, "#b45309", x + 3, y + 8, 10, 2);
115
+ for (const [lx, ly, lw, lh] of [[6, -3, 4, 11], [2, 0, 5, 6], [9, 1, 5, 6], [4, -6, 3, 6], [9, -5, 3, 5]] as const) rect(c, "#15803d", x + lx, y + ly, lw, lh);
116
+ rect(c, "#22c55e", x + 7, y - 3, 2, 8);
117
+ rect(c, "#22c55e", x + 3, y + 1, 2, 3);
118
+ rect(c, "#4ade80", x + 10, y - 4, 1, 3);
119
+ break;
120
+ case "fern":
121
+ rect(c, "#57534e", x + 3, y + 10, 10, 6);
122
+ rect(c, "#78716c", x + 2, y + 9, 12, 2);
123
+ for (let i = 0; i < 7; i++) {
124
+ const lean = i - 3;
125
+ for (let j = 0; j < 5; j++) rect(c, j % 2 ? "#16a34a" : "#15803d", x + 7 + lean * (j / 2 + 1), y + 8 - j * 2 + Math.abs(lean), 2, 2);
126
+ }
127
+ break;
128
+ case "board":
129
+ drawBoard(c, x, y, world);
130
+ break;
131
+ case "whiteboard":
132
+ rect(c, "#9ca3af", x, y - 10, 48, 22);
133
+ rect(c, "#f9fafb", x + 2, y - 8, 44, 18);
134
+ rect(c, "#3b82f6", x + 5, y - 5, 14, 1); rect(c, "#3b82f6", x + 5, y - 2, 9, 1);
135
+ rect(c, "#ef4444", x + 24, y - 6, 8, 6); rect(c, "#f9fafb", x + 25, y - 5, 6, 4);
136
+ rect(c, "#10b981", x + 8, y + 3, 20, 1); rect(c, "#10b981", x + 27, y + 2, 1, 3);
137
+ rect(c, "#6b7280", x + 2, y + 10, 44, 2);
138
+ break;
139
+ case "shelf":
140
+ rect(c, "#5b3a21", x, y - 12, 32, 28);
141
+ for (let shelf = 0; shelf < 2; shelf++) {
142
+ rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
143
+ for (let book = 0; book < 8; book++) {
144
+ const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf + item.x) % 5] as string;
145
+ rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
146
+ rect(c, shade(color, 40), x + 3 + book * 3, y - 6 + shelf * 13, 2, 1);
147
+ }
148
+ }
149
+ break;
150
+ case "server":
151
+ rect(c, OUTLINE, x, y - 13, 16, 29);
152
+ rect(c, "#1f2937", x + 1, y - 12, 14, 27);
153
+ for (let i = 0; i < 5; i++) {
154
+ rect(c, "#111827", x + 2, y - 11 + i * 5, 12, 4);
155
+ const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
156
+ rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
157
+ rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
158
+ rect(c, "#374151", x + 8, y - 10 + i * 5, 5, 1);
159
+ }
160
+ break;
161
+ case "table":
162
+ rect(c, "#44403c", x + 2, y + 16, 44, 12);
163
+ rect(c, "#78716c", x + 2, y + 3, 44, 14);
164
+ rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
165
+ rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
166
+ rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
167
+ rect(c, "#1f2937", x + 12, y + 8, 4, 1);
168
+ rect(c, "#3b82f6", x + 30, y + 10, 4, 1);
169
+ break;
170
+ case "rug":
171
+ rect(c, palette.primary, x, y, 64, 48);
172
+ c.fillStyle = "rgba(0,0,0,0.42)";
173
+ c.fillRect(x + 3, y + 3, 58, 42);
174
+ rect(c, palette.primary, x + 6, y + 6, 52, 1);
175
+ rect(c, palette.primary, x + 6, y + 41, 52, 1);
176
+ rect(c, palette.primary, x + 6, y + 6, 1, 36);
177
+ rect(c, palette.primary, x + 57, y + 6, 1, 36);
178
+ for (let i = 0; i < 6; i++) rect(c, palette.primary, x + 12 + i * 8, y + 22, 4, 4);
179
+ break;
180
+ case "arcade":
181
+ rect(c, OUTLINE, x, y - 14, 16, 30);
182
+ rect(c, "#4c1d95", x + 1, y - 13, 14, 28);
183
+ rect(c, "#111827", x + 3, y - 10, 10, 9);
184
+ rect(c, ["#22c55e", "#ec4899", "#38bdf8"][Math.floor(time * 2) % 3] as string, x + 4 + (Math.floor(time * 4) % 7), y - 8 + (Math.floor(time * 3) % 5), 2, 2);
185
+ rect(c, "#f59e0b", x + 3, y - 13, 10, 2);
186
+ rect(c, "#27272a", x + 2, y + 1, 12, 4);
187
+ rect(c, "#ef4444", x + 4, y + 2, 2, 2);
188
+ rect(c, "#3b82f6", x + 9, y + 2, 2, 2);
189
+ break;
190
+ case "fridge":
191
+ rect(c, OUTLINE, x + 1, y - 12, 14, 28);
192
+ rect(c, "#e5e7eb", x + 2, y - 11, 12, 26);
193
+ rect(c, "#9ca3af", x + 2, y - 3, 12, 1);
194
+ rect(c, "#6b7280", x + 11, y - 8, 1, 3);
195
+ rect(c, "#6b7280", x + 11, y, 1, 5);
196
+ rect(c, "#fde68a", x + 4, y - 9, 3, 3);
197
+ rect(c, "#fca5a5", x + 5, y + 3, 3, 3);
198
+ break;
199
+ case "printer":
200
+ rect(c, "#4b5563", x + 2, y + 8, 12, 8);
201
+ rect(c, OUTLINE, x + 1, y - 2, 14, 11);
202
+ rect(c, "#d1d5db", x + 2, y - 1, 12, 9);
203
+ rect(c, "#f9fafb", x + 4, y - 5 - (Math.floor(time) % 4 === 0 ? 1 : 0), 8, 5);
204
+ rect(c, "#22c55e", x + 11, y + 1, 2, 1);
205
+ rect(c, "#9ca3af", x + 3, y + 5, 10, 1);
206
+ break;
207
+ case "lamp":
208
+ rect(c, "#374151", x + 5, y + 13, 6, 3);
209
+ rect(c, "#4b5563", x + 7, y - 4, 2, 17);
210
+ rect(c, OUTLINE, x + 2, y - 10, 12, 8);
211
+ rect(c, "#fde68a", x + 3, y - 9, 10, 6);
212
+ rect(c, "#fef3c7", x + 4, y - 8, 4, 2);
213
+ break;
214
+ case "beanbag":
215
+ rect(c, OUTLINE, x, y + 3, 16, 13);
216
+ rect(c, shade(palette.primary, -50), x + 1, y + 4, 14, 11);
217
+ rect(c, shade(palette.primary, -20), x + 3, y + 5, 8, 4);
218
+ rect(c, "#00000040", x + 1, y + 13, 14, 2);
219
+ break;
220
+ case "tv":
221
+ rect(c, "#3b2413", x + 2, y + 8, 44, 8);
222
+ rect(c, "#5b3a21", x + 2, y + 8, 44, 2);
223
+ rect(c, OUTLINE, x + 4, y - 14, 40, 22);
224
+ rect(c, "#0b1120", x + 5, y - 13, 38, 20);
225
+ for (let i = 0; i < 9; i++) {
226
+ const h = 2 + ((i * 5 + Math.floor(time * 3)) % 12);
227
+ rect(c, i % 3 === 0 ? palette.primary : i % 3 === 1 ? "#38bdf8" : "#f472b6", x + 8 + i * 4, y + 5 - h, 3, h);
228
+ }
229
+ break;
230
+ case "vending":
231
+ rect(c, OUTLINE, x, y - 14, 16, 30);
232
+ rect(c, "#b91c1c", x + 1, y - 13, 14, 28);
233
+ rect(c, "#0f172a", x + 3, y - 11, 8, 16);
234
+ for (let row = 0; row < 3; row++) for (let col = 0; col < 2; col++) rect(c, ["#fbbf24", "#34d399", "#60a5fa", "#f472b6"][(row + col * 2) % 4] as string, x + 4 + col * 3, y - 10 + row * 5, 2, 4);
235
+ rect(c, "#d1d5db", x + 12, y - 9, 2, 6);
236
+ rect(c, "#111827", x + 3, y + 8, 10, 4);
237
+ break;
238
+ case "trophy":
239
+ rect(c, "#6b4427", x + 2, y + 8, 12, 8);
240
+ rect(c, "#8a5a33", x + 2, y + 8, 12, 2);
241
+ rect(c, "#78350f", x + 5, y + 5, 6, 3);
242
+ rect(c, "#f59e0b", x + 7, y + 1, 2, 4);
243
+ rect(c, OUTLINE, x + 3, y - 8, 10, 10);
244
+ rect(c, "#fbbf24", x + 4, y - 7, 8, 8);
245
+ rect(c, "#fde68a", x + 5, y - 6, 2, 5);
246
+ rect(c, "#fbbf24", x + 1, y - 6, 3, 1); rect(c, "#fbbf24", x + 12, y - 6, 3, 1);
247
+ break;
248
+ case "clock": {
249
+ rect(c, OUTLINE, x + 3, y + 1, 10, 10);
250
+ rect(c, "#f9fafb", x + 4, y + 2, 8, 8);
251
+ rect(c, "#1f2937", x + 8, y + 3, 1, 3);
252
+ rect(c, "#1f2937", x + 8, y + 6, 3, 1);
253
+ break;
254
+ }
255
+ case "poster": {
256
+ const colors = ["#7c3aed", "#0ea5e9", "#db2777", "#059669"];
257
+ const color = colors[(item.x + item.y) % colors.length] as string;
258
+ rect(c, OUTLINE, x + 2, y, 12, 11);
259
+ rect(c, color, x + 3, y + 1, 10, 9);
260
+ rect(c, shade(color, 60), x + 5, y + 3, 6, 3);
261
+ rect(c, "#f9fafb", x + 5, y + 7, 6, 1);
262
+ break;
263
+ }
264
+ }
265
+ }
266
+
267
+ export const OFFICE_PAINTER = {
268
+ furniture: drawFurniture,
269
+ glow(item: Item, world: World, glow: (x: number, y: number, radius: number, color: string) => void): void {
270
+ if (item.type === "lamp") glow(item.x * TILE + 8, item.y * TILE, 36, "rgba(253,224,139,0.30)");
271
+ if (item.type === "arcade" || item.type === "vending" || item.type === "tv") glow(item.x * TILE + 8, item.y * TILE, 26, "rgba(125,211,252,0.16)");
272
+ if (item.type === "desk" && world.agents.some((a) => a.seat?.x === item.x + 1 && a.seat?.y === item.y + 2 && isSeated(a))) {
273
+ glow(item.x * TILE + 24, item.y * TILE + 8, 30, "rgba(125,211,252,0.26)");
274
+ }
275
+ },
276
+ };