@maka/maka-cli 5.182.0 → 5.184.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.
Files changed (37) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +3 -0
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/delay.js +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +331 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/end-turn.js +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +11 -13
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +17 -17
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +5 -5
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +13 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +78 -31
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/game.js +7 -4
  21. package/bundle/typescript/src/commands/game/sideQuest/models/device.js +40 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/models/host.js +4 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +6 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +2 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +2 -2
  28. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +44 -18
  30. package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +8 -0
  31. package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +50 -21
  33. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +6 -2
  34. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -3
  35. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +2 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +4 -0
  37. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.182.0",
3
+ "version": "5.184.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.",
@@ -64,14 +64,14 @@ export class ArCommand extends Command {
64
64
  return `Overlay back on -- over nothing. No commlink, no deck, no AR: you're reading the street with your eyes like it's 2050.`;
65
65
  }
66
66
  const tags = aroTags(this.scene, actor, actor.currentLocation);
67
- const head = `{cyan-fg}The overlay blinks back in.{/cyan-fg}`;
67
+ const head = `{lightblue-fg}The overlay blinks back in.{/lightblue-fg}`;
68
68
  // Turning AR back on while silent is the other moment the question
69
69
  // lands -- you have just done the thing you were unsure was allowed.
70
70
  const note = silentAndSeeingNote(actor);
71
71
  if (tags.length === 0) {
72
- return [head, `{cyan-fg}Nothing here is talking -- a dead patch of street.{/cyan-fg}`, note].filter(Boolean).join('\n');
72
+ return [head, `{lightblue-fg}Nothing here is talking -- a dead patch of street.{/lightblue-fg}`, note].filter(Boolean).join('\n');
73
73
  }
74
- return [head, ...tags.map(t => ` {cyan-fg}${t}{/cyan-fg}`), note].filter(Boolean).join('\n');
74
+ return [head, ...tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`), note].filter(Boolean).join('\n');
75
75
  }
76
76
  }
77
77
  /**
@@ -105,11 +105,11 @@ export class ArosCommand extends Command {
105
105
  const note = silentAndSeeingNote(actor);
106
106
  const tags = aroTags(this.scene, actor, actor.currentLocation);
107
107
  if (tags.length === 0) {
108
- return [`{cyan-fg}Nothing here is talking -- a dead patch of street.{/cyan-fg}`, note].filter(Boolean).join('\n');
108
+ return [`{lightblue-fg}Nothing here is talking -- a dead patch of street.{/lightblue-fg}`, note].filter(Boolean).join('\n');
109
109
  }
110
110
  return [
111
- `{cyan-fg}AR OVERLAY:{/cyan-fg}`,
112
- ...tags.map(t => ` {cyan-fg}${t}{/cyan-fg}`),
111
+ `{lightblue-fg}AR OVERLAY:{/lightblue-fg}`,
112
+ ...tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`),
113
113
  note,
114
114
  ].filter(Boolean).join('\n');
115
115
  }
@@ -218,6 +218,9 @@ export class AttackCommand extends Command {
218
218
  // holstered dry/jammed piece is not what's being attacked with, so it
219
219
  // must not block a bare-fisted attack the player never asked to fail.
220
220
  const weaponItem = this.actor.equipment.rightHand?.weapon ?? this.actor.equipment.leftHand?.weapon;
221
+ if (this.usesEquippedWeapon() && this.actor.weaponDrawn && weaponItem?.isFirearm() && weaponItem.bricked) {
222
+ return `The ${weaponItem.name} is BRICKED -- its electronics burned out under a Data Spike (p.228). Bench work while you rest brings it back; for now, go in swinging.`;
223
+ }
221
224
  if (this.usesEquippedWeapon() && this.actor.weaponDrawn && weaponItem?.isFirearm() && weaponItem.jammed) {
222
225
  return `The ${weaponItem.name}'s smartlink is LOCKED -- someone cracked your PAN. "reboot" to clear it, or go in swinging.`;
223
226
  }
@@ -25,7 +25,7 @@ export class DelayCommand extends Command {
25
25
  return `Too late to delay -- you've already acted this phase (${budget.ledger()}). "end turn" instead.`;
26
26
  }
27
27
  if (this.scene.isHumanControlled(actor)) {
28
- this.logger.log(`{cyan-fg}You hold your action, watching.{/cyan-fg}`, { actor: actor.name });
28
+ this.logger.log(`{yellow-fg}You hold your action, watching.{/yellow-fg}`, { actor: actor.name });
29
29
  }
30
30
  await enc.delayPhase(actor);
31
31
  return this.scene.isHumanControlled(actor) ? '' : `${actor.name} holds their action.`;
@@ -0,0 +1,331 @@
1
+ import { Command } from './command.js';
2
+ import { hint } from '../utilities/hints.js';
3
+ import { rollPool, formatRoll } from '../utilities/dice.js';
4
+ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
5
+ import { hostsInReach, hostOver, gridVicinity, roomsInReach } from '../utilities/grid-reach.js';
6
+ import { hostLabel, matchesCameraName } from '../utilities/grid-names.js';
7
+ import { isWatched, cameraMasterHost } from '../utilities/surveillance.js';
8
+ import { deviceDefensePool, wanDefensePool, hostDefensePool } from '../utilities/matrix-intrusion.js';
9
+ import { accrueOverwatch } from '../utilities/overwatch.js';
10
+ import { leaveMatrix } from '../utilities/planes.js';
11
+ import { applyDeviceOpened } from '../utilities/devices.js';
12
+ import { gridActionModifier, gridNote } from '../utilities/grids.js';
13
+ import { billAction } from '../utilities/action-cost.js';
14
+ import { MAX_MARKS } from '../utilities/marks.js';
15
+ import { sameHostSide } from '../models/player.js';
16
+ /** The marks Control Device wants for a Simple device action (p.238). */
17
+ export const MARKS_TO_CONTROL = 2;
18
+ export class DisableCommand extends Command {
19
+ static verb = 'disable';
20
+ static description = 'Control Device (SR5 p.238): with 2 marks on a device, order it -- a maglock unlocks, a camera cluster loops, a smartgun\'s clip drops. Electronic Warfare + Intuition [Sleaze] v. Device Rating + Firewall. "disable loud <icon>" is a Data Spike instead (see "brick").';
21
+ /** The Data Spike route: `brick` always, `disable loud` by the word. */
22
+ way(args) {
23
+ const rest = [...args];
24
+ if (rest[0]?.toLowerCase() === 'loud' || rest[0]?.toLowerCase() === 'brick') {
25
+ rest.shift();
26
+ return { way: 'spike', rest };
27
+ }
28
+ if (rest[0]?.toLowerCase() === 'quiet')
29
+ rest.shift();
30
+ return { way: 'control', rest };
31
+ }
32
+ async execute(args = []) {
33
+ const actor = this.actor;
34
+ const { way, rest } = this.way(args);
35
+ const named = rest.join(' ').trim();
36
+ if (!named) {
37
+ return way === 'spike'
38
+ ? `Brick what? Name the icon: "brick <device>", "brick cameras", "brick <person>" for their gun.`
39
+ : `Disable what? Name the icon: "disable <device>", "disable cameras", "disable <person>" for their gun.${hint(` (Control Device wants ${MARKS_TO_CONTROL} marks on it -- "hack <it>" first.)`)}`;
40
+ }
41
+ // REACH: riding the Matrix, a living persona, or AR with a working
42
+ // deck in hand -- the same gate a PAN intrusion uses (hack.ts).
43
+ if (actor.plane === 'astral')
44
+ return `The Matrix doesn't answer to spirits.`;
45
+ if (actor.plane !== 'matrix' && !actor.isTechnomancer() && !actor.getCyberdeck()) {
46
+ return `You'd need to be jacked in, or have a working cyberdeck in hand, to reach a device's icon.`;
47
+ }
48
+ const target = this.resolve(named);
49
+ if (typeof target === 'string')
50
+ return target;
51
+ const bill = billAction(this.scene, actor, 'complex', way === 'spike' ? 'Data Spike' : 'Control Device', { attack: way === 'spike' });
52
+ if (bill)
53
+ return bill;
54
+ return way === 'spike' ? this.dataSpike(target) : this.controlDevice(target);
55
+ }
56
+ // ------------------------------------------------------------ targets --
57
+ resolve(named) {
58
+ const actor = this.actor;
59
+ const raw = named.toLowerCase();
60
+ const rooms = this.rooms ?? this.scene.getRooms();
61
+ // 1. THE CAMERA CLUSTER of the room the persona is over / inside.
62
+ if (matchesCameraName(named)) {
63
+ const room = actor.insideHost ?? (actor.plane === 'matrix' ? gridVicinity(actor) : actor.currentLocation);
64
+ if (!isWatched(room))
65
+ return `No cameras on ${room.name} -- nothing to loop.`;
66
+ return { kind: 'camera', room, master: cameraMasterHost(rooms, room) };
67
+ }
68
+ // 2. A DEVICE ICON: inside the host, its WAN; from the grid, a slaved
69
+ // device you hold a host mark for, or a loose wireless one where
70
+ // the body stands. Mirrors hack.ts tryDeviceHack.
71
+ const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.name.toLowerCase().includes(raw)
72
+ || (d.opensExit ?? '').toLowerCase() === raw
73
+ || d.description.toLowerCase().includes(raw));
74
+ const inside = actor.insideHost;
75
+ if (inside) {
76
+ const matches = matching(inside);
77
+ if (matches.length === 1)
78
+ return { kind: 'device', device: matches[0], room: inside, host: inside, viaWan: false };
79
+ if (matches.length > 1)
80
+ return `More than one thing in here answers to "${named}": ${matches.map(d => d.name).join(', ')}.`;
81
+ }
82
+ else {
83
+ const found = hostsInReach(rooms, actor).map(r => ({ room: r, matches: matching(r) })).filter(x => x.matches.length === 1);
84
+ if (found.length === 1) {
85
+ const host = found[0].room;
86
+ const device = found[0].matches[0];
87
+ if ((host.hostMarksBy.get(actor.name) ?? 0) < 1) {
88
+ return `${device.name} hangs off ${hostLabel(host)}'s WAN -- slaved, and a host's devices answer nobody who holds no mark on the host itself (p.233).${hint(` ("hack ${hostLabel(host)}" for a mark first.)`)}`;
89
+ }
90
+ return { kind: 'device', device, room: host, host, viaWan: true };
91
+ }
92
+ const here = actor.plane === 'matrix' ? gridVicinity(actor) : actor.currentLocation;
93
+ const loose = hostOver(here) ? [] : matching(here).filter(d => d.wireless);
94
+ if (loose.length === 1)
95
+ return { kind: 'device', device: loose[0], room: here, viaWan: false };
96
+ }
97
+ // 3. A PERSON'S GUN, broken out of their PAN (p.219). Same reach as
98
+ // a PAN intrusion: bodies on this side of the host wall.
99
+ const reach = (actor.plane === 'matrix' && !actor.insideHost)
100
+ ? roomsInReach(rooms, actor)
101
+ : [{ room: actor.currentLocation, noise: 0 }];
102
+ const bodies = reach.flatMap(({ room: r }) => this.scene.getActorsInRoom(r)
103
+ .filter(a => a !== actor && a.plane === 'meat' && !a.isIncapacitated() && sameHostSide(actor, a)));
104
+ const picked = fuzzyPickName(named, bodies.map(a => a.name));
105
+ const owner = picked ? bodies.find(a => a.name === picked) : undefined;
106
+ if (owner) {
107
+ const gun = owner.equipment.rightHand?.weapon ?? owner.equipment.leftHand?.weapon;
108
+ if (!gun || !gun.isFirearm())
109
+ return `${owner.name} has no firearm on the grid to work -- nothing wireless in their hands.`;
110
+ return { kind: 'gun', owner, gun };
111
+ }
112
+ return `Nothing on the grid answers to "${named}".${hint(` ("look" lists the icons in reach.)`)}`;
113
+ }
114
+ marksOn(t) {
115
+ const me = this.actor.name;
116
+ if (t.kind === 'device')
117
+ return t.device.marksBy.get(me) ?? 0;
118
+ if (t.kind === 'camera')
119
+ return t.master?.hostMarksBy.get(me) ?? 0;
120
+ return t.owner.panMarksBy.get(me) ?? 0;
121
+ }
122
+ nameOf(t) {
123
+ return t.kind === 'device' ? t.device.name : t.kind === 'camera' ? `the camera cluster on ${t.room.name}` : `${t.owner.name}'s ${t.gun.name}`;
124
+ }
125
+ /** What defends: DR + Firewall for an unattended device (the master's
126
+ * where higher, seen from the grid), the host for its cameras, the
127
+ * owner's Intuition + Firewall for a gun on their PAN. */
128
+ defence(t) {
129
+ if (t.kind === 'device') {
130
+ return t.viaWan && t.host?.host ? wanDefensePool(t.device, t.host.host) : deviceDefensePool(t.device);
131
+ }
132
+ if (t.kind === 'camera') {
133
+ return t.master?.host ? hostDefensePool(t.master.host) : { label: 'Device Rating + Firewall', pool: 4 };
134
+ }
135
+ return { label: 'Intuition + Firewall', pool: t.owner.getPanDefensePool('sleaze') };
136
+ }
137
+ godBill(defenseHits, why) {
138
+ const bill = accrueOverwatch(this.scene, this.actor, defenseHits, why);
139
+ if (bill.converged && this.actor.plane === 'matrix') {
140
+ bill.lines.push(...leaveMatrix(this.scene, this.actor, { forced: true, reason: `GOD force-reboots your persona --` }));
141
+ }
142
+ return bill.lines;
143
+ }
144
+ /** The owner of the thing learns something went wrong. */
145
+ wakeOwner(t, made) {
146
+ const actor = this.actor;
147
+ actor.sneaking = false;
148
+ const host = t.kind === 'device' ? t.host : t.kind === 'camera' ? t.master : undefined;
149
+ if (host) {
150
+ host.hostAlert = true;
151
+ if (made)
152
+ host.hostMarksOn.set(actor.name, Math.min(MAX_MARKS, (host.hostMarksOn.get(actor.name) ?? 0) + 1));
153
+ }
154
+ if (t.kind === 'gun' && made) {
155
+ actor.panMarksBy.set(t.owner.name, Math.min(MAX_MARKS, (actor.panMarksBy.get(t.owner.name) ?? 0) + 1));
156
+ t.owner.performAction('notices a Matrix probe', 'their link flashing an intrusion warning');
157
+ }
158
+ this.scene.updateStatus();
159
+ }
160
+ // ------------------------------------------------------ Control Device --
161
+ controlDevice(t) {
162
+ const actor = this.actor;
163
+ const name = this.nameOf(t);
164
+ const marks = this.marksOn(t);
165
+ if (marks < MARKS_TO_CONTROL) {
166
+ const how = t.kind === 'device' ? `"hack ${t.device.name}"` : t.kind === 'camera' ? `"hack ${t.master ? hostLabel(t.master) : 'the host'}"` : `"hack ${t.owner.name}"`;
167
+ return `${name} takes orders from ${MARKS_TO_CONTROL} marks and you hold ${marks} (Control Device, p.238: a Simple device action wants two).${hint(` (${how} places them; or "brick" it -- no marks, but loud, and a bricked lock stays locked.)`)}`;
168
+ }
169
+ const burned = actor.burnedAttributeRefusal('sleaze');
170
+ if (burned)
171
+ return burned;
172
+ // ELECTRONIC WARFARE + INTUITION [SLEAZE] v. Intuition + Firewall
173
+ // (p.238), the no-test device action's test. Unskilled at -1 (p.130).
174
+ const ew = actor.skillRating('electronic-warfare');
175
+ const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, t.kind === 'device' ? t.host?.host?.grid : t.kind === 'camera' ? t.master?.host?.grid : undefined);
176
+ const pool = Math.max(1, actor.intuition + (ew > 0 ? ew : -1) + actor.bonus('hacking') + actor.bonus('matrix-sleaze')
177
+ + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty + grid.total);
178
+ const limit = actor.matrixAttribute('sleaze');
179
+ const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
180
+ const defence = this.defence(t);
181
+ const defenceRoll = rollPool(defence.pool);
182
+ const net = roll.hits - defenceRoll.hits;
183
+ actor.performAction('takes control of a device', name, { quiet: this.scene.isHumanControlled(actor) });
184
+ if (this.scene.isHumanControlled(actor)) {
185
+ this.logger.meta(`Control Device (Electronic Warfare + Intuition [Sleaze])${gridNote(grid)}: ${formatRoll(roll)}`);
186
+ this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
187
+ }
188
+ const godLines = this.godBill(defenceRoll.hits, 'Control Device (sleaze)');
189
+ if (net <= 0) {
190
+ // A blown Sleaze is noticed (p.236): the owner marks you and the
191
+ // host it hangs off is awake.
192
+ this.wakeOwner(t, true);
193
+ const who = t.kind === 'gun' ? `${t.owner.name}'s firewall holds a MARK on your persona now` : `whoever owns it has your icon on file now`;
194
+ return [`${name} refuses the order and SAYS SO -- ${who}.`, ...godLines].join('\n');
195
+ }
196
+ const lines = [];
197
+ if (t.kind === 'device') {
198
+ // THE ORDER: it opens. Same aftermath as a key or a forced door
199
+ // (utilities/devices.ts) -- the exit unseals in the meat.
200
+ const opened = t.device.force(true);
201
+ lines.push(`${t.device.name} takes the order as its owner's: ${opened.message}`);
202
+ lines.push(...applyDeviceOpened(this.scene, actor, t.room, t.device, 'disable'));
203
+ }
204
+ else if (t.kind === 'camera') {
205
+ t.room.camerasLooped = true;
206
+ lines.push(`The cluster on ${t.room.name} takes the order: every lens loops on an empty hallway. Security sees NOTHING there now.${hint(` ("snoop ${t.room.name}" rides the real feed.)`)}`);
207
+ this.scene.addWorldEvent(`An unseen hand looped the cameras on ${t.room.name}.`);
208
+ }
209
+ else {
210
+ t.gun.jammed = true;
211
+ lines.push(`${t.owner.name}'s ${t.gun.name} takes the order: the smartlink seizes and the clip drops -- LOCKED until they reboot.`);
212
+ t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} locking up in their hand`);
213
+ this.scene.addWorldEvent(`An unseen hand seized ${t.owner.name}'s ${t.gun.name}.`);
214
+ }
215
+ this.scene.updateStatus();
216
+ return [...lines, ...godLines].join('\n');
217
+ }
218
+ // ----------------------------------------------------------- Data Spike --
219
+ dataSpike(t) {
220
+ const actor = this.actor;
221
+ const name = this.nameOf(t);
222
+ const burned = actor.burnedAttributeRefusal('attack');
223
+ if (burned)
224
+ return burned;
225
+ if (t.kind === 'device' && t.device.isBricked)
226
+ return `${t.device.name} is already bricked -- dead boards, nothing left to spike.`;
227
+ if (t.kind === 'camera' && t.room.camerasBricked)
228
+ return `The cluster on ${t.room.name} is already dead glass.`;
229
+ if (t.kind === 'gun' && t.gun.bricked)
230
+ return `${t.owner.name}'s ${t.gun.name} is already bricked.`;
231
+ // CYBERCOMBAT + LOGIC [ATTACK] v. Intuition + Firewall (p.239).
232
+ const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, t.kind === 'device' ? t.host?.host?.grid : t.kind === 'camera' ? t.master?.host?.grid : undefined);
233
+ const pool = Math.max(1, actor.getAttackPool() + actor.matrixActionPenalty + actor.bonus('matrix-attack') + grid.total);
234
+ const limit = actor.matrixAttribute('attack');
235
+ const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
236
+ const defence = this.defence(t);
237
+ const defenceRoll = rollPool(defence.pool);
238
+ const net = roll.hits - defenceRoll.hits;
239
+ actor.performAction('throws a Data Spike', name, { quiet: this.scene.isHumanControlled(actor) });
240
+ if (this.scene.isHumanControlled(actor)) {
241
+ this.logger.meta(`Data Spike (Cybercombat + Logic [Attack ${limit}])${gridNote(grid)}: ${formatRoll(roll)}`);
242
+ this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
243
+ }
244
+ const godLines = this.godBill(defenceRoll.hits, 'Data Spike (attack)');
245
+ if (net <= 0) {
246
+ // A failed Attack action is never noticed (p.236), but the
247
+ // target's software rejects the code and sends it back: one
248
+ // unresistable box per net hit the defender got (Data Trails p.181).
249
+ const rebound = Math.max(0, -net);
250
+ const lines = [`${name} shrugs the spike off${rebound > 0 ? ' -- and sends it back' : ''}.`];
251
+ if (rebound > 0) {
252
+ if (actor.isTechnomancer()) {
253
+ actor.takeStun(rebound);
254
+ lines.push(` Rejected code burns back through your living persona -- ${rebound} stun: ${actor.stunSummary()}`);
255
+ }
256
+ else {
257
+ actor.takeMatrixDamage(rebound);
258
+ lines.push(` Rejected code burns back into your deck -- ${rebound} box${rebound === 1 ? '' : 'es'}, unresisted: ${actor.matrixSummary()}`);
259
+ if (actor.matrixCrashed)
260
+ lines.push(...leaveMatrix(this.scene, actor, { forced: true, reason: `Your own spike bricks the deck under you.` }));
261
+ }
262
+ }
263
+ lines.push(` Nobody upstairs noticed -- a spike that doesn't land leaves no trace.`);
264
+ return [...lines, ...godLines].join('\n');
265
+ }
266
+ // DV = Attack + net hits, +2 per mark held (p.239); resisted with
267
+ // Device Rating + Firewall (p.228).
268
+ const marks = this.marksOn(t);
269
+ const dv = limit + net + 2 * marks;
270
+ const soak = rollPool(this.defence(t).pool);
271
+ const dealt = Math.max(0, dv - soak.hits);
272
+ if (this.scene.isHumanControlled(actor)) {
273
+ this.logger.meta(`Data Spike -- ${dv} DV (Attack ${limit} +${net} net${marks > 0 ? ` +${2 * marks} for ${marks} mark${marks === 1 ? '' : 's'}` : ''}) v. ${defence.label}: ${formatRoll(soak)} -> ${dealt}`);
274
+ }
275
+ // An Attack action is never subtle: the owner knows.
276
+ this.wakeOwner(t, false);
277
+ const lines = [];
278
+ if (dealt <= 0) {
279
+ lines.push(`${name} takes the spike and its firewall holds -- barely. It knows it was hit.`);
280
+ return [...lines, ...godLines].join('\n');
281
+ }
282
+ if (t.kind === 'device') {
283
+ t.device.takeMatrixDamage(dealt);
284
+ lines.push(`The spike lands on ${t.device.name} -- ${dealt} box${dealt === 1 ? '' : 'es'} (${t.device.matrixSummary()}).`);
285
+ if (t.device.isBricked) {
286
+ lines.push(t.device.kind === 'lock'
287
+ ? ` ${t.device.name} BRICKS -- sparks, a pop, a smell of hot plastic. And the lock STAYS LOCKED (p.228): a dead maglock holds like a bolt.${hint(` (Marks and "disable" would have opened it; now it is "pick" or "breach".)`)}`
288
+ : ` ${t.device.name} BRICKS -- sparks, a pop, dead electronics. Whatever it was doing, it has stopped.`);
289
+ this.scene.addWorldEvent(`${actor.name} bricked ${t.device.name} in ${t.room.name} with a Data Spike.`);
290
+ }
291
+ }
292
+ else if (t.kind === 'camera') {
293
+ // The cluster has no Device of its own to hold a monitor; a hit
294
+ // that lands past the firewall kills it. Engine ruling.
295
+ t.room.camerasBricked = true;
296
+ lines.push(`The spike lands on the cluster -- ${dealt} box${dealt === 1 ? '' : 'es'} -- and every lens on ${t.room.name} goes to static and stays there. Dead glass.`);
297
+ this.scene.addWorldEvent(`${actor.name} bricked the cameras on ${t.room.name} with a Data Spike.`);
298
+ }
299
+ else {
300
+ // A gun is a Rating 2 device with a monitor of 9 (p.228, the
301
+ // book's own smartgun example): a spike that lands past its
302
+ // firewall for that much bricks it outright.
303
+ const monitor = 8 + 1;
304
+ if (dealt >= monitor) {
305
+ t.gun.bricked = true;
306
+ lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- it sparks, crackles and smokes in their hand. BRICKED: it will not fire again until somebody's bench work.`);
307
+ t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} smoking in their hand`);
308
+ }
309
+ else {
310
+ t.gun.jammed = true;
311
+ lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- ${dealt} box${dealt === 1 ? '' : 'es'}: the smartlink seizes and LOCKS. It boots again on a reboot; another spike like that bricks it.`);
312
+ t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} locking up in their hand`);
313
+ }
314
+ this.scene.addWorldEvent(`${actor.name} spiked ${t.owner.name}'s ${t.gun.name}.`);
315
+ }
316
+ this.scene.updateStatus();
317
+ return [...lines, ...godLines].join('\n');
318
+ }
319
+ }
320
+ /** `brick <icon>`: the Data Spike, by its street name. */
321
+ export class BrickCommand extends DisableCommand {
322
+ static verb = 'brick';
323
+ static description = 'Data Spike (SR5 p.239): Cybercombat + Logic [Attack] v. Device Rating + Firewall, no marks needed, never subtle. Fill a device\'s Matrix condition monitor (8 + DR/2, p.228) and it BRICKS -- a camera goes dead, a smartgun sparks and dies; a lock stays locked. A miss burns your own deck.';
324
+ way(args) {
325
+ const rest = [...args];
326
+ if (rest[0]?.toLowerCase() === 'loud')
327
+ rest.shift();
328
+ return { way: 'spike', rest };
329
+ }
330
+ }
331
+ //# sourceMappingURL=disable.js.map
@@ -92,7 +92,7 @@ export class DownloadCommand extends Command {
92
92
  // is that nobody could find the file should say what it can see.
93
93
  const query = args.join(' ').trim();
94
94
  if (query.length === 0) {
95
- const lines = [`{cyan-fg}Within reach to pull down:{/cyan-fg}`];
95
+ const lines = [`{lightblue-fg}Within reach to pull down:{/lightblue-fg}`];
96
96
  for (const i of loose)
97
97
  lines.push(` ${planeTintItemName(i, actor.plane)} -- loose on the grid here`);
98
98
  for (const c of carried)
@@ -134,11 +134,11 @@ export class DownloadCommand extends Command {
134
134
  this.scene.updateStatus();
135
135
  return fromSprite
136
136
  ? [
137
- `{cyan-fg}${fromSprite.npc.name} opens its hands and the data pours out of them.{/cyan-fg}`,
137
+ `{lightblue-fg}${fromSprite.npc.name} opens its hands and the data pours out of them.{/lightblue-fg}`,
138
138
  `You pull {bold}{underline}${item.name}{/underline}{/bold} down onto a chip -- meat-world cargo now.${hint(` (You can "give ${item.name}" to somebody standing in front of you.)`)}`,
139
139
  ].join('\n')
140
140
  : [
141
- `{cyan-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down off the grid -- written to a chip.{/cyan-fg}`,
141
+ `{lightblue-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down off the grid -- written to a chip.{/lightblue-fg}`,
142
142
  hint(`(Meat-world cargo now: carry it, stash it, or "give" it to somebody.)`),
143
143
  ].join('\n');
144
144
  }
@@ -49,7 +49,7 @@ export class EndTurnCommand extends Command {
49
49
  // Logged now rather than returned: the phases that follow announce
50
50
  // themselves while this command is still running, and a return
51
51
  // value would land under them, out of order.
52
- this.logger.log(`{cyan-fg}You end your Action Phase${budget && budget.spent.length > 0 ? ` -- ${ledger}` : ' -- nothing spent'}.{/cyan-fg}`, { actor: actor.name });
52
+ this.logger.log(`{yellow-fg}You end your Action Phase${budget && budget.spent.length > 0 ? ` -- ${ledger}` : ' -- nothing spent'}.{/yellow-fg}`, { actor: actor.name });
53
53
  }
54
54
  await enc.endPhase(actor);
55
55
  return this.scene.isHumanControlled(actor) ? '' : `${actor.name} ends their Action Phase.`;
@@ -64,7 +64,7 @@ export class EnterHostCommand extends Command {
64
64
  const marks = target.hostMarksBy.get(actor.name) ?? 0;
65
65
  if (marks < 1 && !target.hostCracked && !target.hostSanctioned) {
66
66
  return [
67
- `{cyan-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/cyan-fg}`,
67
+ `{lightblue-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/lightblue-fg}`,
68
68
  hint(`A host opens to anyone holding a MARK on it (p.239) -- "mark ${target.name}" softens it, or "hack" takes one by force.`),
69
69
  ].join('\n');
70
70
  }
@@ -79,14 +79,15 @@ export class EnterHostCommand extends Command {
79
79
  // you there). currentLocation is never written -- it is the body's.
80
80
  actor.matrixPosition = { kind: 'host', host: target.host, from: actor.currentGrid };
81
81
  movePersonaWith(this.game ?? undefined, actor);
82
- actor.performAction('enters a host', `icon crossing into ${target.name}'s architecture`);
82
+ actor.performAction('enters a host', `icon crossing into ${target.name}'s architecture`, { quiet: this.scene.isHumanControlled(actor) });
83
83
  this.logger.write(`${actor.name} entered the host at ${target.name}.`);
84
84
  this.scene.updateStatus();
85
+ // ONE LINE (2026-09-13, "too much text"): the crossing, and the
86
+ // coaching behind the hints toggle.
85
87
  return [
86
- `{cyan-fg}You cross into ${hostLabel(target, { capital: false })}.{/cyan-fg}`,
87
- `{cyan-fg}The outside grid folds shut behind you -- the icons in reach, the PANs, the personas out on the grid: all of it gone, not hidden. In here there is only what the host keeps.{/cyan-fg}`,
88
- hint(`"look" reads the space you're standing in now. "exit" puts you back on the grid.`),
89
- ].join('\n');
88
+ `{lightblue-fg}You cross into ${hostLabel(target, { capital: false })} -- the outside grid folds shut behind you.{/lightblue-fg}`,
89
+ hint(`(In here there is only what the host keeps. "look" reads it; "exit" puts you back on the grid.)`),
90
+ ].filter(Boolean).join('\n');
90
91
  }
91
92
  /**
92
93
  * The host this verb means. Bare `enter` takes the one under your feet
@@ -109,14 +110,14 @@ export class EnterHostCommand extends Command {
109
110
  const over = hostOver(gridVicinity(this.actor)) ?? hostOver(here);
110
111
  if (over)
111
112
  return over;
112
- return `{cyan-fg}Thin grid here -- no host stands over ${gridVicinity(this.actor).name}.{/cyan-fg} ${hint(`Overhead: ${roster} -- "enter <name>" crosses into any of them from here.`)}`;
113
+ return `{lightblue-fg}Thin grid here -- no host stands over ${gridVicinity(this.actor).name}.{/lightblue-fg} ${hint(`Overhead: ${roster} -- "enter <name>" crosses into any of them from here.`)}`;
113
114
  }
114
115
  const host = resolveHostInReach(rooms, this.actor, named);
115
116
  if (host)
116
117
  return host;
117
118
  const street = Object.values(rooms).find(r => r.name.toLowerCase() === named.toLowerCase());
118
119
  if (street)
119
- return `{cyan-fg}${street.name} has no host over it -- nothing to enter. Overhead: ${roster}.{/cyan-fg}`;
120
+ return `{lightblue-fg}${street.name} has no host over it -- nothing to enter. Overhead: ${roster}.{/lightblue-fg}`;
120
121
  return `No host called "${named}" overhead. ${hint(`Overhead: ${roster}.`)}`;
121
122
  }
122
123
  }
@@ -148,13 +149,10 @@ export class ExitHostCommand extends Command {
148
149
  const from = actor.matrixPosition?.kind === 'host' ? actor.matrixPosition.from : undefined;
149
150
  actor.matrixPosition = { kind: 'grid', grid: from };
150
151
  movePersonaWith(this.game ?? undefined, actor);
151
- actor.performAction('exits a host', `icon surfacing out of ${host.name}'s architecture`);
152
+ actor.performAction('exits a host', `icon surfacing out of ${host.name}'s architecture`, { quiet: this.scene.isHumanControlled(actor) });
152
153
  this.logger.write(`${actor.name} exited the host at ${host.name}.`);
153
154
  this.scene.updateStatus();
154
- return [
155
- `{cyan-fg}You step back out of ${hostLabel(host, { capital: false })} and onto the grid -- your body still sits in ${actor.currentLocation.name}.{/cyan-fg}`,
156
- `{cyan-fg}The host seals behind you: one icon again, whatever you saw in there now on the other side of its wall.{/cyan-fg}`,
157
- ].join('\n');
155
+ return `{lightblue-fg}You step back out of ${hostLabel(host, { capital: false })} onto the grid; the host seals behind you. Your body still sits in ${actor.currentLocation.name}.{/lightblue-fg}`;
158
156
  }
159
157
  }
160
158
  //# sourceMappingURL=enter-host.js.map
@@ -173,7 +173,7 @@ export class HackCommand extends BypassCommand {
173
173
  const burned = this.actor.burnedAttributeRefusal(mode === 'attack' ? 'attack' : 'sleaze');
174
174
  if (burned)
175
175
  return burned;
176
- this.actor.performAction('hacks the host', room.name);
176
+ this.actor.performAction('hacks the host', room.name, { quiet: this.scene.isHumanControlled(this.actor) });
177
177
  const attempt = await this.hostIntrusionRoll(mode, declared, room);
178
178
  const success = attempt.success;
179
179
  // GOD BILLS THE DEFENDER'S HITS (p.231-232): "your OS increases by
@@ -276,7 +276,7 @@ export class HackCommand extends BypassCommand {
276
276
  if (marksNow >= 1)
277
277
  lines.push(` One mark is the door: "enter" walks your persona inside (p.239).`);
278
278
  if (perception > 0)
279
- lines.push(` {cyan-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/cyan-fg}`);
279
+ lines.push(` {lightblue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/lightblue-fg}`);
280
280
  if (forcedDv > 0)
281
281
  lines.push(` The forcing burns ${forcedDv} DV into the architecture -- a host has no boards to brick, so it only announces you louder.`);
282
282
  lines.push(` ${MAX_MARKS - marksNow} more and its architecture stops asking who you are.${hint(` ("hack for ${MAX_MARKS - marksNow}" reaches for the rest at once -- ${declarationPenalty(MAX_MARKS - marksNow, this.actor.hasQuality(GO_BIG_QUALITY))} dice.)`)}`);
@@ -340,13 +340,13 @@ export class HackCommand extends BypassCommand {
340
340
  lines.push(` ${iconLocks.length} ${iconLocks.length === 1 ? 'lock hangs' : 'locks hang'} off it, still sealed -- each its own icon behind its own rating: ${iconLocks.map(d => d.name).join(', ')}.${hint(` ("hack <lock>" from in here keys one; the host's key is not the door's.)`)}`);
341
341
  }
342
342
  if (vaultedFiles.length > 0) {
343
- lines.push(` {cyan-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/cyan-fg}`);
343
+ lines.push(` {lightblue-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/lightblue-fg}`);
344
344
  }
345
345
  // AND WHAT THE CRACK DID NOT REACH: a wired site's data sits on an
346
346
  // air-gapped box the host never held (p.233) -- say so, or the next
347
347
  // "take" reads as a contradiction.
348
348
  if (room.offlineServer && room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
349
- lines.push(` {cyan-fg}Nothing on the grid side of the vault -- whatever this place keeps sits on the ${room.offlineServer}, an air-gapped box with no Matrix presence. Your BODY has to be in ${room.name} to cable in (p.233).{/cyan-fg}`);
349
+ lines.push(` {lightblue-fg}Nothing on the grid side of the vault -- whatever this place keeps sits on the ${room.offlineServer}, an air-gapped box with no Matrix presence. Your BODY has to be in ${room.name} to cable in (p.233).{/lightblue-fg}`);
350
350
  }
351
351
  // THE FEEDS ARE A REAL GAIN AND WERE NEVER MENTIONED
352
352
  // (BpR5cjhYuvgCyDSZc, 7aNWnDSHeZMYmS2De: "what have I gained?").
@@ -540,7 +540,7 @@ export class HackCommand extends BypassCommand {
540
540
  ? Math.max(1, actor.getAttackPool() + silence + actor.bonus('matrix-attack') + declarePenalty)
541
541
  : Math.max(1, actor.logic + actor.skillRating('hacking') + actor.bonus('hacking') + actor.bonus('matrix-sleaze')
542
542
  + silence + actor.woundModifier - actor.sustainingPenalty + declarePenalty);
543
- actor.performAction('works a slaved icon', device.name);
543
+ actor.performAction('works a slaved icon', device.name, { quiet: this.scene.isHumanControlled(actor) });
544
544
  const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
545
545
  // From the grid the slave borrows its master's numbers where they are
546
546
  // higher (p.233); inside, it stands alone (Data Trails p.87-88).
@@ -606,18 +606,18 @@ export class HackCommand extends BypassCommand {
606
606
  else {
607
607
  const perception = freeMatrixPerceptionHits(netHits);
608
608
  if (perception > 0)
609
- lines.push(` {cyan-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what it guards (p.240).{/cyan-fg}`);
609
+ lines.push(` {lightblue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what it guards (p.240).{/lightblue-fg}`);
610
610
  }
611
- // ONE MARK COMMANDS IT. Opening a door is a Free action, and p.238
612
- // puts a Free action at one mark -- so the key IS the door, and
613
- // there is no second roll to make.
614
- if (now >= 1 && !device.isOpen()) {
615
- // NAME ONLY A VERB THAT DISPATCHES. An earlier draft offered
616
- // "control <device>", which is not a command this game has --
617
- // caught by advertised-verbs-structured.test.ts, which exists for
618
- // exactly that. `open` is the real one, and it is a MEAT verb: the
619
- // key is placed from in here, the door swings out there.
620
- lines.push(` That is enough to work it: ${device.opensExit ? `the ${device.opensExit} way` : device.name} answers to you now.${hint(` ("open ${device.opensExit ?? device.name}" once your body is back in the room.)`)}`);
611
+ // TWO MARKS COMMAND IT (p.238 Control Device: 1 mark for a Free
612
+ // action, 2 for a Simple, 3 for a Complex -- and unlocking a maglock
613
+ // is the book's own example of the no-test device action, a Simple
614
+ // "Use Simple Device", p.165). The verb is `disable`
615
+ // (commands/disable.ts): the key is turned from in here, the door
616
+ // swings out there. One mark short says so.
617
+ if (!device.isOpen()) {
618
+ lines.push(now >= 2
619
+ ? ` That is enough to command it: ${hint(`"disable ${device.name}" (Control Device, p.238) ${device.opensExit ? `opens the ${device.opensExit} way` : `works it`} from right here.`)}`
620
+ : ` One more mark and it takes orders.${hint(` ("hack ${device.name}" again, then "disable ${device.name}" -- Control Device wants 2 marks for a Simple device action, p.238.)`)}`);
621
621
  }
622
622
  this.scene.updateStatus();
623
623
  return [...lines, ...godLines].join('\n');
@@ -765,7 +765,7 @@ export class HackCommand extends BypassCommand {
765
765
  // Intuition + Firewall against a sleaze, Willpower + Firewall
766
766
  // against a smash (p.240 / p.238), and no limit on either.
767
767
  const defense = rollPool(target.getPanDefensePool(mode === 'attack' ? 'attack' : 'sleaze'));
768
- actor.performAction('runs a PAN intrusion', target.name);
768
+ actor.performAction('runs a PAN intrusion', target.name, { quiet: this.scene.isHumanControlled(actor) });
769
769
  this.logger.write(`${actor.name} PAN-hacks ${target.name}: ${attack.hits} vs ${defense.hits} (silent=${target.runningSilent}).`);
770
770
  // Dice anatomy to the Mechanics ticker (readability pass) --
771
771
  // player-gated like every roll site: an NPC's probe isn't your dice.
@@ -53,7 +53,7 @@ export class HopCommand extends Command {
53
53
  actor.performAction('hops grids', `persona blinking across to ${target.name}`);
54
54
  this.logger.write(`${actor.name} hopped to ${target.key} (legal, ${tier}).`);
55
55
  this.scene.updateStatus();
56
- return `{cyan-fg}The sky changes texture and you're on ${target.name}.{/cyan-fg}${target.userPenalty ? ` ${hint(`Public grid: -2 on everything you do out here.`)}` : ''}`;
56
+ return `{lightblue-fg}The sky changes texture and you're on ${target.name}.{/lightblue-fg}${target.userPenalty ? ` ${hint(`Public grid: -2 on everything you do out here.`)}` : ''}`;
57
57
  }
58
58
  // ILLEGAL HOP (p.240): Hack on the Fly against the grid itself.
59
59
  const burned = actor.burnedAttributeRefusal('sleaze');
@@ -87,7 +87,7 @@ export class HopCommand extends Command {
87
87
  this.logger.write(`${actor.name} hopped to ${target.key} (illegal, ${attempt.hits} v ${defence.hits}).`);
88
88
  this.scene.updateStatus();
89
89
  return [
90
- `{cyan-fg}You slip through a seam nobody sold you and come up on ${target.name}.{/cyan-fg}`,
90
+ `{lightblue-fg}You slip through a seam nobody sold you and come up on ${target.name}.{/lightblue-fg}`,
91
91
  ...bill.lines,
92
92
  ].join('\n');
93
93
  }