@maka/maka-cli 5.196.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 (41) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +6 -1
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +6 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +8 -2
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +5 -5
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +62 -13
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +6 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/install.js +6 -1
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +5 -5
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +6 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/register.js +6 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +19 -3
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +2 -2
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +6 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/shout.js +69 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +2 -2
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +45 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/game.js +13 -5
  30. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +80 -57
  31. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +24 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/action-cost.js +36 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +116 -10
  35. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +83 -6
  36. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
  37. package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
  39. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +8 -0
  41. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.196.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.",
@@ -2,6 +2,7 @@ import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { SKILL_NAMES } from '../models/player.js';
4
4
  import { fuzzyPickName } from '../utilities/fuzzy-match.js';
5
+ import { inLiveCombat } from '../utilities/action-cost.js';
5
6
  /**
6
7
  * Karma spending: "advance" lists every skill AND attribute with its
7
8
  * current rating and the cost to raise it; "advance <name>" pays and
@@ -27,7 +28,11 @@ export class AdvanceCommand extends Command {
27
28
  ];
28
29
  async execute(args = []) {
29
30
  const actor = this.actor;
30
- if (actor.inExchange) {
31
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
32
+ // inLiveCombat: `inExchange` is a re-entrancy lock that is false for
33
+ // most of a Combat Turn, so this refusal said "firefight" and meant
34
+ // "for the half-second one trade of fire is resolving".
35
+ if (inLiveCombat(this.scene, actor)) {
31
36
  return `Mid-exchange is no time for training montages.`;
32
37
  }
33
38
  if (actor.currentLocation?.roomType !== 'Residence') {
@@ -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
  }
@@ -4,6 +4,7 @@ import { priceOf, vendorsInRoom, waresOf, shelfNote } from '../utilities/commerc
4
4
  import { findCatalogItem, ROLE_DOMAINS } from '../utilities/catalog.js';
5
5
  import { optionMark } from '../utilities/log-style.js';
6
6
  import { fuzzyPickName } from '../utilities/fuzzy-match.js';
7
+ import { inLiveCombat } from '../utilities/action-cost.js';
7
8
  /**
8
9
  * Diegetic shopping: "browse" (or bare "buy") in a vendor's room reads
9
10
  * their shelves with live prices; "buy <item>" slides the nuyen across
@@ -20,7 +21,11 @@ export class BuyCommand extends Command {
20
21
  if (actor.plane !== 'meat') {
21
22
  return `Commerce is a meat-world ritual -- come back to your body first.`;
22
23
  }
23
- if (actor.inExchange) {
24
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
25
+ // inLiveCombat: `inExchange` is a re-entrancy lock that is false for
26
+ // most of a Combat Turn, so this refusal said "firefight" and meant
27
+ // "for the half-second one trade of fire is resolving".
28
+ if (inLiveCombat(this.scene, actor)) {
24
29
  return `Nobody rings up a sale in a firefight.`;
25
30
  }
26
31
  const room = actor.currentLocation;
@@ -12,6 +12,7 @@ import { meetLocationFor } from '../utilities/meet-location.js';
12
12
  import { asksForDataWork, noDeckForDataWork } from '../utilities/data-work.js';
13
13
  import { partyCanDeck } from '../factories/scene-chunks.js';
14
14
  import { RecallCommand } from './companions.js';
15
+ import { inLiveCombat } from '../utilities/action-cost.js';
15
16
  export class CallCommand extends Command {
16
17
  static verb = 'call';
17
18
  static description = 'Call someone remotely over a commlink (no need to be in the same room; commlink must be equipped).';
@@ -86,8 +87,13 @@ export class CallCommand extends Command {
86
87
  ? `Cabs take bodies, not personas -- "jack out" first.`
87
88
  : `Cabs take bodies, not ghosts -- "return" first.`;
88
89
  }
89
- if (this.actor.inExchange) {
90
- return `Mid-fight is no time to hail a cab.`;
90
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED -- a cab
91
+ // hailed mid-firefight from the web client, with Initiative rolled
92
+ // and a Halloweener swinging). See inLiveCombat: `inExchange`
93
+ // alone let this through for every beat of a Combat Turn that was
94
+ // not a trade of fire resolving right that instant.
95
+ if (inLiveCombat(this.scene, this.actor)) {
96
+ return `Mid-fight is no time to hail a cab. Finish this, then call.`;
91
97
  }
92
98
  // THE FARE COMES FIRST (playtest 2026-08-26: "I was able to call a
93
99
  // taxi before calling Mr. Krow. I shouldn't be able to until I get
@@ -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
@@ -1,6 +1,6 @@
1
1
  import { BypassCommand } from './bypass-command.js';
2
2
  import { gridActionModifier, gridNote } from '../utilities/grids.js';
3
- import { resolveHostInReach, hostsInReach, hostOver, gridVicinity, roomsInReach, noiseNote } from '../utilities/grid-reach.js';
3
+ import { resolveHostInReach, hostsInReach, hostOver, gridVicinity, roomsInReach, noiseNote, namesHostExactly } from '../utilities/grid-reach.js';
4
4
  import { accrueOverwatch } from '../utilities/overwatch.js';
5
5
  import { hint } from '../utilities/hints.js';
6
6
  import { dropActiveCall } from '../utilities/comm-style.js';
@@ -86,7 +86,20 @@ export class HackCommand extends BypassCommand {
86
86
  // working deck in hand (which is also how enemy deckers get YOU).
87
87
  // Tried first so a name never falls through to the barrier-bypass
88
88
  // path; misses fall through untouched.
89
- if (rest && rest.length > 0) {
89
+ // ...UNLESS THE PLAYER NAMED A HOST OUTRIGHT (WTLfRHxJEnPmN2ht9:
90
+ // "this keeps matching on Torque's PAN even though I gave it exactly
91
+ // the host name"). tryPanHack resolves with fuzzyPickName over every
92
+ // body in reach, whose tier 2 is substring containment in EITHER
93
+ // direction -- so any runner whose name is contained in the host's
94
+ // (or contains a word of it) swallowed the query before the host
95
+ // pass below ever ran, and no spelling of the host's name could get
96
+ // past them. An exact alias hit settles it here instead; anything
97
+ // less falls through and the old order stands, so a genuinely
98
+ // ambiguous word still reaches the PAN it always did.
99
+ const namedHost = rest.length > 0
100
+ ? namesHostExactly(this.rooms ?? this.scene.getRooms(), this.actor, rest.join(' '))
101
+ : undefined;
102
+ if (rest && rest.length > 0 && !namedHost) {
90
103
  const panResult = await this.tryPanHack(rest, mode);
91
104
  if (panResult)
92
105
  return panResult;
@@ -145,9 +158,34 @@ export class HackCommand extends BypassCommand {
145
158
  // before allowing the crack would leave an unmarked decker with no
146
159
  // legal move against a host at all. That is a playability ruling
147
160
  // this engine owns, not something SR5 says.
148
- if (room.hostCracked) {
149
- return `This host is already cracked open -- its security hangs in shreds. Whatever it guarded is yours to take.`;
150
- }
161
+ // A CRACKED HOST IS NOT A HOST YOU STILL HOLD MARKS ON
162
+ // (spcebZTKN24GYRHXW). What stood here was a blanket refusal --
163
+ // "already cracked open, whatever it guarded is yours to take" --
164
+ // keyed on the room-global crack flag and nothing else. That is
165
+ // wrong twice over.
166
+ //
167
+ // Canon first: marks are per-persona and "only last a single
168
+ // Matrix session ... deleted when you reboot" (p.236, Recognition
169
+ // Keys; p.242, Reboot Device erases them in both directions). A
170
+ // decker who takes a host to three marks and then reboots holds
171
+ // NOTHING on it -- "if they want access again, they must re-place
172
+ // the marks" via Brute Force or Hack on the Fly. The host is never
173
+ // permanently keyed to them.
174
+ //
175
+ // Playability second, and this is what the report actually hit: a
176
+ // reboot mid-run left the persona on 0 marks with the flag still
177
+ // up, so `mark`/`hack` refused, and every mark-gated device action
178
+ // on that node then refused for the opposite reason ("takes orders
179
+ // from 2 marks and you hold 0", Control Device p.238). No verb in
180
+ // the game could hand the marks back. The node was dead to that
181
+ // runner for the rest of the run.
182
+ //
183
+ // The crack's WORLD consequences stay permanent and stay here --
184
+ // maglocks released, vault unsealed, archive listed. Those are
185
+ // facts about the site, not about whose keys are in the lattice.
186
+ // Only the standing is per-persona, and the guard for that is the
187
+ // MAX_MARKS check below, which already says the right thing.
188
+ const alreadyCracked = room.hostCracked;
151
189
  // What the host masters, through the room it stands over (models/host.ts).
152
190
  const lockedDoors = room.host.maglocks();
153
191
  // MARKS ARE THE CURRENCY, AND THIS IS AN OPPOSED TEST NOW
@@ -279,7 +317,7 @@ export class HackCommand extends BypassCommand {
279
317
  if (marksNow >= 1)
280
318
  lines.push(` One mark is the door: "enter" walks your persona inside (p.239).`);
281
319
  if (perception > 0)
282
- lines.push(` {lightblue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/lightblue-fg}`);
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}`);
283
321
  if (forcedDv > 0)
284
322
  lines.push(` The forcing burns ${forcedDv} DV into the architecture -- a host has no boards to brick, so it only announces you louder.`);
285
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.)`)}`);
@@ -314,7 +352,14 @@ export class HackCommand extends BypassCommand {
314
352
  // its feeds over. Computed AFTER hostCracked is set, so this is
315
353
  // the state the player is now standing in, not the one they left.
316
354
  const feedsNow = canSnoopFeeds(this.rooms ?? {}, room, this.actor);
317
- const lines = [`You're in. The host's sculpted security folds back layer by layer --`];
355
+ const lines = [alreadyCracked
356
+ // RE-KEYING A NODE THAT IS ALREADY IN SHREDS. Everything below
357
+ // still runs -- a door the last crack released is already
358
+ // unlocked, a file already spilled is already listed -- so the
359
+ // only thing that changes is the sentence that would otherwise
360
+ // claim you just broke in somewhere you had broken before.
361
+ ? `You're back in. The wreckage of the last break-in is still hanging where you left it, and your keys are in the lattice again --`
362
+ : `You're in. The host's sculpted security folds back layer by layer --`];
318
363
  // WHICH DOORS ARE ICONS, AND WHICH ARE JUST THE HOST'S OWN WALLS.
319
364
  //
320
365
  // A door held by a DEVICE has that device's back-reference on it
@@ -343,13 +388,13 @@ export class HackCommand extends BypassCommand {
343
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.)`)}`);
344
389
  }
345
390
  if (vaultedFiles.length > 0) {
346
- lines.push(` {lightblue-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/lightblue-fg}`);
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}`);
347
392
  }
348
393
  // AND WHAT THE CRACK DID NOT REACH: a wired site's data sits on an
349
394
  // air-gapped box the host never held (p.233) -- say so, or the next
350
395
  // "take" reads as a contradiction.
351
396
  if (room.offlineServer && room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
352
- lines.push(` {lightblue-fg}Nothing on the grid side of the vault -- whatever this place keeps sits on the ${room.offlineServer}, an air-gapped box with no Matrix presence. Your BODY has to be in ${room.name} to cable in (p.233).{/lightblue-fg}`);
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}`);
353
398
  }
354
399
  // THE FEEDS ARE A REAL GAIN AND WERE NEVER MENTIONED
355
400
  // (BpR5cjhYuvgCyDSZc, 7aNWnDSHeZMYmS2De: "what have I gained?").
@@ -365,9 +410,13 @@ export class HackCommand extends BypassCommand {
365
410
  if (lockedDoors.length === 0 && vaultedFiles.length === 0 && !feedsNow) {
366
411
  lines.push(` This node guarded nothing -- no maglocks, no vault, no eyes.`);
367
412
  }
368
- // The crack PERSISTS, which nothing told the player either: they
369
- // could not know whether they had bought a moment or a state.
370
- lines.push(` The host stays open from here -- no cracking it twice.`);
413
+ // WHAT PERSISTS AND WHAT DOES NOT, said in one breath because the
414
+ // old line here said only the first half ("the host stays open
415
+ // from here -- no cracking it twice") and a player who then
416
+ // rebooted had been told, in as many words, that they would not
417
+ // need to do this again. They did. See the alreadyCracked comment
418
+ // above: the damage is permanent, the keys are not (p.236).
419
+ lines.push(` The damage stays done -- the vault does not re-seal behind you. Your MARKS do not last: reboot or jack out and the lattice forgets your keys (p.236), and you key back in from scratch.`);
371
420
  if (mode === 'attack') {
372
421
  // Brute-force success is ANNOUNCED (canon: the target knows it's
373
422
  // under attack and screams to its owner -- it just can't see
@@ -634,7 +683,7 @@ export class HackCommand extends BypassCommand {
634
683
  else {
635
684
  const perception = freeMatrixPerceptionHits(netHits);
636
685
  if (perception > 0)
637
- 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}`);
638
687
  }
639
688
  // TWO MARKS COMMAND IT (p.238 Control Device: 1 mark for a Free
640
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');
@@ -2,6 +2,7 @@ import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { fuzzyPickName } from '../utilities/fuzzy-match.js';
4
4
  import { catalogMetamagics } from '../utilities/catalog.js';
5
+ import { inLiveCombat } from '../utilities/action-cost.js';
5
6
  // THE PICKS ARE CATALOG ROWS (kind 'metamagic' on maka-cli.com), read
6
7
  // through catalogMetamagics(); the shape is ../metamagics.ts. The four
7
8
  // metamagics whose rule is not expressible as dice (overclocking, the
@@ -15,7 +16,11 @@ export class InitiateCommand extends Command {
15
16
  static description = 'Initiation/submersion at your hideout (10 + grade x3 karma): each grade lifts Magic/Resonance past 6 and grants a metamagic (or echo). "initiate" lists; "initiate <pick>" pays.';
16
17
  async execute(args = []) {
17
18
  const actor = this.actor;
18
- if (actor.inExchange) {
19
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
20
+ // inLiveCombat: `inExchange` is a re-entrancy lock that is false for
21
+ // most of a Combat Turn, so this refusal said "firefight" and meant
22
+ // "for the half-second one trade of fire is resolving".
23
+ if (inLiveCombat(this.scene, actor)) {
19
24
  return `Transcendence waits for the shooting to stop.`;
20
25
  }
21
26
  if (actor.currentLocation?.roomType !== 'Residence') {
@@ -4,6 +4,7 @@ import { WARE_GRADES, getGrade, gradedAug, STANDARD_GRADE, formatEssence, } from
4
4
  import { catalogAugmentations, findAug } from '../utilities/catalog.js';
5
5
  import { venueSinCheck, venueBustResponse } from '../utilities/sin.js';
6
6
  import { Category } from '../types/shared/item-enum.js';
7
+ import { inLiveCombat } from '../utilities/action-cost.js';
7
8
  /**
8
9
  * The chrome shop: "install" at a street clinic lists the augmentation
9
10
  * catalog against your essence and nuyen; "install <name>" pays, goes
@@ -21,7 +22,11 @@ export class InstallCommand extends Command {
21
22
  if (actor.plane !== 'meat') {
22
23
  return `Chrome goes into meat. Come back to your body first.`;
23
24
  }
24
- if (actor.inExchange) {
25
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
26
+ // inLiveCombat: `inExchange` is a re-entrancy lock that is false for
27
+ // most of a Combat Turn, so this refusal said "firefight" and meant
28
+ // "for the half-second one trade of fire is resolving".
29
+ if (inLiveCombat(this.scene, actor)) {
25
30
  return `Nobody operates in a firefight.`;
26
31
  }
27
32
  if (!InstallCommand.isClinic(actor.currentLocation)) {
@@ -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
@@ -2,6 +2,7 @@ import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { catalogQualities } from '../utilities/catalog.js';
4
4
  import { fuzzyPickName } from '../utilities/fuzzy-match.js';
5
+ import { inLiveCombat } from '../utilities/action-cost.js';
5
6
  /** The qualities on sale: every row that is not retired (a retired one
6
7
  * is dormant on its holder's sheet, never on the board). */
7
8
  const onSale = () => catalogQualities().filter(q => !q.retired);
@@ -22,7 +23,11 @@ export class QualityTradeCommand extends Command {
22
23
  static description = 'Trade qualities with karma at your hideout (SR5e x2 rates): "qualities" lists, "qualities <name>" buys a positive or buys off a negative; "qualities <name> <rating>" buys or raises a rated one.';
23
24
  async execute(args = []) {
24
25
  const actor = this.actor;
25
- if (actor.inExchange) {
26
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
27
+ // inLiveCombat: `inExchange` is a re-entrancy lock that is false for
28
+ // most of a Combat Turn, so this refusal said "firefight" and meant
29
+ // "for the half-second one trade of fire is resolving".
30
+ if (inLiveCombat(this.scene, actor)) {
26
31
  return `Reinventing yourself mid-firefight is called dying.`;
27
32
  }
28
33
  if (actor.currentLocation?.roomType !== 'Residence') {
@@ -1,6 +1,7 @@
1
1
  import { Command } from './command.js';
2
2
  import { rollPool, formatRoll } from '../utilities/dice.js';
3
3
  import { hint } from '../utilities/hints.js';
4
+ import { inLiveCombat } from '../utilities/action-cost.js';
4
5
  /**
5
6
  * Registering (SR5e p.254-256, engine-adapted): make a compiled sprite
6
7
  * PERMANENT. An opposed Resonance + REGISTERING test against TWICE the
@@ -45,7 +46,11 @@ export class RegisterCommand extends Command {
45
46
  }
46
47
  return `No sprite rides with you -- "compile" one first.`;
47
48
  }
48
- if (actor.inExchange) {
49
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
50
+ // inLiveCombat: `inExchange` is a re-entrancy lock that is false for
51
+ // most of a Combat Turn, so this refusal said "firefight" and meant
52
+ // "for the half-second one trade of fire is resolving".
53
+ if (inLiveCombat(this.scene, actor)) {
49
54
  return `Registering takes more focus than the fight will give you.`;
50
55
  }
51
56
  // THE LOGIC CEILING (p.254-256): "You can register a number of
@@ -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);
@@ -2,6 +2,7 @@ import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { priceOf, fenceValueOf, isSellable, isFence } from '../utilities/commerce.js';
4
4
  import { sameSpot, spotOf, spotRefusal } from '../utilities/spots.js';
5
+ import { inLiveCombat } from '../utilities/action-cost.js';
5
6
  /**
6
7
  * Fencing loot: "sell <item>" in front of a fence (Whisper's pawnshop, or
7
8
  * any generated NPC/room in the trade -- see commerce.isFence) quotes 40%
@@ -18,7 +19,11 @@ export class SellCommand extends Command {
18
19
  if (actor.plane !== 'meat') {
19
20
  return `The fence deals in things a hand can pass over a counter. Come back to your body.`;
20
21
  }
21
- if (actor.inExchange) {
22
+ // THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
23
+ // inLiveCombat: `inExchange` is a re-entrancy lock that is false for
24
+ // most of a Combat Turn, so this refusal said "firefight" and meant
25
+ // "for the half-second one trade of fire is resolving".
26
+ if (inLiveCombat(this.scene, actor)) {
22
27
  return `Mid-firefight is a seller's market for exactly nothing.`;
23
28
  }
24
29
  const fence = this.scene.getActorsInRoom(actor.currentLocation)