@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.10
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/THIRD_PARTY_NOTICES.md +40 -0
- 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/antigravity-usage-scan.d.ts +58 -0
- package/dist/antigravity-usage-scan.js +257 -0
- package/dist/antigravity-usage-scan.js.map +1 -0
- package/dist/antigravity-usage-worker.d.ts +1 -0
- package/dist/antigravity-usage-worker.js +10 -0
- package/dist/antigravity-usage-worker.js.map +1 -0
- package/dist/auth-api.js +4 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +64 -0
- package/dist/backend/kiro-identity.js +101 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-retired-model.d.ts +62 -0
- package/dist/backend/kiro-retired-model.js +160 -0
- package/dist/backend/kiro-retired-model.js.map +1 -0
- package/dist/backend/kiro.d.ts +71 -2
- package/dist/backend/kiro.js +352 -11
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +59 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-signin-guide.d.ts +1 -0
- package/dist/backend-signin-guide.js +9 -0
- package/dist/backend-signin-guide.js.map +1 -0
- package/dist/backend-signin-pending.d.ts +10 -0
- package/dist/backend-signin-pending.js +44 -0
- package/dist/backend-signin-pending.js.map +1 -0
- package/dist/browser-backend-signins.d.ts +56 -0
- package/dist/browser-backend-signins.js +189 -0
- package/dist/browser-backend-signins.js.map +1 -0
- package/dist/cache-ledger.d.ts +38 -2
- package/dist/cache-ledger.js +133 -8
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-prices.d.ts +2 -1
- package/dist/cache-prices.js +5 -2
- package/dist/cache-prices.js.map +1 -1
- package/dist/cache-service.d.ts +68 -0
- package/dist/cache-service.js +355 -13
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +71 -14
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +29 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +11 -0
- package/dist/classic-channel-manager.js +39 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +21 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +53 -10
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +31 -1
- package/dist/config.js.map +1 -1
- package/dist/cpu-quota.d.ts +23 -0
- package/dist/cpu-quota.js +86 -0
- package/dist/cpu-quota.js.map +1 -0
- package/dist/daemon.d.ts +23 -0
- package/dist/daemon.js +106 -7
- 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/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-manager.d.ts +72 -0
- package/dist/fleet-manager.js +507 -89
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -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.canvas-worker-off.md +37 -9
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +44 -11
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +19 -0
- package/dist/instance-lifecycle.js +55 -12
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-db-reader.d.ts +5 -0
- package/dist/kiro-db-reader.js +18 -0
- package/dist/kiro-db-reader.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +2 -0
- package/dist/kiro-engine-status.js +9 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-migrate.d.ts +49 -0
- package/dist/kiro-migrate.js +105 -0
- package/dist/kiro-migrate.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +2 -0
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/kiro-transcript-worker.js +1 -0
- package/dist/kiro-transcript-worker.js.map +1 -1
- package/dist/kiro-tui-forensic.d.ts +7 -0
- package/dist/kiro-tui-forensic.js +110 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/kiro-usage-run.d.ts +7 -0
- package/dist/kiro-usage-run.js +39 -0
- package/dist/kiro-usage-run.js.map +1 -0
- package/dist/kiro-usage-scan.d.ts +49 -0
- package/dist/kiro-usage-scan.js +223 -0
- package/dist/kiro-usage-scan.js.map +1 -0
- package/dist/kiro-usage-worker.d.ts +1 -0
- package/dist/kiro-usage-worker.js +10 -0
- package/dist/kiro-usage-worker.js.map +1 -0
- package/dist/kiro-usage.d.ts +58 -0
- package/dist/kiro-usage.js +310 -0
- package/dist/kiro-usage.js.map +1 -0
- package/dist/locale.js +88 -8
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/login-url.d.ts +4 -0
- package/dist/login-url.js +22 -0
- package/dist/login-url.js.map +1 -0
- package/dist/needs-you-hub.d.ts +2 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +8 -2
- package/dist/needs-you.js +6 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/notice-delivery.d.ts +16 -0
- package/dist/notice-delivery.js +35 -0
- package/dist/notice-delivery.js.map +1 -0
- package/dist/office-share-page.d.ts +16 -0
- package/dist/office-share-page.js +85 -0
- package/dist/office-share-page.js.map +1 -0
- package/dist/office-share-service.d.ts +91 -0
- package/dist/office-share-service.js +176 -0
- package/dist/office-share-service.js.map +1 -0
- package/dist/office-share.d.ts +258 -0
- package/dist/office-share.js +549 -0
- package/dist/office-share.js.map +1 -0
- package/dist/opencode-usage-scan.d.ts +28 -0
- package/dist/opencode-usage-scan.js +92 -0
- package/dist/opencode-usage-scan.js.map +1 -0
- package/dist/opencode-usage-worker.d.ts +1 -0
- package/dist/opencode-usage-worker.js +10 -0
- package/dist/opencode-usage-worker.js.map +1 -0
- package/dist/outbound-handlers.js +22 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +26 -2
- package/dist/outbound-schemas.js +27 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +7 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.js +13 -2
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.js +2 -1
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/settings-api.js +41 -16
- 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-form.d.ts +1 -1
- package/dist/setup-form.js +55 -6
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.d.ts +9 -0
- package/dist/setup-host.js +127 -6
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-signin-assets.d.ts +2 -0
- package/dist/setup-signin-assets.js +6 -0
- package/dist/setup-signin-assets.js.map +1 -0
- package/dist/setup-wizard.js +2 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/structured-signin-transport.d.ts +8 -0
- package/dist/structured-signin-transport.js +79 -0
- package/dist/structured-signin-transport.js.map +1 -0
- package/dist/structured-signin.d.ts +131 -0
- package/dist/structured-signin.js +388 -0
- package/dist/structured-signin.js.map +1 -0
- package/dist/tool-permissions.js +12 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +6 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +26 -2
- package/dist/transcript-sources.js +103 -40
- package/dist/transcript-sources.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 +28 -8
- package/dist/ui/backend-signin.js +63 -10
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-editor.js +14 -9
- package/dist/ui/fleet-office-layout.js +70 -32
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +167 -0
- package/dist/ui/fleet-office-pack.d.ts +7 -2
- package/dist/ui/fleet-office-pack.js +119 -15
- package/dist/ui/fleet-office-skin.js +98 -23
- package/dist/ui/fleet-office-store.js +39 -5
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +172 -12
- package/dist/ui/fleet-office.js +547 -186
- package/dist/ui/fleet-usage.js +19 -15
- package/dist/ui/office-share-dialog.js +145 -0
- package/dist/ui/office-share-page.js +164 -0
- package/dist/ui/panel-chat.js +8 -1
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +4 -4
- package/dist/ui/panel-settings.js +37 -11
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +14 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +16 -2
- package/dist/ui/shared/app-i18n.js +4 -2
- package/dist/ui/shared/app-shell.js +14 -6
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +67 -2
- package/dist/ui/shared/office/cozy.png +0 -0
- package/dist/ui/shared/side-panel.js +95 -0
- package/dist/ui/structured-signin.js +115 -0
- package/dist/usage-report.d.ts +20 -0
- package/dist/usage-report.js +54 -17
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +9 -1
- package/dist/usage-rollup.js +20 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +18 -0
- package/dist/web-api.js +201 -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-backend-login.d.ts +1 -7
- package/dist/web-backend-login.js +2 -24
- package/dist/web-backend-login.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 +3 -2
- package/dist/web-office-skins.js +3 -2
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.js +40 -7
- package/dist/web-office-store.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/dist/working-recovery.d.ts +2 -2
- package/dist/working-recovery.js +43 -8
- package/dist/working-recovery.js.map +1 -1
- package/dist/working-state-journal.d.ts +2 -0
- package/dist/working-state-journal.js +16 -1
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +3 -2
- package/templates/windows-shortcuts.ps1 +155 -140
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The Office view's art (fleet-office.js draws with it on a <canvas>). A skin draws a layout that fleet-office-model.js
|
|
2
|
-
// (desks) or fleet-office-zones.js (zones) made.
|
|
3
|
-
// pixel people, desks, lamps
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// (desks) or fleet-office-zones.js (zones) made. Three skins are built in. Two draw everything procedurally — little
|
|
3
|
+
// pixel people, desks, lamps: "pixel" (the desks layout: a room per team, a desk per instance) and "studio" (the zones
|
|
4
|
+
// layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their state). The third,
|
|
5
|
+
// "cozy" (#2631), is the zones layout on one picture shipped with the page (a warm pixel-art office), the same built-in
|
|
6
|
+
// people in it. Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
|
|
6
7
|
// format v1 in fleet-office-pack.js) can replace any part; a part a pack does not supply falls back to the built-in
|
|
7
8
|
// drawing. An admin uploads a pack (#2513, the host keeps it: src/web-office-skins.ts); loadPackSkin() reads it back —
|
|
8
9
|
// its manifest checked here again (checkSkinPack), its pictures by their content ids — and spriteSkin() draws it.
|
|
@@ -40,8 +41,9 @@
|
|
|
40
41
|
// pictures and draws the characters, props and bubbles each frame. In zones the background is kept; furniture, people (walking
|
|
41
42
|
// along the pack's waypoints), tags, props and bubbles are drawn each frame, furniture by depth around the people (depth "y":
|
|
42
43
|
// among them by base y), then the foreground, then the crowd counts, tags and bubbles.
|
|
44
|
+
import { t } from "/assets/app-i18n.js";
|
|
43
45
|
import { frameAt, nameHash } from "./fleet-office-model.js";
|
|
44
|
-
import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox } from "./fleet-office-pack.js";
|
|
46
|
+
import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox, walkStrip } from "./fleet-office-pack.js";
|
|
45
47
|
|
|
46
48
|
export { nameHash };
|
|
47
49
|
|
|
@@ -222,14 +224,16 @@ function drawDeskFront(ctx, d, look, env) {
|
|
|
222
224
|
ctx.fillStyle = pal.text2; ctx.font = "600 8px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
223
225
|
ctx.fillText(fitText(ctx, env.labels.closed, 32), d.x + 48, d.y + 63);
|
|
224
226
|
}
|
|
225
|
-
// The name tag.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
// The name tag (#2554: none when `notag` — a tile too small to read it; the page draws it again on hover).
|
|
228
|
+
if (!look.notag) {
|
|
229
|
+
ctx.fillStyle = pal.surface;
|
|
230
|
+
ctx.fillRect(d.x + 6, d.y + 78, d.w - 12, 16);
|
|
231
|
+
ctx.strokeStyle = look.match ? pal.focus : pal.border;
|
|
232
|
+
ctx.lineWidth = look.match ? 2 : 1;
|
|
233
|
+
ctx.strokeRect(d.x + 6.5, d.y + 78.5, d.w - 13, 15);
|
|
234
|
+
ctx.fillStyle = pal.text; ctx.font = "600 11px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
235
|
+
ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
|
|
236
|
+
}
|
|
233
237
|
ctx.restore();
|
|
234
238
|
if (look.match) { ctx.save(); ctx.strokeStyle = pal.focus; ctx.lineWidth = 2; ctx.strokeRect(d.x + 2, d.y + 2, d.w - 4, d.h - 6); ctx.restore(); }
|
|
235
239
|
}
|
|
@@ -374,7 +378,7 @@ const STUDIO_FURNITURE = [
|
|
|
374
378
|
].sort((a, b) => a.depth - b.depth);
|
|
375
379
|
/** The studio's pack: the same shape as a checked one (fleet-office-pack.js), furniture by `shape` instead of sprites. */
|
|
376
380
|
export const STUDIO_PACK = {
|
|
377
|
-
format: 1, id: "studio", scale: 1, areas: STUDIO_AREAS, door: { x: HALL_X, y: 704 }, furniture: STUDIO_FURNITURE, ...studioGraph(STUDIO_AREAS),
|
|
381
|
+
format: 1, id: "studio", scale: 1, areas: STUDIO_AREAS, door: { x: HALL_X, y: 704 }, furniture: STUDIO_FURNITURE, ...studioGraph(STUDIO_AREAS), straight: false,
|
|
378
382
|
stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
|
|
379
383
|
characters: {}, byInstance: {}, props: {}, bubbles: {},
|
|
380
384
|
};
|
|
@@ -511,8 +515,12 @@ export function spriteSkin(pack, images, base = pixelSkin) {
|
|
|
511
515
|
},
|
|
512
516
|
drawFurniture(ctx, f, active, ms, env) {
|
|
513
517
|
if (f.shape) { drawFurniture(ctx, f, active, ms, env); return; }
|
|
514
|
-
//
|
|
518
|
+
// An NPC walking (#2596): its walk strip for its heading (walkSide mirrored going left), when its picture is loaded.
|
|
519
|
+
const ws = walkStrip(f);
|
|
520
|
+
if (ws && strip(ctx, ws.strip, ms, f.x, f.y, null, ws.mirror)) return;
|
|
521
|
+
// Active: its activeSprite (or its strip) plays; otherwise its strip loops ("loop", #2570) or its first frame holds.
|
|
515
522
|
if (active) strip(ctx, f.activeSprite || f.sprite, ms, f.x, f.y);
|
|
523
|
+
else if (f.loop) strip(ctx, f.sprite, ms, f.x, f.y);
|
|
516
524
|
else strip(ctx, f.sprite, 0, f.x, f.y, 0);
|
|
517
525
|
},
|
|
518
526
|
drawCharacter(ctx, d, look, ms, env) {
|
|
@@ -528,18 +536,84 @@ export function spriteSkin(pack, images, base = pixelSkin) {
|
|
|
528
536
|
const ps = shown({ ...look, heading: at.heading }, !!at.walking);
|
|
529
537
|
return ps ? stripBox(ps.strip, at.x, at.y, ps.mirror) : base.charBox(look, at);
|
|
530
538
|
},
|
|
531
|
-
|
|
539
|
+
// An item in the air: its sender's own (their set's "prop", #2562), else the pack's for its kind, else the built-in one.
|
|
540
|
+
drawProp(ctx, it, ms, env) {
|
|
541
|
+
const mine = it.from ? own(setOf({ name: it.from }) || {}, "prop") : null;
|
|
542
|
+
if (!strip(ctx, mine || own(pack.props, it.hue), ms, it.x, it.y)) base.drawProp(ctx, it, ms, env);
|
|
543
|
+
},
|
|
532
544
|
};
|
|
533
545
|
}
|
|
534
546
|
|
|
547
|
+
// ── The built-in cozy (#2631): the zones layout on a warm pixel-art office drawn as one picture, the built-in people ──
|
|
548
|
+
|
|
549
|
+
/** Its picture, shipped with the page (src/ui/shared/office/cozy.png, auth-api.ts ASSETS: public static, as the font). */
|
|
550
|
+
export const COZY_BACKGROUND = "/assets/office-cozy.png";
|
|
551
|
+
const pts = (...xy) => xy.map(([x, y]) => ({ x, y }));
|
|
552
|
+
// The seats are feet points on the picture: chairs in front of the desks, the sofa, the coffee table and machine, the
|
|
553
|
+
// beanbags (a sleeper's bottom), in front of the whiteboard. The rects put a crowd's first extra spots (fleet-office-zones.js
|
|
554
|
+
// overflowSpots: rows from the rect's top) on the aisles: under the first row of desks, in front of the sofa.
|
|
555
|
+
const COZY_AREAS = {
|
|
556
|
+
desks: { name: "desks", rect: { x: 130, y: 196, w: 430, h: 220 }, seats: pts([193, 238], [336, 238], [481, 238], [193, 354], [336, 354], [481, 354]) },
|
|
557
|
+
lounge: { name: "lounge", rect: { x: 700, y: 196, w: 476, h: 184 }, seats: pts([940, 240], [1025, 310], [1120, 305]) },
|
|
558
|
+
alert: { name: "alert", rect: { x: 900, y: 500, w: 220, h: 110 }, seats: pts([965, 570], [1045, 570]) },
|
|
559
|
+
nap: { name: "nap", rect: { x: 110, y: 470, w: 400, h: 150 }, seats: pts([180, 562], [300, 562], [422, 565]) },
|
|
560
|
+
};
|
|
561
|
+
/** Its walkable graph, on the picture's aisles, every edge across or up/down; each seat's nearest waypoint is in front of it. */
|
|
562
|
+
const COZY_WAYPOINTS = [
|
|
563
|
+
["door", 642, 640], ["hall", 642, 395], ["top", 642, 175],
|
|
564
|
+
// the desks: the aisle under the first row (y 250), the one under the second (y 395), the column beside them (x 560)
|
|
565
|
+
["d-east", 560, 395], ["d-up", 560, 250], ["d1-3", 481, 250], ["d1-2", 336, 250], ["d1-1", 193, 250],
|
|
566
|
+
["d2-3", 481, 395], ["d2-2", 336, 395], ["d2-x", 300, 395], ["d2-1", 193, 395],
|
|
567
|
+
// the beanbags: the aisle above them (y 460)
|
|
568
|
+
["n-x", 300, 460], ["n-1", 180, 460], ["n-3", 422, 460],
|
|
569
|
+
// the lounge: below the coffee table (y 340), up the sofa's west end; the coffee machine's column (x 1140)
|
|
570
|
+
["l-hall", 946, 395], ["l-mid", 946, 340], ["l-west", 860, 340], ["l-sofa", 860, 250], ["l-table", 1025, 340],
|
|
571
|
+
["e-hall", 1140, 395], ["e-coffee", 1140, 305],
|
|
572
|
+
// the whiteboard: in front of it (y 570), from the coffee machine's column
|
|
573
|
+
["a-east", 1140, 570], ["a-mid", 1005, 570],
|
|
574
|
+
].map(([id, x, y]) => ({ id, x, y }));
|
|
575
|
+
const COZY_EDGES = [
|
|
576
|
+
["door", "hall"], ["hall", "top"], ["hall", "d-east"], ["d-east", "d2-3"], ["d2-3", "d2-2"], ["d2-2", "d2-x"], ["d2-x", "d2-1"],
|
|
577
|
+
["d-east", "d-up"], ["d-up", "d1-3"], ["d1-3", "d1-2"], ["d1-2", "d1-1"], ["d2-x", "n-x"], ["n-x", "n-1"], ["n-x", "n-3"],
|
|
578
|
+
["hall", "l-hall"], ["l-hall", "l-mid"], ["l-mid", "l-west"], ["l-west", "l-sofa"], ["l-mid", "l-table"],
|
|
579
|
+
["l-hall", "e-hall"], ["e-hall", "e-coffee"], ["e-hall", "a-east"], ["a-east", "a-mid"],
|
|
580
|
+
];
|
|
581
|
+
/** The cozy's pack: the picture is the room; the one item is the notice board, its tag saying a pack can restyle it. */
|
|
582
|
+
export const COZY_PACK = {
|
|
583
|
+
format: 1, id: "cozy", scale: 1, background: COZY_BACKGROUND, areas: COZY_AREAS, door: { x: 642, y: 662 },
|
|
584
|
+
waypoints: COZY_WAYPOINTS, edges: COZY_EDGES, straight: true,
|
|
585
|
+
furniture: [{ id: "notice", x: 1211, y: 628, depth: 5, get label() { return t("orgo.cozyNotice"); } }],
|
|
586
|
+
stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
|
|
587
|
+
characters: {}, byInstance: {}, props: {}, bubbles: {},
|
|
588
|
+
};
|
|
589
|
+
/** Until its picture is in (loadCozySkin): the studio's floors under the cozy's areas. */
|
|
590
|
+
const cozyBare = spriteSkin(COZY_PACK, new Map(), studioSkin);
|
|
591
|
+
let cozyJob = null;
|
|
592
|
+
|
|
535
593
|
/** The skins built into this page; an uploaded pack is loaded by loadPackSkin. */
|
|
536
|
-
export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin]]);
|
|
537
|
-
export const BUILT_IN_SKINS = ["pixel", "studio"];
|
|
594
|
+
export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin], ["cozy", cozyBare]]);
|
|
595
|
+
export const BUILT_IN_SKINS = ["pixel", "studio", "cozy"];
|
|
596
|
+
/**
|
|
597
|
+
* #2631: the cozy's picture, loaded once; the skin drawn from it then takes the cozy's place in SKINS (skinFor). The
|
|
598
|
+
* skin, or null while it cannot load (the bare one stays; tried again next time).
|
|
599
|
+
*/
|
|
600
|
+
export function loadCozySkin({ image = defaultImage } = {}) {
|
|
601
|
+
if (SKINS.get("cozy") !== cozyBare) return Promise.resolve(SKINS.get("cozy"));
|
|
602
|
+
cozyJob ??= image(COZY_BACKGROUND).then((im) => {
|
|
603
|
+
cozyJob = null;
|
|
604
|
+
if (!im) return null;
|
|
605
|
+
const sk = spriteSkin(COZY_PACK, new Map([[COZY_BACKGROUND, im]]), studioSkin);
|
|
606
|
+
SKINS.set("cozy", sk);
|
|
607
|
+
return sk;
|
|
608
|
+
}, () => { cozyJob = null; return null; });
|
|
609
|
+
return cozyJob;
|
|
610
|
+
}
|
|
538
611
|
/** A skin by id: a built-in one, else a pack loaded here (`packs`: id → skin), else the default (pixel). */
|
|
539
612
|
export const skinFor = (id, packs = null) => SKINS.get(id) || (packs && packs.get(id)) || pixelSkin;
|
|
540
613
|
|
|
541
614
|
const loaded = new Map(); // `${id}@${sha}` → Promise<OfficeSkin | null>
|
|
542
|
-
|
|
615
|
+
// Room for every pack the host keeps (web-office-skins.ts SKIN_LIMITS.packs) and a few replaced ones (each a new sha).
|
|
616
|
+
const MAX_LOADED = 24;
|
|
543
617
|
const defaultImage = (url) => new Promise((resolve) => {
|
|
544
618
|
if (typeof Image !== "function") { resolve(null); return; }
|
|
545
619
|
const im = new Image();
|
|
@@ -550,9 +624,10 @@ const defaultImage = (url) => new Promise((resolve) => {
|
|
|
550
624
|
/**
|
|
551
625
|
* An uploaded pack (GET /ui/office-skins/<id>?v=<sha>) as an OfficeSkin: its manifest checked (null when it is not a
|
|
552
626
|
* pack: the page then draws a built-in style and says so), its pictures loaded from their content ids (one that does
|
|
553
|
-
* not load is drawn by the built-in skin). Kept per id and sha, so a pack is fetched once.
|
|
627
|
+
* not load is drawn by the built-in skin). Kept per id and sha, so a pack is fetched once. `url`: where a picture's path
|
|
628
|
+
* is read (a share link's page reads its own address, #2636).
|
|
554
629
|
*/
|
|
555
|
-
export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = defaultImage } = {}) {
|
|
630
|
+
export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = defaultImage, url = (p) => p } = {}) {
|
|
556
631
|
const key = `${id}@${sha}`;
|
|
557
632
|
if (loaded.has(key)) return loaded.get(key);
|
|
558
633
|
const base = `/ui/office-skins/${encodeURIComponent(id)}`;
|
|
@@ -561,14 +636,14 @@ export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = d
|
|
|
561
636
|
const r = await get(`${base}?v=${encodeURIComponent(sha)}`, { method: "GET", headers: { "Content-Type": "application/json" } });
|
|
562
637
|
if (!r || !r.ok) return null;
|
|
563
638
|
const d = await r.json();
|
|
564
|
-
const c = checkSkinPack(d && d.manifest);
|
|
639
|
+
const c = checkSkinPack(d && d.manifest, { kept: true });
|
|
565
640
|
if (!c.ok || c.pack.id !== id) return null;
|
|
566
641
|
const files = d.files && typeof d.files === "object" ? d.files : {};
|
|
567
642
|
const images = new Map();
|
|
568
643
|
await Promise.all(packImages(c.pack).map(async (src) => {
|
|
569
644
|
const fid = own(files, src);
|
|
570
645
|
if (typeof fid !== "string" || !/^[0-9a-f]{32}$/.test(fid)) return;
|
|
571
|
-
const im = await image(`${base}/img/${fid}.png`);
|
|
646
|
+
const im = await image(url(`${base}/img/${fid}.png`));
|
|
572
647
|
if (im) images.set(src, im);
|
|
573
648
|
}));
|
|
574
649
|
return spriteSkin(c.pack, images);
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
// what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
|
|
4
4
|
// - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
|
|
5
5
|
// office is shown, the auto office's member filter, and its style (skin); #2540: the view (zoom and centre) and the
|
|
6
|
-
// fit mode (Fit or Fill) per office.
|
|
6
|
+
// fit mode (Fit or Fill) per office; #2554: whether All offices shows the lobby tile.
|
|
7
7
|
// - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
|
|
8
8
|
// src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
|
|
9
9
|
// they read (a change in between answers 409).
|
|
10
10
|
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
11
11
|
import { fitModeOk, savedOk } from "./fleet-office-view.js";
|
|
12
12
|
|
|
13
|
-
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep";
|
|
13
|
+
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep", NIGHT_KEY = "agend_org_office_night";
|
|
14
|
+
/** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
|
|
15
|
+
const LOBBY_KEY = "agend_org_office_lobby";
|
|
14
16
|
/** #2540: { "<office>@<style>": [k, cx, cy] (and a 1 after them: a fit) } — the last VIEWS_MAX views, the most recent last. */
|
|
15
17
|
const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
|
|
16
18
|
/** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
|
|
@@ -28,6 +30,20 @@ export function skinListOk(d) {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** `fetch`: the caller's (a lease's, so an answer after the person moved on is dropped); `storage`: default localStorage. */
|
|
33
|
+
/**
|
|
34
|
+
* Agents' acts as the host sends them (GET /ui/office-acts, the `office_acts` event), checked: the well-formed ones.
|
|
35
|
+
* `now`: the host's clock when it sent them — each end is moved onto this page's clock (they may differ).
|
|
36
|
+
*/
|
|
37
|
+
export function actsOk(list, now) {
|
|
38
|
+
const skew = typeof now === "number" && Number.isFinite(now) ? now - Date.now() : 0;
|
|
39
|
+
const str = (v) => typeof v === "string" && v.length > 0 && v.length <= 200, num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
40
|
+
return (Array.isArray(list) ? list : []).filter((a) => a && typeof a === "object" && str(a.instance) && str(a.office) && num(a.until)
|
|
41
|
+
&& (a.x === undefined || num(a.x)) && (a.y === undefined || num(a.y)) && (a.person === undefined || str(a.person))
|
|
42
|
+
&& (a.say === undefined || (typeof a.say === "string" && a.say.length <= 40)) && (a.pose === undefined || ["idle", "working", "needs", "paused"].includes(a.pose)))
|
|
43
|
+
.map((a) => ({ instance: a.instance, office: a.office, until: a.until - skew, ...(num(a.since) ? { since: a.since } : {}), ...(str(a.state) ? { state: a.state } : {}),
|
|
44
|
+
...(num(a.x) && num(a.y) ? { x: a.x, y: a.y } : {}),
|
|
45
|
+
...(a.person ? { person: a.person } : {}), ...(a.say ? { say: a.say } : {}), ...(a.pose ? { pose: a.pose } : {}) }));
|
|
46
|
+
}
|
|
31
47
|
export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {}) {
|
|
32
48
|
const st = () => { try { return storage || globalThis.localStorage || null; } catch { return null; } };
|
|
33
49
|
const read = (k) => { try { const s = st(); return s ? s.getItem(k) : null; } catch { return null; } };
|
|
@@ -55,6 +71,15 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
55
71
|
return r.ok ? { ok: true, rev: d.rev, offices: officesOk(d.offices) } : { ok: false, status: r.status, error: d.error || "offices_failed" };
|
|
56
72
|
} catch { return { ok: false, status: 0, error: "offices_failed" }; }
|
|
57
73
|
},
|
|
74
|
+
/** #2578: what agents are doing in the offices now (office_set): their acts, or null when they could not be read. */
|
|
75
|
+
async acts() {
|
|
76
|
+
try {
|
|
77
|
+
const r = await get("/ui/office-acts", { method: "GET", headers });
|
|
78
|
+
if (!r || !r.ok) return null;
|
|
79
|
+
const d = await r.json();
|
|
80
|
+
return actsOk(d && d.acts, d && d.now);
|
|
81
|
+
} catch { return null; }
|
|
82
|
+
},
|
|
58
83
|
/** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
|
|
59
84
|
async skins() {
|
|
60
85
|
try {
|
|
@@ -80,17 +105,26 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
80
105
|
return r.ok ? { ok: true } : { ok: false, status: r.status, error: d.error || "skin_failed" };
|
|
81
106
|
} catch { return { ok: false, status: 0, error: "skin_failed" }; }
|
|
82
107
|
},
|
|
83
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* The office shown here: an id (one the host no longer has falls back to the auto office where it is used), or
|
|
110
|
+
* #2554's All offices (fleet-office-overview.js ALL_ID; with fewer than two offices stored: the auto office).
|
|
111
|
+
*/
|
|
84
112
|
choice: () => { const v = read(CHOICE_KEY); return typeof v === "string" && v && v.length <= 40 ? v : AUTO_ID; },
|
|
85
113
|
saveChoice: (id) => write(CHOICE_KEY, String(id)),
|
|
86
114
|
filter: () => { const s = read(FILTER_KEY); try { return filterOk(s ? JSON.parse(s) : null); } catch { return filterOk(null); } },
|
|
87
115
|
saveFilter: (f) => write(FILTER_KEY, JSON.stringify(filterOk(f))),
|
|
88
|
-
/** The whole fleet's style here: a skin id ("pixel": desks, "studio": zones); an unknown one is the default where used. */
|
|
89
|
-
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "
|
|
116
|
+
/** The whole fleet's style here: a skin id ("pixel": desks, "studio" and "cozy": zones); none chosen yet: cozy (#2631); an unknown one is the default where used. */
|
|
117
|
+
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "cozy"; },
|
|
90
118
|
saveSkin: (id) => write(SKIN_KEY, String(id)),
|
|
91
119
|
/** Whether paused and stopped people are left out of the Office here (default: shown). */
|
|
92
120
|
hideAsleep: () => read(ASLEEP_KEY) === "1",
|
|
93
121
|
saveHideAsleep: (v) => write(ASLEEP_KEY, v ? "1" : "0"),
|
|
122
|
+
/** #2568: whether the idle go to bed at night (00:00–06:00 on this device's clock) here (default: off). */
|
|
123
|
+
nightOn: () => read(NIGHT_KEY) === "1",
|
|
124
|
+
saveNightOn: (v) => write(NIGHT_KEY, v ? "1" : "0"),
|
|
125
|
+
/** #2554: whether All offices shows the lobby tile (the instances in no stored office) here (default: shown). */
|
|
126
|
+
lobbyTile: () => read(LOBBY_KEY) !== "0",
|
|
127
|
+
saveLobbyTile: (v) => write(LOBBY_KEY, v ? "1" : "0"),
|
|
94
128
|
/** The view kept for `key` (an office and its style): { k, cx, cy } (fleet-office-view.js savedOf), or null. */
|
|
95
129
|
view: (key) => {
|
|
96
130
|
try { const m = JSON.parse(read(VIEW_KEY) || "{}"); const a = m && Object.prototype.hasOwnProperty.call(m, key) ? m[key] : null;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// whole scene shows — non-integer, so the scene fills the panel's width or height. "cover" (Fill): the smallest
|
|
10
10
|
// scale that fills both; what overflows is cropped and stays a pan away. Either is centred.
|
|
11
11
|
// - The view never loses the scene (clampPan): along an axis where the scene is larger than the panel it covers the
|
|
12
|
-
// panel (no gap at an edge); where it is smaller it
|
|
12
|
+
// panel (no gap at an edge); where it is smaller it is centred on that axis (the user: the canvas's middle is the
|
|
13
|
+
// scene's), on opening, Fit, any zoom and a resize alike.
|
|
13
14
|
//
|
|
14
15
|
// Around the scene is a matte: the pack's `matte` (fleet-office-pack.js), else the colour of the background's edge
|
|
15
16
|
// pixels (sampled once per style: edgeMatte), else the skin's own (Studio's hall floor; Desks': the dashboard's
|
|
@@ -65,13 +66,13 @@ export function fitScene(w, h, size, mode = "contain", margin = 0) {
|
|
|
65
66
|
* 4/3 at 150 %, 0.8 at 125 %); the layout wraps at the panel's width over it, so it opens as wide as the panel.
|
|
66
67
|
*/
|
|
67
68
|
export const desksK = (dpr = 1) => Math.max(1, Math.round(dpr)) / dpr;
|
|
68
|
-
/** The view a scene opens on with nothing kept: zones fitted (the office's mode), desks from the top left at desksK. */
|
|
69
|
+
/** The view a scene opens on with nothing kept: zones fitted (the office's mode), desks from the top left at desksK (centred by clampPan on an axis where it is smaller). */
|
|
69
70
|
export const openingView = (w, h, size, dpr, zones, mode = "contain") => (zones ? fitScene(w, h, size, mode) : { k: desksK(dpr), x: 0, y: 0 });
|
|
70
71
|
/** One axis of clampPan: offset `o` of a scene `s` long in a viewport `n` long. */
|
|
71
|
-
const clampAxis = (o, s, n) => (s >= n ? Math.min(0, Math.max(n - s, o)) :
|
|
72
|
+
const clampAxis = (o, s, n) => (s >= n ? Math.min(0, Math.max(n - s, o)) : (n - s) / 2);
|
|
72
73
|
/**
|
|
73
74
|
* The view kept on the scene: along an axis where the scene is at least the viewport it covers the viewport (no gap at
|
|
74
|
-
* either edge); where it is smaller it
|
|
75
|
+
* either edge); where it is smaller it is centred on that axis. Nothing can be scrolled off-screen.
|
|
75
76
|
*/
|
|
76
77
|
export function clampPan(v, w, h, size) {
|
|
77
78
|
return { k: v.k, x: clampAxis(v.x, w * v.k, size.w), y: clampAxis(v.y, h * v.k, size.h) };
|
|
@@ -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) {
|
|
@@ -73,18 +74,22 @@ export function assignSeats(pack, wants, prev = new Map()) {
|
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
76
|
* The zones layout for `nodes` (buildOrgView's) on a pack: everyone but the missing at their seat. The same shape as
|
|
76
|
-
* the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph
|
|
77
|
-
* seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
|
|
77
|
+
* the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph (and
|
|
78
|
+
* its `straight`, #2634), the 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
|
-
waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
92
|
+
waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
// ── Walking ──
|
|
@@ -103,10 +108,159 @@ export function headingAt(pts, t) {
|
|
|
103
108
|
}
|
|
104
109
|
return null;
|
|
105
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* #2634: a polyline with each diagonal leg made an L — across first, then up or down — so a character set's three
|
|
113
|
+
* walking strips always fit; legs already across or up/down are kept, and no point repeats (no zero-length leg).
|
|
114
|
+
*/
|
|
115
|
+
export function straighten(pts) {
|
|
116
|
+
const out = [];
|
|
117
|
+
const add = (x, y) => { const l = out[out.length - 1]; if (!l || l.x !== x || l.y !== y) out.push({ x, y }); };
|
|
118
|
+
for (const p of pts) { const l = out[out.length - 1]; if (l && l.x !== p.x && l.y !== p.y) add(p.x, l.y); add(p.x, p.y); }
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
// ── Night (#2568) ──
|
|
122
|
+
|
|
123
|
+
/** 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). */
|
|
124
|
+
export const NIGHT = { FROM: 0, TO: 6, TICK: 60000 };
|
|
125
|
+
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; };
|
|
126
|
+
|
|
127
|
+
// ── Wandering (#2560) ──
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 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:
|
|
131
|
+
* every PERIOD ms (each person's own phase, from their name) they are AWAY ms at a waypoint within REACH of the seat —
|
|
132
|
+
* 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.
|
|
133
|
+
*/
|
|
134
|
+
export const WANDER = { PERIOD: 40000, AWAY: 8000, REACH: 220, TICK: 1000 };
|
|
135
|
+
/** Where `name`, seated at `seat`, should be at `t` (ms) while idle: a spot near the seat, or the seat. */
|
|
136
|
+
export function wanderTarget(name, seat, graph, t, seats = []) {
|
|
137
|
+
const near = (graph.waypoints || []).filter((w) => Math.hypot(w.x - seat.x, w.y - seat.y) <= WANDER.REACH
|
|
138
|
+
&& !seats.some((s) => Math.hypot(w.x - s.x, w.y - s.y) < 4));
|
|
139
|
+
if (!near.length) return seat;
|
|
140
|
+
const h = nameHash(name), k = t + (h % WANDER.PERIOD), turn = Math.floor(k / WANDER.PERIOD);
|
|
141
|
+
if (k - turn * WANDER.PERIOD >= WANDER.AWAY) return seat;
|
|
142
|
+
const w = near[nameHash(`${name}\u0000${turn}`) % near.length];
|
|
143
|
+
return { x: w.x, y: w.y };
|
|
144
|
+
}
|
|
145
|
+
/** An NPC drawn as furniture (#2596: an item with `walk`): its walker's name — never a person's, and its own phase. */
|
|
146
|
+
export const npcKey = (f) => `furniture/${f.id}`;
|
|
147
|
+
/** Everyone's walk target: their seat, or — idle and `wander` on — wanderTarget's spot (one person a spot: whoever
|
|
148
|
+
* would share it stays seated this time). And, `wander` on, each item of `furniture` with a `walk` (#2596): its spot
|
|
149
|
+
* (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). */
|
|
150
|
+
export function walkTargets(desks, graph, t, wander = true, furniture = [], night = false) {
|
|
151
|
+
// Every seat of the office (anyone's, taken or not: someone may sit down on it any moment), not only the ones in use.
|
|
152
|
+
// And each walking NPC's spot (#2596 review): its home, where it stands outside its stroll and goes back to whenever
|
|
153
|
+
// its stroll would collide, so nobody else's stroll may end there — no two walkers ever share a target.
|
|
154
|
+
const homes = wander ? furniture.filter((f) => f.walk && !(night && f.night)).map((f) => ({ x: f.x, y: f.y })) : [];
|
|
155
|
+
const seats = [...Object.values(graph.areas || {}).flatMap((a) => a.seats || []), ...desks.map((d) => d.seat), ...homes];
|
|
156
|
+
const used = new Set(homes.map((h) => `${h.x},${h.y}`));
|
|
157
|
+
const stroll = (name, home) => {
|
|
158
|
+
const p = wanderTarget(name, home, graph, t, seats), k = `${p.x},${p.y}`;
|
|
159
|
+
if (p === home || used.has(k)) return home;
|
|
160
|
+
used.add(k);
|
|
161
|
+
return p;
|
|
162
|
+
};
|
|
163
|
+
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)]));
|
|
164
|
+
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 }));
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Mail from another office (#2564) ──
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Something sent to a person here by someone who is not (in another office, or in none): it flies in at the door and
|
|
172
|
+
* lands on the floor near the middle of the room; the person walks over, picks it up and walks back to their seat.
|
|
173
|
+
* MAX_WAIT: a letter no one picks up is gone after it; GAP: letters waiting side by side; SEARCH: how far from the
|
|
174
|
+
* middle a landing spot is looked for when no waypoint will do; ASLEEP: who does not get up
|
|
175
|
+
* for it (it waits for them to wake, up to MAX_WAIT).
|
|
176
|
+
*/
|
|
177
|
+
export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
|
|
178
|
+
/**
|
|
179
|
+
* Where the `n`th waiting letter lands: `n` GAPs along from the waypoint nearest the room's middle — the nearest whose
|
|
180
|
+
* spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
|
|
181
|
+
* with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
|
|
182
|
+
* those either, null — the letter is not delivered here (never on a seat).
|
|
183
|
+
*/
|
|
184
|
+
export function dropPoint(L, n = 0) {
|
|
185
|
+
const mid = { x: L.width / 2, y: L.height * 0.55 }, d = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
|
|
186
|
+
const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
|
|
187
|
+
const onSeat = (p) => seats.some((q) => d(p, q) < 4);
|
|
188
|
+
const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
|
|
189
|
+
.filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
|
|
190
|
+
if (spots.length) return { x: spots[0].x, y: spots[0].y };
|
|
191
|
+
const inRoom = (p) => p.x >= 0 && p.y >= 0 && p.x <= L.width && p.y <= L.height;
|
|
192
|
+
for (let r = 0; r <= MAIL.SEARCH; r += 2 * MAIL.GAP) {
|
|
193
|
+
for (let k = 0, m = r ? 16 : 1; k < m; k++) {
|
|
194
|
+
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)) };
|
|
195
|
+
if (inRoom(p) && !onSeat(p)) return p;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const desksIn = (L) => (L.rooms || []).flatMap((r) => r.desks || []).filter((d) => d.node);
|
|
201
|
+
/**
|
|
202
|
+
* The letters of one office. `sync(items, now, L, still)`: each new traffic item to someone drawn here from someone
|
|
203
|
+
* who is not becomes a letter (none with reduced motion, nor for a stale delegation; a zones office only).
|
|
204
|
+
* `frame(now, L, walkers)` → what to draw, { id, hue, kind, from, x, y, t } (t: its flight, 0 on the floor), picking
|
|
205
|
+
* up the letters whose person stands on them, and dropping those gone too long or whose person left. `targets(L)`:
|
|
206
|
+
* name → where to walk to pick one up (the first waiting for them; none while they are asleep). `prime(items)`: mark as
|
|
207
|
+
* seen what was in the air before this office was shown (a new office's or style's mail starts empty). `size()`.
|
|
208
|
+
*/
|
|
209
|
+
export function createMail({ max = MAIL.MAX } = {}) {
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
let letters = [];
|
|
212
|
+
const here = (L, name) => desksIn(L).find((d) => d.node.name === name) || null;
|
|
213
|
+
return {
|
|
214
|
+
sync(items, now, L, still = false) {
|
|
215
|
+
let started = 0;
|
|
216
|
+
for (const it of items || []) {
|
|
217
|
+
if (seen.has(it.id)) continue;
|
|
218
|
+
seen.add(it.id);
|
|
219
|
+
if (seen.size > 2000) seen.delete(seen.values().next().value);
|
|
220
|
+
if (still || it.phase === "stale" || !L || L.mode !== "zones" || !here(L, it.to) || here(L, it.from)) continue;
|
|
221
|
+
const drop = dropPoint(L, letters.filter((l) => l.to !== it.to).length % 4);
|
|
222
|
+
if (!drop) continue; // nowhere off the seats: not delivered here
|
|
223
|
+
const pts = [{ x: L.door.x, y: L.door.y }, drop];
|
|
224
|
+
letters.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now, drop, pts,
|
|
225
|
+
ms: Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(pts) / FLIGHT.SPEED)) });
|
|
226
|
+
started++;
|
|
227
|
+
}
|
|
228
|
+
if (letters.length > max) letters = letters.slice(letters.length - max);
|
|
229
|
+
return started;
|
|
230
|
+
},
|
|
231
|
+
frame(now, L, walkers) {
|
|
232
|
+
const out = [];
|
|
233
|
+
letters = letters.filter((l) => {
|
|
234
|
+
if (!here(L, l.to)) return false;
|
|
235
|
+
const t = (now - l.start) / l.ms;
|
|
236
|
+
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; }
|
|
237
|
+
if (now - l.start - l.ms > MAIL.MAX_WAIT) return false;
|
|
238
|
+
const w = walkers && walkers.at(l.to, now);
|
|
239
|
+
if (w && !w.walking && Math.hypot(w.x - l.drop.x, w.y - l.drop.y) < 4) return false; // picked up
|
|
240
|
+
out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: l.drop.x, y: l.drop.y, t: 0 });
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
return out;
|
|
244
|
+
},
|
|
245
|
+
targets(L, now) {
|
|
246
|
+
const out = new Map();
|
|
247
|
+
for (const l of letters) {
|
|
248
|
+
if (out.has(l.to) || now - l.start < l.ms) continue;
|
|
249
|
+
const d = here(L, l.to);
|
|
250
|
+
if (d && !MAIL.ASLEEP.has(d.node.state) && !d.night) out.set(l.to, l.drop); // asleep, or in bed for the night (#2568)
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
},
|
|
254
|
+
/** What is already in the traffic when this office's mail starts (#2564): seen, so it is not delivered again here. */
|
|
255
|
+
prime(items) { for (const it of items || []) seen.add(it.id); },
|
|
256
|
+
size: () => letters.length,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
106
260
|
/**
|
|
107
261
|
* Where each character is. `sync(targets, now, graph, still)`: targets name → { x, y }; someone new is already there;
|
|
108
|
-
* someone whose target moved walks from where they are now along the graph (routeVia
|
|
109
|
-
* `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
|
|
262
|
+
* someone whose target moved walks from where they are now along the graph (routeVia; each diagonal leg an L when the
|
|
263
|
+
* graph is `straight`, #2634) — or is there at once when `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
|
|
110
264
|
* current step's { dx, dy }, which picks a pack's walkDown / walkUp / walkSide strip; `moving(now)`.
|
|
111
265
|
*/
|
|
112
266
|
export function createWalkers() {
|
|
@@ -125,7 +279,8 @@ export function createWalkers() {
|
|
|
125
279
|
const w = by.get(name);
|
|
126
280
|
if (!w || still) { if (!w || w.to.x !== to.x || w.to.y !== to.y) by.set(name, { pts: null, to }); continue; }
|
|
127
281
|
if (w.to.x === to.x && w.to.y === to.y) continue;
|
|
128
|
-
const here = posAt(w, now),
|
|
282
|
+
const here = posAt(w, now), via = routeVia(graph, { x: here.x, y: here.y }, { x: to.x, y: to.y });
|
|
283
|
+
const pts = graph.straight ? straighten(via) : via; // #2634: timed along the longer way
|
|
129
284
|
by.set(name, { pts, start: now, ms: Math.max(1, pathLength(pts) / WALK.SPEED), to });
|
|
130
285
|
}
|
|
131
286
|
},
|
|
@@ -232,8 +387,13 @@ export function linesFor(state, lang, pack, defaults) {
|
|
|
232
387
|
// ── Furniture ──
|
|
233
388
|
|
|
234
389
|
/** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
|
|
235
|
-
export function activeFurniture(pack, states) {
|
|
390
|
+
export function activeFurniture(pack, states, byName = new Map()) {
|
|
236
391
|
const on = new Set();
|
|
237
|
-
for (const f of (pack && pack.furniture) || [])
|
|
392
|
+
for (const f of (pack && pack.furniture) || []) {
|
|
393
|
+
if (!f.activeWhen) continue;
|
|
394
|
+
// activeFor (#2570): only those people's states count; else anyone's shown.
|
|
395
|
+
const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
|
|
396
|
+
if (hit) on.add(f.id);
|
|
397
|
+
}
|
|
238
398
|
return on;
|
|
239
399
|
}
|