@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/dist/draw.d.ts CHANGED
@@ -3,15 +3,16 @@
3
3
  * 16 pixels a tile. The building is drawn once to a layer (drawBuilding); people, screens
4
4
  * and anything else that moves are drawn on top each frame (drawWorld).
5
5
  */
6
- import { type World } from "./world.js";
7
- export declare const TILE = 16;
8
- export interface Palette {
9
- primary: string;
10
- background: string;
11
- text: string;
12
- accent: string;
6
+ import { type Ctx, type Palette } from "./paint.js";
7
+ import { type Item, type World } from "./world.js";
8
+ export { TILE, type Palette } from "./paint.js";
9
+ /** How one world looks. The engine draws the building, characters and labels around it. */
10
+ export interface Painter {
11
+ furniture(c: Ctx, item: Item, world: World, time: number, palette: Palette): void;
12
+ /** What lights up at night. Call `glow` for anything that does. */
13
+ glow(item: Item, world: World, glow: (x: number, y: number, radius: number, color: string) => void): void;
13
14
  }
14
- type Ctx = CanvasRenderingContext2D;
15
+ export declare function registerPainter(id: string, painter: Painter): void;
15
16
  export interface Daylight {
16
17
  sky: string;
17
18
  sun: string;
@@ -22,5 +23,4 @@ export declare function daylightAt(hour: number): Daylight;
22
23
  export declare function drawBuilding(c: Ctx, world: World, palette: Palette, daylight: Daylight): void;
23
24
  /** Everything that moves. `building` is the layer from drawBuilding. */
24
25
  export declare function drawWorld(c: Ctx, world: World, time: number, palette: Palette, daylight: Daylight, building: CanvasImageSource | undefined, selected?: string): void;
25
- export {};
26
26
  //# sourceMappingURL=draw.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"draw.d.ts","sourceRoot":"","sources":["../src/draw.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAyD,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAE/F,eAAO,MAAM,IAAI,KAAK,CAAC;AAEvB,MAAM,WAAW,OAAO;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAE9F,KAAK,GAAG,GAAG,wBAAwB,CAAC;AAqBpC,MAAM,WAAW,QAAQ;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAErE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAKjD;AA6RD,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAgB7F;AAsUD,wEAAwE;AACxE,wBAAgB,SAAS,CACvB,CAAC,EAAE,GAAG,EACN,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CA+CN"}
1
+ {"version":3,"file":"draw.d.ts","sourceRoot":"","sources":["../src/draw.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAqD,KAAK,GAAG,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AACvG,OAAO,EAAgC,KAAK,IAAI,EAAa,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAG5F,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAIhD,2FAA2F;AAC3F,MAAM,WAAW,OAAO;IACtB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAClF,mEAAmE;IACnE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;CAC3G;AAID,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAElE;AAMD,MAAM,WAAW,QAAQ;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAErE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAKjD;AA8UD,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAgB7F;AAoED,wEAAwE;AACxE,wBAAgB,SAAS,CACvB,CAAC,EAAE,GAAG,EACN,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAgDN"}
package/dist/draw.js CHANGED
@@ -3,23 +3,16 @@
3
3
  * 16 pixels a tile. The building is drawn once to a layer (drawBuilding); people, screens
4
4
  * and anything else that moves are drawn on top each frame (drawWorld).
5
5
  */
6
- import { FURNITURE, isSeated } from "./world.js";
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";
11
- const rect = (c, color, x, y, w, h) => {
12
- c.fillStyle = color;
13
- c.fillRect(Math.round(x), Math.round(y), w, h);
14
- };
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);
20
- const channel = (shift) => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
21
- return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
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);
22
14
  }
15
+ const painterFor = (world) => painters.get(world.pack.id) ?? OFFICE_PAINTER;
23
16
  export function daylightAt(hour) {
24
17
  if (hour >= 7 && hour < 17)
25
18
  return { sky: "#8ed0ff", sun: "rgba(255,244,214,0.10)", night: 0 };
@@ -240,6 +233,24 @@ function drawFloor(c, room) {
240
233
  if (noise === 3)
241
234
  rect(c, "#596070", x + 3, y + 8, 8, 1);
242
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
+ }
243
254
  case "grass":
244
255
  rect(c, "#3f8f4f", x, y, TILE, TILE);
245
256
  for (let i = 0; i < 7; i++)
@@ -263,6 +274,24 @@ const WALL_TOP = "#2a2d48";
263
274
  const WALL_FACE = "#5c628f";
264
275
  const WALL_DARK = "#4a4f78";
265
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
+ }
266
295
  function drawWalls(c, world, palette, daylight) {
267
296
  const open = new Set();
268
297
  for (const room of world.rooms) {
@@ -278,12 +307,25 @@ function drawWalls(c, world, palette, daylight) {
278
307
  continue;
279
308
  const x = tx * TILE;
280
309
  const y = ty * TILE;
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
+ }
281
322
  if (open.has(`${tx},${ty + 1}`)) {
282
323
  rect(c, WALL_FACE, x, y, TILE, TILE);
283
324
  rect(c, WALL_LIGHT, x, y, TILE, 1);
284
325
  rect(c, WALL_DARK, x, y + 12, TILE, 4);
285
326
  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)) {
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)) {
287
329
  // A window, in the colour of the sky right now.
288
330
  rect(c, "#1f2937", x + 1, y + 1, 14, 9);
289
331
  rect(c, daylight.sky, x + 2, y + 2, 12, 7);
@@ -326,6 +368,14 @@ function drawWalls(c, world, palette, daylight) {
326
368
  function drawLabels(c, world) {
327
369
  c.font = LABEL_FONT;
328
370
  c.textBaseline = "top";
371
+ for (const item of world.furniture) {
372
+ if (!item.label)
373
+ continue;
374
+ const width = Math.ceil(c.measureText(item.label).width) + 4;
375
+ rect(c, "#f4ecd8", item.x * TILE + 17, item.y * TILE + 3, width, 10);
376
+ c.fillStyle = "#1c1408";
377
+ c.fillText(item.label, item.x * TILE + 19, item.y * TILE + 4.5);
378
+ }
329
379
  for (const room of world.rooms) {
330
380
  if (!room.name)
331
381
  continue;
@@ -346,11 +396,11 @@ export function drawBuilding(c, world, palette, daylight) {
346
396
  drawFloor(c, room);
347
397
  drawWalls(c, world, palette, daylight);
348
398
  for (const item of world.furniture)
349
- if (item.type === "rug")
350
- drawFurniture(c, item, world, 0, palette);
399
+ if (item.type === "rug" || item.type === "pile")
400
+ painterFor(world).furniture(c, item, world, 0, palette);
351
401
  for (const item of world.furniture)
352
- if (FURNITURE[item.type].wall)
353
- drawFurniture(c, item, world, 0, palette);
402
+ if (specOf(world, item).wall)
403
+ painterFor(world).furniture(c, item, world, 0, palette);
354
404
  // Light from the windows falling across the floor.
355
405
  if (daylight.sun !== "rgba(0,0,0,0)") {
356
406
  c.fillStyle = daylight.sun;
@@ -363,279 +413,6 @@ export function drawBuilding(c, world, palette, daylight) {
363
413
  }
364
414
  }
365
415
  }
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);
378
- // Monitor
379
- rect(c, OUTLINE, x + 14, y - 3, 20, 15);
380
- rect(c, "#374151", x + 22, y + 12, 4, 2);
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);
395
- }
396
- c.fillStyle = "rgba(125,211,252,0.12)";
397
- c.fillRect(x + 10, y + 11, 28, 8);
398
- }
399
- else {
400
- rect(c, "#1f2937", x + 15, y - 2, 18, 13);
401
- rect(c, "#374151", x + 17, y, 3, 1);
402
- }
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);
412
- }
413
- function drawBoard(c, x, y, world) {
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);
418
- const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
419
- const per = 8;
420
- world.tasks.slice(0, per * 2).forEach((task, index) => {
421
- const col = index % per;
422
- const row = Math.floor(index / per);
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);
428
- });
429
- }
430
- function drawFurniture(c, item, world, time, palette) {
431
- const x = item.x * TILE;
432
- const y = item.y * TILE;
433
- switch (item.type) {
434
- case "desk": {
435
- const seatX = item.x + 1;
436
- const seatY = item.y + 2;
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");
439
- break;
440
- }
441
- case "coffee":
442
- rect(c, "#374151", x, y + 2, 32, 14);
443
- rect(c, "#6b7280", x, y + 2, 32, 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);
448
- rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
449
- rect(c, "#f3f4f6", x + 23, y - 1, 5, 5);
450
- rect(c, "#92400e", x + 24, y, 3, 1);
451
- for (let i = 0; i < 3; i++) {
452
- c.fillStyle = "rgba(255,255,255,0.5)";
453
- c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
454
- }
455
- break;
456
- case "water":
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);
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);
465
- break;
466
- case "sofa":
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);
469
- rect(c, "#4b5563", x, y + 3, 48, 11);
470
- rect(c, "#6b7280", x + 3, y + 3, 20, 7);
471
- rect(c, "#6b7280", x + 25, y + 3, 20, 7);
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);
475
- break;
476
- case "plant":
477
- rect(c, "#92400e", x + 4, y + 9, 8, 7);
478
- rect(c, "#b45309", x + 3, y + 8, 10, 2);
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]])
480
- rect(c, "#15803d", x + lx, y + ly, lw, lh);
481
- rect(c, "#22c55e", x + 7, y - 3, 2, 8);
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
- }
493
- break;
494
- case "board":
495
- drawBoard(c, x, y, world);
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;
508
- case "shelf":
509
- rect(c, "#5b3a21", x, y - 12, 32, 28);
510
- for (let shelf = 0; shelf < 2; shelf++) {
511
- rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
512
- for (let book = 0; book < 8; book++) {
513
- const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf + item.x) % 5];
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);
516
- }
517
- }
518
- break;
519
- case "server":
520
- rect(c, OUTLINE, x, y - 13, 16, 29);
521
- rect(c, "#1f2937", x + 1, y - 12, 14, 27);
522
- for (let i = 0; i < 5; i++) {
523
- rect(c, "#111827", x + 2, y - 11 + i * 5, 12, 4);
524
- const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
525
- rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
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);
528
- }
529
- break;
530
- case "table":
531
- rect(c, "#44403c", x + 2, y + 16, 44, 12);
532
- rect(c, "#78716c", x + 2, y + 3, 44, 14);
533
- rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
534
- rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
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);
538
- break;
539
- case "rug":
540
- rect(c, palette.primary, x, y, 64, 48);
541
- c.fillStyle = "rgba(0,0,0,0.42)";
542
- c.fillRect(x + 3, y + 3, 58, 42);
543
- rect(c, palette.primary, x + 6, y + 6, 52, 1);
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);
549
- break;
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
- }
637
- }
638
- }
639
416
  function nameTag(c, agent, selected, width) {
640
417
  c.font = LABEL_FONT;
641
418
  c.textBaseline = "top";
@@ -696,7 +473,7 @@ function drawProgress(c, world, palette) {
696
473
  const w = 96;
697
474
  rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
698
475
  c.fillStyle = "#ffffff";
699
- c.fillText(`QUESTS ${done}/${world.tasks.length}`, 8, 6.5);
476
+ c.fillText(`${world.pack.words.quests} ${done}/${world.tasks.length}`, 8, 6.5);
700
477
  rect(c, "#374151", 8, 16, w - 8, 4);
701
478
  rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
702
479
  }
@@ -708,12 +485,18 @@ export function drawWorld(c, world, time, palette, daylight, building, selected)
708
485
  c.drawImage(building, 0, 0, width, height);
709
486
  else
710
487
  drawBuilding(c, world, palette, daylight);
488
+ // On the floor, so people and furniture stay in front of the lines.
489
+ if (world.showLinks && world.links.length > 0) {
490
+ world.routes ??= linkRoutes(world);
491
+ drawLinks(c, world.routes, time);
492
+ }
493
+ const painter = painterFor(world);
711
494
  const scene = [];
712
495
  for (const item of world.furniture) {
713
- const spec = FURNITURE[item.type];
496
+ const spec = specOf(world, item);
714
497
  if (spec.solid === false)
715
498
  continue;
716
- scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => drawFurniture(c, item, world, time, palette) });
499
+ scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => painterFor(world).furniture(c, item, world, time, palette) });
717
500
  }
718
501
  const shown = world.agents.filter((agent) => agent.state !== "away");
719
502
  for (const agent of shown)
@@ -734,15 +517,8 @@ export function drawWorld(c, world, time, palette, daylight, building, selected)
734
517
  c.fillStyle = gradient;
735
518
  c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
736
519
  };
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
- }
520
+ for (const item of world.furniture)
521
+ painter.glow(item, world, glow);
746
522
  c.restore();
747
523
  }
748
524
  // On top, so tall furniture against a wall can't hide a room's name.