@maka/maka-cli 5.4.0 → 5.5.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 (122) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.d.ts +20 -1
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +76 -11
  4. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.d.ts +16 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +65 -0
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.d.ts +2 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +72 -29
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js.map +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +21 -2
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.d.ts +16 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +39 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -0
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.d.ts +21 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js +82 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js.map +1 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +1 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +71 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +21 -2
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +13 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js +3 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +19 -2
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +9 -2
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +2 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +33 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +43 -20
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.d.ts +15 -0
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +75 -0
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -0
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +5 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +16 -4
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.d.ts +18 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +100 -0
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -0
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.d.ts +1 -0
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +22 -7
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +8 -0
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +55 -8
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +11 -2
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.d.ts +15 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js +35 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js.map +1 -0
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +21 -0
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +4 -4
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js.map +1 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +70 -13
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +8 -0
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js +6 -3
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js.map +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +5 -4
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
  72. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +1 -0
  73. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js.map +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +175 -0
  75. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +673 -0
  76. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -0
  77. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +1 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  79. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +64 -9
  80. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +265 -434
  81. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -1
  82. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +69 -1
  83. package/bundle/typescript/src/commands/game/sideQuest/game.js +388 -67
  84. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  85. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +2 -0
  86. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +92 -3
  87. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  88. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +66 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +151 -15
  90. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  91. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +29 -4
  92. package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
  93. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +4 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +14 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  96. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +4 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +35 -3
  98. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  99. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +275 -105
  100. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +662 -233
  101. package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +8 -0
  102. package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +1 -1
  103. package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +1 -0
  104. package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +6 -3
  105. package/bundle/typescript/src/commands/game/sideQuest/ui.js +19 -20
  106. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  107. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +6 -4
  108. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +31 -5
  109. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  110. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +34 -1
  111. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +72 -0
  112. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  113. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +27 -3
  114. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  115. package/bundle/typescript/src/tools/ai/ai.class.d.ts +8 -1
  116. package/bundle/typescript/src/tools/ai/ai.class.js +8 -3
  117. package/bundle/typescript/src/tools/ai/ai.class.js.map +1 -1
  118. package/bundle/typescript/src/tools/https/https.class.d.ts +2 -2
  119. package/bundle/typescript/src/tools/https/https.class.js +10 -4
  120. package/bundle/typescript/src/tools/https/https.class.js.map +1 -1
  121. package/bundle/typescript/src/tools/https/https.interface.d.ts +2 -0
  122. package/package.json +1 -1
@@ -33,6 +33,7 @@ import { EdgeCommand } from './commands/edge.js';
33
33
  import { SheetCommand } from './commands/sheet.js';
34
34
  import { CallCommand } from './commands/call.js';
35
35
  import { EndCallCommand } from './commands/end-call.js';
36
+ import { fuzzyPickName, levenshteinDistance } from './utilities/fuzzy-match.js';
36
37
  import { PickCommand } from './commands/pick.js';
37
38
  import { HackCommand } from './commands/hack.js';
38
39
  import { CastCommand } from './commands/cast.js';
@@ -44,6 +45,14 @@ import { AssenseCommand } from './commands/assense.js';
44
45
  import { DroneCommand } from './commands/drone.js';
45
46
  import { PanCommand } from './commands/pan.js';
46
47
  import { RebootCommand } from './commands/reboot.js';
48
+ import { BrandishCommand, HolsterCommand } from './commands/brandish.js';
49
+ import { AdvanceCommand } from './commands/advance.js';
50
+ import { equipIntoEmptySlots } from './archetypes.js';
51
+ import { NPC } from './models/npc.js';
52
+ import { HaggleCommand } from './commands/haggle.js';
53
+ import { SneakCommand } from './commands/sneak.js';
54
+ import { PersuadeCommand } from './commands/persuade.js';
55
+ import { BluffCommand } from './commands/bluff.js';
47
56
  import { AutoFight } from './utilities/auto-fight.js';
48
57
  import { CommandFactory } from './factories/command-factory.js';
49
58
  import { SceneSynthesizer } from './factories/scene-factory.js';
@@ -78,6 +87,91 @@ export default class Game {
78
87
  // player-location state out from under whatever the player was already
79
88
  // doing in it.
80
89
  _draftInProgress = false;
90
+ // The HUB scene, when this session started in one (a seed with
91
+ // "hub": true -- the persistent home base, e.g. Tacoma Purple Haze).
92
+ // Kept as the live Scene INSTANCE, same pattern as _homeRoom, so
93
+ // everything in it -- NPC state, shop stock, dropped loot -- persists
94
+ // untouched while runs play out, and returnToHub() just re-points the
95
+ // world at it. undefined for classic sessions (test scene, launch-
96
+ // generated runs), which keep the old continue->draft loop.
97
+ _hubScene;
98
+ _hubName = 'home';
99
+ get hasHub() {
100
+ return this._hubScene !== undefined;
101
+ }
102
+ /** The persistent hideout room -- go.ts's walk-home trigger checks it. */
103
+ get homeRoom() {
104
+ return this._homeRoom;
105
+ }
106
+ // A drafted run waiting for the player to take it (hub sessions only):
107
+ // generation finishing sends a MESSAGE instead of teleporting anyone --
108
+ // "call Mr. Johnson" hears the offer, "travel" goes.
109
+ _pendingRunSeed;
110
+ _pendingRunTier;
111
+ get pendingRun() {
112
+ return this._pendingRunSeed;
113
+ }
114
+ get pendingRunTier() {
115
+ return this._pendingRunTier;
116
+ }
117
+ /**
118
+ * The cab that answers "call taxi" varies per job (player request: not
119
+ * always a van) -- picked once when the pending run lands, so the
120
+ * arrival and the ride read as ONE vehicle.
121
+ */
122
+ static RIDE_POOL = [
123
+ {
124
+ arrive: `A matte-black step-van splashes to the curb in front of you, side door already sliding open.`,
125
+ board: `The door rattles shut and the Haze smears past the grimy windows.`,
126
+ },
127
+ {
128
+ arrive: `A pearl-gray Westwind glides up out of the rain, gullwing lifting -- somebody's billing this to the client.`,
129
+ board: `The gullwing sighs closed and the launch pins you into leather that costs more than your rent.`,
130
+ },
131
+ {
132
+ arrive: `A patchwork gypsy cab rattles around the corner and stops dead in front of you, meter torn out, driver watching the mirrors.`,
133
+ board: `The cabbie pulls off before the door's even latched, humming something older than the district.`,
134
+ },
135
+ {
136
+ arrive: `A rust-eaten GMC Bulldog bulls through traffic and mounts the curb beside you, panel doors ajar.`,
137
+ board: `The Bulldog coughs, catches, and shoves into traffic like it's owed something.`,
138
+ },
139
+ {
140
+ arrive: `An unmarked ambulance rolls up silent, lights dead, back doors swinging open -- nobody looks twice at it.`,
141
+ board: `You ride out flat on the gurney, ceiling lights strobing past the little windows.`,
142
+ },
143
+ {
144
+ arrive: `A growling Harley with a reinforced sidecar pulls a U-turn and stops at your boots, rider jerking a thumb at the car.`,
145
+ board: `You drop into the sidecar; the pipes drown the whole district out.`,
146
+ },
147
+ ];
148
+ _pendingRide;
149
+ /** The pending job's cab flavor -- "call taxi" narrates with it (see call.ts). */
150
+ get pendingRide() {
151
+ return this._pendingRide;
152
+ }
153
+ /**
154
+ * Generates NPC first-impression appearances ("look at <npc>") in the
155
+ * background, one at a time -- the only AI dressing still worth doing
156
+ * now that every scene arrives with real prose. Fire-and-forget: until
157
+ * an appearance lands, look-at just shows a bit less flavor.
158
+ */
159
+ warmNpcAppearancesInBackground() {
160
+ if (!this._useAi)
161
+ return;
162
+ const npcs = this.scene.allActors.filter((a) => a instanceof NPC);
163
+ const context = `${this.sceneSeed?.name ?? ''} -- ${this.scene.story}`;
164
+ void (async () => {
165
+ for (const npc of npcs) {
166
+ try {
167
+ await npc.ensureAIAppearance(context);
168
+ }
169
+ catch {
170
+ // Non-fatal: the next look-at simply has less flavor.
171
+ }
172
+ }
173
+ })();
174
+ }
81
175
  // A single fixed room, created once and never torn down across job
82
176
  // transitions -- the player's own place, unlike every job's generated
83
177
  // rooms which get discarded wholesale when the next job replaces them.
@@ -138,6 +232,22 @@ export default class Game {
138
232
  CommandFactory.registerCommand('drone', DroneCommand);
139
233
  CommandFactory.registerCommand('pan', PanCommand);
140
234
  CommandFactory.registerCommand('reboot', RebootCommand);
235
+ CommandFactory.registerCommand('brandish', BrandishCommand);
236
+ CommandFactory.registerCommand('draw', BrandishCommand);
237
+ CommandFactory.registerCommand('holster', HolsterCommand);
238
+ CommandFactory.registerCommand('stow', HolsterCommand);
239
+ // "end" and "hangup" both bounced in real sessions when players
240
+ // wanted to end a call -- the intent is unambiguous mid-call.
241
+ CommandFactory.registerCommand('end', EndCallCommand);
242
+ CommandFactory.registerCommand('hangup', EndCallCommand);
243
+ CommandFactory.registerCommand('advance', AdvanceCommand);
244
+ CommandFactory.registerCommand('train', AdvanceCommand);
245
+ CommandFactory.registerCommand('haggle', HaggleCommand);
246
+ CommandFactory.registerCommand('negotiate', HaggleCommand);
247
+ CommandFactory.registerCommand('sneak', SneakCommand);
248
+ CommandFactory.registerCommand('persuade', PersuadeCommand);
249
+ CommandFactory.registerCommand('convince', PersuadeCommand);
250
+ CommandFactory.registerCommand('bluff', BluffCommand);
141
251
  }
142
252
  determineStartRoom() {
143
253
  return this.scene.determineStartRoom();
@@ -180,32 +290,19 @@ export default class Game {
180
290
  this.initInputBox(screen);
181
291
  this.initLogBox(screen);
182
292
  this.initStatusBox(screen);
183
- // Create a loading box
184
- const loadingBox = blessed.box({
185
- parent: screen,
186
- top: 'center',
187
- left: 'center',
188
- width: '50%',
189
- height: 'shrink',
190
- content: 'Generating Scene... Please wait.',
191
- border: { type: 'line' },
192
- style: {
193
- fg: 'white',
194
- bg: 'black',
195
- border: { fg: 'cyan' },
196
- }
197
- });
198
- screen.append(loadingBox);
199
- screen.render();
293
+ // No "Generating Scene... Please wait." box anymore: every path now
294
+ // arrives with REAL prose (hand-authored static seeds, chunk-authored
295
+ // generated scenes), so the old blocking AI-redressing pass -- 30-60
296
+ // seconds rewriting text that was already good -- is skipped and the
297
+ // game opens instantly. The one thing still worth generating (NPC
298
+ // first-impression appearances for "look at") warms in the background
299
+ // below.
200
300
  try {
201
- this.scene = await SceneSynthesizer.synthesizeFromJson(this.sceneSeed, this.player, options.useAi);
301
+ this.scene = await SceneSynthesizer.synthesizeFromJson(this.sceneSeed, this.player, false);
202
302
  }
203
303
  catch (err) {
204
304
  throw new Error(`Scene generation failed: ${err.message}`);
205
305
  }
206
- // Remove loading box
207
- screen.remove(loadingBox);
208
- screen.render();
209
306
  this.scene.initialize(this);
210
307
  this.player.currentLocation = this.determineStartRoom();
211
308
  this.registerCommands();
@@ -217,6 +314,24 @@ export default class Game {
217
314
  });
218
315
  this.scene.addRoom(this._homeRoom);
219
316
  this.connectHomeRoom(this.player.currentLocation);
317
+ // Every session starts AT HOME -- the hideout, one door from the job's
318
+ // start room (player request, left as an in-game note: "I'd like to
319
+ // always start in my hideout"). The scene's isStartRoom still anchors
320
+ // where the hideout's door leads; the runner just wakes up on their
321
+ // own cot instead of already standing in the job.
322
+ this.player.currentLocation = this._homeRoom;
323
+ // Starting in a hub makes this a hub SESSION: runs are commissioned
324
+ // by calling the fixer from here, and completing one returns here.
325
+ if (this.scene.isHub) {
326
+ this._hubScene = this.scene;
327
+ this._hubName = this.sceneSeed?.name ?? 'home';
328
+ }
329
+ // Gear up automatically: wearables (and the commlink) slot into any
330
+ // EMPTY slots at load -- no "equip all" ritual to start a session
331
+ // (player request). Occupied slots (archetype signature gear) are
332
+ // never swapped out.
333
+ equipIntoEmptySlots(this.player);
334
+ this.warmNpcAppearancesInBackground();
220
335
  this.initialized = true;
221
336
  // Nudge the player toward "call" up front -- the fixer convention means
222
337
  // the story deliberately doesn't narrate a completed briefing, so
@@ -235,9 +350,14 @@ export default class Game {
235
350
  */
236
351
  canDraftNextScene() {
237
352
  if (this._draftInProgress) {
238
- return { allowed: false, message: `Already drafting the next job -- give it a moment, he'll call back.` };
353
+ return { allowed: false, message: `Still lining it up -- I'll ping you.` };
239
354
  }
240
- if (!this.scene.isCompleted()) {
355
+ if (this._pendingRunSeed) {
356
+ return { allowed: false, message: `You've got my message already. Call a taxi when you're ready to roll.` };
357
+ }
358
+ // A hub is ALWAYS ready to commission the next run -- it has no win
359
+ // condition to complete; that's the whole point of home.
360
+ if (!this.scene.isHub && !this.scene.isCompleted()) {
241
361
  return { allowed: false, message: `This job isn't finished yet.` };
242
362
  }
243
363
  if (!this._useAi) {
@@ -274,6 +394,13 @@ export default class Game {
274
394
  * instant feedback, but this stays safe to call directly too).
275
395
  */
276
396
  async continueToNextScene() {
397
+ // Hub sessions: a finished RUN doesn't chain into the next draft --
398
+ // "continue" takes you home instead, and the next run is commissioned
399
+ // from there (call the fixer). Classic sessions skip this and keep
400
+ // the old continue->draft loop.
401
+ if (this._hubScene && this.scene !== this._hubScene && this.scene.isCompleted()) {
402
+ return this.returnToHub();
403
+ }
277
404
  const gate = this.canDraftNextScene();
278
405
  if (!gate.allowed) {
279
406
  return gate.message ?? `This job isn't finished yet.`;
@@ -286,6 +413,44 @@ export default class Game {
286
413
  this._draftInProgress = false;
287
414
  }
288
415
  }
416
+ /**
417
+ * Re-points the world at the preserved hub scene after a run: the hub's
418
+ * whole state (NPCs, shop stock, anything dropped there) survived
419
+ * untouched because the INSTANCE survived. The player walks back in
420
+ * through their own hideout, firmly in the meat -- any lingering run
421
+ * state (a persona still jacked in, an astral projection, a sneak,
422
+ * open combat bookkeeping) is settled on the way home.
423
+ */
424
+ returnToHub() {
425
+ const hub = this._hubScene;
426
+ if (!hub)
427
+ return `There's no home to go back to.`;
428
+ // Settle cross-plane and combat state that must not leak into the hub
429
+ // (the run's rooms -- including a bodyRoom the player left behind --
430
+ // are about to be orphaned wholesale).
431
+ this.player.plane = 'meat';
432
+ this.player.bodyRoom = undefined;
433
+ this.player.simMode = undefined;
434
+ this.player.astralTicks = 0;
435
+ this.player.activeDeck = undefined;
436
+ this.player.astralPerceiving = false;
437
+ this.player.sneaking = false;
438
+ this.player.inExchange = false;
439
+ this.player.combatOpponent = undefined;
440
+ this.player.activeCallPartner = undefined;
441
+ this.autoFight.cancelPending();
442
+ this.scene = hub;
443
+ this.scene.addRoom(this._homeRoom);
444
+ this.connectHomeRoom(hub.determineStartRoom());
445
+ this.player.currentLocation = this._homeRoom;
446
+ this.player.cameFrom = undefined;
447
+ this.updateEquipment(this.player.equipment);
448
+ this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
449
+ this.updateExits(this.player.currentLocation);
450
+ this.updateStatus();
451
+ Logger.getInstance().write(`Returned to hub "${this._hubName}".`);
452
+ return `\n=== ${this._hubName.toUpperCase()} ===\nThe run's wrapped and the street swallows you back up. You're home -- your hideout, one door off ${hub.determineStartRoom().name}. Call ${FIXER_NAME} when you're hungry for the next job.`;
453
+ }
289
454
  /**
290
455
  * The actual drafting work, split out from continueToNextScene() purely
291
456
  * so that method can wrap it in the try/finally that clears
@@ -309,40 +474,99 @@ export default class Game {
309
474
  // seconds for a tiered scene with AI flavor enabled. Framed as the
310
475
  // fixer's own line when he's actually in this scene, rather than a
311
476
  // bare system message -- he's the one drumming up the next gig.
312
- const waitingLine = fixerStillInScene
313
- ? `\n${CALL_ICON} ${FIXER_NAME}: "I'll call you back when I've got something lined up." (drafting the next job...)`
314
- : '\nDrafting the next job... please wait.';
315
- this.logBox.pushLine(waitingLine);
316
- this.logBox.scrollTo(this.logBox.getScrollHeight());
317
- this._screen.render();
318
- const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, previousFixerName);
477
+ // No system "drafting..." line -- the fixer's own one-ring commission
478
+ // (call.ts) already told the player he'll call back; anything more
479
+ // reads as machinery (real playtest note). The wait resolves as HIS
480
+ // callback either way, success or failure.
481
+ //
482
+ // The player object rides along so the job is drafted FOR this runner
483
+ // -- their archetype, stats, and toolkit (see buildPlayerDossier).
484
+ const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, previousFixerName, this.player);
319
485
  if (!nextSeed) {
320
- return `Couldn't draft a new job this time. Type "continue" to try again, or "quit" to stop here.`;
486
+ return `Incoming call -- ${FIXER_NAME}: "Job fell through, chummer. Client got cold feet before the ink dried. Give me another call in a bit -- I'll shake something loose."`;
321
487
  }
322
- this._tier = nextTier;
323
- this.sceneSeed = nextSeed;
488
+ // HUB sessions: the draft lands as a MESSAGE, never a teleport (real
489
+ // playtest note: being yanked into the run the instant generation
490
+ // finished was jarring). The player invokes it on their own time:
491
+ // "call Mr. Johnson" delivers the offer (haggle window included --
492
+ // see call.ts), and "travel" actually goes (launchPendingRun).
493
+ if (this.hasHub) {
494
+ this._pendingRunSeed = nextSeed;
495
+ this._pendingRunTier = nextTier;
496
+ this._pendingRide = Game.RIDE_POOL[Math.floor(Math.random() * Game.RIDE_POOL.length)];
497
+ return `Your link pings -- 1 new message: ${FIXER_NAME}. ("call ${FIXER_NAME}" to hear the job.)`;
498
+ }
499
+ // Classic sessions transition immediately, hideout-first, as always.
500
+ return await this.activateRunScene(nextSeed, nextTier, { startInHideout: true });
501
+ }
502
+ /**
503
+ * Installs a drafted run as the live scene. Shared by the classic
504
+ * immediate path (hideout start) and the hub's "travel" (start-room
505
+ * start -- you rode there, you arrive AT the job).
506
+ */
507
+ async activateRunScene(seed, tier, opts) {
508
+ this._tier = tier;
509
+ this.sceneSeed = seed;
324
510
  // A call partner from the old scene wouldn't exist in the new one --
325
511
  // hang up before moving on, rather than leave a dangling reference.
326
512
  if (this.player.activeCallPartner) {
513
+ this.player.activeCallPartner.activeCallPartner = undefined;
327
514
  this.player.activeCallPartner = undefined;
328
515
  }
329
- // false: this is a continuation, not a fresh game -- the player already
330
- // has gear from prior jobs, so don't dump another starter kit on top.
331
- this.scene = await SceneSynthesizer.synthesizeFromJson(nextSeed, this.player, this._useAi, false);
516
+ // false, false: no starter kit (the player carries their own gear
517
+ // forward), and no blocking AI-redressing pass either -- the chunked
518
+ // rooms pass already wrote this scene's prose.
519
+ this.scene = await SceneSynthesizer.synthesizeFromJson(seed, this.player, false, false);
332
520
  this.scene.initialize(this);
333
521
  this.player.currentLocation = this.determineStartRoom();
334
522
  // New job, fresh luck (see commands/edge.ts).
335
523
  this.player.refreshEdge();
336
524
  // The home base is owned by Game, not any one Scene -- re-inject it and
337
- // rewire its one exit to point at this job's start room instead of the
338
- // just-abandoned one.
525
+ // rewire its one exit to point at this job's start room.
339
526
  this.scene.addRoom(this._homeRoom);
340
527
  this.connectHomeRoom(this.player.currentLocation);
528
+ if (opts.startInHideout) {
529
+ this.player.currentLocation = this._homeRoom;
530
+ }
341
531
  this.updateEquipment(this.player.equipment);
342
532
  this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
343
533
  this.updateExits(this.player.currentLocation);
344
534
  this.updateStatus();
345
- return `\n=== JOB #${this._tier}: ${nextSeed.name} ===\n${this.scene.story}`;
535
+ // Fresh streets, no trail yet (see Player.cameFrom / the HUD crumb).
536
+ this.player.cameFrom = undefined;
537
+ this.warmNpcAppearancesInBackground();
538
+ return `\n=== JOB #${this._tier}: ${seed.name} ===\n${this.scene.story}`;
539
+ }
540
+ /**
541
+ * "travel": takes the arranged transport to the pending job (see
542
+ * commands/travel.ts). The player arrives AT the run's start room --
543
+ * "You travel to <location>" -- not teleported onto their own cot.
544
+ * A haggle struck during the offer call already mutated the pending
545
+ * seed's payout, and stays spent across the transition (refreshEdge
546
+ * would otherwise re-arm it).
547
+ */
548
+ async launchPendingRun() {
549
+ const seed = this._pendingRunSeed;
550
+ const tier = this._pendingRunTier;
551
+ if (!seed || tier === undefined) {
552
+ return `There's no job waiting. "call ${FIXER_NAME}" to drum one up.`;
553
+ }
554
+ const haggledAtOffer = this.player.haggledThisJob;
555
+ try {
556
+ const banner = await this.activateRunScene(seed, tier, { startInHideout: false });
557
+ this._pendingRunSeed = undefined;
558
+ this._pendingRunTier = undefined;
559
+ this._pendingRide = undefined;
560
+ this.player.haggledThisJob = haggledAtOffer;
561
+ return `You travel to ${this.player.currentLocation.name}.\n${banner}`;
562
+ }
563
+ catch (err) {
564
+ this._pendingRunSeed = undefined;
565
+ this._pendingRunTier = undefined;
566
+ this._pendingRide = undefined;
567
+ Logger.getInstance().error(`launchPendingRun failed: ${err}`);
568
+ return `The ride never shows and the job address goes dead. ${FIXER_NAME} will not enjoy hearing this -- call him for another.`;
569
+ }
346
570
  }
347
571
  async close() {
348
572
  this._inputBox.removeAllListeners(); // removes all event listeners
@@ -471,8 +695,13 @@ export default class Game {
471
695
  // is illegible enough that the stance looked absent entirely.
472
696
  const stanceColor = player.autoStance === 'aggressive' ? 'red' : player.autoStance === 'guarded' ? 'yellow' : 'white';
473
697
  const stance = `{${stanceColor}-fg}⚔ ${player.autoStance.toUpperCase()}{/${stanceColor}-fg}`;
698
+ // The breadcrumb, reading chronologically into the current location:
699
+ // "Neon Strip (west) > Night Market" -- origin first, the direction
700
+ // BACK to it in parens. An 18-room hub made "which way did I come in"
701
+ // a real question.
702
+ const crumb = player.cameFrom ? `${player.cameFrom.room} (${player.cameFrom.back}) > ` : '';
474
703
  if (player.currentLocation) {
475
- lines.push(`{bold}📍 ${player.currentLocation.name}{/bold} | ${stance}`);
704
+ lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}{/bold} | ${stance}`);
476
705
  }
477
706
  else {
478
707
  lines.push(stance);
@@ -493,15 +722,22 @@ export default class Game {
493
722
  // actually live (equipped), the recurring "why can't I call anyone"
494
723
  // friction surfaced at a glance.
495
724
  const weapon = player.getWeaponProfile();
496
- const weaponItem = player.equipment.rightHand?.weapon ?? player.equipment.leftHand?.weapon;
725
+ const weaponItem = player.getCarriedWeapon();
497
726
  const ammoNote = weaponItem?.isFirearm()
498
727
  ? (weaponItem.jammed
499
728
  ? ` {red-fg}[JAMMED -- reboot]{/red-fg}`
500
729
  : weaponItem.ammo === 0 ? ` {red-fg}[DRY]{/red-fg}` : ` [${weaponItem.ammo} rds]`)
501
730
  : '';
502
- const armed = weapon.name === 'bare fists'
503
- ? `{gray-fg}Unarmed{/gray-fg}`
504
- : `Armed: ${weapon.name} (DV ${weapon.damage}${weapon.poolBonus > 0 ? `, +${weapon.poolBonus} dice` : ''})${ammoNote}`;
731
+ // Three armed states on the meat plane: drawn, holstered (weapon
732
+ // carried but riding leather -- "brandish" puts it in hand; combat
733
+ // auto-draws so this is posture, not readiness), or nothing at all.
734
+ // Off-plane the profile is attack programs / astral will and reads as
735
+ // Armed. White, not gray, for the same legibility reason as the stance.
736
+ const armed = player.plane === 'meat' && weaponItem && !player.weaponDrawn
737
+ ? `{white-fg}Holstered: ${weaponItem.name}${ammoNote} -- "brandish"{/white-fg}`
738
+ : weapon.name === 'bare fists'
739
+ ? `{gray-fg}Unarmed{/gray-fg}`
740
+ : `Armed: ${weapon.name} (DV ${weapon.damage}${weapon.poolBonus > 0 ? `, +${weapon.poolBonus} dice` : ''})${ammoNote}`;
505
741
  const armorNote = `Armor +${player.getArmorValue()}`;
506
742
  const hasLink = player.equipment.head?.commlink != null;
507
743
  const link = !hasLink
@@ -517,6 +753,12 @@ export default class Game {
517
753
  if (player.sustainedArmor > 0) {
518
754
  lines.push(`{magenta-fg}Sustained: armor +${player.sustainedArmor} soak (-2 dice to everything else){/magenta-fg}`);
519
755
  }
756
+ if (player.astralPerceiving) {
757
+ lines.push(`{magenta-fg}✧ ASTRAL SENSES OPEN (-2 dice sustained) -- "assense" to close{/magenta-fg}`);
758
+ }
759
+ if (player.sneaking) {
760
+ lines.push(`{white-fg}🐾 SNEAKING -- entries are opposed vs their perception; loud acts break it{/white-fg}`);
761
+ }
520
762
  if (player.activeCallPartner) {
521
763
  lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} On a call with ${player.activeCallPartner.name}{/${CALL_COLOR}-fg}`);
522
764
  }
@@ -562,7 +804,10 @@ export default class Game {
562
804
  return Game.instance;
563
805
  }
564
806
  async handleInput(input) {
565
- const parts = input.trim().split(' ');
807
+ // "jack-in"/"jack-out" read naturally but parse as unknown verbs (a
808
+ // real session typed "jack-in" and bounced) -- normalize the hyphen.
809
+ const normalized = input.replace(/^\s*jack-(in|out)\b/i, 'jack $1');
810
+ const parts = normalized.trim().split(' ');
566
811
  const verb = parts[0].toLowerCase();
567
812
  const args = parts.slice(1);
568
813
  if (verb === 'quit' || verb === 'exit') {
@@ -572,19 +817,13 @@ export default class Game {
572
817
  return 'You are still in the game, type "quit" to exit';
573
818
  }
574
819
  if (verb === 'continue') {
575
- // Instant feedback for "not ready yet" (job not done, no AI
576
- // configured); otherwise kicks off drafting in the background rather
577
- // than freezing this command (and everything after it) behind a
578
- // 20-40+ second generation call -- same as calling the fixer does.
579
- const gate = this.canDraftNextScene();
580
- if (!gate.allowed) {
581
- return gate.message ?? `This job isn't finished yet.`;
582
- }
583
- this.draftNextSceneInBackground();
584
- return `Understood -- drafting the next job now. Feel free to keep exploring in the meantime.`;
820
+ // RETIRED verb, kept only as a redirect: jobs flow through the
821
+ // fixer ("call Mr. Johnson"), and a finished run ends by walking
822
+ // home to your hideout -- no system command in the loop.
823
+ return `That's not how the street works anymore, chummer: "call ${FIXER_NAME}" when you want work. When a run's wrapped, head home to your hideout -- the door knows the way.`;
585
824
  }
586
825
  if (verb === 'help') {
587
- return this.getHelpMessage();
826
+ return this.getHelpMessage(args);
588
827
  }
589
828
  if (verb === 'clear') {
590
829
  return '';
@@ -601,21 +840,103 @@ export default class Game {
601
840
  const result = await command.execute(args);
602
841
  return result;
603
842
  }
843
+ // Typo tolerance: two straight sessions bounced on slips like "searh"
844
+ // and "maop". Reuse the same fuzzy matcher item/NPC names go through
845
+ // (ambiguous guesses deliberately miss). A unique match auto-runs with
846
+ // a note -- EXCEPT anything that ends the session, which only ever
847
+ // gets a suggestion: a typo must never quit the game.
848
+ const knownVerbs = [...Object.keys(CommandFactory.getAllRegisteredCommands()), 'help', 'clear', 'quit', 'exit'];
849
+ let guess = fuzzyPickName(verb, knownVerbs);
850
+ // The shared matcher refuses typo tolerance on words <= 3 chars (too
851
+ // risky for item/NPC names) -- but the verb list is small and
852
+ // distinct, so a unique edit-distance-1 hit is safe ("maop" -> "map").
853
+ if (!guess && verb.length >= 3) {
854
+ const close = knownVerbs.filter(v => levenshteinDistance(verb, v) <= 1);
855
+ if (close.length === 1)
856
+ guess = close[0];
857
+ }
858
+ if (guess && guess !== verb) {
859
+ if (guess === 'quit' || guess === 'exit') {
860
+ return `I don't understand "${verb}" -- did you mean "${guess}"?`;
861
+ }
862
+ const corrected = await this.handleInput([guess, ...args].join(' '));
863
+ return corrected === null ? null : `(read "${verb}" as "${guess}")\n${corrected ?? ''}`;
864
+ }
604
865
  return `I don't understand the command "${verb}".`;
605
866
  }
606
- getHelpMessage() {
607
- const commands = CommandFactory.getAllRegisteredCommands();
608
- let helpText = "Available Commands:\n";
609
- for (const verb in commands) {
610
- if (Object.hasOwn(commands, verb)) {
611
- const CommandClass = commands[verb];
612
- helpText += `- ${verb}: ${CommandClass.getDescription()}\n`;
867
+ /**
868
+ * The help taxonomy: every entry is [primary, ...aliases], grouped the
869
+ * way a player thinks, not the order commands were registered. The
870
+ * overview shows only verbs per category (the flat all-descriptions
871
+ * dump had grown past 45 lines); "help <category>" and "help <command>"
872
+ * drill into the real descriptions. Verbs registered but missing from
873
+ * this table surface in an "other" bucket rather than vanishing -- a
874
+ * future command can't silently fall out of help.
875
+ */
876
+ static HELP_CATEGORIES = [
877
+ { title: 'moving', entries: [['look'], ['go'], ['map', 'exits'], ['search'], ['sneak']] },
878
+ { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
879
+ { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['stance'], ['surrender'], ['edge'], ['rest']] },
880
+ { title: 'magic', entries: [['spells'], ['cast'], ['project'], ['return'], ['assense']] },
881
+ { title: 'matrix', entries: [['jack'], ['hack'], ['pan'], ['silent'], ['reboot'], ['drone']] },
882
+ { title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['haggle', 'negotiate'], ['persuade', 'convince']] },
883
+ { title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
884
+ { title: 'character', entries: [['sheet', 'stats'], ['advance', 'train']] },
885
+ { title: 'session', entries: [['help'], ['clear'], ['quit', 'exit']] },
886
+ ];
887
+ /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
888
+ static BUILTIN_DESCRIPTIONS = {
889
+ help: 'This overview. "help <category>" for one group with full descriptions; "help <command>" for one command.',
890
+ clear: 'Clear the screen.',
891
+ quit: 'Exit the game.',
892
+ };
893
+ describeVerb(verb) {
894
+ const builtin = Game.BUILTIN_DESCRIPTIONS[verb];
895
+ if (builtin)
896
+ return builtin;
897
+ const CommandClass = CommandFactory.getAllRegisteredCommands()[verb];
898
+ return CommandClass?.getDescription() ?? 'No description available.';
899
+ }
900
+ getHelpMessage(args = []) {
901
+ const registered = Object.keys(CommandFactory.getAllRegisteredCommands());
902
+ const covered = new Set(Game.HELP_CATEGORIES.flatMap(c => c.entries.flat()));
903
+ const uncategorized = registered.filter(v => !covered.has(v));
904
+ const query = args.join(' ').trim().toLowerCase();
905
+ // "help <category>": that group, with full descriptions.
906
+ if (query) {
907
+ const categoryNames = Game.HELP_CATEGORIES.map(c => c.title);
908
+ const pickedCategory = fuzzyPickName(query, categoryNames);
909
+ if (pickedCategory) {
910
+ const category = Game.HELP_CATEGORIES.find(c => c.title === pickedCategory);
911
+ const lines = [`=== ${category.title.toUpperCase()} ===`];
912
+ for (const [primary, ...aliases] of category.entries) {
913
+ lines.push(`- ${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
914
+ }
915
+ return lines.join('\n');
613
916
  }
917
+ // "help <command>": one description, aliases resolved to their primary.
918
+ const allVerbs = [...covered, ...uncategorized];
919
+ const pickedVerb = fuzzyPickName(query, allVerbs);
920
+ if (pickedVerb) {
921
+ const entry = Game.HELP_CATEGORIES.flatMap(c => c.entries).find(e => e.includes(pickedVerb));
922
+ const primary = entry?.[0] ?? pickedVerb;
923
+ const aliases = entry?.slice(1) ?? [];
924
+ return `${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`;
925
+ }
926
+ return `No command or category called "${query}". Try plain "help".`;
927
+ }
928
+ // The overview: compact category lines, primaries only.
929
+ const lines = ['=== COMMANDS ==='];
930
+ const pad = Math.max(...Game.HELP_CATEGORIES.map(c => c.title.length));
931
+ for (const category of Game.HELP_CATEGORIES) {
932
+ lines.push(`${category.title.padEnd(pad)} ${category.entries.map(e => e[0]).join(', ')}`);
933
+ }
934
+ if (uncategorized.length > 0) {
935
+ lines.push(`${'other'.padEnd(pad)} ${uncategorized.join(', ')}`);
614
936
  }
615
- helpText += `- clear: Clear the screen.\n`;
616
- helpText += `- continue: Take on a new, tougher job once you've completed the current one.\n`;
617
- helpText += `- quit: Exit the game.\n`;
618
- return helpText;
937
+ lines.push('');
938
+ lines.push(`"help <category>" for a group with descriptions ("help combat"); "help <command>" for one ("help haggle").`);
939
+ return lines.join('\n');
619
940
  }
620
941
  }
621
942
  //# sourceMappingURL=game.js.map