@songsid/agend 2.3.2-beta.14 → 2.3.2-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/codex-session-lookup.d.ts +15 -0
- package/dist/backend/codex-session-lookup.js +58 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +15 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +5 -0
- package/dist/backend/kiro-classic-archive.js +132 -7
- package/dist/backend/kiro-classic-archive.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +4 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-reconcile-store.d.ts +27 -0
- package/dist/backend/kiro-reconcile-store.js +344 -0
- package/dist/backend/kiro-reconcile-store.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +12 -3
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +11 -0
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/cache-service.d.ts +13 -1
- package/dist/cache-service.js +45 -2
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +17 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +4 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/fleet-manager.d.ts +23 -1
- package/dist/fleet-manager.js +127 -9
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-instance.d.ts +24 -0
- package/dist/general-instance.js +53 -0
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +16 -9
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +16 -9
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +80 -0
- package/dist/kiro-reconcile.js.map +1 -0
- package/dist/kiro-tui-forensic.d.ts +7 -0
- package/dist/kiro-tui-forensic.js +17 -3
- package/dist/kiro-tui-forensic.js.map +1 -1
- package/dist/locale.js +20 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/office-levels.d.ts +64 -0
- package/dist/office-levels.js +66 -0
- package/dist/office-levels.js.map +1 -0
- package/dist/office-share-page.d.ts +3 -1
- package/dist/office-share-page.js +8 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +4 -3
- package/dist/office-share-service.js +6 -6
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +35 -8
- package/dist/office-share.js +50 -15
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +20 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +6 -0
- package/dist/outbound-schemas.js +6 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +25 -1
- package/dist/quickstart-api.js +53 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +28 -6
- package/dist/scheduler/task-board.js +66 -11
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/service-installer.d.ts +13 -0
- package/dist/service-installer.js +15 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +25 -0
- package/dist/service-owner.js +43 -2
- package/dist/service-owner.js.map +1 -1
- package/dist/settings-api.js +4 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +2 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/setup-form.js +17 -3
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +2 -0
- package/dist/setup-i18n.js.map +1 -1
- package/dist/task-board-waker.js +7 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +1 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/ui/fleet-office-card.js +114 -0
- package/dist/ui/fleet-office-editor.js +27 -7
- package/dist/ui/fleet-office-layout.js +14 -6
- package/dist/ui/fleet-office-levels.js +105 -0
- package/dist/ui/fleet-office-model.js +9 -1
- package/dist/ui/fleet-office-pack.d.ts +6 -0
- package/dist/ui/fleet-office-pack.js +62 -6
- package/dist/ui/fleet-office-skin.js +39 -3
- package/dist/ui/fleet-office-store.js +31 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +10 -2
- package/dist/ui/fleet-office.js +208 -30
- package/dist/ui/fleet-org.js +2 -0
- package/dist/ui/fleet-task-card.js +2 -2
- package/dist/ui/fleet-task-common.js +30 -7
- package/dist/ui/fleet-task-work.js +2 -2
- package/dist/ui/fleet-tasks.js +10 -11
- package/dist/ui/office-share-dialog.js +31 -7
- package/dist/ui/office-share-page.js +1 -0
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-settings.js +7 -1
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +4 -0
- package/dist/ui/settings-wizard.js +15 -4
- package/dist/ui/shared/app.css +50 -0
- package/dist/ui/shared/setup-host-text.js +2 -0
- package/dist/usage-rollup.d.ts +34 -3
- package/dist/usage-rollup.js +56 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +5 -0
- package/dist/web-api.js +116 -14
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-office-acts.d.ts +5 -2
- package/dist/web-office-acts.js +7 -2
- package/dist/web-office-acts.js.map +1 -1
- package/dist/web-office-pack-tools.d.ts +10 -1
- package/dist/web-office-pack-tools.js +36 -5
- package/dist/web-office-pack-tools.js.map +1 -1
- package/dist/web-office-skins.js +6 -0
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +28 -4
- package/dist/web-office-store.js +49 -8
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
// seats' figures, #2558), a moon, and a thin line to its day place. One that sleeps in place shows it on its day place
|
|
9
9
|
// (and it follows the item); dragged away, the item has its own night place; its Reset puts back the pack's.
|
|
10
10
|
//
|
|
11
|
+
// #2736: the mail drop — where letters from other offices land — is a handle too (an envelope). On a pack without
|
|
12
|
+
// one it stands, dashed, where letters land today (fleet-office-zones.js dropPoint); dragged, the office has its own.
|
|
13
|
+
//
|
|
11
14
|
// #2691: a selected seat lists the office's members under "Reserved for" (several may be picked) and "Lend when
|
|
12
15
|
// away": whoever it is for takes it first, and nobody else does unless it is lent while they are all elsewhere
|
|
13
16
|
// (fleet-office-zones.js assignSeats). Saved as the overrides' `seatFor` by area and slot; a reserved seat's handle
|
|
@@ -22,13 +25,14 @@ import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "/as
|
|
|
22
25
|
import { t, register } from "/assets/app-i18n.js";
|
|
23
26
|
import { SidePanel } from "/assets/side-panel.js";
|
|
24
27
|
import { LOGICAL, overridesBetween, own } from "./fleet-office-pack.js";
|
|
28
|
+
import { dropPoint } from "./fleet-office-zones.js";
|
|
25
29
|
|
|
26
30
|
register("orgl", {
|
|
27
31
|
en: {
|
|
28
32
|
edit: "Edit layout", title: "Edit layout", coords: "Show coordinates", addWaypoint: "Add waypoint", removeWaypoint: "Remove waypoint",
|
|
29
|
-
joinTo: "Join to", join: "Join", unjoin: "Unjoin {0}", corridors: "Corridors", none: "Nothing selected: pick a seat, an item, the door or a waypoint.",
|
|
33
|
+
joinTo: "Join to", join: "Join", unjoin: "Unjoin {0}", corridors: "Corridors", none: "Nothing selected: pick a seat, an item, the door, the mail drop or a waypoint.",
|
|
30
34
|
reset: "Reset to the pack's layout", save: "Save layout", cancel: "Cancel", saved: "Layout saved.", hint: "Drag, or use the arrow keys (Shift: by 10).",
|
|
31
|
-
seat: "Seat {0} in {1}", furniture: "Item {0}", night: "Item {0} (night)", door: "Door", waypoint: "Waypoint {0}", at: "{0} at {1}, {2}",
|
|
35
|
+
seat: "Seat {0} in {1}", furniture: "Item {0}", night: "Item {0} (night)", door: "Door", mail: "Mail drop", mailAuto: "Mail drop (where letters land now; drag to place it)", waypoint: "Waypoint {0}", at: "{0} at {1}, {2}",
|
|
32
36
|
nightReset: "Back to the pack's night place",
|
|
33
37
|
seatFor: "Seat {0} in {1}, reserved for {2}", reservedFor: "Reserved for", lend: "Lend when away", nobody: "No members to pick.",
|
|
34
38
|
changed: "Someone changed the offices meanwhile. Your layout is kept: Save again puts it on the current list.",
|
|
@@ -37,9 +41,9 @@ register("orgl", {
|
|
|
37
41
|
},
|
|
38
42
|
"zh-TW": {
|
|
39
43
|
edit: "編輯配置", title: "編輯配置", coords: "顯示座標", addWaypoint: "新增路徑點", removeWaypoint: "移除路徑點",
|
|
40
|
-
joinTo: "連到", join: "連接", unjoin: "取消連接 {0}", corridors: "走道", none: "
|
|
44
|
+
joinTo: "連到", join: "連接", unjoin: "取消連接 {0}", corridors: "走道", none: "尚未選取:請選座位、家具、門、信件落點或路徑點。",
|
|
41
45
|
reset: "還原成 pack 的配置", save: "儲存配置", cancel: "取消", saved: "配置已儲存。", hint: "拖曳,或用方向鍵移動(Shift:一次 10)。",
|
|
42
|
-
seat: "{1} 的座位 {0}", furniture: "家具 {0}", night: "家具 {0}(夜間)", door: "門", waypoint: "路徑點 {0}", at: "{0},位置 {1}, {2}",
|
|
46
|
+
seat: "{1} 的座位 {0}", furniture: "家具 {0}", night: "家具 {0}(夜間)", door: "門", mail: "信件落點", mailAuto: "信件落點(目前信件落下的位置;拖曳即可指定)", waypoint: "路徑點 {0}", at: "{0},位置 {1}, {2}",
|
|
43
47
|
nightReset: "回到 pack 的夜間位置",
|
|
44
48
|
seatFor: "{1} 的座位 {0},保留給 {2}", reservedFor: "保留給", lend: "不在時借給別人", nobody: "沒有可選的成員。",
|
|
45
49
|
changed: "有人在這期間修改了辦公室。你的配置已保留:再按一次儲存,會套用到目前的清單。",
|
|
@@ -57,7 +61,7 @@ const nightAt = (f) => (f.night.x !== null && f.night.x !== undefined && f.night
|
|
|
57
61
|
/**
|
|
58
62
|
* Every handle on a pack: { key, kind, label, x, y } — seats by area (#2691: `for` and `lend` when reserved),
|
|
59
63
|
* furniture, each item's night place (#2665: an item with a `night`; `day` its day place, `inPlace` while it sleeps
|
|
60
|
-
* there), the door, the waypoints.
|
|
64
|
+
* there), the door, the mail drop (#2736: `auto` where the pack has none — where letters land now), the waypoints.
|
|
61
65
|
*/
|
|
62
66
|
export function handlesOf(pack) {
|
|
63
67
|
const out = [];
|
|
@@ -71,6 +75,8 @@ export function handlesOf(pack) {
|
|
|
71
75
|
out.push({ key: `night:${f.id}`, kind: "night", label: tl("night", f.id), x: n.x, y: n.y, day: { x: f.x, y: f.y }, inPlace: !n.own });
|
|
72
76
|
}
|
|
73
77
|
out.push({ key: "door", kind: "door", label: tl("door"), x: pack.door.x, y: pack.door.y });
|
|
78
|
+
const md = pack.mailDrop || dropPoint({ areas: pack.areas, waypoints: pack.waypoints, width: LOGICAL.w, height: LOGICAL.h });
|
|
79
|
+
if (md) out.push({ key: "mail", kind: "mail", label: tl(pack.mailDrop ? "mail" : "mailAuto"), x: md.x, y: md.y, ...(pack.mailDrop ? {} : { auto: true }) });
|
|
74
80
|
for (const w of pack.waypoints) out.push({ key: `wp:${w.id}`, kind: "waypoint", label: tl("waypoint", w.id), x: w.x, y: w.y });
|
|
75
81
|
return out;
|
|
76
82
|
}
|
|
@@ -87,6 +93,7 @@ export function moveHandle(pack, key, x, y) {
|
|
|
87
93
|
if (kind === "furn") return { ...pack, furniture: pack.furniture.map((f) => (f.id === a ? { ...f, ...inCanvas() } : f)) };
|
|
88
94
|
if (kind === "night") return { ...pack, furniture: pack.furniture.map((f) => (f.id === a && f.night ? { ...f, night: { ...f.night, ...inCanvas() } } : f)) };
|
|
89
95
|
if (kind === "door") return { ...pack, door: inCanvas() };
|
|
96
|
+
if (kind === "mail") return { ...pack, mailDrop: inCanvas() };
|
|
90
97
|
if (kind === "wp") return { ...pack, waypoints: pack.waypoints.map((w) => (w.id === a ? { ...w, ...inCanvas() } : w)) };
|
|
91
98
|
return pack;
|
|
92
99
|
}
|
|
@@ -255,10 +262,11 @@ export function LayoutEditor({ base, start, place = null, stale = "", dock = tru
|
|
|
255
262
|
${Object.values(edited.areas).map((a) => html`<rect key=${`a:${a.name}`} class="ol-area" x=${a.rect.x} y=${a.rect.y} width=${a.rect.w} height=${a.rect.h} />`)}
|
|
256
263
|
${edited.edges.map((e) => { const p = at.get(e[0]), q = at.get(e[1]); return p && q ? html`<line key=${`e:${edgeKey(e)}`} class="ol-edge" x1=${p.x} y1=${p.y} x2=${q.x} y2=${q.y} />` : null; })}
|
|
257
264
|
${handles.filter((h) => h.kind === "night" && !h.inPlace).map((h) => html`<line key=${`nl:${h.key}`} class="ol-night-line" x1=${h.day.x} y1=${h.day.y} x2=${h.x} y2=${h.y} />`)}
|
|
258
|
-
${[...handles.filter((h) => h.kind === "night"), ...handles.filter((h) => h.kind !== "night")].map((h) => html`<g key=${h.key} class=${`ol-handle ol-${h.kind}${h.for ? " reserved" : ""}${sel === h.key ? " on" : ""}`} data-handle=${h.key} tabindex="0" role="button"
|
|
265
|
+
${[...handles.filter((h) => h.kind === "night"), ...handles.filter((h) => h.kind !== "night")].map((h) => html`<g key=${h.key} class=${`ol-handle ol-${h.kind}${h.for ? " reserved" : ""}${h.auto ? " auto" : ""}${sel === h.key ? " on" : ""}`} data-handle=${h.key} tabindex="0" role="button"
|
|
259
266
|
aria-label=${tl("at", h.label, h.x, h.y)} aria-pressed=${sel === h.key ? "true" : "false"}
|
|
260
267
|
onPointerDown=${down(h.key)} onKeyDown=${keyOn(h)} onFocus=${() => setSel(h.key)}>
|
|
261
268
|
${h.kind === "door" ? html`<rect x=${h.x - 9} y=${h.y - 9} width="18" height="18" transform=${`rotate(45 ${h.x} ${h.y})`} />`
|
|
269
|
+
: h.kind === "mail" ? html`<rect x=${h.x - 10} y=${h.y - 7} width="20" height="14" /><path class="ol-flap" d=${`M${h.x - 10} ${h.y - 7}L${h.x} ${h.y + 1}L${h.x + 10} ${h.y - 7}`} />`
|
|
262
270
|
: h.kind === "furniture" ? html`<rect x=${h.x - 8} y=${h.y - 8} width="16" height="16" />`
|
|
263
271
|
: h.kind === "night" ? html`<circle cx=${h.x} cy=${h.y} r=${h.inPlace ? 15 : 8} /><path class="ol-moon" d=${h.inPlace ? moonPath(h.x + 10, h.y - 10) : moonPath(h.x, h.y)} />`
|
|
264
272
|
: h.kind === "seat" && h.for ? html`<circle class="ol-for-ring" cx=${h.x} cy=${h.y} r="13" /><circle cx=${h.x} cy=${h.y} r="9" />`
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// #2661: the Office levels on the page — what the host serves (GET /ui/office-levels, src/office-levels.ts: the level from
|
|
2
|
+
// each instance's lifetime work, and where the viewer may see them, the numbers behind it) put into words and colours:
|
|
3
|
+
// the tier a level is in, its title (the page's generic ones, or a skin pack's `levels.titles`), a title a pack pins on
|
|
4
|
+
// someone (`levels.fixed`, which may hide the number), the badge's text and the XP bar. Pure: no DOM, no fetch.
|
|
5
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
6
|
+
|
|
7
|
+
register("olv", {
|
|
8
|
+
en: { t0: "Novice", t1: "Apprentice", t2: "Adept", t3: "Expert", t4: "Master", t5: "Legend" },
|
|
9
|
+
"zh-TW": { t0: "新手", t1: "學徒", t2: "熟手", t3: "專家", t4: "大師", t5: "傳奇" },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/** The tiers by the level each starts at: grey, green, blue, the filled blue, gold, red (#2661). */
|
|
13
|
+
export const TIER_FROM = [0, 10, 20, 35, 50, 100];
|
|
14
|
+
/** The tier (0–5) a level is in. */
|
|
15
|
+
export const tierOf = (level) => { let i = 0; while (i + 1 < TIER_FROM.length && level >= TIER_FROM[i + 1]) i++; return i; };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A tier's badge colours from the dashboard's tokens (no palette of its own: the status tokens and their tints, as the
|
|
19
|
+
* renderer reads them, readPal). { bg, fg, line }.
|
|
20
|
+
*/
|
|
21
|
+
export function tierColours(tier, pal) {
|
|
22
|
+
switch (tier) {
|
|
23
|
+
case 1: return { bg: pal.okBg, fg: pal.ok, line: pal.ok };
|
|
24
|
+
case 2: return { bg: pal.accentDim, fg: pal.accent, line: pal.accent };
|
|
25
|
+
case 3: return { bg: pal.accent, fg: pal.surface, line: pal.accent };
|
|
26
|
+
case 4: return { bg: pal.warnBg, fg: pal.warn, line: pal.warn };
|
|
27
|
+
case 5: return { bg: pal.dangerBg, fg: pal.danger, line: pal.danger };
|
|
28
|
+
default: return { bg: pal.surface2, fg: pal.text2, line: pal.border };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A level as the badge writes it: up to 999 whole, then 1.1k, 12k (never wider than four characters). */
|
|
33
|
+
export function badgeText(level) {
|
|
34
|
+
if (level < 1000) return String(level);
|
|
35
|
+
const k = level / 1000;
|
|
36
|
+
return k < 10 ? `${Math.floor(k * 10) / 10}k` : `${Math.floor(k)}k`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The title for a level: the pack's table (the last entry it has reached), else the page's tier name. */
|
|
40
|
+
export function titleOf(level, titles) {
|
|
41
|
+
if (Array.isArray(titles) && titles.length) {
|
|
42
|
+
let name = null;
|
|
43
|
+
for (const e of titles) if (level >= e.from) name = e.name;
|
|
44
|
+
if (name) return name;
|
|
45
|
+
}
|
|
46
|
+
return t(`olv.t${tierOf(level)}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What a pack pins on `name` (an instance or an NPC's id): { title, hideLevel } or null. */
|
|
50
|
+
export function fixedOf(name, levels) {
|
|
51
|
+
const f = levels && levels.fixed;
|
|
52
|
+
return f && Object.prototype.hasOwnProperty.call(f, name) ? f[name] : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Work tokens to reach a level (the host's curve: level = floor(√(work ÷ unit))). */
|
|
56
|
+
export const workFor = (level, unit) => level * level * unit;
|
|
57
|
+
|
|
58
|
+
/** The XP bar: { next, toNext, pct (0–1) } — the next level, the work it still needs, how far along this level is. */
|
|
59
|
+
export function xpOf(level, work, unit) {
|
|
60
|
+
const from = workFor(level, unit), to = workFor(level + 1, unit);
|
|
61
|
+
return { next: level + 1, toNext: Math.max(0, to - work), pct: to > from ? Math.min(1, Math.max(0, (work - from) / (to - from))) : 0 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Tokens in short words: 950, 12.3K, 260M, 1.2B. */
|
|
65
|
+
export function tokens(n) {
|
|
66
|
+
const v = Math.max(0, n);
|
|
67
|
+
if (v < 1000) return String(Math.round(v));
|
|
68
|
+
const [d, s] = v >= 1e9 ? [1e9, "B"] : v >= 1e6 ? [1e6, "M"] : [1e3, "K"];
|
|
69
|
+
const x = v / d;
|
|
70
|
+
return `${x < 100 ? Math.floor(x * 10) / 10 : Math.floor(x)}${s}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What the host answered (GET /ui/office-levels), checked: { enabled, unit, detail, levels: name → entry } with every
|
|
75
|
+
* entry's numbers sane (anything odd: no levels at all).
|
|
76
|
+
*/
|
|
77
|
+
export function levelsOk(v) {
|
|
78
|
+
const none = { enabled: false, unit: 100000, detail: false, levels: new Map() };
|
|
79
|
+
if (!v || typeof v !== "object" || v.enabled !== true || typeof v.levels !== "object" || !v.levels) return none;
|
|
80
|
+
const unit = Number.isFinite(v.unit) && v.unit > 0 ? v.unit : 100000;
|
|
81
|
+
const n = (x) => Number.isFinite(x) && x >= 0;
|
|
82
|
+
const out = new Map();
|
|
83
|
+
for (const [name, e] of Object.entries(v.levels)) {
|
|
84
|
+
if (!e || !Number.isInteger(e.level) || e.level < 0) continue;
|
|
85
|
+
const detail = v.detail === true && n(e.workTokens) && e.today && n(e.today.workTokens) && n(e.today.requests);
|
|
86
|
+
out.set(name, detail
|
|
87
|
+
? { level: e.level, workTokens: e.workTokens, estimated: e.estimated === true, today: { workTokens: e.today.workTokens, requests: e.today.requests },
|
|
88
|
+
firstSeen: typeof e.firstSeen === "string" && /^\d{4}-\d{2}-\d{2}$/.test(e.firstSeen) ? e.firstSeen : null }
|
|
89
|
+
: { level: e.level });
|
|
90
|
+
}
|
|
91
|
+
return { enabled: true, unit, detail: v.detail === true, levels: out };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The badge a person's name tag carries: { text, tier } — or { mark: true } for a pinned title that hides the level —
|
|
96
|
+
* or null (levels off, or none for them).
|
|
97
|
+
*/
|
|
98
|
+
export function badgeOf(name, lv, levels) {
|
|
99
|
+
if (!lv || !lv.enabled) return null;
|
|
100
|
+
const fx = fixedOf(name, levels);
|
|
101
|
+
if (fx && fx.hideLevel) return { mark: true, tier: 0 };
|
|
102
|
+
const e = lv && lv.levels.get(name);
|
|
103
|
+
if (!e) return null;
|
|
104
|
+
return { text: badgeText(e.level), tier: tierOf(e.level) };
|
|
105
|
+
}
|
|
@@ -74,13 +74,21 @@ export function officeRooms(view, filter) {
|
|
|
74
74
|
return picked.map((r) => ({ ...r, nodes: r.nodes.filter((n) => pinned.has(n.name)) })).filter((r) => r.nodes.length);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* #2752: an office kept for nobody (its style's NPCs only) stores this one member: a team with no name, which no team
|
|
79
|
+
* has, so a version before it finds nobody too. `[]` still means everyone.
|
|
80
|
+
*/
|
|
81
|
+
export const NOBODY = "team:";
|
|
82
|
+
/** Who an office's members pick: "everyone" ([]), "nobody" (only NOBODY), or "picked". */
|
|
83
|
+
export const membersMode = (members) => (!members.length ? "everyone" : members.every((m) => m === NOBODY) ? "nobody" : "picked");
|
|
84
|
+
/** A stored office's members: instance names, or `team:<name>` for a team's; none means everyone, NOBODY nobody. */
|
|
78
85
|
export function membersOf(office, view) {
|
|
79
86
|
const every = [...view.top, ...view.teams.flatMap((tm) => tm.nodes), ...view.rest.nodes];
|
|
80
87
|
const byName = new Map(every.map((n) => [n.name, n]));
|
|
81
88
|
if (!office.members.length) return [...byName.values()];
|
|
82
89
|
const out = new Map();
|
|
83
90
|
for (const m of office.members) {
|
|
91
|
+
if (m === NOBODY) continue;
|
|
84
92
|
const team = m.startsWith("team:") ? view.teams.find((tm) => tm.name === m.slice(5)) : null;
|
|
85
93
|
for (const n of team ? team.nodes : byName.has(m) ? [byName.get(m)] : []) if (!out.has(n.name)) out.set(n.name, n);
|
|
86
94
|
}
|
|
@@ -12,10 +12,14 @@ export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: nu
|
|
|
12
12
|
export interface CheckedPack {
|
|
13
13
|
format: 1; id: string; scale: number; background?: string; foreground?: string; matte?: string;
|
|
14
14
|
areas: Record<string, PackArea>; stateZones: Record<string, string> | null; door: PackPoint;
|
|
15
|
+
/** #2736: where a letter from another office lands, or null (the waypoint nearest the middle). */
|
|
16
|
+
mailDrop: PackPoint | null;
|
|
15
17
|
waypoints: Array<{ id: string; x: number; y: number }>; edges: Array<[string, string]>;
|
|
16
18
|
furniture: PackFurniture[]; characters: Record<string, Record<string, PackStrip>>; byInstance: Record<string, string>;
|
|
17
19
|
props: Record<string, PackStrip>; bubbles: Record<string, Record<string, string[]>>;
|
|
18
20
|
room: { floor?: string; wall?: string }; desk: { back?: string; front?: string; lit?: string };
|
|
21
|
+
/** #2661: the Office levels' words — titles from each level up, and titles pinned on instances or NPCs. */
|
|
22
|
+
levels?: { titles?: Array<{ from: number; name: string }>; fixed?: Record<string, { title: string; hideLevel: boolean }> };
|
|
19
23
|
}
|
|
20
24
|
export interface LayoutOverrides {
|
|
21
25
|
pack: string;
|
|
@@ -26,6 +30,8 @@ export interface LayoutOverrides {
|
|
|
26
30
|
/** #2691: a seat's reservation by area and slot ("0", "1", …), in place of the pack's; `for: []` is none. */
|
|
27
31
|
seatFor?: Record<string, Record<string, { for: string[]; lend: boolean }>>;
|
|
28
32
|
door?: [number, number];
|
|
33
|
+
/** #2736: where a letter from another office lands. */
|
|
34
|
+
mailDrop?: [number, number];
|
|
29
35
|
waypoints?: Array<{ id: string; x: number; y: number }>;
|
|
30
36
|
edges?: Array<[string, string]>;
|
|
31
37
|
}
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
// the org chart's states (fleet-org.js ORG_STATES) → an area; a
|
|
36
36
|
// state it does not name goes to "default", else the first area
|
|
37
37
|
// "door": [640, 700], optional: where someone new walks in from (default: bottom middle)
|
|
38
|
+
// "mailDrop": [640, 420], optional (#2736): where a letter from another office lands (more
|
|
39
|
+
// waiting step GAP px right of it, past any seat); default: the
|
|
40
|
+
// waypoint nearest the middle off every seat (fleet-office-zones.js
|
|
41
|
+
// dropPoint)
|
|
38
42
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …],
|
|
39
43
|
// optional: the walkable graph (else people walk straight)
|
|
40
44
|
// "straight": true, optional (#2634): every walk goes across, then up or down — each
|
|
@@ -87,6 +91,13 @@
|
|
|
87
91
|
// ≤ 40 characters each), in place of the page's own
|
|
88
92
|
// "room": { "floor": "floor.png", "wall": "wall.png" }, "desk": { "back": …, "front": …, "lit": … }
|
|
89
93
|
// optional: for the desks layout (a pack without stateZones)
|
|
94
|
+
// "levels": { optional (#2661): the Office levels' words
|
|
95
|
+
// "titles": [{ "from": 1, "name": "Intern" }, …], the title from each level up (≤ 32, `from` 0–100000
|
|
96
|
+
// rising, a name 1–24 characters), in place of the page's own
|
|
97
|
+
// "fixed": { "atlas": { "title": "Founder", "hideLevel": true } } }
|
|
98
|
+
// a title pinned on an instance (or an NPC: a furniture item's
|
|
99
|
+
// id) whatever its level (≤ 500); hideLevel: its badge is a mark,
|
|
100
|
+
// its card has no "Lv n" or XP bar
|
|
90
101
|
// }
|
|
91
102
|
//
|
|
92
103
|
// A STRIP is { "src": "idle.png", "frames": 4, "w": 32, "h": 48, "fps": 4, "ax": 16, "ay": 44 }: `frames` frames of
|
|
@@ -120,7 +131,7 @@
|
|
|
120
131
|
// "seatFor": { "desks": { "0": { "for": ["atlas"], "lend": false } } },
|
|
121
132
|
// a seat's reservation by area and slot (#2691), in place of the
|
|
122
133
|
// pack's own for that seat; "for": [] is none (lends nothing)
|
|
123
|
-
// "door": [640, 700],
|
|
134
|
+
// "door": [640, 700], "mailDrop": [640, 420], (#2736: kept beside the offices on disk, as furnitureNight)
|
|
124
135
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
|
|
125
136
|
// the walkable graph, replaced whole (edges need waypoints)
|
|
126
137
|
//
|
|
@@ -150,7 +161,7 @@ const RESERVED = new Set(["cozy"]);
|
|
|
150
161
|
const ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
151
162
|
const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
152
163
|
const HEX = /^#[0-9a-f]{6}$/i;
|
|
153
|
-
const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40, seatFor: 16 };
|
|
164
|
+
const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40, seatFor: 16, titles: 32, title: 24, fixed: 500 };
|
|
154
165
|
|
|
155
166
|
class Refused extends Error { constructor(at, why) { super(`${at}: ${why}`); } }
|
|
156
167
|
/** `o[k]` when it is o's own property, else undefined: a name from a pack or a page never reaches Object.prototype. */
|
|
@@ -239,6 +250,43 @@ function states(v, at) {
|
|
|
239
250
|
return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
|
|
240
251
|
}
|
|
241
252
|
|
|
253
|
+
/** #2661: a title's words — 1–LIMIT.title characters on one line. */
|
|
254
|
+
function titleText(v, at) {
|
|
255
|
+
if (typeof v !== "string" || !v.trim() || [...v.trim()].length > LIMIT.title || /[\u0000-\u001f\u007f]/.test(v)) throw new Refused(at, `1–${LIMIT.title} characters`);
|
|
256
|
+
return v.trim();
|
|
257
|
+
}
|
|
258
|
+
/** #2661: a pack's `levels` (titles, fixed), checked; null when it has none. */
|
|
259
|
+
function levelsOf(v) {
|
|
260
|
+
if (v === undefined) return null;
|
|
261
|
+
obj(v, "levels");
|
|
262
|
+
keys(v, ["titles", "fixed"], "levels");
|
|
263
|
+
const out = {};
|
|
264
|
+
if (v.titles !== undefined) {
|
|
265
|
+
let last = -1;
|
|
266
|
+
out.titles = list(v.titles, LIMIT.titles, "levels.titles").map((t, i) => {
|
|
267
|
+
const at = `levels.titles[${i}]`;
|
|
268
|
+
obj(t, at); keys(t, ["from", "name"], at);
|
|
269
|
+
const from = int(t.from, 0, 100000, `${at}.from`);
|
|
270
|
+
if (from <= last) throw new Refused(`${at}.from`, "higher than the title before it");
|
|
271
|
+
last = from;
|
|
272
|
+
return { from, name: titleText(t.name, `${at}.name`) };
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (v.fixed !== undefined) {
|
|
276
|
+
obj(v.fixed, "levels.fixed");
|
|
277
|
+
if (Object.keys(v.fixed).length > LIMIT.fixed) throw new Refused("levels.fixed", `at most ${LIMIT.fixed}`);
|
|
278
|
+
out.fixed = {};
|
|
279
|
+
for (const [k, f] of Object.entries(v.fixed)) {
|
|
280
|
+
const at = `levels.fixed.${k}`;
|
|
281
|
+
if (inherited(k)) throw new Refused(at, "an instance's name (not an inherited one)");
|
|
282
|
+
obj(f, at); keys(f, ["title", "hideLevel"], at);
|
|
283
|
+
if (f.hideLevel !== undefined && typeof f.hideLevel !== "boolean") throw new Refused(`${at}.hideLevel`, "true or false");
|
|
284
|
+
out.fixed[k] = { title: titleText(f.title, `${at}.title`), hideLevel: f.hideLevel === true };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
242
290
|
/**
|
|
243
291
|
* A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. `kept`: a pack the host
|
|
244
292
|
* keeps already (read back, not uploaded), which may hold a RESERVED id.
|
|
@@ -246,7 +294,7 @@ function states(v, at) {
|
|
|
246
294
|
export function checkSkinPack(x, { kept = false } = {}) {
|
|
247
295
|
try {
|
|
248
296
|
obj(x, "pack");
|
|
249
|
-
keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
|
|
297
|
+
keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "mailDrop", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk", "levels"], "pack");
|
|
250
298
|
if (x.format !== 1) throw new Refused("format", "1");
|
|
251
299
|
if (typeof x.id !== "string" || !ID.test(x.id)) throw new Refused("id", "1–40 letters, digits, _ or -");
|
|
252
300
|
if (BUILT_IN.has(x.id) || (!kept && RESERVED.has(x.id))) throw new Refused("id", "a built-in skin's id");
|
|
@@ -344,14 +392,17 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
344
392
|
});
|
|
345
393
|
}
|
|
346
394
|
}
|
|
395
|
+
const levels = levelsOf(x.levels);
|
|
347
396
|
const room = x.room === undefined ? {} : obj(x.room, "room"), desk = x.desk === undefined ? {} : obj(x.desk, "desk");
|
|
348
397
|
keys(room, ["floor", "wall"], "room"); keys(desk, ["back", "front", "lit"], "desk");
|
|
349
398
|
const opt = (v, at) => (v === undefined ? undefined : src(v, at));
|
|
350
399
|
return { ok: true, pack: {
|
|
351
400
|
format: 1, id: x.id, scale, background: opt(x.background, "background"), foreground: opt(x.foreground, "foreground"), matte: x.matte === undefined ? undefined : x.matte.toLowerCase(), areas, stateZones,
|
|
352
|
-
door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"),
|
|
401
|
+
door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"),
|
|
402
|
+
mailDrop: x.mailDrop === undefined ? null : point(x.mailDrop, "mailDrop"), waypoints, edges, straight: x.straight === true, furniture, characters, byInstance,
|
|
353
403
|
props: x.props === undefined ? {} : strips(x.props, PROPS, "props", scale), bubbles,
|
|
354
404
|
room: { floor: opt(room.floor, "room.floor"), wall: opt(room.wall, "room.wall") }, desk: { back: opt(desk.back, "desk.back"), front: opt(desk.front, "desk.front"), lit: opt(desk.lit, "desk.lit") },
|
|
405
|
+
...(levels ? { levels } : {}),
|
|
355
406
|
} };
|
|
356
407
|
} catch (err) {
|
|
357
408
|
if (err instanceof Refused) return { ok: false, error: err.message };
|
|
@@ -392,7 +443,7 @@ const reserve = (s, r) => { const { for: _f, lend: _l, ...p } = s; return r && r
|
|
|
392
443
|
export function checkLayoutOverrides(x) {
|
|
393
444
|
try {
|
|
394
445
|
obj(x, "overrides");
|
|
395
|
-
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "waypoints", "edges"], "overrides");
|
|
446
|
+
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "mailDrop", "waypoints", "edges"], "overrides");
|
|
396
447
|
if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
|
|
397
448
|
const out = { pack: x.pack };
|
|
398
449
|
if (x.seats !== undefined) {
|
|
@@ -448,6 +499,7 @@ export function checkLayoutOverrides(x) {
|
|
|
448
499
|
}
|
|
449
500
|
}
|
|
450
501
|
if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
|
|
502
|
+
if (x.mailDrop !== undefined) { const q = point(x.mailDrop, "overrides.mailDrop"); out.mailDrop = [q.x, q.y]; }
|
|
451
503
|
if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
|
|
452
504
|
if (x.waypoints !== undefined) {
|
|
453
505
|
const ids = new Set();
|
|
@@ -508,8 +560,9 @@ export function applyOverrides(pack, ov) {
|
|
|
508
560
|
return { ...f, ...(q ? { x: q[0], y: q[1] } : {}), ...(n ? { night: { ...f.night, x: n[0], y: n[1] } } : {}) };
|
|
509
561
|
});
|
|
510
562
|
const door = ov.door ? { x: ov.door[0], y: ov.door[1] } : pack.door;
|
|
563
|
+
const mailDrop = ov.mailDrop ? { x: ov.mailDrop[0], y: ov.mailDrop[1] } : pack.mailDrop ?? null;
|
|
511
564
|
const graph = ov.waypoints ? { waypoints: ov.waypoints.map((w) => ({ ...w })), edges: (ov.edges || []).map((e) => [e[0], e[1]]) } : { waypoints: pack.waypoints, edges: pack.edges };
|
|
512
|
-
return { ok: true, pack: { ...pack, areas, furniture, door, ...graph } };
|
|
565
|
+
return { ok: true, pack: { ...pack, areas, furniture, door, mailDrop, ...graph } };
|
|
513
566
|
} catch (err) {
|
|
514
567
|
if (err instanceof Refused) return { ok: false, error: err.message };
|
|
515
568
|
throw err;
|
|
@@ -575,6 +628,9 @@ export function overridesBetween(pack, edited) {
|
|
|
575
628
|
if (o.night.x !== f.night.x || o.night.y !== f.night.y) (ov.furnitureNight ||= {})[f.id] = [f.night.x, f.night.y];
|
|
576
629
|
}
|
|
577
630
|
if (pack.door.x !== edited.door.x || pack.door.y !== edited.door.y) ov.door = [edited.door.x, edited.door.y];
|
|
631
|
+
// #2736: a mail drop placed (the pack has none) or moved from the pack's.
|
|
632
|
+
const md = pack.mailDrop ?? null, ed = edited.mailDrop ?? null;
|
|
633
|
+
if (ed && (!md || md.x !== ed.x || md.y !== ed.y)) ov.mailDrop = [ed.x, ed.y];
|
|
578
634
|
if (!same(pack.waypoints, edited.waypoints) || !same(pack.edges, edited.edges)) {
|
|
579
635
|
ov.waypoints = edited.waypoints.map((w) => ({ id: w.id, x: w.x, y: w.y }));
|
|
580
636
|
ov.edges = edited.edges.map((e) => [e[0], e[1]]);
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// @typedef {"working"|"idle"|"paused"|"needs"|"walk"} OfficePose (fleet-office-model.js POSES; "away" draws no one)
|
|
14
14
|
// @typedef {{ pal: OfficePalette, mode: "dark"|"light", labels: { lobby, closed, areas: { [area]: string } } }} OfficeEnv
|
|
15
15
|
// @typedef {{ name: string, label: string, state: string, pose: OfficePose|"away", light: "warn"|"danger"|null,
|
|
16
|
-
// lamp: boolean, sign: "closed"|null, coat: string, work: number, dim: boolean, match: boolean, free: boolean
|
|
17
|
-
//
|
|
16
|
+
// lamp: boolean, sign: "closed"|null, coat: string, work: number, dim: boolean, match: boolean, free: boolean,
|
|
17
|
+
// badge?: { text, tier } | { mark: true } }} OfficeLook
|
|
18
|
+
// (one occupant; `free`: standing in a zone, not behind a desk cell; `badge`: its level on its name tag, #2661)
|
|
18
19
|
// @typedef {object} OfficeSkin
|
|
19
20
|
// @property {string} id
|
|
20
21
|
// @property {object} [pack] a checked pack (fleet-office-pack.js); with `pack.stateZones` the zones layout is used
|
|
@@ -44,15 +45,18 @@
|
|
|
44
45
|
import { t } from "/assets/app-i18n.js";
|
|
45
46
|
import { frameAt, nameHash } from "./fleet-office-model.js";
|
|
46
47
|
import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox, walkStrip } from "./fleet-office-pack.js";
|
|
48
|
+
import { tierColours } from "./fleet-office-levels.js";
|
|
47
49
|
|
|
48
50
|
export { nameHash };
|
|
49
51
|
|
|
50
52
|
// ── The palette: the dashboard's tokens, read by the renderer; furniture keeps its own wood and metal ──
|
|
51
53
|
|
|
52
|
-
/** @typedef {{ bg, surface, surface2, border, text, text2, text3, ok, warn, danger, focus, fx: { task, send, report, broadcast } }} OfficePalette */
|
|
54
|
+
/** @typedef {{ bg, surface, surface2, border, text, text2, text3, ok, warn, danger, focus, accent, accentDim, okBg, warnBg, dangerBg, fx: { task, send, report, broadcast } }} OfficePalette */
|
|
53
55
|
export const DEFAULT_PAL = {
|
|
54
56
|
bg: "#212121", surface: "#2f2f2f", surface2: "#3a3a3a", border: "#3d3d3d", text: "#ececec", text2: "#b4b4b4", text3: "#a3a3a3",
|
|
55
57
|
ok: "#4cc38a", warn: "#e3b341", danger: "#ff8a80", focus: "#7ab7ff", fx: { task: "#b794f6", send: "#2dd4bf", report: "#f472b6", broadcast: "#fb923c" },
|
|
58
|
+
// #2661: the level badges' tiers (the status tokens and their tints).
|
|
59
|
+
accent: "#7ab7ff", accentDim: "rgba(122, 183, 255, 0.14)", okBg: "rgba(76, 195, 138, 0.14)", warnBg: "rgba(227, 179, 65, 0.14)", dangerBg: "rgba(255, 138, 128, 0.14)",
|
|
56
60
|
};
|
|
57
61
|
/** Light or dark, from the page's own background token (whatever chose it: the person, the system, a theme). */
|
|
58
62
|
export function modeOf(pal) {
|
|
@@ -235,6 +239,7 @@ function drawDeskFront(ctx, d, look, env) {
|
|
|
235
239
|
ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
|
|
236
240
|
}
|
|
237
241
|
ctx.restore();
|
|
242
|
+
if (!look.notag && look.badge) drawBadge(ctx, d.x + 6, d.y + 78, look.badge, pal);
|
|
238
243
|
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(); }
|
|
239
244
|
}
|
|
240
245
|
|
|
@@ -296,6 +301,36 @@ export function tagBox(ctx, x, y, label) {
|
|
|
296
301
|
ctx.restore();
|
|
297
302
|
return { x: Math.round(x - w / 2), y: y + 3, w, h: 14, text: t };
|
|
298
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* #2661: a level badge on a name tag's top-left corner (x, y: the tag's corner), overlapping its edge like an app's
|
|
306
|
+
* badge — it takes none of the tag's width. `badge`: { text, tier } (fleet-office-levels.js badgeOf), or { mark } for a
|
|
307
|
+
* pinned title that hides the number: a small diamond.
|
|
308
|
+
*/
|
|
309
|
+
export function drawBadge(ctx, x, y, badge, pal) {
|
|
310
|
+
if (!badge) return;
|
|
311
|
+
const c = tierColours(badge.tier, pal);
|
|
312
|
+
ctx.save();
|
|
313
|
+
ctx.globalAlpha = 1;
|
|
314
|
+
if (badge.mark) {
|
|
315
|
+
ctx.translate(x, y); ctx.rotate(Math.PI / 4);
|
|
316
|
+
ctx.fillStyle = pal.surface; ctx.fillRect(-4, -4, 8, 8);
|
|
317
|
+
ctx.fillStyle = c.bg; ctx.fillRect(-4, -4, 8, 8);
|
|
318
|
+
ctx.strokeStyle = c.line; ctx.lineWidth = 1; ctx.strokeRect(-3.5, -3.5, 7, 7);
|
|
319
|
+
ctx.restore();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
ctx.font = BADGE_FONT;
|
|
323
|
+
const w = Math.max(12, Math.ceil((ctx.measureText ? ctx.measureText(badge.text).width : badge.text.length * 5) + 6)), h = 11;
|
|
324
|
+
const bx = Math.round(x - w / 2 + 3), by = Math.round(y - h / 2 - 1);
|
|
325
|
+
// An opaque surface first, so a tint reads the same over any picture.
|
|
326
|
+
ctx.fillStyle = pal.surface; ctx.fillRect(bx, by, w, h);
|
|
327
|
+
ctx.fillStyle = c.bg; ctx.fillRect(bx, by, w, h);
|
|
328
|
+
ctx.strokeStyle = c.line; ctx.lineWidth = 1; ctx.strokeRect(bx + 0.5, by + 0.5, w - 1, h - 1);
|
|
329
|
+
ctx.fillStyle = c.fg; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(badge.text, bx + w / 2, by + h / 2 + 0.5);
|
|
330
|
+
ctx.restore();
|
|
331
|
+
}
|
|
332
|
+
const BADGE_FONT = "700 8px system-ui, sans-serif";
|
|
333
|
+
|
|
299
334
|
/** Zones: a name tag under someone's feet at (x, y). */
|
|
300
335
|
function drawTag(ctx, x, y, look, env) {
|
|
301
336
|
const { pal } = env;
|
|
@@ -307,6 +342,7 @@ function drawTag(ctx, x, y, look, env) {
|
|
|
307
342
|
ctx.strokeStyle = look.match ? pal.focus : pal.border; ctx.lineWidth = look.match ? 2 : 1; ctx.strokeRect(Math.round(x - w / 2) + 0.5, y + 3.5, w - 1, 13);
|
|
308
343
|
ctx.fillStyle = pal.text; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(t, x, y + 10.5);
|
|
309
344
|
ctx.restore();
|
|
345
|
+
if (look.badge) drawBadge(ctx, Math.round(x - w / 2), y + 3, look.badge, pal);
|
|
310
346
|
}
|
|
311
347
|
|
|
312
348
|
/**
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
// The Office view's data, behind one small seam (fleet-office.js uses only these calls):
|
|
2
2
|
// - the offices people made: the host's (GET / PUT /ui/offices, src/web-office-store.ts — one list for every viewer);
|
|
3
3
|
// what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
|
|
4
|
+
// - #2716: the whole-fleet office's style, kept on the host for everyone (GET /ui/offices's autoSkin; PUT
|
|
5
|
+
// /ui/office-style, an admin's) — this browser's own pick (skin(): an admin's copy of it, anyone else's own choice);
|
|
4
6
|
// - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
|
|
5
|
-
// office is shown, the auto office's member filter
|
|
7
|
+
// office is shown, the auto office's member filter; #2540: the view (zoom and centre) and the
|
|
6
8
|
// fit mode (Fit or Fill) per office; #2554: whether All offices shows the lobby tile.
|
|
7
9
|
// - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
|
|
8
10
|
// src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
|
|
9
11
|
// they read (a change in between answers 409).
|
|
12
|
+
// - #2661: everyone's level (GET /ui/office-levels, src/office-levels.ts; fleet-office-levels.js levelsOk checks it).
|
|
10
13
|
// - #2692: the agents' pack changes (GET / POST /ui/office-pack-changes, an admin's): which agents may change packs, the
|
|
11
14
|
// fleet's approval mode, the changes waiting for a yes, and the newest log entries.
|
|
12
15
|
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
13
16
|
import { fitModeOk, savedOk } from "./fleet-office-view.js";
|
|
17
|
+
import { levelsOk } from "./fleet-office-levels.js";
|
|
14
18
|
|
|
15
19
|
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
20
|
/** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
|
|
@@ -65,15 +69,26 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
65
69
|
const write = (k, v) => { try { const s = st(); if (s) s.setItem(k, v); } catch { /* this page only */ } };
|
|
66
70
|
const headers = { "Content-Type": "application/json" };
|
|
67
71
|
return {
|
|
68
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* { rev, offices, autoSkin } from the host (#2716 autoSkin: the whole-fleet office's style, or null: none kept); null
|
|
74
|
+
* when it could not be read (no offices shown but the auto one).
|
|
75
|
+
*/
|
|
69
76
|
async offices() {
|
|
70
77
|
try {
|
|
71
78
|
const r = await get("/ui/offices", { method: "GET", headers });
|
|
72
79
|
if (!r || !r.ok) return null;
|
|
73
80
|
const d = await r.json();
|
|
74
|
-
return { rev: Number.isSafeInteger(d && d.rev) ? d.rev : 0, offices: officesOk(d && d.offices) };
|
|
81
|
+
return { rev: Number.isSafeInteger(d && d.rev) ? d.rev : 0, offices: officesOk(d && d.offices), autoSkin: d && typeof d.autoSkin === "string" && PACK_ID.test(d.autoSkin) ? d.autoSkin : null };
|
|
75
82
|
} catch { return null; }
|
|
76
83
|
},
|
|
84
|
+
/** #2716: the whole-fleet office's style for everyone (an admin session): { ok, autoSkin } or { ok: false, status, error }. */
|
|
85
|
+
async saveAutoSkin(skin) {
|
|
86
|
+
try {
|
|
87
|
+
const r = await get("/ui/office-style", { method: "PUT", headers, body: JSON.stringify({ skin }) });
|
|
88
|
+
const d = await r.json().catch(() => ({}));
|
|
89
|
+
return r.ok ? { ok: true, autoSkin: typeof d.autoSkin === "string" ? d.autoSkin : null } : { ok: false, status: r.status, error: d.error || "office_style_failed" };
|
|
90
|
+
} catch { return { ok: false, status: 0, error: "office_style_failed" }; }
|
|
91
|
+
},
|
|
77
92
|
/**
|
|
78
93
|
* Replace the host's list (an admin session; `rev`: the one read — required: the host refuses a save without it,
|
|
79
94
|
* so two editors never overwrite each other unseen). { ok, rev, offices } or { ok: false, status, error }.
|
|
@@ -95,6 +110,17 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
95
110
|
return actsOk(d && d.acts, d && d.now);
|
|
96
111
|
} catch { return null; }
|
|
97
112
|
},
|
|
113
|
+
/**
|
|
114
|
+
* #2661: everyone's level (GET /ui/office-levels; a share link's own, through its lease), checked (levelsOk); null
|
|
115
|
+
* when it could not be read (the levels shown stay).
|
|
116
|
+
*/
|
|
117
|
+
async levels() {
|
|
118
|
+
try {
|
|
119
|
+
const r = await get("/ui/office-levels", { method: "GET", headers });
|
|
120
|
+
if (!r || !r.ok) return null;
|
|
121
|
+
return levelsOk(await r.json());
|
|
122
|
+
} catch { return null; }
|
|
123
|
+
},
|
|
98
124
|
/** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
|
|
99
125
|
async skins() {
|
|
100
126
|
try {
|
|
@@ -158,6 +184,8 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
158
184
|
saveFilter: (f) => write(FILTER_KEY, JSON.stringify(filterOk(f))),
|
|
159
185
|
/** 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
186
|
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "cozy"; },
|
|
187
|
+
/** #2716: this browser's own pick of it, or null (none made here). */
|
|
188
|
+
skinChosen: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : null; },
|
|
161
189
|
saveSkin: (id) => write(SKIN_KEY, String(id)),
|
|
162
190
|
/** Whether paused and stopped people are left out of the Office here (default: shown). */
|
|
163
191
|
hideAsleep: () => read(ASLEEP_KEY) === "1",
|
|
@@ -97,6 +97,14 @@ export function miniFrame(v, size, w, h) {
|
|
|
97
97
|
const x1 = Math.min(w, (size.w - v.x) / v.k), y1 = Math.min(h, (size.h - v.y) / v.k);
|
|
98
98
|
return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* #2737: the whole scene is in view (the overview's frame covers all of it, to half a unit), so the overview would only
|
|
102
|
+
* repeat the panel — and sit over the one corner of it a person cannot otherwise see.
|
|
103
|
+
*/
|
|
104
|
+
export function wholeInView(v, size, w, h) {
|
|
105
|
+
const f = miniFrame(v, size, w, h);
|
|
106
|
+
return f.x <= 0.5 && f.y <= 0.5 && f.x + f.width >= w - 0.5 && f.y + f.height >= h - 0.5;
|
|
107
|
+
}
|
|
100
108
|
|
|
101
109
|
// ── Kept per device, per office: the zoom and the logical point in the middle (the viewport's size may differ) ──
|
|
102
110
|
|
|
@@ -140,7 +140,7 @@ export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {
|
|
|
140
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) };
|
|
141
141
|
});
|
|
142
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,
|
|
143
|
-
waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, 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, mailDrop: pack.mailDrop || null, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// ── Walking ──
|
|
@@ -227,7 +227,9 @@ export function walkTargets(desks, graph, t, wander = true, furniture = [], nigh
|
|
|
227
227
|
*/
|
|
228
228
|
export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
|
|
229
229
|
/**
|
|
230
|
-
* Where the `n`th waiting letter lands: `
|
|
230
|
+
* Where the `n`th waiting letter lands. #2736: the layout's `mailDrop` when it has one — the `n`th of the slots GAP
|
|
231
|
+
* apart rightwards from it that no seat is on (never beyond the room's right edge); fewer than that, today's rule. Otherwise `n`
|
|
232
|
+
* GAPs along from the waypoint nearest the room's middle — the nearest whose
|
|
231
233
|
* spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
|
|
232
234
|
* with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
|
|
233
235
|
* those either, null — the letter is not delivered here (never on a seat).
|
|
@@ -236,6 +238,12 @@ export function dropPoint(L, n = 0) {
|
|
|
236
238
|
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
239
|
const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
|
|
238
240
|
const onSeat = (p) => seats.some((q) => d(p, q) < 4);
|
|
241
|
+
// #2751 review: the nth off-seat slot (slots GAP apart from the mailDrop), so two letters never share one.
|
|
242
|
+
const md = L.mailDrop;
|
|
243
|
+
if (md) for (let i = 0, free = 0; md.x + i * MAIL.GAP <= L.width; i++) {
|
|
244
|
+
const p = { x: Math.round(md.x + i * MAIL.GAP), y: Math.round(md.y) };
|
|
245
|
+
if (!onSeat(p) && free++ === n) return p;
|
|
246
|
+
}
|
|
239
247
|
const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
|
|
240
248
|
.filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
|
|
241
249
|
if (spots.length) return { x: spots[0].x, y: spots[0].y };
|