@maka/maka-cli 5.49.0 → 5.51.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 (100) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +20 -1
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +9 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +9 -8
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.d.ts +5 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +27 -6
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +2 -2
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +43 -3
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +6 -24
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/loot.d.ts +21 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/loot.js +93 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/loot.js.map +1 -0
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/map.d.ts +4 -4
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +23 -31
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/move.d.ts +32 -3
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +313 -17
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/note.d.ts +25 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/note.js +57 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/note.js.map +1 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +20 -3
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js +9 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js.map +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +1 -0
  38. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js.map +1 -1
  39. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +13 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +66 -9
  41. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +37 -1
  43. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +2 -2
  45. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +2 -2
  46. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +66 -19
  47. package/bundle/typescript/src/commands/game/sideQuest/game.js +264 -91
  48. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/headless.js +17 -2
  50. package/bundle/typescript/src/commands/game/sideQuest/headless.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +9 -0
  52. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +84 -3
  53. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +31 -0
  55. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +28 -0
  56. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +5 -1
  58. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +24 -6
  59. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +85 -2
  61. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +182 -2
  62. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +4 -4
  64. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +17 -7
  65. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  66. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +17 -13
  67. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +29 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +2 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +5 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +9 -0
  71. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +87 -8
  72. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  73. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +28 -0
  74. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +7 -0
  76. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +9 -0
  77. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  78. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.d.ts +22 -0
  79. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js +69 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js.map +1 -0
  81. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.d.ts +186 -0
  82. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +742 -0
  83. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js.map +1 -0
  84. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.d.ts +34 -0
  85. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +536 -0
  86. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js.map +1 -0
  87. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +273 -3
  88. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +890 -24
  89. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  90. package/bundle/typescript/src/commands/set/global.sub.cmd.js +18 -1
  91. package/bundle/typescript/src/commands/set/global.sub.cmd.js.map +1 -1
  92. package/bundle/typescript/src/tools/ai/ai.class.d.ts +1 -1
  93. package/bundle/typescript/src/tools/ai/ai.class.js +39 -7
  94. package/bundle/typescript/src/tools/ai/ai.class.js.map +1 -1
  95. package/bundle/typescript/src/tools/fsi/fsi.interfaces.d.ts +1 -0
  96. package/bundle/typescript/src/tools/fsi/fsi.interfaces.js.map +1 -1
  97. package/bundle/typescript/src/tools/https/https.class.d.ts +19 -0
  98. package/bundle/typescript/src/tools/https/https.class.js +36 -0
  99. package/bundle/typescript/src/tools/https/https.class.js.map +1 -1
  100. package/package.json +1 -1
@@ -12,6 +12,7 @@ import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js'
12
12
  import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
13
13
  import { bestFakeSin, wandRead, processArrest } from './utilities/sin.js';
14
14
  import { renderMapViewport } from './utilities/map-view.js';
15
+ import { renderRoomLayoutCompact } from './utilities/room-view.js';
15
16
  import { getPref } from './utilities/prefs.js';
16
17
  import { rollPool, formatRoll } from './utilities/dice.js';
17
18
  import { Direction } from './types/shared/direction-enum.js';
@@ -21,6 +22,8 @@ import { currentSession } from './utilities/session-context.js';
21
22
  import { PlayersCommand } from './commands/players.js';
22
23
  import { LookCommand } from './commands/look.js';
23
24
  import { TakeCommand } from './commands/take.js';
25
+ import { LootCommand } from './commands/loot.js';
26
+ import { NoteCommand } from './commands/note.js';
24
27
  import { GoCommand } from './commands/go.js';
25
28
  import { UseCommand } from './commands/use.js';
26
29
  import { EquipCommand } from './commands/equip.js';
@@ -146,6 +149,9 @@ export default class Game {
146
149
  /** The floating icon-minimap (map-overlay wave 2026-08-24). Optional:
147
150
  * headless sessions never build widgets. */
148
151
  _mapBox;
152
+ /** Its twin, docked to its right: the current room's own layout
153
+ * (spatial-viz follow-up 2026-08-25). See utilities/room-view.ts. */
154
+ _roomMapBox;
149
155
  _useAi = false;
150
156
  _aiRefusalAnnounced = false;
151
157
  // ACCOUNT BINDING (ruling 2026-08-23): who this runner belongs to,
@@ -1081,9 +1087,13 @@ export default class Game {
1081
1087
  * Returns a player-facing line when something changed, else null.
1082
1088
  */
1083
1089
  /** A hub-cast member by name, for roster displays that need the live
1084
- * actor (contacts.ts reads their commlink to show reachability). */
1090
+ * actor (contacts.ts reads their commlink to show reachability).
1091
+ * ROOM GRIDS wave (2026-08-25): falls back to the CURRENT run scene
1092
+ * so a live client (not yet a hub contact -- see onClientInteraction)
1093
+ * resolves too; call.ts already reaches them the same way. */
1085
1094
  hubActor(name) {
1086
- return this._hubScene?.allActors?.find(a => a.name === name && a !== this.player);
1095
+ return this._hubScene?.allActors?.find(a => a.name === name && a !== this.player)
1096
+ ?? (this.scene !== this._hubScene ? this.scene?.allActors?.find(a => a.name === name && a !== this.player) : undefined);
1087
1097
  }
1088
1098
  /** The whole hub cast -- call.ts merges these into its candidates so
1089
1099
  * a linked contact answers from ANYWHERE (a real mid-run session:
@@ -1415,16 +1425,18 @@ export default class Game {
1415
1425
  // state never is (runs are disposable); reconstructed fresh by
1416
1426
  // activateRunScene, settled into the roster by returnToHub.
1417
1427
  currentClient;
1418
- // Taxi warn-once (player ruling): leaving with the remainder unclaimed
1428
+ // Taxi warn-once (player ruling): leaving with the pay unclaimed
1419
1429
  // arms this; the second "call taxi" forfeits. Mirrors _quitArmed.
1420
1430
  forfeitArmed = false;
1421
1431
  /**
1422
1432
  * The meet and the wrap-up, triggered by the player's first/next real
1423
1433
  * interaction with the client (talk-to, a connected call, a give).
1424
- * Returns the beat's text (advance/remainder lines) or null when this
1425
- * NPC isn't the client / nothing new happens. Canon p.28-29: half up
1426
- * front at the meet, the remainder on delivering proof. Completing the
1427
- * objective BEFORE ever meeting them collapses both beats into one.
1434
+ * Returns the beat's text or null when this NPC isn't the client /
1435
+ * nothing new happens. LUMP SUM (player ruling 2026-08-25, replacing
1436
+ * canon p.28's half-up-front: the advance beat kept misfiring in
1437
+ * play): the meet sets the terms, the wrap-up pays the WHOLE number.
1438
+ * Completing the objective before ever meeting them still collapses
1439
+ * cleanly -- the wrap-up is the only beat that moves money.
1428
1440
  */
1429
1441
  onClientInteraction(npcName) {
1430
1442
  const client = this.currentClient;
@@ -1438,30 +1450,22 @@ export default class Game {
1438
1450
  const lines = [];
1439
1451
  if (!client.met) {
1440
1452
  client.met = true;
1441
- const advance = Math.floor(payout / 2);
1442
- // The team's split comes off the top of every payment (canon: the
1443
- // crew shares the take) -- the player banks their share only.
1444
- const split = this.applyCrewCut(advance);
1445
- const paid = this.scene.payRunner(split.share, 'meet advance');
1446
- client.advancePaid = advance;
1447
- if (paid) {
1448
- lines.push(`THE MEET: ${client.name} slides half across up front -- ${paid.net}¥ certified${paid.tax > 0 ? ` (${paid.tax}¥ skimmed: SIN on file)` : ''}. The other half on delivery.${hint(` (Canon terms, p.28 -- and the rate is negotiable exactly once: "haggle" while you have them.)`)}`);
1449
- if (split.line)
1450
- lines.push(` ${split.line}`);
1451
- }
1452
- Logger.getInstance().write(`Client meet: ${this.player.name} met ${client.name}, advance ${advance} (gross, crew took ${advance - split.share}).`);
1453
+ lines.push(`THE MEET: ${client.name} lays out the terms -- ${payout}¥ certified, in full, on delivery.${hint(` (The rate is negotiable exactly once: "haggle" while you have them.)`)}`);
1454
+ Logger.getInstance().write(`Client meet: ${this.player.name} met ${client.name}, terms ${payout} lump sum on delivery.`);
1453
1455
  }
1454
1456
  if (this.scene.isCompleted() && !client.reported) {
1455
1457
  client.reported = true;
1456
- const remainder = payout - client.advancePaid;
1457
- const split = this.applyCrewCut(remainder);
1458
- const paid = this.scene.payRunner(split.share, 'wrap-up remainder');
1458
+ // The team's split comes off the top (canon: the crew shares the
1459
+ // take) -- the player banks their share only. Payout read LIVE so
1460
+ // a haggle at any point before delivery moves the real number.
1461
+ const split = this.applyCrewCut(payout);
1462
+ const paid = this.scene.payRunner(split.share, 'wrap-up settlement');
1459
1463
  if (paid) {
1460
- lines.push(`WRAP-UP: ${client.name} settles the balance -- ${paid.net}¥ certified${paid.tax > 0 ? ` (${paid.tax}¥ skimmed: SIN on file)` : ''}. Clean work; the name will remember yours.`);
1464
+ lines.push(`WRAP-UP: ${client.name} pays out in full -- ${paid.net}¥ certified${paid.tax > 0 ? ` (${paid.tax}¥ skimmed: SIN on file)` : ''}. Clean work; the name will remember yours.`);
1461
1465
  if (split.line)
1462
1466
  lines.push(` ${split.line}`);
1463
1467
  }
1464
- Logger.getInstance().write(`Client wrap-up: ${this.player.name} reported to ${client.name}, remainder ${remainder} (gross, crew took ${remainder - split.share}).`);
1468
+ Logger.getInstance().write(`Client wrap-up: ${this.player.name} reported to ${client.name}, settlement ${payout} (gross, crew took ${payout - split.share}).`);
1465
1469
  // JOHNSONS DON'T LINGER (real session: a settled Ms. Slate paced
1466
1470
  // in and out of rooms and tailed the runner around the scene).
1467
1471
  // After the settlement beat lands, the client walks -- their AI
@@ -1611,6 +1615,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1611
1615
  CommandFactory.registerCommand('look', LookCommand);
1612
1616
  CommandFactory.registerCommand('go', GoCommand);
1613
1617
  CommandFactory.registerCommand('take', TakeCommand);
1618
+ CommandFactory.registerCommand('loot', LootCommand);
1619
+ CommandFactory.registerCommand('note', NoteCommand);
1614
1620
  CommandFactory.registerCommand('use', UseCommand);
1615
1621
  CommandFactory.registerCommand('equip', EquipCommand);
1616
1622
  CommandFactory.registerCommand('unequip', UnequipCommand);
@@ -1830,6 +1836,29 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1830
1836
  this.initStatusBox(screen);
1831
1837
  this.initMapBox(screen); // docked atop the Mechanics column
1832
1838
  this._screen = screen;
1839
+ // Paint the meat-plane border up front rather than leaving blessed's
1840
+ // default until the first status refresh.
1841
+ this.paintPlaneBorder();
1842
+ // RESIZE REDRAWS (player request 2026-08-25). blessed re-solves its
1843
+ // own percentage geometry on a resize, but the docked panels are
1844
+ // drawn CONTENT, not layout: the district viewport's width and the
1845
+ // room box's elevation justification are both computed from
1846
+ // screen.width at paint time (see updateExits), so after a resize
1847
+ // they kept the old terminal's dimensions until the next command
1848
+ // happened to repaint them -- a map sized for a window that no
1849
+ // longer exists, which is exactly how content spills a border.
1850
+ screen.on('resize', () => {
1851
+ try {
1852
+ this.updateStatus();
1853
+ const here = this.player?.currentLocation;
1854
+ if (here)
1855
+ this.updateExits(here);
1856
+ screen.render();
1857
+ }
1858
+ catch (err) {
1859
+ Logger.getInstance().write(`resize redraw failed: ${err instanceof Error ? err.message : String(err)}`);
1860
+ }
1861
+ });
1833
1862
  }
1834
1863
  /** The engine half of startup: scene synthesis, command registration,
1835
1864
  * hub seeding, resume overlay -- no widget contact. */
@@ -2717,7 +2746,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2717
2746
  // THE MEET opens the job (canon p.28): the seed's client is this
2718
2747
  // run's Johnson -- track them, and point the arrival at the meet.
2719
2748
  this.currentClient = seed.client
2720
- ? { name: seed.client, met: false, advancePaid: 0, reported: false }
2749
+ ? { name: seed.client, met: false, reported: false }
2721
2750
  : undefined;
2722
2751
  this.forfeitArmed = false;
2723
2752
  // THE CLIENT REACHES OUT (ruling 2026-08-24, friction pass): the
@@ -2729,7 +2758,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2729
2758
  const clientRoom = clientActor?.currentLocation?.name;
2730
2759
  const clientSpot = clientActor ? spotOf(clientActor) : undefined;
2731
2760
  const meetLine = seed.client
2732
- ? `\n📞 Your link chirps -- {bold}{underline}${seed.client}{/underline}{/bold}: "${clientRoom ? `I'm at ${clientRoom}${clientSpot ? `, the ${clientSpot}` : ''}` : `I'm on-site`} -- waiting on you. Come sit down before somebody else does." Half up front at the table; the rest on delivery.${hint(` (Find them and talk, or "call ${seed.client}" -- the rate is negotiable at the meet, once.)`)}`
2761
+ ? `\n📞 Your link chirps -- {bold}{underline}${seed.client}{/underline}{/bold}: "${clientRoom ? `I'm at ${clientRoom}${clientSpot ? `, the ${clientSpot}` : ''}` : `I'm on-site`} -- waiting on you. Come sit down before somebody else does." Payment in full on delivery.${hint(` (Find them and talk, or "call ${seed.client}" -- the rate is negotiable at the meet, once.)`)}`
2733
2762
  : '';
2734
2763
  // No "=== JOB #n ===" banner on entry (player request) -- the Game
2735
2764
  // Log window's own title carries the job name now.
@@ -3159,22 +3188,32 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3159
3188
  screen.append(statusBox);
3160
3189
  }
3161
3190
  /**
3162
- * THE MINIMAP (map-overlay wave 2026-08-24): a small icon viewport
3163
- * DOCKED at the top of the Mechanics column -- not floated over the
3164
- * log (a float occluded log text and, being numerically anchored,
3165
- * drifted on terminal resize; percentage geometry re-solves both).
3166
- * The Mechanics pane shifts down while the map is shown and takes the
3167
- * space back when it hides. keys:false -- a keyable box appended
3168
- * after the input steals focus (the "eexxiitt" bug, see initInputBox).
3169
- * Hidden until the first setMapOverlay with content.
3191
+ * THE MINIMAP (map-overlay wave 2026-08-24; split into a pair
3192
+ * 2026-08-25 -- player ask: "ample space to split the existing map in
3193
+ * two, one for the area, one for the room"): two small icon
3194
+ * viewports DOCKED side by side at the top of the Mechanics column --
3195
+ * not floated over the log (a float occluded log text and, being
3196
+ * numerically anchored, drifted on terminal resize; percentage
3197
+ * geometry re-solves both). The Mechanics pane shifts down while
3198
+ * EITHER is shown and takes the space back only once both hide.
3199
+ * keys:false -- a keyable box appended after the input steals focus
3200
+ * (the "eexxiitt" bug, see initInputBox). Hidden until the first
3201
+ * setMapOverlay/setRoomOverlay with content.
3170
3202
  */
3171
- static MAP_DOCK_ROWS = 11; // 9 viewport rows + border
3203
+ // 9 viewport rows + a caption row (the room name / "@ spot", moved off
3204
+ // the HUD 2026-08-25) + border.
3205
+ static MAP_DOCK_ROWS = 12;
3206
+ /** Must track the roomMapBox's `width` below -- updateExits derives
3207
+ * the inner width from it to right-justify the elevation bar. */
3208
+ static ROOM_DOCK_PCT = 0.23;
3209
+ /** Must track the mapBox's `width` below -- used to trim its caption. */
3210
+ static MAP_DOCK_PCT = 0.12;
3172
3211
  initMapBox(screen) {
3173
3212
  const mapBox = blessed.box({
3174
3213
  id: 'mapBox',
3175
3214
  top: 0,
3176
3215
  left: '65%',
3177
- width: '35%',
3216
+ width: '12%',
3178
3217
  height: Game.MAP_DOCK_ROWS,
3179
3218
  border: { type: 'line' },
3180
3219
  style: { border: { fg: 'blue' } },
@@ -3186,19 +3225,48 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3186
3225
  });
3187
3226
  this._mapBox = mapBox;
3188
3227
  screen.append(mapBox);
3228
+ const roomMapBox = blessed.box({
3229
+ id: 'roomMapBox',
3230
+ top: 0,
3231
+ // Much wider than its district twin: this box carries the grid,
3232
+ // the elevation bar down its right side, AND (since the
3233
+ // 2026-08-25 room resize) a `large` room's full 12 cells at two
3234
+ // columns each -- the district view needs only its fixed 15-wide
3235
+ // viewport, so the space is better spent here.
3236
+ left: '77%',
3237
+ width: '23%',
3238
+ height: Game.MAP_DOCK_ROWS,
3239
+ border: { type: 'line' },
3240
+ style: { border: { fg: 'blue' } },
3241
+ label: ' Room ',
3242
+ tags: true,
3243
+ keys: false,
3244
+ content: '',
3245
+ hidden: true,
3246
+ });
3247
+ this._roomMapBox = roomMapBox;
3248
+ screen.append(roomMapBox);
3189
3249
  }
3190
- /** Shifts the Mechanics pane down (map shown) or back up (hidden). */
3250
+ /** Shifts the Mechanics pane down (a map box is shown) or back up
3251
+ * (neither is). Driven by syncMapDock, never called directly. */
3191
3252
  dockMap(shown) {
3192
3253
  if (!this._metaBox)
3193
3254
  return;
3194
3255
  this._metaBox.top = shown ? Game.MAP_DOCK_ROWS : 0;
3195
3256
  this._metaBox.height = shown ? `100%-${11 + Game.MAP_DOCK_ROWS}` : '100%-11';
3196
3257
  }
3258
+ /** Keeps the Mechanics pane's dock offset in sync with whichever
3259
+ * docked box(es) -- district, room, or both -- are currently shown. */
3260
+ syncMapDock() {
3261
+ const anyShown = (this._mapBox !== undefined && !this._mapBox.hidden)
3262
+ || (this._roomMapBox !== undefined && !this._roomMapBox.hidden);
3263
+ this.dockMap(anyShown);
3264
+ }
3197
3265
  /**
3198
- * Paints (or hides) the docked minimap. Solo: updateExits feeds it
3199
- * from the local scene; shared runs: SharedRunSession.route feeds it
3200
- * from the server's per-player kind:'map' events. The preference
3201
- * gates both paths in one place.
3266
+ * Paints (or hides) the docked DISTRICT minimap. Solo: updateExits
3267
+ * feeds it from the local scene; shared runs: SharedRunSession.route
3268
+ * feeds it from the server's per-player kind:'map' events. The
3269
+ * preference gates both this and setRoomOverlay in one place.
3202
3270
  */
3203
3271
  setMapOverlay(lines) {
3204
3272
  if (!this._mapBox)
@@ -3208,15 +3276,41 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3208
3276
  return;
3209
3277
  }
3210
3278
  this._mapBox.setContent(lines.join('\n'));
3211
- this.dockMap(true);
3212
3279
  this._mapBox.show();
3280
+ this.syncMapDock();
3213
3281
  this._screen?.render();
3214
3282
  }
3215
3283
  hideMapOverlay() {
3216
3284
  if (!this._mapBox)
3217
3285
  return;
3218
3286
  this._mapBox.hide();
3219
- this.dockMap(false);
3287
+ this.syncMapDock();
3288
+ this._screen?.render();
3289
+ }
3290
+ /**
3291
+ * Paints (or hides) the docked ROOM minimap -- the district box's
3292
+ * twin, fed by updateExits from utilities/room-view.ts. Empty
3293
+ * whenever the current room has no synthesized grid (spotless --
3294
+ * THE COMPAT RULE) or the player is circulating in it (nothing fixed
3295
+ * to center on): both read as "hide", same as an empty district.
3296
+ */
3297
+ setRoomOverlay(lines) {
3298
+ if (!this._roomMapBox)
3299
+ return;
3300
+ if (lines.length === 0 || !getPref('mapOverlay', true)) {
3301
+ this.hideRoomOverlay();
3302
+ return;
3303
+ }
3304
+ this._roomMapBox.setContent(lines.join('\n'));
3305
+ this._roomMapBox.show();
3306
+ this.syncMapDock();
3307
+ this._screen?.render();
3308
+ }
3309
+ hideRoomOverlay() {
3310
+ if (!this._roomMapBox)
3311
+ return;
3312
+ this._roomMapBox.hide();
3313
+ this.syncMapDock();
3220
3314
  this._screen?.render();
3221
3315
  }
3222
3316
  /**
@@ -3234,6 +3328,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3234
3328
  if (this.remoteSession)
3235
3329
  return;
3236
3330
  this._statusBox.setContent(this.statusLinesFor(this.player).join('\n'));
3331
+ // The log's border tracks the plane -- updateStatus is the one hook
3332
+ // every plane transition already funnels through.
3333
+ this.paintPlaneBorder();
3237
3334
  this._screen.render();
3238
3335
  }
3239
3336
  /** ui plumbing for session mode (utilities/shared-run.ts). */
@@ -3248,32 +3345,75 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3248
3345
  * no widgets. updateStatus paints it for the local player; a shared-run
3249
3346
  * server ships it per player as status events (Phase C).
3250
3347
  */
3348
+ /**
3349
+ * What's in your hands, in one phrase. Three armed states on the meat
3350
+ * plane: drawn, HOLSTERED (carried but riding leather -- "brandish"
3351
+ * puts it in hand; combat auto-draws, so this is posture rather than
3352
+ * readiness), or nothing at all. Off-plane the profile is attack
3353
+ * programs / astral will and reads as Armed. White, not gray, for the
3354
+ * same legibility reason as the stance it now sits beside.
3355
+ */
3356
+ armedLine(player) {
3357
+ const weapon = player.getWeaponProfile();
3358
+ const weaponItem = player.getCarriedWeapon();
3359
+ const ammoNote = weaponItem?.isFirearm()
3360
+ ? (weaponItem.jammed
3361
+ ? ` {red-fg}[JAMMED -- reboot]{/red-fg}`
3362
+ : weaponItem.ammo === 0 ? ` {red-fg}[DRY]{/red-fg}` : ` [${weaponItem.ammo} rds]`)
3363
+ : '';
3364
+ if (player.plane === 'meat' && weaponItem && !player.weaponDrawn) {
3365
+ return `{white-fg}Holstered: ${weaponItem.name}${ammoNote}${hint(' -- "brandish"')}{/white-fg}`;
3366
+ }
3367
+ if (weapon.name === 'bare fists')
3368
+ return `{gray-fg}Unarmed{/gray-fg}`;
3369
+ return `Armed: ${weapon.name} (DV ${weapon.damage}${weapon.poolBonus > 0 ? `, +${weapon.poolBonus} dice` : ''})${ammoNote}`;
3370
+ }
3371
+ /**
3372
+ * THE LOG WEARS THE PLANE (player request 2026-08-25). Jacking in or
3373
+ * stepping out of your body changes what every line in that log even
3374
+ * MEANS, and the only marker was a HUD row well below it. The border
3375
+ * takes the plane's own established colour -- cyan for the Matrix,
3376
+ * magenta for astral, yellow for a jumped-in drone, matching the
3377
+ * tints those planes already use throughout the prose.
3378
+ */
3379
+ planeBorderColor(player) {
3380
+ switch (player.plane) {
3381
+ case 'matrix': return 'cyan';
3382
+ case 'astral': return 'magenta';
3383
+ case 'drone': return 'yellow';
3384
+ default: return 'white';
3385
+ }
3386
+ }
3387
+ paintPlaneBorder() {
3388
+ if (!this._logBox)
3389
+ return;
3390
+ const style = this._logBox.style;
3391
+ style.border = { ...(style.border ?? {}), fg: this.planeBorderColor(this.player) };
3392
+ }
3251
3393
  statusLinesFor(player) {
3252
3394
  const lines = [];
3253
- // Line 1: where you are, and the fight autopilot mode. The stance
3254
- // lives HERE -- the shortest HUD line -- because appended to the long
3255
- // readiness line it wrapped out of the box on narrower terminals and
3256
- // effectively only ever showed in the Tab announcement. Always shown,
3257
- // even MANUAL: the HUD is how players discover Tab cycles it at all
3258
- // (see utilities/auto-fight.ts). MANUAL is white, not gray -- blessed's
3395
+ // Line 1: the fight autopilot mode. The stance lives HERE -- the
3396
+ // shortest HUD line -- because appended to the long readiness line it
3397
+ // wrapped out of the box on narrower terminals and effectively only
3398
+ // ever showed in the Tab announcement. Always shown, even MANUAL: the
3399
+ // HUD is how players discover Tab cycles it at all (see
3400
+ // utilities/auto-fight.ts). MANUAL is white, not gray -- blessed's
3259
3401
  // gray is ANSI "bright black", which on a stock dark terminal palette
3260
3402
  // is illegible enough that the stance looked absent entirely.
3403
+ //
3404
+ // WHERE YOU ARE MOVED OUT (player ruling 2026-08-25): the room name
3405
+ // now captions the Map box and the "@ spot" captions the Room box,
3406
+ // each under the picture it belongs to, so the HUD stops repeating
3407
+ // what two panels already show. The breadcrumb went with it -- the
3408
+ // district map draws the way you came far better than a text trail.
3261
3409
  const stanceColor = player.autoStance === 'aggressive' ? 'red' : player.autoStance === 'guarded' ? 'yellow' : 'white';
3262
- const stance = `{${stanceColor}-fg}⚔ ${player.autoStance.toUpperCase()}{/${stanceColor}-fg}`;
3263
- // The breadcrumb, reading chronologically into the current location:
3264
- // "Neon Strip (west) > Night Market" -- origin first, the direction
3265
- // BACK to it in parens. An 18-room hub made "which way did I come in"
3266
- // a real question.
3267
- const crumb = player.cameFrom ? `${player.cameFrom.room} (${player.cameFrom.back}) > ` : '';
3268
- if (player.currentLocation) {
3269
- // "At" spots: where you stand WITHIN the room, when it has places
3270
- // to stand (see utilities/spots.ts).
3271
- const atNote = spotOf(player) ? ` @ ${player.atSpot}` : '';
3272
- lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}${atNote}{/bold} | ${stance}`);
3273
- }
3274
- else {
3275
- lines.push(stance);
3276
- }
3410
+ // WHAT'S IN YOUR HANDS rides alongside the stance (player ruling
3411
+ // 2026-08-25). The two answer the same question -- how ready are you
3412
+ // -- and the stance line was left short and lonely once the location
3413
+ // moved out to the map captions, while the readiness line below ran
3414
+ // long. armedLine is built further down with the rest of the gear
3415
+ // read; only its placement moved.
3416
+ lines.push(`{${stanceColor}-fg}⚔ ${player.autoStance.toUpperCase()}{/${stanceColor}-fg} ${this.armedLine(player)}`);
3277
3417
  // Line 2: both condition monitors -- in a game where a fight can kill
3278
3418
  // you and casting can knock you out, current damage on either track is
3279
3419
  // never something a player should have to ask for. Colors shift as
@@ -3292,26 +3432,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3292
3432
  const heat = this.heatBand();
3293
3433
  if (heat && player === this.player)
3294
3434
  lines.push(heat);
3295
- // Line 3: readiness -- armed state, armor, and whether the commlink is
3296
- // actually live (equipped), the recurring "why can't I call anyone"
3297
- // friction surfaced at a glance.
3298
- const weapon = player.getWeaponProfile();
3299
- const weaponItem = player.getCarriedWeapon();
3300
- const ammoNote = weaponItem?.isFirearm()
3301
- ? (weaponItem.jammed
3302
- ? ` {red-fg}[JAMMED -- reboot]{/red-fg}`
3303
- : weaponItem.ammo === 0 ? ` {red-fg}[DRY]{/red-fg}` : ` [${weaponItem.ammo} rds]`)
3304
- : '';
3305
- // Three armed states on the meat plane: drawn, holstered (weapon
3306
- // carried but riding leather -- "brandish" puts it in hand; combat
3307
- // auto-draws so this is posture, not readiness), or nothing at all.
3308
- // Off-plane the profile is attack programs / astral will and reads as
3309
- // Armed. White, not gray, for the same legibility reason as the stance.
3310
- const armed = player.plane === 'meat' && weaponItem && !player.weaponDrawn
3311
- ? `{white-fg}Holstered: ${weaponItem.name}${ammoNote}${hint(' -- "brandish"')}{/white-fg}`
3312
- : weapon.name === 'bare fists'
3313
- ? `{gray-fg}Unarmed{/gray-fg}`
3314
- : `Armed: ${weapon.name} (DV ${weapon.damage}${weapon.poolBonus > 0 ? `, +${weapon.poolBonus} dice` : ''})${ammoNote}`;
3435
+ // Line 3: the rest of the readiness read -- armor, and whether the
3436
+ // commlink is actually live (equipped), the recurring "why can't I
3437
+ // call anyone" friction surfaced at a glance. The armed state used
3438
+ // to lead this line; it now rides with the stance on line 1.
3315
3439
  const armorNote = `Armor +${player.getArmorValue()}`;
3316
3440
  const hasLink = player.equipment.head?.commlink != null;
3317
3441
  const link = !hasLink
@@ -3321,7 +3445,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3321
3445
  : player.arOffline
3322
3446
  ? `{red-fg}AR CUT -- reboot{/red-fg}`
3323
3447
  : `{${CALL_COLOR}-fg}Link online{/${CALL_COLOR}-fg}`;
3324
- lines.push(`${armed} | ${armorNote} | ${link}`);
3448
+ lines.push(`${armorNote} | ${link}`);
3325
3449
  // Lines 4+: transient states, present only while active. Future
3326
3450
  // contexts (astral projection, VR hot-sim) join here the same way.
3327
3451
  if (player.sustainedArmor > 0) {
@@ -3414,22 +3538,71 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3414
3538
  this.notePresenceAffectingChange();
3415
3539
  }
3416
3540
  updateExits(_room) {
3417
- // THE ICON OVERLAY (map-overlay wave 2026-08-24): this hook was
3418
- // kept as a no-op for exactly this -- it already fires on movement
3419
- // (go/lock/unlock, ui.ts), scene transitions, and barrier changes.
3541
+ // THE ICON OVERLAYS (map-overlay wave 2026-08-24, split 2026-08-25):
3542
+ // this hook was kept as a no-op for exactly this -- it already
3543
+ // fires on movement (go/lock/unlock, move.ts, ui.ts), scene
3544
+ // transitions, and barrier changes (solve/bypass/lock/unlock).
3420
3545
  // Session mode: the server's per-player kind:'map' events feed the
3421
- // overlay instead (SharedRunSession.route).
3546
+ // district overlay instead (SharedRunSession.route); the room box
3547
+ // has no server-pushed path yet, so it just stays hidden in a
3548
+ // shared run, same as the district box did before that wiring.
3422
3549
  if (!this._mapBox || this.remoteSession)
3423
3550
  return;
3424
3551
  const here = this.player.currentLocation;
3425
3552
  if (!here || this.player.plane === 'matrix') {
3426
3553
  this.hideMapOverlay();
3554
+ this.hideRoomOverlay();
3427
3555
  return;
3428
3556
  }
3429
- this.setMapOverlay(renderMapViewport(this.scene.getRooms(), here, (r, d) => this.scene.barrierSealing(r, d), {
3557
+ const screenW = Number(this._screen?.width) || 0;
3558
+ // Each box's inner width. Derived from the screen rather than read
3559
+ // off the widget: blessed resolves percentage geometry at render
3560
+ // time, so `.width` is still the raw '23%' here. Used to right-
3561
+ // justify the elevation bar and to trim the caption rows.
3562
+ const boxWidth = screenW > 0 ? Math.floor(screenW * Game.ROOM_DOCK_PCT) - 2 : undefined;
3563
+ const mapWidth = screenW > 0 ? Math.floor(screenW * Game.MAP_DOCK_PCT) - 2 : undefined;
3564
+ const caption = (text, width) => {
3565
+ const w = width ?? text.length;
3566
+ return `{bold}${text.length > w ? `${text.slice(0, Math.max(1, w - 1))}…` : text}{/bold}`;
3567
+ };
3568
+ // The district viewport must FIT ITS BOX. It was a hardcoded 15
3569
+ // columns, which was fine while the Map box was 35% wide and
3570
+ // silently overflowed into the Room box once it shrank to 12% --
3571
+ // on a narrower terminal the spill landed inside the neighbouring
3572
+ // border and smartCSR's partial redraws smeared it into stray
3573
+ // characters (playtest 2026-08-25: "getting some artifacting 'g'";
3574
+ // ui.ts documents this same smear from column drift). Odd widths
3575
+ // keep the player marker exactly centred.
3576
+ const mapInner = mapWidth ?? 15;
3577
+ const mapView = Math.max(7, mapInner % 2 === 0 ? mapInner - 1 : mapInner);
3578
+ const mapLines = renderMapViewport(this.scene.getRooms(), here, (r, d) => this.scene.barrierSealing(r, d), {
3430
3579
  visited: this.player.visitedRooms,
3431
3580
  arEyes: this.player.hasMatrixEyes(),
3432
- }));
3581
+ }, mapView);
3582
+ // The captions the HUD used to carry: where you are under the
3583
+ // district, where you stand under the room.
3584
+ if (mapLines.length > 0)
3585
+ mapLines.push(caption(here.name, mapWidth));
3586
+ this.setMapOverlay(mapLines);
3587
+ // A DECORATION MUST NEVER KILL THE RUN. This panel is pure derived
3588
+ // geometry -- if synthesis or rendering ever throws (a malformed
3589
+ // seed, a room shape no one anticipated), the right outcome is a
3590
+ // blank box and a log line, not a dead session mid-job.
3591
+ try {
3592
+ const roomLines = renderRoomLayoutCompact(here, this.player, {
3593
+ boxWidth,
3594
+ isOtherPlayer: a => this.scene.isHumanControlled(a),
3595
+ });
3596
+ if (roomLines.length > 0) {
3597
+ const spot = spotOf(this.player);
3598
+ roomLines.push(caption(spot ? `@ ${spot}` : '(moving through)', boxWidth));
3599
+ }
3600
+ this.setRoomOverlay(roomLines);
3601
+ }
3602
+ catch (err) {
3603
+ Logger.getInstance().write(`Room overlay render failed in ${here.name}: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
3604
+ this.hideRoomOverlay();
3605
+ }
3433
3606
  }
3434
3607
  updateInventory(_playerInventory, _currencyName = 'currency') {
3435
3608
  this.updateStatus();