@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/kiro-identity.d.ts +64 -0
  15. package/dist/backend/kiro-identity.js +101 -0
  16. package/dist/backend/kiro-identity.js.map +1 -1
  17. package/dist/backend/kiro-retired-model.d.ts +62 -0
  18. package/dist/backend/kiro-retired-model.js +160 -0
  19. package/dist/backend/kiro-retired-model.js.map +1 -0
  20. package/dist/backend/kiro.d.ts +71 -2
  21. package/dist/backend/kiro.js +352 -11
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/types.d.ts +59 -0
  24. package/dist/backend/types.js.map +1 -1
  25. package/dist/backend-signin-guide.d.ts +1 -0
  26. package/dist/backend-signin-guide.js +9 -0
  27. package/dist/backend-signin-guide.js.map +1 -0
  28. package/dist/backend-signin-pending.d.ts +10 -0
  29. package/dist/backend-signin-pending.js +44 -0
  30. package/dist/backend-signin-pending.js.map +1 -0
  31. package/dist/browser-backend-signins.d.ts +56 -0
  32. package/dist/browser-backend-signins.js +189 -0
  33. package/dist/browser-backend-signins.js.map +1 -0
  34. package/dist/cache-ledger.d.ts +38 -2
  35. package/dist/cache-ledger.js +133 -8
  36. package/dist/cache-ledger.js.map +1 -1
  37. package/dist/cache-prices.d.ts +2 -1
  38. package/dist/cache-prices.js +5 -2
  39. package/dist/cache-prices.js.map +1 -1
  40. package/dist/cache-service.d.ts +68 -0
  41. package/dist/cache-service.js +355 -13
  42. package/dist/cache-service.js.map +1 -1
  43. package/dist/channel/adapters/discord.d.ts +11 -0
  44. package/dist/channel/adapters/discord.js +71 -14
  45. package/dist/channel/adapters/discord.js.map +1 -1
  46. package/dist/channel/mcp-tools.js +29 -2
  47. package/dist/channel/mcp-tools.js.map +1 -1
  48. package/dist/channel/tool-router.js +2 -1
  49. package/dist/channel/tool-router.js.map +1 -1
  50. package/dist/classic-channel-manager.d.ts +11 -0
  51. package/dist/classic-channel-manager.js +39 -1
  52. package/dist/classic-channel-manager.js.map +1 -1
  53. package/dist/cli.js +21 -2
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config-validator.d.ts +7 -1
  56. package/dist/config-validator.js +53 -10
  57. package/dist/config-validator.js.map +1 -1
  58. package/dist/config.d.ts +19 -0
  59. package/dist/config.js +31 -1
  60. package/dist/config.js.map +1 -1
  61. package/dist/cpu-quota.d.ts +23 -0
  62. package/dist/cpu-quota.js +86 -0
  63. package/dist/cpu-quota.js.map +1 -0
  64. package/dist/daemon.d.ts +23 -0
  65. package/dist/daemon.js +106 -7
  66. package/dist/daemon.js.map +1 -1
  67. package/dist/dashboard-permissions.d.ts +20 -6
  68. package/dist/dashboard-permissions.js +58 -9
  69. package/dist/dashboard-permissions.js.map +1 -1
  70. package/dist/doctor.js +2 -1
  71. package/dist/doctor.js.map +1 -1
  72. package/dist/fleet-manager.d.ts +72 -0
  73. package/dist/fleet-manager.js +507 -89
  74. package/dist/fleet-manager.js.map +1 -1
  75. package/dist/forged-envelope-scan.d.ts +9 -0
  76. package/dist/forged-envelope-scan.js +30 -5
  77. package/dist/forged-envelope-scan.js.map +1 -1
  78. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  79. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  80. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
  81. package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
  82. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +37 -9
  83. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +44 -11
  84. package/dist/instance-config-impact.js +10 -5
  85. package/dist/instance-config-impact.js.map +1 -1
  86. package/dist/instance-lifecycle.d.ts +19 -0
  87. package/dist/instance-lifecycle.js +55 -12
  88. package/dist/instance-lifecycle.js.map +1 -1
  89. package/dist/kiro-db-reader.d.ts +5 -0
  90. package/dist/kiro-db-reader.js +18 -0
  91. package/dist/kiro-db-reader.js.map +1 -1
  92. package/dist/kiro-engine-status.d.ts +2 -0
  93. package/dist/kiro-engine-status.js +9 -0
  94. package/dist/kiro-engine-status.js.map +1 -1
  95. package/dist/kiro-migrate.d.ts +49 -0
  96. package/dist/kiro-migrate.js +105 -0
  97. package/dist/kiro-migrate.js.map +1 -0
  98. package/dist/kiro-transcript-lane.d.ts +2 -0
  99. package/dist/kiro-transcript-lane.js.map +1 -1
  100. package/dist/kiro-transcript-worker.js +1 -0
  101. package/dist/kiro-transcript-worker.js.map +1 -1
  102. package/dist/kiro-tui-forensic.d.ts +7 -0
  103. package/dist/kiro-tui-forensic.js +110 -0
  104. package/dist/kiro-tui-forensic.js.map +1 -0
  105. package/dist/kiro-usage-run.d.ts +7 -0
  106. package/dist/kiro-usage-run.js +39 -0
  107. package/dist/kiro-usage-run.js.map +1 -0
  108. package/dist/kiro-usage-scan.d.ts +49 -0
  109. package/dist/kiro-usage-scan.js +223 -0
  110. package/dist/kiro-usage-scan.js.map +1 -0
  111. package/dist/kiro-usage-worker.d.ts +1 -0
  112. package/dist/kiro-usage-worker.js +10 -0
  113. package/dist/kiro-usage-worker.js.map +1 -0
  114. package/dist/kiro-usage.d.ts +58 -0
  115. package/dist/kiro-usage.js +310 -0
  116. package/dist/kiro-usage.js.map +1 -0
  117. package/dist/locale.js +88 -8
  118. package/dist/locale.js.map +1 -1
  119. package/dist/log-service.js +1 -1
  120. package/dist/log-service.js.map +1 -1
  121. package/dist/login-url.d.ts +4 -0
  122. package/dist/login-url.js +22 -0
  123. package/dist/login-url.js.map +1 -0
  124. package/dist/needs-you-hub.d.ts +2 -1
  125. package/dist/needs-you-hub.js +1 -1
  126. package/dist/needs-you-hub.js.map +1 -1
  127. package/dist/needs-you.d.ts +8 -2
  128. package/dist/needs-you.js +6 -0
  129. package/dist/needs-you.js.map +1 -1
  130. package/dist/notice-delivery.d.ts +16 -0
  131. package/dist/notice-delivery.js +35 -0
  132. package/dist/notice-delivery.js.map +1 -0
  133. package/dist/office-share-page.d.ts +16 -0
  134. package/dist/office-share-page.js +85 -0
  135. package/dist/office-share-page.js.map +1 -0
  136. package/dist/office-share-service.d.ts +91 -0
  137. package/dist/office-share-service.js +176 -0
  138. package/dist/office-share-service.js.map +1 -0
  139. package/dist/office-share.d.ts +258 -0
  140. package/dist/office-share.js +549 -0
  141. package/dist/office-share.js.map +1 -0
  142. package/dist/opencode-usage-scan.d.ts +28 -0
  143. package/dist/opencode-usage-scan.js +92 -0
  144. package/dist/opencode-usage-scan.js.map +1 -0
  145. package/dist/opencode-usage-worker.d.ts +1 -0
  146. package/dist/opencode-usage-worker.js +10 -0
  147. package/dist/opencode-usage-worker.js.map +1 -0
  148. package/dist/outbound-handlers.js +22 -2
  149. package/dist/outbound-handlers.js.map +1 -1
  150. package/dist/outbound-schemas.d.ts +26 -2
  151. package/dist/outbound-schemas.js +27 -2
  152. package/dist/outbound-schemas.js.map +1 -1
  153. package/dist/public-web-gateway.js +7 -1
  154. package/dist/public-web-gateway.js.map +1 -1
  155. package/dist/quickstart-api.js +13 -2
  156. package/dist/quickstart-api.js.map +1 -1
  157. package/dist/quickstart.js +2 -1
  158. package/dist/quickstart.js.map +1 -1
  159. package/dist/remote-card-answer.d.ts +83 -0
  160. package/dist/remote-card-answer.js +208 -0
  161. package/dist/remote-card-answer.js.map +1 -0
  162. package/dist/reply-triggers.d.ts +2 -0
  163. package/dist/reply-triggers.js +3 -0
  164. package/dist/reply-triggers.js.map +1 -0
  165. package/dist/settings-api.d.ts +2 -0
  166. package/dist/settings-api.js +41 -16
  167. package/dist/settings-api.js.map +1 -1
  168. package/dist/settings-change.js +17 -14
  169. package/dist/settings-change.js.map +1 -1
  170. package/dist/settings-effect.js +7 -1
  171. package/dist/settings-effect.js.map +1 -1
  172. package/dist/settings-instance-patch.js +4 -1
  173. package/dist/settings-instance-patch.js.map +1 -1
  174. package/dist/setup-form.d.ts +1 -1
  175. package/dist/setup-form.js +55 -6
  176. package/dist/setup-form.js.map +1 -1
  177. package/dist/setup-host.d.ts +9 -0
  178. package/dist/setup-host.js +127 -6
  179. package/dist/setup-host.js.map +1 -1
  180. package/dist/setup-signin-assets.d.ts +2 -0
  181. package/dist/setup-signin-assets.js +6 -0
  182. package/dist/setup-signin-assets.js.map +1 -0
  183. package/dist/setup-wizard.js +2 -0
  184. package/dist/setup-wizard.js.map +1 -1
  185. package/dist/structured-signin-transport.d.ts +8 -0
  186. package/dist/structured-signin-transport.js +79 -0
  187. package/dist/structured-signin-transport.js.map +1 -0
  188. package/dist/structured-signin.d.ts +131 -0
  189. package/dist/structured-signin.js +388 -0
  190. package/dist/structured-signin.js.map +1 -0
  191. package/dist/tool-permissions.js +12 -0
  192. package/dist/tool-permissions.js.map +1 -1
  193. package/dist/topic-commands.js +6 -2
  194. package/dist/topic-commands.js.map +1 -1
  195. package/dist/transcript-sources.d.ts +26 -2
  196. package/dist/transcript-sources.js +103 -40
  197. package/dist/transcript-sources.js.map +1 -1
  198. package/dist/turn-reply-guard.d.ts +1 -1
  199. package/dist/turn-reply-guard.js +1 -1
  200. package/dist/turn-reply-guard.js.map +1 -1
  201. package/dist/types.d.ts +28 -8
  202. package/dist/ui/backend-signin.js +63 -10
  203. package/dist/ui/chat-strings.js +2 -2
  204. package/dist/ui/fleet-office-editor.js +14 -9
  205. package/dist/ui/fleet-office-layout.js +70 -32
  206. package/dist/ui/fleet-office-model.js +6 -3
  207. package/dist/ui/fleet-office-overview.js +167 -0
  208. package/dist/ui/fleet-office-pack.d.ts +7 -2
  209. package/dist/ui/fleet-office-pack.js +119 -15
  210. package/dist/ui/fleet-office-skin.js +98 -23
  211. package/dist/ui/fleet-office-store.js +39 -5
  212. package/dist/ui/fleet-office-view.js +5 -4
  213. package/dist/ui/fleet-office-zones.js +172 -12
  214. package/dist/ui/fleet-office.js +547 -186
  215. package/dist/ui/fleet-usage.js +19 -15
  216. package/dist/ui/office-share-dialog.js +145 -0
  217. package/dist/ui/office-share-page.js +164 -0
  218. package/dist/ui/panel-chat.js +8 -1
  219. package/dist/ui/panel-log.js +2 -2
  220. package/dist/ui/panel-needs.js +4 -4
  221. package/dist/ui/panel-settings.js +37 -11
  222. package/dist/ui/settings-dialogs.js +8 -1
  223. package/dist/ui/settings-guests.js +17 -7
  224. package/dist/ui/settings-model.js +14 -6
  225. package/dist/ui/settings-registry.js +4 -0
  226. package/dist/ui/settings-strings.js +16 -2
  227. package/dist/ui/shared/app-i18n.js +4 -2
  228. package/dist/ui/shared/app-shell.js +14 -6
  229. package/dist/ui/shared/app-stream.js +2 -1
  230. package/dist/ui/shared/app.css +67 -2
  231. package/dist/ui/shared/office/cozy.png +0 -0
  232. package/dist/ui/shared/side-panel.js +95 -0
  233. package/dist/ui/structured-signin.js +115 -0
  234. package/dist/usage-report.d.ts +20 -0
  235. package/dist/usage-report.js +54 -17
  236. package/dist/usage-report.js.map +1 -1
  237. package/dist/usage-rollup.d.ts +9 -1
  238. package/dist/usage-rollup.js +20 -4
  239. package/dist/usage-rollup.js.map +1 -1
  240. package/dist/web-api.d.ts +18 -0
  241. package/dist/web-api.js +201 -0
  242. package/dist/web-api.js.map +1 -1
  243. package/dist/web-auth.js +1 -1
  244. package/dist/web-auth.js.map +1 -1
  245. package/dist/web-backend-login.d.ts +1 -7
  246. package/dist/web-backend-login.js +2 -24
  247. package/dist/web-backend-login.js.map +1 -1
  248. package/dist/web-office-acts.d.ts +173 -0
  249. package/dist/web-office-acts.js +285 -0
  250. package/dist/web-office-acts.js.map +1 -0
  251. package/dist/web-office-skins.d.ts +3 -2
  252. package/dist/web-office-skins.js +3 -2
  253. package/dist/web-office-skins.js.map +1 -1
  254. package/dist/web-office-store.js +40 -7
  255. package/dist/web-office-store.js.map +1 -1
  256. package/dist/web-settings-patch.d.ts +2 -1
  257. package/dist/web-settings-patch.js +4 -1
  258. package/dist/web-settings-patch.js.map +1 -1
  259. package/dist/working-recovery.d.ts +2 -2
  260. package/dist/working-recovery.js +43 -8
  261. package/dist/working-recovery.js.map +1 -1
  262. package/dist/working-state-journal.d.ts +2 -0
  263. package/dist/working-state-journal.js +16 -1
  264. package/dist/working-state-journal.js.map +1 -1
  265. package/package.json +3 -2
  266. package/templates/windows-shortcuts.ps1 +155 -140
@@ -0,0 +1,167 @@
1
+ // #2554: the Office view's "All offices" — every stored office as a tile on one canvas, side by side, each drawn exactly
2
+ // as its own view would be (its style, its members, seats, poses, bubbles, walking), scaled into its tile. This module
3
+ // is the pure part (fleet-office.js draws it): who is in no office (the lobby tile), where the tiles go, what a point
4
+ // on the overview hits, which tiles are on screen, and the traffic that crosses between tiles.
5
+ //
6
+ // - Shown when at least two offices are stored (overviewOk); an id no stored office can have (ALL_ID: '*' is not an
7
+ // office id's character), kept per device as any office pick is.
8
+ // - The tiles: each as large as its office's drawn content — a zones scene its 1280×720, a desks office its rooms'
9
+ // bounds (cropToRooms: an office of three is small) — scaled down to at most CELL_W × CELL_H, never up; a LABEL band
10
+ // above each (the page's label sits there), GAP apart, M from the edge. Shelf packing in the stored order (the lobby
11
+ // last): a row takes tiles while they fit a row width, each row as tall as its tallest tile and centred across. The
12
+ // row width: whichever (of the widths a run of tiles makes) lets the whole overview show largest in the panel (Fit's
13
+ // contain); a tie keeps the narrower. The same input always gives the same picture.
14
+ // - Traffic between people in different tiles is a straight flight between their seats in overview coordinates
15
+ // (createCrossFlights); between two people in the same tile it is that tile's own (createFlights, along its corridors).
16
+ import { FLIGHT, along, desksOf, membersOf, pathLength } from "./fleet-office-model.js";
17
+ import { hue } from "./fleet-traffic.js";
18
+
19
+ /** The switcher's "All offices" value: '*' is never in a stored office's id (fleet-office-model.js officeOk). */
20
+ export const ALL_ID = "*all";
21
+ /** The lobby tile's key: never an office's id. */
22
+ export const LOBBY_TILE = "\u0000lobby";
23
+ /** The overview's geometry, in logical units. */
24
+ export const OV = { CELL_W: 1280, CELL_H: 720, LABEL: 56, GAP: 24, M: 24 };
25
+
26
+ /** Whether "All offices" is offered: at least two stored offices. */
27
+ export const overviewOk = (stored) => Array.isArray(stored) && stored.length >= 2;
28
+
29
+ /**
30
+ * The people in no stored office, in the org chart's order (General, the teams, the rest): the lobby tile's. An office
31
+ * with no members holds everyone (fleet-office-model.js membersOf), so then nobody is left for the lobby.
32
+ */
33
+ export function lobbyNodes(stored, view) {
34
+ const placed = new Set((stored || []).flatMap((o) => membersOf(o, view).map((n) => n.name)));
35
+ const out = new Map();
36
+ for (const n of [...view.top, ...view.teams.flatMap((tm) => tm.nodes), ...view.rest.nodes]) if (!placed.has(n.name) && !out.has(n.name)) out.set(n.name, n);
37
+ return [...out.values()];
38
+ }
39
+
40
+ /** A scene's scale in its tile: its own size, scaled down to fit a CELL_W × CELL_H cell (never up). */
41
+ export const tileScale = (w, h) => Math.min(1, OV.CELL_W / Math.max(1, w), OV.CELL_H / Math.max(1, h));
42
+
43
+ /**
44
+ * A desks layout cropped to its rooms: everything moved so the rooms' bounds start at (0, 0), its size theirs (the
45
+ * corridors outside the rooms and the margins are not part of a tile). Unchanged when it has no rooms.
46
+ */
47
+ export function cropToRooms(L) {
48
+ if (!L.rooms.length) return L;
49
+ const x0 = Math.min(...L.rooms.map((r) => r.x)), y0 = Math.min(...L.rooms.map((r) => r.y));
50
+ const x1 = Math.max(...L.rooms.map((r) => r.x + r.w)), y1 = Math.max(...L.rooms.map((r) => r.y + r.h));
51
+ const mv = (p) => ({ ...p, x: p.x - x0, y: p.y - y0 });
52
+ return { ...L, rooms: L.rooms.map((r) => ({ ...mv(r), desks: r.desks.map((d) => ({ ...mv(d), seat: mv(d.seat) })) })), waypoints: (L.waypoints || []).map(mv),
53
+ width: x1 - x0, height: y1 - y0 };
54
+ }
55
+
56
+ /**
57
+ * The tiles of `scenes` ([{ key, w, h }], in order) on shelves at most `rowW` wide (a tile wider than that has a row of
58
+ * its own): each { key, x, y, w, h, s (its scale), label: { x, y, w, h } (the band above it) }; and the overview's size.
59
+ * Each row is centred across the widest; its tiles stand at its top, under their labels.
60
+ */
61
+ export function arrangeTiles(scenes, rowW) {
62
+ const { LABEL, GAP, M } = OV;
63
+ const rows = [];
64
+ let row = null;
65
+ for (const sc of scenes) {
66
+ const s = tileScale(sc.w, sc.h), it = { key: sc.key, s, w: Math.max(1, sc.w) * s, h: Math.max(1, sc.h) * s };
67
+ if (!row || row.w + GAP + it.w > rowW + 1e-6) { row = { items: [], w: -GAP, h: 0 }; rows.push(row); }
68
+ row.items.push(it); row.w += GAP + it.w; row.h = Math.max(row.h, it.h);
69
+ }
70
+ const inner = rows.length ? Math.max(...rows.map((r) => r.w)) : 0, tiles = [];
71
+ let y = M;
72
+ for (const r of rows) {
73
+ let x = M + (inner - r.w) / 2;
74
+ for (const it of r.items) {
75
+ tiles.push({ key: it.key, x, y: y + LABEL, w: it.w, h: it.h, s: it.s, label: { x, y, w: it.w, h: LABEL } });
76
+ x += it.w + GAP;
77
+ }
78
+ y += LABEL + r.h + GAP;
79
+ }
80
+ return { rows: rows.length, rowW, tiles, width: 2 * M + inner, height: rows.length ? y - GAP + M : 2 * M };
81
+ }
82
+
83
+ /**
84
+ * The tiles packed for a `size` panel: of the row widths a run of consecutive tiles makes, the one whose overview shows
85
+ * largest there (min of the two fits); a tie keeps the narrower.
86
+ */
87
+ export function tileLayout(scenes, size) {
88
+ const ws = scenes.map((sc) => Math.max(1, sc.w) * tileScale(sc.w, sc.h)), cands = new Set();
89
+ for (let i = 0; i < ws.length; i++) { let run = -OV.GAP; for (let j = i; j < ws.length; j++) { run += OV.GAP + ws[j]; cands.add(run); } }
90
+ let best = arrangeTiles(scenes, 0), bestK = -1;
91
+ for (const rowW of [...cands].sort((a, b) => a - b)) {
92
+ const a = arrangeTiles(scenes, rowW), k = Math.min(size.w / a.width, size.h / a.height);
93
+ if (k > bestK + 1e-9) { best = a; bestK = k; }
94
+ }
95
+ return best;
96
+ }
97
+
98
+ /** The tile whose scene holds the overview point (x, y), or null. */
99
+ export const tileAt = (lay, x, y) => lay.tiles.find((t) => x >= t.x && x < t.x + t.w && y >= t.y && y < t.y + t.h) || null;
100
+ /** The tile whose label band holds the overview point, or null. */
101
+ export const labelAt = (lay, x, y) => lay.tiles.find((t) => x >= t.label.x && x < t.label.x + t.label.w && y >= t.label.y && y < t.label.y + t.label.h) || null;
102
+ /** Overview → the tile's own scene units, and back; a box in the scene → the overview. */
103
+ export const inTile = (t, x, y) => ({ x: (x - t.x) / t.s, y: (y - t.y) / t.s });
104
+ export const fromTile = (t, p) => ({ x: t.x + p.x * t.s, y: t.y + p.y * t.s });
105
+ export const tileBox = (t, b) => ({ x: t.x + b.x * t.s, y: t.y + b.y * t.s, w: b.w * t.s, h: b.h * t.s });
106
+ /** The tiles a view ({ k, x, y }) shows any of in a `size` viewport: the rest are not drawn. */
107
+ export function visibleTiles(lay, v, size) {
108
+ return lay.tiles.filter((t) => {
109
+ const x = v.x + t.x * v.k, y = v.y + t.y * v.k;
110
+ return x < size.w && x + t.w * v.k > 0 && y < size.h && y + t.h * v.k > 0;
111
+ });
112
+ }
113
+
114
+ /**
115
+ * A straight flight between two people in different tiles: [the sender's seat, the receiver's] in overview units (the
116
+ * first tile each is in), or null — when either is drawn nowhere, or some tile holds both (that tile carries it along
117
+ * its corridors). `parts`: [{ tile, L }] in the tiles' order.
118
+ */
119
+ export function crossPath(parts, from, to) {
120
+ let a = null, z = null;
121
+ for (const p of parts) {
122
+ const ds = desksOf(p.L), da = ds.find((d) => d.node.name === from), dz = ds.find((d) => d.node.name === to);
123
+ if (da && dz) return null;
124
+ if (da && !a) a = fromTile(p.tile, da.seat);
125
+ if (dz && !z) z = fromTile(p.tile, dz.seat);
126
+ }
127
+ return a && z ? [a, z] : null;
128
+ }
129
+
130
+ const MAX_SEEN = 2000;
131
+ /**
132
+ * The items in the air between tiles — fleet-office-model.js createFlights's lifecycle (each new item once; none for a
133
+ * stale delegation or with reduced motion), on crossPath's straight line: `frame(now, parts)` → { id, hue, kind, from, x, y,
134
+ * t } in overview units; a trip with no crossing (both in one tile, or either gone) is dropped.
135
+ */
136
+ export function createCrossFlights({ max = FLIGHT.MAX } = {}) {
137
+ const seen = new Set();
138
+ let trips = [];
139
+ return {
140
+ sync(items, now, still = false) {
141
+ let k = 0, started = 0;
142
+ for (const it of items || []) {
143
+ if (seen.has(it.id)) continue;
144
+ seen.add(it.id);
145
+ if (seen.size > MAX_SEEN) seen.delete(seen.values().next().value);
146
+ if (still || it.phase === "stale") continue;
147
+ trips.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now + k++ * FLIGHT.STAGGER_MS, parts: null, pts: null, ms: 0 });
148
+ started++;
149
+ }
150
+ if (trips.length > max) trips = trips.slice(trips.length - max);
151
+ return started;
152
+ },
153
+ frame(now, parts) {
154
+ const out = [];
155
+ trips = trips.filter((f) => {
156
+ if (f.parts !== parts) { f.parts = parts; f.pts = crossPath(parts, f.from, f.to); f.ms = f.pts ? Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(f.pts) / FLIGHT.SPEED)) : 0; }
157
+ if (!f.pts) return false;
158
+ const t = (now - f.start) / f.ms;
159
+ if (t >= 1) return false;
160
+ if (t >= 0) { const p = along(f.pts, t); out.push({ id: f.id, hue: f.hue, kind: f.kind, from: f.from, x: p.x, y: p.y, t }); }
161
+ return true;
162
+ });
163
+ return out;
164
+ },
165
+ size: () => trips.length,
166
+ };
167
+ }
@@ -4,7 +4,8 @@
4
4
  export interface PackStrip { src: string; frames: number; w: number; h: number; fps: number; ax: number; ay: number; dw: number; dh: number }
5
5
  export interface PackPoint { x: number; y: number }
6
6
  export interface PackArea { name: string; rect: { x: number; y: number; w: number; h: number }; seats: PackPoint[] }
7
- export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: number; depth: number | "y"; activeWhen: string[] | null; activeSprite: PackStrip | null }
7
+ /** `label` (#2574): its name tag, or null. */
8
+ export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: number; depth: number | "y"; activeWhen: string[] | null; activeSprite: PackStrip | null; label: string | null }
8
9
  export interface CheckedPack {
9
10
  format: 1; id: string; scale: number; background?: string; foreground?: string; matte?: string;
10
11
  areas: Record<string, PackArea>; stateZones: Record<string, string> | null; door: PackPoint;
@@ -17,6 +18,8 @@ export interface LayoutOverrides {
17
18
  pack: string;
18
19
  seats?: Record<string, Array<[number, number]>>;
19
20
  furniture?: Record<string, [number, number]>;
21
+ /** #2665: an item's night place (an item with a `night` only). */
22
+ furnitureNight?: Record<string, [number, number]>;
20
23
  door?: [number, number];
21
24
  waypoints?: Array<{ id: string; x: number; y: number }>;
22
25
  edges?: Array<[string, string]>;
@@ -28,12 +31,14 @@ export function own<T = unknown>(o: unknown, k: unknown): T | undefined;
28
31
  export function inherited(k: unknown): boolean;
29
32
  export function matteOk(v: unknown): v is string;
30
33
  export function checkStrip(v: unknown, at?: string, scale?: number): PackStrip;
31
- export function checkSkinPack(x: unknown): { ok: true; pack: CheckedPack } | { ok: false; error: string };
34
+ export function checkSkinPack(x: unknown, opts?: { kept?: boolean }): { ok: true; pack: CheckedPack } | { ok: false; error: string };
32
35
  export function skinPackOk(x: unknown): CheckedPack | null;
33
36
  export function packImages(pack: CheckedPack): string[];
34
37
  export function packStrips(pack: CheckedPack): Array<{ at: string; strip: PackStrip }>;
35
38
  export function checkLayoutOverrides(x: unknown): { ok: true; overrides: LayoutOverrides } | { ok: false; error: string };
36
39
  export function applyOverrides(pack: CheckedPack, ov: LayoutOverrides | null | undefined): { ok: true; pack: CheckedPack } | { ok: false; error: string };
40
+ export function fitNight<T extends LayoutOverrides | null | undefined>(pack: CheckedPack, ov: T): T;
37
41
  export function overridesBetween(pack: CheckedPack, edited: CheckedPack): LayoutOverrides | null;
38
42
  export function poseStrip(set: Record<string, PackStrip> | null | undefined, pose: string, heading?: { dx: number; dy: number } | null): { strip: PackStrip; mirror: boolean } | null;
43
+ export function walkStrip(f: { walk?: Record<string, PackStrip> | null; walking?: boolean; heading?: { dx: number; dy: number } | null } | null | undefined): { strip: PackStrip; mirror: boolean } | null;
39
44
  export function stripBox(st: PackStrip, x: number, y: number, mirror?: boolean): { x: number; y: number; w: number; h: number };
@@ -30,15 +30,32 @@
30
30
  // "door": [640, 700], optional: where someone new walks in from (default: bottom middle)
31
31
  // "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …],
32
32
  // optional: the walkable graph (else people walk straight)
33
+ // "straight": true, optional (#2634): every walk goes across, then up or down — each
34
+ // diagonal leg an L, so the walkSide / walkDown / walkUp strips
35
+ // always fit (graph edges laid out across and up/down put the
36
+ // corners on the aisles); default false
33
37
  // "furniture": [ { "id": "rack", "sprite": STRIP, "x": 1020, "y": 140, "depth": 2,
34
- // "activeWhen": "crashed", "activeSprite": STRIP } ],
38
+ // "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "label": "Rack",
39
+ // "night": { "sprite": STRIP, "x": 300, "y": 420 },
40
+ // "walk": { "walk": STRIP, "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP } } ],
41
+ // "label" (#2574): a name tag under it, like a person's
42
+ // (an NPC drawn as furniture). "night" (#2568), optional: at bedtime
43
+ // (when the viewer has it on: 00:00–06:00 their time) drawn as this
44
+ // strip, playing, at (x, y) — its own place for either left out.
45
+ // "walk" (#2596), optional: it strolls near its (x, y) and back
46
+ // like an idle person, drawn walking by these strips as a
47
+ // character set's walking poses ("walk" required); not at bedtime
48
+ // with a "night".
35
49
  // drawn by depth: below 1000 under the people, 1000 and up over
36
50
  // them; "depth": "y" (the string) sorts it among the people by base
37
51
  // y — its anchor's y against each person's feet: someone whose feet
38
52
  // are lower down (a greater y; a tie too) is drawn after it, in
39
53
  // front. (x, y) is where the strip's anchor goes. `activeWhen` (a
40
54
  // state, or a list): while anyone shown is in it, the item plays its
41
- // strip (or `activeSprite`); otherwise it holds the first frame
55
+ // strip (or `activeSprite`); otherwise it holds the first frame —
56
+ // or, with "loop": true (#2570), plays its strip all along.
57
+ // "activeFor" (an instance's name, or a list): only their state
58
+ // counts, not anyone's (a room several people take turns in)
42
59
  // "characters": { "default": { "working": STRIP, "idle": STRIP, "paused": STRIP, "needs": STRIP, "walk": STRIP,
43
60
  // "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP },
44
61
  // "fox": { … } }, "default" with at least "idle" is required; a missing pose is idle.
@@ -47,6 +64,9 @@
47
64
  // walkSide, drawn as it is when moving right and mirrored when moving
48
65
  // left (so draw it facing right). A missing one is "walk", then idle
49
66
  // ("walk" itself is never mirrored)
67
+ // A set may also have "prop": STRIP (#2562) — what its people
68
+ // send: their messages, work and reports fly as it, in place of
69
+ // the pack's "props" (a set without one: the pack's props)
50
70
  // "byInstance": { "atlas": "fox" }, optional: an instance name → its character set
51
71
  // "props": { "task": STRIP, "send": STRIP, "report": STRIP, "broadcast": STRIP }, optional traffic items
52
72
  // "bubbles": { "en": { "working": ["On it."] }, "zh-TW": { … } } optional: short lines per state (≤ 12 a state,
@@ -82,6 +102,7 @@
82
102
  // { "pack": "cozy", the pack they were made on (on another pack they are ignored)
83
103
  // "seats": { "desks": [[120, 220], …] }, an area's seats: as many as the pack's, each inside the area's rect
84
104
  // "furniture": { "rack": [1000, 150] }, an item's (x, y)
105
+ // "furnitureNight": { "elder": [300, 420] }, an item's night place (#2665: an item with a "night" only)
85
106
  // "door": [640, 700],
86
107
  // "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
87
108
  // the walkable graph, replaced whole (edges need waypoints)
@@ -89,14 +110,26 @@
89
110
  // Every coordinate is on the same 1280×720 logical canvas, in the same ranges as the pack's. checkLayoutOverrides is
90
111
  // the shape (the host checks it on every PUT /ui/offices); applyOverrides puts them on a checked pack, refusing a
91
112
  // seat outside its area, an area or furniture the pack does not have, or a seat count that differs (the host checks
92
- // that too, for a pack it keeps).
113
+ // that too, for a pack it keeps). A night place goes on the item's `night` (its x, y), so at bedtime (nightFurniture)
114
+ // it is the office's, else the pack's night x/y, else the item's (moved) day place. The host keeps `furnitureNight`
115
+ // beside the offices on disk, never inside them: an older host's stricter check would drop the office (#2665,
116
+ // web-office-store.ts).
93
117
 
94
118
  export const LOGICAL = { w: 1280, h: 720 };
95
119
  /** The people's depth: furniture below it is drawn under them, the rest over them. */
96
120
  export const CHAR_DEPTH = 1000;
97
121
  export const PACK_STATES = ["needs", "crashed", "stuck", "working", "idle", "paused", "stopped", "missing"];
98
122
  const POSES = ["working", "idle", "paused", "needs", "walk", "walkDown", "walkUp", "walkSide"], PROPS = ["task", "send", "report", "broadcast"];
123
+ /** A character set's strips: its poses, and what its people send (#2562). */
124
+ const SET_STRIPS = [...POSES, "prop"];
125
+ /** A furniture item's walking strips (#2596): a character set's walking poses. */
126
+ const WALKS = ["walk", "walkDown", "walkUp", "walkSide"];
99
127
  const BUILT_IN = new Set(["pixel", "studio"]);
128
+ /**
129
+ * Ids built in after packs could be uploaded under them (#2631: "cozy"): a new upload is refused, but a pack the host
130
+ * kept under one before (`kept`) is still read back, listed and deletable, and wins the id for the offices that name it.
131
+ */
132
+ const RESERVED = new Set(["cozy"]);
100
133
  const ID = /^[A-Za-z0-9_-]{1,40}$/;
101
134
  const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
102
135
  const HEX = /^#[0-9a-f]{6}$/i;
@@ -138,6 +171,24 @@ export function checkStrip(v, at = "strip", scale = 2) {
138
171
  return { src: src(v.src, `${at}.src`), frames: int(v.frames, 1, 32, `${at}.frames`), w, h, fps: v.fps === undefined ? 4 : num(v.fps, 0, 30, `${at}.fps`),
139
172
  ax: v.ax === undefined ? w / 2 : num(v.ax, 0, w, `${at}.ax`), ay: v.ay === undefined ? h : num(v.ay, 0, h, `${at}.ay`), dw, dh };
140
173
  }
174
+ /** A furniture item's night (#2568): { sprite, x, y } — at bedtime it is drawn as `sprite` (playing) at (x, y), its own place when left out. */
175
+ function nightOf(v, at, scale) {
176
+ if (v === undefined) return null;
177
+ obj(v, at); keys(v, ["sprite", "x", "y"], at);
178
+ return { sprite: checkStrip(v.sprite, `${at}.sprite`, scale), x: v.x === undefined ? null : num(v.x, 0, LOGICAL.w, `${at}.x`), y: v.y === undefined ? null : num(v.y, 0, LOGICAL.h, `${at}.y`) };
179
+ }
180
+ /** A furniture item's walk (#2596): its walking strips, picked by heading as a character set's (poseStrip); "walk" required. */
181
+ function walkOf(v, at, scale) {
182
+ if (v === undefined) return null;
183
+ const w = strips(v, WALKS, at, scale);
184
+ if (!w.walk) throw new Refused(`${at}.walk`, "required");
185
+ return w;
186
+ }
187
+ /** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to 8. */
188
+ function names(v, at) {
189
+ const one = (s, here) => { if (typeof s !== "string" || !s || s.length > 200 || inherited(s) || /[\u0000-\u001f\u007f]/.test(s)) throw new Refused(here, "an instance's name"); return s; };
190
+ return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
191
+ }
141
192
  function strips(v, names, at, scale) {
142
193
  obj(v, at);
143
194
  keys(v, names, at);
@@ -148,16 +199,20 @@ function states(v, at) {
148
199
  return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
149
200
  }
150
201
 
151
- /** A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. */
152
- export function checkSkinPack(x) {
202
+ /**
203
+ * A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. `kept`: a pack the host
204
+ * keeps already (read back, not uploaded), which may hold a RESERVED id.
205
+ */
206
+ export function checkSkinPack(x, { kept = false } = {}) {
153
207
  try {
154
208
  obj(x, "pack");
155
- keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "waypoints", "edges", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
209
+ keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
156
210
  if (x.format !== 1) throw new Refused("format", "1");
157
211
  if (typeof x.id !== "string" || !ID.test(x.id)) throw new Refused("id", "1–40 letters, digits, _ or -");
158
- if (BUILT_IN.has(x.id)) throw new Refused("id", "a built-in skin's id");
212
+ if (BUILT_IN.has(x.id) || (!kept && RESERVED.has(x.id))) throw new Refused("id", "a built-in skin's id");
159
213
  const scale = x.scale === undefined ? 2 : int(x.scale, 1, 8, "scale");
160
214
  if (x.matte !== undefined && !matteOk(x.matte)) throw new Refused("matte", "a colour as #rrggbb");
215
+ if (x.straight !== undefined && typeof x.straight !== "boolean") throw new Refused("straight", "true or false");
161
216
  const areas = {};
162
217
  const rawAreas = x.areas === undefined ? {} : obj(x.areas, "areas");
163
218
  if (Object.keys(rawAreas).length > LIMIT.areas) throw new Refused("areas", `at most ${LIMIT.areas}`);
@@ -204,20 +259,25 @@ export function checkSkinPack(x) {
204
259
  const fids = new Set();
205
260
  const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
206
261
  const at = `furniture[${i}]`;
207
- obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite"], at);
262
+ obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "label", "night", "walk"], at);
208
263
  if (!nameOk(f.id) || fids.has(f.id)) throw new Refused(`${at}.id`, "a new name of letters, digits, _ or - (not an inherited one)");
209
264
  fids.add(f.id);
210
265
  if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
266
+ if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
267
+ if (f.loop !== undefined && typeof f.loop !== "boolean") throw new Refused(`${at}.loop`, "true or false");
268
+ if (f.label !== undefined && (typeof f.label !== "string" || !f.label.trim() || [...f.label].length > LIMIT.line || /[\u0000-\u001f\u007f]/.test(f.label))) throw new Refused(`${at}.label`, `1–${LIMIT.line} characters`);
211
269
  return { id: f.id, sprite: checkStrip(f.sprite, `${at}.sprite`, scale), x: num(f.x, 0, LOGICAL.w, `${at}.x`), y: num(f.y, 0, LOGICAL.h, `${at}.y`),
212
270
  depth: f.depth === "y" ? "y" : depth(f.depth ?? 0, `${at}.depth`), activeWhen: f.activeWhen === undefined ? null : states(f.activeWhen, `${at}.activeWhen`),
213
- activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale) };
271
+ activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale),
272
+ activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true, label: f.label === undefined ? null : f.label.trim(), night: nightOf(f.night, `${at}.night`, scale),
273
+ walk: walkOf(f.walk, `${at}.walk`, scale) };
214
274
  }).sort((a, b) => order(a) - order(b));
215
275
  const sets = obj(x.characters, "characters");
216
276
  if (Object.keys(sets).length > LIMIT.sets) throw new Refused("characters", `at most ${LIMIT.sets} sets`);
217
277
  const characters = {};
218
278
  for (const [k, v] of Object.entries(sets)) {
219
279
  if (!nameOk(k)) throw new Refused(`characters.${k}`, "a name of letters, digits, _ or - (not an inherited one)");
220
- characters[k] = strips(v, POSES, `characters.${k}`, scale);
280
+ characters[k] = strips(v, SET_STRIPS, `characters.${k}`, scale);
221
281
  if (!characters[k].idle) throw new Refused(`characters.${k}.idle`, "required");
222
282
  }
223
283
  if (!own(characters, "default")) throw new Refused("characters.default", "required");
@@ -248,7 +308,7 @@ export function checkSkinPack(x) {
248
308
  const opt = (v, at) => (v === undefined ? undefined : src(v, at));
249
309
  return { ok: true, pack: {
250
310
  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,
251
- door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"), waypoints, edges, furniture, characters, byInstance,
311
+ door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"), waypoints, edges, straight: x.straight === true, furniture, characters, byInstance,
252
312
  props: x.props === undefined ? {} : strips(x.props, PROPS, "props", scale), bubbles,
253
313
  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") },
254
314
  } };
@@ -261,13 +321,18 @@ export function checkSkinPack(x) {
261
321
  export const skinPackOk = (x) => { const r = checkSkinPack(x); return r.ok ? r.pack : null; };
262
322
  /** Every image a pack names (what a loader fetches before spriteSkin draws). */
263
323
  export const packImages = (pack) => [...new Set([pack.background, pack.foreground, pack.room.floor, pack.room.wall, pack.desk.back, pack.desk.front, pack.desk.lit,
264
- ...pack.furniture.flatMap((f) => [f.sprite.src, f.activeSprite && f.activeSprite.src]),
324
+ ...pack.furniture.flatMap((f) => [f.sprite.src, f.activeSprite && f.activeSprite.src, f.night && f.night.sprite.src, ...Object.values(f.walk || {}).map((st) => st.src)]),
265
325
  ...Object.values(pack.characters).flatMap((s) => Object.values(s).map((st) => st.src)), ...Object.values(pack.props).map((st) => st.src)].filter(Boolean))];
266
326
 
267
327
  /** Every frame strip a checked pack names, with where (the host checks each image is frames·w × h). */
268
328
  export function packStrips(pack) {
269
329
  const out = [];
270
- for (const f of pack.furniture) { out.push({ at: `furniture.${f.id}.sprite`, strip: f.sprite }); if (f.activeSprite) out.push({ at: `furniture.${f.id}.activeSprite`, strip: f.activeSprite }); }
330
+ for (const f of pack.furniture) {
331
+ out.push({ at: `furniture.${f.id}.sprite`, strip: f.sprite });
332
+ if (f.activeSprite) out.push({ at: `furniture.${f.id}.activeSprite`, strip: f.activeSprite });
333
+ if (f.night) out.push({ at: `furniture.${f.id}.night.sprite`, strip: f.night.sprite });
334
+ for (const [k, st] of Object.entries(f.walk || {})) out.push({ at: `furniture.${f.id}.walk.${k}`, strip: st });
335
+ }
271
336
  for (const [k, set] of Object.entries(pack.characters)) for (const [pose, st] of Object.entries(set)) out.push({ at: `characters.${k}.${pose}`, strip: st });
272
337
  for (const [k, st] of Object.entries(pack.props)) out.push({ at: `props.${k}`, strip: st });
273
338
  return out;
@@ -280,7 +345,7 @@ const OV_LIMIT = { areas: LIMIT.areas, seats: LIMIT.seats, furniture: LIMIT.furn
280
345
  export function checkLayoutOverrides(x) {
281
346
  try {
282
347
  obj(x, "overrides");
283
- keys(x, ["pack", "seats", "furniture", "door", "waypoints", "edges"], "overrides");
348
+ keys(x, ["pack", "seats", "furniture", "furnitureNight", "door", "waypoints", "edges"], "overrides");
284
349
  if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
285
350
  const out = { pack: x.pack };
286
351
  if (x.seats !== undefined) {
@@ -305,6 +370,16 @@ export function checkLayoutOverrides(x) {
305
370
  out.furniture[id] = [q.x, q.y];
306
371
  }
307
372
  }
373
+ if (x.furnitureNight !== undefined) {
374
+ obj(x.furnitureNight, "overrides.furnitureNight");
375
+ if (Object.keys(x.furnitureNight).length > OV_LIMIT.furniture) throw new Refused("overrides.furnitureNight", `at most ${OV_LIMIT.furniture}`);
376
+ out.furnitureNight = {};
377
+ for (const [id, p] of Object.entries(x.furnitureNight)) {
378
+ if (!nameOk(id)) throw new Refused(`overrides.furnitureNight.${id}`, "an item's id");
379
+ const q = point(p, `overrides.furnitureNight.${id}`);
380
+ out.furnitureNight[id] = [q.x, q.y];
381
+ }
382
+ }
308
383
  if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
309
384
  if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
310
385
  if (x.waypoints !== undefined) {
@@ -352,7 +427,13 @@ export function applyOverrides(pack, ov) {
352
427
  }
353
428
  const fids = new Set(pack.furniture.map((f) => f.id));
354
429
  for (const id of Object.keys(ov.furniture || {})) if (!fids.has(id)) throw new Refused(`overrides.furniture.${id}`, "an item the pack has");
355
- const furniture = pack.furniture.map((f) => { const q = own(ov.furniture, f.id); return q ? { ...f, x: q[0], y: q[1] } : f; });
430
+ const nids = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
431
+ for (const id of Object.keys(ov.furnitureNight || {})) if (!nids.has(id)) throw new Refused(`overrides.furnitureNight.${id}`, "an item with a night the pack has");
432
+ const furniture = pack.furniture.map((f) => {
433
+ const q = own(ov.furniture, f.id), n = own(ov.furnitureNight, f.id);
434
+ if (!q && !n) return f;
435
+ return { ...f, ...(q ? { x: q[0], y: q[1] } : {}), ...(n ? { night: { ...f.night, x: n[0], y: n[1] } } : {}) };
436
+ });
356
437
  const door = ov.door ? { x: ov.door[0], y: ov.door[1] } : pack.door;
357
438
  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 };
358
439
  return { ok: true, pack: { ...pack, areas, furniture, door, ...graph } };
@@ -362,6 +443,20 @@ export function applyOverrides(pack, ov) {
362
443
  }
363
444
  }
364
445
 
446
+ /**
447
+ * #2665: overrides with only the night places `pack` can take (an item it has, with a `night`): a pack changed since
448
+ * (an item dropped, or its night) drops that entry, never the rest of the layout. applyOverrides itself stays strict
449
+ * (a write naming an item the pack lacks is refused, as for `furniture`).
450
+ */
451
+ export function fitNight(pack, ov) {
452
+ if (!ov || !ov.furnitureNight || ov.pack !== pack.id) return ov;
453
+ const ok = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
454
+ const kept = Object.entries(ov.furnitureNight).filter(([id]) => ok.has(id));
455
+ if (kept.length === Object.keys(ov.furnitureNight).length) return ov;
456
+ const { furnitureNight: _gone, ...rest } = ov;
457
+ return kept.length ? { ...rest, furnitureNight: Object.fromEntries(kept) } : rest;
458
+ }
459
+
365
460
  /**
366
461
  * What an edited layout keeps as overrides: only the parts that differ from the pack's own (`edited` is a pack with
367
462
  * the same areas and furniture, positions moved); null when nothing does (the office goes back to the pack's).
@@ -375,6 +470,12 @@ export function overridesBetween(pack, edited) {
375
470
  }
376
471
  const at = new Map(pack.furniture.map((f) => [f.id, f]));
377
472
  for (const f of edited.furniture) { const o = at.get(f.id); if (o && (o.x !== f.x || o.y !== f.y)) (ov.furniture ||= {})[f.id] = [f.x, f.y]; }
473
+ // #2665: a night place that differs from the pack's (its own x/y, or in place: null); in place again is no entry.
474
+ for (const f of edited.furniture) {
475
+ const o = at.get(f.id);
476
+ if (!o || !o.night || !f.night || f.night.x === null || f.night.y === null) continue;
477
+ if (o.night.x !== f.night.x || o.night.y !== f.night.y) (ov.furnitureNight ||= {})[f.id] = [f.night.x, f.night.y];
478
+ }
378
479
  if (pack.door.x !== edited.door.x || pack.door.y !== edited.door.y) ov.door = [edited.door.x, edited.door.y];
379
480
  if (!same(pack.waypoints, edited.waypoints) || !same(pack.edges, edited.edges)) {
380
481
  ov.waypoints = edited.waypoints.map((w) => ({ id: w.id, x: w.x, y: w.y }));
@@ -403,6 +504,9 @@ export function poseStrip(set, pose, heading = null) {
403
504
  const st = own(set, pose) || own(set, "idle");
404
505
  return st ? { strip: st, mirror: false } : null;
405
506
  }
507
+ /** The strip a furniture item shows while it walks (#2596: an NPC with `walk`, `walking` and `heading` set where it is
508
+ * drawn): its walk strip by heading, as a character's (poseStrip); null when it stands (its own sprite, as before). */
509
+ export const walkStrip = (f) => (f && f.walking && f.walk ? poseStrip(f.walk, "walk", f.heading) : null);
406
510
  /** The box (logical units) a strip covers with its anchor at (x, y): its displayed size; mirrored about the anchor. */
407
511
  export function stripBox(st, x, y, mirror = false) {
408
512
  const kx = st.dw / st.w, ky = st.dh / st.h;