@maka/maka-cli 5.235.0 → 5.237.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.235.0",
3
+ "version": "5.237.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.",
@@ -72,12 +72,12 @@ export class DisableCommand extends Command {
72
72
  // 2. A DEVICE ICON: inside the host, its WAN; from the grid, a slaved
73
73
  // device you hold a host mark for, or a loose wireless one where
74
74
  // the body stands. Mirrors hack.ts tryDeviceHack.
75
- const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.hasIconFor(actor.name)).filter(d => d.name.toLowerCase().includes(raw)
75
+ const matching = (room, inHost = false) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.hasIconFor(actor.name, inHost)).filter(d => d.name.toLowerCase().includes(raw)
76
76
  || (d.opensExit ?? '').toLowerCase() === raw
77
77
  || d.description.toLowerCase().includes(raw));
78
78
  const inside = actor.insideHost;
79
79
  if (inside) {
80
- const matches = matching(inside);
80
+ const matches = matching(inside, true);
81
81
  if (matches.length === 1)
82
82
  return { kind: 'device', device: matches[0], room: inside, host: inside, viaWan: false };
83
83
  if (matches.length > 1)
@@ -1,4 +1,5 @@
1
1
  import { Command } from './command.js';
2
+ import { gridActionModifier } from '../utilities/grids.js';
2
3
  import { hint } from '../utilities/hints.js';
3
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
4
5
  import { billAction } from '../utilities/action-cost.js';
@@ -111,8 +112,12 @@ export class EditFileCommand extends Command {
111
112
  const computer = actor.skillRating('computer');
112
113
  // Inside a host no grid penalty applies (p.246); silence still taxes
113
114
  // every Matrix action (p.235-236, Player.matrixActionPenalty).
115
+ // The grid tax was NAMED here and never charged -- gridActionModifier
116
+ // is the helper that answers both halves, returning 0 inside a host
117
+ // and the public grid's -2 out on it (p.233).
118
+ const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, actor.currentGrid);
114
119
  const pool = Math.max(1, actor.logic + (computer > 0 ? computer : -1)
115
- + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
120
+ + actor.matrixActionPenalty + grid.total + actor.woundModifier - actor.sustainingPenalty);
116
121
  const limit = actor.matrixAttribute('dataProcessing');
117
122
  const defence = hostDefensePool(host);
118
123
  const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
@@ -1,4 +1,5 @@
1
1
  import { Command } from './command.js';
2
+ import { gridActionModifier } from '../utilities/grids.js';
2
3
  import { hint } from '../utilities/hints.js';
3
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
4
5
  import { billAction } from '../utilities/action-cost.js';
@@ -192,8 +193,12 @@ export class EraseMarkCommand extends Command {
192
193
  if (bill)
193
194
  return bill;
194
195
  const computer = actor.skillRating('computer');
196
+ // The grid you are riding taxes every Matrix action you take from it
197
+ // (p.233); inside a host it is nothing, because a host's interior is
198
+ // not on a grid.
199
+ const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, actor.currentGrid);
195
200
  const pool = Math.max(1, actor.logic + (computer > 0 ? computer : -1)
196
- + penalty + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
201
+ + penalty + actor.matrixActionPenalty + grid.total + actor.woundModifier - actor.sustainingPenalty);
197
202
  const limit = actor.matrixAttribute('attack');
198
203
  // THE PLACER ROLLS, not the icon the key is written on (p.239, see
199
204
  // IPlacer). `chosen` is the icon whose keys this action is scrubbing.
@@ -569,7 +569,7 @@ export class HackCommand extends BypassCommand {
569
569
  if (!named)
570
570
  return null;
571
571
  const raw = named.toLowerCase();
572
- const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.hasIconFor(actor.name)).filter(d => d.name.toLowerCase().includes(raw)
572
+ const matching = (room, inHost = false) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.hasIconFor(actor.name, inHost)).filter(d => d.name.toLowerCase().includes(raw)
573
573
  || (d.opensExit ?? '').toLowerCase() === raw
574
574
  || d.description.toLowerCase().includes(raw));
575
575
  // INSIDE THE HOST you are directly connected to every device on
@@ -579,7 +579,7 @@ export class HackCommand extends BypassCommand {
579
579
  let viaWan = false;
580
580
  let device;
581
581
  if (host) {
582
- const matches = matching(host);
582
+ const matches = matching(host, true);
583
583
  if (matches.length !== 1)
584
584
  return null;
585
585
  device = matches[0];
@@ -608,7 +608,14 @@ export class HackCommand extends BypassCommand {
608
608
  viaWan = true;
609
609
  const onHost = host.hostMarksBy.get(actor.name) ?? 0;
610
610
  if (onHost < 1) {
611
- return `${device.name} is on the map, but it hangs off ${hostLabel(host)}'s WAN -- slaved, and a host's devices answer nobody who holds no mark on the host itself (p.233).${hint(` ("${this.verbName} ${hostLabel(host)}" for a mark first; "enter" once you have one and the lock defends with its own DR ${device.rating} alone.)`)}`;
611
+ // AND THE OTHER DOOR IS A CABLE (eJANPpm7qCaCAJBJw). This
612
+ // refusal used to name one way past it -- go through the host
613
+ // -- which made the host the only road to everything hanging
614
+ // off it. p.233 carves out the exception in the same breath as
615
+ // the rule: a direct connection, and the slave "cannot use its
616
+ // master's ratings to defend itself". It costs the runner's
617
+ // body in the room, which is the whole trade.
618
+ return `${device.name} is on the map, but it hangs off ${hostLabel(host)}'s WAN -- slaved, and a host's devices answer nobody who holds no mark on the host itself (p.233).${hint(` ("${this.verbName} ${hostLabel(host)}" for a mark first; "enter" once you have one and the lock defends with its own DR ${device.rating} alone. Or go the other way: "jack out", walk to it, and "tap ${device.name}" -- a cable doesn't ask the network for permission, and a mark on a slave is a mark on its master.)`)}`;
612
619
  }
613
620
  }
614
621
  else {
@@ -1,4 +1,5 @@
1
1
  import { Command } from './command.js';
2
+ import { gridActionModifier } from '../utilities/grids.js';
2
3
  import { hint } from '../utilities/hints.js';
3
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
4
5
  import { accrueOverwatch } from '../utilities/overwatch.js';
@@ -63,8 +64,12 @@ export class HideCommand extends Command {
63
64
  if (bill)
64
65
  return bill;
65
66
  const ew = actor.skillRating('electronic-warfare');
67
+ // The grid you are riding taxes every Matrix action you take from it
68
+ // (p.233); inside a host it is nothing, because a host's interior is
69
+ // not on a grid.
70
+ const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, actor.currentGrid);
66
71
  const pool = Math.max(1, actor.intuition + (ew > 0 ? ew : -1)
67
- + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
72
+ + actor.matrixActionPenalty + grid.total + actor.woundModifier - actor.sustainingPenalty);
68
73
  const limit = actor.matrixAttribute('sleaze');
69
74
  const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
70
75
  const defence = rollPool(room.hostRating + room.hostAttributes().dataProcessing);
@@ -1,4 +1,5 @@
1
1
  import { Command } from './command.js';
2
+ import { gridActionModifier } from '../utilities/grids.js';
2
3
  import { rollPool, formatRoll } from '../utilities/dice.js';
3
4
  import { accrueOverwatch, settledScore, CONVERGENCE_AT } from '../utilities/overwatch.js';
4
5
  import { leaveMatrix } from '../utilities/planes.js';
@@ -38,8 +39,10 @@ export class OverwatchCommand extends Command {
38
39
  return burned;
39
40
  const limit = actor.matrixAttribute('sleaze');
40
41
  // The check is itself a Sleaze action, so silence taxes it too
41
- // (canon p.235-236, Player.matrixActionPenalty).
42
- const mine = rollPool(Math.max(1, actor.logic + (skill > 0 ? skill : -1) + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty), limit);
42
+ // (canon p.235-236, Player.matrixActionPenalty) -- and so does the
43
+ // grid you take it from (p.233), which is nothing inside a host.
44
+ const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, actor.currentGrid);
45
+ const mine = rollPool(Math.max(1, actor.logic + (skill > 0 ? skill : -1) + actor.matrixActionPenalty + grid.total + actor.woundModifier - actor.sustainingPenalty), limit);
43
46
  const god = rollPool(6);
44
47
  if (showsMechanics(this.scene, actor)) {
45
48
  this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Check Overwatch (EW, sleaze): ${formatRoll(mine)}`);
@@ -1,12 +1,13 @@
1
1
  import { Command } from './command.js';
2
2
  import { billAction } from '../utilities/action-cost.js';
3
- import { hostLabel } from '../utilities/grid-names.js';
3
+ import { hostLabel, matchesCameraName } from '../utilities/grid-names.js';
4
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
5
5
  import { accrueOverwatch } from '../utilities/overwatch.js';
6
6
  import { MAX_MARKS } from '../utilities/marks.js';
7
7
  import { isWatched, cameraMasterHost, directConnectionBlocker } from '../utilities/surveillance.js';
8
8
  import { hint } from '../utilities/hints.js';
9
9
  import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
10
+ import { deviceDefensePool, bruteForceMatrixDv, freeMatrixPerceptionHits, overwatchFromDefense } from '../utilities/matrix-intrusion.js';
10
11
  /**
11
12
  * TAP -- THE DIRECT CONNECTION (backlog items 9 and 10, Niko, session
12
13
  * 2026-08-27T02-07-12-404Z: "check the rules on this -- shouldn't I be
@@ -44,6 +45,15 @@ import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-aud
44
45
  * against a camera's ratings instead of a host's -- and the cost is
45
46
  * that you had to walk in and touch it.
46
47
  *
48
+ * IT IS NOT ONLY FOR CAMERAS ANY MORE (eJANPpm7qCaCAJBJw). The verb
49
+ * shipped pointed at a lens because that is what the reports asked for,
50
+ * and that left every OTHER slaved device -- the maglock, the keypad,
51
+ * the safe, the security furniture p.217 names first -- reachable only
52
+ * by cracking the host first. Backwards: Data Trails p.87-88 works this
53
+ * exact play on a DOOR, and the cable is the reason a hardened site is
54
+ * something you walk into rather than something you read off the sky.
55
+ * `tap <name>` is that, and tapDevice below is the rule.
56
+ *
47
57
  * WHERE THE AIR-GAP CLAUSE WENT. This used to say the cable's one
48
58
  * outright win was an air-gapped host, which had no door onto the grid
49
59
  * at all. There is no such host any more (hhSWzLSXECFtfoAGa): a host is
@@ -55,7 +65,7 @@ import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-aud
55
65
  */
56
66
  export class TapCommand extends Command {
57
67
  static verb = 'tap';
58
- static description = 'Cable into a camera you are standing next to -- a DIRECT CONNECTION (p.232), run as HACK ON THE FLY (p.240). A slaved device can\'t hide behind its host\'s ratings against a cable, so the camera fights you with its own; and a mark on a slaved device is a mark on the host it answers to (p.233), which is your way in. Needs your BODY in the room and a deck in hand. Fail and you are noticed. To cable in loud instead, "force camera" is Brute Force over the same cable (p.238).';
68
+ static description = 'Cable into a device you are standing next to -- "tap" for the cameras, or "tap <keypad/maglock/safe>" for the security furniture. A DIRECT CONNECTION (p.232), run as HACK ON THE FLY (p.240). A slaved device can\'t hide behind its host\'s ratings against a cable, so it fights you with its own; and a mark on a slaved device is a mark on the host it answers to (p.233), which is your way in. Works on an icon you have never spotted and on one whose wireless is off -- a cable is not wireless (p.421). Needs your BODY in the room and a deck in hand. Fail and you are noticed. To cable into the lenses loud instead, "force camera" is Brute Force over the same cable (p.238).';
59
69
  /**
60
70
  * WHICH INTRUSION ACTION THE CABLE CARRIES. Same rule as hack.ts:
61
71
  * the verb decides, not a word inside the target name. `tap loud`
@@ -69,11 +79,32 @@ export class TapCommand extends Command {
69
79
  /** How `force camera` reaches the loud route over the same cable. */
70
80
  withMode(m) { this.modeOverride = m; return this; }
71
81
  mode() { return this.modeOverride ?? 'sleaze'; }
72
- async execute() {
82
+ async execute(args = []) {
73
83
  const actor = this.actor;
74
84
  const room = actor.currentLocation;
75
85
  const rooms = this.rooms ?? {};
76
86
  const mode = this.mode();
87
+ // THE CABLE GOES INTO WHATEVER YOU NAME (eJANPpm7qCaCAJBJw, Maka:
88
+ // "a device slaved to a host cannot be detected or interacted with
89
+ // outside the host"). The cameras were the only thing this verb
90
+ // could reach, which left the maglock, the keypad and the safe --
91
+ // the security furniture p.217 names first -- reachable ONLY by
92
+ // owning the host first. That is backwards from the book's own
93
+ // worked example: Data Trails p.87-88 runs the play on a DOOR.
94
+ const named = args.join(' ').trim();
95
+ if (named && !matchesCameraName(named)) {
96
+ const device = this.resolveDevice(room, named);
97
+ if (device)
98
+ return this.tapDevice(room, device, mode);
99
+ // NAMED SOMETHING, GOT SOMETHING ELSE IS NOT AN OPTION. This used
100
+ // to fall through to the lenses whenever the room had any, so
101
+ // `tap turnstile` in a watched room cabled the cameras and
102
+ // reported a mark on a host the player never aimed at -- a
103
+ // Complex Action spent on the wrong target with no way to tell.
104
+ // A word that names nothing here is a refusal that lists what it
105
+ // could have named.
106
+ return this.nothingNamed(room, named);
107
+ }
77
108
  if (!isWatched(room)) {
78
109
  return `No cameras in here to cable into.${hint(` (Your overlay tags them "◎ Camera cluster" when there are.)`)}`;
79
110
  }
@@ -216,5 +247,203 @@ export class TapCommand extends Command {
216
247
  this.scene.updateStatus();
217
248
  return lines.join('\n');
218
249
  }
250
+ /**
251
+ * WHICH THING IN THIS ROOM THE PLAYER MEANT. The same shape hack.ts's
252
+ * device route uses -- name, the exit it seals, or its description --
253
+ * with two deliberate differences, and they are the whole point of
254
+ * this route:
255
+ *
256
+ * IT DOES NOT ASK hasIconFor. A silent icon is hidden ON THE GRID
257
+ * (p.217, p.235-236). Your body is standing in front of the object
258
+ * with a hand on its housing; you are not looking for it, you are
259
+ * touching it. Requiring a Matrix Perception sweep before you may
260
+ * cable into a keypad you can SEE would be the search rule applied
261
+ * to a thing that was never lost.
262
+ *
263
+ * IT DOES NOT ASK wireless. p.421's "Turning It Off" is exact about
264
+ * what that costs a device: "the items can no longer be wirelessly
265
+ * hacked." A cable is not wireless. A dark maglock is immune to
266
+ * everything on the grid and still opens to a filament -- which is
267
+ * why physical infiltration stays the answer to a hardened site
268
+ * rather than a dead end.
269
+ *
270
+ * Open devices are excluded: a thing standing open has nothing left to
271
+ * key.
272
+ */
273
+ resolveDevice(room, named) {
274
+ const raw = named.toLowerCase();
275
+ const matches = room.openableDevices()
276
+ .filter(d => d.rating > 0)
277
+ .filter(d => d.name.toLowerCase().includes(raw)
278
+ || (d.opensExit ?? '').toLowerCase() === raw
279
+ || d.description.toLowerCase().includes(raw));
280
+ return matches.length === 1 ? matches[0] : undefined;
281
+ }
282
+ /** Named something this room has no cable-end for. Says what it DOES
283
+ * have, because a refusal that lists the connectors is a refusal that
284
+ * teaches the verb. */
285
+ nothingNamed(room, named) {
286
+ const here = room.openableDevices().filter(d => d.rating > 0).map(d => d.name);
287
+ if (isWatched(room))
288
+ here.push('the cameras (bare "tap")');
289
+ return here.length > 0
290
+ ? `Nothing here called "${named}" has a data connector on it. What does: ${here.join(', ')}.`
291
+ : `Nothing in ${room.name} has a port to cable into -- no cameras, no panel, no lock with electronics in it.`;
292
+ }
293
+ /**
294
+ * A DIRECT CONNECTION TO A DEVICE (SR5 p.232-233; Data Trails p.87-88,
295
+ * "A Lesson in Host Invasion" -- which runs this exact play on a DOOR
296
+ * slaved to a host, not on a camera).
297
+ *
298
+ * THE RULE THIS BUYS, and it is the one the report has been circling:
299
+ * "If a slaved device is under attack via a direct connection, it
300
+ * cannot use its master's ratings to defend itself" (p.233). So the
301
+ * keypad that hides behind HR 3 out on the grid -- and that `hack`
302
+ * from the grid refuses outright until you hold a mark on the host --
303
+ * defends here with nothing but its own DR. That is the identical pool
304
+ * `hack` faces from INSIDE the host, because being inside a WAN and
305
+ * holding a cable are the same rule: "if you are in a host that has a
306
+ * WAN, you are considered directly connected to all devices in the
307
+ * WAN" (p.233).
308
+ *
309
+ * AND THE PAYOFF RUNS UPHILL: "if you get a mark on a slave you also
310
+ * get a mark on the master" (p.233). A filament in a maglock is a key
311
+ * to the host it answers to. That is the road INTO a host, which this
312
+ * engine offered only through a camera lens.
313
+ *
314
+ * THE PRICE IS YOUR BODY, which is what keeps it from being a
315
+ * shortcut: directConnectionBlocker wants you in the meat, in the
316
+ * room, with a deck in hand. A persona out on the grid cannot hold a
317
+ * cable.
318
+ */
319
+ async tapDevice(room, device, mode) {
320
+ const actor = this.actor;
321
+ const blocked = directConnectionBlocker(actor, room, device.name);
322
+ if (blocked)
323
+ return blocked;
324
+ const held = device.marksBy.get(actor.name) ?? 0;
325
+ if (held >= MAX_MARKS) {
326
+ return `You already hold ${MAX_MARKS} marks on ${device.name} -- it answers to you as readily as to the house.`;
327
+ }
328
+ const burned = actor.burnedAttributeRefusal(mode === 'attack' ? 'attack' : 'sleaze');
329
+ if (burned)
330
+ return burned;
331
+ // THE MASTER IS THE HOST OVER THIS DEVICE'S OWN ROOM, and nothing
332
+ // further. cameraMasterHost falls back to any host on the site
333
+ // because a camera NETWORK is a site-wide thing the seeds describe;
334
+ // a keypad's master is not, and reaching across the district for one
335
+ // would be inventing a WAN the seed never declared. No host over it
336
+ // means a loose device: the mark lands on the thing you cabled into
337
+ // and propagates nowhere, which is the honest answer.
338
+ const host = room.hasNode ? room : undefined;
339
+ const billed = billAction(this.scene, actor, 'complex', mode === 'attack' ? 'Brute Force' : 'Hack on the Fly');
340
+ if (billed)
341
+ return billed;
342
+ const silence = actor.matrixActionPenalty;
343
+ const limit = actor.matrixAttribute(mode === 'attack' ? 'attack' : 'sleaze');
344
+ const pool = mode === 'attack'
345
+ ? Math.max(1, actor.getAttackPool() + silence + actor.bonus('matrix-attack'))
346
+ : Math.max(1, actor.logic + actor.skillRating('hacking') + actor.bonus('hacking') + actor.bonus('matrix-sleaze')
347
+ + silence + actor.woundModifier - actor.sustainingPenalty);
348
+ actor.performAction('cables into a slaved device', `${room.name} -- a filament out of their deck and into ${device.name}`);
349
+ const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
350
+ // ITS OWN RATINGS. Not wanDefensePool -- that is what a slave borrows
351
+ // when you come at it THROUGH the network (p.233), and the cable is
352
+ // the exception the same sentence carves out.
353
+ const defense = deviceDefensePool(device);
354
+ const defenseRoll = rollPool(defense.pool);
355
+ const netHits = roll.hits - defenseRoll.hits;
356
+ if (showsMechanics(this.scene, actor)) {
357
+ const who = mechanicsActorPrefix(this.scene, actor);
358
+ this.logger.meta(`${who}Direct connection, ${device.name} (${mode === 'attack' ? 'brute force' : 'sleaze'}): ${formatRoll(roll)}`);
359
+ this.logger.meta(` ${defense.label} (DR ${device.rating}) -- its OWN, not ${host ? hostLabel(host) : 'the network'}'s, p.233: ${formatRoll(defenseRoll)} -- net ${netHits >= 0 ? '+' : ''}${netHits}`);
360
+ }
361
+ this.logger.write(`${actor.name} direct-connected to ${device.name} in ${room.name} (${mode}): net ${netHits}, master=${host?.name ?? 'none'}.`);
362
+ // p.231-232: GOD bills the DEFENDER's hits, the same as every other
363
+ // illegal Attack or Sleaze action in this engine.
364
+ const bill = accrueOverwatch(this.scene, actor, overwatchFromDefense(defenseRoll.hits), `direct connection (${device.name}, ${mode})`);
365
+ const godLines = bill.lines;
366
+ if (netHits <= 0) {
367
+ if (mode === 'sleaze') {
368
+ // p.236: a blown Sleaze is NOTICED, and a slaved device that
369
+ // notices tells the host it hangs off.
370
+ actor.sneaking = false;
371
+ if (host) {
372
+ host.hostAlert = true;
373
+ this.scene.addWorldEvent(`${device.name} flags a bad handshake on its cable -- ${hostLabel(host, { capital: true })} is awake.`, { plane: 'matrix' });
374
+ this.scene.updateStatus();
375
+ return [
376
+ `The filament seats, the handshake goes wrong, and ${device.name} says so out loud.`,
377
+ ` The node it answers to knows something is on the wire.`,
378
+ ...godLines,
379
+ ].join('\n');
380
+ }
381
+ this.scene.updateStatus();
382
+ return [
383
+ `The filament seats and ${device.name} refuses the key -- whoever owns it has your icon on file now.`,
384
+ ...godLines,
385
+ ].join('\n');
386
+ }
387
+ // p.236: a failed Attack is never noticed.
388
+ this.scene.updateStatus();
389
+ return [
390
+ `You put your shoulder into it down the wire and ${device.name} just... doesn't give. Nobody upstairs noticed.`,
391
+ ...godLines,
392
+ ].join('\n');
393
+ }
394
+ const now = Math.min(MAX_MARKS, held + 1);
395
+ device.marksBy.set(actor.name, now);
396
+ const lines = [
397
+ `{light-blue-fg}The filament finds the connector and ${device.name} stops being furniture -- it's a device, and it's talking to you.{/light-blue-fg}`,
398
+ ` {light-blue-fg}It never gets to hide behind the network: a cable doesn't ask permission, so it has to defend itself on DR ${device.rating} alone (p.233).{/light-blue-fg}`,
399
+ ` {light-blue-fg}MARK PLACED -- ${now} of ${MAX_MARKS} on ${device.name}.{/light-blue-fg}`,
400
+ ];
401
+ // A MARK ON A SLAVE IS A MARK ON THE MASTER (p.233). One, whatever
402
+ // you took off the slave -- the same rule hack.ts's device route
403
+ // applies, and the reason this verb is a way IN rather than only a
404
+ // way through a door.
405
+ if (host) {
406
+ const onHost = host.hostMarksBy.get(actor.name) ?? 0;
407
+ if (onHost < MAX_MARKS)
408
+ host.hostMarksBy.set(actor.name, onHost + 1);
409
+ const nowHost = host.hostMarksBy.get(actor.name) ?? 0;
410
+ lines.push(` {light-blue-fg}And it lands on the host too: ${device.name} answers to ${hostLabel(host)}, and anything ${hostLabel(host)} trusts to speak for it just handed you a key. [${nowHost}/${MAX_MARKS}]{/light-blue-fg}`);
411
+ lines.push(` {light-blue-fg}${nowHost >= MAX_MARKS
412
+ ? `That node thinks you belong now -- "jack in" and "hack" walks straight in.`
413
+ : `${hostLabel(host, { capital: true })} cracks easier with every key you hold${hint(` -- "jack in" and "hack", or find another slave to cable`)}.`}{/light-blue-fg}`);
414
+ }
415
+ if (mode === 'attack') {
416
+ actor.sneaking = false;
417
+ if (host)
418
+ host.hostAlert = true;
419
+ this.scene.addWorldEvent(`${actor.name} forced ${device.name} over a cable -- the housing shows it.`);
420
+ // p.238: Brute Force burns what it forces, 1 DV per two full net
421
+ // hits, resisted by Device Rating + Firewall.
422
+ const dv = bruteForceMatrixDv(netHits);
423
+ if (dv > 0) {
424
+ const resisted = Math.max(0, dv - rollPool(defense.pool).hits);
425
+ if (resisted > 0) {
426
+ device.takeBarrierDamage(resisted);
427
+ lines.push(` The forcing burns ${resisted} into its boards -- ${device.barrierSummary()}`);
428
+ if (device.isHoled)
429
+ lines.push(` It dies open. Whatever it was holding shut is not shut any more.`);
430
+ }
431
+ }
432
+ }
433
+ else {
434
+ // p.240: Hack on the Fly reads while it keys.
435
+ if (freeMatrixPerceptionHits(netHits) > 0) {
436
+ lines.push(` {light-blue-fg}And you read it while you keyed it (p.240): ${device.name} is a ${device.kind}, DR ${device.rating}${device.opensExit ? `, and it is what holds the ${device.opensExit} way shut` : ''}.{/light-blue-fg}`);
437
+ }
438
+ lines.push(` Nothing logged, nothing flagged. You were never here.`);
439
+ }
440
+ // WHAT THE MARK IS FOR. A key you cannot spend is a key that gets
441
+ // reported as broken.
442
+ lines.push(` {light-blue-fg}${device.name} takes orders from you now${hint(` -- "${device.verbs()[0]} ${device.name}"`)}.{/light-blue-fg}`);
443
+ lines.push(...godLines);
444
+ actor.noteConditionChanged();
445
+ this.scene.updateStatus();
446
+ return lines.join('\n');
447
+ }
219
448
  }
220
449
  //# sourceMappingURL=tap.js.map
@@ -1505,5 +1505,66 @@
1505
1505
  // -2]' where it applied. Inside a host it is correctly nothing -- a
1506
1506
  // host's interior is not on a grid -- and it stacks with p.49's retry
1507
1507
  // penalty, so a public-grid decker gets 5, 3, 1, none.
1508
- export const ENGINE_VERSION = '1.85.1';
1508
+ // 1.85.2 (2026-09-16): SWEEP THE FAMILY -- the grid tax on every verb
1509
+ // 1.85.1 fixed the grid modifier on the sweep. CLAUDE.md's rule is
1510
+ // that a uniform approximation is still a deviation and partial canon
1511
+ // is worse than either extreme, so the other Matrix verbs were audited
1512
+ // rather than left to be reported one at a time.
1513
+ // FOUR MORE WERE BUILDING A POOL WITHOUT IT: Edit File (p.239), Erase
1514
+ // Mark (p.239), Hide (p.240) and Check Overwatch Score (p.238). All
1515
+ // four read Player.matrixActionPenalty -- which is ONLY the
1516
+ // running-silent -2 -- and none called gridActionModifier. edit-file's
1517
+ // own comment even NAMED the rule it was not applying ("inside a host
1518
+ // no grid penalty applies"), which is the tell: the author knew about
1519
+ // the grid and handled only the exempt case.
1520
+ // Each takes it now, and gridActionModifier answers both halves --
1521
+ // zero inside a host (p.246: its interior is not on a grid), the
1522
+ // public grid's -2 out on it (p.233).
1523
+ // NOT CHANGED: the `tap` verb. It is a direct connection to a lens in the
1524
+ // flesh, and the books do not say whether the grid tax still applies
1525
+ // to an action taken down a cable. Left alone and said so rather than
1526
+ // guessed in either direction.
1527
+ // 1.86.0 (2026-09-17): THE CABLE REACHES EVERY SLAVE, AND A SPOT STAYS
1528
+ // ON ITS OWN SIDE OF THE WALL (eJANPpm7qCaCAJBJw).
1529
+ // THE RULES QUESTION FIRST, because the report asked for one: a device
1530
+ // slaved to a host is NOT moved inside it. p.233's entire
1531
+ // slave-defence rule ("whenever a slaved device is called on to make a
1532
+ // defense test, it uses either its own or its master's rating") exists
1533
+ // precisely because you can come at a slave from outside, and the same
1534
+ // paragraph carves out the exception that proves it -- "if a slaved
1535
+ // device is under attack via a direct connection ... it cannot use its
1536
+ // master's ratings to defend itself". Data Trails p.87-88 runs the
1537
+ // whole worked example on a DOOR slaved to a host. Being inside the
1538
+ // host is not the only road; it is the road that costs no cable
1539
+ // ("if you are in a host that has a WAN, you are considered directly
1540
+ // connected to all devices in the WAN").
1541
+ // SO THE GAP WAS THE CABLE. `tap` could reach a camera lens and
1542
+ // nothing else, which left every maglock, keypad and safe -- the
1543
+ // security furniture p.217 names first -- reachable only by cracking
1544
+ // the host first. Exactly backwards from the book's own example.
1545
+ // `tap <device>` is the direct connection now: it rolls the device's
1546
+ // OWN Device Rating + Firewall (the identical pool `hack` faces from
1547
+ // inside the host, because both ARE direct connections), the mark it
1548
+ // buys lands on the master too (p.233), and GOD bills the defender's
1549
+ // hits (p.231-232). It reaches an icon that has never been spotted --
1550
+ // your hand is on the housing, you are not searching for it -- and one
1551
+ // whose WIRELESS IS OFF, because p.421's "Turning It Off" says only
1552
+ // that such a device "can no longer be WIRELESSLY hacked" and a
1553
+ // filament is not wireless. That is what keeps a dark site a thing you
1554
+ // walk into rather than a dead end.
1555
+ // AND THE LEAK: `enter host, glance, exit` lit up every silent icon on
1556
+ // the street. One `spottedBy` set served both vantages, and they are
1557
+ // not the same test -- inside the host a slave hides behind its BARE
1558
+ // rating (one die for a p.356 camera, p.233), outside it borrows the
1559
+ // host's Rating and Sleaze (nine on an HR 3 site). A one-die win was
1560
+ // satisfying a nine-die gate. p.246 is the rule: "the virtual space
1561
+ // inside a host is separate from the outside grid." Device.
1562
+ // spottedInsideBy and Room.camerasSpottedInsideBy keep the sides
1563
+ // apart, one way only -- an OUTSIDE spot still counts inside, because
1564
+ // it beat the harder test.
1565
+ // The other half of that report was that the inside win was invisible:
1566
+ // hostInteriorIcons listed personas, files and ice and never the WAN,
1567
+ // so a device found in there first appeared when you left. It is
1568
+ // listed inside now, where p.233 puts it.
1569
+ export const ENGINE_VERSION = '1.86.0';
1509
1570
  //# sourceMappingURL=engine-version.js.map
@@ -164,16 +164,54 @@ export class Device {
164
164
  * not go missing again between looks.
165
165
  */
166
166
  spottedBy = new Set();
167
+ /**
168
+ * SPOTTED FROM INSIDE THE HOST IT IS SLAVED TO -- a separate record,
169
+ * and the bug that earned it (eJANPpm7qCaCAJBJw, Maka: "Even if I
170
+ * never see the device ... when I leave the host, all slaved devices
171
+ * are automatically visible").
172
+ *
173
+ * There was ONE set, so a sweep won in either place unlocked both --
174
+ * and the two places are not the same test. Inside the host you are
175
+ * directly connected and the slave "cannot use its master's ratings"
176
+ * (p.233), so it hides behind its bare rating: ONE die for a p.356
177
+ * camera. Out on the grid it borrows the host's Rating and Sleaze --
178
+ * nine dice on the reporter's HR 3 bazaar. A one-die win was being
179
+ * spent to satisfy a nine-die gate, so entering a host and glancing
180
+ * around lit up every slaved icon on the street outside it.
181
+ *
182
+ * p.246 is the rule underneath: "the virtual space inside a host is
183
+ * separate from the outside grid." What you learned in there, you
184
+ * learned in there.
185
+ *
186
+ * IT ONLY RUNS ONE WAY. An OUTSIDE spot beats the harder test, so it
187
+ * counts inside too (see hasIconFor); an inside spot does not come
188
+ * back out with you.
189
+ */
190
+ spottedInsideBy = new Set();
167
191
  /** Is this icon on the grid for this viewer? Wireless and rated (the
168
- * old broadcastsAro question), and either loud or already spotted. */
169
- hasIconFor(viewerName) {
192
+ * old broadcastsAro question), and either loud or already spotted
193
+ * FROM WHERE THEY ARE STANDING -- `inside` means inside the host this
194
+ * device is slaved to, where p.233 makes them directly connected. */
195
+ hasIconFor(viewerName, inside = false) {
170
196
  if (!this.broadcastsAro())
171
197
  return false;
172
- return !this.silent || this.spottedBy.has(viewerName);
198
+ if (!this.silent)
199
+ return true;
200
+ if (this.spottedBy.has(viewerName))
201
+ return true;
202
+ return inside && this.spottedInsideBy.has(viewerName);
173
203
  }
174
204
  /** Hidden from this viewer, and therefore worth rolling against. */
175
- isHiddenFrom(viewerName) {
176
- return this.broadcastsAro() && this.silent && !this.spottedBy.has(viewerName);
205
+ isHiddenFrom(viewerName, inside = false) {
206
+ return this.broadcastsAro() && this.silent && !this.hasIconFor(viewerName, inside);
207
+ }
208
+ /** Record a sweep this viewer won, on the side of the wall they won it
209
+ * (p.246). */
210
+ markSpotted(viewerName, inside) {
211
+ if (inside)
212
+ this.spottedInsideBy.add(viewerName);
213
+ else
214
+ this.spottedBy.add(viewerName);
177
215
  }
178
216
  /**
179
217
  * WHAT IT IS MADE OF (p.197-198): Structure is the condition monitor
@@ -81,6 +81,25 @@ export class Room extends AbstractRoom {
81
81
  * of the grid.
82
82
  */
83
83
  camerasSpottedBy = new Set();
84
+ /**
85
+ * AND THE SAME SPLIT Device.spottedInsideBy carries, for the same
86
+ * reason and the same bug. A sweep won INSIDE the host the lenses are
87
+ * slaved to beats their bare rating -- one die, for a p.356 camera --
88
+ * while the grid outside asks for the host's Rating + Sleaze. Sharing
89
+ * one set let the cheap win open the dear door, so stepping into a
90
+ * host and looking around lit the cameras up on the street when you
91
+ * came back out. p.246: the two spaces are separate.
92
+ *
93
+ * Outward-only, like the device's: an outside spot counts everywhere,
94
+ * an inside one stays inside.
95
+ */
96
+ camerasSpottedInsideBy = new Set();
97
+ /** Has this viewer pinned the cluster's icon from where they stand? */
98
+ camerasSpottedFor(viewerName, inside = false) {
99
+ if (this.camerasSpottedBy.has(viewerName))
100
+ return true;
101
+ return inside && this.camerasSpottedInsideBy.has(viewerName);
102
+ }
84
103
  /**
85
104
  * WHAT THE LENSES RATE (eJANPpm7qCaCAJBJw, third rejection: "the
86
105
  * camera cluster doesn't have a DR"), and the reporter's ruling that
@@ -176,7 +176,16 @@ export function aroTags(scene, actor, room) {
176
176
  // still picked, breached, shot and keyed exactly as before.
177
177
  if (!device.hasIconFor(actor.name))
178
178
  continue;
179
- tags.push(`▤ ${device.name} -- wireless, ${drTagFor(device, actor.name)}${device.isOpen() ? ', standing open' : `.${hint(` ("${device.verbs()[0]} ${device.name}")`)}`}`);
179
+ // THE CABLE IS THE OTHER VERB (eJANPpm7qCaCAJBJw). The overlay
180
+ // taught the physical one and nothing else, so a decker standing in
181
+ // front of a slaved keypad had no way to learn that the filament in
182
+ // their deck beats the host's ratings (p.233). Only offered when
183
+ // there is a deck to plug in -- a hint naming gear you do not have
184
+ // is noise.
185
+ const cable = device.isOpen() || !(actor.getCyberdeck() || actor.isTechnomancer())
186
+ ? ''
187
+ : hint(` ("tap ${device.name}" cables in -- it can't hide behind its host against a cable)`);
188
+ tags.push(`▤ ${device.name} -- wireless, ${drTagFor(device, actor.name)}${device.isOpen() ? ', standing open' : `.${hint(` ("${device.verbs()[0]} ${device.name}")`)}${cable}`}`);
180
189
  }
181
190
  // LOOSE DEVICES lying in plain sight. A wireless deck on a table
182
191
  // announces itself; one in a drawer you never opened does not.
@@ -480,6 +480,29 @@ function hostInteriorIcons(scene, actor, room) {
480
480
  ? `◇ ${item.name} [data] -- unsealed, sitting open in the host's archive. ("take" it)`
481
481
  : `◇ ${item.name} [data] -- SEALED in the host's archive. ("hack" the host to break the seal)`);
482
482
  }
483
+ // THE WAN, WHICH IS WHAT YOU CAME IN FOR (p.233: "if you are in a host
484
+ // that has a WAN, you are considered directly connected to all devices
485
+ // in the WAN"; eJANPpm7qCaCAJBJw, Maka: "when I leave the host, all
486
+ // slaved devices are automatically visible").
487
+ //
488
+ // This list did not exist, and its absence is half of that report. A
489
+ // sweep inside the host rolls against the slave's BARE rating -- one
490
+ // die for a p.356 camera, because you are directly connected and it
491
+ // "cannot use its master's ratings" -- and then nothing in here
492
+ // printed what you had just found. The win was invisible until the
493
+ // persona stepped back out onto the grid, where the icon appeared for
494
+ // free against a test it had never beaten. Device.spottedInsideBy now
495
+ // keeps the two sides of that wall apart (p.246); this is the side
496
+ // that had nothing to show.
497
+ //
498
+ // Same gate as the outside list, read from in here: an icon is on this
499
+ // list once THIS viewer has pinned it, and `hack`/`disable` target
500
+ // exactly the same set.
501
+ for (const device of room.devices) {
502
+ if (!device.hasIconFor(actor.name, true))
503
+ continue;
504
+ icons.push(deviceIconLine(device, actor));
505
+ }
483
506
  // THE ICE IS IN THE ROOM WITH YOU, AND THE ROOM USED TO DENY IT.
484
507
  //
485
508
  // A regression this file shipped: hostInteriorIcons listed personas
@@ -2,7 +2,7 @@ import { rollPool, formatRoll } from './dice.js';
2
2
  import { deviceHidePool, wanHidePool } from './matrix-intrusion.js';
3
3
  import { isWatched, cameraMasterHost } from './surveillance.js';
4
4
  import { gridActionModifier, gridNote } from './grids.js';
5
- function hiddenDevice(device, master) {
5
+ function hiddenDevice(device, master, inside) {
6
6
  // p.235-236: the hider rolls LOGIC + SLEAZE. Not Firewall -- that
7
7
  // resists an intrusion (p.238/p.240) and has nothing to do with being
8
8
  // seen. This used to call deviceDefensePool, the INTRUSION pool, so
@@ -13,7 +13,11 @@ function hiddenDevice(device, master) {
13
13
  return {
14
14
  name: device.name,
15
15
  hide: { pool: hide.pool, label: hide.label },
16
- reveal: (viewer) => { device.spottedBy.add(viewer); },
16
+ // WHERE THE WIN IS BANKED (p.246). A sweep inside the host beat the
17
+ // slave's bare rating; the grid outside asks for the master's
18
+ // Rating + Sleaze. Writing both from one roll is what made
19
+ // "enter, glance, exit" reveal a street's worth of silent icons.
20
+ reveal: (viewer) => { device.markSpotted(viewer, inside); },
17
21
  };
18
22
  }
19
23
  function hiddenPersona(actor) {
@@ -37,10 +41,11 @@ export function hiddenIconsIn(scene, room, viewer) {
37
41
  // A device in a room with a host is slaved to it (p.233), and from
38
42
  // the grid it borrows the master's ratings. Inside the host you are
39
43
  // directly connected and it stands alone -- deviceHidePool.
40
- const deviceMaster = viewer.hostInside ? undefined : room.host;
44
+ const inside = !!viewer.hostInside;
45
+ const deviceMaster = inside ? undefined : room.host;
41
46
  const out = room.devices
42
- .filter(d => d.isHiddenFrom(viewer.name))
43
- .map(d => hiddenDevice(d, deviceMaster));
47
+ .filter(d => d.isHiddenFrom(viewer.name, inside))
48
+ .map(d => hiddenDevice(d, deviceMaster, inside));
44
49
  // THE CAMERAS, which p.217 names FIRST. They are not a Device -- the
45
50
  // cluster is derived from the room (surveillance.ts isWatched) -- so
46
51
  // the rating rides the ROOM (Room.cameraRating), seeded per site.
@@ -58,7 +63,7 @@ export function hiddenIconsIn(scene, room, viewer) {
58
63
  // could not have reconciled them because two of the three were never
59
64
  // printed. One seeded number feeds all three now, so what the player
60
65
  // reads is what it rolls (reporter's ruling: per-site, not global).
61
- if (isWatched(room) && !room.camerasSpottedBy.has(viewer.name)) {
66
+ if (isWatched(room) && !room.camerasSpottedFor(viewer.name, inside)) {
62
67
  // THE THIRD CUT, and the reporter found it by doing the arithmetic
63
68
  // out loud: "Device Rating + Firewall on the Camera is 2d6? so, the
64
69
  // firewall of the host is a 1?" No -- Firewall was never the right
@@ -70,13 +75,15 @@ export function hiddenIconsIn(scene, room, viewer) {
70
75
  // stray one is easy -- one rule, not a balance knob (reporter's
71
76
  // ruling 2026-09-16).
72
77
  const masterRoom = scene ? cameraMasterHost(scene.getRooms(), room) : undefined;
73
- const master = viewer.hostInside ? undefined : masterRoom?.host;
78
+ const master = inside ? undefined : masterRoom?.host;
74
79
  const lens = { rating: room.cameraRating };
75
80
  const hide = master ? wanHidePool(lens, master) : deviceHidePool(lens);
76
81
  out.push({
77
82
  name: 'Camera cluster',
78
83
  hide: { pool: hide.pool, label: hide.label },
79
- reveal: (v) => { room.camerasSpottedBy.add(v); },
84
+ // Banked on the side of the wall it was won (p.246), like the
85
+ // devices above.
86
+ reveal: (v) => { (inside ? room.camerasSpottedInsideBy : room.camerasSpottedBy).add(v); },
80
87
  });
81
88
  }
82
89
  for (const a of scene?.allActors ?? []) {
@@ -60,9 +60,16 @@ export function cameraMasterHost(rooms, room) {
60
60
  * the rules allowed this at all, so a refusal that teaches the rule is
61
61
  * worth more than a refusal that just says no.
62
62
  */
63
- export function directConnectionBlocker(actor, room) {
63
+ export function directConnectionBlocker(actor, room, target) {
64
+ // WHAT THE CABLE GOES INTO. The refusals name it, because a player who
65
+ // typed `tap keypad` and is told about "the lens" has been answered
66
+ // about a different object (eJANPpm7qCaCAJBJw). Cameras stay the
67
+ // default: this function was written for them and every existing
68
+ // caller means them.
69
+ const thing = target ?? 'the lens';
70
+ const its = target ? target : 'its cameras';
64
71
  if (actor.plane === 'matrix') {
65
- return `Your persona is out on the grid -- and a direct connection is a CABLE, not a route. You cannot plug a cable in from inside the Matrix (p.232): it takes your body, standing next to the lens, with a hand free. "jack out", walk to ${room.name}, and tap it in the flesh.`;
72
+ return `Your persona is out on the grid -- and a direct connection is a CABLE, not a route. You cannot plug a cable in from inside the Matrix (p.232): it takes your body, standing next to ${thing}, with a hand free. "jack out", walk to ${room.name}, and tap it in the flesh.`;
66
73
  }
67
74
  if (actor.plane === 'astral') {
68
75
  return `A spirit has no hands and no data connector. The Matrix doesn't answer to the astral.`;
@@ -71,13 +78,13 @@ export function directConnectionBlocker(actor, room) {
71
78
  return `Not from here -- a direct connection takes your own body, and yours is somewhere else.`;
72
79
  }
73
80
  if (actor.currentLocation !== room) {
74
- return `You'd have to be standing in ${room.name} to get a cable into its cameras.`;
81
+ return `You'd have to be standing in ${room.name} to get a cable into ${its}.`;
75
82
  }
76
83
  // A cyberdeck (or a datajack, which a technomancer is assumed to run
77
84
  // on) carries the microfilament cable; a bare commlink has the port
78
85
  // but nothing to hack WITH.
79
86
  if (!actor.getCyberdeck() && !actor.isTechnomancer()) {
80
- return `You have no deck to plug in. A commlink can talk to a camera; it can't crack one -- that takes a cyberdeck in hand.`;
87
+ return `You have no deck to plug in. A commlink can talk to ${target ? thing : 'a camera'}; it can't crack ${target ? 'it' : 'one'} -- that takes a cyberdeck in hand.`;
81
88
  }
82
89
  return undefined;
83
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.235.0",
3
+ "version": "5.237.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.",