@maka/maka-cli 5.8.0 → 5.10.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 (166) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +4 -2
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +3 -2
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -1
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +14 -6
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +3 -2
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +24 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +2 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +31 -4
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +16 -2
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.d.ts +7 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +86 -15
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +5 -4
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +2 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +5 -4
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +2 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +5 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +5 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +94 -16
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.d.ts +38 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +178 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js.map +1 -0
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +2 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +7 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/hints.d.ts +10 -0
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/hints.js +24 -0
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/hints.js.map +1 -0
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +2 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +2 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +2 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js.map +1 -1
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js +2 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js.map +1 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +48 -4
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +3 -2
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -1
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/move.d.ts +41 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +160 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +2 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +5 -0
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js +2 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js.map +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +2 -1
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js.map +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +8 -2
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +15 -4
  72. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  73. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +7 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js.map +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +2 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js +3 -2
  78. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js.map +1 -1
  79. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js +11 -1
  80. package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js.map +1 -1
  81. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +24 -13
  82. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -1
  83. package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +2 -1
  84. package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js.map +1 -1
  85. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js +4 -0
  86. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js.map +1 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +37 -4
  88. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +6 -0
  90. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
  91. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js +5 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js.map +1 -1
  93. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +9 -2
  94. package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
  95. package/bundle/typescript/src/commands/game/sideQuest/commands/work.js +2 -1
  96. package/bundle/typescript/src/commands/game/sideQuest/commands/work.js.map +1 -1
  97. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js +2 -1
  98. package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js.map +1 -1
  99. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js +2 -1
  100. package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js.map +1 -1
  101. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +5 -0
  102. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js.map +1 -1
  103. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +11 -0
  104. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +138 -23
  105. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  106. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +12 -0
  107. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  108. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +33 -1
  109. package/bundle/typescript/src/commands/game/sideQuest/game.js +303 -26
  110. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  111. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +2 -0
  112. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +3 -0
  113. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  114. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +13 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +195 -6
  116. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  117. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +11 -2
  118. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +35 -5
  119. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  120. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +12 -0
  121. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +19 -1
  122. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
  123. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +12 -0
  124. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +51 -4
  125. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  126. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +45 -0
  127. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +197 -4
  128. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  129. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +76 -20
  130. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +157 -34
  131. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +208 -0
  132. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.js +14 -0
  133. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.js.map +1 -0
  134. package/bundle/typescript/src/commands/game/sideQuest/types/seed/exit-seed.d.ts +1 -0
  135. package/bundle/typescript/src/commands/game/sideQuest/types/seed/item-seed.d.ts +1 -0
  136. package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +1 -0
  137. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +1 -0
  138. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +6 -0
  139. package/bundle/typescript/src/commands/game/sideQuest/ui.d.ts +6 -0
  140. package/bundle/typescript/src/commands/game/sideQuest/ui.js +100 -7
  141. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  142. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +18 -4
  143. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
  144. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +50 -0
  145. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +232 -0
  146. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -0
  147. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +10 -0
  148. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +17 -0
  149. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  150. package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.d.ts +36 -0
  151. package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.js +57 -0
  152. package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.js.map +1 -0
  153. package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.d.ts +4 -0
  154. package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js +55 -0
  155. package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js.map +1 -0
  156. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.d.ts +71 -0
  157. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +447 -0
  158. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -0
  159. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +8 -7
  160. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  161. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +121 -0
  162. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +315 -0
  163. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -0
  164. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +174 -18
  165. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  166. package/package.json +1 -1
@@ -3,6 +3,10 @@ import { capitalCase } from 'change-case';
3
3
  import { Category } from './types/shared/item-enum.js';
4
4
  import { Item } from './models/item.js';
5
5
  import { Room } from './models/_index.js';
6
+ import { Door } from './models/door.js';
7
+ import { SAVE_VERSION } from './types/save-file.js';
8
+ import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, } from './utilities/persistence.js';
9
+ import { pushSave, burnCloudSaveFor } from './utilities/cloud-saves.js';
6
10
  import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
7
11
  import { rollPool, formatRoll } from './utilities/dice.js';
8
12
  import { Direction } from './types/shared/direction-enum.js';
@@ -33,6 +37,7 @@ import { SilentCommand } from './commands/silent.js';
33
37
  import { RestCommand } from './commands/rest.js';
34
38
  import { ReloadCommand } from './commands/reload.js';
35
39
  import { SubdueCommand } from './commands/subdue.js';
40
+ import { GrappleCommand, ReleaseCommand, StruggleCommand } from './commands/grapple.js';
36
41
  import { EdgeCommand } from './commands/edge.js';
37
42
  import { SheetCommand } from './commands/sheet.js';
38
43
  import { CallCommand } from './commands/call.js';
@@ -59,6 +64,9 @@ import { InitiateCommand } from './commands/initiate.js';
59
64
  import { SummonCommand } from './commands/summon.js';
60
65
  import { OrderCommand } from './commands/order.js';
61
66
  import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
67
+ import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
68
+ import { HintsCommand } from './commands/hints.js';
69
+ import { hint } from './utilities/hints.js';
62
70
  import { InstallCommand } from './commands/install.js';
63
71
  import { BuyCommand } from './commands/buy.js';
64
72
  import { SellCommand } from './commands/sell.js';
@@ -82,6 +90,7 @@ import { SceneSeedGenerator, FIXER_NAME } from './factories/scene-seed-generator
82
90
  import { CALL_ICON, CALL_COLOR } from './utilities/comm-style.js';
83
91
  import { AI } from '../../../tools/ai/ai.class.js';
84
92
  import { BULLET } from './utilities/log-style.js';
93
+ import { spotOf } from './utilities/spots.js';
85
94
  export default class Game {
86
95
  initialized;
87
96
  static instance;
@@ -98,6 +107,10 @@ export default class Game {
98
107
  _screen;
99
108
  _logBox;
100
109
  _metaBox;
110
+ // Quit confirmation state (see handleInput / initInputBox): armed by
111
+ // the first quit/exit or ESC, cleared by anything that isn't a yes.
112
+ _quitArmed = false;
113
+ _escArmedAt = 0;
101
114
  _statusBox;
102
115
  _inputBox;
103
116
  _useAi = false;
@@ -188,6 +201,13 @@ export default class Game {
188
201
  // Drones deployed at travel time re-shell on arrival ("the rotors spin
189
202
  // back up at the curb") -- tracked as their items across the swap.
190
203
  _pendingRedeploys = [];
204
+ // ---------------- PERSISTENCE (see utilities/persistence.ts) ----------
205
+ // Character + hub world survive the process (player rulings): autosave
206
+ // at safe beats only, one slot per character, PERMADEATH on real death.
207
+ _saveFilePath; // from options.saveFilePath; unset = never save
208
+ _dead = false; // set in close(); suppresses any later save
209
+ _resume; // present when this session continues a save
210
+ _hubSeed; // the seed embedded into every save
191
211
  /**
192
212
  * The one correct runtime NPC spawn (mirrors scene-factory's seed
193
213
  * path): construct with a scene-bound registry, add to the scene
@@ -414,7 +434,7 @@ export default class Game {
414
434
  if (kind === 'gig')
415
435
  c.gigs += 1;
416
436
  Logger.getInstance().write(`Contact opened: ${name} (connection ${c.connection}).`);
417
- return `${name} knows your name now -- a CONTACT (Connection ${c.connection}, Loyalty 1). ("contacts" for the roster, "ask ${name} about ..." for legwork.)`;
437
+ return `${name} knows your name now -- a CONTACT (Connection ${c.connection}, Loyalty 1).${hint(` ("contacts" for the roster, "ask ${name} about ..." for legwork.)`)}`;
418
438
  }
419
439
  if (kind === 'gig') {
420
440
  c.gigs += 1;
@@ -428,7 +448,7 @@ export default class Game {
428
448
  c.spent += spent;
429
449
  if (Math.floor(c.spent / 1000) > thousandsBefore && c.loyalty < 3) {
430
450
  c.loyalty += 1;
431
- return `${name} has started keeping your usual ready. (Loyalty ${c.loyalty} -- patronage tops out at 3; run their errands to go deeper.)`;
451
+ return `${name} has started keeping your usual ready.${hint(` (Loyalty ${c.loyalty} -- patronage tops out at 3; run their errands to go deeper.)`)}`;
432
452
  }
433
453
  return null;
434
454
  }
@@ -710,6 +730,12 @@ export default class Game {
710
730
  CommandFactory.registerCommand('rest', RestCommand);
711
731
  CommandFactory.registerCommand('reload', ReloadCommand);
712
732
  CommandFactory.registerCommand('subdue', SubdueCommand);
733
+ // SUBDUING p.195 (see commands/grapple.ts): restrain without damage.
734
+ CommandFactory.registerCommand('grapple', GrappleCommand);
735
+ CommandFactory.registerCommand('restrain', GrappleCommand);
736
+ CommandFactory.registerCommand('clinch', GrappleCommand);
737
+ CommandFactory.registerCommand('release', ReleaseCommand);
738
+ CommandFactory.registerCommand('struggle', StruggleCommand);
713
739
  CommandFactory.registerCommand('edge', EdgeCommand);
714
740
  CommandFactory.registerCommand('sheet', SheetCommand);
715
741
  CommandFactory.registerCommand('stats', SheetCommand);
@@ -747,6 +773,15 @@ export default class Game {
747
773
  CommandFactory.registerCommand('submerge', InitiateCommand);
748
774
  CommandFactory.registerCommand('summon', SummonCommand);
749
775
  CommandFactory.registerCommand('conjure', SummonCommand);
776
+ // "At" spots (utilities/spots.ts): walk/move/approach are one
777
+ // crossing; run and sprint are the fast modes (combat semantics in
778
+ // the spot-move phase).
779
+ CommandFactory.registerCommand('walk', MoveCommand);
780
+ CommandFactory.registerCommand('move', MoveCommand);
781
+ CommandFactory.registerCommand('approach', MoveCommand);
782
+ CommandFactory.registerCommand('run', RunCommand);
783
+ CommandFactory.registerCommand('sprint', SprintCommand);
784
+ CommandFactory.registerCommand('hints', HintsCommand);
750
785
  CommandFactory.registerCommand('order', OrderCommand);
751
786
  CommandFactory.registerCommand('deploy', DeployCommand);
752
787
  CommandFactory.registerCommand('recall', RecallCommand);
@@ -784,6 +819,19 @@ export default class Game {
784
819
  if (this._homeRoomExitDirection) {
785
820
  this._homeRoom.exits.delete(this._homeRoomExitDirection);
786
821
  }
822
+ // The OTHER half of the previous link: sweep any exit on the anchor
823
+ // room that already leads home before choosing a direction. Without
824
+ // this, every homecoming re-ran the search against a room still
825
+ // holding last time's hideout door and stacked a second one on the
826
+ // next free direction (a real session's Neon Strip ended up with
827
+ // "up" AND "down" both reading "To Your Hideout").
828
+ for (const [dir, ex] of [...jobStartRoom.exits.entries()]) {
829
+ const leadsHome = ex instanceof Door
830
+ ? ex.getOtherSide(jobStartRoom) === this._homeRoom
831
+ : ex === this._homeRoom.name;
832
+ if (leadsHome)
833
+ jobStartRoom.exits.delete(dir);
834
+ }
787
835
  const direction = Game.HOME_DIRECTION_PRIORITY.find(d => !jobStartRoom.exits.has(d));
788
836
  if (!direction) {
789
837
  // Every direction is already spoken for on this job's start room --
@@ -811,6 +859,7 @@ export default class Game {
811
859
  async init(options, screen) {
812
860
  this._useAi = options.useAi;
813
861
  this._logFilePath = options.logFilePath;
862
+ this._saveFilePath = options.saveFilePath;
814
863
  this.initInputBox(screen);
815
864
  this.initLogBox(screen);
816
865
  this.initMetaBox(screen);
@@ -853,17 +902,36 @@ export default class Game {
853
902
  if (this.scene.isHub) {
854
903
  this._hubScene = this.scene;
855
904
  this._hubName = this.sceneSeed?.name ?? 'home';
905
+ // Every save embeds this seed -- resume never depends on the
906
+ // original scene file still existing or matching.
907
+ this._hubSeed = this.sceneSeed;
856
908
  // The street starts talking from minute one.
857
909
  this._rumorPool = this.sceneSeed?.rumors ?? [];
858
910
  this.advanceRumor();
859
911
  // The economy wakes up with it: shelf blueprints for restocking,
860
912
  // the first odd job on offer, and the lifestyle's edge perk.
913
+ // snapshotVendorStock deliberately runs BEFORE any resume overlay:
914
+ // the restock blueprint is the seed's PRISTINE shelves (config
915
+ // copies), and restockVendorShelves only refills what the overlay
916
+ // leaves empty.
861
917
  this.snapshotVendorStock();
862
918
  this._gigPool = this.sceneSeed?.gigs ?? [];
863
919
  this.advanceGig();
864
920
  this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
865
- // Home turf knows its own (see seedHomeTurfContacts).
921
+ // Home turf knows its own (see seedHomeTurfContacts). On resume the
922
+ // saved contacts overwrite these loyalty-1 seeds a few lines down.
866
923
  this.seedHomeTurfContacts();
924
+ // The next run starts cooking the moment you're home (player
925
+ // request): generation takes 20-40s, and waiting for the player
926
+ // to CALL first meant "give me a bit" every time. Draft now --
927
+ // Mr. Johnson's "1 new message" ping lands mid-session, and the
928
+ // call finds work already waiting. The gate keeps this a no-op
929
+ // with AI off or a draft/pending seed somehow already in flight.
930
+ // GATED on resume: a restored pendingRun must not be clobbered by
931
+ // a fresh background draft landing 20-40s into the session.
932
+ if (!this._resume && this.canDraftNextScene().allowed) {
933
+ this.draftNextSceneInBackground();
934
+ }
867
935
  }
868
936
  // Gear up automatically: wearables (and the commlink) slot into any
869
937
  // EMPTY slots at load -- no "equip all" ritual to start a session
@@ -872,17 +940,133 @@ export default class Game {
872
940
  equipIntoEmptySlots(this.player);
873
941
  this.warmNpcAppearancesInBackground();
874
942
  this.startPursuitHeartbeat();
943
+ // The banner learns where we are only after the hub/scene bookkeeping
944
+ // above settles.
945
+ this.refreshLogLabel();
946
+ // RESUME (see utilities/persistence.ts): lay the save over the
947
+ // freshly-synthesized hub -- player sheet/gear, hub world diffs,
948
+ // and the Game-level ledgers -- AFTER all the fresh-start seeding
949
+ // above, so the save is authoritative everywhere they overlap.
950
+ if (this._resume && this._hubScene) {
951
+ this.applyResumeState(this._resume);
952
+ }
875
953
  this.initialized = true;
954
+ if (this._resume) {
955
+ const pendingNote = this._pendingRunSeed
956
+ ? hint(`\n\n${FIXER_NAME}'s job is still on offer: "${this._pendingRunSeed.name}". "call taxi" when you're ready, or "call ${FIXER_NAME}" to hear it again.`)
957
+ : '';
958
+ return `Welcome back, ${this.player.name}. The cot remembers your shape.\n ${this.scene.story}${pendingNote}`;
959
+ }
876
960
  // Nudge the player toward "call" up front -- the fixer convention means
877
961
  // the story deliberately doesn't narrate a completed briefing, so
878
962
  // without this hint there'd be no in-game indication that "call" is how
879
963
  // you actually get your orders.
880
964
  const wc = this.scene.getWinCondition();
881
965
  const callHint = (wc?.type === 'give' && wc.toNpc)
882
- ? `\n\nYour commlink is quiet for now, but ${wc.toNpc} is expecting to hear from you -- try "call ${wc.toNpc}" when you're ready to check in.`
966
+ ? hint(`\n\nYour commlink is quiet for now, but ${wc.toNpc} is expecting to hear from you -- try "call ${wc.toNpc}" when you're ready to check in.`)
883
967
  : '';
884
968
  return `Welcome, ${this.player.name}\n ${this.scene.story}${callHint}`;
885
969
  }
970
+ /**
971
+ * Everything a save restores beyond the seed synthesis: the player,
972
+ * the hub world overlay, and the Game-level ledgers. Ordering inside
973
+ * matters less than its POSITION in init(): after seedHomeTurfContacts
974
+ * and snapshotVendorStock (both deliberately pre-overlay).
975
+ */
976
+ applyResumeState(save) {
977
+ restorePlayer(this.player, save.player);
978
+ applyHubOverlay(this.scene, save.hub, this.player, this._homeRoom);
979
+ this.lifestyleTier = save.game.lifestyleTier;
980
+ this.rentDebt = save.game.rentDebt;
981
+ this.syncHomeToLifestyle();
982
+ this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
983
+ this._tier = save.game.tier;
984
+ this.contacts = new Map(save.game.contacts);
985
+ this.fencedCategories = new Set(save.game.fencedCategories);
986
+ this._rumorIndex = save.game.rumorIndex;
987
+ this.activeRumor = save.game.activeRumor;
988
+ this._gigPool = [...save.game.gigPool];
989
+ this._gigIndex = save.game.gigIndex;
990
+ this.gigAvailable = save.game.gigAvailable;
991
+ this.activeGig = save.game.activeGig;
992
+ if (save.game.pendingRun) {
993
+ this._pendingRunSeed = save.game.pendingRun.seed;
994
+ this._pendingRunTier = save.game.pendingRun.tier;
995
+ this._pendingRide = save.game.pendingRun.ride;
996
+ }
997
+ this.player.currentLocation = this._homeRoom;
998
+ this.updateEquipment(this.player.equipment);
999
+ this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
1000
+ this.updateExits(this.player.currentLocation);
1001
+ this.updateStatus();
1002
+ this.refreshLogLabel();
1003
+ Logger.getInstance().write(`Resumed save for ${save.playerName} (saved ${save.savedAt}, reason ${save.reason}).`);
1004
+ }
1005
+ /**
1006
+ * THE single save routine -- every safe beat funnels here. Guards make
1007
+ * it a silent no-op anywhere a save would lie: mid-run (the "runs are
1008
+ * disposable" ruling lives in the scene === _hubScene check), downed
1009
+ * or KO'd, mid-exchange, dead, or before init finishes. Synchronous by
1010
+ * design so the process-exit quit hook can use it; failures log and
1011
+ * never crash a turn.
1012
+ */
1013
+ requestSave(reason) {
1014
+ const logger = Logger.getInstance();
1015
+ if (!this.initialized || this._dead || !this._saveFilePath)
1016
+ return;
1017
+ if (!this.hasHub || this.scene !== this._hubScene || !this._hubSeed)
1018
+ return;
1019
+ if (this.player.isDown() || this.player.isUnconscious())
1020
+ return;
1021
+ if (this.scene.isPlayerExchangeActive())
1022
+ return;
1023
+ try {
1024
+ const save = this.buildSaveFile(reason);
1025
+ writeSaveAtomic(this._saveFilePath, save);
1026
+ logger.write(`Saved (${reason}) -> ${this._saveFilePath}`);
1027
+ // Local-first: the beat is DONE the moment the file lands. The
1028
+ // cloud mirror rides behind, fire-and-forget (see cloud-saves.ts
1029
+ // -- logged-out/offline/expired all degrade to a silent no-op).
1030
+ void pushSave(this._saveFilePath, save);
1031
+ }
1032
+ catch (err) {
1033
+ logger.error(`requestSave(${reason}) failed: ${err}`);
1034
+ }
1035
+ }
1036
+ buildSaveFile(reason) {
1037
+ return {
1038
+ version: SAVE_VERSION,
1039
+ savedAt: new Date().toISOString(),
1040
+ reason,
1041
+ playerName: this.player.name,
1042
+ summary: {
1043
+ archetypeName: this.player.archetypeName,
1044
+ karma: this.player.karma,
1045
+ currency: this.player.currency,
1046
+ lifestyleTier: this.lifestyleTier,
1047
+ hubName: this._hubName,
1048
+ },
1049
+ hubSeed: this._hubSeed,
1050
+ player: serializePlayer(this.player),
1051
+ game: {
1052
+ lifestyleTier: this.lifestyleTier,
1053
+ rentDebt: this.rentDebt,
1054
+ tier: this._tier,
1055
+ contacts: [...this.contacts.entries()],
1056
+ fencedCategories: [...this.fencedCategories],
1057
+ rumorIndex: this._rumorIndex,
1058
+ activeRumor: this.activeRumor,
1059
+ gigPool: [...this._gigPool],
1060
+ gigIndex: this._gigIndex,
1061
+ gigAvailable: this.gigAvailable,
1062
+ activeGig: this.activeGig,
1063
+ pendingRun: this._pendingRunSeed && this._pendingRunTier !== undefined
1064
+ ? { seed: this._pendingRunSeed, tier: this._pendingRunTier, ride: this._pendingRide }
1065
+ : undefined,
1066
+ },
1067
+ hub: captureHubOverlay(this._hubScene, this._hubSeed, this.player, this._homeRoom),
1068
+ };
1069
+ }
886
1070
  /**
887
1071
  * Synchronous readiness check for continueToNextScene() -- lets callers
888
1072
  * give instant feedback for the "not ready yet" cases (job not done, no
@@ -1015,10 +1199,10 @@ export default class Game {
1015
1199
  this.lifestyleTier = LIFESTYLE_TIERS[tierIndex - 1].name;
1016
1200
  // The downgrade is visible the moment you look around.
1017
1201
  this.syncHomeToLifestyle();
1018
- rentLine = `The landlord's drone pings before the door even shuts: ${paid > 0 ? `${paid} paid, ` : ''}${this.rentDebt} nuyen still owed. Your ${tier.name} arrangement lapses -- you're living ${this.lifestyleTier} now. ("pay" the tab, "lifestyle" to climb back.)`;
1202
+ rentLine = `The landlord's drone pings before the door even shuts: ${paid > 0 ? `${paid} paid, ` : ''}${this.rentDebt} nuyen still owed. Your ${tier.name} arrangement lapses -- you're living ${this.lifestyleTier} now.${hint(` ("pay" the tab, "lifestyle" to climb back.)`)}`;
1019
1203
  }
1020
1204
  else {
1021
- rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab. ("pay" when you're flush.)`;
1205
+ rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab.${hint(` ("pay" when you're flush.)`)}`;
1022
1206
  }
1023
1207
  }
1024
1208
  else {
@@ -1041,14 +1225,19 @@ export default class Game {
1041
1225
  this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
1042
1226
  this.updateExits(this.player.currentLocation);
1043
1227
  this.updateStatus();
1228
+ this.refreshLogLabel();
1044
1229
  Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${paid} paid, ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
1045
1230
  // Drones spin back up beside the cot.
1046
1231
  const companionArrivalLines = this.unfoldCompanionsOnArrival();
1047
1232
  const companionBlock = [...companionFoldLines, ...companionArrivalLines];
1233
+ // The homecoming save: everything above (payout, rent, restock,
1234
+ // contacts reset) settled first, so the save reflects the hub as
1235
+ // the player will find it.
1236
+ this.requestSave('homecoming');
1048
1237
  const fateLine = wrappedRun
1049
1238
  ? `The run's wrapped and the street swallows you back up.`
1050
1239
  : `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
1051
- return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}. Call ${FIXER_NAME} when you're hungry for the next job.\n${rentLine}${comfortLine}${restockLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
1240
+ return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}\n${rentLine}${comfortLine}${restockLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
1052
1241
  }
1053
1242
  /**
1054
1243
  * The actual drafting work, split out from continueToNextScene() purely
@@ -1093,7 +1282,7 @@ export default class Game {
1093
1282
  this._pendingRunSeed = nextSeed;
1094
1283
  this._pendingRunTier = nextTier;
1095
1284
  this._pendingRide = Game.RIDE_POOL[Math.floor(Math.random() * Game.RIDE_POOL.length)];
1096
- return `Your link pings -- 1 new message: ${FIXER_NAME}. ("call ${FIXER_NAME}" to hear the job.)`;
1285
+ return `Your link pings -- 1 new message: ${FIXER_NAME}.${hint(` ("call ${FIXER_NAME}" to hear the job.)`)}`;
1097
1286
  }
1098
1287
  // Classic sessions transition immediately, hideout-first, as always.
1099
1288
  return await this.activateRunScene(nextSeed, nextTier, { startInHideout: true });
@@ -1147,8 +1336,11 @@ export default class Game {
1147
1336
  // warming so re-shelled companions get warmed with the cast.
1148
1337
  const arrivalLines = this.unfoldCompanionsOnArrival();
1149
1338
  this.warmNpcAppearancesInBackground();
1339
+ this.refreshLogLabel();
1340
+ // No "=== JOB #n ===" banner on entry (player request) -- the Game
1341
+ // Log window's own title carries the job name now.
1150
1342
  const companionBlock = [...companionLines, ...arrivalLines];
1151
- return `\n=== JOB #${this._tier}: ${seed.name} ===\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
1343
+ return `\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
1152
1344
  }
1153
1345
  /**
1154
1346
  * The needs check at every job departure (SR5e p.56 / p.172, scaled to
@@ -1171,11 +1363,15 @@ export default class Game {
1171
1363
  p.sleepStreak = slept ? 0 : p.sleepStreak + 1;
1172
1364
  p.refreshEdge(fed && slept);
1173
1365
  if (fed && slept) {
1174
- lines.push(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.edge + p.lifestyleEdgeBonus}.)`);
1366
+ // Good-news status with a rules lesson inside -- hint-gated
1367
+ // (player request); the HUD's edge count carries the number anyway.
1368
+ const note = hint(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.edge + p.lifestyleEdgeBonus}.)`);
1369
+ if (note)
1370
+ lines.push(note);
1175
1371
  }
1176
1372
  else {
1177
1373
  const missing = !fed && !slept ? 'meal and no sleep' : !fed ? 'meal' : 'sleep';
1178
- lines.push(`No ${missing} since the last job -- your edge doesn't come back (${p.edgeRemaining} left). A hot meal and a night in your own bed reset it.`);
1374
+ lines.push(`No ${missing} since the last job -- your edge doesn't come back (${p.edgeRemaining} left).${hint(` A hot meal and a night in your own bed reset it.`)}`);
1179
1375
  }
1180
1376
  // Escalating, resisted, LOCKED fatigue per ignored need.
1181
1377
  const events = [];
@@ -1224,6 +1420,9 @@ export default class Game {
1224
1420
  if (!seed || tier === undefined) {
1225
1421
  return `There's no job waiting. "call ${FIXER_NAME}" to drum one up.`;
1226
1422
  }
1423
+ // The departure save: hub state with the job still on offer -- what
1424
+ // a mid-run quit resumes to (the "runs are disposable" ruling).
1425
+ this.requestSave('job-accept');
1227
1426
  const haggledAtOffer = this.player.haggledThisJob;
1228
1427
  try {
1229
1428
  const banner = await this.activateRunScene(seed, tier, { startInHideout: false });
@@ -1242,6 +1441,21 @@ export default class Game {
1242
1441
  }
1243
1442
  }
1244
1443
  async close() {
1444
+ // PERMADEATH (player ruling): close() is reachable ONLY via
1445
+ // Scene.endGame(), and endGame fires ONLY on real death (combat
1446
+ // resolveDeath, street-decides knockout, drain burnout, ward
1447
+ // backlash, stim crash) -- so this IS the one death choke point.
1448
+ // The save burns with the body; _dead also suppresses the quit
1449
+ // hook's save on the way out. Keep that invariant: any future
1450
+ // non-death caller of close() must set a flag before this line.
1451
+ this._dead = true;
1452
+ if (this._saveFilePath) {
1453
+ deleteSave(this._saveFilePath);
1454
+ // The cloud copy burns too -- tombstone-first, so an offline death
1455
+ // can't be resurrected from the mirror (see cloud-saves.ts).
1456
+ burnCloudSaveFor(this._saveFilePath, this.player.name);
1457
+ Logger.getInstance().write(`Permadeath: save deleted (${this._saveFilePath}).`);
1458
+ }
1245
1459
  this.stopPursuitHeartbeat();
1246
1460
  this._inputBox.removeAllListeners(); // removes all event listeners
1247
1461
  this._inputBox.cancel(); // cancels any active input session
@@ -1273,8 +1487,18 @@ export default class Game {
1273
1487
  });
1274
1488
  this._inputBox = inputBox;
1275
1489
  screen.append(inputBox);
1276
- // Key bindings
1277
- screen.key(['escape', 'C-c'], () => process.exit(0));
1490
+ // Key bindings. ESC/Ctrl-C take a SECOND press within 3s to quit
1491
+ // (player request: "I just exited by mistake") -- the first press
1492
+ // warns; typing anything disarms nothing (the window just expires).
1493
+ screen.key(['escape', 'C-c'], () => {
1494
+ const now = Date.now();
1495
+ if (now - this._escArmedAt < 3000) {
1496
+ process.exit(0);
1497
+ }
1498
+ this._escArmedAt = now;
1499
+ Logger.getInstance().logWithColor(`Press again within 3s to quit. (Or "quit" + "y".)`, 'yellow');
1500
+ this._screen?.render();
1501
+ });
1278
1502
  // Focus on the input box by default -- but ONLY if it isn't already
1279
1503
  // focused. screen.append() auto-focuses the first keyable element
1280
1504
  // (Node.insert), and since the sidebar boxes were removed this textbox
@@ -1309,7 +1533,7 @@ export default class Game {
1309
1533
  border: {
1310
1534
  type: 'line'
1311
1535
  },
1312
- label: ' Game Log (PgUp/PgDn to scroll) ',
1536
+ label: this.buildLogLabel(false),
1313
1537
  scrollable: true, // Enable scrolling
1314
1538
  alwaysScroll: true,
1315
1539
  scrollbar: {
@@ -1337,10 +1561,17 @@ export default class Game {
1337
1561
  // yanking the view back down (Logger.log and ui.ts honor it).
1338
1562
  // Paging back to the bottom -- or End -- re-engages the follow.
1339
1563
  logBox.followBottom = true;
1564
+ // CRITICAL: the input box is ALWAYS reading (focus never leaves it),
1565
+ // and blessed's textarea.readInput sets screen.grabKeys = true --
1566
+ // which mutes every screen-level key handler EXCEPT keys listed in
1567
+ // screen.ignoreLocked (screen.js:591). Without this, the bindings
1568
+ // below simply never fire (a real session: "PgUp/Dn don't work").
1569
+ // The textbox itself ignores these keys, so nothing is stolen.
1570
+ screen.ignoreLocked.push('pageup', 'pagedown', 'end');
1340
1571
  const page = () => Math.max(1, (typeof logBox.height === 'number' ? logBox.height : 20) - 3);
1341
1572
  const setFollow = (on) => {
1342
1573
  logBox.followBottom = on;
1343
- logBox.setLabel(on ? ' Game Log (PgUp/PgDn to scroll) ' : ' Game Log -- SCROLLED (End to follow) ');
1574
+ logBox.setLabel(this.buildLogLabel(!on));
1344
1575
  };
1345
1576
  const atBottom = () => logBox.getScrollPerc() >= 100 || logBox.getScrollHeight() <= page() + 3;
1346
1577
  screen.key(['pageup'], () => {
@@ -1361,6 +1592,29 @@ export default class Game {
1361
1592
  });
1362
1593
  screen.append(logBox);
1363
1594
  }
1595
+ /**
1596
+ * The Game Log banner names WHERE you are in the campaign (player
1597
+ * request): the hub by name at home, "JOB #n: <name>" on a run --
1598
+ * plus the scroll state the label already carried.
1599
+ */
1600
+ buildLogLabel(scrolled) {
1601
+ const onHub = this._hubScene !== undefined && this.scene === this._hubScene;
1602
+ const name = onHub ? this._hubName : this.sceneSeed?.name;
1603
+ const where = name ? ` -- ${name}` : '';
1604
+ // The key coaching is hint-gated (player request); the SCROLLED
1605
+ // state itself stays -- it's status, not teaching.
1606
+ return scrolled
1607
+ ? ` Game Log${where} -- SCROLLED${hint(' (End to follow)')} `
1608
+ : ` Game Log${where}${hint(' (PgUp/PgDn to scroll)')} `;
1609
+ }
1610
+ /** Re-stamps the banner after a scene change (init, run start, home). */
1611
+ refreshLogLabel() {
1612
+ const box = this._logBox;
1613
+ if (!box)
1614
+ return;
1615
+ box.setLabel(this.buildLogLabel(box.followBottom === false));
1616
+ this._screen?.render();
1617
+ }
1364
1618
  /**
1365
1619
  * Scroll the log to its tail -- unless the player has paged up to read
1366
1620
  * history (followBottom off), in which case their place is left alone.
@@ -1461,7 +1715,10 @@ export default class Game {
1461
1715
  // a real question.
1462
1716
  const crumb = player.cameFrom ? `${player.cameFrom.room} (${player.cameFrom.back}) > ` : '';
1463
1717
  if (player.currentLocation) {
1464
- lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}{/bold} | ${stance}`);
1718
+ // "At" spots: where you stand WITHIN the room, when it has places
1719
+ // to stand (see utilities/spots.ts).
1720
+ const atNote = spotOf(player) ? ` @ ${player.atSpot}` : '';
1721
+ lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}${atNote}{/bold} | ${stance}`);
1465
1722
  }
1466
1723
  else {
1467
1724
  lines.push(stance);
@@ -1544,7 +1801,7 @@ export default class Game {
1544
1801
  // works the grid while you're back in the meat (the AR-drop exit).
1545
1802
  const remoteAgent = this.companions.find(c => c.kind === 'agent');
1546
1803
  if (remoteAgent && player.plane !== 'matrix') {
1547
- lines.push(`{cyan-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''} ("jack in" to rejoin it, "recall" to fold it home){/cyan-fg}`);
1804
+ lines.push(`{cyan-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''}${hint(' ("jack in" to rejoin it, "recall" to fold it home)')}{/cyan-fg}`);
1548
1805
  }
1549
1806
  // Off-plane context: which reality you're in, your persona's Matrix
1550
1807
  // track (cold-sim damage lands there), and where your empty body lies.
@@ -1590,9 +1847,10 @@ export default class Game {
1590
1847
  this.updateStatus();
1591
1848
  }
1592
1849
  // Public method to get the instance of the Game class
1593
- static async getInstance(player, sceneSeed) {
1850
+ static async getInstance(player, sceneSeed, resume) {
1594
1851
  if (!Game.instance) {
1595
1852
  Game.instance = new Game(player, sceneSeed);
1853
+ Game.instance._resume = resume;
1596
1854
  }
1597
1855
  return Game.instance;
1598
1856
  }
@@ -1603,8 +1861,25 @@ export default class Game {
1603
1861
  const parts = normalized.trim().split(' ');
1604
1862
  const verb = parts[0].toLowerCase();
1605
1863
  const args = parts.slice(1);
1606
- if (verb === 'quit' || verb === 'exit') {
1607
- return null;
1864
+ // Quitting takes a CONFIRM (player request: "I just exited by
1865
+ // mistake"): the first "quit" arms it, "y"/"yes" (or quit again)
1866
+ // seals it, anything else disarms and runs as the command it is.
1867
+ if (this._quitArmed) {
1868
+ this._quitArmed = false;
1869
+ if (/^(y|yes|quit|exit)$/i.test(verb)) {
1870
+ return null;
1871
+ }
1872
+ // fall through -- the answer was a real command; play on.
1873
+ }
1874
+ else if (verb === 'quit' || verb === 'exit') {
1875
+ this._quitArmed = true;
1876
+ return `Really quit? ("y" to confirm -- anything else keeps playing.)`;
1877
+ }
1878
+ // Out cold with the fight settling around you (see
1879
+ // Scene.beginPlayerKnockout): unconsciousness is not a prompt. Only
1880
+ // quit (handled above) still answers.
1881
+ if (this.player.isUnconscious() && this.scene.playerKnockout) {
1882
+ return `You're out cold -- the world moves without you.`;
1608
1883
  }
1609
1884
  if (verb === 'maka') {
1610
1885
  return 'You are still in the game, type "quit" to exit';
@@ -1653,10 +1928,12 @@ export default class Game {
1653
1928
  return `I don't understand "${verb}" -- did you mean "${guess}"?`;
1654
1929
  }
1655
1930
  const corrected = await this.handleInput([guess, ...args].join(' '));
1656
- // The correction note is bookkeeping -- Mechanics ticker, not the
1657
- // narrative (readability pass).
1658
- Logger.getInstance().meta(`(read "${verb}" as "${guess}")`);
1659
- return corrected;
1931
+ // The correction note stays INLINE and always-on (player ruling:
1932
+ // it briefly moved to the Mechanics ticker, and a conversational
1933
+ // "well I'm here..." silently became a SELL with nothing in the
1934
+ // world log explaining the refusal that came back). It's not dice
1935
+ // noise -- it's the only receipt for a command you didn't type.
1936
+ return corrected === null ? null : `(read "${verb}" as "${guess}")\n${corrected ?? ''}`;
1660
1937
  }
1661
1938
  return `I don't understand the command "${verb}".`;
1662
1939
  }
@@ -1670,7 +1947,7 @@ export default class Game {
1670
1947
  * future command can't silently fall out of help.
1671
1948
  */
1672
1949
  static HELP_CATEGORIES = [
1673
- { title: 'moving', entries: [['look'], ['go'], ['map', 'exits'], ['search'], ['sneak']] },
1950
+ { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['map', 'exits'], ['search'], ['sneak']] },
1674
1951
  { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
1675
1952
  { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['order'], ['stance'], ['surrender'], ['edge'], ['rest']] },
1676
1953
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
@@ -1679,7 +1956,7 @@ export default class Game {
1679
1956
  { title: 'street', entries: [['browse', 'buy'], ['sell'], ['work', 'gig'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
1680
1957
  { title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
1681
1958
  { title: 'character', entries: [['sheet', 'stats'], ['advance', 'train'], ['qualities', 'quality'], ['initiate', 'submerge'], ['install', 'chrome']] },
1682
- { title: 'session', entries: [['help'], ['clear'], ['quit', 'exit']] },
1959
+ { title: 'session', entries: [['help'], ['hints'], ['clear'], ['quit', 'exit']] },
1683
1960
  ];
1684
1961
  /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
1685
1962
  static BUILTIN_DESCRIPTIONS = {