@songsid/agend 2.3.2-beta.3 → 2.3.2-beta.4
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/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +5 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro.d.ts +21 -1
- package/dist/backend/kiro.js +89 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/channel/adapters/discord.js +7 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +4 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +5 -0
- package/dist/classic-channel-manager.js +14 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +22 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +6 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.js +16 -4
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +20 -6
- package/dist/dashboard-permissions.js +58 -9
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/fleet-manager.d.ts +16 -1
- package/dist/fleet-manager.js +183 -21
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +6 -2
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +8 -0
- package/dist/instance-lifecycle.js +5 -3
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +2 -6
- package/dist/locale.js.map +1 -1
- package/dist/notice-delivery.d.ts +15 -0
- package/dist/notice-delivery.js +37 -0
- package/dist/notice-delivery.js.map +1 -0
- package/dist/outbound-schemas.d.ts +20 -0
- package/dist/outbound-schemas.js +17 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart.js +2 -0
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-triggers.d.ts +2 -0
- package/dist/reply-triggers.js +3 -0
- package/dist/reply-triggers.js.map +1 -0
- package/dist/settings-api.js +27 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +17 -14
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +4 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-host.js +2 -1
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/tool-permissions.js +10 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +2 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +1 -1
- package/dist/turn-reply-guard.js +1 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +16 -6
- package/dist/ui/fleet-office-editor.js +10 -6
- package/dist/ui/fleet-office-layout.js +34 -28
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +2 -2
- package/dist/ui/fleet-office-pack.d.ts +3 -1
- package/dist/ui/fleet-office-pack.js +58 -7
- package/dist/ui/fleet-office-skin.js +13 -4
- package/dist/ui/fleet-office-store.js +27 -1
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +155 -6
- package/dist/ui/fleet-office.js +169 -41
- package/dist/ui/panel-settings.js +32 -10
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-model.js +14 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +14 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-shell.js +14 -6
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +17 -2
- package/dist/ui/shared/side-panel.js +95 -0
- package/dist/web-api.d.ts +3 -0
- package/dist/web-api.js +11 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-office-acts.d.ts +173 -0
- package/dist/web-office-acts.js +285 -0
- package/dist/web-office-acts.js.map +1 -0
- package/dist/web-office-skins.d.ts +1 -1
- package/dist/web-office-skins.js +1 -1
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/package.json +1 -1
- package/templates/windows-shortcuts.ps1 +11 -2
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// changes it walks (along the pack's waypoints) to the new area. Pure — fleet-office.js draws it. Also here: what a
|
|
4
4
|
// character says in its speech bubble, and which furniture is active.
|
|
5
5
|
import { LOGICAL, own } from "./fleet-office-pack.js";
|
|
6
|
-
import { nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
|
|
6
|
+
import { FLIGHT, nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
|
|
7
|
+
import { hue } from "./fleet-traffic.js";
|
|
7
8
|
|
|
8
9
|
/** The area for a state: the pack's own, else its "default", else its first area. */
|
|
9
10
|
export function zoneOf(pack, state) {
|
|
@@ -76,12 +77,16 @@ export function assignSeats(pack, wants, prev = new Map()) {
|
|
|
76
77
|
* the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph, the
|
|
77
78
|
* seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
|
|
78
79
|
*/
|
|
79
|
-
export function zonesLayout(pack, nodes, prev = new Map()) {
|
|
80
|
-
|
|
80
|
+
export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {}) {
|
|
81
|
+
// At night (#2568) the idle go to bed: the area asleep people go to while it has a free seat; the rest doze where they are.
|
|
82
|
+
const bed = zoneOf(pack, "paused"), beds = (own(pack.areas, bed)?.seats || []).length, sleepers = new Set(), inBed = new Set();
|
|
83
|
+
let used = nodes.filter((n) => n.state !== "idle" && zoneOf(pack, n.state) === bed).length;
|
|
84
|
+
if (night) for (const n of nodes) if (n.state === "idle") { sleepers.add(n.name); if (used < beds) { used++; inBed.add(n.name); } }
|
|
85
|
+
const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: inBed.has(n.name) ? bed : zoneOf(pack, n.state) }));
|
|
81
86
|
const { seats, crowd, over } = assignSeats(pack, wants, prev);
|
|
82
87
|
const desks = nodes.filter((n) => seats.has(n.name)).map((n) => {
|
|
83
88
|
const s = seats.get(n.name);
|
|
84
|
-
return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot };
|
|
89
|
+
return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot, night: sleepers.has(n.name) };
|
|
85
90
|
});
|
|
86
91
|
return { mode: "zones", rooms: [{ key: "\u0000zones", name: null, lobby: false, x: 0, y: 0, w: LOGICAL.w, h: LOGICAL.h, desks }], areas: pack.areas,
|
|
87
92
|
waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
@@ -103,6 +108,145 @@ export function headingAt(pts, t) {
|
|
|
103
108
|
}
|
|
104
109
|
return null;
|
|
105
110
|
}
|
|
111
|
+
// ── Night (#2568) ──
|
|
112
|
+
|
|
113
|
+
/** Bedtime on the viewer's clock: from FROM o'clock to TO o'clock the idle sleep (anyone with work still gets up for it). */
|
|
114
|
+
export const NIGHT = { FROM: 0, TO: 6, TICK: 60000 };
|
|
115
|
+
export const isNight = (d = new Date()) => { const h = d.getHours(); return NIGHT.FROM <= NIGHT.TO ? h >= NIGHT.FROM && h < NIGHT.TO : h >= NIGHT.FROM || h < NIGHT.TO; };
|
|
116
|
+
|
|
117
|
+
// ── Wandering (#2560) ──
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* An idle person now and then strolls to a spot near their seat and back, so a quiet room is not a row of statues:
|
|
121
|
+
* every PERIOD ms (each person's own phase, from their name) they are AWAY ms at a waypoint within REACH of the seat —
|
|
122
|
+
* one no seat of the office is on (taken or free), picked anew each time — then walk back. Pure: the page asks every TICK ms and walks the change.
|
|
123
|
+
*/
|
|
124
|
+
export const WANDER = { PERIOD: 40000, AWAY: 8000, REACH: 220, TICK: 1000 };
|
|
125
|
+
/** Where `name`, seated at `seat`, should be at `t` (ms) while idle: a spot near the seat, or the seat. */
|
|
126
|
+
export function wanderTarget(name, seat, graph, t, seats = []) {
|
|
127
|
+
const near = (graph.waypoints || []).filter((w) => Math.hypot(w.x - seat.x, w.y - seat.y) <= WANDER.REACH
|
|
128
|
+
&& !seats.some((s) => Math.hypot(w.x - s.x, w.y - s.y) < 4));
|
|
129
|
+
if (!near.length) return seat;
|
|
130
|
+
const h = nameHash(name), k = t + (h % WANDER.PERIOD), turn = Math.floor(k / WANDER.PERIOD);
|
|
131
|
+
if (k - turn * WANDER.PERIOD >= WANDER.AWAY) return seat;
|
|
132
|
+
const w = near[nameHash(`${name}\u0000${turn}`) % near.length];
|
|
133
|
+
return { x: w.x, y: w.y };
|
|
134
|
+
}
|
|
135
|
+
/** An NPC drawn as furniture (#2596: an item with `walk`): its walker's name — never a person's, and its own phase. */
|
|
136
|
+
export const npcKey = (f) => `furniture/${f.id}`;
|
|
137
|
+
/** Everyone's walk target: their seat, or — idle and `wander` on — wanderTarget's spot (one person a spot: whoever
|
|
138
|
+
* would share it stays seated this time). And, `wander` on, each item of `furniture` with a `walk` (#2596): its spot
|
|
139
|
+
* (x, y), or a stroll near it as an idle person's (by npcKey) — none at `night` for one with a night place (asleep there). */
|
|
140
|
+
export function walkTargets(desks, graph, t, wander = true, furniture = [], night = false) {
|
|
141
|
+
// Every seat of the office (anyone's, taken or not: someone may sit down on it any moment), not only the ones in use.
|
|
142
|
+
// And each walking NPC's spot (#2596 review): its home, where it stands outside its stroll and goes back to whenever
|
|
143
|
+
// its stroll would collide, so nobody else's stroll may end there — no two walkers ever share a target.
|
|
144
|
+
const homes = wander ? furniture.filter((f) => f.walk && !(night && f.night)).map((f) => ({ x: f.x, y: f.y })) : [];
|
|
145
|
+
const seats = [...Object.values(graph.areas || {}).flatMap((a) => a.seats || []), ...desks.map((d) => d.seat), ...homes];
|
|
146
|
+
const used = new Set(homes.map((h) => `${h.x},${h.y}`));
|
|
147
|
+
const stroll = (name, home) => {
|
|
148
|
+
const p = wanderTarget(name, home, graph, t, seats), k = `${p.x},${p.y}`;
|
|
149
|
+
if (p === home || used.has(k)) return home;
|
|
150
|
+
used.add(k);
|
|
151
|
+
return p;
|
|
152
|
+
};
|
|
153
|
+
const out = new Map(desks.map((d) => [d.node.name, !wander || d.node.state !== "idle" || d.night ? d.seat : stroll(d.node.name, d.seat)]));
|
|
154
|
+
if (wander) for (const f of furniture) if (f.walk && !(night && f.night)) out.set(npcKey(f), stroll(npcKey(f), { x: f.x, y: f.y }));
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Mail from another office (#2564) ──
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Something sent to a person here by someone who is not (in another office, or in none): it flies in at the door and
|
|
162
|
+
* lands on the floor near the middle of the room; the person walks over, picks it up and walks back to their seat.
|
|
163
|
+
* MAX_WAIT: a letter no one picks up is gone after it; GAP: letters waiting side by side; SEARCH: how far from the
|
|
164
|
+
* middle a landing spot is looked for when no waypoint will do; ASLEEP: who does not get up
|
|
165
|
+
* for it (it waits for them to wake, up to MAX_WAIT).
|
|
166
|
+
*/
|
|
167
|
+
export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
|
|
168
|
+
/**
|
|
169
|
+
* Where the `n`th waiting letter lands: `n` GAPs along from the waypoint nearest the room's middle — the nearest whose
|
|
170
|
+
* spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
|
|
171
|
+
* with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
|
|
172
|
+
* those either, null — the letter is not delivered here (never on a seat).
|
|
173
|
+
*/
|
|
174
|
+
export function dropPoint(L, n = 0) {
|
|
175
|
+
const mid = { x: L.width / 2, y: L.height * 0.55 }, d = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
|
|
176
|
+
const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
|
|
177
|
+
const onSeat = (p) => seats.some((q) => d(p, q) < 4);
|
|
178
|
+
const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
|
|
179
|
+
.filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
|
|
180
|
+
if (spots.length) return { x: spots[0].x, y: spots[0].y };
|
|
181
|
+
const inRoom = (p) => p.x >= 0 && p.y >= 0 && p.x <= L.width && p.y <= L.height;
|
|
182
|
+
for (let r = 0; r <= MAIL.SEARCH; r += 2 * MAIL.GAP) {
|
|
183
|
+
for (let k = 0, m = r ? 16 : 1; k < m; k++) {
|
|
184
|
+
const a = (2 * Math.PI * k) / m, p = { x: Math.round(mid.x + r * Math.cos(a) + n * MAIL.GAP), y: Math.round(mid.y + r * Math.sin(a)) };
|
|
185
|
+
if (inRoom(p) && !onSeat(p)) return p;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const desksIn = (L) => (L.rooms || []).flatMap((r) => r.desks || []).filter((d) => d.node);
|
|
191
|
+
/**
|
|
192
|
+
* The letters of one office. `sync(items, now, L, still)`: each new traffic item to someone drawn here from someone
|
|
193
|
+
* who is not becomes a letter (none with reduced motion, nor for a stale delegation; a zones office only).
|
|
194
|
+
* `frame(now, L, walkers)` → what to draw, { id, hue, kind, from, x, y, t } (t: its flight, 0 on the floor), picking
|
|
195
|
+
* up the letters whose person stands on them, and dropping those gone too long or whose person left. `targets(L)`:
|
|
196
|
+
* name → where to walk to pick one up (the first waiting for them; none while they are asleep). `prime(items)`: mark as
|
|
197
|
+
* seen what was in the air before this office was shown (a new office's or style's mail starts empty). `size()`.
|
|
198
|
+
*/
|
|
199
|
+
export function createMail({ max = MAIL.MAX } = {}) {
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
let letters = [];
|
|
202
|
+
const here = (L, name) => desksIn(L).find((d) => d.node.name === name) || null;
|
|
203
|
+
return {
|
|
204
|
+
sync(items, now, L, still = false) {
|
|
205
|
+
let started = 0;
|
|
206
|
+
for (const it of items || []) {
|
|
207
|
+
if (seen.has(it.id)) continue;
|
|
208
|
+
seen.add(it.id);
|
|
209
|
+
if (seen.size > 2000) seen.delete(seen.values().next().value);
|
|
210
|
+
if (still || it.phase === "stale" || !L || L.mode !== "zones" || !here(L, it.to) || here(L, it.from)) continue;
|
|
211
|
+
const drop = dropPoint(L, letters.filter((l) => l.to !== it.to).length % 4);
|
|
212
|
+
if (!drop) continue; // nowhere off the seats: not delivered here
|
|
213
|
+
const pts = [{ x: L.door.x, y: L.door.y }, drop];
|
|
214
|
+
letters.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now, drop, pts,
|
|
215
|
+
ms: Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(pts) / FLIGHT.SPEED)) });
|
|
216
|
+
started++;
|
|
217
|
+
}
|
|
218
|
+
if (letters.length > max) letters = letters.slice(letters.length - max);
|
|
219
|
+
return started;
|
|
220
|
+
},
|
|
221
|
+
frame(now, L, walkers) {
|
|
222
|
+
const out = [];
|
|
223
|
+
letters = letters.filter((l) => {
|
|
224
|
+
if (!here(L, l.to)) return false;
|
|
225
|
+
const t = (now - l.start) / l.ms;
|
|
226
|
+
if (t < 1) { if (t >= 0) { const p = along(l.pts, t); out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: p.x, y: p.y, t }); } return true; }
|
|
227
|
+
if (now - l.start - l.ms > MAIL.MAX_WAIT) return false;
|
|
228
|
+
const w = walkers && walkers.at(l.to, now);
|
|
229
|
+
if (w && !w.walking && Math.hypot(w.x - l.drop.x, w.y - l.drop.y) < 4) return false; // picked up
|
|
230
|
+
out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: l.drop.x, y: l.drop.y, t: 0 });
|
|
231
|
+
return true;
|
|
232
|
+
});
|
|
233
|
+
return out;
|
|
234
|
+
},
|
|
235
|
+
targets(L, now) {
|
|
236
|
+
const out = new Map();
|
|
237
|
+
for (const l of letters) {
|
|
238
|
+
if (out.has(l.to) || now - l.start < l.ms) continue;
|
|
239
|
+
const d = here(L, l.to);
|
|
240
|
+
if (d && !MAIL.ASLEEP.has(d.node.state) && !d.night) out.set(l.to, l.drop); // asleep, or in bed for the night (#2568)
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
},
|
|
244
|
+
/** What is already in the traffic when this office's mail starts (#2564): seen, so it is not delivered again here. */
|
|
245
|
+
prime(items) { for (const it of items || []) seen.add(it.id); },
|
|
246
|
+
size: () => letters.length,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
106
250
|
/**
|
|
107
251
|
* Where each character is. `sync(targets, now, graph, still)`: targets name → { x, y }; someone new is already there;
|
|
108
252
|
* someone whose target moved walks from where they are now along the graph (routeVia) — or is there at once when
|
|
@@ -232,8 +376,13 @@ export function linesFor(state, lang, pack, defaults) {
|
|
|
232
376
|
// ── Furniture ──
|
|
233
377
|
|
|
234
378
|
/** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
|
|
235
|
-
export function activeFurniture(pack, states) {
|
|
379
|
+
export function activeFurniture(pack, states, byName = new Map()) {
|
|
236
380
|
const on = new Set();
|
|
237
|
-
for (const f of (pack && pack.furniture) || [])
|
|
381
|
+
for (const f of (pack && pack.furniture) || []) {
|
|
382
|
+
if (!f.activeWhen) continue;
|
|
383
|
+
// activeFor (#2570): only those people's states count; else anyone's shown.
|
|
384
|
+
const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
|
|
385
|
+
if (hit) on.add(f.id);
|
|
386
|
+
}
|
|
238
387
|
return on;
|
|
239
388
|
}
|
package/dist/ui/fleet-office.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Fleet → Org chart → Office: the fleet as a small pixel office, the third view beside Canvas and List
|
|
2
2
|
// (fleet-canvas.js). The same data as theirs: the structure from /ui/org (buildOrgView), the live state from the app
|
|
3
|
-
// store, the traffic from fleet-traffic.js's store — nothing new is read but the offices people made (/ui/offices).
|
|
3
|
+
// store, the traffic from fleet-traffic.js's store — nothing new is read but the offices people made (/ui/offices) and what agents do in them (/ui/office-acts, #2578).
|
|
4
4
|
//
|
|
5
5
|
// - Two layouts, by skin. Desks (the built-in "pixel" skin, the default): each team is a room, each member a desk with a
|
|
6
6
|
// name tag; General and the instances in no team share the lobby; a grid that wraps at the stage's width
|
|
@@ -62,8 +62,9 @@ import { hits, needle } from "./fleet-canvas-layout.js";
|
|
|
62
62
|
import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
|
|
63
63
|
officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
|
|
64
64
|
import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
|
|
65
|
-
import { CALLING, activeFurniture, bubbleAt, bubbleRank, createWalkers, linesFor, personAt, placeBubbles, thinBubbles, zonesLayout } from "./fleet-office-zones.js";
|
|
66
|
-
import { createOfficeStore } from "./fleet-office-store.js";
|
|
65
|
+
import { CALLING, activeFurniture, bubbleAt, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
|
|
66
|
+
import { actsOk, createOfficeStore } from "./fleet-office-store.js";
|
|
67
|
+
import { POLL_MS } from "./fleet-traffic.js";
|
|
67
68
|
import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
|
|
68
69
|
import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
|
|
69
70
|
import { applyOverrides } from "./fleet-office-pack.js";
|
|
@@ -83,7 +84,7 @@ register("orgo", {
|
|
|
83
84
|
b_working: "On it.|Typing…|Almost there|In the zone|One more test", b_idle: "Tea time|Ready when you are|Stretching|Anything for me?",
|
|
84
85
|
b_paused: "Zzz…|Back later", b_stopped: "Off duty|Zzz…", b_needs: "Need you!|A question for you|Over here!",
|
|
85
86
|
b_crashed: "Ouch, crashed|Help!", b_stuck: "Hmm, stuck?|Still thinking…",
|
|
86
|
-
hideAsleep: "Hide paused and stopped", layoutIgnored: "Its saved layout does not fit this style: drawn as the style's own.",
|
|
87
|
+
hideAsleep: "Hide paused and stopped", nightOn: "Bedtime 0–6: the idle sleep", layoutIgnored: "Its saved layout does not fit this style: drawn as the style's own.",
|
|
87
88
|
fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
|
|
88
89
|
fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
|
|
89
90
|
allOffices: "All offices", labelAll: "All offices: each office is a tile, drawn in its own style. Drag to pan; the wheel, a pinch, + and − zoom, 0 fits, F is fullscreen. Double-click a tile to open that office.",
|
|
@@ -99,7 +100,7 @@ register("orgo", {
|
|
|
99
100
|
b_working: "處理中|打字中…|快好了|專心中|再測一次", b_idle: "喝茶中|隨時可以開工|伸個懶腰|有事找我嗎?",
|
|
100
101
|
b_paused: "Zzz…|晚點回來", b_stopped: "下班了|Zzz…", b_needs: "需要你!|有個問題|在這裡!",
|
|
101
102
|
b_crashed: "糟糕,當掉了|救命!", b_stuck: "嗯,卡住了?|還在想…",
|
|
102
|
-
hideAsleep: "隱藏暫停與停止的成員", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
|
|
103
|
+
hideAsleep: "隱藏暫停與停止的成員", nightOn: "0–6 點就寢:閒著的人去睡", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
|
|
103
104
|
fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
|
|
104
105
|
fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
|
|
105
106
|
allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
|
|
@@ -180,7 +181,8 @@ function looksOf(L, work, q, mode, zones) {
|
|
|
180
181
|
const n = d.node;
|
|
181
182
|
if (!n) return [d.key, { name: "", label: "", state: "missing", pose: "away", light: null, lamp: false, sign: null, coat: "", work: 0, dim: !!q, match: false, free: zones }];
|
|
182
183
|
const match = !!q && hits(n, q);
|
|
183
|
-
|
|
184
|
+
// At night (#2568) the idle are drawn asleep (their state stays idle: the tooltip and the list say so).
|
|
185
|
+
return [d.key, { name: n.name, label: nodeText(n).primary, state: n.state, pose: officePose(d.night ? "paused" : n.state), night: !!d.night, light: lightOf(n.state), lamp: lampOf(n.state),
|
|
184
186
|
sign: zones ? null : signOf(n.state), coat: coatOf(n.i, n.i ? identityOf(n.i) : null, mode), work: work.get(n.name) || 0, dim: !!q && !match, match, free: zones }];
|
|
185
187
|
}));
|
|
186
188
|
}
|
|
@@ -192,17 +194,78 @@ function withOverrides(baseSkin, zones, office) {
|
|
|
192
194
|
return a.ok ? { skin: { ...baseSkin, pack: a.pack }, ignored: false } : { skin: baseSkin, ignored: true };
|
|
193
195
|
}
|
|
194
196
|
/** A stored office's scene (or the lobby tile's): seats in its style's areas, its own seats, or one room wrapping at `deskW`. */
|
|
195
|
-
function officeScene(office, nodes, skin, zones, deskW, prevSeats) {
|
|
196
|
-
if (zones) return zonesLayout(skin.pack, nodes, prevSeats);
|
|
197
|
+
function officeScene(office, nodes, skin, zones, deskW, prevSeats, night = false) {
|
|
198
|
+
if (zones) return zonesLayout(skin.pack, nodes, prevSeats, { night });
|
|
197
199
|
const lay = office.layout || layoutOk(skin.layout);
|
|
198
200
|
return lay ? customLayout({ ...office, layout: lay }, nodes) : autoLayout([{ key: office.id, name: office.name, lobby: !!office.lobby, nodes }], { width: deskW });
|
|
199
201
|
}
|
|
200
202
|
/** What drawPart needs of a scene; its layers are painted on first draw (and again at another resolution). */
|
|
201
203
|
function partOf(L, looks, skin, zones, walkers, flights) {
|
|
202
204
|
const all = L.rooms.flatMap((r) => r.desks);
|
|
203
|
-
const shown = new Set([...looks.values()].map((l) => l.state));
|
|
205
|
+
const shown = new Set([...looks.values()].map((l) => l.state)), byName = new Map([...looks.values()].filter((l) => l.name).map((l) => [l.name, l.state]));
|
|
204
206
|
return { L, looks, skin, zones, walkers, flights, all, desks: all.filter((d) => d.node), layers: null,
|
|
205
|
-
active: zones ? activeFurniture(skin.pack, shown) : new Set(), lines: bubbleDefaults(), lang: lang() };
|
|
207
|
+
active: zones ? activeFurniture(skin.pack, shown, byName) : new Set(), lines: bubbleDefaults(), lang: lang() };
|
|
208
|
+
}
|
|
209
|
+
/** At bedtime (#2568): each item with a `night` drawn as it — its strip playing, at its night place (its own when none). */
|
|
210
|
+
export const nightFurniture = (list) => list.map((f) => (f.night ? { ...f, sprite: f.night.sprite, activeSprite: null, x: f.night.x ?? f.x, y: f.night.y ?? f.y, asleep: true } : f));
|
|
211
|
+
/** #2596: each NPC with a `walk` where its walker is now (`at(key)`): moved there — its tag and depth with it — and, while
|
|
212
|
+
* it walks, `walking` and its `heading` (its walk strip: walkStrip); asleep, or with no walker (not strolling), where it is. */
|
|
213
|
+
export const strollFurniture = (list, at) => list.map((f) => {
|
|
214
|
+
const w = f.walk && !f.asleep ? at(npcKey(f)) : null;
|
|
215
|
+
return w ? { ...f, x: w.x, y: w.y, walking: !!w.walking, heading: w.heading || null } : f;
|
|
216
|
+
});
|
|
217
|
+
/** Where a seated person's bubble points: over the head of the picture drawn — a pack's figure can be taller than the
|
|
218
|
+
* built-in one (#2558: a 134 px figure had its face under the bubble); never lower than the built-in spot. */
|
|
219
|
+
export function speakerAt(sk, look, at) {
|
|
220
|
+
const top = sk.charBox ? sk.charBox(look, at).y + 2 : Infinity;
|
|
221
|
+
return { x: at.x + 10, y: Math.min(at.y - 50, top) };
|
|
222
|
+
}
|
|
223
|
+
/** The pose an area's seats are for: the first state (asleep, working, calling, idle) the pack sends there. */
|
|
224
|
+
const GHOST_POSE = [["paused", "paused"], ["stopped", "paused"], ["working", "working"], ["needs", "needs"], ["crashed", "needs"], ["stuck", "needs"], ["idle", "idle"]];
|
|
225
|
+
/** How faint Edit layout's figures are: seen through, yet readable over a busy picture (a dimmed person is 0.35). */
|
|
226
|
+
const GHOST_ALPHA = 0.6;
|
|
227
|
+
/** Edit layout's faint figures: one on each seat no one is on, { x, y, look } (the pack's default set; drawn at GHOST_ALPHA). */
|
|
228
|
+
export function ghostsOf(pack, people = []) {
|
|
229
|
+
const taken = (s) => people.some((q) => Math.abs(q.at.x - s.x) < 2 && Math.abs(q.at.y - s.y) < 2);
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const [name, a] of Object.entries(pack.areas)) {
|
|
232
|
+
const hit = GHOST_POSE.find(([st]) => pack.stateZones && pack.stateZones[st] === name);
|
|
233
|
+
const pose = hit ? hit[1] : "idle";
|
|
234
|
+
for (const s of a.seats) if (!taken(s)) out.push({ x: s.x, y: s.y, look: { key: `ghost:${name}`, name: "", label: "", state: pose, pose, coat: "#8a8f99", free: true } });
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
/** All offices: the tile `name` is drawn in ({ tile, skin }), or null. */
|
|
239
|
+
const senderTile = (parts, name) => (name ? parts.find((p) => desksOf(p.L).some((d) => d.node.name === name)) || null : null);
|
|
240
|
+
/**
|
|
241
|
+
* Whether an agent's act (office_set, #2578) holds now: not past its end; its person not called (a call wins) nor
|
|
242
|
+
* asleep; and no new work begun since — `seen` (kept by the page) remembers each act whose person was seen not working,
|
|
243
|
+
* so working again after that (or after acting while idle) is new work, and the act gives way. The turn it acted in
|
|
244
|
+
* (working at the time) keeps it.
|
|
245
|
+
*/
|
|
246
|
+
export function actLive(a, state, at = Date.now(), seen = null) {
|
|
247
|
+
if (!a || at >= a.until || CALLING.has(state) || ASLEEP.has(state)) return false;
|
|
248
|
+
if (!seen) return true;
|
|
249
|
+
const k = `${a.instance}@${a.since ?? a.until}`;
|
|
250
|
+
if (state !== "working") { seen.add(k); return true; }
|
|
251
|
+
return !seen.has(k) && (!a.state || a.state === "working");
|
|
252
|
+
}
|
|
253
|
+
/** How far beside the person an agent going to them stands. */
|
|
254
|
+
const ACT_BESIDE = 48;
|
|
255
|
+
/** Where an agent's act sends it at `t`: its point, beside the person it goes to (where they are now), or null. */
|
|
256
|
+
export function actTarget(a, L, walkers, t) {
|
|
257
|
+
if (a.x !== undefined) return { x: a.x, y: a.y };
|
|
258
|
+
if (!a.person) return null;
|
|
259
|
+
const d = desksOf(L).find((x) => x.node.name === a.person), there = (walkers && walkers.at(a.person, t)) || (d && d.seat);
|
|
260
|
+
return there ? { x: Math.round(Math.min(L.width, there.x + ACT_BESIDE)), y: Math.round(there.y) } : null;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* A look in its agent's own pose (office_set, #2578) while its act holds, else as it is. In bed for the night (#2568,
|
|
264
|
+
* `look.night`) it is asleep, as for mail (#2569): bedtime wins over an act.
|
|
265
|
+
*/
|
|
266
|
+
function actPosed(p, look) {
|
|
267
|
+
const act = look && p.acts && p.acts.get(look.name);
|
|
268
|
+
return act && act.pose && actLive(act, look.night ? "paused" : look.state, Date.now(), p.actSeen) ? { ...look, pose: officePose(act.pose) } : look;
|
|
206
269
|
}
|
|
207
270
|
/** The back and front layers: pictures of the scene at `ls` layer px per logical unit (`tagsOff`: no name tags but the Find box's matches). */
|
|
208
271
|
function paintLayers(p, env, ls, tagsOff = false) {
|
|
@@ -248,6 +311,8 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
248
311
|
T();
|
|
249
312
|
const speakers = [], tags = [];
|
|
250
313
|
const up = still ? [] : p.flights.frame(ms, p.L);
|
|
314
|
+
// Mail from another office (#2564): flying in at the door, or on the floor until its person picks it up.
|
|
315
|
+
const post = still || !p.mail || !p.zones ? [] : p.mail.frame(ms > 0 ? ms : now(), p.L, p.walkers);
|
|
251
316
|
let walking = false;
|
|
252
317
|
if (p.zones) {
|
|
253
318
|
// Furniture under the people; the people (nearest last) with the furniture whose depth is "y" among them, by base
|
|
@@ -259,18 +324,26 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
259
324
|
if (at.walking) walking = true;
|
|
260
325
|
return { d, at, look: p.looks.get(d.key) };
|
|
261
326
|
}).sort((x, y) => x.at.y - y.at.y);
|
|
262
|
-
const furniture = sk.pack.furniture;
|
|
263
|
-
|
|
327
|
+
const furniture = strollFurniture(p.night ? nightFurniture(sk.pack.furniture) : sk.pack.furniture, (k) => p.walkers.at(k, t));
|
|
328
|
+
if (furniture.some((f) => f.walking)) walking = true;
|
|
329
|
+
const band = [...furniture.filter((f) => f.depth === "y").map((f) => ({ y: f.y, k: 0, f })), ...people.map((q) => ({ y: q.at.y, k: 1, p: q })),
|
|
330
|
+
...(p.ghosts ? ghostsOf(sk.pack, people).map((g) => ({ y: g.y, k: 1, g })) : [])]
|
|
264
331
|
.sort((x, y) => x.y - y.y || x.k - y.k);
|
|
265
|
-
for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, p.active.has(f.id), ms, env);
|
|
266
|
-
for (const
|
|
267
|
-
|
|
332
|
+
for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
|
|
333
|
+
for (const it of post) if (it.t === 0) sk.drawProp(ctx, it, ms, env); // a letter on the floor: under the people
|
|
334
|
+
for (const { f, p: q, g } of band) {
|
|
335
|
+
if (f) { sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env); continue; }
|
|
336
|
+
if (g) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawCharacter(ctx, { x: g.x - 44, y: g.y - 53, w: 88, h: 53, seat: g }, g.look, 0, env); ctx.restore(); continue; }
|
|
268
337
|
const box = { ...q.d, x: q.at.x - 44, y: q.at.y - 53, w: 88, h: 53, seat: { x: q.at.x, y: q.at.y } };
|
|
269
|
-
|
|
270
|
-
|
|
338
|
+
// An agent's own pose (office_set, #2578) while its act lasts and nothing calls it; walking is walking.
|
|
339
|
+
const posed = actPosed(p, q.look);
|
|
340
|
+
const look = q.at.walking ? { ...q.look, pose: "walk", heading: q.at.heading } : posed;
|
|
341
|
+
sk.drawCharacter(ctx, box, look, ms, env);
|
|
342
|
+
if (!q.at.walking) speakers.push({ look: q.look, ...speakerAt(sk, look, q.at), area: q.d.area });
|
|
271
343
|
}
|
|
272
|
-
for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, p.active.has(f.id), ms, env);
|
|
344
|
+
for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
|
|
273
345
|
for (const it of up) sk.drawProp(ctx, it, ms, env);
|
|
346
|
+
for (const it of post) if (it.t > 0) sk.drawProp(ctx, it, ms, env);
|
|
274
347
|
if (sk.drawForeground) sk.drawForeground(ctx, p.L, env);
|
|
275
348
|
// Who found no spot in an area: a count in its corner (never over its name).
|
|
276
349
|
for (const [ar, n] of p.L.crowd || []) if (p.L.areas[ar] && sk.drawCrowd) sk.drawCrowd(ctx, p.L.areas[ar], n, env);
|
|
@@ -281,8 +354,10 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
281
354
|
sk.drawTag(ctx, q.at.x, q.at.y, q.look, env);
|
|
282
355
|
tags.push(tagBox(ctx, q.at.x, q.at.y, q.look.label));
|
|
283
356
|
}
|
|
357
|
+
// A furniture item's own name (#2574: an NPC drawn as furniture), tagged like a person; none on a tile too small to read.
|
|
358
|
+
if (!tagsOff) for (const f of furniture) if (f.label) { sk.drawTag(ctx, f.x, f.y, { label: f.label }, env); tags.push(tagBox(ctx, f.x, f.y, f.label)); }
|
|
284
359
|
} else {
|
|
285
|
-
for (const d of p.desks) { const look = p.looks.get(d.key); sk.drawCharacter(ctx, d, look, ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12 }); }
|
|
360
|
+
for (const d of p.desks) { const look = p.looks.get(d.key); sk.drawCharacter(ctx, d, actPosed(p, look), ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12 }); }
|
|
286
361
|
blit(p.layers.front);
|
|
287
362
|
// Tags left out of the layer: the hovered desk's front again, with its tag.
|
|
288
363
|
const hd = tagsOff && hot ? p.desks.find((d) => d.key === hot) : null;
|
|
@@ -294,8 +369,11 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
294
369
|
const bubbles = [];
|
|
295
370
|
for (const sp of speakers) {
|
|
296
371
|
if (sp.look.dim || sp.look.pose === "away") continue;
|
|
297
|
-
const
|
|
298
|
-
|
|
372
|
+
const says = sp.look.night ? "paused" : sp.look.state; // asleep at night: its "Zzz…" lines
|
|
373
|
+
// What an agent says itself (office_set, #2578): whole, kept over the area's thinning like a call.
|
|
374
|
+
const act = p.acts && p.acts.get(sp.look.name), own = actLive(act, says, Date.now(), p.actSeen) && act.say ? act.say : null;
|
|
375
|
+
const b = own ? { text: own, shown: [...own].length } : bubbleAt(sp.look.name, says, ms, linesFor(says, p.lang, sk.pack, p.lines));
|
|
376
|
+
if (b) bubbles.push({ key: sp.look.name, area: sp.area, state: sp.look.state, calling: CALLING.has(sp.look.state), rank: own ? 0 : bubbleRank(sp.look.state), shown: b.shown,
|
|
299
377
|
box: bubbleBox(ctx, sp.x, sp.y, b.text), tail: { x: sp.x, y: sp.y }, sp, b });
|
|
300
378
|
}
|
|
301
379
|
// A crowded area speaks less (thinBubbles: calls, then work, a few at most); no bubble covers a name tag. `defer`
|
|
@@ -303,7 +381,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
303
381
|
const placed = placeBubbles(thinBubbles(bubbles, p.zones ? p.L.over : null), 22, tags);
|
|
304
382
|
const say = () => { T(); for (const { sp, b, dy } of placed) sk.drawBubble(ctx, sp.x, sp.y, [...b.text].slice(0, b.shown).join(""), env, -dy); };
|
|
305
383
|
if (defer) defer.push(say); else say();
|
|
306
|
-
return walking || up.length > 0 || p.flights.size() > 0;
|
|
384
|
+
return walking || up.length > 0 || p.flights.size() > 0 || post.some((it) => it.t > 0);
|
|
307
385
|
}
|
|
308
386
|
/**
|
|
309
387
|
* The person at (x, y) in a scene's units, as it draws them: on zones where they walk, in the box the skin draws them
|
|
@@ -356,6 +434,11 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
356
434
|
const [filter, setFilter] = useState(() => store.filter());
|
|
357
435
|
const [style, setStyle] = useState(() => store.skin());
|
|
358
436
|
const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
|
|
437
|
+
// Bedtime (#2568), when it is on here: the idle sleep from NIGHT.FROM to NIGHT.TO on this device's clock (read every NIGHT.TICK).
|
|
438
|
+
const [nightOn, setNightOn] = useState(() => store.nightOn());
|
|
439
|
+
const [nightNow, setNightNow] = useState(() => isNight());
|
|
440
|
+
useEffect(() => { if (!nightOn) return undefined; setNightNow(isNight()); const h = setInterval(() => setNightNow(isNight()), NIGHT.TICK); return () => clearInterval(h); }, [nightOn]);
|
|
441
|
+
const night = nightOn && nightNow;
|
|
359
442
|
const [lobbyOn, setLobbyOn] = useState(() => store.lobbyTile()); // #2554: All offices' lobby tile
|
|
360
443
|
const [packs, setPacks] = useState([]); // the skin packs the host keeps: [{ id, name, sha }]
|
|
361
444
|
const [packSkins, setPackSkins] = useState(() => new Map()); // id@sha → a loaded skin, or null (not a pack)
|
|
@@ -406,6 +489,24 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
406
489
|
const off = sub ? lease.hold(sub("offices", () => { void read(); })) : null;
|
|
407
490
|
return () => { if (off) off(); };
|
|
408
491
|
}, [lease, store]);
|
|
492
|
+
// #2578: what agents do here by their own hand (office_set) — on opening, then as the stream brings it.
|
|
493
|
+
// A read a newer one or the stream overtook is dropped (it is older: review — an opening read answered after the one
|
|
494
|
+
// the stream's return began brought a cleared act back); read again when the stream comes back, and — with no live
|
|
495
|
+
// stream (the public link, or a stream polling) — every POLL_MS while the page is shown.
|
|
496
|
+
const [acts, setActs] = useState([]);
|
|
497
|
+
const actSeen = useRef(new Set());
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
if (!lease) return undefined;
|
|
500
|
+
let heard = 0; // reads begun and events heard: the newest wins
|
|
501
|
+
const read = async () => { const at = ++heard, d = await store.acts(); if (lease.current() && d && at === heard) setActs(d); };
|
|
502
|
+
void read();
|
|
503
|
+
const sub = appStore.get().streamOn;
|
|
504
|
+
const polled = () => globalThis.document?.body?.dataset?.webTransport === "poll" || appStore.get().connection === "polling";
|
|
505
|
+
const offs = sub ? [lease.hold(sub("office_acts", (d) => { heard++; if (d && Array.isArray(d.acts)) setActs(actsOk(d.acts, d.now)); else void read(); })),
|
|
506
|
+
lease.hold(sub("connection", (c) => { if (c === "live") void read(); }))] : [];
|
|
507
|
+
const h = lease.interval(() => { if (polled() && !globalThis.document?.hidden) void read(); }, POLL_MS);
|
|
508
|
+
return () => { for (const off of offs) off(); lease.clear(h); };
|
|
509
|
+
}, [lease, store]);
|
|
409
510
|
// The skin packs: on opening, and again whenever the stream says one was uploaded or deleted.
|
|
410
511
|
useEffect(() => {
|
|
411
512
|
if (!lease) return undefined;
|
|
@@ -489,17 +590,20 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
489
590
|
return withOverrides(baseSkin, zones, custom);
|
|
490
591
|
}, [baseSkin, zones, custom, layout, layoutStale]);
|
|
491
592
|
const skin = placed.skin;
|
|
593
|
+
// While Edit layout is open: a faint figure on every seat, in the pose its area is for (#2558), so a seat is placed by
|
|
594
|
+
// what will stand, sit or lie there, not by a dot.
|
|
595
|
+
const ghosts = zones && !!layout && !layoutStale;
|
|
492
596
|
const keep = (n) => !(hideAsleep && ASLEEP.has(n.state));
|
|
493
597
|
const single = useMemo(() => {
|
|
494
598
|
if (overview) return { rooms: [], width: 1, height: 1 }; // All offices: the tiles below
|
|
495
599
|
const all = custom ? membersOf(custom, view) : [...new Map(officeRooms(view, filter).flatMap((r) => r.nodes).map((n) => [n.name, n])).values()];
|
|
496
600
|
const nodes = all.filter(keep);
|
|
497
|
-
if (zones) { const z = zonesLayout(skin.pack, nodes, seatsRef.current); seatsRef.current = z.seats; return z; }
|
|
601
|
+
if (zones) { const z = zonesLayout(skin.pack, nodes, seatsRef.current, { night }); seatsRef.current = z.seats; return z; }
|
|
498
602
|
// The desks wrap at the stage's width at the scale they open on (desksK: a pixel level near one CSS px a unit).
|
|
499
603
|
const deskW = Math.max(240, Math.floor(width / desksK(ratio)));
|
|
500
604
|
if (!custom) return autoLayout(officeRooms(view, filter).map((r) => (hideAsleep ? { ...r, nodes: r.nodes.filter(keep) } : r)), { width: deskW });
|
|
501
605
|
return officeScene(custom, nodes, skin, false, deskW);
|
|
502
|
-
}, [overview, view, filter, custom, width, ratio, skin, hideAsleep]);
|
|
606
|
+
}, [overview, view, filter, custom, width, ratio, skin, hideAsleep, night]);
|
|
503
607
|
// ── #2554: All offices' tiles — each stored office (then the lobby: whoever is in no office) laid out as its own view
|
|
504
608
|
// lays it out, in its own style (desks wrap at a cell's width), with its own seats, walkers and flights ──
|
|
505
609
|
const lobbyAll = useMemo(() => (overview ? lobbyNodes(stored, view) : []), [overview, stored, view]);
|
|
@@ -516,24 +620,45 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
516
620
|
const sk = withOverrides(base, z, sp.lobby ? null : sp.office).skin;
|
|
517
621
|
const seatKey = `${sp.key}@${pk || base.id}`;
|
|
518
622
|
// A desks office is as large as its rooms (cropped: no margins, no corridors outside them).
|
|
519
|
-
const TL0 = officeScene(sp.office, sp.nodes.filter(keep), sk, z, OV.CELL_W, tileSeats.current.get(seatKey)), TL = z ? TL0 : cropToRooms(TL0);
|
|
623
|
+
const TL0 = officeScene(sp.office, sp.nodes.filter(keep), sk, z, OV.CELL_W, tileSeats.current.get(seatKey), night), TL = z ? TL0 : cropToRooms(TL0);
|
|
520
624
|
if (z) tileSeats.current.set(seatKey, TL0.seats);
|
|
521
625
|
return { key: sp.key, name: sp.name, lobby: sp.lobby, count: sp.nodes.length, skin: sk, zones: z, L: TL, prefix: `${sp.key}\u0001`,
|
|
522
626
|
walkers: mine(tileWalkers, sp.key, createWalkers), flights: mine(tileFlights, sp.key, createFlights) };
|
|
523
627
|
});
|
|
524
|
-
}, [overview, stored, view, packs, packSkins, hideAsleep, lobbyOn, lobbyAll, style]);
|
|
628
|
+
}, [overview, stored, view, packs, packSkins, hideAsleep, lobbyOn, lobbyAll, style, night]);
|
|
525
629
|
const arranged = useMemo(() => tileLayout(tiles.map((tl) => ({ key: tl.key, w: tl.L.width, h: tl.L.height })), size), [tiles, size.w, size.h]);
|
|
526
630
|
const tileLooks = useMemo(() => { const work = workOn(traffic); return tiles.map((tl) => looksOf(tl.L, work, q, mode, tl.zones)); }, [tiles, traffic, q, mode, themeRev]);
|
|
527
631
|
// The scene the view pans and zooms: the office's, or the overview of every tile.
|
|
528
632
|
const L = useMemo(() => (overview ? { mode: "overview", rooms: [], width: arranged.width, height: arranged.height } : single), [overview, single, arranged]);
|
|
529
633
|
const looks = useMemo(() => looksOf(L, workOn(traffic), q, mode, zones), [L, traffic, q, mode, themeRev]);
|
|
530
|
-
|
|
531
|
-
//
|
|
634
|
+
// #2564: what comes in from other offices — one office's and style's own: a letter's door and drop are that layout's,
|
|
635
|
+
// so another office (or style) starts with none (one shown meanwhile is not carried over to a different room).
|
|
636
|
+
// #2578: the acts of the people in this stored office (by name); none elsewhere. On a desks style (review: accepted
|
|
637
|
+
// acts drew nothing there) the line and the pose show; nobody walks (the host refuses go there).
|
|
638
|
+
const actsHere = useMemo(() => new Map(custom && !overview ? acts.filter((a) => a.office === custom.id).map((a) => [a.instance, a]) : []), [acts, custom, overview]);
|
|
639
|
+
const mailKey = `${custom ? custom.id : AUTO_ID}@${baseSkin.id}`;
|
|
640
|
+
const mail = useMemo(() => { const m = createMail(); m.prime(traffic || []); return m; }, [mailKey]);
|
|
641
|
+
useEffect(() => { flights.sync(traffic || [], now(), still); if (zones && !overview && mail.sync(traffic || [], now(), L, still)) loop.current?.wake(); }, [traffic, mail]);
|
|
642
|
+
// Zones: everyone's seat is where they walk to — an idle person now and then strolls near it and back (#2560; not
|
|
643
|
+
// with reduced motion, nor while Edit layout is open: the seats are being placed); someone with mail from another
|
|
644
|
+
// office walks to it first (#2564). An NPC drawn as furniture with a `walk` strolls near its spot alike (#2596).
|
|
645
|
+
const wander = zones && !overview && !still && !layout;
|
|
532
646
|
useEffect(() => {
|
|
533
|
-
if (!zones || overview) return;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
647
|
+
if (!zones || overview) return undefined;
|
|
648
|
+
const go = () => {
|
|
649
|
+
const t = now(), to = walkTargets(desksOf(L), L, t, wander, skin.pack.furniture, night);
|
|
650
|
+
// An act's walk; not someone in bed for the night (#2568: bedtime wins, as for mail).
|
|
651
|
+
for (const d of desksOf(L)) { const a = actsHere.get(d.node.name), at = !d.night && actLive(a, d.node.state, Date.now(), actSeen.current) ? actTarget(a, L, walkers, t) : null; if (at) to.set(d.node.name, at); }
|
|
652
|
+
for (const [name, at] of mail.targets(L, t)) to.set(name, at);
|
|
653
|
+
walkers.sync(to, t, L, still);
|
|
654
|
+
// Reduced motion has no loop: drawn again here, or an act's walk (set after the scene was drawn) never shows.
|
|
655
|
+
if (still) { if (scene.current && !globalThis.document?.hidden) draw(0); } else loop.current?.wake();
|
|
656
|
+
};
|
|
657
|
+
go();
|
|
658
|
+
if (still) return undefined;
|
|
659
|
+
const h = setInterval(go, WANDER.TICK);
|
|
660
|
+
return () => clearInterval(h);
|
|
661
|
+
}, [L, wander, mail, skin, night, actsHere]);
|
|
537
662
|
// …and in each tile on a zones style; the traffic within a tile is its own, between tiles the overview's.
|
|
538
663
|
useEffect(() => {
|
|
539
664
|
for (const tl of tiles) if (tl.zones) tl.walkers.sync(new Map(desksOf(tl.L).map((d) => [d.node.name, d.seat])), now(), tl.L, still);
|
|
@@ -647,7 +772,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
647
772
|
const fitted0 = zones || overview; // All offices opens whole, as a zones scene
|
|
648
773
|
return { v: overview ? fitL(fitMode) : openingView(L.width, L.height, size, ratio, fitted0, fitMode), fit: fitted0 };
|
|
649
774
|
};
|
|
650
|
-
// An office (or style) opened: its kept view, else its opening (zones fitted; desks from the top left). A resize fits
|
|
775
|
+
// An office (or style) opened: its kept view, else its opening (zones fitted; desks from the top left, or centred where smaller). A resize fits
|
|
651
776
|
// a fitted view again (and opens an untouched one again); a view a person moved keeps the same point in the middle.
|
|
652
777
|
// Either way it is then kept on the scene (clampPan): a kept view from a bigger panel cannot leave it off-screen.
|
|
653
778
|
const opened = useRef(null), sized = useRef(null);
|
|
@@ -776,13 +901,13 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
776
901
|
const base = { ctx, ratio, BW, BH, matte, env, dirty: true };
|
|
777
902
|
if (overview) {
|
|
778
903
|
// Each tile a scene of its own, where the overview puts it (arranged.tiles, in the same order).
|
|
779
|
-
const parts = tiles.map((tl, i) => ({ ...partOf(tl.L, tileLooks[i], tl.skin, tl.zones, tl.walkers, tl.flights), key: tl.key, prefix: tl.prefix, tile: arranged.tiles[i] }));
|
|
904
|
+
const parts = tiles.map((tl, i) => ({ ...partOf(tl.L, tileLooks[i], tl.skin, tl.zones, tl.walkers, tl.flights), night, key: tl.key, prefix: tl.prefix, tile: arranged.tiles[i] }));
|
|
780
905
|
scene.current = { ...base, overview: true, parts, lay: arranged };
|
|
781
906
|
} else {
|
|
782
|
-
scene.current = { ...base, overview: false, part: partOf(L, looks, skin, zones, walkers, flights) };
|
|
907
|
+
scene.current = { ...base, overview: false, part: { ...partOf(L, looks, skin, zones, walkers, flights), ghosts, night, mail, acts: actsHere, actSeen: actSeen.current } };
|
|
783
908
|
}
|
|
784
909
|
if (still) draw(0); else loop.current?.wake();
|
|
785
|
-
}, [L, looks, skin, pal, size.w, size.h, ratio, matte, overview, tiles, tileLooks, arranged]);
|
|
910
|
+
}, [L, looks, skin, pal, size.w, size.h, ratio, matte, overview, tiles, tileLooks, arranged, ghosts, night, mail, actsHere]);
|
|
786
911
|
|
|
787
912
|
/** One frame at `ms` (0: still); whether anything is moving (in the air, or someone walking). */
|
|
788
913
|
function draw(ms) {
|
|
@@ -815,10 +940,12 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
815
940
|
}
|
|
816
941
|
for (const say of said) say();
|
|
817
942
|
const up = still ? [] : cross.frame(ms, s.parts);
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
943
|
+
// Each drawn by its sender's tile's skin (#2562: their own prop, else that pack's for its kind), at that tile's scale;
|
|
944
|
+
// a sender in no tile drawn: the built-in item, in overview units.
|
|
945
|
+
for (const it of up) {
|
|
946
|
+
const from = senderTile(s.parts, it.from);
|
|
947
|
+
if (from) { ctx.setTransform(a * from.tile.s, 0, 0, a * from.tile.s, ox, oy); from.skin.drawProp(ctx, { ...it, x: it.x / from.tile.s, y: it.y / from.tile.s }, ms, env); }
|
|
948
|
+
else { ctx.setTransform(a, 0, 0, a, ox, oy); skinFor("pixel").drawProp(ctx, it, ms, env); }
|
|
822
949
|
}
|
|
823
950
|
return moving || up.length > 0 || cross.size() > 0;
|
|
824
951
|
}
|
|
@@ -1012,6 +1139,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
1012
1139
|
</select></label>
|
|
1013
1140
|
<${People} nodes=${everyone} pinned=${f.pinned} onChange=${(pinned) => setF({ ...f, pinned })} />` : null}
|
|
1014
1141
|
<label class="check oo-hide-asleep"><input type="checkbox" checked=${hideAsleep} onChange=${() => { const v = !hideAsleep; store.saveHideAsleep(v); setHideAsleep(v); setTip(null); }} /><span>${to("hideAsleep")}</span></label>
|
|
1142
|
+
${zones || overview ? html`<label class="check oo-night"><input type="checkbox" checked=${nightOn} onChange=${() => { const v = !nightOn; store.saveNightOn(v); setNightOn(v); }} /><span>${to("nightOn")}</span></label>` : null}
|
|
1015
1143
|
${overview ? html`<label class="check oo-lobby-tile" title=${lobbyAll.length ? undefined : to("lobbyEveryone")}><input type="checkbox" checked=${lobbyOn && lobbyAll.length > 0}
|
|
1016
1144
|
disabled=${!lobbyAll.length} onChange=${() => { const v = !lobbyOn; store.saveLobbyTile(v); setLobbyOn(v); setTip(null); }} /><span>${to("lobbyTile")}</span></label>
|
|
1017
1145
|
${lobbyAll.length ? null : html`<span class="muted oo-note oo-lobby-none">${to("lobbyEveryone")}</span>`}` : null}
|
|
@@ -1035,7 +1163,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
1035
1163
|
: html`<button key=${tl.key} ref=${at} type="button" class="oo-tile-label" data-office=${tl.key} title=${to("openOffice", tl.name)} aria-label=${to("openOffice", tl.name)}
|
|
1036
1164
|
onClick=${() => pick(tl.key)}>${text}</button>`;
|
|
1037
1165
|
}) : null}
|
|
1038
|
-
${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay}
|
|
1166
|
+
${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs}
|
|
1039
1167
|
stale=${layoutStale ? t("orgl.stale") : ""} onChange=${onDraft} onSave=${saveLayout} onClose=${closeLayout} />` : null}
|
|
1040
1168
|
${tipText ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
|
|
1041
1169
|
<strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
|