@maka/maka-cli 5.228.0 → 5.230.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.228.0",
3
+ "version": "5.230.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.",
@@ -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(_args = []) {
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
- const link = a.equipment.head?.commlink;
46
- if (link) {
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 deck = a.getAnyCyberdeck();
51
- if (deck) {
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 gun = a.equipment.rightHand?.weapon ?? a.equipment.leftHand?.weapon;
55
- // Only a SMARTGUN is on the PAN (p.433; catalog v13): a plain gun has
56
- // nothing to broadcast and nothing to crack.
57
- if (gun?.isFirearm() && gun.isSmartgun) {
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
- const drone = a.inventory.getAllItems().find(i => i.category === Category.Drone);
61
- if (drone) {
62
- lines.push(` • ${drone.name} (drone) -- recon uplink${hint(' ("drone")')}`);
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
@@ -1371,5 +1371,52 @@
1371
1371
  // shows it with no explanation at all -- indistinguishable from an
1372
1372
  // icon that was never hidden, which is most of why 1.80.0 read to
1373
1373
  // the reporter as nothing having happened.
1374
- export const ENGINE_VERSION = '1.82.0';
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';
1375
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,91 @@ 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
- const link = this._equipment.head?.commlink
964
- ?? this._inventory.getAllItems().find(i => i.category === 'commlink')
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
+ if (item.category === Category.Drone)
1039
+ return true;
1040
+ // ELECTRONICS ONLY IF IT IS ACTUALLY A DEVICE. p.233 is narrow --
1041
+ // "only devices can be slaves, masters, or part of a PAN" -- and
1042
+ // Category.Electronics is not: it is also where the survival kit,
1043
+ // the binoculars and a thermographic vision MOD live. A catalog row
1044
+ // that declares a `device` block is the engine's own record of
1045
+ // having a Device Rating, so that is the test. Without it the
1046
+ // dashboard listed a first-aid tin as hackable gear.
1047
+ return item.category === Category.Electronics && item.row?.device !== undefined;
1048
+ }
970
1049
  /**
971
1050
  * Defense against a PAN intrusion, itemised: the book's attribute plus
972
1051
  * the master's whole Firewall, +2 while running silent (a hidden PAN
@@ -3343,8 +3422,15 @@ export class Player extends AbstractPlayer {
3343
3422
  // (p.433), and a crossbow has nothing to plug into.
3344
3423
  // AND THE GUN MUST BE A SMARTGUN (catalog v13, p.433): the wireless
3345
3424
  // bonus is the smartgun system talking to the smartlink.
3425
+ // AND THE WIRELESS MUST BE ON, which is the whole of p.421's
3426
+ // "Turning It Off": "You lose wireless bonuses, but the items can no
3427
+ // longer be wirelessly hacked." THIS is the bonus that goes -- the
3428
+ // dice -- while smartlinkAccuracyBonus's +2 Accuracy STAYS, because
3429
+ // that one is the smartgun talking to your smartlink over a cable to
3430
+ // your glasses and never needed the radio. Same discipline as the
3431
+ // Matrix brackets: a limit is never a die (CgJ6uTfEvTd8JXdDN).
3346
3432
  const drawnGun = this.weaponDrawn ? this.getCarriedWeapon() : null;
3347
- const smartlink = drawnGun?.isSmartgun && FIREARM_SKILLS.includes(this.activeCombatSkill())
3433
+ const smartlink = drawnGun?.isSmartgun && drawnGun.wirelessOn && FIREARM_SKILLS.includes(this.activeCombatSkill())
3348
3434
  // Chrome again, and for the same reason: the tier is already
3349
3435
  // decided above, and this branch is the implant's own dice.
3350
3436
  ? (this.smartlinkTier() === 'gear' ? 1 : this.augBonus('firearms'))
@@ -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
- const worn = actor.equipment.head?.commlink != null;
30
- const carried = actor.inventory.getAllItems().some(i => i.category === 'commlink');
31
- return worn || carried;
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.
@@ -263,16 +263,22 @@ export function gridIcons(scene, actor, room) {
263
263
  // SAME SELECTOR AS hack.ts's top-priority PAN effect, on
264
264
  // purpose: the icon must name exactly the device the verb will
265
265
  // seize, or "identified rapidly" points at the wrong thing.
266
- // This engine treats a held firearm as smartlinked -- there is
267
- // no separate wireless flag on Item -- so held-and-a-firearm is
268
- // the whole test, and the two surfaces agree by construction.
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.
269
275
  //
270
276
  // Sitting INSIDE the isLinked branch is what makes running
271
277
  // silent hide it, which is canon too: the book notes a user can
272
278
  // hide these icons, and isLinked() is already false while
273
279
  // silent. No second rule needed.
274
280
  const gun = a.equipment.rightHand?.weapon ?? a.equipment.leftHand?.weapon;
275
- if (gun?.isFirearm()) {
281
+ if (gun?.isFirearm() && gun.wirelessOn) {
276
282
  // TERSE ON PURPOSE (AKfemBJJWJP99nx8K, third pass: "overly
277
283
  // verbose"). The earlier line explained WHY a gun breaks out
278
284
  // of the PAN -- "a wireless gun does not hide inside a
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.228.0",
3
+ "version": "5.230.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.",