@maka/maka-cli 5.8.0 → 5.9.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 (154) 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 +3 -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 +10 -0
  109. package/bundle/typescript/src/commands/game/sideQuest/game.js +144 -24
  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 +11 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +187 -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 +9 -2
  118. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +27 -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 +2 -0
  121. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +9 -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 +28 -0
  127. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +165 -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/seed/exit-seed.d.ts +1 -0
  132. package/bundle/typescript/src/commands/game/sideQuest/types/seed/item-seed.d.ts +1 -0
  133. package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +1 -0
  134. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +1 -0
  135. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +6 -0
  136. package/bundle/typescript/src/commands/game/sideQuest/ui.js +52 -3
  137. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  138. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +18 -4
  139. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
  140. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +10 -0
  141. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +17 -0
  142. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  143. package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.d.ts +36 -0
  144. package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.js +57 -0
  145. package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.js.map +1 -0
  146. package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.d.ts +4 -0
  147. package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js +55 -0
  148. package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js.map +1 -0
  149. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +8 -7
  150. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  151. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +121 -0
  152. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +315 -0
  153. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -0
  154. package/package.json +1 -1
@@ -3,6 +3,7 @@ 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';
6
7
  import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
7
8
  import { rollPool, formatRoll } from './utilities/dice.js';
8
9
  import { Direction } from './types/shared/direction-enum.js';
@@ -33,6 +34,7 @@ import { SilentCommand } from './commands/silent.js';
33
34
  import { RestCommand } from './commands/rest.js';
34
35
  import { ReloadCommand } from './commands/reload.js';
35
36
  import { SubdueCommand } from './commands/subdue.js';
37
+ import { GrappleCommand, ReleaseCommand, StruggleCommand } from './commands/grapple.js';
36
38
  import { EdgeCommand } from './commands/edge.js';
37
39
  import { SheetCommand } from './commands/sheet.js';
38
40
  import { CallCommand } from './commands/call.js';
@@ -59,6 +61,9 @@ import { InitiateCommand } from './commands/initiate.js';
59
61
  import { SummonCommand } from './commands/summon.js';
60
62
  import { OrderCommand } from './commands/order.js';
61
63
  import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
64
+ import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
65
+ import { HintsCommand } from './commands/hints.js';
66
+ import { hint } from './utilities/hints.js';
62
67
  import { InstallCommand } from './commands/install.js';
63
68
  import { BuyCommand } from './commands/buy.js';
64
69
  import { SellCommand } from './commands/sell.js';
@@ -82,6 +87,7 @@ import { SceneSeedGenerator, FIXER_NAME } from './factories/scene-seed-generator
82
87
  import { CALL_ICON, CALL_COLOR } from './utilities/comm-style.js';
83
88
  import { AI } from '../../../tools/ai/ai.class.js';
84
89
  import { BULLET } from './utilities/log-style.js';
90
+ import { spotOf } from './utilities/spots.js';
85
91
  export default class Game {
86
92
  initialized;
87
93
  static instance;
@@ -98,6 +104,10 @@ export default class Game {
98
104
  _screen;
99
105
  _logBox;
100
106
  _metaBox;
107
+ // Quit confirmation state (see handleInput / initInputBox): armed by
108
+ // the first quit/exit or ESC, cleared by anything that isn't a yes.
109
+ _quitArmed = false;
110
+ _escArmedAt = 0;
101
111
  _statusBox;
102
112
  _inputBox;
103
113
  _useAi = false;
@@ -414,7 +424,7 @@ export default class Game {
414
424
  if (kind === 'gig')
415
425
  c.gigs += 1;
416
426
  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.)`;
427
+ return `${name} knows your name now -- a CONTACT (Connection ${c.connection}, Loyalty 1).${hint(` ("contacts" for the roster, "ask ${name} about ..." for legwork.)`)}`;
418
428
  }
419
429
  if (kind === 'gig') {
420
430
  c.gigs += 1;
@@ -428,7 +438,7 @@ export default class Game {
428
438
  c.spent += spent;
429
439
  if (Math.floor(c.spent / 1000) > thousandsBefore && c.loyalty < 3) {
430
440
  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.)`;
441
+ return `${name} has started keeping your usual ready.${hint(` (Loyalty ${c.loyalty} -- patronage tops out at 3; run their errands to go deeper.)`)}`;
432
442
  }
433
443
  return null;
434
444
  }
@@ -710,6 +720,12 @@ export default class Game {
710
720
  CommandFactory.registerCommand('rest', RestCommand);
711
721
  CommandFactory.registerCommand('reload', ReloadCommand);
712
722
  CommandFactory.registerCommand('subdue', SubdueCommand);
723
+ // SUBDUING p.195 (see commands/grapple.ts): restrain without damage.
724
+ CommandFactory.registerCommand('grapple', GrappleCommand);
725
+ CommandFactory.registerCommand('restrain', GrappleCommand);
726
+ CommandFactory.registerCommand('clinch', GrappleCommand);
727
+ CommandFactory.registerCommand('release', ReleaseCommand);
728
+ CommandFactory.registerCommand('struggle', StruggleCommand);
713
729
  CommandFactory.registerCommand('edge', EdgeCommand);
714
730
  CommandFactory.registerCommand('sheet', SheetCommand);
715
731
  CommandFactory.registerCommand('stats', SheetCommand);
@@ -747,6 +763,15 @@ export default class Game {
747
763
  CommandFactory.registerCommand('submerge', InitiateCommand);
748
764
  CommandFactory.registerCommand('summon', SummonCommand);
749
765
  CommandFactory.registerCommand('conjure', SummonCommand);
766
+ // "At" spots (utilities/spots.ts): walk/move/approach are one
767
+ // crossing; run and sprint are the fast modes (combat semantics in
768
+ // the spot-move phase).
769
+ CommandFactory.registerCommand('walk', MoveCommand);
770
+ CommandFactory.registerCommand('move', MoveCommand);
771
+ CommandFactory.registerCommand('approach', MoveCommand);
772
+ CommandFactory.registerCommand('run', RunCommand);
773
+ CommandFactory.registerCommand('sprint', SprintCommand);
774
+ CommandFactory.registerCommand('hints', HintsCommand);
750
775
  CommandFactory.registerCommand('order', OrderCommand);
751
776
  CommandFactory.registerCommand('deploy', DeployCommand);
752
777
  CommandFactory.registerCommand('recall', RecallCommand);
@@ -784,6 +809,19 @@ export default class Game {
784
809
  if (this._homeRoomExitDirection) {
785
810
  this._homeRoom.exits.delete(this._homeRoomExitDirection);
786
811
  }
812
+ // The OTHER half of the previous link: sweep any exit on the anchor
813
+ // room that already leads home before choosing a direction. Without
814
+ // this, every homecoming re-ran the search against a room still
815
+ // holding last time's hideout door and stacked a second one on the
816
+ // next free direction (a real session's Neon Strip ended up with
817
+ // "up" AND "down" both reading "To Your Hideout").
818
+ for (const [dir, ex] of [...jobStartRoom.exits.entries()]) {
819
+ const leadsHome = ex instanceof Door
820
+ ? ex.getOtherSide(jobStartRoom) === this._homeRoom
821
+ : ex === this._homeRoom.name;
822
+ if (leadsHome)
823
+ jobStartRoom.exits.delete(dir);
824
+ }
787
825
  const direction = Game.HOME_DIRECTION_PRIORITY.find(d => !jobStartRoom.exits.has(d));
788
826
  if (!direction) {
789
827
  // Every direction is already spoken for on this job's start room --
@@ -864,6 +902,15 @@ export default class Game {
864
902
  this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
865
903
  // Home turf knows its own (see seedHomeTurfContacts).
866
904
  this.seedHomeTurfContacts();
905
+ // The next run starts cooking the moment you're home (player
906
+ // request): generation takes 20-40s, and waiting for the player
907
+ // to CALL first meant "give me a bit" every time. Draft now --
908
+ // Mr. Johnson's "1 new message" ping lands mid-session, and the
909
+ // call finds work already waiting. The gate keeps this a no-op
910
+ // with AI off or a draft/pending seed somehow already in flight.
911
+ if (this.canDraftNextScene().allowed) {
912
+ this.draftNextSceneInBackground();
913
+ }
867
914
  }
868
915
  // Gear up automatically: wearables (and the commlink) slot into any
869
916
  // EMPTY slots at load -- no "equip all" ritual to start a session
@@ -872,6 +919,9 @@ export default class Game {
872
919
  equipIntoEmptySlots(this.player);
873
920
  this.warmNpcAppearancesInBackground();
874
921
  this.startPursuitHeartbeat();
922
+ // The banner learns where we are only after the hub/scene bookkeeping
923
+ // above settles.
924
+ this.refreshLogLabel();
875
925
  this.initialized = true;
876
926
  // Nudge the player toward "call" up front -- the fixer convention means
877
927
  // the story deliberately doesn't narrate a completed briefing, so
@@ -879,7 +929,7 @@ export default class Game {
879
929
  // you actually get your orders.
880
930
  const wc = this.scene.getWinCondition();
881
931
  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.`
932
+ ? 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
933
  : '';
884
934
  return `Welcome, ${this.player.name}\n ${this.scene.story}${callHint}`;
885
935
  }
@@ -1015,10 +1065,10 @@ export default class Game {
1015
1065
  this.lifestyleTier = LIFESTYLE_TIERS[tierIndex - 1].name;
1016
1066
  // The downgrade is visible the moment you look around.
1017
1067
  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.)`;
1068
+ 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
1069
  }
1020
1070
  else {
1021
- rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab. ("pay" when you're flush.)`;
1071
+ rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab.${hint(` ("pay" when you're flush.)`)}`;
1022
1072
  }
1023
1073
  }
1024
1074
  else {
@@ -1041,6 +1091,7 @@ export default class Game {
1041
1091
  this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
1042
1092
  this.updateExits(this.player.currentLocation);
1043
1093
  this.updateStatus();
1094
+ this.refreshLogLabel();
1044
1095
  Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${paid} paid, ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
1045
1096
  // Drones spin back up beside the cot.
1046
1097
  const companionArrivalLines = this.unfoldCompanionsOnArrival();
@@ -1048,7 +1099,7 @@ export default class Game {
1048
1099
  const fateLine = wrappedRun
1049
1100
  ? `The run's wrapped and the street swallows you back up.`
1050
1101
  : `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')}` : ''}`;
1102
+ 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
1103
  }
1053
1104
  /**
1054
1105
  * The actual drafting work, split out from continueToNextScene() purely
@@ -1093,7 +1144,7 @@ export default class Game {
1093
1144
  this._pendingRunSeed = nextSeed;
1094
1145
  this._pendingRunTier = nextTier;
1095
1146
  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.)`;
1147
+ return `Your link pings -- 1 new message: ${FIXER_NAME}.${hint(` ("call ${FIXER_NAME}" to hear the job.)`)}`;
1097
1148
  }
1098
1149
  // Classic sessions transition immediately, hideout-first, as always.
1099
1150
  return await this.activateRunScene(nextSeed, nextTier, { startInHideout: true });
@@ -1147,8 +1198,11 @@ export default class Game {
1147
1198
  // warming so re-shelled companions get warmed with the cast.
1148
1199
  const arrivalLines = this.unfoldCompanionsOnArrival();
1149
1200
  this.warmNpcAppearancesInBackground();
1201
+ this.refreshLogLabel();
1202
+ // No "=== JOB #n ===" banner on entry (player request) -- the Game
1203
+ // Log window's own title carries the job name now.
1150
1204
  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')}` : ''}`;
1205
+ return `\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
1152
1206
  }
1153
1207
  /**
1154
1208
  * The needs check at every job departure (SR5e p.56 / p.172, scaled to
@@ -1171,11 +1225,15 @@ export default class Game {
1171
1225
  p.sleepStreak = slept ? 0 : p.sleepStreak + 1;
1172
1226
  p.refreshEdge(fed && slept);
1173
1227
  if (fed && slept) {
1174
- lines.push(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.edge + p.lifestyleEdgeBonus}.)`);
1228
+ // Good-news status with a rules lesson inside -- hint-gated
1229
+ // (player request); the HUD's edge count carries the number anyway.
1230
+ const note = hint(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.edge + p.lifestyleEdgeBonus}.)`);
1231
+ if (note)
1232
+ lines.push(note);
1175
1233
  }
1176
1234
  else {
1177
1235
  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.`);
1236
+ 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
1237
  }
1180
1238
  // Escalating, resisted, LOCKED fatigue per ignored need.
1181
1239
  const events = [];
@@ -1273,8 +1331,18 @@ export default class Game {
1273
1331
  });
1274
1332
  this._inputBox = inputBox;
1275
1333
  screen.append(inputBox);
1276
- // Key bindings
1277
- screen.key(['escape', 'C-c'], () => process.exit(0));
1334
+ // Key bindings. ESC/Ctrl-C take a SECOND press within 3s to quit
1335
+ // (player request: "I just exited by mistake") -- the first press
1336
+ // warns; typing anything disarms nothing (the window just expires).
1337
+ screen.key(['escape', 'C-c'], () => {
1338
+ const now = Date.now();
1339
+ if (now - this._escArmedAt < 3000) {
1340
+ process.exit(0);
1341
+ }
1342
+ this._escArmedAt = now;
1343
+ Logger.getInstance().logWithColor(`Press again within 3s to quit. (Or "quit" + "y".)`, 'yellow');
1344
+ this._screen?.render();
1345
+ });
1278
1346
  // Focus on the input box by default -- but ONLY if it isn't already
1279
1347
  // focused. screen.append() auto-focuses the first keyable element
1280
1348
  // (Node.insert), and since the sidebar boxes were removed this textbox
@@ -1309,7 +1377,7 @@ export default class Game {
1309
1377
  border: {
1310
1378
  type: 'line'
1311
1379
  },
1312
- label: ' Game Log (PgUp/PgDn to scroll) ',
1380
+ label: this.buildLogLabel(false),
1313
1381
  scrollable: true, // Enable scrolling
1314
1382
  alwaysScroll: true,
1315
1383
  scrollbar: {
@@ -1337,10 +1405,17 @@ export default class Game {
1337
1405
  // yanking the view back down (Logger.log and ui.ts honor it).
1338
1406
  // Paging back to the bottom -- or End -- re-engages the follow.
1339
1407
  logBox.followBottom = true;
1408
+ // CRITICAL: the input box is ALWAYS reading (focus never leaves it),
1409
+ // and blessed's textarea.readInput sets screen.grabKeys = true --
1410
+ // which mutes every screen-level key handler EXCEPT keys listed in
1411
+ // screen.ignoreLocked (screen.js:591). Without this, the bindings
1412
+ // below simply never fire (a real session: "PgUp/Dn don't work").
1413
+ // The textbox itself ignores these keys, so nothing is stolen.
1414
+ screen.ignoreLocked.push('pageup', 'pagedown', 'end');
1340
1415
  const page = () => Math.max(1, (typeof logBox.height === 'number' ? logBox.height : 20) - 3);
1341
1416
  const setFollow = (on) => {
1342
1417
  logBox.followBottom = on;
1343
- logBox.setLabel(on ? ' Game Log (PgUp/PgDn to scroll) ' : ' Game Log -- SCROLLED (End to follow) ');
1418
+ logBox.setLabel(this.buildLogLabel(!on));
1344
1419
  };
1345
1420
  const atBottom = () => logBox.getScrollPerc() >= 100 || logBox.getScrollHeight() <= page() + 3;
1346
1421
  screen.key(['pageup'], () => {
@@ -1361,6 +1436,29 @@ export default class Game {
1361
1436
  });
1362
1437
  screen.append(logBox);
1363
1438
  }
1439
+ /**
1440
+ * The Game Log banner names WHERE you are in the campaign (player
1441
+ * request): the hub by name at home, "JOB #n: <name>" on a run --
1442
+ * plus the scroll state the label already carried.
1443
+ */
1444
+ buildLogLabel(scrolled) {
1445
+ const onHub = this._hubScene !== undefined && this.scene === this._hubScene;
1446
+ const name = onHub ? this._hubName : this.sceneSeed?.name;
1447
+ const where = name ? ` -- ${name}` : '';
1448
+ // The key coaching is hint-gated (player request); the SCROLLED
1449
+ // state itself stays -- it's status, not teaching.
1450
+ return scrolled
1451
+ ? ` Game Log${where} -- SCROLLED${hint(' (End to follow)')} `
1452
+ : ` Game Log${where}${hint(' (PgUp/PgDn to scroll)')} `;
1453
+ }
1454
+ /** Re-stamps the banner after a scene change (init, run start, home). */
1455
+ refreshLogLabel() {
1456
+ const box = this._logBox;
1457
+ if (!box)
1458
+ return;
1459
+ box.setLabel(this.buildLogLabel(box.followBottom === false));
1460
+ this._screen?.render();
1461
+ }
1364
1462
  /**
1365
1463
  * Scroll the log to its tail -- unless the player has paged up to read
1366
1464
  * history (followBottom off), in which case their place is left alone.
@@ -1461,7 +1559,10 @@ export default class Game {
1461
1559
  // a real question.
1462
1560
  const crumb = player.cameFrom ? `${player.cameFrom.room} (${player.cameFrom.back}) > ` : '';
1463
1561
  if (player.currentLocation) {
1464
- lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}{/bold} | ${stance}`);
1562
+ // "At" spots: where you stand WITHIN the room, when it has places
1563
+ // to stand (see utilities/spots.ts).
1564
+ const atNote = spotOf(player) ? ` @ ${player.atSpot}` : '';
1565
+ lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}${atNote}{/bold} | ${stance}`);
1465
1566
  }
1466
1567
  else {
1467
1568
  lines.push(stance);
@@ -1544,7 +1645,7 @@ export default class Game {
1544
1645
  // works the grid while you're back in the meat (the AR-drop exit).
1545
1646
  const remoteAgent = this.companions.find(c => c.kind === 'agent');
1546
1647
  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}`);
1648
+ 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
1649
  }
1549
1650
  // Off-plane context: which reality you're in, your persona's Matrix
1550
1651
  // track (cold-sim damage lands there), and where your empty body lies.
@@ -1603,8 +1704,25 @@ export default class Game {
1603
1704
  const parts = normalized.trim().split(' ');
1604
1705
  const verb = parts[0].toLowerCase();
1605
1706
  const args = parts.slice(1);
1606
- if (verb === 'quit' || verb === 'exit') {
1607
- return null;
1707
+ // Quitting takes a CONFIRM (player request: "I just exited by
1708
+ // mistake"): the first "quit" arms it, "y"/"yes" (or quit again)
1709
+ // seals it, anything else disarms and runs as the command it is.
1710
+ if (this._quitArmed) {
1711
+ this._quitArmed = false;
1712
+ if (/^(y|yes|quit|exit)$/i.test(verb)) {
1713
+ return null;
1714
+ }
1715
+ // fall through -- the answer was a real command; play on.
1716
+ }
1717
+ else if (verb === 'quit' || verb === 'exit') {
1718
+ this._quitArmed = true;
1719
+ return `Really quit? ("y" to confirm -- anything else keeps playing.)`;
1720
+ }
1721
+ // Out cold with the fight settling around you (see
1722
+ // Scene.beginPlayerKnockout): unconsciousness is not a prompt. Only
1723
+ // quit (handled above) still answers.
1724
+ if (this.player.isUnconscious() && this.scene.playerKnockout) {
1725
+ return `You're out cold -- the world moves without you.`;
1608
1726
  }
1609
1727
  if (verb === 'maka') {
1610
1728
  return 'You are still in the game, type "quit" to exit';
@@ -1653,10 +1771,12 @@ export default class Game {
1653
1771
  return `I don't understand "${verb}" -- did you mean "${guess}"?`;
1654
1772
  }
1655
1773
  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;
1774
+ // The correction note stays INLINE and always-on (player ruling:
1775
+ // it briefly moved to the Mechanics ticker, and a conversational
1776
+ // "well I'm here..." silently became a SELL with nothing in the
1777
+ // world log explaining the refusal that came back). It's not dice
1778
+ // noise -- it's the only receipt for a command you didn't type.
1779
+ return corrected === null ? null : `(read "${verb}" as "${guess}")\n${corrected ?? ''}`;
1660
1780
  }
1661
1781
  return `I don't understand the command "${verb}".`;
1662
1782
  }
@@ -1670,7 +1790,7 @@ export default class Game {
1670
1790
  * future command can't silently fall out of help.
1671
1791
  */
1672
1792
  static HELP_CATEGORIES = [
1673
- { title: 'moving', entries: [['look'], ['go'], ['map', 'exits'], ['search'], ['sneak']] },
1793
+ { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['map', 'exits'], ['search'], ['sneak']] },
1674
1794
  { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
1675
1795
  { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['order'], ['stance'], ['surrender'], ['edge'], ['rest']] },
1676
1796
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
@@ -1679,7 +1799,7 @@ export default class Game {
1679
1799
  { title: 'street', entries: [['browse', 'buy'], ['sell'], ['work', 'gig'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
1680
1800
  { title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
1681
1801
  { title: 'character', entries: [['sheet', 'stats'], ['advance', 'train'], ['qualities', 'quality'], ['initiate', 'submerge'], ['install', 'chrome']] },
1682
- { title: 'session', entries: [['help'], ['clear'], ['quit', 'exit']] },
1802
+ { title: 'session', entries: [['help'], ['hints'], ['clear'], ['quit', 'exit']] },
1683
1803
  ];
1684
1804
  /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
1685
1805
  static BUILTIN_DESCRIPTIONS = {