@maka/maka-cli 5.238.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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +23 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +33 -31
- package/bundle/typescript/src/commands/game/sideQuest/commands/players.js +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +80 -37
- package/bundle/typescript/src/commands/game/sideQuest/commands/sneak.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +4 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/wireless.js +107 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +56 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +15 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +13 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +34 -11
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js +33 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +12 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/pan-devices.js +54 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
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
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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,
|
|
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 ?
|
|
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 ?
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { movePersonaWith } from '../utilities/matrix-roster.js';
|
|
3
|
-
import { resolveHostInReach, hostsInReach, gridVicinity, hostOver } from '../utilities/grid-reach.js';
|
|
3
|
+
import { resolveHostInReach, hostsInReach, gridVicinity, hostOver, hostDoorOpenFor } from '../utilities/grid-reach.js';
|
|
4
4
|
import { hint } from '../utilities/hints.js';
|
|
5
5
|
import { hostLabel } from '../utilities/grid-names.js';
|
|
6
6
|
import { billAction } from '../utilities/action-cost.js';
|
|
@@ -61,11 +61,21 @@ export class EnterHostCommand extends Command {
|
|
|
61
61
|
// actions to get a mark has bought the door, and charging them a
|
|
62
62
|
// test here would be inventing a rule the book explicitly denies
|
|
63
63
|
// ("There is no test involved").
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
//
|
|
65
|
+
// AND IT IS THE ONLY GATE (DxfeZTBrP6Bu7MLF4). It used to read `||
|
|
66
|
+
// hostCracked`, which is the SITE's state and never expires -- see
|
|
67
|
+
// grid-reach.ts hostDoorOpenFor for why that was a door nobody could
|
|
68
|
+
// ever close again. A persona whose keys are gone gets its own line:
|
|
69
|
+
// the wreckage it can see from out here is real, and saying "doesn't
|
|
70
|
+
// know you" about a node it personally tore open would read as the
|
|
71
|
+
// engine forgetting the last ten minutes.
|
|
72
|
+
if (!hostDoorOpenFor(target, actor)) {
|
|
73
|
+
const wrecked = target.hostCracked;
|
|
66
74
|
return [
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
wrecked
|
|
76
|
+
? `{light-blue-fg}${hostLabel(target, { capital: true })} is still hanging open where you broke it -- and it does not know YOU. Your keys are out of the lattice; the architecture has nothing shaped like your icon to open for.{/light-blue-fg}`
|
|
77
|
+
: `{light-blue-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/light-blue-fg}`,
|
|
78
|
+
hint(`A host opens to anyone holding a MARK on it, and marks die with the session you placed them in (p.239, p.236) -- "hack ${target.name}" keys it quietly, "force ${target.name}" takes one loud.`),
|
|
69
79
|
].join('\n');
|
|
70
80
|
}
|
|
71
81
|
// A Complex Action (p.239) -- billed only inside a Combat Turn.
|
|
@@ -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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { whereIsThat } from '../utilities/matrix-roster.js';
|
|
2
|
-
import { resolveHostInReach, roomsInReach, noiseNote, gridVicinity } from '../utilities/grid-reach.js';
|
|
2
|
+
import { resolveHostInReach, roomsInReach, noiseNote, gridVicinity, hostDoorOpenFor } from '../utilities/grid-reach.js';
|
|
3
3
|
import { perceptionTest } from '../utilities/perception.js';
|
|
4
4
|
import { matchesHostName, matchesCameraName, hostLabel } from '../utilities/grid-names.js';
|
|
5
5
|
import { Door } from '../models/door.js';
|
|
@@ -392,7 +392,7 @@ export class LookCommand extends Command {
|
|
|
392
392
|
return `That file is on the ${room.offlineServer} -- a box with no Matrix presence. Nothing out here reaches it; your body has to be in ${room.name} (p.233).`;
|
|
393
393
|
}
|
|
394
394
|
if (reach === 'enter') {
|
|
395
|
-
return `That file is INSIDE the host -- from out on the grid you have its shell and nothing through it (p.246). ${(room
|
|
395
|
+
return `That file is INSIDE the host -- from out on the grid you have its shell and nothing through it (p.246). ${hostDoorOpenFor(room, this.actor) ? `"enter" to go in.` : `A mark opens the door: "hack ${hostLabel(room)}" (quiet) or "force ${hostLabel(room)}" (loud), then "enter".`}`;
|
|
396
396
|
}
|
|
397
397
|
// The host's seal covers the HOST's archive. A file on an offline
|
|
398
398
|
// server is not in there -- cracking the building's host does
|
|
@@ -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
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
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', '
|
|
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 --
|
|
81
|
-
: `{${CALL_COLOR}-fg}${CALL_ICON} Your
|
|
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', '
|
|
129
|
+
actor.performAction('goes silent', 'icon hidden');
|
|
92
130
|
this.logger.write(`${actor.name} is now running silent.`);
|
|
93
131
|
this.scene.updateStatus();
|
|
94
|
-
//
|
|
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
|
|
109
|
-
: `{white-fg}${CALL_ICON}
|
|
110
|
-
`SILENT:
|
|
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
|
-
? `
|
|
113
|
-
: `Your
|
|
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(`
|
|
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) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { whereIsThat } from '../utilities/matrix-roster.js';
|
|
3
|
-
import { gridVicinity } from '../utilities/grid-reach.js';
|
|
3
|
+
import { gridVicinity, hostDoorOpenFor } from '../utilities/grid-reach.js';
|
|
4
4
|
import { fileReach } from '../utilities/planes.js';
|
|
5
5
|
import { canReach, ensureAtSpot, sealedRouteTo } from '../utilities/spots.js';
|
|
6
6
|
import { hint } from '../utilities/hints.js';
|
|
@@ -143,7 +143,7 @@ export class TakeCommand extends Command {
|
|
|
143
143
|
return `Those files are on the ${room.offlineServer} -- off the network entirely. No persona reaches that box; your BODY has to be in ${room.name} (p.233).`;
|
|
144
144
|
}
|
|
145
145
|
if (reach === 'enter') {
|
|
146
|
-
return `Those files are INSIDE the host, and you're out on the grid looking at its shell (p.246). ${room
|
|
146
|
+
return `Those files are INSIDE the host, and you're out on the grid looking at its shell (p.246). ${hostDoorOpenFor(room, this.actor) ? `"enter" first.` : `Get a mark on it and "enter" -- or "hack" your way in.`}`;
|
|
147
147
|
}
|
|
148
148
|
// Same split as look.ts: the host's seal does not reach a box that
|
|
149
149
|
// is not on the network. The air gap IS the server's security.
|
|
@@ -291,7 +291,7 @@ export class TakeCommand extends Command {
|
|
|
291
291
|
if (far.reach === 'offline')
|
|
292
292
|
return `${far.item.name} is on the ${far.room.offlineServer} in ${far.room.name} -- an offline server, off the network entirely. No persona reaches that box; your BODY has to be in ${far.room.name} (p.233).`;
|
|
293
293
|
if (far.reach === 'host')
|
|
294
|
-
return `${far.item.name} is INSIDE ${far.room.name}'s host, and you're out on the grid looking at its shell (p.246). ${far.room
|
|
294
|
+
return `${far.item.name} is INSIDE ${far.room.name}'s host, and you're out on the grid looking at its shell (p.246). ${hostDoorOpenFor(far.room, this.actor) ? `"enter ${far.room.name}" first.` : `Get a mark on it and "enter" -- or "hack" your way in.`}`;
|
|
295
295
|
if (far.reach === 'open')
|
|
296
296
|
return `${far.item.name} is loose on the grid -- a dim icon. ${hint(`"download ${far.item.name}" writes it to a chip from here.`)}`;
|
|
297
297
|
}
|
|
@@ -337,7 +337,7 @@ export class TakeCommand extends Command {
|
|
|
337
337
|
return `The ${item.name} sits on the ${room.offlineServer} -- a box with no Matrix presence at all. No persona reaches it from anywhere: get your BODY into ${room.name} and cable in (p.233).`;
|
|
338
338
|
}
|
|
339
339
|
if (reach === 'enter') {
|
|
340
|
-
return `The ${item.name} is INSIDE the host -- you're on the outside grid, reading its shell (p.246). ${(room
|
|
340
|
+
return `The ${item.name} is INSIDE the host -- you're on the outside grid, reading its shell (p.246). ${hostDoorOpenFor(room, this.actor) ? `"enter" first.` : `Get a mark on it and "enter" -- or "hack" your way in.`}`;
|
|
341
341
|
}
|
|
342
342
|
// A LOOSE END IS ERASED, NOT CARRIED (win-condition-seed.ts 'erase',
|
|
343
343
|
// 2026-09-14): pulling the footage down to a chip leaves the desk's
|
|
@@ -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
|
|
@@ -1614,5 +1614,60 @@
|
|
|
1614
1614
|
// names the cable. The AR overlay deliberately says NOTHING: that
|
|
1615
1615
|
// surface annotates broadcasts, which is its stated contract, and the
|
|
1616
1616
|
// first cut of this violated it.
|
|
1617
|
-
|
|
1617
|
+
// 1.88.0 (2026-09-17): THE DOOR INTO A HOST IS A MARK AND NOTHING ELSE
|
|
1618
|
+
// (DxfeZTBrP6Bu7MLF4, Maka: "I shouldn't be able to re-enter the host
|
|
1619
|
+
// if I have no marks on it"). `enter` accepted Room.hostCracked as a
|
|
1620
|
+
// substitute for the key. That flag is ROOM-GLOBAL and permanent for
|
|
1621
|
+
// the save -- once anybody took a node to three marks its door never
|
|
1622
|
+
// shut again, for anybody. The reporter's own transcript walks it:
|
|
1623
|
+
// three marks forced into the Approach host, cracked, JACK OUT (canon
|
|
1624
|
+
// deletes every mark they placed, p.242), jack back in reading MARKS
|
|
1625
|
+
// 0 held -- and straight back inside with the Patrol locking on.
|
|
1626
|
+
// Canon is one sentence (p.239, RAG-checked 2026-09-17): "a host
|
|
1627
|
+
// allows anyone to enter if they've got a mark", marks "only last a
|
|
1628
|
+
// single Matrix session" (p.236), three of them buy nothing that
|
|
1629
|
+
// outlives them, and a host's owner cannot be changed (p.237). This
|
|
1630
|
+
// is 1.56.0's ruling (spcebZTKN24GYRHXW, a cracked host is not a host
|
|
1631
|
+
// you hold marks on) finally applied to the one verb the flag was
|
|
1632
|
+
// actually a door for. The crack's WORLD consequences are untouched:
|
|
1633
|
+
// the vault still hangs open, the archive still reads unsealed, the
|
|
1634
|
+
// maglocks it released stay released -- those are facts about the
|
|
1635
|
+
// site, and the key is a fact about you.
|
|
1636
|
+
// ONE GATE, ONE ANSWER: grid-reach.ts hostDoorOpenFor is now the only
|
|
1637
|
+
// place that decides, and every surface that says "enter" asks it --
|
|
1638
|
+
// the verb, look/take's file refusals, and the grid icon, which used
|
|
1639
|
+
// to print `("enter" walks in)` under a crack and would otherwise have
|
|
1640
|
+
// become an invitation the verb refuses.
|
|
1641
|
+
// THE HAVEN IS THE ONE EXEMPTION and is labelled as an engine ruling,
|
|
1642
|
+
// not canon: a sanctioned host (Room.hostSanctioned, JackPoint) is a
|
|
1643
|
+
// members' place, and membership is the mark.
|
|
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';
|
|
1618
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
|
|
865
|
-
|
|
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
|
|
956
|
-
|
|
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,
|
|
3699
|
-
// An ENGINE ADDITION, not canon: p.474 gives AR a bare
|
|
3700
|
-
// Reaction
|
|
3701
|
-
//
|
|
3702
|
-
//
|
|
3703
|
-
//
|
|
3704
|
-
|
|
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 |
|
|
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
|
|
231
|
-
* (call.ts wants a
|
|
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 |
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
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
|
*
|
|
@@ -41,6 +41,42 @@ export function hostsInReach(rooms, actor) {
|
|
|
41
41
|
export function hostOver(room) {
|
|
42
42
|
return room?.hasNode ? room : undefined;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* IS THE DOOR OPEN TO THIS PERSONA -- the one answer, so that the verb
|
|
46
|
+
* that crosses and every hint that says "enter" cannot disagree
|
|
47
|
+
* (DxfeZTBrP6Bu7MLF4, Maka: "I shouldn't be able to re-enter the host if
|
|
48
|
+
* I have no marks on it").
|
|
49
|
+
*
|
|
50
|
+
* p.239 is one sentence long: "Marks required 1 ... a host allows anyone
|
|
51
|
+
* to enter if they've got a mark." RAG-checked 2026-09-17, which also
|
|
52
|
+
* settles the other half: marks "only last a single Matrix session and
|
|
53
|
+
* are deleted when you reboot" (p.236), reaching three of them buys
|
|
54
|
+
* nothing that outlives them, and a host's owner cannot be changed
|
|
55
|
+
* (p.237) -- so there is no state in canon that stands in for the key.
|
|
56
|
+
*
|
|
57
|
+
* WHAT USED TO STAND IN FOR IT. `enter` accepted Room.hostCracked, which
|
|
58
|
+
* is ROOM-GLOBAL and permanent for the save: once anybody took the node
|
|
59
|
+
* to three marks, that door never closed again -- not for the decker who
|
|
60
|
+
* jacked out (canon wipes their marks, p.242) and not for a team-mate
|
|
61
|
+
* who had never touched it. That is the same confusion 1.56.0 named for
|
|
62
|
+
* `hack`/`mark` (spcebZTKN24GYRHXW: a cracked host is not a host you
|
|
63
|
+
* hold marks on) with the one verb the flag was actually a door for left
|
|
64
|
+
* out. The crack's WORLD consequences are untouched -- a vault hanging
|
|
65
|
+
* open stays open, its archive stays listed -- because those are facts
|
|
66
|
+
* about the site and this is a fact about your keys.
|
|
67
|
+
*
|
|
68
|
+
* THE HAVEN IS THE ONE EXEMPTION, and it is an engine ruling, not canon:
|
|
69
|
+
* a sanctioned host (utilities/jackpoint.ts, Room.hostSanctioned) is a
|
|
70
|
+
* members' place you belong to, and membership is the mark. Labelled
|
|
71
|
+
* here rather than hidden in a boolean.
|
|
72
|
+
*/
|
|
73
|
+
export function hostDoorOpenFor(room, actor) {
|
|
74
|
+
if (!room.hasNode)
|
|
75
|
+
return false;
|
|
76
|
+
if (room.hostSanctioned)
|
|
77
|
+
return true;
|
|
78
|
+
return (room.hostMarksBy.get(actor.name) ?? 0) >= 1;
|
|
79
|
+
}
|
|
44
80
|
/**
|
|
45
81
|
* A host by any name the game shows for it (grid-names.ts), from
|
|
46
82
|
* anywhere on the grid. Bare "host" (or nothing) means the one over the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sameHostSide } from '../models/player.js';
|
|
2
2
|
import { personasInside } from './matrix-roster.js';
|
|
3
|
-
import { hostsInReach, hostLine, roomsInReach, gridVicinity } from './grid-reach.js';
|
|
3
|
+
import { hostsInReach, hostLine, roomsInReach, gridVicinity, hostDoorOpenFor } from './grid-reach.js';
|
|
4
4
|
import { NPC } from '../models/npc.js';
|
|
5
5
|
import { hostLabel, hostPurposeFor } from './grid-names.js';
|
|
6
6
|
import { isLinked } from './comm-style.js';
|
|
@@ -142,7 +142,17 @@ export function gridIcons(scene, actor, room) {
|
|
|
142
142
|
icons.push(`● ${hostLabel(room, { capital: true })} -- three spheres turning over rooms that were never sold to anyone. You're on the list; nothing here is hunting.`);
|
|
143
143
|
}
|
|
144
144
|
else if (room.hostCracked) {
|
|
145
|
-
|
|
145
|
+
// THE WRECKAGE IS THE SITE'S; THE DOOR IS STILL YOURS TO HOLD
|
|
146
|
+
// (DxfeZTBrP6Bu7MLF4). This line offered `"enter" walks in` on the
|
|
147
|
+
// room-global crack alone, so a persona whose marks a jack-out had
|
|
148
|
+
// just wiped (p.242) read an invitation the verb then refused --
|
|
149
|
+
// or, before the verb was fixed, accepted, which is the report.
|
|
150
|
+
// Both halves stay true now: the vault hangs open, and the key is
|
|
151
|
+
// per-persona and dies with the session (p.236).
|
|
152
|
+
const way = hostDoorOpenFor(room, actor)
|
|
153
|
+
? `("enter" walks in on your mark)`
|
|
154
|
+
: `("hack ${hostLabel(room)}" or "force ${hostLabel(room)}" for a mark -- yours are gone, and a host still asks for one, p.239)`;
|
|
155
|
+
icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- cracked open, security in shreds. Whatever it guarded is unsealed, inside. ${way}`);
|
|
146
156
|
}
|
|
147
157
|
else {
|
|
148
158
|
const myMarks = room.hostMarksBy.get(actor.name) ?? 0;
|
|
@@ -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.
|
|
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.",
|