@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
@@ -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);
@@ -3,14 +3,16 @@
3
3
  // what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
4
4
  // - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
5
5
  // office is shown, the auto office's member filter, and its style (skin); #2540: the view (zoom and centre) and the
6
- // fit mode (Fit or Fill) per office.
6
+ // fit mode (Fit or Fill) per office; #2554: whether All offices shows the lobby tile.
7
7
  // - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
8
8
  // src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
9
9
  // they read (a change in between answers 409).
10
10
  import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
11
11
  import { fitModeOk, savedOk } from "./fleet-office-view.js";
12
12
 
13
- const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep";
13
+ const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep", NIGHT_KEY = "agend_org_office_night";
14
+ /** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
15
+ const LOBBY_KEY = "agend_org_office_lobby";
14
16
  /** #2540: { "<office>@<style>": [k, cx, cy] (and a 1 after them: a fit) } — the last VIEWS_MAX views, the most recent last. */
15
17
  const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
16
18
  /** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
@@ -28,6 +30,20 @@ export function skinListOk(d) {
28
30
  }
29
31
 
30
32
  /** `fetch`: the caller's (a lease's, so an answer after the person moved on is dropped); `storage`: default localStorage. */
33
+ /**
34
+ * Agents' acts as the host sends them (GET /ui/office-acts, the `office_acts` event), checked: the well-formed ones.
35
+ * `now`: the host's clock when it sent them — each end is moved onto this page's clock (they may differ).
36
+ */
37
+ export function actsOk(list, now) {
38
+ const skew = typeof now === "number" && Number.isFinite(now) ? now - Date.now() : 0;
39
+ const str = (v) => typeof v === "string" && v.length > 0 && v.length <= 200, num = (v) => typeof v === "number" && Number.isFinite(v);
40
+ return (Array.isArray(list) ? list : []).filter((a) => a && typeof a === "object" && str(a.instance) && str(a.office) && num(a.until)
41
+ && (a.x === undefined || num(a.x)) && (a.y === undefined || num(a.y)) && (a.person === undefined || str(a.person))
42
+ && (a.say === undefined || (typeof a.say === "string" && a.say.length <= 40)) && (a.pose === undefined || ["idle", "working", "needs", "paused"].includes(a.pose)))
43
+ .map((a) => ({ instance: a.instance, office: a.office, until: a.until - skew, ...(num(a.since) ? { since: a.since } : {}), ...(str(a.state) ? { state: a.state } : {}),
44
+ ...(num(a.x) && num(a.y) ? { x: a.x, y: a.y } : {}),
45
+ ...(a.person ? { person: a.person } : {}), ...(a.say ? { say: a.say } : {}), ...(a.pose ? { pose: a.pose } : {}) }));
46
+ }
31
47
  export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {}) {
32
48
  const st = () => { try { return storage || globalThis.localStorage || null; } catch { return null; } };
33
49
  const read = (k) => { try { const s = st(); return s ? s.getItem(k) : null; } catch { return null; } };
@@ -55,6 +71,15 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
55
71
  return r.ok ? { ok: true, rev: d.rev, offices: officesOk(d.offices) } : { ok: false, status: r.status, error: d.error || "offices_failed" };
56
72
  } catch { return { ok: false, status: 0, error: "offices_failed" }; }
57
73
  },
74
+ /** #2578: what agents are doing in the offices now (office_set): their acts, or null when they could not be read. */
75
+ async acts() {
76
+ try {
77
+ const r = await get("/ui/office-acts", { method: "GET", headers });
78
+ if (!r || !r.ok) return null;
79
+ const d = await r.json();
80
+ return actsOk(d && d.acts, d && d.now);
81
+ } catch { return null; }
82
+ },
58
83
  /** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
59
84
  async skins() {
60
85
  try {
@@ -80,17 +105,26 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
80
105
  return r.ok ? { ok: true } : { ok: false, status: r.status, error: d.error || "skin_failed" };
81
106
  } catch { return { ok: false, status: 0, error: "skin_failed" }; }
82
107
  },
83
- /** The office shown here: an id (one the host no longer has falls back to the auto office where it is used). */
108
+ /**
109
+ * The office shown here: an id (one the host no longer has falls back to the auto office where it is used), or
110
+ * #2554's All offices (fleet-office-overview.js ALL_ID; with fewer than two offices stored: the auto office).
111
+ */
84
112
  choice: () => { const v = read(CHOICE_KEY); return typeof v === "string" && v && v.length <= 40 ? v : AUTO_ID; },
85
113
  saveChoice: (id) => write(CHOICE_KEY, String(id)),
86
114
  filter: () => { const s = read(FILTER_KEY); try { return filterOk(s ? JSON.parse(s) : null); } catch { return filterOk(null); } },
87
115
  saveFilter: (f) => write(FILTER_KEY, JSON.stringify(filterOk(f))),
88
- /** The whole fleet's style here: a skin id ("pixel": desks, "studio": zones); an unknown one is the default where used. */
89
- skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "pixel"; },
116
+ /** The whole fleet's style here: a skin id ("pixel": desks, "studio" and "cozy": zones); none chosen yet: cozy (#2631); an unknown one is the default where used. */
117
+ skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "cozy"; },
90
118
  saveSkin: (id) => write(SKIN_KEY, String(id)),
91
119
  /** Whether paused and stopped people are left out of the Office here (default: shown). */
92
120
  hideAsleep: () => read(ASLEEP_KEY) === "1",
93
121
  saveHideAsleep: (v) => write(ASLEEP_KEY, v ? "1" : "0"),
122
+ /** #2568: whether the idle go to bed at night (00:00–06:00 on this device's clock) here (default: off). */
123
+ nightOn: () => read(NIGHT_KEY) === "1",
124
+ saveNightOn: (v) => write(NIGHT_KEY, v ? "1" : "0"),
125
+ /** #2554: whether All offices shows the lobby tile (the instances in no stored office) here (default: shown). */
126
+ lobbyTile: () => read(LOBBY_KEY) !== "0",
127
+ saveLobbyTile: (v) => write(LOBBY_KEY, v ? "1" : "0"),
94
128
  /** The view kept for `key` (an office and its style): { k, cx, cy } (fleet-office-view.js savedOf), or null. */
95
129
  view: (key) => {
96
130
  try { const m = JSON.parse(read(VIEW_KEY) || "{}"); const a = m && Object.prototype.hasOwnProperty.call(m, key) ? m[key] : null;
@@ -9,7 +9,8 @@
9
9
  // whole scene shows — non-integer, so the scene fills the panel's width or height. "cover" (Fill): the smallest
10
10
  // scale that fills both; what overflows is cropped and stays a pan away. Either is centred.
11
11
  // - The view never loses the scene (clampPan): along an axis where the scene is larger than the panel it covers the
12
- // panel (no gap at an edge); where it is smaller it stays wholly inside.
12
+ // panel (no gap at an edge); where it is smaller it is centred on that axis (the user: the canvas's middle is the
13
+ // scene's), on opening, Fit, any zoom and a resize alike.
13
14
  //
14
15
  // Around the scene is a matte: the pack's `matte` (fleet-office-pack.js), else the colour of the background's edge
15
16
  // pixels (sampled once per style: edgeMatte), else the skin's own (Studio's hall floor; Desks': the dashboard's
@@ -65,13 +66,13 @@ export function fitScene(w, h, size, mode = "contain", margin = 0) {
65
66
  * 4/3 at 150 %, 0.8 at 125 %); the layout wraps at the panel's width over it, so it opens as wide as the panel.
66
67
  */
67
68
  export const desksK = (dpr = 1) => Math.max(1, Math.round(dpr)) / dpr;
68
- /** The view a scene opens on with nothing kept: zones fitted (the office's mode), desks from the top left at desksK. */
69
+ /** The view a scene opens on with nothing kept: zones fitted (the office's mode), desks from the top left at desksK (centred by clampPan on an axis where it is smaller). */
69
70
  export const openingView = (w, h, size, dpr, zones, mode = "contain") => (zones ? fitScene(w, h, size, mode) : { k: desksK(dpr), x: 0, y: 0 });
70
71
  /** One axis of clampPan: offset `o` of a scene `s` long in a viewport `n` long. */
71
- const clampAxis = (o, s, n) => (s >= n ? Math.min(0, Math.max(n - s, o)) : Math.min(n - s, Math.max(0, o)));
72
+ const clampAxis = (o, s, n) => (s >= n ? Math.min(0, Math.max(n - s, o)) : (n - s) / 2);
72
73
  /**
73
74
  * The view kept on the scene: along an axis where the scene is at least the viewport it covers the viewport (no gap at
74
- * either edge); where it is smaller it stays wholly inside. Nothing can be scrolled off-screen.
75
+ * either edge); where it is smaller it is centred on that axis. Nothing can be scrolled off-screen.
75
76
  */
76
77
  export function clampPan(v, w, h, size) {
77
78
  return { k: v.k, x: clampAxis(v.x, w * v.k, size.w), y: clampAxis(v.y, h * v.k, size.h) };
@@ -3,7 +3,8 @@
3
3
  // changes it walks (along the pack's waypoints) to the new area. Pure — fleet-office.js draws it. Also here: what a
4
4
  // character says in its speech bubble, and which furniture is active.
5
5
  import { LOGICAL, own } from "./fleet-office-pack.js";
6
- import { nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
6
+ import { FLIGHT, nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
7
+ import { hue } from "./fleet-traffic.js";
7
8
 
8
9
  /** The area for a state: the pack's own, else its "default", else its first area. */
9
10
  export function zoneOf(pack, state) {
@@ -73,18 +74,22 @@ export function assignSeats(pack, wants, prev = new Map()) {
73
74
 
74
75
  /**
75
76
  * The zones layout for `nodes` (buildOrgView's) on a pack: everyone but the missing at their seat. The same shape as
76
- * the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph, the
77
- * seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
77
+ * the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph (and
78
+ * its `straight`, #2634), the seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
78
79
  */
79
- export function zonesLayout(pack, nodes, prev = new Map()) {
80
- const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: zoneOf(pack, n.state) }));
80
+ export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {}) {
81
+ // At night (#2568) the idle go to bed: the area asleep people go to while it has a free seat; the rest doze where they are.
82
+ const bed = zoneOf(pack, "paused"), beds = (own(pack.areas, bed)?.seats || []).length, sleepers = new Set(), inBed = new Set();
83
+ let used = nodes.filter((n) => n.state !== "idle" && zoneOf(pack, n.state) === bed).length;
84
+ if (night) for (const n of nodes) if (n.state === "idle") { sleepers.add(n.name); if (used < beds) { used++; inBed.add(n.name); } }
85
+ const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: inBed.has(n.name) ? bed : zoneOf(pack, n.state) }));
81
86
  const { seats, crowd, over } = assignSeats(pack, wants, prev);
82
87
  const desks = nodes.filter((n) => seats.has(n.name)).map((n) => {
83
88
  const s = seats.get(n.name);
84
- return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot };
89
+ return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot, night: sleepers.has(n.name) };
85
90
  });
86
91
  return { mode: "zones", rooms: [{ key: "\u0000zones", name: null, lobby: false, x: 0, y: 0, w: LOGICAL.w, h: LOGICAL.h, desks }], areas: pack.areas,
87
- waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
92
+ waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
88
93
  }
89
94
 
90
95
  // ── Walking ──
@@ -103,10 +108,159 @@ export function headingAt(pts, t) {
103
108
  }
104
109
  return null;
105
110
  }
111
+ /**
112
+ * #2634: a polyline with each diagonal leg made an L — across first, then up or down — so a character set's three
113
+ * walking strips always fit; legs already across or up/down are kept, and no point repeats (no zero-length leg).
114
+ */
115
+ export function straighten(pts) {
116
+ const out = [];
117
+ const add = (x, y) => { const l = out[out.length - 1]; if (!l || l.x !== x || l.y !== y) out.push({ x, y }); };
118
+ for (const p of pts) { const l = out[out.length - 1]; if (l && l.x !== p.x && l.y !== p.y) add(p.x, l.y); add(p.x, p.y); }
119
+ return out;
120
+ }
121
+ // ── Night (#2568) ──
122
+
123
+ /** Bedtime on the viewer's clock: from FROM o'clock to TO o'clock the idle sleep (anyone with work still gets up for it). */
124
+ export const NIGHT = { FROM: 0, TO: 6, TICK: 60000 };
125
+ export const isNight = (d = new Date()) => { const h = d.getHours(); return NIGHT.FROM <= NIGHT.TO ? h >= NIGHT.FROM && h < NIGHT.TO : h >= NIGHT.FROM || h < NIGHT.TO; };
126
+
127
+ // ── Wandering (#2560) ──
128
+
129
+ /**
130
+ * An idle person now and then strolls to a spot near their seat and back, so a quiet room is not a row of statues:
131
+ * every PERIOD ms (each person's own phase, from their name) they are AWAY ms at a waypoint within REACH of the seat —
132
+ * one no seat of the office is on (taken or free), picked anew each time — then walk back. Pure: the page asks every TICK ms and walks the change.
133
+ */
134
+ export const WANDER = { PERIOD: 40000, AWAY: 8000, REACH: 220, TICK: 1000 };
135
+ /** Where `name`, seated at `seat`, should be at `t` (ms) while idle: a spot near the seat, or the seat. */
136
+ export function wanderTarget(name, seat, graph, t, seats = []) {
137
+ const near = (graph.waypoints || []).filter((w) => Math.hypot(w.x - seat.x, w.y - seat.y) <= WANDER.REACH
138
+ && !seats.some((s) => Math.hypot(w.x - s.x, w.y - s.y) < 4));
139
+ if (!near.length) return seat;
140
+ const h = nameHash(name), k = t + (h % WANDER.PERIOD), turn = Math.floor(k / WANDER.PERIOD);
141
+ if (k - turn * WANDER.PERIOD >= WANDER.AWAY) return seat;
142
+ const w = near[nameHash(`${name}\u0000${turn}`) % near.length];
143
+ return { x: w.x, y: w.y };
144
+ }
145
+ /** An NPC drawn as furniture (#2596: an item with `walk`): its walker's name — never a person's, and its own phase. */
146
+ export const npcKey = (f) => `furniture/${f.id}`;
147
+ /** Everyone's walk target: their seat, or — idle and `wander` on — wanderTarget's spot (one person a spot: whoever
148
+ * would share it stays seated this time). And, `wander` on, each item of `furniture` with a `walk` (#2596): its spot
149
+ * (x, y), or a stroll near it as an idle person's (by npcKey) — none at `night` for one with a night place (asleep there). */
150
+ export function walkTargets(desks, graph, t, wander = true, furniture = [], night = false) {
151
+ // Every seat of the office (anyone's, taken or not: someone may sit down on it any moment), not only the ones in use.
152
+ // And each walking NPC's spot (#2596 review): its home, where it stands outside its stroll and goes back to whenever
153
+ // its stroll would collide, so nobody else's stroll may end there — no two walkers ever share a target.
154
+ const homes = wander ? furniture.filter((f) => f.walk && !(night && f.night)).map((f) => ({ x: f.x, y: f.y })) : [];
155
+ const seats = [...Object.values(graph.areas || {}).flatMap((a) => a.seats || []), ...desks.map((d) => d.seat), ...homes];
156
+ const used = new Set(homes.map((h) => `${h.x},${h.y}`));
157
+ const stroll = (name, home) => {
158
+ const p = wanderTarget(name, home, graph, t, seats), k = `${p.x},${p.y}`;
159
+ if (p === home || used.has(k)) return home;
160
+ used.add(k);
161
+ return p;
162
+ };
163
+ const out = new Map(desks.map((d) => [d.node.name, !wander || d.node.state !== "idle" || d.night ? d.seat : stroll(d.node.name, d.seat)]));
164
+ if (wander) for (const f of furniture) if (f.walk && !(night && f.night)) out.set(npcKey(f), stroll(npcKey(f), { x: f.x, y: f.y }));
165
+ return out;
166
+ }
167
+
168
+ // ── Mail from another office (#2564) ──
169
+
170
+ /**
171
+ * Something sent to a person here by someone who is not (in another office, or in none): it flies in at the door and
172
+ * lands on the floor near the middle of the room; the person walks over, picks it up and walks back to their seat.
173
+ * MAX_WAIT: a letter no one picks up is gone after it; GAP: letters waiting side by side; SEARCH: how far from the
174
+ * middle a landing spot is looked for when no waypoint will do; ASLEEP: who does not get up
175
+ * for it (it waits for them to wake, up to MAX_WAIT).
176
+ */
177
+ export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
178
+ /**
179
+ * Where the `n`th waiting letter lands: `n` GAPs along from the waypoint nearest the room's middle — the nearest whose
180
+ * spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
181
+ * with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
182
+ * those either, null — the letter is not delivered here (never on a seat).
183
+ */
184
+ export function dropPoint(L, n = 0) {
185
+ const mid = { x: L.width / 2, y: L.height * 0.55 }, d = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
186
+ const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
187
+ const onSeat = (p) => seats.some((q) => d(p, q) < 4);
188
+ const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
189
+ .filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
190
+ if (spots.length) return { x: spots[0].x, y: spots[0].y };
191
+ const inRoom = (p) => p.x >= 0 && p.y >= 0 && p.x <= L.width && p.y <= L.height;
192
+ for (let r = 0; r <= MAIL.SEARCH; r += 2 * MAIL.GAP) {
193
+ for (let k = 0, m = r ? 16 : 1; k < m; k++) {
194
+ const a = (2 * Math.PI * k) / m, p = { x: Math.round(mid.x + r * Math.cos(a) + n * MAIL.GAP), y: Math.round(mid.y + r * Math.sin(a)) };
195
+ if (inRoom(p) && !onSeat(p)) return p;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ const desksIn = (L) => (L.rooms || []).flatMap((r) => r.desks || []).filter((d) => d.node);
201
+ /**
202
+ * The letters of one office. `sync(items, now, L, still)`: each new traffic item to someone drawn here from someone
203
+ * who is not becomes a letter (none with reduced motion, nor for a stale delegation; a zones office only).
204
+ * `frame(now, L, walkers)` → what to draw, { id, hue, kind, from, x, y, t } (t: its flight, 0 on the floor), picking
205
+ * up the letters whose person stands on them, and dropping those gone too long or whose person left. `targets(L)`:
206
+ * name → where to walk to pick one up (the first waiting for them; none while they are asleep). `prime(items)`: mark as
207
+ * seen what was in the air before this office was shown (a new office's or style's mail starts empty). `size()`.
208
+ */
209
+ export function createMail({ max = MAIL.MAX } = {}) {
210
+ const seen = new Set();
211
+ let letters = [];
212
+ const here = (L, name) => desksIn(L).find((d) => d.node.name === name) || null;
213
+ return {
214
+ sync(items, now, L, still = false) {
215
+ let started = 0;
216
+ for (const it of items || []) {
217
+ if (seen.has(it.id)) continue;
218
+ seen.add(it.id);
219
+ if (seen.size > 2000) seen.delete(seen.values().next().value);
220
+ if (still || it.phase === "stale" || !L || L.mode !== "zones" || !here(L, it.to) || here(L, it.from)) continue;
221
+ const drop = dropPoint(L, letters.filter((l) => l.to !== it.to).length % 4);
222
+ if (!drop) continue; // nowhere off the seats: not delivered here
223
+ const pts = [{ x: L.door.x, y: L.door.y }, drop];
224
+ letters.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now, drop, pts,
225
+ ms: Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(pts) / FLIGHT.SPEED)) });
226
+ started++;
227
+ }
228
+ if (letters.length > max) letters = letters.slice(letters.length - max);
229
+ return started;
230
+ },
231
+ frame(now, L, walkers) {
232
+ const out = [];
233
+ letters = letters.filter((l) => {
234
+ if (!here(L, l.to)) return false;
235
+ const t = (now - l.start) / l.ms;
236
+ if (t < 1) { if (t >= 0) { const p = along(l.pts, t); out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: p.x, y: p.y, t }); } return true; }
237
+ if (now - l.start - l.ms > MAIL.MAX_WAIT) return false;
238
+ const w = walkers && walkers.at(l.to, now);
239
+ if (w && !w.walking && Math.hypot(w.x - l.drop.x, w.y - l.drop.y) < 4) return false; // picked up
240
+ out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: l.drop.x, y: l.drop.y, t: 0 });
241
+ return true;
242
+ });
243
+ return out;
244
+ },
245
+ targets(L, now) {
246
+ const out = new Map();
247
+ for (const l of letters) {
248
+ if (out.has(l.to) || now - l.start < l.ms) continue;
249
+ const d = here(L, l.to);
250
+ if (d && !MAIL.ASLEEP.has(d.node.state) && !d.night) out.set(l.to, l.drop); // asleep, or in bed for the night (#2568)
251
+ }
252
+ return out;
253
+ },
254
+ /** What is already in the traffic when this office's mail starts (#2564): seen, so it is not delivered again here. */
255
+ prime(items) { for (const it of items || []) seen.add(it.id); },
256
+ size: () => letters.length,
257
+ };
258
+ }
259
+
106
260
  /**
107
261
  * Where each character is. `sync(targets, now, graph, still)`: targets name → { x, y }; someone new is already there;
108
- * someone whose target moved walks from where they are now along the graph (routeVia) — or is there at once when
109
- * `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
262
+ * someone whose target moved walks from where they are now along the graph (routeVia; each diagonal leg an L when the
263
+ * graph is `straight`, #2634) — or is there at once when `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
110
264
  * current step's { dx, dy }, which picks a pack's walkDown / walkUp / walkSide strip; `moving(now)`.
111
265
  */
112
266
  export function createWalkers() {
@@ -125,7 +279,8 @@ export function createWalkers() {
125
279
  const w = by.get(name);
126
280
  if (!w || still) { if (!w || w.to.x !== to.x || w.to.y !== to.y) by.set(name, { pts: null, to }); continue; }
127
281
  if (w.to.x === to.x && w.to.y === to.y) continue;
128
- const here = posAt(w, now), pts = routeVia(graph, { x: here.x, y: here.y }, { x: to.x, y: to.y });
282
+ const here = posAt(w, now), via = routeVia(graph, { x: here.x, y: here.y }, { x: to.x, y: to.y });
283
+ const pts = graph.straight ? straighten(via) : via; // #2634: timed along the longer way
129
284
  by.set(name, { pts, start: now, ms: Math.max(1, pathLength(pts) / WALK.SPEED), to });
130
285
  }
131
286
  },
@@ -232,8 +387,13 @@ export function linesFor(state, lang, pack, defaults) {
232
387
  // ── Furniture ──
233
388
 
234
389
  /** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
235
- export function activeFurniture(pack, states) {
390
+ export function activeFurniture(pack, states, byName = new Map()) {
236
391
  const on = new Set();
237
- for (const f of (pack && pack.furniture) || []) if (f.activeWhen && f.activeWhen.some((s) => states.has(s))) on.add(f.id);
392
+ for (const f of (pack && pack.furniture) || []) {
393
+ if (!f.activeWhen) continue;
394
+ // activeFor (#2570): only those people's states count; else anyone's shown.
395
+ const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
396
+ if (hit) on.add(f.id);
397
+ }
238
398
  return on;
239
399
  }