@maka/maka-cli 5.197.0 → 5.198.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/download.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +4 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +1 -1
- 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/say.js +19 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/shout.js +69 -0
- 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/talk.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +24 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +13 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +80 -57
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +116 -10
- 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/matrix-style.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.198.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 = `{light-blue-fg}The overlay blinks back in.{/light-blue-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, `{light-blue-fg}Nothing here is talking -- a dead patch of street.{/light-blue-fg}`, note].filter(Boolean).join('\n');
|
|
73
73
|
}
|
|
74
|
-
return [head, ...tags.map(t => ` {
|
|
74
|
+
return [head, ...tags.map(t => ` {light-blue-fg}${t}{/light-blue-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 [`{light-blue-fg}Nothing here is talking -- a dead patch of street.{/light-blue-fg}`, note].filter(Boolean).join('\n');
|
|
109
109
|
}
|
|
110
110
|
return [
|
|
111
|
-
`{
|
|
112
|
-
...tags.map(t => ` {
|
|
111
|
+
`{light-blue-fg}AR OVERLAY:{/light-blue-fg}`,
|
|
112
|
+
...tags.map(t => ` {light-blue-fg}${t}{/light-blue-fg}`),
|
|
113
113
|
note,
|
|
114
114
|
].filter(Boolean).join('\n');
|
|
115
115
|
}
|
|
@@ -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 = [`{light-blue-fg}Within reach to pull down:{/light-blue-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)
|
|
@@ -140,11 +140,11 @@ export class DownloadCommand extends Command {
|
|
|
140
140
|
this.scene.updateStatus();
|
|
141
141
|
return fromSprite
|
|
142
142
|
? [
|
|
143
|
-
`{
|
|
143
|
+
`{light-blue-fg}${fromSprite.npc.name} opens its hands and the data pours out of them.{/light-blue-fg}`,
|
|
144
144
|
`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.)`)}`,
|
|
145
145
|
].join('\n')
|
|
146
146
|
: [
|
|
147
|
-
`{
|
|
147
|
+
`{light-blue-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down off the grid -- written to a chip.{/light-blue-fg}`,
|
|
148
148
|
hint(`(Meat-world cargo now: carry it, stash it, or "give" it to somebody.)`),
|
|
149
149
|
].join('\n');
|
|
150
150
|
}
|
|
@@ -115,7 +115,7 @@ export class EditFileCommand extends Command {
|
|
|
115
115
|
const banner = this.scene.checkErased?.(file.name, actor.name);
|
|
116
116
|
this.scene.updateStatus();
|
|
117
117
|
return [
|
|
118
|
-
`{
|
|
118
|
+
`{light-blue-fg}${file.name} comes apart under your hand -- sectors zeroed, index entry gone, the host's own checksum rewritten to agree. ${hostLabel(room, { capital: true })} no longer has it.{/light-blue-fg}`,
|
|
119
119
|
...(banner ? [banner] : []),
|
|
120
120
|
].join('\n');
|
|
121
121
|
}
|
|
@@ -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
|
+
`{light-blue-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/light-blue-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
|
}
|
|
@@ -85,7 +85,7 @@ export class EnterHostCommand extends Command {
|
|
|
85
85
|
// ONE LINE (2026-09-13, "too much text"): the crossing, and the
|
|
86
86
|
// coaching behind the hints toggle.
|
|
87
87
|
return [
|
|
88
|
-
`{
|
|
88
|
+
`{light-blue-fg}You cross into ${hostLabel(target, { capital: false })} -- the outside grid folds shut behind you.{/light-blue-fg}`,
|
|
89
89
|
hint(`(In here there is only what the host keeps. "look" reads it; "exit" puts you back on the grid.)`),
|
|
90
90
|
].filter(Boolean).join('\n');
|
|
91
91
|
}
|
|
@@ -110,14 +110,14 @@ export class EnterHostCommand extends Command {
|
|
|
110
110
|
const over = hostOver(gridVicinity(this.actor)) ?? hostOver(here);
|
|
111
111
|
if (over)
|
|
112
112
|
return over;
|
|
113
|
-
return `{
|
|
113
|
+
return `{light-blue-fg}Thin grid here -- no host stands over ${gridVicinity(this.actor).name}.{/light-blue-fg} ${hint(`Overhead: ${roster} -- "enter <name>" crosses into any of them from here.`)}`;
|
|
114
114
|
}
|
|
115
115
|
const host = resolveHostInReach(rooms, this.actor, named);
|
|
116
116
|
if (host)
|
|
117
117
|
return host;
|
|
118
118
|
const street = Object.values(rooms).find(r => r.name.toLowerCase() === named.toLowerCase());
|
|
119
119
|
if (street)
|
|
120
|
-
return `{
|
|
120
|
+
return `{light-blue-fg}${street.name} has no host over it -- nothing to enter. Overhead: ${roster}.{/light-blue-fg}`;
|
|
121
121
|
return `No host called "${named}" overhead. ${hint(`Overhead: ${roster}.`)}`;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -152,7 +152,7 @@ export class ExitHostCommand extends Command {
|
|
|
152
152
|
actor.performAction('exits a host', `icon surfacing out of ${host.name}'s architecture`, { quiet: this.scene.isHumanControlled(actor) });
|
|
153
153
|
this.logger.write(`${actor.name} exited the host at ${host.name}.`);
|
|
154
154
|
this.scene.updateStatus();
|
|
155
|
-
return `{
|
|
155
|
+
return `{light-blue-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}.{/light-blue-fg}`;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
//# sourceMappingURL=enter-host.js.map
|
|
@@ -317,7 +317,7 @@ export class HackCommand extends BypassCommand {
|
|
|
317
317
|
if (marksNow >= 1)
|
|
318
318
|
lines.push(` One mark is the door: "enter" walks your persona inside (p.239).`);
|
|
319
319
|
if (perception > 0)
|
|
320
|
-
lines.push(` {
|
|
320
|
+
lines.push(` {light-blue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/light-blue-fg}`);
|
|
321
321
|
if (forcedDv > 0)
|
|
322
322
|
lines.push(` The forcing burns ${forcedDv} DV into the architecture -- a host has no boards to brick, so it only announces you louder.`);
|
|
323
323
|
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.)`)}`);
|
|
@@ -388,13 +388,13 @@ export class HackCommand extends BypassCommand {
|
|
|
388
388
|
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.)`)}`);
|
|
389
389
|
}
|
|
390
390
|
if (vaultedFiles.length > 0) {
|
|
391
|
-
lines.push(` {
|
|
391
|
+
lines.push(` {light-blue-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/light-blue-fg}`);
|
|
392
392
|
}
|
|
393
393
|
// AND WHAT THE CRACK DID NOT REACH: a wired site's data sits on an
|
|
394
394
|
// air-gapped box the host never held (p.233) -- say so, or the next
|
|
395
395
|
// "take" reads as a contradiction.
|
|
396
396
|
if (room.offlineServer && room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
|
|
397
|
-
lines.push(` {
|
|
397
|
+
lines.push(` {light-blue-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).{/light-blue-fg}`);
|
|
398
398
|
}
|
|
399
399
|
// THE FEEDS ARE A REAL GAIN AND WERE NEVER MENTIONED
|
|
400
400
|
// (BpR5cjhYuvgCyDSZc, 7aNWnDSHeZMYmS2De: "what have I gained?").
|
|
@@ -683,7 +683,7 @@ export class HackCommand extends BypassCommand {
|
|
|
683
683
|
else {
|
|
684
684
|
const perception = freeMatrixPerceptionHits(netHits);
|
|
685
685
|
if (perception > 0)
|
|
686
|
-
lines.push(` {
|
|
686
|
+
lines.push(` {light-blue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what it guards (p.240).{/light-blue-fg}`);
|
|
687
687
|
}
|
|
688
688
|
// TWO MARKS COMMAND IT (p.238 Control Device: 1 mark for a Free
|
|
689
689
|
// action, 2 for a Simple, 3 for a Complex -- and unlocking a maglock
|
|
@@ -101,7 +101,7 @@ export class HideCommand extends Command {
|
|
|
101
101
|
this.scene.addWorldEvent(`${host.name}'s ice lost ${actor.name} in the traffic.`);
|
|
102
102
|
this.scene.updateStatus();
|
|
103
103
|
return [
|
|
104
|
-
`{
|
|
104
|
+
`{light-blue-fg}You drop into the host's own traffic and become one more packet in a million. ${hostLabel(room, { capital: true })} loses you.{/light-blue-fg}`,
|
|
105
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
106
|
...god.lines,
|
|
107
107
|
].join('\n');
|
|
@@ -71,7 +71,7 @@ export class HopCommand extends Command {
|
|
|
71
71
|
actor.performAction('hops grids', `persona blinking across to ${target.name}`);
|
|
72
72
|
this.logger.write(`${actor.name} hopped to ${target.key} (legal, ${tier}).`);
|
|
73
73
|
this.scene.updateStatus();
|
|
74
|
-
return `{
|
|
74
|
+
return `{light-blue-fg}The sky changes texture and you're on ${target.name}.{/light-blue-fg}${target.userPenalty ? ` ${hint(`Public grid: -2 on everything you do out here.`)}` : ''}${await land()}`;
|
|
75
75
|
}
|
|
76
76
|
// ILLEGAL HOP (p.240): Hack on the Fly against the grid itself.
|
|
77
77
|
const burned = actor.burnedAttributeRefusal('sleaze');
|
|
@@ -105,7 +105,7 @@ export class HopCommand extends Command {
|
|
|
105
105
|
this.logger.write(`${actor.name} hopped to ${target.key} (illegal, ${attempt.hits} v ${defence.hits}).`);
|
|
106
106
|
this.scene.updateStatus();
|
|
107
107
|
return [
|
|
108
|
-
`{
|
|
108
|
+
`{light-blue-fg}You slip through a seam nobody sold you and come up on ${target.name}.{/light-blue-fg}`,
|
|
109
109
|
...bill.lines,
|
|
110
110
|
...(isLooseEnd ? [await land()] : []),
|
|
111
111
|
].join('\n');
|
|
@@ -387,7 +387,7 @@ export class LookCommand extends Command {
|
|
|
387
387
|
const dim = this.scene.getActorsInRoom(far)
|
|
388
388
|
.find(a => a !== this.actor && a.name.toLowerCase() === target.toLowerCase() && this.actor.canPerceive(a));
|
|
389
389
|
if (dim)
|
|
390
|
-
return `${this.inspectPersona(dim)}\n{
|
|
390
|
+
return `${this.inspectPersona(dim)}\n{light-blue-fg} -- a far icon, ${far.name}${noiseNote(noise)}{/light-blue-fg}`;
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
// 3. THE HOST, as a thing you can name. gridIcons already renders it
|
|
@@ -404,8 +404,8 @@ export class LookCommand extends Command {
|
|
|
404
404
|
// `deck` reads out your own array.
|
|
405
405
|
const a = namedHost.hostAttributes();
|
|
406
406
|
return [
|
|
407
|
-
`{
|
|
408
|
-
`{
|
|
407
|
+
`{light-blue-fg}${hostIcon}{/light-blue-fg}`,
|
|
408
|
+
`{light-blue-fg} Attack ${a.attack} Sleaze ${a.sleaze} Data Processing ${a.dataProcessing} Firewall ${a.firewall}{/light-blue-fg}`,
|
|
409
409
|
].join('\n');
|
|
410
410
|
}
|
|
411
411
|
}
|
|
@@ -417,7 +417,7 @@ export class LookCommand extends Command {
|
|
|
417
417
|
if (matchesCameraName(target)) {
|
|
418
418
|
const eye = gridIcons(this.scene, this.actor, room).find(i => i.startsWith('◎'));
|
|
419
419
|
if (eye)
|
|
420
|
-
return `{
|
|
420
|
+
return `{light-blue-fg}${eye}{/light-blue-fg}`;
|
|
421
421
|
// Named correctly, and there is simply nothing here to name.
|
|
422
422
|
// Worth its own line: the old answer said the WORDS were wrong,
|
|
423
423
|
// which sent the player hunting for a synonym that never
|
|
@@ -533,7 +533,7 @@ export class LookCommand extends Command {
|
|
|
533
533
|
* there's a deck anywhere in the shape of it.
|
|
534
534
|
*/
|
|
535
535
|
inspectPersona(target) {
|
|
536
|
-
const lines = [`{
|
|
536
|
+
const lines = [`{light-blue-fg}◆ ${target.name} -- a persona on this grid.{/light-blue-fg}`];
|
|
537
537
|
const companion = this.scene.ownerGame?.companions.find(c => c.npc === target);
|
|
538
538
|
if (companion) {
|
|
539
539
|
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
|
+
`{light-blue-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'}.{/light-blue-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 `{light-blue-fg}Placing a mark IS ${action} -- running it (p.238/p.240).{/light-blue-fg}\n${result}`;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
//# sourceMappingURL=mark.js.map
|
|
@@ -87,6 +87,7 @@ export class SayCommand extends Command {
|
|
|
87
87
|
}
|
|
88
88
|
const message = args?.join(' ');
|
|
89
89
|
const currentPlayer = this.actor;
|
|
90
|
+
let farOff = "";
|
|
90
91
|
const scene = this.context.scene;
|
|
91
92
|
// Speaking out loud ends a sneak (see sneak.ts).
|
|
92
93
|
currentPlayer.sneaking = false;
|
|
@@ -245,8 +246,23 @@ export class SayCommand extends Command {
|
|
|
245
246
|
// (matrix-roster.ts); a body's reach the room, as always.
|
|
246
247
|
if (currentPlayer.plane === 'matrix')
|
|
247
248
|
speakOnTheGrid(this.scene, currentPlayer, message);
|
|
248
|
-
else
|
|
249
|
-
currentPlayer.currentLocation.say(currentPlayer, message);
|
|
249
|
+
else {
|
|
250
|
+
const heard = currentPlayer.currentLocation.say(currentPlayer, message);
|
|
251
|
+
// NOBODY CLOSE ENOUGH is now a thing that can happen, and has to
|
|
252
|
+
// be said (en3p6eCpiRuqsTSKm). Speech used to escalate to a
|
|
253
|
+
// room-wide shout the moment nobody shared your spot, so this
|
|
254
|
+
// case did not exist; with `say` bounded by each listener's
|
|
255
|
+
// hearing range it does, and a player whose words reached nobody
|
|
256
|
+
// would otherwise see their own echo and assume they landed.
|
|
257
|
+
//
|
|
258
|
+
// Only when there was somebody TO miss: an empty room is not a
|
|
259
|
+
// failure to be heard, it is an empty room, and nagging about it
|
|
260
|
+
// every time a runner mutters to themselves would be noise.
|
|
261
|
+
const others = currentPlayer.currentLocation.getActors().filter(a => a !== currentPlayer);
|
|
262
|
+
if ((heard ?? []).length === 0 && others.length > 0 && !(this.actor instanceof NPC)) {
|
|
263
|
+
farOff = `\n Nobody's close enough to catch that.${hint(` ("shout" carries the whole room; "move to <place>" gets you near someone.)`)}`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
250
266
|
// Talking in the CLIENT's presence is the meet (a real session
|
|
251
267
|
// walked up to Ms. Veil, spoke with plain "say", and the advance
|
|
252
268
|
// never triggered -- only talk-to/call/give were hooked). The
|
|
@@ -294,7 +310,7 @@ export class SayCommand extends Command {
|
|
|
294
310
|
? `Your persona speaks and the grid carries it: ${heard.join(', ')} ${heard.length === 1 ? 'is' : 'are'} on this side of the architecture and hears you. Bodies in the room hear nothing -- you have no voice out there.`
|
|
295
311
|
: `Your words go out over the grid and find no persona to land on. Anyone here in the FLESH hears nothing -- a PAN icon is a commlink in somebody's pocket, not a person you can talk to. ("call <name>" reaches a body.)`;
|
|
296
312
|
}
|
|
297
|
-
return
|
|
313
|
+
return farOff;
|
|
298
314
|
}
|
|
299
315
|
/**
|
|
300
316
|
* "say private" -- the answer to "can anyone hear me right now",
|
|
@@ -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{light-blue-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.`)}{/light-blue-fg}`
|
|
317
|
+
: `\n{light-blue-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.`)}{/light-blue-fg}`;
|
|
318
318
|
}
|
|
319
319
|
description += describeContainers(room, this.actor);
|
|
320
320
|
description += describeBarriers(room, this.actor);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { NPC } from '../models/npc.js';
|
|
3
|
+
import { hint } from '../utilities/hints.js';
|
|
4
|
+
import { SHOUT_METERS } from '../utilities/earshot.js';
|
|
5
|
+
import { speakOnTheGrid } from '../utilities/matrix-roster.js';
|
|
6
|
+
/**
|
|
7
|
+
* SHOUTING IS A VERB NOW (en3p6eCpiRuqsTSKm: "add 'shout' can be heard
|
|
8
|
+
* by anyone within a reasonable distance, 50m?").
|
|
9
|
+
*
|
|
10
|
+
* It used to be something that HAPPENED TO YOU. Room.say escalated to a
|
|
11
|
+
* room-wide shout whenever nobody shared your spot, so a runner standing
|
|
12
|
+
* on their own could not say anything quietly, and a runner at a crowded
|
|
13
|
+
* table could not deliberately call across the room. Both halves of the
|
|
14
|
+
* volume decision belonged to the furniture rather than to the player.
|
|
15
|
+
*
|
|
16
|
+
* Now `say` carries to whoever is close enough to hear it (earshot.ts)
|
|
17
|
+
* and this is how you reach everyone else. The range is this engine's
|
|
18
|
+
* number, not the book's -- see SHOUT_METERS -- and in practice it is
|
|
19
|
+
* room-wide, because the largest room the generator builds is tens of
|
|
20
|
+
* metres across.
|
|
21
|
+
*
|
|
22
|
+
* WHAT IT COSTS, and why that is not nothing: every hostile in the room
|
|
23
|
+
* hears you, and shouting is not sneaking. `sneaking` is dropped here
|
|
24
|
+
* the same way `say` drops it -- talking on comms is not a quiet
|
|
25
|
+
* activity, and bellowing across a warehouse is less so.
|
|
26
|
+
*/
|
|
27
|
+
export class ShoutCommand extends Command {
|
|
28
|
+
static verb = 'shout';
|
|
29
|
+
static description = `Raise your voice so the whole place hears it (~${SHOUT_METERS}m), instead of just whoever is standing close. "say" is ordinary speech and only carries as far as a listener's ears reach -- roughly 5m, further for someone with the chrome for it. Shouting reaches everyone in the room, hostiles included, and drops any sneaking.`;
|
|
30
|
+
context;
|
|
31
|
+
constructor(context) {
|
|
32
|
+
super(context);
|
|
33
|
+
this.context = context;
|
|
34
|
+
}
|
|
35
|
+
async execute(args) {
|
|
36
|
+
if (!args || args.length === 0) {
|
|
37
|
+
return `What do you want to shout?${hint(` ("shout <message>" -- "say" keeps it between you and whoever is close.)`)}`;
|
|
38
|
+
}
|
|
39
|
+
const message = args.join(' ');
|
|
40
|
+
const actor = this.actor;
|
|
41
|
+
// THE PLANES, worded as say.ts words them -- one mechanic, one set
|
|
42
|
+
// of sentences. An astral voice never reaches meat ears and a
|
|
43
|
+
// persona's never leaves the grid, and volume changes neither: a
|
|
44
|
+
// louder voice on the wrong plane is still no voice at all.
|
|
45
|
+
if (actor.plane === 'astral') {
|
|
46
|
+
return `You can shout all you like -- the meat world hears nothing. Spirits and projections do.`;
|
|
47
|
+
}
|
|
48
|
+
// Talking is not sneaking (sneak.ts); shouting least of all.
|
|
49
|
+
actor.sneaking = false;
|
|
50
|
+
if (actor.plane === 'matrix') {
|
|
51
|
+
// ON THE GRID THERE IS NO DISTANCE (grid-reach.ts): personas hear
|
|
52
|
+
// each other or they do not, and no amount of volume crosses a
|
|
53
|
+
// host wall. So this routes to the same fan-out `say` uses rather
|
|
54
|
+
// than inventing a louder one.
|
|
55
|
+
const heard = speakOnTheGrid(this.scene, actor, message).map(a => a.name);
|
|
56
|
+
return heard.length > 0
|
|
57
|
+
? `You push it out across the grid: "${message}"\n Heard by ${heard.join(', ')}.`
|
|
58
|
+
: `You push it out across the grid -- nothing out here is listening.`;
|
|
59
|
+
}
|
|
60
|
+
const heard = actor.currentLocation.shout(actor, message);
|
|
61
|
+
if (!(this.actor instanceof NPC)) {
|
|
62
|
+
this.actor.performAction('shouts', actor.currentLocation.name, { quiet: true });
|
|
63
|
+
}
|
|
64
|
+
return heard.length > 0
|
|
65
|
+
? `You raise your voice: "${message}"`
|
|
66
|
+
: `You raise your voice: "${message}"\n Nothing answers -- there's nobody here to hear it.`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=shout.js.map
|
|
@@ -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(`{light-blue-fg}(Security sees a loop of empty hallways -- these eyes answer only to you now.){/light-blue-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 ? ' {light-blue-fg}(yours){/light-blue-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.
|
|
@@ -79,8 +79,8 @@ export function resolveSpokenTarget(parsedArgs, names) {
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
export class TalkCommand extends Command {
|
|
82
|
-
static verb = "
|
|
83
|
-
static description = "Speak privately to
|
|
82
|
+
static verb = "tell";
|
|
83
|
+
static description = "Speak privately to ONE person standing with you -- only they hear it, however loud the room is. \"tell <name> <message>\". (\"talk-to\" is the old spelling and still works.) Compare \"say\", which carries to whoever is close enough to hear, and \"shout\", which reaches the whole place.";
|
|
84
84
|
context;
|
|
85
85
|
constructor(context) {
|
|
86
86
|
super(context);
|
|
@@ -164,9 +164,9 @@ export class TapCommand extends Command {
|
|
|
164
164
|
}
|
|
165
165
|
this.logger.write(`${actor.name} now holds ${now} mark(s) on ${host.name} via the ${room.name} cameras.`);
|
|
166
166
|
const lines = [
|
|
167
|
-
`{
|
|
168
|
-
` {
|
|
169
|
-
` {
|
|
167
|
+
`{light-blue-fg}The filament finds the connector and the camera stops being a camera -- it's a device, and it's talking to you.{/light-blue-fg}`,
|
|
168
|
+
` {light-blue-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.{/light-blue-fg}`,
|
|
169
|
+
` {light-blue-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}]{/light-blue-fg}`,
|
|
170
170
|
];
|
|
171
171
|
if (mode === 'attack') {
|
|
172
172
|
lines.push(` The housing is visibly forced -- the host FELT that one, even if nothing saw your face.`);
|
|
@@ -175,15 +175,15 @@ export class TapCommand extends Command {
|
|
|
175
175
|
lines.push(` Nothing logged, nothing flagged. You were never here.`);
|
|
176
176
|
}
|
|
177
177
|
if (host === room) {
|
|
178
|
-
lines.push(` {
|
|
178
|
+
lines.push(` {light-blue-fg}${now >= MAX_MARKS
|
|
179
179
|
? `The host thinks you belong now -- "hack" walks straight in.`
|
|
180
|
-
: `${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`)}.`}{/light-blue-fg}`);
|
|
181
181
|
}
|
|
182
182
|
else {
|
|
183
|
-
lines.push(` {
|
|
183
|
+
lines.push(` {light-blue-fg}These lenses ride the site network, so the key fits ${hostLabel(host)}${hint(` -- "jack in", "go ${host.name}", "hack"`)}.{/light-blue-fg}`);
|
|
184
184
|
}
|
|
185
185
|
// A mark is all canSnoopFeeds ever wanted (surveillance.ts).
|
|
186
|
-
lines.push(` {
|
|
186
|
+
lines.push(` {light-blue-fg}And the eyes are half yours already${hint(` -- jacked in, "snoop <room>" rides any feed on this network`)}.{/light-blue-fg}`);
|
|
187
187
|
lines.push(...godLines);
|
|
188
188
|
this.scene.updateStatus();
|
|
189
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 ? ' {light-blue-fg}(HELD){/light-blue-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.)`)}`);
|
|
@@ -503,5 +503,28 @@
|
|
|
503
503
|
// to refuse sideQuest.settle -- on a shared run the cab is intercepted
|
|
504
504
|
// before the engine ever sees it, so the server is the only place
|
|
505
505
|
// that path can be stopped.
|
|
506
|
-
|
|
506
|
+
// 1.57.0 (2026-09-15): A VOICE CARRIES AS FAR AS THE EAR, and two
|
|
507
|
+
// spellings change with it.
|
|
508
|
+
// - `say` was resolved by SPOT NAME -- whoever shared your spot, plus
|
|
509
|
+
// anyone CIRCULATING, because "no position" was read as "every
|
|
510
|
+
// position". A patrol officer walking through a back room answered a
|
|
511
|
+
// question put to a Johnson at a table. It also ESCALATED on its own:
|
|
512
|
+
// alone at a spot, a murmur became a room-wide shout nobody asked
|
|
513
|
+
// for. Now `say` reaches whoever is inside their OWN hearing range
|
|
514
|
+
// (utilities/earshot.ts canHearSpeech; range is 5 m plus the
|
|
515
|
+
// listener's perception dice) and never escalates. Canon gives no
|
|
516
|
+
// distance for speech -- it is dice and limits, p.445/p.453-454 --
|
|
517
|
+
// so the metres are this engine's and are labelled as such.
|
|
518
|
+
// - NEW VERB `shout` (~50 m, in practice room-wide) is the old
|
|
519
|
+
// escalation, now deliberate.
|
|
520
|
+
// - `talk-to` is spelled `tell`. Command semantics in shared scenes:
|
|
521
|
+
// the old spelling stays registered as an alias, so nothing that
|
|
522
|
+
// learned it breaks.
|
|
523
|
+
// - COLOUR TAGS: the run-together "lightblue" spelling never parsed
|
|
524
|
+
// in blessed (its tag parser wants "light blue fg", hyphens turned
|
|
525
|
+
// to spaces) and was printed to CLI players as literal braces for
|
|
526
|
+
// months, while the web client rendered it perfectly. 134 tags are
|
|
527
|
+
// now {light-blue-fg}. The site's render-tags.ts maps that back
|
|
528
|
+
// to a real CSS keyword, so the pin must carry both halves.
|
|
529
|
+
export const ENGINE_VERSION = '1.57.0';
|
|
507
530
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -73,6 +73,7 @@ import { LockCommand } from './commands/lock.js';
|
|
|
73
73
|
import { SearchCommand } from './commands/search.js';
|
|
74
74
|
import { SayCommand } from './commands/say.js';
|
|
75
75
|
import { TalkCommand } from './commands/talk.js';
|
|
76
|
+
import { ShoutCommand } from './commands/shout.js';
|
|
76
77
|
import { StoreCommand } from './commands/store.js';
|
|
77
78
|
import { OpenCommand, CloseCommand, PutCommand } from './commands/container-verbs.js';
|
|
78
79
|
import { GiveCommand } from './commands/give.js';
|
|
@@ -708,7 +709,7 @@ export default class Game {
|
|
|
708
709
|
Logger.getInstance().write(`Loose end "${c.id}" left: ${erased ? 'ERASED' : 'still open'}; hub is the scene again.`);
|
|
709
710
|
this.requestSave('cleanup');
|
|
710
711
|
return [erased
|
|
711
|
-
? `\n{
|
|
712
|
+
? `\n{light-blue-fg}The grid closes behind you. ${le?.site ?? 'The site'}'s feeds run clean -- whatever they had of you is gone.{/light-blue-fg}`
|
|
712
713
|
: `\n{yellow-fg}The grid closes behind you. The footage is still up there.{/yellow-fg}${hint(` (Jack in and "hop" again while the desk hasn't reviewed it.)`)}`];
|
|
713
714
|
}
|
|
714
715
|
/** The HUD's read on the district (bands, never the number). */
|
|
@@ -2692,7 +2693,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2692
2693
|
CommandFactory.registerCommand('open', OpenCommand);
|
|
2693
2694
|
CommandFactory.registerCommand('close', CloseCommand);
|
|
2694
2695
|
CommandFactory.registerCommand('put', PutCommand);
|
|
2696
|
+
// TELL IS THE VERB (en3p6eCpiRuqsTSKm: "let's adjust 'talk-to' to
|
|
2697
|
+
// 'tell' to be more concise"). `talk-to` stays registered as an
|
|
2698
|
+
// alias rather than being deleted: it is the spelling in every
|
|
2699
|
+
// transcript, every NPC brief and every backlog item written before
|
|
2700
|
+
// today, and a player who learned it should not be told it is gone.
|
|
2701
|
+
CommandFactory.registerCommand('tell', TalkCommand);
|
|
2695
2702
|
CommandFactory.registerCommand('talk-to', TalkCommand);
|
|
2703
|
+
CommandFactory.registerCommand('shout', ShoutCommand);
|
|
2696
2704
|
CommandFactory.registerCommand('give', GiveCommand);
|
|
2697
2705
|
CommandFactory.registerCommand('read', ReadCommand);
|
|
2698
2706
|
CommandFactory.registerCommand('kill', KillCommand);
|
|
@@ -5955,7 +5963,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5955
5963
|
// works the grid while you're back in the meat (the AR-drop exit).
|
|
5956
5964
|
const remoteAgent = this.companions.find(c => c.kind === 'agent');
|
|
5957
5965
|
if (remoteAgent && player.plane !== 'matrix') {
|
|
5958
|
-
lines.push(`{
|
|
5966
|
+
lines.push(`{light-blue-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)')}{/light-blue-fg}`);
|
|
5959
5967
|
}
|
|
5960
5968
|
// Off-plane context: which reality you're in, your persona's Matrix
|
|
5961
5969
|
// track (cold-sim damage lands there), and where your empty body lies.
|
|
@@ -5974,7 +5982,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5974
5982
|
const track = jackedDeck
|
|
5975
5983
|
? ` Deck A${jackedDeck.attack} S${jackedDeck.sleaze} D${jackedDeck.dataProcessing} F${jackedDeck.firewall} ${jackedDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}${pgms}`
|
|
5976
5984
|
: ` (living persona -- damage is REAL stun)`;
|
|
5977
|
-
lines.push(`{
|
|
5985
|
+
lines.push(`{light-blue-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/light-blue-fg}`);
|
|
5978
5986
|
}
|
|
5979
5987
|
else if (player.plane === 'astral') {
|
|
5980
5988
|
lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
|
|
@@ -6066,7 +6074,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6066
6074
|
const dockW = mapWidth ?? 15;
|
|
6067
6075
|
const lines = hosts.length > 0
|
|
6068
6076
|
? hosts.map(h => hostDockLine(h, this.player, dockW))
|
|
6069
|
-
: ['{
|
|
6077
|
+
: ['{light-blue-fg}nothing overhead{/light-blue-fg}'];
|
|
6070
6078
|
lines.push(caption(this.player.insideHost ? `inside ${this.player.insideHost.name}` : `on the grid from ${gridVicinity(this.player).name}`, mapWidth));
|
|
6071
6079
|
this.setMapOverlay(lines);
|
|
6072
6080
|
if (onTheGrid) {
|
|
@@ -6575,7 +6583,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6575
6583
|
// technomancer-gated (a sprite task, SR5 p.256) and refuses anyone
|
|
6576
6584
|
// else, so the Emerged shelf is where its audience looks.
|
|
6577
6585
|
{ title: 'resonance', entries: [['compile'], ['decompile'], ['thread'], ['unravel'], ['register'], ['standby'], ['sustain'], ['sprites']] },
|
|
6578
|
-
{ title: 'social', entries: [['say'], ['emote', 'me'], ['talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
6586
|
+
{ title: 'social', entries: [['say'], ['shout'], ['emote', 'me'], ['tell', 'talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
6579
6587
|
{ title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['garage'], ['pay']] },
|
|
6580
6588
|
{ title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
|
|
6581
6589
|
{ title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
@@ -9,7 +9,8 @@ import * as fuzzyMatch from '../utilities/fuzzy-match.js';
|
|
|
9
9
|
import { sameHostSide } from './player.js';
|
|
10
10
|
import { AI } from '../../../../tools/ai/ai.class.js';
|
|
11
11
|
import { AbstractRoom } from '../types/shared/abstracts.js';
|
|
12
|
-
import {
|
|
12
|
+
import { spotsActive, actorDistanceMeters, voiceBandFor, voiceTag, doorwayKindOf, exitDoorwayKind } from '../utilities/spots.js';
|
|
13
|
+
import { canHearSpeech } from '../utilities/earshot.js';
|
|
13
14
|
import { synthesizeGrid, } from '../utilities/room-grid.js';
|
|
14
15
|
export class Room extends AbstractRoom {
|
|
15
16
|
logger = Logger.getInstance();
|
|
@@ -724,16 +725,37 @@ Keep it concise and dramatic.`;
|
|
|
724
725
|
const others = mouths - 1;
|
|
725
726
|
return ` [System note: ${others} other ${others === 1 ? 'person' : 'people'} in earshot heard this too, and each is answering in the same moment as you -- none of you can see what the others are about to say. Say YOUR piece, in your own voice, but not the same piece: if the obvious reply is one anyone here would give, make yours specific to you instead. Two people reporting one event reads as a joke.]`;
|
|
726
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* ORDINARY SPEECH, which now carries as far as the LISTENER can hear
|
|
730
|
+
* and no further (en3p6eCpiRuqsTSKm: "'say' should only be perceived
|
|
731
|
+
* by anyone within 3-5m un-augmented. Really, it's the audible
|
|
732
|
+
* perception range of the listener").
|
|
733
|
+
*
|
|
734
|
+
* WHAT THIS REPLACES, and why the old shape produced the report.
|
|
735
|
+
* Speech used to be resolved by SPOT NAME: whoever shared your spot
|
|
736
|
+
* heard you, plus anyone CIRCULATING -- and if nobody shared your
|
|
737
|
+
* spot, the words escalated to a room-wide shout automatically. Two
|
|
738
|
+
* consequences, both reported in one session:
|
|
739
|
+
*
|
|
740
|
+
* - A circulating actor heard everything, because "no spot" was read
|
|
741
|
+
* as "everywhere". That is how Patrol Officer Kessler, `moving
|
|
742
|
+
* through` the back room, answered a question put to Ms. Quill at
|
|
743
|
+
* a table (CugfYsH8DWH8FcfgS).
|
|
744
|
+
* - Standing anywhere on your own turned every murmur into a shout
|
|
745
|
+
* without you asking for one, so there was no way to say something
|
|
746
|
+
* quietly and no verb that meant "I want the room to hear this".
|
|
747
|
+
*
|
|
748
|
+
* Now: earshot.ts owns the rule (canHearSpeech), distance is real
|
|
749
|
+
* metres between cells, and the range belongs to each listener's ears.
|
|
750
|
+
* The escalation is gone -- SHOUTING IS A VERB NOW (commands/shout.ts,
|
|
751
|
+
* Room.shout below). Saying something nobody is close enough to catch
|
|
752
|
+
* is a thing that can happen, and say.ts tells you so.
|
|
753
|
+
*
|
|
754
|
+
* Returns who actually heard it, so the caller can say "nobody did"
|
|
755
|
+
* without asking the same question a second way and risking a
|
|
756
|
+
* different answer.
|
|
757
|
+
*/
|
|
727
758
|
say(actor, message) {
|
|
728
|
-
// "At" spots: speech is LOCAL (player decision: speech local, sight
|
|
729
|
-
// room-wide -- walking into the bar had every NPC in the room
|
|
730
|
-
// engaging at once). Same-spot and circulating listeners hear
|
|
731
|
-
// normally. When NOBODY shares the speaker's spot but others are in
|
|
732
|
-
// the room, the words carry as a SHOUT -- delivered room-wide with
|
|
733
|
-
// a tag so an NPC AI reacts to being called across the room, not to
|
|
734
|
-
// bar-side murmur. When same-spot listeners exist, cross-spot
|
|
735
|
-
// actors hear nothing (v1: the murmur doesn't carry).
|
|
736
|
-
const speakerSpot = spotOf(actor);
|
|
737
759
|
// THE HOST WALL IS ALSO A WALL FOR VOICES (FhXadizkMCxAdMaJx).
|
|
738
760
|
//
|
|
739
761
|
// Every verb that PICKS a target honours sameHostSide -- that is what
|
|
@@ -748,66 +770,67 @@ Keep it concise and dramatic.`;
|
|
|
748
770
|
// have insideHost undefined, so they compare equal and nothing about
|
|
749
771
|
// ordinary speech changes.
|
|
750
772
|
const listeners = [...this.actors].filter(r => r !== actor && sameHostSide(actor, r));
|
|
773
|
+
const audience = listeners.filter(r => canHearSpeech(actor, r));
|
|
751
774
|
// broadcast: true -- room speech is overheard, not addressed. A
|
|
752
775
|
// companion shell at heel uses that distinction to stay quiet (see
|
|
753
|
-
// NPC.hear); targeted channels (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const local = listeners.filter(r => {
|
|
773
|
-
const rs = spotOf(r);
|
|
774
|
-
return rs === undefined || rs === speakerSpot;
|
|
775
|
-
});
|
|
776
|
-
if (local.length > 0) {
|
|
777
|
-
const crowd = this.crowdNote(local);
|
|
778
|
-
for (const recipient of local) {
|
|
779
|
-
void Promise.resolve(recipient.hear(actor, recipient instanceof NPC ? `${message}${crowd}` : message, { broadcast: true })).catch(err => {
|
|
780
|
-
this.logger.error(`${recipient.name} failed to hear ${actor.name}: ${err}`);
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
return;
|
|
776
|
+
// NPC.hear); targeted channels (tell/order/ask...) never set it.
|
|
777
|
+
const crowd = this.crowdNote(audience);
|
|
778
|
+
// CAUGHT, like every other hear() fan-out in the game (say.ts,
|
|
779
|
+
// planes.ts, game.ts heartbeat). These were the only bare ones
|
|
780
|
+
// left: a bare `void` on a rejecting promise is an
|
|
781
|
+
// unhandledRejection, and ui.ts fatal handler kills the session on
|
|
782
|
+
// one -- which is exactly how a 502 from the AI relay ended a live
|
|
783
|
+
// game twice on 2026-08-29. Belt AND braces: NPC.hear now swallows
|
|
784
|
+
// its own reaction failures too, so this is redundant for NPCs on
|
|
785
|
+
// purpose -- it still guards any other Player subclass that grows
|
|
786
|
+
// an async hear().
|
|
787
|
+
//
|
|
788
|
+
// NO VOICE TAG. Everyone here is inside their own hearing range by
|
|
789
|
+
// construction, which is what voiceBandFor calls 'conversation' --
|
|
790
|
+
// the band that has always rendered as no tag at all.
|
|
791
|
+
for (const recipient of audience) {
|
|
792
|
+
void Promise.resolve(recipient.hear(actor, recipient instanceof NPC ? `${message}${crowd}` : message, { broadcast: true })).catch(err => {
|
|
793
|
+
this.logger.error(`${recipient.name} failed to hear ${actor.name}: ${err}`);
|
|
794
|
+
});
|
|
784
795
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
796
|
+
return audience;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* SHOUTING, which used to happen to you and is now something you do
|
|
800
|
+
* (en3p6eCpiRuqsTSKm: "add 'shout' can be heard by anyone within a
|
|
801
|
+
* reasonable distance, 50m?").
|
|
802
|
+
*
|
|
803
|
+
* Reaches the whole room -- see earshot.ts SHOUT_METERS for why 50 m
|
|
804
|
+
* is in practice room-wide and why the number is this engine's rather
|
|
805
|
+
* than the book's. Each listener is told how far the words travelled
|
|
806
|
+
* (voiceTag), so an NPC's AI answers someone calling across a bar
|
|
807
|
+
* differently from someone at its elbow.
|
|
808
|
+
*
|
|
809
|
+
* THE NPC RESTRAINT IS KEPT from the old auto-escalation, because the
|
|
810
|
+
* reason for it has not changed: when every NPC's raised voice went
|
|
811
|
+
* room-wide, two barflies at opposite ends held a shouted conversation
|
|
812
|
+
* about the player's drink order through each other's AI (a real
|
|
813
|
+
* session -- Patch and Old Cray). An NPC shout reaches the PLAYER, the
|
|
814
|
+
* camera rather than the cast. A PLAYER's shout reaches everyone,
|
|
815
|
+
* which is the whole point of typing it.
|
|
816
|
+
*/
|
|
817
|
+
shout(actor, message) {
|
|
818
|
+
const listeners = [...this.actors].filter(r => r !== actor && sameHostSide(actor, r));
|
|
797
819
|
const player = [...this.actors].find(a => !(a instanceof NPC));
|
|
798
|
-
const
|
|
820
|
+
const targets = actor instanceof NPC
|
|
799
821
|
? listeners.filter(r => r === player)
|
|
800
822
|
: listeners;
|
|
801
|
-
const
|
|
802
|
-
for (const recipient of
|
|
823
|
+
const crowd = this.crowdNote(targets);
|
|
824
|
+
for (const recipient of targets) {
|
|
803
825
|
const band = voiceBandFor(actorDistanceMeters(this, actor, recipient));
|
|
804
826
|
// The crowd note goes LAST, after the voice tag: both are system
|
|
805
827
|
// dressing, and the tag describes the words while the note
|
|
806
828
|
// describes the room.
|
|
807
|
-
void Promise.resolve(recipient.hear(actor, `${message}${voiceTag(band)}${recipient instanceof NPC ?
|
|
829
|
+
void Promise.resolve(recipient.hear(actor, `${message}${voiceTag(band)}${recipient instanceof NPC ? crowd : ''}`, { broadcast: true })).catch(err => {
|
|
808
830
|
this.logger.error(`${recipient.name} failed to hear ${actor.name} shout: ${err}`);
|
|
809
831
|
});
|
|
810
832
|
}
|
|
833
|
+
return targets;
|
|
811
834
|
}
|
|
812
835
|
addExit(direction, targetRoom, keyItemOrSolution) {
|
|
813
836
|
let door = this.findExistingDoor(targetRoom);
|
|
@@ -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 `{light-blue-fg}AR OVERLAY:{/light-blue-fg}\n${tags.map(t => ` {light-blue-fg}${t}{/light-blue-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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sameSpot, spotOf, spotsActive, OPEN_FLOOR } from './spots.js';
|
|
1
|
+
import { sameSpot, spotOf, spotsActive, OPEN_FLOOR, IMMEDIATE_VICINITY_M, actorDistanceMeters } from './spots.js';
|
|
2
2
|
/**
|
|
3
3
|
* WHO CAN OVERHEAR A MURMUR (playtest, Deditri: "a private place for
|
|
4
4
|
* calls"). A call is audible to the room, and that is correct and
|
|
@@ -17,17 +17,25 @@ import { sameSpot, spotOf, spotsActive, OPEN_FLOOR } from './spots.js';
|
|
|
17
17
|
* Market's "shadowed edge". Nothing new is invented here; the call path
|
|
18
18
|
* just respects the position everyone else already respects.
|
|
19
19
|
*
|
|
20
|
-
* THE RULE, and why it is not Room.say's rule
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* escalates
|
|
24
|
-
* murmur reaches your spot and stops there.
|
|
20
|
+
* THE RULE, and why it is STILL not Room.say's rule. A murmur into a
|
|
21
|
+
* commlink reaches your spot and stops there, full stop -- talking
|
|
22
|
+
* quietly into a phone is the whole point of a phone, and it never
|
|
23
|
+
* escalates however alone you are standing.
|
|
25
24
|
*
|
|
26
|
-
*
|
|
25
|
+
* Room speech used to share that shape and no longer does. `say` is now
|
|
26
|
+
* bounded by each LISTENER's hearing range in real metres
|
|
27
|
+
* (canHearSpeech below, en3p6eCpiRuqsTSKm) and never escalates on its
|
|
28
|
+
* own; shouting is a verb you type (commands/shout.ts). The two rules
|
|
29
|
+
* live in one file so they can be read against each other, which is the
|
|
30
|
+
* lesson of the drift that produced this module in the first place.
|
|
31
|
+
*
|
|
32
|
+
* WHAT DOES NOT BUY PRIVACY ON A CALL, deliberately:
|
|
27
33
|
* - CIRCULATING (no atSpot) is the compat state and means "anywhere in
|
|
28
|
-
* the room" --
|
|
29
|
-
*
|
|
30
|
-
*
|
|
34
|
+
* the room" -- your half of a CALL still reaches and is reached by
|
|
35
|
+
* everything, because there is no spot for it to stop at. Note this
|
|
36
|
+
* is the opposite of what circulating now means for ordinary speech,
|
|
37
|
+
* and the asymmetry is argued at canHearSpeech: a murmur has a place
|
|
38
|
+
* it comes from and a phone call does not.
|
|
31
39
|
* - THE OPEN FLOOR is a real position but it is bare ground in the
|
|
32
40
|
* middle of the room, no cover, always visible (see spots.ts). Being
|
|
33
41
|
* apart from people is not the same as being out of earshot, and a
|
|
@@ -40,6 +48,104 @@ import { sameSpot, spotOf, spotsActive, OPEN_FLOOR } from './spots.js';
|
|
|
40
48
|
* Nothing here caches, stores, or derives a second notion of where
|
|
41
49
|
* anybody is.
|
|
42
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* HOW FAR ORDINARY SPEECH CARRIES, and how far one listener can hear it
|
|
53
|
+
* (en3p6eCpiRuqsTSKm: "'say' should only be perceived by anyone within
|
|
54
|
+
* 3-5m un-augmented. Really, it's the audible perception range of the
|
|
55
|
+
* listener").
|
|
56
|
+
*
|
|
57
|
+
* CANON IS SILENT ON THE DISTANCE, checked rather than assumed. SR5
|
|
58
|
+
* handles hearing as dice and limits, never as a range table: an audio
|
|
59
|
+
* enhancement "adds its Rating to your limit in audio Perception Tests"
|
|
60
|
+
* (p.445), cyberears give "hearing with normal ranges" (p.453-454), and
|
|
61
|
+
* the only metre figures anywhere near it belong to the directional and
|
|
62
|
+
* laser microphones (p.445, 100 m). There is no rule for how far a
|
|
63
|
+
* sentence travels, so THESE NUMBERS ARE THIS ENGINE'S, said out loud
|
|
64
|
+
* rather than dressed as the book's.
|
|
65
|
+
*
|
|
66
|
+
* What is defensible about them:
|
|
67
|
+
* - 5 m is already this engine's word for conversational range
|
|
68
|
+
* (spots.ts IMMEDIATE_VICINITY_M, and voiceBandFor calls anything
|
|
69
|
+
* inside it 'conversation'). Speech reaching exactly as far as the
|
|
70
|
+
* engine already says a conversation reaches is one number, not two.
|
|
71
|
+
* - The listener's perception dice extend it. Canon gives audio gear a
|
|
72
|
+
* DICE/LIMIT bonus and this engine models speech by DISTANCE, so the
|
|
73
|
+
* conversion is ours; a metre per die is the smallest unit that can
|
|
74
|
+
* express "you hear better than they do" in a model made of 2 m
|
|
75
|
+
* cells. Cyberears (+1) hear a metre further. That is an
|
|
76
|
+
* extrapolation of a canon bonus, not a canon rule.
|
|
77
|
+
*/
|
|
78
|
+
export const SPEECH_METERS = IMMEDIATE_VICINITY_M;
|
|
79
|
+
/**
|
|
80
|
+
* A SHOUT (commands/shout.ts) -- "anyone within a reasonable distance,
|
|
81
|
+
* 50m?" Also this engine's number and not the book's. In practice it is
|
|
82
|
+
* room-wide: the largest room this generator builds is tens of metres
|
|
83
|
+
* across, so the figure is a ceiling that says "the whole place hears
|
|
84
|
+
* you" rather than a boundary anyone will stand outside.
|
|
85
|
+
*/
|
|
86
|
+
export const SHOUT_METERS = 50;
|
|
87
|
+
/**
|
|
88
|
+
* How far THIS listener can hear, in metres. The report's own framing:
|
|
89
|
+
* range is a property of the ear, not of the mouth.
|
|
90
|
+
*
|
|
91
|
+
* Reads the same `perception` pool a Perception test reads, so a piece
|
|
92
|
+
* of chrome and a quality that both say "+1 perception" extend hearing
|
|
93
|
+
* identically and neither needs a line here. Never below the base -- a
|
|
94
|
+
* perception PENALTY should make you miss things you roll for, not
|
|
95
|
+
* shrink the room to nothing.
|
|
96
|
+
*/
|
|
97
|
+
export function hearingRangeMeters(listener) {
|
|
98
|
+
const bonus = listener.augBonus('perception') + listener.qualityBonus('perception');
|
|
99
|
+
return Math.max(SPEECH_METERS, SPEECH_METERS + bonus);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* CAN THIS LISTENER HEAR ORDINARY SPEECH FROM THERE -- the whole of the
|
|
103
|
+
* `say` rule, in one place so the verb, the NPC fan-out and any future
|
|
104
|
+
* reader cannot drift apart the way Room.say and murmurAudience once did.
|
|
105
|
+
*
|
|
106
|
+
* THE CIRCULATING ASYMMETRY, which is the actual defect this item was
|
|
107
|
+
* filed about. "Circulating" (no atSpot) is the compat state meaning
|
|
108
|
+
* "somewhere in this room, unfixed", and the old rule read it as "at
|
|
109
|
+
* every spot at once" -- so a circulating listener heard everything.
|
|
110
|
+
* That is how a patrol officer walking through the back room answered a
|
|
111
|
+
* question put to a Johnson at a table (CugfYsH8DWH8FcfgS, same
|
|
112
|
+
* session): Kessler was `moving through`, so he was nowhere, so he was
|
|
113
|
+
* everywhere.
|
|
114
|
+
*
|
|
115
|
+
* ASK spotOf, NEVER actorCell. A circulating body IS given a cell by
|
|
116
|
+
* seatingIn -- its "drift seat" -- and that cell is deliberately not a
|
|
117
|
+
* position: spots.ts says so in as many words ("a place to DRAW
|
|
118
|
+
* someone, never a position they hold"), and sameSpot and canReach both
|
|
119
|
+
* short-circuit on atSpot before any of it is consulted. A distance
|
|
120
|
+
* check written against the drift seat would have put Kessler a
|
|
121
|
+
* plausible two metres from the table and let him hear after all --
|
|
122
|
+
* correct-looking arithmetic over a number that means nothing.
|
|
123
|
+
*
|
|
124
|
+
* A LISTENER with no position is therefore NOT in earshot of a murmur:
|
|
125
|
+
* someone crossing the room is not standing at your table. A SPEAKER
|
|
126
|
+
* with no position still reaches the room, and that is deliberate rather
|
|
127
|
+
* than an oversight -- they are moving among everybody, there is no
|
|
128
|
+
* "here" for the words to fall short of, and refusing to let a
|
|
129
|
+
* circulating player be heard at all would make the game's main verb
|
|
130
|
+
* fail for the commonest position in it.
|
|
131
|
+
*
|
|
132
|
+
* A room that declares NO spots keeps the old room-wide behaviour
|
|
133
|
+
* outright. There is no geometry in such a room to be near or far in,
|
|
134
|
+
* and inventing one would refuse speech in the small abstract rooms
|
|
135
|
+
* where `say` is all there is.
|
|
136
|
+
*/
|
|
137
|
+
export function canHearSpeech(speaker, listener) {
|
|
138
|
+
const room = speaker.currentLocation;
|
|
139
|
+
if (!spotsActive(room))
|
|
140
|
+
return true;
|
|
141
|
+
// The speaker is unplaced: they are moving among everyone.
|
|
142
|
+
if (spotOf(speaker) === undefined)
|
|
143
|
+
return true;
|
|
144
|
+
// The listener is unplaced: they are passing through, not standing here.
|
|
145
|
+
if (spotOf(listener) === undefined)
|
|
146
|
+
return false;
|
|
147
|
+
return actorDistanceMeters(room, speaker, listener) <= hearingRangeMeters(listener);
|
|
148
|
+
}
|
|
43
149
|
/**
|
|
44
150
|
* Everyone in the speaker's room close enough to catch a murmur.
|
|
45
151
|
* From a named spot: whoever shares it, plus anyone circulating.
|
|
@@ -213,6 +213,6 @@ export function hostDockLine(room, actor, width) {
|
|
|
213
213
|
: '';
|
|
214
214
|
const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}`;
|
|
215
215
|
const cut = [...raw].slice(0, Math.max(4, width)).join('');
|
|
216
|
-
return `{
|
|
216
|
+
return `{light-blue-fg}${cut}{/light-blue-fg}`;
|
|
217
217
|
}
|
|
218
218
|
//# sourceMappingURL=grid-reach.js.map
|
|
@@ -460,20 +460,20 @@ export function gridSculpt(actor, room) {
|
|
|
460
460
|
const purpose = room.host?.purpose;
|
|
461
461
|
const sculpt = room.host?.sculpt;
|
|
462
462
|
return [
|
|
463
|
-
`{
|
|
464
|
-
`{
|
|
463
|
+
`{light-blue-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/light-blue-fg}`,
|
|
464
|
+
`{light-blue-fg}${sculpt ?? 'Architecture all the way around -- the outside grid is a wall away and might as well be weather.'}{/light-blue-fg}`,
|
|
465
465
|
].join('\n');
|
|
466
466
|
}
|
|
467
467
|
// ON THE GRID (SR5 pp.217-218): no street under the persona -- a black
|
|
468
468
|
// flatland, the body's room named only because that is where the
|
|
469
469
|
// signal comes from. A host directly overhead gets a line of its own.
|
|
470
470
|
const body = actor.bodyRoom ?? room;
|
|
471
|
-
const header = `{
|
|
471
|
+
const header = `{light-blue-fg}THE GRID :: jacked in from ${body.name.toUpperCase()}{/light-blue-fg}`;
|
|
472
472
|
const grid = actor.currentGrid;
|
|
473
|
-
const flat = `{
|
|
473
|
+
const flat = `{light-blue-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)' : ''}.` : ''}{/light-blue-fg}`;
|
|
474
474
|
if (!room.hasNode)
|
|
475
475
|
return `${header}\n${flat}`;
|
|
476
|
-
return `${header}\n${flat}\n{
|
|
476
|
+
return `${header}\n${flat}\n{light-blue-fg}${hostLabel(room, { capital: true })} hangs directly overhead, sealed. What it holds is inside it.{/light-blue-fg}`;
|
|
477
477
|
}
|
|
478
478
|
/**
|
|
479
479
|
* The icons rendered as the standard cyan block, or the thin-grid line.
|
|
@@ -502,19 +502,19 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
502
502
|
if (actor.insideHost || !rooms) {
|
|
503
503
|
const icons = gridIcons(scene, actor, room);
|
|
504
504
|
if (icons.length === 0) {
|
|
505
|
-
return `{
|
|
505
|
+
return `{light-blue-fg}Thin grid out here -- background noise, nothing worth cracking.{/light-blue-fg}`;
|
|
506
506
|
}
|
|
507
|
-
return `{
|
|
507
|
+
return `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${icons.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`;
|
|
508
508
|
}
|
|
509
509
|
const out = [];
|
|
510
510
|
const hosts = hostsInReach(rooms, actor);
|
|
511
511
|
out.push(hosts.length > 0
|
|
512
|
-
? `{
|
|
513
|
-
: `{
|
|
512
|
+
? `{light-blue-fg}HOSTS OVERHEAD:{/light-blue-fg}\n${hosts.map(h => ` {light-blue-fg}${hostLine(h, actor)}{/light-blue-fg}`).join('\n')}`
|
|
513
|
+
: `{light-blue-fg}Nothing overhead -- no host stands over this district.{/light-blue-fg}`);
|
|
514
514
|
const near = gridIcons(scene, actor, gridVicinity(actor)).filter(i => !isHostLine(i));
|
|
515
515
|
out.push(near.length > 0
|
|
516
|
-
? `{
|
|
517
|
-
: `{
|
|
516
|
+
? `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${near.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`
|
|
517
|
+
: `{light-blue-fg}Nothing near -- background noise around your signal.{/light-blue-fg}`);
|
|
518
518
|
// FARTHER OUT, WITHOUT AN ADDRESS (SR5 p.235, p.243): Matrix Perception
|
|
519
519
|
// never tells you where a device physically sits -- that is Trace Icon,
|
|
520
520
|
// two marks and an opposed test. So far icons are listed by the noise
|
|
@@ -539,9 +539,9 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
539
539
|
byBand.set(noise, list);
|
|
540
540
|
}
|
|
541
541
|
const far = [...byBand.entries()].sort((a, b) => a[0] - b[0])
|
|
542
|
-
.map(([noise, names]) => ` {
|
|
542
|
+
.map(([noise, names]) => ` {light-blue-fg}· dim, noise -${noise}: ${names.join(', ')}{/light-blue-fg}`);
|
|
543
543
|
if (far.length > 0)
|
|
544
|
-
out.push(`{
|
|
544
|
+
out.push(`{light-blue-fg}FARTHER OUT -- dimmer with distance:{/light-blue-fg}\n${far.join('\n')}`);
|
|
545
545
|
// A blank line between the distance bands (user, 2026-09-02): three
|
|
546
546
|
// sections read as three when they do not touch.
|
|
547
547
|
return out.join('\n\n');
|
|
@@ -556,7 +556,7 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
556
556
|
* the other personas, and the files. Cut to the dock's rows and columns.
|
|
557
557
|
*/
|
|
558
558
|
export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
559
|
-
const cut = (raw) => `{
|
|
559
|
+
const cut = (raw) => `{light-blue-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/light-blue-fg}`;
|
|
560
560
|
const lines = [];
|
|
561
561
|
lines.push(cut(`INSIDE ${host.name}`));
|
|
562
562
|
if (host.purpose)
|
|
@@ -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: [`{light-blue-fg}The grid RIPPLES -- hard, close, and looking. Something vast has nearly found the thread you're hanging by. ("reboot" is a clean slate.){/light-blue-fg}`], converged: false };
|
|
62
62
|
}
|
|
63
63
|
if (before < 20 && actor.overwatchScore >= 20) {
|
|
64
|
-
return { lines: [`{
|
|
64
|
+
return { lines: [`{light-blue-fg}A ripple crosses the grid -- subtle, wrong, deliberate. Somewhere above, an eye has started counting your fingerprints.{/light-blue-fg}`], converged: false };
|
|
65
65
|
}
|
|
66
66
|
return { lines: [], converged: false };
|
|
67
67
|
}
|
|
@@ -454,7 +454,7 @@ export function fileReach(actor, room) {
|
|
|
454
454
|
*/
|
|
455
455
|
export function planeTintItemName(item, viewerPlane) {
|
|
456
456
|
if (item.plane === 'matrix') {
|
|
457
|
-
return `{
|
|
457
|
+
return `{light-blue-fg}${item.name} [data]{/light-blue-fg}`;
|
|
458
458
|
}
|
|
459
459
|
if (item.plane === 'astral') {
|
|
460
460
|
return `{magenta-fg}${item.name} [astral]{/magenta-fg}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.198.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.",
|