@maka/maka-cli 5.15.2 → 5.16.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.
Files changed (31) hide show
  1. package/bundle/typescript/package.json +2 -2
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +4 -0
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +49 -0
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +151 -6
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +34 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/game.js +139 -15
  9. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/headless.d.ts +6 -0
  11. package/bundle/typescript/src/commands/game/sideQuest/headless.js +14 -0
  12. package/bundle/typescript/src/commands/game/sideQuest/headless.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +5 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +4 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/ui.js +25 -2
  17. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +8 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +33 -0
  20. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +10 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.d.ts +45 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +179 -12
  25. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js.map +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/social.d.ts +4 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/social.js +15 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/utilities/social.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +53 -16
  30. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  31. package/package.json +2 -2
@@ -7,7 +7,7 @@ import { Door } from './models/door.js';
7
7
  import { SAVE_VERSION } from './types/save-file.js';
8
8
  import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, restoreItem, readSave, serializeItem, saveSlug, } from './utilities/persistence.js';
9
9
  import { appendCrewLedger } from './utilities/crew-ledger.js';
10
- import { heartbeatPresence, ghostHireable, syncCrewLedger, claimGhostEarnings } from './utilities/social.js';
10
+ import { heartbeatPresence, heartbeatPresenceNow, ghostHireable, syncCrewLedger, claimGhostEarnings } from './utilities/social.js';
11
11
  import { pushSave, burnCloudSaveFor } from './utilities/cloud-saves.js';
12
12
  import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
13
13
  import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
@@ -224,10 +224,8 @@ export default class Game {
224
224
  // IS the hub (the run is nobody's business); silent no-op logged out.
225
225
  _presenceTimer;
226
226
  static PRESENCE_TICK_MS = 60_000;
227
- beatPresence() {
228
- if (!this.hasHub || this.scene !== this._hubScene)
229
- return;
230
- void heartbeatPresence({
227
+ presencePayload() {
228
+ return {
231
229
  playerName: this.player.name,
232
230
  slug: saveSlug(this.player.name),
233
231
  hubName: this._hubName,
@@ -237,7 +235,28 @@ export default class Game {
237
235
  karma: this.player.careerKarma,
238
236
  streetCred: this.player.streetCred,
239
237
  },
240
- });
238
+ };
239
+ }
240
+ beatPresence() {
241
+ if (!this.hasHub || this.scene !== this._hubScene)
242
+ return;
243
+ void heartbeatPresence(this.presencePayload());
244
+ // The hub link rides the same pulse: a failed connect at boot must
245
+ // not mean dead party notifications all session (ensure() no-ops
246
+ // when already connected). Lazy import -- the server never loads
247
+ // the client module.
248
+ void import('./utilities/shared-run.js')
249
+ .then(m => { m.ensureHubLink(this); m.HubLink.reconcileSeats(this); })
250
+ .catch(() => undefined);
251
+ }
252
+ /**
253
+ * An IMMEDIATE presence beat that reports how it went -- the answer
254
+ * to "why can't my friend see me": the regular heartbeat is silent
255
+ * and up to 60s behind, so "crew players" and the hireable toggle
256
+ * push one right now and say so.
257
+ */
258
+ async pushPresenceNow() {
259
+ return heartbeatPresenceNow(this.presencePayload());
241
260
  }
242
261
  startPresenceHeartbeat() {
243
262
  if (this._presenceTimer)
@@ -300,6 +319,9 @@ export default class Game {
300
319
  spawnCrew() {
301
320
  const lines = [];
302
321
  for (const member of this.crew) {
322
+ // Friend invites ride LIVE or not at all -- never as AI shells.
323
+ if (member.ghost?.liveOnly)
324
+ continue;
303
325
  const shell = this.spawnCompanionShell({
304
326
  name: member.name,
305
327
  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.`,
@@ -348,6 +370,9 @@ export default class Game {
348
370
  const lines = [];
349
371
  const fallen = [];
350
372
  for (const member of this.crew) {
373
+ // Friend invites never spawned -- no shell is not a death.
374
+ if (member.ghost?.liveOnly)
375
+ continue;
351
376
  const entry = this.companions.find(c => c.kind === 'ally' && c.npc.name === member.name);
352
377
  if (!entry || entry.npc.deathClaimed) {
353
378
  fallen.push(member.name);
@@ -391,13 +416,18 @@ export default class Game {
391
416
  * line (empty crew: full amount, no line).
392
417
  */
393
418
  applyCrewCut(gross) {
394
- if (this.crew.length === 0 || gross <= 0)
419
+ // Friend invites take no cut (live runs pay the canon team split
420
+ // through payRunner) -- and an all-invite crew must not divide by
421
+ // its own zero.
422
+ const payable = this.crew.filter(m => m.cutPct > 0 && !m.ghost?.liveOnly);
423
+ if (payable.length === 0 || gross <= 0)
395
424
  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);
425
+ const sumPct = payable.reduce((s, m) => s + m.cutPct, 0);
426
+ const totalPct = Math.min(60, sumPct);
427
+ const scale = totalPct / sumPct;
398
428
  const parts = [];
399
429
  let taken = 0;
400
- for (const member of this.crew) {
430
+ for (const member of payable) {
401
431
  const cut = Math.floor(gross * (member.cutPct * scale) / 100);
402
432
  taken += cut;
403
433
  parts.push(`${member.name} ${member.cutPct}% (${cut}¥)`);
@@ -435,6 +465,8 @@ export default class Game {
435
465
  const contract = this.docwagon;
436
466
  if (!contract || !this.initialized || this._dead)
437
467
  return false;
468
+ // (Solo path: THE player, THE contract, hub medevac. Shared runs use
469
+ // tryDocwagonRescueFor below -- per-player contracts, no hub.)
438
470
  const tier = DOCWAGON_TIERS[contract.tier];
439
471
  const logger = Logger.getInstance();
440
472
  const bills = [];
@@ -476,6 +508,76 @@ export default class Game {
476
508
  void this.docwagonMedevac();
477
509
  return true;
478
510
  }
511
+ // ---------------- DocWagon in SHARED runs (per-player, ruling:
512
+ // "full permadeath, but the doc wagon still works if purchased") ----
513
+ /** Each human player's contract, loaded from their own save doc by
514
+ * the headless boot (their `save.game.docwagon`). Solo play never
515
+ * touches this map -- the legacy single-contract path above is the
516
+ * solo behavior, byte for byte. */
517
+ docwagonByPlayer = new Map();
518
+ /** Shared-run hook: a player's HTR extraction (stabilized and carried
519
+ * off the job). The hosting server writes their snapshot back and
520
+ * marks them 'left'. */
521
+ onPlayerExtracted;
522
+ /**
523
+ * The per-player band (shared runs -- scene.players.length > 1):
524
+ * resolves the FALLEN player's own contract, bills THAT player
525
+ * (whatever their stick holds; a guest's shortfall is DocWagon's
526
+ * collections problem, not the rent tab's), and either stabilizes
527
+ * them in place ('resusc') or extracts them from the session ('htr'
528
+ * -- the member-leave flow, via onPlayerExtracted). Returns true when
529
+ * the rescue engages -- the caller must then NOT kill them.
530
+ */
531
+ tryDocwagonRescueFor(player, kind, opts = {}) {
532
+ const contract = this.docwagonByPlayer.get(player.name);
533
+ if (!contract || !this.initialized)
534
+ return false;
535
+ const tier = DOCWAGON_TIERS[contract.tier];
536
+ const logger = Logger.getInstance();
537
+ const bills = [];
538
+ const billPlayer = (amount, label) => {
539
+ if (amount <= 0)
540
+ return '';
541
+ const paid = Math.min(player.currency, amount);
542
+ if (paid > 0)
543
+ player.adjustCurrency(-paid);
544
+ const owed = amount - paid;
545
+ return owed === 0
546
+ ? `DocWagon bills ${player.name} ${amount} for ${label}.`
547
+ : `DocWagon bills ${player.name} ${amount} for ${label} -- ${paid} paid, ${owed} on account (they WILL collect).`;
548
+ };
549
+ if (kind === 'resusc') {
550
+ if (contract.resuscsLeft > 0) {
551
+ contract.resuscsLeft -= 1;
552
+ bills.push(`A contract resuscitation is burned for ${player.name} -- ${contract.resuscsLeft} left this year.`);
553
+ }
554
+ else {
555
+ bills.push(billPlayer(RESUSC_FEE, 'resuscitation'));
556
+ }
557
+ player.healDamage(Math.max(0, player.damageTaken - (player.maxConditionBoxes - 2)));
558
+ player.healStun(Math.max(0, player.stunTaken - Math.max(0, player.maxStunBoxes - 2)));
559
+ logger.logWithColor(`${CALL_ICON} ${player.name}'s wristband SCREAMS the flatline. Ten minutes later there is foam, light, and a trauma team refusing to let go.`, CALL_COLOR);
560
+ }
561
+ else {
562
+ bills.push(billPlayer(tier.htrFee, 'High Threat Response'));
563
+ const hostiles = opts.standingHostiles ?? [];
564
+ if (hostiles.length > 0 && tier.deathComp) {
565
+ const casualtyRoll = rollPool(Math.min(4, hostiles.length));
566
+ if (casualtyRoll.hits > 0) {
567
+ bills.push(billPlayer(DEATH_COMP, 'employee death compensation -- a medic did not make it out'));
568
+ }
569
+ }
570
+ player.healStun(Math.max(0, player.stunTaken - Math.max(0, player.maxStunBoxes - 2)));
571
+ logger.logWithColor(`${CALL_ICON} ${player.name}'s band reads the flatline and howls -- a DocWagon HTR team storms the scene and carries them out behind riot shields.`, CALL_COLOR);
572
+ // Extraction: off the job, alive, gear on the stretcher with them.
573
+ this.scene.removeActorQuietly(player.name);
574
+ this.onPlayerExtracted?.(player);
575
+ }
576
+ for (const b of bills.filter(Boolean))
577
+ logger.logWithColor(b, 'yellow');
578
+ this.scene.addWorldEvent(`A DocWagon team ${kind === 'htr' ? 'extracted' : 'resuscitated'} ${player.name} under the ${contract.tier} contract.`);
579
+ return true;
580
+ }
479
581
  /** Where the wagon drops you (player ruling: the CLINIC, not your own
480
582
  * cot): the district Street Doc's room, generically -- so generated
481
583
  * hubs work too. Falls back to the hideout if the hub has no doc. */
@@ -1328,6 +1430,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1328
1430
  CommandFactory.registerCommand('lead', LeadCommand);
1329
1431
  CommandFactory.registerCommand('command', RecruitCommand);
1330
1432
  CommandFactory.registerCommand('crew', CrewCommand);
1433
+ // "party" is the same table by another name (user vocabulary --
1434
+ // "basic party operations").
1435
+ CommandFactory.registerCommand('party', CrewCommand);
1331
1436
  CommandFactory.registerCommand('hire', HireCommand);
1332
1437
  CommandFactory.registerCommand('dismiss', DismissCommand);
1333
1438
  CommandFactory.registerCommand('train', TrainCommand);
@@ -1605,6 +1710,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1605
1710
  }
1606
1711
  }
1607
1712
  this.initialized = true;
1713
+ // FIRST-BEAT SAVE (M4.5, from a real incident): a fresh character
1714
+ // had no cloud copy until their first rest/quit, so a friend's
1715
+ // party invite bounced with "no living runner". A hub session is
1716
+ // party-loadable within moments of launch now -- requestSave pushes
1717
+ // the cloud mirror behind the local write.
1718
+ if (this._hubScene && this.scene === this._hubScene) {
1719
+ this.requestSave('hub-arrival');
1720
+ }
1608
1721
  if (this._resume) {
1609
1722
  const pendingNote = this._pendingRunSeed
1610
1723
  ? hint(`\n\n${FIXER_NAME}'s job is still on offer: "${this._pendingRunSeed.name}". "call taxi" when you're ready, or "call ${FIXER_NAME}" to hear it again.`)
@@ -1878,7 +1991,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1878
1991
  // then the wrapped-run tally and the training window reset.
1879
1992
  const crewLines = this.scene !== hub ? this.settleCrewAfterRun() : [];
1880
1993
  for (const member of this.crew) {
1881
- if (wrappedRun)
1994
+ // Friend invites weren't on this local run -- no tally for them.
1995
+ if (wrappedRun && !member.ghost?.liveOnly)
1882
1996
  member.jobsRun += 1;
1883
1997
  member.trainedThisCycle = false;
1884
1998
  }
@@ -2030,11 +2144,18 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2030
2144
  // down), and locally we keep only Krow's pitch stub so the whole
2031
2145
  // offer flow (his call, accept/decline, the taxi) works unchanged.
2032
2146
  // Offline owners fall back to the Phase B ghost riders below.
2147
+ let sharedDeclineNote = '';
2033
2148
  if (this.hasHub) {
2034
2149
  const shared = await import('./utilities/shared-run.js')
2035
2150
  .then(m => m.tryCreateSharedSession(this))
2036
2151
  .catch(() => undefined);
2037
- if (shared) {
2152
+ if (shared && 'declined' in shared) {
2153
+ // The crew was invited but the table couldn't seat them -- the
2154
+ // job drafts locally and KROW SAYS WHY (a real playtest lost a
2155
+ // run to this being silent).
2156
+ sharedDeclineNote = ` "Couldn't raise the whole crew -- ${shared.declined} This one's quoted without them."`;
2157
+ }
2158
+ else if (shared) {
2038
2159
  this.pendingSharedRun = { sessionID: shared.sessionID };
2039
2160
  this._pendingRunSeed = {
2040
2161
  name: shared.pitch.name,
@@ -2068,7 +2189,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2068
2189
  //
2069
2190
  // The player object rides along so the job is drafted FOR this runner
2070
2191
  // -- their archetype, stats, and toolkit (see buildPlayerDossier).
2071
- const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, this.player, this.activeRumor, this.crew);
2192
+ // Friend invites don't ride local drafts (they're live players or
2193
+ // nothing) -- the dossier scales to who'll actually show.
2194
+ const localCrew = this.crew.filter(m => !m.ghost?.liveOnly);
2195
+ const nextSeed = await SceneSeedGenerator.generate(this._logFilePath, nextTier, this.player, this.activeRumor, localCrew);
2072
2196
  if (!nextSeed) {
2073
2197
  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."`;
2074
2198
  }
@@ -2082,7 +2206,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2082
2206
  this._pendingRunTier = nextTier;
2083
2207
  this._pendingRide = Game.RIDE_POOL[Math.floor(Math.random() * Game.RIDE_POOL.length)];
2084
2208
  this.pendingRunAccepted = false;
2085
- return `Your link pings -- 1 new message: ${FIXER_NAME}.${hint(` ("call ${FIXER_NAME}" to hear the job.)`)}`;
2209
+ return `Your link pings -- 1 new message: ${FIXER_NAME}.${sharedDeclineNote}${hint(` ("call ${FIXER_NAME}" to hear the job.)`)}`;
2086
2210
  }
2087
2211
  // Classic sessions transition immediately, hideout-first, as always.
2088
2212
  return await this.activateRunScene(nextSeed, nextTier, { startInHideout: true });
@@ -2925,7 +3049,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2925
3049
  { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['stance'], ['surrender'], ['edge'], ['rest']] },
2926
3050
  // The party layer: everyone who walks (or flies, or manifests) at
2927
3051
  // your side answers to these.
2928
- { title: 'party', entries: [['crew'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
3052
+ { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
2929
3053
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
2930
3054
  { title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
2931
3055
  { title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },