@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/src/world.ts CHANGED
@@ -3,57 +3,32 @@
3
3
  * heading. Nothing in here touches a canvas, so all of it can be tested.
4
4
  */
5
5
 
6
- import type { AgentState, FloorStyle, FurnitureType, OfficeAgent, OfficeDoor, OfficeFurniture, OfficeRoom, OfficeScene, OfficeTask, TaskState } from "./types.js";
6
+ import { registerWorld, worldData, type ItemSpec, type WorldData } from "./pack.js";
7
+ import { OFFICE, OFFICE_ITEMS, OFFICE_LAYOUT } from "./worlds/office-data.js";
8
+ import type { LinkRoute } from "./links.js";
9
+ import type { AgentState, FloorStyle, FurnitureType, OfficeAgent, OfficeDoor, OfficeFurniture, OfficeLayout, OfficeRoom, OfficeScene, OfficeTask, TaskState } from "./types.js";
7
10
 
8
11
  export const MAX_COLS = 64;
9
12
  export const MAX_ROWS = 40;
10
13
  const MAX_AGENTS = 40;
11
14
  const MAX_FURNITURE = 300;
12
15
  const MAX_TASKS = 60;
13
- export const WALK_SPEED = 3.2; // tiles a second
14
-
15
- /** Footprint in tiles, and where someone stands to use it, relative to its corner. */
16
- export const FURNITURE: Record<FurnitureType, { w: number; h: number; solid?: boolean; spot?: { x: number; y: number } }> = {
17
- desk: { w: 3, h: 2, spot: { x: 1, y: 2 } },
18
- coffee: { w: 2, h: 1, spot: { x: 0, y: 1 } },
19
- water: { w: 1, h: 1, spot: { x: 0, y: 1 } },
20
- sofa: { w: 3, h: 1, spot: { x: 1, y: 1 } },
21
- plant: { w: 1, h: 1 },
22
- board: { w: 4, h: 1, spot: { x: 1, y: 1 } },
23
- shelf: { w: 2, h: 1, spot: { x: 0, y: 1 } },
24
- server: { w: 1, h: 1 },
25
- table: { w: 3, h: 2, spot: { x: 1, y: 2 } },
26
- rug: { w: 4, h: 3, solid: false },
27
- };
16
+ export const WALK_SPEED = 4.5; // tiles a second
17
+
18
+ /** Footprints and uses, for the office. Other worlds bring their own. */
19
+ export const FURNITURE = OFFICE_ITEMS;
28
20
 
29
21
  const AGENT_STATES: readonly string[] = ["working", "thinking", "blocked", "reviewing", "idle", "done", "away"];
30
22
  const TASK_STATES: readonly string[] = ["todo", "doing", "done"];
31
- const FLOORS: readonly string[] = ["wood", "carpet", "tile"];
32
-
33
- export const DEFAULT_LAYOUT = {
34
- cols: 26,
35
- rows: 17,
36
- rooms: [
37
- { id: "studio", name: "Studio", x: 1, y: 3, w: 15, h: 13, floor: "wood" },
38
- { id: "lounge", name: "Lounge", x: 17, y: 3, w: 8, h: 6, floor: "carpet" },
39
- { id: "quests", name: "Quests", x: 17, y: 10, w: 8, h: 6, floor: "tile" },
40
- ] as OfficeRoom[],
41
- doors: [{ x: 16, y: 6 }, { x: 16, y: 13 }, { x: 23, y: 9 }] as OfficeDoor[],
42
- furniture: [
43
- { type: "desk", x: 2, y: 4 }, { type: "desk", x: 7, y: 4 }, { type: "desk", x: 12, y: 4 },
44
- { type: "desk", x: 2, y: 10 }, { type: "desk", x: 7, y: 10 }, { type: "desk", x: 12, y: 10 },
45
- { type: "plant", x: 1, y: 8 }, { type: "plant", x: 15, y: 8 }, { type: "server", x: 15, y: 15 }, { type: "server", x: 14, y: 15 },
46
- { type: "shelf", x: 1, y: 15 },
47
- { type: "coffee", x: 19, y: 3 }, { type: "water", x: 21, y: 3 }, { type: "sofa", x: 22, y: 3 }, { type: "rug", x: 20, y: 5 }, { type: "plant", x: 24, y: 8 },
48
- { type: "board", x: 19, y: 10 }, { type: "table", x: 19, y: 13 }, { type: "plant", x: 24, y: 15 },
49
- ] as OfficeFurniture[],
50
- };
23
+ const FLOORS: readonly string[] = ["wood", "carpet", "tile", "stone", "concrete", "grass", "hall"];
24
+
25
+ export const DEFAULT_LAYOUT = OFFICE_LAYOUT;
51
26
 
52
27
  const int = (value: unknown, fallback: number): number => (typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback);
53
28
  const str = (value: unknown, max = 120): string => (typeof value === "string" ? value.slice(0, max) : "");
54
29
  const list = <T>(value: unknown, max: number): T[] => (Array.isArray(value) ? (value.slice(0, max) as T[]) : []);
55
30
 
56
- export interface Room { name: string; x: number; y: number; w: number; h: number; floor: FloorStyle }
31
+ export interface Room { id: string; name: string; x: number; y: number; w: number; h: number; floor: FloorStyle; paths: string[] }
57
32
  export interface Item { type: FurnitureType; x: number; y: number }
58
33
  export interface Task { title: string; state: TaskState; agent: string }
59
34
 
@@ -63,6 +38,9 @@ export interface Agent {
63
38
  role: string;
64
39
  state: AgentState;
65
40
  task: string;
41
+ area: string;
42
+ /** Seconds of celebration left after finishing something. */
43
+ party: number;
66
44
  hair: string;
67
45
  skin: string;
68
46
  shirt: string;
@@ -79,6 +57,12 @@ export interface Agent {
79
57
  }
80
58
 
81
59
  export interface World {
60
+ /** What this world is made of. */
61
+ pack: WorldData;
62
+ links: { from: string; to: string }[];
63
+ /** Worked out when first drawn, and again when the building changes. */
64
+ routes?: LinkRoute[];
65
+ showLinks: boolean;
82
66
  cols: number;
83
67
  rows: number;
84
68
  rooms: Room[];
@@ -88,6 +72,7 @@ export interface World {
88
72
  tasks: Task[];
89
73
  walkable: boolean[];
90
74
  spots: { lounge: { x: number; y: number }[]; board: { x: number; y: number }[] };
75
+ seats: { x: number; y: number }[];
91
76
  }
92
77
 
93
78
  export function hash(text: string): number {
@@ -152,6 +137,8 @@ function toAgent(raw: OfficeAgent, index: number): Agent {
152
137
  role: str(raw?.role, 40),
153
138
  state,
154
139
  task: str(raw?.task, 120),
140
+ area: str(raw?.area, 60),
141
+ party: 0,
155
142
  hair: colour(raw?.hair, HAIR[seed % HAIR.length] as string),
156
143
  skin: colour(raw?.skin, SKIN[(seed >>> 4) % SKIN.length] as string),
157
144
  shirt: colour(raw?.shirt, SHIRT[(seed >>> 8) % SHIRT.length] as string),
@@ -169,25 +156,31 @@ function toTask(raw: OfficeTask): Task {
169
156
  return { title: str(raw?.title, 80), state: TASK_STATES.includes(raw?.state as string) ? (raw.state as TaskState) : "todo", agent: str(raw?.agent, 60) };
170
157
  }
171
158
 
172
- /** Build a world from a scene. Bad values are dropped or clamped, so it never throws. */
173
- export function createWorld(scene: OfficeScene = {}): World {
159
+ registerWorld(OFFICE);
160
+
161
+ /** A piece of furniture's spec. Only items in the world's list get into a World. */
162
+ export const specOf = (world: Pick<World, "pack">, item: Item): ItemSpec => world.pack.items[item.type] as ItemSpec;
163
+
164
+ function buildLayout(scene: OfficeLayout, pack: WorldData): Pick<World, "links" | "cols" | "rows" | "rooms" | "doors" | "furniture" | "walkable" | "spots" | "seats"> {
174
165
  const custom = Array.isArray(scene?.rooms) && scene.rooms.length > 0;
175
- const cols = Math.min(Math.max(int(scene?.cols, custom ? 20 : DEFAULT_LAYOUT.cols), 6), MAX_COLS);
176
- const rows = Math.min(Math.max(int(scene?.rows, custom ? 14 : DEFAULT_LAYOUT.rows), 6), MAX_ROWS);
177
- const rawRooms = custom ? list<OfficeRoom>(scene.rooms, 40) : DEFAULT_LAYOUT.rooms;
178
- const rawDoors = custom ? list<OfficeDoor>(scene.doors, 60) : DEFAULT_LAYOUT.doors;
179
- const rawItems = Array.isArray(scene?.furniture) ? list<OfficeFurniture>(scene.furniture, MAX_FURNITURE) : custom ? [] : DEFAULT_LAYOUT.furniture;
166
+ const cols = Math.min(Math.max(int(scene?.cols, custom ? 20 : pack.layout.cols), 6), MAX_COLS);
167
+ const rows = Math.min(Math.max(int(scene?.rows, custom ? 14 : pack.layout.rows), 6), MAX_ROWS);
168
+ const rawRooms = custom ? list<OfficeRoom>(scene.rooms, 40) : pack.layout.rooms;
169
+ const rawDoors = custom ? list<OfficeDoor>(scene.doors, 60) : pack.layout.doors;
170
+ const rawItems = Array.isArray(scene?.furniture) ? list<OfficeFurniture>(scene.furniture, MAX_FURNITURE) : custom ? [] : pack.layout.furniture;
180
171
 
181
172
  // Clamped to the grid: everything later loops over these, so a huge room must not hang the page.
182
- const rooms: Room[] = rawRooms.map((room) => {
173
+ const rooms: Room[] = rawRooms.map((room, index) => {
183
174
  const x = Math.min(Math.max(int(room?.x, 0), 0), cols);
184
175
  const y = Math.min(Math.max(int(room?.y, 0), 0), rows);
185
176
  return {
177
+ id: str(room?.id, 60) || `room-${index + 1}`,
186
178
  name: str(room?.name, 24),
187
179
  x, y,
188
180
  w: Math.min(Math.max(int(room?.w, 0), 0), cols - x),
189
181
  h: Math.min(Math.max(int(room?.h, 0), 0), rows - y),
190
182
  floor: FLOORS.includes(room?.floor as string) ? (room.floor as FloorStyle) : "wood",
183
+ paths: list<string>(room?.paths, 20).filter((p) => typeof p === "string").map((p) => p.slice(0, 200)),
191
184
  };
192
185
  });
193
186
  const walkable = new Array<boolean>(cols * rows).fill(false);
@@ -201,56 +194,131 @@ export function createWorld(scene: OfficeScene = {}): World {
201
194
  for (const door of doors) mark(door.x, door.y, true);
202
195
 
203
196
  const furniture: Item[] = rawItems
204
- .filter((item) => item && Object.hasOwn(FURNITURE, item.type))
197
+ .filter((item) => item && Object.hasOwn(pack.items, item.type))
205
198
  .map((item) => ({ type: item.type, x: int(item.x, -99), y: int(item.y, -99) }));
206
199
  for (const item of furniture) {
207
- const spec = FURNITURE[item.type];
200
+ const spec = pack.items[item.type] as ItemSpec;
208
201
  if (spec.solid === false) continue;
209
202
  for (let y = item.y; y < item.y + spec.h; y++) for (let x = item.x; x < item.x + spec.w; x++) mark(x, y, false);
210
203
  }
211
204
 
212
- const world: World = {
213
- cols, rows, rooms, doors, furniture, walkable,
214
- agents: list<OfficeAgent>(scene?.agents, MAX_AGENTS).map(toAgent),
215
- tasks: list<OfficeTask>(scene?.tasks, MAX_TASKS).map(toTask),
216
- spots: { lounge: [], board: [] },
217
- };
218
-
205
+ const open = (x: number, y: number): boolean => x >= 0 && y >= 0 && x < cols && y < rows && walkable[y * cols + x] === true;
219
206
  const spot = (item: Item): { x: number; y: number } | undefined => {
220
- const offset = FURNITURE[item.type].spot;
207
+ const offset = (pack.items[item.type] as ItemSpec).spot;
221
208
  if (!offset) return undefined;
222
209
  const point = { x: item.x + offset.x, y: item.y + offset.y };
223
- return isWalkable(world, point.x, point.y) ? point : undefined;
210
+ return open(point.x, point.y) ? point : undefined;
224
211
  };
225
- const seats = furniture.filter((item) => item.type === "desk").map(spot).filter((p): p is { x: number; y: number } => Boolean(p));
212
+ const seats = furniture.filter((item) => (pack.items[item.type] as ItemSpec).use === "seat").map(spot).filter((p): p is { x: number; y: number } => Boolean(p));
213
+ const spots: World["spots"] = { lounge: [], board: [] };
226
214
  for (const item of furniture) {
227
215
  const point = spot(item);
228
216
  if (!point) continue;
229
- if (item.type === "coffee" || item.type === "sofa" || item.type === "water" || item.type === "shelf") world.spots.lounge.push(point);
230
- if (item.type === "board" || item.type === "table") world.spots.board.push(point);
217
+ const use = (pack.items[item.type] as ItemSpec).use;
218
+ if (use === "lounge") spots.lounge.push(point);
219
+ if (use === "board") spots.board.push(point);
231
220
  }
221
+ const links = list<{ from: string; to: string }>(scene?.links, 60)
222
+ .map((link) => ({ from: str(link?.from, 60), to: str(link?.to, 60) }))
223
+ .filter((link) => link.from && link.to);
224
+ return { cols, rows, rooms, doors, furniture, walkable, seats, spots, links };
225
+ }
232
226
 
233
- world.agents.forEach((agent, index) => {
234
- agent.seat = seats[index];
235
- placeAgent(world, agent);
236
- });
227
+ /** Build a world from a scene. Bad values are dropped or clamped, so it never throws. */
228
+ export function createWorld(scene: OfficeScene = {}): World {
229
+ const pack = worldData(scene?.world, OFFICE);
230
+ const world: World = {
231
+ pack,
232
+ showLinks: true,
233
+ ...buildLayout(scene, pack),
234
+ agents: list<OfficeAgent>(scene?.agents, MAX_AGENTS).map(toAgent),
235
+ tasks: list<OfficeTask>(scene?.tasks, MAX_TASKS).map(toTask),
236
+ };
237
+ for (const agent of world.agents) placeAgent(world, agent);
237
238
  return world;
238
239
  }
239
240
 
241
+ /** Change the building while everyone is in it. Anyone left inside a wall or a desk moves. */
242
+ export function applyLayout(world: World, layout: OfficeLayout): void {
243
+ Object.assign(world, buildLayout(layout, world.pack), { routes: undefined });
244
+ for (const agent of world.agents) {
245
+ agent.seat = undefined;
246
+ agent.path = [];
247
+ agent.x = Math.round(agent.x);
248
+ agent.y = Math.round(agent.y);
249
+ if (!isWalkable(world, agent.x, agent.y)) {
250
+ placeAgent(world, agent);
251
+ } else {
252
+ updateSeat(world, agent);
253
+ routeTo(world, agent, goalFor(world, agent));
254
+ }
255
+ }
256
+ }
257
+
240
258
  function firstWalkable(world: World): { x: number; y: number } {
241
259
  for (let y = 0; y < world.rows; y++) for (let x = 0; x < world.cols; x++) if (isWalkable(world, x, y)) return { x, y };
242
260
  return { x: 0, y: 0 };
243
261
  }
244
262
 
245
- /** Where this agent's current state wants them to be. */
263
+ const NEEDS_DESK: readonly AgentState[] = ["working", "thinking", "blocked"];
264
+
265
+ /** Desks belong to whoever is working at one: claimed when an agent starts, freed when it stops. */
266
+ function updateSeat(world: World, agent: Agent): void {
267
+ if (!NEEDS_DESK.includes(agent.state)) {
268
+ agent.seat = undefined;
269
+ return;
270
+ }
271
+ const room = world.rooms.find((candidate) => candidate.id === agent.area);
272
+ const inRoom = (seat: { x: number; y: number }): boolean =>
273
+ Boolean(room) && seat.x >= (room as Room).x && seat.x < (room as Room).x + (room as Room).w && seat.y >= (room as Room).y && seat.y < (room as Room).y + (room as Room).h;
274
+ // Moved to another room: this desk is no longer the right one.
275
+ if (agent.seat && (!room || inRoom(agent.seat))) return;
276
+ agent.seat = undefined;
277
+ const taken = new Set(world.agents.filter((other) => other !== agent && other.seat).map((other) => `${other.seat?.x},${other.seat?.y}`));
278
+ const free = world.seats.filter((seat) => !taken.has(`${seat.x},${seat.y}`));
279
+ agent.seat = room ? free.find(inRoom) : free[0];
280
+ }
281
+
282
+ /** Where an agent is standing, or heading to if it is walking. */
283
+ function standing(agent: Agent): { x: number; y: number } {
284
+ const end = agent.path[agent.path.length - 1];
285
+ return end ?? { x: Math.round(agent.x), y: Math.round(agent.y) };
286
+ }
287
+
288
+ /** The closest tile to a spot that no other agent is on, so they don't stand inside each other. */
289
+ function freeTileNear(world: World, agent: Agent, spot: { x: number; y: number }): { x: number; y: number } {
290
+ const others = new Set(world.agents.filter((other) => other !== agent && other.state !== "away").map((other) => {
291
+ const at = standing(other);
292
+ return `${at.x},${at.y}`;
293
+ }));
294
+ for (let radius = 0; radius <= 4; radius++) {
295
+ for (let dy = -radius; dy <= radius; dy++) {
296
+ for (let dx = -radius; dx <= radius; dx++) {
297
+ if (Math.abs(dx) + Math.abs(dy) !== radius) continue;
298
+ const tile = { x: spot.x + dx, y: spot.y + dy };
299
+ if (isWalkable(world, tile.x, tile.y) && !others.has(`${tile.x},${tile.y}`) && findPath(world, spot, tile)) return tile;
300
+ }
301
+ }
302
+ }
303
+ return spot;
304
+ }
305
+
306
+ /** Where this agent's current state wants it to be. */
246
307
  export function goalFor(world: World, agent: Agent): { x: number; y: number } | undefined {
247
- const pick = (spots: { x: number; y: number }[]): { x: number; y: number } | undefined =>
248
- spots.length > 0 ? spots[(agent.seed + Math.floor(agent.rest * 1000)) % spots.length] : undefined;
308
+ const pick = (spots: { x: number; y: number }[]): { x: number; y: number } | undefined => {
309
+ const spot = spots.length > 0 ? spots[(agent.seed + Math.floor(agent.rest * 1000)) % spots.length] : undefined;
310
+ return spot && freeTileNear(world, agent, spot);
311
+ };
249
312
  switch (agent.state) {
250
313
  case "working":
251
314
  case "thinking":
252
- case "blocked":
253
- return agent.seat ?? pick(world.spots.lounge);
315
+ case "blocked": {
316
+ if (agent.seat) return agent.seat;
317
+ // No free desk in the room they are working in: they stand in it anyway.
318
+ const room = world.rooms.find((candidate) => candidate.id === agent.area);
319
+ if (room && room.w > 0 && room.h > 0) return freeTileNear(world, agent, { x: room.x + (room.w >> 1), y: room.y + (room.h >> 1) });
320
+ return pick(world.spots.lounge);
321
+ }
254
322
  case "reviewing":
255
323
  return pick(world.spots.board) ?? agent.seat;
256
324
  case "idle":
@@ -262,6 +330,7 @@ export function goalFor(world: World, agent: Agent): { x: number; y: number } |
262
330
  }
263
331
 
264
332
  function placeAgent(world: World, agent: Agent): void {
333
+ updateSeat(world, agent);
265
334
  const goal = goalFor(world, agent) ?? agent.seat ?? firstWalkable(world);
266
335
  agent.x = goal.x;
267
336
  agent.y = goal.y;
@@ -307,6 +376,7 @@ export function step(world: World, dt: number): void {
307
376
  }
308
377
 
309
378
  agent.walking = false;
379
+ if (agent.party > 0) agent.party = Math.max(0, agent.party - dt);
310
380
  if (agent.state === "idle") {
311
381
  agent.rest -= dt;
312
382
  if (agent.rest <= 0) {
@@ -324,25 +394,39 @@ export function applyUpdate(world: World, scene: Pick<OfficeScene, "agents" | "t
324
394
  if (Array.isArray(scene.tasks)) world.tasks = list<OfficeTask>(scene.tasks, MAX_TASKS).map(toTask);
325
395
  if (!Array.isArray(scene.agents)) return;
326
396
  const existing = new Map(world.agents.map((agent) => [agent.id, agent]));
327
- const seats = world.furniture
328
- .filter((item) => item.type === "desk")
329
- .map((item) => ({ x: item.x + 1, y: item.y + 2 }))
330
- .filter((point) => isWalkable(world, point.x, point.y));
331
- world.agents = list<OfficeAgent>(scene.agents, MAX_AGENTS).map((raw, index) => {
332
- const next = toAgent(raw, index);
397
+ const incoming = list<OfficeAgent>(scene.agents, MAX_AGENTS).map(toAgent);
398
+ world.agents = incoming.map((next) => existing.get(next.id)).filter((agent): agent is Agent => Boolean(agent));
399
+ const updated = incoming.map((next) => {
333
400
  const current = existing.get(next.id);
334
- next.seat = seats[index];
335
401
  if (!current) {
336
402
  placeAgent(world, next);
403
+ world.agents = [...world.agents, next];
404
+ // New arrivals come in through a door and walk to where they are going.
405
+ const door = world.doors[0];
406
+ const goal = { x: next.x, y: next.y };
407
+ if (door && next.state !== "away") {
408
+ next.x = door.x;
409
+ next.y = door.y;
410
+ routeTo(world, next, goal);
411
+ }
337
412
  return next;
338
413
  }
339
- Object.assign(current, { name: next.name, role: next.role, task: next.task, hair: next.hair, skin: next.skin, shirt: next.shirt, seat: next.seat });
340
- if (current.state !== next.state) {
414
+ Object.assign(current, { name: next.name, role: next.role, task: next.task, hair: next.hair, skin: next.skin, shirt: next.shirt });
415
+ const moved = current.area !== next.area;
416
+ current.area = next.area;
417
+ if (current.state !== next.state || moved) {
418
+ if (next.state === "done" && current.state !== "done") current.party = 3;
341
419
  current.state = next.state;
420
+ updateSeat(world, current);
342
421
  if (next.state !== "away") routeTo(world, current, goalFor(world, current));
422
+ } else if (NEEDS_DESK.includes(current.state) && !current.seat) {
423
+ // No desk was free before. One may be now.
424
+ updateSeat(world, current);
425
+ if (current.seat) routeTo(world, current, current.seat);
343
426
  }
344
427
  return current;
345
428
  });
429
+ world.agents = updated;
346
430
  }
347
431
 
348
432
  /** Agent under a point given in tiles, nearest first. */
@@ -0,0 +1,57 @@
1
+ /** The office as data: what furniture exists, how big it is, what it's for, and what things are called. */
2
+
3
+ import type { WorldData } from "../pack.js";
4
+ import type { OfficeDoor, OfficeFurniture, OfficeRoom } from "../types.js";
5
+
6
+ /** Footprint in tiles, and where someone stands to use it, relative to its corner. */
7
+ export const OFFICE_ITEMS: WorldData["items"] = {
8
+ desk: { w: 3, h: 2, spot: { x: 1, y: 2 }, use: "seat" },
9
+ coffee: { w: 2, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
10
+ water: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
11
+ sofa: { w: 3, h: 1, spot: { x: 1, y: 1 }, use: "lounge" },
12
+ plant: { w: 1, h: 1 },
13
+ fern: { w: 1, h: 1 },
14
+ board: { w: 4, h: 1, spot: { x: 1, y: 1 }, use: "board" },
15
+ whiteboard: { w: 3, h: 1, spot: { x: 1, y: 1 }, use: "board" },
16
+ shelf: { w: 2, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
17
+ server: { w: 1, h: 1 },
18
+ table: { w: 3, h: 2, spot: { x: 1, y: 2 }, use: "board" },
19
+ rug: { w: 4, h: 3, solid: false },
20
+ arcade: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
21
+ fridge: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
22
+ printer: { w: 1, h: 1, spot: { x: 0, y: 1 } },
23
+ lamp: { w: 1, h: 1 },
24
+ beanbag: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
25
+ tv: { w: 3, h: 1, spot: { x: 1, y: 2 }, use: "lounge" },
26
+ vending: { w: 1, h: 1, spot: { x: 0, y: 1 }, use: "lounge" },
27
+ trophy: { w: 1, h: 1 },
28
+ clock: { w: 1, h: 1, solid: false, wall: true },
29
+ poster: { w: 1, h: 1, solid: false, wall: true },
30
+ };
31
+
32
+ export const OFFICE_LAYOUT = {
33
+ cols: 26,
34
+ rows: 17,
35
+ rooms: [
36
+ { id: "studio", name: "Studio", x: 1, y: 3, w: 15, h: 13, floor: "wood" },
37
+ { id: "lounge", name: "Lounge", x: 17, y: 3, w: 8, h: 6, floor: "carpet" },
38
+ { id: "quests", name: "Quests", x: 17, y: 10, w: 8, h: 6, floor: "tile" },
39
+ ] as OfficeRoom[],
40
+ doors: [{ x: 16, y: 6 }, { x: 16, y: 13 }, { x: 23, y: 9 }] as OfficeDoor[],
41
+ furniture: [
42
+ { type: "desk", x: 2, y: 4 }, { type: "desk", x: 7, y: 4 }, { type: "desk", x: 12, y: 4 },
43
+ { type: "desk", x: 2, y: 10 }, { type: "desk", x: 7, y: 10 }, { type: "desk", x: 12, y: 10 },
44
+ { type: "plant", x: 1, y: 8 }, { type: "plant", x: 15, y: 8 }, { type: "server", x: 15, y: 15 }, { type: "server", x: 14, y: 15 },
45
+ { type: "shelf", x: 1, y: 15 },
46
+ { type: "coffee", x: 19, y: 3 }, { type: "water", x: 21, y: 3 }, { type: "sofa", x: 22, y: 3 }, { type: "rug", x: 20, y: 5 }, { type: "plant", x: 24, y: 8 },
47
+ { type: "board", x: 19, y: 10 }, { type: "table", x: 19, y: 13 }, { type: "plant", x: 24, y: 15 },
48
+ ] as OfficeFurniture[],
49
+ };
50
+
51
+ export const OFFICE: WorldData = {
52
+ id: "office",
53
+ name: "Pixel Office",
54
+ words: { quests: "QUESTS" },
55
+ items: OFFICE_ITEMS,
56
+ layout: OFFICE_LAYOUT,
57
+ };