@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
@@ -1,43 +1,124 @@
1
1
  import { AttackCommand } from './attack.js';
2
+ import { Category } from '../types/shared/item-enum.js';
3
+ import { hint } from '../utilities/hints.js';
2
4
  /**
3
- * Take them alive: the same exchange machinery as attack (opposed roll,
4
- * retaliation, pacing, movement lock), but the damage lands on the STUN
5
- * track -- a full track is a live capture, not a kill. The tradeoffs that
6
- * keep it honest: the strike ignores your gun entirely (bare-hands pool,
7
- * low base damage, so it's slower than shooting), the target's retaliation
8
- * is fully lethal, and beating someone far past unconsciousness overflows
9
- * into their body -- you can still kill someone you meant to capture.
5
+ * TAKE THEM ALIVE -- the non-lethal half of the meat fork, and since
6
+ * f79LLqTerep4nKWcA the ONLY way to ask for it.
7
+ *
8
+ * It used to be half a verb and half a setting: `subdue` was bare hands
9
+ * and nothing else, while a lethal weapon went Stun only if your standing
10
+ * STANCE happened to be non-lethal when you swung it. Two places to say
11
+ * one thing, one of them invisible. The stance is gone; the verb says it.
12
+ *
13
+ * WHAT CANON ALLOWS, and the whole of what this verb reaches for
14
+ * (RAG-checked p.186 "Changing Damage Types", p.170-171, p.434):
15
+ *
16
+ * BARE HANDS unarmed is Stun by default -- the fallback, and what
17
+ * you get with nothing drawn.
18
+ * STUN GEAR taser, stun baton, shock gloves: electricity is Stun
19
+ * in anyone's hands, in any frame of mind (p.170-171).
20
+ * STUN ROUNDS gel, stick-n-shock: whatever the magazine holds
21
+ * (p.434). The gun fires; the rounds are the mercy.
22
+ * FLAT OF THE BLADE a lethal melee weapon used as a club -- "Accuracy
23
+ * of 3", and "blades used as a club lose all Reach due
24
+ * to the need to hit with the pommel" (p.186).
25
+ *
26
+ * AND WHAT IT REFUSES. A lethal firearm with lethal rounds has no
27
+ * non-lethal door in the book short of a Called Shot, which this engine
28
+ * does not implement. That case used to FIRE and tag the line
29
+ * " (LETHAL -- no stun rounds loaded)" afterwards: the player asked to
30
+ * take someone alive, killed them, and read about it in the past tense.
31
+ * A refusal costs no action and names both ways out.
32
+ *
33
+ * THE TRADEOFFS THAT KEEP IT HONEST are unchanged: the target's
34
+ * retaliation is fully lethal (weaponProfile is never handed this verb's
35
+ * intent), and beating someone far past unconsciousness overflows into
36
+ * their body -- you can still kill someone you meant to capture.
37
+ *
38
+ * SR5 "SUBDUING" (p.195) IS A DIFFERENT ACTION and lives next door in
39
+ * commands/grapple.ts -- the grapple that immobilises and deals no
40
+ * damage, escaped with "struggle". This verb is the knockout.
10
41
  */
11
42
  export class SubdueCommand extends AttackCommand {
12
43
  static verb = 'subdue';
13
44
  /** Touches another actor: authorized per-act for AI-driven actors
14
45
  * (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
15
46
  static npcPolicy = 'authorized';
16
- static description = 'Take someone down ALIVE -- stun damage with your bare hands. Slower than shooting, they fight back for real, and overdoing it can still kill them.';
47
+ static description = 'Take someone down ALIVE. Uses whatever you are holding that CAN go non-lethal: stun gear (taser, stun baton, shock gloves), stun rounds in the magazine, or a lethal melee weapon swung as a club -- Accuracy 3 and no Reach (p.186). Bare hands otherwise, which are Stun by default. A lethal gun with lethal rounds is refused, not fired. They fight back for real, and overdoing it can still kill them. Also "stun", "knock out". ("grapple" is the other thing: SR5 Subduing, p.195 -- hold them still, no damage.)';
48
+ /**
49
+ * How this actor can take someone alive with what is in their hands.
50
+ * One reader, used by the refusal, the draw decision and the profile,
51
+ * so the three can never disagree about what the weapon is for.
52
+ */
53
+ nonLethalRoute() {
54
+ const actor = this.actor;
55
+ const held = actor.weaponDrawn ? actor.getCarriedWeapon() : undefined;
56
+ if (!held)
57
+ return 'hands';
58
+ if (held.isStunWeapon())
59
+ return 'stun-weapon';
60
+ if (held.isFirearm())
61
+ return held.stunAmmo() ? 'stun-ammo' : 'lethal-gun';
62
+ if (held.category === Category.MeleeWeapon)
63
+ return 'club';
64
+ // Anything else in your hands is not a weapon this verb can pull --
65
+ // fall back to the hands themselves rather than inventing a rule.
66
+ return 'hands';
67
+ }
17
68
  async execute(args = []) {
18
- // A chokehold needs hands and a neck: no grappling as a Matrix persona
19
- // or an astral form. (Matrix "non-lethal" is meaningless anyway -- a
20
- // crashed ice program isn't a corpse; and astral entities are put down
21
- // with force, not wrestled.)
69
+ // "knock out <name>" / "knock down <name>": the verb is words[0]
70
+ // everywhere in this dispatcher, so the second word of a two-word
71
+ // spelling arrives as the first arg and would be read as part of the
72
+ // target's name.
73
+ const rest = [...(args ?? [])];
74
+ if (/^(out|down)$/i.test(rest[0] ?? ''))
75
+ rest.shift();
76
+ // A chokehold needs hands and a neck: no knockouts as a Matrix
77
+ // persona or an astral form. (Matrix "non-lethal" is meaningless
78
+ // anyway -- a crashed ice program isn't a corpse; and astral entities
79
+ // are put down with force, not wrestled.)
22
80
  if (this.actor.plane !== 'meat') {
23
- return `You can't grapple anything from here -- subduing takes real hands on a real body.`;
81
+ return `You can't take anything alive from here -- a knockout needs real hands on a real body.`;
82
+ }
83
+ // THE REFUSAL COMES BEFORE THE ROLL, so it costs nothing. See the
84
+ // class comment: this case used to fire and apologise afterwards.
85
+ if (this.nonLethalRoute() === 'lethal-gun') {
86
+ const gun = this.actor.getCarriedWeapon();
87
+ return [
88
+ `${gun?.name ?? 'That gun'} is loaded with lethal rounds -- there is no way to shoot someone alive with it (p.186: short of a called shot, a firearm deals what it is loaded with).`,
89
+ hint(` ("reload gel" or "reload stick-n-shock" if you are carrying either, "holster" and "subdue" bare-handed, or "attack" if you meant it.)`),
90
+ ].join('');
24
91
  }
25
- return super.execute(args);
92
+ return super.execute(rest);
26
93
  }
94
+ /** The weapon speaks unless the answer is your own hands. */
27
95
  usesEquippedWeapon() {
28
- return false;
96
+ return this.nonLethalRoute() !== 'hands';
29
97
  }
30
98
  actionVerb() {
31
99
  return 'moves to subdue';
32
100
  }
33
101
  strikeProfile(exchange, target) {
102
+ const actor = this.actor;
103
+ // WITH SOMETHING IN YOUR HANDS the normal weapon profile builds the
104
+ // strike -- it already knows electricity, stun rounds and the p.186
105
+ // club-swing (Accuracy 3, Reach gone); all it was ever missing was
106
+ // being told that this swing means to leave them breathing.
107
+ if (this.nonLethalRoute() !== 'hands') {
108
+ return exchange.weaponProfile(actor, target, { nonLethal: true });
109
+ }
34
110
  return {
35
111
  label: 'with a subduing hold',
36
112
  // Bare-hands work: rolled on the UNARMED skill regardless of what's
37
113
  // drawn -- a pistol's quality (or your firearms training) doesn't
38
114
  // help you choke someone out. See Player.getUnarmedAttackPool.
39
- pool: Math.max(0, this.actor.getUnarmedAttackPool() + this.actor.consumeEdgeBoost()),
40
- damageBase: 1 + Math.floor(this.actor.strength / 2),
115
+ pool: Math.max(0, actor.getUnarmedAttackPool() + actor.consumeEdgeBoost()),
116
+ // (STR)S, p.187 -- the same number combat-exchange.ts has always
117
+ // used for a bare fist. This said `1 + floor(STR/2)` for no reason
118
+ // the file could name, so a troll's chokehold was weaker than the
119
+ // identical troll's punch; fixed while the verb was being rebuilt
120
+ // around it (f79LLqTerep4nKWcA).
121
+ damageBase: actor.strength + actor.bonus('unarmed-damage'),
41
122
  soakPool: target.getSoakPool(),
42
123
  dealStun: true,
43
124
  };
@@ -54,14 +54,25 @@ import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-aud
54
54
  */
55
55
  export class TapCommand extends Command {
56
56
  static verb = 'tap';
57
- static description = 'Cable into a camera you are standing next to -- a DIRECT CONNECTION (p.232). A slaved device can\'t hide behind its host\'s ratings against a cable, so the camera fights you with its own; and a mark on a slaved device is a mark on the host it answers to (p.233), which is your way in. Needs your BODY in the room and a deck in hand. "tap" is sleaze (fail = noticed); "tap loud" is brute force (fail = unnoticed, the firewall bites).';
58
- async execute(args = []) {
57
+ static description = 'Cable into a camera you are standing next to -- a DIRECT CONNECTION (p.232), run as HACK ON THE FLY (p.240). A slaved device can\'t hide behind its host\'s ratings against a cable, so the camera fights you with its own; and a mark on a slaved device is a mark on the host it answers to (p.233), which is your way in. Needs your BODY in the room and a deck in hand. Fail and you are noticed. To cable in loud instead, "force camera" is Brute Force over the same cable (p.238).';
58
+ /**
59
+ * WHICH INTRUSION ACTION THE CABLE CARRIES. Same rule as hack.ts:
60
+ * the verb decides, not a word inside the target name. `tap loud`
61
+ * used to scan the args for loud/force/brute/attack and flip -- so a
62
+ * camera named for any of those words could not be tapped quietly,
63
+ * and the word itself taught nothing. `force camera` routes here with
64
+ * the Attack mode set (commands/hack.ts), which is the same hop
65
+ * `hack camera` has always made.
66
+ */
67
+ modeOverride;
68
+ /** How `force camera` reaches the loud route over the same cable. */
69
+ withMode(m) { this.modeOverride = m; return this; }
70
+ mode() { return this.modeOverride ?? 'sleaze'; }
71
+ async execute() {
59
72
  const actor = this.actor;
60
73
  const room = actor.currentLocation;
61
74
  const rooms = this.rooms ?? {};
62
- // Same mode vocabulary as hack.ts and mark.ts -- one fork, learned once.
63
- const words = (args ?? []).map(w => w.toLowerCase());
64
- const mode = words.some(w => /^(loud|force|brute|attack)$/.test(w)) ? 'attack' : 'sleaze';
75
+ const mode = this.mode();
65
76
  if (!isWatched(room)) {
66
77
  return `No cameras in here to cable into.${hint(` (Your overlay tags them "◎ Camera cluster" when there are.)`)}`;
67
78
  }
@@ -843,5 +843,66 @@
843
843
  // about someone else's haul, on top of the feed line it already
844
844
  // had. Scoped to the sweeper; and the two performAction calls that
845
845
  // passed a bare "take" now pass "takes", like every sibling verb.
846
- export const ENGINE_VERSION = '1.67.0';
846
+ // 1.68.0 (2026-09-16): THERE IS NO STANCE (f79LLqTerep4nKWcA). The
847
+ // Erase Mark bench came back rejected without the reporter ever
848
+ // reaching Erase Mark: the vocabulary it walked through did not
849
+ // survive being read. "what is 'hack quiet' -- I don't see a 'quiet',
850
+ // is it just 'mark'? How do I know which host/PAN/etc I'm hacking?"
851
+ // ... "'sealed, walls UP, over you' means nothing, it's just jargon."
852
+ // ... and the ruling: "Let's get rid of the stance all together --
853
+ // it's now confusing as we have turn-based combat. The extra
854
+ // 'overrides' is confusing."
855
+ // - ONE VERB PER BOOK ACTION. `hack <target>` IS Hack on the Fly
856
+ // (p.240); `force <target>` (brute, smash) IS Brute Force (p.238) --
857
+ // commands/force.ts, which is HackCommand with one method
858
+ // overridden, so the entire target ladder is shared. The mode words
859
+ // loud/quiet/sleaze/sneak/force/brute/attack are gone from the
860
+ // parser: every word a player types is now part of a NAME.
861
+ // - `mark` STOPS BEING AN ACTION and becomes the ledger, BOTH
862
+ // directions -- what you hold, and what holds you. It routed into
863
+ // hack before, which is the "then what is mark?" in the report: two
864
+ // spellings of one action and no way to tell them apart. It costs
865
+ // nothing and bills nothing. Alias `marks`.
866
+ // - THE MEAT HALF MOVED ONTO ITS VERBS TOO. `attack`/`kill` deal what
867
+ // the gear was sold to deal; `subdue` (stun, knock out) takes them
868
+ // alive and now uses whatever is in your hands that CAN go Stun --
869
+ // stun gear, stun rounds, or a blade swung as a club at Accuracy 3
870
+ // with no Reach (p.186), bare hands otherwise. A lethal gun with
871
+ // lethal rounds is REFUSED before the dice instead of fired and
872
+ // labelled " (LETHAL -- no stun rounds loaded)" afterwards.
873
+ // combat-exchange.weaponProfile takes the intent as an argument.
874
+ // `grapple` is still SR5 Subduing (p.195) and is untouched.
875
+ // - SWEPT THE FAMILY, because a verb fixed alone reproduces the report
876
+ // one layer down: `tap loud` and `disable loud`/`disable quiet` were
877
+ // the same hidden adverb. `force <camera>` is the loud cable now,
878
+ // and `brick` is the only spelling of Data Spike.
879
+ // - SUBDUE'S DV WAS WRONG and is fixed while the file was open:
880
+ // `1 + floor(STR/2)` where RAW unarmed is (STR)S (p.187), the number
881
+ // combat-exchange has always used for a bare fist. A troll's
882
+ // chokehold was weaker than the same troll's punch.
883
+ // - "sealed, walls UP, over you" IS GONE from hostLine, and it was
884
+ // worse than jargon: `standing` read only the marks YOU hold, so a
885
+ // host holding three marks on your persona printed "sealed", the
886
+ // word for a node nobody has touched. Four facts, four plain
887
+ // clauses, swept across every host renderer (hostLine, hostDockLine,
888
+ // gridIcons, hostInteriorPanel): what you hold, what it holds on
889
+ // you, whether it knows someone is in (alert) or can see YOU
890
+ // (spotted), and whether it is the node overhead.
891
+ // - THE HUD'S FIRST LINE carried the stance so players would discover
892
+ // Tab. It carries the MARK LEDGER now, both counts -- the one
893
+ // always-visible surface, in the CLI and in the headless transcript.
894
+ // Only the web character sheet ever showed both directions.
895
+ // - TAB COMPLETES THE NAME YOU ARE TYPING, answered by the engine:
896
+ // Tab sends `complete <box text>` (commands/complete.ts, hidden
897
+ // verb, no action, no bill) and utilities/nameables.ts answers from
898
+ // the live scene. It has to be the engine: the condition beat
899
+ // carries meat-plane BODIES only, so jacked in no client knows a
900
+ // single icon's name, and the CLI on a shared run holds nothing but
901
+ // rendered text. The reply's first line is `{complete}<text>` and
902
+ // the client refills its box from it.
903
+ // SAVE SHAPE: autoStance and matrixStance are no longer written and
904
+ // no longer read; older saves carry them harmlessly. The condition
905
+ // report drops `stance` (the site and Unity drop it in the same
906
+ // release). REMOVED: the `stance` verb.
907
+ export const ENGINE_VERSION = '1.68.0';
847
908
  //# sourceMappingURL=engine-version.js.map
@@ -39,6 +39,10 @@ export const REPRO_FAR_ROOM = 'The Far Side';
39
39
  * Both rooms have it: a doorway has two sides, and a guard posted on
40
40
  * the far side stands in their own. */
41
41
  export const REPRO_DOORWAY_SPOT = 'doorway';
42
+ /** What locks a `barrier.plainDoor` that names no keyItem of its own.
43
+ * A door with no key and no mechanism would simply stand open, which
44
+ * is the opposite of the fixture the author asked for. */
45
+ export const REPRO_PLAIN_DOOR_KEY = 'Door Key';
42
46
  /**
43
47
  * Somewhere to stand that is NOT the doorway, so "move to the door" is
44
48
  * a real move with a real before-state. Without it the runner starts in
@@ -202,7 +206,18 @@ export function buildReproScene(fixture) {
202
206
  const startSide = fixture.start ?? 'barrier';
203
207
  const startRoom = roomFor(startSide);
204
208
  let device;
205
- if (barrier) {
209
+ // THE LOCK THAT IS IN THE DOOR (Gfx5RFFpHJdKB87Bh). barrier.plainDoor
210
+ // seals the exit with keyRequired and builds NO device -- the shape
211
+ // every ordinary locked door in a generated scene has, and the shape
212
+ // this builder could not make until the report proved it mattered.
213
+ // `open` still means what it means -- the door simply is not locked --
214
+ // and it must NOT quietly put the mechanism back, which is the
215
+ // silently-inert shape this file exists to avoid.
216
+ const plainDoor = barrier?.plainDoor === true;
217
+ if (barrier?.plainDoor && !barrier.keyItem && !barrier.open) {
218
+ warnings.push('A plain door with no keyItem has no quiet way through at all -- force is the only route, which is fine if that is the point.');
219
+ }
220
+ if (barrier && !plainDoor) {
206
221
  // INVARIANT 1: a code belongs to a panel and nowhere else. The
207
222
  // Device constructor DROPS it on a lock or a ward, silently and
208
223
  // correctly -- that is the runtime honouring "you can't talk at a
@@ -309,7 +324,13 @@ export function buildReproScene(fixture) {
309
324
  name: REPRO_BARRIER_ROOM,
310
325
  description: barrier
311
326
  ? `A short concrete run ending at one door, ${direction} of you. `
312
- + `${device.name} is fixed beside the doorway${barrier.open ? ', standing open' : ', holding it shut'}. `
327
+ + (plainDoor
328
+ // NOTHING BESIDE THE DOOR, and the prose has to say so: the
329
+ // whole point of a plainDoor bench is that there is no
330
+ // mechanism to work, so describing one would hand the
331
+ // reviewer the wrong target to aim every verb at.
332
+ ? `No panel, no maglock, no keypad -- just a lock in the door itself, and it is ${barrier.open ? 'off the latch' : 'shut'}. `
333
+ : `${device.name} is fixed beside the doorway${barrier.open ? ', standing open' : ', holding it shut'}. `)
313
334
  + `There is nothing else here, which is the point -- whatever is wrong is wrong at that door.`
314
335
  : `A short concrete run with a door ${direction} of you and nothing else in it.`,
315
336
  // fixture.home: the barrier room is the runner's own bed, so a rest
@@ -319,7 +340,16 @@ export function buildReproScene(fixture) {
319
340
  spots,
320
341
  levels: level ? [{ name: level.name, side: level.side ?? 'north', kind: level.kind ?? 'catwalk', climbOnly: level.climbOnly === true }] : undefined,
321
342
  isStartRoom: startSide === 'barrier',
322
- exits: [{ direction, targetRoom: REPRO_FAR_ROOM, atSpot: REPRO_DOORWAY_SPOT }],
343
+ exits: [{
344
+ direction,
345
+ targetRoom: REPRO_FAR_ROOM,
346
+ atSpot: REPRO_DOORWAY_SPOT,
347
+ // INVARIANT 3, the deviceless case: with no mechanism to name the
348
+ // exit, the KEY is what locks the door.
349
+ ...(plainDoor && !barrier?.open
350
+ ? { keyRequired: barrier?.keyItem ?? REPRO_PLAIN_DOOR_KEY }
351
+ : {}),
352
+ }],
323
353
  devices: device ? [device] : undefined,
324
354
  };
325
355
  const farRoom = {
@@ -327,7 +357,7 @@ export function buildReproScene(fixture) {
327
357
  ...watchedFields('far'),
328
358
  name: REPRO_FAR_ROOM,
329
359
  description: `The other side of the door, which lies ${OPPOSITE[direction]} of you`
330
- + `${barrier ? ` and is held from the far side by ${device.name}` : ''}. Bare, and no further out.`,
360
+ + `${barrier && device ? ` and is held from the far side by ${device.name}` : ''}. Bare, and no further out.`,
331
361
  roomType: 'Street',
332
362
  size: 'medium',
333
363
  spots,
@@ -1,6 +1,5 @@
1
1
  import { sustainBurden, sustainTotal } from './utilities/sustaining.js';
2
2
  import { agentRatingFromName } from './utilities/programs.js';
3
- import { MATRIX_STANCE_ACTION } from './models/player.js';
4
3
  import { buildDossierUrl } from './utilities/web-character-generator.js';
5
4
  import { makaHost } from '../../../tools/https/maka-host.js';
6
5
  import { openBrowser } from '../../../tools/browser/open-browser.js';
@@ -115,9 +114,10 @@ import { EndCallCommand } from './commands/end-call.js';
115
114
  import { fuzzyPickName, guessVerb } from './utilities/fuzzy-match.js';
116
115
  import { PickCommand } from './commands/pick.js';
117
116
  import { HackCommand } from './commands/hack.js';
117
+ import { ForceCommand } from './commands/force.js';
118
+ import { CompleteCommand } from './commands/complete.js';
118
119
  import { DisableCommand, BrickCommand } from './commands/disable.js';
119
120
  import { CastCommand } from './commands/cast.js';
120
- import { StanceCommand } from './commands/stance.js';
121
121
  import { FollowCommand, UnfollowCommand } from './commands/follow.js';
122
122
  import { SurrenderCommand } from './commands/surrender.js';
123
123
  import { JackCommand } from './commands/jack.js';
@@ -2884,6 +2884,11 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2884
2884
  CommandFactory.registerCommand('fit', FitCommand);
2885
2885
  CommandFactory.registerCommand('unfit', UnfitCommand);
2886
2886
  CommandFactory.registerCommand('subdue', SubdueCommand);
2887
+ // The non-lethal intent is a VERB now, not a stance (f79LLqTerep4nKWcA).
2888
+ // `knock` carries "knock out <name>" / "knock down <name>"; subdue.ts
2889
+ // strips the second word, which the dispatcher hands it as an arg.
2890
+ CommandFactory.registerCommand('stun', SubdueCommand);
2891
+ CommandFactory.registerCommand('knock', SubdueCommand);
2887
2892
  // SUBDUING p.195 (see commands/grapple.ts): restrain without damage.
2888
2893
  // Canon skill hooks (see SkillName in models/player.ts).
2889
2894
  CommandFactory.registerCommand('palm', PalmCommand);
@@ -2923,10 +2928,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2923
2928
  CommandFactory.registerCommand('end-call', EndCallCommand);
2924
2929
  CommandFactory.registerCommand('pick', PickCommand);
2925
2930
  CommandFactory.registerCommand('hack', HackCommand);
2931
+ // BRUTE FORCE IS ITS OWN VERB (f79LLqTerep4nKWcA), not a word you add
2932
+ // to that one -- see commands/force.ts for why.
2933
+ CommandFactory.registerCommand('force', ForceCommand);
2934
+ CommandFactory.registerCommand('brute', ForceCommand);
2935
+ CommandFactory.registerCommand('smash', ForceCommand);
2926
2936
  CommandFactory.registerCommand('disable', DisableCommand);
2927
2937
  CommandFactory.registerCommand('brick', BrickCommand);
2928
2938
  CommandFactory.registerCommand('cast', CastCommand);
2929
- CommandFactory.registerCommand('stance', StanceCommand);
2930
2939
  CommandFactory.registerCommand('surrender', SurrenderCommand);
2931
2940
  CommandFactory.registerCommand('jack', JackCommand);
2932
2941
  CommandFactory.registerCommand('project', ProjectCommand);
@@ -3081,6 +3090,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3081
3090
  CommandFactory.registerCommand('wipe', EraseCommand);
3082
3091
  // MAtrix Recognition Keys (canon p.235-236).
3083
3092
  CommandFactory.registerCommand('mark', MarkCommand);
3093
+ CommandFactory.registerCommand('marks', MarkCommand);
3094
+ CommandFactory.registerCommand('complete', CompleteCommand);
3084
3095
  CommandFactory.registerCommand('unmark', EraseMarkCommand);
3085
3096
  // SPRITE POWERS (p.256-257): one verb per power, reached as
3086
3097
  // "order <sprite> <power> <target>" -- see commands/sprite-power.ts.
@@ -5892,40 +5903,41 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5892
5903
  }
5893
5904
  statusLinesFor(player) {
5894
5905
  const lines = [];
5895
- // Line 1: the stance -- lethal or non-lethal in the meat (the damage
5896
- // type your next blow means; models/player.ts FightStance), Brute
5897
- // Force or Hack on the Fly on the Matrix. It lives HERE -- the
5898
- // shortest HUD line -- because appended to the long readiness line it
5899
- // wrapped out of the box on narrower terminals and effectively only
5900
- // ever showed in the Tab announcement. Always shown: the HUD is how
5901
- // players discover Tab flips it at all. Red for the lethal routes,
5902
- // yellow for the careful ones -- never gray, which is ANSI "bright
5903
- // black" and illegible on a stock dark palette.
5906
+ // Line 1: what you are holding, and -- jacked in -- the MARK LEDGER,
5907
+ // both directions.
5908
+ //
5909
+ // THIS SLOT USED TO CARRY THE STANCE, and it carried it for a reason
5910
+ // that died with the stance: the HUD was how players discovered Tab
5911
+ // flipped it at all. With no posture left to advertise
5912
+ // (f79LLqTerep4nKWcA) the shortest HUD line is free, and the thing
5913
+ // most worth putting in it is the one the same report asked for --
5914
+ // "when I have a mark on it, show my mark on it, and vice versa --
5915
+ // when it has a mark on me, show that."
5916
+ //
5917
+ // Both counts, always, on the one surface a persona cannot miss.
5918
+ // Only the web character sheet ever printed the two ledgers side by
5919
+ // side; the CLI and the headless transcript (headless.ts status
5920
+ // events run through this very method) printed neither, so a runner
5921
+ // carrying three host marks read a grid map that said "sealed".
5922
+ // Red once anything holds a key on you -- that is the number that
5923
+ // decides whether the ice can find you -- yellow while only you hold
5924
+ // keys, and never gray, which is ANSI "bright black" and illegible
5925
+ // on a stock dark palette.
5904
5926
  //
5905
5927
  // WHERE YOU ARE MOVED OUT (player ruling 2026-08-25): the room name
5906
5928
  // now captions the Map box and the "@ spot" captions the Room box,
5907
5929
  // each under the picture it belongs to, so the HUD stops repeating
5908
- // what two panels already show. The breadcrumb went with it -- the
5909
- // district map draws the way you came far better than a text trail.
5910
- // THE STANCE THE HUD SHOWS IS THE ONE IN FORCE (uMQAhaAysgaKpWFkn,
5911
- // 2026-09-03). Jacked in, the fight stance is not what Tab moves
5912
- // and not what your next action obeys -- the Matrix stance is
5913
- // (Brute Force or Hack on the Fly). Showing the meat stance to
5914
- // a persona would be the same class of bug as the 2026-08-27
5915
- // report that the HUD did not follow Tab at all: a readout naming
5916
- // a setting the player cannot currently change.
5930
+ // what two panels already show.
5917
5931
  const onMatrix = player.plane === 'matrix';
5918
- const stanceWord = onMatrix ? MATRIX_STANCE_ACTION[player.matrixStance] : player.autoStance;
5919
- const stanceColor = onMatrix
5920
- ? (player.matrixStance === 'attack' ? 'red' : 'yellow')
5921
- : player.autoStance === 'lethal' ? 'red' : 'yellow';
5922
- // WHAT'S IN YOUR HANDS rides alongside the stance (player ruling
5923
- // 2026-08-25). The two answer the same question -- how ready are you
5924
- // -- and the stance line was left short and lonely once the location
5925
- // moved out to the map captions, while the readiness line below ran
5926
- // long. armedLine is built further down with the rest of the gear
5927
- // read; only its placement moved.
5928
- lines.push(`{${stanceColor}-fg}${onMatrix ? '◈' : '⚔'} ${stanceWord.toUpperCase()}{/${stanceColor}-fg} ${this.armedLine(player)}`);
5932
+ if (onMatrix) {
5933
+ const held = marksPlaced(this.scene, player.name).reduce((n, m) => n + m.count, 0);
5934
+ const on = marksOnYou(this.scene, player).reduce((n, m) => n + m.count, 0);
5935
+ const markColor = on > 0 ? 'red' : held > 0 ? 'yellow' : 'light-blue';
5936
+ lines.push(`{${markColor}-fg}◈ MARKS ${held} held · ${on} ON YOU{/${markColor}-fg} ${this.armedLine(player)}`);
5937
+ }
5938
+ else {
5939
+ lines.push(`{red-fg}⚔ ARMED{/red-fg} ${this.armedLine(player)}`);
5940
+ }
5929
5941
  // Line 2: both condition monitors -- in a game where a fight can kill
5930
5942
  // you and casting can knock you out, current damage on either track is
5931
5943
  // never something a player should have to ask for. Colors shift as
@@ -6738,14 +6750,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6738
6750
  static HELP_CATEGORIES = [
6739
6751
  { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
6740
6752
  { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
6741
- { title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
6753
+ { title: 'combat', entries: [['attack'], ['kill'], ['subdue', 'stun', 'knock'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
6742
6754
  // The party layer: everyone who walks (or flies, or manifests) at
6743
6755
  // your side answers to these.
6744
6756
  // "players" folded back under crew after playtesting ("crew
6745
6757
  // players" / "crew street"); the bare verb still answers quietly.
6746
6758
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
6747
6759
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
6748
- { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6760
+ { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6749
6761
  // The Emerged get their own shelf (player request: "there MUST be
6750
6762
  // resonance" -- matrix is the place, Resonance is the talent).
6751
6763
  // `sustain` shelves here and not under magic: the command is
@@ -6767,7 +6779,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6767
6779
  // ("first aid", "apply first aid") that used to die at the parser. As
6768
6780
  // bare words they mean nothing, so they are claimed but not advertised
6769
6781
  // -- `heal` is the verb the help screen teaches.
6770
- static HIDDEN_VERBS = ['players', 'first', 'apply'];
6782
+ // `complete` is what Tab sends (commands/complete.ts) -- a question the
6783
+ // input box asks, not a verb anyone types, so it stays out of help.
6784
+ static HIDDEN_VERBS = ['players', 'first', 'apply', 'complete'];
6771
6785
  /** Session verbs that only a play-tester (or admin) can actually use
6772
6786
  * -- the server role-gates both and 403s everyone else. Listed in
6773
6787
  * help ONLY for a confirmed play-tester; see helpCategories. `roles`
@@ -16,38 +16,6 @@ import { physicalLimit } from '../utilities/grapple.js';
16
16
  import { astralLimit } from '../utilities/limits.js';
17
17
  import { Inventory } from './inventory.js';
18
18
  import { AbstractPlayer } from '../types/shared/abstracts.js';
19
- export const STANCE_CYCLE = ['lethal', 'non-lethal'];
20
- /** The old names, still accepted from saves and the stance command. */
21
- export const STANCE_ALIASES = {
22
- guarded: 'non-lethal', aggressive: 'lethal', manual: 'lethal',
23
- nonlethal: 'non-lethal', 'non lethal': 'non-lethal', stun: 'non-lethal', kill: 'lethal',
24
- };
25
- /** What each stance means, in one line -- the HUD's and the verb's. */
26
- export const STANCE_DESCRIPTION = {
27
- lethal: 'your weapons deal the damage they were sold to deal; "attack" and "kill" mean it',
28
- 'non-lethal': 'Stun wherever the gear allows -- stun rounds when you reload, the flat of the blade (p.186), bare hands -- and a lethal gun with lethal rounds says so before it fires',
29
- };
30
- export const MATRIX_STANCE_CYCLE = ['attack', 'sleaze'];
31
- /** What each stance is CALLED in the book -- printed everywhere the
32
- * engine used to say "loud"/"quiet", so the vocabulary teaches the
33
- * real action names (the whole point of uMQAhaAysgaKpWFkn). */
34
- export const MATRIX_STANCE_ACTION = {
35
- attack: 'Brute Force',
36
- sleaze: 'Hack on the Fly',
37
- };
38
- /** The words a player may type, including the engine's older ones. */
39
- export const MATRIX_STANCE_ALIASES = {
40
- loud: 'attack', force: 'attack', brute: 'attack', 'brute force': 'attack', bf: 'attack',
41
- quiet: 'sleaze', sneak: 'sleaze', 'hack on the fly': 'sleaze', hotf: 'sleaze', fly: 'sleaze',
42
- };
43
- export function normalizeMatrixStance(v) {
44
- const k = (v ?? 'attack').toLowerCase();
45
- return MATRIX_STANCE_ALIASES[k] ?? (MATRIX_STANCE_CYCLE.includes(k) ? k : 'attack');
46
- }
47
- export function normalizeStance(v) {
48
- const k = (v ?? 'lethal').toLowerCase();
49
- return STANCE_ALIASES[k] ?? (STANCE_CYCLE.includes(k) ? k : 'lethal');
50
- }
51
19
  /** Meat and a jumped-in drone share the physical world. */
52
20
  export function isPhysicalPlane(plane) {
53
21
  return plane === 'meat' || plane === 'drone';
@@ -194,29 +162,9 @@ export class Player extends AbstractPlayer {
194
162
  // session where an NPC's overlapping AI loops fired two exchanges at
195
163
  // once and the target died TWICE (two kill events, two narrations).
196
164
  inExchange = false;
197
- // The fight stance (see FightStance above): lethal or non-lethal, the
198
- // damage type your next blow is meant to deal. Tab cycles it in the UI;
199
- // "stance" sets it by name. Every action is still yours to type -- the
200
- // Combat Turn hands you the phase and waits for "end turn".
201
- // Accessor (live-sheet pass, 2026-09-01): stance is sheet-visible.
202
- _autoStance = 'lethal';
203
- get autoStance() { return this._autoStance; }
204
- set autoStance(v) {
205
- if (this._autoStance === v)
206
- return;
207
- this._autoStance = v;
208
- this.noteConditionChanged();
209
- }
210
- /** Which of the book's two intrusion actions the next hack uses
211
- * (see MatrixStance). Sheet-visible, like its meat-world twin. */
212
- _matrixStance = 'attack';
213
- get matrixStance() { return this._matrixStance; }
214
- set matrixStance(v) {
215
- if (this._matrixStance === v)
216
- return;
217
- this._matrixStance = v;
218
- this.noteConditionChanged();
219
- }
165
+ // (No stance lives here any more -- see the note at the top of this
166
+ // file. The damage type a blow deals is decided by the verb that threw
167
+ // it, and the intrusion action by the verb that ran it.)
220
168
  // SUBDUING (RAW p.195, the "restrain" ask from a real session's runaway
221
169
  // NPC): a grapple links two actors BY NAME -- grappledBy/grappling are
222
170
  // the two ends, grappleThreshold the escape test's bar (the original
@@ -27,7 +27,7 @@
27
27
  * 2.5.0 adds IReproHost.purpose / .sculpt and IReproItem.file (engine
28
28
  * 1.50.0, hosts with a purpose) -- additive.
29
29
  */
30
- export const REPRO_SCHEMA_VERSION = '2.6.0';
30
+ export const REPRO_SCHEMA_VERSION = '2.7.0';
31
31
  /** THE LIVE REASON, from the field or the card's own words.
32
32
  *
33
33
  * `repro.live` is authoritative -- but the server's validator must