@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.
Files changed (30) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +5 -5
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +4 -4
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +5 -5
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +19 -3
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +2 -2
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/shout.js +69 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +2 -2
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +24 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/game.js +13 -5
  22. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +80 -57
  23. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +116 -10
  25. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +1 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
  29. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
  30. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.197.0",
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 = `{lightblue-fg}The overlay blinks back in.{/lightblue-fg}`;
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, `{lightblue-fg}Nothing here is talking -- a dead patch of street.{/lightblue-fg}`, note].filter(Boolean).join('\n');
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 => ` {lightblue-fg}${t}{/lightblue-fg}`), note].filter(Boolean).join('\n');
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 [`{lightblue-fg}Nothing here is talking -- a dead patch of street.{/lightblue-fg}`, note].filter(Boolean).join('\n');
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
- `{lightblue-fg}AR OVERLAY:{/lightblue-fg}`,
112
- ...tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`),
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 = [`{lightblue-fg}Within reach to pull down:{/lightblue-fg}`];
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
- `{lightblue-fg}${fromSprite.npc.name} opens its hands and the data pours out of them.{/lightblue-fg}`,
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
- `{lightblue-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down off the grid -- written to a chip.{/lightblue-fg}`,
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
- `{lightblue-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.{/lightblue-fg}`,
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
- `{lightblue-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/lightblue-fg}`,
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
- `{lightblue-fg}You cross into ${hostLabel(target, { capital: false })} -- the outside grid folds shut behind you.{/lightblue-fg}`,
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 `{lightblue-fg}Thin grid here -- no host stands over ${gridVicinity(this.actor).name}.{/lightblue-fg} ${hint(`Overhead: ${roster} -- "enter <name>" crosses into any of them from here.`)}`;
113
+ 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 `{lightblue-fg}${street.name} has no host over it -- nothing to enter. Overhead: ${roster}.{/lightblue-fg}`;
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 `{lightblue-fg}You step back out of ${hostLabel(host, { capital: false })} onto the grid; the host seals behind you. Your body still sits in ${actor.currentLocation.name}.{/lightblue-fg}`;
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(` {lightblue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/lightblue-fg}`);
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(` {lightblue-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/lightblue-fg}`);
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(` {lightblue-fg}Nothing on the grid side of the vault -- whatever this place keeps sits on the ${room.offlineServer}, an air-gapped box with no Matrix presence. Your BODY has to be in ${room.name} to cable in (p.233).{/lightblue-fg}`);
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(` {lightblue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what it guards (p.240).{/lightblue-fg}`);
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
- `{lightblue-fg}You drop into the host's own traffic and become one more packet in a million. ${hostLabel(room, { capital: true })} loses you.{/lightblue-fg}`,
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 `{lightblue-fg}The sky changes texture and you're on ${target.name}.{/lightblue-fg}${target.userPenalty ? ` ${hint(`Public grid: -2 on everything you do out here.`)}` : ''}${await land()}`;
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
- `{lightblue-fg}You slip through a seam nobody sold you and come up on ${target.name}.{/lightblue-fg}`,
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{lightblue-fg} -- a far icon, ${far.name}${noiseNote(noise)}{/lightblue-fg}`;
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
- `{lightblue-fg}${hostIcon}{/lightblue-fg}`,
408
- `{lightblue-fg} Attack ${a.attack} Sleaze ${a.sleaze} Data Processing ${a.dataProcessing} Firewall ${a.firewall}{/lightblue-fg}`,
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 `{lightblue-fg}${eye}{/lightblue-fg}`;
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 = [`{lightblue-fg}◆ ${target.name} -- a persona on this grid.{/lightblue-fg}`];
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
- `{lightblue-fg}Black flatland under a black sky. No streets out here -- only icons. The hosts hang overhead; ${actor.insideHost ? '"exit" puts you back under them' : '"go <host>" (or "go <number>") crosses into one you hold a mark on, from anywhere'}.{/lightblue-fg}`,
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 `{lightblue-fg}Placing a mark IS ${action} -- running it (p.238/p.240).{/lightblue-fg}\n${result}`;
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{lightblue-fg}A ${room.offlineServer} sits here, dark and off the network -- no aerial, no marquee, nothing broadcasting. Whatever it holds comes out through a cable and no other way.${hint(` "jack in" RIGHT HERE to touch its files.`)}{/lightblue-fg}`
317
- : `\n{lightblue-fg}The node here hums with locked files -- whatever's worth taking lives on the grid, inside the host.${hint(` "jack in", then "enter" the host.`)}{/lightblue-fg}`;
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(`{lightblue-fg}(Security sees a loop of empty hallways -- these eyes answer only to you now.){/lightblue-fg}`);
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 ? ' {lightblue-fg}(yours){/lightblue-fg}' : ''}${type ? ` -- ${type.blurb}` : ''}`);
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 = "talk-to";
83
- static description = "Speak privately to a person in the room.";
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
- `{lightblue-fg}The filament finds the connector and the camera stops being a camera -- it's a device, and it's talking to you.{/lightblue-fg}`,
168
- ` {lightblue-fg}It never gets to hide behind ${hostLabel(host)}: a cable doesn't ask the network for permission, so the lens has to defend itself with what a lens has. It doesn't have much.{/lightblue-fg}`,
169
- ` {lightblue-fg}MARK PLACED -- and it lands on the host too. The camera answers to ${host.name}, and anything ${host.name} trusts to speak for it just handed you a key. [${now}/${MAX_MARKS}]{/lightblue-fg}`,
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(` {lightblue-fg}${now >= MAX_MARKS
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`)}.`}{/lightblue-fg}`);
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(` {lightblue-fg}These lenses ride the site network, so the key fits ${hostLabel(host)}${hint(` -- "jack in", "go ${host.name}", "hack"`)}.{/lightblue-fg}`);
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(` {lightblue-fg}And the eyes are half yours already${hint(` -- jacked in, "snoop <room>" rides any feed on this network`)}.{/lightblue-fg}`);
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 ? ' {lightblue-fg}(HELD){/lightblue-fg}' : ''}`);
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
- export const ENGINE_VERSION = '1.56.0';
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{lightblue-fg}The grid closes behind you. ${le?.site ?? 'The site'}'s feeds run clean -- whatever they had of you is gone.{/lightblue-fg}`
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(`{lightblue-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''}${hint(' ("jack in" to rejoin it, "recall" to fold it home)')}{/lightblue-fg}`);
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(`{lightblue-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/lightblue-fg}`);
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
- : ['{lightblue-fg}nothing overhead{/lightblue-fg}'];
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 { spotOf, spotsActive, actorDistanceMeters, voiceBandFor, voiceTag, doorwayKindOf, exitDoorwayKind } from '../utilities/spots.js';
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 (talk/order/ask...) never set it.
754
- if (speakerSpot === undefined) {
755
- const crowd = this.crowdNote(listeners);
756
- // CAUGHT, like every other hear() fan-out in the game (say.ts,
757
- // planes.ts, game.ts heartbeat). These three were the only bare
758
- // ones left: a bare `void` on a rejecting promise is an
759
- // unhandledRejection, and ui.ts fatal handler kills the session
760
- // on one -- which is exactly how a 502 from the AI relay ended a
761
- // live game twice on 2026-08-29. Belt AND braces: NPC.hear now
762
- // swallows its own reaction failures too, so this is redundant
763
- // for NPCs on purpose -- it still guards any other Player
764
- // subclass that grows an async hear().
765
- for (const recipient of listeners) {
766
- void Promise.resolve(recipient.hear(actor, recipient instanceof NPC ? `${message}${crowd}` : message, { broadcast: true })).catch(err => {
767
- this.logger.error(`${recipient.name} failed to hear ${actor.name}: ${err}`);
768
- });
769
- }
770
- return;
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
- // VOICE RANGE (spatial pass 2026-08-25): how hard you have to work
786
- // to be heard scales with the actual gap, not a flat "another spot"
787
- // -- since the room resize, a stallholder answering across a 32 m
788
- // market was tagged the same as a neighbour two tables over
789
- // (playtest: "he's basically screaming"). See spots.ts voiceBandFor.
790
- // The shout channel: a lone-at-spot PLAYER calling out reaches the
791
- // whole room. A lone-at-spot NPC reaches only the PLAYER -- the
792
- // camera, not the cast: when every NPC's murmur went room-wide, two
793
- // barflies at opposite ends conducted a shouted conversation about
794
- // the player's drink order (real session: Patch and Old Cray,
795
- // volleying "(calling across the room)" lines through each other's
796
- // AI). Same-spot NPC chat is untouched -- that's the local branch.
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 shoutTargets = actor instanceof NPC
820
+ const targets = actor instanceof NPC
799
821
  ? listeners.filter(r => r === player)
800
822
  : listeners;
801
- const shoutCrowd = this.crowdNote(shoutTargets);
802
- for (const recipient of shoutTargets) {
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 ? shoutCrowd : ''}`, { broadcast: true })).catch(err => {
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 `{lightblue-fg}AR OVERLAY:{/lightblue-fg}\n${tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`).join('\n')}`;
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: room speech that finds
21
- * nobody at your spot escalates to a SHOUT and carries room-wide -- that
22
- * is how you call across a bar. A commlink held to your face never
23
- * escalates; talking quietly into it is the whole point of it. So a
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
- * WHAT DOES NOT BUY PRIVACY, deliberately:
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" -- it both reaches and is reached by everything. It is
29
- * also the common case, and it stays exactly as loud as it has always
30
- * been.
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 `{lightblue-fg}${cut}{/lightblue-fg}`;
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
- `{lightblue-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/lightblue-fg}`,
464
- `{lightblue-fg}${sculpt ?? 'Architecture all the way around -- the outside grid is a wall away and might as well be weather.'}{/lightblue-fg}`,
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 = `{lightblue-fg}THE GRID :: jacked in from ${body.name.toUpperCase()}{/lightblue-fg}`;
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 = `{lightblue-fg}Black flatland under a black sky -- the icons are the only things out here.${grid ? ` Riding ${grid.name}${grid.userPenalty ? ' (-2 on everything you do, p.233)' : ''}.` : ''}{/lightblue-fg}`;
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{lightblue-fg}${hostLabel(room, { capital: true })} hangs directly overhead, sealed. What it holds is inside it.{/lightblue-fg}`;
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 `{lightblue-fg}Thin grid out here -- background noise, nothing worth cracking.{/lightblue-fg}`;
505
+ return `{light-blue-fg}Thin grid out here -- background noise, nothing worth cracking.{/light-blue-fg}`;
506
506
  }
507
- return `{lightblue-fg}ICONS IN REACH:{/lightblue-fg}\n${icons.map(i => ` {lightblue-fg}${i}{/lightblue-fg}`).join('\n')}`;
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
- ? `{lightblue-fg}HOSTS OVERHEAD:{/lightblue-fg}\n${hosts.map(h => ` {lightblue-fg}${hostLine(h, actor)}{/lightblue-fg}`).join('\n')}`
513
- : `{lightblue-fg}Nothing overhead -- no host stands over this district.{/lightblue-fg}`);
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
- ? `{lightblue-fg}ICONS IN REACH:{/lightblue-fg}\n${near.map(i => ` {lightblue-fg}${i}{/lightblue-fg}`).join('\n')}`
517
- : `{lightblue-fg}Nothing near -- background noise around your signal.{/lightblue-fg}`);
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]) => ` {lightblue-fg}· dim, noise -${noise}: ${names.join(', ')}{/lightblue-fg}`);
542
+ .map(([noise, names]) => ` {light-blue-fg}· dim, noise -${noise}: ${names.join(', ')}{/light-blue-fg}`);
543
543
  if (far.length > 0)
544
- out.push(`{lightblue-fg}FARTHER OUT -- dimmer with distance:{/lightblue-fg}\n${far.join('\n')}`);
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) => `{lightblue-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/lightblue-fg}`;
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)
@@ -1,4 +1,4 @@
1
- export const MATRIX_COLOR = 'lightblue';
1
+ export const MATRIX_COLOR = 'light-blue';
2
2
  export const FEED_COLOR = 'white';
3
3
  export const TURN_COLOR = 'yellow';
4
4
  /** Matrix-plane prose, tagged. */
@@ -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: [`{lightblue-fg}The grid RIPPLES -- hard, close, and looking. Something vast has nearly found the thread you're hanging by. ("reboot" is a clean slate.){/lightblue-fg}`], converged: false };
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: [`{lightblue-fg}A ripple crosses the grid -- subtle, wrong, deliberate. Somewhere above, an eye has started counting your fingerprints.{/lightblue-fg}`], converged: false };
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 `{lightblue-fg}${item.name} [data]{/lightblue-fg}`;
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.197.0",
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.",