@maka/maka-cli 5.184.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/disable.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +110 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +4 -1
- 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/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/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.",
|
|
@@ -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));
|
|
@@ -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.`);
|
|
@@ -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
|
|
@@ -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();
|
|
@@ -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();
|
|
@@ -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).
|
|
@@ -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. */
|
|
@@ -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;
|
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.",
|