@blkc-code/render-canvas 0.1.0 → 0.3.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.
package/dist/draw.js CHANGED
@@ -1,90 +1,173 @@
1
1
  /**
2
- * Draws the office. Every piece of art here is original to this project, made of
3
- * rectangles and the character maps below at 16 pixels a tile.
2
+ * Draws the office. Every piece of art here is original to this project, made in code at
3
+ * 16 pixels a tile. The building is drawn once to a layer (drawBuilding); people, screens
4
+ * and anything else that moves are drawn on top each frame (drawWorld).
4
5
  */
5
6
  import { FURNITURE, isSeated } from "./world.js";
6
7
  export const TILE = 16;
8
+ const LABEL_FONT = "bold 7px ui-monospace, Menlo, Consolas, monospace";
9
+ const BODY_FONT = "7px ui-monospace, Menlo, Consolas, monospace";
10
+ const OUTLINE = "#14141f";
7
11
  const rect = (c, color, x, y, w, h) => {
8
12
  c.fillStyle = color;
9
13
  c.fillRect(Math.round(x), Math.round(y), w, h);
10
14
  };
11
- // --- Characters: 12 wide. h hair, s skin, e eye, t shirt, T shirt shadow, l legs, b shoes -----------------
12
- const HEAD_FRONT = ["...hhhhhh...", "..hhhhhhhh..", "..hhhhhhhh..", "..hssssssh..", "..ssessess..", "..ssssssss..", "...ssssss..."];
13
- const HEAD_BACK = ["...hhhhhh...", "..hhhhhhhh..", "..hhhhhhhh..", "..hhhhhhhh..", "..hhhhhhhh..", "..hhhhhhhh..", "...ssssss..."];
14
- const HEAD_SIDE = ["...hhhhhh...", "..hhhhhhhh..", "..hhhhhhhh..", "..hhssssss..", "..hhsssess..", "..hsssssss..", "...ssssss..."];
15
- const BODY = ["..tttttttt..", ".tttttttttt.", ".stttttttts.", ".stTTTTTTts."];
16
- const BODY_SIDE = ["...tttttt...", "...tttttt...", "...ttsttt...", "...TTsTTT..."];
17
- const LEGS_STAND = ["..llllllll..", "..lll..lll..", "..lll..lll..", "..bbb..bbb.."];
18
- const LEGS_A = ["..llllllll..", "..lll..lll..", "..bbb..lll..", ".......bbb.."];
19
- const LEGS_B = ["..llllllll..", "..lll..lll..", "..lll..bbb..", "..bbb......."];
20
- const LEGS_SIDE_STAND = ["...llllll...", "...lll.lll..", "...lll.lll..", "...bbb.bbb.."];
21
- const LEGS_SIDE_A = ["...llllll...", "..lll..lll..", ".lll....lll.", ".bb......bb."];
22
- const LEGS_SIDE_B = ["...llllll...", "...llllll...", "....llll....", "....bbbb...."];
23
- function drawMap(c, map, x, y, colors, flip = false) {
24
- map.forEach((row, ry) => {
25
- for (let rx = 0; rx < row.length; rx++) {
26
- const key = row[flip ? row.length - 1 - rx : rx];
27
- const color = colors[key];
28
- if (color)
29
- rect(c, color, x + rx, y + ry, 1, 1);
30
- }
31
- });
32
- }
33
- function shade(hex, amount) {
34
- const n = parseInt(hex.slice(1).padEnd(6, hex.slice(-1)), 16);
15
+ function shade(color, amount) {
16
+ if (!color.startsWith("#"))
17
+ return color;
18
+ const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
19
+ const n = parseInt(hex.slice(1, 7), 16);
35
20
  const channel = (shift) => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
36
21
  return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
37
22
  }
23
+ export function daylightAt(hour) {
24
+ if (hour >= 7 && hour < 17)
25
+ return { sky: "#8ed0ff", sun: "rgba(255,244,214,0.10)", night: 0 };
26
+ if (hour >= 17 && hour < 20)
27
+ return { sky: "#ffae70", sun: "rgba(255,170,110,0.13)", night: 0.14 };
28
+ if (hour >= 5 && hour < 7)
29
+ return { sky: "#ffc9a3", sun: "rgba(255,200,170,0.08)", night: 0.16 };
30
+ return { sky: "#16204a", sun: "rgba(0,0,0,0)", night: 0.24 };
31
+ }
32
+ /** Parts draw with one outline around the whole figure, so characters read clearly on any floor. */
33
+ function drawParts(c, parts) {
34
+ c.fillStyle = OUTLINE;
35
+ for (const [, x, y, w, h] of parts)
36
+ c.fillRect(Math.round(x) - 1, Math.round(y) - 1, w + 2, h + 2);
37
+ for (const [color, x, y, w, h] of parts)
38
+ rect(c, color, x, y, w, h);
39
+ }
38
40
  function drawAgent(c, agent, time, selected) {
39
- const px = agent.x * TILE + 2;
41
+ const left = agent.x * TILE + 2; // sprite is 12 wide inside a 16 tile
42
+ const feet = agent.y * TILE + 15;
40
43
  const seated = isSeated(agent);
41
- const colors = { h: agent.hair, s: agent.skin, e: "#1a1a24", t: agent.shirt, T: shade(agent.shirt, -36), l: "#39415a", b: "#1f2433" };
42
- const stride = Math.floor(time * 8 + agent.seed) % 2 === 0;
43
- const breathe = !agent.walking && Math.floor(time * 1.5 + agent.seed % 7) % 2 === 0 ? 1 : 0;
44
- const height = seated ? 11 : 15;
45
- // Seated people sit just in front of the desk, on a chair, with their heads clear of the keyboard.
46
- const py = agent.y * TILE + (seated ? 6 : -1) - (agent.walking && stride ? 1 : 0);
44
+ const walking = agent.walking;
45
+ const side = agent.facing === "left" || agent.facing === "right";
46
+ const back = agent.facing === "up";
47
+ const flip = agent.facing === "left";
48
+ const phase = Math.floor(time * 9 + (agent.seed % 5));
49
+ const step = walking ? phase % 4 : 0; // 0 pass, 1 left forward, 2 pass, 3 right forward
50
+ const bob = walking && step % 2 === 1 ? -1 : 0;
51
+ const breathe = !walking && !seated && Math.floor(time * 1.4 + (agent.seed % 7)) % 2 === 0 ? 1 : 0;
52
+ const party = agent.party > 0 ? -Math.abs(Math.round(Math.sin(time * 12) * 3)) : 0;
53
+ const skin = agent.skin;
54
+ const hair = agent.hair;
55
+ const shirt = agent.shirt;
56
+ const dark = shade(shirt, -40);
57
+ const light = shade(shirt, 24);
58
+ const legs = "#39415a";
59
+ const shoe = "#1f2433";
60
+ const style = agent.seed % 5; // short, long, bun, spiky, cap
61
+ const outfit = (agent.seed >>> 5) % 3; // tee, hoodie, shirt and tie
62
+ // Shadow, and a highlight under whoever is selected.
63
+ c.fillStyle = "rgba(0,0,0,0.28)";
64
+ c.fillRect(Math.round(left + 1), Math.round(feet - 1), 10, 2);
65
+ if (selected) {
66
+ c.fillStyle = "rgba(255,255,255,0.45)";
67
+ c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
68
+ }
69
+ if (agent.state === "blocked") {
70
+ c.fillStyle = Math.floor(time * 3) % 2 === 0 ? "rgba(239,68,68,0.55)" : "rgba(239,68,68,0.22)";
71
+ c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
72
+ }
73
+ const lift = bob + party;
74
+ const parts = [];
75
+ const bodyTop = feet - (seated ? 13 : 13) + lift + breathe * 0;
76
+ const headTop = bodyTop - 8 + breathe;
77
+ // Chair behind someone sitting at a desk.
47
78
  if (seated) {
48
- rect(c, "#1f2937", px + 1, agent.y * TILE + 3, 10, 9);
49
- rect(c, "#374151", px + 2, agent.y * TILE + 11, 8, 3);
50
- rect(c, "#111827", px + 5, agent.y * TILE + 14, 2, 2);
79
+ rect(c, "#1f2937", left + 1, agent.y * TILE + 3, 10, 9);
80
+ rect(c, "#374151", left + 2, agent.y * TILE + 11, 8, 3);
81
+ rect(c, "#111827", left + 5, agent.y * TILE + 14, 2, 2);
51
82
  }
52
- // Shadow
53
- c.fillStyle = "rgba(0,0,0,0.25)";
54
- c.fillRect(Math.round(px + 2), Math.round(agent.y * TILE + 14), 8, 2);
55
- if (selected) {
56
- c.fillStyle = "rgba(255,255,255,0.28)";
57
- c.fillRect(Math.round(px - 1), Math.round(agent.y * TILE + 13), 14, 4);
83
+ // Legs
84
+ if (!seated) {
85
+ const legTop = bodyTop + 7;
86
+ if (side) {
87
+ const swing = step === 1 ? 2 : step === 3 ? -2 : 0;
88
+ parts.push([legs, left + 4 + swing, legTop, 3, 5], [shoe, left + 4 + swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
89
+ parts.push([shade(legs, -12), left + 5 - swing, legTop, 3, 5], [shoe, left + 5 - swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
90
+ }
91
+ else {
92
+ const leftUp = step === 1 ? 1 : 0;
93
+ const rightUp = step === 3 ? 1 : 0;
94
+ parts.push([legs, left + 2, legTop, 3, 5 - leftUp], [shoe, left + 2, legTop + 5 - leftUp, 3, 1]);
95
+ parts.push([legs, left + 7, legTop, 3, 5 - rightUp], [shoe, left + 7, legTop + 5 - rightUp, 3, 1]);
96
+ }
58
97
  }
59
- const side = agent.facing === "left" || agent.facing === "right";
60
- const head = side ? HEAD_SIDE : agent.facing === "up" ? HEAD_BACK : HEAD_FRONT;
61
- const body = side ? BODY_SIDE : BODY;
62
- const legs = !agent.walking
63
- ? side ? LEGS_SIDE_STAND : LEGS_STAND
64
- : side ? (stride ? LEGS_SIDE_A : LEGS_SIDE_B) : stride ? LEGS_A : LEGS_B;
65
- const flip = agent.facing === "left";
66
- drawMap(c, head, px, py - height + breathe * 0, colors, flip);
67
- drawMap(c, body, px, py - height + head.length, colors, flip);
68
- if (!seated)
69
- drawMap(c, legs, px, py - height + head.length + body.length, colors, flip);
70
- else if (agent.state === "working") {
71
- // Hands on the keys.
72
- const typing = Math.floor(time * 9 + agent.seed) % 2;
73
- rect(c, agent.skin, px + 1 + typing, py - height + head.length + 3, 1, 1);
74
- rect(c, agent.skin, px + 10 - (1 - typing), py - height + head.length + 3, 1, 1);
98
+ // Body and arms
99
+ const swingArm = walking ? (step === 1 ? 1 : step === 3 ? -1 : 0) : 0;
100
+ if (side) {
101
+ parts.push([shirt, left + 3, bodyTop, 6, 7], [dark, left + 3, bodyTop + 6, 6, 1]);
102
+ parts.push([dark, left + 5 + swingArm, bodyTop + 1, 2, 5], [skin, left + 5 + swingArm, bodyTop + 6, 2, 1]);
103
+ }
104
+ else {
105
+ parts.push([shirt, left + 1, bodyTop, 10, 7], [dark, left + 1, bodyTop + 6, 10, 1]);
106
+ const typing = seated && agent.state === "working";
107
+ const type = Math.floor(time * 10 + agent.seed) % 2;
108
+ parts.push([dark, left - 1, bodyTop + 1 + swingArm + (typing ? -type : 0), 2, 5]);
109
+ parts.push([dark, left + 11, bodyTop + 1 - swingArm + (typing ? type - 1 : 0), 2, 5]);
110
+ parts.push([skin, left - 1, bodyTop + 6 + swingArm + (typing ? -type : 0), 2, 1]);
111
+ parts.push([skin, left + 11, bodyTop + 6 - swingArm + (typing ? type - 1 : 0), 2, 1]);
112
+ if (!back && outfit === 2)
113
+ parts.push(["#f3f4f6", left + 4, bodyTop, 4, 3], [agent.seed % 2 ? "#ef4444" : "#1d4ed8", left + 5, bodyTop, 2, 5]);
114
+ if (!back && outfit === 1)
115
+ parts.push([light, left + 3, bodyTop, 6, 2], [dark, left + 5, bodyTop + 2, 2, 4]);
116
+ if (back && outfit === 1)
117
+ parts.push([light, left + 2, bodyTop, 8, 3]);
118
+ }
119
+ // Head
120
+ parts.push([skin, left + 1, headTop, 10, 8]);
121
+ if (back) {
122
+ parts.push([hair, left + 1, headTop, 10, 7]);
123
+ }
124
+ else if (side) {
125
+ const backX = flip ? left + 7 : left + 1;
126
+ parts.push([hair, left + 1, headTop, 10, 3], [hair, backX, headTop, 4, 6]);
127
+ const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
128
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", flip ? left + 3 : left + 8, headTop + 4, 1, blink ? 1 : 2]);
129
+ }
130
+ else {
131
+ parts.push([hair, left + 1, headTop, 10, 3], [hair, left + 1, headTop + 3, 1, 2], [hair, left + 10, headTop + 3, 1, 2]);
132
+ const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
133
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 3, headTop + 4, 2, blink ? 1 : 2]);
134
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 7, headTop + 4, 2, blink ? 1 : 2]);
135
+ parts.push([shade(skin, -28), left + 5, headTop + 6, 2, 1]);
136
+ }
137
+ if (style === 1)
138
+ parts.push([hair, left, headTop + 2, 2, 8], [hair, left + 10, headTop + 2, 2, 8]); // long
139
+ if (style === 2)
140
+ parts.push([hair, left + 4, headTop - 3, 4, 3]); // bun
141
+ if (style === 3)
142
+ parts.push([hair, left + 2, headTop - 2, 2, 2], [hair, left + 5, headTop - 3, 2, 3], [hair, left + 8, headTop - 2, 2, 2]); // spiky
143
+ if (style === 4) { // cap
144
+ parts.push([light, left, headTop - 1, 12, 3]);
145
+ if (!back && !side)
146
+ parts.push([dark, left + 1, headTop + 2, 10, 1]);
147
+ if (side)
148
+ parts.push([dark, flip ? left - 2 : left + 9, headTop + 1, 5, 1]);
149
+ }
150
+ drawParts(c, parts);
151
+ if (agent.party > 0) {
152
+ const colors = ["#f59e0b", "#ec4899", "#22c55e", "#3b82f6", "#a855f7"];
153
+ for (let i = 0; i < 10; i++) {
154
+ const t = (time * 1.1 + i * 0.137) % 1;
155
+ const angle = i * 2.4 + agent.seed;
156
+ rect(c, colors[i % colors.length], left + 6 + Math.cos(angle) * (4 + t * 14), feet - 22 - t * 6 + t * t * 26, 2, 2);
157
+ }
75
158
  }
76
159
  }
77
160
  function bubble(c, x, y, draw) {
78
- rect(c, "#111827", x - 1, y - 1, 13, 11);
161
+ rect(c, OUTLINE, x - 1, y - 1, 13, 11);
79
162
  rect(c, "#ffffff", x, y, 11, 9);
80
163
  rect(c, "#ffffff", x + 4, y + 9, 3, 1);
81
- rect(c, "#111827", x + 4, y + 10, 3, 1);
164
+ rect(c, OUTLINE, x + 4, y + 10, 3, 1);
82
165
  draw(x, y);
83
166
  }
84
167
  function drawStatus(c, agent, time) {
85
168
  const x = Math.round(agent.x * TILE + 2);
86
- const y = Math.round(agent.y * TILE - 26 + (isSeated(agent) ? 8 : 0));
87
- const pulse = Math.floor(time * 2 + agent.seed % 3) % 2 === 0;
169
+ const y = Math.round(agent.y * TILE - 22 + (isSeated(agent) ? 2 : -2));
170
+ const pulse = Math.floor(time * 2 + (agent.seed % 3)) % 2 === 0;
88
171
  if (agent.state === "blocked") {
89
172
  bubble(c, x, y, (bx, by) => {
90
173
  rect(c, pulse ? "#dc2626" : "#991b1b", bx + 5, by + 1, 2, 5);
@@ -115,116 +198,233 @@ function drawStatus(c, agent, time) {
115
198
  rect(c, color, bx + 4, by + 2, 3, 5);
116
199
  });
117
200
  }
201
+ else if (agent.state === "idle" && !agent.walking && agent.seed % 3 === 0) {
202
+ bubble(c, x, y, (bx, by) => {
203
+ rect(c, "#92400e", bx + 3, by + 3, 4, 4);
204
+ rect(c, "#d97706", bx + 3, by + 3, 4, 1);
205
+ rect(c, "#92400e", bx + 7, by + 4, 1, 2);
206
+ if (pulse)
207
+ rect(c, "#9ca3af", bx + 4, by + 1, 1, 1);
208
+ });
209
+ }
118
210
  }
119
- // --- The room -------------------------------------------------------------------------------------------
120
- function drawFloor(c, room, palette) {
211
+ // --- The building ----------------------------------------------------------------------------------------
212
+ function drawFloor(c, room) {
121
213
  for (let ty = room.y; ty < room.y + room.h; ty++) {
122
214
  for (let tx = room.x; tx < room.x + room.w; tx++) {
123
215
  const x = tx * TILE;
124
216
  const y = ty * TILE;
125
- if (room.floor === "carpet") {
126
- rect(c, "#4c4f7a", x, y, TILE, TILE);
127
- for (let i = 0; i < 6; i++)
128
- rect(c, "#55588a", x + ((tx * 7 + i * 5) % 15), y + ((ty * 3 + i * 7) % 15), 1, 1);
129
- }
130
- else if (room.floor === "tile") {
131
- rect(c, (tx + ty) % 2 === 0 ? "#cfd8e3" : "#bcc7d4", x, y, TILE, TILE);
132
- rect(c, "#aab5c2", x, y + 15, TILE, 1);
133
- rect(c, "#aab5c2", x + 15, y, 1, TILE);
134
- }
135
- else {
136
- rect(c, "#a06f45", x, y, TILE, TILE);
137
- const offset = ty % 2 === 0 ? 0 : 8;
138
- rect(c, "#8f6039", x, y + 7, TILE, 1);
139
- rect(c, "#8f6039", x, y + 15, TILE, 1);
140
- rect(c, "#8f6039", x + ((offset + tx * 8) % 16), y, 1, 7);
141
- rect(c, "#ad7c52", x + 3, y + 3, 4, 1);
217
+ const noise = (tx * 73 + ty * 151) % 17;
218
+ switch (room.floor) {
219
+ case "carpet":
220
+ rect(c, "#4c4f7a", x, y, TILE, TILE);
221
+ for (let i = 0; i < 8; i++)
222
+ rect(c, i % 2 ? "#55588a" : "#454874", x + ((tx * 7 + i * 5) % 15), y + ((ty * 3 + i * 7) % 15), 1, 1);
223
+ break;
224
+ case "tile":
225
+ rect(c, (tx + ty) % 2 === 0 ? "#d6dee8" : "#c2ccd8", x, y, TILE, TILE);
226
+ rect(c, "#aab5c2", x, y + 15, TILE, 1);
227
+ rect(c, "#aab5c2", x + 15, y, 1, TILE);
228
+ rect(c, "#e8eef5", x + 1, y + 1, 5, 1);
229
+ break;
230
+ case "stone":
231
+ rect(c, noise % 3 === 0 ? "#8d93a3" : "#9aa0b0", x, y, TILE, TILE);
232
+ rect(c, "#7a8090", x, y + 15, TILE, 1);
233
+ rect(c, "#7a8090", x + (ty % 2 ? 7 : 15), y, 1, 15);
234
+ rect(c, "#adb3c2", x + 2, y + 2, 3, 1);
235
+ break;
236
+ case "concrete":
237
+ rect(c, "#6b7280", x, y, TILE, TILE);
238
+ for (let i = 0; i < 5; i++)
239
+ rect(c, i % 2 ? "#747b89" : "#636a78", x + ((tx * 11 + i * 3) % 15), y + ((ty * 5 + i * 5) % 15), 1, 1);
240
+ if (noise === 3)
241
+ rect(c, "#596070", x + 3, y + 8, 8, 1);
242
+ break;
243
+ case "grass":
244
+ rect(c, "#3f8f4f", x, y, TILE, TILE);
245
+ for (let i = 0; i < 7; i++)
246
+ rect(c, i % 3 ? "#4aa35c" : "#347a43", x + ((tx * 5 + i * 9) % 15), y + ((ty * 7 + i * 3) % 14), 1, 2);
247
+ break;
248
+ default: {
249
+ const plank = ty % 2 === 0 ? "#a57448" : "#9c6c42";
250
+ rect(c, plank, x, y, TILE, TILE);
251
+ rect(c, "#8a5d36", x, y + 7, TILE, 1);
252
+ rect(c, "#8a5d36", x, y + 15, TILE, 1);
253
+ rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 8 : 0)) % 16), y, 1, 7);
254
+ rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 0 : 8)) % 16), y + 8, 1, 7);
255
+ rect(c, "#b4835a", x + (noise % 9) + 2, y + 3, 4, 1);
256
+ rect(c, "#b4835a", x + (noise % 7) + 4, y + 11, 3, 1);
257
+ }
142
258
  }
143
259
  }
144
260
  }
145
- void palette;
146
261
  }
147
- const WALL_TOP = "#2f3350";
148
- const WALL_FACE = "#565c86";
149
- const WALL_LINE = "#454a70";
150
- function drawWalls(c, world, palette) {
262
+ const WALL_TOP = "#2a2d48";
263
+ const WALL_FACE = "#5c628f";
264
+ const WALL_DARK = "#4a4f78";
265
+ const WALL_LIGHT = "#6d74a4";
266
+ function drawWalls(c, world, palette, daylight) {
151
267
  const open = new Set();
152
268
  for (const room of world.rooms) {
153
269
  for (let y = room.y; y < room.y + room.h; y++)
154
270
  for (let x = room.x; x < room.x + room.w; x++)
155
271
  open.add(`${x},${y}`);
156
272
  }
157
- for (const door of world.doors)
158
- open.add(`${door.x},${door.y}`);
273
+ const doors = new Set(world.doors.map((door) => `${door.x},${door.y}`));
159
274
  for (let ty = 0; ty < world.rows; ty++) {
160
275
  for (let tx = 0; tx < world.cols; tx++) {
161
- if (open.has(`${tx},${ty}`))
276
+ const key = `${tx},${ty}`;
277
+ if (open.has(key) || doors.has(key))
162
278
  continue;
163
279
  const x = tx * TILE;
164
280
  const y = ty * TILE;
165
- const faceBelow = open.has(`${tx},${ty + 1}`);
166
- if (faceBelow) {
167
- // The wall's front, with a band in the Kit's colour and a window every few tiles.
281
+ if (open.has(`${tx},${ty + 1}`)) {
168
282
  rect(c, WALL_FACE, x, y, TILE, TILE);
169
- rect(c, WALL_LINE, x, y + 13, TILE, 3);
170
- rect(c, palette.primary, x, y + 12, TILE, 1);
171
- if (tx % 4 === 3 && ty <= 3) {
283
+ rect(c, WALL_LIGHT, x, y, TILE, 1);
284
+ rect(c, WALL_DARK, x, y + 12, TILE, 4);
285
+ rect(c, palette.primary, x, y + 11, TILE, 1);
286
+ if (tx % 4 === 3 && !world.furniture.some((item) => FURNITURE[item.type].wall && item.x === tx && item.y === ty)) {
287
+ // A window, in the colour of the sky right now.
172
288
  rect(c, "#1f2937", x + 1, y + 1, 14, 9);
173
- rect(c, "#8ed0ff", x + 2, y + 2, 12, 7);
174
- rect(c, "#bfe6ff", x + 3, y + 3, 4, 2);
289
+ rect(c, daylight.sky, x + 2, y + 2, 12, 7);
290
+ rect(c, shade(daylight.sky, 40), x + 3, y + 3, 4, 2);
175
291
  rect(c, "#1f2937", x + 8, y + 2, 1, 7);
292
+ if (daylight.night > 0.3) {
293
+ rect(c, "#fef3c7", x + 4, y + 3, 1, 1);
294
+ rect(c, "#fef3c7", x + 11, y + 5, 1, 1);
295
+ }
176
296
  }
177
297
  }
178
298
  else {
179
299
  rect(c, WALL_TOP, x, y, TILE, TILE);
300
+ if (open.has(`${tx},${ty - 1}`))
301
+ rect(c, "#20233a", x, y, TILE, 3);
302
+ if (open.has(`${tx - 1},${ty}`))
303
+ rect(c, "#20233a", x, y, 2, TILE);
304
+ if (open.has(`${tx + 1},${ty}`))
305
+ rect(c, "#20233a", x + 14, y, 2, TILE);
180
306
  }
181
307
  }
182
308
  }
183
309
  for (const door of world.doors) {
184
- rect(c, "#6b4a2f", door.x * TILE, door.y * TILE, TILE, TILE);
185
- rect(c, "#7c5637", door.x * TILE + 2, door.y * TILE, 12, TILE);
310
+ const x = door.x * TILE;
311
+ const y = door.y * TILE;
312
+ const horizontal = open.has(`${door.x},${door.y - 1}`) || open.has(`${door.x},${door.y + 1}`);
313
+ rect(c, "#7c5637", x, y, TILE, TILE);
314
+ for (let i = 2; i < 16; i += 4)
315
+ rect(c, "#6b4a2f", horizontal ? x : x + i, horizontal ? y + i : y, horizontal ? TILE : 1, horizontal ? 1 : TILE);
316
+ if (horizontal) {
317
+ rect(c, WALL_DARK, x, y, 2, TILE);
318
+ rect(c, WALL_DARK, x + 14, y, 2, TILE);
319
+ }
320
+ else {
321
+ rect(c, WALL_DARK, x, y, TILE, 2);
322
+ rect(c, WALL_DARK, x, y + 14, TILE, 2);
323
+ }
186
324
  }
187
325
  }
188
- function drawDesk(c, x, y, on, time, seed, palette) {
189
- rect(c, "#6b4427", x + 1, y + 13, 46, 17);
190
- rect(c, "#8a5a33", x + 1, y + 11, 46, 11);
191
- rect(c, "#a06c3f", x + 1, y + 11, 46, 2);
192
- rect(c, "#4a2e1a", x + 3, y + 22, 3, 9);
193
- rect(c, "#4a2e1a", x + 42, y + 22, 3, 9);
326
+ function drawLabels(c, world) {
327
+ c.font = LABEL_FONT;
328
+ c.textBaseline = "top";
329
+ for (const room of world.rooms) {
330
+ if (!room.name)
331
+ continue;
332
+ const label = room.name.toUpperCase();
333
+ const width = Math.ceil(c.measureText(label).width) + 6;
334
+ const x = room.x * TILE + 3;
335
+ const y = room.y * TILE - 13;
336
+ rect(c, "#1c1f33", x, y, width, 10);
337
+ rect(c, "#2f3350", x + 1, y + 1, width - 2, 8);
338
+ c.fillStyle = "#f8fafc";
339
+ c.fillText(label, x + 3, y + 1.5);
340
+ }
341
+ }
342
+ /** Everything that doesn't move. Redrawn only when the building or the hour changes. */
343
+ export function drawBuilding(c, world, palette, daylight) {
344
+ rect(c, WALL_TOP, 0, 0, world.cols * TILE, world.rows * TILE);
345
+ for (const room of world.rooms)
346
+ drawFloor(c, room);
347
+ drawWalls(c, world, palette, daylight);
348
+ for (const item of world.furniture)
349
+ if (item.type === "rug")
350
+ drawFurniture(c, item, world, 0, palette);
351
+ for (const item of world.furniture)
352
+ if (FURNITURE[item.type].wall)
353
+ drawFurniture(c, item, world, 0, palette);
354
+ // Light from the windows falling across the floor.
355
+ if (daylight.sun !== "rgba(0,0,0,0)") {
356
+ c.fillStyle = daylight.sun;
357
+ for (const room of world.rooms) {
358
+ for (let tx = room.x; tx < room.x + room.w; tx++) {
359
+ if (tx % 4 !== 3)
360
+ continue;
361
+ c.fillRect(tx * TILE + 2, room.y * TILE, 12, Math.min(room.h, 3) * TILE);
362
+ }
363
+ }
364
+ }
365
+ }
366
+ function drawDesk(c, x, y, on, time, seed, palette, blocked) {
367
+ rect(c, "#55351d", x + 1, y + 22, 46, 8);
368
+ rect(c, "#6b4427", x + 1, y + 13, 46, 10);
369
+ rect(c, "#8a5a33", x + 1, y + 10, 46, 12);
370
+ rect(c, "#a87545", x + 1, y + 10, 46, 2);
371
+ rect(c, "#3b2413", x + 3, y + 22, 3, 9);
372
+ rect(c, "#3b2413", x + 42, y + 22, 3, 9);
373
+ rect(c, "#55351d", x + 30, y + 23, 12, 7);
374
+ rect(c, "#7c5637", x + 31, y + 24, 10, 2);
375
+ rect(c, "#d4a24e", x + 35, y + 24, 2, 1);
376
+ rect(c, "#7c5637", x + 31, y + 27, 10, 2);
377
+ rect(c, "#d4a24e", x + 35, y + 27, 2, 1);
194
378
  // Monitor
195
- rect(c, "#111827", x + 15, y - 1, 18, 13);
379
+ rect(c, OUTLINE, x + 14, y - 3, 20, 15);
196
380
  rect(c, "#374151", x + 22, y + 12, 4, 2);
197
- if (on) {
198
- rect(c, "#0f2a2e", x + 16, y, 16, 11);
199
- const scroll = Math.floor(time * 3 + seed % 13);
200
- for (let line = 0; line < 4; line++) {
201
- const width = 3 + ((seed >> line) + (scroll + line) * 7) % 11;
202
- rect(c, line % 2 === 0 ? palette.primary : "#7dd3fc", x + 17, y + 1 + line * 2.5, width, 1);
381
+ rect(c, "#4b5563", x + 19, y + 14, 10, 1);
382
+ if (blocked) {
383
+ rect(c, "#3f0f14", x + 15, y - 2, 18, 13);
384
+ if (Math.floor(time * 2) % 2 === 0) {
385
+ rect(c, "#ef4444", x + 23, y, 2, 6);
386
+ rect(c, "#ef4444", x + 23, y + 7, 2, 2);
387
+ }
388
+ }
389
+ else if (on) {
390
+ rect(c, "#0f2a2e", x + 15, y - 2, 18, 13);
391
+ const scroll = Math.floor(time * 3 + (seed % 13));
392
+ for (let line = 0; line < 5; line++) {
393
+ const width = 3 + (((seed >> line) & 7) + (scroll + line) * 7) % 12;
394
+ rect(c, line % 3 === 0 ? palette.primary : line % 3 === 1 ? "#7dd3fc" : "#fcd34d", x + 16 + (line % 2) * 2, y - 1 + line * 2.2, width, 1);
203
395
  }
204
- c.fillStyle = "rgba(125,211,252,0.10)";
205
- c.fillRect(x + 11, y + 11, 26, 8);
396
+ c.fillStyle = "rgba(125,211,252,0.12)";
397
+ c.fillRect(x + 10, y + 11, 28, 8);
206
398
  }
207
399
  else {
208
- rect(c, "#1f2937", x + 16, y, 16, 11);
209
- rect(c, "#374151", x + 18, y + 2, 3, 1);
400
+ rect(c, "#1f2937", x + 15, y - 2, 18, 13);
401
+ rect(c, "#374151", x + 17, y, 3, 1);
210
402
  }
211
- // Keyboard and mug
212
- rect(c, "#d1d5db", x + 17, y + 15, 14, 4);
213
- rect(c, "#9ca3af", x + 18, y + 16, 12, 1);
214
- rect(c, "#f3f4f6", x + 38, y + 14, 4, 4);
215
- rect(c, palette.primary, x + 38, y + 16, 4, 1);
403
+ rect(c, "#d1d5db", x + 16, y + 15, 16, 5);
404
+ for (let row = 0; row < 2; row++)
405
+ for (let key = 0; key < 7; key++)
406
+ rect(c, "#9ca3af", x + 17 + key * 2, y + 16 + row * 2, 1, 1);
407
+ rect(c, "#e5e7eb", x + 34, y + 16, 3, 4);
408
+ rect(c, "#f3f4f6", x + 40, y + 13, 5, 5);
409
+ rect(c, palette.primary, x + 40, y + 15, 5, 1);
410
+ rect(c, "#fde68a", x + 4, y + 13, 7, 6);
411
+ rect(c, "#fbbf24", x + 5, y + 15, 5, 1);
216
412
  }
217
413
  function drawBoard(c, x, y, world) {
218
- rect(c, "#2b2118", x, y - 11, 64, 26);
219
- rect(c, "#d9c29c", x + 2, y - 9, 60, 22);
414
+ rect(c, "#3a2c1e", x, y - 12, 64, 27);
415
+ rect(c, "#d9c29c", x + 2, y - 10, 60, 23);
416
+ for (let i = 0; i < 40; i++)
417
+ rect(c, "#c9b08a", x + 3 + (i * 37) % 58, y - 9 + (i * 13) % 21, 1, 1);
220
418
  const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
221
419
  const per = 8;
222
420
  world.tasks.slice(0, per * 2).forEach((task, index) => {
223
421
  const col = index % per;
224
422
  const row = Math.floor(index / per);
225
- rect(c, "#00000033", x + 5 + col * 7, y - 6 + row * 9, 6, 7);
226
- rect(c, colors[task.state], x + 4 + col * 7, y - 7 + row * 9, 6, 7);
227
- rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 9, 4, 1);
423
+ rect(c, "#00000033", x + 5 + col * 7, y - 7 + row * 10, 6, 8);
424
+ rect(c, colors[task.state], x + 4 + col * 7, y - 8 + row * 10, 6, 8);
425
+ rect(c, "#ef4444", x + 6 + col * 7, y - 8 + row * 10, 2, 1);
426
+ rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 10, 4, 1);
427
+ rect(c, "#00000040", x + 5 + col * 7, y - 3 + row * 10, 3, 1);
228
428
  });
229
429
  }
230
430
  function drawFurniture(c, item, world, time, palette) {
@@ -234,112 +434,218 @@ function drawFurniture(c, item, world, time, palette) {
234
434
  case "desk": {
235
435
  const seatX = item.x + 1;
236
436
  const seatY = item.y + 2;
237
- const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a) && a.state !== "blocked");
238
- drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette);
437
+ const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a));
438
+ drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette, user?.state === "blocked");
239
439
  break;
240
440
  }
241
441
  case "coffee":
242
442
  rect(c, "#374151", x, y + 2, 32, 14);
243
443
  rect(c, "#6b7280", x, y + 2, 32, 2);
244
- rect(c, "#111827", x + 4, y - 8, 14, 14);
245
- rect(c, "#ef4444", x + 15, y - 5, 2, 2);
444
+ rect(c, OUTLINE, x + 4, y - 9, 15, 15);
445
+ rect(c, "#27272a", x + 5, y - 8, 13, 13);
446
+ rect(c, Math.floor(time * 2) % 2 ? "#ef4444" : "#7f1d1d", x + 15, y - 6, 2, 2);
447
+ rect(c, "#52525b", x + 7, y - 6, 6, 4);
246
448
  rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
247
- rect(c, "#f3f4f6", x + 22, y - 1, 5, 5);
449
+ rect(c, "#f3f4f6", x + 23, y - 1, 5, 5);
450
+ rect(c, "#92400e", x + 24, y, 3, 1);
248
451
  for (let i = 0; i < 3; i++) {
249
- c.fillStyle = "rgba(255,255,255,0.45)";
452
+ c.fillStyle = "rgba(255,255,255,0.5)";
250
453
  c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
251
454
  }
252
455
  break;
253
456
  case "water":
254
- rect(c, "#e5e7eb", x + 3, y + 3, 10, 13);
255
- rect(c, "#7dd3fc", x + 4, y - 7, 8, 10);
256
- rect(c, "#bae6fd", x + 5, y - 6, 2, 8);
457
+ rect(c, "#d1d5db", x + 3, y + 3, 10, 13);
458
+ rect(c, "#9ca3af", x + 3, y + 14, 10, 2);
459
+ rect(c, "#60a5fa", x + 4, y - 8, 8, 11);
460
+ rect(c, "#bae6fd", x + 5, y - 7, 2, 9);
257
461
  rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
462
+ rect(c, "#ef4444", x + 9, y + 6, 2, 2);
463
+ if (Math.floor(time * 0.4 + item.x) % 5 === 0)
464
+ rect(c, "#e0f2fe", x + 8, y - 2 - ((time * 6) % 7), 1, 1);
258
465
  break;
259
466
  case "sofa":
260
- rect(c, palette.primary, x + 1, y - 6, 46, 9);
261
- rect(c, "#00000030", x + 1, y + 1, 46, 2);
467
+ rect(c, shade(palette.primary, -60), x + 1, y - 7, 46, 11);
468
+ rect(c, shade(palette.primary, -30), x + 2, y - 6, 44, 3);
262
469
  rect(c, "#4b5563", x, y + 3, 48, 11);
263
470
  rect(c, "#6b7280", x + 3, y + 3, 20, 7);
264
471
  rect(c, "#6b7280", x + 25, y + 3, 20, 7);
265
472
  rect(c, "#374151", x, y + 12, 48, 3);
473
+ rect(c, "#374151", x - 1, y - 2, 4, 16);
474
+ rect(c, "#374151", x + 45, y - 2, 4, 16);
266
475
  break;
267
476
  case "plant":
268
477
  rect(c, "#92400e", x + 4, y + 9, 8, 7);
269
478
  rect(c, "#b45309", x + 3, y + 8, 10, 2);
270
- 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]]) {
479
+ 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]])
271
480
  rect(c, "#15803d", x + lx, y + ly, lw, lh);
272
- }
273
481
  rect(c, "#22c55e", x + 7, y - 3, 2, 8);
274
482
  rect(c, "#22c55e", x + 3, y + 1, 2, 3);
483
+ rect(c, "#4ade80", x + 10, y - 4, 1, 3);
484
+ break;
485
+ case "fern":
486
+ rect(c, "#57534e", x + 3, y + 10, 10, 6);
487
+ rect(c, "#78716c", x + 2, y + 9, 12, 2);
488
+ for (let i = 0; i < 7; i++) {
489
+ const lean = i - 3;
490
+ for (let j = 0; j < 5; j++)
491
+ rect(c, j % 2 ? "#16a34a" : "#15803d", x + 7 + lean * (j / 2 + 1), y + 8 - j * 2 + Math.abs(lean), 2, 2);
492
+ }
275
493
  break;
276
494
  case "board":
277
495
  drawBoard(c, x, y, world);
278
496
  break;
497
+ case "whiteboard":
498
+ rect(c, "#9ca3af", x, y - 10, 48, 22);
499
+ rect(c, "#f9fafb", x + 2, y - 8, 44, 18);
500
+ rect(c, "#3b82f6", x + 5, y - 5, 14, 1);
501
+ rect(c, "#3b82f6", x + 5, y - 2, 9, 1);
502
+ rect(c, "#ef4444", x + 24, y - 6, 8, 6);
503
+ rect(c, "#f9fafb", x + 25, y - 5, 6, 4);
504
+ rect(c, "#10b981", x + 8, y + 3, 20, 1);
505
+ rect(c, "#10b981", x + 27, y + 2, 1, 3);
506
+ rect(c, "#6b7280", x + 2, y + 10, 44, 2);
507
+ break;
279
508
  case "shelf":
280
509
  rect(c, "#5b3a21", x, y - 12, 32, 28);
281
510
  for (let shelf = 0; shelf < 2; shelf++) {
282
511
  rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
283
512
  for (let book = 0; book < 8; book++) {
284
- const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf) % 5];
513
+ const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf + item.x) % 5];
285
514
  rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
515
+ rect(c, shade(color, 40), x + 3 + book * 3, y - 6 + shelf * 13, 2, 1);
286
516
  }
287
517
  }
288
518
  break;
289
519
  case "server":
290
- rect(c, "#111827", x + 1, y - 12, 14, 28);
520
+ rect(c, OUTLINE, x, y - 13, 16, 29);
521
+ rect(c, "#1f2937", x + 1, y - 12, 14, 27);
291
522
  for (let i = 0; i < 5; i++) {
292
- rect(c, "#1f2937", x + 2, y - 11 + i * 5, 12, 4);
523
+ rect(c, "#111827", x + 2, y - 11 + i * 5, 12, 4);
293
524
  const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
294
525
  rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
295
526
  rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
527
+ rect(c, "#374151", x + 8, y - 10 + i * 5, 5, 1);
296
528
  }
297
529
  break;
298
530
  case "table":
299
- rect(c, "#57534e", x + 2, y + 14, 44, 14);
531
+ rect(c, "#44403c", x + 2, y + 16, 44, 12);
300
532
  rect(c, "#78716c", x + 2, y + 3, 44, 14);
301
533
  rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
302
534
  rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
303
535
  rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
536
+ rect(c, "#1f2937", x + 12, y + 8, 4, 1);
537
+ rect(c, "#3b82f6", x + 30, y + 10, 4, 1);
304
538
  break;
305
539
  case "rug":
306
540
  rect(c, palette.primary, x, y, 64, 48);
307
- c.fillStyle = "rgba(0,0,0,0.35)";
541
+ c.fillStyle = "rgba(0,0,0,0.42)";
308
542
  c.fillRect(x + 3, y + 3, 58, 42);
309
543
  rect(c, palette.primary, x + 6, y + 6, 52, 1);
310
544
  rect(c, palette.primary, x + 6, y + 41, 52, 1);
545
+ rect(c, palette.primary, x + 6, y + 6, 1, 36);
546
+ rect(c, palette.primary, x + 57, y + 6, 1, 36);
547
+ for (let i = 0; i < 6; i++)
548
+ rect(c, palette.primary, x + 12 + i * 8, y + 22, 4, 4);
311
549
  break;
312
- }
313
- void FURNITURE;
314
- }
315
- function drawLabels(c, world) {
316
- c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
317
- c.textBaseline = "top";
318
- for (const room of world.rooms) {
319
- if (!room.name)
320
- continue;
321
- const label = room.name.toUpperCase();
322
- // On the wall above the room, clear of the furniture.
323
- const x = room.x * TILE + 3;
324
- const y = room.y * TILE - 12;
325
- c.fillStyle = "rgba(0,0,0,0.45)";
326
- c.fillText(label, x + 1, y + 1);
327
- c.fillStyle = "rgba(255,255,255,0.85)";
328
- c.fillText(label, x, y);
550
+ case "arcade":
551
+ rect(c, OUTLINE, x, y - 14, 16, 30);
552
+ rect(c, "#4c1d95", x + 1, y - 13, 14, 28);
553
+ rect(c, "#111827", x + 3, y - 10, 10, 9);
554
+ rect(c, ["#22c55e", "#ec4899", "#38bdf8"][Math.floor(time * 2) % 3], x + 4 + (Math.floor(time * 4) % 7), y - 8 + (Math.floor(time * 3) % 5), 2, 2);
555
+ rect(c, "#f59e0b", x + 3, y - 13, 10, 2);
556
+ rect(c, "#27272a", x + 2, y + 1, 12, 4);
557
+ rect(c, "#ef4444", x + 4, y + 2, 2, 2);
558
+ rect(c, "#3b82f6", x + 9, y + 2, 2, 2);
559
+ break;
560
+ case "fridge":
561
+ rect(c, OUTLINE, x + 1, y - 12, 14, 28);
562
+ rect(c, "#e5e7eb", x + 2, y - 11, 12, 26);
563
+ rect(c, "#9ca3af", x + 2, y - 3, 12, 1);
564
+ rect(c, "#6b7280", x + 11, y - 8, 1, 3);
565
+ rect(c, "#6b7280", x + 11, y, 1, 5);
566
+ rect(c, "#fde68a", x + 4, y - 9, 3, 3);
567
+ rect(c, "#fca5a5", x + 5, y + 3, 3, 3);
568
+ break;
569
+ case "printer":
570
+ rect(c, "#4b5563", x + 2, y + 8, 12, 8);
571
+ rect(c, OUTLINE, x + 1, y - 2, 14, 11);
572
+ rect(c, "#d1d5db", x + 2, y - 1, 12, 9);
573
+ rect(c, "#f9fafb", x + 4, y - 5 - (Math.floor(time) % 4 === 0 ? 1 : 0), 8, 5);
574
+ rect(c, "#22c55e", x + 11, y + 1, 2, 1);
575
+ rect(c, "#9ca3af", x + 3, y + 5, 10, 1);
576
+ break;
577
+ case "lamp":
578
+ rect(c, "#374151", x + 5, y + 13, 6, 3);
579
+ rect(c, "#4b5563", x + 7, y - 4, 2, 17);
580
+ rect(c, OUTLINE, x + 2, y - 10, 12, 8);
581
+ rect(c, "#fde68a", x + 3, y - 9, 10, 6);
582
+ rect(c, "#fef3c7", x + 4, y - 8, 4, 2);
583
+ break;
584
+ case "beanbag":
585
+ rect(c, OUTLINE, x, y + 3, 16, 13);
586
+ rect(c, shade(palette.primary, -50), x + 1, y + 4, 14, 11);
587
+ rect(c, shade(palette.primary, -20), x + 3, y + 5, 8, 4);
588
+ rect(c, "#00000040", x + 1, y + 13, 14, 2);
589
+ break;
590
+ case "tv":
591
+ rect(c, "#3b2413", x + 2, y + 8, 44, 8);
592
+ rect(c, "#5b3a21", x + 2, y + 8, 44, 2);
593
+ rect(c, OUTLINE, x + 4, y - 14, 40, 22);
594
+ rect(c, "#0b1120", x + 5, y - 13, 38, 20);
595
+ for (let i = 0; i < 9; i++) {
596
+ const h = 2 + ((i * 5 + Math.floor(time * 3)) % 12);
597
+ rect(c, i % 3 === 0 ? palette.primary : i % 3 === 1 ? "#38bdf8" : "#f472b6", x + 8 + i * 4, y + 5 - h, 3, h);
598
+ }
599
+ break;
600
+ case "vending":
601
+ rect(c, OUTLINE, x, y - 14, 16, 30);
602
+ rect(c, "#b91c1c", x + 1, y - 13, 14, 28);
603
+ rect(c, "#0f172a", x + 3, y - 11, 8, 16);
604
+ for (let row = 0; row < 3; row++)
605
+ for (let col = 0; col < 2; col++)
606
+ rect(c, ["#fbbf24", "#34d399", "#60a5fa", "#f472b6"][(row + col * 2) % 4], x + 4 + col * 3, y - 10 + row * 5, 2, 4);
607
+ rect(c, "#d1d5db", x + 12, y - 9, 2, 6);
608
+ rect(c, "#111827", x + 3, y + 8, 10, 4);
609
+ break;
610
+ case "trophy":
611
+ rect(c, "#6b4427", x + 2, y + 8, 12, 8);
612
+ rect(c, "#8a5a33", x + 2, y + 8, 12, 2);
613
+ rect(c, "#78350f", x + 5, y + 5, 6, 3);
614
+ rect(c, "#f59e0b", x + 7, y + 1, 2, 4);
615
+ rect(c, OUTLINE, x + 3, y - 8, 10, 10);
616
+ rect(c, "#fbbf24", x + 4, y - 7, 8, 8);
617
+ rect(c, "#fde68a", x + 5, y - 6, 2, 5);
618
+ rect(c, "#fbbf24", x + 1, y - 6, 3, 1);
619
+ rect(c, "#fbbf24", x + 12, y - 6, 3, 1);
620
+ break;
621
+ case "clock": {
622
+ rect(c, OUTLINE, x + 3, y + 1, 10, 10);
623
+ rect(c, "#f9fafb", x + 4, y + 2, 8, 8);
624
+ rect(c, "#1f2937", x + 8, y + 3, 1, 3);
625
+ rect(c, "#1f2937", x + 8, y + 6, 3, 1);
626
+ break;
627
+ }
628
+ case "poster": {
629
+ const colors = ["#7c3aed", "#0ea5e9", "#db2777", "#059669"];
630
+ const color = colors[(item.x + item.y) % colors.length];
631
+ rect(c, OUTLINE, x + 2, y, 12, 11);
632
+ rect(c, color, x + 3, y + 1, 10, 9);
633
+ rect(c, shade(color, 60), x + 5, y + 3, 6, 3);
634
+ rect(c, "#f9fafb", x + 5, y + 7, 6, 1);
635
+ break;
636
+ }
329
637
  }
330
638
  }
331
639
  function nameTag(c, agent, selected, width) {
332
- const name = agent.name;
333
- c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
640
+ c.font = LABEL_FONT;
334
641
  c.textBaseline = "top";
335
- const w = Math.ceil(c.measureText(name).width) + 5;
336
- const cx = agent.x * TILE + 8;
337
- const x = Math.round(Math.max(1, Math.min(width - w - 1, cx - w / 2)));
338
- const y = Math.round(agent.y * TILE + 15);
642
+ const w = Math.ceil(c.measureText(agent.name).width) + 5;
643
+ const x = Math.round(Math.max(1, Math.min(width - w - 1, agent.x * TILE + 8 - w / 2)));
644
+ const y = Math.round(agent.y * TILE + 16);
339
645
  c.fillStyle = selected ? "#ffffff" : "rgba(17,24,39,0.82)";
340
646
  c.fillRect(x, y, w, 10);
341
647
  c.fillStyle = selected ? "#111827" : "#ffffff";
342
- c.fillText(name, x + 2.5, y + 1.5);
648
+ c.fillText(agent.name, x + 2.5, y + 1.5);
343
649
  }
344
650
  function wrap(c, text, max) {
345
651
  const lines = [];
@@ -359,60 +665,87 @@ function wrap(c, text, max) {
359
665
  return lines.slice(0, 3);
360
666
  }
361
667
  const STATE_LABEL = { working: "Working", thinking: "Thinking", blocked: "Blocked", reviewing: "Reviewing", idle: "On a break", done: "Finished", away: "Away" };
362
- function drawCard(c, agent, width) {
363
- c.font = "7px ui-monospace, Menlo, Consolas, monospace";
668
+ function drawCard(c, agent, world) {
669
+ c.font = BODY_FONT;
364
670
  c.textBaseline = "top";
365
- const body = wrap(c, agent.task || STATE_LABEL[agent.state] || "", 104);
366
- const w = 114;
367
- const h = 18 + body.length * 9 + 3;
368
- const x = Math.round(Math.max(2, Math.min(width - w - 2, agent.x * TILE + 8 - w / 2)));
369
- const y = Math.round(Math.max(2, agent.y * TILE - h - 22));
370
- rect(c, "#111827", x - 1, y - 1, w + 2, h + 2);
671
+ const room = world.rooms.find((candidate) => candidate.id === agent.area);
672
+ const body = wrap(c, agent.task || STATE_LABEL[agent.state] || "", 110);
673
+ const w = 120;
674
+ const h = 18 + body.length * 9 + (room ? 9 : 0) + 3;
675
+ const x = Math.round(Math.max(2, Math.min(world.cols * TILE - w - 2, agent.x * TILE + 8 - w / 2)));
676
+ const y = Math.round(Math.max(2, agent.y * TILE - h - 24));
677
+ rect(c, OUTLINE, x - 1, y - 1, w + 2, h + 2);
371
678
  rect(c, "#ffffff", x, y, w, h);
372
679
  c.fillStyle = "#111827";
373
- c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
374
- c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0, 26), x + 4, y + 4);
680
+ c.font = LABEL_FONT;
681
+ c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0, 28), x + 4, y + 4);
375
682
  c.fillStyle = "#374151";
376
- c.font = "7px ui-monospace, Menlo, Consolas, monospace";
683
+ c.font = BODY_FONT;
377
684
  body.forEach((line, index) => c.fillText(line, x + 4, y + 15 + index * 9));
685
+ if (room) {
686
+ c.fillStyle = "#6b7280";
687
+ c.fillText(`In ${room.name}`.slice(0, 28), x + 4, y + 15 + body.length * 9);
688
+ }
378
689
  }
379
690
  function drawProgress(c, world, palette) {
380
691
  if (world.tasks.length === 0)
381
692
  return;
382
693
  const done = world.tasks.filter((task) => task.state === "done").length;
383
- const label = `QUESTS ${done}/${world.tasks.length}`;
384
- c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
694
+ c.font = LABEL_FONT;
385
695
  c.textBaseline = "top";
386
696
  const w = 96;
387
697
  rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
388
698
  c.fillStyle = "#ffffff";
389
- c.fillText(label, 8, 6.5);
699
+ c.fillText(`QUESTS ${done}/${world.tasks.length}`, 8, 6.5);
390
700
  rect(c, "#374151", 8, 16, w - 8, 4);
391
701
  rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
392
702
  }
393
- export function drawWorld(c, world, time, palette, selected) {
703
+ /** Everything that moves. `building` is the layer from drawBuilding. */
704
+ export function drawWorld(c, world, time, palette, daylight, building, selected) {
394
705
  const width = world.cols * TILE;
395
- rect(c, WALL_TOP, 0, 0, width, world.rows * TILE);
396
- for (const room of world.rooms)
397
- drawFloor(c, room, palette);
398
- drawWalls(c, world, palette);
399
- // Flat things first, then everything else in depth order so people pass behind desks.
400
- for (const item of world.furniture)
401
- if (item.type === "rug")
402
- drawFurniture(c, item, world, time, palette);
706
+ const height = world.rows * TILE;
707
+ if (building)
708
+ c.drawImage(building, 0, 0, width, height);
709
+ else
710
+ drawBuilding(c, world, palette, daylight);
403
711
  const scene = [];
404
712
  for (const item of world.furniture) {
405
- if (item.type === "rug")
713
+ const spec = FURNITURE[item.type];
714
+ if (spec.solid === false)
406
715
  continue;
407
- scene.push({ depth: (item.y + FURNITURE[item.type].h) * TILE - 2, draw: () => drawFurniture(c, item, world, time, palette) });
716
+ scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => drawFurniture(c, item, world, time, palette) });
408
717
  }
409
718
  const shown = world.agents.filter((agent) => agent.state !== "away");
410
- for (const agent of shown) {
719
+ for (const agent of shown)
411
720
  scene.push({ depth: agent.y * TILE + 14, draw: () => drawAgent(c, agent, time, agent.id === selected) });
412
- }
413
721
  scene.sort((a, b) => a.depth - b.depth);
414
722
  for (const item of scene)
415
723
  item.draw();
724
+ // Night: the room dims, and screens and lamps light it.
725
+ if (daylight.night > 0) {
726
+ c.fillStyle = `rgba(12,14,48,${daylight.night})`;
727
+ c.fillRect(0, 0, width, height);
728
+ c.save();
729
+ c.globalCompositeOperation = "lighter";
730
+ const glow = (x, y, radius, color) => {
731
+ const gradient = c.createRadialGradient(x, y, 1, x, y, radius);
732
+ gradient.addColorStop(0, color);
733
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
734
+ c.fillStyle = gradient;
735
+ c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
736
+ };
737
+ for (const item of world.furniture) {
738
+ if (item.type === "lamp")
739
+ glow(item.x * TILE + 8, item.y * TILE, 36, "rgba(253,224,139,0.30)");
740
+ if (item.type === "arcade" || item.type === "vending" || item.type === "tv")
741
+ glow(item.x * TILE + 8, item.y * TILE, 26, "rgba(125,211,252,0.16)");
742
+ if (item.type === "desk" && world.agents.some((a) => a.seat?.x === item.x + 1 && a.seat?.y === item.y + 2 && isSeated(a))) {
743
+ glow(item.x * TILE + 24, item.y * TILE + 8, 30, "rgba(125,211,252,0.26)");
744
+ }
745
+ }
746
+ c.restore();
747
+ }
748
+ // On top, so tall furniture against a wall can't hide a room's name.
416
749
  drawLabels(c, world);
417
750
  for (const agent of shown)
418
751
  nameTag(c, agent, agent.id === selected, width);
@@ -422,6 +755,6 @@ export function drawWorld(c, world, time, palette, selected) {
422
755
  drawProgress(c, world, palette);
423
756
  const picked = shown.find((agent) => agent.id === selected);
424
757
  if (picked)
425
- drawCard(c, picked, width);
758
+ drawCard(c, picked, world);
426
759
  }
427
760
  //# sourceMappingURL=draw.js.map