@maka/maka-cli 5.183.0 → 5.185.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 (36) 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/delay.js +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +6 -3
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/end-turn.js +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +11 -13
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +10 -7
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +110 -0
  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/search.js +4 -4
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +13 -7
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +11 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/game.js +7 -4
  21. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +21 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/models/host.js +24 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +2 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +2 -2
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
  28. package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +5 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-brain.js +35 -2
  30. package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +107 -34
  31. package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +3 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +8 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -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/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.183.0",
3
+ "version": "5.185.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
  }
@@ -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.`;
@@ -148,8 +148,11 @@ export class DisableCommand extends Command {
148
148
  const host = t.kind === 'device' ? t.host : t.kind === 'camera' ? t.master : undefined;
149
149
  if (host) {
150
150
  host.hostAlert = true;
151
- if (made)
151
+ if (made) {
152
152
  host.hostMarksOn.set(actor.name, Math.min(MAX_MARKS, (host.hostMarksOn.get(actor.name) ?? 0) + 1));
153
+ // Made is seen (Host.spotted) -- the ice hunts this persona now.
154
+ host.host?.spotted.add(actor.name);
155
+ }
153
156
  }
154
157
  if (t.kind === 'gun' && made) {
155
158
  actor.panMarksBy.set(t.owner.name, Math.min(MAX_MARKS, (actor.panMarksBy.get(t.owner.name) ?? 0) + 1));
@@ -180,7 +183,7 @@ export class DisableCommand extends Command {
180
183
  const defence = this.defence(t);
181
184
  const defenceRoll = rollPool(defence.pool);
182
185
  const net = roll.hits - defenceRoll.hits;
183
- actor.performAction('takes control of a device', name);
186
+ actor.performAction('takes control of a device', name, { quiet: this.scene.isHumanControlled(actor) });
184
187
  if (this.scene.isHumanControlled(actor)) {
185
188
  this.logger.meta(`Control Device (Electronic Warfare + Intuition [Sleaze])${gridNote(grid)}: ${formatRoll(roll)}`);
186
189
  this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
@@ -236,7 +239,7 @@ export class DisableCommand extends Command {
236
239
  const defence = this.defence(t);
237
240
  const defenceRoll = rollPool(defence.pool);
238
241
  const net = roll.hits - defenceRoll.hits;
239
- actor.performAction('throws a Data Spike', name);
242
+ actor.performAction('throws a Data Spike', name, { quiet: this.scene.isHumanControlled(actor) });
240
243
  if (this.scene.isHumanControlled(actor)) {
241
244
  this.logger.meta(`Data Spike (Cybercombat + Logic [Attack ${limit}])${gridNote(grid)}: ${formatRoll(roll)}`);
242
245
  this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
@@ -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
@@ -197,6 +197,9 @@ export class HackCommand extends BypassCommand {
197
197
  // CAUGHT from a user who merely holds a key, so the sentence
198
198
  // has to be true.
199
199
  room.hostMarksOn.set(this.actor.name, Math.min(MAX_MARKS, (room.hostMarksOn.get(this.actor.name) ?? 0) + 1));
200
+ // Being made is being SEEN (Host.spotted): the ice hunts this
201
+ // persona from here, not merely the intrusion.
202
+ room.host?.spotted.add(this.actor.name);
200
203
  this.actor.noteConditionChanged();
201
204
  this.actor.sneaking = false;
202
205
  this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} MADE ${this.actor.name} mid-intrusion -- alarm flagged, ice hunting.`);
@@ -276,7 +279,7 @@ export class HackCommand extends BypassCommand {
276
279
  if (marksNow >= 1)
277
280
  lines.push(` One mark is the door: "enter" walks your persona inside (p.239).`);
278
281
  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}`);
282
+ 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
283
  if (forcedDv > 0)
281
284
  lines.push(` The forcing burns ${forcedDv} DV into the architecture -- a host has no boards to brick, so it only announces you louder.`);
282
285
  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 +343,13 @@ export class HackCommand extends BypassCommand {
340
343
  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
344
  }
342
345
  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}`);
346
+ 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
347
  }
345
348
  // AND WHAT THE CRACK DID NOT REACH: a wired site's data sits on an
346
349
  // air-gapped box the host never held (p.233) -- say so, or the next
347
350
  // "take" reads as a contradiction.
348
351
  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}`);
352
+ 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
353
  }
351
354
  // THE FEEDS ARE A REAL GAIN AND WERE NEVER MENTIONED
352
355
  // (BpR5cjhYuvgCyDSZc, 7aNWnDSHeZMYmS2De: "what have I gained?").
@@ -540,7 +543,7 @@ export class HackCommand extends BypassCommand {
540
543
  ? Math.max(1, actor.getAttackPool() + silence + actor.bonus('matrix-attack') + declarePenalty)
541
544
  : Math.max(1, actor.logic + actor.skillRating('hacking') + actor.bonus('hacking') + actor.bonus('matrix-sleaze')
542
545
  + silence + actor.woundModifier - actor.sustainingPenalty + declarePenalty);
543
- actor.performAction('works a slaved icon', device.name);
546
+ actor.performAction('works a slaved icon', device.name, { quiet: this.scene.isHumanControlled(actor) });
544
547
  const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
545
548
  // From the grid the slave borrows its master's numbers where they are
546
549
  // higher (p.233); inside, it stands alone (Data Trails p.87-88).
@@ -606,7 +609,7 @@ export class HackCommand extends BypassCommand {
606
609
  else {
607
610
  const perception = freeMatrixPerceptionHits(netHits);
608
611
  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}`);
612
+ 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
613
  }
611
614
  // TWO MARKS COMMAND IT (p.238 Control Device: 1 mark for a Free
612
615
  // action, 2 for a Simple, 3 for a Complex -- and unlocking a maglock
@@ -765,7 +768,7 @@ export class HackCommand extends BypassCommand {
765
768
  // Intuition + Firewall against a sleaze, Willpower + Firewall
766
769
  // against a smash (p.240 / p.238), and no limit on either.
767
770
  const defense = rollPool(target.getPanDefensePool(mode === 'attack' ? 'attack' : 'sleaze'));
768
- actor.performAction('runs a PAN intrusion', target.name);
771
+ actor.performAction('runs a PAN intrusion', target.name, { quiet: this.scene.isHumanControlled(actor) });
769
772
  this.logger.write(`${actor.name} PAN-hacks ${target.name}: ${attack.hits} vs ${defense.hits} (silent=${target.runningSilent}).`);
770
773
  // Dice anatomy to the Mechanics ticker (readability pass) --
771
774
  // player-gated like every roll site: an NPC's probe isn't your dice.
@@ -0,0 +1,110 @@
1
+ import { Command } from './command.js';
2
+ import { hint } from '../utilities/hints.js';
3
+ import { rollPool, formatRoll } from '../utilities/dice.js';
4
+ import { accrueOverwatch } from '../utilities/overwatch.js';
5
+ import { leaveMatrix } from '../utilities/planes.js';
6
+ import { billAction, encounterOf } from '../utilities/action-cost.js';
7
+ import { isSpotted } from '../utilities/ic.js';
8
+ import { MAX_MARKS } from '../utilities/marks.js';
9
+ import { hostLabel } from '../utilities/grid-names.js';
10
+ /**
11
+ * HIDE (SR5 p.240, RAG-checked 2026-09-14): a Complex Action, no marks
12
+ * required, Electronic Warfare + Intuition [Sleaze] v. Intuition + Data
13
+ * Processing. "If you succeed, the target loses track of you and must
14
+ * perform a new Matrix Perception action to find you." And the one hard
15
+ * limit the book puts on it: "You can't hide from an icon that has a
16
+ * mark on you, so you'll need to clear those before you can try this."
17
+ *
18
+ * WHY IT EXISTS HERE (the "constantly in combat" report, 2026-09-14):
19
+ * a host that has SPOTTED a persona (Host.spotted) hunts it with every
20
+ * program it can launch, and nothing but leaving ended that. Canon's
21
+ * answer is this action -- drop off the host's scope, and its ice has
22
+ * nothing to swing at until Patrol finds you again (which, on an alerted
23
+ * host, it tries every action: Data Trails p.86, ic.ts ambientPatrol).
24
+ *
25
+ * THE DEFENDER IS THE HOST, and a host has no Intuition: its rating
26
+ * stands in, the same engine ruling matrix-intrusion.ts hostDefensePool
27
+ * already makes for the intrusion roll (Host Rating + Firewall). Here it
28
+ * is Host Rating + Data Processing, because the book names Data
29
+ * Processing for this test. The host and its IC share spotting (p.247),
30
+ * so hiding from the host hides you from every program at once.
31
+ *
32
+ * A SLEAZE ACTION: GOD bills the defender's hits (p.231-232), and a
33
+ * failure is a blown Sleaze -- the target gets one free mark on you and
34
+ * spots you (p.236). Failing to hide from a host that could see you
35
+ * makes it hold you tighter; that is the book's own tension, not ours.
36
+ */
37
+ export class HideCommand extends Command {
38
+ static verb = 'hide';
39
+ static description = 'Hide (Complex Action, SR5 p.240): Electronic Warfare + Intuition [Sleaze] v. the host\'s Rating + Data Processing. Win and the host loses track of your persona -- its ice has nothing to swing at until Patrol finds you again. Impossible while the host holds a mark on you. A Sleaze action: GOD counts it, and a miss hands the host a mark.';
40
+ async execute(_args = []) {
41
+ const actor = this.actor;
42
+ if (actor.plane !== 'matrix') {
43
+ return `Hide is a Matrix action -- it takes a persona to lose. ${hint(`"jack in" first; in the meat, "sneak".`)}`;
44
+ }
45
+ const host = actor.hostInside;
46
+ if (!host) {
47
+ return `Nothing out here has you. Hide is for shaking an icon that has SPOTTED you -- a host's ice, once it's made you inside.`;
48
+ }
49
+ const room = host.over;
50
+ if (!isSpotted(room, actor)) {
51
+ return `${hostLabel(room, { capital: true })} hasn't found you -- there is nothing to hide from yet.${actor.runningSilent ? '' : hint(` ("silent" is what keeps it that way: a loud icon is spotted with no test, p.235.)`)}`;
52
+ }
53
+ const held = room.hostMarksOn.get(actor.name) ?? 0;
54
+ if (held > 0) {
55
+ return `You can't hide from an icon that has a MARK on you (p.240) -- ${hostLabel(room)} holds ${held} on your persona. Those come off with a reboot, not a dodge.${hint(` ("jack out" reboots; "exit" leaves the host.)`)}`;
56
+ }
57
+ // Marker IC eats Sleaze; a bracket of 0 permits no hits at all.
58
+ const burned = actor.burnedAttributeRefusal('sleaze');
59
+ if (burned)
60
+ return burned;
61
+ const bill = billAction(this.scene, actor, 'complex', 'Hide');
62
+ if (bill)
63
+ return bill;
64
+ const ew = actor.skillRating('electronic-warfare');
65
+ const pool = Math.max(1, actor.intuition + (ew > 0 ? ew : -1)
66
+ + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
67
+ const limit = actor.matrixAttribute('sleaze');
68
+ const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
69
+ const defence = rollPool(room.hostRating + room.hostAttributes().dataProcessing);
70
+ if (this.scene.isHumanControlled(actor)) {
71
+ this.logger.meta(`Hide -- Electronic Warfare + Intuition [Sleaze]: ${formatRoll(attempt)}`);
72
+ this.logger.meta(` v. ${hostLabel(room)} (Host Rating + Data Processing, rating standing in for Intuition): ${formatRoll(defence)}`);
73
+ }
74
+ actor.performAction('tries to hide', `persona folding into ${hostLabel(room)}'s traffic`, { quiet: this.scene.isHumanControlled(actor) });
75
+ const god = accrueOverwatch(this.scene, actor, defence.hits, 'hide (sleaze)');
76
+ if (god.converged) {
77
+ return [`The hide never lands --`, ...god.lines, ...leaveMatrix(this.scene, actor, { forced: true, reason: `GOD force-reboots your persona --` })].join('\n');
78
+ }
79
+ if (attempt.hits <= defence.hits) {
80
+ // p.236: a blown Sleaze action is NOTICED -- one free mark, and it
81
+ // certainly still sees you.
82
+ room.hostMarksOn.set(actor.name, Math.min(MAX_MARKS, held + 1));
83
+ room.host?.spotted.add(actor.name);
84
+ actor.noteConditionChanged();
85
+ this.logger.write(`${actor.name} failed to hide in ${room.name}: ${attempt.hits} v ${defence.hits} -- host mark ${held + 1}.`);
86
+ this.scene.updateStatus();
87
+ return [
88
+ `{red-fg}You fold into the traffic and the host folds with you -- it never lost you, and now it holds a MARK on your persona.{/red-fg}`,
89
+ ` No hiding from an icon that has a mark on you (p.240). The ice stays on you.`,
90
+ ...god.lines,
91
+ ].join('\n');
92
+ }
93
+ room.host?.spotted.delete(actor.name);
94
+ room.host?.justHid.add(actor.name);
95
+ actor.noteConditionChanged();
96
+ // Out of the host's sight is out of its fight: nothing may swing at
97
+ // an icon it cannot see. The encounter closes on its own when nobody
98
+ // in view is left (host-combat.ts hostKeepsFighting).
99
+ encounterOf(this.scene, actor)?.leave(actor);
100
+ this.logger.write(`${actor.name} hid from ${room.name}'s host: ${attempt.hits} v ${defence.hits}.`);
101
+ this.scene.addWorldEvent(`${host.name}'s ice lost ${actor.name} in the traffic.`);
102
+ this.scene.updateStatus();
103
+ return [
104
+ `{lightblue-fg}You drop into the host's own traffic and become one more packet in a million. ${hostLabel(room, { capital: true })} loses you.{/lightblue-fg}`,
105
+ ` Its ice has nothing to swing at -- but the host is still awake, and Patrol is looking every action now.${hint(` ("silent" keeps a sweep from seeing you for free; "search" is possible while it looks.)`)}`,
106
+ ...god.lines,
107
+ ].join('\n');
108
+ }
109
+ }
110
+ //# sourceMappingURL=hide.js.map
@@ -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
  }
@@ -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
@@ -175,7 +175,7 @@ export class SearchCommand extends Command {
175
175
  // seconds spent is what makes the next illegal action pay for them.
176
176
  if (actor.overwatchLastTick > 0)
177
177
  actor.overwatchLastTick -= seconds * 1000;
178
- actor.performAction('runs a Matrix Search', host ? `inside ${hostLabel(where)}` : `across ${where.name}'s grid`);
178
+ actor.performAction('runs a Matrix Search', host ? `inside ${hostLabel(where)}` : `across ${where.name}'s grid`, { quiet: this.scene.isHumanControlled(actor) });
179
179
  const files = where.offlineServer ? [] : where.inventory.getAllItems().filter(i => i.plane === 'matrix');
180
180
  const clock = `(${seconds}s of Matrix time${browse ? ', Browse cutting it' : ''})`;
181
181
  if (roll.hits < row.threshold) {
@@ -211,7 +211,7 @@ export class SearchCommand extends Command {
211
211
  // bill inside a Combat Turn, so it is refused there rather than
212
212
  // priced by invention.
213
213
  if (this.actor.plane === 'matrix' && encounterOf(this.scene, this.actor)) {
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.`;
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, "hide" from it (p.240), or get out first.`;
215
215
  }
216
216
  if (this.actor.plane === 'matrix')
217
217
  return this.matrixSearch();
@@ -313,8 +313,8 @@ export class SearchCommand extends Command {
313
313
  // tells a player standing next to an offline server that it is
314
314
  // there and what it wants from them.
315
315
  description += room.offlineServer
316
- ? `\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}`
317
- : `\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}`;
318
318
  }
319
319
  description += describeContainers(room, this.actor);
320
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.
@@ -117,6 +117,12 @@ export class TapCommand extends Command {
117
117
  // p.236: a blown Sleaze action is NOTICED. The camera screams to
118
118
  // its master, which is the same alarm hack.ts raises.
119
119
  host.hostAlert = true;
120
+ // The line below promises "it holds a mark on your persona now"
121
+ // (p.236, a blown Sleaze's free mark) -- and a mark is a sighting
122
+ // (Host.spotted). Both written, so the sentence is true and the
123
+ // ice hunts THIS persona rather than the intrusion in general.
124
+ host.hostMarksOn.set(actor.name, Math.min(3, (host.hostMarksOn.get(actor.name) ?? 0) + 1));
125
+ host.host?.spotted.add(actor.name);
120
126
  actor.sneaking = false;
121
127
  this.scene.addWorldEvent(`Something forced the cameras in ${room.name} -- ${hostLabel(host)} flagged an intrusion and its ice is hunting.`);
122
128
  this.scene.updateStatus();
@@ -158,9 +164,9 @@ export class TapCommand extends Command {
158
164
  }
159
165
  this.logger.write(`${actor.name} now holds ${now} mark(s) on ${host.name} via the ${room.name} cameras.`);
160
166
  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}`,
167
+ `{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}`,
168
+ ` {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}`,
169
+ ` {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
170
  ];
165
171
  if (mode === 'attack') {
166
172
  lines.push(` The housing is visibly forced -- the host FELT that one, even if nothing saw your face.`);
@@ -169,15 +175,15 @@ export class TapCommand extends Command {
169
175
  lines.push(` Nothing logged, nothing flagged. You were never here.`);
170
176
  }
171
177
  if (host === room) {
172
- lines.push(` {cyan-fg}${now >= MAX_MARKS
178
+ lines.push(` {lightblue-fg}${now >= MAX_MARKS
173
179
  ? `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}`);
180
+ : `${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
181
  }
176
182
  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}`);
183
+ 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
184
  }
179
185
  // 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}`);
186
+ 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
187
  lines.push(...godLines);
182
188
  this.scene.updateStatus();
183
189
  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.)`)}`);
@@ -449,5 +449,15 @@
449
449
  // Combat Turn. IC no longer act once per player command, never speak,
450
450
  // and a failed IC attack damages the program (p.247). Ice NPCs are no
451
451
  // longer generated; the vault host's rating is its guard.
452
- export const ENGINE_VERSION = '1.53.0';
452
+ // 1.54.0 (2026-09-14): ALERT IS NOT SPOTTED. A host's ice hunts only the
453
+ // personas the host has SEEN (Host.spotted, shared by all its IC,
454
+ // p.247): a blown Sleaze's free mark, a Patrol sighting, Probe's grip.
455
+ // A successful Attack (Brute Force, the default stance) alerts the host
456
+ // without spotting you (p.236); an alerted host's Patrol then runs
457
+ // Matrix Perception every action (DT p.86) -- a loud icon is seen with
458
+ // no test, a silent one gets its opposed roll, tie to the hider -- and
459
+ // the host fight opens only on a sighting and closes when nobody in
460
+ // view is left. New verb: "hide" (p.240). Matrix Search inside a host
461
+ // is reachable again.
462
+ export const ENGINE_VERSION = '1.54.0';
453
463
  //# sourceMappingURL=engine-version.js.map
@@ -177,6 +177,7 @@ import { CounterspellCommand } from './commands/counterspell.js';
177
177
  import { BreachCommand } from './commands/breach.js';
178
178
  import { EmoteCommand } from './commands/emote.js';
179
179
  import { OverwatchCommand } from './commands/overwatch.js';
180
+ import { HideCommand } from './commands/hide.js';
180
181
  import { MarkCommand } from './commands/mark.js';
181
182
  import { SnoopCommand } from './commands/snoop.js';
182
183
  import { TapCommand } from './commands/tap.js';
@@ -2746,6 +2747,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2746
2747
  // GOD's tally ("os" for the tabletop shorthand).
2747
2748
  CommandFactory.registerCommand('overwatch', OverwatchCommand);
2748
2749
  CommandFactory.registerCommand('os', OverwatchCommand);
2750
+ // Hide (SR5 p.240): drop off a host that has spotted you.
2751
+ CommandFactory.registerCommand('hide', HideCommand);
2749
2752
  // MAtrix Recognition Keys (canon p.235-236).
2750
2753
  CommandFactory.registerCommand('mark', MarkCommand);
2751
2754
  // Camera feeds you own (surveillance).
@@ -5665,7 +5668,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5665
5668
  // works the grid while you're back in the meat (the AR-drop exit).
5666
5669
  const remoteAgent = this.companions.find(c => c.kind === 'agent');
5667
5670
  if (remoteAgent && player.plane !== 'matrix') {
5668
- 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}`);
5671
+ 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}`);
5669
5672
  }
5670
5673
  // Off-plane context: which reality you're in, your persona's Matrix
5671
5674
  // track (cold-sim damage lands there), and where your empty body lies.
@@ -5684,7 +5687,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5684
5687
  const track = jackedDeck
5685
5688
  ? ` Deck A${jackedDeck.attack} S${jackedDeck.sleaze} D${jackedDeck.dataProcessing} F${jackedDeck.firewall} ${jackedDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}${pgms}`
5686
5689
  : ` (living persona -- damage is REAL stun)`;
5687
- lines.push(`{cyan-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/cyan-fg}`);
5690
+ lines.push(`{lightblue-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/lightblue-fg}`);
5688
5691
  }
5689
5692
  else if (player.plane === 'astral') {
5690
5693
  lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
@@ -5776,7 +5779,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5776
5779
  const dockW = mapWidth ?? 15;
5777
5780
  const lines = hosts.length > 0
5778
5781
  ? hosts.map(h => hostDockLine(h, this.player, dockW))
5779
- : ['{cyan-fg}nothing overhead{/cyan-fg}'];
5782
+ : ['{lightblue-fg}nothing overhead{/lightblue-fg}'];
5780
5783
  lines.push(caption(this.player.insideHost ? `inside ${this.player.insideHost.name}` : `on the grid from ${gridVicinity(this.player).name}`, mapWidth));
5781
5784
  this.setMapOverlay(lines);
5782
5785
  if (onTheGrid) {
@@ -6278,7 +6281,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6278
6281
  // players" / "crew street"); the bare verb still answers quietly.
6279
6282
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
6280
6283
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
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']] },
6284
+ { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6282
6285
  // The Emerged get their own shelf (player request: "there MUST be
6283
6286
  // resonance" -- matrix is the place, Resonance is the talent).
6284
6287
  // `sustain` shelves here and not under magic: the command is
@@ -289,4 +289,25 @@ export function markHostOver(session, actorName, marks = 1) {
289
289
  throw new Error('markHostOver: this scene has no host');
290
290
  host.hostMarksBy.set(actorName, marks);
291
291
  }
292
+ /**
293
+ * THE HOST HAS MADE THIS PERSONA -- test setup, not a game move.
294
+ *
295
+ * Since 2026-09-14 an alerted host hunts only what it has SPOTTED
296
+ * (models/host.ts `spotted`, utilities/ic.ts hostIsHunting); alert alone
297
+ * makes it look, every action. A fixture that wants the ice already on a
298
+ * persona states both facts here instead of hoping a Patrol roll lands.
299
+ */
300
+ export function spotPersona(session, actorName) {
301
+ const scene = session.game.scene;
302
+ const actor = scene.getPlayerByName(actorName);
303
+ if (!actor)
304
+ throw new Error(`spotPersona: no actor called ${actorName}`);
305
+ const rooms = Object.values(scene.getRooms());
306
+ const here = actor.hostInside?.over.name;
307
+ const host = rooms.find(r => r.hasNode && r.name === here) ?? rooms.find(r => r.hasNode);
308
+ if (!host?.host)
309
+ throw new Error('spotPersona: this scene has no host');
310
+ host.host.alert = true;
311
+ host.host.spotted.add(actorName);
312
+ }
292
313
  //# sourceMappingURL=headless-harness.js.map
@@ -45,8 +45,31 @@ export class Host {
45
45
  * this host's files: the archive is listed for them from then on. A
46
46
  * cracked host lists it for everyone. Session-only. */
47
47
  searchedBy = new Set();
48
- /** Walls up: the host holds a mark on an intruder and its ice is hunting. */
48
+ /** Walls up: the host knows it has an intruder (a blown Sleaze, a
49
+ * successful Attack, a Patrol sighting) and its Patrol is looking
50
+ * every action (Data Trails p.86). Never clears on its own -- canon
51
+ * prints no stand-down. */
49
52
  alert = false;
53
+ /**
54
+ * PERSONAS THE HOST CAN SEE (2026-09-14, "constantly in combat" report).
55
+ *
56
+ * Alert and spotted are two different facts, and canon keeps them
57
+ * apart: a successful Attack action makes the target "aware that it is
58
+ * under attack ... but it doesn't automatically spot you" (p.236), while
59
+ * a blown Sleaze hands the target a free mark on you, which is being
60
+ * seen (p.236). A host and its IC share spotting (p.247), so one set on
61
+ * the host is every program's eyes. Ice hunts what the host can SEE
62
+ * (utilities/ic.ts hostIsHunting); an alerted host that cannot see you
63
+ * yet sweeps for you every action instead (ic.ts ambientPatrol, the
64
+ * focused branch). Cleared by a successful Hide (p.240) and by the
65
+ * persona's reboot (utilities/marks.ts clearMarksOn). Session-only.
66
+ */
67
+ spotted = new Set();
68
+ /** Personas that just Hid (p.240): the focused sweep lets ONE beat pass
69
+ * before it looks again -- "the target must perform a new Matrix
70
+ * Perception action", and the hide was this beat's action, not the
71
+ * host's. Consumed by ic.ts ambientPatrol. Session-only. */
72
+ justHid = new Set();
50
73
  /** IC running now, by kind name. Names, not actors -- until increment 5. */
51
74
  runningIC = [];
52
75
  /** The running IC as actors, by kind (utilities/ic-actors.ts). Empty where no scene could host them. */
@@ -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) {
@@ -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
@@ -440,20 +440,20 @@ export function gridSculpt(actor, room) {
440
440
  const purpose = room.host?.purpose;
441
441
  const sculpt = room.host?.sculpt;
442
442
  return [
443
- `{cyan-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/cyan-fg}`,
444
- `{cyan-fg}${sculpt ?? 'Architecture all the way around -- the outside grid is a wall away and might as well be weather.'}{/cyan-fg}`,
443
+ `{lightblue-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/lightblue-fg}`,
444
+ `{lightblue-fg}${sculpt ?? 'Architecture all the way around -- the outside grid is a wall away and might as well be weather.'}{/lightblue-fg}`,
445
445
  ].join('\n');
446
446
  }
447
447
  // ON THE GRID (SR5 pp.217-218): no street under the persona -- a black
448
448
  // flatland, the body's room named only because that is where the
449
449
  // signal comes from. A host directly overhead gets a line of its own.
450
450
  const body = actor.bodyRoom ?? room;
451
- const header = `{cyan-fg}THE GRID :: jacked in from ${body.name.toUpperCase()}{/cyan-fg}`;
451
+ const header = `{lightblue-fg}THE GRID :: jacked in from ${body.name.toUpperCase()}{/lightblue-fg}`;
452
452
  const grid = actor.currentGrid;
453
- const flat = `{cyan-fg}Black flatland under a black sky -- the icons are the only things out here.${grid ? ` Riding ${grid.name}${grid.userPenalty ? ' (-2 on everything you do, p.233)' : ''}.` : ''}{/cyan-fg}`;
453
+ const flat = `{lightblue-fg}Black flatland under a black sky -- the icons are the only things out here.${grid ? ` Riding ${grid.name}${grid.userPenalty ? ' (-2 on everything you do, p.233)' : ''}.` : ''}{/lightblue-fg}`;
454
454
  if (!room.hasNode)
455
455
  return `${header}\n${flat}`;
456
- return `${header}\n${flat}\n{cyan-fg}${hostLabel(room, { capital: true })} hangs directly overhead, sealed. What it holds is inside it.{/cyan-fg}`;
456
+ return `${header}\n${flat}\n{lightblue-fg}${hostLabel(room, { capital: true })} hangs directly overhead, sealed. What it holds is inside it.{/lightblue-fg}`;
457
457
  }
458
458
  /**
459
459
  * The icons rendered as the standard cyan block, or the thin-grid line.
@@ -482,19 +482,19 @@ export function gridIconBlock(scene, actor, room, rooms) {
482
482
  if (actor.insideHost || !rooms) {
483
483
  const icons = gridIcons(scene, actor, room);
484
484
  if (icons.length === 0) {
485
- return `{cyan-fg}Thin grid out here -- background noise, nothing worth cracking.{/cyan-fg}`;
485
+ return `{lightblue-fg}Thin grid out here -- background noise, nothing worth cracking.{/lightblue-fg}`;
486
486
  }
487
- return `{cyan-fg}ICONS IN REACH:{/cyan-fg}\n${icons.map(i => ` {cyan-fg}${i}{/cyan-fg}`).join('\n')}`;
487
+ return `{lightblue-fg}ICONS IN REACH:{/lightblue-fg}\n${icons.map(i => ` {lightblue-fg}${i}{/lightblue-fg}`).join('\n')}`;
488
488
  }
489
489
  const out = [];
490
490
  const hosts = hostsInReach(rooms, actor);
491
491
  out.push(hosts.length > 0
492
- ? `{cyan-fg}HOSTS OVERHEAD:{/cyan-fg}\n${hosts.map(h => ` {cyan-fg}${hostLine(h, actor)}{/cyan-fg}`).join('\n')}`
493
- : `{cyan-fg}Nothing overhead -- no host stands over this district.{/cyan-fg}`);
492
+ ? `{lightblue-fg}HOSTS OVERHEAD:{/lightblue-fg}\n${hosts.map(h => ` {lightblue-fg}${hostLine(h, actor)}{/lightblue-fg}`).join('\n')}`
493
+ : `{lightblue-fg}Nothing overhead -- no host stands over this district.{/lightblue-fg}`);
494
494
  const near = gridIcons(scene, actor, gridVicinity(actor)).filter(i => !isHostLine(i));
495
495
  out.push(near.length > 0
496
- ? `{cyan-fg}ICONS IN REACH:{/cyan-fg}\n${near.map(i => ` {cyan-fg}${i}{/cyan-fg}`).join('\n')}`
497
- : `{cyan-fg}Nothing near -- background noise around your signal.{/cyan-fg}`);
496
+ ? `{lightblue-fg}ICONS IN REACH:{/lightblue-fg}\n${near.map(i => ` {lightblue-fg}${i}{/lightblue-fg}`).join('\n')}`
497
+ : `{lightblue-fg}Nothing near -- background noise around your signal.{/lightblue-fg}`);
498
498
  // FARTHER OUT, WITHOUT AN ADDRESS (SR5 p.235, p.243): Matrix Perception
499
499
  // never tells you where a device physically sits -- that is Trace Icon,
500
500
  // two marks and an opposed test. So far icons are listed by the noise
@@ -519,9 +519,9 @@ export function gridIconBlock(scene, actor, room, rooms) {
519
519
  byBand.set(noise, list);
520
520
  }
521
521
  const far = [...byBand.entries()].sort((a, b) => a[0] - b[0])
522
- .map(([noise, names]) => ` {cyan-fg}· dim, noise -${noise}: ${names.join(', ')}{/cyan-fg}`);
522
+ .map(([noise, names]) => ` {lightblue-fg}· dim, noise -${noise}: ${names.join(', ')}{/lightblue-fg}`);
523
523
  if (far.length > 0)
524
- out.push(`{cyan-fg}FARTHER OUT -- dimmer with distance:{/cyan-fg}\n${far.join('\n')}`);
524
+ out.push(`{lightblue-fg}FARTHER OUT -- dimmer with distance:{/lightblue-fg}\n${far.join('\n')}`);
525
525
  // A blank line between the distance bands (user, 2026-09-02): three
526
526
  // sections read as three when they do not touch.
527
527
  return out.join('\n\n');
@@ -536,7 +536,7 @@ export function gridIconBlock(scene, actor, room, rooms) {
536
536
  * the other personas, and the files. Cut to the dock's rows and columns.
537
537
  */
538
538
  export function hostInteriorPanel(scene, actor, host, rows, cols) {
539
- const cut = (raw) => `{cyan-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/cyan-fg}`;
539
+ const cut = (raw) => `{lightblue-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/lightblue-fg}`;
540
540
  const lines = [];
541
541
  lines.push(cut(`INSIDE ${host.name}`));
542
542
  if (host.purpose)
@@ -55,8 +55,12 @@ export async function hostTurnStart(scene, host, enc) {
55
55
  * inside it -- even with every program crashed, the host gets its next
56
56
  * turn to relaunch (p.355-356). */
57
57
  export function hostKeepsFighting(scene, host) {
58
+ // ...and someone it can SEE (Host.spotted, 2026-09-14). A persona that
59
+ // Hides (p.240) is still inside and the host is still alert, but with
60
+ // nobody in view the ice has nothing to swing at: the fight closes and
61
+ // the focused Patrol sweep (ic.ts ambientPatrol) takes over.
58
62
  return host.alert && !host.sanctioned
59
- && personasInside(scene, host).some(p => scene.isHumanControlled(p) && !p.isIncapacitated());
63
+ && personasInside(scene, host).some(p => scene.isHumanControlled(p) && !p.isIncapacitated() && host.spotted.has(p.name));
60
64
  }
61
65
  /**
62
66
  * OPEN THE HOST'S FIGHT on an intruder it has made. The aggressor is a
@@ -1,6 +1,7 @@
1
- import { IC_TYPES, icActs } from './ic.js';
1
+ import { IC_TYPES, icActs, isSpotted, patrolPerception, spot } from './ic.js';
2
2
  import { leaveMatrix } from './planes.js';
3
3
  import { Logger } from './logger.js';
4
+ import { personasInside } from './matrix-roster.js';
4
5
  /**
5
6
  * A PROGRAM'S ACTION PHASE (SR5 p.247): one Complex Action, by rule.
6
7
  *
@@ -25,6 +26,34 @@ export async function runIcActionPhase(enc, ice) {
25
26
  const budget = enc.budgetOf(ice);
26
27
  if (!budget)
27
28
  return;
29
+ // PATROL'S PHASE IS A SEARCH, NOT A SWING (p.248; Data Trails p.86: a
30
+ // suspicious Patrol runs Matrix Perception every action). It spends its
31
+ // Complex on the personas the host has NOT found yet -- a hidden one,
32
+ // a silent newcomer -- and a hit hands them to every other program.
33
+ if (type.kind === 'patrol') {
34
+ const out = { lines: [], meta: [], world: [] };
35
+ const unseen = personasInside(enc.scene, host)
36
+ .filter(p => enc.scene.isHumanControlled(p) && !isSpotted(host.over, p));
37
+ for (const p of unseen) {
38
+ if (patrolPerception(host.over, p, out)) {
39
+ spot(host.over, p);
40
+ if (!enc.has(p))
41
+ enc.join(p);
42
+ out.lines.push(` {red-fg}Patrol IC finds ${p.name}'s icon in the traffic -- the host has them now.{/red-fg}`);
43
+ out.world.push(`${host.name}'s Patrol IC found ${p.name}.`);
44
+ }
45
+ else {
46
+ out.lines.push(` Patrol IC quarters the datastream for ${p.name} and comes up empty -- this pass.`);
47
+ }
48
+ }
49
+ if (unseen.length === 0)
50
+ out.lines.push(` Patrol IC keeps every icon it can see in view and hands each move to the host.`);
51
+ budget.spend('complex', type.name);
52
+ for (const w of out.world)
53
+ enc.scene.addWorldEvent(w);
54
+ await enc.announce(out.lines, out.meta);
55
+ return;
56
+ }
28
57
  const target = pickTarget(enc, ice);
29
58
  if (!target) {
30
59
  logger.write(`Ice: ${ice.name} has nobody in the host to hunt -- phase forfeited.`);
@@ -66,7 +95,11 @@ export async function runIcActionPhase(enc, ice) {
66
95
  * read), then whoever it last engaged, then the first enemy standing.
67
96
  */
68
97
  function pickTarget(enc, ice) {
69
- const enemies = enc.enemiesOf(ice).filter(e => e.plane === 'matrix');
98
+ // Only what the host can SEE (Host.spotted, shared by all its IC,
99
+ // p.247): a persona that Hid is still a participant until the fight
100
+ // notices nobody is left in view, and nothing may swing at it meanwhile.
101
+ const over = ice.icHost?.over;
102
+ const enemies = enc.enemiesOf(ice).filter(e => e.plane === 'matrix' && (!over || isSpotted(over, e)));
70
103
  if (enemies.length === 0)
71
104
  return undefined;
72
105
  const marksOn = ice.icHost?.marksOn;
@@ -140,6 +140,21 @@ const empty = () => ({ lines: [], meta: [], world: [] });
140
140
  * maglocks fall, the vault unseals -- but it stops being an immunity.
141
141
  */
142
142
  export function hostIsHunting(room, actor) {
143
+ return hostSuspectsOrHunts(room, actor)
144
+ // ...AND IT CAN SEE YOU (2026-09-14, "constantly in combat, even
145
+ // running silent"). Alert used to be the whole test, so a host woken
146
+ // by a successful Brute Force -- which canon says leaves it "aware
147
+ // that it is under attack ... but it doesn't automatically spot you"
148
+ // (p.236) -- opened the fight on the next keystroke and the silent
149
+ // test in ambientPatrol was never rolled (it bailed on alert). Ice
150
+ // hunts what the host has SPOTTED (Host.spotted, shared with every
151
+ // program, p.247); an alerted host that has not spotted you sweeps
152
+ // for you every action instead (ambientPatrol, focused).
153
+ && isSpotted(room, actor);
154
+ }
155
+ /** Alert, inside, unsanctioned -- the host is looking, whether or not it
156
+ * has found this persona yet. The two halves below split on that. */
157
+ function hostSuspectsOrHunts(room, actor) {
143
158
  return actor.plane === 'matrix'
144
159
  && room.hasNode
145
160
  // IC RUNS INSIDE THE HOST, AND NOWHERE ELSE (p.246-247; Deditri,
@@ -161,6 +176,27 @@ export function hostIsHunting(room, actor) {
161
176
  && !room.hostSanctioned
162
177
  && room.hostAlert;
163
178
  }
179
+ /** An alerted host that has NOT found this persona: its Patrol is
180
+ * running Matrix Perception every action (Data Trails p.86). */
181
+ export function hostSuspects(room, actor) {
182
+ return hostSuspectsOrHunts(room, actor) && !isSpotted(room, actor);
183
+ }
184
+ /** Has this host (and so all its IC, p.247) got eyes on the persona? */
185
+ export function isSpotted(room, actor) {
186
+ return room.host?.spotted.has(actor.name) ?? false;
187
+ }
188
+ /**
189
+ * THE HOST SEES YOU. One door for every way of being made -- a blown
190
+ * Sleaze's free mark (p.236), a Patrol sighting, Probe's grip -- so the
191
+ * fact that alert and spotted are two things is written in one place.
192
+ */
193
+ export function spot(room, actor) {
194
+ const host = room.host;
195
+ if (!host)
196
+ return;
197
+ host.alert = true;
198
+ host.spotted.add(actor.name);
199
+ }
164
200
  /**
165
201
  * IS THIS PERSONA A LEGITIMATE USER? (p.248, Patrol.)
166
202
  *
@@ -234,39 +270,46 @@ export function ambientPatrol(room, actor) {
234
270
  // back, it is not watching for you. See hostIsHunting.
235
271
  if (room.hostSanctioned)
236
272
  return out;
237
- // Already made: the alerted host's own turn owns that persona
238
- // (runHostTurn dispatches there instead). A CRACKED host is no longer
239
- // exempt -- see hostIsHunting for why that ruling was retired.
240
- if (room.hostAlert)
241
- return out;
242
- if (looksLegitimate(room, actor))
273
+ // Already SEEN: the alerted host's ice owns that persona (hostIsHunting
274
+ // -> the host arena's Combat Turn). A CRACKED host is no longer exempt
275
+ // -- see hostIsHunting for why that ruling was retired.
276
+ if (isSpotted(room, actor))
243
277
  return out;
244
- // The sweep is on a clock (Data Trails p.86) -- it is not looking
245
- // every single moment until it has reason to.
246
- const due = (room.patrolCountdown ?? rollPatrolInterval(room.hostRating)) - 1;
247
- if (due > 0) {
248
- room.patrolCountdown = due;
278
+ // TWO CADENCES, AND THE HOST'S ALERT PICKS BETWEEN THEM (Data Trails
279
+ // p.86, the "constantly in combat" report of 2026-09-14). An
280
+ // unsuspecting host sweeps on the rating interval and lets a marked
281
+ // user be (p.248). A host that KNOWS it has an intruder -- a Brute
282
+ // Force it felt, a Sleaze it caught, a sighting -- runs Matrix
283
+ // Perception "every action", and it is not reassured by a mark: host
284
+ // security stays "vigilant for unauthorized users, even those with
285
+ // marks" (DT p.168-169), and the mark a Brute Force just planted is
286
+ // exactly what it is looking at. This used to return early on alert
287
+ // instead, which is why running silent never got its roll.
288
+ const focused = room.hostAlert;
289
+ // A persona that Hid THIS beat is not re-swept on the same beat: the
290
+ // hide was its action; the host's new perception is its own (p.240).
291
+ if (focused && room.host?.justHid.delete(actor.name))
249
292
  return out;
293
+ if (!focused) {
294
+ if (looksLegitimate(room, actor))
295
+ return out;
296
+ // The sweep is on a clock (Data Trails p.86) -- it is not looking
297
+ // every single moment until it has reason to.
298
+ const due = (room.patrolCountdown ?? rollPatrolInterval(room.hostRating)) - 1;
299
+ if (due > 0) {
300
+ room.patrolCountdown = due;
301
+ return out;
302
+ }
303
+ room.patrolCountdown = rollPatrolInterval(room.hostRating);
250
304
  }
251
- room.patrolCountdown = rollPatrolInterval(room.hostRating);
252
- let spotted;
253
- if (!actor.runningSilent) {
254
- // p.235: no test. A visible icon in the host is simply seen.
255
- spotted = true;
256
- out.meta.push(`Patrol IC -- sweep: icon not running silent, spotted automatically (no test, p.235)`);
257
- }
258
- else {
259
- const look = rollPool(room.hostRating * 2, room.hostAttributes().dataProcessing);
260
- const hide = rollPool(actor.logic + actor.matrixAttribute('sleaze'));
261
- // p.236: a tie goes to the hider.
262
- spotted = look.hits > hide.hits;
263
- out.meta.push(`Patrol IC -- Matrix Perception (Host Rating x2 [Data Processing]): ${formatRoll(look)} v. Logic + Sleaze: ${formatRoll(hide)}`);
264
- }
305
+ const spotted = patrolPerception(room, actor, out);
265
306
  if (!spotted) {
266
- out.lines.push(` Something quarters the datastream around you and moves on. Your silence held.`);
307
+ out.lines.push(focused
308
+ ? ` Patrol IC is HUNTING -- it quarters the datastream around you, hard, and slides past. Your silence held. For now.`
309
+ : ` Something quarters the datastream around you and moves on. Your silence held.`);
267
310
  return out;
268
311
  }
269
- room.hostAlert = true;
312
+ spot(room, actor);
270
313
  actor.sneaking = false;
271
314
  // NAME THE HOST THAT FOUND YOU (rG8LtMoy5DHQJ2NuG: "I got a
272
315
  // notification that an IC has found me, however I haven't entered a
@@ -282,10 +325,32 @@ export function ambientPatrol(room, actor) {
282
325
  // So it says WHICH host, and where it is. That is the whole fix; the
283
326
  // deeper problem -- that this engine has no inside-vs-outside-host
284
327
  // state for the player to have crossed -- is its own item.
285
- out.lines.push(` {red-fg}${hostLabel(room, { capital: true })} makes you -- an unmarked icon where none should be. You're inside its architecture, on ${room.name}'s patch of grid, and it knows it has an intruder.{/red-fg}`);
286
- out.world.push(`${room.name}'s Patrol IC made ${actor.name} on a routine sweep -- alarm flagged, ice hunting.`);
328
+ out.lines.push(focused
329
+ ? ` {red-fg}${hostLabel(room, { capital: true })} makes you -- the hunting Patrol locks onto your icon and every program in the architecture has you now.{/red-fg}`
330
+ : ` {red-fg}${hostLabel(room, { capital: true })} makes you -- an unmarked icon where none should be. You're inside its architecture, on ${room.name}'s patch of grid, and it knows it has an intruder.{/red-fg}`);
331
+ out.world.push(focused
332
+ ? `${room.name}'s hunting Patrol IC found ${actor.name} -- ice closing.`
333
+ : `${room.name}'s Patrol IC made ${actor.name} on a routine sweep -- alarm flagged, ice hunting.`);
287
334
  return out;
288
335
  }
336
+ /**
337
+ * ONE MATRIX PERCEPTION BY THE HOST'S PATROL against one persona, the
338
+ * dice canon prints and nothing more: a non-silent icon is seen with no
339
+ * test (p.235); a silent one is an Opposed Test, Host Rating x2 [Data
340
+ * Processing] (the IC pool, p.247; the perception bracket, p.241) v.
341
+ * Logic + Sleaze, and a tie goes to the hider (p.236). Returns whether
342
+ * the persona was seen; the caller decides what that costs.
343
+ */
344
+ export function patrolPerception(room, actor, out) {
345
+ if (!actor.runningSilent) {
346
+ out.meta.push(`Patrol IC -- sweep: icon not running silent, spotted automatically (no test, p.235)`);
347
+ return true;
348
+ }
349
+ const look = rollPool(room.hostRating * 2, room.hostAttributes().dataProcessing);
350
+ const hide = rollPool(actor.logic + actor.matrixAttribute('sleaze'));
351
+ out.meta.push(`Patrol IC -- Matrix Perception (Host Rating x2 [Data Processing]): ${formatRoll(look)} v. Logic + Sleaze: ${formatRoll(hide)}`);
352
+ return look.hits > hide.hits;
353
+ }
289
354
  /**
290
355
  * THE HOST'S WHOLE TURN: its eyes if it has not made you, its ice if it
291
356
  * has. One entry point, so the caller never has to know which half of
@@ -618,11 +683,17 @@ function dealBiofeedback(actor, dv, type, out) {
618
683
  * pre-alert patrolling exists, the interval table is what it needs.
619
684
  */
620
685
  function patrolActs(room, actor, out) {
621
- // No Attack rating means no bracket to cap the hits with.
622
- const roll = rollPool(room.hostRating * 2);
623
- out.meta.push(`Patrol IC -- Matrix Perception (Host Rating x2): ${formatRoll(roll)}`);
624
- if (roll.hits > 0) {
625
- out.lines.push(` {red-fg}Patrol IC sweeps your icon and hands what it sees to the host.{/red-fg}`);
686
+ // A persona the host can already see (which is what this composition
687
+ // is handed) gets no second roll -- Patrol's perception is the one in
688
+ // patrolPerception, spent on personas it has NOT found (ic-brain.ts
689
+ // runs it against each unspotted icon inside during the fight).
690
+ if (isSpotted(room, actor)) {
691
+ out.lines.push(` {red-fg}Patrol IC keeps your icon in view and hands every move to the host.{/red-fg}`);
692
+ return;
693
+ }
694
+ if (patrolPerception(room, actor, out)) {
695
+ spot(room, actor);
696
+ out.lines.push(` {red-fg}Patrol IC sweeps your icon and hands what it sees to the host -- it has you now.{/red-fg}`);
626
697
  }
627
698
  else {
628
699
  // "Does not take Matrix damage when it fails" -- a whiff costs it
@@ -659,6 +730,8 @@ function probeActs(room, actor, type, out, ice) {
659
730
  }
660
731
  const now = held + 1;
661
732
  room.hostMarksOn.set(actor.name, now);
733
+ // A grip is a sighting: the host and every program see what Probe holds.
734
+ spot(room, actor);
662
735
  out.lines.push(` {red-fg}Probe IC gets a grip -- the host holds ${now} mark${now === 1 ? '' : 's'} on you now${now >= MAX_MARKS ? ', all it can carry' : ''}.{/red-fg}`);
663
736
  out.world.push(`${room.name}'s ice tightened its hold on ${actor.name}.`);
664
737
  }
@@ -44,6 +44,9 @@ export function clearMarksOn(actor, scene) {
44
44
  if ((room.host?.marksOn.get(actor.name) ?? 0) > 0)
45
45
  had++;
46
46
  room.host?.marksOn.delete(actor.name);
47
+ // A rebooted persona is a fresh icon: whatever the host had SPOTTED
48
+ // is gone with the marks (Host.spotted) -- it has to find you again.
49
+ room.host?.spotted.delete(actor.name);
47
50
  }
48
51
  }
49
52
  return had;
@@ -0,0 +1,8 @@
1
+ export const MATRIX_COLOR = 'lightblue';
2
+ export const FEED_COLOR = 'white';
3
+ export const TURN_COLOR = 'yellow';
4
+ /** Matrix-plane prose, tagged. */
5
+ export function mx(text) {
6
+ return `{${MATRIX_COLOR}-fg}${text}{/${MATRIX_COLOR}-fg}`;
7
+ }
8
+ //# sourceMappingURL=matrix-style.js.map
@@ -58,10 +58,10 @@ export function accrueOverwatch(scene, actor, defenseHits, why) {
58
58
  // The grid's built-in warning system (p.221): subtle ripples, once
59
59
  // per band -- the only free hint the player ever gets.
60
60
  if (before < 30 && actor.overwatchScore >= 30) {
61
- return { lines: [`{cyan-fg}The grid RIPPLES -- hard, close, and looking. Something vast has nearly found the thread you're hanging by. ("reboot" is a clean slate.){/cyan-fg}`], converged: false };
61
+ return { lines: [`{lightblue-fg}The grid RIPPLES -- hard, close, and looking. Something vast has nearly found the thread you're hanging by. ("reboot" is a clean slate.){/lightblue-fg}`], converged: false };
62
62
  }
63
63
  if (before < 20 && actor.overwatchScore >= 20) {
64
- return { lines: [`{cyan-fg}A ripple crosses the grid -- subtle, wrong, deliberate. Somewhere above, an eye has started counting your fingerprints.{/cyan-fg}`], converged: false };
64
+ return { lines: [`{lightblue-fg}A ripple crosses the grid -- subtle, wrong, deliberate. Somewhere above, an eye has started counting your fingerprints.{/lightblue-fg}`], converged: false };
65
65
  }
66
66
  return { lines: [], converged: false };
67
67
  }
@@ -107,7 +107,7 @@ export function enterMatrix(scene, actor, mode) {
107
107
  // half-done, which would be a fresh canon violation wearing a
108
108
  // feature's clothes. See the PR for CgJ6uTfEvTd8JXdDN.
109
109
  actor.activeDeck = actor.isTechnomancer() ? undefined : actor.getCyberdeck();
110
- actor.performAction('jacks in', `slumping where they stand, eyes flickering (${mode}-sim)`);
110
+ actor.performAction('jacks in', `slumping where they stand, eyes flickering (${mode}-sim)`, { quiet: scene.isHumanControlled?.(actor) ?? false });
111
111
  scene.addWorldEvent(`${actor.name} jacked into the Matrix in ${actor.bodyRoom.name}.`);
112
112
  Logger.getInstance().write(`${actor.name} entered the Matrix (${mode}-sim, ${actor.activeDeck?.name ?? (actor.isTechnomancer() ? 'living persona' : 'no deck?!')}) from ${actor.bodyRoom.name}.`);
113
113
  const hotWarning = mode === 'hot'
@@ -267,7 +267,7 @@ export function leaveMatrix(scene, actor, opts) {
267
267
  actor.bodyRoom = undefined;
268
268
  lines.push(`You're back in your body in ${body.name}.`);
269
269
  }
270
- actor.performAction('jacks out', 'stirring awake');
270
+ actor.performAction('jacks out', 'stirring awake', { quiet: scene.isHumanControlled?.(actor) ?? false });
271
271
  scene.addWorldEvent(`${actor.name} jacked out of the Matrix.`);
272
272
  scene.updateStatus();
273
273
  return lines;
@@ -449,7 +449,7 @@ export function fileReach(actor, room) {
449
449
  */
450
450
  export function planeTintItemName(item, viewerPlane) {
451
451
  if (item.plane === 'matrix') {
452
- return `{cyan-fg}${item.name} [data]{/cyan-fg}`;
452
+ return `{lightblue-fg}${item.name} [data]{/lightblue-fg}`;
453
453
  }
454
454
  if (item.plane === 'astral') {
455
455
  return `{magenta-fg}${item.name} [astral]{/magenta-fg}`;
@@ -24,6 +24,7 @@ import { join, dirname } from 'node:path';
24
24
  import { Logger } from './logger.js';
25
25
  import { hint } from './hints.js';
26
26
  import { CALL_ICON, CALL_COLOR, END_CALL_SENTINEL } from './comm-style.js';
27
+ import { FEED_COLOR } from './matrix-style.js';
27
28
  import { ENGINE_VERSION } from '../engine-version.js';
28
29
  import { authToken, fetchSave } from './cloud-saves.js';
29
30
  import { saveSlug, restorePlayer, writeSaveAtomic } from './persistence.js';
@@ -1143,7 +1144,7 @@ export class SharedRunSession {
1143
1144
  break;
1144
1145
  }
1145
1146
  for (const line of ev.text.split('\n'))
1146
- g.logBox?.pushLine(`{cyan-fg}${line}{/cyan-fg}`);
1147
+ g.logBox?.pushLine(`{${FEED_COLOR}-fg}${line}{/${FEED_COLOR}-fg}`);
1147
1148
  Logger.getInstance().transcript(ev.text);
1148
1149
  g.followLog?.();
1149
1150
  if (ev.kind === 'death' && ev.data?.player === g.player.name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.183.0",
3
+ "version": "5.185.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.",