@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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/delay.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +6 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-turn.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +11 -13
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +10 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +110 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +4 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +13 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +7 -4
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +21 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +24 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-brain.js +35 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +107 -34
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +2 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
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 = `{
|
|
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, `{
|
|
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 => ` {
|
|
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 [`{
|
|
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
|
-
`{
|
|
112
|
-
...tags.map(t => ` {
|
|
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(`{
|
|
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 = [`{
|
|
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
|
-
`{
|
|
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
|
-
`{
|
|
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(`{
|
|
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
|
-
`{
|
|
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
|
-
`{
|
|
87
|
-
`
|
|
88
|
-
|
|
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 `{
|
|
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 `{
|
|
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(` {
|
|
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(` {
|
|
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(` {
|
|
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(` {
|
|
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 `{
|
|
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
|
-
`{
|
|
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{
|
|
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
|
-
`{
|
|
400
|
-
`{
|
|
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 `{
|
|
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 = [`{
|
|
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
|
-
`{
|
|
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 `{
|
|
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{
|
|
317
|
-
: `\n{
|
|
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(`{
|
|
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 ? ' {
|
|
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
|
-
`{
|
|
162
|
-
` {
|
|
163
|
-
` {
|
|
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(` {
|
|
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`)}.`}{/
|
|
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(` {
|
|
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(` {
|
|
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 ? ' {
|
|
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
|
-
|
|
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(`{
|
|
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(`{
|
|
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
|
-
: ['{
|
|
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
|
|
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}`,
|
|
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 `{
|
|
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{
|
|
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 `{
|
|
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 `{
|
|
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
|
-
`{
|
|
444
|
-
`{
|
|
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 = `{
|
|
451
|
+
const header = `{lightblue-fg}THE GRID :: jacked in from ${body.name.toUpperCase()}{/lightblue-fg}`;
|
|
452
452
|
const grid = actor.currentGrid;
|
|
453
|
-
const flat = `{
|
|
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{
|
|
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 `{
|
|
485
|
+
return `{lightblue-fg}Thin grid out here -- background noise, nothing worth cracking.{/lightblue-fg}`;
|
|
486
486
|
}
|
|
487
|
-
return `{
|
|
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
|
-
? `{
|
|
493
|
-
: `{
|
|
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
|
-
? `{
|
|
497
|
-
: `{
|
|
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]) => ` {
|
|
522
|
+
.map(([noise, names]) => ` {lightblue-fg}· dim, noise -${noise}: ${names.join(', ')}{/lightblue-fg}`);
|
|
523
523
|
if (far.length > 0)
|
|
524
|
-
out.push(`{
|
|
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) => `{
|
|
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
|
-
|
|
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
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
if (room
|
|
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
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
286
|
-
|
|
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
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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: [`{
|
|
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: [`{
|
|
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 `{
|
|
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(`{
|
|
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.
|
|
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.",
|