@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.
- package/dist/draw.d.ts +9 -9
- package/dist/draw.d.ts.map +1 -1
- package/dist/draw.js +82 -306
- package/dist/draw.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +16 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +75 -0
- package/dist/links.js.map +1 -0
- package/dist/office.d.ts +2 -0
- package/dist/office.d.ts.map +1 -1
- package/dist/office.js +7 -3
- package/dist/office.js.map +1 -1
- package/dist/pack.d.ts +31 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +10 -0
- package/dist/pack.js.map +1 -0
- package/dist/paint.d.ts +15 -0
- package/dist/paint.d.ts.map +1 -0
- package/dist/paint.js +18 -0
- package/dist/paint.js.map +1 -0
- package/dist/runtime.js +1 -1
- package/dist/runtime.txt +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/world.d.ts +18 -13
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +42 -64
- package/dist/world.js.map +1 -1
- package/dist/worlds/office-data.d.ts +14 -0
- package/dist/worlds/office-data.d.ts.map +1 -0
- package/dist/worlds/office-data.js +53 -0
- package/dist/worlds/office-data.js.map +1 -0
- package/dist/worlds/office-paint.d.ts +13 -0
- package/dist/worlds/office-paint.d.ts.map +1 -0
- package/dist/worlds/office-paint.js +300 -0
- package/dist/worlds/office-paint.js.map +1 -0
- package/package.json +7 -2
- package/src/draw.ts +80 -289
- package/src/index.ts +3 -0
- package/src/links.ts +76 -0
- package/src/office.ts +8 -3
- package/src/pack.ts +36 -0
- package/src/paint.ts +24 -0
- package/src/types.ts +13 -3
- package/src/world.ts +53 -66
- package/src/worlds/office-data.ts +58 -0
- package/src/worlds/office-paint.ts +284 -0
package/src/draw.ts
CHANGED
|
@@ -4,31 +4,30 @@
|
|
|
4
4
|
* and anything else that moves are drawn on top each frame (drawWorld).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { drawLinks, linkRoutes } from "./links.js";
|
|
8
|
+
import { BODY_FONT, LABEL_FONT, OUTLINE, TILE, rect, shade, type Ctx, type Palette } from "./paint.js";
|
|
9
|
+
import { isSeated, specOf, type Agent, type Item, type Room, type World } from "./world.js";
|
|
10
|
+
import { OFFICE_PAINTER } from "./worlds/office-paint.js";
|
|
8
11
|
|
|
9
|
-
export
|
|
12
|
+
export { TILE, type Palette } from "./paint.js";
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
// --- Worlds ---------------------------------------------------------------------------------------
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** How one world looks. The engine draws the building, characters and labels around it. */
|
|
17
|
+
export interface Painter {
|
|
18
|
+
furniture(c: Ctx, item: Item, world: World, time: number, palette: Palette): void;
|
|
19
|
+
/** What lights up at night. Call `glow` for anything that does. */
|
|
20
|
+
glow(item: Item, world: World, glow: (x: number, y: number, radius: number, color: string) => void): void;
|
|
21
|
+
}
|
|
18
22
|
|
|
19
|
-
const
|
|
20
|
-
c.fillStyle = color;
|
|
21
|
-
c.fillRect(Math.round(x), Math.round(y), w, h);
|
|
22
|
-
};
|
|
23
|
+
const painters = new Map<string, Painter>([["office", OFFICE_PAINTER]]);
|
|
23
24
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
|
|
27
|
-
const n = parseInt(hex.slice(1, 7), 16);
|
|
28
|
-
const channel = (shift: number): number => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
|
|
29
|
-
return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
|
|
25
|
+
export function registerPainter(id: string, painter: Painter): void {
|
|
26
|
+
painters.set(id, painter);
|
|
30
27
|
}
|
|
31
28
|
|
|
29
|
+
const painterFor = (world: World): Painter => painters.get(world.pack.id) ?? OFFICE_PAINTER;
|
|
30
|
+
|
|
32
31
|
// --- Time of day, from the viewer's own clock ------------------------------------------------------------
|
|
33
32
|
|
|
34
33
|
export interface Daylight { sky: string; sun: string; night: number }
|
|
@@ -238,6 +237,20 @@ function drawFloor(c: Ctx, room: Room): void {
|
|
|
238
237
|
for (let i = 0; i < 5; i++) rect(c, i % 2 ? "#747b89" : "#636a78", x + ((tx * 11 + i * 3) % 15), y + ((ty * 5 + i * 5) % 15), 1, 1);
|
|
239
238
|
if (noise === 3) rect(c, "#596070", x + 3, y + 8, 8, 1);
|
|
240
239
|
break;
|
|
240
|
+
case "hall": {
|
|
241
|
+
// Polished floor, with a runner down the middle of a corridor's length.
|
|
242
|
+
rect(c, "#cfc8b8", x, y, TILE, TILE);
|
|
243
|
+
rect(c, "#bdb6a6", x, y + 15, TILE, 1);
|
|
244
|
+
rect(c, "#bdb6a6", x + 15, y, 1, TILE);
|
|
245
|
+
const along = room.w >= room.h;
|
|
246
|
+
const offset = along ? ty - room.y : tx - room.x;
|
|
247
|
+
const size = along ? room.h : room.w;
|
|
248
|
+
if (along && offset === 0) rect(c, "#7f6a55", x, y + 12, TILE, 4);
|
|
249
|
+
if (along && offset === size - 1) rect(c, "#7f6a55", x, y, TILE, 4);
|
|
250
|
+
if (!along && offset === 0) rect(c, "#7f6a55", x + 12, y, 4, TILE);
|
|
251
|
+
if (!along && offset === size - 1) rect(c, "#7f6a55", x, y, 4, TILE);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
241
254
|
case "grass":
|
|
242
255
|
rect(c, "#3f8f4f", x, y, TILE, TILE);
|
|
243
256
|
for (let i = 0; i < 7; i++) rect(c, i % 3 ? "#4aa35c" : "#347a43", x + ((tx * 5 + i * 9) % 15), y + ((ty * 7 + i * 3) % 14), 1, 2);
|
|
@@ -262,6 +275,23 @@ const WALL_FACE = "#5c628f";
|
|
|
262
275
|
const WALL_DARK = "#4a4f78";
|
|
263
276
|
const WALL_LIGHT = "#6d74a4";
|
|
264
277
|
|
|
278
|
+
/** Outside the building: lawn, with the odd shrub and tree. */
|
|
279
|
+
function drawGrounds(c: Ctx, tx: number, ty: number): void {
|
|
280
|
+
const x = tx * TILE;
|
|
281
|
+
const y = ty * TILE;
|
|
282
|
+
rect(c, (tx + ty) % 2 === 0 ? "#4b8a4f" : "#478549", x, y, TILE, TILE);
|
|
283
|
+
const noise = (tx * 73 + ty * 151) % 29;
|
|
284
|
+
for (let i = 0; i < 4; i++) 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
|
+
} else if (noise === 11) {
|
|
289
|
+
rect(c, "#5b3a22", x + 7, y + 10, 2, 5);
|
|
290
|
+
rect(c, "#24592e", x + 2, y + 1, 12, 10);
|
|
291
|
+
rect(c, "#2f6f3a", x + 4, y + 2, 7, 5);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
265
295
|
function drawWalls(c: Ctx, world: World, palette: Palette, daylight: Daylight): void {
|
|
266
296
|
const open = new Set<string>();
|
|
267
297
|
for (const room of world.rooms) {
|
|
@@ -275,12 +305,23 @@ function drawWalls(c: Ctx, world: World, palette: Palette, daylight: Daylight):
|
|
|
275
305
|
if (open.has(key) || doors.has(key)) continue;
|
|
276
306
|
const x = tx * TILE;
|
|
277
307
|
const y = ty * TILE;
|
|
308
|
+
const nearBuilding = (cx: number, cy: number): boolean => {
|
|
309
|
+
for (let ay = cy - 1; ay <= cy + 1; ay++) {
|
|
310
|
+
for (let ax = cx - 1; ax <= cx + 1; ax++) if (open.has(`${ax},${ay}`) || doors.has(`${ax},${ay}`)) return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
};
|
|
314
|
+
if (!nearBuilding(tx, ty)) {
|
|
315
|
+
drawGrounds(c, tx, ty);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
278
318
|
if (open.has(`${tx},${ty + 1}`)) {
|
|
279
319
|
rect(c, WALL_FACE, x, y, TILE, TILE);
|
|
280
320
|
rect(c, WALL_LIGHT, x, y, TILE, 1);
|
|
281
321
|
rect(c, WALL_DARK, x, y + 12, TILE, 4);
|
|
282
322
|
rect(c, palette.primary, x, y + 11, TILE, 1);
|
|
283
|
-
|
|
323
|
+
// Windows look outside, so only an outer wall has them.
|
|
324
|
+
if (tx % 4 === 3 && !nearBuilding(tx, ty - 1) && !world.furniture.some((item) => specOf(world, item).wall && item.x === tx && item.y === ty)) {
|
|
284
325
|
// A window, in the colour of the sky right now.
|
|
285
326
|
rect(c, "#1f2937", x + 1, y + 1, 14, 9);
|
|
286
327
|
rect(c, daylight.sky, x + 2, y + 2, 12, 7);
|
|
@@ -310,6 +351,13 @@ function drawWalls(c: Ctx, world: World, palette: Palette, daylight: Daylight):
|
|
|
310
351
|
function drawLabels(c: Ctx, world: World): void {
|
|
311
352
|
c.font = LABEL_FONT;
|
|
312
353
|
c.textBaseline = "top";
|
|
354
|
+
for (const item of world.furniture) {
|
|
355
|
+
if (!item.label) continue;
|
|
356
|
+
const width = Math.ceil(c.measureText(item.label).width) + 4;
|
|
357
|
+
rect(c, "#f4ecd8", item.x * TILE + 17, item.y * TILE + 3, width, 10);
|
|
358
|
+
c.fillStyle = "#1c1408";
|
|
359
|
+
c.fillText(item.label, item.x * TILE + 19, item.y * TILE + 4.5);
|
|
360
|
+
}
|
|
313
361
|
for (const room of world.rooms) {
|
|
314
362
|
if (!room.name) continue;
|
|
315
363
|
const label = room.name.toUpperCase();
|
|
@@ -328,8 +376,8 @@ export function drawBuilding(c: Ctx, world: World, palette: Palette, daylight: D
|
|
|
328
376
|
rect(c, WALL_TOP, 0, 0, world.cols * TILE, world.rows * TILE);
|
|
329
377
|
for (const room of world.rooms) drawFloor(c, room);
|
|
330
378
|
drawWalls(c, world, palette, daylight);
|
|
331
|
-
for (const item of world.furniture) if (item.type === "rug")
|
|
332
|
-
for (const item of world.furniture) if (
|
|
379
|
+
for (const item of world.furniture) if (item.type === "rug" || item.type === "pile") painterFor(world).furniture(c, item, world, 0, palette);
|
|
380
|
+
for (const item of world.furniture) if (specOf(world, item).wall) painterFor(world).furniture(c, item, world, 0, palette);
|
|
333
381
|
// Light from the windows falling across the floor.
|
|
334
382
|
if (daylight.sun !== "rgba(0,0,0,0)") {
|
|
335
383
|
c.fillStyle = daylight.sun;
|
|
@@ -342,264 +390,6 @@ export function drawBuilding(c: Ctx, world: World, palette: Palette, daylight: D
|
|
|
342
390
|
}
|
|
343
391
|
}
|
|
344
392
|
|
|
345
|
-
function drawDesk(c: Ctx, x: number, y: number, on: boolean, time: number, seed: number, palette: Palette, blocked: boolean): void {
|
|
346
|
-
rect(c, "#55351d", x + 1, y + 22, 46, 8);
|
|
347
|
-
rect(c, "#6b4427", x + 1, y + 13, 46, 10);
|
|
348
|
-
rect(c, "#8a5a33", x + 1, y + 10, 46, 12);
|
|
349
|
-
rect(c, "#a87545", x + 1, y + 10, 46, 2);
|
|
350
|
-
rect(c, "#3b2413", x + 3, y + 22, 3, 9);
|
|
351
|
-
rect(c, "#3b2413", x + 42, y + 22, 3, 9);
|
|
352
|
-
rect(c, "#55351d", x + 30, y + 23, 12, 7);
|
|
353
|
-
rect(c, "#7c5637", x + 31, y + 24, 10, 2);
|
|
354
|
-
rect(c, "#d4a24e", x + 35, y + 24, 2, 1);
|
|
355
|
-
rect(c, "#7c5637", x + 31, y + 27, 10, 2);
|
|
356
|
-
rect(c, "#d4a24e", x + 35, y + 27, 2, 1);
|
|
357
|
-
// Monitor
|
|
358
|
-
rect(c, OUTLINE, x + 14, y - 3, 20, 15);
|
|
359
|
-
rect(c, "#374151", x + 22, y + 12, 4, 2);
|
|
360
|
-
rect(c, "#4b5563", x + 19, y + 14, 10, 1);
|
|
361
|
-
if (blocked) {
|
|
362
|
-
rect(c, "#3f0f14", x + 15, y - 2, 18, 13);
|
|
363
|
-
if (Math.floor(time * 2) % 2 === 0) { rect(c, "#ef4444", x + 23, y, 2, 6); rect(c, "#ef4444", x + 23, y + 7, 2, 2); }
|
|
364
|
-
} else if (on) {
|
|
365
|
-
rect(c, "#0f2a2e", x + 15, y - 2, 18, 13);
|
|
366
|
-
const scroll = Math.floor(time * 3 + (seed % 13));
|
|
367
|
-
for (let line = 0; line < 5; line++) {
|
|
368
|
-
const width = 3 + (((seed >> line) & 7) + (scroll + line) * 7) % 12;
|
|
369
|
-
rect(c, line % 3 === 0 ? palette.primary : line % 3 === 1 ? "#7dd3fc" : "#fcd34d", x + 16 + (line % 2) * 2, y - 1 + line * 2.2, width, 1);
|
|
370
|
-
}
|
|
371
|
-
c.fillStyle = "rgba(125,211,252,0.12)";
|
|
372
|
-
c.fillRect(x + 10, y + 11, 28, 8);
|
|
373
|
-
} else {
|
|
374
|
-
rect(c, "#1f2937", x + 15, y - 2, 18, 13);
|
|
375
|
-
rect(c, "#374151", x + 17, y, 3, 1);
|
|
376
|
-
}
|
|
377
|
-
rect(c, "#d1d5db", x + 16, y + 15, 16, 5);
|
|
378
|
-
for (let row = 0; row < 2; row++) for (let key = 0; key < 7; key++) rect(c, "#9ca3af", x + 17 + key * 2, y + 16 + row * 2, 1, 1);
|
|
379
|
-
rect(c, "#e5e7eb", x + 34, y + 16, 3, 4);
|
|
380
|
-
rect(c, "#f3f4f6", x + 40, y + 13, 5, 5);
|
|
381
|
-
rect(c, palette.primary, x + 40, y + 15, 5, 1);
|
|
382
|
-
rect(c, "#fde68a", x + 4, y + 13, 7, 6);
|
|
383
|
-
rect(c, "#fbbf24", x + 5, y + 15, 5, 1);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function drawBoard(c: Ctx, x: number, y: number, world: World): void {
|
|
387
|
-
rect(c, "#3a2c1e", x, y - 12, 64, 27);
|
|
388
|
-
rect(c, "#d9c29c", x + 2, y - 10, 60, 23);
|
|
389
|
-
for (let i = 0; i < 40; i++) rect(c, "#c9b08a", x + 3 + (i * 37) % 58, y - 9 + (i * 13) % 21, 1, 1);
|
|
390
|
-
const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
|
|
391
|
-
const per = 8;
|
|
392
|
-
world.tasks.slice(0, per * 2).forEach((task, index) => {
|
|
393
|
-
const col = index % per;
|
|
394
|
-
const row = Math.floor(index / per);
|
|
395
|
-
rect(c, "#00000033", x + 5 + col * 7, y - 7 + row * 10, 6, 8);
|
|
396
|
-
rect(c, colors[task.state], x + 4 + col * 7, y - 8 + row * 10, 6, 8);
|
|
397
|
-
rect(c, "#ef4444", x + 6 + col * 7, y - 8 + row * 10, 2, 1);
|
|
398
|
-
rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 10, 4, 1);
|
|
399
|
-
rect(c, "#00000040", x + 5 + col * 7, y - 3 + row * 10, 3, 1);
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function drawFurniture(c: Ctx, item: Item, world: World, time: number, palette: Palette): void {
|
|
404
|
-
const x = item.x * TILE;
|
|
405
|
-
const y = item.y * TILE;
|
|
406
|
-
switch (item.type) {
|
|
407
|
-
case "desk": {
|
|
408
|
-
const seatX = item.x + 1;
|
|
409
|
-
const seatY = item.y + 2;
|
|
410
|
-
const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a));
|
|
411
|
-
drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette, user?.state === "blocked");
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
case "coffee":
|
|
415
|
-
rect(c, "#374151", x, y + 2, 32, 14);
|
|
416
|
-
rect(c, "#6b7280", x, y + 2, 32, 2);
|
|
417
|
-
rect(c, OUTLINE, x + 4, y - 9, 15, 15);
|
|
418
|
-
rect(c, "#27272a", x + 5, y - 8, 13, 13);
|
|
419
|
-
rect(c, Math.floor(time * 2) % 2 ? "#ef4444" : "#7f1d1d", x + 15, y - 6, 2, 2);
|
|
420
|
-
rect(c, "#52525b", x + 7, y - 6, 6, 4);
|
|
421
|
-
rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
|
|
422
|
-
rect(c, "#f3f4f6", x + 23, y - 1, 5, 5);
|
|
423
|
-
rect(c, "#92400e", x + 24, y, 3, 1);
|
|
424
|
-
for (let i = 0; i < 3; i++) {
|
|
425
|
-
c.fillStyle = "rgba(255,255,255,0.5)";
|
|
426
|
-
c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
|
|
427
|
-
}
|
|
428
|
-
break;
|
|
429
|
-
case "water":
|
|
430
|
-
rect(c, "#d1d5db", x + 3, y + 3, 10, 13);
|
|
431
|
-
rect(c, "#9ca3af", x + 3, y + 14, 10, 2);
|
|
432
|
-
rect(c, "#60a5fa", x + 4, y - 8, 8, 11);
|
|
433
|
-
rect(c, "#bae6fd", x + 5, y - 7, 2, 9);
|
|
434
|
-
rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
|
|
435
|
-
rect(c, "#ef4444", x + 9, y + 6, 2, 2);
|
|
436
|
-
if (Math.floor(time * 0.4 + item.x) % 5 === 0) rect(c, "#e0f2fe", x + 8, y - 2 - ((time * 6) % 7), 1, 1);
|
|
437
|
-
break;
|
|
438
|
-
case "sofa":
|
|
439
|
-
rect(c, shade(palette.primary, -60), x + 1, y - 7, 46, 11);
|
|
440
|
-
rect(c, shade(palette.primary, -30), x + 2, y - 6, 44, 3);
|
|
441
|
-
rect(c, "#4b5563", x, y + 3, 48, 11);
|
|
442
|
-
rect(c, "#6b7280", x + 3, y + 3, 20, 7);
|
|
443
|
-
rect(c, "#6b7280", x + 25, y + 3, 20, 7);
|
|
444
|
-
rect(c, "#374151", x, y + 12, 48, 3);
|
|
445
|
-
rect(c, "#374151", x - 1, y - 2, 4, 16);
|
|
446
|
-
rect(c, "#374151", x + 45, y - 2, 4, 16);
|
|
447
|
-
break;
|
|
448
|
-
case "plant":
|
|
449
|
-
rect(c, "#92400e", x + 4, y + 9, 8, 7);
|
|
450
|
-
rect(c, "#b45309", x + 3, y + 8, 10, 2);
|
|
451
|
-
for (const [lx, ly, lw, lh] of [[6, -3, 4, 11], [2, 0, 5, 6], [9, 1, 5, 6], [4, -6, 3, 6], [9, -5, 3, 5]] as const) rect(c, "#15803d", x + lx, y + ly, lw, lh);
|
|
452
|
-
rect(c, "#22c55e", x + 7, y - 3, 2, 8);
|
|
453
|
-
rect(c, "#22c55e", x + 3, y + 1, 2, 3);
|
|
454
|
-
rect(c, "#4ade80", x + 10, y - 4, 1, 3);
|
|
455
|
-
break;
|
|
456
|
-
case "fern":
|
|
457
|
-
rect(c, "#57534e", x + 3, y + 10, 10, 6);
|
|
458
|
-
rect(c, "#78716c", x + 2, y + 9, 12, 2);
|
|
459
|
-
for (let i = 0; i < 7; i++) {
|
|
460
|
-
const lean = i - 3;
|
|
461
|
-
for (let j = 0; j < 5; j++) rect(c, j % 2 ? "#16a34a" : "#15803d", x + 7 + lean * (j / 2 + 1), y + 8 - j * 2 + Math.abs(lean), 2, 2);
|
|
462
|
-
}
|
|
463
|
-
break;
|
|
464
|
-
case "board":
|
|
465
|
-
drawBoard(c, x, y, world);
|
|
466
|
-
break;
|
|
467
|
-
case "whiteboard":
|
|
468
|
-
rect(c, "#9ca3af", x, y - 10, 48, 22);
|
|
469
|
-
rect(c, "#f9fafb", x + 2, y - 8, 44, 18);
|
|
470
|
-
rect(c, "#3b82f6", x + 5, y - 5, 14, 1); rect(c, "#3b82f6", x + 5, y - 2, 9, 1);
|
|
471
|
-
rect(c, "#ef4444", x + 24, y - 6, 8, 6); rect(c, "#f9fafb", x + 25, y - 5, 6, 4);
|
|
472
|
-
rect(c, "#10b981", x + 8, y + 3, 20, 1); rect(c, "#10b981", x + 27, y + 2, 1, 3);
|
|
473
|
-
rect(c, "#6b7280", x + 2, y + 10, 44, 2);
|
|
474
|
-
break;
|
|
475
|
-
case "shelf":
|
|
476
|
-
rect(c, "#5b3a21", x, y - 12, 32, 28);
|
|
477
|
-
for (let shelf = 0; shelf < 2; shelf++) {
|
|
478
|
-
rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
|
|
479
|
-
for (let book = 0; book < 8; book++) {
|
|
480
|
-
const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf + item.x) % 5] as string;
|
|
481
|
-
rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
|
|
482
|
-
rect(c, shade(color, 40), x + 3 + book * 3, y - 6 + shelf * 13, 2, 1);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
break;
|
|
486
|
-
case "server":
|
|
487
|
-
rect(c, OUTLINE, x, y - 13, 16, 29);
|
|
488
|
-
rect(c, "#1f2937", x + 1, y - 12, 14, 27);
|
|
489
|
-
for (let i = 0; i < 5; i++) {
|
|
490
|
-
rect(c, "#111827", x + 2, y - 11 + i * 5, 12, 4);
|
|
491
|
-
const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
|
|
492
|
-
rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
|
|
493
|
-
rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
|
|
494
|
-
rect(c, "#374151", x + 8, y - 10 + i * 5, 5, 1);
|
|
495
|
-
}
|
|
496
|
-
break;
|
|
497
|
-
case "table":
|
|
498
|
-
rect(c, "#44403c", x + 2, y + 16, 44, 12);
|
|
499
|
-
rect(c, "#78716c", x + 2, y + 3, 44, 14);
|
|
500
|
-
rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
|
|
501
|
-
rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
|
|
502
|
-
rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
|
|
503
|
-
rect(c, "#1f2937", x + 12, y + 8, 4, 1);
|
|
504
|
-
rect(c, "#3b82f6", x + 30, y + 10, 4, 1);
|
|
505
|
-
break;
|
|
506
|
-
case "rug":
|
|
507
|
-
rect(c, palette.primary, x, y, 64, 48);
|
|
508
|
-
c.fillStyle = "rgba(0,0,0,0.42)";
|
|
509
|
-
c.fillRect(x + 3, y + 3, 58, 42);
|
|
510
|
-
rect(c, palette.primary, x + 6, y + 6, 52, 1);
|
|
511
|
-
rect(c, palette.primary, x + 6, y + 41, 52, 1);
|
|
512
|
-
rect(c, palette.primary, x + 6, y + 6, 1, 36);
|
|
513
|
-
rect(c, palette.primary, x + 57, y + 6, 1, 36);
|
|
514
|
-
for (let i = 0; i < 6; i++) rect(c, palette.primary, x + 12 + i * 8, y + 22, 4, 4);
|
|
515
|
-
break;
|
|
516
|
-
case "arcade":
|
|
517
|
-
rect(c, OUTLINE, x, y - 14, 16, 30);
|
|
518
|
-
rect(c, "#4c1d95", x + 1, y - 13, 14, 28);
|
|
519
|
-
rect(c, "#111827", x + 3, y - 10, 10, 9);
|
|
520
|
-
rect(c, ["#22c55e", "#ec4899", "#38bdf8"][Math.floor(time * 2) % 3] as string, x + 4 + (Math.floor(time * 4) % 7), y - 8 + (Math.floor(time * 3) % 5), 2, 2);
|
|
521
|
-
rect(c, "#f59e0b", x + 3, y - 13, 10, 2);
|
|
522
|
-
rect(c, "#27272a", x + 2, y + 1, 12, 4);
|
|
523
|
-
rect(c, "#ef4444", x + 4, y + 2, 2, 2);
|
|
524
|
-
rect(c, "#3b82f6", x + 9, y + 2, 2, 2);
|
|
525
|
-
break;
|
|
526
|
-
case "fridge":
|
|
527
|
-
rect(c, OUTLINE, x + 1, y - 12, 14, 28);
|
|
528
|
-
rect(c, "#e5e7eb", x + 2, y - 11, 12, 26);
|
|
529
|
-
rect(c, "#9ca3af", x + 2, y - 3, 12, 1);
|
|
530
|
-
rect(c, "#6b7280", x + 11, y - 8, 1, 3);
|
|
531
|
-
rect(c, "#6b7280", x + 11, y, 1, 5);
|
|
532
|
-
rect(c, "#fde68a", x + 4, y - 9, 3, 3);
|
|
533
|
-
rect(c, "#fca5a5", x + 5, y + 3, 3, 3);
|
|
534
|
-
break;
|
|
535
|
-
case "printer":
|
|
536
|
-
rect(c, "#4b5563", x + 2, y + 8, 12, 8);
|
|
537
|
-
rect(c, OUTLINE, x + 1, y - 2, 14, 11);
|
|
538
|
-
rect(c, "#d1d5db", x + 2, y - 1, 12, 9);
|
|
539
|
-
rect(c, "#f9fafb", x + 4, y - 5 - (Math.floor(time) % 4 === 0 ? 1 : 0), 8, 5);
|
|
540
|
-
rect(c, "#22c55e", x + 11, y + 1, 2, 1);
|
|
541
|
-
rect(c, "#9ca3af", x + 3, y + 5, 10, 1);
|
|
542
|
-
break;
|
|
543
|
-
case "lamp":
|
|
544
|
-
rect(c, "#374151", x + 5, y + 13, 6, 3);
|
|
545
|
-
rect(c, "#4b5563", x + 7, y - 4, 2, 17);
|
|
546
|
-
rect(c, OUTLINE, x + 2, y - 10, 12, 8);
|
|
547
|
-
rect(c, "#fde68a", x + 3, y - 9, 10, 6);
|
|
548
|
-
rect(c, "#fef3c7", x + 4, y - 8, 4, 2);
|
|
549
|
-
break;
|
|
550
|
-
case "beanbag":
|
|
551
|
-
rect(c, OUTLINE, x, y + 3, 16, 13);
|
|
552
|
-
rect(c, shade(palette.primary, -50), x + 1, y + 4, 14, 11);
|
|
553
|
-
rect(c, shade(palette.primary, -20), x + 3, y + 5, 8, 4);
|
|
554
|
-
rect(c, "#00000040", x + 1, y + 13, 14, 2);
|
|
555
|
-
break;
|
|
556
|
-
case "tv":
|
|
557
|
-
rect(c, "#3b2413", x + 2, y + 8, 44, 8);
|
|
558
|
-
rect(c, "#5b3a21", x + 2, y + 8, 44, 2);
|
|
559
|
-
rect(c, OUTLINE, x + 4, y - 14, 40, 22);
|
|
560
|
-
rect(c, "#0b1120", x + 5, y - 13, 38, 20);
|
|
561
|
-
for (let i = 0; i < 9; i++) {
|
|
562
|
-
const h = 2 + ((i * 5 + Math.floor(time * 3)) % 12);
|
|
563
|
-
rect(c, i % 3 === 0 ? palette.primary : i % 3 === 1 ? "#38bdf8" : "#f472b6", x + 8 + i * 4, y + 5 - h, 3, h);
|
|
564
|
-
}
|
|
565
|
-
break;
|
|
566
|
-
case "vending":
|
|
567
|
-
rect(c, OUTLINE, x, y - 14, 16, 30);
|
|
568
|
-
rect(c, "#b91c1c", x + 1, y - 13, 14, 28);
|
|
569
|
-
rect(c, "#0f172a", x + 3, y - 11, 8, 16);
|
|
570
|
-
for (let row = 0; row < 3; row++) for (let col = 0; col < 2; col++) rect(c, ["#fbbf24", "#34d399", "#60a5fa", "#f472b6"][(row + col * 2) % 4] as string, x + 4 + col * 3, y - 10 + row * 5, 2, 4);
|
|
571
|
-
rect(c, "#d1d5db", x + 12, y - 9, 2, 6);
|
|
572
|
-
rect(c, "#111827", x + 3, y + 8, 10, 4);
|
|
573
|
-
break;
|
|
574
|
-
case "trophy":
|
|
575
|
-
rect(c, "#6b4427", x + 2, y + 8, 12, 8);
|
|
576
|
-
rect(c, "#8a5a33", x + 2, y + 8, 12, 2);
|
|
577
|
-
rect(c, "#78350f", x + 5, y + 5, 6, 3);
|
|
578
|
-
rect(c, "#f59e0b", x + 7, y + 1, 2, 4);
|
|
579
|
-
rect(c, OUTLINE, x + 3, y - 8, 10, 10);
|
|
580
|
-
rect(c, "#fbbf24", x + 4, y - 7, 8, 8);
|
|
581
|
-
rect(c, "#fde68a", x + 5, y - 6, 2, 5);
|
|
582
|
-
rect(c, "#fbbf24", x + 1, y - 6, 3, 1); rect(c, "#fbbf24", x + 12, y - 6, 3, 1);
|
|
583
|
-
break;
|
|
584
|
-
case "clock": {
|
|
585
|
-
rect(c, OUTLINE, x + 3, y + 1, 10, 10);
|
|
586
|
-
rect(c, "#f9fafb", x + 4, y + 2, 8, 8);
|
|
587
|
-
rect(c, "#1f2937", x + 8, y + 3, 1, 3);
|
|
588
|
-
rect(c, "#1f2937", x + 8, y + 6, 3, 1);
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
591
|
-
case "poster": {
|
|
592
|
-
const colors = ["#7c3aed", "#0ea5e9", "#db2777", "#059669"];
|
|
593
|
-
const color = colors[(item.x + item.y) % colors.length] as string;
|
|
594
|
-
rect(c, OUTLINE, x + 2, y, 12, 11);
|
|
595
|
-
rect(c, color, x + 3, y + 1, 10, 9);
|
|
596
|
-
rect(c, shade(color, 60), x + 5, y + 3, 6, 3);
|
|
597
|
-
rect(c, "#f9fafb", x + 5, y + 7, 6, 1);
|
|
598
|
-
break;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
393
|
function nameTag(c: Ctx, agent: Agent, selected: boolean, width: number): void {
|
|
604
394
|
c.font = LABEL_FONT;
|
|
605
395
|
c.textBaseline = "top";
|
|
@@ -661,7 +451,7 @@ function drawProgress(c: Ctx, world: World, palette: Palette): void {
|
|
|
661
451
|
const w = 96;
|
|
662
452
|
rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
|
|
663
453
|
c.fillStyle = "#ffffff";
|
|
664
|
-
c.fillText(
|
|
454
|
+
c.fillText(`${world.pack.words.quests} ${done}/${world.tasks.length}`, 8, 6.5);
|
|
665
455
|
rect(c, "#374151", 8, 16, w - 8, 4);
|
|
666
456
|
rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
|
|
667
457
|
}
|
|
@@ -681,11 +471,18 @@ export function drawWorld(
|
|
|
681
471
|
if (building) c.drawImage(building, 0, 0, width, height);
|
|
682
472
|
else drawBuilding(c, world, palette, daylight);
|
|
683
473
|
|
|
474
|
+
// On the floor, so people and furniture stay in front of the lines.
|
|
475
|
+
if (world.showLinks && world.links.length > 0) {
|
|
476
|
+
world.routes ??= linkRoutes(world);
|
|
477
|
+
drawLinks(c, world.routes, time);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const painter = painterFor(world);
|
|
684
481
|
const scene: { depth: number; draw: () => void }[] = [];
|
|
685
482
|
for (const item of world.furniture) {
|
|
686
|
-
const spec =
|
|
483
|
+
const spec = specOf(world, item);
|
|
687
484
|
if (spec.solid === false) continue;
|
|
688
|
-
scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () =>
|
|
485
|
+
scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => painterFor(world).furniture(c, item, world, time, palette) });
|
|
689
486
|
}
|
|
690
487
|
const shown = world.agents.filter((agent) => agent.state !== "away");
|
|
691
488
|
for (const agent of shown) scene.push({ depth: agent.y * TILE + 14, draw: () => drawAgent(c, agent, time, agent.id === selected) });
|
|
@@ -705,13 +502,7 @@ export function drawWorld(
|
|
|
705
502
|
c.fillStyle = gradient;
|
|
706
503
|
c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
707
504
|
};
|
|
708
|
-
for (const item of world.furniture)
|
|
709
|
-
if (item.type === "lamp") glow(item.x * TILE + 8, item.y * TILE, 36, "rgba(253,224,139,0.30)");
|
|
710
|
-
if (item.type === "arcade" || item.type === "vending" || item.type === "tv") glow(item.x * TILE + 8, item.y * TILE, 26, "rgba(125,211,252,0.16)");
|
|
711
|
-
if (item.type === "desk" && world.agents.some((a) => a.seat?.x === item.x + 1 && a.seat?.y === item.y + 2 && isSeated(a))) {
|
|
712
|
-
glow(item.x * TILE + 24, item.y * TILE + 8, 30, "rgba(125,211,252,0.26)");
|
|
713
|
-
}
|
|
714
|
-
}
|
|
505
|
+
for (const item of world.furniture) painter.glow(item, world, glow);
|
|
715
506
|
c.restore();
|
|
716
507
|
}
|
|
717
508
|
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,9 @@ import { createOffice, type OfficeHandle } from "./office.js";
|
|
|
13
13
|
import type { OfficeScene } from "./types.js";
|
|
14
14
|
|
|
15
15
|
export { createOffice } from "./office.js";
|
|
16
|
+
export { registerPainter, type Painter } from "./draw.js";
|
|
17
|
+
export { drawLinks, linkRoutes, type LinkRoute } from "./links.js";
|
|
18
|
+
export { registerWorld, type ItemSpec, type WorldData } from "./pack.js";
|
|
16
19
|
export type { OfficeHandle, OfficeOptions } from "./office.js";
|
|
17
20
|
export * from "./types.js";
|
|
18
21
|
export { FURNITURE, agentAt, applyLayout, applyUpdate, createWorld, findPath, step } from "./world.js";
|
package/src/links.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** How the code depends on itself, drawn as lines along the routes agents really walk. */
|
|
2
|
+
|
|
3
|
+
import { TILE, type Ctx } from "./paint.js";
|
|
4
|
+
import { findPath, isWalkable, type World } from "./world.js";
|
|
5
|
+
|
|
6
|
+
export interface LinkRoute { from: string; to: string; points: { x: number; y: number }[] }
|
|
7
|
+
|
|
8
|
+
const COLOURS = ["#38bdf8", "#f472b6", "#fbbf24", "#a78bfa", "#34d399", "#fb7185"];
|
|
9
|
+
|
|
10
|
+
/** A tile someone can stand on, as near the middle of the room as possible. */
|
|
11
|
+
function standingTile(world: World, roomId: string): { x: number; y: number } | undefined {
|
|
12
|
+
const room = world.rooms.find((candidate) => candidate.id === roomId);
|
|
13
|
+
if (!room) return undefined;
|
|
14
|
+
const cx = room.x + (room.w >> 1);
|
|
15
|
+
const cy = room.y + (room.h >> 1);
|
|
16
|
+
let best: { x: number; y: number } | undefined;
|
|
17
|
+
let bestDistance = Infinity;
|
|
18
|
+
for (let y = room.y; y < room.y + room.h; y++) {
|
|
19
|
+
for (let x = room.x; x < room.x + room.w; x++) {
|
|
20
|
+
const distance = Math.abs(x - cx) + Math.abs(y - cy);
|
|
21
|
+
if (distance < bestDistance && isWalkable(world, x, y)) {
|
|
22
|
+
best = { x, y };
|
|
23
|
+
bestDistance = distance;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Each link's walking route. A link whose rooms aren't on the map, or can't be reached, is left out. */
|
|
31
|
+
export function linkRoutes(world: World): LinkRoute[] {
|
|
32
|
+
const routes: LinkRoute[] = [];
|
|
33
|
+
for (const link of world.links) {
|
|
34
|
+
const start = standingTile(world, link.from);
|
|
35
|
+
const end = standingTile(world, link.to);
|
|
36
|
+
if (!start || !end) continue;
|
|
37
|
+
const path = findPath(world, start, end);
|
|
38
|
+
if (path && path.length > 0) routes.push({ from: link.from, to: link.to, points: [start, ...path] });
|
|
39
|
+
}
|
|
40
|
+
return routes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Lines that move towards what is depended on. Shared stretches sit side by side. */
|
|
44
|
+
export function drawLinks(c: Ctx, routes: LinkRoute[], time: number): void {
|
|
45
|
+
routes.forEach((route, index) => {
|
|
46
|
+
const colour = COLOURS[index % COLOURS.length] as string;
|
|
47
|
+
const offset = ((index % 5) - 2) * 2;
|
|
48
|
+
const at = (point: { x: number; y: number }): [number, number] => [point.x * TILE + 8 + offset, point.y * TILE + 8 + offset];
|
|
49
|
+
c.save();
|
|
50
|
+
c.lineWidth = 2;
|
|
51
|
+
c.strokeStyle = colour;
|
|
52
|
+
c.globalAlpha = 0.85;
|
|
53
|
+
c.setLineDash([6, 5]);
|
|
54
|
+
c.lineDashOffset = -time * 14;
|
|
55
|
+
c.beginPath();
|
|
56
|
+
route.points.forEach((point, i) => {
|
|
57
|
+
const [x, y] = at(point);
|
|
58
|
+
if (i === 0) c.moveTo(x, y);
|
|
59
|
+
else c.lineTo(x, y);
|
|
60
|
+
});
|
|
61
|
+
c.stroke();
|
|
62
|
+
c.setLineDash([]);
|
|
63
|
+
const first = route.points[0];
|
|
64
|
+
const last = route.points[route.points.length - 1];
|
|
65
|
+
if (first && last) {
|
|
66
|
+
const [sx, sy] = at(first);
|
|
67
|
+
const [ex, ey] = at(last);
|
|
68
|
+
c.fillStyle = colour;
|
|
69
|
+
c.fillRect(sx - 2, sy - 2, 4, 4);
|
|
70
|
+
c.beginPath();
|
|
71
|
+
c.arc(ex, ey, 4, 0, Math.PI * 2);
|
|
72
|
+
c.fill();
|
|
73
|
+
}
|
|
74
|
+
c.restore();
|
|
75
|
+
});
|
|
76
|
+
}
|
package/src/office.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface OfficeHandle {
|
|
|
10
10
|
update(scene: Pick<OfficeScene, "agents" | "tasks">): void;
|
|
11
11
|
/** Change the building while everyone is in it. */
|
|
12
12
|
setLayout(layout: OfficeLayout): void;
|
|
13
|
+
/** Show or hide the lines between rooms whose code is linked. */
|
|
14
|
+
setLinksVisible(on: boolean): void;
|
|
13
15
|
/** Pick an agent by id, or pass nothing to clear. */
|
|
14
16
|
select(id?: string): void;
|
|
15
17
|
/** The tile under a pointer event, allowing for the camera. */
|
|
@@ -74,8 +76,10 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
74
76
|
const width = (): number => world.cols * TILE;
|
|
75
77
|
const height = (): number => world.rows * TILE;
|
|
76
78
|
|
|
79
|
+
// A big map starts small on screen, so it can be zoomed further: as far as 4x true size.
|
|
80
|
+
const zoomLimit = (): number => Math.max(MAX_ZOOM, scale > 0 ? 4 / scale : MAX_ZOOM);
|
|
77
81
|
const clampView = (): void => {
|
|
78
|
-
view.zoom = Math.min(
|
|
82
|
+
view.zoom = Math.min(zoomLimit(), Math.max(1, view.zoom));
|
|
79
83
|
view.x = Math.min(width() - width() / view.zoom, Math.max(0, view.x));
|
|
80
84
|
view.y = Math.min(height() - height() / view.zoom, Math.max(0, view.y));
|
|
81
85
|
};
|
|
@@ -190,7 +194,7 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
190
194
|
return;
|
|
191
195
|
}
|
|
192
196
|
const before = event ? tileAt(event) : { x: view.x / TILE + width() / view.zoom / TILE / 2, y: view.y / TILE + height() / view.zoom / TILE / 2 };
|
|
193
|
-
view.zoom = Math.min(
|
|
197
|
+
view.zoom = Math.min(zoomLimit(), Math.max(1, view.zoom * factor));
|
|
194
198
|
if (event) {
|
|
195
199
|
const after = tileAt(event);
|
|
196
200
|
view.x += (before.x - after.x) * TILE;
|
|
@@ -231,7 +235,7 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
231
235
|
setLayout(layout) {
|
|
232
236
|
applyLayout(world, layout);
|
|
233
237
|
buildingDirty = true;
|
|
234
|
-
view.zoom = Math.min(view.zoom,
|
|
238
|
+
view.zoom = Math.min(view.zoom, zoomLimit());
|
|
235
239
|
clampView();
|
|
236
240
|
scale = 0;
|
|
237
241
|
resize();
|
|
@@ -239,6 +243,7 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
239
243
|
select,
|
|
240
244
|
tileAt,
|
|
241
245
|
setOverlay(draw) { overlay = draw; },
|
|
246
|
+
setLinksVisible(on) { world.showLinks = on; if (still) paint(); },
|
|
242
247
|
setInteractive(on) { interactive = on; drag = undefined; if (!on) canvas.style.cursor = "crosshair"; },
|
|
243
248
|
zoom,
|
|
244
249
|
destroy() {
|
package/src/pack.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What makes one world different from another. The engine handles walking, seats, time of day
|
|
3
|
+
* and agents, for any world. A world brings this data, plus a painter (see registerPainter).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OfficeLayout } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export interface ItemSpec {
|
|
9
|
+
w: number;
|
|
10
|
+
h: number;
|
|
11
|
+
solid?: boolean;
|
|
12
|
+
wall?: boolean;
|
|
13
|
+
/** Where someone stands to use it, relative to its corner. */
|
|
14
|
+
spot?: { x: number; y: number };
|
|
15
|
+
/** seat: where an agent works. lounge: where it rests. board: where it reviews. */
|
|
16
|
+
use?: "seat" | "lounge" | "board";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorldData {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
words: {
|
|
23
|
+
/** The progress label over the map. */
|
|
24
|
+
quests: string;
|
|
25
|
+
};
|
|
26
|
+
items: Record<string, ItemSpec>;
|
|
27
|
+
layout: Required<Pick<OfficeLayout, "cols" | "rows" | "rooms" | "doors" | "furniture">>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const worlds = new Map<string, WorldData>();
|
|
31
|
+
|
|
32
|
+
export function registerWorld(data: WorldData): void {
|
|
33
|
+
worlds.set(data.id, data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const worldData = (id: string | undefined, fallback: WorldData): WorldData => (id && worlds.get(id)) || fallback;
|
package/src/paint.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The small tools every painter uses. */
|
|
2
|
+
|
|
3
|
+
export const TILE = 16;
|
|
4
|
+
|
|
5
|
+
export interface Palette { primary: string; background: string; text: string; accent: string }
|
|
6
|
+
|
|
7
|
+
export type Ctx = CanvasRenderingContext2D;
|
|
8
|
+
|
|
9
|
+
export const LABEL_FONT = "bold 7px ui-monospace, Menlo, Consolas, monospace";
|
|
10
|
+
export const BODY_FONT = "7px ui-monospace, Menlo, Consolas, monospace";
|
|
11
|
+
export const OUTLINE = "#14141f";
|
|
12
|
+
|
|
13
|
+
export const rect = (c: Ctx, color: string, x: number, y: number, w: number, h: number): void => {
|
|
14
|
+
c.fillStyle = color;
|
|
15
|
+
c.fillRect(Math.round(x), Math.round(y), w, h);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function shade(color: string, amount: number): string {
|
|
19
|
+
if (!color.startsWith("#")) return color;
|
|
20
|
+
const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
|
|
21
|
+
const n = parseInt(hex.slice(1, 7), 16);
|
|
22
|
+
const channel = (shift: number): number => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
|
|
23
|
+
return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
|
|
24
|
+
}
|