@maka/maka-cli 5.199.0 → 5.201.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/ask.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +139 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +115 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +48 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/presence-gate.js +27 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.201.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.",
|
|
@@ -7,6 +7,7 @@ import { FIXER_NAME, isFixerName } from '../factories/scene-seed-generator.js';
|
|
|
7
7
|
import { sameSpot, spotOf, spotRefusal } from '../utilities/spots.js';
|
|
8
8
|
import { catalogGear } from '../utilities/catalog.js';
|
|
9
9
|
import { PAPER_AVAILABILITY } from '../utilities/sin.js';
|
|
10
|
+
import { cleanupQuoteFor } from '../utilities/cleanup-hire.js';
|
|
10
11
|
/**
|
|
11
12
|
* Legwork (SR5e p.387-388): "ask <contact> about <thing>" taps a
|
|
12
13
|
* CONTACT's network for real information -- in person or over a live
|
|
@@ -105,6 +106,15 @@ export class AskCommand extends Command {
|
|
|
105
106
|
if (/\b(work|gig|errand|hiring|delivery)\b/i.test(topic)) {
|
|
106
107
|
return this.offerWork(target, contact);
|
|
107
108
|
}
|
|
109
|
+
// "ask <contact> about cleanup" is a PRICE, not a network pull: no
|
|
110
|
+
// dice and no per-homecoming budget spent. A runner who cannot reach
|
|
111
|
+
// the Matrix buys the scrub instead of running it
|
|
112
|
+
// (utilities/cleanup-errand.ts, user 2026-09-15).
|
|
113
|
+
if (/\b(clean-?up|footage|feeds?|scrub|cameras?)\b/i.test(topic) && this.game) {
|
|
114
|
+
const quote = cleanupQuoteFor(this.game, target.name, topic.replace(/\b(clean-?up|footage|feeds?|scrub|cameras?|the|at|on|about)\b/gi, '').trim());
|
|
115
|
+
if (quote)
|
|
116
|
+
return quote;
|
|
117
|
+
}
|
|
108
118
|
if (contact.asked) {
|
|
109
119
|
return `${target.name} already worked their network for you since you last went out -- nothing new has come in.${hint(` (Legwork refreshes when you come home from a job.)`)}`;
|
|
110
120
|
}
|
|
@@ -27,13 +27,13 @@ export const SPRITE_TYPES = {
|
|
|
27
27
|
name: 'Fault',
|
|
28
28
|
blurb: 'the attack sprite -- a jagged error in the world\'s code that hunts ice',
|
|
29
29
|
combat: l => ({ logic: l + 1, intuition: l, willpower: l + 1, reaction: l + 1, body: 1, agility: 1, strength: 1, charisma: 1,
|
|
30
|
-
skills: { hacking: l + 1, perception: l } }),
|
|
30
|
+
skills: { hacking: l + 1, perception: l, cybercombat: l + 1 } }),
|
|
31
31
|
},
|
|
32
32
|
machine: {
|
|
33
33
|
name: 'Machine',
|
|
34
34
|
blurb: 'the device whisperer -- drones, locks, and cameras answer it',
|
|
35
35
|
combat: l => ({ logic: l + 2, intuition: l, willpower: l, reaction: l + 1, body: 1, agility: 1, strength: 1, charisma: 1,
|
|
36
|
-
skills: { hacking: l, perception: l + 1 } }),
|
|
36
|
+
skills: { hacking: l, perception: l + 1, hardware: l } }),
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
@@ -310,6 +310,13 @@ export class HireCommand extends Command {
|
|
|
310
310
|
// Mechanics panel (ui.ts MENU_VERBS).
|
|
311
311
|
if (args.length === 0)
|
|
312
312
|
return `Hire whom? "crew hire" shows the board.`;
|
|
313
|
+
// "hire cleanup [<site>]" IS NOT A CREW SEAT (user 2026-09-15). It
|
|
314
|
+
// buys a shadow decker to scrub a loose end for a runner who cannot
|
|
315
|
+
// reach the Matrix themselves -- see utilities/cleanup-errand.ts.
|
|
316
|
+
if (/^clean-?up$/i.test(args[0])) {
|
|
317
|
+
const { hireCleanupErrand } = await import('../utilities/cleanup-hire.js');
|
|
318
|
+
return hireCleanupErrand(this.actor, game, args.slice(1).join(' '), this.logger);
|
|
319
|
+
}
|
|
313
320
|
// ONE TABLE AT A TIME (playtest note): riding another leader's
|
|
314
321
|
// table means their jobs are your jobs -- no building your own
|
|
315
322
|
// crew underneath it. "crew leave" first.
|
|
@@ -13,6 +13,7 @@ import { gridActionModifier, gridNote } from '../utilities/grids.js';
|
|
|
13
13
|
import { billAction } from '../utilities/action-cost.js';
|
|
14
14
|
import { MAX_MARKS } from '../utilities/marks.js';
|
|
15
15
|
import { sameHostSide } from '../models/player.js';
|
|
16
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
16
17
|
/** The marks Control Device wants for a Simple device action (p.238). */
|
|
17
18
|
export const MARKS_TO_CONTROL = 2;
|
|
18
19
|
export class DisableCommand extends Command {
|
|
@@ -184,8 +185,8 @@ export class DisableCommand extends Command {
|
|
|
184
185
|
const defenceRoll = rollPool(defence.pool);
|
|
185
186
|
const net = roll.hits - defenceRoll.hits;
|
|
186
187
|
actor.performAction('takes control of a device', name, { quiet: this.scene.isHumanControlled(actor) });
|
|
187
|
-
if (this.scene
|
|
188
|
-
this.logger.meta(
|
|
188
|
+
if (showsMechanics(this.scene, actor)) {
|
|
189
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Control Device (Electronic Warfare + Intuition [Sleaze])${gridNote(grid)}: ${formatRoll(roll)}`);
|
|
189
190
|
this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
|
|
190
191
|
}
|
|
191
192
|
const godLines = this.godBill(defenceRoll.hits, 'Control Device (sleaze)');
|
|
@@ -240,8 +241,8 @@ export class DisableCommand extends Command {
|
|
|
240
241
|
const defenceRoll = rollPool(defence.pool);
|
|
241
242
|
const net = roll.hits - defenceRoll.hits;
|
|
242
243
|
actor.performAction('throws a Data Spike', name, { quiet: this.scene.isHumanControlled(actor) });
|
|
243
|
-
if (this.scene
|
|
244
|
-
this.logger.meta(
|
|
244
|
+
if (showsMechanics(this.scene, actor)) {
|
|
245
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Data Spike (Cybercombat + Logic [Attack ${limit}])${gridNote(grid)}: ${formatRoll(roll)}`);
|
|
245
246
|
this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
|
|
246
247
|
}
|
|
247
248
|
const godLines = this.godBill(defenceRoll.hits, 'Data Spike (attack)');
|
|
@@ -272,8 +273,8 @@ export class DisableCommand extends Command {
|
|
|
272
273
|
const dv = limit + net + 2 * marks;
|
|
273
274
|
const soak = rollPool(this.defence(t).pool);
|
|
274
275
|
const dealt = Math.max(0, dv - soak.hits);
|
|
275
|
-
if (this.scene
|
|
276
|
-
this.logger.meta(
|
|
276
|
+
if (showsMechanics(this.scene, actor)) {
|
|
277
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Data Spike -- ${dv} DV (Attack ${limit} +${net} net${marks > 0 ? ` +${2 * marks} for ${marks} mark${marks === 1 ? '' : 's'}` : ''}) v. ${defence.label}: ${formatRoll(soak)} -> ${dealt}`);
|
|
277
278
|
}
|
|
278
279
|
// An Attack action is never subtle: the owner knows.
|
|
279
280
|
this.wakeOwner(t, false);
|
|
@@ -3,6 +3,7 @@ import { Door } from '../models/door.js';
|
|
|
3
3
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
4
4
|
import { ownedDrones } from '../utilities/owned-drones.js';
|
|
5
5
|
import { droneReconLaunchLine, droneReconNowhereLine, droneReconReturnLine, droneSlipsVents } from '../utilities/drone-prose.js';
|
|
6
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
6
7
|
/**
|
|
7
8
|
* The rigger's mechanic: launch a carried surveillance drone for a live
|
|
8
9
|
* sweep of every room adjacent to this one -- who's there, what's locked,
|
|
@@ -42,13 +43,20 @@ export class DroneCommand extends Command {
|
|
|
42
43
|
// sweep comes back as static -- relaunch and try again.
|
|
43
44
|
const piloting = actor.skillRating('piloting');
|
|
44
45
|
const roll = rollPool(Math.max(1, actor.reaction + (piloting > 0 ? piloting : -1) + actor.bonus('piloting') + actor.woundModifier - actor.sustainingPenalty));
|
|
46
|
+
// DICE GO TO THE MECHANICS PANEL, like every other test in the engine
|
|
47
|
+
// (wjFBY3zgyWzJP9adw sweep). This one was written into the prose line
|
|
48
|
+
// instead -- the only player roll a rigger had to read out of a
|
|
49
|
+
// sentence.
|
|
50
|
+
if (showsMechanics(this.scene, actor)) {
|
|
51
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Piloting (recon sweep): ${formatRoll(roll)}`);
|
|
52
|
+
}
|
|
45
53
|
if (roll.hits === 0) {
|
|
46
54
|
return [
|
|
47
|
-
droneReconLaunchLine(drone,
|
|
55
|
+
droneReconLaunchLine(drone, false),
|
|
48
56
|
` It clips a railing, spins, and the feed dissolves into static. It limps home with nothing.`,
|
|
49
57
|
].join('\n');
|
|
50
58
|
}
|
|
51
|
-
const lines = [droneReconLaunchLine(drone,
|
|
59
|
+
const lines = [droneReconLaunchLine(drone, true)];
|
|
52
60
|
for (const [direction, exit] of exits) {
|
|
53
61
|
let target;
|
|
54
62
|
let lockNote = '';
|
|
@@ -5,6 +5,7 @@ import { billAction } from '../utilities/action-cost.js';
|
|
|
5
5
|
import { hostDefensePool } from '../utilities/matrix-intrusion.js';
|
|
6
6
|
import { hostLabel } from '../utilities/grid-names.js';
|
|
7
7
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
8
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
8
9
|
/**
|
|
9
10
|
* EDIT FILE (SR5 p.239, RAG-checked 2026-09-14): a Complex Action, Marks
|
|
10
11
|
* Required 1, Computer + Logic [Data Processing] v. Intuition + Firewall.
|
|
@@ -50,6 +51,17 @@ export class EditFileCommand extends Command {
|
|
|
50
51
|
}
|
|
51
52
|
async execute(args = []) {
|
|
52
53
|
const actor = this.actor;
|
|
54
|
+
// "erase mark[s] on <icon>" IS A DIFFERENT ACTION (f79LLqTerep4nKWcA).
|
|
55
|
+
// Edit File deletes a FILE; Erase Mark (p.239) scrubs a recognition
|
|
56
|
+
// key, and they share only the English word. The player who types
|
|
57
|
+
// "erase mark" means the second one, so route it rather than
|
|
58
|
+
// answering "no file called mark".
|
|
59
|
+
if (/^marks?$/i.test(args[0] ?? '') || /^marks?$/i.test(args[1] ?? '')) {
|
|
60
|
+
const { EraseMarkCommand } = await import('./erase-mark.js');
|
|
61
|
+
return new EraseMarkCommand({
|
|
62
|
+
actor: this.actor, scene: this.scene, rooms: this.rooms, screen: this.screen, game: this.game,
|
|
63
|
+
}).execute(args);
|
|
64
|
+
}
|
|
53
65
|
const { op, word, rest } = this.operation(args);
|
|
54
66
|
if (op !== 'delete') {
|
|
55
67
|
const named = word ? `"${word}"` : 'that';
|
|
@@ -79,6 +91,12 @@ export class EditFileCommand extends Command {
|
|
|
79
91
|
if (!file) {
|
|
80
92
|
return `No file called "${query}" in ${hostLabel(room)}'s archive.${files.length > 0 ? hint(` (It holds: ${files.map(f => f.name).join(', ')}.)`) : ''}`;
|
|
81
93
|
}
|
|
94
|
+
// HASH (p.257, sprite-power-actions.ts): "only the sprite can
|
|
95
|
+
// unprotect it" while the algorithm holds. Nothing else edits or
|
|
96
|
+
// deletes a hashed file -- including its owner.
|
|
97
|
+
if (file.hashedBy) {
|
|
98
|
+
return `${file.name} is wrapped in something that is not encryption -- a Resonance algorithm, and it does not answer to Computer. ${hint(`(${file.hashedBy} sealed it. Only ${file.hashedBy} unseals it -- or decompile it and the file is corrupted for good.)`)}`;
|
|
99
|
+
}
|
|
82
100
|
const marks = room.hostMarksBy.get(actor.name) ?? 0;
|
|
83
101
|
if (marks < 1) {
|
|
84
102
|
return `Edit File needs a MARK (p.239) -- you hold none on ${hostLabel(room)}. ${hint(`"hack" the host for one.`)}`;
|
|
@@ -99,8 +117,8 @@ export class EditFileCommand extends Command {
|
|
|
99
117
|
const defence = hostDefensePool(host);
|
|
100
118
|
const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
101
119
|
const resist = rollPool(defence.pool);
|
|
102
|
-
if (this.scene
|
|
103
|
-
this.logger.meta(
|
|
120
|
+
if (showsMechanics(this.scene, actor)) {
|
|
121
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Edit File (delete) -- Computer + Logic [Data Processing]: ${formatRoll(attempt)}`);
|
|
104
122
|
this.logger.meta(` v. ${hostLabel(room)} (${defence.label}, rating standing in for Intuition): ${formatRoll(resist)}`);
|
|
105
123
|
}
|
|
106
124
|
actor.performAction('edits a file', `${file.name} in ${hostLabel(room)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { hint } from '../utilities/hints.js';
|
|
3
|
+
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
4
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
5
|
+
import { hostDefensePool, overwatchFromDefense } from '../utilities/matrix-intrusion.js';
|
|
6
|
+
import { accrueOverwatch } from '../utilities/overwatch.js';
|
|
7
|
+
import { leaveMatrix } from '../utilities/planes.js';
|
|
8
|
+
import { hostLabel, matchesHostName } from '../utilities/grid-names.js';
|
|
9
|
+
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
10
|
+
import { MAX_MARKS } from '../utilities/marks.js';
|
|
11
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
12
|
+
export class EraseMarkCommand extends Command {
|
|
13
|
+
static verb = 'unmark';
|
|
14
|
+
static description = 'Erase Mark (Complex Action, SR5 p.239): "unmark me" scrubs the marks others hold on your persona; "unmark <icon>" needs three marks on that icon first. Computer + Logic [Attack] v. Willpower + Firewall. An Attack action -- GOD counts it. Also "erase mark on <icon>".';
|
|
15
|
+
static npcPolicy = 'authorized';
|
|
16
|
+
/** p.239: two marks at -4, three at -10; one is unpenalised. */
|
|
17
|
+
static penaltyFor(count) {
|
|
18
|
+
if (count >= 3)
|
|
19
|
+
return -10;
|
|
20
|
+
if (count === 2)
|
|
21
|
+
return -4;
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* "unmark me" / "unmark <persona>" / "unmark <host>", plus a leading
|
|
26
|
+
* count: "unmark 2 marks on Deditri". The verb is also reached as
|
|
27
|
+
* "erase mark[s] on <icon>" (commands/edit-file.ts routes it here),
|
|
28
|
+
* so the filler words are stripped rather than parsed.
|
|
29
|
+
*/
|
|
30
|
+
parse(args) {
|
|
31
|
+
const words = args
|
|
32
|
+
.map(w => w.toLowerCase())
|
|
33
|
+
.filter(w => !/^(mark|marks|on|from|off|the|my|a)$/.test(w));
|
|
34
|
+
let count = 1;
|
|
35
|
+
if (words.length > 0 && /^[123]$/.test(words[0])) {
|
|
36
|
+
count = Number(words[0]);
|
|
37
|
+
words.shift();
|
|
38
|
+
}
|
|
39
|
+
return { count, query: words.join(' ').trim() };
|
|
40
|
+
}
|
|
41
|
+
resolve(query) {
|
|
42
|
+
const actor = this.actor;
|
|
43
|
+
if (query === '' || /^(me|myself|self|persona)$/.test(query)) {
|
|
44
|
+
return { kind: 'self', player: actor };
|
|
45
|
+
}
|
|
46
|
+
const rooms = Object.values(this.scene.getRooms());
|
|
47
|
+
const host = rooms.find(r => r.hasNode && matchesHostName(query, r));
|
|
48
|
+
if (host)
|
|
49
|
+
return { kind: 'host', room: host };
|
|
50
|
+
const personas = this.scene.allActors.filter(a => a !== actor && a.plane === 'matrix');
|
|
51
|
+
const picked = fuzzyPickName(query, personas.map(p => p.name));
|
|
52
|
+
const who = picked ? personas.find(p => p.name === picked) : undefined;
|
|
53
|
+
if (who)
|
|
54
|
+
return { kind: 'persona', player: who };
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/** How many marks the ACTOR holds on the target icon -- the p.239 gate. */
|
|
58
|
+
marksHeldOn(target) {
|
|
59
|
+
const actor = this.actor;
|
|
60
|
+
if (target.kind === 'host')
|
|
61
|
+
return target.room.host.marksBy.get(actor.name) ?? 0;
|
|
62
|
+
return target.player.panMarksBy.get(actor.name) ?? 0;
|
|
63
|
+
}
|
|
64
|
+
labelFor(target) {
|
|
65
|
+
if (target.kind === 'host')
|
|
66
|
+
return hostLabel(target.room);
|
|
67
|
+
return target.kind === 'self' ? 'your own persona' : `${target.player.name}'s persona`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Every icon holding a key on the target, each able to give up ONE of
|
|
71
|
+
* them at a time -- `drop()` decrements, it does not clear the entry.
|
|
72
|
+
* A multi-mark erase calls it once per mark, which is what makes the
|
|
73
|
+
* -4/-10 penalties mean anything.
|
|
74
|
+
*
|
|
75
|
+
* THE MARKS ON YOU ARE NOT ALL ON ONE LEDGER. A host that made you
|
|
76
|
+
* keeps its key in Host.marksOn, not in your PAN ledger -- so "unmark
|
|
77
|
+
* me" has to sweep both, or it would leave the ice's mark exactly where
|
|
78
|
+
* the player can still see it on their own sheet (utilities/marks.ts
|
|
79
|
+
* marksOnYou reads the same two places).
|
|
80
|
+
*/
|
|
81
|
+
placersOn(target) {
|
|
82
|
+
const actor = this.actor;
|
|
83
|
+
const out = [];
|
|
84
|
+
const dec = (m, key) => {
|
|
85
|
+
const n = (m.get(key) ?? 0) - 1;
|
|
86
|
+
if (n > 0)
|
|
87
|
+
m.set(key, n);
|
|
88
|
+
else
|
|
89
|
+
m.delete(key);
|
|
90
|
+
};
|
|
91
|
+
if (target.kind === 'self') {
|
|
92
|
+
for (const [placer, count] of actor.panMarksBy.entries()) {
|
|
93
|
+
if (count > 0)
|
|
94
|
+
out.push({ name: placer, count, drop: () => dec(actor.panMarksBy, placer) });
|
|
95
|
+
}
|
|
96
|
+
for (const room of Object.values(this.scene.getRooms())) {
|
|
97
|
+
const host = room.host;
|
|
98
|
+
const n = host?.marksOn.get(actor.name) ?? 0;
|
|
99
|
+
if (n > 0 && host) {
|
|
100
|
+
out.push({
|
|
101
|
+
name: hostLabel(room),
|
|
102
|
+
count: n,
|
|
103
|
+
drop: () => {
|
|
104
|
+
dec(host.marksOn, actor.name);
|
|
105
|
+
// A key gone is a sighting gone: an icon it no longer
|
|
106
|
+
// recognises is one it has to find again (Host.spotted).
|
|
107
|
+
if (!host.marksOn.has(actor.name))
|
|
108
|
+
host.spotted.delete(actor.name);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
const ledger = target.kind === 'host' ? target.room.host.marksBy : target.player.panMarksBy;
|
|
116
|
+
for (const [placer, count] of ledger.entries()) {
|
|
117
|
+
// Your own key is not a mark you "erase" -- p.239 is about reaching
|
|
118
|
+
// into an icon to scrub somebody ELSE's recognition pattern.
|
|
119
|
+
if (count > 0 && placer !== actor.name) {
|
|
120
|
+
out.push({ name: placer, count, drop: () => dec(ledger, placer) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
async execute(args = []) {
|
|
126
|
+
const actor = this.actor;
|
|
127
|
+
if (actor.plane !== 'matrix') {
|
|
128
|
+
return `Marks are Matrix keys -- you scrub them with a persona. ${hint(`"jack in" first.`)}`;
|
|
129
|
+
}
|
|
130
|
+
const { count, query } = this.parse(args);
|
|
131
|
+
const target = this.resolve(query);
|
|
132
|
+
if (!target) {
|
|
133
|
+
return `No icon here answers to "${query}". ${hint(`("unmark me" for the marks others hold on YOU; "unmark <host or persona>" needs three of your marks on it first.)`)}`;
|
|
134
|
+
}
|
|
135
|
+
const label = this.labelFor(target);
|
|
136
|
+
const held = this.marksHeldOn(target);
|
|
137
|
+
const placers = this.placersOn(target);
|
|
138
|
+
if (placers.length === 0) {
|
|
139
|
+
return target.kind === 'self'
|
|
140
|
+
? `Nothing is holding a key on you -- your icon is clean.`
|
|
141
|
+
: `Nobody else holds a mark on ${label} for you to scrub.`;
|
|
142
|
+
}
|
|
143
|
+
// THE THREE-MARK GATE (p.239), waived on your own icon -- see the
|
|
144
|
+
// owner-exemption ruling in the class comment.
|
|
145
|
+
if (target.kind !== 'self' && held < MAX_MARKS) {
|
|
146
|
+
return [
|
|
147
|
+
`You hold ${held}/${MAX_MARKS} marks on ${label} -- Erase Mark needs THREE on the icon you are scrubbing (p.239), not on whoever placed the mark.`,
|
|
148
|
+
hint(` ("hack" ${label} up to ${MAX_MARKS}. To clear marks off YOURSELF instead, "unmark me" -- or "reboot"/"jack out", which wipe every mark on you for free, p.242.)`),
|
|
149
|
+
].join('');
|
|
150
|
+
}
|
|
151
|
+
const wanted = Math.min(count, MAX_MARKS, placers.reduce((n, p) => n + p.count, 0));
|
|
152
|
+
// p.239: "If you go for more than one mark, all of the marks must be
|
|
153
|
+
// on the same icon and from the same icon" -- so a multi-mark erase
|
|
154
|
+
// spends its penalty against ONE placer's keys, not a spread.
|
|
155
|
+
const chosen = placers.reduce((best, p) => (p.count > best.count ? p : best), placers[0]);
|
|
156
|
+
const take = Math.min(wanted, chosen.count);
|
|
157
|
+
const penalty = EraseMarkCommand.penaltyFor(take);
|
|
158
|
+
const burned = actor.burnedAttributeRefusal('attack');
|
|
159
|
+
if (burned)
|
|
160
|
+
return burned;
|
|
161
|
+
const bill = billAction(this.scene, actor, 'complex', 'Erase Mark', { attack: true });
|
|
162
|
+
if (bill)
|
|
163
|
+
return bill;
|
|
164
|
+
const computer = actor.skillRating('computer');
|
|
165
|
+
const pool = Math.max(1, actor.logic + (computer > 0 ? computer : -1)
|
|
166
|
+
+ penalty + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
|
|
167
|
+
const limit = actor.matrixAttribute('attack');
|
|
168
|
+
const defence = this.defenceFor(target);
|
|
169
|
+
const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
170
|
+
const resist = rollPool(defence.pool);
|
|
171
|
+
if (showsMechanics(this.scene, actor)) {
|
|
172
|
+
const many = take > 1 ? ` for ${take} marks (${penalty})` : '';
|
|
173
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Erase Mark${many} -- Computer + Logic [Attack]: ${formatRoll(attempt)}`);
|
|
174
|
+
this.logger.meta(` ${chosen.name} resists (${defence.label}): ${formatRoll(resist)}`);
|
|
175
|
+
}
|
|
176
|
+
actor.performAction('erases a mark', `${chosen.name}'s key on ${label}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
177
|
+
// AN ATTACK ACTION IS ILLEGAL, and GOD bills the defender's hits
|
|
178
|
+
// (p.231-232) whether or not it landed.
|
|
179
|
+
const god = this.godBill(overwatchFromDefense(resist.hits), 'Erase Mark (attack)');
|
|
180
|
+
if (attempt.hits <= resist.hits) {
|
|
181
|
+
this.logger.write(`${actor.name} failed Erase Mark on ${label} (${chosen.name}): ${attempt.hits} v ${resist.hits}.`);
|
|
182
|
+
return [
|
|
183
|
+
`The key holds. ${chosen.name}'s mark is still written into ${label} and your scrub slides off it.`,
|
|
184
|
+
hint(` (A Complex Action each try${take > 1 ? `; going for ${take} at once cost you ${penalty} dice` : ''}.)`),
|
|
185
|
+
...(god.length > 0 ? [`\n${god.join('\n')}`] : []),
|
|
186
|
+
].join('');
|
|
187
|
+
}
|
|
188
|
+
for (let i = 0; i < take; i++)
|
|
189
|
+
chosen.drop();
|
|
190
|
+
actor.noteConditionChanged();
|
|
191
|
+
this.scene.updateStatus();
|
|
192
|
+
this.logger.write(`${actor.name} erased ${take} of ${chosen.name}'s mark(s) on ${label}: ${attempt.hits} v ${resist.hits}.`);
|
|
193
|
+
const left = this.remainingOn(target, actor);
|
|
194
|
+
return [
|
|
195
|
+
`{light-blue-fg}${chosen.name}'s ${take > 1 ? `${take} keys slough` : 'key sloughs'} off ${label} -- the recognition pattern unwrites itself and ${target.kind === 'self' ? 'your icon stops answering to it' : 'the icon forgets it ever agreed'}.{/light-blue-fg}`,
|
|
196
|
+
target.kind === 'self'
|
|
197
|
+
? (left > 0
|
|
198
|
+
? `\n${left} mark${left === 1 ? '' : 's'} still on you.`
|
|
199
|
+
: `\nNothing is holding a key on you now.`)
|
|
200
|
+
: '',
|
|
201
|
+
...(god.length > 0 ? [`\n${god.join('\n')}`] : []),
|
|
202
|
+
].join('');
|
|
203
|
+
}
|
|
204
|
+
/** p.239: Willpower + Firewall. A host has no Willpower; its rating stands in. */
|
|
205
|
+
defenceFor(target) {
|
|
206
|
+
if (target.kind === 'host') {
|
|
207
|
+
const d = hostDefensePool(target.room.host);
|
|
208
|
+
return { label: `${d.label}, rating standing in for Willpower`, pool: d.pool };
|
|
209
|
+
}
|
|
210
|
+
const who = target.player;
|
|
211
|
+
return {
|
|
212
|
+
label: 'Willpower + Firewall',
|
|
213
|
+
pool: Math.max(0, who.willpower + who.matrixAttribute('firewall')),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
remainingOn(target, actor) {
|
|
217
|
+
if (target.kind !== 'self')
|
|
218
|
+
return 0;
|
|
219
|
+
let n = 0;
|
|
220
|
+
for (const v of actor.panMarksBy.values())
|
|
221
|
+
n += v;
|
|
222
|
+
for (const room of Object.values(this.scene.getRooms())) {
|
|
223
|
+
n += room.host?.marksOn.get(actor.name) ?? 0;
|
|
224
|
+
}
|
|
225
|
+
return n;
|
|
226
|
+
}
|
|
227
|
+
/** GOD's bill, and the forced dump when it converges -- the shape every
|
|
228
|
+
* Attack-action verb in this engine shares (commands/disable.ts). */
|
|
229
|
+
godBill(defenseHits, why) {
|
|
230
|
+
const bill = accrueOverwatch(this.scene, this.actor, defenseHits, why);
|
|
231
|
+
if (bill.converged && this.actor.plane === 'matrix') {
|
|
232
|
+
bill.lines.push(...leaveMatrix(this.scene, this.actor, { forced: true, reason: `GOD force-reboots your persona --` }));
|
|
233
|
+
}
|
|
234
|
+
return bill.lines;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=erase-mark.js.map
|
|
@@ -13,6 +13,7 @@ import { TapCommand } from './tap.js';
|
|
|
13
13
|
import { sameHostSide } from '../models/player.js';
|
|
14
14
|
import { MAX_MARKS } from '../utilities/marks.js';
|
|
15
15
|
import { billAction } from '../utilities/action-cost.js';
|
|
16
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
16
17
|
import { declarationPenalty, parseMarkDeclaration, hostDefensePool, wanDefensePool, freeMatrixPerceptionHits, bruteForceMatrixDv, overwatchFromDefense, GO_BIG_QUALITY, deviceDefensePool } from '../utilities/matrix-intrusion.js';
|
|
17
18
|
/**
|
|
18
19
|
* MARKS A PAN INTRUSION MUST ALREADY HOLD before it can command the
|
|
@@ -254,8 +255,8 @@ export class HackCommand extends BypassCommand {
|
|
|
254
255
|
const bite = rollPool(this.actor.getSoakPool());
|
|
255
256
|
const dealt = Math.max(0, 3 - bite.hits);
|
|
256
257
|
// The roll is ticker material; the bite is world (readability pass).
|
|
257
|
-
if (this.scene
|
|
258
|
-
this.logger.meta(
|
|
258
|
+
if (showsMechanics(this.scene, this.actor)) {
|
|
259
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, this.actor)}Firewall: ${formatRoll(bite)}`);
|
|
259
260
|
}
|
|
260
261
|
const lines = [`The host's defenses snap shut around your intrusion --`];
|
|
261
262
|
if (dealt > 0) {
|
|
@@ -599,9 +600,14 @@ export class HackCommand extends BypassCommand {
|
|
|
599
600
|
const defense = viaWan ? wanDefensePool(device, host.host) : deviceDefensePool(device);
|
|
600
601
|
const defenseRoll = rollPool(defense.pool);
|
|
601
602
|
const netHits = roll.hits - defenseRoll.hits;
|
|
602
|
-
|
|
603
|
+
// THE AUDIENCE, NOT THE ROLLER (wjFBY3zgyWzJP9adw). This asked
|
|
604
|
+
// whether the actor was human, so an ordered sprite's marks landed
|
|
605
|
+
// with no dice anywhere -- the exact report. A companion's master
|
|
606
|
+
// typed the order and is owed its arithmetic.
|
|
607
|
+
if (showsMechanics(this.scene, actor)) {
|
|
603
608
|
const declaring = declared > 1 ? ` for ${declared} marks (${declarePenalty})` : '';
|
|
604
|
-
|
|
609
|
+
const who = mechanicsActorPrefix(this.scene, actor);
|
|
610
|
+
this.logger.meta(`${who}${mode === 'attack' ? 'Brute Force (attack)' : 'Hack on the Fly (sleaze)'} on ${device.name}${declaring}: ${formatRoll(roll)}`);
|
|
605
611
|
this.logger.meta(`${defense.label} (DR ${device.rating}): ${formatRoll(defenseRoll)} -- net ${netHits >= 0 ? '+' : ''}${netHits}`);
|
|
606
612
|
}
|
|
607
613
|
const godLines = this.godBill(overwatchFromDefense(defenseRoll.hits), `device intrusion (${mode})`);
|
|
@@ -738,9 +744,10 @@ export class HackCommand extends BypassCommand {
|
|
|
738
744
|
const defense = hostDefensePool(room.host);
|
|
739
745
|
const defenseRoll = rollPool(defense.pool);
|
|
740
746
|
const netHits = roll.hits - defenseRoll.hits;
|
|
741
|
-
if (this.scene
|
|
747
|
+
if (showsMechanics(this.scene, this.actor)) {
|
|
742
748
|
const declaring = declared > 1 ? ` for ${declared} marks (${declarePenalty})` : '';
|
|
743
|
-
|
|
749
|
+
const who = mechanicsActorPrefix(this.scene, this.actor);
|
|
750
|
+
this.logger.meta(`${who}${mode === 'attack' ? 'Brute Force (attack)' : 'Hack on the Fly (sleaze)'}${declaring}${gridNote(grid)}: ${formatRoll(roll)}`);
|
|
744
751
|
this.logger.meta(`${defense.label}: ${formatRoll(defenseRoll)} -- net ${netHits >= 0 ? '+' : ''}${netHits}`);
|
|
745
752
|
}
|
|
746
753
|
return { success: netHits > 0, netHits, defenseHits: defenseRoll.hits };
|
|
@@ -853,9 +860,11 @@ export class HackCommand extends BypassCommand {
|
|
|
853
860
|
actor.performAction('runs a PAN intrusion', target.name, { quiet: this.scene.isHumanControlled(actor) });
|
|
854
861
|
this.logger.write(`${actor.name} PAN-hacks ${target.name}: ${attack.hits} vs ${defense.hits} (silent=${target.runningSilent}).`);
|
|
855
862
|
// Dice anatomy to the Mechanics ticker (readability pass) --
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
863
|
+
// audience-gated: a passer-by's probe isn't your dice, but the agent
|
|
864
|
+
// YOU set on a PAN is (showsMechanics).
|
|
865
|
+
if (showsMechanics(this.scene, actor)) {
|
|
866
|
+
const who = mechanicsActorPrefix(this.scene, actor);
|
|
867
|
+
this.logger.meta(`${who}PAN intrusion (${mode === 'attack' ? 'brute force' : 'sleaze'}${markDice > 0 ? `, +${markDice} marks` : ''})${noiseNote(noise)}${gridNote(grid)}: ${formatRoll(attack)}`);
|
|
859
868
|
this.logger.meta(` their firewall: ${formatRoll(defense)}`);
|
|
860
869
|
}
|
|
861
870
|
// GOD counts the defender's hits as evidence (canon p.231-232) --
|
|
@@ -1023,8 +1032,8 @@ export class HackCommand extends BypassCommand {
|
|
|
1023
1032
|
// seed's enforcer still fumbles terminals about like it used to.
|
|
1024
1033
|
const pool = Math.max(1, this.actor.logic + this.actor.skillRating('hacking') + this.actor.bonus('hacking') + this.actor.woundModifier - this.actor.sustainingPenalty);
|
|
1025
1034
|
const roll = rollPool(pool, undefined, { gremlins: this.actor.deckGremlins });
|
|
1026
|
-
if (this.scene
|
|
1027
|
-
this.logger.meta(
|
|
1035
|
+
if (showsMechanics(this.scene, this.actor)) {
|
|
1036
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, this.actor)}Hacking: ${formatRoll(roll)} (2+ to crack)`);
|
|
1028
1037
|
}
|
|
1029
1038
|
return roll.hits >= 2;
|
|
1030
1039
|
}
|
|
@@ -7,6 +7,7 @@ import { billAction, encounterOf } from '../utilities/action-cost.js';
|
|
|
7
7
|
import { isSpotted } from '../utilities/ic.js';
|
|
8
8
|
import { MAX_MARKS } from '../utilities/marks.js';
|
|
9
9
|
import { hostLabel } from '../utilities/grid-names.js';
|
|
10
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
10
11
|
/**
|
|
11
12
|
* HIDE (SR5 p.240, RAG-checked 2026-09-14): a Complex Action, no marks
|
|
12
13
|
* required, Electronic Warfare + Intuition [Sleaze] v. Intuition + Data
|
|
@@ -67,8 +68,8 @@ export class HideCommand extends Command {
|
|
|
67
68
|
const limit = actor.matrixAttribute('sleaze');
|
|
68
69
|
const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
69
70
|
const defence = rollPool(room.hostRating + room.hostAttributes().dataProcessing);
|
|
70
|
-
if (this.scene
|
|
71
|
-
this.logger.meta(
|
|
71
|
+
if (showsMechanics(this.scene, actor)) {
|
|
72
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Hide -- Electronic Warfare + Intuition [Sleaze]: ${formatRoll(attempt)}`);
|
|
72
73
|
this.logger.meta(` v. ${hostLabel(room)} (Host Rating + Data Processing, rating standing in for Intuition): ${formatRoll(defence)}`);
|
|
73
74
|
}
|
|
74
75
|
actor.performAction('tries to hide', `persona folding into ${hostLabel(room)}'s traffic`, { quiet: this.scene.isHumanControlled(actor) });
|
|
@@ -4,6 +4,7 @@ import { enterMatrix, leaveMatrix } from '../utilities/planes.js';
|
|
|
4
4
|
import { hint } from '../utilities/hints.js';
|
|
5
5
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
6
6
|
import { billAction } from '../utilities/action-cost.js';
|
|
7
|
+
import { showsMechanics } from '../utilities/mechanics-audience.js';
|
|
7
8
|
/**
|
|
8
9
|
* The Matrix door, both directions: "jack in [hot|cold]" and "jack out".
|
|
9
10
|
* Needs a working CYBERDECK carried (a commlink is calls and AR only) --
|
|
@@ -65,7 +66,7 @@ export class JackCommand extends Command {
|
|
|
65
66
|
const lines = [
|
|
66
67
|
`You throw everything at the disconnect --`,
|
|
67
68
|
];
|
|
68
|
-
if (this.scene
|
|
69
|
+
if (showsMechanics(this.scene, actor)) {
|
|
69
70
|
this.logger.meta(firewall > 0
|
|
70
71
|
? `Jack Out -- Hardware + Willpower [Firewall]: ${formatRoll(attempt)}`
|
|
71
72
|
: `Jack Out -- Hardware + Willpower, UNBRACKETED (no functional Firewall left, p.229): ${formatRoll(attempt)}`);
|
|
@@ -73,7 +74,7 @@ export class JackCommand extends Command {
|
|
|
73
74
|
const held = [];
|
|
74
75
|
for (const name of lockers) {
|
|
75
76
|
const hold = rollPool(room.hostRating * 2, room.hostAttributes().attack);
|
|
76
|
-
if (this.scene
|
|
77
|
+
if (showsMechanics(this.scene, actor)) {
|
|
77
78
|
this.logger.meta(` v. ${name} keep-alive (Host Rating x2 [Attack]): ${formatRoll(hold)}`);
|
|
78
79
|
}
|
|
79
80
|
// Ties go to the locker: you need to BEAT each of them.
|
|
@@ -18,6 +18,7 @@ import { spotsActive, whoIsWhereLine, highlightSpots, highlightActors, canReach,
|
|
|
18
18
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
19
19
|
import { findCatalogItem, describeEffect } from '../utilities/catalog.js';
|
|
20
20
|
import { billAction } from '../utilities/action-cost.js';
|
|
21
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
21
22
|
export class LookCommand extends Command {
|
|
22
23
|
static verb = 'look';
|
|
23
24
|
static description = 'Examine your surroundings';
|
|
@@ -166,8 +167,8 @@ export class LookCommand extends Command {
|
|
|
166
167
|
// the two verbs cannot drift apart again.
|
|
167
168
|
const { pool, limit } = perceptionTest(this.actor);
|
|
168
169
|
const roll = rollPool(pool, limit);
|
|
169
|
-
if (this.scene
|
|
170
|
-
this.logger.meta(
|
|
170
|
+
if (showsMechanics(this.scene, this.actor)) {
|
|
171
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, this.actor)}Perception (look): ${formatRoll(roll)} (2+ spots partial cover, 4+ good)`);
|
|
171
172
|
}
|
|
172
173
|
const who = whoIsWhereLine(room, this.actor, roll.hits);
|
|
173
174
|
if (who)
|
|
@@ -182,16 +182,26 @@ export class OrderCommand extends Command {
|
|
|
182
182
|
// task, then the fight's own per-fight charge found the bargain
|
|
183
183
|
// already at zero and dismissed the spirit mid-lunge.
|
|
184
184
|
const isAttackOrder = /\b(attack|kill|subdue|fight|slay|destroy)\b/i.test(cmd);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
// A SPRITE'S TASKS ARE A BARGAIN TOO (wjFBY3zgyWzJP9adw). This billed
|
|
186
|
+
// spirits only, so an ordered sprite worked for free and its ledger
|
|
187
|
+
// line never printed -- while combat-exchange.ts has always charged
|
|
188
|
+
// BOTH kinds per fight. Canon is explicit: a compiled sprite owes
|
|
189
|
+
// (hits) tasks and "using a power costs one task" (p.254, p.256).
|
|
190
|
+
// The vocabulary differs -- a spirit owes services, a sprite tasks --
|
|
191
|
+
// so the line says which.
|
|
192
|
+
const billsTask = (entry.kind === 'spirit' || entry.kind === 'sprite') && !isAttackOrder;
|
|
193
|
+
const owed = entry.kind === 'sprite' ? 'task' : 'service';
|
|
194
|
+
if (billsTask && (entry.services ?? 0) <= 0) {
|
|
195
|
+
return entry.kind === 'sprite'
|
|
196
|
+
? `${entry.npc.name} flickers and does not move. Its tasks are spent -- compile or register another.`
|
|
197
|
+
: `${entry.npc.name} regards you flatly. The bargain is spent -- no services remain.`;
|
|
188
198
|
}
|
|
189
199
|
const chargeService = () => {
|
|
190
|
-
if (!
|
|
200
|
+
if (!billsTask)
|
|
191
201
|
return;
|
|
192
202
|
entry.services = (entry.services ?? 0) - 1;
|
|
193
203
|
// Resource arithmetic -- Mechanics ticker (readability pass).
|
|
194
|
-
this.logger.meta(`👥
|
|
204
|
+
this.logger.meta(`👥 ${entry.npc.name}: one ${owed} spent (${entry.services} left).`, 'green');
|
|
195
205
|
};
|
|
196
206
|
// Tier 1 -- ATOMIC: a known command runs deterministically, no AI.
|
|
197
207
|
// A parsed command whose RESULT is a refusal is NOT obedience: a real
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
3
3
|
import { accrueOverwatch, settledScore, CONVERGENCE_AT } from '../utilities/overwatch.js';
|
|
4
4
|
import { leaveMatrix } from '../utilities/planes.js';
|
|
5
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
5
6
|
/**
|
|
6
7
|
* Check Overwatch Score (canon p.238): Electronic Warfare + Logic
|
|
7
8
|
* [Sleaze] vs GOD's flat 6 dice. Win the read and you see the exact
|
|
@@ -40,8 +41,8 @@ export class OverwatchCommand extends Command {
|
|
|
40
41
|
// (canon p.235-236, Player.matrixActionPenalty).
|
|
41
42
|
const mine = rollPool(Math.max(1, actor.logic + (skill > 0 ? skill : -1) + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty), limit);
|
|
42
43
|
const god = rollPool(6);
|
|
43
|
-
if (this.scene
|
|
44
|
-
this.logger.meta(
|
|
44
|
+
if (showsMechanics(this.scene, actor)) {
|
|
45
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Check Overwatch (EW, sleaze): ${formatRoll(mine)}`);
|
|
45
46
|
this.logger.meta(` GOD's counter-sweep: ${formatRoll(god)}`);
|
|
46
47
|
}
|
|
47
48
|
// The number they glimpse is the score BEFORE this check's own bill
|