@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
@@ -379,7 +379,7 @@ export class LookCommand extends Command {
379
379
  const dim = this.scene.getActorsInRoom(far)
380
380
  .find(a => a !== this.actor && a.name.toLowerCase() === target.toLowerCase() && this.actor.canPerceive(a));
381
381
  if (dim)
382
- return `${this.inspectPersona(dim)}\n{cyan-fg} -- a far icon, ${far.name}${noiseNote(noise)}{/cyan-fg}`;
382
+ return `${this.inspectPersona(dim)}\n{lightblue-fg} -- a far icon, ${far.name}${noiseNote(noise)}{/lightblue-fg}`;
383
383
  }
384
384
  }
385
385
  // 3. THE HOST, as a thing you can name. gridIcons already renders it
@@ -396,8 +396,8 @@ export class LookCommand extends Command {
396
396
  // `deck` reads out your own array.
397
397
  const a = namedHost.hostAttributes();
398
398
  return [
399
- `{cyan-fg}${hostIcon}{/cyan-fg}`,
400
- `{cyan-fg} Attack ${a.attack} Sleaze ${a.sleaze} Data Processing ${a.dataProcessing} Firewall ${a.firewall}{/cyan-fg}`,
399
+ `{lightblue-fg}${hostIcon}{/lightblue-fg}`,
400
+ `{lightblue-fg} Attack ${a.attack} Sleaze ${a.sleaze} Data Processing ${a.dataProcessing} Firewall ${a.firewall}{/lightblue-fg}`,
401
401
  ].join('\n');
402
402
  }
403
403
  }
@@ -409,7 +409,7 @@ export class LookCommand extends Command {
409
409
  if (matchesCameraName(target)) {
410
410
  const eye = gridIcons(this.scene, this.actor, room).find(i => i.startsWith('◎'));
411
411
  if (eye)
412
- return `{cyan-fg}${eye}{/cyan-fg}`;
412
+ return `{lightblue-fg}${eye}{/lightblue-fg}`;
413
413
  // Named correctly, and there is simply nothing here to name.
414
414
  // Worth its own line: the old answer said the WORDS were wrong,
415
415
  // which sent the player hunting for a synonym that never
@@ -525,7 +525,7 @@ export class LookCommand extends Command {
525
525
  * there's a deck anywhere in the shape of it.
526
526
  */
527
527
  inspectPersona(target) {
528
- const lines = [`{cyan-fg}◆ ${target.name} -- a persona on this grid.{/cyan-fg}`];
528
+ const lines = [`{lightblue-fg}◆ ${target.name} -- a persona on this grid.{/lightblue-fg}`];
529
529
  const companion = this.scene.ownerGame?.companions.find(c => c.npc === target);
530
530
  if (companion) {
531
531
  lines.push(`Yours -- it goes where you go, and it answers when you speak.`);
@@ -93,7 +93,7 @@ export class MapCommand extends Command {
93
93
  actor.insideHost
94
94
  ? `THE GRID -- INSIDE ${hostLabel(actor.insideHost, { capital: true }).toUpperCase()}`
95
95
  : `THE GRID -- ${actor.currentGrid ? `on ${actor.currentGrid.name}, ` : ''}jacked in from ${body.name}`,
96
- `{cyan-fg}Black flatland under a black sky. No streets out here -- only icons. The hosts hang overhead; ${actor.insideHost ? '"exit" puts you back under them' : '"go <host>" (or "go <number>") crosses into one you hold a mark on, from anywhere'}.{/cyan-fg}`,
96
+ `{lightblue-fg}Black flatland under a black sky. No streets out here -- only icons. The hosts hang overhead; ${actor.insideHost ? '"exit" puts you back under them' : '"go <host>" (or "go <number>") crosses into one you hold a mark on, from anywhere'}.{/lightblue-fg}`,
97
97
  ];
98
98
  if (hosts.length === 0) {
99
99
  lines.push(' Nothing overhead -- no host stands over this district.');
@@ -51,7 +51,7 @@ export class MarkCommand extends Command {
51
51
  screen: this.screen,
52
52
  game: this.game,
53
53
  }).execute(args);
54
- return `{cyan-fg}Placing a mark IS ${action} -- running it (p.238/p.240).{/cyan-fg}\n${result}`;
54
+ return `{lightblue-fg}Placing a mark IS ${action} -- running it (p.238/p.240).{/lightblue-fg}\n${result}`;
55
55
  }
56
56
  }
57
57
  //# sourceMappingURL=mark.js.map
@@ -163,6 +163,19 @@ export class RestCommand extends Command {
163
163
  lines.push(` Bench work on the ${deck.name}: ${formatRoll(repairRoll)}`);
164
164
  lines.push(` ${repaired} box${repaired === 1 ? '' : 'es'} repaired: ${deck.deckSummary()}${wasBricked && !deck.isBricked ? ' -- it boots again.' : ''}`);
165
165
  }
166
+ // BRICKED GEAR (p.228 Repairing Matrix Damage): an hour with a
167
+ // toolkit and Hardware + Logic -- the rest is the hour, and every
168
+ // hit either clears boxes or halves the time, so by the end of a
169
+ // night's rest a bricked smartgun boots again. A reboot never does
170
+ // this; that is the difference between bricked and jammed.
171
+ const brickedGear = [
172
+ ...actor.inventory.getAllItems(),
173
+ actor.equipment.rightHand?.weapon, actor.equipment.leftHand?.weapon,
174
+ ].filter((i) => !!i && i.bricked);
175
+ for (const gear of new Set(brickedGear)) {
176
+ gear.bricked = false;
177
+ lines.push(` Bench work on the ${gear.name}: the burnt boards come out, and it powers up again.`);
178
+ }
166
179
  for (const drone of damagedDrones) {
167
180
  const wasWrecked = drone.isWrecked;
168
181
  const repairRoll = rollPool(actor.logic + actor.skillRating('hardware') + actor.bonus('piloting'));
@@ -2,7 +2,9 @@ import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { planeTintItemName, emphasizeFind, objectiveMark, fileReach } from '../utilities/planes.js';
4
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
5
- import { perceptionTest } from '../utilities/perception.js';
5
+ import { perceptionTest, matrixSearchTest, matrixSearchSeconds, MATRIX_SEARCH } from '../utilities/perception.js';
6
+ import { gridVicinity } from '../utilities/grid-reach.js';
7
+ import { hostLabel } from '../utilities/grid-names.js';
6
8
  import { canReach, exitSpot, spotsActive, revealWithPerception, theSpot, sealedRouteTo, } from '../utilities/spots.js';
7
9
  import { placeName } from '../utilities/log-style.js';
8
10
  import { billAction, encounterOf } from '../utilities/action-cost.js';
@@ -135,6 +137,73 @@ ${coach}`;
135
137
  export class SearchCommand extends Command {
136
138
  static verb = 'search';
137
139
  static description = 'Search for items in your area';
140
+ /**
141
+ * MATRIX SEARCH (SR5 p.241, player ruling 2026-09-13: "a true matrix
142
+ * search, not perception, revealing what is inside a host if it's
143
+ * cracked open").
144
+ *
145
+ * A Special action: Computer + Intuition [Data Processing] against a
146
+ * threshold from the Matrix Search Table, and it takes TIME -- one
147
+ * minute inside a host whatever the information, net hits over the
148
+ * threshold dividing it, a miss spending it all, a Browse program
149
+ * halving it (utilities/perception.ts). The time is real: it moves
150
+ * the persona's Overwatch clock back, so a slow search inside a host
151
+ * is what GOD bills for (utilities/overwatch.ts billTime, +2d6 per
152
+ * 15 minutes).
153
+ *
154
+ * WHAT IT FINDS. Inside a host, the host's archive: from then on the
155
+ * interior lists its files for this persona (Host.searchedBy,
156
+ * grid-view.ts). A CRACKED host spills them without the roll -- the
157
+ * seal is broken, there is nothing to find. Out on the grid it is the
158
+ * public-knowledge row: the loose data icons in reach.
159
+ */
160
+ matrixSearch() {
161
+ const actor = this.actor;
162
+ const host = actor.hostInside;
163
+ const where = host ? host.over : gridVicinity(actor);
164
+ const row = host ? MATRIX_SEARCH.inHost : MATRIX_SEARCH.public;
165
+ const browse = actor.activeDeck?.hasProgram('browse') ?? false;
166
+ const { pool, limit } = matrixSearchTest(actor);
167
+ const roll = rollPool(pool, limit);
168
+ const seconds = matrixSearchSeconds(row.seconds, roll.hits, row.threshold, browse);
169
+ const isPlayer = this.scene.isHumanControlled(actor);
170
+ if (isPlayer) {
171
+ this.logger.meta(`Matrix Search (Computer + Intuition [Data Processing], threshold ${row.threshold}${browse ? ', Browse' : ''}): ${formatRoll(roll)} -- ${seconds}s`);
172
+ }
173
+ // THE MINUTES ARE GOD'S (p.231-232): Overwatch bills time on the
174
+ // grid, and a search is time. Winding the last tick back by the
175
+ // seconds spent is what makes the next illegal action pay for them.
176
+ if (actor.overwatchLastTick > 0)
177
+ actor.overwatchLastTick -= seconds * 1000;
178
+ actor.performAction('runs a Matrix Search', host ? `inside ${hostLabel(where)}` : `across ${where.name}'s grid`, { quiet: this.scene.isHumanControlled(actor) });
179
+ const files = where.offlineServer ? [] : where.inventory.getAllItems().filter(i => i.plane === 'matrix');
180
+ const clock = `(${seconds}s of Matrix time${browse ? ', Browse cutting it' : ''})`;
181
+ if (roll.hits < row.threshold) {
182
+ return host
183
+ ? `You dig through ${hostLabel(where)}'s archive and the index keeps folding away from you -- nothing this pass. ${clock}${hint(` Try again; the host is not going anywhere.`)}`
184
+ : `You sweep ${where.name}'s grid for loose data and turn up nothing this pass. ${clock}`;
185
+ }
186
+ if (host) {
187
+ host.searchedBy.add(actor.name);
188
+ where.searchedBy.add(actor.name);
189
+ if (files.length === 0) {
190
+ return `The archive is indexed and EMPTY -- ${hostLabel(where)} keeps nothing worth the trip. ${clock}`;
191
+ }
192
+ const list = files.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
193
+ const seal = where.hostCracked
194
+ ? hint(` ("take" them -- the seal is already broken.)`)
195
+ : hint(` (sealed in the host's data vault: "hack" the node to break the seal, then "take".)`);
196
+ return `The index gives up ${hostLabel(where)}'s archive: ${list}. ${clock}${seal}`;
197
+ }
198
+ where.searchedBy.add(actor.name);
199
+ const loose = where.hasNode ? [] : files;
200
+ if (loose.length === 0) {
201
+ const sealedElsewhere = files.length > 0 && where.hasNode;
202
+ return `Nothing loose on ${where.name}'s grid. ${clock}${sealedElsewhere ? hint(` (${hostLabel(where)} keeps its files INSIDE -- "enter" it and search from in there.)`) : ''}`;
203
+ }
204
+ const list = loose.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
205
+ return `Loose on ${where.name}'s grid: ${list}. ${clock}${hint(` ("take" rides them out on your deck.)`)}`;
206
+ }
138
207
  async execute(_args) {
139
208
  const room = this.actor.currentLocation;
140
209
  // A MATRIX SEARCH IS MEASURED IN MINUTES (p.241: base time one
@@ -144,6 +213,8 @@ export class SearchCommand extends Command {
144
213
  if (this.actor.plane === 'matrix' && encounterOf(this.scene, this.actor)) {
145
214
  return `Not with the ice on you -- a Matrix Search takes minutes (p.241), and a Combat Turn is three seconds. Crash the ice or get out first.`;
146
215
  }
216
+ if (this.actor.plane === 'matrix')
217
+ return this.matrixSearch();
147
218
  // OBSERVE IN DETAIL (SR5 p.165): the Perception Test below is the
148
219
  // Simple Action the book names, inside a Combat Turn.
149
220
  const bill = billAction(this.scene, this.actor, 'simple', 'Observe in Detail');
@@ -155,17 +226,14 @@ export class SearchCommand extends Command {
155
226
  // room stays uncased ("take all" stays gated). Barriers (puzzles) are
156
227
  // in plain sight and show regardless -- you can't fail to notice the
157
228
  // maglock you're staring at.
158
- // On the GRID a search is Matrix Search (canon: Computer, Logic) --
159
- // best-of with perception so pre-computer deckers lose nothing.
160
- // A loaded Browse program is purpose-built for Matrix Search: +2.
161
- const browse = this.actor.plane === 'matrix' && this.actor.activeDeck?.hasProgram('browse') ? 2 : 0;
162
229
  // ACTIVE: this is the deliberate hunt, so it carries p.135-136's +3
163
230
  // for "perceiver is specifically looking for it" -- the modifier
164
231
  // that makes `search` mechanically different from `look` rather
165
232
  // than only differently named (FNgptvRhbA5yMunbB). The pool and the
166
233
  // bracket both live in utilities/perception.ts; look.ts rolls the
167
- // same test without the +3.
168
- const { pool, limit } = perceptionTest(this.actor, { active: true, browse });
234
+ // same test without the +3. The grid never comes through here: a
235
+ // persona's search is matrixSearch() above.
236
+ const { pool, limit } = perceptionTest(this.actor, { active: true });
169
237
  const roll = rollPool(pool, limit);
170
238
  // Roll anatomy to the Mechanics ticker for the player; an NPC's
171
239
  // return string is its AI history, so IT keeps the roll inline
@@ -234,28 +302,7 @@ export class SearchCommand extends Command {
234
302
  if (spotted.length > 0) {
235
303
  description += `\n{yellow-fg}You pick ${spotted.map(a => a.name).join(', ')} out of the shadows -- ${spotted.length === 1 ? 'they were' : 'they were'} there the whole time.{/yellow-fg}`;
236
304
  }
237
- // Data is LOOT -- say so while the persona is standing on it, with the
238
- // vault state attached: sealed files need the host cracked first. A
239
- // real session's decker found the paydata jacked-in, didn't realize it
240
- // was takeable from the grid, and went hunting for it in the meat.
241
- if (this.actor.plane === 'matrix' && found.some(i => i.plane === 'matrix')) {
242
- description += room.hasNode && !room.offlineServer && !room.hostCracked
243
- ? `\n{cyan-fg}(the files sit sealed in the host's data vault -- "hack" the node to unseal them){/cyan-fg}`
244
- : hint(`\n{cyan-fg}(data files are loot -- "take" them right here on the grid; they ride out on your deck){/cyan-fg}`);
245
- }
246
- // A persona sweeping the grid OUTSIDE a host: the files exist and
247
- // this sweep cannot reach them. Say why, rather than reading empty
248
- // -- a silent nothing is what made the old air-gap case feel broken.
249
- if (this.actor.plane === 'matrix' && !filesInReach
250
- && room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
251
- if (room.offlineServer) {
252
- description += `\n{cyan-fg}There is a ${room.offlineServer} in this room and it is NOT on the Matrix -- nothing you can do from out here touches it.${hint(` Walk in and cable into it in the flesh.`)}{/cyan-fg}`;
253
- }
254
- else {
255
- const marked = (room.hostMarksBy.get(this.actor.name) ?? 0) > 0 || room.hostCracked;
256
- description += `\n{cyan-fg}This sweep only covers the open grid. Whatever the host holds is INSIDE it, behind its wall (p.246).${hint(marked ? ` "enter" and search again.` : ` A mark is the door -- "mark" the host, then "enter".`)}{/cyan-fg}`;
257
- }
258
- }
305
+ // The grid never reaches here: a persona's search is matrixSearch().
259
306
  // The mirror hint: a meat-side search can't see matrix files, but it
260
307
  // shouldn't pretend the room is empty when the good stuff is one
261
308
  // "jack in" away.
@@ -266,8 +313,8 @@ export class SearchCommand extends Command {
266
313
  // tells a player standing next to an offline server that it is
267
314
  // there and what it wants from them.
268
315
  description += room.offlineServer
269
- ? `\n{cyan-fg}A ${room.offlineServer} sits here, dark and off the network -- no aerial, no marquee, nothing broadcasting. Whatever it holds comes out through a cable and no other way.${hint(` "jack in" RIGHT HERE to touch its files.`)}{/cyan-fg}`
270
- : `\n{cyan-fg}The node here hums with locked files -- whatever's worth taking lives on the grid, inside the host.${hint(` "jack in", then "enter" the host.`)}{/cyan-fg}`;
316
+ ? `\n{lightblue-fg}A ${room.offlineServer} sits here, dark and off the network -- no aerial, no marquee, nothing broadcasting. Whatever it holds comes out through a cable and no other way.${hint(` "jack in" RIGHT HERE to touch its files.`)}{/lightblue-fg}`
317
+ : `\n{lightblue-fg}The node here hums with locked files -- whatever's worth taking lives on the grid, inside the host.${hint(` "jack in", then "enter" the host.`)}{/lightblue-fg}`;
271
318
  }
272
319
  description += describeContainers(room, this.actor);
273
320
  description += describeBarriers(room, this.actor);
@@ -49,7 +49,7 @@ export class SnoopCommand extends Command {
49
49
  this.logger.write(`Snoop: ${actor.name} watching ${target.name} via camera feed.`);
50
50
  const lines = [`CAMERA FEED: ${target.name.toUpperCase()}`];
51
51
  if (!camerasLive(allRooms, target)) {
52
- lines.push(`{cyan-fg}(Security sees a loop of empty hallways -- these eyes answer only to you now.){/cyan-fg}`);
52
+ lines.push(`{lightblue-fg}(Security sees a loop of empty hallways -- these eyes answer only to you now.){/lightblue-fg}`);
53
53
  }
54
54
  const seen = this.scene.getActorsInRoom(target)
55
55
  .filter(a => a !== actor && a.plane === 'meat' && !a.sneaking);
@@ -73,7 +73,7 @@ export class SpritesCommand extends Command {
73
73
  const name = type?.name ?? set.spriteKey;
74
74
  const isMine = mineType === set.spriteKey;
75
75
  const powers = powersForSprite(set.spriteKey);
76
- lines.push(`${name.toUpperCase()} SPRITE${isMine ? ' {cyan-fg}(yours){/cyan-fg}' : ''}${type ? ` -- ${type.blurb}` : ''}`);
76
+ lines.push(`${name.toUpperCase()} SPRITE${isMine ? ' {lightblue-fg}(yours){/lightblue-fg}' : ''}${type ? ` -- ${type.blurb}` : ''}`);
77
77
  lines.push(` Skills: ${skillsForSprite(set.spriteKey).join(', ')}${hint(` (${set.page})`)}`);
78
78
  if (!detailed) {
79
79
  // The names alone, which is the comparison view.
@@ -158,9 +158,9 @@ export class TapCommand extends Command {
158
158
  }
159
159
  this.logger.write(`${actor.name} now holds ${now} mark(s) on ${host.name} via the ${room.name} cameras.`);
160
160
  const lines = [
161
- `{cyan-fg}The filament finds the connector and the camera stops being a camera -- it's a device, and it's talking to you.{/cyan-fg}`,
162
- ` {cyan-fg}It never gets to hide behind ${hostLabel(host)}: a cable doesn't ask the network for permission, so the lens has to defend itself with what a lens has. It doesn't have much.{/cyan-fg}`,
163
- ` {cyan-fg}MARK PLACED -- and it lands on the host too. The camera answers to ${host.name}, and anything ${host.name} trusts to speak for it just handed you a key. [${now}/${MAX_MARKS}]{/cyan-fg}`,
161
+ `{lightblue-fg}The filament finds the connector and the camera stops being a camera -- it's a device, and it's talking to you.{/lightblue-fg}`,
162
+ ` {lightblue-fg}It never gets to hide behind ${hostLabel(host)}: a cable doesn't ask the network for permission, so the lens has to defend itself with what a lens has. It doesn't have much.{/lightblue-fg}`,
163
+ ` {lightblue-fg}MARK PLACED -- and it lands on the host too. The camera answers to ${host.name}, and anything ${host.name} trusts to speak for it just handed you a key. [${now}/${MAX_MARKS}]{/lightblue-fg}`,
164
164
  ];
165
165
  if (mode === 'attack') {
166
166
  lines.push(` The housing is visibly forced -- the host FELT that one, even if nothing saw your face.`);
@@ -169,15 +169,15 @@ export class TapCommand extends Command {
169
169
  lines.push(` Nothing logged, nothing flagged. You were never here.`);
170
170
  }
171
171
  if (host === room) {
172
- lines.push(` {cyan-fg}${now >= MAX_MARKS
172
+ lines.push(` {lightblue-fg}${now >= MAX_MARKS
173
173
  ? `The host thinks you belong now -- "hack" walks straight in.`
174
- : `${hostLabel(host, { capital: true })} cracks easier with every key you hold${hint(` -- "jack in" and "hack", or come back for another lens`)}.`}{/cyan-fg}`);
174
+ : `${hostLabel(host, { capital: true })} cracks easier with every key you hold${hint(` -- "jack in" and "hack", or come back for another lens`)}.`}{/lightblue-fg}`);
175
175
  }
176
176
  else {
177
- lines.push(` {cyan-fg}These lenses ride the site network, so the key fits ${hostLabel(host)}${hint(` -- "jack in", "go ${host.name}", "hack"`)}.{/cyan-fg}`);
177
+ lines.push(` {lightblue-fg}These lenses ride the site network, so the key fits ${hostLabel(host)}${hint(` -- "jack in", "go ${host.name}", "hack"`)}.{/lightblue-fg}`);
178
178
  }
179
179
  // A mark is all canSnoopFeeds ever wanted (surveillance.ts).
180
- lines.push(` {cyan-fg}And the eyes are half yours already${hint(` -- jacked in, "snoop <room>" rides any feed on this network`)}.{/cyan-fg}`);
180
+ lines.push(` {lightblue-fg}And the eyes are half yours already${hint(` -- jacked in, "snoop <room>" rides any feed on this network`)}.{/lightblue-fg}`);
181
181
  lines.push(...godLines);
182
182
  this.scene.updateStatus();
183
183
  return lines.join('\n');
@@ -67,7 +67,7 @@ export class ThreadCommand extends Command {
67
67
  const lines = [`COMPLEX FORMS (thread <form> <level>)`];
68
68
  for (const f of FORMS) {
69
69
  const held = actor.sustainedForms.some(s => s.key === f.key);
70
- lines.push(` ${f.name.padEnd(16)} ${f.kind === 'sustained' ? '[sustained]' : '[instant] '} ${f.blurb}${held ? ' {cyan-fg}(HELD){/cyan-fg}' : ''}`);
70
+ lines.push(` ${f.name.padEnd(16)} ${f.kind === 'sustained' ? '[sustained]' : '[instant] '} ${f.blurb}${held ? ' {lightblue-fg}(HELD){/lightblue-fg}' : ''}`);
71
71
  }
72
72
  if (actor.sustainedForms.length > 0) {
73
73
  lines.push('', ` Holding ${actor.sustainedForms.length} form${actor.sustainedForms.length === 1 ? '' : 's'} (${sustainTotal(actor.sustainedLedger().filter(w => w.kind === 'form'))}).${hint(` ("thread drop <form>" releases.)`)}`);
@@ -109,6 +109,7 @@ import { EndCallCommand } from './commands/end-call.js';
109
109
  import { fuzzyPickName, guessVerb } from './utilities/fuzzy-match.js';
110
110
  import { PickCommand } from './commands/pick.js';
111
111
  import { HackCommand } from './commands/hack.js';
112
+ import { DisableCommand, BrickCommand } from './commands/disable.js';
112
113
  import { CastCommand } from './commands/cast.js';
113
114
  import { StanceCommand } from './commands/stance.js';
114
115
  import { FollowCommand, UnfollowCommand } from './commands/follow.js';
@@ -2596,6 +2597,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2596
2597
  CommandFactory.registerCommand('end-call', EndCallCommand);
2597
2598
  CommandFactory.registerCommand('pick', PickCommand);
2598
2599
  CommandFactory.registerCommand('hack', HackCommand);
2600
+ CommandFactory.registerCommand('disable', DisableCommand);
2601
+ CommandFactory.registerCommand('brick', BrickCommand);
2599
2602
  CommandFactory.registerCommand('cast', CastCommand);
2600
2603
  CommandFactory.registerCommand('stance', StanceCommand);
2601
2604
  CommandFactory.registerCommand('surrender', SurrenderCommand);
@@ -5662,7 +5665,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5662
5665
  // works the grid while you're back in the meat (the AR-drop exit).
5663
5666
  const remoteAgent = this.companions.find(c => c.kind === 'agent');
5664
5667
  if (remoteAgent && player.plane !== 'matrix') {
5665
- lines.push(`{cyan-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''}${hint(' ("jack in" to rejoin it, "recall" to fold it home)')}{/cyan-fg}`);
5668
+ lines.push(`{lightblue-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''}${hint(' ("jack in" to rejoin it, "recall" to fold it home)')}{/lightblue-fg}`);
5666
5669
  }
5667
5670
  // Off-plane context: which reality you're in, your persona's Matrix
5668
5671
  // track (cold-sim damage lands there), and where your empty body lies.
@@ -5681,7 +5684,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5681
5684
  const track = jackedDeck
5682
5685
  ? ` Deck A${jackedDeck.attack} S${jackedDeck.sleaze} D${jackedDeck.dataProcessing} F${jackedDeck.firewall} ${jackedDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}${pgms}`
5683
5686
  : ` (living persona -- damage is REAL stun)`;
5684
- lines.push(`{cyan-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/cyan-fg}`);
5687
+ lines.push(`{lightblue-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/lightblue-fg}`);
5685
5688
  }
5686
5689
  else if (player.plane === 'astral') {
5687
5690
  lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
@@ -5773,7 +5776,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5773
5776
  const dockW = mapWidth ?? 15;
5774
5777
  const lines = hosts.length > 0
5775
5778
  ? hosts.map(h => hostDockLine(h, this.player, dockW))
5776
- : ['{cyan-fg}nothing overhead{/cyan-fg}'];
5779
+ : ['{lightblue-fg}nothing overhead{/lightblue-fg}'];
5777
5780
  lines.push(caption(this.player.insideHost ? `inside ${this.player.insideHost.name}` : `on the grid from ${gridVicinity(this.player).name}`, mapWidth));
5778
5781
  this.setMapOverlay(lines);
5779
5782
  if (onTheGrid) {
@@ -6275,7 +6278,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6275
6278
  // players" / "crew street"); the bare verb still answers quietly.
6276
6279
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
6277
6280
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
6278
- { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6281
+ { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6279
6282
  // The Emerged get their own shelf (player request: "there MUST be
6280
6283
  // resonance" -- matrix is the place, Resonance is the talent).
6281
6284
  // `sustain` shelves here and not under magic: the command is
@@ -126,6 +126,16 @@ export class Device {
126
126
  structure;
127
127
  armor;
128
128
  _barrierDamage = 0;
129
+ /**
130
+ * THE MATRIX CONDITION MONITOR (p.228): 8 + (Device Rating / 2)
131
+ * boxes, resisted with Device Rating + Firewall, filled by a Data
132
+ * Spike (commands/disable.ts, "brick"). Full = BRICKED: "the device
133
+ * ceases functioning" -- a camera records nothing, a smartgun sparks
134
+ * and dies -- but "a lock stays locked" (p.228): bricking is never a
135
+ * way through a maglock. Repaired on the bench (rest.ts), never by a
136
+ * reboot. A ward is not electronics and has no monitor at all.
137
+ */
138
+ _matrixDamage = 0;
129
139
  /**
130
140
  * THE LOCKSMITH EXTENDED TEST IN PROGRESS (p.48, p.359-360): picking a
131
141
  * maglock by hand accumulates hits toward Rating x 2 across attempts,
@@ -207,6 +217,33 @@ export class Device {
207
217
  * exact fault this codebase keeps having to close (see the gun in
208
218
  * aroTags, AKfemBJJWJP99nx8K).
209
219
  */
220
+ // ---- The Matrix condition monitor (commands/disable.ts) ----
221
+ /** p.228: 8 + (Device Rating / 2), rounded up as the deck's is. 0 for
222
+ * a thing with no rating (a ward): nothing there to brick. */
223
+ get matrixMonitor() {
224
+ return this.rating > 0 ? 8 + Math.ceil(this.rating / 2) : 0;
225
+ }
226
+ get matrixDamage() {
227
+ return this._matrixDamage;
228
+ }
229
+ /** Returns boxes actually taken. */
230
+ takeMatrixDamage(boxes) {
231
+ if (this.matrixMonitor <= 0)
232
+ return 0;
233
+ const taken = Math.max(0, Math.floor(boxes));
234
+ this._matrixDamage = Math.min(this.matrixMonitor, this._matrixDamage + taken);
235
+ return taken;
236
+ }
237
+ get isBricked() {
238
+ return this.matrixMonitor > 0 && this._matrixDamage >= this.matrixMonitor;
239
+ }
240
+ /** Bench work (rest.ts): the whole monitor, at once. */
241
+ repairMatrix() {
242
+ this._matrixDamage = 0;
243
+ }
244
+ matrixSummary() {
245
+ return this.matrixMonitor > 0 ? `${this._matrixDamage}/${this.matrixMonitor} boxes` : '';
246
+ }
210
247
  broadcastsAro() {
211
248
  return this.wireless && this.rating > 0;
212
249
  }
@@ -328,11 +365,13 @@ export class Device {
328
365
  const text = hint(AFFORDANCE_FLAVOR[this.kind].hint);
329
366
  return text.length > 0 ? text : undefined;
330
367
  }
331
- /** Persistence-only: one flag in, nothing out. */
368
+ /** Persistence-only: flags in, nothing out. */
332
369
  restoreState(state) {
333
370
  this.open = state.open;
334
371
  if (state.damage !== undefined)
335
372
  this._barrierDamage = Math.max(0, Math.min(this.structure, Math.floor(state.damage)));
373
+ if (state.bricked !== undefined)
374
+ this._matrixDamage = state.bricked ? this.matrixMonitor : 0;
336
375
  }
337
376
  }
338
377
  //# sourceMappingURL=device.js.map
@@ -41,6 +41,10 @@ export class Host {
41
41
  marksBy = new Map();
42
42
  /** player name -> marks the host holds on that player. */
43
43
  marksOn = new Map();
44
+ /** Personas whose Matrix Search (p.241, commands/search.ts) turned up
45
+ * this host's files: the archive is listed for them from then on. A
46
+ * cracked host lists it for everyone. Session-only. */
47
+ searchedBy = new Set();
44
48
  /** Walls up: the host holds a mark on an intruder and its ice is hunting. */
45
49
  alert = false;
46
50
  /** IC running now, by kind name. Names, not actors -- until increment 5. */
@@ -43,6 +43,12 @@ export class Item extends AbstractItem {
43
43
  // A jammed firearm can't fire (fists until cleared); "reboot" clears
44
44
  // every jammed device you carry, in the lull.
45
45
  jammed = false;
46
+ // BRICKED (p.228, commands/disable.ts "brick"): a Data Spike filled the
47
+ // device's Matrix condition monitor. Dead electronics -- a smartgun
48
+ // "sparks, crackles, and smokes" and will not fire -- until bench
49
+ // work (rest.ts). A reboot does NOT clear it; that is the whole
50
+ // difference from `jammed`.
51
+ bricked = false;
46
52
  _transferable;
47
53
  // New currency-related fields
48
54
  _currencyAmount = 0;
@@ -8,6 +8,7 @@ import { hint } from '../utilities/hints.js';
8
8
  import { Category } from '../types/shared/item-enum.js';
9
9
  import { Logger } from '../utilities/logger.js';
10
10
  import { CALL_ICON, CALL_COLOR } from '../utilities/comm-style.js';
11
+ import { FEED_COLOR } from '../utilities/matrix-style.js';
11
12
  // The Physical limit already lives here (subduing, p.195) -- imported
12
13
  // rather than re-derived so meat combat and the grapple cannot drift
13
14
  // into two different formulas for the same canon number.
@@ -2851,7 +2852,7 @@ export class Player extends AbstractPlayer {
2851
2852
  // as before -- only the human's log line is dropped.
2852
2853
  if (quiet)
2853
2854
  return;
2854
- this.logger.logWithColor(`${actor.name} ${verb} ${details}`, 'cyan');
2855
+ this.logger.logWithColor(`${actor.name} ${verb} ${details}`, FEED_COLOR);
2855
2856
  }
2856
2857
  }
2857
2858
  performAction(verb, details, opts) {
@@ -469,6 +469,12 @@ export class Room extends AbstractRoom {
469
469
  warded = false;
470
470
  // See IRoomConstructorConfig.watched (utilities/surveillance.ts).
471
471
  watched = false;
472
+ /** The cluster is LOOPED by Control Device (commands/disable.ts): the
473
+ * house sees empty hallways until the run ends. Session-only. */
474
+ camerasLooped = false;
475
+ /** The cluster is BRICKED by a Data Spike (p.228): dead lenses, until
476
+ * somebody's bench work nobody in this run is doing. Session-only. */
477
+ camerasBricked = false;
472
478
  /**
473
479
  * THE HOST STANDING OVER THIS ROOM, as an object (models/host.ts) --
474
480
  * increment 1 of the Matrix position model. Every host field this class
@@ -256,7 +256,7 @@ export function aroBlock(scene, actor, room) {
256
256
  const tags = aroTags(scene, actor, room);
257
257
  if (tags.length === 0)
258
258
  return '';
259
- return `{cyan-fg}AR OVERLAY:{/cyan-fg}\n${tags.map(t => ` {cyan-fg}${t}{/cyan-fg}`).join('\n')}`;
259
+ return `{lightblue-fg}AR OVERLAY:{/lightblue-fg}\n${tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`).join('\n')}`;
260
260
  }
261
261
  /** Gear that talks to the grid on its own, lying LOOSE in the room.
262
262
  * Weapons are absent here because a carried one is handled above, as
@@ -475,7 +475,7 @@ export class CombatEncounter {
475
475
  // A persona has no metres to spend: the grid sees no distance.
476
476
  const ground = this.arena.kind === 'room' ? `; ${this.movementLine(p.actor)}` : '';
477
477
  const budgetLine = `Your Action Phase -- Combat Turn ${this.turn}, pass ${this.pass}, Initiative ${p.score}. ${this.budget.describe()}${ground}. "end turn" when you're done.`;
478
- this.logger.log(`\n{cyan-fg}${budgetLine}{/cyan-fg}`, { actor: p.actor.name });
478
+ this.logger.log(`\n{yellow-fg}${budgetLine}{/yellow-fg}`, { actor: p.actor.name });
479
479
  this.scene.updateStatus();
480
480
  this.onHumanPhase?.(p.actor, this);
481
481
  return;
@@ -742,7 +742,7 @@ export class CombatEncounter {
742
742
  return undefined;
743
743
  if (this.phaseActor === player && this.budget) {
744
744
  const ground = this.arena.kind === 'room' ? ` · ${player.movementLeftMeters} m` : '';
745
- return `{cyan-fg}⏱ YOUR PHASE{/cyan-fg} T${this.turn}/P${this.pass} Init ${p.score} · ${this.budget.describe()}${ground} · "end turn"`;
745
+ return `{yellow-fg}⏱ YOUR PHASE{/yellow-fg} T${this.turn}/P${this.pass} Init ${p.score} · ${this.budget.describe()}${ground} · "end turn"`;
746
746
  }
747
747
  const who = this.phaseActor ? `${this.phaseActor.name} acting` : 'resolving';
748
748
  return `⏱ COMBAT T${this.turn}/P${this.pass} Init ${p.score} · ${who}${p.fullDefenseTurn === this.turn ? ' · FULL DEFENSE' : ''}`;
@@ -106,6 +106,6 @@ export function hostDockLine(room, actor, width) {
106
106
  const tag = room.hostCracked ? ' open' : marks > 0 ? ` ${marks}/3` : '';
107
107
  const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}`;
108
108
  const cut = [...raw].slice(0, Math.max(4, width)).join('');
109
- return `{cyan-fg}${cut}{/cyan-fg}`;
109
+ return `{lightblue-fg}${cut}{/lightblue-fg}`;
110
110
  }
111
111
  //# sourceMappingURL=grid-reach.js.map