@blkc-code/render-canvas 0.1.0 → 0.3.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 +13 -3
- package/dist/draw.d.ts.map +1 -1
- package/dist/draw.js +529 -196
- package/dist/draw.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/office.d.ts +23 -2
- package/dist/office.d.ts.map +1 -1
- package/dist/office.js +132 -20
- package/dist/office.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/runtime.txt +1 -1
- package/dist/types.d.ts +9 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/world.d.ts +15 -3
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +138 -32
- package/dist/world.js.map +1 -1
- package/package.json +1 -1
- package/src/draw.ts +489 -193
- package/src/index.ts +1 -1
- package/src/office.ts +139 -21
- package/src/runtime.ts +4 -1
- package/src/types.ts +20 -4
- package/src/world.ts +139 -36
package/src/draw.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
7
|
import { FURNITURE, isSeated, type Agent, type Item, type Room, type World } from "./world.js";
|
|
@@ -11,97 +12,174 @@ export interface Palette { primary: string; background: string; text: string; ac
|
|
|
11
12
|
|
|
12
13
|
type Ctx = CanvasRenderingContext2D;
|
|
13
14
|
|
|
15
|
+
const LABEL_FONT = "bold 7px ui-monospace, Menlo, Consolas, monospace";
|
|
16
|
+
const BODY_FONT = "7px ui-monospace, Menlo, Consolas, monospace";
|
|
17
|
+
const OUTLINE = "#14141f";
|
|
18
|
+
|
|
14
19
|
const rect = (c: Ctx, color: string, x: number, y: number, w: number, h: number): void => {
|
|
15
20
|
c.fillStyle = color;
|
|
16
21
|
c.fillRect(Math.round(x), Math.round(y), w, h);
|
|
17
22
|
};
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
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...."];
|
|
32
|
-
|
|
33
|
-
function drawMap(c: Ctx, map: string[], x: number, y: number, colors: Record<string, string>, flip = false): void {
|
|
34
|
-
map.forEach((row, ry) => {
|
|
35
|
-
for (let rx = 0; rx < row.length; rx++) {
|
|
36
|
-
const key = row[flip ? row.length - 1 - rx : rx] as string;
|
|
37
|
-
const color = colors[key];
|
|
38
|
-
if (color) rect(c, color, x + rx, y + ry, 1, 1);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function shade(hex: string, amount: number): string {
|
|
44
|
-
const n = parseInt(hex.slice(1).padEnd(6, hex.slice(-1)), 16);
|
|
24
|
+
function shade(color: string, amount: number): string {
|
|
25
|
+
if (!color.startsWith("#")) return color;
|
|
26
|
+
const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
|
|
27
|
+
const n = parseInt(hex.slice(1, 7), 16);
|
|
45
28
|
const channel = (shift: number): number => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
|
|
46
29
|
return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
|
|
47
30
|
}
|
|
48
31
|
|
|
32
|
+
// --- Time of day, from the viewer's own clock ------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface Daylight { sky: string; sun: string; night: number }
|
|
35
|
+
|
|
36
|
+
export function daylightAt(hour: number): Daylight {
|
|
37
|
+
if (hour >= 7 && hour < 17) return { sky: "#8ed0ff", sun: "rgba(255,244,214,0.10)", night: 0 };
|
|
38
|
+
if (hour >= 17 && hour < 20) return { sky: "#ffae70", sun: "rgba(255,170,110,0.13)", night: 0.14 };
|
|
39
|
+
if (hour >= 5 && hour < 7) return { sky: "#ffc9a3", sun: "rgba(255,200,170,0.08)", night: 0.16 };
|
|
40
|
+
return { sky: "#16204a", sun: "rgba(0,0,0,0)", night: 0.24 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Characters ------------------------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
type Part = [color: string, x: number, y: number, w: number, h: number];
|
|
46
|
+
|
|
47
|
+
/** Parts draw with one outline around the whole figure, so characters read clearly on any floor. */
|
|
48
|
+
function drawParts(c: Ctx, parts: Part[]): void {
|
|
49
|
+
c.fillStyle = OUTLINE;
|
|
50
|
+
for (const [, x, y, w, h] of parts) c.fillRect(Math.round(x) - 1, Math.round(y) - 1, w + 2, h + 2);
|
|
51
|
+
for (const [color, x, y, w, h] of parts) rect(c, color, x, y, w, h);
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
function drawAgent(c: Ctx, agent: Agent, time: number, selected: boolean): void {
|
|
50
|
-
const
|
|
55
|
+
const left = agent.x * TILE + 2; // sprite is 12 wide inside a 16 tile
|
|
56
|
+
const feet = agent.y * TILE + 15;
|
|
51
57
|
const seated = isSeated(agent);
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
+
const walking = agent.walking;
|
|
59
|
+
const side = agent.facing === "left" || agent.facing === "right";
|
|
60
|
+
const back = agent.facing === "up";
|
|
61
|
+
const flip = agent.facing === "left";
|
|
62
|
+
const phase = Math.floor(time * 9 + (agent.seed % 5));
|
|
63
|
+
const step = walking ? phase % 4 : 0; // 0 pass, 1 left forward, 2 pass, 3 right forward
|
|
64
|
+
const bob = walking && step % 2 === 1 ? -1 : 0;
|
|
65
|
+
const breathe = !walking && !seated && Math.floor(time * 1.4 + (agent.seed % 7)) % 2 === 0 ? 1 : 0;
|
|
66
|
+
const party = agent.party > 0 ? -Math.abs(Math.round(Math.sin(time * 12) * 3)) : 0;
|
|
67
|
+
|
|
68
|
+
const skin = agent.skin;
|
|
69
|
+
const hair = agent.hair;
|
|
70
|
+
const shirt = agent.shirt;
|
|
71
|
+
const dark = shade(shirt, -40);
|
|
72
|
+
const light = shade(shirt, 24);
|
|
73
|
+
const legs = "#39415a";
|
|
74
|
+
const shoe = "#1f2433";
|
|
75
|
+
const style = agent.seed % 5; // short, long, bun, spiky, cap
|
|
76
|
+
const outfit = (agent.seed >>> 5) % 3; // tee, hoodie, shirt and tie
|
|
77
|
+
|
|
78
|
+
// Shadow, and a highlight under whoever is selected.
|
|
79
|
+
c.fillStyle = "rgba(0,0,0,0.28)";
|
|
80
|
+
c.fillRect(Math.round(left + 1), Math.round(feet - 1), 10, 2);
|
|
81
|
+
if (selected) {
|
|
82
|
+
c.fillStyle = "rgba(255,255,255,0.45)";
|
|
83
|
+
c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
|
|
84
|
+
}
|
|
85
|
+
if (agent.state === "blocked") {
|
|
86
|
+
c.fillStyle = Math.floor(time * 3) % 2 === 0 ? "rgba(239,68,68,0.55)" : "rgba(239,68,68,0.22)";
|
|
87
|
+
c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
|
|
88
|
+
}
|
|
58
89
|
|
|
90
|
+
const lift = bob + party;
|
|
91
|
+
const parts: Part[] = [];
|
|
92
|
+
const bodyTop = feet - (seated ? 13 : 13) + lift + breathe * 0;
|
|
93
|
+
const headTop = bodyTop - 8 + breathe;
|
|
94
|
+
|
|
95
|
+
// Chair behind someone sitting at a desk.
|
|
59
96
|
if (seated) {
|
|
60
|
-
rect(c, "#1f2937",
|
|
61
|
-
rect(c, "#374151",
|
|
62
|
-
rect(c, "#111827",
|
|
97
|
+
rect(c, "#1f2937", left + 1, agent.y * TILE + 3, 10, 9);
|
|
98
|
+
rect(c, "#374151", left + 2, agent.y * TILE + 11, 8, 3);
|
|
99
|
+
rect(c, "#111827", left + 5, agent.y * TILE + 14, 2, 2);
|
|
63
100
|
}
|
|
64
101
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
102
|
+
// Legs
|
|
103
|
+
if (!seated) {
|
|
104
|
+
const legTop = bodyTop + 7;
|
|
105
|
+
if (side) {
|
|
106
|
+
const swing = step === 1 ? 2 : step === 3 ? -2 : 0;
|
|
107
|
+
parts.push([legs, left + 4 + swing, legTop, 3, 5], [shoe, left + 4 + swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
|
|
108
|
+
parts.push([shade(legs, -12), left + 5 - swing, legTop, 3, 5], [shoe, left + 5 - swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
|
|
109
|
+
} else {
|
|
110
|
+
const leftUp = step === 1 ? 1 : 0;
|
|
111
|
+
const rightUp = step === 3 ? 1 : 0;
|
|
112
|
+
parts.push([legs, left + 2, legTop, 3, 5 - leftUp], [shoe, left + 2, legTop + 5 - leftUp, 3, 1]);
|
|
113
|
+
parts.push([legs, left + 7, legTop, 3, 5 - rightUp], [shoe, left + 7, legTop + 5 - rightUp, 3, 1]);
|
|
114
|
+
}
|
|
72
115
|
}
|
|
73
116
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
117
|
+
// Body and arms
|
|
118
|
+
const swingArm = walking ? (step === 1 ? 1 : step === 3 ? -1 : 0) : 0;
|
|
119
|
+
if (side) {
|
|
120
|
+
parts.push([shirt, left + 3, bodyTop, 6, 7], [dark, left + 3, bodyTop + 6, 6, 1]);
|
|
121
|
+
parts.push([dark, left + 5 + swingArm, bodyTop + 1, 2, 5], [skin, left + 5 + swingArm, bodyTop + 6, 2, 1]);
|
|
122
|
+
} else {
|
|
123
|
+
parts.push([shirt, left + 1, bodyTop, 10, 7], [dark, left + 1, bodyTop + 6, 10, 1]);
|
|
124
|
+
const typing = seated && agent.state === "working";
|
|
125
|
+
const type = Math.floor(time * 10 + agent.seed) % 2;
|
|
126
|
+
parts.push([dark, left - 1, bodyTop + 1 + swingArm + (typing ? -type : 0), 2, 5]);
|
|
127
|
+
parts.push([dark, left + 11, bodyTop + 1 - swingArm + (typing ? type - 1 : 0), 2, 5]);
|
|
128
|
+
parts.push([skin, left - 1, bodyTop + 6 + swingArm + (typing ? -type : 0), 2, 1]);
|
|
129
|
+
parts.push([skin, left + 11, bodyTop + 6 - swingArm + (typing ? type - 1 : 0), 2, 1]);
|
|
130
|
+
if (!back && outfit === 2) parts.push(["#f3f4f6", left + 4, bodyTop, 4, 3], [agent.seed % 2 ? "#ef4444" : "#1d4ed8", left + 5, bodyTop, 2, 5]);
|
|
131
|
+
if (!back && outfit === 1) parts.push([light, left + 3, bodyTop, 6, 2], [dark, left + 5, bodyTop + 2, 2, 4]);
|
|
132
|
+
if (back && outfit === 1) parts.push([light, left + 2, bodyTop, 8, 3]);
|
|
133
|
+
}
|
|
81
134
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
135
|
+
// Head
|
|
136
|
+
parts.push([skin, left + 1, headTop, 10, 8]);
|
|
137
|
+
if (back) {
|
|
138
|
+
parts.push([hair, left + 1, headTop, 10, 7]);
|
|
139
|
+
} else if (side) {
|
|
140
|
+
const backX = flip ? left + 7 : left + 1;
|
|
141
|
+
parts.push([hair, left + 1, headTop, 10, 3], [hair, backX, headTop, 4, 6]);
|
|
142
|
+
const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
|
|
143
|
+
parts.push([blink ? shade(skin, -40) : "#1a1a24", flip ? left + 3 : left + 8, headTop + 4, 1, blink ? 1 : 2]);
|
|
144
|
+
} else {
|
|
145
|
+
parts.push([hair, left + 1, headTop, 10, 3], [hair, left + 1, headTop + 3, 1, 2], [hair, left + 10, headTop + 3, 1, 2]);
|
|
146
|
+
const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
|
|
147
|
+
parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 3, headTop + 4, 2, blink ? 1 : 2]);
|
|
148
|
+
parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 7, headTop + 4, 2, blink ? 1 : 2]);
|
|
149
|
+
parts.push([shade(skin, -28), left + 5, headTop + 6, 2, 1]);
|
|
150
|
+
}
|
|
151
|
+
if (style === 1) parts.push([hair, left, headTop + 2, 2, 8], [hair, left + 10, headTop + 2, 2, 8]); // long
|
|
152
|
+
if (style === 2) parts.push([hair, left + 4, headTop - 3, 4, 3]); // bun
|
|
153
|
+
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
|
|
154
|
+
if (style === 4) { // cap
|
|
155
|
+
parts.push([light, left, headTop - 1, 12, 3]);
|
|
156
|
+
if (!back && !side) parts.push([dark, left + 1, headTop + 2, 10, 1]);
|
|
157
|
+
if (side) parts.push([dark, flip ? left - 2 : left + 9, headTop + 1, 5, 1]);
|
|
158
|
+
}
|
|
159
|
+
drawParts(c, parts);
|
|
160
|
+
|
|
161
|
+
if (agent.party > 0) {
|
|
162
|
+
const colors = ["#f59e0b", "#ec4899", "#22c55e", "#3b82f6", "#a855f7"];
|
|
163
|
+
for (let i = 0; i < 10; i++) {
|
|
164
|
+
const t = (time * 1.1 + i * 0.137) % 1;
|
|
165
|
+
const angle = i * 2.4 + agent.seed;
|
|
166
|
+
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);
|
|
167
|
+
}
|
|
90
168
|
}
|
|
91
169
|
}
|
|
92
170
|
|
|
93
171
|
function bubble(c: Ctx, x: number, y: number, draw: (x: number, y: number) => void): void {
|
|
94
|
-
rect(c,
|
|
172
|
+
rect(c, OUTLINE, x - 1, y - 1, 13, 11);
|
|
95
173
|
rect(c, "#ffffff", x, y, 11, 9);
|
|
96
174
|
rect(c, "#ffffff", x + 4, y + 9, 3, 1);
|
|
97
|
-
rect(c,
|
|
175
|
+
rect(c, OUTLINE, x + 4, y + 10, 3, 1);
|
|
98
176
|
draw(x, y);
|
|
99
177
|
}
|
|
100
178
|
|
|
101
179
|
function drawStatus(c: Ctx, agent: Agent, time: number): void {
|
|
102
180
|
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;
|
|
181
|
+
const y = Math.round(agent.y * TILE - 22 + (isSeated(agent) ? 2 : -2));
|
|
182
|
+
const pulse = Math.floor(time * 2 + (agent.seed % 3)) % 2 === 0;
|
|
105
183
|
if (agent.state === "blocked") {
|
|
106
184
|
bubble(c, x, y, (bx, by) => {
|
|
107
185
|
rect(c, pulse ? "#dc2626" : "#991b1b", bx + 5, by + 1, 2, 5);
|
|
@@ -122,115 +200,203 @@ function drawStatus(c: Ctx, agent: Agent, time: number): void {
|
|
|
122
200
|
bubble(c, x, y, (bx, by) => {
|
|
123
201
|
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
202
|
});
|
|
203
|
+
} else if (agent.state === "idle" && !agent.walking && agent.seed % 3 === 0) {
|
|
204
|
+
bubble(c, x, y, (bx, by) => {
|
|
205
|
+
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);
|
|
206
|
+
if (pulse) rect(c, "#9ca3af", bx + 4, by + 1, 1, 1);
|
|
207
|
+
});
|
|
125
208
|
}
|
|
126
209
|
}
|
|
127
210
|
|
|
128
|
-
// --- The
|
|
211
|
+
// --- The building ----------------------------------------------------------------------------------------
|
|
129
212
|
|
|
130
|
-
function drawFloor(c: Ctx, room: Room
|
|
213
|
+
function drawFloor(c: Ctx, room: Room): void {
|
|
131
214
|
for (let ty = room.y; ty < room.y + room.h; ty++) {
|
|
132
215
|
for (let tx = room.x; tx < room.x + room.w; tx++) {
|
|
133
216
|
const x = tx * TILE;
|
|
134
217
|
const y = ty * TILE;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
218
|
+
const noise = (tx * 73 + ty * 151) % 17;
|
|
219
|
+
switch (room.floor) {
|
|
220
|
+
case "carpet":
|
|
221
|
+
rect(c, "#4c4f7a", x, y, TILE, TILE);
|
|
222
|
+
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);
|
|
223
|
+
break;
|
|
224
|
+
case "tile":
|
|
225
|
+
rect(c, (tx + ty) % 2 === 0 ? "#d6dee8" : "#c2ccd8", x, y, TILE, TILE);
|
|
226
|
+
rect(c, "#aab5c2", x, y + 15, TILE, 1);
|
|
227
|
+
rect(c, "#aab5c2", x + 15, y, 1, TILE);
|
|
228
|
+
rect(c, "#e8eef5", x + 1, y + 1, 5, 1);
|
|
229
|
+
break;
|
|
230
|
+
case "stone":
|
|
231
|
+
rect(c, noise % 3 === 0 ? "#8d93a3" : "#9aa0b0", x, y, TILE, TILE);
|
|
232
|
+
rect(c, "#7a8090", x, y + 15, TILE, 1);
|
|
233
|
+
rect(c, "#7a8090", x + (ty % 2 ? 7 : 15), y, 1, 15);
|
|
234
|
+
rect(c, "#adb3c2", x + 2, y + 2, 3, 1);
|
|
235
|
+
break;
|
|
236
|
+
case "concrete":
|
|
237
|
+
rect(c, "#6b7280", x, y, TILE, TILE);
|
|
238
|
+
for (let i = 0; i < 5; i++) rect(c, i % 2 ? "#747b89" : "#636a78", x + ((tx * 11 + i * 3) % 15), y + ((ty * 5 + i * 5) % 15), 1, 1);
|
|
239
|
+
if (noise === 3) rect(c, "#596070", x + 3, y + 8, 8, 1);
|
|
240
|
+
break;
|
|
241
|
+
case "grass":
|
|
242
|
+
rect(c, "#3f8f4f", x, y, TILE, TILE);
|
|
243
|
+
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);
|
|
244
|
+
break;
|
|
245
|
+
default: {
|
|
246
|
+
const plank = ty % 2 === 0 ? "#a57448" : "#9c6c42";
|
|
247
|
+
rect(c, plank, x, y, TILE, TILE);
|
|
248
|
+
rect(c, "#8a5d36", x, y + 7, TILE, 1);
|
|
249
|
+
rect(c, "#8a5d36", x, y + 15, TILE, 1);
|
|
250
|
+
rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 8 : 0)) % 16), y, 1, 7);
|
|
251
|
+
rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 0 : 8)) % 16), y + 8, 1, 7);
|
|
252
|
+
rect(c, "#b4835a", x + (noise % 9) + 2, y + 3, 4, 1);
|
|
253
|
+
rect(c, "#b4835a", x + (noise % 7) + 4, y + 11, 3, 1);
|
|
254
|
+
}
|
|
149
255
|
}
|
|
150
256
|
}
|
|
151
257
|
}
|
|
152
|
-
void palette;
|
|
153
258
|
}
|
|
154
259
|
|
|
155
|
-
const WALL_TOP = "#
|
|
156
|
-
const WALL_FACE = "#
|
|
157
|
-
const
|
|
260
|
+
const WALL_TOP = "#2a2d48";
|
|
261
|
+
const WALL_FACE = "#5c628f";
|
|
262
|
+
const WALL_DARK = "#4a4f78";
|
|
263
|
+
const WALL_LIGHT = "#6d74a4";
|
|
158
264
|
|
|
159
|
-
function drawWalls(c: Ctx, world: World, palette: Palette): void {
|
|
265
|
+
function drawWalls(c: Ctx, world: World, palette: Palette, daylight: Daylight): void {
|
|
160
266
|
const open = new Set<string>();
|
|
161
267
|
for (const room of world.rooms) {
|
|
162
268
|
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
269
|
}
|
|
164
|
-
|
|
270
|
+
const doors = new Set(world.doors.map((door) => `${door.x},${door.y}`));
|
|
165
271
|
|
|
166
272
|
for (let ty = 0; ty < world.rows; ty++) {
|
|
167
273
|
for (let tx = 0; tx < world.cols; tx++) {
|
|
168
|
-
|
|
274
|
+
const key = `${tx},${ty}`;
|
|
275
|
+
if (open.has(key) || doors.has(key)) continue;
|
|
169
276
|
const x = tx * TILE;
|
|
170
277
|
const y = ty * TILE;
|
|
171
|
-
|
|
172
|
-
if (faceBelow) {
|
|
173
|
-
// The wall's front, with a band in the Kit's colour and a window every few tiles.
|
|
278
|
+
if (open.has(`${tx},${ty + 1}`)) {
|
|
174
279
|
rect(c, WALL_FACE, x, y, TILE, TILE);
|
|
175
|
-
rect(c,
|
|
176
|
-
rect(c,
|
|
177
|
-
|
|
280
|
+
rect(c, WALL_LIGHT, x, y, TILE, 1);
|
|
281
|
+
rect(c, WALL_DARK, x, y + 12, TILE, 4);
|
|
282
|
+
rect(c, palette.primary, x, y + 11, TILE, 1);
|
|
283
|
+
if (tx % 4 === 3 && !world.furniture.some((item) => FURNITURE[item.type].wall && item.x === tx && item.y === ty)) {
|
|
284
|
+
// A window, in the colour of the sky right now.
|
|
178
285
|
rect(c, "#1f2937", x + 1, y + 1, 14, 9);
|
|
179
|
-
rect(c,
|
|
180
|
-
rect(c,
|
|
286
|
+
rect(c, daylight.sky, x + 2, y + 2, 12, 7);
|
|
287
|
+
rect(c, shade(daylight.sky, 40), x + 3, y + 3, 4, 2);
|
|
181
288
|
rect(c, "#1f2937", x + 8, y + 2, 1, 7);
|
|
289
|
+
if (daylight.night > 0.3) { rect(c, "#fef3c7", x + 4, y + 3, 1, 1); rect(c, "#fef3c7", x + 11, y + 5, 1, 1); }
|
|
182
290
|
}
|
|
183
291
|
} else {
|
|
184
292
|
rect(c, WALL_TOP, x, y, TILE, TILE);
|
|
293
|
+
if (open.has(`${tx},${ty - 1}`)) rect(c, "#20233a", x, y, TILE, 3);
|
|
294
|
+
if (open.has(`${tx - 1},${ty}`)) rect(c, "#20233a", x, y, 2, TILE);
|
|
295
|
+
if (open.has(`${tx + 1},${ty}`)) rect(c, "#20233a", x + 14, y, 2, TILE);
|
|
185
296
|
}
|
|
186
297
|
}
|
|
187
298
|
}
|
|
188
299
|
for (const door of world.doors) {
|
|
189
|
-
|
|
190
|
-
|
|
300
|
+
const x = door.x * TILE;
|
|
301
|
+
const y = door.y * TILE;
|
|
302
|
+
const horizontal = open.has(`${door.x},${door.y - 1}`) || open.has(`${door.x},${door.y + 1}`);
|
|
303
|
+
rect(c, "#7c5637", x, y, TILE, TILE);
|
|
304
|
+
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);
|
|
305
|
+
if (horizontal) { rect(c, WALL_DARK, x, y, 2, TILE); rect(c, WALL_DARK, x + 14, y, 2, TILE); }
|
|
306
|
+
else { rect(c, WALL_DARK, x, y, TILE, 2); rect(c, WALL_DARK, x, y + 14, TILE, 2); }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function drawLabels(c: Ctx, world: World): void {
|
|
311
|
+
c.font = LABEL_FONT;
|
|
312
|
+
c.textBaseline = "top";
|
|
313
|
+
for (const room of world.rooms) {
|
|
314
|
+
if (!room.name) continue;
|
|
315
|
+
const label = room.name.toUpperCase();
|
|
316
|
+
const width = Math.ceil(c.measureText(label).width) + 6;
|
|
317
|
+
const x = room.x * TILE + 3;
|
|
318
|
+
const y = room.y * TILE - 13;
|
|
319
|
+
rect(c, "#1c1f33", x, y, width, 10);
|
|
320
|
+
rect(c, "#2f3350", x + 1, y + 1, width - 2, 8);
|
|
321
|
+
c.fillStyle = "#f8fafc";
|
|
322
|
+
c.fillText(label, x + 3, y + 1.5);
|
|
191
323
|
}
|
|
192
324
|
}
|
|
193
325
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
rect(c,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
326
|
+
/** Everything that doesn't move. Redrawn only when the building or the hour changes. */
|
|
327
|
+
export function drawBuilding(c: Ctx, world: World, palette: Palette, daylight: Daylight): void {
|
|
328
|
+
rect(c, WALL_TOP, 0, 0, world.cols * TILE, world.rows * TILE);
|
|
329
|
+
for (const room of world.rooms) drawFloor(c, room);
|
|
330
|
+
drawWalls(c, world, palette, daylight);
|
|
331
|
+
for (const item of world.furniture) if (item.type === "rug") drawFurniture(c, item, world, 0, palette);
|
|
332
|
+
for (const item of world.furniture) if (FURNITURE[item.type].wall) drawFurniture(c, item, world, 0, palette);
|
|
333
|
+
// Light from the windows falling across the floor.
|
|
334
|
+
if (daylight.sun !== "rgba(0,0,0,0)") {
|
|
335
|
+
c.fillStyle = daylight.sun;
|
|
336
|
+
for (const room of world.rooms) {
|
|
337
|
+
for (let tx = room.x; tx < room.x + room.w; tx++) {
|
|
338
|
+
if (tx % 4 !== 3) continue;
|
|
339
|
+
c.fillRect(tx * TILE + 2, room.y * TILE, 12, Math.min(room.h, 3) * TILE);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function drawDesk(c: Ctx, x: number, y: number, on: boolean, time: number, seed: number, palette: Palette, blocked: boolean): void {
|
|
346
|
+
rect(c, "#55351d", x + 1, y + 22, 46, 8);
|
|
347
|
+
rect(c, "#6b4427", x + 1, y + 13, 46, 10);
|
|
348
|
+
rect(c, "#8a5a33", x + 1, y + 10, 46, 12);
|
|
349
|
+
rect(c, "#a87545", x + 1, y + 10, 46, 2);
|
|
350
|
+
rect(c, "#3b2413", x + 3, y + 22, 3, 9);
|
|
351
|
+
rect(c, "#3b2413", x + 42, y + 22, 3, 9);
|
|
352
|
+
rect(c, "#55351d", x + 30, y + 23, 12, 7);
|
|
353
|
+
rect(c, "#7c5637", x + 31, y + 24, 10, 2);
|
|
354
|
+
rect(c, "#d4a24e", x + 35, y + 24, 2, 1);
|
|
355
|
+
rect(c, "#7c5637", x + 31, y + 27, 10, 2);
|
|
356
|
+
rect(c, "#d4a24e", x + 35, y + 27, 2, 1);
|
|
200
357
|
// Monitor
|
|
201
|
-
rect(c,
|
|
358
|
+
rect(c, OUTLINE, x + 14, y - 3, 20, 15);
|
|
202
359
|
rect(c, "#374151", x + 22, y + 12, 4, 2);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
360
|
+
rect(c, "#4b5563", x + 19, y + 14, 10, 1);
|
|
361
|
+
if (blocked) {
|
|
362
|
+
rect(c, "#3f0f14", x + 15, y - 2, 18, 13);
|
|
363
|
+
if (Math.floor(time * 2) % 2 === 0) { rect(c, "#ef4444", x + 23, y, 2, 6); rect(c, "#ef4444", x + 23, y + 7, 2, 2); }
|
|
364
|
+
} else if (on) {
|
|
365
|
+
rect(c, "#0f2a2e", x + 15, y - 2, 18, 13);
|
|
366
|
+
const scroll = Math.floor(time * 3 + (seed % 13));
|
|
367
|
+
for (let line = 0; line < 5; line++) {
|
|
368
|
+
const width = 3 + (((seed >> line) & 7) + (scroll + line) * 7) % 12;
|
|
369
|
+
rect(c, line % 3 === 0 ? palette.primary : line % 3 === 1 ? "#7dd3fc" : "#fcd34d", x + 16 + (line % 2) * 2, y - 1 + line * 2.2, width, 1);
|
|
209
370
|
}
|
|
210
|
-
c.fillStyle = "rgba(125,211,252,0.
|
|
211
|
-
c.fillRect(x +
|
|
371
|
+
c.fillStyle = "rgba(125,211,252,0.12)";
|
|
372
|
+
c.fillRect(x + 10, y + 11, 28, 8);
|
|
212
373
|
} else {
|
|
213
|
-
rect(c, "#1f2937", x +
|
|
214
|
-
rect(c, "#374151", x +
|
|
374
|
+
rect(c, "#1f2937", x + 15, y - 2, 18, 13);
|
|
375
|
+
rect(c, "#374151", x + 17, y, 3, 1);
|
|
215
376
|
}
|
|
216
|
-
|
|
217
|
-
rect(c, "#
|
|
218
|
-
rect(c, "#
|
|
219
|
-
rect(c, "#f3f4f6", x +
|
|
220
|
-
rect(c, palette.primary, x +
|
|
377
|
+
rect(c, "#d1d5db", x + 16, y + 15, 16, 5);
|
|
378
|
+
for (let row = 0; row < 2; row++) for (let key = 0; key < 7; key++) rect(c, "#9ca3af", x + 17 + key * 2, y + 16 + row * 2, 1, 1);
|
|
379
|
+
rect(c, "#e5e7eb", x + 34, y + 16, 3, 4);
|
|
380
|
+
rect(c, "#f3f4f6", x + 40, y + 13, 5, 5);
|
|
381
|
+
rect(c, palette.primary, x + 40, y + 15, 5, 1);
|
|
382
|
+
rect(c, "#fde68a", x + 4, y + 13, 7, 6);
|
|
383
|
+
rect(c, "#fbbf24", x + 5, y + 15, 5, 1);
|
|
221
384
|
}
|
|
222
385
|
|
|
223
386
|
function drawBoard(c: Ctx, x: number, y: number, world: World): void {
|
|
224
|
-
rect(c, "#
|
|
225
|
-
rect(c, "#d9c29c", x + 2, y -
|
|
387
|
+
rect(c, "#3a2c1e", x, y - 12, 64, 27);
|
|
388
|
+
rect(c, "#d9c29c", x + 2, y - 10, 60, 23);
|
|
389
|
+
for (let i = 0; i < 40; i++) rect(c, "#c9b08a", x + 3 + (i * 37) % 58, y - 9 + (i * 13) % 21, 1, 1);
|
|
226
390
|
const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
|
|
227
391
|
const per = 8;
|
|
228
392
|
world.tasks.slice(0, per * 2).forEach((task, index) => {
|
|
229
393
|
const col = index % per;
|
|
230
394
|
const row = Math.floor(index / per);
|
|
231
|
-
rect(c, "#00000033", x + 5 + col * 7, y -
|
|
232
|
-
rect(c, colors[task.state], x + 4 + col * 7, y -
|
|
233
|
-
rect(c, "#
|
|
395
|
+
rect(c, "#00000033", x + 5 + col * 7, y - 7 + row * 10, 6, 8);
|
|
396
|
+
rect(c, colors[task.state], x + 4 + col * 7, y - 8 + row * 10, 6, 8);
|
|
397
|
+
rect(c, "#ef4444", x + 6 + col * 7, y - 8 + row * 10, 2, 1);
|
|
398
|
+
rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 10, 4, 1);
|
|
399
|
+
rect(c, "#00000040", x + 5 + col * 7, y - 3 + row * 10, 3, 1);
|
|
234
400
|
});
|
|
235
401
|
}
|
|
236
402
|
|
|
@@ -241,113 +407,209 @@ function drawFurniture(c: Ctx, item: Item, world: World, time: number, palette:
|
|
|
241
407
|
case "desk": {
|
|
242
408
|
const seatX = item.x + 1;
|
|
243
409
|
const seatY = item.y + 2;
|
|
244
|
-
const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a)
|
|
245
|
-
drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette);
|
|
410
|
+
const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a));
|
|
411
|
+
drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette, user?.state === "blocked");
|
|
246
412
|
break;
|
|
247
413
|
}
|
|
248
414
|
case "coffee":
|
|
249
415
|
rect(c, "#374151", x, y + 2, 32, 14);
|
|
250
416
|
rect(c, "#6b7280", x, y + 2, 32, 2);
|
|
251
|
-
rect(c,
|
|
252
|
-
rect(c, "#
|
|
417
|
+
rect(c, OUTLINE, x + 4, y - 9, 15, 15);
|
|
418
|
+
rect(c, "#27272a", x + 5, y - 8, 13, 13);
|
|
419
|
+
rect(c, Math.floor(time * 2) % 2 ? "#ef4444" : "#7f1d1d", x + 15, y - 6, 2, 2);
|
|
420
|
+
rect(c, "#52525b", x + 7, y - 6, 6, 4);
|
|
253
421
|
rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
|
|
254
|
-
rect(c, "#f3f4f6", x +
|
|
422
|
+
rect(c, "#f3f4f6", x + 23, y - 1, 5, 5);
|
|
423
|
+
rect(c, "#92400e", x + 24, y, 3, 1);
|
|
255
424
|
for (let i = 0; i < 3; i++) {
|
|
256
|
-
c.fillStyle = "rgba(255,255,255,0.
|
|
425
|
+
c.fillStyle = "rgba(255,255,255,0.5)";
|
|
257
426
|
c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
|
|
258
427
|
}
|
|
259
428
|
break;
|
|
260
429
|
case "water":
|
|
261
|
-
rect(c, "#
|
|
262
|
-
rect(c, "#
|
|
263
|
-
rect(c, "#
|
|
430
|
+
rect(c, "#d1d5db", x + 3, y + 3, 10, 13);
|
|
431
|
+
rect(c, "#9ca3af", x + 3, y + 14, 10, 2);
|
|
432
|
+
rect(c, "#60a5fa", x + 4, y - 8, 8, 11);
|
|
433
|
+
rect(c, "#bae6fd", x + 5, y - 7, 2, 9);
|
|
264
434
|
rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
|
|
435
|
+
rect(c, "#ef4444", x + 9, y + 6, 2, 2);
|
|
436
|
+
if (Math.floor(time * 0.4 + item.x) % 5 === 0) rect(c, "#e0f2fe", x + 8, y - 2 - ((time * 6) % 7), 1, 1);
|
|
265
437
|
break;
|
|
266
438
|
case "sofa":
|
|
267
|
-
rect(c, palette.primary, x + 1, y -
|
|
268
|
-
rect(c,
|
|
439
|
+
rect(c, shade(palette.primary, -60), x + 1, y - 7, 46, 11);
|
|
440
|
+
rect(c, shade(palette.primary, -30), x + 2, y - 6, 44, 3);
|
|
269
441
|
rect(c, "#4b5563", x, y + 3, 48, 11);
|
|
270
442
|
rect(c, "#6b7280", x + 3, y + 3, 20, 7);
|
|
271
443
|
rect(c, "#6b7280", x + 25, y + 3, 20, 7);
|
|
272
444
|
rect(c, "#374151", x, y + 12, 48, 3);
|
|
445
|
+
rect(c, "#374151", x - 1, y - 2, 4, 16);
|
|
446
|
+
rect(c, "#374151", x + 45, y - 2, 4, 16);
|
|
273
447
|
break;
|
|
274
448
|
case "plant":
|
|
275
449
|
rect(c, "#92400e", x + 4, y + 9, 8, 7);
|
|
276
450
|
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
|
-
}
|
|
451
|
+
for (const [lx, ly, lw, lh] of [[6, -3, 4, 11], [2, 0, 5, 6], [9, 1, 5, 6], [4, -6, 3, 6], [9, -5, 3, 5]] as const) rect(c, "#15803d", x + lx, y + ly, lw, lh);
|
|
280
452
|
rect(c, "#22c55e", x + 7, y - 3, 2, 8);
|
|
281
453
|
rect(c, "#22c55e", x + 3, y + 1, 2, 3);
|
|
454
|
+
rect(c, "#4ade80", x + 10, y - 4, 1, 3);
|
|
455
|
+
break;
|
|
456
|
+
case "fern":
|
|
457
|
+
rect(c, "#57534e", x + 3, y + 10, 10, 6);
|
|
458
|
+
rect(c, "#78716c", x + 2, y + 9, 12, 2);
|
|
459
|
+
for (let i = 0; i < 7; i++) {
|
|
460
|
+
const lean = i - 3;
|
|
461
|
+
for (let j = 0; j < 5; j++) rect(c, j % 2 ? "#16a34a" : "#15803d", x + 7 + lean * (j / 2 + 1), y + 8 - j * 2 + Math.abs(lean), 2, 2);
|
|
462
|
+
}
|
|
282
463
|
break;
|
|
283
464
|
case "board":
|
|
284
465
|
drawBoard(c, x, y, world);
|
|
285
466
|
break;
|
|
467
|
+
case "whiteboard":
|
|
468
|
+
rect(c, "#9ca3af", x, y - 10, 48, 22);
|
|
469
|
+
rect(c, "#f9fafb", x + 2, y - 8, 44, 18);
|
|
470
|
+
rect(c, "#3b82f6", x + 5, y - 5, 14, 1); rect(c, "#3b82f6", x + 5, y - 2, 9, 1);
|
|
471
|
+
rect(c, "#ef4444", x + 24, y - 6, 8, 6); rect(c, "#f9fafb", x + 25, y - 5, 6, 4);
|
|
472
|
+
rect(c, "#10b981", x + 8, y + 3, 20, 1); rect(c, "#10b981", x + 27, y + 2, 1, 3);
|
|
473
|
+
rect(c, "#6b7280", x + 2, y + 10, 44, 2);
|
|
474
|
+
break;
|
|
286
475
|
case "shelf":
|
|
287
476
|
rect(c, "#5b3a21", x, y - 12, 32, 28);
|
|
288
477
|
for (let shelf = 0; shelf < 2; shelf++) {
|
|
289
478
|
rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
|
|
290
479
|
for (let book = 0; book < 8; book++) {
|
|
291
|
-
const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf) % 5] as string;
|
|
480
|
+
const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf + item.x) % 5] as string;
|
|
292
481
|
rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
|
|
482
|
+
rect(c, shade(color, 40), x + 3 + book * 3, y - 6 + shelf * 13, 2, 1);
|
|
293
483
|
}
|
|
294
484
|
}
|
|
295
485
|
break;
|
|
296
486
|
case "server":
|
|
297
|
-
rect(c,
|
|
487
|
+
rect(c, OUTLINE, x, y - 13, 16, 29);
|
|
488
|
+
rect(c, "#1f2937", x + 1, y - 12, 14, 27);
|
|
298
489
|
for (let i = 0; i < 5; i++) {
|
|
299
|
-
rect(c, "#
|
|
490
|
+
rect(c, "#111827", x + 2, y - 11 + i * 5, 12, 4);
|
|
300
491
|
const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
|
|
301
492
|
rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
|
|
302
493
|
rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
|
|
494
|
+
rect(c, "#374151", x + 8, y - 10 + i * 5, 5, 1);
|
|
303
495
|
}
|
|
304
496
|
break;
|
|
305
497
|
case "table":
|
|
306
|
-
rect(c, "#
|
|
498
|
+
rect(c, "#44403c", x + 2, y + 16, 44, 12);
|
|
307
499
|
rect(c, "#78716c", x + 2, y + 3, 44, 14);
|
|
308
500
|
rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
|
|
309
501
|
rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
|
|
310
502
|
rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
|
|
503
|
+
rect(c, "#1f2937", x + 12, y + 8, 4, 1);
|
|
504
|
+
rect(c, "#3b82f6", x + 30, y + 10, 4, 1);
|
|
311
505
|
break;
|
|
312
506
|
case "rug":
|
|
313
507
|
rect(c, palette.primary, x, y, 64, 48);
|
|
314
|
-
c.fillStyle = "rgba(0,0,0,0.
|
|
508
|
+
c.fillStyle = "rgba(0,0,0,0.42)";
|
|
315
509
|
c.fillRect(x + 3, y + 3, 58, 42);
|
|
316
510
|
rect(c, palette.primary, x + 6, y + 6, 52, 1);
|
|
317
511
|
rect(c, palette.primary, x + 6, y + 41, 52, 1);
|
|
512
|
+
rect(c, palette.primary, x + 6, y + 6, 1, 36);
|
|
513
|
+
rect(c, palette.primary, x + 57, y + 6, 1, 36);
|
|
514
|
+
for (let i = 0; i < 6; i++) rect(c, palette.primary, x + 12 + i * 8, y + 22, 4, 4);
|
|
318
515
|
break;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
516
|
+
case "arcade":
|
|
517
|
+
rect(c, OUTLINE, x, y - 14, 16, 30);
|
|
518
|
+
rect(c, "#4c1d95", x + 1, y - 13, 14, 28);
|
|
519
|
+
rect(c, "#111827", x + 3, y - 10, 10, 9);
|
|
520
|
+
rect(c, ["#22c55e", "#ec4899", "#38bdf8"][Math.floor(time * 2) % 3] as string, x + 4 + (Math.floor(time * 4) % 7), y - 8 + (Math.floor(time * 3) % 5), 2, 2);
|
|
521
|
+
rect(c, "#f59e0b", x + 3, y - 13, 10, 2);
|
|
522
|
+
rect(c, "#27272a", x + 2, y + 1, 12, 4);
|
|
523
|
+
rect(c, "#ef4444", x + 4, y + 2, 2, 2);
|
|
524
|
+
rect(c, "#3b82f6", x + 9, y + 2, 2, 2);
|
|
525
|
+
break;
|
|
526
|
+
case "fridge":
|
|
527
|
+
rect(c, OUTLINE, x + 1, y - 12, 14, 28);
|
|
528
|
+
rect(c, "#e5e7eb", x + 2, y - 11, 12, 26);
|
|
529
|
+
rect(c, "#9ca3af", x + 2, y - 3, 12, 1);
|
|
530
|
+
rect(c, "#6b7280", x + 11, y - 8, 1, 3);
|
|
531
|
+
rect(c, "#6b7280", x + 11, y, 1, 5);
|
|
532
|
+
rect(c, "#fde68a", x + 4, y - 9, 3, 3);
|
|
533
|
+
rect(c, "#fca5a5", x + 5, y + 3, 3, 3);
|
|
534
|
+
break;
|
|
535
|
+
case "printer":
|
|
536
|
+
rect(c, "#4b5563", x + 2, y + 8, 12, 8);
|
|
537
|
+
rect(c, OUTLINE, x + 1, y - 2, 14, 11);
|
|
538
|
+
rect(c, "#d1d5db", x + 2, y - 1, 12, 9);
|
|
539
|
+
rect(c, "#f9fafb", x + 4, y - 5 - (Math.floor(time) % 4 === 0 ? 1 : 0), 8, 5);
|
|
540
|
+
rect(c, "#22c55e", x + 11, y + 1, 2, 1);
|
|
541
|
+
rect(c, "#9ca3af", x + 3, y + 5, 10, 1);
|
|
542
|
+
break;
|
|
543
|
+
case "lamp":
|
|
544
|
+
rect(c, "#374151", x + 5, y + 13, 6, 3);
|
|
545
|
+
rect(c, "#4b5563", x + 7, y - 4, 2, 17);
|
|
546
|
+
rect(c, OUTLINE, x + 2, y - 10, 12, 8);
|
|
547
|
+
rect(c, "#fde68a", x + 3, y - 9, 10, 6);
|
|
548
|
+
rect(c, "#fef3c7", x + 4, y - 8, 4, 2);
|
|
549
|
+
break;
|
|
550
|
+
case "beanbag":
|
|
551
|
+
rect(c, OUTLINE, x, y + 3, 16, 13);
|
|
552
|
+
rect(c, shade(palette.primary, -50), x + 1, y + 4, 14, 11);
|
|
553
|
+
rect(c, shade(palette.primary, -20), x + 3, y + 5, 8, 4);
|
|
554
|
+
rect(c, "#00000040", x + 1, y + 13, 14, 2);
|
|
555
|
+
break;
|
|
556
|
+
case "tv":
|
|
557
|
+
rect(c, "#3b2413", x + 2, y + 8, 44, 8);
|
|
558
|
+
rect(c, "#5b3a21", x + 2, y + 8, 44, 2);
|
|
559
|
+
rect(c, OUTLINE, x + 4, y - 14, 40, 22);
|
|
560
|
+
rect(c, "#0b1120", x + 5, y - 13, 38, 20);
|
|
561
|
+
for (let i = 0; i < 9; i++) {
|
|
562
|
+
const h = 2 + ((i * 5 + Math.floor(time * 3)) % 12);
|
|
563
|
+
rect(c, i % 3 === 0 ? palette.primary : i % 3 === 1 ? "#38bdf8" : "#f472b6", x + 8 + i * 4, y + 5 - h, 3, h);
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
case "vending":
|
|
567
|
+
rect(c, OUTLINE, x, y - 14, 16, 30);
|
|
568
|
+
rect(c, "#b91c1c", x + 1, y - 13, 14, 28);
|
|
569
|
+
rect(c, "#0f172a", x + 3, y - 11, 8, 16);
|
|
570
|
+
for (let row = 0; row < 3; row++) for (let col = 0; col < 2; col++) rect(c, ["#fbbf24", "#34d399", "#60a5fa", "#f472b6"][(row + col * 2) % 4] as string, x + 4 + col * 3, y - 10 + row * 5, 2, 4);
|
|
571
|
+
rect(c, "#d1d5db", x + 12, y - 9, 2, 6);
|
|
572
|
+
rect(c, "#111827", x + 3, y + 8, 10, 4);
|
|
573
|
+
break;
|
|
574
|
+
case "trophy":
|
|
575
|
+
rect(c, "#6b4427", x + 2, y + 8, 12, 8);
|
|
576
|
+
rect(c, "#8a5a33", x + 2, y + 8, 12, 2);
|
|
577
|
+
rect(c, "#78350f", x + 5, y + 5, 6, 3);
|
|
578
|
+
rect(c, "#f59e0b", x + 7, y + 1, 2, 4);
|
|
579
|
+
rect(c, OUTLINE, x + 3, y - 8, 10, 10);
|
|
580
|
+
rect(c, "#fbbf24", x + 4, y - 7, 8, 8);
|
|
581
|
+
rect(c, "#fde68a", x + 5, y - 6, 2, 5);
|
|
582
|
+
rect(c, "#fbbf24", x + 1, y - 6, 3, 1); rect(c, "#fbbf24", x + 12, y - 6, 3, 1);
|
|
583
|
+
break;
|
|
584
|
+
case "clock": {
|
|
585
|
+
rect(c, OUTLINE, x + 3, y + 1, 10, 10);
|
|
586
|
+
rect(c, "#f9fafb", x + 4, y + 2, 8, 8);
|
|
587
|
+
rect(c, "#1f2937", x + 8, y + 3, 1, 3);
|
|
588
|
+
rect(c, "#1f2937", x + 8, y + 6, 3, 1);
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
case "poster": {
|
|
592
|
+
const colors = ["#7c3aed", "#0ea5e9", "#db2777", "#059669"];
|
|
593
|
+
const color = colors[(item.x + item.y) % colors.length] as string;
|
|
594
|
+
rect(c, OUTLINE, x + 2, y, 12, 11);
|
|
595
|
+
rect(c, color, x + 3, y + 1, 10, 9);
|
|
596
|
+
rect(c, shade(color, 60), x + 5, y + 3, 6, 3);
|
|
597
|
+
rect(c, "#f9fafb", x + 5, y + 7, 6, 1);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
336
600
|
}
|
|
337
601
|
}
|
|
338
602
|
|
|
339
603
|
function nameTag(c: Ctx, agent: Agent, selected: boolean, width: number): void {
|
|
340
|
-
|
|
341
|
-
c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
|
|
604
|
+
c.font = LABEL_FONT;
|
|
342
605
|
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);
|
|
606
|
+
const w = Math.ceil(c.measureText(agent.name).width) + 5;
|
|
607
|
+
const x = Math.round(Math.max(1, Math.min(width - w - 1, agent.x * TILE + 8 - w / 2)));
|
|
608
|
+
const y = Math.round(agent.y * TILE + 16);
|
|
347
609
|
c.fillStyle = selected ? "#ffffff" : "rgba(17,24,39,0.82)";
|
|
348
610
|
c.fillRect(x, y, w, 10);
|
|
349
611
|
c.fillStyle = selected ? "#111827" : "#ffffff";
|
|
350
|
-
c.fillText(name, x + 2.5, y + 1.5);
|
|
612
|
+
c.fillText(agent.name, x + 2.5, y + 1.5);
|
|
351
613
|
}
|
|
352
614
|
|
|
353
615
|
function wrap(c: Ctx, text: string, max: number): string[] {
|
|
@@ -368,62 +630,96 @@ function wrap(c: Ctx, text: string, max: number): string[] {
|
|
|
368
630
|
|
|
369
631
|
const STATE_LABEL: Record<string, string> = { working: "Working", thinking: "Thinking", blocked: "Blocked", reviewing: "Reviewing", idle: "On a break", done: "Finished", away: "Away" };
|
|
370
632
|
|
|
371
|
-
function drawCard(c: Ctx, agent: Agent,
|
|
372
|
-
c.font =
|
|
633
|
+
function drawCard(c: Ctx, agent: Agent, world: World): void {
|
|
634
|
+
c.font = BODY_FONT;
|
|
373
635
|
c.textBaseline = "top";
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
|
|
636
|
+
const room = world.rooms.find((candidate) => candidate.id === agent.area);
|
|
637
|
+
const body = wrap(c, agent.task || STATE_LABEL[agent.state] || "", 110);
|
|
638
|
+
const w = 120;
|
|
639
|
+
const h = 18 + body.length * 9 + (room ? 9 : 0) + 3;
|
|
640
|
+
const x = Math.round(Math.max(2, Math.min(world.cols * TILE - w - 2, agent.x * TILE + 8 - w / 2)));
|
|
641
|
+
const y = Math.round(Math.max(2, agent.y * TILE - h - 24));
|
|
642
|
+
rect(c, OUTLINE, x - 1, y - 1, w + 2, h + 2);
|
|
380
643
|
rect(c, "#ffffff", x, y, w, h);
|
|
381
644
|
c.fillStyle = "#111827";
|
|
382
|
-
c.font =
|
|
383
|
-
c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0,
|
|
645
|
+
c.font = LABEL_FONT;
|
|
646
|
+
c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0, 28), x + 4, y + 4);
|
|
384
647
|
c.fillStyle = "#374151";
|
|
385
|
-
c.font =
|
|
648
|
+
c.font = BODY_FONT;
|
|
386
649
|
body.forEach((line, index) => c.fillText(line, x + 4, y + 15 + index * 9));
|
|
650
|
+
if (room) {
|
|
651
|
+
c.fillStyle = "#6b7280";
|
|
652
|
+
c.fillText(`In ${room.name}`.slice(0, 28), x + 4, y + 15 + body.length * 9);
|
|
653
|
+
}
|
|
387
654
|
}
|
|
388
655
|
|
|
389
656
|
function drawProgress(c: Ctx, world: World, palette: Palette): void {
|
|
390
657
|
if (world.tasks.length === 0) return;
|
|
391
658
|
const done = world.tasks.filter((task) => task.state === "done").length;
|
|
392
|
-
|
|
393
|
-
c.font = "bold 7px ui-monospace, Menlo, Consolas, monospace";
|
|
659
|
+
c.font = LABEL_FONT;
|
|
394
660
|
c.textBaseline = "top";
|
|
395
661
|
const w = 96;
|
|
396
662
|
rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
|
|
397
663
|
c.fillStyle = "#ffffff";
|
|
398
|
-
c.fillText(
|
|
664
|
+
c.fillText(`QUESTS ${done}/${world.tasks.length}`, 8, 6.5);
|
|
399
665
|
rect(c, "#374151", 8, 16, w - 8, 4);
|
|
400
666
|
rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
|
|
401
667
|
}
|
|
402
668
|
|
|
403
|
-
|
|
669
|
+
/** Everything that moves. `building` is the layer from drawBuilding. */
|
|
670
|
+
export function drawWorld(
|
|
671
|
+
c: Ctx,
|
|
672
|
+
world: World,
|
|
673
|
+
time: number,
|
|
674
|
+
palette: Palette,
|
|
675
|
+
daylight: Daylight,
|
|
676
|
+
building: CanvasImageSource | undefined,
|
|
677
|
+
selected?: string,
|
|
678
|
+
): void {
|
|
404
679
|
const width = world.cols * TILE;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
680
|
+
const height = world.rows * TILE;
|
|
681
|
+
if (building) c.drawImage(building, 0, 0, width, height);
|
|
682
|
+
else drawBuilding(c, world, palette, daylight);
|
|
408
683
|
|
|
409
|
-
// Flat things first, then everything else in depth order so people pass behind desks.
|
|
410
|
-
for (const item of world.furniture) if (item.type === "rug") drawFurniture(c, item, world, time, palette);
|
|
411
684
|
const scene: { depth: number; draw: () => void }[] = [];
|
|
412
685
|
for (const item of world.furniture) {
|
|
413
|
-
|
|
414
|
-
|
|
686
|
+
const spec = FURNITURE[item.type];
|
|
687
|
+
if (spec.solid === false) continue;
|
|
688
|
+
scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => drawFurniture(c, item, world, time, palette) });
|
|
415
689
|
}
|
|
416
690
|
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
|
-
}
|
|
691
|
+
for (const agent of shown) scene.push({ depth: agent.y * TILE + 14, draw: () => drawAgent(c, agent, time, agent.id === selected) });
|
|
420
692
|
scene.sort((a, b) => a.depth - b.depth);
|
|
421
693
|
for (const item of scene) item.draw();
|
|
422
694
|
|
|
695
|
+
// Night: the room dims, and screens and lamps light it.
|
|
696
|
+
if (daylight.night > 0) {
|
|
697
|
+
c.fillStyle = `rgba(12,14,48,${daylight.night})`;
|
|
698
|
+
c.fillRect(0, 0, width, height);
|
|
699
|
+
c.save();
|
|
700
|
+
c.globalCompositeOperation = "lighter";
|
|
701
|
+
const glow = (x: number, y: number, radius: number, color: string): void => {
|
|
702
|
+
const gradient = c.createRadialGradient(x, y, 1, x, y, radius);
|
|
703
|
+
gradient.addColorStop(0, color);
|
|
704
|
+
gradient.addColorStop(1, "rgba(0,0,0,0)");
|
|
705
|
+
c.fillStyle = gradient;
|
|
706
|
+
c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
|
707
|
+
};
|
|
708
|
+
for (const item of world.furniture) {
|
|
709
|
+
if (item.type === "lamp") glow(item.x * TILE + 8, item.y * TILE, 36, "rgba(253,224,139,0.30)");
|
|
710
|
+
if (item.type === "arcade" || item.type === "vending" || item.type === "tv") glow(item.x * TILE + 8, item.y * TILE, 26, "rgba(125,211,252,0.16)");
|
|
711
|
+
if (item.type === "desk" && world.agents.some((a) => a.seat?.x === item.x + 1 && a.seat?.y === item.y + 2 && isSeated(a))) {
|
|
712
|
+
glow(item.x * TILE + 24, item.y * TILE + 8, 30, "rgba(125,211,252,0.26)");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
c.restore();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// On top, so tall furniture against a wall can't hide a room's name.
|
|
423
719
|
drawLabels(c, world);
|
|
424
720
|
for (const agent of shown) nameTag(c, agent, agent.id === selected, width);
|
|
425
721
|
for (const agent of shown) if (agent.id !== selected) drawStatus(c, agent, time);
|
|
426
722
|
drawProgress(c, world, palette);
|
|
427
723
|
const picked = shown.find((agent) => agent.id === selected);
|
|
428
|
-
if (picked) drawCard(c, picked,
|
|
724
|
+
if (picked) drawCard(c, picked, world);
|
|
429
725
|
}
|