@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.
- package/dist/draw.d.ts +22 -12
- package/dist/draw.d.ts.map +1 -1
- package/dist/draw.js +374 -273
- package/dist/draw.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- 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 +25 -2
- package/dist/office.d.ts.map +1 -1
- package/dist/office.js +134 -20
- 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.js.map +1 -1
- package/dist/runtime.txt +1 -1
- package/dist/types.d.ts +20 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/world.d.ts +29 -13
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +150 -72
- 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 +52 -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 +292 -0
- package/dist/worlds/office-paint.js.map +1 -0
- package/package.json +7 -2
- package/src/draw.ts +352 -272
- package/src/index.ts +4 -1
- package/src/links.ts +76 -0
- package/src/office.ts +142 -21
- package/src/pack.ts +36 -0
- package/src/paint.ts +24 -0
- package/src/runtime.ts +4 -1
- package/src/types.ts +31 -5
- package/src/world.ts +161 -77
- package/src/worlds/office-data.ts +57 -0
- 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
|
|
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 =
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
export const FURNITURE
|
|
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
|
-
|
|
173
|
-
|
|
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 :
|
|
176
|
-
const rows = Math.min(Math.max(int(scene?.rows, custom ? 14 :
|
|
177
|
-
const rawRooms = custom ? list<OfficeRoom>(scene.rooms, 40) :
|
|
178
|
-
const rawDoors = custom ? list<OfficeDoor>(scene.doors, 60) :
|
|
179
|
-
const rawItems = Array.isArray(scene?.furniture) ? list<OfficeFurniture>(scene.furniture, MAX_FURNITURE) : custom ? [] :
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
210
|
+
return open(point.x, point.y) ? point : undefined;
|
|
224
211
|
};
|
|
225
|
-
const seats = furniture.filter((item) => item.type === "
|
|
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
|
-
|
|
230
|
-
if (
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
340
|
-
|
|
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
|
+
};
|