@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.
- package/dist/draw.d.ts +9 -9
- package/dist/draw.d.ts.map +1 -1
- package/dist/draw.js +73 -305
- 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 +2 -0
- 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 +13 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/world.d.ts +15 -11
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +31 -59
- 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 +73 -289
- package/src/index.ts +3 -0
- package/src/links.ts +76 -0
- package/src/office.ts +3 -0
- package/src/pack.ts +36 -0
- package/src/paint.ts +24 -0
- package/src/types.ts +12 -2
- package/src/world.ts +41 -60
- package/src/worlds/office-data.ts +57 -0
- package/src/worlds/office-paint.ts +276 -0
package/src/world.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* heading. Nothing in here touches a canvas, so all of it can be tested.
|
|
4
4
|
*/
|
|
5
5
|
|
|
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";
|
|
6
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;
|
|
@@ -12,54 +15,14 @@ const MAX_FURNITURE = 300;
|
|
|
12
15
|
const MAX_TASKS = 60;
|
|
13
16
|
export const WALK_SPEED = 4.5; // tiles a second
|
|
14
17
|
|
|
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
|
-
fern: { w: 1, h: 1 },
|
|
23
|
-
board: { w: 4, h: 1, spot: { x: 1, y: 1 } },
|
|
24
|
-
whiteboard: { w: 3, h: 1, spot: { x: 1, y: 1 } },
|
|
25
|
-
shelf: { w: 2, h: 1, spot: { x: 0, y: 1 } },
|
|
26
|
-
server: { w: 1, h: 1 },
|
|
27
|
-
table: { w: 3, h: 2, spot: { x: 1, y: 2 } },
|
|
28
|
-
rug: { w: 4, h: 3, solid: false },
|
|
29
|
-
arcade: { w: 1, h: 1, spot: { x: 0, y: 1 } },
|
|
30
|
-
fridge: { w: 1, h: 1, spot: { x: 0, y: 1 } },
|
|
31
|
-
printer: { w: 1, h: 1, spot: { x: 0, y: 1 } },
|
|
32
|
-
lamp: { w: 1, h: 1 },
|
|
33
|
-
beanbag: { w: 1, h: 1, spot: { x: 0, y: 1 } },
|
|
34
|
-
tv: { w: 3, h: 1, spot: { x: 1, y: 2 } },
|
|
35
|
-
vending: { w: 1, h: 1, spot: { x: 0, y: 1 } },
|
|
36
|
-
trophy: { w: 1, h: 1 },
|
|
37
|
-
clock: { w: 1, h: 1, solid: false, wall: true },
|
|
38
|
-
poster: { w: 1, h: 1, solid: false, wall: true },
|
|
39
|
-
};
|
|
18
|
+
/** Footprints and uses, for the office. Other worlds bring their own. */
|
|
19
|
+
export const FURNITURE = OFFICE_ITEMS;
|
|
40
20
|
|
|
41
21
|
const AGENT_STATES: readonly string[] = ["working", "thinking", "blocked", "reviewing", "idle", "done", "away"];
|
|
42
22
|
const TASK_STATES: readonly string[] = ["todo", "doing", "done"];
|
|
43
|
-
const FLOORS: readonly string[] = ["wood", "carpet", "tile", "stone", "concrete", "grass"];
|
|
44
|
-
|
|
45
|
-
export const DEFAULT_LAYOUT =
|
|
46
|
-
cols: 26,
|
|
47
|
-
rows: 17,
|
|
48
|
-
rooms: [
|
|
49
|
-
{ id: "studio", name: "Studio", x: 1, y: 3, w: 15, h: 13, floor: "wood" },
|
|
50
|
-
{ id: "lounge", name: "Lounge", x: 17, y: 3, w: 8, h: 6, floor: "carpet" },
|
|
51
|
-
{ id: "quests", name: "Quests", x: 17, y: 10, w: 8, h: 6, floor: "tile" },
|
|
52
|
-
] as OfficeRoom[],
|
|
53
|
-
doors: [{ x: 16, y: 6 }, { x: 16, y: 13 }, { x: 23, y: 9 }] as OfficeDoor[],
|
|
54
|
-
furniture: [
|
|
55
|
-
{ type: "desk", x: 2, y: 4 }, { type: "desk", x: 7, y: 4 }, { type: "desk", x: 12, y: 4 },
|
|
56
|
-
{ type: "desk", x: 2, y: 10 }, { type: "desk", x: 7, y: 10 }, { type: "desk", x: 12, y: 10 },
|
|
57
|
-
{ type: "plant", x: 1, y: 8 }, { type: "plant", x: 15, y: 8 }, { type: "server", x: 15, y: 15 }, { type: "server", x: 14, y: 15 },
|
|
58
|
-
{ type: "shelf", x: 1, y: 15 },
|
|
59
|
-
{ 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 },
|
|
60
|
-
{ type: "board", x: 19, y: 10 }, { type: "table", x: 19, y: 13 }, { type: "plant", x: 24, y: 15 },
|
|
61
|
-
] as OfficeFurniture[],
|
|
62
|
-
};
|
|
23
|
+
const FLOORS: readonly string[] = ["wood", "carpet", "tile", "stone", "concrete", "grass", "hall"];
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_LAYOUT = OFFICE_LAYOUT;
|
|
63
26
|
|
|
64
27
|
const int = (value: unknown, fallback: number): number => (typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback);
|
|
65
28
|
const str = (value: unknown, max = 120): string => (typeof value === "string" ? value.slice(0, max) : "");
|
|
@@ -94,6 +57,12 @@ export interface Agent {
|
|
|
94
57
|
}
|
|
95
58
|
|
|
96
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;
|
|
97
66
|
cols: number;
|
|
98
67
|
rows: number;
|
|
99
68
|
rooms: Room[];
|
|
@@ -187,13 +156,18 @@ function toTask(raw: OfficeTask): Task {
|
|
|
187
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) };
|
|
188
157
|
}
|
|
189
158
|
|
|
190
|
-
|
|
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"> {
|
|
191
165
|
const custom = Array.isArray(scene?.rooms) && scene.rooms.length > 0;
|
|
192
|
-
const cols = Math.min(Math.max(int(scene?.cols, custom ? 20 :
|
|
193
|
-
const rows = Math.min(Math.max(int(scene?.rows, custom ? 14 :
|
|
194
|
-
const rawRooms = custom ? list<OfficeRoom>(scene.rooms, 40) :
|
|
195
|
-
const rawDoors = custom ? list<OfficeDoor>(scene.doors, 60) :
|
|
196
|
-
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;
|
|
197
171
|
|
|
198
172
|
// Clamped to the grid: everything later loops over these, so a huge room must not hang the page.
|
|
199
173
|
const rooms: Room[] = rawRooms.map((room, index) => {
|
|
@@ -220,36 +194,43 @@ function buildLayout(scene: OfficeLayout): Pick<World, "cols" | "rows" | "rooms"
|
|
|
220
194
|
for (const door of doors) mark(door.x, door.y, true);
|
|
221
195
|
|
|
222
196
|
const furniture: Item[] = rawItems
|
|
223
|
-
.filter((item) => item && Object.hasOwn(
|
|
197
|
+
.filter((item) => item && Object.hasOwn(pack.items, item.type))
|
|
224
198
|
.map((item) => ({ type: item.type, x: int(item.x, -99), y: int(item.y, -99) }));
|
|
225
199
|
for (const item of furniture) {
|
|
226
|
-
const spec =
|
|
200
|
+
const spec = pack.items[item.type] as ItemSpec;
|
|
227
201
|
if (spec.solid === false) continue;
|
|
228
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);
|
|
229
203
|
}
|
|
230
204
|
|
|
231
205
|
const open = (x: number, y: number): boolean => x >= 0 && y >= 0 && x < cols && y < rows && walkable[y * cols + x] === true;
|
|
232
206
|
const spot = (item: Item): { x: number; y: number } | undefined => {
|
|
233
|
-
const offset =
|
|
207
|
+
const offset = (pack.items[item.type] as ItemSpec).spot;
|
|
234
208
|
if (!offset) return undefined;
|
|
235
209
|
const point = { x: item.x + offset.x, y: item.y + offset.y };
|
|
236
210
|
return open(point.x, point.y) ? point : undefined;
|
|
237
211
|
};
|
|
238
|
-
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));
|
|
239
213
|
const spots: World["spots"] = { lounge: [], board: [] };
|
|
240
214
|
for (const item of furniture) {
|
|
241
215
|
const point = spot(item);
|
|
242
216
|
if (!point) continue;
|
|
243
|
-
|
|
244
|
-
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);
|
|
245
220
|
}
|
|
246
|
-
|
|
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 };
|
|
247
225
|
}
|
|
248
226
|
|
|
249
227
|
/** Build a world from a scene. Bad values are dropped or clamped, so it never throws. */
|
|
250
228
|
export function createWorld(scene: OfficeScene = {}): World {
|
|
229
|
+
const pack = worldData(scene?.world, OFFICE);
|
|
251
230
|
const world: World = {
|
|
252
|
-
|
|
231
|
+
pack,
|
|
232
|
+
showLinks: true,
|
|
233
|
+
...buildLayout(scene, pack),
|
|
253
234
|
agents: list<OfficeAgent>(scene?.agents, MAX_AGENTS).map(toAgent),
|
|
254
235
|
tasks: list<OfficeTask>(scene?.tasks, MAX_TASKS).map(toTask),
|
|
255
236
|
};
|
|
@@ -259,7 +240,7 @@ export function createWorld(scene: OfficeScene = {}): World {
|
|
|
259
240
|
|
|
260
241
|
/** Change the building while everyone is in it. Anyone left inside a wall or a desk moves. */
|
|
261
242
|
export function applyLayout(world: World, layout: OfficeLayout): void {
|
|
262
|
-
Object.assign(world, buildLayout(layout));
|
|
243
|
+
Object.assign(world, buildLayout(layout, world.pack), { routes: undefined });
|
|
263
244
|
for (const agent of world.agents) {
|
|
264
245
|
agent.seat = undefined;
|
|
265
246
|
agent.path = [];
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the office looks. Every piece of art here is original to this project, made in code at
|
|
3
|
+
* 16 pixels a tile.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { OUTLINE, TILE, rect, shade, type Ctx, type Palette } from "../paint.js";
|
|
7
|
+
import { isSeated, type Item, type World } from "../world.js";
|
|
8
|
+
|
|
9
|
+
function drawDesk(c: Ctx, x: number, y: number, on: boolean, time: number, seed: number, palette: Palette, blocked: boolean): void {
|
|
10
|
+
rect(c, "#55351d", x + 1, y + 22, 46, 8);
|
|
11
|
+
rect(c, "#6b4427", x + 1, y + 13, 46, 10);
|
|
12
|
+
rect(c, "#8a5a33", x + 1, y + 10, 46, 12);
|
|
13
|
+
rect(c, "#a87545", x + 1, y + 10, 46, 2);
|
|
14
|
+
rect(c, "#3b2413", x + 3, y + 22, 3, 9);
|
|
15
|
+
rect(c, "#3b2413", x + 42, y + 22, 3, 9);
|
|
16
|
+
rect(c, "#55351d", x + 30, y + 23, 12, 7);
|
|
17
|
+
rect(c, "#7c5637", x + 31, y + 24, 10, 2);
|
|
18
|
+
rect(c, "#d4a24e", x + 35, y + 24, 2, 1);
|
|
19
|
+
rect(c, "#7c5637", x + 31, y + 27, 10, 2);
|
|
20
|
+
rect(c, "#d4a24e", x + 35, y + 27, 2, 1);
|
|
21
|
+
// Monitor
|
|
22
|
+
rect(c, OUTLINE, x + 14, y - 3, 20, 15);
|
|
23
|
+
rect(c, "#374151", x + 22, y + 12, 4, 2);
|
|
24
|
+
rect(c, "#4b5563", x + 19, y + 14, 10, 1);
|
|
25
|
+
if (blocked) {
|
|
26
|
+
rect(c, "#3f0f14", x + 15, y - 2, 18, 13);
|
|
27
|
+
if (Math.floor(time * 2) % 2 === 0) { rect(c, "#ef4444", x + 23, y, 2, 6); rect(c, "#ef4444", x + 23, y + 7, 2, 2); }
|
|
28
|
+
} else if (on) {
|
|
29
|
+
rect(c, "#0f2a2e", x + 15, y - 2, 18, 13);
|
|
30
|
+
const scroll = Math.floor(time * 3 + (seed % 13));
|
|
31
|
+
for (let line = 0; line < 5; line++) {
|
|
32
|
+
const width = 3 + (((seed >> line) & 7) + (scroll + line) * 7) % 12;
|
|
33
|
+
rect(c, line % 3 === 0 ? palette.primary : line % 3 === 1 ? "#7dd3fc" : "#fcd34d", x + 16 + (line % 2) * 2, y - 1 + line * 2.2, width, 1);
|
|
34
|
+
}
|
|
35
|
+
c.fillStyle = "rgba(125,211,252,0.12)";
|
|
36
|
+
c.fillRect(x + 10, y + 11, 28, 8);
|
|
37
|
+
} else {
|
|
38
|
+
rect(c, "#1f2937", x + 15, y - 2, 18, 13);
|
|
39
|
+
rect(c, "#374151", x + 17, y, 3, 1);
|
|
40
|
+
}
|
|
41
|
+
rect(c, "#d1d5db", x + 16, y + 15, 16, 5);
|
|
42
|
+
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);
|
|
43
|
+
rect(c, "#e5e7eb", x + 34, y + 16, 3, 4);
|
|
44
|
+
rect(c, "#f3f4f6", x + 40, y + 13, 5, 5);
|
|
45
|
+
rect(c, palette.primary, x + 40, y + 15, 5, 1);
|
|
46
|
+
rect(c, "#fde68a", x + 4, y + 13, 7, 6);
|
|
47
|
+
rect(c, "#fbbf24", x + 5, y + 15, 5, 1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function drawBoard(c: Ctx, x: number, y: number, world: World): void {
|
|
51
|
+
rect(c, "#3a2c1e", x, y - 12, 64, 27);
|
|
52
|
+
rect(c, "#d9c29c", x + 2, y - 10, 60, 23);
|
|
53
|
+
for (let i = 0; i < 40; i++) rect(c, "#c9b08a", x + 3 + (i * 37) % 58, y - 9 + (i * 13) % 21, 1, 1);
|
|
54
|
+
const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
|
|
55
|
+
const per = 8;
|
|
56
|
+
world.tasks.slice(0, per * 2).forEach((task, index) => {
|
|
57
|
+
const col = index % per;
|
|
58
|
+
const row = Math.floor(index / per);
|
|
59
|
+
rect(c, "#00000033", x + 5 + col * 7, y - 7 + row * 10, 6, 8);
|
|
60
|
+
rect(c, colors[task.state], x + 4 + col * 7, y - 8 + row * 10, 6, 8);
|
|
61
|
+
rect(c, "#ef4444", x + 6 + col * 7, y - 8 + row * 10, 2, 1);
|
|
62
|
+
rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 10, 4, 1);
|
|
63
|
+
rect(c, "#00000040", x + 5 + col * 7, y - 3 + row * 10, 3, 1);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function drawFurniture(c: Ctx, item: Item, world: World, time: number, palette: Palette): void {
|
|
68
|
+
const x = item.x * TILE;
|
|
69
|
+
const y = item.y * TILE;
|
|
70
|
+
switch (item.type) {
|
|
71
|
+
case "desk": {
|
|
72
|
+
const seatX = item.x + 1;
|
|
73
|
+
const seatY = item.y + 2;
|
|
74
|
+
const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a));
|
|
75
|
+
drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette, user?.state === "blocked");
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "coffee":
|
|
79
|
+
rect(c, "#374151", x, y + 2, 32, 14);
|
|
80
|
+
rect(c, "#6b7280", x, y + 2, 32, 2);
|
|
81
|
+
rect(c, OUTLINE, x + 4, y - 9, 15, 15);
|
|
82
|
+
rect(c, "#27272a", x + 5, y - 8, 13, 13);
|
|
83
|
+
rect(c, Math.floor(time * 2) % 2 ? "#ef4444" : "#7f1d1d", x + 15, y - 6, 2, 2);
|
|
84
|
+
rect(c, "#52525b", x + 7, y - 6, 6, 4);
|
|
85
|
+
rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
|
|
86
|
+
rect(c, "#f3f4f6", x + 23, y - 1, 5, 5);
|
|
87
|
+
rect(c, "#92400e", x + 24, y, 3, 1);
|
|
88
|
+
for (let i = 0; i < 3; i++) {
|
|
89
|
+
c.fillStyle = "rgba(255,255,255,0.5)";
|
|
90
|
+
c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case "water":
|
|
94
|
+
rect(c, "#d1d5db", x + 3, y + 3, 10, 13);
|
|
95
|
+
rect(c, "#9ca3af", x + 3, y + 14, 10, 2);
|
|
96
|
+
rect(c, "#60a5fa", x + 4, y - 8, 8, 11);
|
|
97
|
+
rect(c, "#bae6fd", x + 5, y - 7, 2, 9);
|
|
98
|
+
rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
|
|
99
|
+
rect(c, "#ef4444", x + 9, y + 6, 2, 2);
|
|
100
|
+
if (Math.floor(time * 0.4 + item.x) % 5 === 0) rect(c, "#e0f2fe", x + 8, y - 2 - ((time * 6) % 7), 1, 1);
|
|
101
|
+
break;
|
|
102
|
+
case "sofa":
|
|
103
|
+
rect(c, shade(palette.primary, -60), x + 1, y - 7, 46, 11);
|
|
104
|
+
rect(c, shade(palette.primary, -30), x + 2, y - 6, 44, 3);
|
|
105
|
+
rect(c, "#4b5563", x, y + 3, 48, 11);
|
|
106
|
+
rect(c, "#6b7280", x + 3, y + 3, 20, 7);
|
|
107
|
+
rect(c, "#6b7280", x + 25, y + 3, 20, 7);
|
|
108
|
+
rect(c, "#374151", x, y + 12, 48, 3);
|
|
109
|
+
rect(c, "#374151", x - 1, y - 2, 4, 16);
|
|
110
|
+
rect(c, "#374151", x + 45, y - 2, 4, 16);
|
|
111
|
+
break;
|
|
112
|
+
case "plant":
|
|
113
|
+
rect(c, "#92400e", x + 4, y + 9, 8, 7);
|
|
114
|
+
rect(c, "#b45309", x + 3, y + 8, 10, 2);
|
|
115
|
+
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);
|
|
116
|
+
rect(c, "#22c55e", x + 7, y - 3, 2, 8);
|
|
117
|
+
rect(c, "#22c55e", x + 3, y + 1, 2, 3);
|
|
118
|
+
rect(c, "#4ade80", x + 10, y - 4, 1, 3);
|
|
119
|
+
break;
|
|
120
|
+
case "fern":
|
|
121
|
+
rect(c, "#57534e", x + 3, y + 10, 10, 6);
|
|
122
|
+
rect(c, "#78716c", x + 2, y + 9, 12, 2);
|
|
123
|
+
for (let i = 0; i < 7; i++) {
|
|
124
|
+
const lean = i - 3;
|
|
125
|
+
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);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
case "board":
|
|
129
|
+
drawBoard(c, x, y, world);
|
|
130
|
+
break;
|
|
131
|
+
case "whiteboard":
|
|
132
|
+
rect(c, "#9ca3af", x, y - 10, 48, 22);
|
|
133
|
+
rect(c, "#f9fafb", x + 2, y - 8, 44, 18);
|
|
134
|
+
rect(c, "#3b82f6", x + 5, y - 5, 14, 1); rect(c, "#3b82f6", x + 5, y - 2, 9, 1);
|
|
135
|
+
rect(c, "#ef4444", x + 24, y - 6, 8, 6); rect(c, "#f9fafb", x + 25, y - 5, 6, 4);
|
|
136
|
+
rect(c, "#10b981", x + 8, y + 3, 20, 1); rect(c, "#10b981", x + 27, y + 2, 1, 3);
|
|
137
|
+
rect(c, "#6b7280", x + 2, y + 10, 44, 2);
|
|
138
|
+
break;
|
|
139
|
+
case "shelf":
|
|
140
|
+
rect(c, "#5b3a21", x, y - 12, 32, 28);
|
|
141
|
+
for (let shelf = 0; shelf < 2; shelf++) {
|
|
142
|
+
rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
|
|
143
|
+
for (let book = 0; book < 8; book++) {
|
|
144
|
+
const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf + item.x) % 5] as string;
|
|
145
|
+
rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
|
|
146
|
+
rect(c, shade(color, 40), x + 3 + book * 3, y - 6 + shelf * 13, 2, 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
case "server":
|
|
151
|
+
rect(c, OUTLINE, x, y - 13, 16, 29);
|
|
152
|
+
rect(c, "#1f2937", x + 1, y - 12, 14, 27);
|
|
153
|
+
for (let i = 0; i < 5; i++) {
|
|
154
|
+
rect(c, "#111827", x + 2, y - 11 + i * 5, 12, 4);
|
|
155
|
+
const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
|
|
156
|
+
rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
|
|
157
|
+
rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
|
|
158
|
+
rect(c, "#374151", x + 8, y - 10 + i * 5, 5, 1);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case "table":
|
|
162
|
+
rect(c, "#44403c", x + 2, y + 16, 44, 12);
|
|
163
|
+
rect(c, "#78716c", x + 2, y + 3, 44, 14);
|
|
164
|
+
rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
|
|
165
|
+
rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
|
|
166
|
+
rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
|
|
167
|
+
rect(c, "#1f2937", x + 12, y + 8, 4, 1);
|
|
168
|
+
rect(c, "#3b82f6", x + 30, y + 10, 4, 1);
|
|
169
|
+
break;
|
|
170
|
+
case "rug":
|
|
171
|
+
rect(c, palette.primary, x, y, 64, 48);
|
|
172
|
+
c.fillStyle = "rgba(0,0,0,0.42)";
|
|
173
|
+
c.fillRect(x + 3, y + 3, 58, 42);
|
|
174
|
+
rect(c, palette.primary, x + 6, y + 6, 52, 1);
|
|
175
|
+
rect(c, palette.primary, x + 6, y + 41, 52, 1);
|
|
176
|
+
rect(c, palette.primary, x + 6, y + 6, 1, 36);
|
|
177
|
+
rect(c, palette.primary, x + 57, y + 6, 1, 36);
|
|
178
|
+
for (let i = 0; i < 6; i++) rect(c, palette.primary, x + 12 + i * 8, y + 22, 4, 4);
|
|
179
|
+
break;
|
|
180
|
+
case "arcade":
|
|
181
|
+
rect(c, OUTLINE, x, y - 14, 16, 30);
|
|
182
|
+
rect(c, "#4c1d95", x + 1, y - 13, 14, 28);
|
|
183
|
+
rect(c, "#111827", x + 3, y - 10, 10, 9);
|
|
184
|
+
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);
|
|
185
|
+
rect(c, "#f59e0b", x + 3, y - 13, 10, 2);
|
|
186
|
+
rect(c, "#27272a", x + 2, y + 1, 12, 4);
|
|
187
|
+
rect(c, "#ef4444", x + 4, y + 2, 2, 2);
|
|
188
|
+
rect(c, "#3b82f6", x + 9, y + 2, 2, 2);
|
|
189
|
+
break;
|
|
190
|
+
case "fridge":
|
|
191
|
+
rect(c, OUTLINE, x + 1, y - 12, 14, 28);
|
|
192
|
+
rect(c, "#e5e7eb", x + 2, y - 11, 12, 26);
|
|
193
|
+
rect(c, "#9ca3af", x + 2, y - 3, 12, 1);
|
|
194
|
+
rect(c, "#6b7280", x + 11, y - 8, 1, 3);
|
|
195
|
+
rect(c, "#6b7280", x + 11, y, 1, 5);
|
|
196
|
+
rect(c, "#fde68a", x + 4, y - 9, 3, 3);
|
|
197
|
+
rect(c, "#fca5a5", x + 5, y + 3, 3, 3);
|
|
198
|
+
break;
|
|
199
|
+
case "printer":
|
|
200
|
+
rect(c, "#4b5563", x + 2, y + 8, 12, 8);
|
|
201
|
+
rect(c, OUTLINE, x + 1, y - 2, 14, 11);
|
|
202
|
+
rect(c, "#d1d5db", x + 2, y - 1, 12, 9);
|
|
203
|
+
rect(c, "#f9fafb", x + 4, y - 5 - (Math.floor(time) % 4 === 0 ? 1 : 0), 8, 5);
|
|
204
|
+
rect(c, "#22c55e", x + 11, y + 1, 2, 1);
|
|
205
|
+
rect(c, "#9ca3af", x + 3, y + 5, 10, 1);
|
|
206
|
+
break;
|
|
207
|
+
case "lamp":
|
|
208
|
+
rect(c, "#374151", x + 5, y + 13, 6, 3);
|
|
209
|
+
rect(c, "#4b5563", x + 7, y - 4, 2, 17);
|
|
210
|
+
rect(c, OUTLINE, x + 2, y - 10, 12, 8);
|
|
211
|
+
rect(c, "#fde68a", x + 3, y - 9, 10, 6);
|
|
212
|
+
rect(c, "#fef3c7", x + 4, y - 8, 4, 2);
|
|
213
|
+
break;
|
|
214
|
+
case "beanbag":
|
|
215
|
+
rect(c, OUTLINE, x, y + 3, 16, 13);
|
|
216
|
+
rect(c, shade(palette.primary, -50), x + 1, y + 4, 14, 11);
|
|
217
|
+
rect(c, shade(palette.primary, -20), x + 3, y + 5, 8, 4);
|
|
218
|
+
rect(c, "#00000040", x + 1, y + 13, 14, 2);
|
|
219
|
+
break;
|
|
220
|
+
case "tv":
|
|
221
|
+
rect(c, "#3b2413", x + 2, y + 8, 44, 8);
|
|
222
|
+
rect(c, "#5b3a21", x + 2, y + 8, 44, 2);
|
|
223
|
+
rect(c, OUTLINE, x + 4, y - 14, 40, 22);
|
|
224
|
+
rect(c, "#0b1120", x + 5, y - 13, 38, 20);
|
|
225
|
+
for (let i = 0; i < 9; i++) {
|
|
226
|
+
const h = 2 + ((i * 5 + Math.floor(time * 3)) % 12);
|
|
227
|
+
rect(c, i % 3 === 0 ? palette.primary : i % 3 === 1 ? "#38bdf8" : "#f472b6", x + 8 + i * 4, y + 5 - h, 3, h);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
case "vending":
|
|
231
|
+
rect(c, OUTLINE, x, y - 14, 16, 30);
|
|
232
|
+
rect(c, "#b91c1c", x + 1, y - 13, 14, 28);
|
|
233
|
+
rect(c, "#0f172a", x + 3, y - 11, 8, 16);
|
|
234
|
+
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);
|
|
235
|
+
rect(c, "#d1d5db", x + 12, y - 9, 2, 6);
|
|
236
|
+
rect(c, "#111827", x + 3, y + 8, 10, 4);
|
|
237
|
+
break;
|
|
238
|
+
case "trophy":
|
|
239
|
+
rect(c, "#6b4427", x + 2, y + 8, 12, 8);
|
|
240
|
+
rect(c, "#8a5a33", x + 2, y + 8, 12, 2);
|
|
241
|
+
rect(c, "#78350f", x + 5, y + 5, 6, 3);
|
|
242
|
+
rect(c, "#f59e0b", x + 7, y + 1, 2, 4);
|
|
243
|
+
rect(c, OUTLINE, x + 3, y - 8, 10, 10);
|
|
244
|
+
rect(c, "#fbbf24", x + 4, y - 7, 8, 8);
|
|
245
|
+
rect(c, "#fde68a", x + 5, y - 6, 2, 5);
|
|
246
|
+
rect(c, "#fbbf24", x + 1, y - 6, 3, 1); rect(c, "#fbbf24", x + 12, y - 6, 3, 1);
|
|
247
|
+
break;
|
|
248
|
+
case "clock": {
|
|
249
|
+
rect(c, OUTLINE, x + 3, y + 1, 10, 10);
|
|
250
|
+
rect(c, "#f9fafb", x + 4, y + 2, 8, 8);
|
|
251
|
+
rect(c, "#1f2937", x + 8, y + 3, 1, 3);
|
|
252
|
+
rect(c, "#1f2937", x + 8, y + 6, 3, 1);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case "poster": {
|
|
256
|
+
const colors = ["#7c3aed", "#0ea5e9", "#db2777", "#059669"];
|
|
257
|
+
const color = colors[(item.x + item.y) % colors.length] as string;
|
|
258
|
+
rect(c, OUTLINE, x + 2, y, 12, 11);
|
|
259
|
+
rect(c, color, x + 3, y + 1, 10, 9);
|
|
260
|
+
rect(c, shade(color, 60), x + 5, y + 3, 6, 3);
|
|
261
|
+
rect(c, "#f9fafb", x + 5, y + 7, 6, 1);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const OFFICE_PAINTER = {
|
|
268
|
+
furniture: drawFurniture,
|
|
269
|
+
glow(item: Item, world: World, glow: (x: number, y: number, radius: number, color: string) => void): void {
|
|
270
|
+
if (item.type === "lamp") glow(item.x * TILE + 8, item.y * TILE, 36, "rgba(253,224,139,0.30)");
|
|
271
|
+
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)");
|
|
272
|
+
if (item.type === "desk" && world.agents.some((a) => a.seat?.x === item.x + 1 && a.seat?.y === item.y + 2 && isSeated(a))) {
|
|
273
|
+
glow(item.x * TILE + 24, item.y * TILE + 8, 30, "rgba(125,211,252,0.26)");
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
};
|