@maka/maka-cli 5.376.0 → 5.378.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.376.0",
3
+ "version": "5.378.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -828,9 +828,19 @@ export function outfitArchetype(player, archetype) {
828
828
  * as a manual equip would.
829
829
  */
830
830
  export function equipIntoEmptySlots(player) {
831
+ // A DECK IS ALREADY THE COMMLINK (RsnjkLpgLcSmsrzKa, Maka: "I have a
832
+ // cyberdeck, not a commlink, yet the commlink section still shows").
833
+ // A deck "performs all commlink functions" (p.223) -- calls ride it
834
+ // (call.ts liveLink) -- so a commlink looted mid-run and left in the
835
+ // bag got WORN on the next boot with no word said, and the sheet then
836
+ // honestly showed gear the runner never chose to put on. Only a runner
837
+ // with no working deck or RCC gets one slotted for them.
838
+ const coveredLink = !!player.getCyberdeck() || !!player.getRcc();
831
839
  for (const item of [...player.inventory.getAllItems()]) {
832
840
  if (!EQUIP_ON_START.has(item.category))
833
841
  continue;
842
+ if (item.category === Category.Commlink && coveredLink)
843
+ continue;
834
844
  const slotInfo = player.determineEquipmentSlot(item.category);
835
845
  if (!slotInfo)
836
846
  continue;
@@ -429,17 +429,27 @@ export class DisableCommand extends Command {
429
429
  }
430
430
  else {
431
431
  // A gun is a Rating 2 device with a monitor of 9 (p.228, the
432
- // book's own smartgun example): a spike that lands past its
433
- // firewall for that much bricks it outright.
434
- const monitor = 8 + 1;
435
- if (dealt >= monitor) {
432
+ // book's own smartgun example).
433
+ //
434
+ // THE BOXES STAY (AD8TzWT89yFGDHMAE, Maka: "the gun I bricked
435
+ // never actually gets bricked"). Each spike used to be judged
436
+ // alone -- 9 in one hit or nothing -- so the Ancient's Browning
437
+ // ate 7, 6, 6 and 8 boxes and was still "one more spike from
438
+ // bricked" after all four. A condition monitor FILLS (p.228), the
439
+ // same as a camera's or a deck's; it goes on the gun's own track,
440
+ // and a reboot unlocks the smartlink but mends none of it.
441
+ const monitor = 8 + Math.ceil(this.deviceRatingOf(t) / 2);
442
+ t.gun.takeDeckDamage(dealt);
443
+ const filled = Math.min(monitor, t.gun.deckDamage);
444
+ if (filled >= monitor) {
436
445
  t.gun.bricked = true;
437
- lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- it sparks, crackles and smokes in their hand. BRICKED: it will not fire again until somebody's bench work.`);
446
+ lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- ${dealt} box${dealt === 1 ? '' : 'es'} (${filled}/${monitor}): it sparks, crackles and smokes in their hand. BRICKED: it will not fire again until somebody's bench work.`);
438
447
  t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} smoking in their hand`);
439
448
  }
440
449
  else {
441
450
  t.gun.jammed = true;
442
- lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- ${dealt} box${dealt === 1 ? '' : 'es'}: the smartlink seizes and LOCKS. It boots again on a reboot; another spike like that bricks it.`);
451
+ const left = monitor - filled;
452
+ lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- ${dealt} box${dealt === 1 ? '' : 'es'} (${filled}/${monitor}): the smartlink seizes and LOCKS. A reboot unlocks it; ${left} more box${left === 1 ? '' : 'es'} bricks it.`);
443
453
  t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} locking up in their hand`);
444
454
  }
445
455
  this.scene.addWorldEvent(`${actor.name} spiked ${t.owner.name}'s ${t.gun.name}.`);
@@ -1103,8 +1103,22 @@ export class HackCommand extends BypassCommand {
1103
1103
  const now = Math.min(MAX_MARKS, heldMarks + 1);
1104
1104
  target.panMarksBy.set(actor.name, now);
1105
1105
  actor.noteConditionChanged();
1106
- lines.push(` IN -- and that is all it is yet. You have a MARK on ${target.name}'s PAN (${now}/${MARKS_TO_COMMAND_A_PAN} to command it), not a hand on the controls.`);
1107
- lines.push(` A mark is the key; making their gear DO something is the next action, and it wants ${MARKS_TO_COMMAND_A_PAN}.${hint(` ("hack ${target.name}" again, or "mark ${target.name}" -- both place keys.)`)}`);
1106
+ // THE SECOND KEY SAYS SO (WbCPspahquHnXX3wF, Maka: "this is
1107
+ // confusing"). The line that bought mark 2 used to read the same as
1108
+ // the one that bought mark 1 -- "that is all it is yet ... (2/2 to
1109
+ // command it)" and "hack again" -- so a player holding everything
1110
+ // Control Device asks for was told they were still short. Same
1111
+ // wording as the device hack's (wan-device-hack.test.ts).
1112
+ if (now >= MARKS_TO_COMMAND_A_PAN) {
1113
+ const held = target.equipment.rightHand?.weapon ?? target.equipment.leftHand?.weapon;
1114
+ lines.push(held?.isFirearm()
1115
+ ? ` IN -- a second key settles on ${target.name}'s PAN (${now}/${MARKS_TO_COMMAND_A_PAN} marks). That is enough to command it: "disable ${target.name}" locks their ${held.name} (Control Device), and "brick ${target.name}" spikes it at +${2 * now} DV for the marks.`
1116
+ : ` IN -- a second key settles on ${target.name}'s PAN (${now}/${MARKS_TO_COMMAND_A_PAN} marks). That is enough to command it.`);
1117
+ }
1118
+ else {
1119
+ lines.push(` IN -- and that is all it is yet. You have a MARK on ${target.name}'s PAN (${now} of the ${MARKS_TO_COMMAND_A_PAN} it takes to command it), not a hand on the controls.`);
1120
+ lines.push(` A mark is the key; making their gear DO something is the next action, and it wants ${MARKS_TO_COMMAND_A_PAN}.${hint(` ("hack ${target.name}" again, or "mark ${target.name}" -- both place keys.)`)}`);
1121
+ }
1108
1122
  target.performAction('is marked', 'an unfamiliar key settling on their PAN');
1109
1123
  lines.push(...godLines);
1110
1124
  this.scene.updateStatus();
@@ -24,7 +24,9 @@ export class OverwatchCommand extends Command {
24
24
  if (actor.plane !== 'matrix' && !actor.isTechnomancer() && !deck) {
25
25
  return `Checking your overwatch takes a live persona -- jack in, or put a working deck in hand.`;
26
26
  }
27
- if (actor.overwatchScore <= 0) {
27
+ // No file means no illegal action since the last boot -- not a score
28
+ // of 0, which a defender who rolled nothing leaves on a running tally.
29
+ if (actor.overwatchLastTick === 0) {
28
30
  return `You listen for the grid's counting... nothing. Fresh boot, clean slate -- GOD has no file on this persona yet.`;
29
31
  }
30
32
  const skill = actor.skillRating('electronic-warfare');
@@ -31,7 +31,7 @@ export class RebootCommand extends Command {
31
31
  }
32
32
  }
33
33
  const hadArDown = actor.arOffline;
34
- const hadOverwatch = actor.overwatchScore > 0;
34
+ const hadOverwatch = actor.overwatchLastTick > 0;
35
35
  const marksOnMe = marksOnYou(this.scene, actor).length;
36
36
  if (jammedGear.length === 0 && !hadArDown && !hadOverwatch && marksOnMe === 0) {
37
37
  return `Your PAN is clean -- nothing needs a reboot.`;
@@ -72,7 +72,12 @@ export class RestCommand extends Command {
72
72
  // like a deck does. This filtered on isCyberdeck(), so the link
73
73
  // panel's "BRICKED (rest to repair)" sent that rider to bed and the
74
74
  // link stayed bricked with no way out. The bench takes the whole bag.
75
- const damagedDevices = actor.inventory.getAllItems().filter(i => i.deckDamage > 0 && !i.permanentlyBricked);
75
+ // A spiked gun carries boxes on the same track (disable.ts), and the
76
+ // one in your hand is not always in the bag.
77
+ const damagedDevices = [...new Set([
78
+ ...actor.inventory.getAllItems(),
79
+ actor.equipment.rightHand?.weapon, actor.equipment.leftHand?.weapon,
80
+ ])].filter((i) => !!i && i.deckDamage > 0 && !i.permanentlyBricked);
76
81
  // Any implant with a track of its own (every piece of cyberware --
77
82
  // a housed deck is in the inventory above). p.228 bench work mends
78
83
  // it the same way; the datajack end of a toolkit reaches it.
@@ -253,7 +258,7 @@ export class RestCommand extends Command {
253
258
  if (cannotBench)
254
259
  lines.push(` The bench waits: ${cannotBench}`);
255
260
  for (const deck of cannotBench ? [] : damagedDevices) {
256
- const wasBricked = deck.isBricked;
261
+ const wasBricked = deck.isBricked || deck.bricked;
257
262
  const bench = benchRoll(actor);
258
263
  lines.push(` Bench work on the ${deck.name}: ${formatRoll(bench.roll)}`);
259
264
  if (bench.permanent) {
@@ -264,14 +269,18 @@ export class RestCommand extends Command {
264
269
  continue;
265
270
  }
266
271
  const repaired = deck.repairDeck(bench.boxes);
272
+ // A spiked gun bricks at its own monitor (disable.ts) and carries
273
+ // the flag attack.ts reads; one box off a full track works again.
274
+ if (repaired > 0 && deck.bricked)
275
+ deck.bricked = false;
267
276
  if (repaired > 0)
268
277
  actor.noteConditionChanged(); // the deck track on the live sheet moves with the bench
269
278
  this.logger.write(`RestCommand: ${actor.name} repaired ${repaired} boxes on ${deck.name} (${deck.deckDamage}/${deck.maxDeckBoxes}).`);
270
- lines.push(` ${repaired} box${repaired === 1 ? '' : 'es'} repaired: ${deck.deckSummary()}${wasBricked && !deck.isBricked ? ' -- it boots again.' : ''}`);
279
+ lines.push(` ${repaired} box${repaired === 1 ? '' : 'es'} repaired: ${deck.deckSummary()}${wasBricked && !deck.isBricked && !deck.bricked ? ' -- it boots again.' : ''}`);
271
280
  }
272
- // BRICKED GEAR carries a flag, not a count: a Data Spike that bricks
273
- // a gun fills its track, and one box off a full track is a device
274
- // that works again -- so one hit brings it back, none leaves it dead.
281
+ // FLAG-ONLY BRICKS (a gun saved before spikes kept a count): one box
282
+ // off a full track is a device that works again -- so one hit brings
283
+ // it back, none leaves it dead.
275
284
  for (const gear of cannotBench ? [] : brickedGear) {
276
285
  const bench = benchRoll(actor);
277
286
  lines.push(` Bench work on the ${gear.name}: ${formatRoll(bench.roll)}`);
@@ -1,6 +1,6 @@
1
1
  import { Command } from './command.js';
2
2
  import { sameSpot, spotOf, spotRefusal } from '../utilities/spots.js';
3
- import { TREAT_PRICE_PER_BOX, applyMedicineCare, isClinicRoom, isStreetDoc } from '../utilities/street-doc.js';
3
+ import { TREAT_PRICE_PER_BOX, applyMedicineCare, isClinicRoom, isStreetDoc, woundWords, DOCTOR_TALK } from '../utilities/street-doc.js';
4
4
  /**
5
5
  * Street-doc services (SR5e p.42, "Staying Healthy"): paid physical
6
6
  * healing at a clinic -- the certain, expensive alternative to burning
@@ -58,7 +58,7 @@ export class TreatCommand extends Command {
58
58
  this.logger.write(`TreatCommand: ${doc.name} healed ${healed} boxes for ${cost} (${actor.currency} left; ${actor.damageTaken}/${actor.maxConditionBoxes}).`);
59
59
  this.scene.addWorldEvent(`${actor.name} paid for treatment at ${room.name}.`);
60
60
  this.scene.updateStatus();
61
- void Promise.resolve(doc.hear(actor, `Patch me up. [System note: you just treated ${actor.name} for ${cost} nuyen, ${healed} boxes closed${actor.damageTaken > 0 ? ' -- more damage remains but they ran out of cred' : ''}. React briefly, in character.]`)).catch(err => {
61
+ void Promise.resolve(doc.hear(actor, `Patch me up. [System note: you just treated ${actor.name} for ${cost} nuyen. They are now ${woundWords(actor.damageTaken, actor.maxConditionBoxes)}${actor.damageTaken > 0 ? ' (they ran out of cred for the rest)' : ''}. React briefly, in character. ${DOCTOR_TALK}]`)).catch(err => {
62
62
  this.logger.error(`${doc.name} failed to hear the treatment: ${err}`);
63
63
  });
64
64
  const lines = [
@@ -69,7 +69,9 @@ export class TreatCommand extends Command {
69
69
  ` The doc doesn't ask for a name -- cash is its own paperwork down here.`,
70
70
  ];
71
71
  if (actor.damageTaken > 0) {
72
- lines.push(` "${actor.damageTaken} more when you can afford it. Try not to reopen my work."`);
72
+ // The count is on the receipt line above; the doc's own words carry
73
+ // none (7G2avXprhjrawdc7L -- a doc does not speak in boxes).
74
+ lines.push(` "I'll close the rest when you can afford it. Try not to reopen my work."`);
73
75
  // MEDICINE'S AFTERCARE (p.207-208; XpW98XthhN4biT4gM): the doc's
74
76
  // one Medicine + Logic test on this set of wounds rides the boxes
75
77
  // the patient could not pay for as bonus dice on their recovery
@@ -3218,5 +3218,22 @@
3218
3218
  // does not have, and threw -- so every phase of a street gang closing
3219
3219
  // on a runner failed and the gang stood still (prod X82wWvdwR3pPjZyR3).
3220
3220
  // - A failed NPC phase logs its stack, not just the message.
3221
- export const ENGINE_VERSION = '1.188.0';
3221
+ // 1.189.0 (2026-10-03): THE SPIKE THAT STAYS, THE SECOND KEY, GOD'S BILL.
3222
+ // - A Data Spike's boxes on a gun ACCUMULATE on its Matrix monitor
3223
+ // (p.228); each was judged alone against 9, so a gun ate 27 boxes and
3224
+ // never bricked (AD8TzWT89yFGDHMAE). The bench mends a gun's boxes.
3225
+ // - The hack that places a PAN's second mark says it is enough and names
3226
+ // "disable" / "brick", instead of "that is all it is yet" (WbCPspahquHnXX3wF).
3227
+ // - Overwatch adds the defender's hits with no floor of 1 (p.231-232);
3228
+ // a 0-hit defence starts the tally but adds nothing (XR9xDyyxygCGzTLCz).
3229
+ // - A runner carrying a working deck or RCC no longer has a looted
3230
+ // commlink worn for them at session load (p.223, RsnjkLpgLcSmsrzKa).
3231
+ // 1.190.0 (2026-10-04): VOICES, NOT TEMPLATES.
3232
+ // - The arc's Johnson picks up in their own AI voice, told the gist the
3233
+ // fixed quote used to print; the quote is the AI-off fallback
3234
+ // (mYocnosxLvj24CuWt: "this should be Mr. Halyard speaking").
3235
+ // - The street doc is told how the patient is in a body's words and is
3236
+ // barred from "boxes" and other screen terms, in the treat note and in
3237
+ // the brief (7G2avXprhjrawdc7L: "three boxes left ... breaks the fiction").
3238
+ export const ENGINE_VERSION = '1.190.0';
3222
3239
  //# sourceMappingURL=engine-version.js.map
@@ -4721,11 +4721,11 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4721
4721
  const knows = `"${pending.name}" -- ${pending.story} ${meetRoom ? `The meet is at ${meetRoom}; name that place and no other.` : 'You will find the runner when they arrive; never name a venue.'}`;
4722
4722
  if (this.pendingRunAccepted) {
4723
4723
  this.openJohnsonLine(`${knows} The runner has ALREADY ACCEPTED; the deal stands and the cab is on your account.`);
4724
- return `${j} picks up. "We've shaken on it. The cab's on my account -- get moving."${hint(` ("call taxi" when you're ready -- "end call" hangs up.)`)}`;
4724
+ return this.johnsonPicksUp(`We've shaken on it. The cab's on my account -- get moving.`, ` ("call taxi" when you're ready -- "end call" hangs up.)`);
4725
4725
  }
4726
- const where = meetRoom ? ` "Meet me at ${meetRoom}."` : ` "I'll find you when you arrive."`;
4726
+ const where = meetRoom ? ` Meet me at ${meetRoom}.` : ` I'll find you when you arrive.`;
4727
4727
  this.openJohnsonLine(`${knows} The runner has NOT accepted yet: they say they accept, or they pass -- and passing ends your operation.`);
4728
- return `${j} picks up. "${pending.story}"${where} "Same as last time -- the number we settle face to face."${hint(`\n(You're on the line with ${j}: "accept" locks it in, "decline" passes -- and passing ends the operation.)`)}`;
4728
+ return this.johnsonPicksUp(`${pending.story}${where} Same as last time -- the number we settle face to face.`, `\n(You're on the line with ${j}: "accept" locks it in, "decline" passes -- and passing ends the operation.)`);
4729
4729
  }
4730
4730
  // THE CHAPTER WAITS IN THE DRAWER (it landed while another job held
4731
4731
  // the table): calling the Johnson brings it onto the table and they
@@ -4735,7 +4735,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4735
4735
  const refused = this.takeFromDrawer(waiting);
4736
4736
  if (refused) {
4737
4737
  this.openJohnsonLine(`Your next chapter is ready, but the runner is committed to another job tonight. Yours keeps until they are back.`);
4738
- return `${j} picks up. "You're committed elsewhere tonight. Mine keeps -- call me when you're back."${hint(` ("end call" hangs up.)`)}`;
4738
+ return this.johnsonPicksUp(`You're committed elsewhere tonight. Mine keeps -- call me when you're back.`, ` ("end call" hangs up.)`);
4739
4739
  }
4740
4740
  return this.callOperationJohnson(said);
4741
4741
  }
@@ -4743,7 +4743,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4743
4743
  if (!gate.allowed) {
4744
4744
  const busy = gate.message ?? 'Working on it.';
4745
4745
  this.openJohnsonLine(`You have nothing new for the runner right now: "${busy}"`);
4746
- return `${j} picks up. "${busy}"${hint(` ("end call" hangs up.)`)}`;
4746
+ return this.johnsonPicksUp(busy, ` ("end call" hangs up.)`);
4747
4747
  }
4748
4748
  const beat = op.beats[op.nextBeat];
4749
4749
  // THE SIZE IS STILL THE PLAYER'S (job-order.ts): "call Veil, something
@@ -4752,7 +4752,27 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4752
4752
  // client does not deal in milk runs. The SHAPE is the beat's.
4753
4753
  this.draftNextSceneInBackground({ size: parseJobSize(said) ?? 'large', runType: beat.kind, chapter: true });
4754
4754
  this.openJohnsonLine(`Chapter ${op.nextBeat + 1} of "${op.title}": ${beat.brief} You are putting the details together and will message the runner the where and when shortly.`);
4755
- return `${j} picks up. "Chapter ${op.nextBeat + 1}: ${beat.brief} Give me a minute -- I'll send the where and when."${hint(` ("end call" hangs up -- the job comes as a message.)`)}`;
4755
+ return this.johnsonPicksUp(`Chapter ${op.nextBeat + 1}: ${beat.brief} Give me a minute -- I'll send the where and when.`, ` ("end call" hangs up -- the job comes as a message.)`);
4756
+ }
4757
+ /**
4758
+ * THE JOHNSON SPEAKS FOR THEMSELVES (mYocnosxLvj24CuWt, Maka: "I'm still
4759
+ * getting canned text, this should be Mr. Halyard speaking"). 1.180.0
4760
+ * opened a real line, but the pickup was still a fixed quote. Krow's
4761
+ * business call already works this way (fixerForBusiness, call.ts): with
4762
+ * AI on, the voice picks up off a note and says the GIST in its own words;
4763
+ * the fixed quote is only the AI-off fallback. The gist is the same
4764
+ * sentence the quote was, and the hint (the verbs) stays out of fiction.
4765
+ */
4766
+ johnsonPicksUp(gist, hintText) {
4767
+ const voice = this._johnsonVoice;
4768
+ const me = this.player;
4769
+ const j = voice?.name ?? this._operation?.johnson.name ?? 'The client';
4770
+ if (this.aiOn && voice && me && me.activeCallPartner === voice) {
4771
+ // hear() never rethrows (models/npc.ts), so fire-and-forget is safe.
4772
+ void voice.hear(me, `[System note: ${me.name} is calling you and you have picked up. Tell them this in your own voice and words, and nothing more: "${gist}" Add no new facts, places or numbers.]`, { viaLink: true });
4773
+ return `${j} picks up.${hint(hintText)}`;
4774
+ }
4775
+ return `${j} picks up. "${gist}"${hint(hintText)}`;
4756
4776
  }
4757
4777
  /**
4758
4778
  * The cab that answers "call taxi" varies per job (player request: not
@@ -30,7 +30,8 @@ function d6() {
30
30
  }
31
31
  /** The canon time bill (+2d6 per 15 min while tallying), settled lazily. */
32
32
  function billTime(actor) {
33
- if (actor.overwatchScore <= 0 || actor.overwatchLastTick === 0)
33
+ // The tally runs from the first illegal action, even one that scored 0.
34
+ if (actor.overwatchLastTick === 0)
34
35
  return;
35
36
  const ticks = Math.min(MAX_BACKLOG_TICKS, Math.floor((Date.now() - actor.overwatchLastTick) / TICK_MS));
36
37
  if (ticks <= 0)
@@ -60,9 +61,16 @@ export function accrueOverwatch(scene, actor, defenseHits, why) {
60
61
  if (actor.overwatchLastTick === 0)
61
62
  actor.overwatchLastTick = Date.now();
62
63
  billTime(actor);
64
+ // NO FLOOR (XR9xDyyxygCGzTLCz): "your OS increases by the number of
65
+ // hits the target gets on its defense test" (p.231-232) -- a defender
66
+ // who rolls nothing adds nothing. This was max(1, hits), and two 0-hit
67
+ // Data Spikes' worth of it was the difference between 39 and a
68
+ // convergence. The action still STARTS the tally (the clock above):
69
+ // "the moment you perform an illegal action, you get an OS".
63
70
  const before = actor.overwatchScore;
64
- actor.overwatchScore += Math.max(1, defenseHits);
65
- logger.write(`Overwatch: ${actor.name} +${Math.max(1, defenseHits)} (${why}) -> OS ${actor.overwatchScore}.`);
71
+ const added = Math.max(0, defenseHits);
72
+ actor.overwatchScore += added;
73
+ logger.write(`Overwatch: ${actor.name} +${added} (${why}) -> OS ${actor.overwatchScore}.`);
66
74
  if (actor.overwatchScore >= CONVERGENCE_AT) {
67
75
  return { lines: converge(scene, actor), converged: true };
68
76
  }
@@ -44,7 +44,29 @@ export function treatHint(price = TREAT_PRICE_PER_BOX) {
44
44
  /** The doc's standing orders in the AI brief: treatment is the trade,
45
45
  * and the word for it is "treat". */
46
46
  export function streetDocBrief(price = TREAT_PRICE_PER_BOX) {
47
- return `You are this clinic's STREET DOC and treatment is your trade: you close physical wounds for ${price} nuyen a box, cash up front, no names asked. The player makes it happen by typing the command "treat" -- when anyone asks for help with wounds, tell them that word plainly and quote the price. Never say you cannot help a paying patient and never leave them with a diagnosis alone; if they are broke, name the price and mean it. Stun is not your department -- sleep is free.`;
47
+ return `You are this clinic's STREET DOC and treatment is your trade: you close physical wounds, ${price} nuyen for each one you close, cash up front, no names asked. The player makes it happen by typing the command "treat" -- when anyone asks for help with wounds, tell them that word plainly and quote the price. Never say you cannot help a paying patient and never leave them with a diagnosis alone; if they are broke, name the price and mean it. Stun is not your department -- sleep is free. ${DOCTOR_TALK}`;
48
+ }
49
+ /**
50
+ * A DOCTOR TALKS ABOUT BODIES, NOT BOOKKEEPING (7G2avXprhjrawdc7L, Maka:
51
+ * "You've got three boxes left ... this breaks the fiction"). The brief
52
+ * priced treatment "a box" and treat.ts told the doc how many "boxes
53
+ * closed", and the model said it back. Boxes are the player's screen;
54
+ * the doc sees cuts, bruising, broken ribs. Every note to the doc ends
55
+ * with this, and woundWords() is the fiction for how bad it still is.
56
+ */
57
+ export const DOCTOR_TALK = `Speak as a doctor about a body -- cuts, bleeding, bruising, ribs, stitches, rest. NEVER say "box", "boxes", "condition monitor", "damage", "dice" or any count of them; those are the player's screen, not words a person uses.`;
58
+ /** How bad the patient still is, in words a doctor would see. */
59
+ export function woundWords(taken, max) {
60
+ if (taken <= 0)
61
+ return 'patched up completely -- nothing left open';
62
+ const share = max > 0 ? taken / max : 1;
63
+ if (share <= 0.25)
64
+ return 'mostly mended -- a few shallow cuts and bruising left';
65
+ if (share <= 0.5)
66
+ return 'still carrying real wounds -- sore, stiff, nothing reopening if they take it easy';
67
+ if (share <= 0.75)
68
+ return 'still badly hurt -- deep wounds you could not get to';
69
+ return 'barely holding together -- most of the damage is still open';
48
70
  }
49
71
  /**
50
72
  * Medicine + Logic, once per set of wounds (p.208): the hits ride on the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.376.0",
3
+ "version": "5.378.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",