@kylecheng3146/agent-ops 0.6.0 → 0.7.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/README.md +55 -1
- package/dist/packages/cli/src/args.js +19 -2
- package/dist/packages/cli/src/bin.js +14 -4
- package/dist/packages/cli/src/cli.js +20 -2
- package/dist/packages/cli/src/codex-loop-process.js +19 -2
- package/dist/packages/cli/src/commands/advance.js +19 -2
- package/dist/packages/cli/src/commands/init.js +2 -0
- package/dist/packages/cli/src/commands/office.js +20 -0
- package/dist/packages/cli/src/commands/run.js +6 -2
- package/dist/packages/cli/src/commands/update.js +8 -0
- package/dist/packages/cli/src/context.js +1 -0
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +23 -2
- package/dist/packages/cli/src/loop-entry.js +2 -1
- package/dist/packages/cli/src/office-entry.js +230 -0
- package/dist/packages/cli/src/run-deps.js +9 -2
- package/dist/packages/cli/src/run-entry.js +9 -0
- package/dist/packages/cli/src/wizard.js +14 -2
- package/dist/runtime/src/adapters/agy/config.js +4 -3
- package/dist/runtime/src/adapters/agy/events.js +9 -0
- package/dist/runtime/src/adapters/claude/config.js +3 -3
- package/dist/runtime/src/adapters/claude/events.js +8 -0
- package/dist/runtime/src/adapters/codex/config.js +3 -3
- package/dist/runtime/src/adapters/codex/events.js +8 -0
- package/dist/runtime/src/adapters/opencode/config.js +5 -4
- package/dist/runtime/src/adapters/opencode/events.js +8 -0
- package/dist/runtime/src/config/explain.js +5 -0
- package/dist/runtime/src/hooks/codex-loop.js +4 -0
- package/dist/runtime/src/install/harness.js +44 -1
- package/dist/runtime/src/install/plan.js +16 -11
- package/dist/runtime/src/install/preauth.js +44 -18
- package/dist/runtime/src/install/profiles.js +8 -2
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/office/collect.js +233 -0
- package/dist/runtime/src/office/page.js +873 -0
- package/dist/runtime/src/office/scene.js +168 -0
- package/dist/runtime/src/office/server.js +139 -0
- package/dist/runtime/src/office/sessions.js +115 -0
- package/dist/runtime/src/office/snapshot.js +107 -0
- package/dist/runtime/src/parallel/guard.js +7 -1
- package/dist/runtime/src/review/slots.js +1 -1
- package/dist/runtime/src/run/phase.js +37 -0
- package/dist/runtime/src/run/service.js +8 -0
- package/dist/runtime/src/schema/validate.js +13 -2
- package/docs/en/spec/harness-adapters.md +113 -1
- package/docs/zh-TW/spec/harness-adapters.md +93 -1
- package/package.json +1 -1
- package/schemas/config.schema.json +10 -1
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { sceneModel } from "./scene.js";
|
|
2
|
+
/** Warm cream, oak and sage colors, kept to a single 16-color pixel palette. */
|
|
3
|
+
export const PALETTE = ["#2b241f", "#355a4b", "#6d9275", "#b7c7a3", "#8a5033", "#729ead", "#d9aa72", "#ead8b8",
|
|
4
|
+
"#8e7561", "#6b5d50", "#adc39a", "#f2e5c9", "#e8c99c", "#c86f4a", "#f2c95c", "#fff7e6"];
|
|
5
|
+
/** Each person has a fixed 16-color palette, independent of the room colors. */
|
|
6
|
+
export function avatarPalette(identity, viewer = false) {
|
|
7
|
+
let hash = 0;
|
|
8
|
+
for (const character of identity)
|
|
9
|
+
hash = (Math.imul(hash, 31) + character.charCodeAt(0)) >>> 0;
|
|
10
|
+
const hair = viewer ? ["#302c3b", "#514757", "#756575"] : [
|
|
11
|
+
["#302c3b", "#514757", "#756575"], ["#51372f", "#80533d", "#b47c51"], ["#745338", "#a37c4f", "#d1ad72"]
|
|
12
|
+
][hash % 3];
|
|
13
|
+
const skin = viewer ? ["#bf8264", "#e6ae87", "#f5cda4"] : [
|
|
14
|
+
["#bf8264", "#e6ae87", "#f5cda4"], ["#9b624b", "#c08762", "#e0ae82"], ["#654039", "#96624e", "#bf8c68"]
|
|
15
|
+
][Math.floor(hash / 9) % 3];
|
|
16
|
+
const clothes = viewer ? ["#bb893b", "#e3b64f", "#f7d878"] : [
|
|
17
|
+
["#37596d", "#5687a1", "#87b3c7"], ["#934d49", "#c57060", "#e6a087"],
|
|
18
|
+
["#3b6658", "#61947b", "#92bea1"], ["#625776", "#8c7da4", "#b7a9ca"]
|
|
19
|
+
][Math.floor(hash / 27) % 4];
|
|
20
|
+
return ["#292633", ...hair, ...skin, ...clothes, "#404958", "#707a8b", "#2e3443", "#626e7f", "#fff1db", "#d58c76"];
|
|
21
|
+
}
|
|
22
|
+
/** Original 32×48 pixel people. Self-contained so the inline client uses this same art. */
|
|
23
|
+
export function avatarSprite(identity, direction = "down", pose = 0, viewer = false) {
|
|
24
|
+
let hash = 0;
|
|
25
|
+
for (const character of identity)
|
|
26
|
+
hash = (Math.imul(hash, 31) + character.charCodeAt(0)) >>> 0;
|
|
27
|
+
const hair = "2", hairLight = "3";
|
|
28
|
+
const style = viewer ? 0 : Math.floor(hash / 3) % 3;
|
|
29
|
+
const skin = "5", skinLight = "6", skinShade = "4", outfit = ["8", "7", "9"];
|
|
30
|
+
const grid = Array.from({ length: 48 }, () => Array(32).fill("."));
|
|
31
|
+
const rect = (x, y, width, height, color) => {
|
|
32
|
+
for (let row = y; row < y + height; row++)
|
|
33
|
+
for (let column = x; column < x + width; column++) {
|
|
34
|
+
if (row >= 0 && row < 48 && column >= 0 && column < 32)
|
|
35
|
+
grid[row][column] = color;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const stride = pose === 1 ? -1 : pose === 3 ? 1 : 0;
|
|
39
|
+
const side = direction === "left" || direction === "right";
|
|
40
|
+
// Slim trousers, cuffs and shaded shoes; opposite feet alternate during a walk.
|
|
41
|
+
for (let leg = 0; leg < 2; leg++) {
|
|
42
|
+
const x = side ? 12 + leg * 4 : 10 + leg * 7, lift = leg ? -stride : stride;
|
|
43
|
+
rect(x, 35 + lift, 5, 10, "0");
|
|
44
|
+
rect(x + 1, 36 + lift, 3, 7, "a");
|
|
45
|
+
rect(x + 1, 37 + lift, 1, 5, "b");
|
|
46
|
+
rect(x + 1, 43 + lift, 3, 1, "e");
|
|
47
|
+
rect(x - 1, 44 + lift, 6, 3, "c");
|
|
48
|
+
rect(x, 44 + lift, 4, 1, "d");
|
|
49
|
+
rect(x, 46 + lift, 4, 1, "e");
|
|
50
|
+
}
|
|
51
|
+
const bodyX = side ? 11 : 8, bodyW = side ? 11 : 16;
|
|
52
|
+
rect(bodyX + 2, 23, bodyW - 4, 1, "0");
|
|
53
|
+
rect(bodyX, 24, bodyW, 12, "0");
|
|
54
|
+
rect(bodyX + 1, 25, bodyW - 2, 10, outfit[0]);
|
|
55
|
+
rect(bodyX + 1, 33, bodyW - 2, 2, outfit[1]);
|
|
56
|
+
rect(bodyX + bodyW - 3, 27, 2, 6, outfit[1]);
|
|
57
|
+
rect(bodyX + 2, 27, 2, 4, outfit[2]);
|
|
58
|
+
rect(bodyX + 3, 26, bodyW - 6, 1, outfit[2]);
|
|
59
|
+
if (direction !== "up") {
|
|
60
|
+
rect(side ? 18 : 13, 22, side ? 3 : 6, 3, skinShade);
|
|
61
|
+
rect(side ? 18 : 13, 22, side ? 2 : 5, 2, skin);
|
|
62
|
+
rect(side ? 18 : 12, 25, side ? 2 : 8, 1, outfit[1]);
|
|
63
|
+
rect(side ? 18 : 13, 25, side ? 1 : 6, 1, outfit[2]);
|
|
64
|
+
if (!side) {
|
|
65
|
+
rect(18, 29, 3, 3, outfit[1]);
|
|
66
|
+
rect(18, 29, 3, 1, outfit[2]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const arms = side ? [12] : [5, 24];
|
|
70
|
+
arms.forEach((x, index) => {
|
|
71
|
+
const swing = stride * (index ? -1 : 1);
|
|
72
|
+
rect(x, 25 + swing, 3, 10, "0");
|
|
73
|
+
rect(x + 1, 26 + swing, 2, 4, outfit[0]);
|
|
74
|
+
rect(x + 1, 29 + swing, 2, 1, outfit[1]);
|
|
75
|
+
rect(x + 1, 30 + swing, 2, 4, skinShade);
|
|
76
|
+
rect(x + 1, 30 + swing, 1, 3, skinLight);
|
|
77
|
+
});
|
|
78
|
+
// Rounded jaw and small dark eyes, with warm skin shading instead of white face blocks.
|
|
79
|
+
rect(9, 9, 14, 1, "0");
|
|
80
|
+
rect(7, 10, 18, 9, "0");
|
|
81
|
+
rect(8, 19, 16, 2, "0");
|
|
82
|
+
rect(10, 21, 12, 1, "0");
|
|
83
|
+
rect(9, 10, 14, 11, skin);
|
|
84
|
+
rect(8, 12, 16, 7, skin);
|
|
85
|
+
rect(22, 13, 2, 6, skinShade);
|
|
86
|
+
rect(19, 20, 3, 1, skinShade);
|
|
87
|
+
rect(10, 13, 3, 1, skinLight);
|
|
88
|
+
rect(6, 15, 2, 4, "0");
|
|
89
|
+
rect(24, 15, 2, 4, "0");
|
|
90
|
+
rect(7, 15, 1, 3, skin);
|
|
91
|
+
rect(24, 15, 1, 3, skin);
|
|
92
|
+
if (side) {
|
|
93
|
+
rect(23, 14, 3, 4, "0");
|
|
94
|
+
rect(23, 15, 2, 2, skin);
|
|
95
|
+
rect(21, 15, 2, 2, "0");
|
|
96
|
+
rect(21, 15, 1, 1, "e");
|
|
97
|
+
rect(23, 19, 1, 1, skinShade);
|
|
98
|
+
}
|
|
99
|
+
else if (direction === "down") {
|
|
100
|
+
rect(10, 14, 3, 1, hairLight);
|
|
101
|
+
rect(19, 14, 3, 1, hairLight);
|
|
102
|
+
rect(11, 15, 2, 2, "0");
|
|
103
|
+
rect(19, 15, 2, 2, "0");
|
|
104
|
+
rect(11, 15, 1, 1, "e");
|
|
105
|
+
rect(19, 15, 1, 1, "e");
|
|
106
|
+
rect(9, 18, 2, 1, skinShade);
|
|
107
|
+
rect(21, 18, 2, 1, skinShade);
|
|
108
|
+
rect(15, 19, 2, 1, skinShade);
|
|
109
|
+
}
|
|
110
|
+
const crown = ["00000000", "00HHHHHHHH00", "0HHHHHHHHHHHH0", "0HHHHHHHHHHHHHH0", "0HHHHhhHHHHHHHHHH0",
|
|
111
|
+
"0HHHhhhhhHHHHHHHHH0", "0HHHHHHhhhhhHHHHHHH0", "0HHHHHHHHHHHHHHHHHH0", "0HHHHHHHHHHHHHHHHHH0"];
|
|
112
|
+
crown.forEach((row, y) => [...row].forEach((pixel, x) => rect(Math.floor((32 - row.length) / 2) + x, y + 2, 1, 1, pixel === "H" ? hair : pixel === "h" ? hairLight : pixel)));
|
|
113
|
+
rect(5, 11, side ? 10 : 4, 5, hair);
|
|
114
|
+
rect(7, 11, side ? 9 : 7, 2, hair);
|
|
115
|
+
rect(11, 12, 2, 2, hair);
|
|
116
|
+
rect(14, 11, 2, 1, hair);
|
|
117
|
+
rect(6, 11, 1, 3, hairLight);
|
|
118
|
+
if (direction === "up") {
|
|
119
|
+
rect(6, 10, 20, 8, hair);
|
|
120
|
+
rect(8, 18, 16, 2, hair);
|
|
121
|
+
rect(10, 20, 12, 1, "0");
|
|
122
|
+
rect(7, 12, 1, 4, hairLight);
|
|
123
|
+
rect(22, 16, 2, 1, hairLight);
|
|
124
|
+
rect(11, 19, 10, 1, hairLight);
|
|
125
|
+
}
|
|
126
|
+
else if (!side) {
|
|
127
|
+
rect(23, 11, 3, 5, hair);
|
|
128
|
+
rect(21, 11, 4, 2, hair);
|
|
129
|
+
rect(20, 12, 2, 1, hair);
|
|
130
|
+
}
|
|
131
|
+
if (style === 1) { // Bob: longer sides and a low back, distinct from the short cut.
|
|
132
|
+
rect(5, 14, side ? 9 : 4, 8, hair);
|
|
133
|
+
if (!side)
|
|
134
|
+
rect(23, 14, 3, 8, hair);
|
|
135
|
+
if (direction === "up")
|
|
136
|
+
rect(8, 18, 16, 5, hair);
|
|
137
|
+
rect(6, 16, 1, 4, hairLight);
|
|
138
|
+
}
|
|
139
|
+
else if (style === 2) { // Curly outline with a raised crown and uneven fringe.
|
|
140
|
+
rect(8, 1, 3, 3, hair);
|
|
141
|
+
rect(14, 1, 3, 3, hair);
|
|
142
|
+
rect(21, 2, 3, 3, hair);
|
|
143
|
+
rect(5, 6, 2, 2, hairLight);
|
|
144
|
+
rect(14, 11, 2, 2, hair);
|
|
145
|
+
rect(19, 10, 2, 2, hair);
|
|
146
|
+
}
|
|
147
|
+
return grid.map(row => (direction === "left" ? row.reverse() : row).join(""));
|
|
148
|
+
}
|
|
149
|
+
/** Every sprite is a character matrix: one hex digit per palette index. */
|
|
150
|
+
export const SPRITES = {
|
|
151
|
+
person: [
|
|
152
|
+
"..................", "....8888888888....", "...888888888888...", "..888cccccc8888...",
|
|
153
|
+
"..88c0cccc0c88....", "..88cccccccc88....", "...88cccccc88.....", "....cccccccc......",
|
|
154
|
+
"...cSSSSSSSSSc....", "..cSSSSSSSSSSSc...", "..cSSSSSSSSSSSc...", "...SSSSSSSSSSSS...",
|
|
155
|
+
"....SSSSSSSSSS....", "....ccSSSScc......", ".....999999.......", "....99999999......",
|
|
156
|
+
"....99....99......", "...999....999.....", "...999....999.....", "..9999....9999...."
|
|
157
|
+
],
|
|
158
|
+
step: [
|
|
159
|
+
"..................", "....8888888888....", "...888888888888...", "..888cccccc8888...",
|
|
160
|
+
"..88c0cccc0c88....", "..88cccccccc88....", "...88cccccc88.....", "....cccccccc......",
|
|
161
|
+
"...cSSSSSSSSSc....", "..cSSSSSSSSSSSc...", "..cSSSSSSSSSSSc...", "...SSSSSSSSSSSS...",
|
|
162
|
+
"....SSSSSSSSSS....", "....ccSSSScc......", ".....999999.......", "....99999999......",
|
|
163
|
+
".....99999........", "....999999........", "...999....999.....", "..9999....9999...."
|
|
164
|
+
],
|
|
165
|
+
desk: [
|
|
166
|
+
"..........88888888..............", "..........81111118..............", "..........81bb1b18..............",
|
|
167
|
+
"..........81111118..............", "..........88888888..............", ".............8888...............",
|
|
168
|
+
"00000000000000000000000000000000", "0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0", "06666666666666666666666666666660",
|
|
169
|
+
"06666666666666666666666666666660", "00000000000000000000000000000000", "060..........................060",
|
|
170
|
+
"060..........................060", "060..........................060", "060..........................060", "000..........................000"
|
|
171
|
+
],
|
|
172
|
+
whiteboard: [
|
|
173
|
+
"00000000000000000000000000000000", "0ffffffffffffffffffffffffffffff0", "0ff11f1111fffff44f4fffff22fffff0",
|
|
174
|
+
"0ffffffffffffffffffffffffffffff0", "0ff1111f11ff44444fffff2222fffff0", "0ffffffffffffffffffffffffffffff0",
|
|
175
|
+
"0ff11111ffff444fffff22fffffffff0", "0ffffffffffffffffffffffffffffff0", "0ffffffffffffffffffffffffffffff0",
|
|
176
|
+
"00000000000000000000000000000000", "0777777777777777777777777777777.", "..............0880..............",
|
|
177
|
+
"..............0880..............", "..............0880..............", "..............0880..............", "............00000000............"
|
|
178
|
+
],
|
|
179
|
+
bench: [
|
|
180
|
+
"......b.........a.........d.....", ".....0b0.......0a0.......0d0....", ".....0b0.......0a0.......0d0....",
|
|
181
|
+
"....0bbb0.....0aaa0.....0ddd0...", "...0bbbbb0...0aaaaa0...0ddddd0..", "...0000000...0000000...0000000..",
|
|
182
|
+
"00000000000000000000000000000000", "0ffffffffffffffffffffffffffffff0", "07777777777777777777777777777770",
|
|
183
|
+
"00000000000000000000000000000000", "070888888888888888888888888880.0", "070..........................070",
|
|
184
|
+
"070..........................070", "070..........................070", "070..........................070", "000..........................000"
|
|
185
|
+
],
|
|
186
|
+
table: [
|
|
187
|
+
"................................", "....000000000000000000000000....", "..0066666666666666666666666600..",
|
|
188
|
+
".06666666666666666666666666666..", "0666666ffff666666666ffff6666660.", "0666666ffff666666666ffff6666660.",
|
|
189
|
+
"0666666666666666666666666666660.", ".066666666666666666666666666660.", "..0066666666666666666666666600..",
|
|
190
|
+
"....000000000000000000000000....", "......060..............060......", "......060..............060......",
|
|
191
|
+
"......060..............060......", "......060..............060......", "......000..............000......", "................................"
|
|
192
|
+
],
|
|
193
|
+
rug: [
|
|
194
|
+
"000000000000000000000000", "0aaaaaaaaaaaaaaaaaaaaaa0", "0a22222222222222222222a0", "0a22222222222222222222a0",
|
|
195
|
+
"0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0",
|
|
196
|
+
"0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0",
|
|
197
|
+
"0a22222222222222222222a0", "0a22222222222222222222a0", "0aaaaaaaaaaaaaaaaaaaaaa0", "000000000000000000000000"
|
|
198
|
+
],
|
|
199
|
+
chair: [
|
|
200
|
+
"....000000....", "...06666660...", "...06666660...", "...06666660...", "....000000....", "....066660....",
|
|
201
|
+
"....066660....", "....066660....", "....000000....", "...000..000...", "...060..060...", "...060..060...",
|
|
202
|
+
"..0000..0000..", ".............."
|
|
203
|
+
],
|
|
204
|
+
window: [
|
|
205
|
+
"000000000000000000000000", "0bbbbbbbbbbbbbbbbbbbbbb0", "0bffffffffffffffffffffb0", "0bffffffff0000ffffffffb0",
|
|
206
|
+
"0bffffffff0000ffffffffb0", "0bffffffffffffffffffffb0", "0bffffffffffffffffffffb0", "000000000000000000000000"
|
|
207
|
+
],
|
|
208
|
+
door: [
|
|
209
|
+
"0000000000000000", "0666666666666660", "0644444444444460", "0646666664666460", "0646666664666460",
|
|
210
|
+
"0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460", "06466666646ee460",
|
|
211
|
+
"06466666646ee460", "0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460",
|
|
212
|
+
"0646666664666460", "0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460",
|
|
213
|
+
"0646666664666460", "0644444444444460", "0666666666666660", "0000000000000000", "0aaaaaaaaaaaaaa0",
|
|
214
|
+
"0a2a2a2a2a2a2aa0", "0aaaaaaaaaaaaaa0", "0000000000000000"
|
|
215
|
+
],
|
|
216
|
+
shelf: [
|
|
217
|
+
"00000000000000000000000000000000", "06666666666666666666666666666660", "06............................60",
|
|
218
|
+
"06............................60", "06............................60", "06............................60", "06............................60",
|
|
219
|
+
"06............................60", "06............................60", "06666666666666666666666666666660", "06............................60",
|
|
220
|
+
"06............................60", "06............................60", "06............................60", "06............................60",
|
|
221
|
+
"06............................60", "06............................60", "06666666666666666666666666666660", "00000000000000000000000000000000"
|
|
222
|
+
],
|
|
223
|
+
book: ["SSS", "SfS", "SSS", "SSS", "SSS", "SSS", "SSS"],
|
|
224
|
+
clock: [
|
|
225
|
+
".....000000.....", "...00ffffff00...", "..0ffff0ffff0...", ".0fffffffffff0..", ".0fffffffffff0..",
|
|
226
|
+
"0ffffffffffffff0", "0f0ffffffffff0f0", "0ffffffffffffff0", "0ffffffffffffff0", ".0fffffffffff0..",
|
|
227
|
+
".0fffffffffff0..", "..0ffff0ffff0...", "...00ffffff00...", ".....000000.....", "................", "................"
|
|
228
|
+
],
|
|
229
|
+
plant: [
|
|
230
|
+
"......a..a......", "...a..aa.a..a...", "....aa2aa2aa....", "..a.2aa22aa2.a..", "...aa2a22a2aa...",
|
|
231
|
+
"....2aa22aa2....", ".....2a22a2.....", "......2222......", ".....000000.....", ".....066660.....",
|
|
232
|
+
".....064460.....", "......0660......", "......0660......", "......0000......", "................", "................"
|
|
233
|
+
],
|
|
234
|
+
paper: ["000000", "0ffff0", "000000"],
|
|
235
|
+
alert: [".00.", "0ee0", "0ee0", "0ee0", "0ee0", ".00.", "0ee0", ".00."],
|
|
236
|
+
floor: [
|
|
237
|
+
"7777777777777777", "6666666666666666", "666666b666666666", "66666666666b6666", "7777777777777777",
|
|
238
|
+
"6666666666666666", "6666b66666666666", "6666666666666666", "7777777777777777", "6666666666666666",
|
|
239
|
+
"6666666666666b66", "6666666666666666", "7777777777777777", "666666b666666666", "6666666666666666",
|
|
240
|
+
"6666666666666666"
|
|
241
|
+
],
|
|
242
|
+
wall: [
|
|
243
|
+
"7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
|
|
244
|
+
"7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
|
|
245
|
+
"7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
|
|
246
|
+
"7777777777777777"
|
|
247
|
+
],
|
|
248
|
+
baseboard: ["bbbbbbbbbbbbbbbb", "0000000000000000"]
|
|
249
|
+
};
|
|
250
|
+
const STYLE = String.raw `
|
|
251
|
+
:root{color-scheme:light}
|
|
252
|
+
*{box-sizing:border-box}
|
|
253
|
+
html,body{width:100%;height:100%;overflow:hidden}
|
|
254
|
+
body{margin:0;background:#2b241f;color:#2b241f;font:clamp(12px,1.25vw,18px)/1.25 "Courier New",monospace}
|
|
255
|
+
button{font:inherit;color:inherit}
|
|
256
|
+
#app{height:100dvh;display:flex;flex-direction:column;overflow:hidden;background:#ead8b8}
|
|
257
|
+
#header{min-height:54px;display:flex;align-items:center;gap:12px;padding:8px 18px;background:#2b241f;color:#fff7e6;border-bottom:4px solid #8a5033;white-space:nowrap}
|
|
258
|
+
#brand{font-weight:bold;font-size:clamp(16px,2vw,26px);letter-spacing:.02em}
|
|
259
|
+
#crumb{flex:1;overflow:hidden;text-overflow:ellipsis;color:#f2e5c9}
|
|
260
|
+
#header button{border:2px solid #f2e5c9;background:#6d9275;color:#fff7e6;padding:5px 9px;cursor:pointer;box-shadow:2px 2px 0 #000}
|
|
261
|
+
#header button:hover,#header button:focus-visible{background:#adc39a;color:#2b241f;outline:3px solid #f2c95c;outline-offset:2px}
|
|
262
|
+
#header button[hidden]{display:none}
|
|
263
|
+
#live{color:#adc39a}
|
|
264
|
+
#wrap{min-height:0;flex:1;display:flex;flex-direction:column;align-items:center;justify-content:space-between;gap:4px;padding:4px 8px;overflow:hidden;background:#ead8b8}
|
|
265
|
+
#office{display:block;max-width:100%;max-height:none;width:auto;height:auto;image-rendering:pixelated;image-rendering:crisp-edges;border:4px solid #6b5d50;box-shadow:6px 6px 0 #8a5033;outline:none;cursor:pointer}
|
|
266
|
+
#office:focus-visible{outline:4px solid #f2c95c;outline-offset:4px}
|
|
267
|
+
#dialogue{width:100%;min-height:2.25em;padding:6px 12px;background:#fff7e6;border:2px solid #8a5033;color:#2b241f;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
268
|
+
#room-nav{width:100%;display:flex;justify-content:center;align-items:center;gap:5px;min-height:28px;overflow:hidden}
|
|
269
|
+
#room-nav button{max-width:18ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:2px solid #8a5033;background:#f2e5c9;padding:3px 7px;cursor:pointer}
|
|
270
|
+
#room-nav button[aria-current=true]{background:#6d9275;color:#fff7e6;border-color:#2b241f}
|
|
271
|
+
#room-nav button:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
|
|
272
|
+
#status{position:fixed;z-index:4;inset:6dvh 50% auto auto;transform:translateX(50%);width:min(92vw,680px);max-height:88dvh;overflow:hidden;display:flex;flex-direction:column;gap:4px;padding:16px;background:#fff7e6;border:4px solid #6b5d50;box-shadow:8px 8px 0 #2b241f;color:#2b241f}
|
|
273
|
+
#status[hidden]{display:none}
|
|
274
|
+
#status h2{margin:0 0 8px;color:#355a4b;font-size:1.25em}
|
|
275
|
+
#status .dialog-body{min-height:0;overflow:hidden}
|
|
276
|
+
#status p{margin:5px 0;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
277
|
+
#status ul{margin:5px 0;padding-left:22px}
|
|
278
|
+
#status li{overflow-wrap:anywhere}
|
|
279
|
+
#status .actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-top:12px}
|
|
280
|
+
#status>.actions{flex:0 0 auto}
|
|
281
|
+
#status code{display:inline-block;max-width:calc(100% - 74px);overflow-wrap:anywhere;white-space:pre-wrap;background:#f2e5c9;padding:2px 4px}
|
|
282
|
+
#status .dialog-pager{border-top:2px solid #d9aa72;padding-top:6px}
|
|
283
|
+
#status button{border:2px solid #2b241f;background:#d9aa72;padding:5px 10px;cursor:pointer}
|
|
284
|
+
#status button:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
|
|
285
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
286
|
+
@media(max-width:760px){#header{flex-wrap:wrap;gap:6px;padding:6px 8px}#brand{font-size:16px}#header button{order:1;font-size:12px;padding:3px 5px}#crumb{min-width:70px}#live{font-size:12px}}
|
|
287
|
+
@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
|
|
288
|
+
`;
|
|
289
|
+
const CLIENT = String.raw `
|
|
290
|
+
var PALETTE = __PALETTE__, SPRITES = __SPRITES__, T = 8, S = 2, U = 1, dpr = 1;
|
|
291
|
+
var sceneModel = __SCENE__;
|
|
292
|
+
var avatarSprite = __AVATAR__, avatarPalette = __AVATAR_COLORS__, avatarImages = {};
|
|
293
|
+
var canvas = document.getElementById("office"), ctx = canvas.getContext("2d"), app = document.getElementById("app");
|
|
294
|
+
ctx.imageSmoothingEnabled = false;
|
|
295
|
+
var dialogueBox = document.getElementById("dialogue"), statusBox = document.getElementById("status"), roomNav = document.getElementById("room-nav"), wrap = document.getElementById("wrap");
|
|
296
|
+
var crumb = document.getElementById("crumb"), backButton = document.getElementById("back"), recentButton = document.getElementById("recent");
|
|
297
|
+
var languageButton = document.getElementById("language"), live = document.getElementById("live");
|
|
298
|
+
var snapshot = null, model = null, positions = {}, roomCanvases = {}, frame = 0, lineIndex = 0, typed = 0, offline = false;
|
|
299
|
+
var mode = "overview", selectedKey = null, showRecent = false, detailPage = 0, dialogClose = null, detailPreviousFocus = null, detailTarget = null;
|
|
300
|
+
var detailSignature = "";
|
|
301
|
+
var navPage = 0, navSignature = "", NAV_PAGE_SIZE = 8;
|
|
302
|
+
var supervisor = {x:43, y:19, targetX:43, targetY:19}, supervisorReady = false, hallwayReturn = null;
|
|
303
|
+
var roomSupervisor = {x:32, y:34, targetX:32, targetY:34};
|
|
304
|
+
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
305
|
+
var STRINGS = {
|
|
306
|
+
en: {viewer:"You", overview:"Office overview", rooms:"rooms", people:"people", recent:"Recently completed", recentOn:"Hide completed", back:"Back to overview", connected:"● Connected", offline:"○ Reconnecting", quiet:"The office is quiet. No agent is at work.", waiting:"waiting for your answer", enter:"Enter room", close:"Close", previous:"Previous", next:"Next", page:"Page", details:"Details", progress:"Progress", verify:"Verify", review:"Review", pending:"Awaiting answer", task:"Task", status:"Status", phase:"Phase", host:"Host", now:"Now", owner:"Owner", currentWork:"Current work", question:"Pending question", questions:"Questions", workList:"Work list", clickWork:"Click for work", keyboardWork:"Press L for work list", keyboardPhase:"Press 1-5 for phase work", keyboardQuestions:"Press Q for questions", noPeople:"No known work is in this phase", noQuestions:"No pending questions", files:"Changed files", commands:"Commands", copy:"Copy", copied:"Copied", unknown:"unknown", unassigned:"unassigned", phases:{planning:"Planning", implementing:"Implementing", verifying:"Verifying", reviewing:"Reviewing", integrating:"Integrating", unknown:"Unassigned"}, statuses:{active:"active", idle:"idle", running:"running", reviewing:"reviewing", verifying:"verifying", blocked:"blocked", delivered:"delivered", complete:"complete", unknown:"unknown", unassigned:"unassigned", pending:"pending"}},
|
|
307
|
+
zh: {viewer:"你", overview:"辦公室總覽", rooms:"個房間", people:"位成員", recent:"最近完成", recentOn:"隱藏已完成", back:"返回總覽", connected:"● 已連線", offline:"○ 重新連線中", quiet:"辦公室很安靜,目前沒有成員工作。", waiting:"等待你的回覆", enter:"進入房間", close:"關閉", previous:"上一頁", next:"下一頁", page:"頁", details:"詳細資料", progress:"進度", verify:"驗證", review:"審查", pending:"待回覆", task:"任務", status:"狀態", phase:"階段", host:"主機", now:"目前", owner:"負責人", currentWork:"目前工作", question:"待回覆問題", questions:"問題", workList:"工作清單", clickWork:"點擊查看工作", keyboardWork:"按 L 開啟工作清單", keyboardPhase:"按 1-5 查看階段工作", keyboardQuestions:"按 Q 查看問題", noPeople:"此階段目前沒有已知工作", noQuestions:"目前沒有待回覆問題", files:"變更檔案", commands:"指令", copy:"複製", copied:"已複製", unknown:"未知", unassigned:"未分配", phases:{planning:"規劃", implementing:"開發", verifying:"驗證", reviewing:"審查", integrating:"整合", unknown:"未分配"}, statuses:{active:"進行中", idle:"閒置", running:"工作中", reviewing:"審查中", verifying:"驗證中", blocked:"受阻", delivered:"已交付", complete:"已完成", unknown:"未知", unassigned:"未分配", pending:"待回覆"}}
|
|
308
|
+
};
|
|
309
|
+
function getLanguage(){
|
|
310
|
+
try { var cookie = document.cookie.split(";").map(function(part){ return part.trim().split("="); }).find(function(pair){ return pair[0] === "agent-office-language"; }); if (cookie && (cookie[1] === "en" || cookie[1] === "zh")) return cookie[1]; } catch (_) {}
|
|
311
|
+
try { var stored = localStorage.getItem("agent-office-language"); if (stored === "en" || stored === "zh") return stored; } catch (_) {}
|
|
312
|
+
return (navigator.language || "en").toLowerCase().indexOf("zh") === 0 ? "zh" : "en";
|
|
313
|
+
}
|
|
314
|
+
var lang = getLanguage();
|
|
315
|
+
function t(key){ return STRINGS[lang][key] || key; }
|
|
316
|
+
function phaseLabel(phase){ return STRINGS[lang].phases[phase] || phase; }
|
|
317
|
+
function statusText(status){ return STRINGS[lang].statuses[status] || safeText(status); }
|
|
318
|
+
function roleText(kind){return (lang==='zh'?{coordinator:'統籌',worker:'成員',reviewer:'審查',visitor:'訪客'}:{coordinator:'Lead',worker:'Agent',reviewer:'Review',visitor:'Guest'})[kind]||kind;}
|
|
319
|
+
function outcomeText(value){ return value === "pending" ? (lang === "zh" ? "尚未完成" : "pending") : safeText(value); }
|
|
320
|
+
function localizeText(value){
|
|
321
|
+
var text = safeText(value);
|
|
322
|
+
if (text === "The office is quiet. No agent is at work.") return t("quiet");
|
|
323
|
+
if (text === "waiting for your answer") return t("waiting");
|
|
324
|
+
if (text === "reviewing") return phaseLabel("reviewing");
|
|
325
|
+
if (text === "planning" || text === "implementing" || text === "verifying" || text === "integrating") return phaseLabel(text);
|
|
326
|
+
if (STRINGS.en.statuses[text]) return statusText(text);
|
|
327
|
+
if(lang==='zh')text=text.replace(/^writing tests$/u,'撰寫測試').replace(/^writing docs$/u,'撰寫文件').replace(/^editing /u,'編輯 ');
|
|
328
|
+
return text.replace(/waiting for your answer/gu, t("waiting"));
|
|
329
|
+
}
|
|
330
|
+
function saveLanguage(){ try { localStorage.setItem("agent-office-language", lang); } catch (_) {} try { document.cookie = "agent-office-language=" + lang + "; Max-Age=31536000; Path=/; SameSite=Strict"; } catch (_) {} }
|
|
331
|
+
function safeText(value, fallback){ return typeof value === "string" && value.length ? value : (fallback || t("unknown")); }
|
|
332
|
+
function short(value, limit){ value = safeText(value); limit = limit || 22; return value.length > limit ? value.slice(0, limit - 1) + "…" : value; }
|
|
333
|
+
function draw(name, x, y, shirt){
|
|
334
|
+
var rows = SPRITES[name]; if (!rows) return;
|
|
335
|
+
var scale = name === "floor" || name === "wall" || name === "baseboard" ? 1 : S;
|
|
336
|
+
for (var r = 0; r < rows.length; r++) for (var c = 0; c < rows[r].length; c++) {
|
|
337
|
+
var ch = rows[r][c]; if (ch === ".") continue;
|
|
338
|
+
ctx.fillStyle = ch === "S" ? shirt : PALETTE[parseInt(ch, 16)]; ctx.fillRect(x + c * scale, y + r * scale, scale, scale);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function avatarScale(surface){return Math.max(1,Math.round(Math.min(surface.width/(model.roomCols*T),surface.height/(model.roomRows*T))*T/9));}
|
|
342
|
+
function advanceAvatar(state, tx, ty, speed){
|
|
343
|
+
var dx=tx-state.x,dy=ty-state.y;
|
|
344
|
+
if(Math.abs(dx)>.05||Math.abs(dy)>.05)state.direction=Math.abs(dx)>Math.abs(dy)?(dx<0?'left':'right'):(dy<0?'up':'down');
|
|
345
|
+
if(reducedMotion){state.x=tx;state.y=ty;}else{state.x+=Math.max(-speed,Math.min(speed,dx));state.y+=Math.max(-speed,Math.min(speed,dy));}
|
|
346
|
+
state.walking=!reducedMotion&&(Math.abs(state.x-tx)>.05||Math.abs(state.y-ty)>.05);
|
|
347
|
+
}
|
|
348
|
+
function drawAvatar(identity, state, x, y, scale, viewer){
|
|
349
|
+
var pose=state.walking&&!reducedMotion?Math.floor(frame/7)%4:0,direction=state.direction||'down',key=JSON.stringify([identity,direction,pose,!!viewer]);
|
|
350
|
+
var image=avatarImages[key];
|
|
351
|
+
if(!image){
|
|
352
|
+
image=document.createElement('canvas');image.width=32;image.height=48;var paint=image.getContext('2d'),rows=avatarSprite(identity,direction,pose,!!viewer),colors=avatarPalette(identity,!!viewer);
|
|
353
|
+
for(var row=0;row<rows.length;row++)for(var col=0;col<rows[row].length;col++){var pixel=rows[row][col];if(pixel!=='.'){paint.fillStyle=colors[parseInt(pixel,16)];paint.fillRect(col,row,1,1);}}
|
|
354
|
+
avatarImages[key]=image;
|
|
355
|
+
}
|
|
356
|
+
x=Math.round(x);y=Math.round(y);
|
|
357
|
+
ctx.fillStyle='rgba(43,36,31,.18)';ctx.fillRect(x+8*scale,y+46*scale,17*scale,2*scale);
|
|
358
|
+
ctx.drawImage(image,x,y,32*scale,48*scale);
|
|
359
|
+
}
|
|
360
|
+
function propSize(kind){ var s = SPRITES[kind] || [""]; return {w:s[0].length * S, h:s.length * S}; }
|
|
361
|
+
function roomMetrics(width,height){var rx=width/model.roomCols,ry=height/model.roomRows;return {rx:rx,ry:ry,unit:Math.max(.5,Math.min(rx,ry)/8)};}
|
|
362
|
+
function propGeometry(prop,width,height){
|
|
363
|
+
var metrics=roomMetrics(width,height),rawX=prop.x*metrics.rx,rawY=prop.y*metrics.ry;
|
|
364
|
+
// layout sizes keep the existing scale decisions; visual sizes follow the
|
|
365
|
+
// actual rounded pixel art drawn by furniture() so clicks never drift away.
|
|
366
|
+
var layoutWidths={window:58,plant:32,shelf:28,door:24},visualWidths={desk:75,bench:75,table:65,window:58,plant:32,shelf:28,door:24,whiteboard:65,chair:21},heights={desk:45,bench:45,table:54,window:31,whiteboard:49,shelf:61,plant:31,chair:32,door:46};
|
|
367
|
+
var visualHeights={desk:45,bench:49,table:54,window:34,plant:34,shelf:61,door:46,whiteboard:49,chair:32},visualXOffsets={window:-3},visualYOffsets={bench:-4,window:-3,plant:-3};
|
|
368
|
+
var layoutWidth=layoutWidths[prop.kind]||75,visualWidth=visualWidths[prop.kind]||layoutWidth,artHeight=heights[prop.kind]||45;
|
|
369
|
+
var rawScale=Math.max(.3,Math.min(metrics.unit*1.85,(height-rawY-2*metrics.unit)/artHeight,(width-rawX-2*metrics.unit)/layoutWidth)),scale=Math.max(1,Math.round(rawScale)),visualHeight=visualHeights[prop.kind]||artHeight,visualY=Math.round(rawY+(visualYOffsets[prop.kind]||0)*scale);
|
|
370
|
+
return {x:Math.round(rawX+(visualXOffsets[prop.kind]||0)*scale),y:visualY,rawX:rawX,rawY:rawY,width:visualWidth*scale,height:visualHeight*scale,scale:scale,rawScale:rawScale,unit:metrics.unit};
|
|
371
|
+
}
|
|
372
|
+
function phaseAreaGeometry(area,width,height){var metrics=roomMetrics(width,height);return {x:area.x*metrics.rx,y:area.y*metrics.ry,width:area.width*metrics.rx,height:area.height*metrics.ry,rx:metrics.rx,ry:metrics.ry,unit:metrics.unit};}
|
|
373
|
+
function progressBoardGeometry(width,height){var metrics=roomMetrics(width,height);return {x:24*metrics.rx,y:metrics.ry,width:27*metrics.rx,height:Math.max(7*metrics.ry,66*metrics.unit),unit:metrics.unit};}
|
|
374
|
+
function pendingBoardGeometry(width,height){var board=progressBoardGeometry(width,height),inset=4*board.unit;return {x:board.x+inset,y:board.y+50*board.unit,width:Math.max(0,board.width-inset*2),height:Math.max(0,Math.min(15*board.unit,board.height-50*board.unit)),unit:board.unit};}
|
|
375
|
+
function surfaceForFloor(floor){if(mode==='room')return {x:0,y:0,width:canvas.width,height:canvas.height};var box=roomBox(floor);return {x:Math.round(box.x*T),y:Math.round(box.y*T),width:Math.round(box.width*T),height:Math.round(box.height*T)};}
|
|
376
|
+
function canvasPoint(event){var rect=canvas.getBoundingClientRect();return {x:(event.clientX-rect.left)*canvas.width/rect.width,y:(event.clientY-rect.top)*canvas.height/rect.height};}
|
|
377
|
+
function inside(point,rect){return point.x>=rect.x&&point.x<rect.x+rect.width&&point.y>=rect.y&&point.y<rect.y+rect.height;}
|
|
378
|
+
function fillText(value, x, y, size, color){ ctx.fillStyle = color || PALETTE[0]; ctx.font = "bold " + Math.max(5, Math.round((size || 10) * U)) + "px monospace"; ctx.textBaseline = "top"; ctx.fillText(short(value, 36), x, y); }
|
|
379
|
+
function roomByKey(key){ if (!model) return null; for (var i = 0; i < model.floors.length; i++) if (model.floors[i].key === key) return model.floors[i]; return null; }
|
|
380
|
+
function visibleRooms(){ if (!model) return []; return model.floors.filter(function(f){ return showRecent || !f.completedAt; }); }
|
|
381
|
+
function activeRoom(){ return selectedKey ? roomByKey(selectedKey) : null; }
|
|
382
|
+
function hasRecent(){ return !!(model && model.floors.some(function(f){ return !!f.completedAt; })); }
|
|
383
|
+
function setCanvasSize(width, height){
|
|
384
|
+
width = Math.max(1, Math.round(width)); height = Math.max(1, Math.round(height));
|
|
385
|
+
if (canvas.width !== width) canvas.width = width;
|
|
386
|
+
if (canvas.height !== height) canvas.height = height;
|
|
387
|
+
ctx.imageSmoothingEnabled = false;
|
|
388
|
+
}
|
|
389
|
+
function drawBackdrop(cols, rows, room){
|
|
390
|
+
if (!room) setCanvasSize(cols * T, rows * T);
|
|
391
|
+
var width = canvas.width, height = canvas.height, unit = Math.max(1, Math.min(width / 576, height / 320)), wallHeight = Math.round(height * .18);
|
|
392
|
+
ctx.fillStyle = PALETTE[15]; ctx.fillRect(0, 0, width, wallHeight);
|
|
393
|
+
ctx.fillStyle = PALETTE[12]; ctx.fillRect(0, wallHeight, width, height - wallHeight);
|
|
394
|
+
var plankH = 12 * unit, plankW = 64 * unit;
|
|
395
|
+
for (var row = 0, y = wallHeight; y < height; row++, y += plankH) {
|
|
396
|
+
ctx.fillStyle = row % 3 === 0 ? PALETTE[7] : PALETTE[12]; ctx.fillRect(0, y, width, plankH - unit);
|
|
397
|
+
for (var x = -(row % 2) * plankW / 2; x < width; x += plankW) {
|
|
398
|
+
ctx.fillStyle = PALETTE[6]; ctx.fillRect(x, y, unit, plankH);
|
|
399
|
+
ctx.fillStyle = PALETTE[7]; ctx.fillRect(x + 8 * unit, y + 4 * unit, 24 * unit, unit);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
ctx.fillStyle = PALETTE[6]; ctx.fillRect(0, wallHeight - 3 * unit, width, 3 * unit);
|
|
403
|
+
ctx.fillStyle = 'rgba(43,36,31,.12)'; ctx.fillRect(0, wallHeight, width, 4 * unit);
|
|
404
|
+
ctx.fillStyle = PALETTE[9]; ctx.fillRect(0, height - 2 * unit, width, 2 * unit);
|
|
405
|
+
}
|
|
406
|
+
// Furniture stays square-pixeled while its placement follows the room size.
|
|
407
|
+
function furniture(kind, x, y, unit){
|
|
408
|
+
ctx.save(); ctx.translate(Math.round(x), Math.round(y)); unit=Math.max(1,Math.round(unit));ctx.scale(unit, unit);
|
|
409
|
+
function r(x,y,w,h,c){ctx.fillStyle=PALETTE[c];ctx.fillRect(x,y,w,h);}
|
|
410
|
+
function monitor(x,y){r(x,y,22,16,9);r(x+1,y+1,20,13,0);r(x+3,y+3,16,9,5);for(var i=0;i<3;i++){r(x+4,y+4+i*2,4+i*3,1,i%2?14:3);}r(x+10,y+16,2,3,9);r(x+6,y+19,10,1,9);}
|
|
411
|
+
function cup(x,y){r(x,y,5,6,15);r(x+1,y,3,1,4);r(x+5,y+1,2,3,8);}
|
|
412
|
+
function drawer(x,y){r(x,y,17,19,9);r(x+1,y+1,15,7,8);r(x+1,y+10,15,7,8);r(x+6,y+3,5,1,15);r(x+6,y+12,5,1,15);}
|
|
413
|
+
if(kind==='desk'||kind==='bench'){
|
|
414
|
+
r(2,31,72,5,8);drawer(4,28);drawer(52,28);r(1,17,74,13,4);r(2,17,72,9,6);r(2,17,72,2,7);r(25,30,3,15,9);r(70,30,3,15,9);
|
|
415
|
+
monitor(8,0);monitor(34,1);r(15,23,23,3,9);for(var k=0;k<6;k++)r(17+k*3,24,2,1,15);cup(61,19);
|
|
416
|
+
r(43,20,11,5,15);r(45,21,7,1,8);r(45,23,6,1,8);r(66,7,6,9,9);r(67,8,4,6,3);
|
|
417
|
+
if(kind==='bench'){r(2,-4,72,3,9);for(var j=0;j<7;j++){r(5+j*9,-1,2,8,8);r(4+j*9,1,4,3,j%2?14:5);}r(59,0,10,16,9);r(61,2,6,5,3);r(61,9,6,4,5);}
|
|
418
|
+
} else if(kind==='table'){
|
|
419
|
+
r(6,0,47,12,1);r(8,1,43,9,2);r(2,12,63,30,4);r(3,12,61,26,6);r(3,12,61,2,7);r(7,42,3,12,9);r(55,42,3,12,9);
|
|
420
|
+
r(10,19,14,12,15);r(30,22,13,12,15);for(var l=0;l<4;l++){r(12,21+l*2,10,1,8);r(32,24+l*2,9,1,8);}cup(51,19);monitor(41,0);
|
|
421
|
+
} else if(kind==='window'){
|
|
422
|
+
r(0,0,52,28,8);r(2,2,48,23,5);r(4,4,44,19,3);r(4,4,44,13,5);r(8,6,15,2,15);r(30,9,10,2,15);r(3,24,48,4,6);r(24,2,3,23,15);r(2,2,48,2,15);r(2,12,48,2,15);
|
|
423
|
+
r(-2,-3,56,4,9);r(0,-2,52,2,7);r(-3,27,58,4,4);r(-2,27,56,2,6);r(36,20,7,7,4);r(35,17,9,5,2);r(39,14,4,6,1);
|
|
424
|
+
} else if(kind==='whiteboard'){
|
|
425
|
+
r(0,0,65,38,9);r(2,2,61,33,15);r(4,4,57,1,7);r(5,38,3,9,9);r(57,38,3,9,9);r(2,47,9,2,9);r(54,47,9,2,9);
|
|
426
|
+
for(var n=0;n<6;n++){var bx=7+n%3*18,by=9+Math.floor(n/3)*16;r(bx,by,12,9,n%2?3:14);r(bx+2,by+2,8,1,8);r(bx+2,by+5,6,1,8);if(n<5){r(bx+13,by+5,4,1,8);}}
|
|
427
|
+
} else if(kind==='shelf'){
|
|
428
|
+
r(0,0,28,58,4);r(2,2,24,54,9);for(var shelf=0;shelf<3;shelf++){r(2,17+shelf*18,24,2,6);for(var book=0;book<5;book++){r(4+book*4,4+shelf*18+(book%2)*2,3,12-(book%2)*2,[2,5,13,7,3][book]);r(5+book*4,6+shelf*18,1,6,15);}}
|
|
429
|
+
r(0,58,28,3,8);r(3,44,9,11,6);r(14,44,10,11,6);r(6,46,4,1,15);r(17,46,4,1,15);
|
|
430
|
+
} else if(kind==='plant'){
|
|
431
|
+
r(9,18,16,13,8);r(10,18,14,3,6);r(11,21,12,9,7);r(14,4,3,15,1);r(5,7,11,5,2);r(1,5,9,4,3);r(17,3,12,5,2);r(23,0,8,5,3);r(14,-3,5,10,2);r(6,-2,9,5,1);r(20,10,10,5,1);r(3,13,10,4,2);r(11,11,11,5,3);
|
|
432
|
+
} else if(kind==='chair'){
|
|
433
|
+
r(2,0,18,13,1);r(3,1,16,10,2);r(2,13,18,9,2);r(3,14,16,5,3);r(10,22,2,8,9);r(3,29,18,2,9);r(2,28,3,4,9);r(18,28,3,4,9);
|
|
434
|
+
} else if(kind==='door'){
|
|
435
|
+
r(0,0,24,46,8);r(2,2,20,42,4);r(4,4,16,38,6);r(5,5,14,13,7);r(5,22,14,18,7);r(15,21,3,2,14);r(5,28,14,8,1);r(8,31,8,2,15);r(13,29,2,6,15);
|
|
436
|
+
}
|
|
437
|
+
ctx.restore();
|
|
438
|
+
}
|
|
439
|
+
function drawRoomBase(floor){
|
|
440
|
+
drawBackdrop(model.roomCols, model.roomRows, floor);
|
|
441
|
+
var metrics=roomMetrics(canvas.width,canvas.height),rx=metrics.rx,ry=metrics.ry,unit=metrics.unit,labelUnit = Math.min(U, Math.max(1, unit * 1.25));
|
|
442
|
+
var savedU = U; U = labelUnit;
|
|
443
|
+
// Sage rugs and oak furniture give each phase a real place in the room.
|
|
444
|
+
floor.phaseAreas.forEach(function(a){
|
|
445
|
+
var area=phaseAreaGeometry(a,canvas.width,canvas.height);
|
|
446
|
+
if(a.phase==='implementing'||a.phase==='reviewing'){
|
|
447
|
+
var x=area.x,y=area.y+area.ry,w=area.width,h=area.height-area.ry;
|
|
448
|
+
ctx.fillStyle=PALETTE[3];ctx.fillRect(x,y,w,h);ctx.strokeStyle=PALETTE[2];ctx.lineWidth=2*unit;ctx.strokeRect(x+3*unit,y+3*unit,w-6*unit,h-6*unit);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
floor.props.filter(function(p){return p.kind!=='rug'&&p.kind!=='clock';}).forEach(function(p){
|
|
452
|
+
var geometry=propGeometry(p,canvas.width,canvas.height),px=geometry.rawX,py=geometry.rawY,artUnit=geometry.rawScale;
|
|
453
|
+
if(p.kind!=='window'){ctx.fillStyle='rgba(43,36,31,.14)';ctx.fillRect(px+4*unit,py+28*unit,45*unit,6*unit);}
|
|
454
|
+
furniture(p.kind,px,py,artUnit);
|
|
455
|
+
if(p.kind==='shelf')for(var book=0;book<Math.min(5,floor.books.lit);book++){ctx.fillStyle=PALETTE[14];ctx.fillRect(px+(4+book*4)*Math.round(artUnit),py+4*Math.round(artUnit),3*Math.round(artUnit),2*Math.round(artUnit));}
|
|
456
|
+
if(p.kind==='desk')for(var paper=0;paper<Math.min(4,floor.papers[p.key]||0);paper++){ctx.fillStyle=PALETTE[15];ctx.fillRect(px+44*artUnit,py+(20-paper)*artUnit,9*artUnit,2*artUnit);}
|
|
457
|
+
});
|
|
458
|
+
floor.phaseAreas.forEach(function(a){var area=phaseAreaGeometry(a,canvas.width,canvas.height),ax=area.x,ay=area.y;ctx.fillStyle=PALETTE[15];ctx.fillRect(ax,ay,Math.min(area.width,72*labelUnit),13*labelUnit);fillText(phaseLabel(a.phase),ax+4*labelUnit,ay+2*labelUnit,9,PALETTE[1]);});
|
|
459
|
+
var titleX=2*rx,titleY=ry,w=19*rx;
|
|
460
|
+
ctx.fillStyle=PALETTE[4];ctx.fillRect(titleX,titleY,w,4*ry);ctx.fillStyle=PALETTE[6];ctx.fillRect(titleX+2*unit,titleY+2*unit,w-4*unit,4*ry-4*unit);
|
|
461
|
+
fillText(short(floor.title,20),titleX+6*unit,titleY+6*unit,11);fillText(statusText(floor.status),titleX+6*unit,titleY+22*unit,8,PALETTE[4]);
|
|
462
|
+
var b=floor.board,board=progressBoardGeometry(canvas.width,canvas.height),bx=board.x,by=board.y,bw=board.width,bh=board.height;
|
|
463
|
+
ctx.fillStyle=PALETTE[9];ctx.fillRect(bx,by,bw,bh);ctx.fillStyle=PALETTE[15];ctx.fillRect(bx+2*unit,by+2*unit,bw-4*unit,bh-4*unit);
|
|
464
|
+
fillText('▣ '+t('clickWork'),bx+6*unit,by+5*unit,8,PALETTE[1]);
|
|
465
|
+
fillText(t('progress')+' '+b.passed+'/'+(b.total||'?'),bx+6*unit,by+16*unit,10,PALETTE[1]);
|
|
466
|
+
ctx.fillStyle=PALETTE[3];ctx.fillRect(bx+6*unit,by+29*unit,bw-12*unit,5*unit);ctx.fillStyle=PALETTE[2];ctx.fillRect(bx+6*unit,by+29*unit,(bw-12*unit)*Math.min(1,b.total?b.passed/b.total:0),5*unit);
|
|
467
|
+
fillText(t('verify')+' '+outcomeText(b.verify)+' '+t('review')+' '+outcomeText(b.review),bx+6*unit,by+40*unit,8,PALETTE[4]);
|
|
468
|
+
var pendingRow=pendingBoardGeometry(canvas.width,canvas.height);
|
|
469
|
+
if(b.pending)fillText('! '+t('pending')+' '+b.pending,pendingRow.x+2*unit,pendingRow.y+3*unit,8,PALETTE[13]);
|
|
470
|
+
var oldS=S;S=Math.max(1,Math.round(unit));var cx=52*rx,cy=ry;draw('clock',cx,cy);
|
|
471
|
+
ctx.strokeStyle=PALETTE[13];ctx.lineWidth=2*S;ctx.beginPath();ctx.moveTo(cx+8*S,cy+7*S);var angle=-Math.PI/2+2*Math.PI*floor.clock;ctx.lineTo(cx+8*S+Math.cos(angle)*5*S,cy+7*S+Math.sin(angle)*5*S);ctx.stroke();S=oldS;
|
|
472
|
+
U=savedU;
|
|
473
|
+
ctx.strokeStyle=PALETTE[9];ctx.lineWidth=3*unit;ctx.strokeRect(unit,unit,canvas.width-2*unit,canvas.height-2*unit);
|
|
474
|
+
}
|
|
475
|
+
function drawActor(floor, actor, surface){
|
|
476
|
+
var tx=actor.x*T,ty=actor.y*T,pos=positions[actor.key]||{x:tx,y:ty};
|
|
477
|
+
advanceAvatar(pos,tx,ty,T/5);positions[actor.key]=pos;
|
|
478
|
+
surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
|
|
479
|
+
var box=actorBox(actor,surface),oldS=S,oldU=U;S=box.scale;U=box.unit;
|
|
480
|
+
var px=box.x,py=box.y;
|
|
481
|
+
drawAvatar(actor.id,pos,px,py,S,false);
|
|
482
|
+
if(!pos.walking&&!reducedMotion&&Math.floor(frame/30)%2===0){ctx.fillStyle=PALETTE[15];ctx.fillRect(px+12*S,py+30*S,8*S,4*S);}
|
|
483
|
+
var text=short(actor.label,10)+' · '+roleText(actor.kind)+' · '+short(statusText(actor.status),8),font=9*U;
|
|
484
|
+
ctx.font='bold '+font+'px monospace';var labelWidth=Math.min(surface.width-8*U,(ctx.measureText?ctx.measureText(text).width:text.length*font*.62)+8*U),labelX=Math.max(surface.x+4*U,Math.min(surface.x+surface.width-labelWidth-4*U,px-3*U)),labelY=Math.min(surface.y+surface.height-16*U,py+49*S);
|
|
485
|
+
ctx.fillStyle=PALETTE[15];ctx.fillRect(labelX,labelY,labelWidth,14*U);ctx.strokeStyle=PALETTE[8];ctx.lineWidth=U;ctx.strokeRect(labelX,labelY,labelWidth,14*U);fillText(text,labelX+3*U,labelY+2*U,9,actor.alert?PALETTE[13]:PALETTE[0]);
|
|
486
|
+
var alert=alertGeometry(actor,surface);
|
|
487
|
+
if(actor.alert)draw('alert',alert.x,alert.y);
|
|
488
|
+
S=oldS;U=oldU;
|
|
489
|
+
}
|
|
490
|
+
function actorBox(actor,surface){
|
|
491
|
+
var pos=positions[actor.key]||{x:actor.x*T,y:actor.y*T},scale=avatarScale(surface),unit=Math.max(dpr,Math.min(U,scale));
|
|
492
|
+
return {scale:scale,unit:unit,x:Math.round(Math.max(surface.x,Math.min(surface.x+surface.width-32*scale,surface.x+pos.x/(model.roomCols*T)*surface.width))),y:Math.round(Math.max(surface.y,Math.min(surface.y+surface.height-48*scale-16*unit,surface.y+pos.y/(model.roomRows*T)*surface.height)))};
|
|
493
|
+
}
|
|
494
|
+
function alertGeometry(actor,surface){var box=actorBox(actor,surface);return {x:box.x+14*box.scale,y:box.y-9*box.scale,width:4*box.scale,height:8*box.scale,scale:box.scale};}
|
|
495
|
+
function fitCanvas(){
|
|
496
|
+
var wrapW = wrap && wrap.clientWidth ? wrap.clientWidth : window.innerWidth;
|
|
497
|
+
var wrapH = wrap && wrap.clientHeight ? wrap.clientHeight : window.innerHeight - 54;
|
|
498
|
+
var reserved = (dialogueBox && dialogueBox.offsetHeight || 36) + (roomNav && roomNav.offsetHeight || 30) + 24;
|
|
499
|
+
var maxW = Math.max(220, wrapW - 18), maxH = Math.max(160, wrapH - reserved);
|
|
500
|
+
var ratio = Math.min(maxW / canvas.width, maxH / canvas.height);
|
|
501
|
+
canvas.style.width = Math.max(1, Math.floor(canvas.width * ratio)) + "px"; canvas.style.height = Math.max(1, Math.floor(canvas.height * ratio)) + "px";
|
|
502
|
+
}
|
|
503
|
+
function chooseRenderScale(){
|
|
504
|
+
if(!model)return;
|
|
505
|
+
var wrapW=wrap&&wrap.clientWidth?wrap.clientWidth:window.innerWidth,wrapH=wrap&&wrap.clientHeight?wrap.clientHeight:window.innerHeight-54;
|
|
506
|
+
var reserved=(dialogueBox&&dialogueBox.offsetHeight||36)+(roomNav&&roomNav.offsetHeight||30)+24;
|
|
507
|
+
var maxW=Math.max(220,wrapW-18),maxH=Math.max(160,wrapH-reserved),nextDpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)),oldT=T;
|
|
508
|
+
model.cols=model.rows*maxW/maxH;
|
|
509
|
+
T=Math.max(4,maxH/model.rows*nextDpr);S=T/4;U=T/8;dpr=nextDpr;
|
|
510
|
+
if(oldT!==T){Object.keys(positions).forEach(function(key){positions[key].x*=T/oldT;positions[key].y*=T/oldT;});roomCanvases={};}
|
|
511
|
+
}
|
|
512
|
+
function overviewLayout(rooms){
|
|
513
|
+
var count=rooms.length,gap=.6,result=[];if(!count)return result;
|
|
514
|
+
if(count===1)return [{x:gap,y:gap,width:model.cols-6-gap,height:model.rows-gap*2}];
|
|
515
|
+
var hallW=6,left=(model.cols-hallW)/2,right=left,teamIndex=rooms.findIndex(function(f){return f.kind==='run';});
|
|
516
|
+
if(teamIndex>=0&&count<=6){
|
|
517
|
+
left=(model.cols-hallW)*.58;right=model.cols-hallW-left;
|
|
518
|
+
result[teamIndex]={x:gap,y:gap,width:left-gap,height:model.rows-gap*2};
|
|
519
|
+
var others=rooms.filter(function(_,i){return i!==teamIndex;}),height=(model.rows-gap*(others.length+1))/Math.max(1,others.length);
|
|
520
|
+
others.forEach(function(f,i){result[rooms.indexOf(f)]={x:left+hallW,y:gap+i*(height+gap),width:right-gap,height:height};});
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
var leftCount=Math.ceil(count/2),rightCount=Math.floor(count/2);
|
|
524
|
+
rooms.forEach(function(f,i){var side=i%2,n=side?rightCount:leftCount,row=Math.floor(i/2),height=(model.rows-gap*(n+1))/n;result[i]={x:side?left+hallW:gap,y:gap+row*(height+gap),width:(side?right:left)-gap,height:height};});
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
function roomBox(floor){var rooms=visibleRooms(),index=rooms.indexOf(floor);return overviewLayout(rooms)[index]||floor.overview;}
|
|
528
|
+
function hallwayBounds(){
|
|
529
|
+
var rooms=visibleRooms(),layout=overviewLayout(rooms),left=layout.find(function(box){return box.x<model.cols/2-3;});
|
|
530
|
+
return {x:left?left.x+left.width:model.cols/2-3,width:6,y:.6,height:model.rows-1.2};
|
|
531
|
+
}
|
|
532
|
+
function ensureSupervisor(){
|
|
533
|
+
if (supervisorReady || !model) return;
|
|
534
|
+
var hall = hallwayBounds(); supervisor.x = supervisor.targetX = hall.x + hall.width / 2; supervisor.y = supervisor.targetY = model.rows - 5; supervisorReady = true;
|
|
535
|
+
}
|
|
536
|
+
function roomEntrances(){
|
|
537
|
+
var hall=hallwayBounds();return visibleRooms().map(function(floor){var box=roomBox(floor),left=box.x<hall.x;return {floor:floor,x:left?hall.x+.35:hall.x+hall.width-.35,y:box.y+box.height/2,height:box.height};});
|
|
538
|
+
}
|
|
539
|
+
function supervisorBounds(){var hall=hallwayBounds();return {minX:hall.x+.2,maxX:hall.x+hall.width-.2,minY:1,maxY:model.rows-3};}
|
|
540
|
+
function drawHallway(){
|
|
541
|
+
var hall = hallwayBounds(), x = hall.x * T, y = hall.y * T, w = hall.width * T, h = hall.height * T;
|
|
542
|
+
ctx.fillStyle = "rgba(183,199,163,.55)"; ctx.fillRect(x, y, w, h);
|
|
543
|
+
ctx.strokeStyle = PALETTE[3]; ctx.lineWidth = Math.max(1, 2 * U); ctx.strokeRect(x, y, w, h);
|
|
544
|
+
for (var line = y + 14 * U; line < y + h; line += 28 * U) { ctx.strokeStyle = "rgba(138,80,51,.28)"; ctx.beginPath(); ctx.moveTo(x + 3 * U, line); ctx.lineTo(x + w - 3 * U, line); ctx.stroke(); }
|
|
545
|
+
var oldS=S;S=T/8;draw('plant',x+T,y+T,PALETTE[2]);draw('plant',x+T,y+h-4*T,PALETTE[2]);S=oldS;
|
|
546
|
+
roomEntrances().forEach(function(entry){ var ex = entry.x * T, ey = entry.y * T; ctx.fillStyle = PALETTE[6]; ctx.fillRect(ex - 5 * U, ey - 14 * U, 10 * U, 28 * U); ctx.strokeStyle = PALETTE[4]; ctx.strokeRect(ex - 5 * U, ey - 14 * U, 10 * U, 28 * U); });
|
|
547
|
+
}
|
|
548
|
+
function drawSupervisor(){
|
|
549
|
+
ensureSupervisor(); if (!model) return;
|
|
550
|
+
advanceAvatar(supervisor,supervisor.targetX,supervisor.targetY,.7);
|
|
551
|
+
var moving=supervisor.walking,scale=Math.max(1,...visibleRooms().map(function(floor){var b=roomBox(floor);return avatarScale({width:b.width*T,height:b.height*T});}));
|
|
552
|
+
var px=supervisor.x*T-16*scale,py=Math.min(canvas.height-48*scale,supervisor.y*T);drawAvatar('viewer',supervisor,px,py,scale,true);
|
|
553
|
+
drawViewerBadge(supervisor.x*T,py,7);
|
|
554
|
+
var entry=roomEntrances().sort(function(a,b){return Math.hypot(a.x-supervisor.x,a.y-supervisor.y)-Math.hypot(b.x-supervisor.x,b.y-supervisor.y);})[0]; if(entry&&(Math.abs(entry.x-supervisor.x)>1.25||Math.abs(entry.y-supervisor.y)>Math.min(1.6,entry.height*.35)))entry=null;
|
|
555
|
+
if (!moving && mode === "overview" && entry) { selectedKey = entry.floor.key; hallwayReturn = {x:supervisor.x, y:supervisor.y}; enterRoomFromHallway(entry.floor); }
|
|
556
|
+
}
|
|
557
|
+
function enterRoomFromHallway(floor){
|
|
558
|
+
if (!floor) return;
|
|
559
|
+
detailTarget=null;detailPage=0;
|
|
560
|
+
roomSupervisor.x = roomSupervisor.targetX = floor.kind === "run" ? model.roomCols - 7 : 6;
|
|
561
|
+
roomSupervisor.y = roomSupervisor.targetY = Math.max(8, Math.min(model.roomRows - 6, Math.round(model.roomRows / 2)));
|
|
562
|
+
mode = "room"; lineIndex = 0; typed = 0; updateHeader(); canvas.focus();
|
|
563
|
+
}
|
|
564
|
+
function drawRoomSupervisor(){
|
|
565
|
+
if (!model) return;
|
|
566
|
+
advanceAvatar(roomSupervisor,roomSupervisor.targetX,roomSupervisor.targetY,.7);
|
|
567
|
+
var scale=avatarScale({width:canvas.width,height:canvas.height});
|
|
568
|
+
var px=roomSupervisor.x/model.roomCols*canvas.width-16*scale,py=Math.min(canvas.height-48*scale,roomSupervisor.y/model.roomRows*canvas.height);drawAvatar('viewer',roomSupervisor,px,py,scale,true);
|
|
569
|
+
drawViewerBadge(px+16*scale,py,8);
|
|
570
|
+
}
|
|
571
|
+
function drawViewerBadge(center,top,size){
|
|
572
|
+
var text=t("viewer"),font=Math.max(5,Math.round(size*U)),pad=Math.max(2,3*U);ctx.font="bold "+font+"px monospace";
|
|
573
|
+
var width=ctx.measureText(text).width+pad*2,height=Math.max(13*U,font+pad*2),x=Math.max(pad,Math.min(canvas.width-width-pad,center-width/2)),y=Math.max(pad,top-height-3*U);
|
|
574
|
+
ctx.fillStyle=PALETTE[15];ctx.fillRect(x,y,width,height);ctx.strokeStyle=PALETTE[1];ctx.strokeRect(x,y,width,height);fillText(text,x+pad,y+pad,size,PALETTE[1]);
|
|
575
|
+
}
|
|
576
|
+
function moveSupervisor(dx, dy){
|
|
577
|
+
if (!model || !statusBox.hidden) return;
|
|
578
|
+
var state=mode==='room'?roomSupervisor:supervisor;state.direction=dx?(dx<0?'left':'right'):(dy<0?'up':'down');
|
|
579
|
+
var step = mode === "room" ? 1 : Math.min(1.25,(model.rows-2)/Math.max(1,Math.ceil(visibleRooms().length/2))/3);
|
|
580
|
+
if (mode === "room") { roomSupervisor.targetX = Math.max(2, Math.min(model.roomCols - 5, roomSupervisor.targetX + dx * step)); roomSupervisor.targetY = Math.max(7, Math.min(model.roomRows - 7, roomSupervisor.targetY + dy * step)); if (reducedMotion) { roomSupervisor.x = roomSupervisor.targetX; roomSupervisor.y = roomSupervisor.targetY; } render(); return; }
|
|
581
|
+
ensureSupervisor(); var bounds = supervisorBounds(); supervisor.targetX = Math.max(bounds.minX, Math.min(bounds.maxX, supervisor.targetX + dx * step)); supervisor.targetY = Math.max(bounds.minY, Math.min(bounds.maxY, supervisor.targetY + dy * step)); if (reducedMotion) { supervisor.x = supervisor.targetX; supervisor.y = supervisor.targetY; } render();
|
|
582
|
+
}
|
|
583
|
+
function renderRoomImage(floor, width, height){
|
|
584
|
+
width=Math.round(width);height=Math.round(height);
|
|
585
|
+
var signature=lang+'|'+T+'|'+width+'x'+height+'|'+JSON.stringify([floor.title,floor.status,floor.board,floor.props,floor.papers,floor.books,floor.clock]);
|
|
586
|
+
var cached=roomCanvases[floor.key];if(cached&&cached.signature===signature)return cached.canvas;
|
|
587
|
+
var roomCanvas=cached?cached.canvas:document.createElement('canvas');
|
|
588
|
+
if(typeof roomCanvas.getContext!=='function')return null;
|
|
589
|
+
roomCanvas.width=width;roomCanvas.height=height;var roomCtx=roomCanvas.getContext('2d');if(!roomCtx)return null;
|
|
590
|
+
var oldCanvas=canvas,oldCtx=ctx;canvas=roomCanvas;ctx=roomCtx;ctx.imageSmoothingEnabled=false;drawRoomBase(floor);canvas=oldCanvas;ctx=oldCtx;
|
|
591
|
+
roomCanvases[floor.key]={canvas:roomCanvas,signature:signature};return roomCanvas;
|
|
592
|
+
}
|
|
593
|
+
function drawOverviewRoom(floor){
|
|
594
|
+
var surface=surfaceForFloor(floor);
|
|
595
|
+
ctx.save();ctx.beginPath();ctx.rect(surface.x,surface.y,surface.width,surface.height);ctx.clip();
|
|
596
|
+
var image=renderRoomImage(floor,surface.width,surface.height);if(image)ctx.drawImage(image,surface.x,surface.y);
|
|
597
|
+
floor.actors.forEach(function(actor){drawActor(floor,actor,surface);});
|
|
598
|
+
if(floor.completedAt){ctx.fillStyle='rgba(53,90,75,.18)';ctx.fillRect(surface.x,surface.y,surface.width,surface.height);}ctx.restore();
|
|
599
|
+
}
|
|
600
|
+
function render(){
|
|
601
|
+
if(!model)return;chooseRenderScale();var floor=activeRoom();
|
|
602
|
+
if(mode==='room'&&floor){
|
|
603
|
+
setCanvasSize(model.cols*T,model.rows*T);var image=renderRoomImage(floor,canvas.width,canvas.height);if(image)ctx.drawImage(image,0,0);else drawRoomBase(floor);
|
|
604
|
+
floor.actors.forEach(function(actor){drawActor(floor,actor);});drawRoomSupervisor();
|
|
605
|
+
}else{drawBackdrop(model.cols,model.rows,null);drawHallway();visibleRooms().forEach(drawOverviewRoom);drawSupervisor();if(!visibleRooms().length)fillText(t('quiet'),4*T,18*T,12);}
|
|
606
|
+
fitCanvas();
|
|
607
|
+
}
|
|
608
|
+
function renderRoomNav(rooms){
|
|
609
|
+
NAV_PAGE_SIZE=Math.max(1,Math.min(8,Math.floor((window.innerWidth-160)/120)));
|
|
610
|
+
if (mode !== "overview") { if (navSignature !== lang + "|room") { roomNav.textContent = ""; navSignature = lang + "|room"; } return; }
|
|
611
|
+
var pages = Math.max(1, Math.ceil(rooms.length / NAV_PAGE_SIZE));
|
|
612
|
+
navPage = Math.max(0, Math.min(navPage, pages - 1));
|
|
613
|
+
var signature = lang + "|overview|" + (showRecent ? "recent" : "active") + "|" + selectedKey + "|" + navPage + "|" + rooms.map(function(f){ return f.key; }).join(",");
|
|
614
|
+
if (signature === navSignature) return;
|
|
615
|
+
navSignature = signature; roomNav.textContent = "";
|
|
616
|
+
if (pages > 1) { var previous = document.createElement("button"); previous.type = "button"; previous.textContent = "‹ " + t("previous"); previous.disabled = navPage === 0; previous.addEventListener("click", function(){ navPage--; updateHeader(); }); roomNav.appendChild(previous); }
|
|
617
|
+
rooms.slice(navPage * NAV_PAGE_SIZE, navPage * NAV_PAGE_SIZE + NAV_PAGE_SIZE).forEach(function(floor, offset){ var button = document.createElement("button"); button.type = "button"; button.textContent = (navPage * NAV_PAGE_SIZE + offset + 1) + ". " + short(floor.title, 18); button.title = t("enter") + ": " + floor.title; button.setAttribute("aria-current", floor.key === selectedKey ? "true" : "false"); button.addEventListener("click", function(){ selectedKey = floor.key; enterRoom(); }); roomNav.appendChild(button); });
|
|
618
|
+
if (pages > 1) { var next = document.createElement("button"); next.type = "button"; next.textContent = t("next") + " ›"; next.disabled = navPage === pages - 1; next.addEventListener("click", function(){ navPage++; updateHeader(); }); roomNav.appendChild(next); }
|
|
619
|
+
}
|
|
620
|
+
function updateHeader(){
|
|
621
|
+
if(document.documentElement)document.documentElement.lang=lang==='zh'?'zh-Hant':'en';
|
|
622
|
+
canvas.setAttribute('aria-label',(lang==='zh'?'方向鍵或 WASD 移動 Supervisor;Enter 進入房間或查看細節;Esc 返回。':'Arrow keys or WASD move Supervisor; Enter opens rooms or details; Escape returns.')+' '+t('keyboardWork')+';'+t('keyboardPhase')+';'+t('keyboardQuestions'));
|
|
623
|
+
var rooms = visibleRooms(), people = rooms.reduce(function(n, f){ return n + f.actors.length; }, 0);
|
|
624
|
+
crumb.textContent = mode === "room" && activeRoom() ? activeRoom().title : t("overview") + " · " + rooms.length + " " + t("rooms") + " · " + people + " " + t("people");
|
|
625
|
+
backButton.hidden = mode !== "room"; backButton.textContent = "← " + t("back"); recentButton.textContent = (showRecent ? "✓ " + t("recentOn") : "▣ " + t("recent")) + (hasRecent() ? " (" + model.floors.filter(function(f){ return !!f.completedAt; }).length + ")" : "");
|
|
626
|
+
languageButton.textContent = lang === "zh" ? "繁中 / EN" : "EN / 繁中"; live.textContent = offline ? t("offline") : t("connected");
|
|
627
|
+
renderRoomNav(rooms);
|
|
628
|
+
}
|
|
629
|
+
function tickDialogue(){
|
|
630
|
+
if (!model) return;
|
|
631
|
+
var floors = mode === "room" && activeRoom() ? [activeRoom()] : visibleRooms(), lines = [];
|
|
632
|
+
floors.forEach(function(floor){ floor.actors.forEach(function(actor){ lines.push(floor.title + ": " + actor.label + " · " + localizeText(actor.narration)); }); if (floor.questions.length) lines.push(floor.title + ": " + t("waiting")); });
|
|
633
|
+
if (!lines.length) lines = [t("quiet")];
|
|
634
|
+
var line = offline ? t("offline") : localizeText(lines[lineIndex % Math.max(1, lines.length)]);
|
|
635
|
+
typed = Math.min(line.length, typed + 2); dialogueBox.textContent = line.slice(0, typed);
|
|
636
|
+
if (typed === line.length && frame % 180 === 0) { lineIndex++; typed = 0; }
|
|
637
|
+
}
|
|
638
|
+
function enterRoom(){ if (!selectedKey && visibleRooms()[0]) selectedKey = visibleRooms()[0].key; var floor = activeRoom(); if (!floor) return; enterRoomFromHallway(floor); render(); }
|
|
639
|
+
function goOverview(){ mode = "overview"; var hall = hallwayBounds(); supervisor.x = supervisor.targetX = hall.x + hall.width / 2; supervisor.y = supervisor.targetY = hall.y + hall.height - 5; hallwayReturn = null; updateHeader(); render(); canvas.focus(); }
|
|
640
|
+
function changeLanguage(){ lang = lang === "zh" ? "en" : "zh"; saveLanguage(); updateHeader(); render(); }
|
|
641
|
+
function showDialog(title, fill){
|
|
642
|
+
if (statusBox.hidden) detailPreviousFocus = document.activeElement || canvas;
|
|
643
|
+
statusBox.textContent = ""; var heading = document.createElement("h2"); heading.id = "status-title"; heading.textContent = title; statusBox.appendChild(heading);
|
|
644
|
+
var body = document.createElement("div"); body.className = "dialog-body"; statusBox.appendChild(body); fill(body);
|
|
645
|
+
var actions = document.createElement("div"); actions.className = "actions";
|
|
646
|
+
var close = document.createElement("button"); close.type = "button"; close.textContent = t("close"); close.addEventListener("click", closeDialog); actions.appendChild(close);
|
|
647
|
+
statusBox.appendChild(actions); statusBox.hidden = false; dialogClose = close;
|
|
648
|
+
if (app) { app.inert = true; app.setAttribute("aria-hidden", "true"); }
|
|
649
|
+
close.focus();
|
|
650
|
+
}
|
|
651
|
+
function closeDialog(){ statusBox.hidden = true; if (app) { app.inert = false; app.removeAttribute("aria-hidden"); } var previous = detailPreviousFocus || canvas; if (!previous || previous.isConnected === false || !previous.focus) previous = canvas; dialogClose = null; detailPreviousFocus = null; if (previous && previous.focus) previous.focus(); }
|
|
652
|
+
function addLine(box, label, value){ var p = document.createElement("p"); p.textContent = label + ": " + safeText(value); box.appendChild(p); }
|
|
653
|
+
function addCommand(box, value, copyValue){
|
|
654
|
+
var row = document.createElement("p"), code = document.createElement("code"), button = document.createElement("button");
|
|
655
|
+
code.textContent = value; button.type = "button"; button.textContent = t("copy"); button.addEventListener("click", function(){
|
|
656
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(copyValue || value).then(function(){ button.textContent = t("copied"); }, function(){});
|
|
657
|
+
}); row.appendChild(code); row.appendChild(button); box.appendChild(row);
|
|
658
|
+
}
|
|
659
|
+
function pageItems(items, box, renderItem){
|
|
660
|
+
var small=window.innerWidth<480, pageSize=Math.max(1,Math.min(small?3:5,Math.floor((window.innerHeight*.88-145)/(small?75:56)))), pages;
|
|
661
|
+
items = items.flatMap(function(item){var value=item.value||'', limit=window.innerWidth<480?45:90, chunks=[];for(var offset=0;offset<value.length;offset+=limit)chunks.push(Object.assign({},item,{value:value.slice(offset,offset+limit),copyValue:item.kind==='command'?value:undefined}));return chunks.length?chunks:[item];});
|
|
662
|
+
pages = Math.max(1,Math.ceil(items.length/pageSize));detailPage=Math.max(0,Math.min(detailPage,pages-1));
|
|
663
|
+
items.slice(detailPage * pageSize, detailPage * pageSize + pageSize).forEach(function(item){ renderItem(item, box); });
|
|
664
|
+
var paging = document.createElement("div"); paging.className = "actions dialog-pager"; var page = document.createElement("span"); page.textContent = t("page") + " " + (detailPage + 1) + "/" + pages; paging.appendChild(page);
|
|
665
|
+
if (pages > 1) { var previous = document.createElement("button"); previous.type = "button"; previous.textContent = t("previous"); previous.disabled = detailPage === 0; previous.addEventListener("click", function(){ detailPage--; openSelectedDetail(); }); var next = document.createElement("button"); next.type = "button"; next.textContent = t("next"); next.disabled = detailPage === pages - 1; next.addEventListener("click", function(){ detailPage++; openSelectedDetail(); }); paging.appendChild(previous); paging.appendChild(next); }
|
|
666
|
+
box.appendChild(paging);
|
|
667
|
+
}
|
|
668
|
+
function sourceFor(floor){
|
|
669
|
+
if (!snapshot) return null;
|
|
670
|
+
if (floor.kind === "run") return snapshot.runs.find(function(r){ return r.runId === floor.key; }) || null;
|
|
671
|
+
if (floor.kind === "desk") return floor.sourceIndex === null ? null : snapshot.lobby[floor.sourceIndex] || null;
|
|
672
|
+
if (floor.kind === "review") return floor.sourceIndex === null ? null : snapshot.reviews[floor.sourceIndex] || null;
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
function sourceSignature(floor){return JSON.stringify(sourceFor(floor),function(key,value){return key==='budget'?undefined:value;});}
|
|
676
|
+
function renderDetailItem(item, box){
|
|
677
|
+
if (item.kind === "command") return addCommand(box, item.value, item.copyValue);
|
|
678
|
+
if (item.kind === "file") { var file = document.createElement("p"); file.textContent = "• " + item.value; box.appendChild(file); return; }
|
|
679
|
+
if (item.kind === "heading") { var heading = document.createElement("p"); heading.textContent = item.value; box.appendChild(heading); return; }
|
|
680
|
+
addLine(box, item.label, item.value);
|
|
681
|
+
}
|
|
682
|
+
function progressText(progress){return progress ? progress.passed+'/'+(progress.total||'?')+' · '+t('verify')+' '+outcomeText(progress.verify||'pending')+' · '+t('review')+' '+outcomeText(progress.review||'pending') : t('unknown');}
|
|
683
|
+
function workItemForActor(floor,actor){return {kind:'work',floorKey:floor.key,actorKey:actor.key,label:t('owner')+': '+actor.label+' · '+roleText(actor.kind),value:t('currentWork')+': '+localizeText(actor.narration)+' · '+t('phase')+': '+phaseLabel(actor.phase)+' · '+t('status')+': '+statusText(actor.status)+' · '+t('progress')+': '+progressText(actor.progress)+' · '+t('pending')+': '+actor.questionCount};}
|
|
684
|
+
function workItemsFor(floor){
|
|
685
|
+
var items=[
|
|
686
|
+
{kind:'line',label:t('task'),value:floor.board.taskId==='unassigned'?t('unassigned'):floor.board.taskId},
|
|
687
|
+
{kind:'line',label:t('status'),value:statusText(floor.status)},
|
|
688
|
+
{kind:'line',label:t('progress'),value:progressText({passed:floor.board.passed,total:floor.board.total,verify:floor.board.verify,review:floor.board.review})},
|
|
689
|
+
{kind:'line',label:t('pending'),value:String(floor.board.pending)}
|
|
690
|
+
];
|
|
691
|
+
floor.actors.forEach(function(actor){items.push(workItemForActor(floor,actor));});
|
|
692
|
+
floor.questions.forEach(function(question){items.push({kind:'question',label:t('question')+' · ! '+question.questionId,value:question.prompt});});
|
|
693
|
+
if (!floor.actors.length && !floor.questions.length) items.push({kind:'line',label:t('people'),value:'0'});
|
|
694
|
+
return items;
|
|
695
|
+
}
|
|
696
|
+
function phaseWorkItemsFor(floor,phase){
|
|
697
|
+
var items=[{kind:'line',label:t('phase'),value:phaseLabel(phase)}],actors=floor.actors.filter(function(actor){return actor.phase===phase;});
|
|
698
|
+
if(!actors.length)items.push({kind:'line',label:t('currentWork'),value:t('noPeople')});
|
|
699
|
+
actors.forEach(function(actor){items.push(workItemForActor(floor,actor));});
|
|
700
|
+
return items;
|
|
701
|
+
}
|
|
702
|
+
function questionItemsFor(floor){
|
|
703
|
+
var items=[];
|
|
704
|
+
if(!floor.questions.length)items.push({kind:'line',label:t('questions'),value:t('noQuestions')});
|
|
705
|
+
floor.questions.forEach(function(question){items.push({kind:'line',label:t('question')+' · ! '+question.questionId,value:question.prompt});});
|
|
706
|
+
var source=sourceFor(floor);
|
|
707
|
+
if(source&&source.commands&&source.commands.length){items.push({kind:'heading',value:t('commands')+':' });source.commands.forEach(function(command){items.push({kind:'command',value:command});});}
|
|
708
|
+
return items;
|
|
709
|
+
}
|
|
710
|
+
function renderWorkItem(item,box){
|
|
711
|
+
if(item.kind==='work'){
|
|
712
|
+
var row=document.createElement('p'),button=document.createElement('button'),detail=document.createElement('span');
|
|
713
|
+
button.type='button';button.textContent=item.label;button.title=t('details');button.addEventListener('click',function(){var floor=roomByKey(item.floorKey),actor=floor&&floor.actors.find(function(candidate){return candidate.key===item.actorKey;});if(floor&&actor)openActorDetail(floor,actor);});
|
|
714
|
+
detail.textContent=' · '+safeText(item.value);row.appendChild(button);row.appendChild(detail);box.appendChild(row);return;
|
|
715
|
+
}
|
|
716
|
+
if(item.kind==='question'){addLine(box,item.label,localizeText(item.value));return;}
|
|
717
|
+
renderDetailItem(item,box);
|
|
718
|
+
}
|
|
719
|
+
function openWorkList(floor){
|
|
720
|
+
if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='work')detailPage=0;
|
|
721
|
+
detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'work'};
|
|
722
|
+
showDialog(floor.title+' · '+t('workList'),function(box){pageItems(workItemsFor(floor),box,renderWorkItem);});
|
|
723
|
+
}
|
|
724
|
+
function openPhaseWorkList(floor,phase){
|
|
725
|
+
if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='phase'||detailTarget.phase!==phase)detailPage=0;
|
|
726
|
+
detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'phase',phase:phase};
|
|
727
|
+
showDialog(floor.title+' · '+phaseLabel(phase),function(box){pageItems(phaseWorkItemsFor(floor,phase),box,renderWorkItem);});
|
|
728
|
+
}
|
|
729
|
+
function openQuestionList(floor){
|
|
730
|
+
if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='questions')detailPage=0;
|
|
731
|
+
detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'questions'};
|
|
732
|
+
showDialog(floor.title+' · '+t('questions'),function(box){pageItems(questionItemsFor(floor),box,renderDetailItem);});
|
|
733
|
+
}
|
|
734
|
+
function openActorDetail(floor, actor){
|
|
735
|
+
var source = sourceFor(floor), items = [], entries = [];
|
|
736
|
+
if (source && actor.kind !== "reviewer" && floor.kind === "run") { var found = source.agents.find(function(a){ return a.id === actor.id; }); if (found && found.diff) items = found.diff.paths || []; }
|
|
737
|
+
if (source && floor.kind === "desk" && source.diff) items = source.diff.paths || [];
|
|
738
|
+
items.forEach(function(item){ entries.push({kind:"file", value:item}); });
|
|
739
|
+
if (source && source.commands) source.commands.forEach(function(item){ entries.push({kind:"command", value:item}); });
|
|
740
|
+
if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== actor.key) detailPage = 0;
|
|
741
|
+
var detailItems = [
|
|
742
|
+
{kind:"line", label:lang==='zh'?'職責':'Role', value:roleText(actor.kind)},
|
|
743
|
+
{kind:"line", label:t("status"), value:statusText(actor.status)}, {kind:"line", label:t("phase"), value:phaseLabel(actor.phase)},
|
|
744
|
+
{kind:"line", label:t("task"), value:actor.taskId === "unassigned" ? t("unassigned") : actor.taskId},
|
|
745
|
+
{kind:"line", label:t("host"), value:localizeText(actor.host)}, {kind:"line", label:t("now"), value:localizeText(actor.narration)}
|
|
746
|
+
];
|
|
747
|
+
if (actor.questionCount) detailItems.push({kind:"line", label:t("pending"), value:String(actor.questionCount)});
|
|
748
|
+
if (actor.progress) detailItems.push({kind:"line", label:t("progress"), value:actor.progress.passed + "/" + actor.progress.total + " " + t("verify") + " " + outcomeText(actor.progress.verify || "-") + " " + t("review") + " " + outcomeText(actor.progress.review || "-")});
|
|
749
|
+
if (actor.questionCount && floor.questions.length) floor.questions.forEach(function(question){ detailItems.push({kind:"line", label:"! " + question.questionId, value:question.prompt}); });
|
|
750
|
+
if (entries.length) { detailItems.push({kind:"heading", value:t("files") + " / " + t("commands") + ":"}); entries.forEach(function(entry){ detailItems.push(entry); }); }
|
|
751
|
+
detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: actor.key, view:'actor'}; showDialog(actor.label, function(box){ pageItems(detailItems, box, renderDetailItem); });
|
|
752
|
+
}
|
|
753
|
+
function openRoomDetail(floor){
|
|
754
|
+
if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== null) detailPage = 0;
|
|
755
|
+
detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: null, view:'room'}; var source = sourceFor(floor), items = source && source.diff ? source.diff.paths || [] : [], entries = [];
|
|
756
|
+
items.forEach(function(item){ entries.push({kind:"file", value:item}); });
|
|
757
|
+
if (source && source.commands) source.commands.forEach(function(item){ entries.push({kind:"command", value:item}); });
|
|
758
|
+
var detailItems = [
|
|
759
|
+
{kind:"line", label:t("status"), value:statusText(floor.status)}, {kind:"line", label:t("phase"), value:phaseLabel(floor.phase)},
|
|
760
|
+
{kind:"line", label:t("task"), value:floor.board.taskId === "unassigned" ? t("unassigned") : floor.board.taskId},
|
|
761
|
+
{kind:"line", label:t("progress"), value:floor.board.passed + "/" + (floor.board.total || "?")},
|
|
762
|
+
{kind:"line", label:t("verify"), value:outcomeText(floor.board.verify)}, {kind:"line", label:t("review"), value:outcomeText(floor.board.review)},
|
|
763
|
+
{kind:"line", label:t("pending"), value:String(floor.board.pending)}
|
|
764
|
+
];
|
|
765
|
+
floor.questions.forEach(function(question){ detailItems.push({kind:"line", label:"! " + question.questionId, value:question.prompt}); });
|
|
766
|
+
if (entries.length) { detailItems.push({kind:"heading", value:t("files") + " / " + t("commands") + ":"}); entries.forEach(function(entry){ detailItems.push(entry); }); }
|
|
767
|
+
showDialog(floor.title, function(box){ pageItems(detailItems, box, renderDetailItem); });
|
|
768
|
+
}
|
|
769
|
+
function openSelectedDetail(){
|
|
770
|
+
var target = detailTarget, floor = target ? roomByKey(target.floorKey) : activeRoom(); if (!floor) return;
|
|
771
|
+
if (target && target.view === 'work') return openWorkList(floor);
|
|
772
|
+
if (target && target.view === 'phase') return openPhaseWorkList(floor,target.phase);
|
|
773
|
+
if (target && target.view === 'questions') return openQuestionList(floor);
|
|
774
|
+
if (target && target.actorKey) { var actor = floor.actors.find(function(a){ return a.key === target.actorKey; }); if (actor) return openActorDetail(floor, actor); }
|
|
775
|
+
return openRoomDetail(floor);
|
|
776
|
+
}
|
|
777
|
+
function hitOverview(event){
|
|
778
|
+
var point=canvasPoint(event),x=point.x/T,y=point.y/T;
|
|
779
|
+
return visibleRooms().find(function(f){ var b = roomBox(f); return x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height; }) || null;
|
|
780
|
+
}
|
|
781
|
+
function hitActor(event, floor, surface){
|
|
782
|
+
var point=canvasPoint(event);surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
|
|
783
|
+
for (var i = floor.actors.length - 1; i >= 0; i--) { var a=floor.actors[i],box=actorBox(a,surface);if(inside(point,{x:box.x,y:box.y,width:32*box.scale,height:48*box.scale}))return a; }
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
function surfaceRect(rect,surface){return {x:rect.x+surface.x,y:rect.y+surface.y,width:rect.width,height:rect.height};}
|
|
787
|
+
function hitQuestionBadge(event,floor,surface){
|
|
788
|
+
var point=canvasPoint(event);surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
|
|
789
|
+
for(var i=floor.actors.length-1;i>=0;i--){var actor=floor.actors[i];if(actor.questionCount>0&&inside(point,alertGeometry(actor,surface)))return actor;}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
function actorForProp(floor,prop){
|
|
793
|
+
var prefix=floor.key+':desk:';
|
|
794
|
+
if(prop.kind!=='desk'||prop.key.indexOf(prefix)!==0)return null;
|
|
795
|
+
var id=prop.key.slice(prefix.length);
|
|
796
|
+
return floor.actors.find(function(actor){return actor.id===id;})||null;
|
|
797
|
+
}
|
|
798
|
+
function hitWorkBoard(event,floor,surface){
|
|
799
|
+
var point=canvasPoint(event),board=progressBoardGeometry(surface.width,surface.height);
|
|
800
|
+
board.x+=surface.x;board.y+=surface.y;
|
|
801
|
+
if(floor.board.pending>0){var pending=pendingBoardGeometry(surface.width,surface.height);pending.x+=surface.x;pending.y+=surface.y;if(inside(point,pending))return 'questions';}
|
|
802
|
+
if(inside(point,board))return 'progress';
|
|
803
|
+
var whiteboard=floor.props.find(function(prop){return prop.kind==='whiteboard';});
|
|
804
|
+
if(whiteboard){var rect=surfaceRect(propGeometry(whiteboard,surface.width,surface.height),surface);if(inside(point,rect))return 'whiteboard';}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
function hitWorkTarget(event,floor,surface){
|
|
808
|
+
var point=canvasPoint(event),props=floor.props;
|
|
809
|
+
for(var i=props.length-1;i>=0;i--){var prop=props[i];if(prop.kind!=='desk'&&prop.kind!=='bench'&&prop.kind!=='table')continue;var rect=surfaceRect(propGeometry(prop,surface.width,surface.height),surface);if(inside(point,rect))return {kind:'prop',phase:prop.phase,prop:prop,actor:actorForProp(floor,prop)};}
|
|
810
|
+
for(var j=floor.phaseAreas.length-1;j>=0;j--){var area=phaseAreaGeometry(floor.phaseAreas[j],surface.width,surface.height);if(inside(point,surfaceRect(area,surface)))return {kind:'phase',phase:floor.phaseAreas[j].phase,prop:null,actor:null};}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
canvas.addEventListener("click", function(event){
|
|
814
|
+
if (!model) return;
|
|
815
|
+
if (mode === "overview") {
|
|
816
|
+
var floor = hitOverview(event); if (!floor) return; selectedKey=floor.key;var surface=surfaceForFloor(floor),actor=hitActor(event,floor,surface);
|
|
817
|
+
if(actor){enterRoom();openActorDetail(activeRoom()||floor,actor);return;}
|
|
818
|
+
var badge=hitQuestionBadge(event,floor,surface);if(badge){openQuestionList(floor);return;}
|
|
819
|
+
var boardHit=hitWorkBoard(event,floor,surface);if(boardHit==='questions'){openQuestionList(floor);return;}if(boardHit){openWorkList(floor);return;}
|
|
820
|
+
var work=hitWorkTarget(event,floor,surface);if(work){if(work.actor)openActorDetail(floor,work.actor);else if(work.phase)openPhaseWorkList(floor,work.phase);return;}
|
|
821
|
+
enterRoom();return;
|
|
822
|
+
}
|
|
823
|
+
var room = activeRoom(); if (!room) return; var roomSurface=surfaceForFloor(room), actor = hitActor(event, room, roomSurface); if (actor) openActorDetail(room, actor); else {
|
|
824
|
+
var roomBadge=hitQuestionBadge(event,room,roomSurface);if(roomBadge){openQuestionList(room);return;}
|
|
825
|
+
var roomBoard=hitWorkBoard(event,room,roomSurface);if(roomBoard==='questions'){openQuestionList(room);return;}if(roomBoard){openWorkList(room);return;}
|
|
826
|
+
var roomWork=hitWorkTarget(event,room,roomSurface);if(roomWork){if(roomWork.actor)openActorDetail(room,roomWork.actor);else if(roomWork.phase)openPhaseWorkList(room,roomWork.phase);else openRoomDetail(room);return;}
|
|
827
|
+
openRoomDetail(room);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
canvas.addEventListener("keydown", function(event){
|
|
831
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
832
|
+
if (event.key === "Escape" || event.key === "Backspace") { event.preventDefault(); if (!statusBox.hidden) closeDialog(); else if (mode === "room") goOverview(); return; }
|
|
833
|
+
var keyboardFloor=activeRoom()||roomByKey(selectedKey)||visibleRooms()[0];
|
|
834
|
+
if (event.key === "l" || event.key === "L") { if (keyboardFloor) { event.preventDefault(); openWorkList(keyboardFloor); return; } }
|
|
835
|
+
if (event.key === "q" || event.key === "Q") { if (keyboardFloor) { event.preventDefault(); openQuestionList(keyboardFloor); return; } }
|
|
836
|
+
var phaseKeys={"1":"planning","2":"implementing","3":"verifying","4":"reviewing","5":"integrating"};
|
|
837
|
+
if (phaseKeys[event.key] && keyboardFloor) { event.preventDefault(); openPhaseWorkList(keyboardFloor,phaseKeys[event.key]); return; }
|
|
838
|
+
if (mode === "overview" || mode === "room") {
|
|
839
|
+
var move = {ArrowLeft:[-1,0], ArrowRight:[1,0], ArrowUp:[0,-1], ArrowDown:[0,1], a:[-1,0], d:[1,0], w:[0,-1], s:[0,1], A:[-1,0], D:[1,0], W:[0,-1], S:[0,1]}[event.key];
|
|
840
|
+
if (move) { event.preventDefault(); moveSupervisor(move[0], move[1]); return; }
|
|
841
|
+
}
|
|
842
|
+
var rooms = visibleRooms(); if (!rooms.length) return;
|
|
843
|
+
var index = Math.max(0, rooms.findIndex(function(f){ return f.key === selectedKey; }));
|
|
844
|
+
if (event.key === "ArrowRight" || event.key === "ArrowDown") { event.preventDefault(); selectedKey = rooms[(index + 1) % rooms.length].key; updateHeader(); render(); }
|
|
845
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowUp") { event.preventDefault(); selectedKey = rooms[(index - 1 + rooms.length) % rooms.length].key; updateHeader(); render(); }
|
|
846
|
+
if (event.key === "Enter" && mode === "overview") { event.preventDefault(); enterRoom(); }
|
|
847
|
+
if (event.key === "Enter" && mode === "room") { event.preventDefault(); openSelectedDetail(); }
|
|
848
|
+
});
|
|
849
|
+
backButton.addEventListener("click", goOverview); recentButton.addEventListener("click", function(){ showRecent = !showRecent; if (!showRecent && activeRoom() && activeRoom().completedAt) mode = "overview"; updateHeader(); render(); });
|
|
850
|
+
languageButton.addEventListener("click", changeLanguage); statusBox.addEventListener("keydown", function(event){
|
|
851
|
+
if (event.key === "Escape") { event.preventDefault(); closeDialog(); return; }
|
|
852
|
+
if (event.key !== "Tab") return;
|
|
853
|
+
var focusables = statusBox.querySelectorAll ? Array.prototype.slice.call(statusBox.querySelectorAll("button:not([disabled])")) : (dialogClose ? [dialogClose] : []);
|
|
854
|
+
if (!focusables.length) return;
|
|
855
|
+
var first = focusables[0], last = focusables[focusables.length - 1];
|
|
856
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
857
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
858
|
+
});
|
|
859
|
+
function poll(){ fetch("snapshot.json" + location.search, {cache:"no-store"}).then(function(r){ if (!r.ok) throw new Error(String(r.status)); return r.json(); }).then(function(s){ snapshot = s; model = sceneModel(s); offline = false; var selectedMissing=mode==='room'&&!!selectedKey&&!roomByKey(selectedKey),dialogMissing=!statusBox.hidden&&!!detailTarget&&!roomByKey(detailTarget.floorKey); if(selectedMissing||dialogMissing){if(dialogMissing)detailPreviousFocus=canvas;if(!statusBox.hidden)closeDialog();detailTarget=null;detailPage=0;detailSignature="";mode='overview';} if (!selectedKey || !roomByKey(selectedKey)) selectedKey = visibleRooms()[0] && visibleRooms()[0].key; updateHeader(); render(); if (!statusBox.hidden && detailTarget && roomByKey(detailTarget.floorKey)) { var signature=sourceSignature(roomByKey(detailTarget.floorKey)); if(signature!==detailSignature){detailSignature=signature;openSelectedDetail();} } }, function(){ offline = true; updateHeader(); }); }
|
|
860
|
+
function loop(){ frame++; render(); tickDialogue(); requestAnimationFrame(loop); }
|
|
861
|
+
updateHeader(); poll(); setInterval(poll, 2000); window.addEventListener("resize", render); requestAnimationFrame(loop);
|
|
862
|
+
`;
|
|
863
|
+
/** One inline page; the nonce binds its only script and style under the server's CSP. */
|
|
864
|
+
export function officePage(nonce) {
|
|
865
|
+
const script = CLIENT.replace("__PALETTE__", () => JSON.stringify(PALETTE)).replace("__SPRITES__", () => JSON.stringify(SPRITES))
|
|
866
|
+
.replace("__SCENE__", () => sceneModel.toString()).replace("__AVATAR__", () => avatarSprite.toString()).replace("__AVATAR_COLORS__", () => avatarPalette.toString());
|
|
867
|
+
return `<!doctype html>
|
|
868
|
+
<html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
869
|
+
<meta name="referrer" content="no-referrer"><title>agent-ops Office · Preview</title><style nonce="${nonce}">${STYLE}</style></head>
|
|
870
|
+
<body><div id="app"><header id="header"><div id="brand">agent-ops Office · Preview</div><div id="crumb">Office overview</div><button id="back" type="button" hidden>← Back to overview</button><button id="recent" type="button">▣ Recently completed</button><button id="language" type="button">繁中 / EN</button><span id="live" aria-live="polite">● Connected</span></header>
|
|
871
|
+
<main id="wrap"><canvas id="office" tabindex="0" width="576" height="304" aria-label="Office rooms; use arrow keys and Enter to explore"></canvas><div id="dialogue" role="status" aria-live="polite"></div><nav id="room-nav" aria-label="Office rooms"></nav></main></div>
|
|
872
|
+
<section id="status" role="dialog" aria-modal="true" aria-labelledby="status-title" tabindex="-1" hidden></section><script nonce="${nonce}">${script}</script></body></html>`;
|
|
873
|
+
}
|