@maka/maka-cli 5.10.0 → 5.11.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/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/call.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.d.ts +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +6 -4
- 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/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/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/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/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 +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/source.d.ts +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/source.js +159 -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 +57 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +326 -13
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +19 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +48 -9
- 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 +22 -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.sub.cmd.js +7 -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,12 @@ 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 { PerformCommand } from './commands/perform.js';
|
|
41
48
|
import { EdgeCommand } from './commands/edge.js';
|
|
42
49
|
import { SheetCommand } from './commands/sheet.js';
|
|
43
50
|
import { CallCommand } from './commands/call.js';
|
|
@@ -152,7 +159,191 @@ export default class Game {
|
|
|
152
159
|
// against the real pending-run seed. One pull per contact per
|
|
153
160
|
// homecoming (asked resets in returnToHub).
|
|
154
161
|
contacts = new Map();
|
|
162
|
+
// SWAG orders in flight (SR5e p.387/418, commands/source.ts): a
|
|
163
|
+
// contact shopping the black market for you. Paid up front on a won
|
|
164
|
+
// Availability test; the goods land at the NEXT HOMECOMING (the
|
|
165
|
+
// delivery-times table, abstracted to the job-cycle clock the same
|
|
166
|
+
// way rest is). Rides the save (ISavedGameState.procurements).
|
|
167
|
+
pendingProcurements = [];
|
|
168
|
+
// DOCWAGON (SR5e p.450, utilities/docwagon.ts): the wristband between
|
|
169
|
+
// a runner and the morgue. Rides the save; ticks down at homecomings.
|
|
170
|
+
docwagon;
|
|
171
|
+
// LEADERSHIP's Command action (SR5e p.141-142, commands/lead.ts): a
|
|
172
|
+
// recruited contact rides the NEXT run as an 'ally' companion shell
|
|
173
|
+
// (spawned at activateRunScene, released at homecoming). RAW's teeth:
|
|
174
|
+
// a refusal stands until Loyalty deepens past the level it happened
|
|
175
|
+
// at. Both ride the save.
|
|
176
|
+
enlistedAlly;
|
|
177
|
+
commandRefusals = {};
|
|
178
|
+
// PERFORMANCE's busking gate (commands/perform.ts): one set per job
|
|
179
|
+
// cycle, reset at homecoming like a contact's legwork. Deliberately
|
|
180
|
+
// NOT saved -- a relaunch is as good as a homecoming for new material.
|
|
181
|
+
performedThisCycle = false;
|
|
182
|
+
/** The recruited contact joins at the jobsite curb ("takes their own
|
|
183
|
+
* ride and meets you there" -- the companion convention). */
|
|
184
|
+
spawnEnlistedAlly() {
|
|
185
|
+
const ally = this.enlistedAlly;
|
|
186
|
+
if (!ally)
|
|
187
|
+
return [];
|
|
188
|
+
const shell = this.spawnCompanionShell({
|
|
189
|
+
name: ally.name,
|
|
190
|
+
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.`,
|
|
191
|
+
kind: 'ally',
|
|
192
|
+
combat: ally.combat,
|
|
193
|
+
room: this.player.currentLocation,
|
|
194
|
+
plane: 'meat',
|
|
195
|
+
force: Number(ally.combat.body ?? 3),
|
|
196
|
+
});
|
|
197
|
+
return shell ? [`${ally.name} is already leaning on the wall when you arrive -- "Took my own ride. Let's work."`] : [];
|
|
198
|
+
}
|
|
199
|
+
/** Bills a DocWagon fee: cred first, shortfall onto the rent tab (the
|
|
200
|
+
* street's one universal collections agency). Returns the line. */
|
|
201
|
+
billDocwagon(amount, label) {
|
|
202
|
+
if (amount <= 0)
|
|
203
|
+
return '';
|
|
204
|
+
const paid = Math.min(this.player.currency, amount);
|
|
205
|
+
if (paid > 0)
|
|
206
|
+
this.player.adjustCurrency(-paid);
|
|
207
|
+
const owed = amount - paid;
|
|
208
|
+
if (owed > 0)
|
|
209
|
+
this.rentDebt += owed;
|
|
210
|
+
return owed > 0
|
|
211
|
+
? `DocWagon bills ${amount} for ${label} -- ${paid} paid, ${owed} on your tab.`
|
|
212
|
+
: `DocWagon bills ${amount} for ${label}.`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The band answers (SR5e p.450). 'htr': a High Threat Response team
|
|
216
|
+
* extracts a flatlining KO'd runner instead of the street deciding.
|
|
217
|
+
* 'resusc': clinical death reversed -- a free resuscitation or the
|
|
218
|
+
* 5,000 fee -- the sanctioned carve-out to permadeath (player ruling;
|
|
219
|
+
* overflow-past-Body instant death isn't modeled yet, so every death
|
|
220
|
+
* is currently within reach of the crash cart). Returns true when the
|
|
221
|
+
* rescue engages -- the caller must then NOT end the game.
|
|
222
|
+
*/
|
|
223
|
+
tryDocwagonRescue(kind, opts = {}) {
|
|
224
|
+
const contract = this.docwagon;
|
|
225
|
+
if (!contract || !this.initialized || this._dead)
|
|
226
|
+
return false;
|
|
227
|
+
const tier = DOCWAGON_TIERS[contract.tier];
|
|
228
|
+
const logger = Logger.getInstance();
|
|
229
|
+
const bills = [];
|
|
230
|
+
if (kind === 'resusc') {
|
|
231
|
+
if (contract.resuscsLeft > 0) {
|
|
232
|
+
contract.resuscsLeft -= 1;
|
|
233
|
+
bills.push(`A contract resuscitation is burned -- ${contract.resuscsLeft} left this year.`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
bills.push(this.billDocwagon(RESUSC_FEE, 'resuscitation'));
|
|
237
|
+
}
|
|
238
|
+
// Back from the white light, barely: two boxes shy of the track.
|
|
239
|
+
const p = this.player;
|
|
240
|
+
p.healDamage(Math.max(0, p.damageTaken - (p.maxConditionBoxes - 2)));
|
|
241
|
+
p.healStun(Math.max(0, p.stunTaken - Math.max(0, p.maxStunBoxes - 2)));
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
bills.push(this.billDocwagon(tier.htrFee, 'High Threat Response'));
|
|
245
|
+
// Extraction under fire: hostiles still standing can cost DocWagon
|
|
246
|
+
// a medic -- and the client eats the 20,000 death compensation
|
|
247
|
+
// (p.450), unless the contract says otherwise.
|
|
248
|
+
const hostiles = opts.standingHostiles ?? [];
|
|
249
|
+
if (hostiles.length > 0 && tier.deathComp) {
|
|
250
|
+
const casualtyRoll = rollPool(Math.min(4, hostiles.length));
|
|
251
|
+
if (casualtyRoll.hits > 0) {
|
|
252
|
+
bills.push(this.billDocwagon(DEATH_COMP, 'employee death compensation -- a medic did not make it out'));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Conscious enough to be carried: the band's medics stabilize.
|
|
256
|
+
const p = this.player;
|
|
257
|
+
p.healStun(Math.max(0, p.stunTaken - Math.max(0, p.maxStunBoxes - 2)));
|
|
258
|
+
}
|
|
259
|
+
logger.logWithColor(kind === 'resusc'
|
|
260
|
+
? `${CALL_ICON} Your wristband SCREAMS the flatline. Ten minutes later there is foam, light, and a trauma team refusing to let you go.`
|
|
261
|
+
: `${CALL_ICON} The band reads the flatline and howls -- a DocWagon HTR team storms the scene behind riot shields and suppression foam.`, CALL_COLOR);
|
|
262
|
+
for (const b of bills.filter(Boolean))
|
|
263
|
+
logger.logWithColor(b, 'yellow');
|
|
264
|
+
this.scene.addWorldEvent(`A DocWagon team extracted ${this.player.name} under the ${contract.tier} contract.`);
|
|
265
|
+
void this.docwagonMedevac();
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
/** Where the wagon drops you (player ruling: the CLINIC, not your own
|
|
269
|
+
* cot): the district Street Doc's room, generically -- so generated
|
|
270
|
+
* hubs work too. Falls back to the hideout if the hub has no doc. */
|
|
271
|
+
docwagonDropOff() {
|
|
272
|
+
for (const [name, c] of this.contacts) {
|
|
273
|
+
if (c.role === 'Street Doc') {
|
|
274
|
+
const doc = this.hubActor(name);
|
|
275
|
+
if (doc?.currentLocation)
|
|
276
|
+
return doc.currentLocation;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return this._homeRoom;
|
|
280
|
+
}
|
|
281
|
+
/** The ride out on a stretcher: mid-run this is a homecoming (rent and
|
|
282
|
+
* all), then the recovery bed at the clinic. */
|
|
283
|
+
async docwagonMedevac() {
|
|
284
|
+
const logger = Logger.getInstance();
|
|
285
|
+
try {
|
|
286
|
+
const clinic = this.docwagonDropOff();
|
|
287
|
+
if (this.hasHub && this.scene !== this._hubScene) {
|
|
288
|
+
const home = await this.continueToNextScene();
|
|
289
|
+
logger.log(home);
|
|
290
|
+
}
|
|
291
|
+
this.player.currentLocation = clinic;
|
|
292
|
+
this.player.cameFrom = undefined;
|
|
293
|
+
this.updateExits(this.player.currentLocation);
|
|
294
|
+
this.updateStatus();
|
|
295
|
+
logger.log(clinic === this._homeRoom
|
|
296
|
+
? `You wake on your own cot, taped, stapled, and alive.`
|
|
297
|
+
: `You wake on a recovery cot under bad fluorescents -- ${clinic.name}. Taped, stapled, and alive.`);
|
|
298
|
+
this.requestSave('homecoming');
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
logger.error(`docwagonMedevac failed: ${err}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
155
304
|
/** How much juice the name carries, read off the role. */
|
|
305
|
+
/**
|
|
306
|
+
* A contact's ROLE, the SR5e archetype list (p.98: fixer, arms dealer,
|
|
307
|
+
* smuggler, talismonger, street doc, mechanic, bartender...) -- same
|
|
308
|
+
* keyword school as connectionFor below. Stored on the contact entry
|
|
309
|
+
* at open time so the roster (and the save) carry it.
|
|
310
|
+
*/
|
|
311
|
+
static roleOf(npc) {
|
|
312
|
+
const hay = `${npc.description ?? ''} ${npc.name}`.toLowerCase();
|
|
313
|
+
// Mr. Johnson is NOT the fixer (player ruling: don't overload his
|
|
314
|
+
// role). Canon splits them: a Johnson is the anonymous CLIENT who
|
|
315
|
+
// commissions the run; a fixer is the street middleman (scene1's
|
|
316
|
+
// Mr. Krow, e.g.). He gets his own archetype label.
|
|
317
|
+
if (npc.name === FIXER_NAME || /job broker|johnson/.test(hay))
|
|
318
|
+
return 'Job Broker';
|
|
319
|
+
if (/fixer/.test(hay))
|
|
320
|
+
return 'Fixer';
|
|
321
|
+
// "charms" deliberately absent: a market kid hawking LUCKY charms is
|
|
322
|
+
// a Vendor, not a talismonger (Skitter proved it).
|
|
323
|
+
if (/talismonger|magical goods|foci|spell formula/.test(hay))
|
|
324
|
+
return 'Talismonger';
|
|
325
|
+
if (/street doc|clinic|surgeon|medic|stitch|chop shop/.test(hay))
|
|
326
|
+
return 'Street Doc';
|
|
327
|
+
if (/fence|pawn|launder|no questions/.test(hay))
|
|
328
|
+
return 'Fence';
|
|
329
|
+
if (/smuggl/.test(hay))
|
|
330
|
+
return 'Smuggler';
|
|
331
|
+
if (/mechanic|drone|rigger|garage|airframe/.test(hay))
|
|
332
|
+
return 'Mechanic';
|
|
333
|
+
if (/gunsmith|weapons? (and armor|dealer)|armorer|ironmonger/.test(hay))
|
|
334
|
+
return 'Arms Dealer';
|
|
335
|
+
if (/bartender|barkeep|tends the .{0,40}bar|pour(s|ed) drinks/.test(hay))
|
|
336
|
+
return 'Bartender';
|
|
337
|
+
if (/decker|data broker|host|electronics/.test(hay))
|
|
338
|
+
return 'Data Broker';
|
|
339
|
+
if (/priest|sister|chapel|choir|shaman/.test(hay))
|
|
340
|
+
return 'Spiritual';
|
|
341
|
+
if (/noodle|food|cook|kitchen|vendor|stall|merchant|shopkeep|market/.test(hay))
|
|
342
|
+
return 'Vendor';
|
|
343
|
+
if (/gang|muscle|enforcer|lieutenant|sentry|lookout|bruiser/.test(hay))
|
|
344
|
+
return 'Muscle';
|
|
345
|
+
return 'Street Contact';
|
|
346
|
+
}
|
|
156
347
|
static connectionFor(npc) {
|
|
157
348
|
const hay = `${npc.description ?? ''} ${npc.name}`.toLowerCase();
|
|
158
349
|
if (npc.name === FIXER_NAME || /fixer|johnson/.test(hay))
|
|
@@ -180,7 +371,7 @@ export default class Game {
|
|
|
180
371
|
continue;
|
|
181
372
|
if (this.contacts.has(actor.name))
|
|
182
373
|
continue;
|
|
183
|
-
this.contacts.set(actor.name, { connection: Game.connectionFor(actor), loyalty: 1, spent: 0, gigs: 0, asked: false });
|
|
374
|
+
this.contacts.set(actor.name, { connection: Game.connectionFor(actor), loyalty: 1, spent: 0, gigs: 0, asked: false, role: Game.roleOf(actor) });
|
|
184
375
|
}
|
|
185
376
|
Logger.getInstance().write(`Home turf: ${this.contacts.size} contacts seeded from the hub cast.`);
|
|
186
377
|
}
|
|
@@ -419,6 +610,11 @@ export default class Game {
|
|
|
419
610
|
* cumulative spend raises it at each full 1,000 nuyen (cap 3).
|
|
420
611
|
* Returns a player-facing line when something changed, else null.
|
|
421
612
|
*/
|
|
613
|
+
/** A hub-cast member by name, for roster displays that need the live
|
|
614
|
+
* actor (contacts.ts reads their commlink to show reachability). */
|
|
615
|
+
hubActor(name) {
|
|
616
|
+
return this._hubScene?.allActors?.find(a => a.name === name && a !== this.player);
|
|
617
|
+
}
|
|
422
618
|
touchContact(name, kind, spent = 0) {
|
|
423
619
|
if (!this.hasHub)
|
|
424
620
|
return null;
|
|
@@ -427,7 +623,7 @@ export default class Game {
|
|
|
427
623
|
return null;
|
|
428
624
|
let c = this.contacts.get(name);
|
|
429
625
|
if (!c) {
|
|
430
|
-
c = { connection: Game.connectionFor(npc), loyalty: 1, spent: 0, gigs: 0, asked: false };
|
|
626
|
+
c = { connection: Game.connectionFor(npc), loyalty: 1, spent: 0, gigs: 0, asked: false, role: Game.roleOf(npc) };
|
|
431
627
|
this.contacts.set(name, c);
|
|
432
628
|
if (kind === 'trade')
|
|
433
629
|
c.spent += spent;
|
|
@@ -623,6 +819,18 @@ export default class Game {
|
|
|
623
819
|
// "call Mr. Johnson" hears the offer, "travel" goes.
|
|
624
820
|
_pendingRunSeed;
|
|
625
821
|
_pendingRunTier;
|
|
822
|
+
// The explicit handshake (player ruling, commands/job-offer.ts):
|
|
823
|
+
// "accept" locks the offer -- rate final, haggle closed, decline
|
|
824
|
+
// refused -- until "call taxi" installs it. Reset wherever the
|
|
825
|
+
// pending seed changes hands.
|
|
826
|
+
pendingRunAccepted = false;
|
|
827
|
+
/** Trash the offer wholesale ("decline", and any future fixer snub). */
|
|
828
|
+
clearPendingRun() {
|
|
829
|
+
this._pendingRunSeed = undefined;
|
|
830
|
+
this._pendingRunTier = undefined;
|
|
831
|
+
this._pendingRide = undefined;
|
|
832
|
+
this.pendingRunAccepted = false;
|
|
833
|
+
}
|
|
626
834
|
get pendingRun() {
|
|
627
835
|
return this._pendingRunSeed;
|
|
628
836
|
}
|
|
@@ -731,6 +939,22 @@ export default class Game {
|
|
|
731
939
|
CommandFactory.registerCommand('reload', ReloadCommand);
|
|
732
940
|
CommandFactory.registerCommand('subdue', SubdueCommand);
|
|
733
941
|
// SUBDUING p.195 (see commands/grapple.ts): restrain without damage.
|
|
942
|
+
// Canon skill hooks (see SkillName in models/player.ts).
|
|
943
|
+
CommandFactory.registerCommand('palm', PalmCommand);
|
|
944
|
+
CommandFactory.registerCommand('perform', PerformCommand);
|
|
945
|
+
// LEADERSHIP (commands/lead.ts): rally/direct/inspire + Command recruiting.
|
|
946
|
+
CommandFactory.registerCommand('lead', LeadCommand);
|
|
947
|
+
CommandFactory.registerCommand('command', RecruitCommand);
|
|
948
|
+
// DOCWAGON (commands/docwagon.ts): the wristband between you and the morgue.
|
|
949
|
+
CommandFactory.registerCommand('docwagon', DocwagonCommand);
|
|
950
|
+
// SWAG (commands/source.ts): contacts shop the black market for you.
|
|
951
|
+
CommandFactory.registerCommand('source', SourceCommand);
|
|
952
|
+
CommandFactory.registerCommand('procure', SourceCommand);
|
|
953
|
+
// Job-offer outcomes (commands/job-offer.ts): explicit accept/decline.
|
|
954
|
+
CommandFactory.registerCommand('accept', AcceptCommand);
|
|
955
|
+
CommandFactory.registerCommand('decline', DeclineCommand);
|
|
956
|
+
CommandFactory.registerCommand('deny', DeclineCommand);
|
|
957
|
+
CommandFactory.registerCommand('refuse', DeclineCommand);
|
|
734
958
|
CommandFactory.registerCommand('grapple', GrappleCommand);
|
|
735
959
|
CommandFactory.registerCommand('restrain', GrappleCommand);
|
|
736
960
|
CommandFactory.registerCommand('clinch', GrappleCommand);
|
|
@@ -982,6 +1206,39 @@ export default class Game {
|
|
|
982
1206
|
this.player.lifestyleEdgeBonus = this.lifestyle.edgeBonus;
|
|
983
1207
|
this._tier = save.game.tier;
|
|
984
1208
|
this.contacts = new Map(save.game.contacts);
|
|
1209
|
+
// Roles are DERIVED, not earned -- re-derive them all on every
|
|
1210
|
+
// resume so old saves self-heal (pre-role saves read all "Street
|
|
1211
|
+
// Contact"; the next vintage had Johnson stored as "Fixer" before
|
|
1212
|
+
// the Job Broker split). Missing actors keep whatever the save said.
|
|
1213
|
+
for (const [name, c] of this.contacts) {
|
|
1214
|
+
const npc = this._hubScene?.allActors?.find(a => a.name === name);
|
|
1215
|
+
if (npc)
|
|
1216
|
+
c.role = Game.roleOf(npc);
|
|
1217
|
+
else if (!c.role)
|
|
1218
|
+
c.role = 'Street Contact';
|
|
1219
|
+
}
|
|
1220
|
+
// SEED-TRUTH for commlinks: the hub overlay replaces NPC inventories
|
|
1221
|
+
// wholesale, so a link granted to the CAST by a newer seed (player
|
|
1222
|
+
// request: Torque, Ratchet, Doc, Old Cray, Skitter, and Whisper all
|
|
1223
|
+
// answer calls now) would be wiped by an older save. Re-grant any
|
|
1224
|
+
// seed-held commlink its bearer no longer carries.
|
|
1225
|
+
for (const seedItem of this._hubSeed?.items ?? []) {
|
|
1226
|
+
if (seedItem.category !== 'Commlink' || !seedItem.heldBy)
|
|
1227
|
+
continue;
|
|
1228
|
+
const bearer = this._hubScene?.allActors?.find(a => a.name === seedItem.heldBy);
|
|
1229
|
+
if (!bearer)
|
|
1230
|
+
continue;
|
|
1231
|
+
const hasLink = bearer.inventory.getAllItems().some(i => i.category === Category.Commlink)
|
|
1232
|
+
|| bearer.equipment.head?.commlink != null;
|
|
1233
|
+
if (!hasLink) {
|
|
1234
|
+
bearer.inventory.addItem(restoreItem(seedItem));
|
|
1235
|
+
Logger.getInstance().write(`Resume heal: re-granted "${seedItem.name}" to ${seedItem.heldBy}.`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
this.pendingProcurements = [...(save.game.procurements ?? [])];
|
|
1239
|
+
this.docwagon = save.game.docwagon ? { ...save.game.docwagon } : undefined;
|
|
1240
|
+
this.enlistedAlly = save.game.enlistedAlly ? { ...save.game.enlistedAlly } : undefined;
|
|
1241
|
+
this.commandRefusals = { ...(save.game.commandRefusals ?? {}) };
|
|
985
1242
|
this.fencedCategories = new Set(save.game.fencedCategories);
|
|
986
1243
|
this._rumorIndex = save.game.rumorIndex;
|
|
987
1244
|
this.activeRumor = save.game.activeRumor;
|
|
@@ -1022,6 +1279,15 @@ export default class Game {
|
|
|
1022
1279
|
return;
|
|
1023
1280
|
try {
|
|
1024
1281
|
const save = this.buildSaveFile(reason);
|
|
1282
|
+
// Carry the cloud-sync stamp FORWARD through local writes: the
|
|
1283
|
+
// stamp means "the cloud's savedAt as of our last mirror", and a
|
|
1284
|
+
// new local beat doesn't change what the cloud holds. Without
|
|
1285
|
+
// this, every quit (whose async push can't outlive the process)
|
|
1286
|
+
// left a stampless file that the next launch misread as a FORK
|
|
1287
|
+
// and prompted over (a real session hit it on every reload).
|
|
1288
|
+
const prev = readSave(this._saveFilePath);
|
|
1289
|
+
if (prev.ok && prev.save.sync)
|
|
1290
|
+
save.sync = prev.save.sync;
|
|
1025
1291
|
writeSaveAtomic(this._saveFilePath, save);
|
|
1026
1292
|
logger.write(`Saved (${reason}) -> ${this._saveFilePath}`);
|
|
1027
1293
|
// Local-first: the beat is DONE the moment the file lands. The
|
|
@@ -1053,6 +1319,10 @@ export default class Game {
|
|
|
1053
1319
|
rentDebt: this.rentDebt,
|
|
1054
1320
|
tier: this._tier,
|
|
1055
1321
|
contacts: [...this.contacts.entries()],
|
|
1322
|
+
procurements: [...this.pendingProcurements],
|
|
1323
|
+
docwagon: this.docwagon ? { ...this.docwagon } : undefined,
|
|
1324
|
+
enlistedAlly: this.enlistedAlly ? { ...this.enlistedAlly } : undefined,
|
|
1325
|
+
commandRefusals: { ...this.commandRefusals },
|
|
1056
1326
|
fencedCategories: [...this.fencedCategories],
|
|
1057
1327
|
rumorIndex: this._rumorIndex,
|
|
1058
1328
|
activeRumor: this.activeRumor,
|
|
@@ -1181,6 +1451,8 @@ export default class Game {
|
|
|
1181
1451
|
// pull each per homecoming (see commands/ask.ts).
|
|
1182
1452
|
for (const contact of this.contacts.values())
|
|
1183
1453
|
contact.asked = false;
|
|
1454
|
+
// Fresh material for the buskers too (commands/perform.ts).
|
|
1455
|
+
this.performedThisCycle = false;
|
|
1184
1456
|
// LIFESTYLE upkeep comes due with the homecoming: this tier's share
|
|
1185
1457
|
// plus any standing debt, paid from carried cred as far as it goes.
|
|
1186
1458
|
// Coming up short SLIDES YOU DOWN a tier (the district notices) and
|
|
@@ -1230,14 +1502,56 @@ export default class Game {
|
|
|
1230
1502
|
// Drones spin back up beside the cot.
|
|
1231
1503
|
const companionArrivalLines = this.unfoldCompanionsOnArrival();
|
|
1232
1504
|
const companionBlock = [...companionFoldLines, ...companionArrivalLines];
|
|
1505
|
+
// The commanded ally's tour ends with the run (RAW's Command lasts
|
|
1506
|
+
// turns; ours lasts the job): the crew disbands at the curb.
|
|
1507
|
+
let allyLine = '';
|
|
1508
|
+
if (this.enlistedAlly) {
|
|
1509
|
+
allyLine = `\n${this.enlistedAlly.name} peels off at the curb -- "Call it even, chummer." The crew's back to just you.`;
|
|
1510
|
+
this.enlistedAlly = undefined;
|
|
1511
|
+
}
|
|
1512
|
+
// DOCWAGON's year ticks in homecomings (12 = a street year, player
|
|
1513
|
+
// ruling): the contract lapses quietly unless renewed at Doc's.
|
|
1514
|
+
let docwagonLine = '';
|
|
1515
|
+
if (this.docwagon) {
|
|
1516
|
+
this.docwagon.homecomingsLeft -= 1;
|
|
1517
|
+
if (this.docwagon.homecomingsLeft <= 0) {
|
|
1518
|
+
docwagonLine = `\nYour DocWagon ${this.docwagon.tier} contract LAPSES -- the wristband goes dark.${hint(` ("docwagon <tier>" at Doc's renews it.)`)}`;
|
|
1519
|
+
this.docwagon = undefined;
|
|
1520
|
+
}
|
|
1521
|
+
else if (this.docwagon.homecomingsLeft <= 2) {
|
|
1522
|
+
docwagonLine = `\nDocWagon renewal coming due: ${this.docwagon.homecomingsLeft} homecoming${this.docwagon.homecomingsLeft === 1 ? '' : 's'} left on the ${this.docwagon.tier} contract.`;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
// SWAG deliveries (commands/source.ts): the contact's shopping run
|
|
1526
|
+
// lands with the homecoming -- straight into your gear, paid for at
|
|
1527
|
+
// order time.
|
|
1528
|
+
let deliveryLine = '';
|
|
1529
|
+
if (this.pendingProcurements.length > 0) {
|
|
1530
|
+
const delivered = [];
|
|
1531
|
+
for (const order of this.pendingProcurements.splice(0)) {
|
|
1532
|
+
try {
|
|
1533
|
+
const item = restoreItem(order.itemConfig);
|
|
1534
|
+
item.owner = this.player.name;
|
|
1535
|
+
this.player.inventory.addItem(item);
|
|
1536
|
+
delivered.push(`${item.name} (${order.contact} came through)`);
|
|
1537
|
+
}
|
|
1538
|
+
catch (err) {
|
|
1539
|
+
Logger.getInstance().error(`Procurement delivery failed: ${err}`);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (delivered.length > 0) {
|
|
1543
|
+
this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
|
|
1544
|
+
deliveryLine = `\nWaiting with your gear: ${delivered.join(', ')}.`;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1233
1547
|
// The homecoming save: everything above (payout, rent, restock,
|
|
1234
|
-
// contacts reset) settled first, so the save reflects
|
|
1235
|
-
// the player will find it.
|
|
1548
|
+
// contacts reset, deliveries) settled first, so the save reflects
|
|
1549
|
+
// the hub as the player will find it.
|
|
1236
1550
|
this.requestSave('homecoming');
|
|
1237
1551
|
const fateLine = wrappedRun
|
|
1238
1552
|
? `The run's wrapped and the street swallows you back up.`
|
|
1239
1553
|
: `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')}` : ''}`;
|
|
1554
|
+
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
1555
|
}
|
|
1242
1556
|
/**
|
|
1243
1557
|
* The actual drafting work, split out from continueToNextScene() purely
|
|
@@ -1282,6 +1596,7 @@ export default class Game {
|
|
|
1282
1596
|
this._pendingRunSeed = nextSeed;
|
|
1283
1597
|
this._pendingRunTier = nextTier;
|
|
1284
1598
|
this._pendingRide = Game.RIDE_POOL[Math.floor(Math.random() * Game.RIDE_POOL.length)];
|
|
1599
|
+
this.pendingRunAccepted = false;
|
|
1285
1600
|
return `Your link pings -- 1 new message: ${FIXER_NAME}.${hint(` ("call ${FIXER_NAME}" to hear the job.)`)}`;
|
|
1286
1601
|
}
|
|
1287
1602
|
// Classic sessions transition immediately, hideout-first, as always.
|
|
@@ -1335,11 +1650,13 @@ export default class Game {
|
|
|
1335
1650
|
// Deployed drones spin back up at the curb -- BEFORE appearance
|
|
1336
1651
|
// warming so re-shelled companions get warmed with the cast.
|
|
1337
1652
|
const arrivalLines = this.unfoldCompanionsOnArrival();
|
|
1653
|
+
// A commanded ally meets you at the jobsite (commands/lead.ts).
|
|
1654
|
+
const allyLines = this.spawnEnlistedAlly();
|
|
1338
1655
|
this.warmNpcAppearancesInBackground();
|
|
1339
1656
|
this.refreshLogLabel();
|
|
1340
1657
|
// No "=== JOB #n ===" banner on entry (player request) -- the Game
|
|
1341
1658
|
// Log window's own title carries the job name now.
|
|
1342
|
-
const companionBlock = [...companionLines, ...arrivalLines];
|
|
1659
|
+
const companionBlock = [...companionLines, ...arrivalLines, ...allyLines];
|
|
1343
1660
|
return `\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
1344
1661
|
}
|
|
1345
1662
|
/**
|
|
@@ -1426,16 +1743,12 @@ export default class Game {
|
|
|
1426
1743
|
const haggledAtOffer = this.player.haggledThisJob;
|
|
1427
1744
|
try {
|
|
1428
1745
|
const banner = await this.activateRunScene(seed, tier, { startInHideout: false });
|
|
1429
|
-
this.
|
|
1430
|
-
this._pendingRunTier = undefined;
|
|
1431
|
-
this._pendingRide = undefined;
|
|
1746
|
+
this.clearPendingRun();
|
|
1432
1747
|
this.player.haggledThisJob = haggledAtOffer;
|
|
1433
1748
|
return `You travel to ${this.player.currentLocation.name}.\n${banner}`;
|
|
1434
1749
|
}
|
|
1435
1750
|
catch (err) {
|
|
1436
|
-
this.
|
|
1437
|
-
this._pendingRunTier = undefined;
|
|
1438
|
-
this._pendingRide = undefined;
|
|
1751
|
+
this.clearPendingRun();
|
|
1439
1752
|
Logger.getInstance().error(`launchPendingRun failed: ${err}`);
|
|
1440
1753
|
return `The ride never shows and the job address goes dead. ${FIXER_NAME} will not enjoy hearing this -- call him for another.`;
|
|
1441
1754
|
}
|