@maka/maka-cli 5.219.0 → 5.221.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 +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +18 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +22 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/shout.js +15 -4
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +75 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js +65 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +15 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +12 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-roster.js +54 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/nameables.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/silent-icons.js +137 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.221.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.",
|
|
@@ -72,7 +72,7 @@ export class DisableCommand extends Command {
|
|
|
72
72
|
// 2. A DEVICE ICON: inside the host, its WAN; from the grid, a slaved
|
|
73
73
|
// device you hold a host mark for, or a loose wireless one where
|
|
74
74
|
// the body stands. Mirrors hack.ts tryDeviceHack.
|
|
75
|
-
const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.name.toLowerCase().includes(raw)
|
|
75
|
+
const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.hasIconFor(actor.name)).filter(d => d.name.toLowerCase().includes(raw)
|
|
76
76
|
|| (d.opensExit ?? '').toLowerCase() === raw
|
|
77
77
|
|| d.description.toLowerCase().includes(raw));
|
|
78
78
|
const inside = actor.insideHost;
|
|
@@ -569,7 +569,7 @@ export class HackCommand extends BypassCommand {
|
|
|
569
569
|
if (!named)
|
|
570
570
|
return null;
|
|
571
571
|
const raw = named.toLowerCase();
|
|
572
|
-
const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.name.toLowerCase().includes(raw)
|
|
572
|
+
const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.hasIconFor(actor.name)).filter(d => d.name.toLowerCase().includes(raw)
|
|
573
573
|
|| (d.opensExit ?? '').toLowerCase() === raw
|
|
574
574
|
|| d.description.toLowerCase().includes(raw));
|
|
575
575
|
// INSIDE THE HOST you are directly connected to every device on
|
|
@@ -19,6 +19,7 @@ import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
|
19
19
|
import { findCatalogItem, describeEffect } from '../utilities/catalog.js';
|
|
20
20
|
import { billAction } from '../utilities/action-cost.js';
|
|
21
21
|
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
22
|
+
import { spotHiddenIcons, spotLine, spotMechanics } from '../utilities/silent-icons.js';
|
|
22
23
|
export class LookCommand extends Command {
|
|
23
24
|
static verb = 'look';
|
|
24
25
|
static description = 'Examine your surroundings';
|
|
@@ -322,8 +323,25 @@ export class LookCommand extends Command {
|
|
|
322
323
|
// IS the meat world: the furniture, the smells, the people leaning
|
|
323
324
|
// on it. gridSculpt (grid-view.ts) orients on what a persona
|
|
324
325
|
// actually stands in: the room's name and the density of its grid.
|
|
326
|
+
// SWEEP FOR WHAT IS HIDING, BEFORE DRAWING THE LIST (p.217: "some
|
|
327
|
+
// icons are deliberately hidden from view, such as locks and other
|
|
328
|
+
// security devices ... and of course people who prefer not to be
|
|
329
|
+
// seen"; p.235-236, p.241 for the test). `look` on the grid IS
|
|
330
|
+
// Matrix Perception and is already billed as one above, so the
|
|
331
|
+
// sweep costs exactly what canon charges and a second look is a
|
|
332
|
+
// second action. Run FIRST so anything this sweep turns up appears
|
|
333
|
+
// in the icon block below rather than one look later.
|
|
334
|
+
const swept = spotHiddenIcons(this.scene, this.actor, this.actor.insideHost === room ? room : gridVicinity(this.actor));
|
|
325
335
|
const lines = [gridSculpt(this.actor, room)];
|
|
326
336
|
lines.push(gridIconBlock(this.scene, this.actor, room, this.rooms));
|
|
337
|
+
if (swept) {
|
|
338
|
+
lines.push(spotLine(swept));
|
|
339
|
+
if (showsMechanics(this.scene, this.actor)) {
|
|
340
|
+
for (const m of spotMechanics(swept)) {
|
|
341
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, this.actor)}${m}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
327
345
|
// THE COACHING HAS TO MATCH THE ROOM YOU ARE IN. Inside a host,
|
|
328
346
|
// `go` is refused (go.ts checkHostBoundary) and `map` draws the
|
|
329
347
|
// outside grid you cannot reach -- so offering both is the same
|
|
@@ -313,10 +313,28 @@ export class SayCommand extends Command {
|
|
|
313
313
|
// presence on the grid and is not -- "call" is still the verb that
|
|
314
314
|
// reaches a body.
|
|
315
315
|
if (currentPlayer.plane === 'matrix' && !(this.actor instanceof NPC)) {
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
const { personas, links } = speakOnTheGrid(this.scene, currentPlayer, '');
|
|
317
|
+
const iconNames = personas.map(a => a.name);
|
|
318
|
+
const linkNames = links.map(a => a.name);
|
|
319
|
+
const lines = [];
|
|
320
|
+
if (iconNames.length > 0) {
|
|
321
|
+
lines.push(`Your persona speaks and the grid carries it: ${iconNames.join(', ')} ${iconNames.length === 1 ? 'is' : 'are'} on this side of the architecture and hears you.`);
|
|
322
|
+
}
|
|
323
|
+
// A COMMLINK IS FOR TALKING (FhXadizkMCxAdMaJx). This used to be
|
|
324
|
+
// the refusal -- "a PAN icon is a commlink in somebody's pocket,
|
|
325
|
+
// not a person you can talk to" -- which had the fact right and
|
|
326
|
+
// drew the wrong conclusion. It IS a commlink in their pocket, and
|
|
327
|
+
// that is precisely a thing you can speak out of.
|
|
328
|
+
if (linkNames.length > 0) {
|
|
329
|
+
lines.push(`It comes out of ${linkNames.join(', ')}'s commlink where ${linkNames.length === 1 ? 'they stand' : 'they stand'} in the flesh -- your voice out of ${linkNames.length === 1 ? 'their' : 'their'} pocket, not out of the air.`);
|
|
330
|
+
}
|
|
331
|
+
if (lines.length === 0) {
|
|
332
|
+
return `Your words go out over the grid and land on nothing. No persona on this side of the architecture, and nobody near your body is carrying a live commlink for them to come out of. ("call <name>" reaches somebody further off.)`;
|
|
333
|
+
}
|
|
334
|
+
if (linkNames.length === 0) {
|
|
335
|
+
lines.push(`Bodies in the room hear nothing -- nobody there is carrying a live link.`);
|
|
336
|
+
}
|
|
337
|
+
return lines.join('\n');
|
|
320
338
|
}
|
|
321
339
|
return farOff;
|
|
322
340
|
}
|
|
@@ -52,10 +52,21 @@ export class ShoutCommand extends Command {
|
|
|
52
52
|
// each other or they do not, and no amount of volume crosses a
|
|
53
53
|
// host wall. So this routes to the same fan-out `say` uses rather
|
|
54
54
|
// than inventing a louder one.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// Both halves of the fan-out, named apart: a persona beside you and
|
|
56
|
+
// a commlink in somebody's pocket are two different places for your
|
|
57
|
+
// voice to come out of, and only one of them is standing in the
|
|
58
|
+
// same room as your body (FhXadizkMCxAdMaJx).
|
|
59
|
+
const { personas, links } = speakOnTheGrid(this.scene, actor, message);
|
|
60
|
+
const heard = personas.map(a => a.name);
|
|
61
|
+
const onLink = links.map(a => a.name);
|
|
62
|
+
if (heard.length === 0 && onLink.length === 0) {
|
|
63
|
+
return `You push it out across the grid -- nothing out here is listening.`;
|
|
64
|
+
}
|
|
65
|
+
const who = [
|
|
66
|
+
...(heard.length > 0 ? [`Heard by ${heard.join(', ')}.`] : []),
|
|
67
|
+
...(onLink.length > 0 ? [`Out of ${onLink.join(', ')}'s commlink, where they stand in the flesh.`] : []),
|
|
68
|
+
];
|
|
69
|
+
return `You push it out across the grid: "${message}"\n ${who.join('\n ')}`;
|
|
59
70
|
}
|
|
60
71
|
const heard = actor.currentLocation.shout(actor, message);
|
|
61
72
|
if (!(this.actor instanceof NPC)) {
|
|
@@ -1205,5 +1205,79 @@
|
|
|
1205
1205
|
// other at all. The meat case is pinned in the same test file, because
|
|
1206
1206
|
// a fix that traded this report for the older one it was built from
|
|
1207
1207
|
// would be no fix.
|
|
1208
|
-
|
|
1208
|
+
// 1.79.0 (2026-09-16): A COMMLINK IS FOR TALKING (FhXadizkMCxAdMaJx,
|
|
1209
|
+
// Maka, rejecting the first fix: "this looks like it's backwards. If I
|
|
1210
|
+
// jack in, and Grunt's icon is in reach, he SHOULD hear me through his
|
|
1211
|
+
// icon when I do a 'say'"). He is right and the books agree. p.246
|
|
1212
|
+
// names this as the exception to the host wall itself -- from outside
|
|
1213
|
+
// a host "you can't interact directly with icons inside it, ALTHOUGH
|
|
1214
|
+
// you can still send messages, make commcalls, and that sort of thing"
|
|
1215
|
+
// -- and nothing anywhere requires the person on the other end to be
|
|
1216
|
+
// jacked in. The old answer, "a PAN icon is a commlink in somebody's
|
|
1217
|
+
// pocket, not a person you can talk to", had the fact right and drew
|
|
1218
|
+
// the wrong conclusion from it: a commlink in somebody's pocket is
|
|
1219
|
+
// exactly a thing a voice can come out of.
|
|
1220
|
+
// - `say` and `shout` from the grid now reach the PERSONAS beside you
|
|
1221
|
+
// (as always) AND the live commlinks of people standing around your
|
|
1222
|
+
// BODY, delivered with hear()'s existing viaLink so the NPC knows it
|
|
1223
|
+
// came out of their pocket rather than from a mouth in the room.
|
|
1224
|
+
// - SCOPED TO THE VICINITY, not the district. Canon would let a
|
|
1225
|
+
// persona call a commlink anywhere on the grid -- distance is noise,
|
|
1226
|
+
// not range -- but a `say` that rang every commlink in the barrens
|
|
1227
|
+
// is a broadcast nobody asked for. "In reach" means what it means in
|
|
1228
|
+
// the icon list the reporter was reading: the room the body is in.
|
|
1229
|
+
// `call <name>` still reaches further.
|
|
1230
|
+
// - isLinked is the test, the same predicate that decides whether a
|
|
1231
|
+
// body is a ▣ icon at all, so the icon list and the voice agree: no
|
|
1232
|
+
// link, no icon, no pocket for the words to come out of -- and the
|
|
1233
|
+
// refusal says which of the two reasons it was.
|
|
1234
|
+
// The old test asserted the right refusal for the WRONG REASON: its
|
|
1235
|
+
// comment claimed "Krow is HERE, in the flesh, with a PAN icon on the
|
|
1236
|
+
// grid" while its fixture gave him no commlink at all, so there was
|
|
1237
|
+
// never a PAN icon in it. Pinned properly now, both ways.
|
|
1238
|
+
// 1.80.0 (2026-09-16): SECURITY ICONS ARE DELIBERATELY HIDDEN
|
|
1239
|
+
// (eJANPpm7qCaCAJBJw). The reporter rejected the last pass with "double
|
|
1240
|
+
// check canon again", then quoted p.217 in full, and the full sentence
|
|
1241
|
+
// is the whole design:
|
|
1242
|
+
// "some icons are deliberately hidden from view, such as locks and
|
|
1243
|
+
// other security devices, baby monitors, maintenance monitors, and
|
|
1244
|
+
// of course PEOPLE WHO PREFER NOT TO BE SEEN."
|
|
1245
|
+
// Two things fall out of that list. It is ONE list -- the maglock and
|
|
1246
|
+
// the runner who went dark in the same breath -- and the last clause
|
|
1247
|
+
// identifies the mechanic: "people who prefer not to be seen" is
|
|
1248
|
+
// RUNNING SILENT (p.235-236), the only hiding the rules define. So
|
|
1249
|
+
// nothing here is invented; the sentence points at a rule that existed.
|
|
1250
|
+
// - Device.silent, true by default for the security kinds ('lock',
|
|
1251
|
+
// 'panel' -- the maglock, the keypad, the safe). A ward is the Art
|
|
1252
|
+
// and has no wireless mode to hide in.
|
|
1253
|
+
// - CAMERAS TOO, which p.217 names FIRST. The cluster is not a Device
|
|
1254
|
+
// (it is derived from the room), so it hides on Room.camerasSpottedBy
|
|
1255
|
+
// and resists with its master host's rating -- a site's lenses are as
|
|
1256
|
+
// quiet as the security that runs them.
|
|
1257
|
+
// - AND PEOPLE. Going dark used to make an icon simply unfindable:
|
|
1258
|
+
// isLinked answers false and nothing anywhere rolled to beat it, so
|
|
1259
|
+
// the book's own example of a hidden icon was the one case with no
|
|
1260
|
+
// way through. Player.matrixSpottedBy closes it.
|
|
1261
|
+
// - THE SWEEP is utilities/silent-icons.ts, fired by `look` on the grid
|
|
1262
|
+
// -- because `look` there IS Matrix Perception (p.241) and is already
|
|
1263
|
+
// billed as the Complex Action it is. A second look is a second
|
|
1264
|
+
// action, which is what makes a dark room cost something. Ties go to
|
|
1265
|
+
// the hider (p.236). Per viewer: one decker spotting an icon does not
|
|
1266
|
+
// hand it to the team.
|
|
1267
|
+
// - A MISS STILL REPORTS SOMETHING IS THERE, which is step one of the
|
|
1268
|
+
// canon two-step: a hit buys "there are icons running silent in the
|
|
1269
|
+
// vicinity" (p.235-236). A failed sweep is not silence -- it is the
|
|
1270
|
+
// difference between a hidden lock and a door with no lock on it.
|
|
1271
|
+
// - THE ICON HIDES; THE OBJECT DOES NOT. This is a Matrix fact and
|
|
1272
|
+
// nothing else. The maglock is still screwed to the door, still
|
|
1273
|
+
// described by a meat-world look, still picked, breached, shot and
|
|
1274
|
+
// keyed exactly as before. Only the grid view, the AR overlay,
|
|
1275
|
+
// Matrix-side Tab and Matrix-side targeting ask the question -- and
|
|
1276
|
+
// the AR overlay matters: hide the icon on the grid alone and the
|
|
1277
|
+
// street overlay quietly becomes a free Matrix Perception.
|
|
1278
|
+
// NOT INVENTED, deliberately: the -2 a silent icon pays is left on what
|
|
1279
|
+
// it DOES (p.235-236, already charged to players through
|
|
1280
|
+
// matrixActionPenalty) rather than added to its hide roll. Spending it
|
|
1281
|
+
// twice would be a house rule wearing a citation.
|
|
1282
|
+
export const ENGINE_VERSION = '1.80.0';
|
|
1209
1283
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -16,6 +16,15 @@ export const DEFAULT_DEVICE_RATING = {
|
|
|
16
16
|
lock: 2,
|
|
17
17
|
panel: 2,
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* WHICH KINDS ARE BORN DARK (p.217-218: "some icons are deliberately
|
|
21
|
+
* hidden from view, such as locks and other security devices"; user
|
|
22
|
+
* ruling 2026-09-16 on what that covers -- "cameras, locks, sensors
|
|
23
|
+
* etc"). In this engine a 'lock' and a 'panel' ARE the security
|
|
24
|
+
* furniture: the maglock, the keypad, the safe. A 'ward' is the Art and
|
|
25
|
+
* has no wireless mode to hide in.
|
|
26
|
+
*/
|
|
27
|
+
export const SILENT_BY_DEFAULT = new Set(['lock', 'panel']);
|
|
19
28
|
/**
|
|
20
29
|
* THE BOOK'S BARRIER TIERS (p.197 Barrier Ratings, RAG-checked 2026-09-03):
|
|
21
30
|
* Structure / Armor. Fragile is standard glass; Reinforced is a security
|
|
@@ -116,6 +125,56 @@ export class Device {
|
|
|
116
125
|
*/
|
|
117
126
|
rating;
|
|
118
127
|
wireless;
|
|
128
|
+
/**
|
|
129
|
+
* RUNNING SILENT (p.235-236), and why a maglock is born that way
|
|
130
|
+
* (eJANPpm7qCaCAJBJw; user ruling 2026-09-16: security devices means
|
|
131
|
+
* "cameras, locks, sensors etc").
|
|
132
|
+
*
|
|
133
|
+
* p.217-218: "some icons are deliberately hidden from view, such as
|
|
134
|
+
* locks and other security devices." The books do not give that
|
|
135
|
+
* sentence a mechanic of its own, and RUNNING SILENT is the only
|
|
136
|
+
* hiding the rules define -- a state an owner CHOOSES, which is
|
|
137
|
+
* exactly what a site does with its own security. Splintered State
|
|
138
|
+
* p.47-50 runs the same play with a whole host ("the entire Security
|
|
139
|
+
* Host is running silent... a character must succeed a Matrix
|
|
140
|
+
* Perception test... the Security Host receives a -2 dice pool penalty
|
|
141
|
+
* to all actions due to running silent"), so this is published
|
|
142
|
+
* practice rather than an invention.
|
|
143
|
+
*
|
|
144
|
+
* IT IS A TRADE, NOT A WALL. Silent costs the device -2 on everything
|
|
145
|
+
* it does (see deviceDefensePool), so a lock that is hard to FIND is
|
|
146
|
+
* easier to crack once found. That is the whole bargain, and it is why
|
|
147
|
+
* this is not simply "security devices are invisible".
|
|
148
|
+
*
|
|
149
|
+
* THE ICON HIDES; THE OBJECT DOES NOT. This is a Matrix fact and
|
|
150
|
+
* nothing else. The maglock is still screwed to the door, still
|
|
151
|
+
* described by a meat-world `look`, still picked, breached, shot and
|
|
152
|
+
* opened with a key exactly as before -- bypass-command.ts and the
|
|
153
|
+
* spot system never ask this question. Only the grid view, the AR
|
|
154
|
+
* overlay and Matrix-side targeting do.
|
|
155
|
+
*/
|
|
156
|
+
silent;
|
|
157
|
+
/**
|
|
158
|
+
* WHO HAS SPOTTED IT, by name -- the other half of running silent.
|
|
159
|
+
*
|
|
160
|
+
* Session-scoped and per VIEWER, the same shape as Room.revealedBy and
|
|
161
|
+
* Host.spotted, and for the same reason: one decker beating the
|
|
162
|
+
* opposed test does not hand the icon to everybody on the team. Once
|
|
163
|
+
* found, it stays found -- a silent icon you have already pinned does
|
|
164
|
+
* not go missing again between looks.
|
|
165
|
+
*/
|
|
166
|
+
spottedBy = new Set();
|
|
167
|
+
/** Is this icon on the grid for this viewer? Wireless and rated (the
|
|
168
|
+
* old broadcastsAro question), and either loud or already spotted. */
|
|
169
|
+
hasIconFor(viewerName) {
|
|
170
|
+
if (!this.broadcastsAro())
|
|
171
|
+
return false;
|
|
172
|
+
return !this.silent || this.spottedBy.has(viewerName);
|
|
173
|
+
}
|
|
174
|
+
/** Hidden from this viewer, and therefore worth rolling against. */
|
|
175
|
+
isHiddenFrom(viewerName) {
|
|
176
|
+
return this.broadcastsAro() && this.silent && !this.spottedBy.has(viewerName);
|
|
177
|
+
}
|
|
119
178
|
/**
|
|
120
179
|
* WHAT IT IS MADE OF (p.197-198): Structure is the condition monitor
|
|
121
180
|
* shooting it fills, Armor is what it soaks with. Reaching Structure
|
|
@@ -166,6 +225,12 @@ export class Device {
|
|
|
166
225
|
// because a seeded keypad nobody thought about is a keypad on the
|
|
167
226
|
// grid -- which is the state the report says the game should model.
|
|
168
227
|
this.wireless = config.kind === 'ward' ? false : (config.wireless ?? true);
|
|
228
|
+
// SECURITY IS SILENT BY DEFAULT (p.217-218: "locks and other
|
|
229
|
+
// security devices"). In this engine a 'lock' and a 'panel' ARE the
|
|
230
|
+
// security furniture -- the maglock, the keypad, the safe -- so both
|
|
231
|
+
// are born dark unless a seed says otherwise. A 'ward' has no
|
|
232
|
+
// wireless mode to hide in the first place.
|
|
233
|
+
this.silent = config.kind === 'ward' ? false : (config.silent ?? SILENT_BY_DEFAULT.has(config.kind));
|
|
169
234
|
}
|
|
170
235
|
isOpen() {
|
|
171
236
|
return this.open;
|
|
@@ -150,6 +150,22 @@ export class Player extends AbstractPlayer {
|
|
|
150
150
|
// but stops broadcasting -- no calls out, and nobody can raise this
|
|
151
151
|
// actor (call.ts checks both ends). Toggling it drops any active call.
|
|
152
152
|
runningSilent = false;
|
|
153
|
+
/**
|
|
154
|
+
* WHO HAS PICKED THIS ICON OUT OF THE NOISE, by name.
|
|
155
|
+
*
|
|
156
|
+
* The other half of running silent, and the half that was missing:
|
|
157
|
+
* going dark used to make an icon simply unfindable, because isLinked
|
|
158
|
+
* answers false and nothing anywhere rolled to beat it. p.217 lists
|
|
159
|
+
* "people who prefer not to be seen" beside the locks and the
|
|
160
|
+
* maintenance monitors, and p.236 gives the way through all of them --
|
|
161
|
+
* an Opposed Computer + Intuition [Data Processing] v. Logic + Sleaze.
|
|
162
|
+
* utilities/silent-icons.ts rolls it; this remembers who won.
|
|
163
|
+
*
|
|
164
|
+
* Session-scoped and per VIEWER, the same shape as Device.spottedBy,
|
|
165
|
+
* Room.revealedBy and Host.spotted: one decker beating the test does
|
|
166
|
+
* not hand the icon to the whole team.
|
|
167
|
+
*/
|
|
168
|
+
matrixSpottedBy = new Set();
|
|
153
169
|
// Whoever this actor most recently traded combat exchanges with, and
|
|
154
170
|
// when -- set for BOTH sides by CombatExchange.beginExchange(). Walking
|
|
155
171
|
// out on a live, recent opponent invites a parting shot (see go.ts);
|
|
@@ -59,6 +59,21 @@ export class Room extends AbstractRoom {
|
|
|
59
59
|
// searchedBy/visitedBy (fresh Rooms per scene), and per-VIEWER: one
|
|
60
60
|
// runner spotting the sniper doesn't hand them to everyone.
|
|
61
61
|
revealedBy = new Map();
|
|
62
|
+
/**
|
|
63
|
+
* WHO HAS SPOTTED THE CAMERAS, by name (eJANPpm7qCaCAJBJw; p.217 names
|
|
64
|
+
* cameras first in its list of icons "deliberately hidden from view").
|
|
65
|
+
*
|
|
66
|
+
* The camera cluster is not a Device -- it is derived from the room
|
|
67
|
+
* itself (utilities/surveillance.ts isWatched), so it cannot carry
|
|
68
|
+
* Device.spottedBy and needs its own set. Same contract as that one:
|
|
69
|
+
* session-scoped, per VIEWER, and once found it stays found.
|
|
70
|
+
*
|
|
71
|
+
* The LENSES are not hidden, only their icon. A body in the room still
|
|
72
|
+
* sees cameras on the wall with its eyes and can still "tap" one in
|
|
73
|
+
* the flesh; what takes a Matrix Perception is picking their icon out
|
|
74
|
+
* of the grid.
|
|
75
|
+
*/
|
|
76
|
+
camerasSpottedBy = new Set();
|
|
62
77
|
/**
|
|
63
78
|
* THE FALLEN (player ruling 2026-08-25). A killed actor leaves a BODY
|
|
64
79
|
* where they dropped, holding everything they carried, instead of
|
|
@@ -140,7 +140,9 @@ export function aroTags(scene, actor, room) {
|
|
|
140
140
|
}
|
|
141
141
|
// THE EYES. Whether they are looped is knowledge you had to take from
|
|
142
142
|
// the host, so it shows only to whoever owns the feeds.
|
|
143
|
-
|
|
143
|
+
// Same gate as the grid: this overlay draws ICONS, and a camera icon
|
|
144
|
+
// is the first thing p.217 lists as deliberately hidden until spotted.
|
|
145
|
+
if (isWatched(room) && room.camerasSpottedBy.has(actor.name)) {
|
|
144
146
|
const rooms = scene.getRooms();
|
|
145
147
|
// The verb rides the tag: standing in the room IS the requirement
|
|
146
148
|
// for a direct connection (commands/tap.ts), so this is the one
|
|
@@ -165,7 +167,12 @@ export function aroTags(scene, actor, room) {
|
|
|
165
167
|
// The RATING RIDES THE TAG for the same reason the host's [HR] does --
|
|
166
168
|
// it is the number you decide against before touching anything.
|
|
167
169
|
for (const device of room.devices) {
|
|
168
|
-
|
|
170
|
+
// The same gate as the grid view. This overlay draws ICONS, and an
|
|
171
|
+
// icon running silent is not one of them until it has been spotted
|
|
172
|
+
// (p.217, p.235-236). The OBJECT is untouched: the maglock is still
|
|
173
|
+
// screwed to the door, still described by a meat-world `look`, and
|
|
174
|
+
// still picked, breached, shot and keyed exactly as before.
|
|
175
|
+
if (!device.hasIconFor(actor.name))
|
|
169
176
|
continue;
|
|
170
177
|
tags.push(`▤ ${device.name} -- wireless, {bold}[DR ${device.rating}]{/bold}${device.isOpen() ? ', standing open' : `.${hint(` ("${device.verbs()[0]} ${device.name}")`)}`}`);
|
|
171
178
|
}
|
|
@@ -256,13 +256,23 @@ export function gridIcons(scene, actor, room) {
|
|
|
256
256
|
// device has an icon, it looks like the object, and it carries a
|
|
257
257
|
// Device Rating (p.219, p.234).
|
|
258
258
|
for (const device of room.devices) {
|
|
259
|
-
|
|
259
|
+
// HIDDEN ICONS ARE HIDDEN (p.217, eJANPpm7qCaCAJBJw). A silent lock
|
|
260
|
+
// is not on the grid until THIS viewer has beaten it in an opposed
|
|
261
|
+
// Matrix Perception (utilities/silent-icons.ts) -- per viewer,
|
|
262
|
+
// because one decker spotting it does not hand it to the team.
|
|
263
|
+
if (!device.hasIconFor(actor.name))
|
|
260
264
|
continue;
|
|
261
265
|
icons.push(deviceIconLine(device, actor));
|
|
262
266
|
}
|
|
263
267
|
// The cameras, as a THING (surveillance.ts): live for security, looped,
|
|
264
268
|
// or yours to ride.
|
|
265
|
-
|
|
269
|
+
//
|
|
270
|
+
// p.217 NAMES CAMERAS FIRST among the icons "deliberately hidden from
|
|
271
|
+
// view" (eJANPpm7qCaCAJBJw). The lenses are still bolted to the wall,
|
|
272
|
+
// still described in the meat, and a body still "tap"s one in the
|
|
273
|
+
// flesh -- it is the ICON that takes a Matrix Perception to pick out
|
|
274
|
+
// of the noise (utilities/silent-icons.ts).
|
|
275
|
+
if (isWatched(room) && room.camerasSpottedBy.has(actor.name)) {
|
|
266
276
|
const rooms = scene.getRooms();
|
|
267
277
|
if (!camerasLive(rooms, room)) {
|
|
268
278
|
icons.push(`◎ Camera cluster -- looped on empty hallways. Security sees NOTHING here.${canSnoopFeeds(rooms, room, actor) ? ` ("snoop" rides the feeds you own)` : ''}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { roomsInReach } from './grid-reach.js';
|
|
2
2
|
import { NPC } from '../models/npc.js';
|
|
3
|
+
import { isLinked } from './comm-style.js';
|
|
3
4
|
/**
|
|
4
5
|
* WHO A PERSONA CAN REACH (increment 2 of the Matrix position model,
|
|
5
6
|
* 2026-09-02). The persona's position is `Player.matrixPosition` -- on a
|
|
@@ -35,13 +36,62 @@ export function cybercombatCandidates(scene, actor) {
|
|
|
35
36
|
export function speechAudience(scene, actor) {
|
|
36
37
|
return personasOnMySide(scene, actor);
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* THE COMMLINKS IN EARSHOT OF YOUR BODY (FhXadizkMCxAdMaJx, Maka: "this
|
|
41
|
+
* looks like it's backwards. If I jack in, and Grunt's icon is in reach,
|
|
42
|
+
* he SHOULD hear me through his icon when I do a 'say'").
|
|
43
|
+
*
|
|
44
|
+
* He is right, and the books agree. A commlink is a COMMUNICATION
|
|
45
|
+
* device: p.246 says that even across a host wall "you can still send
|
|
46
|
+
* messages, make commcalls, and that sort of thing", and nothing
|
|
47
|
+
* anywhere requires the person on the other end to be jacked in -- a
|
|
48
|
+
* commcall rings a commlink in somebody's pocket, which is what a
|
|
49
|
+
* commlink is for. The engine's old answer, "a PAN icon is a commlink in
|
|
50
|
+
* somebody's pocket, not a person you can talk to", had the fact right
|
|
51
|
+
* and drew the wrong conclusion from it.
|
|
52
|
+
*
|
|
53
|
+
* SCOPED TO THE VICINITY, not the district. Canon would let a persona
|
|
54
|
+
* call a commlink anywhere on the grid -- distance is noise, not range
|
|
55
|
+
* -- but `say` is the verb for speaking to the people around you, and a
|
|
56
|
+
* `say` that rang every commlink in the barrens would be a broadcast
|
|
57
|
+
* nobody asked for. The room your body is slumped in is what "in reach"
|
|
58
|
+
* means in the icon list the reporter was reading, so it is what it
|
|
59
|
+
* means here. `call <name>` is still the verb for reaching somebody
|
|
60
|
+
* further off, and it still works.
|
|
61
|
+
*
|
|
62
|
+
* A PAN IS THE TEST, not a pulse: isLinked is the same predicate that
|
|
63
|
+
* decides whether a body shows up as a ▣ icon at all. Somebody with no
|
|
64
|
+
* commlink, or running silent, has nothing for your voice to come out
|
|
65
|
+
* of -- and does not appear in the icon list either, so the two answers
|
|
66
|
+
* agree.
|
|
67
|
+
*/
|
|
68
|
+
export function panAudience(scene, actor) {
|
|
69
|
+
const here = actor.bodyRoom ?? actor.currentLocation;
|
|
70
|
+
if (!here)
|
|
71
|
+
return [];
|
|
72
|
+
return scene.allActors.filter(a => a !== actor
|
|
73
|
+
&& a.plane === 'meat'
|
|
74
|
+
&& (a.bodyRoom ?? a.currentLocation) === here
|
|
75
|
+
&& isLinked(a));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Delivers a persona's words to its audience, the way Room.say delivers
|
|
79
|
+
* a body's -- to the personas beside it, and out of the commlinks of the
|
|
80
|
+
* people standing around its body.
|
|
81
|
+
*/
|
|
39
82
|
export function speakOnTheGrid(scene, actor, message) {
|
|
40
|
-
const
|
|
41
|
-
for (const recipient of
|
|
83
|
+
const personas = speechAudience(scene, actor);
|
|
84
|
+
for (const recipient of personas) {
|
|
42
85
|
void Promise.resolve(recipient.hear(actor, message, { broadcast: true })).catch(() => undefined);
|
|
43
86
|
}
|
|
44
|
-
|
|
87
|
+
// viaLink, so the NPC knows this arrived out of their commlink rather
|
|
88
|
+
// than from a mouth in the room -- which is the whole difference, and
|
|
89
|
+
// the thing their reply has to be able to react to.
|
|
90
|
+
const links = panAudience(scene, actor);
|
|
91
|
+
for (const recipient of links) {
|
|
92
|
+
void Promise.resolve(recipient.hear(actor, message, { broadcast: true, viaLink: true })).catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
return { personas, links };
|
|
45
95
|
}
|
|
46
96
|
/**
|
|
47
97
|
* The combat exchange's "same place": on the matrix plane it is the
|
|
@@ -36,6 +36,11 @@ function matrixNameables(scene, actor, rooms) {
|
|
|
36
36
|
push(out, seen, { glyph: '▣', name: body.name, what: 'a body in reach -- its PAN' });
|
|
37
37
|
}
|
|
38
38
|
for (const device of room.openableDevices()) {
|
|
39
|
+
// Tab must not hand you the name of an icon you have not spotted
|
|
40
|
+
// (p.217) -- that is the same "Tab is a free search" defect
|
|
41
|
+
// yaw2btNX8aNEEF9D3 fixed for meat-world loot, one plane over.
|
|
42
|
+
if (!device.hasIconFor(actor.name))
|
|
43
|
+
continue;
|
|
39
44
|
push(out, seen, { glyph: '▤', name: device.name, what: `device, rating ${device.rating}` });
|
|
40
45
|
}
|
|
41
46
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { rollPool, formatRoll } from './dice.js';
|
|
2
|
+
import { deviceDefensePool } from './matrix-intrusion.js';
|
|
3
|
+
import { isWatched, cameraMasterHost } from './surveillance.js';
|
|
4
|
+
import { DEFAULT_DEVICE_RATING } from '../models/device.js';
|
|
5
|
+
function hiddenDevice(device) {
|
|
6
|
+
return {
|
|
7
|
+
name: device.name,
|
|
8
|
+
// Its rating stands in for the Mental attribute it has not got
|
|
9
|
+
// (p.237-238) -- the same pool it defends an intrusion with.
|
|
10
|
+
hide: { pool: deviceDefensePool(device).pool, label: 'Device Rating (no Logic to hide behind)' },
|
|
11
|
+
reveal: (viewer) => { device.spottedBy.add(viewer); },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function hiddenPersona(actor) {
|
|
15
|
+
return {
|
|
16
|
+
name: actor.name,
|
|
17
|
+
// p.236/p.241, verbatim: "the target defending with Logic + Sleaze".
|
|
18
|
+
hide: { pool: Math.max(0, actor.logic + actor.matrixAttribute('sleaze')), label: 'Logic + Sleaze' },
|
|
19
|
+
reveal: (viewer) => { actor.matrixSpottedBy.add(viewer); },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Everything running silent within reach of this viewer that they have
|
|
24
|
+
* not already pinned.
|
|
25
|
+
*
|
|
26
|
+
* DEVICES come from the room the sweep is aimed at. PEOPLE come from the
|
|
27
|
+
* scene, because a runner who went dark is still standing somewhere, and
|
|
28
|
+
* the icon you are looking for is their PAN -- which sits where their
|
|
29
|
+
* body does.
|
|
30
|
+
*/
|
|
31
|
+
export function hiddenIconsIn(scene, room, viewer) {
|
|
32
|
+
const out = room.devices
|
|
33
|
+
.filter(d => d.isHiddenFrom(viewer.name))
|
|
34
|
+
.map(hiddenDevice);
|
|
35
|
+
// THE CAMERAS, which p.217 names FIRST. They are not a Device -- the
|
|
36
|
+
// cluster is derived from the room (surveillance.ts isWatched) -- so
|
|
37
|
+
// they hide on the room's own set and resist with their master host's
|
|
38
|
+
// rating where there is one: a site's lenses are as quiet as the
|
|
39
|
+
// security that runs them, and a slave borrows its master's numbers
|
|
40
|
+
// (p.233). Without a host they fall back to the door-lock rating
|
|
41
|
+
// canon prices ordinary fixtures at (p.356).
|
|
42
|
+
if (isWatched(room) && !room.camerasSpottedBy.has(viewer.name)) {
|
|
43
|
+
const master = scene ? cameraMasterHost(scene.getRooms(), room) : undefined;
|
|
44
|
+
const rating = master?.hostRating ?? DEFAULT_DEVICE_RATING.lock ?? 2;
|
|
45
|
+
out.push({
|
|
46
|
+
name: 'Camera cluster',
|
|
47
|
+
hide: { pool: Math.max(0, rating * 2), label: master ? 'its host\'s rating' : 'Device Rating' },
|
|
48
|
+
reveal: (v) => { room.camerasSpottedBy.add(v); },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
for (const a of scene?.allActors ?? []) {
|
|
52
|
+
if (a === viewer)
|
|
53
|
+
continue;
|
|
54
|
+
if (!a.runningSilent)
|
|
55
|
+
continue;
|
|
56
|
+
if (a.matrixSpottedBy.has(viewer.name))
|
|
57
|
+
continue;
|
|
58
|
+
// Where the BODY is, because that is where the commlink is.
|
|
59
|
+
if ((a.bodyRoom ?? a.currentLocation) !== room)
|
|
60
|
+
continue;
|
|
61
|
+
out.push(hiddenPersona(a));
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* ONE MATRIX PERCEPTION, AGAINST EVERYTHING HIDING IN THE ROOM.
|
|
67
|
+
*
|
|
68
|
+
* Canon resolves one icon per action. This rolls the sweeper's pool ONCE
|
|
69
|
+
* and compares it against each hider's own resistance, which is a
|
|
70
|
+
* deliberate simplification worth naming: rolling a fresh attacker pool
|
|
71
|
+
* per icon would make a room with six locks six times as easy to sweep
|
|
72
|
+
* for the same single action, and asking a player to NAME an icon they
|
|
73
|
+
* cannot see yet is not a question anybody can answer. One action, one
|
|
74
|
+
* roll; the icons differ by what they hide behind.
|
|
75
|
+
*
|
|
76
|
+
* `look` is where it fires, because `look` on the grid IS Matrix
|
|
77
|
+
* Perception (p.241) and is already billed as the Complex Action it is.
|
|
78
|
+
* A second look is a second action, which is what makes a dark room
|
|
79
|
+
* cost something.
|
|
80
|
+
*/
|
|
81
|
+
export function spotHiddenIcons(scene, actor, room) {
|
|
82
|
+
const hidden = hiddenIconsIn(scene, room, actor);
|
|
83
|
+
if (hidden.length === 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
// Computer + Intuition [Data Processing] (p.241), carrying the
|
|
86
|
+
// sweeper's own silence, wounds and sustaining the way every other
|
|
87
|
+
// Matrix action in this engine does.
|
|
88
|
+
const computer = actor.skillRating('computer');
|
|
89
|
+
const pool = Math.max(1, actor.intuition + (computer > 0 ? computer : -1)
|
|
90
|
+
+ actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
|
|
91
|
+
const roll = rollPool(pool, actor.matrixAttribute('dataProcessing'), { gremlins: actor.deckGremlins });
|
|
92
|
+
const found = [];
|
|
93
|
+
const missed = [];
|
|
94
|
+
const against = [];
|
|
95
|
+
for (const icon of hidden) {
|
|
96
|
+
const resist = rollPool(icon.hide.pool);
|
|
97
|
+
against.push({ name: icon.name, label: icon.hide.label, roll: resist });
|
|
98
|
+
// Ties go to the hider: "on a tie or more hits by the defender, it
|
|
99
|
+
// stays hidden and out of reach" (p.236).
|
|
100
|
+
if (roll.hits > resist.hits) {
|
|
101
|
+
icon.reveal(actor.name);
|
|
102
|
+
found.push(icon.name);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
missed.push(icon.name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { found, missed, roll, against };
|
|
109
|
+
}
|
|
110
|
+
/** The mechanics lines for a sweep, for a screen that shows them. */
|
|
111
|
+
export function spotMechanics(outcome) {
|
|
112
|
+
return [
|
|
113
|
+
`Matrix Perception, silent icons (Computer + Intuition [Data Processing]): ${formatRoll(outcome.roll)}`,
|
|
114
|
+
...outcome.against.map(a => ` ${a.name} hides (${a.label}): ${formatRoll(a.roll)}`),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* What the runner is TOLD, which is never nothing.
|
|
119
|
+
*
|
|
120
|
+
* A miss still reports that something is there -- that is step 1 above,
|
|
121
|
+
* the hit that buys "there are icons running silent in the vicinity",
|
|
122
|
+
* and it is the whole reason a hidden lock reads differently from a door
|
|
123
|
+
* with no lock on it.
|
|
124
|
+
*/
|
|
125
|
+
export function spotLine(outcome) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
if (outcome.found.length > 0) {
|
|
128
|
+
lines.push(`You pick ${outcome.found.length === 1 ? 'an icon' : `${outcome.found.length} icons`} out of the noise -- ${outcome.found.join(', ')}, running silent and not quiet enough.`);
|
|
129
|
+
}
|
|
130
|
+
if (outcome.missed.length > 0) {
|
|
131
|
+
lines.push(outcome.missed.length === 1
|
|
132
|
+
? `Something else here is running silent and you cannot pin it -- a shape in the static that stops when you look at it. (Look again; each sweep is its own Matrix Perception, p.241.)`
|
|
133
|
+
: `${outcome.missed.length} more icons are running silent here, and none of them will hold still. (Look again; each sweep is its own Matrix Perception, p.241.)`);
|
|
134
|
+
}
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=silent-icons.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.221.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.",
|