@maka/maka-cli 5.63.1 → 5.65.0

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 (205) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +29 -8
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.d.ts +42 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +86 -3
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.d.ts +29 -2
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js +151 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.d.ts +40 -13
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +107 -25
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.d.ts +11 -15
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +13 -16
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js.map +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.d.ts +8 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +30 -2
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.d.ts +40 -49
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +121 -160
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +29 -5
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.d.ts +7 -8
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +13 -13
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +22 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/cover.d.ts +30 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/cover.js +65 -0
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/cover.js.map +1 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.d.ts +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +2 -2
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +25 -3
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +2 -3
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +2 -2
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/jobs.js +18 -10
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/jobs.js.map +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/journal.d.ts +44 -0
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/journal.js +136 -0
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/journal.js.map +1 -0
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/look.d.ts +20 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +157 -5
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +18 -4
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -1
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.d.ts +2 -3
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +2 -2
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/players.js +8 -2
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/players.js.map +1 -1
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/posture.d.ts +45 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/posture.js +115 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/posture.js.map +1 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +46 -12
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +30 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +11 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +12 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.d.ts +47 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +91 -5
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/time.d.ts +20 -0
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/time.js +61 -0
  72. package/bundle/typescript/src/commands/game/sideQuest/commands/time.js.map +1 -0
  73. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/commands/use.d.ts +17 -0
  76. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +136 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
  78. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  79. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +15 -1
  80. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  81. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +128 -44
  82. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +551 -254
  83. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  84. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +82 -84
  85. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  86. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +23 -4
  87. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +95 -79
  88. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.d.ts +30 -0
  90. package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js +176 -0
  91. package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js.map +1 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +104 -0
  93. package/bundle/typescript/src/commands/game/sideQuest/game.js +268 -16
  94. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  95. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +78 -0
  96. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +188 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/headless.d.ts +5 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/headless.js +78 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/headless.js.map +1 -1
  101. package/bundle/typescript/src/commands/game/sideQuest/models/_index.d.ts +0 -1
  102. package/bundle/typescript/src/commands/game/sideQuest/models/_index.js +0 -1
  103. package/bundle/typescript/src/commands/game/sideQuest/models/_index.js.map +1 -1
  104. package/bundle/typescript/src/commands/game/sideQuest/models/device.d.ts +121 -0
  105. package/bundle/typescript/src/commands/game/sideQuest/models/device.js +145 -0
  106. package/bundle/typescript/src/commands/game/sideQuest/models/device.js.map +1 -0
  107. package/bundle/typescript/src/commands/game/sideQuest/models/door.d.ts +27 -8
  108. package/bundle/typescript/src/commands/game/sideQuest/models/door.js +15 -10
  109. package/bundle/typescript/src/commands/game/sideQuest/models/door.js.map +1 -1
  110. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +143 -0
  111. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +253 -30
  112. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  113. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +68 -0
  114. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +102 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  116. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +80 -34
  117. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +126 -54
  118. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  119. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +1 -4
  120. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +0 -22
  121. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  122. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +78 -109
  123. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +4 -101
  124. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +24 -0
  125. package/bundle/typescript/src/commands/game/sideQuest/types/seed/device-seed.d.ts +52 -0
  126. package/bundle/typescript/src/commands/game/sideQuest/types/seed/device-seed.js +2 -0
  127. package/bundle/typescript/src/commands/game/sideQuest/types/seed/device-seed.js.map +1 -0
  128. package/bundle/typescript/src/commands/game/sideQuest/types/seed/legacy-puzzle-seed.d.ts +63 -0
  129. package/bundle/typescript/src/commands/game/sideQuest/types/seed/legacy-puzzle-seed.js +2 -0
  130. package/bundle/typescript/src/commands/game/sideQuest/types/seed/legacy-puzzle-seed.js.map +1 -0
  131. package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +6 -0
  132. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +4 -0
  133. package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +3 -2
  134. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.d.ts +2 -28
  135. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.js +1 -14
  136. package/bundle/typescript/src/commands/game/sideQuest/types/shared/abstracts.js.map +1 -1
  137. package/bundle/typescript/src/commands/game/sideQuest/ui.js +24 -0
  138. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  139. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.d.ts +55 -0
  140. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +36 -0
  141. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js.map +1 -0
  142. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.d.ts +22 -0
  143. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +26 -0
  144. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js.map +1 -1
  145. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-dump.js +6 -0
  146. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-dump.js.map +1 -1
  147. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-review.d.ts +45 -0
  148. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-review.js +116 -0
  149. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-review.js.map +1 -0
  150. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.d.ts +73 -1
  151. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.js +56 -1
  152. package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.js.map +1 -1
  153. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +37 -4
  154. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  155. package/bundle/typescript/src/commands/game/sideQuest/utilities/credentials.d.ts +26 -0
  156. package/bundle/typescript/src/commands/game/sideQuest/utilities/credentials.js +48 -0
  157. package/bundle/typescript/src/commands/game/sideQuest/utilities/credentials.js.map +1 -0
  158. package/bundle/typescript/src/commands/game/sideQuest/utilities/devices.d.ts +23 -0
  159. package/bundle/typescript/src/commands/game/sideQuest/utilities/devices.js +55 -0
  160. package/bundle/typescript/src/commands/game/sideQuest/utilities/devices.js.map +1 -0
  161. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.d.ts +57 -2
  162. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js +46 -3
  163. package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js.map +1 -1
  164. package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.d.ts +38 -0
  165. package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js +42 -0
  166. package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js.map +1 -1
  167. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +72 -17
  168. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  169. package/bundle/typescript/src/commands/game/sideQuest/utilities/playtester.d.ts +50 -0
  170. package/bundle/typescript/src/commands/game/sideQuest/utilities/playtester.js +43 -0
  171. package/bundle/typescript/src/commands/game/sideQuest/utilities/playtester.js.map +1 -0
  172. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.d.ts +14 -3
  173. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +45 -31
  174. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js.map +1 -1
  175. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +66 -8
  176. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js.map +1 -1
  177. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +81 -15
  178. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +142 -33
  179. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  180. package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.d.ts +88 -0
  181. package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.js +113 -0
  182. package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.js.map +1 -0
  183. package/bundle/typescript/src/commands/game/sideQuest/utilities/world-clock.d.ts +83 -0
  184. package/bundle/typescript/src/commands/game/sideQuest/utilities/world-clock.js +106 -0
  185. package/bundle/typescript/src/commands/game/sideQuest/utilities/world-clock.js.map +1 -0
  186. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +54 -5
  187. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  188. package/bundle/typescript/src/maka.js +1 -1
  189. package/bundle/typescript/src/maka.js.map +1 -1
  190. package/bundle/typescript/src/tools/sdk-loader/relocate.d.ts +19 -2
  191. package/bundle/typescript/src/tools/sdk-loader/relocate.js +22 -2
  192. package/bundle/typescript/src/tools/sdk-loader/relocate.js.map +1 -1
  193. package/package.json +1 -1
  194. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.d.ts +0 -6
  195. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js +0 -72
  196. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js.map +0 -1
  197. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.d.ts +0 -6
  198. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js +0 -26
  199. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js.map +0 -1
  200. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +0 -143
  201. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +0 -366
  202. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +0 -1
  203. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +0 -28
  204. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.js +0 -2
  205. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.js.map +0 -1
@@ -49,6 +49,12 @@ export default class Game {
49
49
  private _quitArmed;
50
50
  private _escArmedAt;
51
51
  private _statusBox;
52
+ /** The clock gutter left of the prompt, its ticker, and the face it
53
+ * last painted -- compared so a repaint only happens on the minute
54
+ * rollover (see paintClock). */
55
+ private _clockBox?;
56
+ private _clockTimer?;
57
+ private _clockFace;
52
58
  private _inputBox;
53
59
  /** The floating icon-minimap (map-overlay wave 2026-08-24). Optional:
54
60
  * headless sessions never build widgets. */
@@ -62,6 +68,20 @@ export default class Game {
62
68
  private _anonymous;
63
69
  private _logFilePath;
64
70
  private _tier;
71
+ /**
72
+ * The run's difficulty tier. `activateRunScene` sets it from the tier
73
+ * the scene was generated at; a HEADLESS boot never calls that, so a
74
+ * shared run sat at the default 1 no matter what it was actually
75
+ * generated as (audit 2026-08-27).
76
+ *
77
+ * That does not sit still: the stored tier rides into the save
78
+ * snapshot and `continueToNextScene` reads `_tier + 1` for the next
79
+ * draft, so an under-recorded tier propagates forward into content
80
+ * that has not been generated yet. Exposed so the headless boot can
81
+ * state the tier it was handed instead of guessing.
82
+ */
83
+ get tier(): number;
84
+ set tier(value: number);
65
85
  private _draftInProgress;
66
86
  private _hubScene?;
67
87
  private _hubName;
@@ -287,6 +307,38 @@ export default class Game {
287
307
  * setCharacterLocation (the ONLY path that binds room observers), and
288
308
  * hand it the scene for its AI context.
289
309
  */
310
+ /**
311
+ * THE FIXER IS REACHABLE FROM THE FIELD (player go 2026-08-27: "on a
312
+ * multi-crew session, I should still be able to TALK to Mr. Krow if I
313
+ * need something").
314
+ *
315
+ * A shared run executes on a server whose Game has no hub scene, so
316
+ * `hubCast()` is empty and NOBODY from home is a call candidate --
317
+ * including the fixer. Solo mid-run they are (see call.ts's own note
318
+ * about "call Ratchet" bouncing while the roster promised a phone
319
+ * icon), so the same fiction behaved differently by mode, invisibly.
320
+ *
321
+ * The ruling that keeps the ROSTER home is untouched, because the
322
+ * roster and the fixer are different KINDS of thing. The roster is
323
+ * per-player relationship state -- your loyalty, your favours, your
324
+ * ledger -- which is not merely awkward on a shared server but
325
+ * meaningless there, since four players have four different Mama Wus.
326
+ * The fixer is world furniture: one fixed identity, the same Krow for
327
+ * everyone at the table, carrying no per-player state. A shared-run
328
+ * Krow is well-defined in a way a shared-run roster is not, so
329
+ * answering yes here costs nothing that ruling was protecting.
330
+ *
331
+ * Deliberately NOT the whole hub cast: that needs per-player contact
332
+ * identity on the server and would reopen the ruling.
333
+ *
334
+ * He is built lazily and kept OFF the scene -- he is a voice on a
335
+ * commlink, not a body in the room, so he must never appear in "look",
336
+ * be attacked, or be looted. Returns undefined in any session that
337
+ * already has a real one (a hub session, or a static scene that ships
338
+ * him as a cast member), so there is only ever one Krow.
339
+ */
340
+ remoteFixer(): NPC | undefined;
341
+ private _remoteFixer?;
290
342
  spawnCompanionShell(opts: {
291
343
  name: string;
292
344
  description: string;
@@ -525,6 +577,24 @@ export default class Game {
525
577
  reported: boolean;
526
578
  dead?: boolean;
527
579
  };
580
+ /**
581
+ * WHEN THE RUN STARTED -- the one fact the journal cannot derive.
582
+ *
583
+ * commands/journal.ts renders current state rather than recording
584
+ * events, deliberately: a journal that logs what happened can drift
585
+ * from the world it describes, and a derived one cannot disagree with
586
+ * the thing it is reading. Client, pay and objective all already exist
587
+ * elsewhere and are read live. Elapsed time is the exception -- its
588
+ * ORIGIN is not recoverable from any current state -- so this is the
589
+ * single new field the feature adds, said out loud rather than grown
590
+ * quietly into a record.
591
+ *
592
+ * Not saved, and does not need to be: like currentClient above, mid-run
593
+ * state never persists (runs are disposable -- a quit resumes to the
594
+ * hub with the job back on offer), so there is no save-schema change
595
+ * here. Stamped by activateRunScene, cleared by settleClientReputation.
596
+ */
597
+ runStartedAt?: Date;
528
598
  forfeitArmed: boolean;
529
599
  /**
530
600
  * The meet and the wrap-up, triggered by the player's first/next real
@@ -694,6 +764,21 @@ export default class Game {
694
764
  */
695
765
  launchPendingRun(): Promise<string>;
696
766
  close(): Promise<void>;
767
+ /**
768
+ * THE CLOCK TICKER (playtest 2026-08-27). The gutter rendered once,
769
+ * whenever the pane happened to paint, and then showed that moment
770
+ * forever. Parked beside a live prompt it is under the player's eye
771
+ * for the whole run, so a stale one is worse than none at all -- and
772
+ * the move made that matter MORE, not less.
773
+ *
774
+ * Repaints on the MINUTE ROLLOVER, not on every tick: it wakes each
775
+ * second but only touches the screen when the rendered string
776
+ * actually changes. A forced once-a-second render would flicker the
777
+ * whole terminal and cost more than the feature is worth.
778
+ */
779
+ private paintClock;
780
+ private startClockTicker;
781
+ stopClockTicker(): void;
697
782
  stopPresenceHeartbeat(): void;
698
783
  /**
699
784
  * Full per-session teardown (Phase C): everything init/play armed that
@@ -759,6 +844,10 @@ export default class Game {
759
844
  * the inner width from it to right-justify the elevation bar. */
760
845
  private static readonly ROOM_DOCK_PCT;
761
846
  /** Must track the mapBox's `width` below -- used to trim its caption. */
847
+ /** Columns the clock gutter takes from the input field. Wide enough
848
+ * for "HH:MM" inside a border; adjustable if it costs too much
849
+ * typing room in play. */
850
+ private static readonly CLOCK_GUTTER_W;
762
851
  private static readonly MAP_DOCK_PCT;
763
852
  private initMapBox;
764
853
  /** Shifts the Mechanics pane down (a map box is shown) or back up
@@ -872,8 +961,23 @@ export default class Game {
872
961
  * answers). Without this, the uncategorized sweep resurrects them
873
962
  * in an "other" bucket. */
874
963
  private static readonly HIDDEN_VERBS;
964
+ /** Session verbs that only a play-tester (or admin) can actually use
965
+ * -- the server role-gates both and 403s everyone else. Listed in
966
+ * help ONLY for a confirmed play-tester; see helpCategories. `roles`
967
+ * rides along because it is admin-only, a strictly narrower group. */
968
+ private static readonly PLAYTESTER_VERBS;
875
969
  /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
876
970
  private static readonly BUILTIN_DESCRIPTIONS;
877
971
  private describeVerb;
972
+ /**
973
+ * The help index as THIS account should see it (backlog wave 2, item
974
+ * D: "let's remove Seattle Backlog for non playtesters"). The backlog
975
+ * is role-gated on the server and 403s everyone else, so listing it
976
+ * for every player only advertises a refusal. Hidden unless the
977
+ * account is a CONFIRMED play-tester -- see utilities/playtester.ts
978
+ * for why "unknown" (offline, logged out) hides rather than shows,
979
+ * and why the verb itself stays registered and usable regardless.
980
+ */
981
+ private helpCategories;
878
982
  private getHelpMessage;
879
983
  }
@@ -1,7 +1,7 @@
1
1
  import blessed from 'blessed';
2
2
  import { dirname } from 'path';
3
3
  import { capitalCase } from 'change-case';
4
- import { Category } from './types/shared/item-enum.js';
4
+ import { Category, Size, Rating } from './types/shared/item-enum.js';
5
5
  import { Item } from './models/item.js';
6
6
  import { Room } from './models/_index.js';
7
7
  import { Door } from './models/door.js';
@@ -27,13 +27,15 @@ import { TakeCommand } from './commands/take.js';
27
27
  import { LootCommand } from './commands/loot.js';
28
28
  import { NoteCommand } from './commands/note.js';
29
29
  import { BacklogCommand } from './commands/backlog.js';
30
+ // Whether this account may actually USE the backlog -- gates what the
31
+ // help index offers, never what the verb accepts (item D).
32
+ import { isPlaytester, probePlaytesterStanding } from './utilities/playtester.js';
30
33
  import { RolesCommand } from './commands/roles.js';
31
34
  import { SinCommand } from './commands/sin.js';
32
35
  import { GoCommand } from './commands/go.js';
33
36
  import { UseCommand } from './commands/use.js';
34
37
  import { EquipCommand } from './commands/equip.js';
35
38
  import { UnequipCommand } from './commands/unequip.js';
36
- import { SolveCommand } from './commands/solve.js';
37
39
  import { DropCommand } from './commands/drop.js';
38
40
  import { UnlockCommand } from './commands/unlock.js';
39
41
  import { LockCommand } from './commands/lock.js';
@@ -109,6 +111,9 @@ import { SellCommand } from './commands/sell.js';
109
111
  import { PayCommand } from './commands/pay.js';
110
112
  import { LifestyleCommand } from './commands/lifestyle.js';
111
113
  import { JobsCommand } from './commands/jobs.js';
114
+ import { JournalCommand } from './commands/journal.js';
115
+ import { CoverCommand } from './commands/cover.js';
116
+ import { PostureCommand, LieDownCommand, StandCommand } from './commands/posture.js';
112
117
  import { TreatCommand } from './commands/treat.js';
113
118
  import { WorkCommand } from './commands/work.js';
114
119
  import { EatCommand } from './commands/eat.js';
@@ -127,6 +132,10 @@ import { OverwatchCommand } from './commands/overwatch.js';
127
132
  import { MarkCommand } from './commands/mark.js';
128
133
  import { SnoopCommand } from './commands/snoop.js';
129
134
  import { TapCommand } from './commands/tap.js';
135
+ import { TimeCommand } from './commands/time.js';
136
+ // The HUD's clock -- read in the engine so a shared run reads the
137
+ // host's wall clock, never each client's own (item B).
138
+ import { clockTime, worldNow } from './utilities/world-clock.js';
130
139
  import { AutoFight } from './utilities/auto-fight.js';
131
140
  import { CommandFactory } from './factories/command-factory.js';
132
141
  import { SceneSynthesizer } from './factories/scene-factory.js';
@@ -156,6 +165,12 @@ export default class Game {
156
165
  _quitArmed = false;
157
166
  _escArmedAt = 0;
158
167
  _statusBox;
168
+ /** The clock gutter left of the prompt, its ticker, and the face it
169
+ * last painted -- compared so a repaint only happens on the minute
170
+ * rollover (see paintClock). */
171
+ _clockBox;
172
+ _clockTimer;
173
+ _clockFace = '';
159
174
  _inputBox;
160
175
  /** The floating icon-minimap (map-overlay wave 2026-08-24). Optional:
161
176
  * headless sessions never build widgets. */
@@ -172,6 +187,24 @@ export default class Game {
172
187
  _anonymous = false;
173
188
  _logFilePath = '';
174
189
  _tier = 1;
190
+ /**
191
+ * The run's difficulty tier. `activateRunScene` sets it from the tier
192
+ * the scene was generated at; a HEADLESS boot never calls that, so a
193
+ * shared run sat at the default 1 no matter what it was actually
194
+ * generated as (audit 2026-08-27).
195
+ *
196
+ * That does not sit still: the stored tier rides into the save
197
+ * snapshot and `continueToNextScene` reads `_tier + 1` for the next
198
+ * draft, so an under-recorded tier propagates forward into content
199
+ * that has not been generated yet. Exposed so the headless boot can
200
+ * state the tier it was handed instead of guessing.
201
+ */
202
+ get tier() {
203
+ return this._tier;
204
+ }
205
+ set tier(value) {
206
+ this._tier = value;
207
+ }
175
208
  // Guards against two overlapping continueToNextScene() calls -- found via
176
209
  // a real session where calling the fixer twice in a row (nothing stopped
177
210
  // it, and a 20-40+ second draft invites a second, impatient call) fired
@@ -865,6 +898,80 @@ export default class Game {
865
898
  * setCharacterLocation (the ONLY path that binds room observers), and
866
899
  * hand it the scene for its AI context.
867
900
  */
901
+ /**
902
+ * THE FIXER IS REACHABLE FROM THE FIELD (player go 2026-08-27: "on a
903
+ * multi-crew session, I should still be able to TALK to Mr. Krow if I
904
+ * need something").
905
+ *
906
+ * A shared run executes on a server whose Game has no hub scene, so
907
+ * `hubCast()` is empty and NOBODY from home is a call candidate --
908
+ * including the fixer. Solo mid-run they are (see call.ts's own note
909
+ * about "call Ratchet" bouncing while the roster promised a phone
910
+ * icon), so the same fiction behaved differently by mode, invisibly.
911
+ *
912
+ * The ruling that keeps the ROSTER home is untouched, because the
913
+ * roster and the fixer are different KINDS of thing. The roster is
914
+ * per-player relationship state -- your loyalty, your favours, your
915
+ * ledger -- which is not merely awkward on a shared server but
916
+ * meaningless there, since four players have four different Mama Wus.
917
+ * The fixer is world furniture: one fixed identity, the same Krow for
918
+ * everyone at the table, carrying no per-player state. A shared-run
919
+ * Krow is well-defined in a way a shared-run roster is not, so
920
+ * answering yes here costs nothing that ruling was protecting.
921
+ *
922
+ * Deliberately NOT the whole hub cast: that needs per-player contact
923
+ * identity on the server and would reopen the ruling.
924
+ *
925
+ * He is built lazily and kept OFF the scene -- he is a voice on a
926
+ * commlink, not a body in the room, so he must never appear in "look",
927
+ * be attacked, or be looted. Returns undefined in any session that
928
+ * already has a real one (a hub session, or a static scene that ships
929
+ * him as a cast member), so there is only ever one Krow.
930
+ */
931
+ remoteFixer() {
932
+ if (this.hasHub || !this.scene)
933
+ return undefined;
934
+ if (this.scene.allActors.some(a => isFixerName(a.name)))
935
+ return undefined;
936
+ if (this._remoteFixer)
937
+ return this._remoteFixer;
938
+ // His own private "room": never added to the scene, so nothing can
939
+ // walk into it. It exists because an NPC needs somewhere to stand
940
+ // for its own AI context to resolve.
941
+ const line = new Room({
942
+ name: 'On The Line',
943
+ description: 'Somewhere across the sprawl, a man takes a call he was expecting.',
944
+ roomType: 'void',
945
+ });
946
+ const npc = new NPC({
947
+ dialog: [
948
+ `Talk to me, chummer -- you're on the clock.`,
949
+ `I broker the work. What happens on site is your business.`,
950
+ ],
951
+ description: `The fixer who put this job together. He is not on site and never will be -- he brokers, he does not run. Reachable by commlink because that is the whole point of a fixer. He knows the JOB exists and who hired for it; he does NOT know the inside of the place, the codes, or what the runners are looking at right now.`,
952
+ playerConstructorConfig: {
953
+ playerName: FIXER_NAME,
954
+ startLocation: line,
955
+ combat: { body: 3, agility: 3, reaction: 3, willpower: 4, logic: 4, intuition: 4, charisma: 5, strength: 3, edge: 3 },
956
+ },
957
+ }, new CommandRegistry(this.scene));
958
+ // A fixer with no commlink is a contradiction, and call.ts gates on
959
+ // carrying one (hasCommlink) before it will connect.
960
+ // NOTE the enum VALUES here. A runtime Item validates against the
961
+ // enum's values ('tiny'); a SAVED item uses the enum KEYS ('Tiny').
962
+ // The two conventions are inverses and both are load-bearing.
963
+ npc.inventory.addItem(new Item({
964
+ name: 'Fixer\'s Commlink',
965
+ description: 'The number everyone has and nobody sells.',
966
+ size: Size.Tiny, category: Category.Commlink, shape: 'Rectangle',
967
+ color: 'Black', texture: 'Matte', rating: Rating.Common, weight: 0.1,
968
+ }));
969
+ npc.scene = this.scene;
970
+ this._remoteFixer = npc;
971
+ Logger.getInstance().write(`Remote fixer materialised for a hubless session: ${FIXER_NAME}.`);
972
+ return npc;
973
+ }
974
+ _remoteFixer;
868
975
  spawnCompanionShell(opts) {
869
976
  if (this.scene.getActor(opts.name)) {
870
977
  Logger.getInstance().write(`spawnCompanionShell: name "${opts.name}" already in scene -- refusing.`);
@@ -1466,6 +1573,24 @@ export default class Game {
1466
1573
  // state never is (runs are disposable); reconstructed fresh by
1467
1574
  // activateRunScene, settled into the roster by returnToHub.
1468
1575
  currentClient;
1576
+ /**
1577
+ * WHEN THE RUN STARTED -- the one fact the journal cannot derive.
1578
+ *
1579
+ * commands/journal.ts renders current state rather than recording
1580
+ * events, deliberately: a journal that logs what happened can drift
1581
+ * from the world it describes, and a derived one cannot disagree with
1582
+ * the thing it is reading. Client, pay and objective all already exist
1583
+ * elsewhere and are read live. Elapsed time is the exception -- its
1584
+ * ORIGIN is not recoverable from any current state -- so this is the
1585
+ * single new field the feature adds, said out loud rather than grown
1586
+ * quietly into a record.
1587
+ *
1588
+ * Not saved, and does not need to be: like currentClient above, mid-run
1589
+ * state never persists (runs are disposable -- a quit resumes to the
1590
+ * hub with the job back on offer), so there is no save-schema change
1591
+ * here. Stamped by activateRunScene, cleared by settleClientReputation.
1592
+ */
1593
+ runStartedAt;
1469
1594
  // Taxi warn-once (player ruling): leaving with the pay unclaimed
1470
1595
  // arms this; the second "call taxi" forfeits. Mirrors _quitArmed.
1471
1596
  forfeitArmed = false;
@@ -1541,6 +1666,7 @@ export default class Game {
1541
1666
  settleClientReputation(wrappedRun) {
1542
1667
  const client = this.currentClient;
1543
1668
  this.currentClient = undefined;
1669
+ this.runStartedAt = undefined;
1544
1670
  this.forfeitArmed = false;
1545
1671
  if (!client)
1546
1672
  return '';
@@ -1670,7 +1796,6 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1670
1796
  CommandFactory.registerCommand('use', UseCommand);
1671
1797
  CommandFactory.registerCommand('equip', EquipCommand);
1672
1798
  CommandFactory.registerCommand('unequip', UnequipCommand);
1673
- CommandFactory.registerCommand('solve', SolveCommand);
1674
1799
  CommandFactory.registerCommand('drop', DropCommand);
1675
1800
  CommandFactory.registerCommand('unlock', UnlockCommand);
1676
1801
  CommandFactory.registerCommand('lock', LockCommand);
@@ -1799,6 +1924,13 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1799
1924
  CommandFactory.registerCommand('drink', DrinkCommand);
1800
1925
  CommandFactory.registerCommand('lifestyle', LifestyleCommand);
1801
1926
  CommandFactory.registerCommand('jobs', JobsCommand);
1927
+ CommandFactory.registerCommand('journal', JournalCommand);
1928
+ CommandFactory.registerCommand('cover', CoverCommand);
1929
+ CommandFactory.registerCommand('sit', PostureCommand);
1930
+ CommandFactory.registerCommand('kneel', PostureCommand);
1931
+ CommandFactory.registerCommand('lie', LieDownCommand);
1932
+ CommandFactory.registerCommand('prone', LieDownCommand);
1933
+ CommandFactory.registerCommand('stand', StandCommand);
1802
1934
  CommandFactory.registerCommand('treat', TreatCommand);
1803
1935
  CommandFactory.registerCommand('work', WorkCommand);
1804
1936
  CommandFactory.registerCommand('gig', WorkCommand);
@@ -1827,6 +1959,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1827
1959
  // "jackpoint" are what players reach for when they mean the same act.
1828
1960
  CommandFactory.registerCommand('tap', TapCommand);
1829
1961
  CommandFactory.registerCommand('splice', TapCommand);
1962
+ // The wall clock (item B). "clock" too -- half of us type that.
1963
+ CommandFactory.registerCommand('time', TimeCommand);
1964
+ CommandFactory.registerCommand('clock', TimeCommand);
1830
1965
  }
1831
1966
  determineStartRoom() {
1832
1967
  return this.scene.determineStartRoom();
@@ -1928,6 +2063,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1928
2063
  this._saveFilePath = options.saveFilePath;
1929
2064
  this._owner = options.owner;
1930
2065
  this._anonymous = options.anonymousRunner === true;
2066
+ // Ask the server, once, whether this account is a play-tester, so
2067
+ // the help index can stop advertising a backlog that will only 403
2068
+ // (item D). Fire-and-forget on purpose: it never blocks the boot,
2069
+ // never throws, and an unanswered probe just leaves the menu entry
2070
+ // hidden while the verb stays usable. See utilities/playtester.ts.
2071
+ void probePlaytesterStanding({ anonymous: this._anonymous });
1931
2072
  // ONE loud notice the moment the AI provider refuses on ACCOUNT
1932
2073
  // grounds (empty credits, bad key). Without it a dead key mutes
1933
2074
  // every NPC and makes every job "fall through" invisibly -- a real
@@ -2041,6 +2182,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2041
2182
  equipIntoEmptySlots(this.player);
2042
2183
  this.warmNpcAppearancesInBackground();
2043
2184
  this.startPursuitHeartbeat();
2185
+ this.startClockTicker();
2044
2186
  // The banner learns where we are only after the hub/scene bookkeeping
2045
2187
  // above settles.
2046
2188
  this.refreshLogLabel();
@@ -2761,7 +2903,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2761
2903
  // playtest note: being yanked into the run the instant generation
2762
2904
  // finished was jarring). The player invokes it on their own time:
2763
2905
  // "call Mr. Johnson" delivers the offer (haggle window included --
2764
- // see call.ts), and "travel" actually goes (launchPendingRun).
2906
+ // see call.ts), and "call taxi" actually goes (launchPendingRun).
2765
2907
  if (this.hasHub) {
2766
2908
  // The mid-generation race (see draftedForParty above): if the live
2767
2909
  // party changed while Krow was on the phone, this seed was quoted
@@ -2795,6 +2937,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2795
2937
  this.camPingedRooms.clear();
2796
2938
  this.sweptRooms.clear();
2797
2939
  this.sceneSeed = seed;
2940
+ // The clock starts when the run does (commands/journal.ts).
2941
+ this.runStartedAt = worldNow();
2798
2942
  // A call partner from the old scene wouldn't exist in the new one --
2799
2943
  // hang up before moving on, rather than leave a dangling reference.
2800
2944
  if (this.player.activeCallPartner) {
@@ -3007,6 +3151,45 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3007
3151
  });
3008
3152
  }
3009
3153
  }
3154
+ /**
3155
+ * THE CLOCK TICKER (playtest 2026-08-27). The gutter rendered once,
3156
+ * whenever the pane happened to paint, and then showed that moment
3157
+ * forever. Parked beside a live prompt it is under the player's eye
3158
+ * for the whole run, so a stale one is worse than none at all -- and
3159
+ * the move made that matter MORE, not less.
3160
+ *
3161
+ * Repaints on the MINUTE ROLLOVER, not on every tick: it wakes each
3162
+ * second but only touches the screen when the rendered string
3163
+ * actually changes. A forced once-a-second render would flicker the
3164
+ * whole terminal and cost more than the feature is worth.
3165
+ */
3166
+ paintClock() {
3167
+ if (!this._clockBox)
3168
+ return;
3169
+ const face = clockTime();
3170
+ if (face === this._clockFace)
3171
+ return;
3172
+ this._clockFace = face;
3173
+ this._clockBox.setContent(`{center}{cyan-fg}${face}{/cyan-fg}{/center}`);
3174
+ this._screen?.render();
3175
+ }
3176
+ startClockTicker() {
3177
+ // No gutter, nothing to repaint. Headless sessions have no screen,
3178
+ // and a server hosting many of them should not spin one interval
3179
+ // per session to call a function that early-returns.
3180
+ if (!this._clockBox || this._clockTimer)
3181
+ return;
3182
+ this._clockTimer = setInterval(() => this.paintClock(), 1000);
3183
+ // Never hold the process open for a clock: a session that is
3184
+ // otherwise finished should exit, not linger for the next minute.
3185
+ this._clockTimer.unref?.();
3186
+ }
3187
+ stopClockTicker() {
3188
+ if (this._clockTimer) {
3189
+ clearInterval(this._clockTimer);
3190
+ this._clockTimer = undefined;
3191
+ }
3192
+ }
3010
3193
  stopPresenceHeartbeat() {
3011
3194
  if (this._presenceTimer) {
3012
3195
  clearInterval(this._presenceTimer);
@@ -3020,6 +3203,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3020
3203
  */
3021
3204
  async dispose() {
3022
3205
  this.stopPresenceHeartbeat();
3206
+ // A stray interval outliving a closed shared session would be a
3207
+ // nasty thing to find later, so it retires with the rest of them.
3208
+ this.stopClockTicker();
3023
3209
  if (this._presenceEquipTimer)
3024
3210
  clearTimeout(this._presenceEquipTimer);
3025
3211
  this.stopPursuitHeartbeat();
@@ -3033,10 +3219,39 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3033
3219
  }
3034
3220
  initInputBox(screen) {
3035
3221
  // Create an input box for user commands
3036
- const inputBox = blessed.textbox({
3222
+ // THE CLOCK GUTTER (playtest 2026-08-27: "if it makes sense, the
3223
+ // clock can be to the left of the input box"). A fixed-width pane
3224
+ // at left:0 with the input narrowed to match.
3225
+ //
3226
+ // This is also where the truncation bug goes to die rather than
3227
+ // move house. The clock used to be the LAST field on the Armor
3228
+ // line, so it was the first thing clipped when that pane got tight
3229
+ // -- which is why the player could not find it. A dedicated
3230
+ // fixed-width gutter cannot be squeezed by its neighbours: the
3231
+ // input field yields the columns instead, and long input already
3232
+ // scrolls horizontally. The cost is CLOCK_GUTTER_W columns of
3233
+ // typing room.
3234
+ //
3235
+ // A sibling widget does not touch the textbox's internal cursor --
3236
+ // it just gets a narrower width -- so there is no cursor maths here.
3237
+ const clockBox = blessed.box({
3238
+ id: 'clockBox',
3037
3239
  bottom: 0,
3038
3240
  left: 0,
3039
- width: '100%',
3241
+ width: Game.CLOCK_GUTTER_W,
3242
+ height: 3,
3243
+ tags: true,
3244
+ border: { type: 'line' },
3245
+ style: { border: { fg: 'cyan' } },
3246
+ content: '',
3247
+ });
3248
+ this._clockBox = clockBox;
3249
+ screen.append(clockBox);
3250
+ this.paintClock();
3251
+ const inputBox = blessed.textbox({
3252
+ bottom: 0,
3253
+ left: Game.CLOCK_GUTTER_W,
3254
+ width: `100%-${Game.CLOCK_GUTTER_W}`,
3040
3255
  height: 3,
3041
3256
  tags: true,
3042
3257
  border: { type: 'line' },
@@ -3307,6 +3522,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3307
3522
  * the inner width from it to right-justify the elevation bar. */
3308
3523
  static ROOM_DOCK_PCT = 0.23;
3309
3524
  /** Must track the mapBox's `width` below -- used to trim its caption. */
3525
+ /** Columns the clock gutter takes from the input field. Wide enough
3526
+ * for "HH:MM" inside a border; adjustable if it costs too much
3527
+ * typing room in play. */
3528
+ static CLOCK_GUTTER_W = 8;
3310
3529
  static MAP_DOCK_PCT = 0.12;
3311
3530
  initMapBox(screen) {
3312
3531
  const mapBox = blessed.box({
@@ -3554,6 +3773,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3554
3773
  // decide whether calls land while you are away from your body.
3555
3774
  const { overlay, link } = arStatusSlots(player);
3556
3775
  const arSlot = player.plane === 'meat' ? ` | ${overlay}` : '';
3776
+ // THE WALL CLOCK rides the readiness line (item B). Rendered HERE,
3777
+ // in the engine, which in a shared run is the server -- so every
3778
+ // player's HUD shows the host's clock and "after midnight" is one
3779
+ // moment for the whole table. utilities/world-clock.ts has the why.
3780
+ // Time only, not the date: the date is what "time" is for.
3781
+ // The clock LEFT this line for the gutter beside the prompt. It was
3782
+ // the last field here, so it was the first thing clipped whenever
3783
+ // the pane got tight -- the reason the player could not find it.
3557
3784
  lines.push(`${armorNote}${arSlot} | ${link}`);
3558
3785
  // Lines 4+: transient states, present only while active. Future
3559
3786
  // contexts (astral projection, VR hot-sim) join here the same way.
@@ -3899,9 +4126,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3899
4126
  * future command can't silently fall out of help.
3900
4127
  */
3901
4128
  static HELP_CATEGORIES = [
3902
- { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['follow', 'unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
4129
+ { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow', 'unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
3903
4130
  { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
3904
- { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['stance'], ['surrender'], ['edge'], ['rest']] },
4131
+ { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['stance'], ['surrender'], ['edge'], ['rest']] },
3905
4132
  // The party layer: everyone who walks (or flies, or manifests) at
3906
4133
  // your side answers to these.
3907
4134
  // "players" folded back under crew after playtesting ("crew
@@ -3913,10 +4140,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3913
4140
  // resonance" -- matrix is the place, Resonance is the talent).
3914
4141
  { title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['register']] },
3915
4142
  { title: 'social', entries: [['say'], ['emote', 'me'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
3916
- { title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
3917
- { title: 'barriers', entries: [['solve'], ['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
4143
+ { title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
4144
+ { title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
3918
4145
  { title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
3919
- { title: 'session', entries: [['help'], ['hints'], ['note'], ['backlog'], ['roles'], ['clear'], ['quit', 'exit']] },
4146
+ { title: 'session', entries: [['help'], ['hints'], ['time', 'clock'], ['note'], ['backlog'], ['roles'], ['clear'], ['quit', 'exit']] },
3920
4147
  ];
3921
4148
  /** Registered but deliberately UNLISTED: reachable as subcommands or
3922
4149
  * muscle-memory ("players" folded back under crew after playtesting
@@ -3924,6 +4151,11 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3924
4151
  * answers). Without this, the uncategorized sweep resurrects them
3925
4152
  * in an "other" bucket. */
3926
4153
  static HIDDEN_VERBS = ['players'];
4154
+ /** Session verbs that only a play-tester (or admin) can actually use
4155
+ * -- the server role-gates both and 403s everyone else. Listed in
4156
+ * help ONLY for a confirmed play-tester; see helpCategories. `roles`
4157
+ * rides along because it is admin-only, a strictly narrower group. */
4158
+ static PLAYTESTER_VERBS = new Set(['backlog', 'roles']);
3927
4159
  /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
3928
4160
  static BUILTIN_DESCRIPTIONS = {
3929
4161
  help: 'This overview. "help <category>" for one group with full descriptions; "help <command>" for one command.',
@@ -3937,17 +4169,37 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3937
4169
  const CommandClass = CommandFactory.getAllRegisteredCommands()[verb];
3938
4170
  return CommandClass?.getDescription() ?? 'No description available.';
3939
4171
  }
4172
+ /**
4173
+ * The help index as THIS account should see it (backlog wave 2, item
4174
+ * D: "let's remove Seattle Backlog for non playtesters"). The backlog
4175
+ * is role-gated on the server and 403s everyone else, so listing it
4176
+ * for every player only advertises a refusal. Hidden unless the
4177
+ * account is a CONFIRMED play-tester -- see utilities/playtester.ts
4178
+ * for why "unknown" (offline, logged out) hides rather than shows,
4179
+ * and why the verb itself stays registered and usable regardless.
4180
+ */
4181
+ helpCategories() {
4182
+ if (isPlaytester())
4183
+ return Game.HELP_CATEGORIES;
4184
+ return Game.HELP_CATEGORIES.map(c => c.title !== 'session' ? c : {
4185
+ ...c,
4186
+ entries: c.entries.filter(e => !Game.PLAYTESTER_VERBS.has(e[0])),
4187
+ });
4188
+ }
3940
4189
  getHelpMessage(args = []) {
3941
4190
  const registered = Object.keys(CommandFactory.getAllRegisteredCommands());
4191
+ const helpCategories = this.helpCategories();
4192
+ // The uncategorized sweep must not resurrect what helpCategories
4193
+ // just hid, so the restricted verbs stay "covered" either way.
3942
4194
  const covered = new Set([...Game.HELP_CATEGORIES.flatMap(c => c.entries.flat()), ...Game.HIDDEN_VERBS]);
3943
4195
  const uncategorized = registered.filter(v => !covered.has(v));
3944
4196
  const query = args.join(' ').trim().toLowerCase();
3945
4197
  // "help <category>": that group, with full descriptions.
3946
4198
  if (query) {
3947
- const categoryNames = Game.HELP_CATEGORIES.map(c => c.title);
4199
+ const categoryNames = helpCategories.map(c => c.title);
3948
4200
  const pickedCategory = fuzzyPickName(query, categoryNames);
3949
4201
  if (pickedCategory) {
3950
- const category = Game.HELP_CATEGORIES.find(c => c.title === pickedCategory);
4202
+ const category = helpCategories.find(c => c.title === pickedCategory);
3951
4203
  const lines = [`${category.title.toUpperCase()}`];
3952
4204
  for (const [primary, ...aliases] of category.entries) {
3953
4205
  lines.push(`${BULLET}${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
@@ -3962,7 +4214,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3962
4214
  const pickedVerb = fuzzyPickName(words[0], [...covered, ...uncategorized])
3963
4215
  ?? fuzzyPickName(query, [...covered, ...uncategorized]);
3964
4216
  if (pickedVerb) {
3965
- const entry = Game.HELP_CATEGORIES.flatMap(c => c.entries).find(e => e.includes(pickedVerb));
4217
+ const entry = helpCategories.flatMap(c => c.entries).find(e => e.includes(pickedVerb));
3966
4218
  const primary = entry?.[0] ?? pickedVerb;
3967
4219
  const aliases = entry?.slice(1) ?? [];
3968
4220
  const CommandClass = CommandFactory.getAllRegisteredCommands()[primary];
@@ -4018,7 +4270,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4018
4270
  // line outgrows the Mechanics column (help renders there now --
4019
4271
  // blessed's mid-list wrap lost the left-column alignment).
4020
4272
  const lines = ['COMMANDS'];
4021
- const pad = Math.max(...Game.HELP_CATEGORIES.map(c => c.title.length));
4273
+ const pad = Math.max(...helpCategories.map(c => c.title.length));
4022
4274
  const pushWrapped = (title, verbs) => {
4023
4275
  const rows = [];
4024
4276
  let cur = '';
@@ -4037,7 +4289,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4037
4289
  for (const r of rows.slice(1))
4038
4290
  lines.push(`${' '.repeat(pad)} ${r}`);
4039
4291
  };
4040
- for (const category of Game.HELP_CATEGORIES) {
4292
+ for (const category of helpCategories) {
4041
4293
  pushWrapped(category.title, category.entries.map(e => e[0]));
4042
4294
  }
4043
4295
  if (uncategorized.length > 0) {