@maka/maka-cli 5.379.1 → 5.380.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.379.1",
3
+ "version": "5.380.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.",
@@ -3,10 +3,11 @@ 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
+ import { reachableDevices } from '../utilities/pan-devices.js';
7
7
  import { leaveMatrix } from '../utilities/planes.js';
8
8
  import { findWirelessImplant, implantTrack } from '../utilities/implant-matrix.js';
9
9
  import { augFor } from '../utilities/catalog.js';
10
+ import { panStatus } from '../utilities/pan-status.js';
10
11
  /**
11
12
  * The PAN dashboard: your personal area network at a glance -- master
12
13
  * device and its firewall, every slaved device with what it grants while
@@ -299,28 +300,32 @@ export class PanCommand extends Command {
299
300
  `No devices, no PAN. You are unhackable and unreachable -- the monk's build.`,
300
301
  ].join('\n');
301
302
  }
302
- // THREE STATES, NOT TWO (RXPvXqTwEyH9v3Zk7). The line this replaced
303
- // read "SILENT -- hidden (+2 firewall), but no calls, no AR, no
304
- // Matrix", and every clause after the comma described a DIFFERENT
305
- // state: p.421's wireless-off. Worse, "no AR" contradicted the
306
- // engine's own RAG-checked ruling three files away (utilities/ar.ts
307
- // arSightUp: silence is what you EMIT, AR is what you RECEIVE), so
308
- // one runner could read two opposite answers about one flag.
309
- //
310
- // Running silent costs the -2 and nothing else (p.235). Turning the
311
- // radio off is what costs you the bonuses and the traffic (p.421).
312
- const state = radioIsDark(a)
313
- ? `{white-fg}RADIO OFF{/white-fg} -- nothing broadcasting: no bonuses, no calls, and nothing to hack${hint(` ("wireless on")`)}`
314
- : a.runningSilent
315
- ? `{magenta-fg}SILENT{/magenta-fg} -- icon hidden (+2 firewall), -2 dice on your Matrix actions; bonuses and calls untouched`
316
- : a.arOffline
317
- ? `{red-fg}LOUD, AR CUT{/red-fg} -- someone hacked your overlay dark ("reboot")`
318
- : `{green-fg}LOUD{/green-fg} -- full bonuses, and hackable`;
303
+ // FOUR SWITCHES, FOUR ROWS (player 2026-10-04: "running loud isn't
304
+ // the same as wireless bonus"). This was one "Broadcast:" ladder whose
305
+ // LOUD rung read "full bonuses", as if being loud paid them; and its
306
+ // SILENT rung promised "+2 firewall", which p.235-236 does not grant
307
+ // (RAG-checked 2026-10-04: a silent icon must be FOUND first, and
308
+ // that is the whole of the protection). Wireless decides the bonuses
309
+ // (p.421), silence decides whether you can be targeted (p.235), AR is
310
+ // what you receive (p.229), and the grid charges its own toll (p.233).
311
+ // utilities/pan-status.ts answers all four -- the web card reads it too.
312
+ const st = panStatus(a);
313
+ const arLine = st.ar
314
+ ? `{green-fg}ON{/green-fg}`
315
+ : `{white-fg}OFF{/white-fg} -- ${AR_WHY[st.arWhy ?? ''] ?? 'no overlay'}`;
316
+ const gridToll = st.modifiers.find(m => m.source === 'the public grid');
319
317
  const slaves = a.panSlaves();
320
318
  const lines = [
321
319
  `PAN: ${a.name.toUpperCase()}`,
322
320
  '',
323
- `Broadcast: ${state}`,
321
+ `Wireless: ${st.wireless === 'on'
322
+ ? `{green-fg}ON{/green-fg} -- wireless bonuses paid, and every device on the air is hackable (p.421)${hint(' ("wireless off")')}`
323
+ : `{white-fg}OFF{/white-fg} -- no bonuses, no calls, nothing to hack (p.421)${hint(' ("wireless on")')}`}`,
324
+ `Silent: ${st.silent
325
+ ? `{magenta-fg}ON{/magenta-fg} -- your icon must be found before it can be targeted; -2 dice on your Matrix actions (p.235). Wireless bonuses untouched.`
326
+ : `OFF -- your icon is in plain view of any decker in reach${hint(' ("silent")')}`}`,
327
+ `AR: ${arLine}`,
328
+ `Grid: ${st.grid.name}${st.grid.on ? '' : ' (where "jack in" lands you)'}${gridToll ? ` -- {yellow-fg}${gridToll.value} on ${gridToll.applies} (${gridToll.cite}){/yellow-fg}` : ''}`,
324
329
  // WHICH ICON IS YOU. The master and the persona are two different
325
330
  // facts and they come apart exactly where this item lives: a
326
331
  // technomancer's commlink masters their gear while their MIND runs
@@ -362,7 +367,13 @@ export class PanCommand extends Command {
362
367
  lines.push(' (none -- the master is the whole network)');
363
368
  }
364
369
  for (const item of slaves) {
365
- lines.push(` • ${this.slaveLine(item)}`);
370
+ lines.push(` • ${this.slaveLine(item, st.devices.find(d => d.name === item.name))}`);
371
+ }
372
+ // The master earns its own wireless bonus too (the AR initiative add
373
+ // rides the live link, which is usually the master).
374
+ const masterRow = st.devices.find(d => d.role === 'master');
375
+ if (masterRow?.bonus) {
376
+ lines.splice(lines.indexOf('Slaved devices:'), 0, `Master's wireless bonus: ${bonusText(masterRow)}`, '');
366
377
  }
367
378
  const over = a.panOverflow();
368
379
  if (over > 0) {
@@ -376,17 +387,24 @@ export class PanCommand extends Command {
376
387
  lines.push('');
377
388
  lines.push('Switched off:');
378
389
  for (const item of off) {
379
- lines.push(` · ${item.name} -- wireless OFF, no icon, nothing to hack${hint(` ("pan on ${item.name}")`)}`);
390
+ const lost = st.devices.find(d => d.name === item.name)?.bonus;
391
+ lines.push(` · ${item.name} -- wireless OFF, no icon, nothing to hack${lost ? ` -- {white-fg}forgoing ${lost}{/white-fg}` : ''}${hint(` ("pan on ${item.name}")`)}`);
380
392
  }
381
393
  }
394
+ // Wireless chrome has no Item, so it is listed apart.
395
+ const chrome = st.devices.filter(d => d.role === 'implant');
396
+ if (chrome.length > 0) {
397
+ lines.push('');
398
+ lines.push('Wireless implants:');
399
+ for (const d of chrome)
400
+ lines.push(` • ${d.name} -- wireless ${d.wireless ? 'ON' : 'OFF'}${d.note ? ` {red-fg}${d.note}{/red-fg}` : ''}`);
401
+ }
382
402
  lines.push('');
383
403
  // THE TWO SWITCHES, NAMED APART. A player reading this screen is
384
404
  // deciding between them, and until this item they had only one verb.
385
- lines.push(radioIsDark(a)
386
- ? `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.')}`
387
- : a.runningSilent
388
- ? `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.')}`
389
- : `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.')}`);
405
+ lines.push(a.runningSilent
406
+ ? `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.')}`
407
+ : `Running loud: any decker in reach can try your firewall -- jammed guns and dead AR are how that feels.${hint(' "silent" hides your icon and keeps your bonuses; "wireless off" kills the broadcast and the bonuses with it.')}`);
390
408
  return lines.join('\n');
391
409
  }
392
410
  /** The one line that says which icon on the grid is actually you. */
@@ -401,34 +419,51 @@ export class PanCommand extends Command {
401
419
  const muted = a.resonanceMuted() ? ` -- {red-fg}Resonance MUTED{/red-fg} while you ride it (p.251)` + hint(` ("pan link persona")`) : '';
402
420
  return `${rig.name} [DR ${rig.deviceRating}] -- ${how}${muted}`;
403
421
  }
404
- /** What one slaved device is and what being on the net buys it. */
405
- slaveLine(item) {
422
+ /**
423
+ * What one slaved device is and what its WIRELESS buys it.
424
+ *
425
+ * A SLAVE'S BONUS DIES WITH ITS OWN RADIO, NOT WITH SILENCE (p.421 vs
426
+ * p.235, RXPvXqTwEyH9v3Zk7). The bonus text and whether it is being
427
+ * paid come from pan-status.ts, the same rows the web card draws.
428
+ */
429
+ slaveLine(item, row) {
406
430
  const a = this.actor;
407
- // A SLAVE'S BONUS DIES WITH ITS RADIO, NOT WITH SILENCE (p.421 vs
408
- // p.235, RXPvXqTwEyH9v3Zk7). This asked `a.runningSilent`, which is
409
- // the one thing canon says does NOT cost you wireless bonuses.
410
- const inactive = !item.wirelessOn || a.arOffline;
431
+ const bonus = row?.bonus ? ` -- wireless: ${bonusText(row)}` : '';
411
432
  if (item.isCyberdeck()) {
412
- return `${item.name} (cyberdeck) -- Matrix access; ${item.deckSummary()}${item.isBricked ? ' BRICKED (rest to repair)' : ''}`;
433
+ return `${item.name} (cyberdeck) -- Matrix access; ${item.deckSummary()}${item.isBricked ? ' BRICKED (rest to repair)' : ''}${bonus}`;
413
434
  }
414
435
  if (item.category === Category.Commlink) {
415
- const bonus = inactive ? 'inactive' : `+${item.qualityBonus} initiative (AR overlay)`;
416
- return `${item.name} (commlink) -- calls; ${bonus}`;
436
+ return `${item.name} (commlink) -- calls${bonus}`;
417
437
  }
418
438
  // Only a SMARTGUN is on the PAN (p.433; catalog v13): a plain gun has
419
439
  // nothing to broadcast and nothing to crack.
420
440
  if (item.isFirearm()) {
421
- return `${item.name} (smartgun) -- ${item.jammed ? '{red-fg}JAMMED -- smartlink cracked ("reboot"){/red-fg}' : 'smartlink live'}`;
441
+ return `${item.name} (smartgun)${item.jammed ? ' -- {red-fg}JAMMED -- smartlink cracked ("reboot"){/red-fg}' : ''}${bonus}`;
422
442
  }
423
443
  if (item.isRcc()) {
424
444
  return `${item.name} (RCC) -- slaved, not mastering: its drone network is idle${hint(` ("pan link ${item.name.toLowerCase()}")`)}`;
425
445
  }
426
446
  if (item.category === Category.Drone) {
427
447
  return a.rccMaster()
428
- ? `${item.name} (drone) -- on the console: group orders, frame-to-frame jumps${hint(' ("order drones ...", "jump <drone>")')}`
448
+ ? `${item.name} (drone)${bonus}${hint(' ("order drones ...", "jump <drone>")')}`
429
449
  : `${item.name} (drone) -- recon uplink${hint(' ("drone")')}`;
430
450
  }
431
- return `${item.name} (${item.category})`;
451
+ return `${item.name} (${item.category})${bonus}`;
432
452
  }
433
453
  }
454
+ /** Why the overlay is down, in the dashboard's words (pan-status arWhy). */
455
+ const AR_WHY = {
456
+ muted: 'you muted it ("ar on")',
457
+ hacked: '{red-fg}someone hacked your overlay dark{/red-fg} ("reboot")',
458
+ 'no-display': 'nothing to see it on -- glasses, contacts, cybereyes or a DNI',
459
+ 'no-pan': 'no PAN master to feed it',
460
+ 'off-plane': 'you are not in your body',
461
+ };
462
+ /** One device's wireless bonus, green while paid, dim with the reason when not. */
463
+ function bonusText(row) {
464
+ if (row.bonusLive)
465
+ return `{green-fg}${row.bonus}{/green-fg}`;
466
+ const why = !row.wireless ? 'wireless OFF' : row.role === 'overflow' ? 'over capacity' : 'not paid';
467
+ return `{white-fg}${row.bonus} -- ${why}{/white-fg}`;
468
+ }
434
469
  //# sourceMappingURL=pan.js.map
@@ -3253,5 +3253,15 @@
3253
3253
  // written to a chip -- a persona is not a device, and a datachip has no
3254
3254
  // wireless to write through. Reaches only the deviceless technomancer;
3255
3255
  // one with a commlink copies onto it and keeps their Resonance.
3256
- export const ENGINE_VERSION = '1.191.1';
3256
+ // 1.192.0 (2026-10-04): THE PAN, FOUR SWITCHES APART.
3257
+ // - "pan" prints Wireless / Silent / AR / Grid as four rows instead of
3258
+ // one Broadcast ladder whose LOUD rung read "full bonuses" -- being loud
3259
+ // pays nothing; wireless does (p.421). The SILENT rung's "+2 firewall"
3260
+ // is gone from the text: p.235-236 grants no such thing.
3261
+ // - Every device says what its wireless buys and whether it is being
3262
+ // paid: the smartgun's +1/+2 dice (p.433), the live link's AR initiative
3263
+ // add (engine rule), the RCC's drone network (p.267).
3264
+ // - The condition beat carries `pan` (utilities/pan-status.ts) for the
3265
+ // web sheet's PAN card; same rows as the verb.
3266
+ export const ENGINE_VERSION = '1.192.0';
3257
3267
  //# sourceMappingURL=engine-version.js.map
@@ -7,6 +7,7 @@ import { overlaid } from './overlay-fill.js';
7
7
  import { PLAYER_GLYPH } from './map-glyphs.js';
8
8
  import { headcountBand } from './crowd.js';
9
9
  import { pending, contractorName } from './law-response.js';
10
+ import { panStatus } from './pan-status.js';
10
11
  /**
11
12
  * THE LIVE CONDITION EMITTER -- the CLI half of the realtime sheet lane
12
13
  * (contract agreed with maka-cli.com, 2026-09-01). On every condition
@@ -303,6 +304,17 @@ export function buildConditionReport(player, deps) {
303
304
  // The ONE honest AR answer (2026-09-01): muted, cut, and no-rig
304
305
  // all collapse into arSightUp, computed at fire time.
305
306
  ar: arSightUp(player),
307
+ // THE PAN CARD (player 2026-10-04: "I don't see any of my matrix
308
+ // modifiers. When silent, I don't see on the sheet 'silent'"): wireless,
309
+ // silent, AR and grid as four separate facts, and every device's
310
+ // wireless bonus with whether it is being paid -- utilities/pan-status.ts,
311
+ // the same rows the `pan` verb prints. A decoration never kills the beat.
312
+ ...(() => { try {
313
+ return { pan: panStatus(player) };
314
+ }
315
+ catch {
316
+ return {};
317
+ } })(),
306
318
  // THE HEADER'S NUMBERS (contract 2026-09-10, web accepted first;
307
319
  // ZpgTTPgX4Hn7qL5M7: the play page's HUD shows the sheet header). A
308
320
  // browser run writes no mid-run save and its roster summary is
@@ -453,7 +465,7 @@ function noteRoster(report) {
453
465
  const ADDITIVE_FIELDS = [
454
466
  'ar', 'actors', 'deckLine', 'deckDamage', 'astralPerceiving', 'bodyRoom', 'sustainingPenalty',
455
467
  'matrix', 'panMarks', 'hostMarksOnYou', 'marksPlaced', 'marksOnYou', 'deckStored', 'droneDamage', 'companions', 'pools', 'initiative',
456
- 'karma', 'nuyen', 'edge', 'grid', 'bodies', 'phaseActor', 'crowd', 'overflowMax', 'stabilized', 'brink', 'gear',
468
+ 'karma', 'nuyen', 'edge', 'grid', 'bodies', 'phaseActor', 'crowd', 'overflowMax', 'stabilized', 'brink', 'gear', 'pan',
457
469
  ];
458
470
  export function crowdReport(player) {
459
471
  const room = player.currentLocation;
@@ -0,0 +1,147 @@
1
+ import { Category } from '../types/shared/item-enum.js';
2
+ import { switchableDevices, radioIsDark } from './pan-devices.js';
3
+ import { arSightUp } from './ar.js';
4
+ import { homeGridFor } from './planes.js';
5
+ import { hostLabel } from './grid-names.js';
6
+ import { wirelessImplants, implantTrack } from './implant-matrix.js';
7
+ function kindOf(item) {
8
+ if (item.isCyberdeck())
9
+ return 'cyberdeck';
10
+ if (item.isRcc())
11
+ return 'rcc';
12
+ if (item.category === Category.Commlink)
13
+ return 'commlink';
14
+ if (item.isFirearm())
15
+ return 'smartgun';
16
+ if (item.category === Category.Drone)
17
+ return 'drone';
18
+ return 'device';
19
+ }
20
+ /**
21
+ * THE WIRELESS BONUS ONE DEVICE EARNS -- only the ones the engine pays,
22
+ * read from the same places the pools read them, so a line here is never
23
+ * a promise the dice do not keep.
24
+ */
25
+ function bonusOf(a, item) {
26
+ const radio = item.wirelessOn;
27
+ // p.433: "+1 if you're using gear with a smartlink or +2 if you're
28
+ // using an augmentation for which you paid Essence" -- Player
29
+ // getAttackPool's `smartlink` term, same tier, same chrome dice.
30
+ if (item.isFirearm() && item.isSmartgun) {
31
+ const tier = a.smartlinkTier();
32
+ if (!tier)
33
+ return { bonus: 'no smartlink to talk to -- no dice', live: false };
34
+ const dice = tier === 'gear' ? 1 : a.augBonus('firearms');
35
+ return { bonus: `+${dice} ${dice === 1 ? 'die' : 'dice'} to attacks (smartlink ${tier === 'gear' ? 'gear' : 'implant'}, p.433)`, live: radio };
36
+ }
37
+ // The AR overlay's initiative add (Player.getInitiativeAttribute): an
38
+ // ENGINE ADDITION, not canon -- p.474 gives AR a bare Reaction +
39
+ // Intuition -- paid by the live link alone, gated on its radio and on
40
+ // nobody having hacked the overlay dark.
41
+ if (a.liveLink() === item && item.qualityBonus > 0) {
42
+ return { bonus: `+${item.qualityBonus} initiative in AR (engine rule)`, live: radio && !a.arOffline };
43
+ }
44
+ // p.266-267: one order reaches every slaved drone, frame to frame.
45
+ if (item.isRcc()) {
46
+ return a.rccMaster() === item
47
+ ? { bonus: 'drone network: group orders, frame-to-frame jumps (p.267)', live: radio }
48
+ : { bonus: 'drone network idle -- not mastering', live: false };
49
+ }
50
+ if (item.category === Category.Drone && a.rccMaster()) {
51
+ return { bonus: 'on the console: group orders (p.267)', live: radio };
52
+ }
53
+ return {};
54
+ }
55
+ function arWhy(a) {
56
+ if (arSightUp(a))
57
+ return undefined;
58
+ if (a.plane !== 'meat')
59
+ return 'off-plane';
60
+ if (a.arMuted)
61
+ return 'muted';
62
+ if (a.arOffline)
63
+ return 'hacked';
64
+ if (!a.getPanMaster() && !a.isTechnomancer())
65
+ return 'no-pan';
66
+ return 'no-display';
67
+ }
68
+ function gridOf(a) {
69
+ if (a.plane === 'matrix') {
70
+ const host = a.hostInside;
71
+ if (host)
72
+ return { name: hostLabel(host.over), kind: 'host', on: true };
73
+ if (a.currentGrid)
74
+ return { name: a.currentGrid.name, kind: a.currentGrid.kind, on: true };
75
+ }
76
+ const scene = a.currentLocation?.scene;
77
+ if (scene) {
78
+ const g = homeGridFor(scene, a);
79
+ return { name: g.name, kind: g.kind, on: false };
80
+ }
81
+ return { name: 'the public grid', kind: 'public', on: false };
82
+ }
83
+ /** The persona line without colour tags -- pan.ts adds its own. */
84
+ function personaOf(a) {
85
+ const rig = a.personaRig();
86
+ if (!rig)
87
+ return a.isTechnomancer() ? `living persona (Resonance ${a.resonance})` : 'none';
88
+ return `${rig.name} [DR ${rig.deviceRating}]${a.resonanceMuted() ? ' -- Resonance muted' : ''}`;
89
+ }
90
+ export function panStatus(a) {
91
+ const devices = switchableDevices(a);
92
+ const master = a.getPanMaster();
93
+ const slaves = new Set(a.panSlaves());
94
+ const silent = a.runningSilent;
95
+ const grid = gridOf(a);
96
+ const modifiers = [];
97
+ if (silent)
98
+ modifiers.push({ source: 'running silent', value: '-2 dice', applies: 'your Matrix actions', cite: 'p.235' });
99
+ // Inside a host the grid does not matter (p.246) -- gridOf says 'host'.
100
+ if (grid.kind === 'public')
101
+ modifiers.push({ source: 'the public grid', value: '-2 dice', applies: 'your Matrix actions', cite: 'p.233' });
102
+ const rows = devices.map(item => {
103
+ const role = item === master ? 'master'
104
+ : !item.wirelessOn ? 'off'
105
+ : slaves.has(item) ? 'slave' : 'overflow';
106
+ const { bonus, live } = bonusOf(a, item);
107
+ const note = item.isBricked ? 'BRICKED' : item.jammed ? 'JAMMED -- smartlink cracked ("reboot")' : undefined;
108
+ return {
109
+ name: item.name,
110
+ kind: kindOf(item),
111
+ role,
112
+ wireless: item.wirelessOn,
113
+ ...(bonus ? { bonus, bonusLive: Boolean(live) && role !== 'overflow' } : {}),
114
+ ...(note ? { note } : {}),
115
+ };
116
+ });
117
+ // WIRELESS CHROME (implant-matrix.ts): on the air or not, no Item to carry.
118
+ for (const key of wirelessImplants(a)) {
119
+ const t = implantTrack(a, key);
120
+ if (!t || t.item)
121
+ continue;
122
+ rows.push({
123
+ name: t.name,
124
+ kind: 'implant',
125
+ role: 'implant',
126
+ wireless: t.wirelessOn,
127
+ ...(t.bricked ? { note: 'BRICKED' } : {}),
128
+ });
129
+ }
130
+ return {
131
+ wireless: devices.length === 0 ? 'none' : radioIsDark(a) ? 'off' : 'on',
132
+ silent,
133
+ ar: arSightUp(a),
134
+ ...(arWhy(a) ? { arWhy: arWhy(a) } : {}),
135
+ grid,
136
+ persona: personaOf(a),
137
+ ...(master ? { master: { name: master.name, deviceRating: master.deviceRating } } : {}),
138
+ slaved: slaves.size,
139
+ capacity: a.panCapacity(),
140
+ ...(master || a.isTechnomancer()
141
+ ? { defence: { sleaze: a.panDefenceLine('sleaze'), attack: a.panDefenceLine('attack') } }
142
+ : {}),
143
+ modifiers,
144
+ devices: rows,
145
+ };
146
+ }
147
+ //# sourceMappingURL=pan-status.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.379.1",
3
+ "version": "5.380.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.",