@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.11
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 +69 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +11 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +3 -0
- package/dist/agent-whats-new.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-classic-archive.d.ts +52 -0
- package/dist/backend/kiro-classic-archive.js +444 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +83 -0
- package/dist/backend/kiro-identity.js +144 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-migration-verify.d.ts +10 -0
- package/dist/backend/kiro-migration-verify.js +52 -0
- package/dist/backend/kiro-migration-verify.js.map +1 -0
- 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-v2-store-worker.js +21 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +8 -0
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +169 -2
- package/dist/backend/kiro.js +703 -28
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +108 -0
- package/dist/backend/types.js +9 -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/ipc-timeouts.js +3 -0
- package/dist/channel/ipc-timeouts.js.map +1 -1
- package/dist/channel/mcp-tools.js +41 -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 +54 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +55 -10
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +33 -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 +68 -0
- package/dist/daemon.js +448 -26
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +31 -6
- package/dist/dashboard-permissions.js +84 -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-control-audit.d.ts +1 -1
- package/dist/fleet-control-audit.js +3 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-manager.d.ts +133 -2
- package/dist/fleet-manager.js +905 -94
- 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 +24 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +49 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +49 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +47 -9
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +54 -11
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +23 -0
- package/dist/instance-lifecycle.js +62 -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 +60 -0
- package/dist/kiro-migrate.js +160 -0
- package/dist/kiro-migrate.js.map +1 -0
- package/dist/kiro-session-proof.d.ts +41 -0
- package/dist/kiro-session-proof.js +89 -0
- package/dist/kiro-session-proof.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 +12 -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 +230 -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 +10 -2
- package/dist/needs-you.js +10 -2
- 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 +268 -0
- package/dist/office-share.js +574 -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 +65 -2
- package/dist/outbound-schemas.js +56 -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/remote-card-prompt.d.ts +38 -0
- package/dist/remote-card-prompt.js +86 -0
- package/dist/remote-card-prompt.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 +20 -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 +29 -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 +27 -2
- package/dist/transcript-sources.js +104 -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 +33 -8
- package/dist/ui/backend-signin.js +63 -10
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +4 -4
- package/dist/ui/fleet-office-editor.js +68 -11
- package/dist/ui/fleet-office-layout.js +110 -35
- 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 +13 -3
- package/dist/ui/fleet-office-pack.js +214 -24
- package/dist/ui/fleet-office-skin.js +98 -23
- package/dist/ui/fleet-office-store.js +82 -5
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +232 -21
- package/dist/ui/fleet-office.js +549 -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 +22 -3
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +5 -5
- package/dist/ui/panel-settings.js +38 -12
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +22 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +18 -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 +85 -4
- 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 +23 -1
- package/dist/web-api.js +292 -3
- 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-pack-read.d.ts +35 -0
- package/dist/web-office-pack-read.js +252 -0
- package/dist/web-office-pack-read.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +232 -0
- package/dist/web-office-pack-tools.js +754 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +71 -6
- package/dist/web-office-skins.js +321 -35
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +5 -2
- package/dist/web-office-store.js +69 -10
- 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
|
@@ -3,19 +3,36 @@
|
|
|
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
|
+
// - #2692: the agents' pack changes (GET / POST /ui/office-pack-changes, an admin's): which agents may change packs, the
|
|
11
|
+
// fleet's approval mode, the changes waiting for a yes, and the newest log entries.
|
|
10
12
|
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
11
13
|
import { fitModeOk, savedOk } from "./fleet-office-view.js";
|
|
12
14
|
|
|
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";
|
|
15
|
+
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";
|
|
16
|
+
/** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
|
|
17
|
+
const LOBBY_KEY = "agend_org_office_lobby";
|
|
14
18
|
/** #2540: { "<office>@<style>": [k, cx, cy] (and a 1 after them: a fit) } — the last VIEWS_MAX views, the most recent last. */
|
|
15
19
|
const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
|
|
16
20
|
/** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
|
|
17
21
|
const FIT_KEY = "agend_org_office_fit";
|
|
18
22
|
const PACK_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
23
|
+
/** #2692: GET /ui/office-pack-changes, checked: { approval, agents: [names], pending: [views], log: [entries] } (anything odd dropped). */
|
|
24
|
+
export function packChangesOk(d) {
|
|
25
|
+
const str = (v, n = 200) => (typeof v === "string" ? v.slice(0, n) : "");
|
|
26
|
+
const approval = d && (d.approval === "apply" || d.approval === "approve") ? d.approval : "approve";
|
|
27
|
+
const agents = Array.isArray(d && d.agents) ? d.agents.filter((n) => typeof n === "string" && n).slice(0, 500).map((n) => n.slice(0, 200)) : [];
|
|
28
|
+
const pending = (Array.isArray(d && d.pending) ? d.pending.slice(0, 32) : []).filter((v) => v && /^[0-9a-f]{32}$/.test(String(v.id)) && typeof v.instance === "string")
|
|
29
|
+
.map((v) => ({ id: v.id, instance: str(v.instance), action: str(v.action, 20), at: Number(v.at) || 0, expires_at: Number(v.expires_at) || 0,
|
|
30
|
+
packs: (Array.isArray(v.packs) ? v.packs.slice(0, 16) : []).filter((p) => p && typeof p.id === "string").map((p) => ({ id: str(p.id, 40), name: str(p.name, 60) || str(p.id, 40), files: Number(p.files) || 0, bytes: Number(p.bytes) || 0, replaces: typeof p.replaces === "string" ? p.replaces.slice(0, 32) : null })),
|
|
31
|
+
office: str(v.office, 40), style: str(v.style, 40), keep_previous: v.keep_previous === true, drops: Array.isArray(v.drops) ? v.drops.length : 0 }));
|
|
32
|
+
const log = (Array.isArray(d && d.log) ? d.log.slice(0, 50) : []).filter((e) => e && typeof e.at === "string")
|
|
33
|
+
.map((e) => ({ at: str(e.at, 40), by: str(e.by), action: str(e.action, 20), pack: str(e.pack, 200), outcome: str(e.outcome), files: Number(e.files) || 0, bytes: Number(e.bytes) || 0, approved_by: str(e.approved_by) }));
|
|
34
|
+
return { approval, agents, pending, log };
|
|
35
|
+
}
|
|
19
36
|
/** The host's list of skin packs, checked: [{ id, name, sha }] (anything odd dropped). */
|
|
20
37
|
export function skinListOk(d) {
|
|
21
38
|
const out = [], ids = new Set();
|
|
@@ -28,6 +45,20 @@ export function skinListOk(d) {
|
|
|
28
45
|
}
|
|
29
46
|
|
|
30
47
|
/** `fetch`: the caller's (a lease's, so an answer after the person moved on is dropped); `storage`: default localStorage. */
|
|
48
|
+
/**
|
|
49
|
+
* Agents' acts as the host sends them (GET /ui/office-acts, the `office_acts` event), checked: the well-formed ones.
|
|
50
|
+
* `now`: the host's clock when it sent them — each end is moved onto this page's clock (they may differ).
|
|
51
|
+
*/
|
|
52
|
+
export function actsOk(list, now) {
|
|
53
|
+
const skew = typeof now === "number" && Number.isFinite(now) ? now - Date.now() : 0;
|
|
54
|
+
const str = (v) => typeof v === "string" && v.length > 0 && v.length <= 200, num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
55
|
+
return (Array.isArray(list) ? list : []).filter((a) => a && typeof a === "object" && str(a.instance) && str(a.office) && num(a.until)
|
|
56
|
+
&& (a.x === undefined || num(a.x)) && (a.y === undefined || num(a.y)) && (a.person === undefined || str(a.person))
|
|
57
|
+
&& (a.say === undefined || (typeof a.say === "string" && a.say.length <= 40)) && (a.pose === undefined || ["idle", "working", "needs", "paused"].includes(a.pose)))
|
|
58
|
+
.map((a) => ({ instance: a.instance, office: a.office, until: a.until - skew, ...(num(a.since) ? { since: a.since } : {}), ...(str(a.state) ? { state: a.state } : {}),
|
|
59
|
+
...(num(a.x) && num(a.y) ? { x: a.x, y: a.y } : {}),
|
|
60
|
+
...(a.person ? { person: a.person } : {}), ...(a.say ? { say: a.say } : {}), ...(a.pose ? { pose: a.pose } : {}) }));
|
|
61
|
+
}
|
|
31
62
|
export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {}) {
|
|
32
63
|
const st = () => { try { return storage || globalThis.localStorage || null; } catch { return null; } };
|
|
33
64
|
const read = (k) => { try { const s = st(); return s ? s.getItem(k) : null; } catch { return null; } };
|
|
@@ -55,6 +86,15 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
55
86
|
return r.ok ? { ok: true, rev: d.rev, offices: officesOk(d.offices) } : { ok: false, status: r.status, error: d.error || "offices_failed" };
|
|
56
87
|
} catch { return { ok: false, status: 0, error: "offices_failed" }; }
|
|
57
88
|
},
|
|
89
|
+
/** #2578: what agents are doing in the offices now (office_set): their acts, or null when they could not be read. */
|
|
90
|
+
async acts() {
|
|
91
|
+
try {
|
|
92
|
+
const r = await get("/ui/office-acts", { method: "GET", headers });
|
|
93
|
+
if (!r || !r.ok) return null;
|
|
94
|
+
const d = await r.json();
|
|
95
|
+
return actsOk(d && d.acts, d && d.now);
|
|
96
|
+
} catch { return null; }
|
|
97
|
+
},
|
|
58
98
|
/** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
|
|
59
99
|
async skins() {
|
|
60
100
|
try {
|
|
@@ -80,17 +120,54 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
80
120
|
return r.ok ? { ok: true } : { ok: false, status: r.status, error: d.error || "skin_failed" };
|
|
81
121
|
} catch { return { ok: false, status: 0, error: "skin_failed" }; }
|
|
82
122
|
},
|
|
83
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* #2692: the agents' skin-pack changes (an admin session): { approval, agents, pending, log }, or null (not offered,
|
|
125
|
+
* not an admin, not read).
|
|
126
|
+
*/
|
|
127
|
+
async packChanges() {
|
|
128
|
+
try {
|
|
129
|
+
const r = await get("/ui/office-pack-changes", { method: "GET", headers });
|
|
130
|
+
if (!r || !r.ok) return null;
|
|
131
|
+
return packChangesOk(await r.json());
|
|
132
|
+
} catch { return null; }
|
|
133
|
+
},
|
|
134
|
+
/** #2692: the fleet's approval mode for agents' pack changes ("apply" | "approve"). */
|
|
135
|
+
async setPackApproval(approval) {
|
|
136
|
+
try {
|
|
137
|
+
const r = await get("/ui/office-pack-changes", { method: "POST", headers, body: JSON.stringify({ approval }) });
|
|
138
|
+
const d = await r.json().catch(() => ({}));
|
|
139
|
+
return r.ok ? { ok: true, changes: packChangesOk(d) } : { ok: false, status: r.status, error: d.error || "pack_changes_failed" };
|
|
140
|
+
} catch { return { ok: false, status: 0, error: "pack_changes_failed" }; }
|
|
141
|
+
},
|
|
142
|
+
/** #2692: an admin's yes or no on a pending pack change: { ok, text } or { ok: false, status, error, detail }. */
|
|
143
|
+
async decidePackChange(id, decision) {
|
|
144
|
+
if (!/^[0-9a-f]{32}$/.test(String(id))) return { ok: false, status: 0, error: "change_unknown", detail: "" };
|
|
145
|
+
try {
|
|
146
|
+
const r = await get(`/ui/office-pack-changes/${id}`, { method: "POST", headers, body: JSON.stringify({ decision }) });
|
|
147
|
+
const d = await r.json().catch(() => ({}));
|
|
148
|
+
return r.ok ? { ok: true, text: String(d.text || "") } : { ok: false, status: r.status, error: d.error || "pack_changes_failed", detail: String(d.detail || "") };
|
|
149
|
+
} catch { return { ok: false, status: 0, error: "pack_changes_failed", detail: "" }; }
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* The office shown here: an id (one the host no longer has falls back to the auto office where it is used), or
|
|
153
|
+
* #2554's All offices (fleet-office-overview.js ALL_ID; with fewer than two offices stored: the auto office).
|
|
154
|
+
*/
|
|
84
155
|
choice: () => { const v = read(CHOICE_KEY); return typeof v === "string" && v && v.length <= 40 ? v : AUTO_ID; },
|
|
85
156
|
saveChoice: (id) => write(CHOICE_KEY, String(id)),
|
|
86
157
|
filter: () => { const s = read(FILTER_KEY); try { return filterOk(s ? JSON.parse(s) : null); } catch { return filterOk(null); } },
|
|
87
158
|
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 : "
|
|
159
|
+
/** 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. */
|
|
160
|
+
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "cozy"; },
|
|
90
161
|
saveSkin: (id) => write(SKIN_KEY, String(id)),
|
|
91
162
|
/** Whether paused and stopped people are left out of the Office here (default: shown). */
|
|
92
163
|
hideAsleep: () => read(ASLEEP_KEY) === "1",
|
|
93
164
|
saveHideAsleep: (v) => write(ASLEEP_KEY, v ? "1" : "0"),
|
|
165
|
+
/** #2568: whether the idle go to bed at night (00:00–06:00 on this device's clock) here (default: off). */
|
|
166
|
+
nightOn: () => read(NIGHT_KEY) === "1",
|
|
167
|
+
saveNightOn: (v) => write(NIGHT_KEY, v ? "1" : "0"),
|
|
168
|
+
/** #2554: whether All offices shows the lobby tile (the instances in no stored office) here (default: shown). */
|
|
169
|
+
lobbyTile: () => read(LOBBY_KEY) !== "0",
|
|
170
|
+
saveLobbyTile: (v) => write(LOBBY_KEY, v ? "1" : "0"),
|
|
94
171
|
/** The view kept for `key` (an office and its style): { k, cx, cy } (fleet-office-view.js savedOf), or null. */
|
|
95
172
|
view: (key) => {
|
|
96
173
|
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) {
|
|
@@ -41,50 +42,105 @@ export function overflowSpots(area, need = 0) {
|
|
|
41
42
|
* slot }). Someone still in the same area keeps their slot; the others take the first free seat, in the area's order,
|
|
42
43
|
* then an overflow spot (slot -1, -2, …). No two share one. Whoever finds neither is counted in `crowd` (area → n).
|
|
43
44
|
* Returns { seats: Map name → { area, slot, x, y }, crowd: Map }.
|
|
45
|
+
*
|
|
46
|
+
* #2691: a seat may be reserved (its `for`). Whoever it is for, wanting its area, takes a free seat reserved for them
|
|
47
|
+
* before anything else (the one they sat on last time, else their first in the area's order) — owners are seated
|
|
48
|
+
* before anyone keeps a slot, so a borrower on an owner's seat moves off when the owner comes. Everyone else keeps
|
|
49
|
+
* their slot or takes the first free seat not reserved for someone else: a plain one, else a reserved one that says
|
|
50
|
+
* `lend` (one no owner of it took). A reserved seat not lent, none of whose owners wants the area, is unavailable:
|
|
51
|
+
* the area's overflow spots and `over` count without it. Seats without `for` work as before.
|
|
44
52
|
*/
|
|
45
53
|
export function assignSeats(pack, wants, prev = new Map()) {
|
|
46
|
-
const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map(), demand = new Map();
|
|
54
|
+
const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map(), demand = new Map(), wanting = new Map();
|
|
47
55
|
const areaOf = (a) => own(pack.areas, a);
|
|
48
|
-
for (const w of wants)
|
|
56
|
+
for (const w of wants) {
|
|
57
|
+
demand.set(w.area, (demand.get(w.area) || 0) + 1);
|
|
58
|
+
if (!wanting.has(w.area)) wanting.set(w.area, new Set());
|
|
59
|
+
wanting.get(w.area).add(w.name);
|
|
60
|
+
}
|
|
61
|
+
const mine = (s, name) => !!s.for && s.for.includes(name);
|
|
62
|
+
const lent = (s) => !!s.for && s.lend === true;
|
|
63
|
+
// The seats someone wanting the area may end up on: plain, lent, or reserved for someone who wants it.
|
|
64
|
+
const usable = (area) => areaOf(area).seats.filter((s) => !s.for || s.lend === true || s.for.some((n) => wanting.get(area).has(n))).length;
|
|
49
65
|
const spotsOf = (area) => {
|
|
50
|
-
if (!spill.has(area)) spill.set(area, overflowSpots(areaOf(area), Math.max(0, (demand.get(area) || 0) -
|
|
66
|
+
if (!spill.has(area)) spill.set(area, overflowSpots(areaOf(area), Math.max(0, (demand.get(area) || 0) - usable(area))));
|
|
51
67
|
return spill.get(area);
|
|
52
68
|
};
|
|
53
69
|
const pos = (area, slot) => (slot >= 0 ? areaOf(area).seats[slot] : spotsOf(area)[-slot - 1]);
|
|
54
70
|
const usedIn = (area) => { if (!taken.has(area)) taken.set(area, new Set()); return taken.get(area); };
|
|
55
71
|
const give = (name, area, slot) => { const p = pos(area, slot); usedIn(area).add(slot); seats.set(name, { area, slot, x: p.x, y: p.y }); };
|
|
72
|
+
const free = (w) => !seats.has(w.name) && !!areaOf(w.area);
|
|
73
|
+
// Owners first: their own seat from last time, then their first free one.
|
|
74
|
+
for (const w of wants) {
|
|
75
|
+
const p = prev.get(w.name), s = p && p.area === w.area && p.slot >= 0 && areaOf(w.area) ? areaOf(w.area).seats[p.slot] : null;
|
|
76
|
+
if (s && mine(s, w.name) && !usedIn(w.area).has(p.slot)) give(w.name, w.area, p.slot);
|
|
77
|
+
}
|
|
78
|
+
for (const w of wants) {
|
|
79
|
+
if (!free(w)) continue;
|
|
80
|
+
const used = usedIn(w.area), slot = areaOf(w.area).seats.findIndex((s, i) => mine(s, w.name) && !used.has(i));
|
|
81
|
+
if (slot >= 0) give(w.name, w.area, slot);
|
|
82
|
+
}
|
|
56
83
|
for (const w of wants) {
|
|
57
84
|
const p = prev.get(w.name);
|
|
58
|
-
if (!p || p.area !== w.area || !
|
|
85
|
+
if (!free(w) || !p || p.area !== w.area || !pos(w.area, p.slot) || usedIn(w.area).has(p.slot)) continue;
|
|
86
|
+
const s = p.slot >= 0 ? areaOf(w.area).seats[p.slot] : null;
|
|
87
|
+
if (s && s.for && !lent(s)) continue; // reserved for someone else (theirs would be taken above)
|
|
59
88
|
give(w.name, w.area, p.slot);
|
|
60
89
|
}
|
|
61
90
|
for (const w of wants) {
|
|
62
|
-
if (
|
|
91
|
+
if (!free(w)) continue;
|
|
63
92
|
const used = usedIn(w.area), area = areaOf(w.area);
|
|
64
|
-
let slot = area.seats.findIndex((
|
|
93
|
+
let slot = area.seats.findIndex((s, i) => !s.for && !used.has(i));
|
|
94
|
+
if (slot < 0) slot = area.seats.findIndex((s, i) => lent(s) && !used.has(i));
|
|
65
95
|
if (slot < 0) { const j = spotsOf(w.area).findIndex((_, i) => !used.has(-i - 1)); slot = j < 0 ? null : -j - 1; }
|
|
66
96
|
if (slot === null) crowd.set(w.area, (crowd.get(w.area) || 0) + 1);
|
|
67
97
|
else give(w.name, w.area, slot);
|
|
68
98
|
}
|
|
69
|
-
// Over capacity: more people want the area than it has seats (their bubbles and tags are thinned, fleet-office.js).
|
|
70
|
-
const over = new Set([...demand].filter(([a, n]) => areaOf(a) && n >
|
|
99
|
+
// Over capacity: more people want the area than it has seats for them (their bubbles and tags are thinned, fleet-office.js).
|
|
100
|
+
const over = new Set([...demand].filter(([a, n]) => areaOf(a) && n > usable(a)).map(([a]) => a));
|
|
71
101
|
return { seats, crowd, over };
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
/**
|
|
75
105
|
* 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).
|
|
106
|
+
* the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph (and
|
|
107
|
+
* its `straight`, #2634), the seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
|
|
78
108
|
*/
|
|
79
|
-
export function zonesLayout(pack, nodes, prev = new Map()) {
|
|
80
|
-
|
|
81
|
-
const
|
|
109
|
+
export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {}) {
|
|
110
|
+
// 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.
|
|
111
|
+
const bed = zoneOf(pack, "paused"), bedSeats = own(pack.areas, bed)?.seats || [], sleepers = new Set(), inBed = new Set();
|
|
112
|
+
const wantsOf = (beds) => nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: beds.has(n.name) ? bed : zoneOf(pack, n.state) }));
|
|
113
|
+
let placed = assignSeats(pack, wantsOf(inBed), prev);
|
|
114
|
+
if (night) {
|
|
115
|
+
// #2691 review: who goes to bed is decided by the seat assignment itself (assignSeats' reservation rules), one
|
|
116
|
+
// sleeper at a time — whoever has a bed of their own there first, then the rest in the fleet's order. A sleeper
|
|
117
|
+
// goes to bed only if it gives them a bed (never an overflow spot) without taking one from anyone already in one
|
|
118
|
+
// (the awake first, then the sleepers admitted before), unless it is a bed reserved for them: an owner comes first,
|
|
119
|
+
// as by day. Everyone else dozes where they are.
|
|
120
|
+
const inABed = (r) => new Set([...r.seats].filter(([, s]) => s.area === bed && s.slot >= 0).map(([name]) => name));
|
|
121
|
+
const idle = nodes.filter((n) => n.state === "idle");
|
|
122
|
+
const hasBed = (n) => bedSeats.some((s) => s.for && s.for.includes(n.name));
|
|
123
|
+
for (const n of idle) sleepers.add(n.name);
|
|
124
|
+
for (const n of [...idle.filter(hasBed), ...idle.filter((n) => !hasBed(n))]) {
|
|
125
|
+
const trial = assignSeats(pack, wantsOf(new Set([...inBed, n.name])), prev), s = trial.seats.get(n.name);
|
|
126
|
+
if (!s || s.area !== bed || s.slot < 0) {
|
|
127
|
+
if (!hasBed(n)) break; // no bed free for anyone without one: none for the rest either
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const before = inABed(placed), after = inABed(trial);
|
|
131
|
+
const theirs = !!bedSeats[s.slot].for && bedSeats[s.slot].for.includes(n.name);
|
|
132
|
+
if (!theirs && [...before].some((m) => !after.has(m))) continue;
|
|
133
|
+
inBed.add(n.name);
|
|
134
|
+
placed = trial;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const { seats, crowd, over } = placed;
|
|
82
138
|
const desks = nodes.filter((n) => seats.has(n.name)).map((n) => {
|
|
83
139
|
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 };
|
|
140
|
+
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
141
|
});
|
|
86
142
|
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 };
|
|
143
|
+
waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
88
144
|
}
|
|
89
145
|
|
|
90
146
|
// ── Walking ──
|
|
@@ -103,10 +159,159 @@ export function headingAt(pts, t) {
|
|
|
103
159
|
}
|
|
104
160
|
return null;
|
|
105
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* #2634: a polyline with each diagonal leg made an L — across first, then up or down — so a character set's three
|
|
164
|
+
* walking strips always fit; legs already across or up/down are kept, and no point repeats (no zero-length leg).
|
|
165
|
+
*/
|
|
166
|
+
export function straighten(pts) {
|
|
167
|
+
const out = [];
|
|
168
|
+
const add = (x, y) => { const l = out[out.length - 1]; if (!l || l.x !== x || l.y !== y) out.push({ x, y }); };
|
|
169
|
+
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); }
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
// ── Night (#2568) ──
|
|
173
|
+
|
|
174
|
+
/** 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). */
|
|
175
|
+
export const NIGHT = { FROM: 0, TO: 6, TICK: 60000 };
|
|
176
|
+
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; };
|
|
177
|
+
|
|
178
|
+
// ── Wandering (#2560) ──
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 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:
|
|
182
|
+
* every PERIOD ms (each person's own phase, from their name) they are AWAY ms at a waypoint within REACH of the seat —
|
|
183
|
+
* 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.
|
|
184
|
+
*/
|
|
185
|
+
export const WANDER = { PERIOD: 40000, AWAY: 8000, REACH: 220, TICK: 1000 };
|
|
186
|
+
/** Where `name`, seated at `seat`, should be at `t` (ms) while idle: a spot near the seat, or the seat. */
|
|
187
|
+
export function wanderTarget(name, seat, graph, t, seats = []) {
|
|
188
|
+
const near = (graph.waypoints || []).filter((w) => Math.hypot(w.x - seat.x, w.y - seat.y) <= WANDER.REACH
|
|
189
|
+
&& !seats.some((s) => Math.hypot(w.x - s.x, w.y - s.y) < 4));
|
|
190
|
+
if (!near.length) return seat;
|
|
191
|
+
const h = nameHash(name), k = t + (h % WANDER.PERIOD), turn = Math.floor(k / WANDER.PERIOD);
|
|
192
|
+
if (k - turn * WANDER.PERIOD >= WANDER.AWAY) return seat;
|
|
193
|
+
const w = near[nameHash(`${name}\u0000${turn}`) % near.length];
|
|
194
|
+
return { x: w.x, y: w.y };
|
|
195
|
+
}
|
|
196
|
+
/** An NPC drawn as furniture (#2596: an item with `walk`): its walker's name — never a person's, and its own phase. */
|
|
197
|
+
export const npcKey = (f) => `furniture/${f.id}`;
|
|
198
|
+
/** Everyone's walk target: their seat, or — idle and `wander` on — wanderTarget's spot (one person a spot: whoever
|
|
199
|
+
* would share it stays seated this time). And, `wander` on, each item of `furniture` with a `walk` (#2596): its spot
|
|
200
|
+
* (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). */
|
|
201
|
+
export function walkTargets(desks, graph, t, wander = true, furniture = [], night = false) {
|
|
202
|
+
// Every seat of the office (anyone's, taken or not: someone may sit down on it any moment), not only the ones in use.
|
|
203
|
+
// And each walking NPC's spot (#2596 review): its home, where it stands outside its stroll and goes back to whenever
|
|
204
|
+
// its stroll would collide, so nobody else's stroll may end there — no two walkers ever share a target.
|
|
205
|
+
const homes = wander ? furniture.filter((f) => f.walk && !(night && f.night)).map((f) => ({ x: f.x, y: f.y })) : [];
|
|
206
|
+
const seats = [...Object.values(graph.areas || {}).flatMap((a) => a.seats || []), ...desks.map((d) => d.seat), ...homes];
|
|
207
|
+
const used = new Set(homes.map((h) => `${h.x},${h.y}`));
|
|
208
|
+
const stroll = (name, home) => {
|
|
209
|
+
const p = wanderTarget(name, home, graph, t, seats), k = `${p.x},${p.y}`;
|
|
210
|
+
if (p === home || used.has(k)) return home;
|
|
211
|
+
used.add(k);
|
|
212
|
+
return p;
|
|
213
|
+
};
|
|
214
|
+
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)]));
|
|
215
|
+
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 }));
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Mail from another office (#2564) ──
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Something sent to a person here by someone who is not (in another office, or in none): it flies in at the door and
|
|
223
|
+
* lands on the floor near the middle of the room; the person walks over, picks it up and walks back to their seat.
|
|
224
|
+
* MAX_WAIT: a letter no one picks up is gone after it; GAP: letters waiting side by side; SEARCH: how far from the
|
|
225
|
+
* middle a landing spot is looked for when no waypoint will do; ASLEEP: who does not get up
|
|
226
|
+
* for it (it waits for them to wake, up to MAX_WAIT).
|
|
227
|
+
*/
|
|
228
|
+
export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
|
|
229
|
+
/**
|
|
230
|
+
* Where the `n`th waiting letter lands: `n` GAPs along from the waypoint nearest the room's middle — the nearest whose
|
|
231
|
+
* spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
|
|
232
|
+
* with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
|
|
233
|
+
* those either, null — the letter is not delivered here (never on a seat).
|
|
234
|
+
*/
|
|
235
|
+
export function dropPoint(L, n = 0) {
|
|
236
|
+
const mid = { x: L.width / 2, y: L.height * 0.55 }, d = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
|
|
237
|
+
const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
|
|
238
|
+
const onSeat = (p) => seats.some((q) => d(p, q) < 4);
|
|
239
|
+
const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
|
|
240
|
+
.filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
|
|
241
|
+
if (spots.length) return { x: spots[0].x, y: spots[0].y };
|
|
242
|
+
const inRoom = (p) => p.x >= 0 && p.y >= 0 && p.x <= L.width && p.y <= L.height;
|
|
243
|
+
for (let r = 0; r <= MAIL.SEARCH; r += 2 * MAIL.GAP) {
|
|
244
|
+
for (let k = 0, m = r ? 16 : 1; k < m; k++) {
|
|
245
|
+
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)) };
|
|
246
|
+
if (inRoom(p) && !onSeat(p)) return p;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const desksIn = (L) => (L.rooms || []).flatMap((r) => r.desks || []).filter((d) => d.node);
|
|
252
|
+
/**
|
|
253
|
+
* The letters of one office. `sync(items, now, L, still)`: each new traffic item to someone drawn here from someone
|
|
254
|
+
* who is not becomes a letter (none with reduced motion, nor for a stale delegation; a zones office only).
|
|
255
|
+
* `frame(now, L, walkers)` → what to draw, { id, hue, kind, from, x, y, t } (t: its flight, 0 on the floor), picking
|
|
256
|
+
* up the letters whose person stands on them, and dropping those gone too long or whose person left. `targets(L)`:
|
|
257
|
+
* name → where to walk to pick one up (the first waiting for them; none while they are asleep). `prime(items)`: mark as
|
|
258
|
+
* seen what was in the air before this office was shown (a new office's or style's mail starts empty). `size()`.
|
|
259
|
+
*/
|
|
260
|
+
export function createMail({ max = MAIL.MAX } = {}) {
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
let letters = [];
|
|
263
|
+
const here = (L, name) => desksIn(L).find((d) => d.node.name === name) || null;
|
|
264
|
+
return {
|
|
265
|
+
sync(items, now, L, still = false) {
|
|
266
|
+
let started = 0;
|
|
267
|
+
for (const it of items || []) {
|
|
268
|
+
if (seen.has(it.id)) continue;
|
|
269
|
+
seen.add(it.id);
|
|
270
|
+
if (seen.size > 2000) seen.delete(seen.values().next().value);
|
|
271
|
+
if (still || it.phase === "stale" || !L || L.mode !== "zones" || !here(L, it.to) || here(L, it.from)) continue;
|
|
272
|
+
const drop = dropPoint(L, letters.filter((l) => l.to !== it.to).length % 4);
|
|
273
|
+
if (!drop) continue; // nowhere off the seats: not delivered here
|
|
274
|
+
const pts = [{ x: L.door.x, y: L.door.y }, drop];
|
|
275
|
+
letters.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now, drop, pts,
|
|
276
|
+
ms: Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(pts) / FLIGHT.SPEED)) });
|
|
277
|
+
started++;
|
|
278
|
+
}
|
|
279
|
+
if (letters.length > max) letters = letters.slice(letters.length - max);
|
|
280
|
+
return started;
|
|
281
|
+
},
|
|
282
|
+
frame(now, L, walkers) {
|
|
283
|
+
const out = [];
|
|
284
|
+
letters = letters.filter((l) => {
|
|
285
|
+
if (!here(L, l.to)) return false;
|
|
286
|
+
const t = (now - l.start) / l.ms;
|
|
287
|
+
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; }
|
|
288
|
+
if (now - l.start - l.ms > MAIL.MAX_WAIT) return false;
|
|
289
|
+
const w = walkers && walkers.at(l.to, now);
|
|
290
|
+
if (w && !w.walking && Math.hypot(w.x - l.drop.x, w.y - l.drop.y) < 4) return false; // picked up
|
|
291
|
+
out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: l.drop.x, y: l.drop.y, t: 0 });
|
|
292
|
+
return true;
|
|
293
|
+
});
|
|
294
|
+
return out;
|
|
295
|
+
},
|
|
296
|
+
targets(L, now) {
|
|
297
|
+
const out = new Map();
|
|
298
|
+
for (const l of letters) {
|
|
299
|
+
if (out.has(l.to) || now - l.start < l.ms) continue;
|
|
300
|
+
const d = here(L, l.to);
|
|
301
|
+
if (d && !MAIL.ASLEEP.has(d.node.state) && !d.night) out.set(l.to, l.drop); // asleep, or in bed for the night (#2568)
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
},
|
|
305
|
+
/** What is already in the traffic when this office's mail starts (#2564): seen, so it is not delivered again here. */
|
|
306
|
+
prime(items) { for (const it of items || []) seen.add(it.id); },
|
|
307
|
+
size: () => letters.length,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
106
311
|
/**
|
|
107
312
|
* 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
|
|
313
|
+
* someone whose target moved walks from where they are now along the graph (routeVia; each diagonal leg an L when the
|
|
314
|
+
* 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
315
|
* current step's { dx, dy }, which picks a pack's walkDown / walkUp / walkSide strip; `moving(now)`.
|
|
111
316
|
*/
|
|
112
317
|
export function createWalkers() {
|
|
@@ -125,7 +330,8 @@ export function createWalkers() {
|
|
|
125
330
|
const w = by.get(name);
|
|
126
331
|
if (!w || still) { if (!w || w.to.x !== to.x || w.to.y !== to.y) by.set(name, { pts: null, to }); continue; }
|
|
127
332
|
if (w.to.x === to.x && w.to.y === to.y) continue;
|
|
128
|
-
const here = posAt(w, now),
|
|
333
|
+
const here = posAt(w, now), via = routeVia(graph, { x: here.x, y: here.y }, { x: to.x, y: to.y });
|
|
334
|
+
const pts = graph.straight ? straighten(via) : via; // #2634: timed along the longer way
|
|
129
335
|
by.set(name, { pts, start: now, ms: Math.max(1, pathLength(pts) / WALK.SPEED), to });
|
|
130
336
|
}
|
|
131
337
|
},
|
|
@@ -232,8 +438,13 @@ export function linesFor(state, lang, pack, defaults) {
|
|
|
232
438
|
// ── Furniture ──
|
|
233
439
|
|
|
234
440
|
/** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
|
|
235
|
-
export function activeFurniture(pack, states) {
|
|
441
|
+
export function activeFurniture(pack, states, byName = new Map()) {
|
|
236
442
|
const on = new Set();
|
|
237
|
-
for (const f of (pack && pack.furniture) || [])
|
|
443
|
+
for (const f of (pack && pack.furniture) || []) {
|
|
444
|
+
if (!f.activeWhen) continue;
|
|
445
|
+
// activeFor (#2570): only those people's states count; else anyone's shown.
|
|
446
|
+
const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
|
|
447
|
+
if (hit) on.add(f.id);
|
|
448
|
+
}
|
|
238
449
|
return on;
|
|
239
450
|
}
|