@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/draw.ts
CHANGED
|
@@ -1,107 +1,184 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Draws the office. Every piece of art here is original to this project, made
|
|
3
|
-
*
|
|
2
|
+
* Draws the office. Every piece of art here is original to this project, made in code at
|
|
3
|
+
* 16 pixels a tile. The building is drawn once to a layer (drawBuilding); people, screens
|
|
4
|
+
* and anything else that moves are drawn on top each frame (drawWorld).
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
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";
|
|
7
11
|
|
|
8
|
-
export
|
|
12
|
+
export { TILE, type Palette } from "./paint.js";
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
// --- Worlds ---------------------------------------------------------------------------------------
|
|
11
15
|
|
|
12
|
-
|
|
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
|
+
}
|
|
22
|
+
|
|
23
|
+
const painters = new Map<string, Painter>([["office", OFFICE_PAINTER]]);
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
25
|
+
export function registerPainter(id: string, painter: Painter): void {
|
|
26
|
+
painters.set(id, painter);
|
|
27
|
+
}
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
const painterFor = (world: World): Painter => painters.get(world.pack.id) ?? OFFICE_PAINTER;
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
const HEAD_BACK = ["...hhhhhh...", "..hhhhhhhh..", "..hhhhhhhh..", "..hhhhhhhh..", "..hhhhhhhh..", "..hhhhhhhh..", "...ssssss..."];
|
|
23
|
-
const HEAD_SIDE = ["...hhhhhh...", "..hhhhhhhh..", "..hhhhhhhh..", "..hhssssss..", "..hhsssess..", "..hsssssss..", "...ssssss..."];
|
|
24
|
-
const BODY = ["..tttttttt..", ".tttttttttt.", ".stttttttts.", ".stTTTTTTts."];
|
|
25
|
-
const BODY_SIDE = ["...tttttt...", "...tttttt...", "...ttsttt...", "...TTsTTT..."];
|
|
26
|
-
const LEGS_STAND = ["..llllllll..", "..lll..lll..", "..lll..lll..", "..bbb..bbb.."];
|
|
27
|
-
const LEGS_A = ["..llllllll..", "..lll..lll..", "..bbb..lll..", ".......bbb.."];
|
|
28
|
-
const LEGS_B = ["..llllllll..", "..lll..lll..", "..lll..bbb..", "..bbb......."];
|
|
29
|
-
const LEGS_SIDE_STAND = ["...llllll...", "...lll.lll..", "...lll.lll..", "...bbb.bbb.."];
|
|
30
|
-
const LEGS_SIDE_A = ["...llllll...", "..lll..lll..", ".lll....lll.", ".bb......bb."];
|
|
31
|
-
const LEGS_SIDE_B = ["...llllll...", "...llllll...", "....llll....", "....bbbb...."];
|
|
31
|
+
// --- Time of day, from the viewer's own clock ------------------------------------------------------------
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
33
|
+
export interface Daylight { sky: string; sun: string; night: number }
|
|
34
|
+
|
|
35
|
+
export function daylightAt(hour: number): Daylight {
|
|
36
|
+
if (hour >= 7 && hour < 17) return { sky: "#8ed0ff", sun: "rgba(255,244,214,0.10)", night: 0 };
|
|
37
|
+
if (hour >= 17 && hour < 20) return { sky: "#ffae70", sun: "rgba(255,170,110,0.13)", night: 0.14 };
|
|
38
|
+
if (hour >= 5 && hour < 7) return { sky: "#ffc9a3", sun: "rgba(255,200,170,0.08)", night: 0.16 };
|
|
39
|
+
return { sky: "#16204a", sun: "rgba(0,0,0,0)", night: 0.24 };
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// --- Characters ------------------------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
type Part = [color: string, x: number, y: number, w: number, h: number];
|
|
45
|
+
|
|
46
|
+
/** Parts draw with one outline around the whole figure, so characters read clearly on any floor. */
|
|
47
|
+
function drawParts(c: Ctx, parts: Part[]): void {
|
|
48
|
+
c.fillStyle = OUTLINE;
|
|
49
|
+
for (const [, x, y, w, h] of parts) c.fillRect(Math.round(x) - 1, Math.round(y) - 1, w + 2, h + 2);
|
|
50
|
+
for (const [color, x, y, w, h] of parts) rect(c, color, x, y, w, h);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
function drawAgent(c: Ctx, agent: Agent, time: number, selected: boolean): void {
|
|
50
|
-
const
|
|
54
|
+
const left = agent.x * TILE + 2; // sprite is 12 wide inside a 16 tile
|
|
55
|
+
const feet = agent.y * TILE + 15;
|
|
51
56
|
const seated = isSeated(agent);
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
57
|
+
const walking = agent.walking;
|
|
58
|
+
const side = agent.facing === "left" || agent.facing === "right";
|
|
59
|
+
const back = agent.facing === "up";
|
|
60
|
+
const flip = agent.facing === "left";
|
|
61
|
+
const phase = Math.floor(time * 9 + (agent.seed % 5));
|
|
62
|
+
const step = walking ? phase % 4 : 0; // 0 pass, 1 left forward, 2 pass, 3 right forward
|
|
63
|
+
const bob = walking && step % 2 === 1 ? -1 : 0;
|
|
64
|
+
const breathe = !walking && !seated && Math.floor(time * 1.4 + (agent.seed % 7)) % 2 === 0 ? 1 : 0;
|
|
65
|
+
const party = agent.party > 0 ? -Math.abs(Math.round(Math.sin(time * 12) * 3)) : 0;
|
|
66
|
+
|
|
67
|
+
const skin = agent.skin;
|
|
68
|
+
const hair = agent.hair;
|
|
69
|
+
const shirt = agent.shirt;
|
|
70
|
+
const dark = shade(shirt, -40);
|
|
71
|
+
const light = shade(shirt, 24);
|
|
72
|
+
const legs = "#39415a";
|
|
73
|
+
const shoe = "#1f2433";
|
|
74
|
+
const style = agent.seed % 5; // short, long, bun, spiky, cap
|
|
75
|
+
const outfit = (agent.seed >>> 5) % 3; // tee, hoodie, shirt and tie
|
|
76
|
+
|
|
77
|
+
// Shadow, and a highlight under whoever is selected.
|
|
78
|
+
c.fillStyle = "rgba(0,0,0,0.28)";
|
|
79
|
+
c.fillRect(Math.round(left + 1), Math.round(feet - 1), 10, 2);
|
|
80
|
+
if (selected) {
|
|
81
|
+
c.fillStyle = "rgba(255,255,255,0.45)";
|
|
82
|
+
c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
|
|
83
|
+
}
|
|
84
|
+
if (agent.state === "blocked") {
|
|
85
|
+
c.fillStyle = Math.floor(time * 3) % 2 === 0 ? "rgba(239,68,68,0.55)" : "rgba(239,68,68,0.22)";
|
|
86
|
+
c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const lift = bob + party;
|
|
90
|
+
const parts: Part[] = [];
|
|
91
|
+
const bodyTop = feet - (seated ? 13 : 13) + lift + breathe * 0;
|
|
92
|
+
const headTop = bodyTop - 8 + breathe;
|
|
58
93
|
|
|
94
|
+
// Chair behind someone sitting at a desk.
|
|
59
95
|
if (seated) {
|
|
60
|
-
rect(c, "#1f2937",
|
|
61
|
-
rect(c, "#374151",
|
|
62
|
-
rect(c, "#111827",
|
|
96
|
+
rect(c, "#1f2937", left + 1, agent.y * TILE + 3, 10, 9);
|
|
97
|
+
rect(c, "#374151", left + 2, agent.y * TILE + 11, 8, 3);
|
|
98
|
+
rect(c, "#111827", left + 5, agent.y * TILE + 14, 2, 2);
|
|
63
99
|
}
|
|
64
100
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
// Legs
|
|
102
|
+
if (!seated) {
|
|
103
|
+
const legTop = bodyTop + 7;
|
|
104
|
+
if (side) {
|
|
105
|
+
const swing = step === 1 ? 2 : step === 3 ? -2 : 0;
|
|
106
|
+
parts.push([legs, left + 4 + swing, legTop, 3, 5], [shoe, left + 4 + swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
|
|
107
|
+
parts.push([shade(legs, -12), left + 5 - swing, legTop, 3, 5], [shoe, left + 5 - swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
|
|
108
|
+
} else {
|
|
109
|
+
const leftUp = step === 1 ? 1 : 0;
|
|
110
|
+
const rightUp = step === 3 ? 1 : 0;
|
|
111
|
+
parts.push([legs, left + 2, legTop, 3, 5 - leftUp], [shoe, left + 2, legTop + 5 - leftUp, 3, 1]);
|
|
112
|
+
parts.push([legs, left + 7, legTop, 3, 5 - rightUp], [shoe, left + 7, legTop + 5 - rightUp, 3, 1]);
|
|
113
|
+
}
|
|
72
114
|
}
|
|
73
115
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
116
|
+
// Body and arms
|
|
117
|
+
const swingArm = walking ? (step === 1 ? 1 : step === 3 ? -1 : 0) : 0;
|
|
118
|
+
if (side) {
|
|
119
|
+
parts.push([shirt, left + 3, bodyTop, 6, 7], [dark, left + 3, bodyTop + 6, 6, 1]);
|
|
120
|
+
parts.push([dark, left + 5 + swingArm, bodyTop + 1, 2, 5], [skin, left + 5 + swingArm, bodyTop + 6, 2, 1]);
|
|
121
|
+
} else {
|
|
122
|
+
parts.push([shirt, left + 1, bodyTop, 10, 7], [dark, left + 1, bodyTop + 6, 10, 1]);
|
|
123
|
+
const typing = seated && agent.state === "working";
|
|
124
|
+
const type = Math.floor(time * 10 + agent.seed) % 2;
|
|
125
|
+
parts.push([dark, left - 1, bodyTop + 1 + swingArm + (typing ? -type : 0), 2, 5]);
|
|
126
|
+
parts.push([dark, left + 11, bodyTop + 1 - swingArm + (typing ? type - 1 : 0), 2, 5]);
|
|
127
|
+
parts.push([skin, left - 1, bodyTop + 6 + swingArm + (typing ? -type : 0), 2, 1]);
|
|
128
|
+
parts.push([skin, left + 11, bodyTop + 6 - swingArm + (typing ? type - 1 : 0), 2, 1]);
|
|
129
|
+
if (!back && outfit === 2) parts.push(["#f3f4f6", left + 4, bodyTop, 4, 3], [agent.seed % 2 ? "#ef4444" : "#1d4ed8", left + 5, bodyTop, 2, 5]);
|
|
130
|
+
if (!back && outfit === 1) parts.push([light, left + 3, bodyTop, 6, 2], [dark, left + 5, bodyTop + 2, 2, 4]);
|
|
131
|
+
if (back && outfit === 1) parts.push([light, left + 2, bodyTop, 8, 3]);
|
|
132
|
+
}
|
|
81
133
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
134
|
+
// Head
|
|
135
|
+
parts.push([skin, left + 1, headTop, 10, 8]);
|
|
136
|
+
if (back) {
|
|
137
|
+
parts.push([hair, left + 1, headTop, 10, 7]);
|
|
138
|
+
} else if (side) {
|
|
139
|
+
const backX = flip ? left + 7 : left + 1;
|
|
140
|
+
parts.push([hair, left + 1, headTop, 10, 3], [hair, backX, headTop, 4, 6]);
|
|
141
|
+
const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
|
|
142
|
+
parts.push([blink ? shade(skin, -40) : "#1a1a24", flip ? left + 3 : left + 8, headTop + 4, 1, blink ? 1 : 2]);
|
|
143
|
+
} else {
|
|
144
|
+
parts.push([hair, left + 1, headTop, 10, 3], [hair, left + 1, headTop + 3, 1, 2], [hair, left + 10, headTop + 3, 1, 2]);
|
|
145
|
+
const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
|
|
146
|
+
parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 3, headTop + 4, 2, blink ? 1 : 2]);
|
|
147
|
+
parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 7, headTop + 4, 2, blink ? 1 : 2]);
|
|
148
|
+
parts.push([shade(skin, -28), left + 5, headTop + 6, 2, 1]);
|
|
149
|
+
}
|
|
150
|
+
if (style === 1) parts.push([hair, left, headTop + 2, 2, 8], [hair, left + 10, headTop + 2, 2, 8]); // long
|
|
151
|
+
if (style === 2) parts.push([hair, left + 4, headTop - 3, 4, 3]); // bun
|
|
152
|
+
if (style === 3) parts.push([hair, left + 2, headTop - 2, 2, 2], [hair, left + 5, headTop - 3, 2, 3], [hair, left + 8, headTop - 2, 2, 2]); // spiky
|
|
153
|
+
if (style === 4) { // cap
|
|
154
|
+
parts.push([light, left, headTop - 1, 12, 3]);
|
|
155
|
+
if (!back && !side) parts.push([dark, left + 1, headTop + 2, 10, 1]);
|
|
156
|
+
if (side) parts.push([dark, flip ? left - 2 : left + 9, headTop + 1, 5, 1]);
|
|
157
|
+
}
|
|
158
|
+
drawParts(c, parts);
|
|
159
|
+
|
|
160
|
+
if (agent.party > 0) {
|
|
161
|
+
const colors = ["#f59e0b", "#ec4899", "#22c55e", "#3b82f6", "#a855f7"];
|
|
162
|
+
for (let i = 0; i < 10; i++) {
|
|
163
|
+
const t = (time * 1.1 + i * 0.137) % 1;
|
|
164
|
+
const angle = i * 2.4 + agent.seed;
|
|
165
|
+
rect(c, colors[i % colors.length] as string, left + 6 + Math.cos(angle) * (4 + t * 14), feet - 22 - t * 6 + t * t * 26, 2, 2);
|
|
166
|
+
}
|
|
90
167
|
}
|
|
91
168
|
}
|
|
92
169
|
|
|
93
170
|
function bubble(c: Ctx, x: number, y: number, draw: (x: number, y: number) => void): void {
|
|
94
|
-
rect(c,
|
|
171
|
+
rect(c, OUTLINE, x - 1, y - 1, 13, 11);
|
|
95
172
|
rect(c, "#ffffff", x, y, 11, 9);
|
|
96
173
|
rect(c, "#ffffff", x + 4, y + 9, 3, 1);
|
|
97
|
-
rect(c,
|
|
174
|
+
rect(c, OUTLINE, x + 4, y + 10, 3, 1);
|
|
98
175
|
draw(x, y);
|
|
99
176
|
}
|
|
100
177
|
|
|
101
178
|
function drawStatus(c: Ctx, agent: Agent, time: number): void {
|
|
102
179
|
const x = Math.round(agent.x * TILE + 2);
|
|
103
|
-
const y = Math.round(agent.y * TILE -
|
|
104
|
-
const pulse = Math.floor(time * 2 + agent.seed % 3) % 2 === 0;
|
|
180
|
+
const y = Math.round(agent.y * TILE - 22 + (isSeated(agent) ? 2 : -2));
|
|
181
|
+
const pulse = Math.floor(time * 2 + (agent.seed % 3)) % 2 === 0;
|
|
105
182
|
if (agent.state === "blocked") {
|
|
106
183
|
bubble(c, x, y, (bx, by) => {
|
|
107
184
|
rect(c, pulse ? "#dc2626" : "#991b1b", bx + 5, by + 1, 2, 5);
|
|
@@ -122,232 +199,200 @@ function drawStatus(c: Ctx, agent: Agent, time: number): void {
|
|
|
122
199
|
bubble(c, x, y, (bx, by) => {
|
|
123
200
|
rect(c, color, bx + 5, by + 1, 1, 7); rect(c, color, bx + 2, by + 3, 7, 2); rect(c, color, bx + 4, by + 2, 3, 5);
|
|
124
201
|
});
|
|
202
|
+
} else if (agent.state === "idle" && !agent.walking && agent.seed % 3 === 0) {
|
|
203
|
+
bubble(c, x, y, (bx, by) => {
|
|
204
|
+
rect(c, "#92400e", bx + 3, by + 3, 4, 4); rect(c, "#d97706", bx + 3, by + 3, 4, 1); rect(c, "#92400e", bx + 7, by + 4, 1, 2);
|
|
205
|
+
if (pulse) rect(c, "#9ca3af", bx + 4, by + 1, 1, 1);
|
|
206
|
+
});
|
|
125
207
|
}
|
|
126
208
|
}
|
|
127
209
|
|
|
128
|
-
// --- The
|
|
210
|
+
// --- The building ----------------------------------------------------------------------------------------
|
|
129
211
|
|
|
130
|
-
function drawFloor(c: Ctx, room: Room
|
|
212
|
+
function drawFloor(c: Ctx, room: Room): void {
|
|
131
213
|
for (let ty = room.y; ty < room.y + room.h; ty++) {
|
|
132
214
|
for (let tx = room.x; tx < room.x + room.w; tx++) {
|
|
133
215
|
const x = tx * TILE;
|
|
134
216
|
const y = ty * TILE;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
217
|
+
const noise = (tx * 73 + ty * 151) % 17;
|
|
218
|
+
switch (room.floor) {
|
|
219
|
+
case "carpet":
|
|
220
|
+
rect(c, "#4c4f7a", x, y, TILE, TILE);
|
|
221
|
+
for (let i = 0; i < 8; i++) rect(c, i % 2 ? "#55588a" : "#454874", x + ((tx * 7 + i * 5) % 15), y + ((ty * 3 + i * 7) % 15), 1, 1);
|
|
222
|
+
break;
|
|
223
|
+
case "tile":
|
|
224
|
+
rect(c, (tx + ty) % 2 === 0 ? "#d6dee8" : "#c2ccd8", x, y, TILE, TILE);
|
|
225
|
+
rect(c, "#aab5c2", x, y + 15, TILE, 1);
|
|
226
|
+
rect(c, "#aab5c2", x + 15, y, 1, TILE);
|
|
227
|
+
rect(c, "#e8eef5", x + 1, y + 1, 5, 1);
|
|
228
|
+
break;
|
|
229
|
+
case "stone":
|
|
230
|
+
rect(c, noise % 3 === 0 ? "#8d93a3" : "#9aa0b0", x, y, TILE, TILE);
|
|
231
|
+
rect(c, "#7a8090", x, y + 15, TILE, 1);
|
|
232
|
+
rect(c, "#7a8090", x + (ty % 2 ? 7 : 15), y, 1, 15);
|
|
233
|
+
rect(c, "#adb3c2", x + 2, y + 2, 3, 1);
|
|
234
|
+
break;
|
|
235
|
+
case "concrete":
|
|
236
|
+
rect(c, "#6b7280", x, y, TILE, TILE);
|
|
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);
|
|
238
|
+
if (noise === 3) rect(c, "#596070", x + 3, y + 8, 8, 1);
|
|
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
|
+
}
|
|
254
|
+
case "grass":
|
|
255
|
+
rect(c, "#3f8f4f", x, y, TILE, TILE);
|
|
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);
|
|
257
|
+
break;
|
|
258
|
+
default: {
|
|
259
|
+
const plank = ty % 2 === 0 ? "#a57448" : "#9c6c42";
|
|
260
|
+
rect(c, plank, x, y, TILE, TILE);
|
|
261
|
+
rect(c, "#8a5d36", x, y + 7, TILE, 1);
|
|
262
|
+
rect(c, "#8a5d36", x, y + 15, TILE, 1);
|
|
263
|
+
rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 8 : 0)) % 16), y, 1, 7);
|
|
264
|
+
rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 0 : 8)) % 16), y + 8, 1, 7);
|
|
265
|
+
rect(c, "#b4835a", x + (noise % 9) + 2, y + 3, 4, 1);
|
|
266
|
+
rect(c, "#b4835a", x + (noise % 7) + 4, y + 11, 3, 1);
|
|
267
|
+
}
|
|
149
268
|
}
|
|
150
269
|
}
|
|
151
270
|
}
|
|
152
|
-
void palette;
|
|
153
271
|
}
|
|
154
272
|
|
|
155
|
-
const WALL_TOP = "#
|
|
156
|
-
const WALL_FACE = "#
|
|
157
|
-
const
|
|
273
|
+
const WALL_TOP = "#2a2d48";
|
|
274
|
+
const WALL_FACE = "#5c628f";
|
|
275
|
+
const WALL_DARK = "#4a4f78";
|
|
276
|
+
const WALL_LIGHT = "#6d74a4";
|
|
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
|
+
}
|
|
158
294
|
|
|
159
|
-
function drawWalls(c: Ctx, world: World, palette: Palette): void {
|
|
295
|
+
function drawWalls(c: Ctx, world: World, palette: Palette, daylight: Daylight): void {
|
|
160
296
|
const open = new Set<string>();
|
|
161
297
|
for (const room of world.rooms) {
|
|
162
298
|
for (let y = room.y; y < room.y + room.h; y++) for (let x = room.x; x < room.x + room.w; x++) open.add(`${x},${y}`);
|
|
163
299
|
}
|
|
164
|
-
|
|
300
|
+
const doors = new Set(world.doors.map((door) => `${door.x},${door.y}`));
|
|
165
301
|
|
|
166
302
|
for (let ty = 0; ty < world.rows; ty++) {
|
|
167
303
|
for (let tx = 0; tx < world.cols; tx++) {
|
|
168
|
-
|
|
304
|
+
const key = `${tx},${ty}`;
|
|
305
|
+
if (open.has(key) || doors.has(key)) continue;
|
|
169
306
|
const x = tx * TILE;
|
|
170
307
|
const y = ty * TILE;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
318
|
+
if (open.has(`${tx},${ty + 1}`)) {
|
|
174
319
|
rect(c, WALL_FACE, x, y, TILE, TILE);
|
|
175
|
-
rect(c,
|
|
176
|
-
rect(c,
|
|
177
|
-
|
|
320
|
+
rect(c, WALL_LIGHT, x, y, TILE, 1);
|
|
321
|
+
rect(c, WALL_DARK, x, y + 12, TILE, 4);
|
|
322
|
+
rect(c, palette.primary, x, y + 11, TILE, 1);
|
|
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)) {
|
|
325
|
+
// A window, in the colour of the sky right now.
|
|
178
326
|
rect(c, "#1f2937", x + 1, y + 1, 14, 9);
|
|
179
|
-
rect(c,
|
|
180
|
-
rect(c,
|
|
327
|
+
rect(c, daylight.sky, x + 2, y + 2, 12, 7);
|
|
328
|
+
rect(c, shade(daylight.sky, 40), x + 3, y + 3, 4, 2);
|
|
181
329
|
rect(c, "#1f2937", x + 8, y + 2, 1, 7);
|
|
330
|
+
if (daylight.night > 0.3) { rect(c, "#fef3c7", x + 4, y + 3, 1, 1); rect(c, "#fef3c7", x + 11, y + 5, 1, 1); }
|
|
182
331
|
}
|
|
183
332
|
} else {
|
|
184
333
|
rect(c, WALL_TOP, x, y, TILE, TILE);
|
|
334
|
+
if (open.has(`${tx},${ty - 1}`)) rect(c, "#20233a", x, y, TILE, 3);
|
|
335
|
+
if (open.has(`${tx - 1},${ty}`)) rect(c, "#20233a", x, y, 2, TILE);
|
|
336
|
+
if (open.has(`${tx + 1},${ty}`)) rect(c, "#20233a", x + 14, y, 2, TILE);
|
|
185
337
|
}
|
|
186
338
|
}
|
|
187
339
|
}
|
|
188
340
|
for (const door of world.doors) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
rect(c, "#8a5a33", x + 1, y + 11, 46, 11);
|
|
197
|
-
rect(c, "#a06c3f", x + 1, y + 11, 46, 2);
|
|
198
|
-
rect(c, "#4a2e1a", x + 3, y + 22, 3, 9);
|
|
199
|
-
rect(c, "#4a2e1a", x + 42, y + 22, 3, 9);
|
|
200
|
-
// Monitor
|
|
201
|
-
rect(c, "#111827", x + 15, y - 1, 18, 13);
|
|
202
|
-
rect(c, "#374151", x + 22, y + 12, 4, 2);
|
|
203
|
-
if (on) {
|
|
204
|
-
rect(c, "#0f2a2e", x + 16, y, 16, 11);
|
|
205
|
-
const scroll = Math.floor(time * 3 + seed % 13);
|
|
206
|
-
for (let line = 0; line < 4; line++) {
|
|
207
|
-
const width = 3 + ((seed >> line) + (scroll + line) * 7) % 11;
|
|
208
|
-
rect(c, line % 2 === 0 ? palette.primary : "#7dd3fc", x + 17, y + 1 + line * 2.5, width, 1);
|
|
209
|
-
}
|
|
210
|
-
c.fillStyle = "rgba(125,211,252,0.10)";
|
|
211
|
-
c.fillRect(x + 11, y + 11, 26, 8);
|
|
212
|
-
} else {
|
|
213
|
-
rect(c, "#1f2937", x + 16, y, 16, 11);
|
|
214
|
-
rect(c, "#374151", x + 18, y + 2, 3, 1);
|
|
215
|
-
}
|
|
216
|
-
// Keyboard and mug
|
|
217
|
-
rect(c, "#d1d5db", x + 17, y + 15, 14, 4);
|
|
218
|
-
rect(c, "#9ca3af", x + 18, y + 16, 12, 1);
|
|
219
|
-
rect(c, "#f3f4f6", x + 38, y + 14, 4, 4);
|
|
220
|
-
rect(c, palette.primary, x + 38, y + 16, 4, 1);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function drawBoard(c: Ctx, x: number, y: number, world: World): void {
|
|
224
|
-
rect(c, "#2b2118", x, y - 11, 64, 26);
|
|
225
|
-
rect(c, "#d9c29c", x + 2, y - 9, 60, 22);
|
|
226
|
-
const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
|
|
227
|
-
const per = 8;
|
|
228
|
-
world.tasks.slice(0, per * 2).forEach((task, index) => {
|
|
229
|
-
const col = index % per;
|
|
230
|
-
const row = Math.floor(index / per);
|
|
231
|
-
rect(c, "#00000033", x + 5 + col * 7, y - 6 + row * 9, 6, 7);
|
|
232
|
-
rect(c, colors[task.state], x + 4 + col * 7, y - 7 + row * 9, 6, 7);
|
|
233
|
-
rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 9, 4, 1);
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function drawFurniture(c: Ctx, item: Item, world: World, time: number, palette: Palette): void {
|
|
238
|
-
const x = item.x * TILE;
|
|
239
|
-
const y = item.y * TILE;
|
|
240
|
-
switch (item.type) {
|
|
241
|
-
case "desk": {
|
|
242
|
-
const seatX = item.x + 1;
|
|
243
|
-
const seatY = item.y + 2;
|
|
244
|
-
const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a) && a.state !== "blocked");
|
|
245
|
-
drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette);
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
case "coffee":
|
|
249
|
-
rect(c, "#374151", x, y + 2, 32, 14);
|
|
250
|
-
rect(c, "#6b7280", x, y + 2, 32, 2);
|
|
251
|
-
rect(c, "#111827", x + 4, y - 8, 14, 14);
|
|
252
|
-
rect(c, "#ef4444", x + 15, y - 5, 2, 2);
|
|
253
|
-
rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
|
|
254
|
-
rect(c, "#f3f4f6", x + 22, y - 1, 5, 5);
|
|
255
|
-
for (let i = 0; i < 3; i++) {
|
|
256
|
-
c.fillStyle = "rgba(255,255,255,0.45)";
|
|
257
|
-
c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
|
|
258
|
-
}
|
|
259
|
-
break;
|
|
260
|
-
case "water":
|
|
261
|
-
rect(c, "#e5e7eb", x + 3, y + 3, 10, 13);
|
|
262
|
-
rect(c, "#7dd3fc", x + 4, y - 7, 8, 10);
|
|
263
|
-
rect(c, "#bae6fd", x + 5, y - 6, 2, 8);
|
|
264
|
-
rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
|
|
265
|
-
break;
|
|
266
|
-
case "sofa":
|
|
267
|
-
rect(c, palette.primary, x + 1, y - 6, 46, 9);
|
|
268
|
-
rect(c, "#00000030", x + 1, y + 1, 46, 2);
|
|
269
|
-
rect(c, "#4b5563", x, y + 3, 48, 11);
|
|
270
|
-
rect(c, "#6b7280", x + 3, y + 3, 20, 7);
|
|
271
|
-
rect(c, "#6b7280", x + 25, y + 3, 20, 7);
|
|
272
|
-
rect(c, "#374151", x, y + 12, 48, 3);
|
|
273
|
-
break;
|
|
274
|
-
case "plant":
|
|
275
|
-
rect(c, "#92400e", x + 4, y + 9, 8, 7);
|
|
276
|
-
rect(c, "#b45309", x + 3, y + 8, 10, 2);
|
|
277
|
-
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) {
|
|
278
|
-
rect(c, "#15803d", x + lx, y + ly, lw, lh);
|
|
279
|
-
}
|
|
280
|
-
rect(c, "#22c55e", x + 7, y - 3, 2, 8);
|
|
281
|
-
rect(c, "#22c55e", x + 3, y + 1, 2, 3);
|
|
282
|
-
break;
|
|
283
|
-
case "board":
|
|
284
|
-
drawBoard(c, x, y, world);
|
|
285
|
-
break;
|
|
286
|
-
case "shelf":
|
|
287
|
-
rect(c, "#5b3a21", x, y - 12, 32, 28);
|
|
288
|
-
for (let shelf = 0; shelf < 2; shelf++) {
|
|
289
|
-
rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
|
|
290
|
-
for (let book = 0; book < 8; book++) {
|
|
291
|
-
const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf) % 5] as string;
|
|
292
|
-
rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
break;
|
|
296
|
-
case "server":
|
|
297
|
-
rect(c, "#111827", x + 1, y - 12, 14, 28);
|
|
298
|
-
for (let i = 0; i < 5; i++) {
|
|
299
|
-
rect(c, "#1f2937", x + 2, y - 11 + i * 5, 12, 4);
|
|
300
|
-
const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
|
|
301
|
-
rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
|
|
302
|
-
rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
|
|
303
|
-
}
|
|
304
|
-
break;
|
|
305
|
-
case "table":
|
|
306
|
-
rect(c, "#57534e", x + 2, y + 14, 44, 14);
|
|
307
|
-
rect(c, "#78716c", x + 2, y + 3, 44, 14);
|
|
308
|
-
rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
|
|
309
|
-
rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
|
|
310
|
-
rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
|
|
311
|
-
break;
|
|
312
|
-
case "rug":
|
|
313
|
-
rect(c, palette.primary, x, y, 64, 48);
|
|
314
|
-
c.fillStyle = "rgba(0,0,0,0.35)";
|
|
315
|
-
c.fillRect(x + 3, y + 3, 58, 42);
|
|
316
|
-
rect(c, palette.primary, x + 6, y + 6, 52, 1);
|
|
317
|
-
rect(c, palette.primary, x + 6, y + 41, 52, 1);
|
|
318
|
-
break;
|
|
341
|
+
const x = door.x * TILE;
|
|
342
|
+
const y = door.y * TILE;
|
|
343
|
+
const horizontal = open.has(`${door.x},${door.y - 1}`) || open.has(`${door.x},${door.y + 1}`);
|
|
344
|
+
rect(c, "#7c5637", x, y, TILE, TILE);
|
|
345
|
+
for (let i = 2; i < 16; i += 4) rect(c, "#6b4a2f", horizontal ? x : x + i, horizontal ? y + i : y, horizontal ? TILE : 1, horizontal ? 1 : TILE);
|
|
346
|
+
if (horizontal) { rect(c, WALL_DARK, x, y, 2, TILE); rect(c, WALL_DARK, x + 14, y, 2, TILE); }
|
|
347
|
+
else { rect(c, WALL_DARK, x, y, TILE, 2); rect(c, WALL_DARK, x, y + 14, TILE, 2); }
|
|
319
348
|
}
|
|
320
|
-
void FURNITURE;
|
|
321
349
|
}
|
|
322
350
|
|
|
323
351
|
function drawLabels(c: Ctx, world: World): void {
|
|
324
|
-
c.font =
|
|
352
|
+
c.font = LABEL_FONT;
|
|
325
353
|
c.textBaseline = "top";
|
|
326
354
|
for (const room of world.rooms) {
|
|
327
355
|
if (!room.name) continue;
|
|
328
356
|
const label = room.name.toUpperCase();
|
|
329
|
-
|
|
357
|
+
const width = Math.ceil(c.measureText(label).width) + 6;
|
|
330
358
|
const x = room.x * TILE + 3;
|
|
331
|
-
const y = room.y * TILE -
|
|
332
|
-
c
|
|
333
|
-
c
|
|
334
|
-
c.fillStyle = "
|
|
335
|
-
c.fillText(label, x, y);
|
|
359
|
+
const y = room.y * TILE - 13;
|
|
360
|
+
rect(c, "#1c1f33", x, y, width, 10);
|
|
361
|
+
rect(c, "#2f3350", x + 1, y + 1, width - 2, 8);
|
|
362
|
+
c.fillStyle = "#f8fafc";
|
|
363
|
+
c.fillText(label, x + 3, y + 1.5);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Everything that doesn't move. Redrawn only when the building or the hour changes. */
|
|
368
|
+
export function drawBuilding(c: Ctx, world: World, palette: Palette, daylight: Daylight): void {
|
|
369
|
+
rect(c, WALL_TOP, 0, 0, world.cols * TILE, world.rows * TILE);
|
|
370
|
+
for (const room of world.rooms) drawFloor(c, room);
|
|
371
|
+
drawWalls(c, world, palette, daylight);
|
|
372
|
+
for (const item of world.furniture) if (item.type === "rug") painterFor(world).furniture(c, item, world, 0, palette);
|
|
373
|
+
for (const item of world.furniture) if (specOf(world, item).wall) painterFor(world).furniture(c, item, world, 0, palette);
|
|
374
|
+
// Light from the windows falling across the floor.
|
|
375
|
+
if (daylight.sun !== "rgba(0,0,0,0)") {
|
|
376
|
+
c.fillStyle = daylight.sun;
|
|
377
|
+
for (const room of world.rooms) {
|
|
378
|
+
for (let tx = room.x; tx < room.x + room.w; tx++) {
|
|
379
|
+
if (tx % 4 !== 3) continue;
|
|
380
|
+
c.fillRect(tx * TILE + 2, room.y * TILE, 12, Math.min(room.h, 3) * TILE);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
336
383
|
}
|
|
337
384
|
}
|
|
338
385
|
|
|
339
386
|
function nameTag(c: Ctx, agent: Agent, selected: boolean, width: number): void {
|
|
340
|
-
|
|
341
|
-
c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
|
|
387
|
+
c.font = LABEL_FONT;
|
|
342
388
|
c.textBaseline = "top";
|
|
343
|
-
const w = Math.ceil(c.measureText(name).width) + 5;
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
const y = Math.round(agent.y * TILE + 15);
|
|
389
|
+
const w = Math.ceil(c.measureText(agent.name).width) + 5;
|
|
390
|
+
const x = Math.round(Math.max(1, Math.min(width - w - 1, agent.x * TILE + 8 - w / 2)));
|
|
391
|
+
const y = Math.round(agent.y * TILE + 16);
|
|
347
392
|
c.fillStyle = selected ? "#ffffff" : "rgba(17,24,39,0.82)";
|
|
348
393
|
c.fillRect(x, y, w, 10);
|
|
349
394
|
c.fillStyle = selected ? "#111827" : "#ffffff";
|
|
350
|
-
c.fillText(name, x + 2.5, y + 1.5);
|
|
395
|
+
c.fillText(agent.name, x + 2.5, y + 1.5);
|
|
351
396
|
}
|
|
352
397
|
|
|
353
398
|
function wrap(c: Ctx, text: string, max: number): string[] {
|
|
@@ -368,62 +413,97 @@ function wrap(c: Ctx, text: string, max: number): string[] {
|
|
|
368
413
|
|
|
369
414
|
const STATE_LABEL: Record<string, string> = { working: "Working", thinking: "Thinking", blocked: "Blocked", reviewing: "Reviewing", idle: "On a break", done: "Finished", away: "Away" };
|
|
370
415
|
|
|
371
|
-
function drawCard(c: Ctx, agent: Agent,
|
|
372
|
-
c.font =
|
|
416
|
+
function drawCard(c: Ctx, agent: Agent, world: World): void {
|
|
417
|
+
c.font = BODY_FONT;
|
|
373
418
|
c.textBaseline = "top";
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
|
|
419
|
+
const room = world.rooms.find((candidate) => candidate.id === agent.area);
|
|
420
|
+
const body = wrap(c, agent.task || STATE_LABEL[agent.state] || "", 110);
|
|
421
|
+
const w = 120;
|
|
422
|
+
const h = 18 + body.length * 9 + (room ? 9 : 0) + 3;
|
|
423
|
+
const x = Math.round(Math.max(2, Math.min(world.cols * TILE - w - 2, agent.x * TILE + 8 - w / 2)));
|
|
424
|
+
const y = Math.round(Math.max(2, agent.y * TILE - h - 24));
|
|
425
|
+
rect(c, OUTLINE, x - 1, y - 1, w + 2, h + 2);
|
|
380
426
|
rect(c, "#ffffff", x, y, w, h);
|
|
381
427
|
c.fillStyle = "#111827";
|
|
382
|
-
c.font =
|
|
383
|
-
c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0,
|
|
428
|
+
c.font = LABEL_FONT;
|
|
429
|
+
c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0, 28), x + 4, y + 4);
|
|
384
430
|
c.fillStyle = "#374151";
|
|
385
|
-
c.font =
|
|
431
|
+
c.font = BODY_FONT;
|
|
386
432
|
body.forEach((line, index) => c.fillText(line, x + 4, y + 15 + index * 9));
|
|
433
|
+
if (room) {
|
|
434
|
+
c.fillStyle = "#6b7280";
|
|
435
|
+
c.fillText(`In ${room.name}`.slice(0, 28), x + 4, y + 15 + body.length * 9);
|
|
436
|
+
}
|
|
387
437
|
}
|
|
388
438
|
|
|
389
439
|
function drawProgress(c: Ctx, world: World, palette: Palette): void {
|
|
390
440
|
if (world.tasks.length === 0) return;
|
|
391
441
|
const done = world.tasks.filter((task) => task.state === "done").length;
|
|
392
|
-
|
|
393
|
-
c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
|
|
442
|
+
c.font = LABEL_FONT;
|
|
394
443
|
c.textBaseline = "top";
|
|
395
444
|
const w = 96;
|
|
396
445
|
rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
|
|
397
446
|
c.fillStyle = "#ffffff";
|
|
398
|
-
c.fillText(
|
|
447
|
+
c.fillText(`${world.pack.words.quests} ${done}/${world.tasks.length}`, 8, 6.5);
|
|
399
448
|
rect(c, "#374151", 8, 16, w - 8, 4);
|
|
400
449
|
rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
|
|
401
450
|
}
|
|
402
451
|
|
|
403
|
-
|
|
452
|
+
/** Everything that moves. `building` is the layer from drawBuilding. */
|
|
453
|
+
export function drawWorld(
|
|
454
|
+
c: Ctx,
|
|
455
|
+
world: World,
|
|
456
|
+
time: number,
|
|
457
|
+
palette: Palette,
|
|
458
|
+
daylight: Daylight,
|
|
459
|
+
building: CanvasImageSource | undefined,
|
|
460
|
+
selected?: string,
|
|
461
|
+
): void {
|
|
404
462
|
const width = world.cols * TILE;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
463
|
+
const height = world.rows * TILE;
|
|
464
|
+
if (building) c.drawImage(building, 0, 0, width, height);
|
|
465
|
+
else drawBuilding(c, world, palette, daylight);
|
|
466
|
+
|
|
467
|
+
// On the floor, so people and furniture stay in front of the lines.
|
|
468
|
+
if (world.showLinks && world.links.length > 0) {
|
|
469
|
+
world.routes ??= linkRoutes(world);
|
|
470
|
+
drawLinks(c, world.routes, time);
|
|
471
|
+
}
|
|
408
472
|
|
|
409
|
-
|
|
410
|
-
for (const item of world.furniture) if (item.type === "rug") drawFurniture(c, item, world, time, palette);
|
|
473
|
+
const painter = painterFor(world);
|
|
411
474
|
const scene: { depth: number; draw: () => void }[] = [];
|
|
412
475
|
for (const item of world.furniture) {
|
|
413
|
-
|
|
414
|
-
|
|
476
|
+
const spec = specOf(world, item);
|
|
477
|
+
if (spec.solid === false) continue;
|
|
478
|
+
scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => painterFor(world).furniture(c, item, world, time, palette) });
|
|
415
479
|
}
|
|
416
480
|
const shown = world.agents.filter((agent) => agent.state !== "away");
|
|
417
|
-
for (const agent of shown) {
|
|
418
|
-
scene.push({ depth: agent.y * TILE + 14, draw: () => drawAgent(c, agent, time, agent.id === selected) });
|
|
419
|
-
}
|
|
481
|
+
for (const agent of shown) scene.push({ depth: agent.y * TILE + 14, draw: () => drawAgent(c, agent, time, agent.id === selected) });
|
|
420
482
|
scene.sort((a, b) => a.depth - b.depth);
|
|
421
483
|
for (const item of scene) item.draw();
|
|
422
484
|
|
|
485
|
+
// Night: the room dims, and screens and lamps light it.
|
|
486
|
+
if (daylight.night > 0) {
|
|
487
|
+
c.fillStyle = `rgba(12,14,48,${daylight.night})`;
|
|
488
|
+
c.fillRect(0, 0, width, height);
|
|
489
|
+
c.save();
|
|
490
|
+
c.globalCompositeOperation = "lighter";
|
|
491
|
+
const glow = (x: number, y: number, radius: number, color: string): void => {
|
|
492
|
+
const gradient = c.createRadialGradient(x, y, 1, x, y, radius);
|
|
493
|
+
gradient.addColorStop(0, color);
|
|
494
|
+
gradient.addColorStop(1, "rgba(0,0,0,0)");
|
|
495
|
+
c.fillStyle = gradient;
|
|
496
|
+
c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
497
|
+
};
|
|
498
|
+
for (const item of world.furniture) painter.glow(item, world, glow);
|
|
499
|
+
c.restore();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// On top, so tall furniture against a wall can't hide a room's name.
|
|
423
503
|
drawLabels(c, world);
|
|
424
504
|
for (const agent of shown) nameTag(c, agent, agent.id === selected, width);
|
|
425
505
|
for (const agent of shown) if (agent.id !== selected) drawStatus(c, agent, time);
|
|
426
506
|
drawProgress(c, world, palette);
|
|
427
507
|
const picked = shown.find((agent) => agent.id === selected);
|
|
428
|
-
if (picked) drawCard(c, picked,
|
|
508
|
+
if (picked) drawCard(c, picked, world);
|
|
429
509
|
}
|