@maka/maka-cli 5.227.0 → 5.229.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/disable.js +14 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +12 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +122 -18
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +97 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +18 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +82 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +30 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +5 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +91 -12
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/silent-icons.js +43 -22
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.229.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.",
|
|
@@ -134,7 +134,20 @@ export class DisableCommand extends Command {
|
|
|
134
134
|
return t.viaWan && t.host?.host ? wanDefensePool(t.device, t.host.host) : deviceDefensePool(t.device);
|
|
135
135
|
}
|
|
136
136
|
if (t.kind === 'camera') {
|
|
137
|
-
|
|
137
|
+
// THE LENSES RATE WHAT THE SITE SAYS THEY RATE (Room.cameraRating,
|
|
138
|
+
// eJANPpm7qCaCAJBJw). This used to borrow the host's whole pool
|
|
139
|
+
// while silent-icons.ts hid the same cluster behind a hard-coded 4
|
|
140
|
+
// and the icon line printed no rating at all -- three answers to
|
|
141
|
+
// one question, two of them invisible. A slaved camera still
|
|
142
|
+
// borrows its master where the master is HIGHER (p.233, the same
|
|
143
|
+
// rule wanDefensePool applies to a slaved device), so a corp host
|
|
144
|
+
// still hardens its own cameras; what changed is that the floor is
|
|
145
|
+
// the seeded rating rather than a constant nobody could read.
|
|
146
|
+
const own = { label: 'Device Rating + Firewall', pool: Math.max(0, t.room.cameraRating * 2) };
|
|
147
|
+
if (!t.master?.host)
|
|
148
|
+
return own;
|
|
149
|
+
const viaHost = hostDefensePool(t.master.host);
|
|
150
|
+
return viaHost.pool >= own.pool ? viaHost : own;
|
|
138
151
|
}
|
|
139
152
|
return { label: 'Intuition + Firewall', pool: t.owner.getPanDefensePool('sleaze') };
|
|
140
153
|
}
|
|
@@ -333,7 +333,17 @@ export class LookCommand extends Command {
|
|
|
333
333
|
// in the icon block below rather than one look later.
|
|
334
334
|
const swept = spotHiddenIcons(this.scene, this.actor, this.actor.insideHost === room ? room : gridVicinity(this.actor));
|
|
335
335
|
const lines = [gridSculpt(this.actor, room)];
|
|
336
|
-
|
|
336
|
+
// THE SWEEP IS REPORTED BEFORE THE LIST IT CHANGED
|
|
337
|
+
// (eJANPpm7qCaCAJBJw, third rejection: "I still see the Service
|
|
338
|
+
// Keypad and Camera cluster").
|
|
339
|
+
//
|
|
340
|
+
// The sweep runs first so anything it wins appears in this look
|
|
341
|
+
// rather than one look later -- that part was right. But its
|
|
342
|
+
// SENTENCE was pushed after the icon block, so the player read a
|
|
343
|
+
// newly-revealed keypad with nothing above it saying why, and the
|
|
344
|
+
// "you pick an icon out of the noise" line arrived two blocks down.
|
|
345
|
+
// Revealing something and explaining it in that order reads exactly
|
|
346
|
+
// like never having hidden it, which is what was reported.
|
|
337
347
|
if (swept) {
|
|
338
348
|
lines.push(spotLine(swept));
|
|
339
349
|
if (showsMechanics(this.scene, this.actor)) {
|
|
@@ -342,6 +352,7 @@ export class LookCommand extends Command {
|
|
|
342
352
|
}
|
|
343
353
|
}
|
|
344
354
|
}
|
|
355
|
+
lines.push(gridIconBlock(this.scene, this.actor, room, this.rooms, swept?.found));
|
|
345
356
|
// THE COACHING HAS TO MATCH THE ROOM YOU ARE IN. Inside a host,
|
|
346
357
|
// `go` is refused (go.ts checkHostBoundary) and `map` draws the
|
|
347
358
|
// outside grid you cannot reach -- so offering both is the same
|
|
@@ -1,19 +1,99 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
|
+
import { Player } from '../models/player.js';
|
|
3
4
|
import { Category } from '../types/shared/item-enum.js';
|
|
5
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
4
6
|
/**
|
|
5
7
|
* The PAN dashboard: your personal area network at a glance -- master
|
|
6
8
|
* device and its firewall, every slaved device with what it grants while
|
|
7
9
|
* loud, and the silent-vs-loud tradeoff spelled out. This is the screen
|
|
8
10
|
* you check before deciding whether the AR bonus is worth being hackable.
|
|
11
|
+
*
|
|
12
|
+
* AND THE SWITCH (CgJ6uTfEvTd8JXdDN): "pan off <device>" drops one
|
|
13
|
+
* device off the network and "pan on <device>" puts it back. The
|
|
14
|
+
* reporter asked for `pan link <device>` and `pan link <device> on|off`;
|
|
15
|
+
* `link`/`unlink` are accepted as spellings of the same two verbs, but
|
|
16
|
+
* membership itself is DERIVED rather than hand-built, which is the
|
|
17
|
+
* reporter's own ruling: "most equipment is wireless, so a lot can just
|
|
18
|
+
* be that easy." Nobody has to assemble their network before it works.
|
|
9
19
|
*/
|
|
10
20
|
export class PanCommand extends Command {
|
|
11
21
|
static verb = 'pan';
|
|
12
|
-
static description = 'Your personal area network: master device, slaved gear, wireless bonuses, and what running silent costs and protects.';
|
|
13
|
-
async execute(
|
|
22
|
+
static description = 'Your personal area network: master device, slaved gear, wireless bonuses, and what running silent costs and protects. "pan off <device>" drops one off the net.';
|
|
23
|
+
async execute(args = []) {
|
|
24
|
+
const [head, ...rest] = args;
|
|
25
|
+
const verb = (head ?? '').toLowerCase();
|
|
26
|
+
// "pan link <device> off" (the reporter's spelling) and "pan off
|
|
27
|
+
// <device>" mean the same thing. A trailing on/off wins over the
|
|
28
|
+
// leading verb, so `pan link X off` unlinks rather than linking.
|
|
29
|
+
if (verb === 'on' || verb === 'off' || verb === 'link' || verb === 'unlink') {
|
|
30
|
+
const tail = (rest[rest.length - 1] ?? '').toLowerCase();
|
|
31
|
+
const trailing = tail === 'on' || tail === 'off' ? tail : undefined;
|
|
32
|
+
const nameParts = trailing ? rest.slice(0, -1) : rest;
|
|
33
|
+
const wanted = trailing ?? (verb === 'on' || verb === 'link' ? 'on' : 'off');
|
|
34
|
+
return this.switchDevice(nameParts.join(' ').trim(), wanted === 'on');
|
|
35
|
+
}
|
|
36
|
+
return this.dashboard();
|
|
37
|
+
}
|
|
38
|
+
/** One device on or off the network (p.233 -- slaving is a choice). */
|
|
39
|
+
switchDevice(name, on) {
|
|
40
|
+
const a = this.actor;
|
|
41
|
+
if (!name)
|
|
42
|
+
return `${on ? 'Link' : 'Unlink'} what? Try "pan ${on ? 'on' : 'off'} <device>".`;
|
|
43
|
+
const candidates = [
|
|
44
|
+
a.equipment.head?.commlink,
|
|
45
|
+
a.equipment.rightHand?.weapon,
|
|
46
|
+
a.equipment.leftHand?.weapon,
|
|
47
|
+
...a.inventory.getAllItems(),
|
|
48
|
+
].filter((i) => i != null);
|
|
49
|
+
const needle = name.toLowerCase();
|
|
50
|
+
const item = candidates.find(i => i.name.toLowerCase() === needle)
|
|
51
|
+
?? candidates.find(i => i.name.toLowerCase().includes(needle));
|
|
52
|
+
if (!item)
|
|
53
|
+
return `You are not carrying "${name}".`;
|
|
54
|
+
if (!Player.canJoinPan(item)) {
|
|
55
|
+
// p.233 is explicit that only DEVICES can be in a PAN, so this is
|
|
56
|
+
// a refusal with a reason rather than a silent no-op.
|
|
57
|
+
return `${item.name} is not a device -- only devices can be slaved to a PAN (p.233). Nothing to switch.`;
|
|
58
|
+
}
|
|
59
|
+
if (item.wirelessOn === on) {
|
|
60
|
+
return `${item.name} is already wireless ${on ? 'ON' : 'OFF'}.`;
|
|
61
|
+
}
|
|
62
|
+
// p.421, "Turning It Off": "Toggling an individual device's wireless
|
|
63
|
+
// functionality off is a Free Action." Priced where the book prices
|
|
64
|
+
// it -- which also means it is free OUT of combat, because
|
|
65
|
+
// billAction only charges inside an encounter.
|
|
66
|
+
const bill = billAction(this.scene, a, 'free', `Wireless ${on ? 'On' : 'Off'}`);
|
|
67
|
+
if (bill)
|
|
68
|
+
return bill;
|
|
69
|
+
item.wirelessOn = on;
|
|
70
|
+
if (!on) {
|
|
71
|
+
return [
|
|
72
|
+
`${item.name} drops off the network -- wireless OFF.`,
|
|
73
|
+
`It broadcasts nothing now: no bonus, no icon, nothing to hack. It also stops borrowing your master's ratings to defend itself (p.233), which is only a loss if anyone was looking.`,
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
const over = a.panOverflow();
|
|
77
|
+
const lines = [`${item.name} joins the network -- wireless ON.`];
|
|
78
|
+
if (over > 0) {
|
|
79
|
+
// Canon ceiling, said out loud rather than truncating in silence.
|
|
80
|
+
lines.push(`Your master handles ${a.panCapacity()} slaved devices (Device Rating x 3, p.233) and you are carrying ${over} too many. The overflow is not on the net -- switch something off.`);
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
dashboard() {
|
|
14
85
|
const a = this.actor;
|
|
15
86
|
const master = a.getPanMaster();
|
|
16
87
|
if (!master) {
|
|
88
|
+
const off = a.inventory.getAllItems().filter(i => !i.wirelessOn && Player.canJoinPan(i));
|
|
89
|
+
if (off.length > 0) {
|
|
90
|
+
// The honest reason, because "no PAN" reads like a bug when you
|
|
91
|
+
// are holding a commlink you switched off yourself.
|
|
92
|
+
return [
|
|
93
|
+
`No PAN -- everything that could master one is switched off.`,
|
|
94
|
+
...off.map(i => ` • ${i.name} -- wireless OFF${hint(` ("pan on ${i.name}")`)}`),
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
17
97
|
return `No devices, no PAN. You are unhackable and unreachable -- the monk's build.`;
|
|
18
98
|
}
|
|
19
99
|
const state = a.runningSilent
|
|
@@ -21,11 +101,12 @@ export class PanCommand extends Command {
|
|
|
21
101
|
: a.arOffline
|
|
22
102
|
? `{red-fg}LOUD, AR CUT{/red-fg} -- someone hacked your overlay dark ("reboot")`
|
|
23
103
|
: `{green-fg}LOUD{/green-fg} -- full bonuses, and hackable`;
|
|
104
|
+
const slaves = a.panSlaves();
|
|
24
105
|
const lines = [
|
|
25
106
|
`PAN: ${a.name.toUpperCase()}`,
|
|
26
107
|
'',
|
|
27
108
|
`Broadcast: ${state}`,
|
|
28
|
-
`Master: ${master.name}`,
|
|
109
|
+
`Master: ${master.name} [DR ${master.deviceRating}] -- holds ${slaves.length}/${a.panCapacity()} slaved devices (DR x 3, p.233)`,
|
|
29
110
|
// TWO DEFENCES, BECAUSE THERE ARE TWO (p.240 / p.238). What the
|
|
30
111
|
// intruder is DOING picks the attribute, so a quiet crack and a
|
|
31
112
|
// smash meet different pools -- for most characters, different
|
|
@@ -42,24 +123,26 @@ export class PanCommand extends Command {
|
|
|
42
123
|
'',
|
|
43
124
|
'Slaved devices:',
|
|
44
125
|
];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const bonus = a.runningSilent || a.arOffline ? 'inactive' : `+${link.qualityBonus} initiative (AR overlay)`;
|
|
48
|
-
lines.push(` • ${link.name} (commlink) -- calls; ${bonus}`);
|
|
126
|
+
if (slaves.length === 0) {
|
|
127
|
+
lines.push(' (none -- the master is the whole network)');
|
|
49
128
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
lines.push(` • ${deck.name} (cyberdeck) -- Matrix access; ${deck.deckSummary()}${deck.isBricked ? ' BRICKED (rest to repair)' : ''}`);
|
|
129
|
+
for (const item of slaves) {
|
|
130
|
+
lines.push(` • ${this.slaveLine(item)}`);
|
|
53
131
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
lines.push(` • ${gun.name} (smartgun) -- ${gun.jammed ? '{red-fg}JAMMED -- smartlink cracked ("reboot"){/red-fg}' : 'smartlink live'}`);
|
|
132
|
+
const over = a.panOverflow();
|
|
133
|
+
if (over > 0) {
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push(`{red-fg}${over} device${over === 1 ? '' : 's'} over capacity{/red-fg} -- a master handles only (Device Rating x 3) slaves (p.233). The excess is off the net.`);
|
|
59
136
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
137
|
+
// Anything deliberately switched off is listed too: a device you
|
|
138
|
+
// cannot see on this screen is a device you will forget you muted.
|
|
139
|
+
const off = [...a.inventory.getAllItems()].filter(i => !i.wirelessOn && Player.canJoinPan(i));
|
|
140
|
+
if (off.length > 0) {
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push('Switched off:');
|
|
143
|
+
for (const item of off) {
|
|
144
|
+
lines.push(` · ${item.name} -- wireless OFF, no icon, nothing to hack${hint(` ("pan on ${item.name}")`)}`);
|
|
145
|
+
}
|
|
63
146
|
}
|
|
64
147
|
lines.push('');
|
|
65
148
|
lines.push(a.runningSilent
|
|
@@ -67,5 +150,26 @@ export class PanCommand extends Command {
|
|
|
67
150
|
: `Running loud: any decker in reach can try your firewall -- jammed guns and dead AR are how that feels.${hint(' "silent" goes dark.')}`);
|
|
68
151
|
return lines.join('\n');
|
|
69
152
|
}
|
|
153
|
+
/** What one slaved device is and what being on the net buys it. */
|
|
154
|
+
slaveLine(item) {
|
|
155
|
+
const a = this.actor;
|
|
156
|
+
const inactive = a.runningSilent || a.arOffline;
|
|
157
|
+
if (item.isCyberdeck()) {
|
|
158
|
+
return `${item.name} (cyberdeck) -- Matrix access; ${item.deckSummary()}${item.isBricked ? ' BRICKED (rest to repair)' : ''}`;
|
|
159
|
+
}
|
|
160
|
+
if (item.category === Category.Commlink) {
|
|
161
|
+
const bonus = inactive ? 'inactive' : `+${item.qualityBonus} initiative (AR overlay)`;
|
|
162
|
+
return `${item.name} (commlink) -- calls; ${bonus}`;
|
|
163
|
+
}
|
|
164
|
+
// Only a SMARTGUN is on the PAN (p.433; catalog v13): a plain gun has
|
|
165
|
+
// nothing to broadcast and nothing to crack.
|
|
166
|
+
if (item.isFirearm()) {
|
|
167
|
+
return `${item.name} (smartgun) -- ${item.jammed ? '{red-fg}JAMMED -- smartlink cracked ("reboot"){/red-fg}' : 'smartlink live'}`;
|
|
168
|
+
}
|
|
169
|
+
if (item.category === Category.Drone) {
|
|
170
|
+
return `${item.name} (drone) -- recon uplink${hint(' ("drone")')}`;
|
|
171
|
+
}
|
|
172
|
+
return `${item.name} (${item.category})`;
|
|
173
|
+
}
|
|
70
174
|
}
|
|
71
175
|
//# sourceMappingURL=pan.js.map
|
|
@@ -1251,9 +1251,13 @@
|
|
|
1251
1251
|
// 'panel' -- the maglock, the keypad, the safe). A ward is the Art
|
|
1252
1252
|
// and has no wireless mode to hide in.
|
|
1253
1253
|
// - CAMERAS TOO, which p.217 names FIRST. The cluster is not a Device
|
|
1254
|
-
// (it is derived from the room), so it hides on Room.camerasSpottedBy
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1254
|
+
// (it is derived from the room), so it hides on Room.camerasSpottedBy.
|
|
1255
|
+
// (This entry ORIGINALLY said it resisted with its MASTER HOST'S
|
|
1256
|
+
// rating. That was wrong and 1.80.1 removed it the same day -- a
|
|
1257
|
+
// lens behind a rating-8 host was a 16-die hide against a sweep
|
|
1258
|
+
// capped near 5, and it measured ZERO finds in twelve looks. It
|
|
1259
|
+
// resists with its own rating; see 1.82.0 for where that number
|
|
1260
|
+
// now lives.)
|
|
1257
1261
|
// - AND PEOPLE. Going dark used to make an icon simply unfindable:
|
|
1258
1262
|
// isLinked answers false and nothing anywhere rolled to beat it, so
|
|
1259
1263
|
// the book's own example of a hidden icon was the one case with no
|
|
@@ -1325,5 +1329,94 @@
|
|
|
1325
1329
|
// hole had been open: they leaned on the old "no gear, stay quiet"
|
|
1326
1330
|
// branch and have been rolling unlimited all along. They carry decks
|
|
1327
1331
|
// now. The VR gate (a direct neural interface, p.222-223) is untouched.
|
|
1328
|
-
|
|
1332
|
+
// 1.82.0 (2026-09-16): WHAT MATRIX PERCEPTION BUYS MUST BE EARNED
|
|
1333
|
+
// (eJANPpm7qCaCAJBJw, third rejection). 1.80.0 hid the ICON and stopped
|
|
1334
|
+
// there. The reporter read the transcript and found the rest:
|
|
1335
|
+
// "I shouldn't even see 'Service Keypad hides' at all if I don't
|
|
1336
|
+
// clear the firewall."
|
|
1337
|
+
// They were right, and the leak was in the DICE, not the prose. The
|
|
1338
|
+
// narrative line was correctly vague -- "something here is running
|
|
1339
|
+
// silent and you cannot pin it" -- and then the mechanics pane named
|
|
1340
|
+
// the thing on the very next line, win or lose:
|
|
1341
|
+
// Service Keypad hides (Device Rating (no Logic to hide behind)): 6d6 -> 2 hits
|
|
1342
|
+
// A FAILED SWEEP TAUGHT YOU MORE THAN A SUCCESSFUL ONE WOULD HAVE.
|
|
1343
|
+
// - A LOST SWEEP NAMES NOTHING. ISpotOutcome.missed is now a COUNT,
|
|
1344
|
+
// not a list of names, and the losing branch of the loop never puts
|
|
1345
|
+
// a name in the outcome at all. This is the shape of the fix that
|
|
1346
|
+
// matters: the old type carried the names and merely trusted every
|
|
1347
|
+
// caller to remember not to print them. A name that was never earned
|
|
1348
|
+
// cannot now be printed by accident, because it is not there. The
|
|
1349
|
+
// dice still show: "something holds its cover: 6d6 -> 2 hits".
|
|
1350
|
+
// - RATINGS ARE PROTECTED TOO, which is the reporter's other question
|
|
1351
|
+
// answered. p.235 sells "the target's device rating" for a hit, and
|
|
1352
|
+
// the engine printed [DR n] on every icon for free. It is now
|
|
1353
|
+
// [DR ?] until the icon is yours: ratingKnownBy = not silent, or
|
|
1354
|
+
// spotted by you. NOTE THE SMALLER CLAIM -- a never-silent device
|
|
1355
|
+
// shows its rating for nothing. p.235 charges a hit for it even
|
|
1356
|
+
// then; this engine does not, because a loud icon has nothing to
|
|
1357
|
+
// roll against and inventing a resistance for it would be inventing
|
|
1358
|
+
// a rule. Said plainly rather than papered over.
|
|
1359
|
+
// - THE CAMERA CLUSTER HAS A RATING AT LAST, which was the third
|
|
1360
|
+
// complaint. Room.cameraRating, ONE number, feeding all three places
|
|
1361
|
+
// that disagreed: what it hides behind (Device Rating + Firewall),
|
|
1362
|
+
// what it defends Control Device / Data Spike with, and what the
|
|
1363
|
+
// grid prints. It is 1 (p.356 lists Camera at 1; the same list gives
|
|
1364
|
+
// the door lock the 2 the engine already uses). It is a FIELD, so a
|
|
1365
|
+
// corp floor that wants p.421's mil-spec band is a one-line change
|
|
1366
|
+
// in the seed layer, not a rewrite.
|
|
1367
|
+
// - AND THE SWEEP READS AS A SWEEP: the narration moved ABOVE the icon
|
|
1368
|
+
// block (you were reading revealed icons before the sentence that
|
|
1369
|
+
// explained them), and an icon found this look is tagged "<- just
|
|
1370
|
+
// spotted, was running silent". Without the tag, every later look
|
|
1371
|
+
// shows it with no explanation at all -- indistinguishable from an
|
|
1372
|
+
// icon that was never hidden, which is most of why 1.80.0 read to
|
|
1373
|
+
// the reporter as nothing having happened.
|
|
1374
|
+
// 1.83.0 (2026-09-16): THE WIRELESS SWITCH (CgJ6uTfEvTd8JXdDN)
|
|
1375
|
+
// The reporter rejected the last pass and wrote the spec themselves:
|
|
1376
|
+
// "1) new command 'pan' shows you your current connected devices.
|
|
1377
|
+
// 2) 'pan link <device>' ... 3) 'pan link <device> on|off' 4) if no
|
|
1378
|
+
// commlink/deck/control rig is linked and on, and you're a
|
|
1379
|
+
// Technomancer, you default to your persona."
|
|
1380
|
+
// RAG-checked, and it is not a house rule -- it is p.421, "Turning It
|
|
1381
|
+
// Off", almost word for word:
|
|
1382
|
+
// "Toggling an individual device's wireless functionality off is a
|
|
1383
|
+
// Free Action ... YOU LOSE WIRELESS BONUSES, but the items can no
|
|
1384
|
+
// longer be wirelessly hacked."
|
|
1385
|
+
// That sentence is a TRADE, and both halves are now real.
|
|
1386
|
+
// - Item.wirelessOn, default ON. Membership in a PAN stays DERIVED
|
|
1387
|
+
// rather than becoming a list you assemble by hand -- which is the
|
|
1388
|
+
// reporter's own ruling ("most equipment is wireless, so a lot can
|
|
1389
|
+
// just be that easy") and is what keeps every existing character's
|
|
1390
|
+
// network exactly as it was. The switch is the override, not a
|
|
1391
|
+
// chore. Saved SPARSE and INVERTED (`wirelessOff`), so a save
|
|
1392
|
+
// written before today has no key and comes back on the air.
|
|
1393
|
+
// - `pan off <device>` / `pan on <device>`, with the reporter's own
|
|
1394
|
+
// `pan link <device> [on|off]` spelling accepted for the same two
|
|
1395
|
+
// verbs. Billed as p.421's FREE ACTION, which also means free
|
|
1396
|
+
// outside a fight, since billAction only charges in an encounter.
|
|
1397
|
+
// - THE "NO LONGER HACKED" HALF IS REAL, which is the half a switch
|
|
1398
|
+
// like this usually fakes: a muted commlink stops making you
|
|
1399
|
+
// isLinked (no calls, and no PAN icon for a sweep to find), a muted
|
|
1400
|
+
// gun has no grid icon for `hack` to seize, and a muted deck cannot
|
|
1401
|
+
// master a PAN.
|
|
1402
|
+
// - AND THE "LOSE WIRELESS BONUSES" HALF, split exactly where p.421
|
|
1403
|
+
// and p.433 split it: the smartgun's wireless +1 DICE goes, and its
|
|
1404
|
+
// +2 ACCURACY STAYS -- that one rides a cable to your glasses and
|
|
1405
|
+
// never needed the radio. Same discipline as the Matrix brackets: a
|
|
1406
|
+
// limit is never a die.
|
|
1407
|
+
// - Player.panSlaves/panCapacity/panOverflow, so the dashboard, the
|
|
1408
|
+
// capacity rule and "is this on the net?" all read ONE list. p.233
|
|
1409
|
+
// caps a master at (Device Rating x 3) slaves and the engine had
|
|
1410
|
+
// never counted; over-capacity is now said out loud rather than
|
|
1411
|
+
// silently truncated. This is the same drift that cost us three
|
|
1412
|
+
// disagreeing camera ratings in 1.82.0.
|
|
1413
|
+
// NOT SHIPPED, deliberately, and it is the other half of the item: a
|
|
1414
|
+
// technomancer deliberately RIDING a deck ("Using Mundane
|
|
1415
|
+
// Electronics", p.251). A device persona mutes every Resonance
|
|
1416
|
+
// ability, so that mode is worth nothing until compile/register/
|
|
1417
|
+
// thread and their siblings all know about it -- shipping the switch
|
|
1418
|
+
// alone would be a fresh canon violation wearing a feature's clothes.
|
|
1419
|
+
// A technomancer still comes up on the living persona, which is what
|
|
1420
|
+
// the reporter's point 4 asks for.
|
|
1421
|
+
export const ENGINE_VERSION = '1.83.0';
|
|
1329
1422
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -49,6 +49,24 @@ export class Item extends AbstractItem {
|
|
|
49
49
|
// work (rest.ts). A reboot does NOT clear it; that is the whole
|
|
50
50
|
// difference from `jammed`.
|
|
51
51
|
bricked = false;
|
|
52
|
+
/**
|
|
53
|
+
* THE WIRELESS SWITCH (p.233 PANs and WANs; CgJ6uTfEvTd8JXdDN: "'pan
|
|
54
|
+
* link <device> on|off'"). Slaving is a thing an owner CHOOSES, and
|
|
55
|
+
* the choice has a price both ways: a device on the network borrows
|
|
56
|
+
* its master's ratings to defend itself, and a device off it is not
|
|
57
|
+
* on the Matrix at all -- no wireless bonus, no icon to find, nothing
|
|
58
|
+
* to hack.
|
|
59
|
+
*
|
|
60
|
+
* DEFAULT ON, which is the reporter's own ruling ("most equipment is
|
|
61
|
+
* wireless, so a lot can just be that easy") and is why membership
|
|
62
|
+
* stays DERIVED rather than becoming a list you have to build by
|
|
63
|
+
* hand. Every runner who never touches `pan` keeps exactly the PAN
|
|
64
|
+
* they have today; this switch is the override.
|
|
65
|
+
*
|
|
66
|
+
* Saved SPARSE, as `wirelessOff` -- absent means on, so every save
|
|
67
|
+
* written before this existed restores unchanged.
|
|
68
|
+
*/
|
|
69
|
+
wirelessOn = true;
|
|
52
70
|
/**
|
|
53
71
|
* RUINED (Room.layOutBody, catalog v21 NPC kits): armor that soaked
|
|
54
72
|
* the rounds that killed its wearer. Rated 0 (armorValue, armorBonus)
|
|
@@ -544,10 +544,11 @@ export class Player extends AbstractPlayer {
|
|
|
544
544
|
// across jack-outs, and a full track BRICKS the deck -- forced dump,
|
|
545
545
|
// and no re-entry on that deck until it's repaired.
|
|
546
546
|
activeDeck;
|
|
547
|
-
/** The best working cyberdeck carried (bricked ones don't count
|
|
547
|
+
/** The best working cyberdeck carried (bricked ones don't count, and
|
|
548
|
+
* neither does one you have switched off -- p.233, `pan off`). */
|
|
548
549
|
getCyberdeck() {
|
|
549
550
|
return this._inventory.getAllItems()
|
|
550
|
-
.filter(i => i.isCyberdeck() && !i.isBricked)
|
|
551
|
+
.filter(i => i.isCyberdeck() && !i.isBricked && i.wirelessOn)
|
|
551
552
|
.sort((a, b) => b.qualityBonus - a.qualityBonus)[0];
|
|
552
553
|
}
|
|
553
554
|
/** Any deck at all, bricked included -- for status displays and repair. */
|
|
@@ -960,13 +961,82 @@ export class Player extends AbstractPlayer {
|
|
|
960
961
|
// Worn first, then CARRIED (playtest 2026-08-24: NPCs keep their
|
|
961
962
|
// links in inventory, not the head slot -- a pocketed commlink is
|
|
962
963
|
// still the PAN's master; only running silent hides it).
|
|
963
|
-
|
|
964
|
-
|
|
964
|
+
// A device switched off masters nothing (p.233, `pan off`).
|
|
965
|
+
const worn = this._equipment.head?.commlink;
|
|
966
|
+
const link = (worn?.wirelessOn ? worn : undefined)
|
|
967
|
+
?? this._inventory.getAllItems().find(i => i.category === 'commlink' && i.wirelessOn)
|
|
965
968
|
?? undefined;
|
|
966
969
|
if (deck && link)
|
|
967
970
|
return deck.qualityBonus >= link.qualityBonus ? deck : link;
|
|
968
971
|
return deck ?? link;
|
|
969
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* HOW MANY SLAVES A MASTER CAN HOLD: Device Rating x 3 (p.233, "your
|
|
975
|
+
* commlink or deck can handle up to (Device Rating x 3) slaved
|
|
976
|
+
* devices"). Zero without a master, because a PAN is defined as the
|
|
977
|
+
* slaves PLUS the master -- there is no network to join.
|
|
978
|
+
*/
|
|
979
|
+
panCapacity() {
|
|
980
|
+
const master = this.getPanMaster();
|
|
981
|
+
return master ? master.deviceRating * 3 : 0;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* THE PAN, ITEMISED (CgJ6uTfEvTd8JXdDN: "new command 'pan' shows you
|
|
985
|
+
* your current connected devices"). One list, so the dashboard, the
|
|
986
|
+
* capacity check and anything that asks "is this on the network?"
|
|
987
|
+
* cannot drift apart -- which is the bug pattern the camera cluster
|
|
988
|
+
* just cost us in eJANPpm7qCaCAJBJw.
|
|
989
|
+
*
|
|
990
|
+
* MEMBERSHIP IS DERIVED, not a hand-built list, per the reporter's own
|
|
991
|
+
* ruling: "most equipment is wireless, so a lot can just be that
|
|
992
|
+
* easy." What you carry that can be on a PAN, is -- unless you switch
|
|
993
|
+
* it off. So no existing character's network changes, and `pan off`
|
|
994
|
+
* is the override rather than `pan link` being a chore.
|
|
995
|
+
*
|
|
996
|
+
* Only DEVICES can be slaves (p.233), which is why a plain revolver is
|
|
997
|
+
* not here and a smartgun is: the smartlink is the device.
|
|
998
|
+
*/
|
|
999
|
+
panSlaves() {
|
|
1000
|
+
const master = this.getPanMaster();
|
|
1001
|
+
if (!master)
|
|
1002
|
+
return [];
|
|
1003
|
+
const carried = this._inventory.getAllItems();
|
|
1004
|
+
const worn = [
|
|
1005
|
+
this._equipment.head?.commlink,
|
|
1006
|
+
this._equipment.rightHand?.weapon,
|
|
1007
|
+
this._equipment.leftHand?.weapon,
|
|
1008
|
+
].filter((i) => i !== undefined && i !== null);
|
|
1009
|
+
const seen = new Set([master]);
|
|
1010
|
+
const out = [];
|
|
1011
|
+
for (const item of [...worn, ...carried]) {
|
|
1012
|
+
if (seen.has(item) || !item.wirelessOn)
|
|
1013
|
+
continue;
|
|
1014
|
+
if (!Player.canJoinPan(item))
|
|
1015
|
+
continue;
|
|
1016
|
+
seen.add(item);
|
|
1017
|
+
out.push(item);
|
|
1018
|
+
}
|
|
1019
|
+
// Over capacity is the master's problem, not a silent truncation --
|
|
1020
|
+
// panOverflow() reports the excess so `pan` can say so out loud.
|
|
1021
|
+
return out.slice(0, this.panCapacity());
|
|
1022
|
+
}
|
|
1023
|
+
/** Slaves that do not fit under the master's (DR x 3) ceiling. */
|
|
1024
|
+
panOverflow() {
|
|
1025
|
+
const master = this.getPanMaster();
|
|
1026
|
+
if (!master)
|
|
1027
|
+
return 0;
|
|
1028
|
+
const wanted = this._inventory.getAllItems().filter(i => i !== master && i.wirelessOn && Player.canJoinPan(i)).length;
|
|
1029
|
+
return Math.max(0, wanted - this.panCapacity());
|
|
1030
|
+
}
|
|
1031
|
+
/** Whether an item is the kind of thing a PAN can hold at all (p.233:
|
|
1032
|
+
* "only devices can be slaves, masters, or part of a PAN"). */
|
|
1033
|
+
static canJoinPan(item) {
|
|
1034
|
+
if (item.isCyberdeck() || item.category === Category.Commlink)
|
|
1035
|
+
return true;
|
|
1036
|
+
if (item.isFirearm())
|
|
1037
|
+
return item.isSmartgun;
|
|
1038
|
+
return item.category === Category.Drone || item.category === Category.Electronics;
|
|
1039
|
+
}
|
|
970
1040
|
/**
|
|
971
1041
|
* Defense against a PAN intrusion, itemised: the book's attribute plus
|
|
972
1042
|
* the master's whole Firewall, +2 while running silent (a hidden PAN
|
|
@@ -3343,8 +3413,15 @@ export class Player extends AbstractPlayer {
|
|
|
3343
3413
|
// (p.433), and a crossbow has nothing to plug into.
|
|
3344
3414
|
// AND THE GUN MUST BE A SMARTGUN (catalog v13, p.433): the wireless
|
|
3345
3415
|
// bonus is the smartgun system talking to the smartlink.
|
|
3416
|
+
// AND THE WIRELESS MUST BE ON, which is the whole of p.421's
|
|
3417
|
+
// "Turning It Off": "You lose wireless bonuses, but the items can no
|
|
3418
|
+
// longer be wirelessly hacked." THIS is the bonus that goes -- the
|
|
3419
|
+
// dice -- while smartlinkAccuracyBonus's +2 Accuracy STAYS, because
|
|
3420
|
+
// that one is the smartgun talking to your smartlink over a cable to
|
|
3421
|
+
// your glasses and never needed the radio. Same discipline as the
|
|
3422
|
+
// Matrix brackets: a limit is never a die (CgJ6uTfEvTd8JXdDN).
|
|
3346
3423
|
const drawnGun = this.weaponDrawn ? this.getCarriedWeapon() : null;
|
|
3347
|
-
const smartlink = drawnGun?.isSmartgun && FIREARM_SKILLS.includes(this.activeCombatSkill())
|
|
3424
|
+
const smartlink = drawnGun?.isSmartgun && drawnGun.wirelessOn && FIREARM_SKILLS.includes(this.activeCombatSkill())
|
|
3348
3425
|
// Chrome again, and for the same reason: the tier is already
|
|
3349
3426
|
// decided above, and this branch is the implant's own dice.
|
|
3350
3427
|
? (this.smartlinkTier() === 'gear' ? 1 : this.augBonus('firearms'))
|
|
@@ -12,6 +12,13 @@ import { AbstractRoom } from '../types/shared/abstracts.js';
|
|
|
12
12
|
import { spotsActive, actorDistanceMeters, voiceBandFor, voiceTag, doorwayKindOf, exitDoorwayKind } from '../utilities/spots.js';
|
|
13
13
|
import { canHearSpeech, canHearShout } from '../utilities/earshot.js';
|
|
14
14
|
import { synthesizeGrid, attachExitCell, detachExitCell, } from '../utilities/room-grid.js';
|
|
15
|
+
/**
|
|
16
|
+
* A CAMERA'S DEVICE RATING when the scene does not say (p.356, which
|
|
17
|
+
* lists "Camera" at 1 -- the same table that gives the door lock the 2
|
|
18
|
+
* DEFAULT_DEVICE_RATING already uses). A seed raises it: p.421 bands
|
|
19
|
+
* ordinary security devices at 2 and mil-spec at 4.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_CAMERA_RATING = 1;
|
|
15
22
|
export class Room extends AbstractRoom {
|
|
16
23
|
logger = Logger.getInstance();
|
|
17
24
|
actors = new Set();
|
|
@@ -74,6 +81,29 @@ export class Room extends AbstractRoom {
|
|
|
74
81
|
* of the grid.
|
|
75
82
|
*/
|
|
76
83
|
camerasSpottedBy = new Set();
|
|
84
|
+
/**
|
|
85
|
+
* WHAT THE LENSES RATE (eJANPpm7qCaCAJBJw, third rejection: "the
|
|
86
|
+
* camera cluster doesn't have a DR"), and the reporter's ruling that
|
|
87
|
+
* it should be per-site rather than one global number.
|
|
88
|
+
*
|
|
89
|
+
* The cluster is not a Device -- it is derived from the room itself
|
|
90
|
+
* (utilities/surveillance.ts isWatched) -- so it had no rating to
|
|
91
|
+
* carry, and the codebase had quietly grown THREE answers that
|
|
92
|
+
* disagreed: it hid behind a hard-coded 4, it defended a Data Spike
|
|
93
|
+
* with the HOST's pool (commands/disable.ts), and it displayed no
|
|
94
|
+
* rating at all. One number now feeds all three, so what a player
|
|
95
|
+
* reads is what it rolls.
|
|
96
|
+
*
|
|
97
|
+
* ONE FOR NOW (user ruling 2026-09-16), which is what p.356 lists a
|
|
98
|
+
* Camera at -- the same table the engine already takes its door-lock 2
|
|
99
|
+
* from. Per-site seeding was scoped out deliberately: the defect here
|
|
100
|
+
* was three numbers disagreeing, and one field fixes that without a
|
|
101
|
+
* seed knob the generator could get wrong. It is a FIELD rather than a
|
|
102
|
+
* constant so that making it per-site later is a one-line change at
|
|
103
|
+
* the seed layer and nothing else moves. p.421 bands ordinary security
|
|
104
|
+
* devices at 2 and mil-spec at 4 when that day comes.
|
|
105
|
+
*/
|
|
106
|
+
cameraRating = DEFAULT_CAMERA_RATING;
|
|
77
107
|
/**
|
|
78
108
|
* THE FALLEN (player ruling 2026-08-25). A killed actor leaves a BODY
|
|
79
109
|
* where they dropped, holding everything they carried, instead of
|
|
@@ -2,6 +2,7 @@ import { isLinked, CALL_COLOR } from './comm-style.js';
|
|
|
2
2
|
import { isWatched, camerasLive, canSnoopFeeds } from './surveillance.js';
|
|
3
3
|
import { Category } from '../types/shared/item-enum.js';
|
|
4
4
|
import { hint } from './hints.js';
|
|
5
|
+
import { cameraDrTag, drTagFor } from './grid-view.js';
|
|
5
6
|
/**
|
|
6
7
|
* AROs -- AUGMENTED REALITY OBJECTS (player request 2026-08-26: "we
|
|
7
8
|
* need AROs on anything online"). The 2075 street is not bare: anything
|
|
@@ -147,9 +148,10 @@ export function aroTags(scene, actor, room) {
|
|
|
147
148
|
// The verb rides the tag: standing in the room IS the requirement
|
|
148
149
|
// for a direct connection (commands/tap.ts), so this is the one
|
|
149
150
|
// place the player is always in a position to use it.
|
|
151
|
+
const camDr = cameraDrTag(room, actor.name);
|
|
150
152
|
tags.push(canSnoopFeeds(rooms, room, actor) && !camerasLive(rooms, room)
|
|
151
|
-
? `◎ Camera cluster -- wireless, and looping empty hallways for the house.`
|
|
152
|
-
: `◎ Camera cluster -- wireless, and watching.${hint(` ("tap" cables into it -- a slaved lens can't hide behind its host)`)}`);
|
|
153
|
+
? `◎ Camera cluster ${camDr}-- wireless, and looping empty hallways for the house.`
|
|
154
|
+
: `◎ Camera cluster ${camDr}-- wireless, and watching.${hint(` ("tap" cables into it -- a slaved lens can't hide behind its host)`)}`);
|
|
153
155
|
}
|
|
154
156
|
// THE FIXED DEVICES -- the keypad on the wall, the maglock on the
|
|
155
157
|
// grate (YjBA9xhY2NDmCFw5d, Deditri: "The keypad should be
|
|
@@ -174,7 +176,7 @@ export function aroTags(scene, actor, room) {
|
|
|
174
176
|
// still picked, breached, shot and keyed exactly as before.
|
|
175
177
|
if (!device.hasIconFor(actor.name))
|
|
176
178
|
continue;
|
|
177
|
-
tags.push(`▤ ${device.name} -- wireless,
|
|
179
|
+
tags.push(`▤ ${device.name} -- wireless, ${drTagFor(device, actor.name)}${device.isOpen() ? ', standing open' : `.${hint(` ("${device.verbs()[0]} ${device.name}")`)}`}`);
|
|
178
180
|
}
|
|
179
181
|
// LOOSE DEVICES lying in plain sight. A wireless deck on a table
|
|
180
182
|
// announces itself; one in a drawer you never opened does not.
|
|
@@ -26,9 +26,17 @@ export const END_CALL_SENTINEL = 'end-call';
|
|
|
26
26
|
export function isLinked(actor) {
|
|
27
27
|
if (actor.runningSilent)
|
|
28
28
|
return false;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
36
|
+
const worn = actor.equipment.head?.commlink;
|
|
37
|
+
const wornOn = worn != null && worn.wirelessOn !== false;
|
|
38
|
+
const carried = actor.inventory.getAllItems().some(i => i.category === 'commlink' && i.wirelessOn !== false);
|
|
39
|
+
return wornOn || carried;
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
42
|
* DROP A LIVE CALL, BOTH SIDES, and say who was on it.
|
|
@@ -23,6 +23,65 @@ const NEAR_DIVIDER = '-- and NEAR it, out on the open grid (not inside the host)
|
|
|
23
23
|
export function isNearDivider(line) {
|
|
24
24
|
return line === NEAR_DIVIDER;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* A DEVICE RATING IS SOMETHING YOU BUY WITH A HIT, not something the
|
|
28
|
+
* screen gives away (eJANPpm7qCaCAJBJw -- "are device ratings
|
|
29
|
+
* themselves, protected information?"; p.235).
|
|
30
|
+
*
|
|
31
|
+
* p.235 lists what one hit on a Matrix Perception test buys, and "the
|
|
32
|
+
* target's device rating" is an entry on it, beside "the type of icon"
|
|
33
|
+
* and "the rating of one of the target's Matrix attributes". The engine
|
|
34
|
+
* printed [DR n] on every icon for nothing, which is the same defect the
|
|
35
|
+
* silent-icon work fixed one field over: the rating was free.
|
|
36
|
+
*
|
|
37
|
+
* WHAT COUNTS AS HAVING READ IT is the sweep that found the icon
|
|
38
|
+
* (utilities/silent-icons.ts). Winning it buys the icon AND its rating
|
|
39
|
+
* together -- one hit, one look, one fact.
|
|
40
|
+
*
|
|
41
|
+
* AND A LOUD ICON PUBLISHES ITS RATING, which is where this stops short
|
|
42
|
+
* of p.235 on purpose. Strictly the book charges a hit for the rating of
|
|
43
|
+
* ANY icon, loud or not; modelling that needs a per-fact ledger of what
|
|
44
|
+
* each viewer has bought about each icon, which nothing else in this
|
|
45
|
+
* engine keeps and which would put a roll between a player and a number
|
|
46
|
+
* they can already see on the shop window. The first cut gated every
|
|
47
|
+
* rating on `spottedBy` and the consequence was immediate: a device that
|
|
48
|
+
* was never silent is never spotted, so its rating read [DR ?] forever
|
|
49
|
+
* with no way to earn it. So the line is drawn at HIDING -- a rating you
|
|
50
|
+
* were never meant to see is protected, a rating the thing broadcasts is
|
|
51
|
+
* not -- and that is a smaller claim than p.235's, stated here rather
|
|
52
|
+
* than dressed up as the whole rule.
|
|
53
|
+
*/
|
|
54
|
+
export function ratingKnownBy(device, viewerName) {
|
|
55
|
+
return !device.silent || device.spottedBy.has(viewerName);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The bracketed rating, or `[DR ?]` when it has not been read.
|
|
59
|
+
*
|
|
60
|
+
* A QUESTION MARK RATHER THAN NOTHING, on purpose: the icon's SHAPE is
|
|
61
|
+
* free (p.219 -- it looks like the thing it is), so a player should see
|
|
62
|
+
* that this object HAS a rating and that they have not bought it yet.
|
|
63
|
+
* Printing nothing would read as "this one has no rating", which is a
|
|
64
|
+
* different and wrong fact, and is exactly the ambiguity that made the
|
|
65
|
+
* camera cluster's missing DR look like an oversight.
|
|
66
|
+
*/
|
|
67
|
+
export function drTag(known, rating) {
|
|
68
|
+
return known ? `{bold}[DR ${rating}]{/bold}` : `{bold}[DR ?]{/bold}`;
|
|
69
|
+
}
|
|
70
|
+
/** One rule for both surfaces: the grid and the AR overlay ask this, so
|
|
71
|
+
* a rating the street can read and a persona cannot is impossible. */
|
|
72
|
+
export function drTagFor(device, viewerName) {
|
|
73
|
+
return drTag(ratingKnownBy(device, viewerName), device.rating);
|
|
74
|
+
}
|
|
75
|
+
/** The camera cluster's rating tag. Cameras are always security kit and
|
|
76
|
+
* therefore always silent, so unlike a device there is no loud case:
|
|
77
|
+
* reading the lenses is the only way to their number. */
|
|
78
|
+
export function cameraDrTag(room, viewerName) {
|
|
79
|
+
return drTag(room.camerasSpottedBy.has(viewerName), room.cameraRating);
|
|
80
|
+
}
|
|
81
|
+
/** Trailing space for the icon lines that interpolate it mid-sentence. */
|
|
82
|
+
function cameraDr(room, viewerName) {
|
|
83
|
+
return `${cameraDrTag(room, viewerName)} `;
|
|
84
|
+
}
|
|
26
85
|
/**
|
|
27
86
|
* A DEVICE ICON, AND WHAT WORKS IT (commands/disable.ts, 2026-09-13).
|
|
28
87
|
* Control Device (p.238) needs 2 marks on the device -- "disable" -- and
|
|
@@ -30,7 +89,7 @@ export function isNearDivider(line) {
|
|
|
30
89
|
* locked (p.228), so its line says so rather than offering a way in.
|
|
31
90
|
*/
|
|
32
91
|
export function deviceIconLine(device, actor) {
|
|
33
|
-
const dr =
|
|
92
|
+
const dr = drTag(ratingKnownBy(device, actor.name), device.rating);
|
|
34
93
|
if (device.isOpen())
|
|
35
94
|
return `▤ ${device.name} ${dr} -- its icon standing open, nothing left to hold.`;
|
|
36
95
|
if (device.isBricked) {
|
|
@@ -204,16 +263,22 @@ export function gridIcons(scene, actor, room) {
|
|
|
204
263
|
// SAME SELECTOR AS hack.ts's top-priority PAN effect, on
|
|
205
264
|
// purpose: the icon must name exactly the device the verb will
|
|
206
265
|
// seize, or "identified rapidly" points at the wrong thing.
|
|
207
|
-
// This engine treats a held firearm as smartlinked
|
|
208
|
-
//
|
|
209
|
-
//
|
|
266
|
+
// This engine treats a held firearm as smartlinked, so
|
|
267
|
+
// held-and-a-firearm is the whole test and the two surfaces
|
|
268
|
+
// agree by construction -- EXCEPT for a gun whose owner has
|
|
269
|
+
// switched it off the net (Item.wirelessOn, `pan off <gun>`,
|
|
270
|
+
// CgJ6uTfEvTd8JXdDN). A device off the network broadcasts
|
|
271
|
+
// nothing, so there is no icon here to identify and nothing for
|
|
272
|
+
// `hack` to seize; the trade is p.421's, priced there -- the
|
|
273
|
+
// wireless DICE bonus goes (getAttackPool reads the same flag)
|
|
274
|
+
// while the smartgun's +2 Accuracy stays.
|
|
210
275
|
//
|
|
211
276
|
// Sitting INSIDE the isLinked branch is what makes running
|
|
212
277
|
// silent hide it, which is canon too: the book notes a user can
|
|
213
278
|
// hide these icons, and isLinked() is already false while
|
|
214
279
|
// silent. No second rule needed.
|
|
215
280
|
const gun = a.equipment.rightHand?.weapon ?? a.equipment.leftHand?.weapon;
|
|
216
|
-
if (gun?.isFirearm()) {
|
|
281
|
+
if (gun?.isFirearm() && gun.wirelessOn) {
|
|
217
282
|
// TERSE ON PURPOSE (AKfemBJJWJP99nx8K, third pass: "overly
|
|
218
283
|
// verbose"). The earlier line explained WHY a gun breaks out
|
|
219
284
|
// of the PAN -- "a wireless gun does not hide inside a
|
|
@@ -275,10 +340,10 @@ export function gridIcons(scene, actor, room) {
|
|
|
275
340
|
if (isWatched(room) && room.camerasSpottedBy.has(actor.name)) {
|
|
276
341
|
const rooms = scene.getRooms();
|
|
277
342
|
if (!camerasLive(rooms, room)) {
|
|
278
|
-
icons.push(`◎ Camera cluster -- looped on empty hallways. Security sees NOTHING here.${canSnoopFeeds(rooms, room, actor) ? ` ("snoop" rides the feeds you own)` : ''}`);
|
|
343
|
+
icons.push(`◎ Camera cluster ${cameraDr(room, actor.name)}-- looped on empty hallways. Security sees NOTHING here.${canSnoopFeeds(rooms, room, actor) ? ` ("snoop" rides the feeds you own)` : ''}`);
|
|
279
344
|
}
|
|
280
345
|
else if (canSnoopFeeds(rooms, room, actor)) {
|
|
281
|
-
icons.push(`◎ Camera cluster -- live, and answering to YOU as much as the house. ("snoop <room>" watches remotely)`);
|
|
346
|
+
icons.push(`◎ Camera cluster ${cameraDr(room, actor.name)}-- live, and answering to YOU as much as the house. ("snoop <room>" watches remotely)`);
|
|
282
347
|
}
|
|
283
348
|
else {
|
|
284
349
|
// Seen from the grid, the cluster is worth naming as a WAY IN,
|
|
@@ -296,7 +361,7 @@ export function gridIcons(scene, actor, room) {
|
|
|
296
361
|
// exactly one, so the reader was left to guess at a fact the
|
|
297
362
|
// engine had already handed them.
|
|
298
363
|
const owner = cameraMasterHost(scene.getRooms(), room);
|
|
299
|
-
icons.push(`◎ Camera cluster -- live, feeding ${owner ? hostLabel(owner) : `the site's host`}. Loud acts here travel. (own the host to loop them -- or walk in and "tap" a lens in the flesh: slaved to the host means a key to the host)`);
|
|
364
|
+
icons.push(`◎ Camera cluster ${cameraDr(room, actor.name)}-- live, feeding ${owner ? hostLabel(owner) : `the site's host`}. Loud acts here travel. (own the host to loop them -- or walk in and "tap" a lens in the flesh: slaved to the host means a key to the host)`);
|
|
300
365
|
}
|
|
301
366
|
}
|
|
302
367
|
// THE DIVIDER (eJANPpm7qCaCAJBJw). Only when there is a host to be
|
|
@@ -570,14 +635,28 @@ export function gridSculpt(actor, room) {
|
|
|
570
635
|
* the dimmer their icons"). `rooms` absent means the caller has only
|
|
571
636
|
* the one room, and the block reads as it always did.
|
|
572
637
|
*/
|
|
573
|
-
export function gridIconBlock(scene, actor, room, rooms) {
|
|
638
|
+
export function gridIconBlock(scene, actor, room, rooms, justFound) {
|
|
574
639
|
const isHostLine = (i) => i.startsWith('●') || i.startsWith('■');
|
|
640
|
+
// WHAT THIS LOOK JUST TURNED UP, tagged where it is listed
|
|
641
|
+
// (eJANPpm7qCaCAJBJw: "I still see the Service Keypad and Camera
|
|
642
|
+
// cluster"). An icon a sweep has just won is otherwise identical on
|
|
643
|
+
// the page to one that was never hidden -- and on every LATER look
|
|
644
|
+
// there is no sweep line at all, because there is nothing left to
|
|
645
|
+
// roll against, so without this the moment of finding something is
|
|
646
|
+
// invisible and the whole mechanic reads as not having run.
|
|
647
|
+
const mark = (icon) => {
|
|
648
|
+
if (!justFound?.length)
|
|
649
|
+
return icon;
|
|
650
|
+
return justFound.some(n => icon.includes(n))
|
|
651
|
+
? `${icon} {bold}<- just spotted, was running silent{/bold}`
|
|
652
|
+
: icon;
|
|
653
|
+
};
|
|
575
654
|
if (actor.insideHost || !rooms) {
|
|
576
655
|
const icons = gridIcons(scene, actor, room);
|
|
577
656
|
if (icons.length === 0) {
|
|
578
657
|
return `{light-blue-fg}Thin grid out here -- background noise, nothing worth cracking.{/light-blue-fg}`;
|
|
579
658
|
}
|
|
580
|
-
return `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${icons.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`;
|
|
659
|
+
return `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${icons.map(i => ` {light-blue-fg}${mark(i)}{/light-blue-fg}`).join('\n')}`;
|
|
581
660
|
}
|
|
582
661
|
const out = [];
|
|
583
662
|
const hosts = hostsInReach(rooms, actor);
|
|
@@ -629,12 +708,12 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
629
708
|
return line;
|
|
630
709
|
// Box-drawing, so the last child closes the branch and the eye can
|
|
631
710
|
// see where the WAN ends.
|
|
632
|
-
const kids = slaved.map((i, idx) => ` {light-blue-fg}${idx === slaved.length - 1 ? '└─' : '├─'} ${i}{/light-blue-fg}`);
|
|
711
|
+
const kids = slaved.map((i, idx) => ` {light-blue-fg}${idx === slaved.length - 1 ? '└─' : '├─'} ${mark(i)}{/light-blue-fg}`);
|
|
633
712
|
return [line, ...kids].join('\n');
|
|
634
713
|
}).join('\n')}`
|
|
635
714
|
: `{light-blue-fg}Nothing overhead -- no host stands over this district.{/light-blue-fg}`);
|
|
636
715
|
out.push(loose.length > 0
|
|
637
|
-
? `{light-blue-fg}ICONS IN REACH${slaved.length > 0 ? ' -- answering to nobody up there' : ''}:{/light-blue-fg}\n${loose.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`
|
|
716
|
+
? `{light-blue-fg}ICONS IN REACH${slaved.length > 0 ? ' -- answering to nobody up there' : ''}:{/light-blue-fg}\n${loose.map(i => ` {light-blue-fg}${mark(i)}{/light-blue-fg}`).join('\n')}`
|
|
638
717
|
: slaved.length > 0
|
|
639
718
|
? `{light-blue-fg}Nothing else near -- everything around you hangs off the host.{/light-blue-fg}`
|
|
640
719
|
: `{light-blue-fg}Nothing near -- background noise around your signal.{/light-blue-fg}`);
|
|
@@ -165,6 +165,10 @@ export function serializeItem(item) {
|
|
|
165
165
|
out.jammed = true;
|
|
166
166
|
if (item.bricked)
|
|
167
167
|
out.bricked = true;
|
|
168
|
+
// Sparse and INVERTED on purpose: wireless is on by default, so a save
|
|
169
|
+
// written before the switch existed has no key and restores to on.
|
|
170
|
+
if (!item.wirelessOn)
|
|
171
|
+
out.wirelessOff = true;
|
|
168
172
|
if (item.deckDamage > 0)
|
|
169
173
|
out.deckDamage = item.deckDamage;
|
|
170
174
|
if (item.droneDamage > 0)
|
|
@@ -224,6 +228,8 @@ export function restoreItem(json) {
|
|
|
224
228
|
item.jammed = true;
|
|
225
229
|
if (json.bricked)
|
|
226
230
|
item.bricked = true;
|
|
231
|
+
if (json.wirelessOff)
|
|
232
|
+
item.wirelessOn = false;
|
|
227
233
|
if (json.ruined)
|
|
228
234
|
item.ruined = true;
|
|
229
235
|
if (json.deckDamage)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { rollPool, formatRoll } from './dice.js';
|
|
2
2
|
import { deviceDefensePool } from './matrix-intrusion.js';
|
|
3
3
|
import { isWatched } from './surveillance.js';
|
|
4
|
-
import { DEFAULT_DEVICE_RATING } from '../models/device.js';
|
|
5
4
|
function hiddenDevice(device) {
|
|
6
5
|
return {
|
|
7
6
|
name: device.name,
|
|
@@ -34,25 +33,27 @@ export function hiddenIconsIn(scene, room, viewer) {
|
|
|
34
33
|
.map(hiddenDevice);
|
|
35
34
|
// THE CAMERAS, which p.217 names FIRST. They are not a Device -- the
|
|
36
35
|
// cluster is derived from the room (surveillance.ts isWatched) -- so
|
|
37
|
-
//
|
|
38
|
-
// rating: an ordinary fixture, which canon prices at 2 (p.356).
|
|
36
|
+
// the rating rides the ROOM (Room.cameraRating), seeded per site.
|
|
39
37
|
//
|
|
40
38
|
// THE FIRST CUT GAVE THEM THEIR MASTER HOST'S RATING and that was
|
|
41
39
|
// wrong twice over. Canon first: p.233's slaving rule is about what a
|
|
42
|
-
// slave DEFENDS AN ATTACK with
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
40
|
+
// slave DEFENDS AN ATTACK with, and hiding is not an attack being
|
|
41
|
+
// defended. Then arithmetic: a rating-8 host gave its lenses 16 dice
|
|
42
|
+
// against a sweep the Data Processing limit caps near 5, so a camera
|
|
43
|
+
// on any corporate host was not hard to find, it was impossible --
|
|
44
|
+
// measured at zero finds in twelve looks.
|
|
45
|
+
//
|
|
46
|
+
// THE SECOND CUT hard-coded a 4 in here, which fixed the arithmetic
|
|
47
|
+
// and left the engine holding THREE answers to one question: this
|
|
48
|
+
// hid behind 4, commands/disable.ts defended a Data Spike with the
|
|
49
|
+
// HOST's pool, and the icon line showed no rating at all. A player
|
|
50
|
+
// could not have reconciled them because two of the three were never
|
|
51
|
+
// printed. One seeded number feeds all three now, so what the player
|
|
52
|
+
// reads is what it rolls (reporter's ruling: per-site, not global).
|
|
51
53
|
if (isWatched(room) && !room.camerasSpottedBy.has(viewer.name)) {
|
|
52
|
-
const rating = DEFAULT_DEVICE_RATING.lock ?? 2;
|
|
53
54
|
out.push({
|
|
54
55
|
name: 'Camera cluster',
|
|
55
|
-
hide: { pool: Math.max(0,
|
|
56
|
+
hide: { pool: Math.max(0, room.cameraRating * 2), label: 'Device Rating + Firewall' },
|
|
56
57
|
reveal: (v) => { room.camerasSpottedBy.add(v); },
|
|
57
58
|
});
|
|
58
59
|
}
|
|
@@ -98,28 +99,44 @@ export function spotHiddenIcons(scene, actor, room) {
|
|
|
98
99
|
+ actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
|
|
99
100
|
const roll = rollPool(pool, actor.matrixAttribute('dataProcessing'), { gremlins: actor.deckGremlins });
|
|
100
101
|
const found = [];
|
|
101
|
-
|
|
102
|
+
let missed = 0;
|
|
102
103
|
const against = [];
|
|
103
104
|
for (const icon of hidden) {
|
|
104
105
|
const resist = rollPool(icon.hide.pool);
|
|
105
|
-
against.push({ name: icon.name, label: icon.hide.label, roll: resist });
|
|
106
106
|
// Ties go to the hider: "on a tie or more hits by the defender, it
|
|
107
107
|
// stays hidden and out of reach" (p.236).
|
|
108
108
|
if (roll.hits > resist.hits) {
|
|
109
109
|
icon.reveal(actor.name);
|
|
110
110
|
found.push(icon.name);
|
|
111
|
+
against.push({ name: icon.name, label: icon.hide.label, roll: resist });
|
|
111
112
|
}
|
|
112
113
|
else {
|
|
113
|
-
missed
|
|
114
|
+
missed += 1;
|
|
115
|
+
// THE NAME DOES NOT LEAVE THIS BRANCH. Only the dice do -- see
|
|
116
|
+
// ISpotOutcome.missed. Attaching the name here "just for the
|
|
117
|
+
// mechanics line" is exactly how it leaked the first time.
|
|
118
|
+
against.push({ roll: resist });
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
return { found, missed, roll, against };
|
|
117
122
|
}
|
|
118
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* The mechanics lines for a sweep.
|
|
125
|
+
*
|
|
126
|
+
* AN ICON YOU DID NOT BEAT IS NOT NAMED HERE (user ruling 2026-09-16).
|
|
127
|
+
* Your own roll is always shown -- it is yours. What it was rolled
|
|
128
|
+
* against is shown only where you won it: a loss prints its dice and
|
|
129
|
+
* calls the roller "something", because naming it would hand over for
|
|
130
|
+
* free precisely what the opposed test exists to charge for (p.235:
|
|
131
|
+
* the type of an icon and its device rating are things a Matrix
|
|
132
|
+
* Perception HIT buys).
|
|
133
|
+
*/
|
|
119
134
|
export function spotMechanics(outcome) {
|
|
120
135
|
return [
|
|
121
136
|
`Matrix Perception, silent icons (Computer + Intuition [Data Processing]): ${formatRoll(outcome.roll)}`,
|
|
122
|
-
...outcome.against.map(a =>
|
|
137
|
+
...outcome.against.map(a => (a.name
|
|
138
|
+
? ` ${a.name} hides (${a.label}): ${formatRoll(a.roll)}`
|
|
139
|
+
: ` something holds its cover: ${formatRoll(a.roll)}`)),
|
|
123
140
|
];
|
|
124
141
|
}
|
|
125
142
|
/**
|
|
@@ -135,10 +152,14 @@ export function spotLine(outcome) {
|
|
|
135
152
|
if (outcome.found.length > 0) {
|
|
136
153
|
lines.push(`You pick ${outcome.found.length === 1 ? 'an icon' : `${outcome.found.length} icons`} out of the noise -- ${outcome.found.join(', ')}, running silent and not quiet enough.`);
|
|
137
154
|
}
|
|
138
|
-
if (outcome.missed
|
|
139
|
-
|
|
155
|
+
if (outcome.missed > 0) {
|
|
156
|
+
// A COUNT AND NOTHING ELSE. You learn that the room is holding
|
|
157
|
+
// something back -- which is what a hit buys under p.235-236 -- and
|
|
158
|
+
// not what it is. `missed` is a number precisely so this line cannot
|
|
159
|
+
// grow a name later.
|
|
160
|
+
lines.push(outcome.missed === 1
|
|
140
161
|
? `Something else here is running silent and you cannot pin it -- a shape in the static that stops when you look at it. (Look again; each sweep is its own Matrix Perception, p.241.)`
|
|
141
|
-
: `${outcome.missed
|
|
162
|
+
: `${outcome.missed} more icons are running silent here, and none of them will hold still. (Look again; each sweep is its own Matrix Perception, p.241.)`);
|
|
142
163
|
}
|
|
143
164
|
return lines.join('\n');
|
|
144
165
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.229.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.",
|