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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +69 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +11 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.js +3 -0
  7. package/dist/agent-whats-new.js.map +1 -1
  8. package/dist/antigravity-usage-scan.d.ts +58 -0
  9. package/dist/antigravity-usage-scan.js +257 -0
  10. package/dist/antigravity-usage-scan.js.map +1 -0
  11. package/dist/antigravity-usage-worker.d.ts +1 -0
  12. package/dist/antigravity-usage-worker.js +10 -0
  13. package/dist/antigravity-usage-worker.js.map +1 -0
  14. package/dist/auth-api.js +4 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/backend/kiro-classic-archive.d.ts +52 -0
  17. package/dist/backend/kiro-classic-archive.js +444 -0
  18. package/dist/backend/kiro-classic-archive.js.map +1 -0
  19. package/dist/backend/kiro-identity.d.ts +83 -0
  20. package/dist/backend/kiro-identity.js +144 -0
  21. package/dist/backend/kiro-identity.js.map +1 -1
  22. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  23. package/dist/backend/kiro-migration-verify.js +52 -0
  24. package/dist/backend/kiro-migration-verify.js.map +1 -0
  25. package/dist/backend/kiro-retired-model.d.ts +62 -0
  26. package/dist/backend/kiro-retired-model.js +160 -0
  27. package/dist/backend/kiro-retired-model.js.map +1 -0
  28. package/dist/backend/kiro-v2-store-worker.js +21 -1
  29. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  30. package/dist/backend/kiro-v2-store.d.ts +8 -0
  31. package/dist/backend/kiro-v2-store.js.map +1 -1
  32. package/dist/backend/kiro.d.ts +169 -2
  33. package/dist/backend/kiro.js +703 -28
  34. package/dist/backend/kiro.js.map +1 -1
  35. package/dist/backend/types.d.ts +108 -0
  36. package/dist/backend/types.js +9 -0
  37. package/dist/backend/types.js.map +1 -1
  38. package/dist/backend-signin-guide.d.ts +1 -0
  39. package/dist/backend-signin-guide.js +9 -0
  40. package/dist/backend-signin-guide.js.map +1 -0
  41. package/dist/backend-signin-pending.d.ts +10 -0
  42. package/dist/backend-signin-pending.js +44 -0
  43. package/dist/backend-signin-pending.js.map +1 -0
  44. package/dist/browser-backend-signins.d.ts +56 -0
  45. package/dist/browser-backend-signins.js +189 -0
  46. package/dist/browser-backend-signins.js.map +1 -0
  47. package/dist/cache-ledger.d.ts +38 -2
  48. package/dist/cache-ledger.js +133 -8
  49. package/dist/cache-ledger.js.map +1 -1
  50. package/dist/cache-prices.d.ts +2 -1
  51. package/dist/cache-prices.js +5 -2
  52. package/dist/cache-prices.js.map +1 -1
  53. package/dist/cache-service.d.ts +68 -0
  54. package/dist/cache-service.js +355 -13
  55. package/dist/cache-service.js.map +1 -1
  56. package/dist/channel/adapters/discord.d.ts +11 -0
  57. package/dist/channel/adapters/discord.js +71 -14
  58. package/dist/channel/adapters/discord.js.map +1 -1
  59. package/dist/channel/ipc-timeouts.js +3 -0
  60. package/dist/channel/ipc-timeouts.js.map +1 -1
  61. package/dist/channel/mcp-tools.js +41 -2
  62. package/dist/channel/mcp-tools.js.map +1 -1
  63. package/dist/channel/tool-router.js +2 -1
  64. package/dist/channel/tool-router.js.map +1 -1
  65. package/dist/classic-channel-manager.d.ts +11 -0
  66. package/dist/classic-channel-manager.js +39 -1
  67. package/dist/classic-channel-manager.js.map +1 -1
  68. package/dist/cli.js +54 -2
  69. package/dist/cli.js.map +1 -1
  70. package/dist/config-validator.d.ts +7 -1
  71. package/dist/config-validator.js +55 -10
  72. package/dist/config-validator.js.map +1 -1
  73. package/dist/config.d.ts +19 -0
  74. package/dist/config.js +33 -1
  75. package/dist/config.js.map +1 -1
  76. package/dist/cpu-quota.d.ts +23 -0
  77. package/dist/cpu-quota.js +86 -0
  78. package/dist/cpu-quota.js.map +1 -0
  79. package/dist/daemon.d.ts +68 -0
  80. package/dist/daemon.js +448 -26
  81. package/dist/daemon.js.map +1 -1
  82. package/dist/dashboard-permissions.d.ts +31 -6
  83. package/dist/dashboard-permissions.js +84 -9
  84. package/dist/dashboard-permissions.js.map +1 -1
  85. package/dist/doctor.js +2 -1
  86. package/dist/doctor.js.map +1 -1
  87. package/dist/fleet-control-audit.d.ts +1 -1
  88. package/dist/fleet-control-audit.js +3 -0
  89. package/dist/fleet-control-audit.js.map +1 -1
  90. package/dist/fleet-manager.d.ts +133 -2
  91. package/dist/fleet-manager.js +905 -94
  92. package/dist/fleet-manager.js.map +1 -1
  93. package/dist/forged-envelope-scan.d.ts +9 -0
  94. package/dist/forged-envelope-scan.js +30 -5
  95. package/dist/forged-envelope-scan.js.map +1 -1
  96. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  97. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +24 -1
  98. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +49 -1
  99. package/dist/general-knowledge/skills/web-appearance/SKILL.md +49 -1
  100. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +47 -9
  101. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +54 -11
  102. package/dist/instance-config-impact.js +10 -5
  103. package/dist/instance-config-impact.js.map +1 -1
  104. package/dist/instance-lifecycle.d.ts +23 -0
  105. package/dist/instance-lifecycle.js +62 -12
  106. package/dist/instance-lifecycle.js.map +1 -1
  107. package/dist/kiro-db-reader.d.ts +5 -0
  108. package/dist/kiro-db-reader.js +18 -0
  109. package/dist/kiro-db-reader.js.map +1 -1
  110. package/dist/kiro-engine-status.d.ts +2 -0
  111. package/dist/kiro-engine-status.js +9 -0
  112. package/dist/kiro-engine-status.js.map +1 -1
  113. package/dist/kiro-migrate.d.ts +60 -0
  114. package/dist/kiro-migrate.js +160 -0
  115. package/dist/kiro-migrate.js.map +1 -0
  116. package/dist/kiro-session-proof.d.ts +41 -0
  117. package/dist/kiro-session-proof.js +89 -0
  118. package/dist/kiro-session-proof.js.map +1 -0
  119. package/dist/kiro-transcript-lane.d.ts +2 -0
  120. package/dist/kiro-transcript-lane.js.map +1 -1
  121. package/dist/kiro-transcript-worker.js +1 -0
  122. package/dist/kiro-transcript-worker.js.map +1 -1
  123. package/dist/kiro-tui-forensic.d.ts +12 -0
  124. package/dist/kiro-tui-forensic.js +110 -0
  125. package/dist/kiro-tui-forensic.js.map +1 -0
  126. package/dist/kiro-usage-run.d.ts +7 -0
  127. package/dist/kiro-usage-run.js +39 -0
  128. package/dist/kiro-usage-run.js.map +1 -0
  129. package/dist/kiro-usage-scan.d.ts +49 -0
  130. package/dist/kiro-usage-scan.js +223 -0
  131. package/dist/kiro-usage-scan.js.map +1 -0
  132. package/dist/kiro-usage-worker.d.ts +1 -0
  133. package/dist/kiro-usage-worker.js +10 -0
  134. package/dist/kiro-usage-worker.js.map +1 -0
  135. package/dist/kiro-usage.d.ts +58 -0
  136. package/dist/kiro-usage.js +310 -0
  137. package/dist/kiro-usage.js.map +1 -0
  138. package/dist/locale.js +230 -8
  139. package/dist/locale.js.map +1 -1
  140. package/dist/log-service.js +1 -1
  141. package/dist/log-service.js.map +1 -1
  142. package/dist/login-url.d.ts +4 -0
  143. package/dist/login-url.js +22 -0
  144. package/dist/login-url.js.map +1 -0
  145. package/dist/needs-you-hub.d.ts +2 -1
  146. package/dist/needs-you-hub.js +1 -1
  147. package/dist/needs-you-hub.js.map +1 -1
  148. package/dist/needs-you.d.ts +10 -2
  149. package/dist/needs-you.js +10 -2
  150. package/dist/needs-you.js.map +1 -1
  151. package/dist/notice-delivery.d.ts +16 -0
  152. package/dist/notice-delivery.js +35 -0
  153. package/dist/notice-delivery.js.map +1 -0
  154. package/dist/office-share-page.d.ts +16 -0
  155. package/dist/office-share-page.js +85 -0
  156. package/dist/office-share-page.js.map +1 -0
  157. package/dist/office-share-service.d.ts +91 -0
  158. package/dist/office-share-service.js +176 -0
  159. package/dist/office-share-service.js.map +1 -0
  160. package/dist/office-share.d.ts +268 -0
  161. package/dist/office-share.js +574 -0
  162. package/dist/office-share.js.map +1 -0
  163. package/dist/opencode-usage-scan.d.ts +28 -0
  164. package/dist/opencode-usage-scan.js +92 -0
  165. package/dist/opencode-usage-scan.js.map +1 -0
  166. package/dist/opencode-usage-worker.d.ts +1 -0
  167. package/dist/opencode-usage-worker.js +10 -0
  168. package/dist/opencode-usage-worker.js.map +1 -0
  169. package/dist/outbound-handlers.js +22 -2
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +65 -2
  172. package/dist/outbound-schemas.js +56 -2
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-web-gateway.js +7 -1
  175. package/dist/public-web-gateway.js.map +1 -1
  176. package/dist/quickstart-api.js +13 -2
  177. package/dist/quickstart-api.js.map +1 -1
  178. package/dist/quickstart.js +2 -1
  179. package/dist/quickstart.js.map +1 -1
  180. package/dist/remote-card-answer.d.ts +83 -0
  181. package/dist/remote-card-answer.js +208 -0
  182. package/dist/remote-card-answer.js.map +1 -0
  183. package/dist/remote-card-prompt.d.ts +38 -0
  184. package/dist/remote-card-prompt.js +86 -0
  185. package/dist/remote-card-prompt.js.map +1 -0
  186. package/dist/reply-triggers.d.ts +2 -0
  187. package/dist/reply-triggers.js +3 -0
  188. package/dist/reply-triggers.js.map +1 -0
  189. package/dist/settings-api.d.ts +2 -0
  190. package/dist/settings-api.js +41 -16
  191. package/dist/settings-api.js.map +1 -1
  192. package/dist/settings-change.js +20 -14
  193. package/dist/settings-change.js.map +1 -1
  194. package/dist/settings-effect.js +7 -1
  195. package/dist/settings-effect.js.map +1 -1
  196. package/dist/settings-instance-patch.js +4 -1
  197. package/dist/settings-instance-patch.js.map +1 -1
  198. package/dist/setup-form.d.ts +1 -1
  199. package/dist/setup-form.js +55 -6
  200. package/dist/setup-form.js.map +1 -1
  201. package/dist/setup-host.d.ts +9 -0
  202. package/dist/setup-host.js +127 -6
  203. package/dist/setup-host.js.map +1 -1
  204. package/dist/setup-signin-assets.d.ts +2 -0
  205. package/dist/setup-signin-assets.js +6 -0
  206. package/dist/setup-signin-assets.js.map +1 -0
  207. package/dist/setup-wizard.js +2 -0
  208. package/dist/setup-wizard.js.map +1 -1
  209. package/dist/structured-signin-transport.d.ts +8 -0
  210. package/dist/structured-signin-transport.js +79 -0
  211. package/dist/structured-signin-transport.js.map +1 -0
  212. package/dist/structured-signin.d.ts +131 -0
  213. package/dist/structured-signin.js +388 -0
  214. package/dist/structured-signin.js.map +1 -0
  215. package/dist/tool-permissions.js +29 -0
  216. package/dist/tool-permissions.js.map +1 -1
  217. package/dist/topic-commands.js +6 -2
  218. package/dist/topic-commands.js.map +1 -1
  219. package/dist/transcript-sources.d.ts +27 -2
  220. package/dist/transcript-sources.js +104 -40
  221. package/dist/transcript-sources.js.map +1 -1
  222. package/dist/turn-reply-guard.d.ts +1 -1
  223. package/dist/turn-reply-guard.js +1 -1
  224. package/dist/turn-reply-guard.js.map +1 -1
  225. package/dist/types.d.ts +33 -8
  226. package/dist/ui/backend-signin.js +63 -10
  227. package/dist/ui/chat-store.js +3 -2
  228. package/dist/ui/chat-strings.js +4 -4
  229. package/dist/ui/fleet-office-editor.js +68 -11
  230. package/dist/ui/fleet-office-layout.js +110 -35
  231. package/dist/ui/fleet-office-model.js +6 -3
  232. package/dist/ui/fleet-office-overview.js +167 -0
  233. package/dist/ui/fleet-office-pack.d.ts +13 -3
  234. package/dist/ui/fleet-office-pack.js +214 -24
  235. package/dist/ui/fleet-office-skin.js +98 -23
  236. package/dist/ui/fleet-office-store.js +82 -5
  237. package/dist/ui/fleet-office-view.js +5 -4
  238. package/dist/ui/fleet-office-zones.js +232 -21
  239. package/dist/ui/fleet-office.js +549 -186
  240. package/dist/ui/fleet-usage.js +19 -15
  241. package/dist/ui/office-share-dialog.js +145 -0
  242. package/dist/ui/office-share-page.js +164 -0
  243. package/dist/ui/panel-chat.js +22 -3
  244. package/dist/ui/panel-log.js +4 -4
  245. package/dist/ui/panel-needs.js +5 -5
  246. package/dist/ui/panel-settings.js +38 -12
  247. package/dist/ui/settings-dialogs.js +8 -1
  248. package/dist/ui/settings-guests.js +17 -7
  249. package/dist/ui/settings-model.js +22 -6
  250. package/dist/ui/settings-registry.js +4 -0
  251. package/dist/ui/settings-strings.js +18 -2
  252. package/dist/ui/shared/app-i18n.js +4 -2
  253. package/dist/ui/shared/app-shell.js +14 -6
  254. package/dist/ui/shared/app-stream.js +2 -1
  255. package/dist/ui/shared/app.css +85 -4
  256. package/dist/ui/shared/office/cozy.png +0 -0
  257. package/dist/ui/shared/side-panel.js +95 -0
  258. package/dist/ui/structured-signin.js +115 -0
  259. package/dist/usage-report.d.ts +20 -0
  260. package/dist/usage-report.js +54 -17
  261. package/dist/usage-report.js.map +1 -1
  262. package/dist/usage-rollup.d.ts +9 -1
  263. package/dist/usage-rollup.js +20 -4
  264. package/dist/usage-rollup.js.map +1 -1
  265. package/dist/web-api.d.ts +23 -1
  266. package/dist/web-api.js +292 -3
  267. package/dist/web-api.js.map +1 -1
  268. package/dist/web-auth.js +1 -1
  269. package/dist/web-auth.js.map +1 -1
  270. package/dist/web-backend-login.d.ts +1 -7
  271. package/dist/web-backend-login.js +2 -24
  272. package/dist/web-backend-login.js.map +1 -1
  273. package/dist/web-office-acts.d.ts +173 -0
  274. package/dist/web-office-acts.js +285 -0
  275. package/dist/web-office-acts.js.map +1 -0
  276. package/dist/web-office-pack-read.d.ts +35 -0
  277. package/dist/web-office-pack-read.js +252 -0
  278. package/dist/web-office-pack-read.js.map +1 -0
  279. package/dist/web-office-pack-tools.d.ts +232 -0
  280. package/dist/web-office-pack-tools.js +754 -0
  281. package/dist/web-office-pack-tools.js.map +1 -0
  282. package/dist/web-office-skins.d.ts +71 -6
  283. package/dist/web-office-skins.js +321 -35
  284. package/dist/web-office-skins.js.map +1 -1
  285. package/dist/web-office-store.d.ts +5 -2
  286. package/dist/web-office-store.js +69 -10
  287. package/dist/web-office-store.js.map +1 -1
  288. package/dist/web-settings-patch.d.ts +2 -1
  289. package/dist/web-settings-patch.js +4 -1
  290. package/dist/web-settings-patch.js.map +1 -1
  291. package/dist/working-recovery.d.ts +2 -2
  292. package/dist/working-recovery.js +43 -8
  293. package/dist/working-recovery.js.map +1 -1
  294. package/dist/working-state-journal.d.ts +2 -0
  295. package/dist/working-state-journal.js +16 -1
  296. package/dist/working-state-journal.js.map +1 -1
  297. package/package.json +3 -2
  298. package/templates/windows-shortcuts.ps1 +155 -140
@@ -24,21 +24,45 @@
24
24
  // "areas": { named zones: a rect [x, y, w, h] and its seats, in the order
25
25
  // "desks": { "rect": [40, 120, 620, 440], "seats": [[120, 220], [260, 220], …] }, they fill up
26
26
  // "lounge": { "rect": [700, 120, 540, 260], "seats": [[760, 260], …] } },
27
+ // a seat is [x, y], or (#2691) { "x": 760, "y": 260, "for":
28
+ // ["atlas", …], "lend": false }: reserved for those instances
29
+ // (names checked like byInstance's, ≤ 16, none twice) — whoever
30
+ // of them comes to the area takes it first, and nobody else does
31
+ // unless "lend" (default false) lends it while they are all
32
+ // elsewhere (a borrower moves off when one comes). A checked
33
+ // seat is { x, y } — plus, reserved, `for` and `lend`
27
34
  // "stateZones": { "working": "desks", "idle": "lounge", "needs": "alert", "default": "lounge" },
28
35
  // the org chart's states (fleet-org.js ORG_STATES) → an area; a
29
36
  // state it does not name goes to "default", else the first area
30
37
  // "door": [640, 700], optional: where someone new walks in from (default: bottom middle)
31
38
  // "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …],
32
39
  // optional: the walkable graph (else people walk straight)
40
+ // "straight": true, optional (#2634): every walk goes across, then up or down — each
41
+ // diagonal leg an L, so the walkSide / walkDown / walkUp strips
42
+ // always fit (graph edges laid out across and up/down put the
43
+ // corners on the aisles); default false
33
44
  // "furniture": [ { "id": "rack", "sprite": STRIP, "x": 1020, "y": 140, "depth": 2,
34
- // "activeWhen": "crashed", "activeSprite": STRIP } ],
45
+ // "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "label": "Rack",
46
+ // "night": { "sprite": STRIP, "x": 300, "y": 420 },
47
+ // "walk": { "walk": STRIP, "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP } } ],
48
+ // "label" (#2574): a name tag under it, like a person's
49
+ // (an NPC drawn as furniture). "night" (#2568), optional: at bedtime
50
+ // (when the viewer has it on: 00:00–06:00 their time) drawn as this
51
+ // strip, playing, at (x, y) — its own place for either left out.
52
+ // "walk" (#2596), optional: it strolls near its (x, y) and back
53
+ // like an idle person, drawn walking by these strips as a
54
+ // character set's walking poses ("walk" required); not at bedtime
55
+ // with a "night".
35
56
  // drawn by depth: below 1000 under the people, 1000 and up over
36
57
  // them; "depth": "y" (the string) sorts it among the people by base
37
58
  // y — its anchor's y against each person's feet: someone whose feet
38
59
  // are lower down (a greater y; a tie too) is drawn after it, in
39
60
  // front. (x, y) is where the strip's anchor goes. `activeWhen` (a
40
61
  // state, or a list): while anyone shown is in it, the item plays its
41
- // strip (or `activeSprite`); otherwise it holds the first frame
62
+ // strip (or `activeSprite`); otherwise it holds the first frame —
63
+ // or, with "loop": true (#2570), plays its strip all along.
64
+ // "activeFor" (an instance's name, or a list): only their state
65
+ // counts, not anyone's (a room several people take turns in)
42
66
  // "characters": { "default": { "working": STRIP, "idle": STRIP, "paused": STRIP, "needs": STRIP, "walk": STRIP,
43
67
  // "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP },
44
68
  // "fox": { … } }, "default" with at least "idle" is required; a missing pose is idle.
@@ -47,6 +71,9 @@
47
71
  // walkSide, drawn as it is when moving right and mirrored when moving
48
72
  // left (so draw it facing right). A missing one is "walk", then idle
49
73
  // ("walk" itself is never mirrored)
74
+ // A set may also have "prop": STRIP (#2562) — what its people
75
+ // send: their messages, work and reports fly as it, in place of
76
+ // the pack's "props" (a set without one: the pack's props)
50
77
  // "byInstance": { "atlas": "fox" }, optional: an instance name → its character set
51
78
  // "props": { "task": STRIP, "send": STRIP, "report": STRIP, "broadcast": STRIP }, optional traffic items
52
79
  // "bubbles": { "en": { "working": ["On it."] }, "zh-TW": { … } } optional: short lines per state (≤ 12 a state,
@@ -82,6 +109,10 @@
82
109
  // { "pack": "cozy", the pack they were made on (on another pack they are ignored)
83
110
  // "seats": { "desks": [[120, 220], …] }, an area's seats: as many as the pack's, each inside the area's rect
84
111
  // "furniture": { "rack": [1000, 150] }, an item's (x, y)
112
+ // "furnitureNight": { "elder": [300, 420] }, an item's night place (#2665: an item with a "night" only)
113
+ // "seatFor": { "desks": { "0": { "for": ["atlas"], "lend": false } } },
114
+ // a seat's reservation by area and slot (#2691), in place of the
115
+ // pack's own for that seat; "for": [] is none (lends nothing)
85
116
  // "door": [640, 700],
86
117
  // "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
87
118
  // the walkable graph, replaced whole (edges need waypoints)
@@ -89,18 +120,30 @@
89
120
  // Every coordinate is on the same 1280×720 logical canvas, in the same ranges as the pack's. checkLayoutOverrides is
90
121
  // the shape (the host checks it on every PUT /ui/offices); applyOverrides puts them on a checked pack, refusing a
91
122
  // 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).
123
+ // that too, for a pack it keeps). A night place goes on the item's `night` (its x, y), so at bedtime (nightFurniture)
124
+ // it is the office's, else the pack's night x/y, else the item's (moved) day place. The host keeps `furnitureNight`
125
+ // and `seatFor` (#2691) beside the offices on disk, never inside them: an older host's stricter check would drop the
126
+ // office (#2665, web-office-store.ts).
93
127
 
94
128
  export const LOGICAL = { w: 1280, h: 720 };
95
129
  /** The people's depth: furniture below it is drawn under them, the rest over them. */
96
130
  export const CHAR_DEPTH = 1000;
97
131
  export const PACK_STATES = ["needs", "crashed", "stuck", "working", "idle", "paused", "stopped", "missing"];
98
132
  const POSES = ["working", "idle", "paused", "needs", "walk", "walkDown", "walkUp", "walkSide"], PROPS = ["task", "send", "report", "broadcast"];
133
+ /** A character set's strips: its poses, and what its people send (#2562). */
134
+ const SET_STRIPS = [...POSES, "prop"];
135
+ /** A furniture item's walking strips (#2596): a character set's walking poses. */
136
+ const WALKS = ["walk", "walkDown", "walkUp", "walkSide"];
99
137
  const BUILT_IN = new Set(["pixel", "studio"]);
138
+ /**
139
+ * Ids built in after packs could be uploaded under them (#2631: "cozy"): a new upload is refused, but a pack the host
140
+ * kept under one before (`kept`) is still read back, listed and deletable, and wins the id for the offices that name it.
141
+ */
142
+ const RESERVED = new Set(["cozy"]);
100
143
  const ID = /^[A-Za-z0-9_-]{1,40}$/;
101
144
  const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
102
145
  const HEX = /^#[0-9a-f]{6}$/i;
103
- const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40 };
146
+ const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40, seatFor: 16 };
104
147
 
105
148
  class Refused extends Error { constructor(at, why) { super(`${at}: ${why}`); } }
106
149
  /** `o[k]` when it is o's own property, else undefined: a name from a pack or a page never reaches Object.prototype. */
@@ -138,6 +181,47 @@ export function checkStrip(v, at = "strip", scale = 2) {
138
181
  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
182
  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
183
  }
184
+ /** 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. */
185
+ function nightOf(v, at, scale) {
186
+ if (v === undefined) return null;
187
+ obj(v, at); keys(v, ["sprite", "x", "y"], at);
188
+ 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`) };
189
+ }
190
+ /** A furniture item's walk (#2596): its walking strips, picked by heading as a character set's (poseStrip); "walk" required. */
191
+ function walkOf(v, at, scale) {
192
+ if (v === undefined) return null;
193
+ const w = strips(v, WALKS, at, scale);
194
+ if (!w.walk) throw new Refused(`${at}.walk`, "required");
195
+ return w;
196
+ }
197
+ /** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to `max` (8). */
198
+ function names(v, at, max = 8) {
199
+ 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; };
200
+ return Array.isArray(v) ? list(v, max, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
201
+ }
202
+ /** Whom a seat is reserved for (#2691): instances' names (as names()), up to LIMIT.seatFor, none twice; `empty` may be []. */
203
+ function owners(v, at, empty = false) {
204
+ const out = names(v, at, LIMIT.seatFor);
205
+ if (!out.length && !empty) throw new Refused(at, "at least one instance");
206
+ if (new Set(out).size !== out.length) throw new Refused(at, "a name twice");
207
+ return out;
208
+ }
209
+ /**
210
+ * A seat (#2691): [x, y], or { x, y, for, lend } — reserved for `for` (lent to anyone while they are all elsewhere when
211
+ * `lend`). Checked: { x, y }, with `for` and `lend` only when reserved, so a plain seat is what it always was.
212
+ */
213
+ function seatOf(v, at, rect) {
214
+ let p = null, extra = {};
215
+ if (isObj(v)) {
216
+ keys(v, ["x", "y", "for", "lend"], at);
217
+ p = { x: num(v.x, 0, LOGICAL.w, `${at}.x`), y: num(v.y, 0, LOGICAL.h, `${at}.y`) };
218
+ if (v.lend !== undefined && typeof v.lend !== "boolean") throw new Refused(`${at}.lend`, "true or false");
219
+ if (v.lend !== undefined && v.for === undefined) throw new Refused(`${at}.lend`, "only with for");
220
+ if (v.for !== undefined) extra = { for: owners(v.for, `${at}.for`), lend: v.lend === true };
221
+ } else p = point(v, at);
222
+ if (p.x < rect.x || p.x > rect.x + rect.w || p.y < rect.y || p.y > rect.y + rect.h) throw new Refused(at, "outside its area's rect");
223
+ return { ...p, ...extra };
224
+ }
141
225
  function strips(v, names, at, scale) {
142
226
  obj(v, at);
143
227
  keys(v, names, at);
@@ -148,16 +232,20 @@ function states(v, at) {
148
232
  return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
149
233
  }
150
234
 
151
- /** A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. */
152
- export function checkSkinPack(x) {
235
+ /**
236
+ * A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. `kept`: a pack the host
237
+ * keeps already (read back, not uploaded), which may hold a RESERVED id.
238
+ */
239
+ export function checkSkinPack(x, { kept = false } = {}) {
153
240
  try {
154
241
  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");
242
+ keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
156
243
  if (x.format !== 1) throw new Refused("format", "1");
157
244
  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");
245
+ if (BUILT_IN.has(x.id) || (!kept && RESERVED.has(x.id))) throw new Refused("id", "a built-in skin's id");
159
246
  const scale = x.scale === undefined ? 2 : int(x.scale, 1, 8, "scale");
160
247
  if (x.matte !== undefined && !matteOk(x.matte)) throw new Refused("matte", "a colour as #rrggbb");
248
+ if (x.straight !== undefined && typeof x.straight !== "boolean") throw new Refused("straight", "true or false");
161
249
  const areas = {};
162
250
  const rawAreas = x.areas === undefined ? {} : obj(x.areas, "areas");
163
251
  if (Object.keys(rawAreas).length > LIMIT.areas) throw new Refused("areas", `at most ${LIMIT.areas}`);
@@ -168,11 +256,7 @@ export function checkSkinPack(x) {
168
256
  if (!Array.isArray(a.rect) || a.rect.length !== 4) throw new Refused(`${at}.rect`, "[x, y, w, h]");
169
257
  const [rx, ry] = [num(a.rect[0], 0, LOGICAL.w, `${at}.rect[0]`), num(a.rect[1], 0, LOGICAL.h, `${at}.rect[1]`)];
170
258
  const rect = { x: rx, y: ry, w: num(a.rect[2], 1, LOGICAL.w - rx, `${at}.rect[2]`), h: num(a.rect[3], 1, LOGICAL.h - ry, `${at}.rect[3]`) };
171
- const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => {
172
- const p = point(s, `${at}.seats[${i}]`);
173
- if (p.x < rect.x || p.x > rect.x + rect.w || p.y < rect.y || p.y > rect.y + rect.h) throw new Refused(`${at}.seats[${i}]`, "outside its area's rect");
174
- return p;
175
- });
259
+ const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => seatOf(s, `${at}.seats[${i}]`, rect));
176
260
  if (!seats.length) throw new Refused(`${at}.seats`, "at least one seat");
177
261
  areas[name] = { name, rect, seats };
178
262
  }
@@ -204,20 +288,25 @@ export function checkSkinPack(x) {
204
288
  const fids = new Set();
205
289
  const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
206
290
  const at = `furniture[${i}]`;
207
- obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite"], at);
291
+ obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "label", "night", "walk"], at);
208
292
  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
293
  fids.add(f.id);
210
294
  if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
295
+ if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
296
+ if (f.loop !== undefined && typeof f.loop !== "boolean") throw new Refused(`${at}.loop`, "true or false");
297
+ 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
298
  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
299
  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) };
300
+ activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale),
301
+ 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),
302
+ walk: walkOf(f.walk, `${at}.walk`, scale) };
214
303
  }).sort((a, b) => order(a) - order(b));
215
304
  const sets = obj(x.characters, "characters");
216
305
  if (Object.keys(sets).length > LIMIT.sets) throw new Refused("characters", `at most ${LIMIT.sets} sets`);
217
306
  const characters = {};
218
307
  for (const [k, v] of Object.entries(sets)) {
219
308
  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);
309
+ characters[k] = strips(v, SET_STRIPS, `characters.${k}`, scale);
221
310
  if (!characters[k].idle) throw new Refused(`characters.${k}.idle`, "required");
222
311
  }
223
312
  if (!own(characters, "default")) throw new Refused("characters.default", "required");
@@ -248,7 +337,7 @@ export function checkSkinPack(x) {
248
337
  const opt = (v, at) => (v === undefined ? undefined : src(v, at));
249
338
  return { ok: true, pack: {
250
339
  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,
340
+ 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
341
  props: x.props === undefined ? {} : strips(x.props, PROPS, "props", scale), bubbles,
253
342
  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
343
  } };
@@ -261,13 +350,18 @@ export function checkSkinPack(x) {
261
350
  export const skinPackOk = (x) => { const r = checkSkinPack(x); return r.ok ? r.pack : null; };
262
351
  /** Every image a pack names (what a loader fetches before spriteSkin draws). */
263
352
  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]),
353
+ ...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
354
  ...Object.values(pack.characters).flatMap((s) => Object.values(s).map((st) => st.src)), ...Object.values(pack.props).map((st) => st.src)].filter(Boolean))];
266
355
 
267
356
  /** Every frame strip a checked pack names, with where (the host checks each image is frames·w × h). */
268
357
  export function packStrips(pack) {
269
358
  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 }); }
359
+ for (const f of pack.furniture) {
360
+ out.push({ at: `furniture.${f.id}.sprite`, strip: f.sprite });
361
+ if (f.activeSprite) out.push({ at: `furniture.${f.id}.activeSprite`, strip: f.activeSprite });
362
+ if (f.night) out.push({ at: `furniture.${f.id}.night.sprite`, strip: f.night.sprite });
363
+ for (const [k, st] of Object.entries(f.walk || {})) out.push({ at: `furniture.${f.id}.walk.${k}`, strip: st });
364
+ }
271
365
  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
366
  for (const [k, st] of Object.entries(pack.props)) out.push({ at: `props.${k}`, strip: st });
273
367
  return out;
@@ -276,11 +370,17 @@ export function packStrips(pack) {
276
370
  // ── Layout overrides ──
277
371
 
278
372
  const OV_LIMIT = { areas: LIMIT.areas, seats: LIMIT.seats, furniture: LIMIT.furniture, waypoints: LIMIT.waypoints, edges: LIMIT.edges };
373
+ /** A seat's slot in `seatFor` (#2691): its index in the area's order, as a key ("0", "1", …). */
374
+ const SLOT = /^(0|[1-9][0-9]{0,2})$/;
375
+ /** A seat's reservation as an override keeps it: { for, lend }, or null (not reserved). */
376
+ const reservedOf = (s) => (s.for && s.for.length ? { for: [...s.for], lend: s.lend === true } : null);
377
+ /** A seat with reservation `r` ({ for, lend }; for [] or null: none) in place of its own. */
378
+ const reserve = (s, r) => { const { for: _f, lend: _l, ...p } = s; return r && r.for.length ? { ...p, for: [...r.for], lend: r.lend === true } : p; };
279
379
  /** An office's layout overrides, strictly (their shape and ranges): { ok: true, overrides } or { ok: false, error }. */
280
380
  export function checkLayoutOverrides(x) {
281
381
  try {
282
382
  obj(x, "overrides");
283
- keys(x, ["pack", "seats", "furniture", "door", "waypoints", "edges"], "overrides");
383
+ keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "waypoints", "edges"], "overrides");
284
384
  if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
285
385
  const out = { pack: x.pack };
286
386
  if (x.seats !== undefined) {
@@ -305,6 +405,36 @@ export function checkLayoutOverrides(x) {
305
405
  out.furniture[id] = [q.x, q.y];
306
406
  }
307
407
  }
408
+ if (x.furnitureNight !== undefined) {
409
+ obj(x.furnitureNight, "overrides.furnitureNight");
410
+ if (Object.keys(x.furnitureNight).length > OV_LIMIT.furniture) throw new Refused("overrides.furnitureNight", `at most ${OV_LIMIT.furniture}`);
411
+ out.furnitureNight = {};
412
+ for (const [id, p] of Object.entries(x.furnitureNight)) {
413
+ if (!nameOk(id)) throw new Refused(`overrides.furnitureNight.${id}`, "an item's id");
414
+ const q = point(p, `overrides.furnitureNight.${id}`);
415
+ out.furnitureNight[id] = [q.x, q.y];
416
+ }
417
+ }
418
+ if (x.seatFor !== undefined) {
419
+ obj(x.seatFor, "overrides.seatFor");
420
+ if (Object.keys(x.seatFor).length > OV_LIMIT.areas) throw new Refused("overrides.seatFor", `at most ${OV_LIMIT.areas} areas`);
421
+ out.seatFor = {};
422
+ for (const [area, slots] of Object.entries(x.seatFor)) {
423
+ const at = `overrides.seatFor.${area}`;
424
+ if (!nameOk(area)) throw new Refused(at, "an area's name");
425
+ obj(slots, at);
426
+ if (Object.keys(slots).length > OV_LIMIT.seats) throw new Refused(at, `at most ${OV_LIMIT.seats} seats`);
427
+ out.seatFor[area] = {};
428
+ for (const [slot, r] of Object.entries(slots)) {
429
+ const here = `${at}.${slot}`;
430
+ if (!SLOT.test(slot) || Number(slot) >= OV_LIMIT.seats) throw new Refused(here, `a seat's number, 0 to ${OV_LIMIT.seats - 1}`);
431
+ obj(r, here); keys(r, ["for", "lend"], here);
432
+ if (!Array.isArray(r.for)) throw new Refused(`${here}.for`, "a list");
433
+ if (r.lend !== undefined && typeof r.lend !== "boolean") throw new Refused(`${here}.lend`, "true or false");
434
+ out.seatFor[area][slot] = { for: owners(r.for, `${here}.for`, true), lend: r.lend === true };
435
+ }
436
+ }
437
+ }
308
438
  if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
309
439
  if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
310
440
  if (x.waypoints !== undefined) {
@@ -347,12 +477,24 @@ export function applyOverrides(pack, ov) {
347
477
  const r = a.rect;
348
478
  areas[name] = { ...a, seats: pts.map(([x, y], i) => {
349
479
  if (x < r.x || x > r.x + r.w || y < r.y || y > r.y + r.h) throw new Refused(`${at}[${i}]`, "outside its area's rect");
350
- return { x, y };
480
+ return { ...a.seats[i], x, y }; // its reservation kept (#2691)
351
481
  }) };
352
482
  }
483
+ for (const [name, slots] of Object.entries(ov.seatFor || {})) {
484
+ const a = own(areas, name), at = `overrides.seatFor.${name}`;
485
+ if (!a) throw new Refused(at, "an area the pack defines");
486
+ for (const slot of Object.keys(slots)) if (!a.seats[Number(slot)]) throw new Refused(`${at}.${slot}`, `a seat the area has (it has ${a.seats.length})`);
487
+ areas[name] = { ...a, seats: a.seats.map((s, i) => { const v = own(slots, String(i)); return v ? reserve(s, v) : s; }) };
488
+ }
353
489
  const fids = new Set(pack.furniture.map((f) => f.id));
354
490
  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; });
491
+ const nids = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
492
+ 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");
493
+ const furniture = pack.furniture.map((f) => {
494
+ const q = own(ov.furniture, f.id), n = own(ov.furnitureNight, f.id);
495
+ if (!q && !n) return f;
496
+ return { ...f, ...(q ? { x: q[0], y: q[1] } : {}), ...(n ? { night: { ...f.night, x: n[0], y: n[1] } } : {}) };
497
+ });
356
498
  const door = ov.door ? { x: ov.door[0], y: ov.door[1] } : pack.door;
357
499
  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
500
  return { ok: true, pack: { ...pack, areas, furniture, door, ...graph } };
@@ -362,6 +504,40 @@ export function applyOverrides(pack, ov) {
362
504
  }
363
505
  }
364
506
 
507
+ /**
508
+ * #2665: overrides with only the night places `pack` can take (an item it has, with a `night`): a pack changed since
509
+ * (an item dropped, or its night) drops that entry, never the rest of the layout. applyOverrides itself stays strict
510
+ * (a write naming an item the pack lacks is refused, as for `furniture`).
511
+ */
512
+ export function fitNight(pack, ov) {
513
+ if (!ov || !ov.furnitureNight || ov.pack !== pack.id) return ov;
514
+ const ok = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
515
+ const kept = Object.entries(ov.furnitureNight).filter(([id]) => ok.has(id));
516
+ if (kept.length === Object.keys(ov.furnitureNight).length) return ov;
517
+ const { furnitureNight: _gone, ...rest } = ov;
518
+ return kept.length ? { ...rest, furnitureNight: Object.fromEntries(kept) } : rest;
519
+ }
520
+
521
+ /**
522
+ * #2691: overrides with only the reservations `pack` can take (an area it has, a seat that area has): a pack changed
523
+ * since drops that entry, never the rest of the layout (as fitNight; applyOverrides stays strict).
524
+ */
525
+ export function fitSeatFor(pack, ov) {
526
+ if (!ov || !ov.seatFor || ov.pack !== pack.id) return ov;
527
+ let dropped = false;
528
+ const kept = {};
529
+ for (const [area, slots] of Object.entries(ov.seatFor)) {
530
+ const a = own(pack.areas, area);
531
+ const ok = Object.entries(slots).filter(([slot]) => a && !!a.seats[Number(slot)]);
532
+ // An area the pack no longer has is dropped even with no slots under it (`{}`: 0 kept of 0, yet applyOverrides refuses it).
533
+ if (!a || ok.length !== Object.keys(slots).length) dropped = true;
534
+ if (ok.length) kept[area] = Object.fromEntries(ok);
535
+ }
536
+ if (!dropped) return ov;
537
+ const { seatFor: _gone, ...rest } = ov;
538
+ return Object.keys(kept).length ? { ...rest, seatFor: kept } : rest;
539
+ }
540
+
365
541
  /**
366
542
  * What an edited layout keeps as overrides: only the parts that differ from the pack's own (`edited` is a pack with
367
543
  * the same areas and furniture, positions moved); null when nothing does (the office goes back to the pack's).
@@ -370,11 +546,22 @@ export function overridesBetween(pack, edited) {
370
546
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
371
547
  const ov = { pack: pack.id };
372
548
  for (const [name, a] of Object.entries(pack.areas)) {
373
- const e = own(edited.areas, name);
374
- if (e && !same(a.seats, e.seats)) (ov.seats ||= {})[name] = e.seats.map((s) => [s.x, s.y]);
549
+ const e = own(edited.areas, name), xy = (s) => [s.x, s.y];
550
+ if (e && !same(a.seats.map(xy), e.seats.map(xy))) (ov.seats ||= {})[name] = e.seats.map(xy);
551
+ // #2691: a seat whose reservation differs from the pack's (none where the pack has one: for []).
552
+ if (e) e.seats.forEach((s, i) => {
553
+ const was = a.seats[i] ? reservedOf(a.seats[i]) : null, now = reservedOf(s);
554
+ if (a.seats[i] && !same(was, now)) ((ov.seatFor ||= {})[name] ||= {})[String(i)] = now || { for: [], lend: false };
555
+ });
375
556
  }
376
557
  const at = new Map(pack.furniture.map((f) => [f.id, f]));
377
558
  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]; }
559
+ // #2665: a night place that differs from the pack's (its own x/y, or in place: null); in place again is no entry.
560
+ for (const f of edited.furniture) {
561
+ const o = at.get(f.id);
562
+ if (!o || !o.night || !f.night || f.night.x === null || f.night.y === null) continue;
563
+ if (o.night.x !== f.night.x || o.night.y !== f.night.y) (ov.furnitureNight ||= {})[f.id] = [f.night.x, f.night.y];
564
+ }
378
565
  if (pack.door.x !== edited.door.x || pack.door.y !== edited.door.y) ov.door = [edited.door.x, edited.door.y];
379
566
  if (!same(pack.waypoints, edited.waypoints) || !same(pack.edges, edited.edges)) {
380
567
  ov.waypoints = edited.waypoints.map((w) => ({ id: w.id, x: w.x, y: w.y }));
@@ -403,6 +590,9 @@ export function poseStrip(set, pose, heading = null) {
403
590
  const st = own(set, pose) || own(set, "idle");
404
591
  return st ? { strip: st, mirror: false } : null;
405
592
  }
593
+ /** The strip a furniture item shows while it walks (#2596: an NPC with `walk`, `walking` and `heading` set where it is
594
+ * drawn): its walk strip by heading, as a character's (poseStrip); null when it stands (its own sprite, as before). */
595
+ export const walkStrip = (f) => (f && f.walking && f.walk ? poseStrip(f.walk, "walk", f.heading) : null);
406
596
  /** The box (logical units) a strip covers with its anchor at (x, y): its displayed size; mirrored about the anchor. */
407
597
  export function stripBox(st, x, y, mirror = false) {
408
598
  const kx = st.dw / st.w, ky = st.dh / st.h;
@@ -1,8 +1,9 @@
1
1
  // The Office view's art (fleet-office.js draws with it on a <canvas>). A skin draws a layout that fleet-office-model.js
2
- // (desks) or fleet-office-zones.js (zones) made. Two skins are built in and draw everything procedurally — little
3
- // pixel people, desks, lamps — so no art ships: "pixel" (the desks layout: a room per team, a desk per instance) and
4
- // "studio" (the zones layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their
5
- // state). Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
2
+ // (desks) or fleet-office-zones.js (zones) made. Three skins are built in. Two draw everything procedurally — little
3
+ // pixel people, desks, lamps: "pixel" (the desks layout: a room per team, a desk per instance) and "studio" (the zones
4
+ // layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their state). The third,
5
+ // "cozy" (#2631), is the zones layout on one picture shipped with the page (a warm pixel-art office), the same built-in
6
+ // people in it. Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
6
7
  // format v1 in fleet-office-pack.js) can replace any part; a part a pack does not supply falls back to the built-in
7
8
  // drawing. An admin uploads a pack (#2513, the host keeps it: src/web-office-skins.ts); loadPackSkin() reads it back —
8
9
  // its manifest checked here again (checkSkinPack), its pictures by their content ids — and spriteSkin() draws it.
@@ -40,8 +41,9 @@
40
41
  // pictures and draws the characters, props and bubbles each frame. In zones the background is kept; furniture, people (walking
41
42
  // along the pack's waypoints), tags, props and bubbles are drawn each frame, furniture by depth around the people (depth "y":
42
43
  // among them by base y), then the foreground, then the crowd counts, tags and bubbles.
44
+ import { t } from "/assets/app-i18n.js";
43
45
  import { frameAt, nameHash } from "./fleet-office-model.js";
44
- import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox } from "./fleet-office-pack.js";
46
+ import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox, walkStrip } from "./fleet-office-pack.js";
45
47
 
46
48
  export { nameHash };
47
49
 
@@ -222,14 +224,16 @@ function drawDeskFront(ctx, d, look, env) {
222
224
  ctx.fillStyle = pal.text2; ctx.font = "600 8px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
223
225
  ctx.fillText(fitText(ctx, env.labels.closed, 32), d.x + 48, d.y + 63);
224
226
  }
225
- // The name tag.
226
- ctx.fillStyle = pal.surface;
227
- ctx.fillRect(d.x + 6, d.y + 78, d.w - 12, 16);
228
- ctx.strokeStyle = look.match ? pal.focus : pal.border;
229
- ctx.lineWidth = look.match ? 2 : 1;
230
- ctx.strokeRect(d.x + 6.5, d.y + 78.5, d.w - 13, 15);
231
- ctx.fillStyle = pal.text; ctx.font = "600 11px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
232
- ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
227
+ // The name tag (#2554: none when `notag` — a tile too small to read it; the page draws it again on hover).
228
+ if (!look.notag) {
229
+ ctx.fillStyle = pal.surface;
230
+ ctx.fillRect(d.x + 6, d.y + 78, d.w - 12, 16);
231
+ ctx.strokeStyle = look.match ? pal.focus : pal.border;
232
+ ctx.lineWidth = look.match ? 2 : 1;
233
+ ctx.strokeRect(d.x + 6.5, d.y + 78.5, d.w - 13, 15);
234
+ ctx.fillStyle = pal.text; ctx.font = "600 11px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
235
+ ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
236
+ }
233
237
  ctx.restore();
234
238
  if (look.match) { ctx.save(); ctx.strokeStyle = pal.focus; ctx.lineWidth = 2; ctx.strokeRect(d.x + 2, d.y + 2, d.w - 4, d.h - 6); ctx.restore(); }
235
239
  }
@@ -374,7 +378,7 @@ const STUDIO_FURNITURE = [
374
378
  ].sort((a, b) => a.depth - b.depth);
375
379
  /** The studio's pack: the same shape as a checked one (fleet-office-pack.js), furniture by `shape` instead of sprites. */
376
380
  export const STUDIO_PACK = {
377
- format: 1, id: "studio", scale: 1, areas: STUDIO_AREAS, door: { x: HALL_X, y: 704 }, furniture: STUDIO_FURNITURE, ...studioGraph(STUDIO_AREAS),
381
+ format: 1, id: "studio", scale: 1, areas: STUDIO_AREAS, door: { x: HALL_X, y: 704 }, furniture: STUDIO_FURNITURE, ...studioGraph(STUDIO_AREAS), straight: false,
378
382
  stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
379
383
  characters: {}, byInstance: {}, props: {}, bubbles: {},
380
384
  };
@@ -511,8 +515,12 @@ export function spriteSkin(pack, images, base = pixelSkin) {
511
515
  },
512
516
  drawFurniture(ctx, f, active, ms, env) {
513
517
  if (f.shape) { drawFurniture(ctx, f, active, ms, env); return; }
514
- // Active: its strip (or activeSprite) plays; otherwise its first frame holds.
518
+ // An NPC walking (#2596): its walk strip for its heading (walkSide mirrored going left), when its picture is loaded.
519
+ const ws = walkStrip(f);
520
+ if (ws && strip(ctx, ws.strip, ms, f.x, f.y, null, ws.mirror)) return;
521
+ // Active: its activeSprite (or its strip) plays; otherwise its strip loops ("loop", #2570) or its first frame holds.
515
522
  if (active) strip(ctx, f.activeSprite || f.sprite, ms, f.x, f.y);
523
+ else if (f.loop) strip(ctx, f.sprite, ms, f.x, f.y);
516
524
  else strip(ctx, f.sprite, 0, f.x, f.y, 0);
517
525
  },
518
526
  drawCharacter(ctx, d, look, ms, env) {
@@ -528,18 +536,84 @@ export function spriteSkin(pack, images, base = pixelSkin) {
528
536
  const ps = shown({ ...look, heading: at.heading }, !!at.walking);
529
537
  return ps ? stripBox(ps.strip, at.x, at.y, ps.mirror) : base.charBox(look, at);
530
538
  },
531
- drawProp(ctx, it, ms, env) { if (!strip(ctx, own(pack.props, it.hue), ms, it.x, it.y)) base.drawProp(ctx, it, ms, env); },
539
+ // An item in the air: its sender's own (their set's "prop", #2562), else the pack's for its kind, else the built-in one.
540
+ drawProp(ctx, it, ms, env) {
541
+ const mine = it.from ? own(setOf({ name: it.from }) || {}, "prop") : null;
542
+ if (!strip(ctx, mine || own(pack.props, it.hue), ms, it.x, it.y)) base.drawProp(ctx, it, ms, env);
543
+ },
532
544
  };
533
545
  }
534
546
 
547
+ // ── The built-in cozy (#2631): the zones layout on a warm pixel-art office drawn as one picture, the built-in people ──
548
+
549
+ /** Its picture, shipped with the page (src/ui/shared/office/cozy.png, auth-api.ts ASSETS: public static, as the font). */
550
+ export const COZY_BACKGROUND = "/assets/office-cozy.png";
551
+ const pts = (...xy) => xy.map(([x, y]) => ({ x, y }));
552
+ // The seats are feet points on the picture: chairs in front of the desks, the sofa, the coffee table and machine, the
553
+ // beanbags (a sleeper's bottom), in front of the whiteboard. The rects put a crowd's first extra spots (fleet-office-zones.js
554
+ // overflowSpots: rows from the rect's top) on the aisles: under the first row of desks, in front of the sofa.
555
+ const COZY_AREAS = {
556
+ desks: { name: "desks", rect: { x: 130, y: 196, w: 430, h: 220 }, seats: pts([193, 238], [336, 238], [481, 238], [193, 354], [336, 354], [481, 354]) },
557
+ lounge: { name: "lounge", rect: { x: 700, y: 196, w: 476, h: 184 }, seats: pts([940, 240], [1025, 310], [1120, 305]) },
558
+ alert: { name: "alert", rect: { x: 900, y: 500, w: 220, h: 110 }, seats: pts([965, 570], [1045, 570]) },
559
+ nap: { name: "nap", rect: { x: 110, y: 470, w: 400, h: 150 }, seats: pts([180, 562], [300, 562], [422, 565]) },
560
+ };
561
+ /** Its walkable graph, on the picture's aisles, every edge across or up/down; each seat's nearest waypoint is in front of it. */
562
+ const COZY_WAYPOINTS = [
563
+ ["door", 642, 640], ["hall", 642, 395], ["top", 642, 175],
564
+ // the desks: the aisle under the first row (y 250), the one under the second (y 395), the column beside them (x 560)
565
+ ["d-east", 560, 395], ["d-up", 560, 250], ["d1-3", 481, 250], ["d1-2", 336, 250], ["d1-1", 193, 250],
566
+ ["d2-3", 481, 395], ["d2-2", 336, 395], ["d2-x", 300, 395], ["d2-1", 193, 395],
567
+ // the beanbags: the aisle above them (y 460)
568
+ ["n-x", 300, 460], ["n-1", 180, 460], ["n-3", 422, 460],
569
+ // the lounge: below the coffee table (y 340), up the sofa's west end; the coffee machine's column (x 1140)
570
+ ["l-hall", 946, 395], ["l-mid", 946, 340], ["l-west", 860, 340], ["l-sofa", 860, 250], ["l-table", 1025, 340],
571
+ ["e-hall", 1140, 395], ["e-coffee", 1140, 305],
572
+ // the whiteboard: in front of it (y 570), from the coffee machine's column
573
+ ["a-east", 1140, 570], ["a-mid", 1005, 570],
574
+ ].map(([id, x, y]) => ({ id, x, y }));
575
+ const COZY_EDGES = [
576
+ ["door", "hall"], ["hall", "top"], ["hall", "d-east"], ["d-east", "d2-3"], ["d2-3", "d2-2"], ["d2-2", "d2-x"], ["d2-x", "d2-1"],
577
+ ["d-east", "d-up"], ["d-up", "d1-3"], ["d1-3", "d1-2"], ["d1-2", "d1-1"], ["d2-x", "n-x"], ["n-x", "n-1"], ["n-x", "n-3"],
578
+ ["hall", "l-hall"], ["l-hall", "l-mid"], ["l-mid", "l-west"], ["l-west", "l-sofa"], ["l-mid", "l-table"],
579
+ ["l-hall", "e-hall"], ["e-hall", "e-coffee"], ["e-hall", "a-east"], ["a-east", "a-mid"],
580
+ ];
581
+ /** The cozy's pack: the picture is the room; the one item is the notice board, its tag saying a pack can restyle it. */
582
+ export const COZY_PACK = {
583
+ format: 1, id: "cozy", scale: 1, background: COZY_BACKGROUND, areas: COZY_AREAS, door: { x: 642, y: 662 },
584
+ waypoints: COZY_WAYPOINTS, edges: COZY_EDGES, straight: true,
585
+ furniture: [{ id: "notice", x: 1211, y: 628, depth: 5, get label() { return t("orgo.cozyNotice"); } }],
586
+ stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
587
+ characters: {}, byInstance: {}, props: {}, bubbles: {},
588
+ };
589
+ /** Until its picture is in (loadCozySkin): the studio's floors under the cozy's areas. */
590
+ const cozyBare = spriteSkin(COZY_PACK, new Map(), studioSkin);
591
+ let cozyJob = null;
592
+
535
593
  /** The skins built into this page; an uploaded pack is loaded by loadPackSkin. */
536
- export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin]]);
537
- export const BUILT_IN_SKINS = ["pixel", "studio"];
594
+ export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin], ["cozy", cozyBare]]);
595
+ export const BUILT_IN_SKINS = ["pixel", "studio", "cozy"];
596
+ /**
597
+ * #2631: the cozy's picture, loaded once; the skin drawn from it then takes the cozy's place in SKINS (skinFor). The
598
+ * skin, or null while it cannot load (the bare one stays; tried again next time).
599
+ */
600
+ export function loadCozySkin({ image = defaultImage } = {}) {
601
+ if (SKINS.get("cozy") !== cozyBare) return Promise.resolve(SKINS.get("cozy"));
602
+ cozyJob ??= image(COZY_BACKGROUND).then((im) => {
603
+ cozyJob = null;
604
+ if (!im) return null;
605
+ const sk = spriteSkin(COZY_PACK, new Map([[COZY_BACKGROUND, im]]), studioSkin);
606
+ SKINS.set("cozy", sk);
607
+ return sk;
608
+ }, () => { cozyJob = null; return null; });
609
+ return cozyJob;
610
+ }
538
611
  /** A skin by id: a built-in one, else a pack loaded here (`packs`: id → skin), else the default (pixel). */
539
612
  export const skinFor = (id, packs = null) => SKINS.get(id) || (packs && packs.get(id)) || pixelSkin;
540
613
 
541
614
  const loaded = new Map(); // `${id}@${sha}` → Promise<OfficeSkin | null>
542
- const MAX_LOADED = 16;
615
+ // Room for every pack the host keeps (web-office-skins.ts SKIN_LIMITS.packs) and a few replaced ones (each a new sha).
616
+ const MAX_LOADED = 24;
543
617
  const defaultImage = (url) => new Promise((resolve) => {
544
618
  if (typeof Image !== "function") { resolve(null); return; }
545
619
  const im = new Image();
@@ -550,9 +624,10 @@ const defaultImage = (url) => new Promise((resolve) => {
550
624
  /**
551
625
  * An uploaded pack (GET /ui/office-skins/<id>?v=<sha>) as an OfficeSkin: its manifest checked (null when it is not a
552
626
  * pack: the page then draws a built-in style and says so), its pictures loaded from their content ids (one that does
553
- * not load is drawn by the built-in skin). Kept per id and sha, so a pack is fetched once.
627
+ * not load is drawn by the built-in skin). Kept per id and sha, so a pack is fetched once. `url`: where a picture's path
628
+ * is read (a share link's page reads its own address, #2636).
554
629
  */
555
- export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = defaultImage } = {}) {
630
+ export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = defaultImage, url = (p) => p } = {}) {
556
631
  const key = `${id}@${sha}`;
557
632
  if (loaded.has(key)) return loaded.get(key);
558
633
  const base = `/ui/office-skins/${encodeURIComponent(id)}`;
@@ -561,14 +636,14 @@ export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = d
561
636
  const r = await get(`${base}?v=${encodeURIComponent(sha)}`, { method: "GET", headers: { "Content-Type": "application/json" } });
562
637
  if (!r || !r.ok) return null;
563
638
  const d = await r.json();
564
- const c = checkSkinPack(d && d.manifest);
639
+ const c = checkSkinPack(d && d.manifest, { kept: true });
565
640
  if (!c.ok || c.pack.id !== id) return null;
566
641
  const files = d.files && typeof d.files === "object" ? d.files : {};
567
642
  const images = new Map();
568
643
  await Promise.all(packImages(c.pack).map(async (src) => {
569
644
  const fid = own(files, src);
570
645
  if (typeof fid !== "string" || !/^[0-9a-f]{32}$/.test(fid)) return;
571
- const im = await image(`${base}/img/${fid}.png`);
646
+ const im = await image(url(`${base}/img/${fid}.png`));
572
647
  if (im) images.set(src, im);
573
648
  }));
574
649
  return spriteSkin(c.pack, images);