@maka/maka-cli 5.206.0 → 5.208.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 (28) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/complete.js +125 -0
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +14 -14
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/force.js +40 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +61 -54
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +2 -2
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +49 -39
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +14 -4
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +98 -17
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +16 -5
  13. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +62 -1
  14. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +34 -4
  15. package/bundle/typescript/src/commands/game/sideQuest/game.js +49 -35
  16. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +3 -55
  17. package/bundle/typescript/src/commands/game/sideQuest/types/repro.js +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/ui.js +32 -30
  19. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +20 -8
  20. package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +2 -3
  21. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +50 -17
  22. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +56 -9
  23. package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +3 -2
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/nameables.js +107 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +3 -7
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
  27. package/package.json +1 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +0 -121
@@ -7,6 +7,7 @@ import { Logger } from './utilities/logger.js';
7
7
  import { getArchetype, outfitArchetype } from './archetypes.js';
8
8
  import { attachLineEditor, attachArrowMovement } from './utilities/line-editor.js';
9
9
  import { attachInputHistory } from './utilities/input-history.js';
10
+ import { COMPLETE_MARKER } from './commands/complete.js';
10
11
  import { TERMINAL_RESET_SEQUENCE } from './utilities/terminal-reset.js';
11
12
  import { readSave } from './utilities/persistence.js';
12
13
  import { requireCatalog, CatalogUnavailableError } from './utilities/catalog.js';
@@ -322,10 +323,11 @@ export async function main(playerName, sceneSeed, options) {
322
323
  game.updateInventory(game.player.inventory, capitalCase(game.scene.getCurrencyType()));
323
324
  game.updateExits(game.player.currentLocation);
324
325
  game.updateStatus();
325
- // Tab flips the stance: lethal <-> non-lethal in the meat, Brute
326
- // Force <-> Hack on the Fly on the Matrix (models/player.ts; "stance"
327
- // is the typed twin). Bound on the input box because that's where
328
- // focus lives during play.
326
+ // TAB COMPLETES THE NAME YOU ARE TYPING (f79LLqTerep4nKWcA). It used
327
+ // to flip the stance; the stance is gone, and what the same report
328
+ // asked for in its place is a way to know which icon you are about
329
+ // to hit. Bound on the input box because that's where focus lives
330
+ // during play.
329
331
  game.inputBox.key(['tab'], () => {
330
332
  // Blessed's textarea reader appends the literal '\t' to the box
331
333
  // value on this same keypress (listener order isn't guaranteed
@@ -339,36 +341,36 @@ export async function main(playerName, sceneSeed, options) {
339
341
  };
340
342
  stripTab();
341
343
  setImmediate(() => { stripTab(); screen.render(); });
342
- // ONE ROUTE, BOTH PLANES, BOTH RUN SHAPES. This used to fork: a
343
- // shared run sent "stance next" to the server while a local run
344
- // mutated game.player.autoStance here by hand. That duplicate is
345
- // what made Tab plane-blind -- the Matrix stance
346
- // (uMQAhaAysgaKpWFkn, 2026-09-03) would have had to be added
347
- // twice, and the second copy is the one that gets forgotten.
344
+ // ONE ROUTE, BOTH PLANES, BOTH RUN SHAPES -- and the reason the
345
+ // completion is answered by the ENGINE rather than built here.
348
346
  //
349
- // SHARED RUN: THE STANCE LIVES ON THE SERVER (playtest
350
- // 2026-08-27: "the combat stance isn't updating in the menu
351
- // bar"). game.player here is the LOCAL HUB player -- in a shared
352
- // run the runner on the table is a different object in a
353
- // different process, so cycling this one moved nothing the
354
- // engine reads and nothing the HUD paints (updateStatus
355
- // early-returns on remoteSession; the status box belongs to the
356
- // server's events). Send the typed twin instead and let the
357
- // engine cycle from the ACTOR's real stance.
347
+ // Locally this process holds the whole scene and could list the
348
+ // names itself. In a SHARED RUN it holds nothing: game.player is
349
+ // the LOCAL HUB player, the runner on the table is a different
350
+ // object in a different process, and the room arrives as rendered
351
+ // text with the structured payload dropped (utilities/shared-run.ts).
352
+ // A completer built on what is in memory here would work solo and
353
+ // silently do nothing on the grid with three other people -- which
354
+ // is the same duplicate-path trap the old stance binding fell into.
355
+ // So: send the box, let the engine answer, on both run shapes.
358
356
  void (async () => {
359
- const res = await game.handleInput('stance next');
357
+ const box = game.inputBox.getValue().replace(/\t/g, '');
358
+ const res = await game.handleInput(`complete ${box}`);
360
359
  if (!res)
361
360
  return;
362
- // Red for the lethal routes, yellow for the careful ones (same
363
- // palette as the HUD stance in game.ts): Brute Force is the loud
364
- // one, Hack on the Fly the careful one. Never gray -- ANSI
365
- // bright-black is illegible on stock dark palettes.
366
- const named = res.match(/(?:Matrix s|S)tance:\s*([\w-]+)/i)?.[1]?.toLowerCase();
367
- const c = named === 'lethal' || named === 'brute' ? 'red'
368
- : named === 'non-lethal' || named === 'non' || named === 'hack' ? 'yellow'
369
- : 'white';
370
- gameLog.meta(`⚔ ${res.split('\n')[0]}`, c);
371
- game.updateStatus();
361
+ const lines = res.split('\n');
362
+ // THE MARKER LINE refills the box (see commands/complete.ts).
363
+ // Everything else is the candidate list, and goes to the ticker
364
+ // rather than the transcript: Tab is not a turn.
365
+ if (lines[0]?.startsWith(COMPLETE_MARKER)) {
366
+ const filled = lines[0].slice(COMPLETE_MARKER.length);
367
+ game.inputBox.setValue(filled);
368
+ lines.shift();
369
+ }
370
+ for (const l of lines) {
371
+ if (l.trim().length > 0)
372
+ gameLog.meta(l, 'white');
373
+ }
372
374
  screen.render();
373
375
  })();
374
376
  });
@@ -318,8 +318,13 @@ export class CombatExchange {
318
318
  * involuntary strikes (retaliation) where refusing to swing at all would
319
319
  * make no sense; a deliberately INITIATED attack with a dry gun is
320
320
  * blocked with a reload prompt before ever reaching here (attack.ts).
321
+ *
322
+ * `opts.nonLethal` is the verb's INTENT to take the target alive -- set
323
+ * by `subdue` and by nothing else. Retaliation never passes it, which
324
+ * is the honest reading of p.236's cousin in the meat: someone you are
325
+ * choking out is not obliged to pull their own punch.
321
326
  */
322
- weaponProfile(attacker, defender) {
327
+ weaponProfile(attacker, defender, opts = {}) {
323
328
  const weapon = attacker.getWeaponProfile();
324
329
  // The physical weapon only backs a MEAT-plane strike by someone who
325
330
  // has it DRAWN (see Player.weaponDrawn) -- a holstered gun never
@@ -380,16 +385,24 @@ export class CombatExchange {
380
385
  // - electricity is Stun: taser, stun baton, shock gloves (p.170-171, p.424);
381
386
  // - stun ammunition: gel rounds Stun at AP +1, stick-n-shock Stun at
382
387
  // DV -2 / AP -5 (p.434) -- whatever the magazine was loaded with;
383
- // - NON-LETHAL BY STANCE: a lethal melee weapon swung as a club deals
388
+ // - NON-LETHAL BY INTENT: a lethal melee weapon swung as a club deals
384
389
  // Stun at Accuracy 3 and no Reach (p.186 "Changing Damage Types").
385
- // A lethal gun with lethal rounds has no such door: it stays
386
- // Physical and the label says so, so the stance never lies.
390
+ // A lethal gun with lethal rounds has no such door.
391
+ //
392
+ // THE INTENT COMES FROM THE VERB NOW (f79LLqTerep4nKWcA). It used to
393
+ // come from `attacker.autoStance`, a standing posture set by Tab --
394
+ // so whether your blade cut or clubbed depended on a setting made
395
+ // some turns ago, and the only way to find out which was to read the
396
+ // label after the swing. `attack` is the lethal verb and `subdue` is
397
+ // the one that takes them alive; each says so when it builds the
398
+ // strike, and `lethalAnyway` no longer needs to exist, because
399
+ // subdue refuses a lethal gun outright rather than firing it and
400
+ // tagging the corpse (commands/subdue.ts).
387
401
  const stunWeapon = !!weaponItem?.isStunWeapon();
388
402
  const ammoMods = weaponItem?.isFirearm() ? weaponItem.loadedAmmoMods() : undefined;
389
403
  const stunAmmo = weaponItem?.isFirearm() ? weaponItem.stunAmmo() : undefined;
390
- const nonLethal = attacker.autoStance === 'non-lethal' && attacker.plane === 'meat';
404
+ const nonLethal = opts.nonLethal === true && attacker.plane === 'meat';
391
405
  const flatOfTheBlade = nonLethal && !!weaponItem && weaponItem.category === Category.MeleeWeapon && !stunWeapon;
392
- const lethalAnyway = nonLethal && !!weaponItem?.isFirearm() && !stunWeapon && !stunAmmo;
393
406
  const stun = stunWeapon || !!stunAmmo || flatOfTheBlade || !!fistsAreStun;
394
407
  const ap = (weapon.armorPen ?? 0) + (ammoMods?.apMod ?? 0);
395
408
  const meatDefender = defender.plane === 'meat';
@@ -401,8 +414,7 @@ export class CombatExchange {
401
414
  const roundTag = ammoMods && !stunAmmo && (ammoMods.dvMod !== 0 || ammoMods.apMod !== 0) ? `, ${ammoMods.kind}` : '';
402
415
  const tag = flatOfTheBlade ? ' (flat of the blade, STUN)'
403
416
  : stunAmmo ? ` (${stunAmmo.kind}, STUN)`
404
- : stunWeapon ? ' (STUN)'
405
- : lethalAnyway ? ' (LETHAL -- no stun rounds loaded)' : '';
417
+ : stunWeapon ? ' (STUN)' : '';
406
418
  return {
407
419
  label: `with ${weapon.name}${ap !== 0 ? ` (AP ${ap > 0 ? '+' : ''}${ap}${roundTag})` : roundTag ? ` (${roundTag.slice(2)})` : ''}${tag}`,
408
420
  pool: attacker.getAttackPool() + edgeBoost,
@@ -143,7 +143,6 @@ export function buildConditionReport(player, deps) {
143
143
  ? { grid: player.currentGrid?.name, host: player.hostInside?.name }
144
144
  : undefined,
145
145
  simMode: player.simMode,
146
- stance: player.autoStance,
147
146
  drawn: player.weaponDrawn,
148
147
  weapon: player.weaponDrawn ? player.getCarriedWeapon()?.name : undefined,
149
148
  sustained: player.sustainedNames(),
@@ -348,8 +347,8 @@ extra) {
348
347
  // sheet's "where" stays as fresh as its monitors). Read at FIRE
349
348
  // time, not schedule time -- the coalescing beat means the player
350
349
  // may have moved since the mutation that armed it.
351
- // The full state installment (contract 2026-09-01): plane/sim/
352
- // stance and brandish flow through their accessors' own reports;
350
+ // The full state installment (contract 2026-09-01): plane/sim mode
351
+ // and brandish flow through their accessors' own reports;
353
352
  // sustained and deck programs are composed here and ride whatever
354
353
  // beat fires next. All read at FIRE time, like position.
355
354
  const report = buildConditionReport(player, { saveSlug, arSightUp, isHostile: extra?.isHostile, isParty: extra?.isParty, phaseActor: extra?.phaseActor });
@@ -172,23 +172,50 @@ export function noiseNote(noise) {
172
172
  export function hostLine(room, actor) {
173
173
  const inside = actor.insideHost === room;
174
174
  const marks = room.hostMarksBy.get(actor.name) ?? 0;
175
- // STANDING IS TWO FACTS, NOT ONE (spcebZTKN24GYRHXW: "it's saying
176
- // I've already cracked open (hold 3 marks) but I don't").
175
+ // SAY IT IN WORDS (f79LLqTerep4nKWcA, Maka: "'sealed, walls UP, over
176
+ // you' means nothing, it's just jargon. Remove it, when I have a mark
177
+ // on it, show my mark on it, and vice versa -- when it has a mark on
178
+ // me, show that.").
179
+ //
180
+ // This row was three tokens of shorthand welded together, and the
181
+ // first of them was not merely opaque, it was WRONG: `standing` read
182
+ // only the marks YOU hold, so a host holding three marks on your
183
+ // persona -- owning you outright -- printed "sealed", the word for a
184
+ // node nobody has touched. The two ledgers were always separate in
185
+ // the model (Host.marksBy / Host.marksOn); only the renderer collapsed
186
+ // them.
177
187
  //
178
- // "cracked open" is the SITE's state -- somebody took this node to
179
- // three marks and its vault has been hanging open ever since. The
180
- // marks count is YOUR state, and canon wipes it on every reboot
181
- // (p.236). The two used to be one string with the crack winning, so a
182
- // rebooted persona holding nothing read a line that says the place is
183
- // yours -- while every device on it refused them for want of marks.
184
- // Print both whenever they disagree.
185
- const held = `${marks}/${MAX_MARKS} marks`;
186
- const standing = room.hostSanctioned ? 'haven'
187
- : room.hostCracked ? (marks >= MAX_MARKS ? 'cracked open' : `cracked open, but you hold ${held}`)
188
- : marks > 0 ? held
189
- : 'sealed';
190
- const alert = room.hostAlert && !room.hostSanctioned ? ', walls UP' : '';
191
- const over = hostOver(gridVicinity(actor)) === room ? ', over you' : '';
188
+ // Four facts, four plain clauses, and never more than the row has
189
+ // earned:
190
+ // - what YOU hold on it, counted;
191
+ // - what IT holds on you, counted -- the number that decides whether
192
+ // its ice can come find you;
193
+ // - whether it has been cracked, or is a haven you belong in;
194
+ // - whether it knows someone is in (Host.alert) or can see YOU
195
+ // specifically (Host.spotted) -- two different facts that
196
+ // "walls UP" flattened into one (p.236).
197
+ //
198
+ // STANDING IS TWO FACTS, NOT ONE (spcebZTKN24GYRHXW: "it's saying
199
+ // I've already cracked open (hold 3 marks) but I don't"). "Cracked
200
+ // open" is the SITE's state -- somebody took this node to three marks
201
+ // and its vault has hung open since. The marks count is YOUR state,
202
+ // and canon wipes it on every reboot (p.236). Print both whenever they
203
+ // disagree.
204
+ const onYou = room.hostMarksOn.get(actor.name) ?? 0;
205
+ const yours = marks > 0 ? `you hold ${marks} of ${MAX_MARKS} marks` : 'you hold no marks';
206
+ const theirs = onYou > 0
207
+ ? `it holds ${onYou} on YOU`
208
+ : undefined;
209
+ const standing = room.hostSanctioned ? 'a haven -- you belong here'
210
+ : room.hostCracked ? (marks >= MAX_MARKS ? `cracked open, and ${yours}` : `cracked open, but ${yours}`)
211
+ : yours;
212
+ // Seen beats merely alerted: if it can pick your icon out it does not
213
+ // need to sweep for you.
214
+ const eyes = room.hostSanctioned ? ''
215
+ : room.host?.spotted.has(actor.name) ? ', and it can SEE you'
216
+ : room.hostAlert ? ', and it knows someone is on the grid' : '';
217
+ const alert = theirs ? `, ${theirs}${eyes}` : eyes;
218
+ const over = hostOver(gridVicinity(actor)) === room ? ', directly overhead' : '';
192
219
  const onGrid = room.host?.grid ? ` on ${room.host.grid.name}` : '';
193
220
  // A haven shows no rating and no eyes (jackpoint.ts): it is not a
194
221
  // thing you crack, so the numbers you would crack it with stay off.
@@ -216,7 +243,13 @@ export function hostDockLine(room, actor, width) {
216
243
  : room.hostCracked ? ` open ${marks}/${MAX_MARKS}`
217
244
  : marks > 0 ? ` ${marks}/${MAX_MARKS}`
218
245
  : '';
219
- const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}`;
246
+ // AND WHAT IT HOLDS ON YOU (f79LLqTerep4nKWcA). The dock had room for
247
+ // one ledger and showed the flattering one; a persona with a host's
248
+ // key riding it could read every panel in the game and never meet the
249
+ // number. Two glyphs: yours forward, theirs back.
250
+ const onYou = room.hostMarksOn.get(actor.name) ?? 0;
251
+ const onYouTag = onYou > 0 ? ` <${onYou}` : '';
252
+ const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}${onYouTag}`;
220
253
  const cut = [...raw].slice(0, Math.max(4, width)).join('');
221
254
  return `{light-blue-fg}${cut}{/light-blue-fg}`;
222
255
  }
@@ -16,6 +16,13 @@ import { isWatched, camerasLive, canSnoopFeeds } from './surveillance.js';
16
16
  * ▣ PAN, ▲ dangerous device, ◇ data. (There is no air-gapped-host glyph
17
17
  * any more -- see hhSWzLSXECFtfoAGa and canReachHost.)
18
18
  */
19
+ /** The heading gridIcons drops between a host's icon and the icons merely
20
+ * standing NEAR it. See the comment where it is spliced in. */
21
+ const NEAR_DIVIDER = '-- and NEAR it, out on the open grid (not inside the host): --';
22
+ /** A heading is not an icon: the distance bands must not name it. */
23
+ export function isNearDivider(line) {
24
+ return line === NEAR_DIVIDER;
25
+ }
19
26
  /**
20
27
  * A DEVICE ICON, AND WHAT WORKS IT (commands/disable.ts, 2026-09-13).
21
28
  * Control Device (p.238) needs 2 marks on the device -- "disable" -- and
@@ -89,14 +96,30 @@ export function gridIcons(scene, actor, room) {
89
96
  // is still unearned -- a host that has already told you its
90
97
  // business has nothing left to sell. Built as a list so a dropped
91
98
  // entry cannot leave a dangling separator.
99
+ // EVERY WAY NAMES THE ICON (f79LLqTerep4nKWcA: "How do I know which
100
+ // host/PAN/etc I'm hacking?"). These used to read `"hack" slips in
101
+ // on sleaze` / `"hack loud" smashes` / `"mark" softens it first` --
102
+ // a bare verb, a verb plus an adverb that looks like a target, and
103
+ // a third spelling of the first one. Now: one verb per book action,
104
+ // each shown with the name you would actually type.
105
+ const name = hostLabel(room);
92
106
  const ways = [
93
- ...(myMarks > 0 ? ['"enter" walks in on your mark'] : []),
94
- '"hack" slips in on sleaze',
95
- '"hack loud" smashes',
96
- '"mark" softens it first',
97
- ...(found ? [] : ['"search" reads what it runs (p.241)']),
107
+ ...(myMarks > 0 ? [`"enter" walks in on your mark`] : []),
108
+ `"hack ${name}" slips a mark in quiet (Hack on the Fly, p.240)`,
109
+ `"force ${name}" smashes one in (Brute Force, p.238)`,
110
+ ...(found ? [] : [`"search" reads what it runs (p.241)`]),
98
111
  ];
99
- icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- sculpted ice geometry, sealed. Whatever it holds is inside it.${room.hostAlert ? ' Walls UP -- it holds a mark on an intruder.' : ''}${myMarks > 0 ? ` [your marks: ${myMarks}/3]` : ''} (${ways.join('; ')})`);
112
+ // BOTH LEDGERS, PLAINLY. "Walls UP -- it holds a mark on an
113
+ // intruder" named no number and no one: the reader could not tell
114
+ // whether the intruder it meant was them.
115
+ const onYou = room.hostMarksOn.get(actor.name) ?? 0;
116
+ const standing = [
117
+ myMarks > 0 ? `you hold ${myMarks} of 3 marks on it` : undefined,
118
+ onYou > 0 ? `it holds ${onYou} on YOU` : undefined,
119
+ room.host?.spotted.has(actor.name) ? 'and it can see you'
120
+ : room.hostAlert ? 'and it knows someone is on the grid' : undefined,
121
+ ].filter(Boolean).join(', ');
122
+ icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- sculpted ice geometry, sealed. Whatever it holds is inside it.${standing.length > 0 ? ` ${standing.charAt(0).toUpperCase()}${standing.slice(1)}.` : ''} (${ways.join('; ')})`);
100
123
  }
101
124
  }
102
125
  // WHERE THE HOST'S OWN ICON ENDS AND THE STREET BEGINS.
@@ -262,12 +285,19 @@ export function gridIcons(scene, actor, room) {
262
285
  // is no wall for a reader to put an icon on the wrong side of, and a
263
286
  // heading over an empty list is noise.
264
287
  //
288
+ // IT IS A HEADING, NOT AN ICON, and gridIconBlock's distance bands have
289
+ // to know that: they take each entry, strip its glyph and cut at " -- "
290
+ // to get a name, which turned this sentence into an icon called "and
291
+ // NEAR it, out on the open grid (not inside the host):" listed beside
292
+ // the real ones. A constant so the one predicate that filters it out
293
+ // (isNearDivider, below) cannot drift from the string it matches.
294
+ //
265
295
  // It says NEAR, not IN, and it says why they are visible -- proximity
266
296
  // to your body, not a leak in the host (p.235). A reader who wondered
267
297
  // whether the host was showing them its insides now has the answer in
268
298
  // the list itself instead of having to file a report to get it.
269
299
  if (room.hasNode && icons.length > afterHost) {
270
- icons.splice(afterHost, 0, `-- and NEAR it, out on the open grid (not inside the host): --`);
300
+ icons.splice(afterHost, 0, NEAR_DIVIDER);
271
301
  }
272
302
  return icons;
273
303
  }
@@ -542,7 +572,7 @@ export function gridIconBlock(scene, actor, room, rooms) {
542
572
  for (const { room: r, noise } of roomsInReach(rooms, actor).slice(1)) {
543
573
  let icons = [];
544
574
  try {
545
- icons = gridIcons(scene, actor, r).filter(i => !isHostLine(i));
575
+ icons = gridIcons(scene, actor, r).filter(i => !isHostLine(i) && !isNearDivider(i));
546
576
  }
547
577
  catch {
548
578
  icons = [];
@@ -583,9 +613,26 @@ export function hostInteriorPanel(scene, actor, host, rows, cols) {
583
613
  if (purpose)
584
614
  lines.push(cut(purpose));
585
615
  const marks = host.marksBy.get(actor.name) ?? 0;
616
+ // BOTH DIRECTIONS IN THE DOCK TOO (f79LLqTerep4nKWcA). "marks 2/3" was
617
+ // only ever the marks YOU hold, and "WALLS UP" was the host's whole
618
+ // side of the ledger compressed to two words that named no number.
619
+ // The dock is narrow, so this is the terse spelling of the same four
620
+ // facts hostLine spells out: yours forward, theirs back, and what it
621
+ // can see.
622
+ const onYou = host.marksOn.get(actor.name) ?? 0;
586
623
  lines.push(cut(host.sanctioned
587
624
  ? 'haven -- welcome here'
588
- : `HR ${host.rating} · marks ${marks}/3${host.alert ? ' · WALLS UP' : ''}${host.cracked ? ' · cracked' : ''}`));
625
+ : `HR ${host.rating} · you ${marks}/3 · on you ${onYou}${host.cracked ? ' · cracked' : ''}`));
626
+ // ITS EYES GET THEIR OWN ROW rather than a third token on the one
627
+ // above: the dock is ~30 columns and the ledger already fills it, so a
628
+ // cut line would eat exactly the word that matters. Seen beats merely
629
+ // alerted -- an icon it can pick out does not need to sweep for you.
630
+ if (!host.sanctioned) {
631
+ if (host.spotted.has(actor.name))
632
+ lines.push(cut('it can SEE you'));
633
+ else if (host.alert)
634
+ lines.push(cut('it knows someone is in'));
635
+ }
589
636
  for (const kind of host.runningIC) {
590
637
  const ice = host.iceActors.get(kind);
591
638
  lines.push(cut(`▲ ${kind}${ice ? ` ${ice.damageTaken}/${ice.maxConditionBoxes}` : ''}`));
@@ -2,8 +2,9 @@ import { hostLabel } from './grid-names.js';
2
2
  import { Logger } from './logger.js';
3
3
  /**
4
4
  * MARKS -- MAtrix Recognition Keys (canon p.235-236, player go
5
- * 2026-08-24). Placing a mark is the "mark" command (commands/mark.ts,
6
- * same Attack/Sleaze fork and GOD bill as hack); this module is the
5
+ * 2026-08-24). Placing a mark is "hack" (Hack on the Fly, p.240) or
6
+ * "force" (Brute Force, p.238) -- never "mark", which since
7
+ * f79LLqTerep4nKWcA is the LEDGER and nothing else; this module is the
7
8
  * BOOKKEEPING: capped at 3 per icon, keyed by placer, and wiped by the
8
9
  * canon events -- the PLACER's reboot/jack-out/convergence erases all
9
10
  * marks they placed anywhere (a fresh persona has no keys), and a
@@ -0,0 +1,107 @@
1
+ import { hostLabel } from './grid-names.js';
2
+ import { gridVicinity, hostsInReach, roomsInReach } from './grid-reach.js';
3
+ import { cybercombatCandidates } from './matrix-roster.js';
4
+ import { visibleActorsIn } from './spots.js';
5
+ function push(out, seen, n) {
6
+ const key = n.name.toLowerCase();
7
+ if (n.name.trim().length === 0 || seen.has(key))
8
+ return;
9
+ seen.add(key);
10
+ out.push(n);
11
+ }
12
+ /** Every icon a persona could name from where it stands. */
13
+ function matrixNameables(scene, actor, rooms) {
14
+ const out = [];
15
+ const seen = new Set();
16
+ // HOSTS have no distance on the grid (p.246) -- every one in reach is
17
+ // nameable from anywhere, which is precisely why naming one matters.
18
+ for (const room of hostsInReach(rooms, actor)) {
19
+ push(out, seen, { glyph: '●', name: hostLabel(room), what: `host, rating ${room.hostRating}` });
20
+ }
21
+ // PERSONAS AND CONSTRUCTS on your side of the host wall -- the same
22
+ // set cybercombat resolves against, so what Tab offers is what
23
+ // `attack` will find.
24
+ for (const a of cybercombatCandidates(scene, actor)) {
25
+ if (a === actor)
26
+ continue;
27
+ push(out, seen, { glyph: '◆', name: a.name, what: 'persona' });
28
+ }
29
+ // PANs AND DEVICES. A body in reach carries a PAN you can crack by its
30
+ // owner's name (hack.ts tryPanHack resolves it exactly that way), and
31
+ // every slaved or loose device answers to its own name.
32
+ for (const { room } of roomsInReach(rooms, actor)) {
33
+ for (const body of room.getActors()) {
34
+ if (body === actor)
35
+ continue;
36
+ push(out, seen, { glyph: '▣', name: body.name, what: 'a body in reach -- its PAN' });
37
+ }
38
+ for (const device of room.openableDevices()) {
39
+ push(out, seen, { glyph: '▤', name: device.name, what: `device, rating ${device.rating}` });
40
+ }
41
+ }
42
+ // THE ARCHIVE, once a Matrix Search has listed it (Host.searchedBy) or
43
+ // the node is cracked -- the same gate every other renderer asks.
44
+ const inside = actor.insideHost;
45
+ const host = inside?.host;
46
+ if (host && (host.cracked || host.searchedBy.has(actor.name))) {
47
+ for (const file of host.files()) {
48
+ push(out, seen, { glyph: '◇', name: file.name, what: 'file' });
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ /** Everything a body could name in the room it is standing in. */
54
+ function meatNameables(actor) {
55
+ const out = [];
56
+ const seen = new Set();
57
+ const room = actor.currentLocation;
58
+ if (!room)
59
+ return out;
60
+ for (const a of visibleActorsIn(room, actor)) {
61
+ if (a === actor)
62
+ continue;
63
+ push(out, seen, { glyph: '◆', name: a.name, what: 'someone here' });
64
+ }
65
+ for (const device of room.openableDevices()) {
66
+ push(out, seen, { glyph: '▤', name: device.name, what: 'a device' });
67
+ }
68
+ for (const item of room.getItemsForActor(actor)) {
69
+ push(out, seen, { glyph: '◇', name: item.name, what: 'on the ground' });
70
+ }
71
+ // WHAT YOU ARE CARRYING is nameable too -- "equip", "drop", "reload
72
+ // <box>" and "use" all read the pack, and a long ammunition name is
73
+ // exactly the sort of thing nobody should have to spell.
74
+ for (const item of actor.inventory.getAllItems()) {
75
+ push(out, seen, { glyph: '▪', name: item.name, what: 'carried' });
76
+ }
77
+ // The ways out, by the name "go" takes.
78
+ for (const dir of Object.keys(room.exits ?? {})) {
79
+ push(out, seen, { glyph: '→', name: dir, what: 'a way out' });
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * The nameable set for whichever plane this actor is standing on.
85
+ * Astral and rigged frames fall back to the meat room: an astral form
86
+ * still names the people and the ward in front of it.
87
+ */
88
+ export function nameablesFor(scene, actor, rooms) {
89
+ if (actor.plane === 'matrix') {
90
+ return matrixNameables(scene, actor, rooms ?? scene.getRooms());
91
+ }
92
+ return meatNameables(actor);
93
+ }
94
+ /** The roster a refusal prints: just the names, ready to join. */
95
+ export function nameableNames(scene, actor, rooms) {
96
+ return nameablesFor(scene, actor, rooms).map(n => n.name);
97
+ }
98
+ /** Where the persona is, for a "nothing here" line that says where here is. */
99
+ export function whereAmI(actor) {
100
+ if (actor.plane !== 'matrix')
101
+ return actor.currentLocation?.name ?? 'nowhere';
102
+ const inside = actor.insideHost;
103
+ if (inside)
104
+ return hostLabel(inside);
105
+ return actor.currentGrid?.name ?? gridVicinity(actor).name;
106
+ }
107
+ //# sourceMappingURL=nameables.js.map
@@ -1,5 +1,4 @@
1
1
  import * as fs from 'fs';
2
- import { normalizeMatrixStance, normalizeStance } from '../models/player.js';
3
2
  import { resolveSpot } from './spots.js';
4
3
  import { isAspectKey } from '../aspects.js';
5
4
  import * as path from 'path';
@@ -374,8 +373,6 @@ export function serializePlayer(p) {
374
373
  hungerStreak: p.hungerStreak, sleepStreak: p.sleepStreak,
375
374
  hungerFatigue: p.hungerFatigue, sleepFatigue: p.sleepFatigue,
376
375
  },
377
- autoStance: p.autoStance,
378
- matrixStance: p.matrixStance,
379
376
  inventory: p.inventory.getAllItems().map(serializeItem),
380
377
  equipment: serializeEquipment(p.equipment),
381
378
  };
@@ -542,10 +539,9 @@ export function restorePlayer(p, s) {
542
539
  p.sleepStreak = s.needs.sleepStreak;
543
540
  p.edgeRemaining = s.edgeRemaining;
544
541
  p.haggledThisJob = s.haggledThisJob;
545
- p.autoStance = normalizeStance(s.autoStance);
546
- // Older saves have no Matrix stance; normalizeMatrixStance defaults to
547
- // Brute Force, which is the shipped default (models/player.ts).
548
- p.matrixStance = normalizeMatrixStance(s.matrixStance);
542
+ // (s.autoStance / s.matrixStance are ignored: the stance was retired in
543
+ // f79LLqTerep4nKWcA and the choice lives on the verbs. Older saves still
544
+ // carry both fields; reading them would restore a setting nothing obeys.)
549
545
  if (s.archetypeName)
550
546
  p.archetypeName = s.archetypeName;
551
547
  if (s.gender)
@@ -123,7 +123,7 @@ export function enterMatrix(scene, actor, mode) {
123
123
  : `Your ${actor.activeDeck?.name ?? 'deck'} spins up and the room dissolves into geometry -- you're in. Your body slumps where you stood in ${actor.bodyRoom.name}, empty.`;
124
124
  return [
125
125
  `${entry}${hotWarning}${deckNote} You come up on ${actor.currentGrid?.name ?? 'the grid'}.${actor.currentGrid?.userPenalty ? ' Public grid: -2 on everything you do out here.' : ''}`,
126
- hint(`No streets out here: "map" lights the hosts overhead, "mark" then "enter" (or "go") a host to get inside its architecture from anywhere, "hack" to force it, "jack out" to return.`),
126
+ hint(`No streets out here: "map" lights the hosts overhead and names them, "hack <host>" slips a mark in quiet (p.240) and "force <host>" smashes one in (p.238), one mark lets "enter <host>" walk you inside from anywhere, "mark" shows who holds what, "jack out" to return.`),
127
127
  ].filter(l => l.length > 0);
128
128
  }
129
129
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.206.0",
3
+ "version": "5.208.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.",