@maka/maka-cli 5.239.0 → 5.240.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.239.0",
3
+ "version": "5.240.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.",
@@ -13,6 +13,7 @@ import { asksForDataWork, noDeckForDataWork } from '../utilities/data-work.js';
13
13
  import { partyCanDeck } from '../factories/scene-chunks.js';
14
14
  import { RecallCommand } from './companions.js';
15
15
  import { inLiveCombat } from '../utilities/action-cost.js';
16
+ import { radioIsDark } from '../utilities/pan-devices.js';
16
17
  export class CallCommand extends Command {
17
18
  static verb = 'call';
18
19
  static description = 'Call someone remotely over a commlink (no need to be in the same room; commlink must be equipped).';
@@ -56,9 +57,17 @@ export class CallCommand extends Command {
56
57
  if (!this.hasEquippedCommlink(this.actor)) {
57
58
  return `You need to equip a commlink before you can call anyone.`;
58
59
  }
59
- // Running silent kills the broadcast both ways (see silent.ts).
60
- if (this.actor.runningSilent) {
61
- return `You're running silent -- your link isn't broadcasting. Go loud ("silent") before calling anyone.`;
60
+ // A DEAD RADIO STOPS A CALL; SILENCE DOES NOT (RXPvXqTwEyH9v3Zk7).
61
+ // This asked `runningSilent` and refused, on the strength of a claim
62
+ // canon does not make: p.235 says silent running "reduces your
63
+ // traffic to and from the Matrix, but it doesn't stop it entirely".
64
+ // Losing your traffic is p.421's wireless-off, so that is what
65
+ // refuses here now -- and a silent decker can finally call their own
66
+ // team, which is the whole reason a runner goes silent in the first
67
+ // place. The INBOUND direction is a different question and still
68
+ // refuses while silent; see the target check further down.
69
+ if (radioIsDark(this.actor)) {
70
+ return `Every radio you own is switched off -- nothing of yours is on the air.${hint(` ("wireless on" to come back up.)`)}`;
62
71
  }
63
72
  // Hacked dark is a different kind of quiet (see hack.ts PAN attacks).
64
73
  if (this.actor.arOffline) {
@@ -316,8 +325,18 @@ export class CallCommand extends Command {
316
325
  if (!onTheBond && !this.hasCommlink(target)) {
317
326
  return `You don't get through -- ${target.name} doesn't seem to have a commlink.`;
318
327
  }
328
+ // TWO WAYS TO FAIL TO REACH SOMEONE, and they are different facts
329
+ // about them (RXPvXqTwEyH9v3Zk7). A silent icon is out there and
330
+ // simply cannot be pointed at without winning a Matrix Perception
331
+ // test for it first (p.235) -- which a phone call is not. A dark
332
+ // radio is not out there at all (p.421). The wording says which,
333
+ // because to a player deciding what to do next they are not the
334
+ // same problem.
335
+ if (radioIsDark(target)) {
336
+ return `Nothing rings -- ${target.name}'s gear is all switched off. Wherever they are, they are not on the air.`;
337
+ }
319
338
  if (target.runningSilent) {
320
- return `You can't raise ${target.name} -- their link isn't broadcasting. They're running silent.`;
339
+ return `You can't raise ${target.name} -- their icon isn't where it should be. They're running silent, and a call has nothing to point at.`;
321
340
  }
322
341
  // HUB flow: calling the fixer from home isn't a conversation -- it's
323
342
  // the COMMISSION. He answers, tells you to stay by your link, and
@@ -1,6 +1,6 @@
1
1
  import { Command } from './command.js';
2
2
  import { hint, hintsEnabled } from '../utilities/hints.js';
3
- import { CALL_ICON, isLinked } from '../utilities/comm-style.js';
3
+ import { CALL_ICON, isReachable } from '../utilities/comm-style.js';
4
4
  /**
5
5
  * The roster: who talks to you, how plugged-in they are, and how deep
6
6
  * the relationship runs. Contacts are EARNED (see Game.touchContact) --
@@ -103,7 +103,7 @@ export class ContactsCommand extends Command {
103
103
  // the street, not just how much juice they carry. The 📞 marks
104
104
  // who's LINKED -- reachable by "call" from anywhere in the hub.
105
105
  const actor = this.game.hubActor(name);
106
- const linked = actor ? isLinked(actor) : false;
106
+ const linked = actor ? isReachable(actor) : false;
107
107
  // The note is the relationship's memory -- a client's grudge or a
108
108
  // sloppy wrap-up (returnToHub's upsert) -- compacted to its first
109
109
  // word as a marker, hint-on shows the whole line.
@@ -122,7 +122,7 @@ export class ContactsCommand extends Command {
122
122
  * jump once they're a real contact; Loyalty reads as unearned. */
123
123
  clientRow(name) {
124
124
  const actor = this.game.hubActor(name);
125
- const linked = actor ? isLinked(actor) : false;
125
+ const linked = actor ? isReachable(actor) : false;
126
126
  return ` ${name.padEnd(17)} ${'Johnson'.padEnd(14)} ${linked ? CALL_ICON : ' '} Conn 4, Loyalty - (this run's client)`;
127
127
  }
128
128
  }
@@ -78,7 +78,7 @@ export class EquipmentCommand extends Command {
78
78
  : `${dni.item?.name ?? 'trode net'} (${dni.host ? `built into your ${dni.host.name}${dni.worn ? '' : ', in the bag'}` : dni.worn ? 'worn' : 'in the bag'}) -- VR ready`;
79
79
  lines.push(` Interface: ${dniLine}`);
80
80
  if (link) {
81
- lines.push(` Commlink: ${link.name}${actor.runningSilent ? ' -- running silent' : ''}`);
81
+ lines.push(` Commlink: ${link.name}${!link.wirelessOn ? ' -- wireless OFF' : actor.runningSilent ? ' -- running silent (icon hidden, still broadcasting)' : ''}`);
82
82
  }
83
83
  }
84
84
  // WORN, by region, in words.
@@ -3,6 +3,7 @@ import { hint } from '../utilities/hints.js';
3
3
  import { Player } from '../models/player.js';
4
4
  import { Category } from '../types/shared/item-enum.js';
5
5
  import { billAction } from '../utilities/action-cost.js';
6
+ import { reachableDevices, radioIsDark } from '../utilities/pan-devices.js';
6
7
  /**
7
8
  * The PAN dashboard: your personal area network at a glance -- master
8
9
  * device and its firewall, every slaved device with what it grants while
@@ -35,29 +36,11 @@ export class PanCommand extends Command {
35
36
  }
36
37
  return this.dashboard();
37
38
  }
38
- /**
39
- * Everything on this runner the switch can reach, held and worn gear
40
- * INCLUDED. Equipping moves an item out of `inventory.getAllItems()`,
41
- * so a list built from inventory alone loses the gun in your hand --
42
- * which is the one device a player is most likely to mute. Both the
43
- * lookup and the "switched off" list read this, so a device you can
44
- * turn off is always a device you can see is off.
45
- */
39
+ /** Everything the switch can reach on this runner. Lives in
40
+ * utilities/pan-devices.ts now that `wireless` sweeps the same list
41
+ * (p.421 prices the one-device and all-devices forms together). */
46
42
  reachableDevices() {
47
- const a = this.actor;
48
- const seen = new Set();
49
- return [
50
- a.equipment.head?.commlink,
51
- a.equipment.head?.eyes,
52
- a.equipment.rightHand?.weapon,
53
- a.equipment.leftHand?.weapon,
54
- ...a.inventory.getAllItems(),
55
- ].filter((i) => {
56
- if (i == null || seen.has(i))
57
- return false;
58
- seen.add(i);
59
- return true;
60
- });
43
+ return reachableDevices(this.actor);
61
44
  }
62
45
  /** One device on or off the network (p.233 -- slaving is a choice). */
63
46
  switchDevice(name, on) {
@@ -123,11 +106,23 @@ export class PanCommand extends Command {
123
106
  }
124
107
  return `No devices, no PAN. You are unhackable and unreachable -- the monk's build.`;
125
108
  }
126
- const state = a.runningSilent
127
- ? `{magenta-fg}SILENT{/magenta-fg} -- hidden (+2 firewall), but no calls, no AR, no Matrix`
128
- : a.arOffline
129
- ? `{red-fg}LOUD, AR CUT{/red-fg} -- someone hacked your overlay dark ("reboot")`
130
- : `{green-fg}LOUD{/green-fg} -- full bonuses, and hackable`;
109
+ // THREE STATES, NOT TWO (RXPvXqTwEyH9v3Zk7). The line this replaced
110
+ // read "SILENT -- hidden (+2 firewall), but no calls, no AR, no
111
+ // Matrix", and every clause after the comma described a DIFFERENT
112
+ // state: p.421's wireless-off. Worse, "no AR" contradicted the
113
+ // engine's own RAG-checked ruling three files away (utilities/ar.ts
114
+ // arSightUp: silence is what you EMIT, AR is what you RECEIVE), so
115
+ // one runner could read two opposite answers about one flag.
116
+ //
117
+ // Running silent costs the -2 and nothing else (p.235). Turning the
118
+ // radio off is what costs you the bonuses and the traffic (p.421).
119
+ const state = radioIsDark(a)
120
+ ? `{white-fg}RADIO OFF{/white-fg} -- nothing broadcasting: no bonuses, no calls, and nothing to hack${hint(` ("wireless on")`)}`
121
+ : a.runningSilent
122
+ ? `{magenta-fg}SILENT{/magenta-fg} -- icon hidden (+2 firewall), -2 dice on your Matrix actions; bonuses and calls untouched`
123
+ : a.arOffline
124
+ ? `{red-fg}LOUD, AR CUT{/red-fg} -- someone hacked your overlay dark ("reboot")`
125
+ : `{green-fg}LOUD{/green-fg} -- full bonuses, and hackable`;
131
126
  const slaves = a.panSlaves();
132
127
  const lines = [
133
128
  `PAN: ${a.name.toUpperCase()}`,
@@ -172,15 +167,22 @@ export class PanCommand extends Command {
172
167
  }
173
168
  }
174
169
  lines.push('');
175
- lines.push(a.runningSilent
176
- ? `Silent running: enemy deckers must FIND you before they can crack you -- but a good one can force you loud.`
177
- : `Running loud: any decker in reach can try your firewall -- jammed guns and dead AR are how that feels.${hint(' "silent" goes dark.')}`);
170
+ // THE TWO SWITCHES, NAMED APART. A player reading this screen is
171
+ // deciding between them, and until this item they had only one verb.
172
+ lines.push(radioIsDark(a)
173
+ ? `Radio off: there is no icon of yours anywhere on the Matrix, and a cable ("tap") is the only way anyone reaches your gear.${hint(' "wireless on" to come back up.')}`
174
+ : a.runningSilent
175
+ ? `Silent running: enemy deckers must FIND you before they can crack you -- but a good one can force you loud.${hint(' "wireless off" is the harder version: no icon at all, and no bonuses either.')}`
176
+ : `Running loud: any decker in reach can try your firewall -- jammed guns and dead AR are how that feels.${hint(' "silent" hides your icon; "wireless off" kills the broadcast outright.')}`);
178
177
  return lines.join('\n');
179
178
  }
180
179
  /** What one slaved device is and what being on the net buys it. */
181
180
  slaveLine(item) {
182
181
  const a = this.actor;
183
- const inactive = a.runningSilent || a.arOffline;
182
+ // A SLAVE'S BONUS DIES WITH ITS RADIO, NOT WITH SILENCE (p.421 vs
183
+ // p.235, RXPvXqTwEyH9v3Zk7). This asked `a.runningSilent`, which is
184
+ // the one thing canon says does NOT cost you wireless bonuses.
185
+ const inactive = !item.wirelessOn || a.arOffline;
184
186
  if (item.isCyberdeck()) {
185
187
  return `${item.name} (cyberdeck) -- Matrix access; ${item.deckSummary()}${item.isBricked ? ' BRICKED (rest to repair)' : ''}`;
186
188
  }
@@ -2,6 +2,7 @@ import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { fetchPlayers } from '../utilities/social.js';
4
4
  import { authToken } from '../utilities/cloud-saves.js';
5
+ import { radioIsDark } from '../utilities/pan-devices.js';
5
6
  /**
6
7
  * THE STREET ("crew players"). Real players' runners, shown by STREET
7
8
  * NAME only -- no account handles, no emails. A runner is "online" only
@@ -31,11 +32,17 @@ export class PlayersCommand extends Command {
31
32
  // "headers like that are too cluttery"). The heading itself stays --
32
33
  // it carries what the panel divider can't say for it.
33
34
  const lines = [`THE STREET (real runners, via maka-cli.com)`];
34
- const myLink = game.player.liveLink() != null && !game.player.runningSilent;
35
+ // THREE REASONS THE BEACON IS DARK, and the line names which
36
+ // (RXPvXqTwEyH9v3Zk7). It used to offer two, with "running silent"
37
+ // standing in for the switched-off case as well -- which is exactly
38
+ // the conflation that item was filed about. The cure differs per
39
+ // reason, so the reason has to be the right one.
40
+ const dark = radioIsDark(game.player);
41
+ const myLink = game.player.liveLink() != null && !game.player.runningSilent && !dark;
35
42
  lines.push(beat === 'ok'
36
43
  ? myLink
37
44
  ? ` Your beacon: broadcasting -- the street can see ${this.actor.name}.`
38
- : ` Your beacon: DARK -- ${game.player.runningSilent ? 'you\'re running silent' : 'no commlink equipped'}; the street can't see you.`
45
+ : ` Your beacon: DARK -- ${dark ? 'your wireless is OFF ("wireless on")' : game.player.runningSilent ? 'you\'re running silent ("silent" to go loud)' : 'no commlink equipped'}; the street can't see you.`
39
46
  : beat === 'logged-out'
40
47
  ? ` Your beacon: DARK -- \`maka login\` and the street will see you.`
41
48
  : ` Your beacon: DARK -- the grid didn't answer; nobody sees you right now.`);
@@ -1,32 +1,53 @@
1
1
  import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { Category } from '../types/shared/item-enum.js';
4
- import { CALL_ICON, CALL_COLOR, dropActiveCall } from '../utilities/comm-style.js';
4
+ import { CALL_ICON, CALL_COLOR } from '../utilities/comm-style.js';
5
+ import { billAction } from '../utilities/action-cost.js';
6
+ import { radioIsDark } from '../utilities/pan-devices.js';
5
7
  /**
6
- * Running silent, the Shadowrun way: your commlink stays on you but stops
7
- * broadcasting. While silent, you can't place calls and NOBODY can raise
8
- * you -- including the fixer, so a job callback will not find you until
9
- * you go loud again. Toggling silent drops any call you're on. The HUD's
10
- * Link line tracks the state (see game.ts updateStatus).
8
+ * RUNNING SILENT (p.235-236): your radio stays ON and your icon stops
9
+ * being findable. That is the whole of it.
11
10
  *
12
- * What silence COSTS and what it does NOT (canon p.235-236, checked
13
- * 2026-08-27): it hides your icon -- finding you takes a Matrix
14
- * Perception test first, then an Opposed test -- and it charges -2 dice
15
- * on every Matrix action you take (Player.matrixActionPenalty), on top
16
- * of the calls and the commlink initiative bonus this engine already
17
- * takes. It does NOT blind you: your own AR overlay keeps painting,
18
- * because AR is what you receive (p.229). See utilities/ar.ts arSightUp.
11
+ * WHAT THIS VERB STOPPED MEANING (RXPvXqTwEyH9v3Zk7, Maka: "we need to
12
+ * work out wireless vs silent. Silent should only be in the matrix, with
13
+ * its meat counterpart of 'sneak'. What turns off your broadcast should
14
+ * be 'wireless'"). Until that item, `silent` also killed your calls,
15
+ * your street-list beacon and your commlink's initiative bonus, and the
16
+ * HUD announced it in the meat as "Link SILENT" -- so the word meant
17
+ * "go dark", there was no other verb that did, and a player could not
18
+ * tell the two states apart because the engine did not either.
19
19
  *
20
- * WHERE IT WORKS: anywhere. Meat, AR, VR, and inside a host -- switching
21
- * to silent running is a Simple Action and the book puts no precondition
22
- * on it (p.235-236). It is not a device you power down, so being jacked
23
- * in is not an obstacle to it; Data Trails p.68-69 assumes a hacker runs
24
- * silent while already inside a host. See execute() for the refusal this
25
- * replaced and why it was wrong (qM7FmMABSMa9hZbqG).
20
+ * Canon separates them cleanly, and the split is now the code's:
21
+ * - RUNNING SILENT (p.235, Simple Action) "reduces your traffic to and
22
+ * from the Matrix, but it doesn't stop it entirely". It costs -2
23
+ * dice on ALL your Matrix actions (Player.matrixActionPenalty) and
24
+ * hides your icon behind an Opposed Computer + Intuition [Data
25
+ * Processing] v. Logic + Sleaze test. It costs you NO wireless
26
+ * bonuses -- the rules never take them.
27
+ * - WIRELESS OFF (p.421, Free Action, commands/wireless.ts) is what
28
+ * takes the bonuses, the calls and your Matrix presence entirely.
29
+ *
30
+ * It does NOT blind you: your own AR overlay keeps painting, because AR
31
+ * is what you receive (p.229). See utilities/ar.ts arSightUp.
32
+ *
33
+ * AND IT DOES NOT UN-SEE YOU. p.235, Spotting Duration: "Once you've
34
+ * spotted an icon in the Matrix, you continue to spot it even if it
35
+ * initiates silent running." Only Hide, a reboot or a jack-out clears a
36
+ * spot, so this verb deliberately never touches Player.matrixSpottedBy.
37
+ *
38
+ * WHERE IT WORKS: anywhere. Meat, AR, VR, and inside a host -- the book
39
+ * names "your commlink, deck, other device, or persona" and puts no
40
+ * precondition on the action (p.235-236). A body in a room hiding its
41
+ * PAN from a hostile decker is the ordinary case, not an edge one; Data
42
+ * Trails p.68-69 assumes a hacker runs silent while already INSIDE a
43
+ * host. The meat/Matrix pair the reporter asked for is enforced in the
44
+ * VOCABULARY -- this verb talks about your ICON, `sneak` talks about
45
+ * your body -- rather than by refusing a legal action. See execute() for
46
+ * the refusal this replaced and why it was wrong (qM7FmMABSMa9hZbqG).
26
47
  */
27
48
  export class SilentCommand extends Command {
28
49
  static verb = 'silent';
29
- static description = 'Toggle your commlink broadcast: running silent hides your icon -- no calls in OR out (nobody can reach you, not even the fixer), no commlink initiative bonus, and -2 dice on your own Matrix actions, until you go loud again. Your AR overlay keeps painting.';
50
+ static description = 'Toggle running silent (p.235): your icon stops being findable -- anything hunting you needs an Opposed Matrix Perception test first -- at -2 dice on all your own Matrix actions. Your radio stays ON: calls, AR and wireless bonuses are untouched. "wireless off" is what goes properly dark.';
30
51
  async execute(_args = []) {
31
52
  const actor = this.actor;
32
53
  // JACKED IN IS EXACTLY WHEN YOU WANT THIS (qM7FmMABSMa9hZbqG, Deditri:
@@ -61,37 +82,56 @@ export class SilentCommand extends Command {
61
82
  if (!hasAnyLink) {
62
83
  return `You have no commlink, deck or persona to silence.`;
63
84
  }
85
+ // NOTHING BROADCASTING, NOTHING TO HIDE (RXPvXqTwEyH9v3Zk7). Running
86
+ // silent is a mode a LIVE icon runs in (p.235: it "reduces your
87
+ // traffic", which presupposes traffic). With every radio switched
88
+ // off there is no icon at all, so silence would be a state with no
89
+ // effect that still charged -2 dice. A technomancer's living persona
90
+ // has no switch to throw, so it is never dark this way.
91
+ if (!actor.isTechnomancer() && radioIsDark(actor)) {
92
+ return `Every radio you own is already off -- there is no icon of yours out there to hide.${hint(` ("wireless on" first if you want to be findable-but-silent instead of simply absent.)`)}`;
93
+ }
64
94
  // WHOSE VOICE THE ANSWER IS IN. Now that this verb works on the grid,
65
95
  // the meat-world wording would land on a persona that has no overlay
66
96
  // and, for a technomancer, no link either -- "your AR overlay is
67
97
  // unaffected" is a strange thing to tell someone who is currently IN
68
98
  // VR. Same mechanics either way; only the noun changes.
69
99
  const inVr = actor.plane === 'matrix';
100
+ // A SIMPLE ACTION, PRICED WHERE THE BOOK PRICES IT (p.235:
101
+ // "Switching to silent running is a Simple Action"). This verb billed
102
+ // nothing at all before RXPvXqTwEyH9v3Zk7 -- so mid-firefight a
103
+ // runner could go silent, go loud and go silent again for free while
104
+ // `pan`, three files away, correctly charged for a Free Action.
105
+ // billAction only charges inside an encounter, so this stays free
106
+ // out of combat.
107
+ const bill = billAction(this.scene, actor, 'simple', 'Running Silent');
108
+ if (bill)
109
+ return bill;
70
110
  // Going loud again.
71
111
  if (actor.runningSilent) {
72
112
  actor.runningSilent = false;
73
- actor.performAction('goes loud', 'commlink broadcasting');
113
+ actor.performAction('goes loud', 'icon broadcasting openly');
74
114
  this.logger.write(`${actor.name} stops running silent.`);
75
115
  this.scene.updateStatus();
76
116
  // The street sees the link state (players list) -- beat now.
77
117
  if (this.game?.hasHub)
78
118
  void this.game.pushPresenceNow();
79
119
  return inVr
80
- ? `{${CALL_COLOR}-fg}${CALL_ICON} Your icon swims back into the open -- broadcasting again, and anything looking can see you.{/${CALL_COLOR}-fg}`
81
- : `{${CALL_COLOR}-fg}${CALL_ICON} Your link comes back up -- broadcasting again. Calls can reach you now.{/${CALL_COLOR}-fg}`;
82
- }
83
- // Going silent: drop any active call first, both sides.
84
- let hangupNote = '';
85
- const partner = dropActiveCall(actor);
86
- if (partner) {
87
- this.logger.write(`${actor.name} dropped the call with ${partner} by going silent.`);
88
- hangupNote = ` The call with ${partner} drops mid-word.`;
120
+ ? `{${CALL_COLOR}-fg}${CALL_ICON} Your icon swims back into the open -- anything looking can see you now, with no test to make.{/${CALL_COLOR}-fg}`
121
+ : `{${CALL_COLOR}-fg}${CALL_ICON} Your icon comes back into the open -- anyone sweeping for it finds it now, and the -2 on your Matrix actions lifts.{/${CALL_COLOR}-fg}`;
89
122
  }
123
+ // NO HANG-UP HERE ANY MORE. Dropping the call moved to
124
+ // commands/wireless.ts, where the state that actually kills your
125
+ // traffic lives (p.421). Canon is explicit that silence does not:
126
+ // it "reduces your traffic to and from the Matrix, but it doesn't
127
+ // stop it entirely" (p.235).
90
128
  actor.runningSilent = true;
91
- actor.performAction('goes silent', 'commlink dark');
129
+ actor.performAction('goes silent', 'icon hidden');
92
130
  this.logger.write(`${actor.name} is now running silent.`);
93
131
  this.scene.updateStatus();
94
- // Silent means OFF the street list too -- beat now.
132
+ // A hidden icon is off the street list too -- beat now. (The list is
133
+ // "who can be FOUND on the grid", which silence answers; whether
134
+ // they can be CALLED is a separate question now, and still yes.)
95
135
  if (this.game?.hasHub)
96
136
  void this.game.pushPresenceNow();
97
137
  // NOT {gray-fg} (standing ruling): blessed gray is ANSI
@@ -105,12 +145,15 @@ export class SilentCommand extends Command {
105
145
  // more small print. The exemption gets its own sentence now.
106
146
  return [
107
147
  inVr
108
- ? `{white-fg}${CALL_ICON} Your persona stops broadcasting -- your icon thins out of the traffic around you.${hangupNote}`
109
- : `{white-fg}${CALL_ICON} You kill your link's broadcast.${hangupNote}`,
110
- `SILENT: your icon is hidden. No calls in or out, no commlink initiative bonus, and -2 dice on your own Matrix actions while you cover your tracks.`,
148
+ ? `{white-fg}${CALL_ICON} Your persona thins out of the traffic around you -- there, but no longer where anything can point at.`
149
+ : `{white-fg}${CALL_ICON} Your PAN's icon thins out of the grid's traffic -- still there, just no longer findable.`,
150
+ `SILENT: anything hunting you needs a Matrix Perception hit to know you exist, then an Opposed test to pin you (p.235). It costs -2 dice on all your own Matrix actions.`,
151
+ // THE RADIO IS STILL ON, SAID OUT LOUD, because until this item
152
+ // the verb claimed the opposite and a reporter had to ask. This is
153
+ // the sentence the item was filed for.
111
154
  inVr
112
- ? `You are still jacked in and still see everything you saw a moment ago -- silence is what you BROADCAST, not what you can see.${hint(` ("silent" again to go loud.)`)}{/white-fg}`
113
- : `Your AR overlay is UNAFFECTED and still painting -- silence is what you BROADCAST, not what you can see.${hint(` ("silent" again to go loud.)`)}{/white-fg}`,
155
+ ? `Your radio is untouched -- you are still jacked in, still see everything you saw a moment ago, and your gear keeps its wireless bonuses. Silence is what you BROADCAST, not what you can see.${hint(` ("silent" again to go loud; "wireless off" is the harder version that costs you the bonuses too.)`)}{/white-fg}`
156
+ : `Your radio is untouched -- AR still painting, calls still landing, wireless bonuses intact. Silence is what you BROADCAST, not what you can see.${hint(` ("silent" again to go loud; "wireless off" is what actually kills the broadcast.)`)}{/white-fg}`,
114
157
  ].join(' ');
115
158
  }
116
159
  }
@@ -16,7 +16,7 @@ export class SneakCommand extends Command {
16
16
  const actor = this.actor;
17
17
  if (actor.plane !== 'meat') {
18
18
  return actor.plane === 'matrix'
19
- ? `Personas don't tiptoe.${hint(` Run silent ("silent") if you want to be hard to find on the grid.`)}`
19
+ ? `Personas don't tiptoe.${hint(` Sneaking is the MEAT half of this; on the grid the verb is "silent", which hides your icon instead of your body.`)}`
20
20
  : `The astral doesn't care how softly you drift.`;
21
21
  }
22
22
  if (actor.inExchange) {
@@ -0,0 +1,107 @@
1
+ import { Command } from './command.js';
2
+ import { hint } from '../utilities/hints.js';
3
+ import { CALL_ICON, CALL_COLOR, dropActiveCall } from '../utilities/comm-style.js';
4
+ import { billAction } from '../utilities/action-cost.js';
5
+ import { switchableDevices, radioIsDark } from '../utilities/pan-devices.js';
6
+ /**
7
+ * THE MASTER SWITCH (RXPvXqTwEyH9v3Zk7, Maka: "What turns off your
8
+ * broadcast should be 'wireless'").
9
+ *
10
+ * SR5 p.421, "Turning It Off", prices both scales in one sentence:
11
+ * "Toggling an individual device's wireless functionality off is a Free
12
+ * Action, as is toggling all of your wireless devices to 'wireless off.'
13
+ * You lose wireless bonuses, but the items can no longer be wirelessly
14
+ * hacked." The per-device half has existed since CgJ6uTfEvTd8JXdDN as
15
+ * `pan on|off <device>`. This is the other half, which canon names
16
+ * explicitly and the engine never had -- so the only way to go dark was
17
+ * `silent`, and that is how one verb came to mean two things.
18
+ *
19
+ * WHAT THIS IS NOT. Running silent (commands/silent.ts, p.235-236) is a
20
+ * DIFFERENT state and this verb must never be mistaken for it:
21
+ *
22
+ * wireless off (p.421) running silent (p.235)
23
+ * toggle Free Action Simple Action
24
+ * on the Matrix not at all yes -- traffic "reduced,
25
+ * not stopped entirely"
26
+ * wirelessly hackable no yes, once found
27
+ * wireless bonuses LOST kept
28
+ * costs you the bonuses -2 dice, all Matrix actions
29
+ *
30
+ * A cable still reaches a dark device (`tap`, p.233/p.421: the universal
31
+ * access port is not wireless and "nothing is completely safe from a
32
+ * hacker with a datajack"). Going dark is cover, not armour.
33
+ */
34
+ export class WirelessCommand extends Command {
35
+ static verb = 'wireless';
36
+ static description = 'Switch every device you carry on or off the air at once (p.421, a Free Action). Wireless OFF costs you every wireless bonus and every call, and leaves nothing of yours on the Matrix to hack. Distinct from "silent", which hides your icon but keeps you broadcasting.';
37
+ async execute(args = []) {
38
+ const actor = this.actor;
39
+ const head = (args[0] ?? '').toLowerCase();
40
+ const devices = switchableDevices(actor);
41
+ if (devices.length === 0) {
42
+ return `You are carrying nothing with a radio in it. Nothing to switch.`;
43
+ }
44
+ // Bare "wireless" reports AND toggles, like `silent` and `sneak` --
45
+ // but only after the report, because a player who typed it to LOOK
46
+ // should not discover they have thrown the switch. An explicit
47
+ // on/off always wins.
48
+ const dark = radioIsDark(actor);
49
+ const wanted = head === 'on' ? true
50
+ : head === 'off' ? false
51
+ : head === '' ? dark
52
+ : undefined;
53
+ if (wanted === undefined) {
54
+ return `"wireless on" or "wireless off" -- or "pan off <device>" for just the one.`;
55
+ }
56
+ const changing = devices.filter(i => i.wirelessOn !== wanted);
57
+ if (changing.length === 0) {
58
+ return wanted
59
+ ? `Everything you carry is already on the air.`
60
+ : `Everything you carry is already dark -- no icons, nothing to hack.${hint(` ("wireless on" to come back up.)`)}`;
61
+ }
62
+ // p.421 prices the all-devices form the same as the single one, so
63
+ // this is billed exactly like commands/pan.ts's switch -- which also
64
+ // means it is free OUT of combat, because billAction only charges
65
+ // inside an encounter.
66
+ const bill = billAction(this.scene, actor, 'free', `Wireless ${wanted ? 'On' : 'Off'}`);
67
+ if (bill)
68
+ return bill;
69
+ for (const item of changing)
70
+ item.wirelessOn = wanted;
71
+ if (wanted) {
72
+ actor.performAction('brings its gear back on the air', 'wireless on');
73
+ this.logger.write(`${actor.name} switches ${changing.length} device(s) wireless ON.`);
74
+ this.scene.updateStatus();
75
+ if (this.game?.hasHub)
76
+ void this.game.pushPresenceNow();
77
+ return [
78
+ `{${CALL_COLOR}-fg}${CALL_ICON} ${changing.map(i => i.name).join(', ')} -- back on the air.{/${CALL_COLOR}-fg}`,
79
+ `{white-fg}Wireless bonuses are live again, calls can reach you, and so can anyone who goes looking.${actor.runningSilent ? ` You are still running SILENT, so your icon stays hidden.` : ''}{/white-fg}`,
80
+ ].join(' ');
81
+ }
82
+ // GOING DARK HANGS UP, and this is the verb that should do it --
83
+ // silent.ts used to, on the strength of a claim canon does not make.
84
+ // See utilities/comm-style.ts dropActiveCall for why both sides are
85
+ // cleared here rather than by each caller.
86
+ let hangupNote = '';
87
+ const partner = dropActiveCall(actor);
88
+ if (partner) {
89
+ this.logger.write(`${actor.name} dropped the call with ${partner} by going wireless-off.`);
90
+ hangupNote = ` The call with ${partner} drops mid-word.`;
91
+ }
92
+ actor.performAction('kills its radios', 'wireless off');
93
+ this.logger.write(`${actor.name} switches ${changing.length} device(s) wireless OFF.`);
94
+ this.scene.updateStatus();
95
+ // Dark means OFF the street list too -- beat now, same as silent.
96
+ if (this.game?.hasHub)
97
+ void this.game.pushPresenceNow();
98
+ // NOT {gray-fg} (standing ruling): blessed gray is ANSI bright-black
99
+ // and vanishes on stock dark palettes. White is the neutral.
100
+ return [
101
+ `{white-fg}${CALL_ICON} You thumb every radio you own off: ${changing.map(i => i.name).join(', ')}.${hangupNote}`,
102
+ `WIRELESS OFF: nothing of yours is on the Matrix at all -- no icon to find, nothing to hack (p.421).`,
103
+ `It costs you every wireless bonus: no calls, no AR initiative, no smartlink in the pool.${hint(` A cable still reaches you -- "tap" works on dark gear. "wireless on" to come back up.`)}{/white-fg}`,
104
+ ].join(' ');
105
+ }
106
+ }
107
+ //# sourceMappingURL=wireless.js.map
@@ -1641,5 +1641,33 @@
1641
1641
  // THE HAVEN IS THE ONE EXEMPTION and is labelled as an engine ruling,
1642
1642
  // not canon: a sanctioned host (Room.hostSanctioned, JackPoint) is a
1643
1643
  // members' place, and membership is the mark.
1644
- export const ENGINE_VERSION = '1.88.0';
1644
+ // 1.89.0 (2026-09-17): WIRELESS vs SILENT vs SNEAK (RXPvXqTwEyH9v3Zk7,
1645
+ // Maka: "we need to work out wireless vs silent. Silent should only be
1646
+ // in the matrix, with its meat counterpart of 'sneak'. What turns off
1647
+ // your broadcast should be 'wireless'"). `Player.runningSilent` was
1648
+ // one flag doing two canon jobs: it hid your icon (p.235, right) and
1649
+ // also killed your calls, your street beacon and your commlink's
1650
+ // initiative bonus (p.421's wireless-off, wrong). There was no verb
1651
+ // for the second, so `silent` was the only way to go dark and the HUD
1652
+ // announced it in the meat as "Link SILENT".
1653
+ // NEW VERB `wireless [on|off]` -- p.421 names the all-devices form
1654
+ // explicitly ("as is toggling all of your wireless devices to
1655
+ // 'wireless off'") and prices it a Free Action; it sweeps the same
1656
+ // device list `pan on|off <device>` uses, now shared in
1657
+ // utilities/pan-devices.ts. It takes the hang-up, the wireless
1658
+ // bonuses (smartgun +1, the AR initiative bonus) and both directions
1659
+ // of `call`.
1660
+ // `silent` is running silent and nothing else: a SIMPLE action now
1661
+ // (p.235; it billed nothing before), refused when every radio is
1662
+ // already off, and it keeps AR, calls and every wireless bonus. It
1663
+ // stays legal in the meat -- the book silences "commlink, deck, other
1664
+ // device, or persona" -- and the meat/Matrix pair the reporter asked
1665
+ // for is carried by the VOCABULARY instead: the HUD slot reads
1666
+ // "Icon SILENT" (was "Link SILENT") with a new "WIRELESS OFF" rung
1667
+ // above it, and `sneak` keeps the body.
1668
+ // utilities/comm-style.ts isLinked() split: isReachable() is the CALL
1669
+ // question (a live radio) and isLinked() stays the ICON question (a
1670
+ // live radio AND not silent). PRESENCE gains a sparse `wirelessOff`
1671
+ // beside `linkUp`, so the site can name which dark it is.
1672
+ export const ENGINE_VERSION = '1.89.0';
1645
1673
  //# sourceMappingURL=engine-version.js.map
@@ -126,10 +126,12 @@ import { ProjectCommand, ReturnCommand } from './commands/project.js';
126
126
  import { AssenseCommand } from './commands/assense.js';
127
127
  import { DroneCommand } from './commands/drone.js';
128
128
  import { PanCommand } from './commands/pan.js';
129
+ import { WirelessCommand } from './commands/wireless.js';
129
130
  import { ArCommand, ArosCommand } from './commands/ar.js';
130
131
  // The HUD's AR read (statusLinesFor) -- built there so it asks the same
131
132
  // predicate the overlay itself does; see utilities/ar.ts for why.
132
133
  import { arStatusSlots } from './utilities/ar.js';
134
+ import { radioIsDark } from './utilities/pan-devices.js';
133
135
  import { RebootCommand } from './commands/reboot.js';
134
136
  import { BrandishCommand, HolsterCommand } from './commands/brandish.js';
135
137
  import { AdvanceCommand } from './commands/advance.js';
@@ -861,8 +863,18 @@ export default class Game {
861
863
  slug: saveSlug(this.player.name),
862
864
  hubName: this._hubName,
863
865
  // ONLINE means the LINK is live (player ruling): commlink equipped
864
- // and broadcasting. Pull the link or run silent -> off the street.
865
- linkUp: this.player.liveLink() != null && !this.player.runningSilent,
866
+ // and FINDABLE. Pull the link, kill the radio or run silent -> off
867
+ // the street. Two states answer it now (RXPvXqTwEyH9v3Zk7), and
868
+ // `linkUp` deliberately keeps meaning the union of them: the
869
+ // street list is "who can be found on the grid", and both a silent
870
+ // icon and a dead radio fail that for different reasons.
871
+ linkUp: this.player.liveLink() != null
872
+ && !this.player.runningSilent
873
+ && !radioIsDark(this.player),
874
+ // WHICH DARK IT IS, so the site can say so instead of guessing.
875
+ // Sparse on purpose: absent means "this client did not report",
876
+ // which is what every build before this one is.
877
+ wirelessOff: radioIsDark(this.player) ? true : undefined,
866
878
  summary: {
867
879
  archetypeName: this.player.archetypeName,
868
880
  karma: this.player.careerKarma,
@@ -2948,6 +2960,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2948
2960
  CommandFactory.registerCommand('assense', AssenseCommand);
2949
2961
  CommandFactory.registerCommand('drone', DroneCommand);
2950
2962
  CommandFactory.registerCommand('pan', PanCommand);
2963
+ CommandFactory.registerCommand('wireless', WirelessCommand);
2951
2964
  // THE AR LAYER (commands/ar.ts): `ar` mutes your own overlay,
2952
2965
  // `aros` reads what it is showing you -- player requests
2953
2966
  // 2026-08-26, "we need AROs on anything online" and "there
@@ -952,8 +952,13 @@ Available in-game commands (use with [COMMAND] prefix):
952
952
  - talk-to <target> — initiate conversation with another character
953
953
  - call <target> [message] — call someone remotely over a commlink (works even if they're not in your room; requires having a commlink equipped, and the target having one too)
954
954
  - end-call — hang up your current commlink call
955
- - silent — toggle your commlink broadcast. While running silent, you can't place
956
- calls and nobody can call YOU. Toggle again to go loud.
955
+ - silent — toggle running silent. Your radio stays ON and your calls still work; what
956
+ changes is that your Matrix icon stops being findable, and every Matrix action you
957
+ take costs 2 dice. Nobody can call YOU while silent — they have nothing to point at.
958
+ Toggle again to go loud.
959
+ - wireless [on|off] — switch every device you carry on or off the air at once. Wireless
960
+ OFF is the real blackout: no calls either way, no wireless bonuses on any of your
961
+ gear, and nothing of yours on the Matrix at all to find or hack.
957
962
  - read <target> - Reads an item of Category "Book"
958
963
  - attack <target> — attack someone in your room. Resolved as opposed dice-pool combat
959
964
  (your weapon and armor matter); an initiative roll decides who strikes first each
@@ -3695,13 +3695,19 @@ export class Player extends AbstractPlayer {
3695
3695
  }
3696
3696
  // The live link, not just the worn one: a deck drives AR too (p.223).
3697
3697
  const link = this.liveLink();
3698
- // AR needs a link that's on you, broadcasting, and not hacked dark.
3699
- // An ENGINE ADDITION, not canon: p.474 gives AR a bare Intuition +
3700
- // Reaction. It predates this change and is left alone deliberately,
3701
- // because it is the counterweight that makes running silent cost
3702
- // something -- but it is named here so nobody later "verifies" it
3703
- // against the book and finds it missing.
3704
- const arBonus = link && !this.runningSilent && !this.arOffline ? link.qualityBonus : 0;
3698
+ // AR needs a link that's on you, with its radio ON, and not hacked
3699
+ // dark. An ENGINE ADDITION, not canon: p.474 gives AR a bare
3700
+ // Intuition + Reaction, and it is named here so nobody later
3701
+ // "verifies" it against the book and finds it missing.
3702
+ //
3703
+ // IT HANGS OFF THE RADIO, NOT OFF SILENCE (RXPvXqTwEyH9v3Zk7). This
3704
+ // asked `!this.runningSilent`, described as "the counterweight that
3705
+ // makes running silent cost something" -- but p.235 already prices
3706
+ // silence at -2 dice on every Matrix action and is explicit that it
3707
+ // costs you no wireless bonuses. A wireless bonus is exactly what
3708
+ // this is, so it belongs to the state that p.421 says takes them:
3709
+ // wireless off. The counterweight moves rather than disappears.
3710
+ const arBonus = link && link.wirelessOn && !this.arOffline ? link.qualityBonus : 0;
3705
3711
  // INCREASE REFLEXES (p.288) adds +1 Initiative per hit AND a die per
3706
3712
  // two hits, capped at +5D6. This engine models the spell as a
3707
3713
  // BOOLEAN -- cast.ts stores no hit count -- so the exact canon grant
@@ -3,6 +3,7 @@ import { isWatched, camerasLive, canSnoopFeeds, camerasBroadcast } from './surve
3
3
  import { Category } from '../types/shared/item-enum.js';
4
4
  import { hint } from './hints.js';
5
5
  import { cameraDrTag, drTagFor } from './grid-view.js';
6
+ import { radioIsDark } from './pan-devices.js';
6
7
  /**
7
8
  * AROs -- AUGMENTED REALITY OBJECTS (player request 2026-08-26: "we
8
9
  * need AROs on anything online"). The 2075 street is not bare: anything
@@ -62,7 +63,7 @@ export function arSightUp(actor) {
62
63
  * The mechanic is settled and is NOT in question here.
63
64
  *
64
65
  * That they had to ask is the bug. The HUD does show both axes at once
65
- * ("◉ AR ON | Link SILENT"), and `silent` says so when you toggle it --
66
+ * ("◉ AR ON | Icon SILENT"), and `silent` says so when you toggle it --
66
67
  * but a player who went dark twenty minutes ago has nothing in front of
67
68
  * them affirming that the combination is legal rather than a state they
68
69
  * have got themselves stuck in. Two independent axes look like a
@@ -227,12 +228,13 @@ export function aroTags(scene, actor, room) {
227
228
  * OVERLAY -- is the world being painted FOR me? Muting is my own
228
229
  * choice ("ar" flips it back); a hacker cutting it is not
229
230
  * ("reboot" is the only cure).
230
- * LINK -- is my commlink BROADCASTING? That is what calls ride
231
- * (call.ts wants a worn commlink) and what silence kills.
231
+ * LINK -- is my radio on the air, and is my icon findable on it?
232
+ * That is what calls ride (call.ts wants a live link) and what
233
+ * `wireless` and `silent` answer, in that order.
232
234
  *
233
235
  * They are genuinely independent -- canon p.229 vs p.235-236: AR is
234
236
  * what you RECEIVE, silence is what you EMIT -- so a silent runner
235
- * reads "AR ON | Link SILENT", both true at once. One slot could never
237
+ * reads "AR ON | Icon SILENT", both true at once. One slot could never
236
238
  * say that, which was the whole complaint.
237
239
  *
238
240
  * The overlay slot asks arSightUp() rather than re-deriving state, so
@@ -275,13 +277,34 @@ export function arStatusSlots(actor) {
275
277
  // performs all commlink functions). The old "AR ON | Link offline"
276
278
  // deck-only reading was the honest display of a canon deviation; the
277
279
  // mechanics moved, and the display moves with them.
278
- const link = actor.liveLink() == null
279
- ? `{white-fg}Link offline{/white-fg}`
280
- : actor.arOffline
281
- ? `{red-fg}Link CRACKED -- reboot{/red-fg}`
282
- : actor.runningSilent
283
- ? `{magenta-fg}Link SILENT{/magenta-fg}`
284
- : `{${CALL_COLOR}-fg}Link online{/${CALL_COLOR}-fg}`;
280
+ //
281
+ // FIVE RUNGS, AND THE TOP TWO ARE THE POINT (RXPvXqTwEyH9v3Zk7, Maka:
282
+ // "we need to work out wireless vs silent"). This slot used to say
283
+ // "Link SILENT", in the meat, for a state that is about your ICON on
284
+ // the Matrix -- so the only word the game gave a runner for "I am
285
+ // hidden" was attached to the wrong noun and the wrong plane, and
286
+ // there was no rung at all for the state that genuinely kills your
287
+ // broadcast. Now there are both, and they read as different things:
288
+ //
289
+ // WIRELESS OFF -- p.421. No radio on at all: no icon, no bonuses,
290
+ // no calls. Your own choice; "wireless on" undoes it.
291
+ // Icon SILENT -- p.235. Radio ON, icon hidden, -2 dice. Says ICON
292
+ // because that is precisely what is hidden -- the
293
+ // link itself is still broadcasting.
294
+ //
295
+ // RADIO-OFF LEADS, ahead even of a cracked PAN: "every radio I own is
296
+ // switched off" is both the stronger statement and the only one that
297
+ // names its own cure, and a hacker has nothing on the air to crack
298
+ // anyway. Below it the existing order is untouched.
299
+ const link = radioIsDark(actor)
300
+ ? `{white-fg}WIRELESS OFF{/white-fg}`
301
+ : actor.liveLink() == null
302
+ ? `{white-fg}Link offline{/white-fg}`
303
+ : actor.arOffline
304
+ ? `{red-fg}Link CRACKED -- reboot{/red-fg}`
305
+ : actor.runningSilent
306
+ ? `{magenta-fg}Icon SILENT{/magenta-fg}`
307
+ : `{${CALL_COLOR}-fg}Link online{/${CALL_COLOR}-fg}`;
285
308
  return { overlay, link };
286
309
  }
287
310
  /** The tags as the standard cyan block, or '' when there is nothing to
@@ -18,26 +18,45 @@ export const CALL_COLOR = 'cyan';
18
18
  * any typed message from colliding with it. */
19
19
  export const END_CALL_SENTINEL = 'end-call';
20
20
  /**
21
- * Whether an actor is REACHABLE over the grid: a commlink carried or
22
- * worn, and the link actually broadcasting. Same shape as call.ts's own
23
- * reachability checks -- shared here so rosters (contacts.ts) can show
24
- * who's a call away without re-deriving it.
21
+ * IS THERE A RADIO ON THIS PERSON -- a commlink carried or worn with its
22
+ * wireless switch ON. This is the CALL question: can a voice get to
23
+ * them, and can theirs get out.
24
+ *
25
+ * SILENCE IS NOT AN ANSWER TO IT (RXPvXqTwEyH9v3Zk7, and p.235 in one
26
+ * clause: running silent "reduces your traffic to and from the Matrix,
27
+ * but it doesn't stop it entirely"). This predicate and isLinked() below
28
+ * used to be one function that returned false for BOTH a dead radio and
29
+ * a silent one, which is how `silent` came to mean "go dark" -- the
30
+ * thing the reporter asked us to untangle. Losing your traffic is what
31
+ * `wireless off` buys (p.421); silence only hides the icon.
32
+ *
33
+ * A LINK YOU SWITCHED OFF IS NOT A LINK (p.233, `pan off <device>`;
34
+ * CgJ6uTfEvTd8JXdDN). Undefined counts as ON, so every caller that
35
+ * passes a plain shape (NPC fixtures, saves written before the switch)
36
+ * is unchanged.
25
37
  */
26
- export function isLinked(actor) {
27
- if (actor.runningSilent)
28
- return false;
29
- // A LINK YOU SWITCHED OFF IS NOT A LINK (p.233, `pan off <device>`;
30
- // CgJ6uTfEvTd8JXdDN). This used to ask only whether a commlink was
31
- // present, so a runner who deliberately took theirs off the air still
32
- // took calls and still hung a PAN icon on the grid for anyone to
33
- // find -- the switch would have been a lie everywhere it mattered.
34
- // Undefined counts as ON, so every caller that passes a plain shape
35
- // (NPC fixtures, saves written before the switch) is unchanged.
38
+ export function isReachable(actor) {
36
39
  const worn = actor.equipment.head?.commlink;
37
40
  const wornOn = worn != null && worn.wirelessOn !== false;
38
41
  const carried = actor.inventory.getAllItems().some(i => i.category === 'commlink' && i.wirelessOn !== false);
39
42
  return wornOn || carried;
40
43
  }
44
+ /**
45
+ * DOES THIS PERSON HANG AN ICON anyone can see -- a live radio AND not
46
+ * running silent. This is the VISIBILITY question, and it is what the
47
+ * AR overlay (ar.ts), the grid's PAN icons (grid-view.ts) and the Matrix
48
+ * roster all ask: a silent icon is correctly absent from every one of
49
+ * them until somebody wins the opposed Matrix Perception test for it
50
+ * (p.235-236, utilities/silent-icons.ts).
51
+ *
52
+ * Keeps the name because every existing caller wanted exactly this; the
53
+ * half that was wrong split off into isReachable() above.
54
+ */
55
+ export function isLinked(actor) {
56
+ if (actor.runningSilent)
57
+ return false;
58
+ return isReachable(actor);
59
+ }
41
60
  /**
42
61
  * DROP A LIVE CALL, BOTH SIDES, and say who was on it.
43
62
  *
@@ -0,0 +1,54 @@
1
+ import { Player } from '../models/player.js';
2
+ /**
3
+ * WHAT THE WIRELESS SWITCH CAN REACH ON ONE RUNNER.
4
+ *
5
+ * Lifted out of commands/pan.ts (RXPvXqTwEyH9v3Zk7) when `wireless`
6
+ * became a second caller. SR5 p.421 prices the two forms side by side --
7
+ * "Toggling an individual device's wireless functionality off is a Free
8
+ * Action, as is toggling all of your wireless devices to 'wireless off'"
9
+ * -- so they are one switch at two scales, and they must agree about
10
+ * which devices exist. Two hand-built lists would eventually differ, and
11
+ * the failure would be silent: `wireless off` leaving a smartgun quietly
12
+ * broadcasting because only `pan` knew to look in the hand.
13
+ */
14
+ /**
15
+ * Everything on this runner the switch can reach, held and worn gear
16
+ * INCLUDED. Equipping moves an item out of `inventory.getAllItems()`,
17
+ * so a list built from inventory alone loses the gun in your hand --
18
+ * which is the one device a player is most likely to mute. The lookup,
19
+ * the "switched off" list and the whole-PAN sweep all read this, so a
20
+ * device you can turn off is always a device you can see is off.
21
+ */
22
+ export function reachableDevices(actor) {
23
+ const seen = new Set();
24
+ return [
25
+ actor.equipment.head?.commlink,
26
+ actor.equipment.head?.eyes,
27
+ actor.equipment.rightHand?.weapon,
28
+ actor.equipment.leftHand?.weapon,
29
+ ...actor.inventory.getAllItems(),
30
+ ].filter((i) => {
31
+ if (i == null || seen.has(i))
32
+ return false;
33
+ seen.add(i);
34
+ return true;
35
+ });
36
+ }
37
+ /** Of those, the ones a PAN can actually hold (p.233) -- what the
38
+ * whole-PAN switch acts on, and the only things `wireless` counts. */
39
+ export function switchableDevices(actor) {
40
+ return reachableDevices(actor).filter(i => Player.canJoinPan(i));
41
+ }
42
+ /**
43
+ * EVERY RADIO THIS RUNNER OWNS IS OFF.
44
+ *
45
+ * Deliberately false for someone carrying nothing: owning no devices is
46
+ * "the monk's build", a different fact with a different HUD slot
47
+ * ("Link offline"), and telling a barehanded runner their wireless is
48
+ * off would name a switch they have never touched.
49
+ */
50
+ export function radioIsDark(actor) {
51
+ const devices = switchableDevices(actor);
52
+ return devices.length > 0 && devices.every(i => !i.wirelessOn);
53
+ }
54
+ //# sourceMappingURL=pan-devices.js.map
@@ -25,7 +25,7 @@ export const HELP_CATEGORIES = [
25
25
  // players" / "crew street"); the bare verb still answers quietly.
26
26
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
27
27
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
28
- { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
28
+ { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['wireless'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
29
29
  // The Emerged get their own shelf (player request: "there MUST be
30
30
  // resonance" -- matrix is the place, Resonance is the talent).
31
31
  // `sustain` shelves here and not under magic: the command is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.239.0",
3
+ "version": "5.240.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.",