@blkc-code/render-canvas 0.3.0 → 0.7.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 +82 -306
  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 +7 -3
  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 +14 -2
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/world.d.ts +18 -13
  30. package/dist/world.d.ts.map +1 -1
  31. package/dist/world.js +42 -64
  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 +53 -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 +300 -0
  40. package/dist/worlds/office-paint.js.map +1 -0
  41. package/package.json +7 -2
  42. package/src/draw.ts +80 -289
  43. package/src/index.ts +3 -0
  44. package/src/links.ts +76 -0
  45. package/src/office.ts +8 -3
  46. package/src/pack.ts +36 -0
  47. package/src/paint.ts +24 -0
  48. package/src/types.ts +13 -3
  49. package/src/world.ts +53 -66
  50. package/src/worlds/office-data.ts +58 -0
  51. package/src/worlds/office-paint.ts +284 -0
package/src/types.ts CHANGED
@@ -2,11 +2,14 @@
2
2
 
3
3
  export type AgentState = "working" | "thinking" | "blocked" | "reviewing" | "idle" | "done" | "away";
4
4
  export type TaskState = "todo" | "doing" | "done";
5
- export type FloorStyle = "wood" | "carpet" | "tile" | "stone" | "concrete" | "grass";
6
- export type FurnitureType =
5
+ export type FloorStyle = "wood" | "carpet" | "tile" | "stone" | "concrete" | "grass" | "hall";
6
+ export type OfficeFurnitureType =
7
7
  | "desk" | "coffee" | "sofa" | "plant" | "fern" | "board" | "whiteboard" | "shelf" | "server" | "table" | "rug"
8
8
  | "water" | "arcade" | "fridge" | "printer" | "lamp" | "beanbag" | "tv" | "vending" | "clock" | "poster" | "trophy";
9
9
 
10
+ /** An office item, or whatever else the world defines. */
11
+ export type FurnitureType = OfficeFurnitureType | (string & {});
12
+
10
13
  export interface OfficeRoom {
11
14
  id?: string;
12
15
  name?: string;
@@ -19,7 +22,7 @@ export interface OfficeRoom {
19
22
  paths?: string[];
20
23
  }
21
24
  export interface OfficeDoor { x: number; y: number }
22
- export interface OfficeFurniture { type: FurnitureType; x: number; y: number }
25
+ export interface OfficeFurniture { type: FurnitureType; x: number; y: number; label?: string }
23
26
  export interface OfficeAgent {
24
27
  id: string;
25
28
  name?: string;
@@ -35,15 +38,22 @@ export interface OfficeAgent {
35
38
  }
36
39
  export interface OfficeTask { title: string; state?: TaskState; agent?: string }
37
40
 
41
+ /** `from` depends on `to`. */
42
+ export interface OfficeLink { from: string; to: string }
43
+
38
44
  export interface OfficeLayout {
39
45
  cols?: number;
40
46
  rows?: number;
41
47
  rooms?: OfficeRoom[];
42
48
  doors?: OfficeDoor[];
43
49
  furniture?: OfficeFurniture[];
50
+ /** How the rooms' code depends on each other. Drawn as lines between them. */
51
+ links?: OfficeLink[];
44
52
  }
45
53
 
46
54
  export interface OfficeScene extends OfficeLayout {
55
+ /** Which world to draw. The office if left out or unknown. */
56
+ world?: string;
47
57
  agents?: OfficeAgent[];
48
58
  tasks?: OfficeTask[];
49
59
  }
package/src/world.ts CHANGED
@@ -3,70 +3,36 @@
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
- export const MAX_COLS = 64;
9
- export const MAX_ROWS = 40;
11
+ export const MAX_COLS = 1024;
12
+ export const MAX_ROWS = 1024;
10
13
  const MAX_AGENTS = 40;
11
- const MAX_FURNITURE = 300;
14
+ const MAX_FURNITURE = 60000;
15
+ const MAX_ROOMS = 8000;
16
+ const MAX_DOORS = 16000;
17
+ const MAX_LINKS = 4000;
12
18
  const MAX_TASKS = 60;
13
19
  export const WALK_SPEED = 4.5; // tiles a second
14
20
 
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
- };
21
+ /** Footprints and uses, for the office. Other worlds bring their own. */
22
+ export const FURNITURE = OFFICE_ITEMS;
40
23
 
41
24
  const AGENT_STATES: readonly string[] = ["working", "thinking", "blocked", "reviewing", "idle", "done", "away"];
42
25
  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
- };
26
+ const FLOORS: readonly string[] = ["wood", "carpet", "tile", "stone", "concrete", "grass", "hall"];
27
+
28
+ export const DEFAULT_LAYOUT = OFFICE_LAYOUT;
63
29
 
64
30
  const int = (value: unknown, fallback: number): number => (typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback);
65
31
  const str = (value: unknown, max = 120): string => (typeof value === "string" ? value.slice(0, max) : "");
66
32
  const list = <T>(value: unknown, max: number): T[] => (Array.isArray(value) ? (value.slice(0, max) as T[]) : []);
67
33
 
68
34
  export interface Room { id: string; name: string; x: number; y: number; w: number; h: number; floor: FloorStyle; paths: string[] }
69
- export interface Item { type: FurnitureType; x: number; y: number }
35
+ export interface Item { type: FurnitureType; x: number; y: number; label?: string }
70
36
  export interface Task { title: string; state: TaskState; agent: string }
71
37
 
72
38
  export interface Agent {
@@ -94,6 +60,12 @@ export interface Agent {
94
60
  }
95
61
 
96
62
  export interface World {
63
+ /** What this world is made of. */
64
+ pack: WorldData;
65
+ links: { from: string; to: string }[];
66
+ /** Worked out when first drawn, and again when the building changes. */
67
+ routes?: LinkRoute[];
68
+ showLinks: boolean;
97
69
  cols: number;
98
70
  rows: number;
99
71
  rooms: Room[];
@@ -187,13 +159,18 @@ function toTask(raw: OfficeTask): Task {
187
159
  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
160
  }
189
161
 
190
- function buildLayout(scene: OfficeLayout): Pick<World, "cols" | "rows" | "rooms" | "doors" | "furniture" | "walkable" | "spots" | "seats"> {
162
+ registerWorld(OFFICE);
163
+
164
+ /** A piece of furniture's spec. Only items in the world's list get into a World. */
165
+ export const specOf = (world: Pick<World, "pack">, item: Item): ItemSpec => world.pack.items[item.type] as ItemSpec;
166
+
167
+ function buildLayout(scene: OfficeLayout, pack: WorldData): Pick<World, "links" | "cols" | "rows" | "rooms" | "doors" | "furniture" | "walkable" | "spots" | "seats"> {
191
168
  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;
169
+ const cols = Math.min(Math.max(int(scene?.cols, custom ? 20 : pack.layout.cols), 6), MAX_COLS);
170
+ const rows = Math.min(Math.max(int(scene?.rows, custom ? 14 : pack.layout.rows), 6), MAX_ROWS);
171
+ const rawRooms = custom ? list<OfficeRoom>(scene.rooms, MAX_ROOMS) : pack.layout.rooms;
172
+ const rawDoors = custom ? list<OfficeDoor>(scene.doors, MAX_DOORS) : pack.layout.doors;
173
+ const rawItems = Array.isArray(scene?.furniture) ? list<OfficeFurniture>(scene.furniture, MAX_FURNITURE) : custom ? [] : pack.layout.furniture;
197
174
 
198
175
  // Clamped to the grid: everything later loops over these, so a huge room must not hang the page.
199
176
  const rooms: Room[] = rawRooms.map((room, index) => {
@@ -201,7 +178,7 @@ function buildLayout(scene: OfficeLayout): Pick<World, "cols" | "rows" | "rooms"
201
178
  const y = Math.min(Math.max(int(room?.y, 0), 0), rows);
202
179
  return {
203
180
  id: str(room?.id, 60) || `room-${index + 1}`,
204
- name: str(room?.name, 24),
181
+ name: str(room?.name, 60),
205
182
  x, y,
206
183
  w: Math.min(Math.max(int(room?.w, 0), 0), cols - x),
207
184
  h: Math.min(Math.max(int(room?.h, 0), 0), rows - y),
@@ -220,36 +197,46 @@ function buildLayout(scene: OfficeLayout): Pick<World, "cols" | "rows" | "rooms"
220
197
  for (const door of doors) mark(door.x, door.y, true);
221
198
 
222
199
  const furniture: Item[] = rawItems
223
- .filter((item) => item && Object.hasOwn(FURNITURE, item.type))
224
- .map((item) => ({ type: item.type, x: int(item.x, -99), y: int(item.y, -99) }));
200
+ .filter((item) => item && Object.hasOwn(pack.items, item.type))
201
+ .map((item) => {
202
+ const label = str(item.label, 40);
203
+ return { type: item.type, x: int(item.x, -99), y: int(item.y, -99), ...(label ? { label } : {}) };
204
+ });
225
205
  for (const item of furniture) {
226
- const spec = FURNITURE[item.type];
206
+ const spec = pack.items[item.type] as ItemSpec;
227
207
  if (spec.solid === false) continue;
228
208
  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
209
  }
230
210
 
231
211
  const open = (x: number, y: number): boolean => x >= 0 && y >= 0 && x < cols && y < rows && walkable[y * cols + x] === true;
232
212
  const spot = (item: Item): { x: number; y: number } | undefined => {
233
- const offset = FURNITURE[item.type].spot;
213
+ const offset = (pack.items[item.type] as ItemSpec).spot;
234
214
  if (!offset) return undefined;
235
215
  const point = { x: item.x + offset.x, y: item.y + offset.y };
236
216
  return open(point.x, point.y) ? point : undefined;
237
217
  };
238
- const seats = furniture.filter((item) => item.type === "desk").map(spot).filter((p): p is { x: number; y: number } => Boolean(p));
218
+ 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
219
  const spots: World["spots"] = { lounge: [], board: [] };
240
220
  for (const item of furniture) {
241
221
  const point = spot(item);
242
222
  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);
223
+ const use = (pack.items[item.type] as ItemSpec).use;
224
+ if (use === "lounge") spots.lounge.push(point);
225
+ if (use === "board") spots.board.push(point);
245
226
  }
246
- return { cols, rows, rooms, doors, furniture, walkable, seats, spots };
227
+ const links = list<{ from: string; to: string }>(scene?.links, MAX_LINKS)
228
+ .map((link) => ({ from: str(link?.from, 60), to: str(link?.to, 60) }))
229
+ .filter((link) => link.from && link.to);
230
+ return { cols, rows, rooms, doors, furniture, walkable, seats, spots, links };
247
231
  }
248
232
 
249
233
  /** Build a world from a scene. Bad values are dropped or clamped, so it never throws. */
250
234
  export function createWorld(scene: OfficeScene = {}): World {
235
+ const pack = worldData(scene?.world, OFFICE);
251
236
  const world: World = {
252
- ...buildLayout(scene),
237
+ pack,
238
+ showLinks: true,
239
+ ...buildLayout(scene, pack),
253
240
  agents: list<OfficeAgent>(scene?.agents, MAX_AGENTS).map(toAgent),
254
241
  tasks: list<OfficeTask>(scene?.tasks, MAX_TASKS).map(toTask),
255
242
  };
@@ -259,7 +246,7 @@ export function createWorld(scene: OfficeScene = {}): World {
259
246
 
260
247
  /** Change the building while everyone is in it. Anyone left inside a wall or a desk moves. */
261
248
  export function applyLayout(world: World, layout: OfficeLayout): void {
262
- Object.assign(world, buildLayout(layout));
249
+ Object.assign(world, buildLayout(layout, world.pack), { routes: undefined });
263
250
  for (const agent of world.agents) {
264
251
  agent.seat = undefined;
265
252
  agent.path = [];
@@ -0,0 +1,58 @@
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
+ pile: { w: 1, h: 1, solid: false },
21
+ arcade: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
22
+ fridge: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
23
+ printer: { w: 1, h: 1, spot: { x: 0, y: 1 } },
24
+ lamp: { w: 1, h: 1 },
25
+ beanbag: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
26
+ tv: { w: 3, h: 1, spot: { x: 1, y: 2 }, use: "lounge" },
27
+ vending: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
28
+ trophy: { w: 1, h: 1 },
29
+ clock: { w: 1, h: 1, solid: false, wall: true },
30
+ poster: { w: 1, h: 1, solid: false, wall: true },
31
+ };
32
+
33
+ export const OFFICE_LAYOUT = {
34
+ cols: 26,
35
+ rows: 17,
36
+ rooms: [
37
+ { id: "studio", name: "Studio", x: 1, y: 3, w: 15, h: 13, floor: "wood" },
38
+ { id: "lounge", name: "Lounge", x: 17, y: 3, w: 8, h: 6, floor: "carpet" },
39
+ { id: "quests", name: "Quests", x: 17, y: 10, w: 8, h: 6, floor: "tile" },
40
+ ] as OfficeRoom[],
41
+ doors: [{ x: 16, y: 6 }, { x: 16, y: 13 }, { x: 23, y: 9 }] as OfficeDoor[],
42
+ furniture: [
43
+ { type: "desk", x: 2, y: 4 }, { type: "desk", x: 7, y: 4 }, { type: "desk", x: 12, y: 4 },
44
+ { type: "desk", x: 2, y: 10 }, { type: "desk", x: 7, y: 10 }, { type: "desk", x: 12, y: 10 },
45
+ { type: "plant", x: 1, y: 8 }, { type: "plant", x: 15, y: 8 }, { type: "server", x: 15, y: 15 }, { type: "server", x: 14, y: 15 },
46
+ { type: "shelf", x: 1, y: 15 },
47
+ { 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 },
48
+ { type: "board", x: 19, y: 10 }, { type: "table", x: 19, y: 13 }, { type: "plant", x: 24, y: 15 },
49
+ ] as OfficeFurniture[],
50
+ };
51
+
52
+ export const OFFICE: WorldData = {
53
+ id: "office",
54
+ name: "Pixel Office",
55
+ words: { quests: "QUESTS" },
56
+ items: OFFICE_ITEMS,
57
+ layout: OFFICE_LAYOUT,
58
+ };
@@ -0,0 +1,284 @@
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 "pile":
171
+ // A stack of boxes, for a group of loose files.
172
+ rect(c, "#14141f", x + 1, y + 5, 14, 10);
173
+ rect(c, "#b98b55", x + 2, y + 6, 12, 8);
174
+ rect(c, "#d3a874", x + 2, y + 6, 12, 2);
175
+ rect(c, "#14141f", x + 4, y + 1, 8, 5);
176
+ rect(c, "#d3a874", x + 5, y + 2, 6, 3);
177
+ break;
178
+ case "rug":
179
+ rect(c, palette.primary, x, y, 64, 48);
180
+ c.fillStyle = "rgba(0,0,0,0.42)";
181
+ c.fillRect(x + 3, y + 3, 58, 42);
182
+ rect(c, palette.primary, x + 6, y + 6, 52, 1);
183
+ rect(c, palette.primary, x + 6, y + 41, 52, 1);
184
+ rect(c, palette.primary, x + 6, y + 6, 1, 36);
185
+ rect(c, palette.primary, x + 57, y + 6, 1, 36);
186
+ for (let i = 0; i < 6; i++) rect(c, palette.primary, x + 12 + i * 8, y + 22, 4, 4);
187
+ break;
188
+ case "arcade":
189
+ rect(c, OUTLINE, x, y - 14, 16, 30);
190
+ rect(c, "#4c1d95", x + 1, y - 13, 14, 28);
191
+ rect(c, "#111827", x + 3, y - 10, 10, 9);
192
+ 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);
193
+ rect(c, "#f59e0b", x + 3, y - 13, 10, 2);
194
+ rect(c, "#27272a", x + 2, y + 1, 12, 4);
195
+ rect(c, "#ef4444", x + 4, y + 2, 2, 2);
196
+ rect(c, "#3b82f6", x + 9, y + 2, 2, 2);
197
+ break;
198
+ case "fridge":
199
+ rect(c, OUTLINE, x + 1, y - 12, 14, 28);
200
+ rect(c, "#e5e7eb", x + 2, y - 11, 12, 26);
201
+ rect(c, "#9ca3af", x + 2, y - 3, 12, 1);
202
+ rect(c, "#6b7280", x + 11, y - 8, 1, 3);
203
+ rect(c, "#6b7280", x + 11, y, 1, 5);
204
+ rect(c, "#fde68a", x + 4, y - 9, 3, 3);
205
+ rect(c, "#fca5a5", x + 5, y + 3, 3, 3);
206
+ break;
207
+ case "printer":
208
+ rect(c, "#4b5563", x + 2, y + 8, 12, 8);
209
+ rect(c, OUTLINE, x + 1, y - 2, 14, 11);
210
+ rect(c, "#d1d5db", x + 2, y - 1, 12, 9);
211
+ rect(c, "#f9fafb", x + 4, y - 5 - (Math.floor(time) % 4 === 0 ? 1 : 0), 8, 5);
212
+ rect(c, "#22c55e", x + 11, y + 1, 2, 1);
213
+ rect(c, "#9ca3af", x + 3, y + 5, 10, 1);
214
+ break;
215
+ case "lamp":
216
+ rect(c, "#374151", x + 5, y + 13, 6, 3);
217
+ rect(c, "#4b5563", x + 7, y - 4, 2, 17);
218
+ rect(c, OUTLINE, x + 2, y - 10, 12, 8);
219
+ rect(c, "#fde68a", x + 3, y - 9, 10, 6);
220
+ rect(c, "#fef3c7", x + 4, y - 8, 4, 2);
221
+ break;
222
+ case "beanbag":
223
+ rect(c, OUTLINE, x, y + 3, 16, 13);
224
+ rect(c, shade(palette.primary, -50), x + 1, y + 4, 14, 11);
225
+ rect(c, shade(palette.primary, -20), x + 3, y + 5, 8, 4);
226
+ rect(c, "#00000040", x + 1, y + 13, 14, 2);
227
+ break;
228
+ case "tv":
229
+ rect(c, "#3b2413", x + 2, y + 8, 44, 8);
230
+ rect(c, "#5b3a21", x + 2, y + 8, 44, 2);
231
+ rect(c, OUTLINE, x + 4, y - 14, 40, 22);
232
+ rect(c, "#0b1120", x + 5, y - 13, 38, 20);
233
+ for (let i = 0; i < 9; i++) {
234
+ const h = 2 + ((i * 5 + Math.floor(time * 3)) % 12);
235
+ rect(c, i % 3 === 0 ? palette.primary : i % 3 === 1 ? "#38bdf8" : "#f472b6", x + 8 + i * 4, y + 5 - h, 3, h);
236
+ }
237
+ break;
238
+ case "vending":
239
+ rect(c, OUTLINE, x, y - 14, 16, 30);
240
+ rect(c, "#b91c1c", x + 1, y - 13, 14, 28);
241
+ rect(c, "#0f172a", x + 3, y - 11, 8, 16);
242
+ 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);
243
+ rect(c, "#d1d5db", x + 12, y - 9, 2, 6);
244
+ rect(c, "#111827", x + 3, y + 8, 10, 4);
245
+ break;
246
+ case "trophy":
247
+ rect(c, "#6b4427", x + 2, y + 8, 12, 8);
248
+ rect(c, "#8a5a33", x + 2, y + 8, 12, 2);
249
+ rect(c, "#78350f", x + 5, y + 5, 6, 3);
250
+ rect(c, "#f59e0b", x + 7, y + 1, 2, 4);
251
+ rect(c, OUTLINE, x + 3, y - 8, 10, 10);
252
+ rect(c, "#fbbf24", x + 4, y - 7, 8, 8);
253
+ rect(c, "#fde68a", x + 5, y - 6, 2, 5);
254
+ rect(c, "#fbbf24", x + 1, y - 6, 3, 1); rect(c, "#fbbf24", x + 12, y - 6, 3, 1);
255
+ break;
256
+ case "clock": {
257
+ rect(c, OUTLINE, x + 3, y + 1, 10, 10);
258
+ rect(c, "#f9fafb", x + 4, y + 2, 8, 8);
259
+ rect(c, "#1f2937", x + 8, y + 3, 1, 3);
260
+ rect(c, "#1f2937", x + 8, y + 6, 3, 1);
261
+ break;
262
+ }
263
+ case "poster": {
264
+ const colors = ["#7c3aed", "#0ea5e9", "#db2777", "#059669"];
265
+ const color = colors[(item.x + item.y) % colors.length] as string;
266
+ rect(c, OUTLINE, x + 2, y, 12, 11);
267
+ rect(c, color, x + 3, y + 1, 10, 9);
268
+ rect(c, shade(color, 60), x + 5, y + 3, 6, 3);
269
+ rect(c, "#f9fafb", x + 5, y + 7, 6, 1);
270
+ break;
271
+ }
272
+ }
273
+ }
274
+
275
+ export const OFFICE_PAINTER = {
276
+ furniture: drawFurniture,
277
+ glow(item: Item, world: World, glow: (x: number, y: number, radius: number, color: string) => void): void {
278
+ if (item.type === "lamp") glow(item.x * TILE + 8, item.y * TILE, 36, "rgba(253,224,139,0.30)");
279
+ 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)");
280
+ if (item.type === "desk" && world.agents.some((a) => a.seat?.x === item.x + 1 && a.seat?.y === item.y + 2 && isSeated(a))) {
281
+ glow(item.x * TILE + 24, item.y * TILE + 8, 30, "rgba(125,211,252,0.26)");
282
+ }
283
+ },
284
+ };