@maka/maka-cli 5.207.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 (26) 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/game.js +49 -35
  15. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +3 -55
  16. package/bundle/typescript/src/commands/game/sideQuest/ui.js +32 -30
  17. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +20 -8
  18. package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +2 -3
  19. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +50 -17
  20. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +56 -9
  21. package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +3 -2
  22. package/bundle/typescript/src/commands/game/sideQuest/utilities/nameables.js +107 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +3 -7
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
  25. package/package.json +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +0 -121
@@ -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.207.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.",
@@ -1,121 +0,0 @@
1
- import { Command } from './command.js';
2
- import { STANCE_CYCLE, STANCE_ALIASES, STANCE_DESCRIPTION, MATRIX_STANCE_CYCLE, MATRIX_STANCE_ALIASES, MATRIX_STANCE_ACTION } from '../models/player.js';
3
- import { fuzzyPickName } from '../utilities/fuzzy-match.js';
4
- /**
5
- * The typed twin of the Tab key: set (or inspect) the stance. On either
6
- * plane the stance is what your next action DOES -- in the meat, the
7
- * damage type your blow is meant to deal (lethal / non-lethal, see
8
- * FightStance in models/player.ts); on the Matrix, which intrusion
9
- * action a hack is (Brute Force / Hack on the Fly). It never decides
10
- * whether you act: since the Combat Turn (2026-09-06) every phase is
11
- * yours to play and "end turn" to commit, and the fight autopilot this
12
- * verb used to set was retired with it (2026-09-07).
13
- */
14
- export class StanceCommand extends Command {
15
- static verb = 'stance';
16
- static description = 'Set your stance -- what your next action does, never whether you take it. ON THE MATRIX it picks the intrusion action: attack (Brute Force, p.238) or sleaze (Hack on the Fly, p.240); "deck config" still sets the array. IN THE MEAT it picks the damage type you mean: lethal (the gear as sold) or non-lethal (Stun wherever the gear allows -- stun rounds when you reload, the flat of the blade at Accuracy 3 per p.186, bare hands; a lethal gun with lethal rounds warns you it stays lethal). Tab flips it.';
17
- /**
18
- * ON THE MATRIX, TAB FLIPS THE INTRUSION ACTION instead of the fight
19
- * autopilot (mjmcee, uMQAhaAysgaKpWFkn). One verb and one key, two
20
- * planes -- because the player's question is the same in both ("how
21
- * hard am I about to go?") and they are never on both planes at once.
22
- */
23
- matrixLines(actor) {
24
- const cur = actor.matrixStance;
25
- return [
26
- `Matrix stance: ${MATRIX_STANCE_ACTION[cur].toUpperCase()} (${cur}) -- ${StanceCommand.describeMatrix(cur)}.`,
27
- '',
28
- ...MATRIX_STANCE_CYCLE.map(m => ` ${MATRIX_STANCE_ACTION[m].padEnd(16)} -- ${StanceCommand.describeMatrix(m)}`),
29
- '',
30
- `Set with "stance attack" / "stance sleaze", or Tab to flip. "deck config" still sets the array itself.`,
31
- ].join('\n');
32
- }
33
- /** What each route costs and buys, in the book's terms (p.238/240). */
34
- static describeMatrix(m) {
35
- return m === 'attack'
36
- ? 'Cybercombat + Logic [Attack]; a miss goes unnoticed but the firewall bites, and net hits burn the target'
37
- : 'Hacking + Logic [Sleaze]; a miss gets you MADE, and net hits read the target while you key it';
38
- }
39
- async execute(args = []) {
40
- const actor = this.actor;
41
- const onMatrix = actor.plane === 'matrix';
42
- if (!args || args.length === 0) {
43
- if (onMatrix)
44
- return this.matrixLines(actor);
45
- const lines = [
46
- `Stance: ${actor.autoStance.toUpperCase()} -- ${STANCE_DESCRIPTION[actor.autoStance]}.`,
47
- '',
48
- ...STANCE_CYCLE.map(s => ` ${s.padEnd(10)} -- ${STANCE_DESCRIPTION[s]}`),
49
- '',
50
- `Set with "stance <name>", or Tab to flip. The stance is what a blow does, not who throws it -- every action is yours to type, and "end turn" commits the phase.`,
51
- ];
52
- return lines.join('\n');
53
- }
54
- // "stance next" -- THE TAB KEY'S WIRE FORM (playtest 2026-08-27:
55
- // "the combat stance isn't updating in the menu bar"). Tab used to
56
- // cycle by mutating game.player.autoStance directly, which in a
57
- // SHARED RUN is the local hub player, not the runner on the table:
58
- // the server's stance never moved, the autopilot never changed
59
- // temperature, and the HUD -- painted from the server's status
60
- // events -- never showed the switch. Tab now sends this instead, so
61
- // the cycle advances from the ACTOR's real stance wherever the
62
- // engine happens to be running. Checked before the fuzzy match: it
63
- // is a control word, not a stance name, and fuzzyPickName would
64
- // otherwise try to spell-correct it into one.
65
- const raw = args.join(' ').trim().toLowerCase();
66
- if (raw === 'next' || raw === 'cycle') {
67
- if (onMatrix) {
68
- const i = MATRIX_STANCE_CYCLE.indexOf(actor.matrixStance);
69
- const next = MATRIX_STANCE_CYCLE[(i + 1) % MATRIX_STANCE_CYCLE.length];
70
- actor.matrixStance = next;
71
- this.scene.updateStatus();
72
- this.logger.write(`${actor.name} flipped to ${MATRIX_STANCE_ACTION[next]}.`);
73
- return `Matrix stance: ${MATRIX_STANCE_ACTION[next].toUpperCase()} -- ${StanceCommand.describeMatrix(next)}.`;
74
- }
75
- const cur = STANCE_CYCLE.indexOf(actor.autoStance);
76
- const nextStance = STANCE_CYCLE[(cur + 1) % STANCE_CYCLE.length];
77
- actor.autoStance = nextStance;
78
- this.scene.updateStatus();
79
- this.logger.write(`${actor.name} cycled stance to ${nextStance}.`);
80
- return `Stance: ${nextStance.toUpperCase()} -- ${STANCE_DESCRIPTION[nextStance]}.`;
81
- }
82
- // A Matrix route by name, from either plane's vocabulary -- so
83
- // "stance sleaze" works while jacked in and reads as a typo
84
- // otherwise, rather than fuzzy-matching into a fight stance.
85
- const matrixNamed = MATRIX_STANCE_ALIASES[raw] ?? (MATRIX_STANCE_CYCLE.includes(raw) ? raw : undefined);
86
- if (onMatrix && matrixNamed) {
87
- if (matrixNamed === actor.matrixStance)
88
- return `Already ${MATRIX_STANCE_ACTION[matrixNamed].toUpperCase()} -- ${StanceCommand.describeMatrix(matrixNamed)}.`;
89
- actor.matrixStance = matrixNamed;
90
- this.scene.updateStatus();
91
- this.logger.write(`${actor.name} set Matrix stance to ${MATRIX_STANCE_ACTION[matrixNamed]}.`);
92
- return `Matrix stance: ${MATRIX_STANCE_ACTION[matrixNamed].toUpperCase()} -- ${StanceCommand.describeMatrix(matrixNamed)}.`;
93
- }
94
- if (onMatrix) {
95
- return `That's not a Matrix route. Choose: ${MATRIX_STANCE_CYCLE.join(', ')} (${MATRIX_STANCE_CYCLE.map(m => MATRIX_STANCE_ACTION[m]).join(' / ')}).`;
96
- }
97
- // THE EXACT NAME ALWAYS WINS. "stance non-lethal" -- the very word the
98
- // refusal below lists -- used to be refused: fuzzyPickName normalizes
99
- // punctuation to whitespace, so "non-lethal" and the "non lethal"
100
- // alias both became "non lethal", the exact tier saw two candidates,
101
- // and an ambiguous guess deliberately misses. Caught by a bench that
102
- // typed the name off the "Choose:" line (2jbprbSYqF7EPXrkR's dry run).
103
- // The old words (and their typos) still land: fuzzy over the cycle AND the aliases, then through the alias map.
104
- const pickedRaw = STANCE_CYCLE.includes(raw)
105
- ? raw
106
- : STANCE_ALIASES[raw] ?? fuzzyPickName(args.join(' '), [...STANCE_CYCLE, ...Object.keys(STANCE_ALIASES)]);
107
- const picked = pickedRaw ? (STANCE_ALIASES[pickedRaw] ?? pickedRaw) : undefined;
108
- if (!picked) {
109
- return `That's not a stance. Choose: ${STANCE_CYCLE.join(', ')}.`;
110
- }
111
- const stance = picked;
112
- if (stance === actor.autoStance) {
113
- return `Already ${stance.toUpperCase()} -- ${STANCE_DESCRIPTION[stance]}.`;
114
- }
115
- actor.autoStance = stance;
116
- this.scene.updateStatus();
117
- this.logger.write(`${actor.name} set stance to ${stance}.`);
118
- return `Stance: ${stance.toUpperCase()} -- ${STANCE_DESCRIPTION[stance]}.`;
119
- }
120
- }
121
- //# sourceMappingURL=stance.js.map