@blkc-code/render-canvas 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/draw.d.ts +9 -9
  2. package/dist/draw.d.ts.map +1 -1
  3. package/dist/draw.js +73 -305
  4. package/dist/draw.js.map +1 -1
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/links.d.ts +16 -0
  10. package/dist/links.d.ts.map +1 -0
  11. package/dist/links.js +75 -0
  12. package/dist/links.js.map +1 -0
  13. package/dist/office.d.ts +2 -0
  14. package/dist/office.d.ts.map +1 -1
  15. package/dist/office.js +2 -0
  16. package/dist/office.js.map +1 -1
  17. package/dist/pack.d.ts +31 -0
  18. package/dist/pack.d.ts.map +1 -0
  19. package/dist/pack.js +10 -0
  20. package/dist/pack.js.map +1 -0
  21. package/dist/paint.d.ts +15 -0
  22. package/dist/paint.d.ts.map +1 -0
  23. package/dist/paint.js +18 -0
  24. package/dist/paint.js.map +1 -0
  25. package/dist/runtime.js +1 -1
  26. package/dist/runtime.txt +1 -1
  27. package/dist/types.d.ts +13 -2
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/world.d.ts +15 -11
  30. package/dist/world.d.ts.map +1 -1
  31. package/dist/world.js +31 -59
  32. package/dist/world.js.map +1 -1
  33. package/dist/worlds/office-data.d.ts +14 -0
  34. package/dist/worlds/office-data.d.ts.map +1 -0
  35. package/dist/worlds/office-data.js +52 -0
  36. package/dist/worlds/office-data.js.map +1 -0
  37. package/dist/worlds/office-paint.d.ts +13 -0
  38. package/dist/worlds/office-paint.d.ts.map +1 -0
  39. package/dist/worlds/office-paint.js +292 -0
  40. package/dist/worlds/office-paint.js.map +1 -0
  41. package/package.json +7 -2
  42. package/src/draw.ts +73 -289
  43. package/src/index.ts +3 -0
  44. package/src/links.ts +76 -0
  45. package/src/office.ts +3 -0
  46. package/src/pack.ts +36 -0
  47. package/src/paint.ts +24 -0
  48. package/src/types.ts +12 -2
  49. package/src/world.ts +41 -60
  50. package/src/worlds/office-data.ts +57 -0
  51. package/src/worlds/office-paint.ts +276 -0
package/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;AAuUD,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);
@@ -347,10 +389,10 @@ export function drawBuilding(c, world, palette, daylight) {
347
389
  drawWalls(c, world, palette, daylight);
348
390
  for (const item of world.furniture)
349
391
  if (item.type === "rug")
350
- drawFurniture(c, item, world, 0, palette);
392
+ painterFor(world).furniture(c, item, world, 0, palette);
351
393
  for (const item of world.furniture)
352
- if (FURNITURE[item.type].wall)
353
- drawFurniture(c, item, world, 0, palette);
394
+ if (specOf(world, item).wall)
395
+ painterFor(world).furniture(c, item, world, 0, palette);
354
396
  // Light from the windows falling across the floor.
355
397
  if (daylight.sun !== "rgba(0,0,0,0)") {
356
398
  c.fillStyle = daylight.sun;
@@ -363,279 +405,6 @@ export function drawBuilding(c, world, palette, daylight) {
363
405
  }
364
406
  }
365
407
  }
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
408
  function nameTag(c, agent, selected, width) {
640
409
  c.font = LABEL_FONT;
641
410
  c.textBaseline = "top";
@@ -696,7 +465,7 @@ function drawProgress(c, world, palette) {
696
465
  const w = 96;
697
466
  rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
698
467
  c.fillStyle = "#ffffff";
699
- c.fillText(`QUESTS ${done}/${world.tasks.length}`, 8, 6.5);
468
+ c.fillText(`${world.pack.words.quests} ${done}/${world.tasks.length}`, 8, 6.5);
700
469
  rect(c, "#374151", 8, 16, w - 8, 4);
701
470
  rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
702
471
  }
@@ -708,12 +477,18 @@ export function drawWorld(c, world, time, palette, daylight, building, selected)
708
477
  c.drawImage(building, 0, 0, width, height);
709
478
  else
710
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);
711
486
  const scene = [];
712
487
  for (const item of world.furniture) {
713
- const spec = FURNITURE[item.type];
488
+ const spec = specOf(world, item);
714
489
  if (spec.solid === false)
715
490
  continue;
716
- scene.push({ depth: (item.y + spec.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) });
717
492
  }
718
493
  const shown = world.agents.filter((agent) => agent.state !== "away");
719
494
  for (const agent of shown)
@@ -734,15 +509,8 @@ export function drawWorld(c, world, time, palette, daylight, building, selected)
734
509
  c.fillStyle = gradient;
735
510
  c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
736
511
  };
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
- }
512
+ for (const item of world.furniture)
513
+ painter.glow(item, world, glow);
746
514
  c.restore();
747
515
  }
748
516
  // On top, so tall furniture against a wall can't hide a room's name.