@maka/maka-cli 5.7.0 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js +16 -31
- package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +6 -4
- 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 +19 -7
- 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 +35 -9
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.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 +118 -29
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.d.ts +33 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +138 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -0
- 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 +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +138 -19
- 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 +14 -5
- 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 +25 -3
- 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 +53 -8
- 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.d.ts +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +152 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -0
- 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/pick.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/pick.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 +3 -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 +24 -6
- 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.d.ts +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +113 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js.map +1 -0
- 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 +91 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +501 -23
- 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 +44 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +349 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +36 -7
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +9 -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 +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +57 -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 +28 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +172 -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/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.js +81 -5
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +21 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +25 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +128 -28
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +7 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +8 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.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/log-style.d.ts +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js +15 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js +49 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +31 -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/ssl.js +10 -15
- package/bundle/typescript/src/commands/ssl.js.map +1 -1
- package/bundle/typescript/src/templates/react/kit/mui/auth/password-reset/basic-password-reset-page.js.tsx +3 -4
- package/bundle/typescript/src/tools/ssl/ssl.class.d.ts +8 -0
- package/bundle/typescript/src/tools/ssl/ssl.class.js +53 -0
- package/bundle/typescript/src/tools/ssl/ssl.class.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { capitalCase } from 'change-case';
|
|
|
3
3
|
import { Category } from './types/shared/item-enum.js';
|
|
4
4
|
import { Item } from './models/item.js';
|
|
5
5
|
import { Room } from './models/_index.js';
|
|
6
|
+
import { Door } from './models/door.js';
|
|
6
7
|
import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
|
|
7
8
|
import { rollPool, formatRoll } from './utilities/dice.js';
|
|
8
9
|
import { Direction } from './types/shared/direction-enum.js';
|
|
@@ -33,6 +34,7 @@ import { SilentCommand } from './commands/silent.js';
|
|
|
33
34
|
import { RestCommand } from './commands/rest.js';
|
|
34
35
|
import { ReloadCommand } from './commands/reload.js';
|
|
35
36
|
import { SubdueCommand } from './commands/subdue.js';
|
|
37
|
+
import { GrappleCommand, ReleaseCommand, StruggleCommand } from './commands/grapple.js';
|
|
36
38
|
import { EdgeCommand } from './commands/edge.js';
|
|
37
39
|
import { SheetCommand } from './commands/sheet.js';
|
|
38
40
|
import { CallCommand } from './commands/call.js';
|
|
@@ -56,6 +58,12 @@ import { AskCommand } from './commands/ask.js';
|
|
|
56
58
|
import { ContactsCommand } from './commands/contacts.js';
|
|
57
59
|
import { JumpCommand } from './commands/jump.js';
|
|
58
60
|
import { InitiateCommand } from './commands/initiate.js';
|
|
61
|
+
import { SummonCommand } from './commands/summon.js';
|
|
62
|
+
import { OrderCommand } from './commands/order.js';
|
|
63
|
+
import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
|
|
64
|
+
import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
|
|
65
|
+
import { HintsCommand } from './commands/hints.js';
|
|
66
|
+
import { hint } from './utilities/hints.js';
|
|
59
67
|
import { InstallCommand } from './commands/install.js';
|
|
60
68
|
import { BuyCommand } from './commands/buy.js';
|
|
61
69
|
import { SellCommand } from './commands/sell.js';
|
|
@@ -67,6 +75,7 @@ import { EatCommand } from './commands/eat.js';
|
|
|
67
75
|
import { DrinkCommand } from './commands/drink.js';
|
|
68
76
|
import { equipIntoEmptySlots } from './archetypes.js';
|
|
69
77
|
import { NPC } from './models/npc.js';
|
|
78
|
+
import { CommandRegistry } from './commands/command-registry.js';
|
|
70
79
|
import { HaggleCommand } from './commands/haggle.js';
|
|
71
80
|
import { SneakCommand } from './commands/sneak.js';
|
|
72
81
|
import { PersuadeCommand } from './commands/persuade.js';
|
|
@@ -76,6 +85,9 @@ import { CommandFactory } from './factories/command-factory.js';
|
|
|
76
85
|
import { SceneSynthesizer } from './factories/scene-factory.js';
|
|
77
86
|
import { SceneSeedGenerator, FIXER_NAME } from './factories/scene-seed-generator.js';
|
|
78
87
|
import { CALL_ICON, CALL_COLOR } from './utilities/comm-style.js';
|
|
88
|
+
import { AI } from '../../../tools/ai/ai.class.js';
|
|
89
|
+
import { BULLET } from './utilities/log-style.js';
|
|
90
|
+
import { spotOf } from './utilities/spots.js';
|
|
79
91
|
export default class Game {
|
|
80
92
|
initialized;
|
|
81
93
|
static instance;
|
|
@@ -91,6 +103,11 @@ export default class Game {
|
|
|
91
103
|
// give the log the whole screen.
|
|
92
104
|
_screen;
|
|
93
105
|
_logBox;
|
|
106
|
+
_metaBox;
|
|
107
|
+
// Quit confirmation state (see handleInput / initInputBox): armed by
|
|
108
|
+
// the first quit/exit or ESC, cleared by anything that isn't a yes.
|
|
109
|
+
_quitArmed = false;
|
|
110
|
+
_escArmedAt = 0;
|
|
94
111
|
_statusBox;
|
|
95
112
|
_inputBox;
|
|
96
113
|
_useAi = false;
|
|
@@ -164,6 +181,227 @@ export default class Game {
|
|
|
164
181
|
}
|
|
165
182
|
Logger.getInstance().write(`Home turf: ${this.contacts.size} contacts seeded from the hub cast.`);
|
|
166
183
|
}
|
|
184
|
+
// ==================== COMPANIONS: the party layer ====================
|
|
185
|
+
// Every companion is a per-scene NPC SHELL plus persistent STATE, and
|
|
186
|
+
// scene transitions are covered by narration, never instance rebinding
|
|
187
|
+
// (design decision with the player): spirits fade (RAW p.300: the
|
|
188
|
+
// summons doesn't outlast the ride), drones fold to the pack, agents
|
|
189
|
+
// into the deck -- damage lives on the bound Item and redeploys free.
|
|
190
|
+
// Tenants: summoned spirits (summon.ts), matrix agents (agent-cmd.ts),
|
|
191
|
+
// autonomous drones (deploy in drone-deploy.ts). Future tenants
|
|
192
|
+
// (enlisted allies, anthroform frames) ride the same rail with
|
|
193
|
+
// different words ("takes their own ride and meets you there").
|
|
194
|
+
// Roster invariant: entries are LIVE SHELLS only -- dismissal and
|
|
195
|
+
// death both remove the entry; persistent state (the frame, the deck)
|
|
196
|
+
// lives on Items in the player's possession, never here.
|
|
197
|
+
companions = [];
|
|
198
|
+
// Drones deployed at travel time re-shell on arrival ("the rotors spin
|
|
199
|
+
// back up at the curb") -- tracked as their items across the swap.
|
|
200
|
+
_pendingRedeploys = [];
|
|
201
|
+
/**
|
|
202
|
+
* The one correct runtime NPC spawn (mirrors scene-factory's seed
|
|
203
|
+
* path): construct with a scene-bound registry, add to the scene
|
|
204
|
+
* (names must be unique -- addActor throws), place via
|
|
205
|
+
* setCharacterLocation (the ONLY path that binds room observers), and
|
|
206
|
+
* hand it the scene for its AI context.
|
|
207
|
+
*/
|
|
208
|
+
spawnCompanionShell(opts) {
|
|
209
|
+
if (this.scene.getActor(opts.name)) {
|
|
210
|
+
Logger.getInstance().write(`spawnCompanionShell: name "${opts.name}" already in scene -- refusing.`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const npc = new NPC({
|
|
214
|
+
dialog: [],
|
|
215
|
+
description: opts.description,
|
|
216
|
+
playerConstructorConfig: { playerName: opts.name, startLocation: opts.room, combat: opts.combat },
|
|
217
|
+
}, new CommandRegistry(this.scene));
|
|
218
|
+
npc.plane = opts.plane;
|
|
219
|
+
npc.allyOf = this.player.name;
|
|
220
|
+
npc.companionKind = opts.kind;
|
|
221
|
+
npc.boundDevice = opts.boundDevice;
|
|
222
|
+
this.scene.addActor(npc);
|
|
223
|
+
this.scene.setCharacterLocation(npc, opts.room.name);
|
|
224
|
+
npc.scene = this.scene;
|
|
225
|
+
this.companions.push({ npc, kind: opts.kind, services: opts.services, force: opts.force, boundDevice: opts.boundDevice });
|
|
226
|
+
Logger.getInstance().write(`Companion spawned: ${opts.name} (${opts.kind}, force ${opts.force}) in ${opts.room.name}.`);
|
|
227
|
+
return npc;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The autonomous-drone shell, shared by the deploy command and
|
|
231
|
+
* arrival re-shelling: the frame's dog-brain (Pilot = 2 + quality)
|
|
232
|
+
* stands in for every attribute it lacks (RAW p.269-271).
|
|
233
|
+
*/
|
|
234
|
+
// Cargo sling by frame class (player request: capacity enforced -- a
|
|
235
|
+
// palm-sized Fly-Spy is not a mule). Keyed by the frame Item's size.
|
|
236
|
+
static DRONE_CARGO_KG = {
|
|
237
|
+
tiny: 0.5, small: 2, medium: 8, large: 20, huge: 40,
|
|
238
|
+
};
|
|
239
|
+
deployDroneShell(item, room) {
|
|
240
|
+
const pilot = 2 + item.qualityBonus;
|
|
241
|
+
const armed = /armed|combat|gun|roto|assault|sentry|weapon/i.test(`${item.name} ${item.description ?? ''}`);
|
|
242
|
+
const shell = this.spawnCompanionShell({
|
|
243
|
+
name: `${item.name} (autopilot)`,
|
|
244
|
+
description: `${this.player.name}'s drone running its dog-brain autopilot: it follows them, watches, and ${armed ? 'shoots what threatens them' : 'has cameras where guns would go -- it never attacks'}.`,
|
|
245
|
+
kind: 'drone',
|
|
246
|
+
combat: { body: 2 + item.qualityBonus, agility: pilot, reaction: pilot, logic: pilot, intuition: pilot, willpower: pilot, strength: 1, charisma: 1, skills: { perception: pilot, ...(armed ? { firearms: pilot } : {}) } },
|
|
247
|
+
room, plane: 'meat', force: pilot, boundDevice: item,
|
|
248
|
+
});
|
|
249
|
+
// Take-command weight checks (Player.addInventory) enforce this cap
|
|
250
|
+
// for the shell exactly as they do for a runner.
|
|
251
|
+
if (shell)
|
|
252
|
+
shell.maxCarryingWeight = Game.DRONE_CARGO_KG[String(item.size)] ?? 2;
|
|
253
|
+
return shell;
|
|
254
|
+
}
|
|
255
|
+
/** Drops a companion's shell and its roster entry. */
|
|
256
|
+
dismissCompanion(entry, reason) {
|
|
257
|
+
if (this.scene.getActor(entry.npc.name) === entry.npc) {
|
|
258
|
+
// Cargo comes home with the shell (player request: fetch-and-carry
|
|
259
|
+
// runs -- a real session's drone took a credstick and had no way
|
|
260
|
+
// to hand it over). A deliberate dismissal passes whatever the
|
|
261
|
+
// shell carried to its master; without this, scene.removeActor
|
|
262
|
+
// dumps it on the floor wherever the shell happened to be.
|
|
263
|
+
const carried = entry.npc.inventory.getAllItems().map(i => i.name);
|
|
264
|
+
const delivered = carried.filter(n => entry.npc.inventory.transferItemTo(n, this.player.inventory));
|
|
265
|
+
if (delivered.length > 0) {
|
|
266
|
+
Logger.getInstance().logWithColor(`👥 From ${entry.npc.name}'s cargo, now yours: ${delivered.join(', ')}.`, 'green');
|
|
267
|
+
}
|
|
268
|
+
// Mark deliberate before removeActor fires onActorRemoved, so no
|
|
269
|
+
// wreckage is charged to a clean dismissal.
|
|
270
|
+
entry.npc.deathClaimed = true;
|
|
271
|
+
this.scene.removeActor(entry.npc.name);
|
|
272
|
+
}
|
|
273
|
+
const idx = this.companions.indexOf(entry);
|
|
274
|
+
if (idx >= 0)
|
|
275
|
+
this.companions.splice(idx, 1);
|
|
276
|
+
Logger.getInstance().write(`Companion dismissed: ${entry.npc.name} -- ${reason}`);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* An actor left the world (Scene.removeActor). For a companion shell
|
|
280
|
+
* that DIED (not a deliberate dismissal -- those set deathClaimed
|
|
281
|
+
* first and prune themselves), settle the wreckage: a drone's frame is
|
|
282
|
+
* wrecked where it fell, an agent derezzes (the deck already ate the
|
|
283
|
+
* damage), a spirit is banished outright.
|
|
284
|
+
*/
|
|
285
|
+
onActorRemoved(name) {
|
|
286
|
+
const entry = this.companions.find(c => c.npc.name === name);
|
|
287
|
+
if (!entry)
|
|
288
|
+
return;
|
|
289
|
+
const idx = this.companions.indexOf(entry);
|
|
290
|
+
if (idx >= 0)
|
|
291
|
+
this.companions.splice(idx, 1);
|
|
292
|
+
if (entry.kind === 'drone' && entry.boundDevice && !entry.npc.deathClaimed) {
|
|
293
|
+
entry.boundDevice.takeDroneDamage(entry.boundDevice.maxDroneBoxes);
|
|
294
|
+
Logger.getInstance().logWithColor(`👥 The ${entry.boundDevice.name} is WRECKED where it fell -- rest to repair the frame.`, 'yellow');
|
|
295
|
+
}
|
|
296
|
+
this.updateStatus();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Spirits with no services left depart once no exchange holds them
|
|
300
|
+
* (called from CombatExchange.finishExchange): the bargain is spent.
|
|
301
|
+
*/
|
|
302
|
+
settleSpiritServices() {
|
|
303
|
+
for (const entry of [...this.companions]) {
|
|
304
|
+
if (entry.kind !== 'spirit' || (entry.services ?? 0) > 0)
|
|
305
|
+
continue;
|
|
306
|
+
if (entry.npc.inExchange)
|
|
307
|
+
continue;
|
|
308
|
+
// The paid fight is fought to the END. Between exchanges of the
|
|
309
|
+
// same fight inExchange is briefly false -- a real session's
|
|
310
|
+
// spirit spent its last service to join a fight and was dismissed
|
|
311
|
+
// 3ms later, before a single blow. Same freshness window as the
|
|
312
|
+
// per-fight billing in combat-exchange.
|
|
313
|
+
if (entry.npc.combatOpponent && Date.now() - entry.npc.lastExchangeAt < 60_000)
|
|
314
|
+
continue;
|
|
315
|
+
Logger.getInstance().logWithColor(`👥 ${entry.npc.name}'s bargain is spent -- it bows once and thins into nothing.`, 'green');
|
|
316
|
+
this.dismissCompanion(entry, 'services exhausted');
|
|
317
|
+
this.updateStatus();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// ==================== Pursuit heartbeat ====================
|
|
321
|
+
// Companions are stimulus-driven like every NPC: a directive burst ends
|
|
322
|
+
// at the reflect cap ("Max recursion reached") and a shell alone in a
|
|
323
|
+
// distant room then NEVER acts again -- a real session's drone was
|
|
324
|
+
// ordered to "find Mr. Krow", walked three rooms, and froze mid-hunt.
|
|
325
|
+
// The heartbeat re-spurs every shell with an open objective on a slow
|
|
326
|
+
// tick until it reports DONE/FAILED (which clears the objective -- see
|
|
327
|
+
// NPC.relayToMaster) or is dismissed.
|
|
328
|
+
_pursuitTimer;
|
|
329
|
+
_pursuitInFlight = new Set();
|
|
330
|
+
static PURSUIT_TICK_MS = 30_000;
|
|
331
|
+
startPursuitHeartbeat() {
|
|
332
|
+
if (this._pursuitTimer)
|
|
333
|
+
return;
|
|
334
|
+
this._pursuitTimer = setInterval(() => this.spurCompanionPursuits(), Game.PURSUIT_TICK_MS);
|
|
335
|
+
// Never hold the process open on our account.
|
|
336
|
+
this._pursuitTimer.unref?.();
|
|
337
|
+
}
|
|
338
|
+
stopPursuitHeartbeat() {
|
|
339
|
+
if (this._pursuitTimer)
|
|
340
|
+
clearInterval(this._pursuitTimer);
|
|
341
|
+
this._pursuitTimer = undefined;
|
|
342
|
+
}
|
|
343
|
+
spurCompanionPursuits() {
|
|
344
|
+
// The tick doubles as the spent-bargain sweep: finishExchange's own
|
|
345
|
+
// settle call now skips spirits whose fight is still warm (see
|
|
346
|
+
// settleSpiritServices), so without this a victorious spirit would
|
|
347
|
+
// linger at zero services forever.
|
|
348
|
+
this.settleSpiritServices();
|
|
349
|
+
if (!AI.isConfigured())
|
|
350
|
+
return;
|
|
351
|
+
for (const entry of this.companions) {
|
|
352
|
+
const npc = entry.npc;
|
|
353
|
+
if (!npc.objective)
|
|
354
|
+
continue;
|
|
355
|
+
if (npc.isIncapacitated() || npc.inExchange || npc.waitingForLull)
|
|
356
|
+
continue;
|
|
357
|
+
// A stale shell (already removed from the world) gets no spur.
|
|
358
|
+
if (this.scene.getActor(npc.name) !== npc)
|
|
359
|
+
continue;
|
|
360
|
+
// One spur in flight per shell -- a slow AI burst must not stack
|
|
361
|
+
// with the next tick.
|
|
362
|
+
if (this._pursuitInFlight.has(npc))
|
|
363
|
+
continue;
|
|
364
|
+
this._pursuitInFlight.add(npc);
|
|
365
|
+
Logger.getInstance().write(`Pursuit heartbeat: spurring ${npc.name} -- ${npc.objective}`);
|
|
366
|
+
void Promise.resolve(npc.hear(this.player, `[System note: HEARTBEAT -- your standing directive is still open: "${npc.objective}". Continue it NOW with [COMMAND] actions -- several steps if needed. If it is complete, say a line starting "DONE: <result>"; if it cannot be done, say a line starting "FAILED: <why>". Do not go quiet.]`, { viaLink: true }))
|
|
367
|
+
.catch(err => Logger.getInstance().error(`Pursuit heartbeat failed for ${npc.name}: ${err}`))
|
|
368
|
+
.finally(() => this._pursuitInFlight.delete(npc));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** Pre-travel: every shell folds, each with its own goodbye. */
|
|
372
|
+
foldCompanionsForTravel() {
|
|
373
|
+
const lines = [];
|
|
374
|
+
for (const entry of [...this.companions]) {
|
|
375
|
+
if (entry.kind === 'spirit') {
|
|
376
|
+
lines.push(`Your bound spirit thins into the rain -- the summons doesn't outlast the ride.`);
|
|
377
|
+
}
|
|
378
|
+
else if (entry.kind === 'drone') {
|
|
379
|
+
lines.push(`The ${entry.boundDevice?.name ?? 'drone'} autopilots home to your hand for the ride.`);
|
|
380
|
+
if (entry.boundDevice)
|
|
381
|
+
this._pendingRedeploys.push(entry.boundDevice);
|
|
382
|
+
}
|
|
383
|
+
else if (entry.kind === 'agent') {
|
|
384
|
+
lines.push(`Your agent folds back into the deck.`);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
lines.push(`${entry.npc.name} takes their own ride -- they'll meet you there.`);
|
|
388
|
+
}
|
|
389
|
+
this.dismissCompanion(entry, `travel: ${entry.kind} folds`);
|
|
390
|
+
}
|
|
391
|
+
return lines;
|
|
392
|
+
}
|
|
393
|
+
/** Post-arrival: re-shell what was deployed when you left (drones). */
|
|
394
|
+
unfoldCompanionsOnArrival() {
|
|
395
|
+
const lines = [];
|
|
396
|
+
for (const item of this._pendingRedeploys.splice(0)) {
|
|
397
|
+
if (item.isWrecked)
|
|
398
|
+
continue;
|
|
399
|
+
const shell = this.deployDroneShell(item, this.player.currentLocation);
|
|
400
|
+
if (shell)
|
|
401
|
+
lines.push(`The ${item.name}'s rotors spin back up at the curb.`);
|
|
402
|
+
}
|
|
403
|
+
return lines;
|
|
404
|
+
}
|
|
167
405
|
/**
|
|
168
406
|
* Records a real transaction with a hub NPC: first contact opens the
|
|
169
407
|
* relationship at Loyalty 1 (a safeguard -- the hub cast is pre-seeded
|
|
@@ -186,7 +424,7 @@ export default class Game {
|
|
|
186
424
|
if (kind === 'gig')
|
|
187
425
|
c.gigs += 1;
|
|
188
426
|
Logger.getInstance().write(`Contact opened: ${name} (connection ${c.connection}).`);
|
|
189
|
-
return `${name} knows your name now -- a CONTACT (Connection ${c.connection}, Loyalty 1)
|
|
427
|
+
return `${name} knows your name now -- a CONTACT (Connection ${c.connection}, Loyalty 1).${hint(` ("contacts" for the roster, "ask ${name} about ..." for legwork.)`)}`;
|
|
190
428
|
}
|
|
191
429
|
if (kind === 'gig') {
|
|
192
430
|
c.gigs += 1;
|
|
@@ -200,7 +438,7 @@ export default class Game {
|
|
|
200
438
|
c.spent += spent;
|
|
201
439
|
if (Math.floor(c.spent / 1000) > thousandsBefore && c.loyalty < 3) {
|
|
202
440
|
c.loyalty += 1;
|
|
203
|
-
return `${name} has started keeping your usual ready
|
|
441
|
+
return `${name} has started keeping your usual ready.${hint(` (Loyalty ${c.loyalty} -- patronage tops out at 3; run their errands to go deeper.)`)}`;
|
|
204
442
|
}
|
|
205
443
|
return null;
|
|
206
444
|
}
|
|
@@ -482,6 +720,12 @@ export default class Game {
|
|
|
482
720
|
CommandFactory.registerCommand('rest', RestCommand);
|
|
483
721
|
CommandFactory.registerCommand('reload', ReloadCommand);
|
|
484
722
|
CommandFactory.registerCommand('subdue', SubdueCommand);
|
|
723
|
+
// SUBDUING p.195 (see commands/grapple.ts): restrain without damage.
|
|
724
|
+
CommandFactory.registerCommand('grapple', GrappleCommand);
|
|
725
|
+
CommandFactory.registerCommand('restrain', GrappleCommand);
|
|
726
|
+
CommandFactory.registerCommand('clinch', GrappleCommand);
|
|
727
|
+
CommandFactory.registerCommand('release', ReleaseCommand);
|
|
728
|
+
CommandFactory.registerCommand('struggle', StruggleCommand);
|
|
485
729
|
CommandFactory.registerCommand('edge', EdgeCommand);
|
|
486
730
|
CommandFactory.registerCommand('sheet', SheetCommand);
|
|
487
731
|
CommandFactory.registerCommand('stats', SheetCommand);
|
|
@@ -517,6 +761,21 @@ export default class Game {
|
|
|
517
761
|
CommandFactory.registerCommand('rig', JumpCommand);
|
|
518
762
|
CommandFactory.registerCommand('initiate', InitiateCommand);
|
|
519
763
|
CommandFactory.registerCommand('submerge', InitiateCommand);
|
|
764
|
+
CommandFactory.registerCommand('summon', SummonCommand);
|
|
765
|
+
CommandFactory.registerCommand('conjure', SummonCommand);
|
|
766
|
+
// "At" spots (utilities/spots.ts): walk/move/approach are one
|
|
767
|
+
// crossing; run and sprint are the fast modes (combat semantics in
|
|
768
|
+
// the spot-move phase).
|
|
769
|
+
CommandFactory.registerCommand('walk', MoveCommand);
|
|
770
|
+
CommandFactory.registerCommand('move', MoveCommand);
|
|
771
|
+
CommandFactory.registerCommand('approach', MoveCommand);
|
|
772
|
+
CommandFactory.registerCommand('run', RunCommand);
|
|
773
|
+
CommandFactory.registerCommand('sprint', SprintCommand);
|
|
774
|
+
CommandFactory.registerCommand('hints', HintsCommand);
|
|
775
|
+
CommandFactory.registerCommand('order', OrderCommand);
|
|
776
|
+
CommandFactory.registerCommand('deploy', DeployCommand);
|
|
777
|
+
CommandFactory.registerCommand('recall', RecallCommand);
|
|
778
|
+
CommandFactory.registerCommand('agent', AgentCommand);
|
|
520
779
|
CommandFactory.registerCommand('install', InstallCommand);
|
|
521
780
|
CommandFactory.registerCommand('chrome', InstallCommand);
|
|
522
781
|
CommandFactory.registerCommand('buy', BuyCommand);
|
|
@@ -550,6 +809,19 @@ export default class Game {
|
|
|
550
809
|
if (this._homeRoomExitDirection) {
|
|
551
810
|
this._homeRoom.exits.delete(this._homeRoomExitDirection);
|
|
552
811
|
}
|
|
812
|
+
// The OTHER half of the previous link: sweep any exit on the anchor
|
|
813
|
+
// room that already leads home before choosing a direction. Without
|
|
814
|
+
// this, every homecoming re-ran the search against a room still
|
|
815
|
+
// holding last time's hideout door and stacked a second one on the
|
|
816
|
+
// next free direction (a real session's Neon Strip ended up with
|
|
817
|
+
// "up" AND "down" both reading "To Your Hideout").
|
|
818
|
+
for (const [dir, ex] of [...jobStartRoom.exits.entries()]) {
|
|
819
|
+
const leadsHome = ex instanceof Door
|
|
820
|
+
? ex.getOtherSide(jobStartRoom) === this._homeRoom
|
|
821
|
+
: ex === this._homeRoom.name;
|
|
822
|
+
if (leadsHome)
|
|
823
|
+
jobStartRoom.exits.delete(dir);
|
|
824
|
+
}
|
|
553
825
|
const direction = Game.HOME_DIRECTION_PRIORITY.find(d => !jobStartRoom.exits.has(d));
|
|
554
826
|
if (!direction) {
|
|
555
827
|
// Every direction is already spoken for on this job's start room --
|
|
@@ -568,6 +840,9 @@ export default class Game {
|
|
|
568
840
|
get logBox() {
|
|
569
841
|
return this._logBox;
|
|
570
842
|
}
|
|
843
|
+
get metaBox() {
|
|
844
|
+
return this._metaBox;
|
|
845
|
+
}
|
|
571
846
|
get inputBox() {
|
|
572
847
|
return this._inputBox;
|
|
573
848
|
}
|
|
@@ -576,6 +851,7 @@ export default class Game {
|
|
|
576
851
|
this._logFilePath = options.logFilePath;
|
|
577
852
|
this.initInputBox(screen);
|
|
578
853
|
this.initLogBox(screen);
|
|
854
|
+
this.initMetaBox(screen);
|
|
579
855
|
this.initStatusBox(screen);
|
|
580
856
|
// No "Generating Scene... Please wait." box anymore: every path now
|
|
581
857
|
// arrives with REAL prose (hand-authored static seeds, chunk-authored
|
|
@@ -626,6 +902,15 @@ export default class Game {
|
|
|
626
902
|
this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
|
|
627
903
|
// Home turf knows its own (see seedHomeTurfContacts).
|
|
628
904
|
this.seedHomeTurfContacts();
|
|
905
|
+
// The next run starts cooking the moment you're home (player
|
|
906
|
+
// request): generation takes 20-40s, and waiting for the player
|
|
907
|
+
// to CALL first meant "give me a bit" every time. Draft now --
|
|
908
|
+
// Mr. Johnson's "1 new message" ping lands mid-session, and the
|
|
909
|
+
// call finds work already waiting. The gate keeps this a no-op
|
|
910
|
+
// with AI off or a draft/pending seed somehow already in flight.
|
|
911
|
+
if (this.canDraftNextScene().allowed) {
|
|
912
|
+
this.draftNextSceneInBackground();
|
|
913
|
+
}
|
|
629
914
|
}
|
|
630
915
|
// Gear up automatically: wearables (and the commlink) slot into any
|
|
631
916
|
// EMPTY slots at load -- no "equip all" ritual to start a session
|
|
@@ -633,6 +918,10 @@ export default class Game {
|
|
|
633
918
|
// never swapped out.
|
|
634
919
|
equipIntoEmptySlots(this.player);
|
|
635
920
|
this.warmNpcAppearancesInBackground();
|
|
921
|
+
this.startPursuitHeartbeat();
|
|
922
|
+
// The banner learns where we are only after the hub/scene bookkeeping
|
|
923
|
+
// above settles.
|
|
924
|
+
this.refreshLogLabel();
|
|
636
925
|
this.initialized = true;
|
|
637
926
|
// Nudge the player toward "call" up front -- the fixer convention means
|
|
638
927
|
// the story deliberately doesn't narrate a completed briefing, so
|
|
@@ -640,7 +929,7 @@ export default class Game {
|
|
|
640
929
|
// you actually get your orders.
|
|
641
930
|
const wc = this.scene.getWinCondition();
|
|
642
931
|
const callHint = (wc?.type === 'give' && wc.toNpc)
|
|
643
|
-
? `\n\nYour commlink is quiet for now, but ${wc.toNpc} is expecting to hear from you -- try "call ${wc.toNpc}" when you're ready to check in.`
|
|
932
|
+
? hint(`\n\nYour commlink is quiet for now, but ${wc.toNpc} is expecting to hear from you -- try "call ${wc.toNpc}" when you're ready to check in.`)
|
|
644
933
|
: '';
|
|
645
934
|
return `Welcome, ${this.player.name}\n ${this.scene.story}${callHint}`;
|
|
646
935
|
}
|
|
@@ -731,6 +1020,8 @@ export default class Game {
|
|
|
731
1020
|
// Read the run's fate BEFORE the scene swap below discards it -- a
|
|
732
1021
|
// walked-away job gets a different homecoming line than a wrapped one.
|
|
733
1022
|
const wrappedRun = this.scene !== hub && this.scene.isCompleted();
|
|
1023
|
+
// Companions fold for the ride home, same as the trip out.
|
|
1024
|
+
const companionFoldLines = this.foldCompanionsForTravel();
|
|
734
1025
|
// Settle cross-plane and combat state that must not leak into the hub
|
|
735
1026
|
// (the run's rooms -- including a bodyRoom the player left behind --
|
|
736
1027
|
// are about to be orphaned wholesale).
|
|
@@ -774,10 +1065,10 @@ export default class Game {
|
|
|
774
1065
|
this.lifestyleTier = LIFESTYLE_TIERS[tierIndex - 1].name;
|
|
775
1066
|
// The downgrade is visible the moment you look around.
|
|
776
1067
|
this.syncHomeToLifestyle();
|
|
777
|
-
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
|
|
1068
|
+
rentLine = `The landlord's drone pings before the door even shuts: ${paid > 0 ? `${paid} paid, ` : ''}${this.rentDebt} nuyen still owed. Your ${tier.name} arrangement lapses -- you're living ${this.lifestyleTier} now.${hint(` ("pay" the tab, "lifestyle" to climb back.)`)}`;
|
|
778
1069
|
}
|
|
779
1070
|
else {
|
|
780
|
-
rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab
|
|
1071
|
+
rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab.${hint(` ("pay" when you're flush.)`)}`;
|
|
781
1072
|
}
|
|
782
1073
|
}
|
|
783
1074
|
else {
|
|
@@ -800,11 +1091,15 @@ export default class Game {
|
|
|
800
1091
|
this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
|
|
801
1092
|
this.updateExits(this.player.currentLocation);
|
|
802
1093
|
this.updateStatus();
|
|
1094
|
+
this.refreshLogLabel();
|
|
803
1095
|
Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${paid} paid, ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
|
|
1096
|
+
// Drones spin back up beside the cot.
|
|
1097
|
+
const companionArrivalLines = this.unfoldCompanionsOnArrival();
|
|
1098
|
+
const companionBlock = [...companionFoldLines, ...companionArrivalLines];
|
|
804
1099
|
const fateLine = wrappedRun
|
|
805
1100
|
? `The run's wrapped and the street swallows you back up.`
|
|
806
1101
|
: `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
|
|
807
|
-
return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}
|
|
1102
|
+
return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}\n${rentLine}${comfortLine}${restockLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
808
1103
|
}
|
|
809
1104
|
/**
|
|
810
1105
|
* The actual drafting work, split out from continueToNextScene() purely
|
|
@@ -849,7 +1144,7 @@ export default class Game {
|
|
|
849
1144
|
this._pendingRunSeed = nextSeed;
|
|
850
1145
|
this._pendingRunTier = nextTier;
|
|
851
1146
|
this._pendingRide = Game.RIDE_POOL[Math.floor(Math.random() * Game.RIDE_POOL.length)];
|
|
852
|
-
return `Your link pings -- 1 new message: ${FIXER_NAME}
|
|
1147
|
+
return `Your link pings -- 1 new message: ${FIXER_NAME}.${hint(` ("call ${FIXER_NAME}" to hear the job.)`)}`;
|
|
853
1148
|
}
|
|
854
1149
|
// Classic sessions transition immediately, hideout-first, as always.
|
|
855
1150
|
return await this.activateRunScene(nextSeed, nextTier, { startInHideout: true });
|
|
@@ -868,6 +1163,9 @@ export default class Game {
|
|
|
868
1163
|
this.player.activeCallPartner.activeCallPartner = undefined;
|
|
869
1164
|
this.player.activeCallPartner = undefined;
|
|
870
1165
|
}
|
|
1166
|
+
// Companions fold for the ride (spirits depart, drones to the pack,
|
|
1167
|
+
// agents into the deck) -- their lines join the JOB banner below.
|
|
1168
|
+
const companionLines = this.foldCompanionsForTravel();
|
|
871
1169
|
// false, false: no starter kit (the player carries their own gear
|
|
872
1170
|
// forward), and no blocking AI-redressing pass either -- the chunked
|
|
873
1171
|
// rooms pass already wrote this scene's prose.
|
|
@@ -896,8 +1194,15 @@ export default class Game {
|
|
|
896
1194
|
this.updateStatus();
|
|
897
1195
|
// Fresh streets, no trail yet (see Player.cameFrom / the HUD crumb).
|
|
898
1196
|
this.player.cameFrom = undefined;
|
|
1197
|
+
// Deployed drones spin back up at the curb -- BEFORE appearance
|
|
1198
|
+
// warming so re-shelled companions get warmed with the cast.
|
|
1199
|
+
const arrivalLines = this.unfoldCompanionsOnArrival();
|
|
899
1200
|
this.warmNpcAppearancesInBackground();
|
|
900
|
-
|
|
1201
|
+
this.refreshLogLabel();
|
|
1202
|
+
// No "=== JOB #n ===" banner on entry (player request) -- the Game
|
|
1203
|
+
// Log window's own title carries the job name now.
|
|
1204
|
+
const companionBlock = [...companionLines, ...arrivalLines];
|
|
1205
|
+
return `\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
901
1206
|
}
|
|
902
1207
|
/**
|
|
903
1208
|
* The needs check at every job departure (SR5e p.56 / p.172, scaled to
|
|
@@ -920,11 +1225,15 @@ export default class Game {
|
|
|
920
1225
|
p.sleepStreak = slept ? 0 : p.sleepStreak + 1;
|
|
921
1226
|
p.refreshEdge(fed && slept);
|
|
922
1227
|
if (fed && slept) {
|
|
923
|
-
|
|
1228
|
+
// Good-news status with a rules lesson inside -- hint-gated
|
|
1229
|
+
// (player request); the HUD's edge count carries the number anyway.
|
|
1230
|
+
const note = hint(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.edge + p.lifestyleEdgeBonus}.)`);
|
|
1231
|
+
if (note)
|
|
1232
|
+
lines.push(note);
|
|
924
1233
|
}
|
|
925
1234
|
else {
|
|
926
1235
|
const missing = !fed && !slept ? 'meal and no sleep' : !fed ? 'meal' : 'sleep';
|
|
927
|
-
lines.push(`No ${missing} since the last job -- your edge doesn't come back (${p.edgeRemaining} left)
|
|
1236
|
+
lines.push(`No ${missing} since the last job -- your edge doesn't come back (${p.edgeRemaining} left).${hint(` A hot meal and a night in your own bed reset it.`)}`);
|
|
928
1237
|
}
|
|
929
1238
|
// Escalating, resisted, LOCKED fatigue per ignored need.
|
|
930
1239
|
const events = [];
|
|
@@ -991,6 +1300,7 @@ export default class Game {
|
|
|
991
1300
|
}
|
|
992
1301
|
}
|
|
993
1302
|
async close() {
|
|
1303
|
+
this.stopPursuitHeartbeat();
|
|
994
1304
|
this._inputBox.removeAllListeners(); // removes all event listeners
|
|
995
1305
|
this._inputBox.cancel(); // cancels any active input session
|
|
996
1306
|
this._inputBox.detach(); // optionally remove from screen
|
|
@@ -1021,8 +1331,18 @@ export default class Game {
|
|
|
1021
1331
|
});
|
|
1022
1332
|
this._inputBox = inputBox;
|
|
1023
1333
|
screen.append(inputBox);
|
|
1024
|
-
// Key bindings
|
|
1025
|
-
|
|
1334
|
+
// Key bindings. ESC/Ctrl-C take a SECOND press within 3s to quit
|
|
1335
|
+
// (player request: "I just exited by mistake") -- the first press
|
|
1336
|
+
// warns; typing anything disarms nothing (the window just expires).
|
|
1337
|
+
screen.key(['escape', 'C-c'], () => {
|
|
1338
|
+
const now = Date.now();
|
|
1339
|
+
if (now - this._escArmedAt < 3000) {
|
|
1340
|
+
process.exit(0);
|
|
1341
|
+
}
|
|
1342
|
+
this._escArmedAt = now;
|
|
1343
|
+
Logger.getInstance().logWithColor(`Press again within 3s to quit. (Or "quit" + "y".)`, 'yellow');
|
|
1344
|
+
this._screen?.render();
|
|
1345
|
+
});
|
|
1026
1346
|
// Focus on the input box by default -- but ONLY if it isn't already
|
|
1027
1347
|
// focused. screen.append() auto-focuses the first keyable element
|
|
1028
1348
|
// (Node.insert), and since the sidebar boxes were removed this textbox
|
|
@@ -1047,13 +1367,17 @@ export default class Game {
|
|
|
1047
1367
|
id: 'logBox',
|
|
1048
1368
|
top: '0%',
|
|
1049
1369
|
left: '0%',
|
|
1050
|
-
|
|
1370
|
+
// COLUMNS, not rows (playtest note: a 3-row Mechanics strip made
|
|
1371
|
+
// combat rolls "near impossible to read" -- they scrolled out as
|
|
1372
|
+
// fast as they landed). The world log takes the left ~two-thirds;
|
|
1373
|
+
// the Mechanics column stands full-height beside it.
|
|
1374
|
+
width: '65%',
|
|
1051
1375
|
// Fixed-row footer: 8 rows of HUD (status box) + 3 rows of input.
|
|
1052
1376
|
height: '100%-11',
|
|
1053
1377
|
border: {
|
|
1054
1378
|
type: 'line'
|
|
1055
1379
|
},
|
|
1056
|
-
label:
|
|
1380
|
+
label: this.buildLogLabel(false),
|
|
1057
1381
|
scrollable: true, // Enable scrolling
|
|
1058
1382
|
alwaysScroll: true,
|
|
1059
1383
|
scrollbar: {
|
|
@@ -1073,8 +1397,113 @@ export default class Game {
|
|
|
1073
1397
|
tags: true // Intercept tags
|
|
1074
1398
|
});
|
|
1075
1399
|
this._logBox = logBox;
|
|
1400
|
+
// Scrolling without focus (player report: "can't scroll the game log
|
|
1401
|
+
// anymore"): focus lives in the input box on every path, so the
|
|
1402
|
+
// box's own keys never fire, and the wheel died with mouse tracking.
|
|
1403
|
+
// SCREEN-level PgUp/PgDn scroll the log while you keep typing; while
|
|
1404
|
+
// you're up in history, followBottom stops every new line from
|
|
1405
|
+
// yanking the view back down (Logger.log and ui.ts honor it).
|
|
1406
|
+
// Paging back to the bottom -- or End -- re-engages the follow.
|
|
1407
|
+
logBox.followBottom = true;
|
|
1408
|
+
// CRITICAL: the input box is ALWAYS reading (focus never leaves it),
|
|
1409
|
+
// and blessed's textarea.readInput sets screen.grabKeys = true --
|
|
1410
|
+
// which mutes every screen-level key handler EXCEPT keys listed in
|
|
1411
|
+
// screen.ignoreLocked (screen.js:591). Without this, the bindings
|
|
1412
|
+
// below simply never fire (a real session: "PgUp/Dn don't work").
|
|
1413
|
+
// The textbox itself ignores these keys, so nothing is stolen.
|
|
1414
|
+
screen.ignoreLocked.push('pageup', 'pagedown', 'end');
|
|
1415
|
+
const page = () => Math.max(1, (typeof logBox.height === 'number' ? logBox.height : 20) - 3);
|
|
1416
|
+
const setFollow = (on) => {
|
|
1417
|
+
logBox.followBottom = on;
|
|
1418
|
+
logBox.setLabel(this.buildLogLabel(!on));
|
|
1419
|
+
};
|
|
1420
|
+
const atBottom = () => logBox.getScrollPerc() >= 100 || logBox.getScrollHeight() <= page() + 3;
|
|
1421
|
+
screen.key(['pageup'], () => {
|
|
1422
|
+
logBox.scroll(-page());
|
|
1423
|
+
setFollow(false);
|
|
1424
|
+
screen.render();
|
|
1425
|
+
});
|
|
1426
|
+
screen.key(['pagedown'], () => {
|
|
1427
|
+
logBox.scroll(page());
|
|
1428
|
+
if (atBottom())
|
|
1429
|
+
setFollow(true);
|
|
1430
|
+
screen.render();
|
|
1431
|
+
});
|
|
1432
|
+
screen.key(['end'], () => {
|
|
1433
|
+
logBox.scrollTo(logBox.getScrollHeight());
|
|
1434
|
+
setFollow(true);
|
|
1435
|
+
screen.render();
|
|
1436
|
+
});
|
|
1076
1437
|
screen.append(logBox);
|
|
1077
1438
|
}
|
|
1439
|
+
/**
|
|
1440
|
+
* The Game Log banner names WHERE you are in the campaign (player
|
|
1441
|
+
* request): the hub by name at home, "JOB #n: <name>" on a run --
|
|
1442
|
+
* plus the scroll state the label already carried.
|
|
1443
|
+
*/
|
|
1444
|
+
buildLogLabel(scrolled) {
|
|
1445
|
+
const onHub = this._hubScene !== undefined && this.scene === this._hubScene;
|
|
1446
|
+
const name = onHub ? this._hubName : this.sceneSeed?.name;
|
|
1447
|
+
const where = name ? ` -- ${name}` : '';
|
|
1448
|
+
// The key coaching is hint-gated (player request); the SCROLLED
|
|
1449
|
+
// state itself stays -- it's status, not teaching.
|
|
1450
|
+
return scrolled
|
|
1451
|
+
? ` Game Log${where} -- SCROLLED${hint(' (End to follow)')} `
|
|
1452
|
+
: ` Game Log${where}${hint(' (PgUp/PgDn to scroll)')} `;
|
|
1453
|
+
}
|
|
1454
|
+
/** Re-stamps the banner after a scene change (init, run start, home). */
|
|
1455
|
+
refreshLogLabel() {
|
|
1456
|
+
const box = this._logBox;
|
|
1457
|
+
if (!box)
|
|
1458
|
+
return;
|
|
1459
|
+
box.setLabel(this.buildLogLabel(box.followBottom === false));
|
|
1460
|
+
this._screen?.render();
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Scroll the log to its tail -- unless the player has paged up to read
|
|
1464
|
+
* history (followBottom off), in which case their place is left alone.
|
|
1465
|
+
* Every "new line arrived" site goes through here.
|
|
1466
|
+
*/
|
|
1467
|
+
followLog() {
|
|
1468
|
+
const box = this._logBox;
|
|
1469
|
+
if (!box || box.followBottom === false)
|
|
1470
|
+
return;
|
|
1471
|
+
box.scrollTo(box.getScrollHeight());
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* The MECHANICS column (readability pass, player request: the log was
|
|
1475
|
+
* a "wall of text"): dice anatomy and resource arithmetic land here
|
|
1476
|
+
* (via Logger.meta) instead of the world log, so the narrative stays
|
|
1477
|
+
* prose. A full-height COLUMN beside the world log, not a bottom
|
|
1478
|
+
* strip -- the first playtest's 3-row ticker scrolled combat rolls
|
|
1479
|
+
* away faster than they could be read. De-emphasis is the BLUE border
|
|
1480
|
+
* -- gray is banned (ANSI bright-black, illegible on stock dark
|
|
1481
|
+
* palettes). Follow-only (no keys); the full interleaved record lives
|
|
1482
|
+
* in session.log. Roll lines wrap at the column width -- fine in a
|
|
1483
|
+
* tall column. NO mouse (see initInputBox): never arm mouse tracking.
|
|
1484
|
+
*/
|
|
1485
|
+
initMetaBox(screen) {
|
|
1486
|
+
const metaBox = blessed.box({
|
|
1487
|
+
id: 'metaBox',
|
|
1488
|
+
top: '0%',
|
|
1489
|
+
left: '65%',
|
|
1490
|
+
width: '35%',
|
|
1491
|
+
// Same height as the world log: HUD (8) + input (3) below both.
|
|
1492
|
+
height: '100%-11',
|
|
1493
|
+
border: {
|
|
1494
|
+
type: 'line'
|
|
1495
|
+
},
|
|
1496
|
+
style: {
|
|
1497
|
+
border: { fg: 'blue' }
|
|
1498
|
+
},
|
|
1499
|
+
label: ' Mechanics ',
|
|
1500
|
+
scrollable: true,
|
|
1501
|
+
alwaysScroll: true,
|
|
1502
|
+
tags: true,
|
|
1503
|
+
});
|
|
1504
|
+
this._metaBox = metaBox;
|
|
1505
|
+
screen.append(metaBox);
|
|
1506
|
+
}
|
|
1078
1507
|
/**
|
|
1079
1508
|
* The HUD: a fixed strip between the log and the input box for the
|
|
1080
1509
|
* important real-time state -- condition tracks, armed/link status,
|
|
@@ -1130,7 +1559,10 @@ export default class Game {
|
|
|
1130
1559
|
// a real question.
|
|
1131
1560
|
const crumb = player.cameFrom ? `${player.cameFrom.room} (${player.cameFrom.back}) > ` : '';
|
|
1132
1561
|
if (player.currentLocation) {
|
|
1133
|
-
|
|
1562
|
+
// "At" spots: where you stand WITHIN the room, when it has places
|
|
1563
|
+
// to stand (see utilities/spots.ts).
|
|
1564
|
+
const atNote = spotOf(player) ? ` @ ${player.atSpot}` : '';
|
|
1565
|
+
lines.push(`${crumb}{bold}📍 ${player.currentLocation.name}${atNote}{/bold} | ${stance}`);
|
|
1134
1566
|
}
|
|
1135
1567
|
else {
|
|
1136
1568
|
lines.push(stance);
|
|
@@ -1191,6 +1623,30 @@ export default class Game {
|
|
|
1191
1623
|
if (player.activeCallPartner) {
|
|
1192
1624
|
lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} On a call with ${player.activeCallPartner.name}{/${CALL_COLOR}-fg}`);
|
|
1193
1625
|
}
|
|
1626
|
+
// The party: one line per live companion shell, with the tracks that
|
|
1627
|
+
// matter for its kind. A spirit shows its BODY too (it's a full
|
|
1628
|
+
// Force-statted combatant and hits land on it -- player request:
|
|
1629
|
+
// the track was real but invisible); drones show hull, agents deck.
|
|
1630
|
+
for (const c of this.companions) {
|
|
1631
|
+
const track = c.kind === 'spirit'
|
|
1632
|
+
? `${c.services ?? 0} service${(c.services ?? 0) === 1 ? '' : 's'} ${c.npc.conditionSummary()}${c.npc.stunTaken > 0 ? ` stun ${c.npc.stunSummary()}` : ''}`
|
|
1633
|
+
: c.kind === 'drone' && c.boundDevice ? `hull ${c.boundDevice.droneSummary()}`
|
|
1634
|
+
: c.kind === 'agent' && c.boundDevice ? `deck ${c.boundDevice.deckSummary()}`
|
|
1635
|
+
: c.npc.conditionSummary();
|
|
1636
|
+
// Each kind speaks its own language (a real rigger session read a
|
|
1637
|
+
// generic "(F3)" as spell Force on a Fly-Spy): drones fly on a
|
|
1638
|
+
// PILOT rating, agents run at a RATING, and a spirit's Force is
|
|
1639
|
+
// already in its name.
|
|
1640
|
+
const grade = c.kind === 'drone' ? `Pilot ${c.force} -- ` : c.kind === 'agent' ? `Rating ${c.force} -- ` : '';
|
|
1641
|
+
lines.push(`{green-fg}👥 ${c.npc.name} -- ${grade}${track}{/green-fg}`);
|
|
1642
|
+
}
|
|
1643
|
+
// DECK-AR INDICATOR (player request): "Link online" is the
|
|
1644
|
+
// COMMLINK's AR overlay; this is the DECK's -- lit when an agent
|
|
1645
|
+
// works the grid while you're back in the meat (the AR-drop exit).
|
|
1646
|
+
const remoteAgent = this.companions.find(c => c.kind === 'agent');
|
|
1647
|
+
if (remoteAgent && player.plane !== 'matrix') {
|
|
1648
|
+
lines.push(`{cyan-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''}${hint(' ("jack in" to rejoin it, "recall" to fold it home)')}{/cyan-fg}`);
|
|
1649
|
+
}
|
|
1194
1650
|
// Off-plane context: which reality you're in, your persona's Matrix
|
|
1195
1651
|
// track (cold-sim damage lands there), and where your empty body lies.
|
|
1196
1652
|
if (player.plane === 'matrix') {
|
|
@@ -1248,8 +1704,25 @@ export default class Game {
|
|
|
1248
1704
|
const parts = normalized.trim().split(' ');
|
|
1249
1705
|
const verb = parts[0].toLowerCase();
|
|
1250
1706
|
const args = parts.slice(1);
|
|
1251
|
-
|
|
1252
|
-
|
|
1707
|
+
// Quitting takes a CONFIRM (player request: "I just exited by
|
|
1708
|
+
// mistake"): the first "quit" arms it, "y"/"yes" (or quit again)
|
|
1709
|
+
// seals it, anything else disarms and runs as the command it is.
|
|
1710
|
+
if (this._quitArmed) {
|
|
1711
|
+
this._quitArmed = false;
|
|
1712
|
+
if (/^(y|yes|quit|exit)$/i.test(verb)) {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
// fall through -- the answer was a real command; play on.
|
|
1716
|
+
}
|
|
1717
|
+
else if (verb === 'quit' || verb === 'exit') {
|
|
1718
|
+
this._quitArmed = true;
|
|
1719
|
+
return `Really quit? ("y" to confirm -- anything else keeps playing.)`;
|
|
1720
|
+
}
|
|
1721
|
+
// Out cold with the fight settling around you (see
|
|
1722
|
+
// Scene.beginPlayerKnockout): unconsciousness is not a prompt. Only
|
|
1723
|
+
// quit (handled above) still answers.
|
|
1724
|
+
if (this.player.isUnconscious() && this.scene.playerKnockout) {
|
|
1725
|
+
return `You're out cold -- the world moves without you.`;
|
|
1253
1726
|
}
|
|
1254
1727
|
if (verb === 'maka') {
|
|
1255
1728
|
return 'You are still in the game, type "quit" to exit';
|
|
@@ -1298,6 +1771,11 @@ export default class Game {
|
|
|
1298
1771
|
return `I don't understand "${verb}" -- did you mean "${guess}"?`;
|
|
1299
1772
|
}
|
|
1300
1773
|
const corrected = await this.handleInput([guess, ...args].join(' '));
|
|
1774
|
+
// The correction note stays INLINE and always-on (player ruling:
|
|
1775
|
+
// it briefly moved to the Mechanics ticker, and a conversational
|
|
1776
|
+
// "well I'm here..." silently became a SELL with nothing in the
|
|
1777
|
+
// world log explaining the refusal that came back). It's not dice
|
|
1778
|
+
// noise -- it's the only receipt for a command you didn't type.
|
|
1301
1779
|
return corrected === null ? null : `(read "${verb}" as "${guess}")\n${corrected ?? ''}`;
|
|
1302
1780
|
}
|
|
1303
1781
|
return `I don't understand the command "${verb}".`;
|
|
@@ -1312,16 +1790,16 @@ export default class Game {
|
|
|
1312
1790
|
* future command can't silently fall out of help.
|
|
1313
1791
|
*/
|
|
1314
1792
|
static HELP_CATEGORIES = [
|
|
1315
|
-
{ title: 'moving', entries: [['look'], ['go'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
1793
|
+
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
1316
1794
|
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
|
|
1317
|
-
{ title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['stance'], ['surrender'], ['edge'], ['rest']] },
|
|
1318
|
-
{ title: 'magic', entries: [['spells'], ['cast'], ['project'], ['return'], ['assense']] },
|
|
1319
|
-
{ title: 'matrix', entries: [['jack'], ['hack'], ['pan'], ['silent'], ['reboot'], ['drone'], ['jump', 'rig']] },
|
|
1795
|
+
{ title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['order'], ['stance'], ['surrender'], ['edge'], ['rest']] },
|
|
1796
|
+
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
|
|
1797
|
+
{ title: 'matrix', entries: [['jack'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['deploy'], ['recall'], ['jump', 'rig']] },
|
|
1320
1798
|
{ title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
1321
1799
|
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['work', 'gig'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
1322
1800
|
{ title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
|
|
1323
1801
|
{ title: 'character', entries: [['sheet', 'stats'], ['advance', 'train'], ['qualities', 'quality'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
1324
|
-
{ title: 'session', entries: [['help'], ['clear'], ['quit', 'exit']] },
|
|
1802
|
+
{ title: 'session', entries: [['help'], ['hints'], ['clear'], ['quit', 'exit']] },
|
|
1325
1803
|
];
|
|
1326
1804
|
/** Descriptions for the verbs handled inline in handleInput() rather than registered. */
|
|
1327
1805
|
static BUILTIN_DESCRIPTIONS = {
|
|
@@ -1349,7 +1827,7 @@ export default class Game {
|
|
|
1349
1827
|
const category = Game.HELP_CATEGORIES.find(c => c.title === pickedCategory);
|
|
1350
1828
|
const lines = [`=== ${category.title.toUpperCase()} ===`];
|
|
1351
1829
|
for (const [primary, ...aliases] of category.entries) {
|
|
1352
|
-
lines.push(
|
|
1830
|
+
lines.push(`${BULLET}${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
|
|
1353
1831
|
}
|
|
1354
1832
|
return lines.join('\n');
|
|
1355
1833
|
}
|