@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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/complete.js +125 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/force.js +40 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +61 -54
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +49 -39
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +14 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +98 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +16 -5
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +62 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +49 -35
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +3 -55
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +32 -30
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +20 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +2 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +50 -17
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +56 -9
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/nameables.js +107 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +3 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
- package/package.json +1 -1
- 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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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
|
|
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(
|
|
92
|
+
return super.execute(rest);
|
|
26
93
|
}
|
|
94
|
+
/** The weapon speaks unless the answer is your own hands. */
|
|
27
95
|
usesEquippedWeapon() {
|
|
28
|
-
return
|
|
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,
|
|
40
|
-
|
|
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.
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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:
|
|
5896
|
-
//
|
|
5897
|
-
//
|
|
5898
|
-
//
|
|
5899
|
-
//
|
|
5900
|
-
//
|
|
5901
|
-
//
|
|
5902
|
-
//
|
|
5903
|
-
//
|
|
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.
|
|
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
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
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'], ['
|
|
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
|
-
|
|
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
|
-
//
|
|
198
|
-
// damage type
|
|
199
|
-
//
|
|
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
|
|
@@ -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
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// focus lives
|
|
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
|
|
343
|
-
//
|
|
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
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
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
|
|
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
|
-
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
});
|