@maka/maka-cli 5.13.0 → 5.14.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 +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +5 -5
- 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/call.js +48 -37
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js +25 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +6 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.d.ts +43 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +259 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +15 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/give.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +8 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.d.ts +5 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +45 -27
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js +12 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.d.ts +5 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js +31 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +21 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/palm.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/palm.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/perform.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/perform.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/reboot.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +21 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.d.ts +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +81 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/source.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/source.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +15 -2
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +70 -35
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +16 -5
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +36 -8
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +139 -4
- package/bundle/typescript/src/commands/game/sideQuest/game.js +591 -85
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/headless.d.ts +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +185 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +50 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +59 -11
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +242 -109
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +11 -11
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +22 -2
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +7 -5
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.d.ts +6 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +36 -24
- 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 +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +59 -18
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/command-queue.d.ts +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/command-queue.js +56 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/command-queue.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/crew-ledger.d.ts +21 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/crew-ledger.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/crew-ledger.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/emitter.d.ts +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/emitter.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/emitter.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.d.ts +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js +5 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/fuzzy-match.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.d.ts +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js +24 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hints.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.d.ts +6 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js +30 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-context.d.ts +28 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-context.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-context.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/social.d.ts +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/social.js +166 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/social.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
- package/bundle/typescript/src/commands/generate.js +7 -0
- package/bundle/typescript/src/commands/generate.js.map +1 -1
- package/bundle/typescript/src/tools/mpi/mpi.class.js +46 -3
- package/bundle/typescript/src/tools/mpi/mpi.class.js.map +1 -1
- package/bundle/typescript/src/tools/mpi/sdk-runtime.d.ts +1 -0
- package/bundle/typescript/src/tools/mpi/sdk-runtime.js +23 -0
- package/bundle/typescript/src/tools/mpi/sdk-runtime.js.map +1 -1
- package/bundle/typescript/src/tools/pkg/pkg.class.js +11 -1
- package/bundle/typescript/src/tools/pkg/pkg.class.js.map +1 -1
- package/bundle/typescript/src/tools/scaffold/scaffold.class.js +8 -1
- package/bundle/typescript/src/tools/scaffold/scaffold.class.js.map +1 -1
- package/package.json +2 -2
|
@@ -5,13 +5,17 @@ import { Item } from './models/item.js';
|
|
|
5
5
|
import { Room } from './models/_index.js';
|
|
6
6
|
import { Door } from './models/door.js';
|
|
7
7
|
import { SAVE_VERSION } from './types/save-file.js';
|
|
8
|
-
import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, restoreItem, readSave, } from './utilities/persistence.js';
|
|
8
|
+
import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, restoreItem, readSave, serializeItem, saveSlug, } from './utilities/persistence.js';
|
|
9
|
+
import { appendCrewLedger } from './utilities/crew-ledger.js';
|
|
10
|
+
import { heartbeatPresence, ghostHireable, syncCrewLedger, claimGhostEarnings } from './utilities/social.js';
|
|
9
11
|
import { pushSave, burnCloudSaveFor } from './utilities/cloud-saves.js';
|
|
10
12
|
import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
|
|
11
13
|
import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
|
|
12
14
|
import { rollPool, formatRoll } from './utilities/dice.js';
|
|
13
15
|
import { Direction } from './types/shared/direction-enum.js';
|
|
14
16
|
import { Logger } from './utilities/logger.js';
|
|
17
|
+
import { CommandQueue } from './utilities/command-queue.js';
|
|
18
|
+
import { currentSession } from './utilities/session-context.js';
|
|
15
19
|
import { LookCommand } from './commands/look.js';
|
|
16
20
|
import { TakeCommand } from './commands/take.js';
|
|
17
21
|
import { GoCommand } from './commands/go.js';
|
|
@@ -43,6 +47,7 @@ import { AcceptCommand, DeclineCommand } from './commands/job-offer.js';
|
|
|
43
47
|
import { SourceCommand } from './commands/source.js';
|
|
44
48
|
import { DocwagonCommand } from './commands/docwagon.js';
|
|
45
49
|
import { LeadCommand, RecruitCommand } from './commands/lead.js';
|
|
50
|
+
import { CrewCommand, HireCommand, DismissCommand, TrainCommand } from './commands/crew.js';
|
|
46
51
|
import { PalmCommand } from './commands/palm.js';
|
|
47
52
|
import { CatalogCommand } from './commands/catalog.js';
|
|
48
53
|
import { refreshCatalog } from './utilities/catalog.js';
|
|
@@ -87,7 +92,7 @@ import { TreatCommand } from './commands/treat.js';
|
|
|
87
92
|
import { WorkCommand } from './commands/work.js';
|
|
88
93
|
import { EatCommand } from './commands/eat.js';
|
|
89
94
|
import { DrinkCommand } from './commands/drink.js';
|
|
90
|
-
import { equipIntoEmptySlots } from './archetypes.js';
|
|
95
|
+
import { equipIntoEmptySlots, ARCHETYPES, createArchetypeGear } from './archetypes.js';
|
|
91
96
|
import { NPC } from './models/npc.js';
|
|
92
97
|
import { CommandRegistry } from './commands/command-registry.js';
|
|
93
98
|
import { HaggleCommand } from './commands/haggle.js';
|
|
@@ -97,7 +102,7 @@ import { BluffCommand } from './commands/bluff.js';
|
|
|
97
102
|
import { AutoFight } from './utilities/auto-fight.js';
|
|
98
103
|
import { CommandFactory } from './factories/command-factory.js';
|
|
99
104
|
import { SceneSynthesizer } from './factories/scene-factory.js';
|
|
100
|
-
import { SceneSeedGenerator, FIXER_NAME } from './factories/scene-seed-generator.js';
|
|
105
|
+
import { SceneSeedGenerator, FIXER_NAME, isFixerName } from './factories/scene-seed-generator.js';
|
|
101
106
|
import { CALL_ICON, CALL_COLOR } from './utilities/comm-style.js';
|
|
102
107
|
import { AI } from '../../../tools/ai/ai.class.js';
|
|
103
108
|
import { BULLET } from './utilities/log-style.js';
|
|
@@ -162,6 +167,9 @@ export default class Game {
|
|
|
162
167
|
// grow. "ask" (commands/ask.ts) is what the roster is FOR: legwork
|
|
163
168
|
// against the real pending-run seed. One pull per contact per
|
|
164
169
|
// homecoming (asked resets in returnToHub).
|
|
170
|
+
// `note` is the relationship's one-line memory -- a client's grudge
|
|
171
|
+
// ("walked out on their job") or a sloppy wrap-up -- rendered as a
|
|
172
|
+
// compact marker on the roster (contacts.ts) and carried by the save.
|
|
165
173
|
contacts = new Map();
|
|
166
174
|
// SWAG orders in flight (SR5e p.387/418, commands/source.ts): a
|
|
167
175
|
// contact shopping the black market for you. Paid up front on a won
|
|
@@ -172,33 +180,232 @@ export default class Game {
|
|
|
172
180
|
// DOCWAGON (SR5e p.450, utilities/docwagon.ts): the wristband between
|
|
173
181
|
// a runner and the morgue. Rides the save; ticks down at homecomings.
|
|
174
182
|
docwagon;
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
183
|
+
// ==================== THE CREW (the party system) ====================
|
|
184
|
+
// Persistent teammates who ride EVERY run (player vision: the hub
|
|
185
|
+
// stays solo housekeeping; the party is for the job). A slot holds an
|
|
186
|
+
// NPC hire, a deep-loyalty contact (Leadership's Command action,
|
|
187
|
+
// p.141-142), or -- Phase B -- another PLAYER's character riding as a
|
|
188
|
+
// ghost (source 'ghost', their cut credited home via the ledger).
|
|
189
|
+
// Live shells spawn per run (spawnCrew) and are disposable; the
|
|
190
|
+
// roster below is the persistent state and rides the save. Changing
|
|
191
|
+
// the crew SCRAPS any pending job (player ruling: the quote was for a
|
|
192
|
+
// different team -- Mr. Krow needs a fresh call).
|
|
193
|
+
crew = [];
|
|
194
|
+
// RAW's teeth on the Command recruit: a refusal stands until Loyalty
|
|
195
|
+
// deepens past the level it happened at. Rides the save.
|
|
181
196
|
commandRefusals = {};
|
|
197
|
+
// DEPRECATED one-slot predecessor -- read once by applyResumeState to
|
|
198
|
+
// migrate old saves into `crew`, never written again.
|
|
199
|
+
enlistedAlly;
|
|
200
|
+
/** Leadership fields the table: anyone runs with a partner, a leader
|
|
201
|
+
* runs a team. Ceiling 5 -- the size of the future multiplayer table. */
|
|
202
|
+
get crewCap() {
|
|
203
|
+
return Math.min(5, 1 + this.player.skillRating('leadership'));
|
|
204
|
+
}
|
|
205
|
+
/** The crew-change rule (player ruling): any roster change voids the
|
|
206
|
+
* standing quote. Returns the line to show, or ''. */
|
|
207
|
+
scrapPendingRunForCrewChange() {
|
|
208
|
+
if (!this._pendingRunSeed)
|
|
209
|
+
return '';
|
|
210
|
+
this.clearPendingRun();
|
|
211
|
+
return `\nThe standing job was quoted for a different crew -- ${FIXER_NAME} needs a fresh call.`;
|
|
212
|
+
}
|
|
182
213
|
// PERFORMANCE's busking gate (commands/perform.ts): one set per job
|
|
183
214
|
// cycle, reset at homecoming like a contact's legwork. Deliberately
|
|
184
215
|
// NOT saved -- a relaunch is as good as a homecoming for new material.
|
|
185
216
|
performedThisCycle = false;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
217
|
+
// THE FOR-HIRE BOARD: archetype-built candidates, fresh faces each
|
|
218
|
+
// homecoming (the gig-pool rotation school). Not saved -- the street's
|
|
219
|
+
// talent pool churns; whoever you HIRED is the persistent part.
|
|
220
|
+
crewForHire = [];
|
|
221
|
+
// PRESENCE (social layer, Phase B): a hub heartbeat announces this
|
|
222
|
+
// runner to the street's player list on maka-cli.com -- and carries
|
|
223
|
+
// the "hireable while away" ghost opt-in. Only beats while the scene
|
|
224
|
+
// IS the hub (the run is nobody's business); silent no-op logged out.
|
|
225
|
+
_presenceTimer;
|
|
226
|
+
static PRESENCE_TICK_MS = 60_000;
|
|
227
|
+
beatPresence() {
|
|
228
|
+
if (!this.hasHub || this.scene !== this._hubScene)
|
|
229
|
+
return;
|
|
230
|
+
void heartbeatPresence({
|
|
231
|
+
playerName: this.player.name,
|
|
232
|
+
slug: saveSlug(this.player.name),
|
|
233
|
+
hubName: this._hubName,
|
|
234
|
+
ghostHireable: ghostHireable(),
|
|
235
|
+
summary: {
|
|
236
|
+
archetypeName: this.player.archetypeName,
|
|
237
|
+
karma: this.player.careerKarma,
|
|
238
|
+
streetCred: this.player.streetCred,
|
|
239
|
+
},
|
|
200
240
|
});
|
|
201
|
-
|
|
241
|
+
}
|
|
242
|
+
startPresenceHeartbeat() {
|
|
243
|
+
if (this._presenceTimer)
|
|
244
|
+
return;
|
|
245
|
+
this.beatPresence();
|
|
246
|
+
this._presenceTimer = setInterval(() => this.beatPresence(), Game.PRESENCE_TICK_MS);
|
|
247
|
+
this._presenceTimer.unref();
|
|
248
|
+
}
|
|
249
|
+
/** Ghost earnings home delivery: what YOUR runner made riding in
|
|
250
|
+
* other players' crews since last time. Fire-and-forget from hub
|
|
251
|
+
* arrival beats; pays onto the job credstick when anything landed. */
|
|
252
|
+
async collectGhostEarnings() {
|
|
253
|
+
try {
|
|
254
|
+
const claimed = await claimGhostEarnings(saveSlug(this.player.name));
|
|
255
|
+
if (!claimed || this.scene !== this._hubScene)
|
|
256
|
+
return;
|
|
257
|
+
const paid = this.scene.payRunner(claimed.total, 'ghost earnings');
|
|
258
|
+
if (!paid)
|
|
259
|
+
return;
|
|
260
|
+
const jobs = claimed.entries.map(e => `"${e.job}" with ${e.hiredBy} (${e.amount}¥)`).join(', ');
|
|
261
|
+
Logger.getInstance().logWithColor(`While you were gone, your runner rode other crews: ${jobs}. ${paid.net}¥ certified lands on your stick.`, 'green');
|
|
262
|
+
this.updateStatus();
|
|
263
|
+
}
|
|
264
|
+
catch { /* offline -- next homecoming */ }
|
|
265
|
+
}
|
|
266
|
+
static HIRE_NAMES = [
|
|
267
|
+
'Jax', 'Sable', 'Vex', 'Rook', 'Nyx', 'Cinder', 'Halo', 'Grifter',
|
|
268
|
+
'Static', 'Mercy', 'Tinder', 'Chrome Marie', 'Deacon', 'Fetch', 'Loa',
|
|
269
|
+
];
|
|
270
|
+
/** Deal 2-3 fresh candidates. Names dodge the live cast and roster. */
|
|
271
|
+
rollHireCandidates() {
|
|
272
|
+
const taken = new Set([
|
|
273
|
+
...this.crew.map(m => m.name),
|
|
274
|
+
...(this._hubScene?.allActors ?? []).map(a => a.name),
|
|
275
|
+
]);
|
|
276
|
+
const names = Game.HIRE_NAMES.filter(n => !taken.has(n)).sort(() => Math.random() - 0.5);
|
|
277
|
+
const count = 2 + Math.floor(Math.random() * 2);
|
|
278
|
+
this.crewForHire = [];
|
|
279
|
+
for (let i = 0; i < count && i < names.length; i++) {
|
|
280
|
+
const archetype = ARCHETYPES[Math.floor(Math.random() * ARCHETYPES.length)];
|
|
281
|
+
// Asking cut scales with muscle: sum of the offensive statline.
|
|
282
|
+
const c = archetype.combat;
|
|
283
|
+
const power = (c.body ?? 3) + (c.agility ?? 3) + (c.logic ?? 3) + (c.magic ?? 0);
|
|
284
|
+
const asking = Math.min(30, 12 + Math.floor(power / 2) + Math.floor(Math.random() * 5));
|
|
285
|
+
this.crewForHire.push({
|
|
286
|
+
name: names[i],
|
|
287
|
+
source: 'hired',
|
|
288
|
+
archetypeName: archetype.name,
|
|
289
|
+
description: `${names[i]}, a ${archetype.name.toLowerCase()} for hire off the Purple Haze boards. ${archetype.description}`,
|
|
290
|
+
combat: { ...archetype.combat },
|
|
291
|
+
gear: createArchetypeGear(archetype).map(item => serializeItem(item)),
|
|
292
|
+
cutPct: asking,
|
|
293
|
+
jobsRun: 0,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** The crew joins at the jobsite curb ("took their own ride" -- the
|
|
298
|
+
* companion convention): one 'ally' shell per member, carrying and
|
|
299
|
+
* wearing their own gear. */
|
|
300
|
+
spawnCrew() {
|
|
301
|
+
const lines = [];
|
|
302
|
+
for (const member of this.crew) {
|
|
303
|
+
const shell = this.spawnCompanionShell({
|
|
304
|
+
name: member.name,
|
|
305
|
+
description: `${member.description} Right now they run as ${this.player.name}'s crew: they fight beside them, follow their lead, and expect their ${member.cutPct}% of the take.`,
|
|
306
|
+
kind: 'ally',
|
|
307
|
+
combat: member.combat,
|
|
308
|
+
room: this.player.currentLocation,
|
|
309
|
+
plane: 'meat',
|
|
310
|
+
force: Number(member.combat.body ?? 3),
|
|
311
|
+
});
|
|
312
|
+
if (!shell)
|
|
313
|
+
continue;
|
|
314
|
+
// Their kit comes with them -- weapons/armor worn, the rest carried
|
|
315
|
+
// (the scene-factory auto-equip convention).
|
|
316
|
+
for (const g of member.gear) {
|
|
317
|
+
try {
|
|
318
|
+
const item = restoreItem(g);
|
|
319
|
+
// Carried gear has no "at" spot -- a stale one from the room
|
|
320
|
+
// it was once shelved in haunts search hints scenes later.
|
|
321
|
+
item.atSpot = undefined;
|
|
322
|
+
shell.addInventory(item);
|
|
323
|
+
// Category enum VALUES are lowercase ('ranged weapon') --
|
|
324
|
+
// match case-insensitively or the crew arrives unarmed.
|
|
325
|
+
if (/weapon|armor|helmet|shield|clothing|boots|gloves/i.test(String(item.category))) {
|
|
326
|
+
shell.equip(item); // refusals return strings; extras stay carried
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
Logger.getInstance().write(`spawnCrew: could not restore "${g.name}" for ${member.name} (carry cap?).`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
lines.push(`${member.name} is already leaning on the wall when you arrive -- "Took my own ride. Let's work."`);
|
|
334
|
+
}
|
|
335
|
+
return lines;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Homecoming settlement for the crew (returnToHub, BEFORE the fold --
|
|
339
|
+
* dismissCompanion sends shell cargo to the PLAYER, and crew gear is
|
|
340
|
+
* THEIRS): write each survivor's inventory+equipment back into the
|
|
341
|
+
* roster (loot they picked up stays with them), heal them (the cot
|
|
342
|
+
* convention), and bury the fallen -- crew death is PERMANENT, like
|
|
343
|
+
* everything else on this street. Returns homecoming lines.
|
|
344
|
+
*/
|
|
345
|
+
settleCrewAfterRun() {
|
|
346
|
+
if (this.crew.length === 0)
|
|
347
|
+
return [];
|
|
348
|
+
const lines = [];
|
|
349
|
+
const fallen = [];
|
|
350
|
+
for (const member of this.crew) {
|
|
351
|
+
const entry = this.companions.find(c => c.kind === 'ally' && c.npc.name === member.name);
|
|
352
|
+
if (!entry || entry.npc.deathClaimed) {
|
|
353
|
+
fallen.push(member.name);
|
|
354
|
+
lines.push(`${member.name} didn't make it home. The crew pours one out; the street keeps the rest.`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
// Out cold is NOT dead (a real session's KO'd Jax -- "breathing"
|
|
358
|
+
// -- got an obituary and fell off the roster): the crew drags
|
|
359
|
+
// their own to the cab. Gear they dropped in the scene stays
|
|
360
|
+
// dropped unless somebody picked it up.
|
|
361
|
+
if (entry.npc.isDown() || entry.npc.isUnconscious()) {
|
|
362
|
+
lines.push(`${member.name} gets dragged to the cab, breathing. They'll wake up mean, but they'll wake up.`);
|
|
363
|
+
}
|
|
364
|
+
// Gear write-back: everything carried or worn rides the roster.
|
|
365
|
+
const shell = entry.npc;
|
|
366
|
+
const kit = shell.inventory.getAllItems().map(i => serializeItem(i));
|
|
367
|
+
for (const group of Object.values(shell.equipment)) {
|
|
368
|
+
if (!group)
|
|
369
|
+
continue;
|
|
370
|
+
for (const item of Object.values(group)) {
|
|
371
|
+
if (item)
|
|
372
|
+
kit.push(serializeItem(item));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
member.gear = kit;
|
|
376
|
+
// Empty the shell so the fold's cargo-transfer moves nothing.
|
|
377
|
+
for (const i of [...shell.inventory.getAllItems()])
|
|
378
|
+
shell.inventory.removeItem(i.name);
|
|
379
|
+
}
|
|
380
|
+
for (const name of fallen) {
|
|
381
|
+
this.crew = this.crew.filter(m => m.name !== name);
|
|
382
|
+
}
|
|
383
|
+
return lines;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The team's split, taken at every pay moment (canon: the crew shares
|
|
387
|
+
* the take; in multiplayer each player literally receives their
|
|
388
|
+
* share). Total cut capped at 60% -- the wheelman never out-earns the
|
|
389
|
+
* one holding the job. Ghost members' shares land on the local ledger
|
|
390
|
+
* for Phase B to sync home. Returns the player's share + the itemized
|
|
391
|
+
* line (empty crew: full amount, no line).
|
|
392
|
+
*/
|
|
393
|
+
applyCrewCut(gross) {
|
|
394
|
+
if (this.crew.length === 0 || gross <= 0)
|
|
395
|
+
return { share: gross, line: '' };
|
|
396
|
+
const totalPct = Math.min(60, this.crew.reduce((s, m) => s + m.cutPct, 0));
|
|
397
|
+
const scale = totalPct / this.crew.reduce((s, m) => s + m.cutPct, 0);
|
|
398
|
+
const parts = [];
|
|
399
|
+
let taken = 0;
|
|
400
|
+
for (const member of this.crew) {
|
|
401
|
+
const cut = Math.floor(gross * (member.cutPct * scale) / 100);
|
|
402
|
+
taken += cut;
|
|
403
|
+
parts.push(`${member.name} ${member.cutPct}% (${cut}¥)`);
|
|
404
|
+
if (member.ghost) {
|
|
405
|
+
appendCrewLedger({ slug: member.ghost.slug, owner: member.ghost.owner, amount: cut, job: this.currentJobName ?? 'a job', when: new Date().toISOString() });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { share: gross - taken, line: `Crew cut: ${parts.join(', ')}.` };
|
|
202
409
|
}
|
|
203
410
|
/** Bills a DocWagon fee: cred first, shortfall onto the rent tab (the
|
|
204
411
|
* street's one universal collections agency). Returns the line. */
|
|
@@ -314,14 +521,15 @@ export default class Game {
|
|
|
314
521
|
*/
|
|
315
522
|
static roleOf(npc) {
|
|
316
523
|
const hay = `${npc.description ?? ''} ${npc.name}`.toLowerCase();
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (/fixer/.test(hay))
|
|
524
|
+
// Canon roles (p.390-392, player ruling): Mr. Krow is the FIXER --
|
|
525
|
+
// the broker who sets up the meets. A JOHNSON is a per-run client's
|
|
526
|
+
// deniable face; those join the roster from finished runs
|
|
527
|
+
// (returnToHub's client upsert), tagged by the /johnson|client rep/
|
|
528
|
+
// text their generated descriptions carry.
|
|
529
|
+
if (isFixerName(npc.name) || /fixer|job broker/.test(hay))
|
|
324
530
|
return 'Fixer';
|
|
531
|
+
if (/johnson|client rep|deniable/.test(hay))
|
|
532
|
+
return 'Johnson';
|
|
325
533
|
// "charms" deliberately absent: a market kid hawking LUCKY charms is
|
|
326
534
|
// a Vendor, not a talismonger (Skitter proved it).
|
|
327
535
|
if (/talismonger|magical goods|foci|spell formula/.test(hay))
|
|
@@ -350,7 +558,7 @@ export default class Game {
|
|
|
350
558
|
}
|
|
351
559
|
static connectionFor(npc) {
|
|
352
560
|
const hay = `${npc.description ?? ''} ${npc.name}`.toLowerCase();
|
|
353
|
-
if (npc.name
|
|
561
|
+
if (isFixerName(npc.name) || /fixer|johnson/.test(hay))
|
|
354
562
|
return 5;
|
|
355
563
|
if (/fence|pawn|launder|smuggl|no questions/.test(hay))
|
|
356
564
|
return 4;
|
|
@@ -488,6 +696,19 @@ export default class Game {
|
|
|
488
696
|
* damage), a spirit is banished outright.
|
|
489
697
|
*/
|
|
490
698
|
onActorRemoved(name) {
|
|
699
|
+
// KILLING YOUR OWN JOHNSON (player ruling: it must FAIL the job; a
|
|
700
|
+
// real session shot Ms. Veil over a maglock code and the run kept
|
|
701
|
+
// pretending she'd pay). The paymaster's death zeroes the payout,
|
|
702
|
+
// charges the p.368 notoriety, and marks the client dead so the
|
|
703
|
+
// homecoming files a corpse, not a contact.
|
|
704
|
+
if (this.currentClient && !this.currentClient.dead && name === this.currentClient.name) {
|
|
705
|
+
this.currentClient.dead = true;
|
|
706
|
+
const wc = this.scene?.getWinCondition?.();
|
|
707
|
+
if (wc)
|
|
708
|
+
wc.payout = 0;
|
|
709
|
+
const stained = this.player.addNotoriety(`killed their own Johnson on "${this.currentJobName ?? 'a job'}"`);
|
|
710
|
+
Logger.getInstance().logWithColor(`\n=== JOB DEAD === You just killed your paymaster. Whatever this was worth, nobody's left to pay it${stained ? ` -- and word WILL travel (+1 Notoriety, now ${this.player.notoriety})` : ''}.`, 'red');
|
|
711
|
+
}
|
|
491
712
|
const entry = this.companions.find(c => c.npc.name === name);
|
|
492
713
|
if (!entry)
|
|
493
714
|
return;
|
|
@@ -849,6 +1070,134 @@ export default class Game {
|
|
|
849
1070
|
get pendingRunTier() {
|
|
850
1071
|
return this._pendingRunTier;
|
|
851
1072
|
}
|
|
1073
|
+
// ==================== The run's CLIENT (canon p.27-29) ====================
|
|
1074
|
+
// Every generated run carries its own Johnson (seed.client) -- the
|
|
1075
|
+
// employer's deniable face who holds THE MEET. The fixer brokered the
|
|
1076
|
+
// introduction and is out of the loop from here. Not saved: mid-run
|
|
1077
|
+
// state never is (runs are disposable); reconstructed fresh by
|
|
1078
|
+
// activateRunScene, settled into the roster by returnToHub.
|
|
1079
|
+
currentClient;
|
|
1080
|
+
// Taxi warn-once (player ruling): leaving with the remainder unclaimed
|
|
1081
|
+
// arms this; the second "call taxi" forfeits. Mirrors _quitArmed.
|
|
1082
|
+
forfeitArmed = false;
|
|
1083
|
+
/**
|
|
1084
|
+
* The meet and the wrap-up, triggered by the player's first/next real
|
|
1085
|
+
* interaction with the client (talk-to, a connected call, a give).
|
|
1086
|
+
* Returns the beat's text (advance/remainder lines) or null when this
|
|
1087
|
+
* NPC isn't the client / nothing new happens. Canon p.28-29: half up
|
|
1088
|
+
* front at the meet, the remainder on delivering proof. Completing the
|
|
1089
|
+
* objective BEFORE ever meeting them collapses both beats into one.
|
|
1090
|
+
*/
|
|
1091
|
+
onClientInteraction(npcName) {
|
|
1092
|
+
const client = this.currentClient;
|
|
1093
|
+
if (!client || !this.scene || this.scene === this._hubScene)
|
|
1094
|
+
return null;
|
|
1095
|
+
if (npcName !== client.name)
|
|
1096
|
+
return null;
|
|
1097
|
+
if (client.dead)
|
|
1098
|
+
return null;
|
|
1099
|
+
const payout = this.scene.getWinCondition()?.payout ?? 0;
|
|
1100
|
+
const lines = [];
|
|
1101
|
+
if (!client.met) {
|
|
1102
|
+
client.met = true;
|
|
1103
|
+
const advance = Math.floor(payout / 2);
|
|
1104
|
+
// The team's split comes off the top of every payment (canon: the
|
|
1105
|
+
// crew shares the take) -- the player banks their share only.
|
|
1106
|
+
const split = this.applyCrewCut(advance);
|
|
1107
|
+
const paid = this.scene.payRunner(split.share, 'meet advance');
|
|
1108
|
+
client.advancePaid = advance;
|
|
1109
|
+
if (paid) {
|
|
1110
|
+
lines.push(`THE MEET: ${client.name} slides half across up front -- ${paid.net}¥ certified${paid.tax > 0 ? ` (${paid.tax}¥ skimmed: SIN on file)` : ''}. The other half on delivery.${hint(` (Canon terms, p.28 -- and the rate is negotiable exactly once: "haggle" while you have them.)`)}`);
|
|
1111
|
+
if (split.line)
|
|
1112
|
+
lines.push(` ${split.line}`);
|
|
1113
|
+
}
|
|
1114
|
+
Logger.getInstance().write(`Client meet: ${this.player.name} met ${client.name}, advance ${advance} (gross, crew took ${advance - split.share}).`);
|
|
1115
|
+
}
|
|
1116
|
+
if (this.scene.isCompleted() && !client.reported) {
|
|
1117
|
+
client.reported = true;
|
|
1118
|
+
const remainder = payout - client.advancePaid;
|
|
1119
|
+
const split = this.applyCrewCut(remainder);
|
|
1120
|
+
const paid = this.scene.payRunner(split.share, 'wrap-up remainder');
|
|
1121
|
+
if (paid) {
|
|
1122
|
+
lines.push(`WRAP-UP: ${client.name} settles the balance -- ${paid.net}¥ certified${paid.tax > 0 ? ` (${paid.tax}¥ skimmed: SIN on file)` : ''}. Clean work; the name will remember yours.`);
|
|
1123
|
+
if (split.line)
|
|
1124
|
+
lines.push(` ${split.line}`);
|
|
1125
|
+
}
|
|
1126
|
+
Logger.getInstance().write(`Client wrap-up: ${this.player.name} reported to ${client.name}, remainder ${remainder} (gross, crew took ${remainder - split.share}).`);
|
|
1127
|
+
// JOHNSONS DON'T LINGER (real session: a settled Ms. Slate paced
|
|
1128
|
+
// in and out of rooms and tailed the runner around the scene).
|
|
1129
|
+
// After the settlement beat lands, the client walks -- their AI
|
|
1130
|
+
// gets no say in staying, because deniable faces leave. Guarded
|
|
1131
|
+
// against the scene having ended or swapped by the time it fires.
|
|
1132
|
+
const sceneAtWrap = this.scene;
|
|
1133
|
+
const clientName = client.name;
|
|
1134
|
+
setTimeout(() => {
|
|
1135
|
+
try {
|
|
1136
|
+
if (this.scene !== sceneAtWrap)
|
|
1137
|
+
return;
|
|
1138
|
+
const actor = sceneAtWrap.allActors.find(a => a.name === clientName);
|
|
1139
|
+
if (!actor || actor.inExchange || actor.deathClaimed)
|
|
1140
|
+
return;
|
|
1141
|
+
Logger.getInstance().logWithColor(`${clientName} settles the tab, turns up a collar, and is gone -- Johnsons don't linger.`, 'cyan');
|
|
1142
|
+
sceneAtWrap.removeActorQuietly(clientName);
|
|
1143
|
+
this.updateStatus();
|
|
1144
|
+
}
|
|
1145
|
+
catch { /* the exit must never crash a scene */ }
|
|
1146
|
+
}, 8000);
|
|
1147
|
+
}
|
|
1148
|
+
return lines.length > 0 ? lines.join('\n') : null;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* The client's verdict, filed at homecoming (returnToHub): whatever
|
|
1152
|
+
* happened, their name joins the roster -- clean work opens a
|
|
1153
|
+
* relationship (role 'Johnson', repeat clients deepen like gigs), a
|
|
1154
|
+
* forfeited remainder or a walked-out job files a note the roster
|
|
1155
|
+
* shows (p.368 already charged the walkout's notoriety at the curb).
|
|
1156
|
+
* Clears currentClient; returns the homecoming line.
|
|
1157
|
+
*/
|
|
1158
|
+
settleClientReputation(wrappedRun) {
|
|
1159
|
+
const client = this.currentClient;
|
|
1160
|
+
this.currentClient = undefined;
|
|
1161
|
+
this.forfeitArmed = false;
|
|
1162
|
+
if (!client)
|
|
1163
|
+
return '';
|
|
1164
|
+
// Dead Johnsons don't take calls (player ruling: a killed client
|
|
1165
|
+
// must not appear in contacts) -- the notoriety was charged at the
|
|
1166
|
+
// kill; homecoming just states the arithmetic.
|
|
1167
|
+
if (client.dead) {
|
|
1168
|
+
this.contacts.delete(client.name);
|
|
1169
|
+
Logger.getInstance().write(`Client reputation settled: ${client.name} (dead -- no contact).`);
|
|
1170
|
+
return `
|
|
1171
|
+
${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and their friends ask questions.`;
|
|
1172
|
+
}
|
|
1173
|
+
const existing = this.contacts.get(client.name);
|
|
1174
|
+
let line;
|
|
1175
|
+
if (wrappedRun && client.reported) {
|
|
1176
|
+
if (existing) {
|
|
1177
|
+
existing.gigs += 1;
|
|
1178
|
+
existing.loyalty = Math.min(6, existing.loyalty + 1);
|
|
1179
|
+
existing.note = undefined;
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
this.contacts.set(client.name, { connection: 4, loyalty: 2, spent: 0, gigs: 1, asked: false, role: 'Johnson' });
|
|
1183
|
+
}
|
|
1184
|
+
line = `\nWord gets around: ${client.name} is good for it -- and so are you. (CONTACT: Johnson, Loyalty ${this.contacts.get(client.name).loyalty})`;
|
|
1185
|
+
}
|
|
1186
|
+
else if (wrappedRun && !client.reported) {
|
|
1187
|
+
const entry = existing ?? { connection: 4, loyalty: 1, spent: 0, gigs: 0, asked: false, role: 'Johnson' };
|
|
1188
|
+
entry.note = 'sloppy wrap-up';
|
|
1189
|
+
this.contacts.set(client.name, entry);
|
|
1190
|
+
line = `\nThe job got done, but you never settled with ${client.name} -- the remainder stays theirs, and that's how they'll tell it.`;
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
const entry = existing ?? { connection: 4, loyalty: 1, spent: 0, gigs: 0, asked: false, role: 'Johnson' };
|
|
1194
|
+
entry.note = 'grudge -- walked out on their job';
|
|
1195
|
+
this.contacts.set(client.name, entry);
|
|
1196
|
+
line = `\n${client.name} will remember this one. Not fondly.`;
|
|
1197
|
+
}
|
|
1198
|
+
Logger.getInstance().write(`Client reputation settled: ${client.name} (${wrappedRun ? (client.reported ? 'clean' : 'unsettled') : 'walked'}).`);
|
|
1199
|
+
return line;
|
|
1200
|
+
}
|
|
852
1201
|
/**
|
|
853
1202
|
* The cab that answers "call taxi" varies per job (player request: not
|
|
854
1203
|
* always a van) -- picked once when the pending run lands, so the
|
|
@@ -957,6 +1306,10 @@ export default class Game {
|
|
|
957
1306
|
// LEADERSHIP (commands/lead.ts): rally/direct/inspire + Command recruiting.
|
|
958
1307
|
CommandFactory.registerCommand('lead', LeadCommand);
|
|
959
1308
|
CommandFactory.registerCommand('command', RecruitCommand);
|
|
1309
|
+
CommandFactory.registerCommand('crew', CrewCommand);
|
|
1310
|
+
CommandFactory.registerCommand('hire', HireCommand);
|
|
1311
|
+
CommandFactory.registerCommand('dismiss', DismissCommand);
|
|
1312
|
+
CommandFactory.registerCommand('train', TrainCommand);
|
|
960
1313
|
// DOCWAGON (commands/docwagon.ts): the wristband between you and the morgue.
|
|
961
1314
|
CommandFactory.registerCommand('docwagon', DocwagonCommand);
|
|
962
1315
|
// SWAG (commands/source.ts): contacts shop the black market for you.
|
|
@@ -998,8 +1351,9 @@ export default class Game {
|
|
|
998
1351
|
// wanted to end a call -- the intent is unambiguous mid-call.
|
|
999
1352
|
CommandFactory.registerCommand('end', EndCallCommand);
|
|
1000
1353
|
CommandFactory.registerCommand('hangup', EndCallCommand);
|
|
1354
|
+
// 'train' used to alias advance; it belongs to crew instruction now
|
|
1355
|
+
// (commands/crew.ts) -- self-training is "advance".
|
|
1001
1356
|
CommandFactory.registerCommand('advance', AdvanceCommand);
|
|
1002
|
-
CommandFactory.registerCommand('train', AdvanceCommand);
|
|
1003
1357
|
CommandFactory.registerCommand('qualities', QualityTradeCommand);
|
|
1004
1358
|
CommandFactory.registerCommand('quality', QualityTradeCommand);
|
|
1005
1359
|
CommandFactory.registerCommand('ask', AskCommand);
|
|
@@ -1097,16 +1451,28 @@ export default class Game {
|
|
|
1097
1451
|
return this._inputBox;
|
|
1098
1452
|
}
|
|
1099
1453
|
async init(options, screen) {
|
|
1454
|
+
this.initUi(screen);
|
|
1455
|
+
return this.initEngine(options);
|
|
1456
|
+
}
|
|
1457
|
+
/** The blessed half of startup -- widgets only. Headless sessions
|
|
1458
|
+
* (Phase C) never call this; everything below survives without it
|
|
1459
|
+
* (updateStatus/refreshLogLabel/close are all screen-guarded). */
|
|
1460
|
+
initUi(screen) {
|
|
1461
|
+
this.initInputBox(screen);
|
|
1462
|
+
this.initLogBox(screen);
|
|
1463
|
+
this.initMetaBox(screen);
|
|
1464
|
+
this.initStatusBox(screen);
|
|
1465
|
+
this._screen = screen;
|
|
1466
|
+
}
|
|
1467
|
+
/** The engine half of startup: scene synthesis, command registration,
|
|
1468
|
+
* hub seeding, resume overlay -- no widget contact. */
|
|
1469
|
+
async initEngine(options) {
|
|
1100
1470
|
this._useAi = options.useAi;
|
|
1101
1471
|
this._logFilePath = options.logFilePath;
|
|
1102
1472
|
this._saveFilePath = options.saveFilePath;
|
|
1103
1473
|
// The swag catalog warms from maka-cli.com in the background
|
|
1104
1474
|
// (utilities/catalog.ts): live list > local cache > baked-in floor.
|
|
1105
1475
|
void refreshCatalog();
|
|
1106
|
-
this.initInputBox(screen);
|
|
1107
|
-
this.initLogBox(screen);
|
|
1108
|
-
this.initMetaBox(screen);
|
|
1109
|
-
this.initStatusBox(screen);
|
|
1110
1476
|
// No "Generating Scene... Please wait." box anymore: every path now
|
|
1111
1477
|
// arrives with REAL prose (hand-authored static seeds, chunk-authored
|
|
1112
1478
|
// generated scenes), so the old blocking AI-redressing pass -- 30-60
|
|
@@ -1123,7 +1489,6 @@ export default class Game {
|
|
|
1123
1489
|
this.scene.initialize(this);
|
|
1124
1490
|
this.player.currentLocation = this.determineStartRoom();
|
|
1125
1491
|
this.registerCommands();
|
|
1126
|
-
this._screen = screen;
|
|
1127
1492
|
// The description follows the rent from day one (see
|
|
1128
1493
|
// syncHomeToLifestyle) -- a fresh session at the default Squatter tier
|
|
1129
1494
|
// reads exactly as the hideout always has.
|
|
@@ -1164,6 +1529,14 @@ export default class Game {
|
|
|
1164
1529
|
// Home turf knows its own (see seedHomeTurfContacts). On resume the
|
|
1165
1530
|
// saved contacts overwrite these loyalty-1 seeds a few lines down.
|
|
1166
1531
|
this.seedHomeTurfContacts();
|
|
1532
|
+
// First deal of the for-hire board (rotates each homecoming).
|
|
1533
|
+
this.rollHireCandidates();
|
|
1534
|
+
// The street learns you're home (social layer): presence beats,
|
|
1535
|
+
// unsynced ghost earnings go up, and anything YOUR runner earned
|
|
1536
|
+
// riding other crews comes down. All fire-and-forget.
|
|
1537
|
+
this.startPresenceHeartbeat();
|
|
1538
|
+
void syncCrewLedger();
|
|
1539
|
+
void this.collectGhostEarnings();
|
|
1167
1540
|
// The next run starts cooking the moment you're home (player
|
|
1168
1541
|
// request): generation takes 20-40s, and waiting for the player
|
|
1169
1542
|
// to CALL first meant "give me a bit" every time. Draft now --
|
|
@@ -1192,6 +1565,15 @@ export default class Game {
|
|
|
1192
1565
|
// above, so the save is authoritative everywhere they overlap.
|
|
1193
1566
|
if (this._resume && this._hubScene) {
|
|
1194
1567
|
this.applyResumeState(this._resume);
|
|
1568
|
+
// The fixer reaches out on resume TOO (player report: "Krow
|
|
1569
|
+
// doesn't kick off a new job when I load in"). The init-time
|
|
1570
|
+
// draft above stays gated on resume because the overlay hadn't
|
|
1571
|
+
// landed yet -- HERE we know whether a pendingRun actually
|
|
1572
|
+
// survived the save, so an empty docket starts cooking now and
|
|
1573
|
+
// Krow's "1 new message" ping lands mid-session as usual.
|
|
1574
|
+
if (!this._pendingRunSeed && this.canDraftNextScene().allowed) {
|
|
1575
|
+
this.draftNextSceneInBackground();
|
|
1576
|
+
}
|
|
1195
1577
|
}
|
|
1196
1578
|
this.initialized = true;
|
|
1197
1579
|
if (this._resume) {
|
|
@@ -1256,7 +1638,23 @@ export default class Game {
|
|
|
1256
1638
|
}
|
|
1257
1639
|
this.pendingProcurements = [...(save.game.procurements ?? [])];
|
|
1258
1640
|
this.docwagon = save.game.docwagon ? { ...save.game.docwagon } : undefined;
|
|
1259
|
-
|
|
1641
|
+
// THE CREW rides the save; the deprecated one-slot enlistedAlly
|
|
1642
|
+
// migrates in once (old saves) and is never written again.
|
|
1643
|
+
this.crew = (save.game.crew ?? []).map(m => ({ ...m, gear: [...m.gear] }));
|
|
1644
|
+
if (this.crew.length === 0 && save.game.enlistedAlly) {
|
|
1645
|
+
const legacy = save.game.enlistedAlly;
|
|
1646
|
+
this.crew.push({
|
|
1647
|
+
name: legacy.name,
|
|
1648
|
+
source: 'contact',
|
|
1649
|
+
description: legacy.description,
|
|
1650
|
+
combat: { ...legacy.combat },
|
|
1651
|
+
gear: [],
|
|
1652
|
+
cutPct: 10,
|
|
1653
|
+
jobsRun: 0,
|
|
1654
|
+
});
|
|
1655
|
+
Logger.getInstance().write(`Migrated legacy enlistedAlly "${legacy.name}" into the crew roster.`);
|
|
1656
|
+
}
|
|
1657
|
+
this.enlistedAlly = undefined;
|
|
1260
1658
|
this.commandRefusals = { ...(save.game.commandRefusals ?? {}) };
|
|
1261
1659
|
this.fencedCategories = new Set(save.game.fencedCategories);
|
|
1262
1660
|
this._rumorIndex = save.game.rumorIndex;
|
|
@@ -1340,7 +1738,7 @@ export default class Game {
|
|
|
1340
1738
|
contacts: [...this.contacts.entries()],
|
|
1341
1739
|
procurements: [...this.pendingProcurements],
|
|
1342
1740
|
docwagon: this.docwagon ? { ...this.docwagon } : undefined,
|
|
1343
|
-
|
|
1741
|
+
crew: this.crew.map(m => ({ ...m, gear: [...m.gear] })),
|
|
1344
1742
|
commandRefusals: { ...this.commandRefusals },
|
|
1345
1743
|
fencedCategories: [...this.fencedCategories],
|
|
1346
1744
|
rumorIndex: this._rumorIndex,
|
|
@@ -1443,6 +1841,18 @@ export default class Game {
|
|
|
1443
1841
|
// Read the run's fate BEFORE the scene swap below discards it -- a
|
|
1444
1842
|
// walked-away job gets a different homecoming line than a wrapped one.
|
|
1445
1843
|
const wrappedRun = this.scene !== hub && this.scene.isCompleted();
|
|
1844
|
+
// THE CLIENT REMEMBERS (player ruling: the run's outcome is
|
|
1845
|
+
// reputation with the CLIENT, not the fixer).
|
|
1846
|
+
const clientLine = this.scene !== hub ? this.settleClientReputation(wrappedRun) : '';
|
|
1847
|
+
// CREW settlement BEFORE the fold (gear write-back + obituaries --
|
|
1848
|
+
// dismissCompanion would ship their kit to the player otherwise),
|
|
1849
|
+
// then the wrapped-run tally and the training window reset.
|
|
1850
|
+
const crewLines = this.scene !== hub ? this.settleCrewAfterRun() : [];
|
|
1851
|
+
for (const member of this.crew) {
|
|
1852
|
+
if (wrappedRun)
|
|
1853
|
+
member.jobsRun += 1;
|
|
1854
|
+
member.trainedThisCycle = false;
|
|
1855
|
+
}
|
|
1446
1856
|
// Companions fold for the ride home, same as the trip out.
|
|
1447
1857
|
const companionFoldLines = this.foldCompanionsForTravel();
|
|
1448
1858
|
// Settle cross-plane and combat state that must not leak into the hub
|
|
@@ -1523,11 +1933,16 @@ export default class Game {
|
|
|
1523
1933
|
const companionBlock = [...companionFoldLines, ...companionArrivalLines];
|
|
1524
1934
|
// The commanded ally's tour ends with the run (RAW's Command lasts
|
|
1525
1935
|
// turns; ours lasts the job): the crew disbands at the curb.
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1936
|
+
// The crew stays hired between jobs (the roster persists); their
|
|
1937
|
+
// homecoming lines (obituaries, write-back) rode in above. Fresh
|
|
1938
|
+
// faces on the for-hire board each homecoming.
|
|
1939
|
+
this.rollHireCandidates();
|
|
1940
|
+
// Social settlement: presence resumes, ghost cuts sync up, earnings
|
|
1941
|
+
// come home.
|
|
1942
|
+
this.beatPresence();
|
|
1943
|
+
void syncCrewLedger();
|
|
1944
|
+
void this.collectGhostEarnings();
|
|
1945
|
+
const allyLine = crewLines.length > 0 ? `\n${crewLines.join('\n')}` : '';
|
|
1531
1946
|
// DOCWAGON's year ticks in homecomings (12 = a street year, player
|
|
1532
1947
|
// ruling): the contract lapses quietly unless renewed at Doc's.
|
|
1533
1948
|
let docwagonLine = '';
|
|
@@ -1570,7 +1985,7 @@ export default class Game {
|
|
|
1570
1985
|
const fateLine = wrappedRun
|
|
1571
1986
|
? `The run's wrapped and the street swallows you back up.`
|
|
1572
1987
|
: `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
|
|
1573
|
-
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}${deliveryLine}${docwagonLine}${allyLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
1988
|
+
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.`)}${clientLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}${allyLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
1574
1989
|
}
|
|
1575
1990
|
/**
|
|
1576
1991
|
* The actual drafting work, split out from continueToNextScene() purely
|
|
@@ -1580,15 +1995,9 @@ export default class Game {
|
|
|
1580
1995
|
*/
|
|
1581
1996
|
async draftNextScene() {
|
|
1582
1997
|
const nextTier = this._tier + 1;
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
1586
|
-
// identity (is there an actor actually named FIXER_NAME?), not via the
|
|
1587
|
-
// win condition's toNpc -- that's often a one-off contact the fixer
|
|
1588
|
-
// pointed the runner toward, not the fixer himself (see the "broker,
|
|
1589
|
-
// not always the destination" convention).
|
|
1590
|
-
const fixerStillInScene = this.scene.allActors.some(a => a.name === FIXER_NAME);
|
|
1591
|
-
const previousFixerName = fixerStillInScene ? FIXER_NAME : undefined;
|
|
1998
|
+
// Each run invents its OWN client Johnson (canon roles: the fixer
|
|
1999
|
+
// brokers from the hub and never appears in run scenes) -- the old
|
|
2000
|
+
// fixer-identity carryforward is gone with him.
|
|
1592
2001
|
// Append, don't replace -- this used to be setContent(), which wiped
|
|
1593
2002
|
// the whole log box (including the JOB COMPLETE banner and everything
|
|
1594
2003
|
// before it) for the entire generation wait, which can run 20-40+
|
|
@@ -1602,7 +2011,7 @@ export default class Game {
|
|
|
1602
2011
|
//
|
|
1603
2012
|
// The player object rides along so the job is drafted FOR this runner
|
|
1604
2013
|
// -- their archetype, stats, and toolkit (see buildPlayerDossier).
|
|
1605
|
-
const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier,
|
|
2014
|
+
const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, this.player, this.activeRumor, this.crew);
|
|
1606
2015
|
if (!nextSeed) {
|
|
1607
2016
|
return `Incoming call -- ${FIXER_NAME}: "Job fell through, chummer. Client got cold feet before the ink dried. Give me another call in a bit -- I'll shake something loose."`;
|
|
1608
2017
|
}
|
|
@@ -1669,14 +2078,26 @@ export default class Game {
|
|
|
1669
2078
|
// Deployed drones spin back up at the curb -- BEFORE appearance
|
|
1670
2079
|
// warming so re-shelled companions get warmed with the cast.
|
|
1671
2080
|
const arrivalLines = this.unfoldCompanionsOnArrival();
|
|
1672
|
-
//
|
|
1673
|
-
const allyLines = this.
|
|
2081
|
+
// The crew meets you at the jobsite (commands/crew.ts, lead.ts).
|
|
2082
|
+
const allyLines = this.spawnCrew();
|
|
1674
2083
|
this.warmNpcAppearancesInBackground();
|
|
1675
2084
|
this.refreshLogLabel();
|
|
2085
|
+
// THE MEET opens the job (canon p.28): the seed's client is this
|
|
2086
|
+
// run's Johnson -- track them, and point the arrival at the meet.
|
|
2087
|
+
this.currentClient = seed.client
|
|
2088
|
+
? { name: seed.client, met: false, advancePaid: 0, reported: false }
|
|
2089
|
+
: undefined;
|
|
2090
|
+
this.forfeitArmed = false;
|
|
2091
|
+
const clientRoom = seed.client
|
|
2092
|
+
? this.scene.allActors.find(a => a.name === seed.client)?.currentLocation?.name
|
|
2093
|
+
: undefined;
|
|
2094
|
+
const meetLine = seed.client
|
|
2095
|
+
? `\nYour contact on this one goes by ${seed.client}${clientRoom ? ` -- holding the meet at ${clientRoom}` : ''}. Half up front when you sit down; the rest on delivery.${hint(` (Find them and talk, or "call ${seed.client}" -- the rate is negotiable at the meet, once.)`)}`
|
|
2096
|
+
: '';
|
|
1676
2097
|
// No "=== JOB #n ===" banner on entry (player request) -- the Game
|
|
1677
2098
|
// Log window's own title carries the job name now.
|
|
1678
2099
|
const companionBlock = [...companionLines, ...arrivalLines, ...allyLines];
|
|
1679
|
-
return `\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
2100
|
+
return `\n${this.scene.story}${meetLine}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
1680
2101
|
}
|
|
1681
2102
|
/**
|
|
1682
2103
|
* The needs check at every job departure (SR5e p.56 / p.172, scaled to
|
|
@@ -1789,13 +2210,41 @@ export default class Game {
|
|
|
1789
2210
|
Logger.getInstance().write(`Permadeath: save deleted (${this._saveFilePath}).`);
|
|
1790
2211
|
}
|
|
1791
2212
|
this.stopPursuitHeartbeat();
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
this._inputBox
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
}
|
|
2213
|
+
// Widget teardown only where widgets exist -- endGame() reaches here
|
|
2214
|
+
// from headless sessions too (Phase C), where there is no screen.
|
|
2215
|
+
if (this._inputBox) {
|
|
2216
|
+
this._inputBox.removeAllListeners(); // removes all event listeners
|
|
2217
|
+
this._inputBox.cancel(); // cancels any active input session
|
|
2218
|
+
this._inputBox.detach(); // optionally remove from screen
|
|
2219
|
+
}
|
|
2220
|
+
if (this._screen) {
|
|
2221
|
+
this._screen.render(); // re-render if needed
|
|
2222
|
+
this._screen.key(['C-c'], () => {
|
|
2223
|
+
process.exit(2);
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
stopPresenceHeartbeat() {
|
|
2228
|
+
if (this._presenceTimer) {
|
|
2229
|
+
clearInterval(this._presenceTimer);
|
|
2230
|
+
this._presenceTimer = undefined;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Full per-session teardown (Phase C): everything init/play armed that
|
|
2235
|
+
* outlives a turn -- for the hosting server's idle eviction and
|
|
2236
|
+
* session close. Solo play never calls this (process exit does it).
|
|
2237
|
+
*/
|
|
2238
|
+
async dispose() {
|
|
2239
|
+
this.stopPresenceHeartbeat();
|
|
2240
|
+
this.stopPursuitHeartbeat();
|
|
2241
|
+
this.autoFight.cancelPending();
|
|
2242
|
+
for (const ko of this.scene?.playerKnockouts.values() ?? []) {
|
|
2243
|
+
if (ko.timer)
|
|
2244
|
+
clearTimeout(ko.timer);
|
|
2245
|
+
}
|
|
2246
|
+
this.scene?.playerKnockouts.clear();
|
|
2247
|
+
await this.commandQueue.drain();
|
|
1799
2248
|
}
|
|
1800
2249
|
initInputBox(screen) {
|
|
1801
2250
|
// Create an input box for user commands
|
|
@@ -2052,7 +2501,15 @@ export default class Game {
|
|
|
2052
2501
|
// fire from synthesis paths before the blessed UI exists.
|
|
2053
2502
|
if (!this._statusBox)
|
|
2054
2503
|
return;
|
|
2055
|
-
|
|
2504
|
+
this._statusBox.setContent(this.statusLinesFor(this.player).join('\n'));
|
|
2505
|
+
this._screen.render();
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* The HUD as data: every status line for `player`, pure state reads --
|
|
2509
|
+
* no widgets. updateStatus paints it for the local player; a shared-run
|
|
2510
|
+
* server ships it per player as status events (Phase C).
|
|
2511
|
+
*/
|
|
2512
|
+
statusLinesFor(player) {
|
|
2056
2513
|
const lines = [];
|
|
2057
2514
|
// Line 1: where you are, and the fight autopilot mode. The stance
|
|
2058
2515
|
// lives HERE -- the shortest HUD line -- because appended to the long
|
|
@@ -2189,8 +2646,7 @@ export default class Game {
|
|
|
2189
2646
|
const mount = player.droneArmed ? ' ⌖ armed' : ' (recon -- no mounts)';
|
|
2190
2647
|
lines.push(`{yellow-fg}✈ JUMPED IN: ${drone?.name ?? 'drone'} ${(player.simMode ?? 'cold').toUpperCase()}-SIM${mount}${hull} -- body at ${player.bodyRoom?.name ?? '???'}{/yellow-fg}`);
|
|
2191
2648
|
}
|
|
2192
|
-
|
|
2193
|
-
this._screen.render();
|
|
2649
|
+
return lines;
|
|
2194
2650
|
}
|
|
2195
2651
|
// The three methods below were the render paths for the old always-on
|
|
2196
2652
|
// sidebar boxes. Those boxes are gone (the log owns the whole screen; see
|
|
@@ -2225,7 +2681,50 @@ export default class Game {
|
|
|
2225
2681
|
* correction call overwrites it with the corrected verb.
|
|
2226
2682
|
*/
|
|
2227
2683
|
lastResolvedVerb = '';
|
|
2684
|
+
/**
|
|
2685
|
+
* Serializes every inbound command (see utilities/command-queue.ts):
|
|
2686
|
+
* the engine's await points assume one command in flight, and a second
|
|
2687
|
+
* human on a DDP connection must wait its turn, not interleave.
|
|
2688
|
+
*/
|
|
2689
|
+
commandQueue = new CommandQueue();
|
|
2690
|
+
/**
|
|
2691
|
+
* Shared-run hook (Phase C): fires when a NON-primary human player
|
|
2692
|
+
* dies in this scene, so the server can burn their save. The primary
|
|
2693
|
+
* player's death keeps the solo path (Scene.endGame -> close(),
|
|
2694
|
+
* permadeath).
|
|
2695
|
+
*/
|
|
2696
|
+
onPlayerDeath;
|
|
2228
2697
|
async handleInput(input) {
|
|
2698
|
+
return (await this.handleInputAs(this.player.name, input)).text;
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Runs `input` AS the named human player -- the symmetric-multiplayer
|
|
2702
|
+
* command entry: every human gets the same full privileged context the
|
|
2703
|
+
* solo player always had. Queued; internal re-entries (typo
|
|
2704
|
+
* correction, say.ts's speech-accept) run inline via the queue's
|
|
2705
|
+
* re-entrancy guard.
|
|
2706
|
+
*/
|
|
2707
|
+
async handleInputAs(actorName, input) {
|
|
2708
|
+
return this.commandQueue.enqueue(() => {
|
|
2709
|
+
// Ambient event scoping (see emitter.ts): while this command runs,
|
|
2710
|
+
// the session context knows WHOSE command it is -- headless
|
|
2711
|
+
// emitters default its output to that player's room/eyes. Solo
|
|
2712
|
+
// play has no session context; this is a no-op there.
|
|
2713
|
+
const ctx = currentSession();
|
|
2714
|
+
const prev = ctx?.dispatch;
|
|
2715
|
+
if (ctx)
|
|
2716
|
+
ctx.dispatch = { actorName };
|
|
2717
|
+
return Promise.resolve(this.runInputAs(actorName, input)).finally(() => {
|
|
2718
|
+
if (ctx)
|
|
2719
|
+
ctx.dispatch = prev;
|
|
2720
|
+
});
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
async runInputAs(actorName, input) {
|
|
2724
|
+
const actor = this.scene.getPlayerByName(actorName);
|
|
2725
|
+
if (!actor) {
|
|
2726
|
+
return { text: `No runner named "${actorName}" is in this scene.`, verb: '' };
|
|
2727
|
+
}
|
|
2229
2728
|
// "jack-in"/"jack-out" read naturally but parse as unknown verbs (a
|
|
2230
2729
|
// real session typed "jack-in" and bounced) -- normalize the hyphen.
|
|
2231
2730
|
const normalized = input.replace(/^\s*jack-(in|out)\b/i, 'jack $1');
|
|
@@ -2233,43 +2732,48 @@ export default class Game {
|
|
|
2233
2732
|
const verb = parts[0].toLowerCase();
|
|
2234
2733
|
const args = parts.slice(1);
|
|
2235
2734
|
this.lastResolvedVerb = verb;
|
|
2735
|
+
const out = (text) => ({ text, verb: this.lastResolvedVerb });
|
|
2736
|
+
// Empty input is a nervous Enter, not a command (a real session's
|
|
2737
|
+
// triple keystroke printed three 'I don't understand ""' lines).
|
|
2738
|
+
if (!verb)
|
|
2739
|
+
return out('');
|
|
2236
2740
|
// Quitting takes a CONFIRM (player request: "I just exited by
|
|
2237
2741
|
// mistake"): the first "quit" arms it, "y"/"yes" (or quit again)
|
|
2238
2742
|
// seals it, anything else disarms and runs as the command it is.
|
|
2239
2743
|
if (this._quitArmed) {
|
|
2240
2744
|
this._quitArmed = false;
|
|
2241
2745
|
if (/^(y|yes|quit|exit)$/i.test(verb)) {
|
|
2242
|
-
return null;
|
|
2746
|
+
return out(null);
|
|
2243
2747
|
}
|
|
2244
2748
|
// fall through -- the answer was a real command; play on.
|
|
2245
2749
|
}
|
|
2246
2750
|
else if (verb === 'quit' || verb === 'exit') {
|
|
2247
2751
|
this._quitArmed = true;
|
|
2248
|
-
return `Really quit? ("y" to confirm -- anything else keeps playing.)
|
|
2752
|
+
return out(`Really quit? ("y" to confirm -- anything else keeps playing.)`);
|
|
2249
2753
|
}
|
|
2250
2754
|
// Out cold with the fight settling around you (see
|
|
2251
2755
|
// Scene.beginPlayerKnockout): unconsciousness is not a prompt. Only
|
|
2252
2756
|
// quit (handled above) still answers.
|
|
2253
|
-
if (
|
|
2254
|
-
return `You're out cold -- the world moves without you
|
|
2757
|
+
if (actor.isUnconscious() && this.scene.getKnockout(actor.name)) {
|
|
2758
|
+
return out(`You're out cold -- the world moves without you.`);
|
|
2255
2759
|
}
|
|
2256
2760
|
if (verb === 'maka') {
|
|
2257
|
-
return 'You are still in the game, type "quit" to exit';
|
|
2761
|
+
return out('You are still in the game, type "quit" to exit');
|
|
2258
2762
|
}
|
|
2259
2763
|
if (verb === 'continue') {
|
|
2260
2764
|
// RETIRED verb, kept only as a redirect: jobs flow through the
|
|
2261
2765
|
// fixer ("call Mr. Johnson"), and a finished run ends by walking
|
|
2262
2766
|
// home to your hideout -- no system command in the loop.
|
|
2263
|
-
return `That's not how the street works anymore, chummer: "call ${FIXER_NAME}" when you want work. When a run's wrapped, head home to your hideout -- the door knows the way
|
|
2767
|
+
return out(`That's not how the street works anymore, chummer: "call ${FIXER_NAME}" when you want work. When a run's wrapped, head home to your hideout -- the door knows the way.`);
|
|
2264
2768
|
}
|
|
2265
2769
|
if (verb === 'help') {
|
|
2266
|
-
return this.getHelpMessage(args);
|
|
2770
|
+
return out(this.getHelpMessage(args));
|
|
2267
2771
|
}
|
|
2268
2772
|
if (verb === 'clear') {
|
|
2269
|
-
return '';
|
|
2773
|
+
return out('');
|
|
2270
2774
|
}
|
|
2271
2775
|
const context = {
|
|
2272
|
-
actor
|
|
2776
|
+
actor,
|
|
2273
2777
|
rooms: this.scene.getRooms(),
|
|
2274
2778
|
scene: this.scene,
|
|
2275
2779
|
screen: this._screen,
|
|
@@ -2278,7 +2782,7 @@ export default class Game {
|
|
|
2278
2782
|
const command = CommandFactory.createCommand(verb, context);
|
|
2279
2783
|
if (command) {
|
|
2280
2784
|
const result = await command.execute(args);
|
|
2281
|
-
return result;
|
|
2785
|
+
return out(result);
|
|
2282
2786
|
}
|
|
2283
2787
|
// Typo tolerance: two straight sessions bounced on slips like "searh"
|
|
2284
2788
|
// and "maop". Reuse the same fuzzy matcher item/NPC names go through
|
|
@@ -2297,17 +2801,19 @@ export default class Game {
|
|
|
2297
2801
|
}
|
|
2298
2802
|
if (guess && guess !== verb) {
|
|
2299
2803
|
if (guess === 'quit' || guess === 'exit') {
|
|
2300
|
-
return `I don't understand "${verb}" -- did you mean "${guess}"
|
|
2804
|
+
return out(`I don't understand "${verb}" -- did you mean "${guess}"?`);
|
|
2301
2805
|
}
|
|
2302
|
-
const corrected = await this.
|
|
2806
|
+
const corrected = await this.runInputAs(actorName, [guess, ...args].join(' '));
|
|
2303
2807
|
// The correction note stays INLINE and always-on (player ruling:
|
|
2304
2808
|
// it briefly moved to the Mechanics ticker, and a conversational
|
|
2305
2809
|
// "well I'm here..." silently became a SELL with nothing in the
|
|
2306
2810
|
// world log explaining the refusal that came back). It's not dice
|
|
2307
2811
|
// noise -- it's the only receipt for a command you didn't type.
|
|
2308
|
-
return corrected === null
|
|
2812
|
+
return corrected.text === null
|
|
2813
|
+
? out(null)
|
|
2814
|
+
: out(`(read "${verb}" as "${guess}")\n${corrected.text ?? ''}`);
|
|
2309
2815
|
}
|
|
2310
|
-
return `I don't understand the command "${verb}"
|
|
2816
|
+
return out(`I don't understand the command "${verb}".`);
|
|
2311
2817
|
}
|
|
2312
2818
|
/**
|
|
2313
2819
|
* The help taxonomy: every entry is [primary, ...aliases], grouped the
|
|
@@ -2324,13 +2830,13 @@ export default class Game {
|
|
|
2324
2830
|
{ title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['stance'], ['surrender'], ['edge'], ['rest']] },
|
|
2325
2831
|
// The party layer: everyone who walks (or flies, or manifests) at
|
|
2326
2832
|
// your side answers to these.
|
|
2327
|
-
{ title: 'party', entries: [['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
|
|
2833
|
+
{ title: 'party', entries: [['crew'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
|
|
2328
2834
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
|
|
2329
2835
|
{ title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
2330
2836
|
{ title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
2331
2837
|
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
2332
2838
|
{ title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
|
|
2333
|
-
{ title: 'character', entries: [['sheet', 'stats'], ['advance'
|
|
2839
|
+
{ title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
2334
2840
|
{ title: 'session', entries: [['help'], ['hints'], ['clear'], ['quit', 'exit']] },
|
|
2335
2841
|
];
|
|
2336
2842
|
/** Descriptions for the verbs handled inline in handleInput() rather than registered. */
|