@maka/maka-cli 5.171.0 → 5.172.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/commands/move.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +54 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/treat.js +13 -4
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +15 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +56 -27
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +18 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +30 -5
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +16 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-doc.js +67 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.172.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.",
|
|
@@ -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
|
-
|
|
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.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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).
|
|
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 =
|
|
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
|
-
|
|
27
|
-
|
|
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 &&
|
|
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,19 @@
|
|
|
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
|
-
|
|
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
|
+
export const ENGINE_VERSION = '1.47.0';
|
|
400
414
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -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
|
|
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
|
|
@@ -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
|
|
255
|
-
:
|
|
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;
|
|
@@ -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.
|
|
3
|
+
"version": "5.172.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.",
|