@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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +14 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +24 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +31 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +16 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.d.ts +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +86 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +5 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +5 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +94 -16
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.d.ts +38 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +178 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hints.d.ts +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hints.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hints.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/lifestyle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +48 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.d.ts +41 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +160 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/project.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/project.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +15 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/solve.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +24 -13
- package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +37 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/unlock.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/work.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/work.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/item-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/puzzle-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +138 -23
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +33 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +303 -26
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +195 -6
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +11 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +35 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +19 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +51 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +45 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +197 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +76 -20
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +157 -34
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +208 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/exit-seed.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/item-seed.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/player-seed.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/puzzle-seed.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.d.ts +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +100 -7
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +18 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +232 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +17 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.d.ts +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.js +57 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grapple.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.d.ts +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js +55 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.d.ts +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +447 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +8 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +121 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +315 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +174 -18
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
- 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)
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
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
|
|
1657
|
-
//
|
|
1658
|
-
|
|
1659
|
-
|
|
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 = {
|