@maka/maka-cli 5.10.0 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +6 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +28 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.d.ts +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js +99 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +12 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +14 -8
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/deck.d.ts +30 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/deck.js +142 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/deck.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.d.ts +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js +87 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +29 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +9 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +9 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/install.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/install.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.d.ts +32 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js +94 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.d.ts +39 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js +187 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/palm.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/palm.js +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/palm.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/perform.d.ts +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/perform.js +48 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/perform.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +8 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +8 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.d.ts +6 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +80 -65
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/source.d.ts +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/source.js +176 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/source.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +6 -3
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +71 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +424 -29
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +28 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +83 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +29 -7
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +23 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +78 -17
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +299 -60
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +73 -3
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +32 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +112 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +28 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +128 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +41 -9
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.d.ts +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/docwagon.d.ts +33 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/docwagon.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/docwagon.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +35 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.d.ts +39 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +63 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.d.ts +18 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js +91 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +25 -16
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,8 +5,9 @@ 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, } from './utilities/persistence.js';
|
|
8
|
+
import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, restoreItem, readSave, } from './utilities/persistence.js';
|
|
9
9
|
import { pushSave, burnCloudSaveFor } from './utilities/cloud-saves.js';
|
|
10
|
+
import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
|
|
10
11
|
import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
|
|
11
12
|
import { rollPool, formatRoll } from './utilities/dice.js';
|
|
12
13
|
import { Direction } from './types/shared/direction-enum.js';
|
|
@@ -38,6 +39,14 @@ import { RestCommand } from './commands/rest.js';
|
|
|
38
39
|
import { ReloadCommand } from './commands/reload.js';
|
|
39
40
|
import { SubdueCommand } from './commands/subdue.js';
|
|
40
41
|
import { GrappleCommand, ReleaseCommand, StruggleCommand } from './commands/grapple.js';
|
|
42
|
+
import { AcceptCommand, DeclineCommand } from './commands/job-offer.js';
|
|
43
|
+
import { SourceCommand } from './commands/source.js';
|
|
44
|
+
import { DocwagonCommand } from './commands/docwagon.js';
|
|
45
|
+
import { LeadCommand, RecruitCommand } from './commands/lead.js';
|
|
46
|
+
import { PalmCommand } from './commands/palm.js';
|
|
47
|
+
import { CatalogCommand } from './commands/catalog.js';
|
|
48
|
+
import { refreshCatalog } from './utilities/catalog.js';
|
|
49
|
+
import { PerformCommand } from './commands/perform.js';
|
|
41
50
|
import { EdgeCommand } from './commands/edge.js';
|
|
42
51
|
import { SheetCommand } from './commands/sheet.js';
|
|
43
52
|
import { CallCommand } from './commands/call.js';
|
|
@@ -64,6 +73,8 @@ import { InitiateCommand } from './commands/initiate.js';
|
|
|
64
73
|
import { SummonCommand } from './commands/summon.js';
|
|
65
74
|
import { OrderCommand } from './commands/order.js';
|
|
66
75
|
import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
|
|
76
|
+
import { DeckCommand, LoadProgramCommand, UnloadProgramCommand } from './commands/deck.js';
|
|
77
|
+
import { getProgram } from './utilities/programs.js';
|
|
67
78
|
import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
|
|
68
79
|
import { HintsCommand } from './commands/hints.js';
|
|
69
80
|
import { hint } from './utilities/hints.js';
|
|
@@ -152,7 +163,191 @@ export default class Game {
|
|
|
152
163
|
// against the real pending-run seed. One pull per contact per
|
|
153
164
|
// homecoming (asked resets in returnToHub).
|
|
154
165
|
contacts = new Map();
|
|
166
|
+
// SWAG orders in flight (SR5e p.387/418, commands/source.ts): a
|
|
167
|
+
// contact shopping the black market for you. Paid up front on a won
|
|
168
|
+
// Availability test; the goods land at the NEXT HOMECOMING (the
|
|
169
|
+
// delivery-times table, abstracted to the job-cycle clock the same
|
|
170
|
+
// way rest is). Rides the save (ISavedGameState.procurements).
|
|
171
|
+
pendingProcurements = [];
|
|
172
|
+
// DOCWAGON (SR5e p.450, utilities/docwagon.ts): the wristband between
|
|
173
|
+
// a runner and the morgue. Rides the save; ticks down at homecomings.
|
|
174
|
+
docwagon;
|
|
175
|
+
// LEADERSHIP's Command action (SR5e p.141-142, commands/lead.ts): a
|
|
176
|
+
// recruited contact rides the NEXT run as an 'ally' companion shell
|
|
177
|
+
// (spawned at activateRunScene, released at homecoming). RAW's teeth:
|
|
178
|
+
// a refusal stands until Loyalty deepens past the level it happened
|
|
179
|
+
// at. Both ride the save.
|
|
180
|
+
enlistedAlly;
|
|
181
|
+
commandRefusals = {};
|
|
182
|
+
// PERFORMANCE's busking gate (commands/perform.ts): one set per job
|
|
183
|
+
// cycle, reset at homecoming like a contact's legwork. Deliberately
|
|
184
|
+
// NOT saved -- a relaunch is as good as a homecoming for new material.
|
|
185
|
+
performedThisCycle = false;
|
|
186
|
+
/** The recruited contact joins at the jobsite curb ("takes their own
|
|
187
|
+
* ride and meets you there" -- the companion convention). */
|
|
188
|
+
spawnEnlistedAlly() {
|
|
189
|
+
const ally = this.enlistedAlly;
|
|
190
|
+
if (!ally)
|
|
191
|
+
return [];
|
|
192
|
+
const shell = this.spawnCompanionShell({
|
|
193
|
+
name: ally.name,
|
|
194
|
+
description: `${ally.description} Right now they run as ${this.player.name}'s crew: they fight beside them, follow their lead, and expect to be home by morning.`,
|
|
195
|
+
kind: 'ally',
|
|
196
|
+
combat: ally.combat,
|
|
197
|
+
room: this.player.currentLocation,
|
|
198
|
+
plane: 'meat',
|
|
199
|
+
force: Number(ally.combat.body ?? 3),
|
|
200
|
+
});
|
|
201
|
+
return shell ? [`${ally.name} is already leaning on the wall when you arrive -- "Took my own ride. Let's work."`] : [];
|
|
202
|
+
}
|
|
203
|
+
/** Bills a DocWagon fee: cred first, shortfall onto the rent tab (the
|
|
204
|
+
* street's one universal collections agency). Returns the line. */
|
|
205
|
+
billDocwagon(amount, label) {
|
|
206
|
+
if (amount <= 0)
|
|
207
|
+
return '';
|
|
208
|
+
const paid = Math.min(this.player.currency, amount);
|
|
209
|
+
if (paid > 0)
|
|
210
|
+
this.player.adjustCurrency(-paid);
|
|
211
|
+
const owed = amount - paid;
|
|
212
|
+
if (owed > 0)
|
|
213
|
+
this.rentDebt += owed;
|
|
214
|
+
return owed > 0
|
|
215
|
+
? `DocWagon bills ${amount} for ${label} -- ${paid} paid, ${owed} on your tab.`
|
|
216
|
+
: `DocWagon bills ${amount} for ${label}.`;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The band answers (SR5e p.450). 'htr': a High Threat Response team
|
|
220
|
+
* extracts a flatlining KO'd runner instead of the street deciding.
|
|
221
|
+
* 'resusc': clinical death reversed -- a free resuscitation or the
|
|
222
|
+
* 5,000 fee -- the sanctioned carve-out to permadeath (player ruling;
|
|
223
|
+
* overflow-past-Body instant death isn't modeled yet, so every death
|
|
224
|
+
* is currently within reach of the crash cart). Returns true when the
|
|
225
|
+
* rescue engages -- the caller must then NOT end the game.
|
|
226
|
+
*/
|
|
227
|
+
tryDocwagonRescue(kind, opts = {}) {
|
|
228
|
+
const contract = this.docwagon;
|
|
229
|
+
if (!contract || !this.initialized || this._dead)
|
|
230
|
+
return false;
|
|
231
|
+
const tier = DOCWAGON_TIERS[contract.tier];
|
|
232
|
+
const logger = Logger.getInstance();
|
|
233
|
+
const bills = [];
|
|
234
|
+
if (kind === 'resusc') {
|
|
235
|
+
if (contract.resuscsLeft > 0) {
|
|
236
|
+
contract.resuscsLeft -= 1;
|
|
237
|
+
bills.push(`A contract resuscitation is burned -- ${contract.resuscsLeft} left this year.`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
bills.push(this.billDocwagon(RESUSC_FEE, 'resuscitation'));
|
|
241
|
+
}
|
|
242
|
+
// Back from the white light, barely: two boxes shy of the track.
|
|
243
|
+
const p = this.player;
|
|
244
|
+
p.healDamage(Math.max(0, p.damageTaken - (p.maxConditionBoxes - 2)));
|
|
245
|
+
p.healStun(Math.max(0, p.stunTaken - Math.max(0, p.maxStunBoxes - 2)));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
bills.push(this.billDocwagon(tier.htrFee, 'High Threat Response'));
|
|
249
|
+
// Extraction under fire: hostiles still standing can cost DocWagon
|
|
250
|
+
// a medic -- and the client eats the 20,000 death compensation
|
|
251
|
+
// (p.450), unless the contract says otherwise.
|
|
252
|
+
const hostiles = opts.standingHostiles ?? [];
|
|
253
|
+
if (hostiles.length > 0 && tier.deathComp) {
|
|
254
|
+
const casualtyRoll = rollPool(Math.min(4, hostiles.length));
|
|
255
|
+
if (casualtyRoll.hits > 0) {
|
|
256
|
+
bills.push(this.billDocwagon(DEATH_COMP, 'employee death compensation -- a medic did not make it out'));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Conscious enough to be carried: the band's medics stabilize.
|
|
260
|
+
const p = this.player;
|
|
261
|
+
p.healStun(Math.max(0, p.stunTaken - Math.max(0, p.maxStunBoxes - 2)));
|
|
262
|
+
}
|
|
263
|
+
logger.logWithColor(kind === 'resusc'
|
|
264
|
+
? `${CALL_ICON} Your wristband SCREAMS the flatline. Ten minutes later there is foam, light, and a trauma team refusing to let you go.`
|
|
265
|
+
: `${CALL_ICON} The band reads the flatline and howls -- a DocWagon HTR team storms the scene behind riot shields and suppression foam.`, CALL_COLOR);
|
|
266
|
+
for (const b of bills.filter(Boolean))
|
|
267
|
+
logger.logWithColor(b, 'yellow');
|
|
268
|
+
this.scene.addWorldEvent(`A DocWagon team extracted ${this.player.name} under the ${contract.tier} contract.`);
|
|
269
|
+
void this.docwagonMedevac();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
/** Where the wagon drops you (player ruling: the CLINIC, not your own
|
|
273
|
+
* cot): the district Street Doc's room, generically -- so generated
|
|
274
|
+
* hubs work too. Falls back to the hideout if the hub has no doc. */
|
|
275
|
+
docwagonDropOff() {
|
|
276
|
+
for (const [name, c] of this.contacts) {
|
|
277
|
+
if (c.role === 'Street Doc') {
|
|
278
|
+
const doc = this.hubActor(name);
|
|
279
|
+
if (doc?.currentLocation)
|
|
280
|
+
return doc.currentLocation;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return this._homeRoom;
|
|
284
|
+
}
|
|
285
|
+
/** The ride out on a stretcher: mid-run this is a homecoming (rent and
|
|
286
|
+
* all), then the recovery bed at the clinic. */
|
|
287
|
+
async docwagonMedevac() {
|
|
288
|
+
const logger = Logger.getInstance();
|
|
289
|
+
try {
|
|
290
|
+
const clinic = this.docwagonDropOff();
|
|
291
|
+
if (this.hasHub && this.scene !== this._hubScene) {
|
|
292
|
+
const home = await this.continueToNextScene();
|
|
293
|
+
logger.log(home);
|
|
294
|
+
}
|
|
295
|
+
this.player.currentLocation = clinic;
|
|
296
|
+
this.player.cameFrom = undefined;
|
|
297
|
+
this.updateExits(this.player.currentLocation);
|
|
298
|
+
this.updateStatus();
|
|
299
|
+
logger.log(clinic === this._homeRoom
|
|
300
|
+
? `You wake on your own cot, taped, stapled, and alive.`
|
|
301
|
+
: `You wake on a recovery cot under bad fluorescents -- ${clinic.name}. Taped, stapled, and alive.`);
|
|
302
|
+
this.requestSave('homecoming');
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
logger.error(`docwagonMedevac failed: ${err}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
155
308
|
/** How much juice the name carries, read off the role. */
|
|
309
|
+
/**
|
|
310
|
+
* A contact's ROLE, the SR5e archetype list (p.98: fixer, arms dealer,
|
|
311
|
+
* smuggler, talismonger, street doc, mechanic, bartender...) -- same
|
|
312
|
+
* keyword school as connectionFor below. Stored on the contact entry
|
|
313
|
+
* at open time so the roster (and the save) carry it.
|
|
314
|
+
*/
|
|
315
|
+
static roleOf(npc) {
|
|
316
|
+
const hay = `${npc.description ?? ''} ${npc.name}`.toLowerCase();
|
|
317
|
+
// Mr. Johnson is NOT the fixer (player ruling: don't overload his
|
|
318
|
+
// role). Canon splits them: a Johnson is the anonymous CLIENT who
|
|
319
|
+
// commissions the run; a fixer is the street middleman (scene1's
|
|
320
|
+
// Mr. Krow, e.g.). He gets his own archetype label.
|
|
321
|
+
if (npc.name === FIXER_NAME || /job broker|johnson/.test(hay))
|
|
322
|
+
return 'Job Broker';
|
|
323
|
+
if (/fixer/.test(hay))
|
|
324
|
+
return 'Fixer';
|
|
325
|
+
// "charms" deliberately absent: a market kid hawking LUCKY charms is
|
|
326
|
+
// a Vendor, not a talismonger (Skitter proved it).
|
|
327
|
+
if (/talismonger|magical goods|foci|spell formula/.test(hay))
|
|
328
|
+
return 'Talismonger';
|
|
329
|
+
if (/street doc|clinic|surgeon|medic|stitch|chop shop/.test(hay))
|
|
330
|
+
return 'Street Doc';
|
|
331
|
+
if (/fence|pawn|launder|no questions/.test(hay))
|
|
332
|
+
return 'Fence';
|
|
333
|
+
if (/smuggl/.test(hay))
|
|
334
|
+
return 'Smuggler';
|
|
335
|
+
if (/mechanic|drone|rigger|garage|airframe/.test(hay))
|
|
336
|
+
return 'Mechanic';
|
|
337
|
+
if (/gunsmith|weapons? (and armor|dealer)|armorer|ironmonger/.test(hay))
|
|
338
|
+
return 'Arms Dealer';
|
|
339
|
+
if (/bartender|barkeep|tends the .{0,40}bar|pour(s|ed) drinks/.test(hay))
|
|
340
|
+
return 'Bartender';
|
|
341
|
+
if (/decker|data broker|host|electronics/.test(hay))
|
|
342
|
+
return 'Data Broker';
|
|
343
|
+
if (/priest|sister|chapel|choir|shaman/.test(hay))
|
|
344
|
+
return 'Spiritual';
|
|
345
|
+
if (/noodle|food|cook|kitchen|vendor|stall|merchant|shopkeep|market/.test(hay))
|
|
346
|
+
return 'Vendor';
|
|
347
|
+
if (/gang|muscle|enforcer|lieutenant|sentry|lookout|bruiser/.test(hay))
|
|
348
|
+
return 'Muscle';
|
|
349
|
+
return 'Street Contact';
|
|
350
|
+
}
|
|
156
351
|
static connectionFor(npc) {
|
|
157
352
|
const hay = `${npc.description ?? ''} ${npc.name}`.toLowerCase();
|
|
158
353
|
if (npc.name === FIXER_NAME || /fixer|johnson/.test(hay))
|
|
@@ -180,7 +375,7 @@ export default class Game {
|
|
|
180
375
|
continue;
|
|
181
376
|
if (this.contacts.has(actor.name))
|
|
182
377
|
continue;
|
|
183
|
-
this.contacts.set(actor.name, { connection: Game.connectionFor(actor), loyalty: 1, spent: 0, gigs: 0, asked: false });
|
|
378
|
+
this.contacts.set(actor.name, { connection: Game.connectionFor(actor), loyalty: 1, spent: 0, gigs: 0, asked: false, role: Game.roleOf(actor) });
|
|
184
379
|
}
|
|
185
380
|
Logger.getInstance().write(`Home turf: ${this.contacts.size} contacts seeded from the hub cast.`);
|
|
186
381
|
}
|
|
@@ -419,6 +614,19 @@ export default class Game {
|
|
|
419
614
|
* cumulative spend raises it at each full 1,000 nuyen (cap 3).
|
|
420
615
|
* Returns a player-facing line when something changed, else null.
|
|
421
616
|
*/
|
|
617
|
+
/** A hub-cast member by name, for roster displays that need the live
|
|
618
|
+
* actor (contacts.ts reads their commlink to show reachability). */
|
|
619
|
+
hubActor(name) {
|
|
620
|
+
return this._hubScene?.allActors?.find(a => a.name === name && a !== this.player);
|
|
621
|
+
}
|
|
622
|
+
/** The whole hub cast -- call.ts merges these into its candidates so
|
|
623
|
+
* a linked contact answers from ANYWHERE (a real mid-run session:
|
|
624
|
+
* "call Ratchet" bounced with "you don't know anyone by that name"
|
|
625
|
+
* while the roster promised 📞). The hub Scene instance survives
|
|
626
|
+
* runs, so these are live actors, not ghosts. */
|
|
627
|
+
hubCast() {
|
|
628
|
+
return this._hubScene?.allActors?.filter(a => a !== this.player) ?? [];
|
|
629
|
+
}
|
|
422
630
|
touchContact(name, kind, spent = 0) {
|
|
423
631
|
if (!this.hasHub)
|
|
424
632
|
return null;
|
|
@@ -427,7 +635,7 @@ export default class Game {
|
|
|
427
635
|
return null;
|
|
428
636
|
let c = this.contacts.get(name);
|
|
429
637
|
if (!c) {
|
|
430
|
-
c = { connection: Game.connectionFor(npc), loyalty: 1, spent: 0, gigs: 0, asked: false };
|
|
638
|
+
c = { connection: Game.connectionFor(npc), loyalty: 1, spent: 0, gigs: 0, asked: false, role: Game.roleOf(npc) };
|
|
431
639
|
this.contacts.set(name, c);
|
|
432
640
|
if (kind === 'trade')
|
|
433
641
|
c.spent += spent;
|
|
@@ -623,6 +831,18 @@ export default class Game {
|
|
|
623
831
|
// "call Mr. Johnson" hears the offer, "travel" goes.
|
|
624
832
|
_pendingRunSeed;
|
|
625
833
|
_pendingRunTier;
|
|
834
|
+
// The explicit handshake (player ruling, commands/job-offer.ts):
|
|
835
|
+
// "accept" locks the offer -- rate final, haggle closed, decline
|
|
836
|
+
// refused -- until "call taxi" installs it. Reset wherever the
|
|
837
|
+
// pending seed changes hands.
|
|
838
|
+
pendingRunAccepted = false;
|
|
839
|
+
/** Trash the offer wholesale ("decline", and any future fixer snub). */
|
|
840
|
+
clearPendingRun() {
|
|
841
|
+
this._pendingRunSeed = undefined;
|
|
842
|
+
this._pendingRunTier = undefined;
|
|
843
|
+
this._pendingRide = undefined;
|
|
844
|
+
this.pendingRunAccepted = false;
|
|
845
|
+
}
|
|
626
846
|
get pendingRun() {
|
|
627
847
|
return this._pendingRunSeed;
|
|
628
848
|
}
|
|
@@ -731,6 +951,23 @@ export default class Game {
|
|
|
731
951
|
CommandFactory.registerCommand('reload', ReloadCommand);
|
|
732
952
|
CommandFactory.registerCommand('subdue', SubdueCommand);
|
|
733
953
|
// SUBDUING p.195 (see commands/grapple.ts): restrain without damage.
|
|
954
|
+
// Canon skill hooks (see SkillName in models/player.ts).
|
|
955
|
+
CommandFactory.registerCommand('palm', PalmCommand);
|
|
956
|
+
CommandFactory.registerCommand('perform', PerformCommand);
|
|
957
|
+
// LEADERSHIP (commands/lead.ts): rally/direct/inspire + Command recruiting.
|
|
958
|
+
CommandFactory.registerCommand('lead', LeadCommand);
|
|
959
|
+
CommandFactory.registerCommand('command', RecruitCommand);
|
|
960
|
+
// DOCWAGON (commands/docwagon.ts): the wristband between you and the morgue.
|
|
961
|
+
CommandFactory.registerCommand('docwagon', DocwagonCommand);
|
|
962
|
+
// SWAG (commands/source.ts): contacts shop the black market for you.
|
|
963
|
+
CommandFactory.registerCommand('source', SourceCommand);
|
|
964
|
+
CommandFactory.registerCommand('catalog', CatalogCommand);
|
|
965
|
+
CommandFactory.registerCommand('procure', SourceCommand);
|
|
966
|
+
// Job-offer outcomes (commands/job-offer.ts): explicit accept/decline.
|
|
967
|
+
CommandFactory.registerCommand('accept', AcceptCommand);
|
|
968
|
+
CommandFactory.registerCommand('decline', DeclineCommand);
|
|
969
|
+
CommandFactory.registerCommand('deny', DeclineCommand);
|
|
970
|
+
CommandFactory.registerCommand('refuse', DeclineCommand);
|
|
734
971
|
CommandFactory.registerCommand('grapple', GrappleCommand);
|
|
735
972
|
CommandFactory.registerCommand('restrain', GrappleCommand);
|
|
736
973
|
CommandFactory.registerCommand('clinch', GrappleCommand);
|
|
@@ -786,6 +1023,9 @@ export default class Game {
|
|
|
786
1023
|
CommandFactory.registerCommand('deploy', DeployCommand);
|
|
787
1024
|
CommandFactory.registerCommand('recall', RecallCommand);
|
|
788
1025
|
CommandFactory.registerCommand('agent', AgentCommand);
|
|
1026
|
+
CommandFactory.registerCommand('deck', DeckCommand);
|
|
1027
|
+
CommandFactory.registerCommand('load', LoadProgramCommand);
|
|
1028
|
+
CommandFactory.registerCommand('unload', UnloadProgramCommand);
|
|
789
1029
|
CommandFactory.registerCommand('install', InstallCommand);
|
|
790
1030
|
CommandFactory.registerCommand('chrome', InstallCommand);
|
|
791
1031
|
CommandFactory.registerCommand('buy', BuyCommand);
|
|
@@ -860,6 +1100,9 @@ export default class Game {
|
|
|
860
1100
|
this._useAi = options.useAi;
|
|
861
1101
|
this._logFilePath = options.logFilePath;
|
|
862
1102
|
this._saveFilePath = options.saveFilePath;
|
|
1103
|
+
// The swag catalog warms from maka-cli.com in the background
|
|
1104
|
+
// (utilities/catalog.ts): live list > local cache > baked-in floor.
|
|
1105
|
+
void refreshCatalog();
|
|
863
1106
|
this.initInputBox(screen);
|
|
864
1107
|
this.initLogBox(screen);
|
|
865
1108
|
this.initMetaBox(screen);
|
|
@@ -982,6 +1225,39 @@ export default class Game {
|
|
|
982
1225
|
this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
|
|
983
1226
|
this._tier = save.game.tier;
|
|
984
1227
|
this.contacts = new Map(save.game.contacts);
|
|
1228
|
+
// Roles are DERIVED, not earned -- re-derive them all on every
|
|
1229
|
+
// resume so old saves self-heal (pre-role saves read all "Street
|
|
1230
|
+
// Contact"; the next vintage had Johnson stored as "Fixer" before
|
|
1231
|
+
// the Job Broker split). Missing actors keep whatever the save said.
|
|
1232
|
+
for (const [name, c] of this.contacts) {
|
|
1233
|
+
const npc = this._hubScene?.allActors?.find(a => a.name === name);
|
|
1234
|
+
if (npc)
|
|
1235
|
+
c.role = Game.roleOf(npc);
|
|
1236
|
+
else if (!c.role)
|
|
1237
|
+
c.role = 'Street Contact';
|
|
1238
|
+
}
|
|
1239
|
+
// SEED-TRUTH for commlinks: the hub overlay replaces NPC inventories
|
|
1240
|
+
// wholesale, so a link granted to the CAST by a newer seed (player
|
|
1241
|
+
// request: Torque, Ratchet, Doc, Old Cray, Skitter, and Whisper all
|
|
1242
|
+
// answer calls now) would be wiped by an older save. Re-grant any
|
|
1243
|
+
// seed-held commlink its bearer no longer carries.
|
|
1244
|
+
for (const seedItem of this._hubSeed?.items ?? []) {
|
|
1245
|
+
if (seedItem.category !== 'Commlink' || !seedItem.heldBy)
|
|
1246
|
+
continue;
|
|
1247
|
+
const bearer = this._hubScene?.allActors?.find(a => a.name === seedItem.heldBy);
|
|
1248
|
+
if (!bearer)
|
|
1249
|
+
continue;
|
|
1250
|
+
const hasLink = bearer.inventory.getAllItems().some(i => i.category === Category.Commlink)
|
|
1251
|
+
|| bearer.equipment.head?.commlink != null;
|
|
1252
|
+
if (!hasLink) {
|
|
1253
|
+
bearer.inventory.addItem(restoreItem(seedItem));
|
|
1254
|
+
Logger.getInstance().write(`Resume heal: re-granted "${seedItem.name}" to ${seedItem.heldBy}.`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
this.pendingProcurements = [...(save.game.procurements ?? [])];
|
|
1258
|
+
this.docwagon = save.game.docwagon ? { ...save.game.docwagon } : undefined;
|
|
1259
|
+
this.enlistedAlly = save.game.enlistedAlly ? { ...save.game.enlistedAlly } : undefined;
|
|
1260
|
+
this.commandRefusals = { ...(save.game.commandRefusals ?? {}) };
|
|
985
1261
|
this.fencedCategories = new Set(save.game.fencedCategories);
|
|
986
1262
|
this._rumorIndex = save.game.rumorIndex;
|
|
987
1263
|
this.activeRumor = save.game.activeRumor;
|
|
@@ -1022,6 +1298,15 @@ export default class Game {
|
|
|
1022
1298
|
return;
|
|
1023
1299
|
try {
|
|
1024
1300
|
const save = this.buildSaveFile(reason);
|
|
1301
|
+
// Carry the cloud-sync stamp FORWARD through local writes: the
|
|
1302
|
+
// stamp means "the cloud's savedAt as of our last mirror", and a
|
|
1303
|
+
// new local beat doesn't change what the cloud holds. Without
|
|
1304
|
+
// this, every quit (whose async push can't outlive the process)
|
|
1305
|
+
// left a stampless file that the next launch misread as a FORK
|
|
1306
|
+
// and prompted over (a real session hit it on every reload).
|
|
1307
|
+
const prev = readSave(this._saveFilePath);
|
|
1308
|
+
if (prev.ok && prev.save.sync)
|
|
1309
|
+
save.sync = prev.save.sync;
|
|
1025
1310
|
writeSaveAtomic(this._saveFilePath, save);
|
|
1026
1311
|
logger.write(`Saved (${reason}) -> ${this._saveFilePath}`);
|
|
1027
1312
|
// Local-first: the beat is DONE the moment the file lands. The
|
|
@@ -1053,6 +1338,10 @@ export default class Game {
|
|
|
1053
1338
|
rentDebt: this.rentDebt,
|
|
1054
1339
|
tier: this._tier,
|
|
1055
1340
|
contacts: [...this.contacts.entries()],
|
|
1341
|
+
procurements: [...this.pendingProcurements],
|
|
1342
|
+
docwagon: this.docwagon ? { ...this.docwagon } : undefined,
|
|
1343
|
+
enlistedAlly: this.enlistedAlly ? { ...this.enlistedAlly } : undefined,
|
|
1344
|
+
commandRefusals: { ...this.commandRefusals },
|
|
1056
1345
|
fencedCategories: [...this.fencedCategories],
|
|
1057
1346
|
rumorIndex: this._rumorIndex,
|
|
1058
1347
|
activeRumor: this.activeRumor,
|
|
@@ -1181,6 +1470,8 @@ export default class Game {
|
|
|
1181
1470
|
// pull each per homecoming (see commands/ask.ts).
|
|
1182
1471
|
for (const contact of this.contacts.values())
|
|
1183
1472
|
contact.asked = false;
|
|
1473
|
+
// Fresh material for the buskers too (commands/perform.ts).
|
|
1474
|
+
this.performedThisCycle = false;
|
|
1184
1475
|
// LIFESTYLE upkeep comes due with the homecoming: this tier's share
|
|
1185
1476
|
// plus any standing debt, paid from carried cred as far as it goes.
|
|
1186
1477
|
// Coming up short SLIDES YOU DOWN a tier (the district notices) and
|
|
@@ -1230,14 +1521,56 @@ export default class Game {
|
|
|
1230
1521
|
// Drones spin back up beside the cot.
|
|
1231
1522
|
const companionArrivalLines = this.unfoldCompanionsOnArrival();
|
|
1232
1523
|
const companionBlock = [...companionFoldLines, ...companionArrivalLines];
|
|
1524
|
+
// The commanded ally's tour ends with the run (RAW's Command lasts
|
|
1525
|
+
// turns; ours lasts the job): the crew disbands at the curb.
|
|
1526
|
+
let allyLine = '';
|
|
1527
|
+
if (this.enlistedAlly) {
|
|
1528
|
+
allyLine = `\n${this.enlistedAlly.name} peels off at the curb -- "Call it even, chummer." The crew's back to just you.`;
|
|
1529
|
+
this.enlistedAlly = undefined;
|
|
1530
|
+
}
|
|
1531
|
+
// DOCWAGON's year ticks in homecomings (12 = a street year, player
|
|
1532
|
+
// ruling): the contract lapses quietly unless renewed at Doc's.
|
|
1533
|
+
let docwagonLine = '';
|
|
1534
|
+
if (this.docwagon) {
|
|
1535
|
+
this.docwagon.homecomingsLeft -= 1;
|
|
1536
|
+
if (this.docwagon.homecomingsLeft <= 0) {
|
|
1537
|
+
docwagonLine = `\nYour DocWagon ${this.docwagon.tier} contract LAPSES -- the wristband goes dark.${hint(` ("docwagon <tier>" at Doc's renews it.)`)}`;
|
|
1538
|
+
this.docwagon = undefined;
|
|
1539
|
+
}
|
|
1540
|
+
else if (this.docwagon.homecomingsLeft <= 2) {
|
|
1541
|
+
docwagonLine = `\nDocWagon renewal coming due: ${this.docwagon.homecomingsLeft} homecoming${this.docwagon.homecomingsLeft === 1 ? '' : 's'} left on the ${this.docwagon.tier} contract.`;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// SWAG deliveries (commands/source.ts): the contact's shopping run
|
|
1545
|
+
// lands with the homecoming -- straight into your gear, paid for at
|
|
1546
|
+
// order time.
|
|
1547
|
+
let deliveryLine = '';
|
|
1548
|
+
if (this.pendingProcurements.length > 0) {
|
|
1549
|
+
const delivered = [];
|
|
1550
|
+
for (const order of this.pendingProcurements.splice(0)) {
|
|
1551
|
+
try {
|
|
1552
|
+
const item = restoreItem(order.itemConfig);
|
|
1553
|
+
item.owner = this.player.name;
|
|
1554
|
+
this.player.inventory.addItem(item);
|
|
1555
|
+
delivered.push(`${item.name} (${order.contact} came through)`);
|
|
1556
|
+
}
|
|
1557
|
+
catch (err) {
|
|
1558
|
+
Logger.getInstance().error(`Procurement delivery failed: ${err}`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (delivered.length > 0) {
|
|
1562
|
+
this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
|
|
1563
|
+
deliveryLine = `\nWaiting with your gear: ${delivered.join(', ')}.`;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1233
1566
|
// The homecoming save: everything above (payout, rent, restock,
|
|
1234
|
-
// contacts reset) settled first, so the save reflects
|
|
1235
|
-
// the player will find it.
|
|
1567
|
+
// contacts reset, deliveries) settled first, so the save reflects
|
|
1568
|
+
// the hub as the player will find it.
|
|
1236
1569
|
this.requestSave('homecoming');
|
|
1237
1570
|
const fateLine = wrappedRun
|
|
1238
1571
|
? `The run's wrapped and the street swallows you back up.`
|
|
1239
1572
|
: `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
|
|
1240
|
-
return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}\n${rentLine}${comfortLine}${restockLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
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')}` : ''}`;
|
|
1241
1574
|
}
|
|
1242
1575
|
/**
|
|
1243
1576
|
* The actual drafting work, split out from continueToNextScene() purely
|
|
@@ -1282,6 +1615,7 @@ export default class Game {
|
|
|
1282
1615
|
this._pendingRunSeed = nextSeed;
|
|
1283
1616
|
this._pendingRunTier = nextTier;
|
|
1284
1617
|
this._pendingRide = Game.RIDE_POOL[Math.floor(Math.random() * Game.RIDE_POOL.length)];
|
|
1618
|
+
this.pendingRunAccepted = false;
|
|
1285
1619
|
return `Your link pings -- 1 new message: ${FIXER_NAME}.${hint(` ("call ${FIXER_NAME}" to hear the job.)`)}`;
|
|
1286
1620
|
}
|
|
1287
1621
|
// Classic sessions transition immediately, hideout-first, as always.
|
|
@@ -1335,11 +1669,13 @@ export default class Game {
|
|
|
1335
1669
|
// Deployed drones spin back up at the curb -- BEFORE appearance
|
|
1336
1670
|
// warming so re-shelled companions get warmed with the cast.
|
|
1337
1671
|
const arrivalLines = this.unfoldCompanionsOnArrival();
|
|
1672
|
+
// A commanded ally meets you at the jobsite (commands/lead.ts).
|
|
1673
|
+
const allyLines = this.spawnEnlistedAlly();
|
|
1338
1674
|
this.warmNpcAppearancesInBackground();
|
|
1339
1675
|
this.refreshLogLabel();
|
|
1340
1676
|
// No "=== JOB #n ===" banner on entry (player request) -- the Game
|
|
1341
1677
|
// Log window's own title carries the job name now.
|
|
1342
|
-
const companionBlock = [...companionLines, ...arrivalLines];
|
|
1678
|
+
const companionBlock = [...companionLines, ...arrivalLines, ...allyLines];
|
|
1343
1679
|
return `\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
1344
1680
|
}
|
|
1345
1681
|
/**
|
|
@@ -1426,16 +1762,12 @@ export default class Game {
|
|
|
1426
1762
|
const haggledAtOffer = this.player.haggledThisJob;
|
|
1427
1763
|
try {
|
|
1428
1764
|
const banner = await this.activateRunScene(seed, tier, { startInHideout: false });
|
|
1429
|
-
this.
|
|
1430
|
-
this._pendingRunTier = undefined;
|
|
1431
|
-
this._pendingRide = undefined;
|
|
1765
|
+
this.clearPendingRun();
|
|
1432
1766
|
this.player.haggledThisJob = haggledAtOffer;
|
|
1433
1767
|
return `You travel to ${this.player.currentLocation.name}.\n${banner}`;
|
|
1434
1768
|
}
|
|
1435
1769
|
catch (err) {
|
|
1436
|
-
this.
|
|
1437
|
-
this._pendingRunTier = undefined;
|
|
1438
|
-
this._pendingRide = undefined;
|
|
1770
|
+
this.clearPendingRun();
|
|
1439
1771
|
Logger.getInstance().error(`launchPendingRun failed: ${err}`);
|
|
1440
1772
|
return `The ride never shows and the job address goes dead. ${FIXER_NAME} will not enjoy hearing this -- call him for another.`;
|
|
1441
1773
|
}
|
|
@@ -1652,12 +1984,35 @@ export default class Game {
|
|
|
1652
1984
|
style: {
|
|
1653
1985
|
border: { fg: 'blue' }
|
|
1654
1986
|
},
|
|
1655
|
-
label
|
|
1987
|
+
// The label teaches its own scroll keys -- a real session hit
|
|
1988
|
+
// PgUp/PgDn (the world log's) and concluded the panel can't move.
|
|
1989
|
+
label: ' Mechanics (C-PgUp/Dn) ',
|
|
1656
1990
|
scrollable: true,
|
|
1657
1991
|
alwaysScroll: true,
|
|
1658
1992
|
tags: true,
|
|
1659
1993
|
});
|
|
1660
1994
|
this._metaBox = metaBox;
|
|
1995
|
+
// Scrolling (player report: menus render here now and PgUp/PgDn
|
|
1996
|
+
// don't reach it): PgUp/PgDn stay the WORLD LOG's (that binding is
|
|
1997
|
+
// itself a playtest fix -- see initLogBox), so the Mechanics column
|
|
1998
|
+
// pages on Ctrl-PgUp/PgDn. Same focus problem, same cure: the input
|
|
1999
|
+
// box holds focus forever and grabKeys mutes screen-level handlers,
|
|
2000
|
+
// so the modified names must join ignoreLocked too. Ctrl-End snaps
|
|
2001
|
+
// back to the bottom (new content always scrolls to bottom anyway).
|
|
2002
|
+
screen.ignoreLocked.push('C-pageup', 'C-pagedown', 'C-end');
|
|
2003
|
+
const metaPage = () => Math.max(1, (typeof metaBox.height === 'number' ? metaBox.height : 20) - 3);
|
|
2004
|
+
screen.key(['C-pageup'], () => {
|
|
2005
|
+
metaBox.scroll(-metaPage());
|
|
2006
|
+
screen.render();
|
|
2007
|
+
});
|
|
2008
|
+
screen.key(['C-pagedown'], () => {
|
|
2009
|
+
metaBox.scroll(metaPage());
|
|
2010
|
+
screen.render();
|
|
2011
|
+
});
|
|
2012
|
+
screen.key(['C-end'], () => {
|
|
2013
|
+
metaBox.scrollTo(metaBox.getScrollHeight());
|
|
2014
|
+
screen.render();
|
|
2015
|
+
});
|
|
1661
2016
|
screen.append(metaBox);
|
|
1662
2017
|
}
|
|
1663
2018
|
/**
|
|
@@ -1751,7 +2106,7 @@ export default class Game {
|
|
|
1751
2106
|
// Off-plane the profile is attack programs / astral will and reads as
|
|
1752
2107
|
// Armed. White, not gray, for the same legibility reason as the stance.
|
|
1753
2108
|
const armed = player.plane === 'meat' && weaponItem && !player.weaponDrawn
|
|
1754
|
-
? `{white-fg}Holstered: ${weaponItem.name}${ammoNote} -- "brandish"{/white-fg}`
|
|
2109
|
+
? `{white-fg}Holstered: ${weaponItem.name}${ammoNote}${hint(' -- "brandish"')}{/white-fg}`
|
|
1755
2110
|
: weapon.name === 'bare fists'
|
|
1756
2111
|
? `{gray-fg}Unarmed{/gray-fg}`
|
|
1757
2112
|
: `Armed: ${weapon.name} (DV ${weapon.damage}${weapon.poolBonus > 0 ? `, +${weapon.poolBonus} dice` : ''})${ammoNote}`;
|
|
@@ -1771,10 +2126,10 @@ export default class Game {
|
|
|
1771
2126
|
lines.push(`{magenta-fg}Sustained: armor +${player.sustainedArmor} soak (-2 dice to everything else){/magenta-fg}`);
|
|
1772
2127
|
}
|
|
1773
2128
|
if (player.astralPerceiving) {
|
|
1774
|
-
lines.push(`{magenta-fg}✧ ASTRAL SENSES OPEN (-2 dice sustained) -- "assense" to close{/magenta-fg}`);
|
|
2129
|
+
lines.push(`{magenta-fg}✧ ASTRAL SENSES OPEN (-2 dice sustained)${hint(' -- "assense" to close')}{/magenta-fg}`);
|
|
1775
2130
|
}
|
|
1776
2131
|
if (player.sneaking) {
|
|
1777
|
-
lines.push(`{white-fg}🐾 SNEAKING -- entries are opposed vs their perception; loud acts break it{/white-fg}`);
|
|
2132
|
+
lines.push(`{white-fg}🐾 SNEAKING${hint(' -- entries are opposed vs their perception; loud acts break it')}{/white-fg}`);
|
|
1778
2133
|
}
|
|
1779
2134
|
if (player.activeCallPartner) {
|
|
1780
2135
|
lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} On a call with ${player.activeCallPartner.name}{/${CALL_COLOR}-fg}`);
|
|
@@ -1806,11 +2161,19 @@ export default class Game {
|
|
|
1806
2161
|
// Off-plane context: which reality you're in, your persona's Matrix
|
|
1807
2162
|
// track (cold-sim damage lands there), and where your empty body lies.
|
|
1808
2163
|
if (player.plane === 'matrix') {
|
|
1809
|
-
// Matrix damage is hardware damage: show the DECK's track
|
|
2164
|
+
// Matrix damage is hardware damage: show the DECK's track -- and
|
|
2165
|
+
// its stat line: the ASDF deal ("deck config" is a live combat
|
|
2166
|
+
// decision now, so the HUD shows what you're currently rigged for)
|
|
2167
|
+
// plus running programs, first-word-abbreviated to keep the line
|
|
2168
|
+
// inside the box (Biofeedback Filter -> Biofeedback). A
|
|
1810
2169
|
// technomancer has no deck -- their stun track (already on line 2)
|
|
1811
2170
|
// is what the ice chews on.
|
|
1812
|
-
const
|
|
1813
|
-
|
|
2171
|
+
const jackedDeck = player.activeDeck;
|
|
2172
|
+
const pgms = jackedDeck && jackedDeck.loadedPrograms.length > 0
|
|
2173
|
+
? ` [${jackedDeck.loadedPrograms.map(k => getProgram(k)?.name.split(' ')[0] ?? k).join(' ')}]`
|
|
2174
|
+
: '';
|
|
2175
|
+
const track = jackedDeck
|
|
2176
|
+
? ` Deck A${jackedDeck.attack} S${jackedDeck.sleaze} D${jackedDeck.dataProcessing} F${jackedDeck.firewall} ${jackedDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}${pgms}`
|
|
1814
2177
|
: ` (living persona -- damage is REAL stun)`;
|
|
1815
2178
|
lines.push(`{cyan-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/cyan-fg}`);
|
|
1816
2179
|
}
|
|
@@ -1854,6 +2217,14 @@ export default class Game {
|
|
|
1854
2217
|
}
|
|
1855
2218
|
return Game.instance;
|
|
1856
2219
|
}
|
|
2220
|
+
/**
|
|
2221
|
+
* The verb the LAST handleInput actually ran, after typo correction
|
|
2222
|
+
* ("sjeet" -> "sheet"). ui.ts routes menu output to the Mechanics
|
|
2223
|
+
* panel on THIS, not the typed word -- a real session's corrected
|
|
2224
|
+
* "sjeet" dumped the whole sheet into the world log. The recursive
|
|
2225
|
+
* correction call overwrites it with the corrected verb.
|
|
2226
|
+
*/
|
|
2227
|
+
lastResolvedVerb = '';
|
|
1857
2228
|
async handleInput(input) {
|
|
1858
2229
|
// "jack-in"/"jack-out" read naturally but parse as unknown verbs (a
|
|
1859
2230
|
// real session typed "jack-in" and bounced) -- normalize the hyphen.
|
|
@@ -1861,6 +2232,7 @@ export default class Game {
|
|
|
1861
2232
|
const parts = normalized.trim().split(' ');
|
|
1862
2233
|
const verb = parts[0].toLowerCase();
|
|
1863
2234
|
const args = parts.slice(1);
|
|
2235
|
+
this.lastResolvedVerb = verb;
|
|
1864
2236
|
// Quitting takes a CONFIRM (player request: "I just exited by
|
|
1865
2237
|
// mistake"): the first "quit" arms it, "y"/"yes" (or quit again)
|
|
1866
2238
|
// seals it, anything else disarms and runs as the command it is.
|
|
@@ -1949,11 +2321,14 @@ export default class Game {
|
|
|
1949
2321
|
static HELP_CATEGORIES = [
|
|
1950
2322
|
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
1951
2323
|
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
|
|
1952
|
-
{ title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['
|
|
2324
|
+
{ title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['stance'], ['surrender'], ['edge'], ['rest']] },
|
|
2325
|
+
// The party layer: everyone who walks (or flies, or manifests) at
|
|
2326
|
+
// your side answers to these.
|
|
2327
|
+
{ title: 'party', entries: [['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
|
|
1953
2328
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
|
|
1954
|
-
{ title: 'matrix', entries: [['jack'], ['
|
|
1955
|
-
{ title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
1956
|
-
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['work', 'gig'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
2329
|
+
{ title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
2330
|
+
{ title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
2331
|
+
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
1957
2332
|
{ title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
|
|
1958
2333
|
{ title: 'character', entries: [['sheet', 'stats'], ['advance', 'train'], ['qualities', 'quality'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
1959
2334
|
{ title: 'session', entries: [['help'], ['hints'], ['clear'], ['quit', 'exit']] },
|
|
@@ -1961,8 +2336,8 @@ export default class Game {
|
|
|
1961
2336
|
/** Descriptions for the verbs handled inline in handleInput() rather than registered. */
|
|
1962
2337
|
static BUILTIN_DESCRIPTIONS = {
|
|
1963
2338
|
help: 'This overview. "help <category>" for one group with full descriptions; "help <command>" for one command.',
|
|
1964
|
-
clear: 'Clear the
|
|
1965
|
-
quit: 'Exit the game.',
|
|
2339
|
+
clear: 'Clear the world log and the Mechanics panel.',
|
|
2340
|
+
quit: 'Exit the game (asks to confirm; a hub campaign autosaves on the way out).',
|
|
1966
2341
|
};
|
|
1967
2342
|
describeVerb(verb) {
|
|
1968
2343
|
const builtin = Game.BUILTIN_DESCRIPTIONS[verb];
|
|
@@ -1999,14 +2374,34 @@ export default class Game {
|
|
|
1999
2374
|
}
|
|
2000
2375
|
return `No command or category called "${query}". Try plain "help".`;
|
|
2001
2376
|
}
|
|
2002
|
-
// The overview:
|
|
2377
|
+
// The overview: category lines with primaries only, chunked so no
|
|
2378
|
+
// line outgrows the Mechanics column (help renders there now --
|
|
2379
|
+
// blessed's mid-list wrap lost the left-column alignment).
|
|
2003
2380
|
const lines = ['=== COMMANDS ==='];
|
|
2004
2381
|
const pad = Math.max(...Game.HELP_CATEGORIES.map(c => c.title.length));
|
|
2382
|
+
const pushWrapped = (title, verbs) => {
|
|
2383
|
+
const rows = [];
|
|
2384
|
+
let cur = '';
|
|
2385
|
+
for (const v of verbs) {
|
|
2386
|
+
const next = cur ? `${cur}, ${v}` : v;
|
|
2387
|
+
if (next.length > 40 && cur) {
|
|
2388
|
+
rows.push(cur + ',');
|
|
2389
|
+
cur = v;
|
|
2390
|
+
}
|
|
2391
|
+
else {
|
|
2392
|
+
cur = next;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
rows.push(cur);
|
|
2396
|
+
lines.push(`${title.padEnd(pad)} ${rows[0]}`);
|
|
2397
|
+
for (const r of rows.slice(1))
|
|
2398
|
+
lines.push(`${' '.repeat(pad)} ${r}`);
|
|
2399
|
+
};
|
|
2005
2400
|
for (const category of Game.HELP_CATEGORIES) {
|
|
2006
|
-
|
|
2401
|
+
pushWrapped(category.title, category.entries.map(e => e[0]));
|
|
2007
2402
|
}
|
|
2008
2403
|
if (uncategorized.length > 0) {
|
|
2009
|
-
|
|
2404
|
+
pushWrapped('other', uncategorized);
|
|
2010
2405
|
}
|
|
2011
2406
|
lines.push('');
|
|
2012
2407
|
lines.push(`"help <category>" for a group with descriptions ("help combat"); "help <command>" for one ("help haggle").`);
|