@maka/maka-cli 5.171.0 → 5.173.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.171.0",
3
+ "version": "5.173.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.",
@@ -160,7 +160,14 @@ export class BacklogCommand extends Command {
160
160
  // different timestamps on different machines -- so without this
161
161
  // two reports about the same moment cannot be tied together. Seen
162
162
  // for real: one run produced items stamped 00-18-43 and 00-18-15.
163
- sharedRunId: game?.remoteSession?.id,
163
+ // A HOSTED RUN NAMES ITSELF (2026-09-12, the reporter's note on
164
+ // ZdhkP8mAP9QhpqFvA): the browser client's session has no log
165
+ // file to upload, but its id is the key to the event stream the
166
+ // site keeps -- and the site reads the transcript off that
167
+ // (game-backlog-vetting.ts sessionEventsExcerpt). The CLI's own
168
+ // shared-run link keeps its id; the headless host's is the same
169
+ // GameSessions id under another name.
170
+ sharedRunId: game?.remoteSession?.id ?? currentSession()?.sessionId,
164
171
  cliVersion: cliVersion(),
165
172
  engineVersion: engineVersion(),
166
173
  platform: process.platform,
@@ -920,7 +920,12 @@ export class MoveCommand extends Command {
920
920
  actor.atSpot = spotForCell(room, cell);
921
921
  // A shell of yours on that square steps aside (spots.ts heelsTo).
922
922
  yieldSquareToMaster(room, actor);
923
- actor.performAction('steps', `${taken} ${taken === 1 ? 'pace' : 'paces'} ${direction}`);
923
+ // A HUMAN'S PACES ARE QUIET TO THE OTHER HUMANS (uq3nyr2so2ch6E7Wj):
924
+ // the room's NPCs still see the step (their reactions ride the same
925
+ // event); the humans sharing the room watch the dot move instead of
926
+ // reading a line per pace. An NPC's steps stay narrated -- a guard
927
+ // closing the distance is worth a line.
928
+ actor.performAction('steps', `${taken} ${taken === 1 ? 'pace' : 'paces'} ${direction}`, { quiet: this.scene.isHumanControlled(actor) });
924
929
  this.trailCompanions();
925
930
  this.scene.updateStatus();
926
931
  this.scene.updateExits(room);
@@ -7,13 +7,15 @@ import { ownedDrones } from '../utilities/owned-drones.js';
7
7
  * see use.ts): catch your breath somewhere safe. "Safe" means a
8
8
  * Residence-type room (your Hideout qualifies) or a room with no one else
9
9
  * in it -- nobody naps in front of an audience in the sprawl. Each rest
10
- * rolls Body and recovers hits + 1 stun; repeat as needed. Rest never
11
- * touches the physical track (bullet holes take medkits or a heal spell),
12
- * and you can't rest mid-combat-exchange.
10
+ * rolls Body and recovers hits + 1 stun; repeat as needed. A night in a
11
+ * bed that's yours is also the day of NATURAL RECOVERY for the physical
12
+ * track (SR5 p.207, built 2026-09-12 for vFMhSGwLhsfpiY7bs): Body x 2,
13
+ * each hit a box, only once the stun is gone. You can't rest
14
+ * mid-combat-exchange.
13
15
  */
14
16
  export class RestCommand extends Command {
15
17
  static verb = 'rest';
16
- static description = 'Catch your breath somewhere safe to recover Stun (spell drain, stim crashes). Physical wounds need a medkit or a heal spell.';
18
+ static description = 'Catch your breath somewhere safe to recover Stun (spell drain, stim crashes). A night in your own bed also knits physical wounds (Body x 2, once the stun is gone) -- a medkit, a clinic ("treat") or a heal spell is the faster road.';
17
19
  async execute(_args = []) {
18
20
  const actor = this.actor;
19
21
  const room = actor.currentLocation;
@@ -35,7 +37,7 @@ export class RestCommand extends Command {
35
37
  .filter(i => i.droneDamage > 0);
36
38
  const othersHere = this.scene.getActorsInRoom(room).filter(a => a !== actor);
37
39
  const isResidence = room.roomType?.toLowerCase() === 'residence';
38
- if (actor.stunTaken === 0 && damagedDecks.length === 0 && damagedDrones.length === 0) {
40
+ if (actor.stunTaken === 0 && actor.damageTaken === 0 && damagedDecks.length === 0 && damagedDrones.length === 0) {
39
41
  // Nothing to heal -- but sleeping is its own point now (needs
40
42
  // system: edge refills only fed-and-slept, see Game.
41
43
  // applyDepartureNeeds). A bed you own still counts slept-in even
@@ -101,6 +103,52 @@ export class RestCommand extends Command {
101
103
  lines.push(` Rest: ${formatRoll(roll)}${tierDice > 0 ? ` (+${tierDice} -- a real bed)` : tierDice < 0 ? ` (${tierDice} -- the Street sleeps badly)` : ''}${healerDice ? ` (+${healerDice} -- quick healer)` : ''}${insomnia ? ' (halved -- sleep never comes easy)' : ''}${inDebtAtHome ? ' (halved -- shivering is not sleeping)' : ''}`);
102
104
  lines.push(` ${recovered} stun recovered: ${actor.stunSummary()}`);
103
105
  }
106
+ // NATURAL RECOVERY, PHYSICAL (vFMhSGwLhsfpiY7bs, 2026-09-12: "when I
107
+ // rest, my physical health track doesn't heal"). Until now rest said
108
+ // bullet holes were medkit-or-spell business, and canon disagrees:
109
+ // p.207 heals Physical by rest -- Body x 2 (1 day) Extended Test,
110
+ // each hit a box -- once the Stun track is clear, since "Physical
111
+ // damage cannot be healed through rest if the character also has
112
+ // Stun damage". The engine's night in your own bed is that day (a
113
+ // job-site corner is not: it clears the head and nothing else);
114
+ // the stun roll above runs first, so a rest that empties the stun
115
+ // track goes on to knit bone the same night. Modifiers ride as they
116
+ // do for stun: the bed's lifestyle dice, Quick Healer, and the
117
+ // doc's Medicine aftercare (+1 per hit, p.208). Glitches per p.207:
118
+ // a glitch only costs time this engine has no clock for; a critical
119
+ // glitch adds 1D3 boxes -- fresh damage, a fresh set of wounds.
120
+ if (actor.damageTaken > 0 && !actor.isDown()) {
121
+ if (actor.stunTaken > 0) {
122
+ lines.push(` The wounds wait: a body clears the stun before it knits bone.${hint(' (Rest again once your head is clear.)')}`);
123
+ }
124
+ else if (!isResidence) {
125
+ lines.push(` Bone knits on a full day's rest in a bed that's yours -- a corner of a job site only clears the head.`);
126
+ }
127
+ else {
128
+ const tierDice = this.game?.lifestyle?.restDice ?? 0;
129
+ const healerDice = actor.bonus('healing');
130
+ const careDice = actor.medicineCareDice;
131
+ const roll = rollPool(Math.max(1, actor.body * 2 + tierDice + healerDice + careDice));
132
+ const knit = roll.criticalGlitch ? 0 : roll.hits;
133
+ const healed = knit > 0 ? actor.healDamage(knit) : 0;
134
+ let worse = 0;
135
+ if (roll.criticalGlitch)
136
+ worse = actor.takeDamage(1 + Math.floor(Math.random() * 3));
137
+ this.logger.write(`RestCommand: ${actor.name} recovery (Body x2) knit ${healed} physical (${actor.damageTaken}/${actor.maxConditionBoxes})${careDice ? ` [aftercare +${careDice}d]` : ''}${roll.criticalGlitch ? ` [critical glitch: +${worse}]` : ''}.`);
138
+ lines.push(` Recovery: ${formatRoll(roll)} (Body x 2${tierDice > 0 ? `, +${tierDice} the bed` : tierDice < 0 ? `, ${tierDice} the Street` : ''}${healerDice ? `, +${healerDice} quick healer` : ''}${careDice ? `, +${careDice} the doc's aftercare` : ''})`);
139
+ if (roll.criticalGlitch) {
140
+ lines.push(` Something tears in the night -- ${worse} box${worse === 1 ? '' : 'es'} worse: ${actor.conditionSummary()}`);
141
+ }
142
+ else if (healed > 0) {
143
+ lines.push(` ${healed} box${healed === 1 ? '' : 'es'} knit: ${actor.conditionSummary()}`);
144
+ }
145
+ else {
146
+ lines.push(` Nothing knits tonight -- the body takes its time.`);
147
+ }
148
+ if (actor.damageTaken === 0)
149
+ actor.clearMedicineCare();
150
+ }
151
+ }
104
152
  for (const deck of damagedDecks) {
105
153
  const wasBricked = deck.isBricked;
106
154
  // Juryrigger (p.75): +2 on the bench -- coaxing boards back to life
@@ -124,7 +172,7 @@ export class RestCommand extends Command {
124
172
  lines.push(` Airframe work on the ${drone.name}: ${formatRoll(repairRoll)}`);
125
173
  lines.push(` ${repaired} box${repaired === 1 ? '' : 'es'} trued and patched: ${drone.droneSummary()}${wasWrecked && !drone.isWrecked ? ' -- the rotors answer again.' : ''}`);
126
174
  }
127
- lines.push(actor.stunTaken === 0 && !damagedDecks.some(d => d.deckDamage > 0) && !damagedDrones.some(d => d.droneDamage > 0)
175
+ lines.push(actor.stunTaken === 0 && actor.damageTaken === 0 && !damagedDecks.some(d => d.deckDamage > 0) && !damagedDrones.some(d => d.droneDamage > 0)
128
176
  ? ` Head clear, gear humming. Back to work.`
129
177
  : hint(` (Rest again to keep recovering.)`));
130
178
  this.actor.performAction('rests', '');
@@ -1,5 +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
4
  /**
4
5
  * Street-doc services (SR5e p.42, "Staying Healthy"): paid physical
5
6
  * healing at a clinic -- the certain, expensive alternative to burning
@@ -10,7 +11,7 @@ import { sameSpot, spotOf, spotRefusal } from '../utilities/spots.js';
10
11
  export class TreatCommand extends Command {
11
12
  static verb = 'treat';
12
13
  static description = 'Pay a street doc to close your physical wounds -- 100 nuyen per box, no dice, no risk. Works at a clinic with the doc present.';
13
- static PRICE_PER_BOX = 100;
14
+ static PRICE_PER_BOX = TREAT_PRICE_PER_BOX;
14
15
  async execute(_args = []) {
15
16
  const actor = this.actor;
16
17
  if (actor.plane !== 'meat') {
@@ -23,12 +24,13 @@ export class TreatCommand extends Command {
23
24
  return `You're past treatment.`;
24
25
  }
25
26
  const room = actor.currentLocation;
26
- const isClinic = room.roomType === 'Clinic' || /clinic|infirmary|surgery/.test(`${room.roomType ?? ''} ${room.name}`.toLowerCase());
27
- if (!isClinic) {
27
+ // The clinic and the doc are decided in utilities/street-doc.ts --
28
+ // the same predicates that tell the doc's AI brief what it sells.
29
+ if (!isClinicRoom(room)) {
28
30
  return `Treatment happens at a clinic -- there's one on Market Row.`;
29
31
  }
30
32
  const doc = this.scene.getActorsInRoom(room)
31
- .find((a) => a !== actor && a.plane === 'meat' && !a.isIncapacitated() && a.skillRating('first-aid') >= 4);
33
+ .find((a) => a !== actor && isStreetDoc(a));
32
34
  if (!doc) {
33
35
  return `The clinic's open but nobody with steady hands is in it.`;
34
36
  }
@@ -64,6 +66,13 @@ export class TreatCommand extends Command {
64
66
  ];
65
67
  if (actor.damageTaken > 0) {
66
68
  lines.push(` "${actor.damageTaken} more when you can afford it. Try not to reopen my work."`);
69
+ // MEDICINE'S AFTERCARE (p.207-208; XpW98XthhN4biT4gM): the doc's
70
+ // one Medicine + Logic test on this set of wounds rides the boxes
71
+ // the patient could not pay for as bonus dice on their recovery
72
+ // rests. Once per set; free with any treatment.
73
+ const care = applyMedicineCare(doc, actor);
74
+ if (care)
75
+ lines.push(care);
67
76
  }
68
77
  // Bleeding on someone's table is how you get to know them.
69
78
  const bond = this.game?.touchContact(doc.name, 'trade', cost);
@@ -396,5 +396,24 @@
396
396
  // room's live grid, every body's engine-resolved square, and the room
397
397
  // panel's own hostile test; the map event carries the fog-applied
398
398
  // district layout (data.district). Envelope and beat shape both grew.
399
- export const ENGINE_VERSION = '1.45.0';
399
+ // 1.46.0 (2026-09-12): EVERY HUMAN AT THE TABLE HAS A SHEET. The condition
400
+ // sink is handed the reporting actor's name, and every human at a hosted
401
+ // table reports (late joiners included), not just the primary. The beat's
402
+ // actors carry kind 'party' for the table's other humans and
403
+ // `phaseActor` (a name, or null) for whose Action Phase is live in the
404
+ // room; the encounter beats every human in its room when the phase
405
+ // changes hands. Another human's plain paces are no longer narrated to
406
+ // the room's other humans.
407
+ // 1.47.0 (2026-09-12): THE BODY HEALS. Rest at home runs canon's natural
408
+ // recovery for the Physical track (Body x 2, each hit a box, once the
409
+ // stun is gone; critical glitch worsens). The clinic's treatment throws
410
+ // in Medicine aftercare (+hits as recovery dice, once per set of wounds,
411
+ // persisted), the street doc has the Medicine skill, and a doc asked
412
+ // about wounds names the verb and the price.
413
+ // 1.48.0 (2026-09-12): THE ROOM AND THE REPORT. A hosted run's backlog
414
+ // report names its session (sharedRunId) so the site can vet it with the
415
+ // run's own transcript; an exit-serving spot that is furniture (a booth,
416
+ // a counter) sits beside its doorway instead of on it; a job's objective
417
+ // is clamped to a carriable weight at scene build.
418
+ export const ENGINE_VERSION = '1.48.0';
400
419
  //# sourceMappingURL=engine-version.js.map
@@ -5,6 +5,9 @@ import { Door } from '../models/door.js';
5
5
  import { RoomFactory } from './room-factory.js';
6
6
  import { NPCFactory } from './npc-factory.js';
7
7
  import { ItemFactory } from './item-factory.js';
8
+ /** The heaviest a job's prize may be: a Strength 1 runner carries 30 kg
9
+ * (player.ts maxCarryingWeight), and they still need room for gear. */
10
+ export const OBJECTIVE_MAX_WEIGHT_KG = 15;
8
11
  import { Category } from '../types/shared/item-enum.js';
9
12
  import { Logger } from '../utilities/logger.js';
10
13
  import { migrateLegacySeed } from './seed-migration.js';
@@ -262,6 +265,21 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
262
265
  const itemMap = new Map();
263
266
  for (const itemJson of json.items ?? []) {
264
267
  const item = await itemFactory.createItemFromJson(itemJson);
268
+ // THE PRIZE HAS TO BE CARRIABLE (KSfzQzuHTx7kHjohi, 2026-09-12:
269
+ // "I shouldn't have an item I need to return, that I can't carry
270
+ // -- this case is too heavy"). The seed's items are written by a
271
+ // model that does not know the carry cap (20 + Strength x 10 kg,
272
+ // player.ts maxCarryingWeight); a "heavy case" at 60 kg is a job
273
+ // no runner can finish. The win condition's item is clamped to a
274
+ // weight the weakest runner can lift -- an objective you cannot
275
+ // pick up is a seed error, not a challenge. Other items keep
276
+ // whatever the seed said.
277
+ if (json.winCondition?.item === item.name) {
278
+ if (item.weight > OBJECTIVE_MAX_WEIGHT_KG) {
279
+ console.warn(`[scene] objective "${item.name}" weighs ${item.weight} kg -- clamped to ${OBJECTIVE_MAX_WEIGHT_KG} so it can be carried.`);
280
+ item.weight = OBJECTIVE_MAX_WEIGHT_KG;
281
+ }
282
+ }
265
283
  itemMap.set(item.name, item);
266
284
  // Step 4.5: Distribute Items
267
285
  if (itemJson.heldBy) {
@@ -2393,33 +2393,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2393
2393
  // nowhere else, so NPCs and crew shells (which are Players too)
2394
2394
  // never report; the anonymous/bench gate is evaluated at fire time
2395
2395
  // because _anonymous is decided after construction.
2396
- this.player.onConditionChanged = () => {
2397
- // THE TERMINAL'S ROOM PANE RIDES THE SAME SIGNAL (q6tBaFn5EDmGMoJFh):
2398
- // the room now reports every occupant's move to the humans in it
2399
- // (Room.noteOccupancyChanged), and this is the one hook a human's
2400
- // report fires -- so a body stepping across the room while the
2401
- // player watches repaints here at once, not on the next keystroke
2402
- // or the 2-second map heartbeat. Same call a typed move makes;
2403
- // updateExits is a no-op without a screen, and combat repaints
2404
- // itself blow by blow (see mapHeartbeatTick).
2405
- if (this._mapBox && !this.player.inExchange)
2406
- this.updateExits(this.player.currentLocation);
2407
- if (this._anonymous || this._bench)
2408
- return;
2409
- // Checked HERE too, before the import -- a dynamic import after a
2410
- // Jest file's teardown is itself the failure, so the gate cannot
2411
- // live only on the far side of one. Irrelevant when a conditionSink
2412
- // is wired (no DDP hop for that path to silence).
2413
- if (!this._conditionSink && process.env.MAKA_NO_DDP === '1')
2414
- return;
2415
- void import('./utilities/condition-report.js')
2416
- .then(m => m.reportConditionSoon(this.player, this._conditionSink, {
2417
- // The room panel's own red (updateExits below): stance OR live
2418
- // enmity in the viewer's encounter. Same predicate, same map.
2419
- isHostile: a => hostileByStance(a) || enemyInFight(this.scene, this.player, a),
2420
- }))
2421
- .catch(() => undefined);
2422
- };
2396
+ this.wireConditionReporting(this.player);
2423
2397
  // A ROOM CHANGE IS A SAVE BEAT (player request 2026-09-02): where you
2424
2398
  // stand in the hub is what a resume restores, so it has to reach the
2425
2399
  // save -- and the server -- as you walk, not only at quit. requestSave
@@ -2450,6 +2424,61 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2450
2424
  });
2451
2425
  }).catch(() => undefined);
2452
2426
  }
2427
+ /**
2428
+ * THE LIVE-SHEET HOOK, PER HUMAN (2026-09-12, genfBamjTvyHTdWhu: "in a
2429
+ * shared run, I'm not the leader. I have no room map"). This used to be
2430
+ * set inline in the constructor for the primary alone, so on a hosted
2431
+ * table only the leader's overlay ever moved -- every other human's
2432
+ * web Room box waited on a beat that never came. Same hook, now
2433
+ * installable on any human at the table: the constructor calls it for
2434
+ * the primary; headless.ts's addPlayer calls it for a late joiner.
2435
+ *
2436
+ * A NON-PRIMARY REPORTS ONLY THROUGH AN IN-PROCESS SINK. The HubLink
2437
+ * path reports as the LOGGED-IN ACCOUNT's own runner (the server keys
2438
+ * the overlay off this.userId), so a second human's beat sent that way
2439
+ * would land on somebody else's sheet. Without a sink -- the solo CLI,
2440
+ * a harness -- a late joiner simply gets no hook, which is what they
2441
+ * always had. NPCs and crew shells (Players too) never come through
2442
+ * here, so they never report.
2443
+ */
2444
+ wireConditionReporting(player) {
2445
+ const primary = player === this.player;
2446
+ if (!primary && !this._conditionSink)
2447
+ return;
2448
+ player.onConditionChanged = () => {
2449
+ // THE TERMINAL'S ROOM PANE RIDES THE SAME SIGNAL (q6tBaFn5EDmGMoJFh):
2450
+ // the room now reports every occupant's move to the humans in it
2451
+ // (Room.noteOccupancyChanged), and this is the one hook a human's
2452
+ // report fires -- so a body stepping across the room while the
2453
+ // player watches repaints here at once, not on the next keystroke
2454
+ // or the 2-second map heartbeat. Same call a typed move makes;
2455
+ // updateExits is a no-op without a screen, and combat repaints
2456
+ // itself blow by blow (see mapHeartbeatTick).
2457
+ if (primary && this._mapBox && !this.player.inExchange)
2458
+ this.updateExits(this.player.currentLocation);
2459
+ if (this._anonymous || this._bench)
2460
+ return;
2461
+ // Checked HERE too, before the import -- a dynamic import after a
2462
+ // Jest file's teardown is itself the failure, so the gate cannot
2463
+ // live only on the far side of one. Irrelevant when a conditionSink
2464
+ // is wired (no DDP hop for that path to silence).
2465
+ if (!this._conditionSink && process.env.MAKA_NO_DDP === '1')
2466
+ return;
2467
+ void import('./utilities/condition-report.js')
2468
+ .then(m => m.reportConditionSoon(player, this._conditionSink, {
2469
+ // The room panel's own red (updateExits below): stance OR live
2470
+ // enmity in the viewer's encounter. Same predicate, same map.
2471
+ isHostile: a => hostileByStance(a) || enemyInFight(this.scene, player, a),
2472
+ // The table's other humans (tngyd7sRda2omZQGF) -- the same
2473
+ // "is this a human" the scene answers everywhere else.
2474
+ isParty: a => a !== player && this.scene.isHumanControlled(a),
2475
+ // Whose Action Phase is live in the viewer's room
2476
+ // (PYK6zCKhQcjRFwLSQ); undefined between phases and out of a fight.
2477
+ phaseActor: () => this.scene.encounterIn(player.currentLocation)?.phaseActor?.name,
2478
+ }))
2479
+ .catch(() => undefined);
2480
+ };
2481
+ }
2453
2482
  registerCommands() {
2454
2483
  CommandFactory.registerCommand('look', LookCommand);
2455
2484
  CommandFactory.registerCommand('go', GoCommand);
@@ -400,6 +400,18 @@ export async function createHeadlessSession(opts) {
400
400
  // NPC stepping on its own reflect chain between commands
401
401
  // reaches the browser without anyone typing.
402
402
  game.player.noteConditionChanged();
403
+ // EVERY OTHER HUMAN WHO SAW THE ROOM CHANGE (genfBamjTvyHTdWhu):
404
+ // a member's overlay is written from the member's OWN beat now
405
+ // (Game.wireConditionReporting), so the same once-per-command
406
+ // nudge covers the typist and whoever shares their room -- a
407
+ // kill or a loot changes the room for all of them.
408
+ const typist = game.scene.getPlayerByName(actorName);
409
+ for (const p of game.scene.getPlayers()) {
410
+ if (p === game.player || game.scene.deadPlayers.has(p.name))
411
+ continue;
412
+ if (p === typist || (typist && p.currentLocation === typist.currentLocation))
413
+ p.noteConditionChanged();
414
+ }
403
415
  return res;
404
416
  })),
405
417
  addPlayer: (save) => runInSession(ctx, async () => {
@@ -424,9 +436,20 @@ export async function createHeadlessSession(opts) {
424
436
  logger.write(`addPlayer: ${existing.name} is already seated -- treating as a RECONNECT, not a join.`);
425
437
  logger.status(existing.name, game.statusLinesFor(existing));
426
438
  pushMap();
439
+ // The browser's Room box reads the overlay, not the room
440
+ // event: one fresh beat so a reconnecting member repaints
441
+ // their own map too (genfBamjTvyHTdWhu).
442
+ existing.noteConditionChanged();
427
443
  return existing.name;
428
444
  }
429
445
  const p = buildPlayerFromSave(save);
446
+ // A MEMBER'S OWN LIVE-SHEET LANE (genfBamjTvyHTdWhu: "in a
447
+ // shared run, I'm not the leader. I have no room map"). The
448
+ // hook the constructor installs on the primary, installed on
449
+ // the joiner too: their beats reach the sink under their own
450
+ // name and the host writes them to THEIR overlay. Wired before
451
+ // the spawn so the seating below is the first beat.
452
+ game.wireConditionReporting(p);
430
453
  const room = spawnRoom();
431
454
  game.scene.addPlayer(p, room);
432
455
  // ROOM GRIDS (spatial wave 2026-08-25, included shared-run
@@ -438,6 +461,7 @@ export async function createHeadlessSession(opts) {
438
461
  if (spot)
439
462
  p.atSpot = spot;
440
463
  loadContract(save);
464
+ p.noteConditionChanged();
441
465
  logger.system(`${p.name} steps out of a taxi and into the job.`, 'all', 'joined', { player: p.name });
442
466
  logger.status(p.name, game.statusLinesFor(p));
443
467
  pushMap();
@@ -1,4 +1,6 @@
1
1
  import { registerNotice, registerProvocation, PROVOCATION } from '../utilities/hostile-contact.js';
2
+ import { hint } from '../utilities/hints.js';
3
+ import { WOUND_TALK, isClinicRoom, isStreetDoc, streetDocBrief, treatHint } from '../utilities/street-doc.js';
2
4
  import { capitalCase } from 'change-case';
3
5
  import { tableCrewNote, tableDeliveryNote } from '../utilities/table-notes.js';
4
6
  import { Player } from './player.js';
@@ -1611,6 +1613,18 @@ ${worldEventsSummary}
1611
1613
  return;
1612
1614
  }
1613
1615
  this.logger.write('Attempting to respond');
1616
+ // THE DOC NAMES THE VERB (XpW98XthhN4biT4gM, 2026-09-12: "I just
1617
+ // asked the doc for help with my wounds ... not just 'do nothing'").
1618
+ // Whatever the model makes of it, a bleeding patient asking a street
1619
+ // doc about wounds gets the one line that actually helps, under the
1620
+ // reply: the command and the price. Deterministic, so it does not
1621
+ // depend on the AI reading its own brief (streetDocBrief below).
1622
+ if (isStreetDoc(this) && isClinicRoom(this.currentLocation) && WOUND_TALK.test(message)
1623
+ && (actor.damageTaken > 0 || actor.stunTaken > 0)) {
1624
+ const h = hint(treatHint());
1625
+ if (h)
1626
+ this.logger.logWithColor(h, 'white', { actor: actor.name });
1627
+ }
1614
1628
  // A fresh stimulus earns at most one fresh speech (see the field doc)
1615
1629
  // -- and a clean slate of attempts. Something new has happened, so
1616
1630
  // what didn't work against the OLD situation is worth trying again.
@@ -1665,6 +1679,9 @@ ${worldEventsSummary}
1665
1679
  // circling closer"). Same shape as payoutSummary above -- nothing
1666
1680
  // told the model the run's actual heat, so a reassuring line had
1667
1681
  // nothing to check itself against.
1682
+ // What this NPC SELLS, told to the model (XpW98XthhN4biT4gM): the
1683
+ // street doc's trade is treatment, and the word for it is "treat".
1684
+ const servicesSummary = isStreetDoc(this) && isClinicRoom(this.currentLocation) ? streetDocBrief() : '';
1668
1685
  const heat = this._scene?.ownerGame?.heatSummaryForDialogue?.();
1669
1686
  const heatSummary = heat
1670
1687
  ? `The district's current HEAT on this run is ${heat}. If you would naturally reassure them that things are quiet, clean, or under control, that claim has to be TRUE against this -- never tell them the coast is clear, that nobody noticed, or that things went quiet if heat says otherwise. Speak honestly about how hot it actually is instead.`
@@ -2124,6 +2141,7 @@ Your objective is to drive the story line.`}
2124
2141
  ${dialogueSummary}
2125
2142
  ${eph ? '' : payoutSummary}
2126
2143
  ${eph ? '' : heatSummary}
2144
+ ${eph ? '' : servicesSummary}
2127
2145
  ${eph ? '' : jobItemSummary}
2128
2146
 
2129
2147
  ${roomDesc}${spotBlock}${combatSpatialBlock}
@@ -2827,19 +2827,29 @@ export class Player extends AbstractPlayer {
2827
2827
  }
2828
2828
  this.logger.logWithColor(`{white-fg}${actor.name}:{/white-fg} ${message}`, (actor === this) ? 'yellow' : 'green', { actor: this.name });
2829
2829
  }
2830
- see(actor, verb, details) {
2830
+ see(actor, verb, details, quiet) {
2831
2831
  if (actor !== this && this.canPerceive(actor)) {
2832
2832
  this.logger.write(`You see ${actor.name} ${verb} ${details}`);
2833
+ // A QUIET ACT IS SEEN, NOT NARRATED (2026-09-12, uq3nyr2so2ch6E7Wj:
2834
+ // "let's not show the individual steps the other players are
2835
+ // making"). Your own plain step already says nothing (move.ts,
2836
+ // QUIET STEPS: the map shows where you stand); another human's
2837
+ // step printed "<name> steps 2 paces north" to everyone sharing
2838
+ // the room, every pace, and the map was already moving their dot.
2839
+ // The event still fans out -- an NPC's see() reacts to it the same
2840
+ // as before -- only the human's log line is dropped.
2841
+ if (quiet)
2842
+ return;
2833
2843
  this.logger.logWithColor(`${actor.name} ${verb} ${details}`, 'cyan');
2834
2844
  }
2835
2845
  }
2836
- performAction(verb, details) {
2846
+ performAction(verb, details, opts) {
2837
2847
  const message = `${this.name} performs '${verb}'${details ? `: ${details}` : ''}`;
2838
2848
  this.logger.write(message);
2839
- this.emit('action', { actor: this, verb, details });
2849
+ this.emit('action', { actor: this, verb, details, quiet: opts?.quiet === true });
2840
2850
  }
2841
- _onSeenAction = ({ actor, verb, details }) => {
2842
- void this.see(actor, verb, details);
2851
+ _onSeenAction = ({ actor, verb, details, quiet }) => {
2852
+ void this.see(actor, verb, details, quiet);
2843
2853
  };
2844
2854
  // FOG OF WAR (map ruling 2026-08-24): the map only shows where you've
2845
2855
  // been. Room names, stamped at the one choke point every movement path
@@ -2929,6 +2939,20 @@ export class Player extends AbstractPlayer {
2929
2939
  * healed" (p.205). Set by cast.ts's heal, cleared with the above.
2930
2940
  */
2931
2941
  magicallyHealed = false;
2942
+ /**
2943
+ * MEDICINE'S AFTERCARE (SR5 p.207-208; 2026-09-12, XpW98XthhN4biT4gM):
2944
+ * a Medicine + Logic test tended to these wounds, once per set, and its
2945
+ * hits ride every natural-recovery rest as bonus dice until the wounds
2946
+ * close. Cleared with the set. Persisted (persistence.ts) -- the doc
2947
+ * is in the hub and so is the bed, but a save can sit between them.
2948
+ */
2949
+ medicineCareGiven = false;
2950
+ medicineCareDice = 0;
2951
+ /** The wounds closed: nothing left for the aftercare to help. */
2952
+ clearMedicineCare() {
2953
+ this.medicineCareGiven = false;
2954
+ this.medicineCareDice = 0;
2955
+ }
2932
2956
  /** Applies damage boxes; returns the number actually taken. */
2933
2957
  /**
2934
2958
  * THE LIVE-SHEET HOOK (2026-09-01): Game sets this on the PRIMARY
@@ -2960,6 +2984,7 @@ export class Player extends AbstractPlayer {
2960
2984
  clearWoundSet() {
2961
2985
  this.firstAidGiven = false;
2962
2986
  this.magicallyHealed = false;
2987
+ this.clearMedicineCare();
2963
2988
  }
2964
2989
  /**
2965
2990
  * Clears damage boxes; returns how many were actually healed (can be
@@ -624,6 +624,7 @@
624
624
  "charisma": 3,
625
625
  "skills": {
626
626
  "first-aid": 6,
627
+ "medicine": 6,
627
628
  "perception": 4,
628
629
  "negotiation": 3
629
630
  }
@@ -389,9 +389,26 @@ export class CombatEncounter {
389
389
  this.running = false;
390
390
  }
391
391
  }
392
+ /**
393
+ * WHOSE PHASE, ON EVERY HUMAN'S MAP (2026-09-12, PYK6zCKhQcjRFwLSQ: "on
394
+ * the map, who's turn it is I'd like a stroke around the icon that
395
+ * pulses"). The condition beat carries the room's live phase holder
396
+ * (condition-report.ts phaseActor); a beat only fires off a Player's
397
+ * own hook, and a phase changing hands mutates nobody's monitors -- so
398
+ * the encounter tells every human in its room, the way the room does
399
+ * for a move (Room.noteOccupancyChanged). Coalesced downstream: a
400
+ * phase that opens and closes in one tick is one beat per watcher.
401
+ */
402
+ notePhaseChanged() {
403
+ for (const a of this.room.getActors()) {
404
+ if (this.scene.isHumanControlled(a))
405
+ a.noteConditionChanged();
406
+ }
407
+ }
392
408
  async beginPhase(p) {
393
409
  this.phaseActor = p.actor;
394
410
  this.budget = new ActionBudget();
411
+ this.notePhaseChanged();
395
412
  p.surprised = false;
396
413
  p.delayed = false;
397
414
  p.nudges = 0;
@@ -452,6 +469,7 @@ export class CombatEncounter {
452
469
  p.delayed = true;
453
470
  this.phaseActor = undefined;
454
471
  this.budget = undefined;
472
+ this.notePhaseChanged();
455
473
  if (others.length === 0) {
456
474
  // Nobody left to wait for -- the delayed phase is simply now.
457
475
  p.delayed = false;
@@ -469,6 +487,7 @@ export class CombatEncounter {
469
487
  p.delayed = false;
470
488
  this.phaseActor = undefined;
471
489
  this.budget = undefined;
490
+ this.notePhaseChanged();
472
491
  this.scene.notifyExchangeSettled(this.participants.map(x => x.actor));
473
492
  }
474
493
  /** The phase actor has left the room or fallen: the pass moves on. */
@@ -594,6 +613,7 @@ export class CombatEncounter {
594
613
  this.ended = true;
595
614
  this.phaseActor = undefined;
596
615
  this.budget = undefined;
616
+ this.notePhaseChanged();
597
617
  for (const p of this.participants) {
598
618
  p.actor.clearAim();
599
619
  p.actor.recoilRoundsFired = 0;
@@ -99,7 +99,7 @@ function roomGridFor(player) {
99
99
  }
100
100
  }
101
101
  export function buildConditionReport(player, deps) {
102
- const { saveSlug, arSightUp, isHostile } = deps;
102
+ const { saveSlug, arSightUp, isHostile, isParty, phaseActor } = deps;
103
103
  // BRICKED INCLUDED (web report 2026-09-02: "my deck got bricked, but its
104
104
  // status still says 4/12"): a forced dump clears activeDeck and
105
105
  // getCyberdeck() hides a bricked deck, so the beat read nothing and the
@@ -250,9 +250,12 @@ export function buildConditionReport(player, deps) {
250
250
  // THE SAME RED THE ROOM PANEL USES (see IConditionReportDeps
251
251
  // .isHostile): stance flag OR live enmity, and never a
252
252
  // surrendered one -- hostileByStance owns that clause.
253
+ // PARTY BEFORE EVERYTHING BUT DOWN (tngyd7sRda2omZQGF): another
254
+ // human at the table is your party whatever else is true of them.
253
255
  kind: a.isIncapacitated() ? 'downed'
254
- : a.allyOf ? 'ally'
255
- : (isHostile?.(a) ?? hostileByStance(a)) ? 'hostile' : 'neutral',
256
+ : isParty?.(a) ? 'party'
257
+ : a.allyOf ? 'ally'
258
+ : (isHostile?.(a) ?? hostileByStance(a)) ? 'hostile' : 'neutral',
256
259
  role: actorRole(a),
257
260
  spot: a.atSpot,
258
261
  cell: a.atCell ?? seats.get(a.name),
@@ -261,6 +264,13 @@ export function buildConditionReport(player, deps) {
261
264
  // panel's 💀. Gone from the list the moment a body is picked clean
262
265
  // (loot.ts removeBody), which with the per-command beat is at once.
263
266
  bodies: roomBodies(player),
267
+ // WHOSE PHASE (PYK6zCKhQcjRFwLSQ): a name while an Action Phase is
268
+ // live in the viewer's room, `null` when none is -- an explicit
269
+ // nobody, because the site $sets only what a beat carries, and an
270
+ // absent field would leave the last phase holder pulsing after the
271
+ // fight was over. Not on the beat at all when the caller has no
272
+ // Scene to ask (harness, bench).
273
+ ...(phaseActor ? { phaseActor: phaseActor() ?? null } : {}),
264
274
  };
265
275
  }
266
276
  function roomBodies(player) {
@@ -297,7 +307,7 @@ function noteRoster(report) {
297
307
  const ADDITIVE_FIELDS = [
298
308
  'ar', 'actors', 'deckLine', 'deckDamage', 'astralPerceiving', 'bodyRoom', 'sustainingPenalty',
299
309
  'matrix', 'panMarks', 'hostMarksOnYou', 'marksPlaced', 'marksOnYou', 'deckStored', 'droneDamage', 'companions', 'pools', 'initiative',
300
- 'karma', 'nuyen', 'edge', 'grid', 'bodies',
310
+ 'karma', 'nuyen', 'edge', 'grid', 'bodies', 'phaseActor',
301
311
  ];
302
312
  /** Refusal reasons already written to the session log -- one line each. */
303
313
  const announcedRefusals = new Map();
@@ -342,13 +352,13 @@ extra) {
342
352
  // stance and brandish flow through their accessors' own reports;
343
353
  // sustained and deck programs are composed here and ride whatever
344
354
  // beat fires next. All read at FIRE time, like position.
345
- const report = buildConditionReport(player, { saveSlug, arSightUp, isHostile: extra?.isHostile });
355
+ const report = buildConditionReport(player, { saveSlug, arSightUp, isHostile: extra?.isHostile, isParty: extra?.isParty, phaseActor: extra?.phaseActor });
346
356
  noteRoster(report);
347
357
  if (sink) {
348
358
  // IN-PROCESS: no HubLink, no MAKA_NO_DDP gate, no version-skew
349
359
  // retry -- the engine module and its caller are always the same
350
360
  // deployed version here (see ConditionSink's doc comment).
351
- await sink(report);
361
+ await sink(report, player.name);
352
362
  return;
353
363
  }
354
364
  const { HubLink } = await import('./shared-run.js');
@@ -359,6 +359,8 @@ export function serializePlayer(p) {
359
359
  stunTaken: p.stunTaken,
360
360
  firstAidGiven: p.firstAidGiven,
361
361
  magicallyHealed: p.magicallyHealed,
362
+ medicineCareGiven: p.medicineCareGiven,
363
+ medicineCareDice: p.medicineCareDice,
362
364
  needs: {
363
365
  ateThisCycle: p.ateThisCycle, sleptThisCycle: p.sleptThisCycle,
364
366
  hungerStreak: p.hungerStreak, sleepStreak: p.sleepStreak,
@@ -522,6 +524,8 @@ export function restorePlayer(p, s) {
522
524
  // -- a reload would launder a spent medkit into a fresh one.
523
525
  p.firstAidGiven = s.firstAidGiven ?? false;
524
526
  p.magicallyHealed = s.magicallyHealed ?? false;
527
+ p.medicineCareGiven = s.medicineCareGiven ?? false;
528
+ p.medicineCareDice = s.medicineCareDice ?? 0;
525
529
  p.addLockedFatigue('hunger', s.needs.hungerFatigue);
526
530
  p.addLockedFatigue('sleep', s.needs.sleepFatigue);
527
531
  p.ateThisCycle = s.needs.ateThisCycle;
@@ -1,4 +1,11 @@
1
1
  import { Direction } from '../types/shared/direction-enum.js';
2
+ export function doorwayKindOf(spotName) {
3
+ if (/stair|\bsteps?\b|ladder|hatch|shaft|chute|\bramp\b|\blift\b|elevator|escalator/i.test(spotName))
4
+ return 'vertical';
5
+ if (/door|entrance|entry|threshold|gate|arch|portal|airlock/i.test(spotName))
6
+ return 'lateral';
7
+ return undefined;
8
+ }
2
9
  export const METERS_PER_CELL = 2;
3
10
  /**
4
11
  * How tall one storey is. A CHOICE, not a citation -- SR5 never says how
@@ -609,7 +616,23 @@ export function synthesizeGrid(input) {
609
616
  // own walls; the hub sits where all of them are plausibly reachable.
610
617
  for (const [spotName, served] of exitCellsBySpot.entries()) {
611
618
  if (served.length === 1) {
612
- grid.spotCells.set(spotName, served[0]);
619
+ // FURNITURE BESIDE THE DOOR, NOT IN IT (XanDMPmRrbJQ8ta8e,
620
+ // 2026-09-12: "the corner booth is right in front of the door
621
+ // north ... doors and booths should not be on top of each
622
+ // other"). A seed may name any spot as the one an exit is
623
+ // reached from -- "the booth by the north door". Only a spot
624
+ // that IS a doorway (a door, a gate, the stairs) takes the
625
+ // doorway cell; a booth, a counter or a bar takes the nearest
626
+ // free cell beside it instead. The doorway and its approach are
627
+ // already claimed above, so "nearest free" is next to the door,
628
+ // never on it or in front of it.
629
+ if (doorwayKindOf(spotName)) {
630
+ grid.spotCells.set(spotName, served[0]);
631
+ continue;
632
+ }
633
+ const beside = nearestFree(served[0], occupied, dims, exists);
634
+ claim(beside);
635
+ grid.spotCells.set(spotName, beside);
613
636
  continue;
614
637
  }
615
638
  const mid = {
@@ -2,7 +2,10 @@ import { Direction } from '../types/shared/direction-enum.js';
2
2
  import { significantWords, wordsAreCloseEnough } from './fuzzy-match.js';
3
3
  import { hint } from './hints.js';
4
4
  import { placeName } from './log-style.js';
5
- import { findPath, distanceMeters, coverBetween, blockingRegionFor, pathClimbSteps, levelFootprint, key, isCellBlocked, pathSqueezes, METERS_PER_CELL, } from './room-grid.js';
5
+ import { findPath, distanceMeters, coverBetween, blockingRegionFor, pathClimbSteps, levelFootprint, key, isCellBlocked, pathSqueezes, METERS_PER_CELL, doorwayKindOf, } from './room-grid.js';
6
+ // The doorway vocabulary moved to room-grid.ts (the grid seats exit
7
+ // spots by it); every caller of spots.ts keeps its import.
8
+ export { doorwayKindOf };
6
9
  /**
7
10
  * Intra-room locations ("at" spots -- player request: every room felt
8
11
  * like everything stood at your elbow; walking into the bar had both
@@ -1283,13 +1286,31 @@ export function lockedRouteHint(room, direction, door) {
1283
1286
  }
1284
1287
  return '';
1285
1288
  }
1286
- export function doorwayKindOf(spotName) {
1287
- if (/stair|\bsteps?\b|ladder|hatch|shaft|chute|\bramp\b|\blift\b|elevator|escalator/i.test(spotName))
1288
- return 'vertical';
1289
- if (/door|entrance|entry|threshold|gate|arch|portal|airlock/i.test(spotName))
1290
- return 'lateral';
1291
- return undefined;
1292
- }
1289
+ /**
1290
+ * WHAT KIND OF WAY THROUGH a spot's name describes, or undefined when
1291
+ * it isn't a way through at all (a bar, a workbench, the stalls).
1292
+ *
1293
+ * THE ONE DOORWAY PREDICATE (item A, 2026-08-27). There were two
1294
+ * separately-written copies of this regex -- here in entrySpotFor and
1295
+ * in Room.namedDoorwayFor -- free to drift apart, and neither knew the
1296
+ * distinction that actually matters: a "back door" is a LATERAL way
1297
+ * through and a "way down" is a VERTICAL one. Kind-blindness is what
1298
+ * let namedDoorwayFor bind a spot called "the back door" to a DOWN
1299
+ * exit, which put the basement's barrier at the back door (the player's
1300
+ * report) and, because vertical exits are centre-placed, simultaneously
1301
+ * put the back door in the middle of the room (the older report). One
1302
+ * defect, two bug reports.
1303
+ *
1304
+ * This is a PERMANENT component, not a migration shim. Generation is
1305
+ * moving toward binding every exit to a spot explicitly, but that data
1306
+ * is model-written: validation can make it right more often, never
1307
+ * always. And every scene seeded before that lands has nothing but
1308
+ * names, forever. So this is the honest floor under a seed that is
1309
+ * silent or lying, and it is the LAST resort -- an explicit
1310
+ * room.exitSpots binding always wins.
1311
+ *
1312
+ * (Definition now in room-grid.ts -- see the re-export at the top.)
1313
+ */
1293
1314
  /** The kind of way through a given direction NEEDS. */
1294
1315
  export function exitDoorwayKind(direction) {
1295
1316
  return direction === Direction.UP || direction === Direction.DOWN ? 'vertical' : 'lateral';
@@ -0,0 +1,67 @@
1
+ import { rollPool, formatRoll } from './dice.js';
2
+ /**
3
+ * THE STREET DOC, IN ONE PLACE (2026-09-12; XpW98XthhN4biT4gM "I just
4
+ * asked the doc for help with my wounds -- I want them to help me with my
5
+ * wounds, for a cost of course... not just 'do nothing'", and
6
+ * 4Yz2PGLNoFAfdd2Wt's note: "The Doc in the hub should have a heal
7
+ * ability ... He should charge of course, and have a very high skill in
8
+ * all things medical").
9
+ *
10
+ * The clinic verb (`treat`) has existed since the venue ruling of
11
+ * 2026-08-24 -- what did not exist was any way for the DOC to point a
12
+ * bleeding player at it. `ask`/`say` went to the AI with no idea the
13
+ * NPC sold anything, so "help me with my wounds" got a diagnosis and no
14
+ * treatment. The predicate `treat` gates on, the words the doc is told,
15
+ * and the deterministic hint the player sees all live here so the three
16
+ * cannot drift.
17
+ *
18
+ * MEDICINE'S AFTERCARE (SR5 p.207-208): Medicine does not close boxes; a
19
+ * Medicine + Logic [Mental] test gives +1 die per hit to the patient's
20
+ * later Recovery tests, once per set of wounds, never in combat. The
21
+ * clinic throws it in with treatment -- see applyMedicineCare.
22
+ */
23
+ /** 100 nuyen a box -- the book's medkit-supply price point (treat.ts). */
24
+ export const TREAT_PRICE_PER_BOX = 100;
25
+ export function isClinicRoom(room) {
26
+ if (!room)
27
+ return false;
28
+ return room.roomType === 'Clinic'
29
+ || /clinic|infirmary|surgery/.test(`${room.roomType ?? ''} ${room.name}`.toLowerCase());
30
+ }
31
+ /** Steady hands: the same bar `treat` has always used (First Aid 4+),
32
+ * or a Medicine rating that says the same thing. A body, awake. */
33
+ export function isStreetDoc(actor) {
34
+ return actor.plane === 'meat'
35
+ && !actor.isIncapacitated()
36
+ && (actor.skillRating('first-aid') >= 4 || actor.skillRating('medicine') >= 4);
37
+ }
38
+ /** What a patient says when they mean their wounds. */
39
+ export const WOUND_TALK = /\b(wound|wounds|wounded|hurt|hurting|bleed|bleeding|heal|healing|patch|stitch|stitches|injur\w*|medic\w*|treat\w*|damage|shot|bullet|fix me)\b/i;
40
+ /** The deterministic line under the doc's reply -- the verb, the price. */
41
+ export function treatHint(price = TREAT_PRICE_PER_BOX) {
42
+ return `("treat" -- the doc closes physical wounds, ${price}¥ a box, cash up front. Aftercare rides free.)`;
43
+ }
44
+ /** The doc's standing orders in the AI brief: treatment is the trade,
45
+ * and the word for it is "treat". */
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.`;
48
+ }
49
+ /**
50
+ * Medicine + Logic, once per set of wounds (p.208): the hits ride on the
51
+ * patient as bonus dice for natural recovery (rest.ts) until the wounds
52
+ * close or fresh damage opens a new set. Returns the line to print, or
53
+ * undefined when there is nothing to apply (no Medicine on the doc, no
54
+ * wounds, or this set already tended).
55
+ */
56
+ export function applyMedicineCare(doc, patient) {
57
+ if (patient.damageTaken === 0 || patient.medicineCareGiven)
58
+ return undefined;
59
+ const skill = doc.skillRating('medicine');
60
+ if (skill <= 0)
61
+ return undefined;
62
+ const roll = rollPool(skill + doc.logic);
63
+ patient.medicineCareGiven = true;
64
+ patient.medicineCareDice = roll.hits;
65
+ return ` Aftercare -- Medicine + Logic: ${formatRoll(roll)}. +${roll.hits} ${roll.hits === 1 ? 'die' : 'dice'} on your recovery rests until these wounds close.`;
66
+ }
67
+ //# sourceMappingURL=street-doc.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.171.0",
3
+ "version": "5.173.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.",