@blkc-code/render-canvas 0.1.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 (53) hide show
  1. package/dist/draw.d.ts +22 -12
  2. package/dist/draw.d.ts.map +1 -1
  3. package/dist/draw.js +374 -273
  4. package/dist/draw.js.map +1 -1
  5. package/dist/index.d.ts +4 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -1
  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 +25 -2
  14. package/dist/office.d.ts.map +1 -1
  15. package/dist/office.js +134 -20
  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.js.map +1 -1
  27. package/dist/runtime.txt +1 -1
  28. package/dist/types.d.ts +20 -3
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/world.d.ts +29 -13
  31. package/dist/world.d.ts.map +1 -1
  32. package/dist/world.js +150 -72
  33. package/dist/world.js.map +1 -1
  34. package/dist/worlds/office-data.d.ts +14 -0
  35. package/dist/worlds/office-data.d.ts.map +1 -0
  36. package/dist/worlds/office-data.js +52 -0
  37. package/dist/worlds/office-data.js.map +1 -0
  38. package/dist/worlds/office-paint.d.ts +13 -0
  39. package/dist/worlds/office-paint.d.ts.map +1 -0
  40. package/dist/worlds/office-paint.js +292 -0
  41. package/dist/worlds/office-paint.js.map +1 -0
  42. package/package.json +7 -2
  43. package/src/draw.ts +352 -272
  44. package/src/index.ts +4 -1
  45. package/src/links.ts +76 -0
  46. package/src/office.ts +142 -21
  47. package/src/pack.ts +36 -0
  48. package/src/paint.ts +24 -0
  49. package/src/runtime.ts +4 -1
  50. package/src/types.ts +31 -5
  51. package/src/world.ts +161 -77
  52. package/src/worlds/office-data.ts +57 -0
  53. package/src/worlds/office-paint.ts +276 -0
package/dist/draw.js CHANGED
@@ -1,90 +1,166 @@
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
- import { FURNITURE, isSeated } from "./world.js";
6
- export const TILE = 16;
7
- const rect = (c, color, x, y, w, h) => {
8
- c.fillStyle = color;
9
- c.fillRect(Math.round(x), Math.round(y), w, h);
10
- };
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
- });
6
+ import { drawLinks, linkRoutes } from "./links.js";
7
+ import { BODY_FONT, LABEL_FONT, OUTLINE, TILE, rect, shade } from "./paint.js";
8
+ import { isSeated, specOf } from "./world.js";
9
+ import { OFFICE_PAINTER } from "./worlds/office-paint.js";
10
+ export { TILE } from "./paint.js";
11
+ const painters = new Map([["office", OFFICE_PAINTER]]);
12
+ export function registerPainter(id, painter) {
13
+ painters.set(id, painter);
14
+ }
15
+ const painterFor = (world) => painters.get(world.pack.id) ?? OFFICE_PAINTER;
16
+ export function daylightAt(hour) {
17
+ if (hour >= 7 && hour < 17)
18
+ return { sky: "#8ed0ff", sun: "rgba(255,244,214,0.10)", night: 0 };
19
+ if (hour >= 17 && hour < 20)
20
+ return { sky: "#ffae70", sun: "rgba(255,170,110,0.13)", night: 0.14 };
21
+ if (hour >= 5 && hour < 7)
22
+ return { sky: "#ffc9a3", sun: "rgba(255,200,170,0.08)", night: 0.16 };
23
+ return { sky: "#16204a", sun: "rgba(0,0,0,0)", night: 0.24 };
32
24
  }
33
- function shade(hex, amount) {
34
- const n = parseInt(hex.slice(1).padEnd(6, hex.slice(-1)), 16);
35
- const channel = (shift) => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
36
- return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
25
+ /** Parts draw with one outline around the whole figure, so characters read clearly on any floor. */
26
+ function drawParts(c, parts) {
27
+ c.fillStyle = OUTLINE;
28
+ for (const [, x, y, w, h] of parts)
29
+ c.fillRect(Math.round(x) - 1, Math.round(y) - 1, w + 2, h + 2);
30
+ for (const [color, x, y, w, h] of parts)
31
+ rect(c, color, x, y, w, h);
37
32
  }
38
33
  function drawAgent(c, agent, time, selected) {
39
- const px = agent.x * TILE + 2;
34
+ const left = agent.x * TILE + 2; // sprite is 12 wide inside a 16 tile
35
+ const feet = agent.y * TILE + 15;
40
36
  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);
37
+ const walking = agent.walking;
38
+ const side = agent.facing === "left" || agent.facing === "right";
39
+ const back = agent.facing === "up";
40
+ const flip = agent.facing === "left";
41
+ const phase = Math.floor(time * 9 + (agent.seed % 5));
42
+ const step = walking ? phase % 4 : 0; // 0 pass, 1 left forward, 2 pass, 3 right forward
43
+ const bob = walking && step % 2 === 1 ? -1 : 0;
44
+ const breathe = !walking && !seated && Math.floor(time * 1.4 + (agent.seed % 7)) % 2 === 0 ? 1 : 0;
45
+ const party = agent.party > 0 ? -Math.abs(Math.round(Math.sin(time * 12) * 3)) : 0;
46
+ const skin = agent.skin;
47
+ const hair = agent.hair;
48
+ const shirt = agent.shirt;
49
+ const dark = shade(shirt, -40);
50
+ const light = shade(shirt, 24);
51
+ const legs = "#39415a";
52
+ const shoe = "#1f2433";
53
+ const style = agent.seed % 5; // short, long, bun, spiky, cap
54
+ const outfit = (agent.seed >>> 5) % 3; // tee, hoodie, shirt and tie
55
+ // Shadow, and a highlight under whoever is selected.
56
+ c.fillStyle = "rgba(0,0,0,0.28)";
57
+ c.fillRect(Math.round(left + 1), Math.round(feet - 1), 10, 2);
58
+ if (selected) {
59
+ c.fillStyle = "rgba(255,255,255,0.45)";
60
+ c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
61
+ }
62
+ if (agent.state === "blocked") {
63
+ c.fillStyle = Math.floor(time * 3) % 2 === 0 ? "rgba(239,68,68,0.55)" : "rgba(239,68,68,0.22)";
64
+ c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
65
+ }
66
+ const lift = bob + party;
67
+ const parts = [];
68
+ const bodyTop = feet - (seated ? 13 : 13) + lift + breathe * 0;
69
+ const headTop = bodyTop - 8 + breathe;
70
+ // Chair behind someone sitting at a desk.
47
71
  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);
72
+ rect(c, "#1f2937", left + 1, agent.y * TILE + 3, 10, 9);
73
+ rect(c, "#374151", left + 2, agent.y * TILE + 11, 8, 3);
74
+ rect(c, "#111827", left + 5, agent.y * TILE + 14, 2, 2);
51
75
  }
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);
76
+ // Legs
77
+ if (!seated) {
78
+ const legTop = bodyTop + 7;
79
+ if (side) {
80
+ const swing = step === 1 ? 2 : step === 3 ? -2 : 0;
81
+ parts.push([legs, left + 4 + swing, legTop, 3, 5], [shoe, left + 4 + swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
82
+ parts.push([shade(legs, -12), left + 5 - swing, legTop, 3, 5], [shoe, left + 5 - swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
83
+ }
84
+ else {
85
+ const leftUp = step === 1 ? 1 : 0;
86
+ const rightUp = step === 3 ? 1 : 0;
87
+ parts.push([legs, left + 2, legTop, 3, 5 - leftUp], [shoe, left + 2, legTop + 5 - leftUp, 3, 1]);
88
+ parts.push([legs, left + 7, legTop, 3, 5 - rightUp], [shoe, left + 7, legTop + 5 - rightUp, 3, 1]);
89
+ }
58
90
  }
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);
91
+ // Body and arms
92
+ const swingArm = walking ? (step === 1 ? 1 : step === 3 ? -1 : 0) : 0;
93
+ if (side) {
94
+ parts.push([shirt, left + 3, bodyTop, 6, 7], [dark, left + 3, bodyTop + 6, 6, 1]);
95
+ parts.push([dark, left + 5 + swingArm, bodyTop + 1, 2, 5], [skin, left + 5 + swingArm, bodyTop + 6, 2, 1]);
96
+ }
97
+ else {
98
+ parts.push([shirt, left + 1, bodyTop, 10, 7], [dark, left + 1, bodyTop + 6, 10, 1]);
99
+ const typing = seated && agent.state === "working";
100
+ const type = Math.floor(time * 10 + agent.seed) % 2;
101
+ parts.push([dark, left - 1, bodyTop + 1 + swingArm + (typing ? -type : 0), 2, 5]);
102
+ parts.push([dark, left + 11, bodyTop + 1 - swingArm + (typing ? type - 1 : 0), 2, 5]);
103
+ parts.push([skin, left - 1, bodyTop + 6 + swingArm + (typing ? -type : 0), 2, 1]);
104
+ parts.push([skin, left + 11, bodyTop + 6 - swingArm + (typing ? type - 1 : 0), 2, 1]);
105
+ if (!back && outfit === 2)
106
+ parts.push(["#f3f4f6", left + 4, bodyTop, 4, 3], [agent.seed % 2 ? "#ef4444" : "#1d4ed8", left + 5, bodyTop, 2, 5]);
107
+ if (!back && outfit === 1)
108
+ parts.push([light, left + 3, bodyTop, 6, 2], [dark, left + 5, bodyTop + 2, 2, 4]);
109
+ if (back && outfit === 1)
110
+ parts.push([light, left + 2, bodyTop, 8, 3]);
111
+ }
112
+ // Head
113
+ parts.push([skin, left + 1, headTop, 10, 8]);
114
+ if (back) {
115
+ parts.push([hair, left + 1, headTop, 10, 7]);
116
+ }
117
+ else if (side) {
118
+ const backX = flip ? left + 7 : left + 1;
119
+ parts.push([hair, left + 1, headTop, 10, 3], [hair, backX, headTop, 4, 6]);
120
+ const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
121
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", flip ? left + 3 : left + 8, headTop + 4, 1, blink ? 1 : 2]);
122
+ }
123
+ else {
124
+ parts.push([hair, left + 1, headTop, 10, 3], [hair, left + 1, headTop + 3, 1, 2], [hair, left + 10, headTop + 3, 1, 2]);
125
+ const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
126
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 3, headTop + 4, 2, blink ? 1 : 2]);
127
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 7, headTop + 4, 2, blink ? 1 : 2]);
128
+ parts.push([shade(skin, -28), left + 5, headTop + 6, 2, 1]);
129
+ }
130
+ if (style === 1)
131
+ parts.push([hair, left, headTop + 2, 2, 8], [hair, left + 10, headTop + 2, 2, 8]); // long
132
+ if (style === 2)
133
+ parts.push([hair, left + 4, headTop - 3, 4, 3]); // bun
134
+ if (style === 3)
135
+ parts.push([hair, left + 2, headTop - 2, 2, 2], [hair, left + 5, headTop - 3, 2, 3], [hair, left + 8, headTop - 2, 2, 2]); // spiky
136
+ if (style === 4) { // cap
137
+ parts.push([light, left, headTop - 1, 12, 3]);
138
+ if (!back && !side)
139
+ parts.push([dark, left + 1, headTop + 2, 10, 1]);
140
+ if (side)
141
+ parts.push([dark, flip ? left - 2 : left + 9, headTop + 1, 5, 1]);
142
+ }
143
+ drawParts(c, parts);
144
+ if (agent.party > 0) {
145
+ const colors = ["#f59e0b", "#ec4899", "#22c55e", "#3b82f6", "#a855f7"];
146
+ for (let i = 0; i < 10; i++) {
147
+ const t = (time * 1.1 + i * 0.137) % 1;
148
+ const angle = i * 2.4 + agent.seed;
149
+ rect(c, colors[i % colors.length], left + 6 + Math.cos(angle) * (4 + t * 14), feet - 22 - t * 6 + t * t * 26, 2, 2);
150
+ }
75
151
  }
76
152
  }
77
153
  function bubble(c, x, y, draw) {
78
- rect(c, "#111827", x - 1, y - 1, 13, 11);
154
+ rect(c, OUTLINE, x - 1, y - 1, 13, 11);
79
155
  rect(c, "#ffffff", x, y, 11, 9);
80
156
  rect(c, "#ffffff", x + 4, y + 9, 3, 1);
81
- rect(c, "#111827", x + 4, y + 10, 3, 1);
157
+ rect(c, OUTLINE, x + 4, y + 10, 3, 1);
82
158
  draw(x, y);
83
159
  }
84
160
  function drawStatus(c, agent, time) {
85
161
  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;
162
+ const y = Math.round(agent.y * TILE - 22 + (isSeated(agent) ? 2 : -2));
163
+ const pulse = Math.floor(time * 2 + (agent.seed % 3)) % 2 === 0;
88
164
  if (agent.state === "blocked") {
89
165
  bubble(c, x, y, (bx, by) => {
90
166
  rect(c, pulse ? "#dc2626" : "#991b1b", bx + 5, by + 1, 2, 5);
@@ -115,231 +191,230 @@ function drawStatus(c, agent, time) {
115
191
  rect(c, color, bx + 4, by + 2, 3, 5);
116
192
  });
117
193
  }
194
+ else if (agent.state === "idle" && !agent.walking && agent.seed % 3 === 0) {
195
+ bubble(c, x, y, (bx, by) => {
196
+ rect(c, "#92400e", bx + 3, by + 3, 4, 4);
197
+ rect(c, "#d97706", bx + 3, by + 3, 4, 1);
198
+ rect(c, "#92400e", bx + 7, by + 4, 1, 2);
199
+ if (pulse)
200
+ rect(c, "#9ca3af", bx + 4, by + 1, 1, 1);
201
+ });
202
+ }
118
203
  }
119
- // --- The room -------------------------------------------------------------------------------------------
120
- function drawFloor(c, room, palette) {
204
+ // --- The building ----------------------------------------------------------------------------------------
205
+ function drawFloor(c, room) {
121
206
  for (let ty = room.y; ty < room.y + room.h; ty++) {
122
207
  for (let tx = room.x; tx < room.x + room.w; tx++) {
123
208
  const x = tx * TILE;
124
209
  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);
210
+ const noise = (tx * 73 + ty * 151) % 17;
211
+ switch (room.floor) {
212
+ case "carpet":
213
+ rect(c, "#4c4f7a", x, y, TILE, TILE);
214
+ for (let i = 0; i < 8; i++)
215
+ rect(c, i % 2 ? "#55588a" : "#454874", x + ((tx * 7 + i * 5) % 15), y + ((ty * 3 + i * 7) % 15), 1, 1);
216
+ break;
217
+ case "tile":
218
+ rect(c, (tx + ty) % 2 === 0 ? "#d6dee8" : "#c2ccd8", x, y, TILE, TILE);
219
+ rect(c, "#aab5c2", x, y + 15, TILE, 1);
220
+ rect(c, "#aab5c2", x + 15, y, 1, TILE);
221
+ rect(c, "#e8eef5", x + 1, y + 1, 5, 1);
222
+ break;
223
+ case "stone":
224
+ rect(c, noise % 3 === 0 ? "#8d93a3" : "#9aa0b0", x, y, TILE, TILE);
225
+ rect(c, "#7a8090", x, y + 15, TILE, 1);
226
+ rect(c, "#7a8090", x + (ty % 2 ? 7 : 15), y, 1, 15);
227
+ rect(c, "#adb3c2", x + 2, y + 2, 3, 1);
228
+ break;
229
+ case "concrete":
230
+ rect(c, "#6b7280", x, y, TILE, TILE);
231
+ for (let i = 0; i < 5; i++)
232
+ rect(c, i % 2 ? "#747b89" : "#636a78", x + ((tx * 11 + i * 3) % 15), y + ((ty * 5 + i * 5) % 15), 1, 1);
233
+ if (noise === 3)
234
+ rect(c, "#596070", x + 3, y + 8, 8, 1);
235
+ break;
236
+ case "hall": {
237
+ // Polished floor, with a runner down the middle of a corridor's length.
238
+ rect(c, "#cfc8b8", x, y, TILE, TILE);
239
+ rect(c, "#bdb6a6", x, y + 15, TILE, 1);
240
+ rect(c, "#bdb6a6", x + 15, y, 1, TILE);
241
+ const along = room.w >= room.h;
242
+ const offset = along ? ty - room.y : tx - room.x;
243
+ const size = along ? room.h : room.w;
244
+ if (along && offset === 0)
245
+ rect(c, "#7f6a55", x, y + 12, TILE, 4);
246
+ if (along && offset === size - 1)
247
+ rect(c, "#7f6a55", x, y, TILE, 4);
248
+ if (!along && offset === 0)
249
+ rect(c, "#7f6a55", x + 12, y, 4, TILE);
250
+ if (!along && offset === size - 1)
251
+ rect(c, "#7f6a55", x, y, 4, TILE);
252
+ break;
253
+ }
254
+ case "grass":
255
+ rect(c, "#3f8f4f", x, y, TILE, TILE);
256
+ for (let i = 0; i < 7; i++)
257
+ rect(c, i % 3 ? "#4aa35c" : "#347a43", x + ((tx * 5 + i * 9) % 15), y + ((ty * 7 + i * 3) % 14), 1, 2);
258
+ break;
259
+ default: {
260
+ const plank = ty % 2 === 0 ? "#a57448" : "#9c6c42";
261
+ rect(c, plank, x, y, TILE, TILE);
262
+ rect(c, "#8a5d36", x, y + 7, TILE, 1);
263
+ rect(c, "#8a5d36", x, y + 15, TILE, 1);
264
+ rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 8 : 0)) % 16), y, 1, 7);
265
+ rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 0 : 8)) % 16), y + 8, 1, 7);
266
+ rect(c, "#b4835a", x + (noise % 9) + 2, y + 3, 4, 1);
267
+ rect(c, "#b4835a", x + (noise % 7) + 4, y + 11, 3, 1);
268
+ }
142
269
  }
143
270
  }
144
271
  }
145
- void palette;
146
272
  }
147
- const WALL_TOP = "#2f3350";
148
- const WALL_FACE = "#565c86";
149
- const WALL_LINE = "#454a70";
150
- function drawWalls(c, world, palette) {
273
+ const WALL_TOP = "#2a2d48";
274
+ const WALL_FACE = "#5c628f";
275
+ const WALL_DARK = "#4a4f78";
276
+ const WALL_LIGHT = "#6d74a4";
277
+ /** Outside the building: lawn, with the odd shrub and tree. */
278
+ function drawGrounds(c, tx, ty) {
279
+ const x = tx * TILE;
280
+ const y = ty * TILE;
281
+ rect(c, (tx + ty) % 2 === 0 ? "#4b8a4f" : "#478549", x, y, TILE, TILE);
282
+ const noise = (tx * 73 + ty * 151) % 29;
283
+ for (let i = 0; i < 4; i++)
284
+ rect(c, "#5a9b5c", x + ((tx * 5 + i * 7) % 15), y + ((ty * 3 + i * 5) % 15), 1, 2);
285
+ if (noise === 4) {
286
+ rect(c, "#2f6a38", x + 3, y + 7, 10, 7);
287
+ rect(c, "#3c7f45", x + 5, y + 8, 5, 3);
288
+ }
289
+ else if (noise === 11) {
290
+ rect(c, "#5b3a22", x + 7, y + 10, 2, 5);
291
+ rect(c, "#24592e", x + 2, y + 1, 12, 10);
292
+ rect(c, "#2f6f3a", x + 4, y + 2, 7, 5);
293
+ }
294
+ }
295
+ function drawWalls(c, world, palette, daylight) {
151
296
  const open = new Set();
152
297
  for (const room of world.rooms) {
153
298
  for (let y = room.y; y < room.y + room.h; y++)
154
299
  for (let x = room.x; x < room.x + room.w; x++)
155
300
  open.add(`${x},${y}`);
156
301
  }
157
- for (const door of world.doors)
158
- open.add(`${door.x},${door.y}`);
302
+ const doors = new Set(world.doors.map((door) => `${door.x},${door.y}`));
159
303
  for (let ty = 0; ty < world.rows; ty++) {
160
304
  for (let tx = 0; tx < world.cols; tx++) {
161
- if (open.has(`${tx},${ty}`))
305
+ const key = `${tx},${ty}`;
306
+ if (open.has(key) || doors.has(key))
162
307
  continue;
163
308
  const x = tx * TILE;
164
309
  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.
310
+ const nearBuilding = (cx, cy) => {
311
+ for (let ay = cy - 1; ay <= cy + 1; ay++) {
312
+ for (let ax = cx - 1; ax <= cx + 1; ax++)
313
+ if (open.has(`${ax},${ay}`) || doors.has(`${ax},${ay}`))
314
+ return true;
315
+ }
316
+ return false;
317
+ };
318
+ if (!nearBuilding(tx, ty)) {
319
+ drawGrounds(c, tx, ty);
320
+ continue;
321
+ }
322
+ if (open.has(`${tx},${ty + 1}`)) {
168
323
  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) {
324
+ rect(c, WALL_LIGHT, x, y, TILE, 1);
325
+ rect(c, WALL_DARK, x, y + 12, TILE, 4);
326
+ rect(c, palette.primary, x, y + 11, TILE, 1);
327
+ // Windows look outside, so only an outer wall has them.
328
+ if (tx % 4 === 3 && !nearBuilding(tx, ty - 1) && !world.furniture.some((item) => specOf(world, item).wall && item.x === tx && item.y === ty)) {
329
+ // A window, in the colour of the sky right now.
172
330
  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);
331
+ rect(c, daylight.sky, x + 2, y + 2, 12, 7);
332
+ rect(c, shade(daylight.sky, 40), x + 3, y + 3, 4, 2);
175
333
  rect(c, "#1f2937", x + 8, y + 2, 1, 7);
334
+ if (daylight.night > 0.3) {
335
+ rect(c, "#fef3c7", x + 4, y + 3, 1, 1);
336
+ rect(c, "#fef3c7", x + 11, y + 5, 1, 1);
337
+ }
176
338
  }
177
339
  }
178
340
  else {
179
341
  rect(c, WALL_TOP, x, y, TILE, TILE);
342
+ if (open.has(`${tx},${ty - 1}`))
343
+ rect(c, "#20233a", x, y, TILE, 3);
344
+ if (open.has(`${tx - 1},${ty}`))
345
+ rect(c, "#20233a", x, y, 2, TILE);
346
+ if (open.has(`${tx + 1},${ty}`))
347
+ rect(c, "#20233a", x + 14, y, 2, TILE);
180
348
  }
181
349
  }
182
350
  }
183
351
  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);
186
- }
187
- }
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);
194
- // Monitor
195
- rect(c, "#111827", x + 15, y - 1, 18, 13);
196
- 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);
352
+ const x = door.x * TILE;
353
+ const y = door.y * TILE;
354
+ const horizontal = open.has(`${door.x},${door.y - 1}`) || open.has(`${door.x},${door.y + 1}`);
355
+ rect(c, "#7c5637", x, y, TILE, TILE);
356
+ for (let i = 2; i < 16; i += 4)
357
+ rect(c, "#6b4a2f", horizontal ? x : x + i, horizontal ? y + i : y, horizontal ? TILE : 1, horizontal ? 1 : TILE);
358
+ if (horizontal) {
359
+ rect(c, WALL_DARK, x, y, 2, TILE);
360
+ rect(c, WALL_DARK, x + 14, y, 2, TILE);
203
361
  }
204
- c.fillStyle = "rgba(125,211,252,0.10)";
205
- c.fillRect(x + 11, y + 11, 26, 8);
206
- }
207
- else {
208
- rect(c, "#1f2937", x + 16, y, 16, 11);
209
- rect(c, "#374151", x + 18, y + 2, 3, 1);
210
- }
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);
216
- }
217
- 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);
220
- const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
221
- const per = 8;
222
- world.tasks.slice(0, per * 2).forEach((task, index) => {
223
- const col = index % per;
224
- 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);
228
- });
229
- }
230
- function drawFurniture(c, item, world, time, palette) {
231
- const x = item.x * TILE;
232
- const y = item.y * TILE;
233
- switch (item.type) {
234
- case "desk": {
235
- const seatX = item.x + 1;
236
- 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);
239
- break;
362
+ else {
363
+ rect(c, WALL_DARK, x, y, TILE, 2);
364
+ rect(c, WALL_DARK, x, y + 14, TILE, 2);
240
365
  }
241
- case "coffee":
242
- rect(c, "#374151", x, y + 2, 32, 14);
243
- 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);
246
- rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
247
- rect(c, "#f3f4f6", x + 22, y - 1, 5, 5);
248
- for (let i = 0; i < 3; i++) {
249
- c.fillStyle = "rgba(255,255,255,0.45)";
250
- c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
251
- }
252
- break;
253
- 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);
257
- rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
258
- break;
259
- case "sofa":
260
- rect(c, palette.primary, x + 1, y - 6, 46, 9);
261
- rect(c, "#00000030", x + 1, y + 1, 46, 2);
262
- rect(c, "#4b5563", x, y + 3, 48, 11);
263
- rect(c, "#6b7280", x + 3, y + 3, 20, 7);
264
- rect(c, "#6b7280", x + 25, y + 3, 20, 7);
265
- rect(c, "#374151", x, y + 12, 48, 3);
266
- break;
267
- case "plant":
268
- rect(c, "#92400e", x + 4, y + 9, 8, 7);
269
- 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]]) {
271
- rect(c, "#15803d", x + lx, y + ly, lw, lh);
272
- }
273
- rect(c, "#22c55e", x + 7, y - 3, 2, 8);
274
- rect(c, "#22c55e", x + 3, y + 1, 2, 3);
275
- break;
276
- case "board":
277
- drawBoard(c, x, y, world);
278
- break;
279
- case "shelf":
280
- rect(c, "#5b3a21", x, y - 12, 32, 28);
281
- for (let shelf = 0; shelf < 2; shelf++) {
282
- rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
283
- for (let book = 0; book < 8; book++) {
284
- const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf) % 5];
285
- rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
286
- }
287
- }
288
- break;
289
- case "server":
290
- rect(c, "#111827", x + 1, y - 12, 14, 28);
291
- for (let i = 0; i < 5; i++) {
292
- rect(c, "#1f2937", x + 2, y - 11 + i * 5, 12, 4);
293
- const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
294
- rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
295
- rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
296
- }
297
- break;
298
- case "table":
299
- rect(c, "#57534e", x + 2, y + 14, 44, 14);
300
- rect(c, "#78716c", x + 2, y + 3, 44, 14);
301
- rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
302
- rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
303
- rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
304
- break;
305
- case "rug":
306
- rect(c, palette.primary, x, y, 64, 48);
307
- c.fillStyle = "rgba(0,0,0,0.35)";
308
- c.fillRect(x + 3, y + 3, 58, 42);
309
- rect(c, palette.primary, x + 6, y + 6, 52, 1);
310
- rect(c, palette.primary, x + 6, y + 41, 52, 1);
311
- break;
312
366
  }
313
- void FURNITURE;
314
367
  }
315
368
  function drawLabels(c, world) {
316
- c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
369
+ c.font = LABEL_FONT;
317
370
  c.textBaseline = "top";
318
371
  for (const room of world.rooms) {
319
372
  if (!room.name)
320
373
  continue;
321
374
  const label = room.name.toUpperCase();
322
- // On the wall above the room, clear of the furniture.
375
+ const width = Math.ceil(c.measureText(label).width) + 6;
323
376
  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);
377
+ const y = room.y * TILE - 13;
378
+ rect(c, "#1c1f33", x, y, width, 10);
379
+ rect(c, "#2f3350", x + 1, y + 1, width - 2, 8);
380
+ c.fillStyle = "#f8fafc";
381
+ c.fillText(label, x + 3, y + 1.5);
382
+ }
383
+ }
384
+ /** Everything that doesn't move. Redrawn only when the building or the hour changes. */
385
+ export function drawBuilding(c, world, palette, daylight) {
386
+ rect(c, WALL_TOP, 0, 0, world.cols * TILE, world.rows * TILE);
387
+ for (const room of world.rooms)
388
+ drawFloor(c, room);
389
+ drawWalls(c, world, palette, daylight);
390
+ for (const item of world.furniture)
391
+ if (item.type === "rug")
392
+ painterFor(world).furniture(c, item, world, 0, palette);
393
+ for (const item of world.furniture)
394
+ if (specOf(world, item).wall)
395
+ painterFor(world).furniture(c, item, world, 0, palette);
396
+ // Light from the windows falling across the floor.
397
+ if (daylight.sun !== "rgba(0,0,0,0)") {
398
+ c.fillStyle = daylight.sun;
399
+ for (const room of world.rooms) {
400
+ for (let tx = room.x; tx < room.x + room.w; tx++) {
401
+ if (tx % 4 !== 3)
402
+ continue;
403
+ c.fillRect(tx * TILE + 2, room.y * TILE, 12, Math.min(room.h, 3) * TILE);
404
+ }
405
+ }
329
406
  }
330
407
  }
331
408
  function nameTag(c, agent, selected, width) {
332
- const name = agent.name;
333
- c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
409
+ c.font = LABEL_FONT;
334
410
  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);
411
+ const w = Math.ceil(c.measureText(agent.name).width) + 5;
412
+ const x = Math.round(Math.max(1, Math.min(width - w - 1, agent.x * TILE + 8 - w / 2)));
413
+ const y = Math.round(agent.y * TILE + 16);
339
414
  c.fillStyle = selected ? "#ffffff" : "rgba(17,24,39,0.82)";
340
415
  c.fillRect(x, y, w, 10);
341
416
  c.fillStyle = selected ? "#111827" : "#ffffff";
342
- c.fillText(name, x + 2.5, y + 1.5);
417
+ c.fillText(agent.name, x + 2.5, y + 1.5);
343
418
  }
344
419
  function wrap(c, text, max) {
345
420
  const lines = [];
@@ -359,60 +434,86 @@ function wrap(c, text, max) {
359
434
  return lines.slice(0, 3);
360
435
  }
361
436
  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";
437
+ function drawCard(c, agent, world) {
438
+ c.font = BODY_FONT;
364
439
  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);
440
+ const room = world.rooms.find((candidate) => candidate.id === agent.area);
441
+ const body = wrap(c, agent.task || STATE_LABEL[agent.state] || "", 110);
442
+ const w = 120;
443
+ const h = 18 + body.length * 9 + (room ? 9 : 0) + 3;
444
+ const x = Math.round(Math.max(2, Math.min(world.cols * TILE - w - 2, agent.x * TILE + 8 - w / 2)));
445
+ const y = Math.round(Math.max(2, agent.y * TILE - h - 24));
446
+ rect(c, OUTLINE, x - 1, y - 1, w + 2, h + 2);
371
447
  rect(c, "#ffffff", x, y, w, h);
372
448
  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);
449
+ c.font = LABEL_FONT;
450
+ c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0, 28), x + 4, y + 4);
375
451
  c.fillStyle = "#374151";
376
- c.font = "7px ui-monospace, Menlo, Consolas, monospace";
452
+ c.font = BODY_FONT;
377
453
  body.forEach((line, index) => c.fillText(line, x + 4, y + 15 + index * 9));
454
+ if (room) {
455
+ c.fillStyle = "#6b7280";
456
+ c.fillText(`In ${room.name}`.slice(0, 28), x + 4, y + 15 + body.length * 9);
457
+ }
378
458
  }
379
459
  function drawProgress(c, world, palette) {
380
460
  if (world.tasks.length === 0)
381
461
  return;
382
462
  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";
463
+ c.font = LABEL_FONT;
385
464
  c.textBaseline = "top";
386
465
  const w = 96;
387
466
  rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
388
467
  c.fillStyle = "#ffffff";
389
- c.fillText(label, 8, 6.5);
468
+ c.fillText(`${world.pack.words.quests} ${done}/${world.tasks.length}`, 8, 6.5);
390
469
  rect(c, "#374151", 8, 16, w - 8, 4);
391
470
  rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
392
471
  }
393
- export function drawWorld(c, world, time, palette, selected) {
472
+ /** Everything that moves. `building` is the layer from drawBuilding. */
473
+ export function drawWorld(c, world, time, palette, daylight, building, selected) {
394
474
  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);
475
+ const height = world.rows * TILE;
476
+ if (building)
477
+ c.drawImage(building, 0, 0, width, height);
478
+ else
479
+ drawBuilding(c, world, palette, daylight);
480
+ // On the floor, so people and furniture stay in front of the lines.
481
+ if (world.showLinks && world.links.length > 0) {
482
+ world.routes ??= linkRoutes(world);
483
+ drawLinks(c, world.routes, time);
484
+ }
485
+ const painter = painterFor(world);
403
486
  const scene = [];
404
487
  for (const item of world.furniture) {
405
- if (item.type === "rug")
488
+ const spec = specOf(world, item);
489
+ if (spec.solid === false)
406
490
  continue;
407
- scene.push({ depth: (item.y + FURNITURE[item.type].h) * TILE - 2, draw: () => drawFurniture(c, item, world, time, palette) });
491
+ scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => painterFor(world).furniture(c, item, world, time, palette) });
408
492
  }
409
493
  const shown = world.agents.filter((agent) => agent.state !== "away");
410
- for (const agent of shown) {
494
+ for (const agent of shown)
411
495
  scene.push({ depth: agent.y * TILE + 14, draw: () => drawAgent(c, agent, time, agent.id === selected) });
412
- }
413
496
  scene.sort((a, b) => a.depth - b.depth);
414
497
  for (const item of scene)
415
498
  item.draw();
499
+ // Night: the room dims, and screens and lamps light it.
500
+ if (daylight.night > 0) {
501
+ c.fillStyle = `rgba(12,14,48,${daylight.night})`;
502
+ c.fillRect(0, 0, width, height);
503
+ c.save();
504
+ c.globalCompositeOperation = "lighter";
505
+ const glow = (x, y, radius, color) => {
506
+ const gradient = c.createRadialGradient(x, y, 1, x, y, radius);
507
+ gradient.addColorStop(0, color);
508
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
509
+ c.fillStyle = gradient;
510
+ c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
511
+ };
512
+ for (const item of world.furniture)
513
+ painter.glow(item, world, glow);
514
+ c.restore();
515
+ }
516
+ // On top, so tall furniture against a wall can't hide a room's name.
416
517
  drawLabels(c, world);
417
518
  for (const agent of shown)
418
519
  nameTag(c, agent, agent.id === selected, width);
@@ -422,6 +523,6 @@ export function drawWorld(c, world, time, palette, selected) {
422
523
  drawProgress(c, world, palette);
423
524
  const picked = shown.find((agent) => agent.id === selected);
424
525
  if (picked)
425
- drawCard(c, picked, width);
526
+ drawCard(c, picked, world);
426
527
  }
427
528
  //# sourceMappingURL=draw.js.map