@maka/maka-cli 5.198.0 → 5.199.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/attack.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/command.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +49 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +4 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/kill.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/palm.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +52 -1
- package/bundle/typescript/src/commands/game/sideQuest/factions.js +135 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +53 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +165 -14
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +12 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +84 -8
- package/bundle/typescript/src/commands/game/sideQuest/types/repro.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/beat-echo.js +160 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +38 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hostile-contact.js +48 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/narration-limits.js +235 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-authorization.js +122 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.199.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.",
|
|
@@ -33,6 +33,9 @@ import { spendMovementMeters } from '../utilities/movement-cost.js';
|
|
|
33
33
|
*/
|
|
34
34
|
export class AttackCommand extends Command {
|
|
35
35
|
static verb = 'attack';
|
|
36
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
37
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
38
|
+
static npcPolicy = 'authorized';
|
|
36
39
|
static description = 'Attack someone in the room -- dice-pool combat with your equipped weapon. They WILL hit back. "attack <door>" shoots a barrier apart instead (unopposed; its Armor and Structure decide, p.197).';
|
|
37
40
|
async execute(args = []) {
|
|
38
41
|
if (!args || args.length === 0) {
|
|
@@ -89,6 +89,9 @@ const COMBAT_SPELL_FLAVOR = {
|
|
|
89
89
|
*/
|
|
90
90
|
export class CastCommand extends BypassCommand {
|
|
91
91
|
static verb = 'cast';
|
|
92
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
93
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
94
|
+
static npcPolicy = 'authorized';
|
|
92
95
|
static description = 'Cast a spell you know, at a FORCE you choose: "cast manabolt 8 at <target>" (default Force = your Magic; drain scales with it, and OVERCASTING past your talent makes the drain PHYSICAL). "spells" lists what you know. Bare "cast" tries to unweave a warded barrier.';
|
|
93
96
|
deviceKind = 'ward';
|
|
94
97
|
verbName = 'cast';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/commands/command-registry.ts
|
|
2
2
|
import { CommandFactory } from '../factories/command-factory.js';
|
|
3
3
|
import { Logger } from '../utilities/logger.js';
|
|
4
|
+
import { authorizeNpcAction } from '../utilities/npc-authorization.js';
|
|
4
5
|
export class CommandRegistry {
|
|
5
6
|
scene;
|
|
6
7
|
constructor(scene) {
|
|
@@ -12,6 +13,10 @@ export class CommandRegistry {
|
|
|
12
13
|
* execute() internally, and callers ran it a second time on top of that
|
|
13
14
|
* with no arguments at all, which crashed any command whose execute()
|
|
14
15
|
* doesn't guard against a missing `args` array.)
|
|
16
|
+
*
|
|
17
|
+
* `null` = no such verb. A result carrying `refusal` = a real verb the
|
|
18
|
+
* engine refused for this actor; `command` is absent and the caller
|
|
19
|
+
* must not execute anything.
|
|
15
20
|
*/
|
|
16
21
|
async parse(commandString, actor) {
|
|
17
22
|
const parts = commandString.trim().split(' ');
|
|
@@ -49,6 +54,26 @@ export class CommandRegistry {
|
|
|
49
54
|
Logger.getInstance().write(`Refused "${verb}" from ${actor.name}: that verb is the player's alone.`);
|
|
50
55
|
return null;
|
|
51
56
|
}
|
|
57
|
+
// THE AI PROPOSES, THE ENGINE DISPOSES (aCErAfvEW7G23FWDM).
|
|
58
|
+
//
|
|
59
|
+
// The same choke point, one question further out: humanOnly asks
|
|
60
|
+
// whether this verb belongs to the player's ACCOUNT, and this asks
|
|
61
|
+
// whether an AI-driven actor may do this PARTICULAR thing to this
|
|
62
|
+
// PARTICULAR target right now. An NPC is entitled to attack; it was
|
|
63
|
+
// equally entitled, until this line, to lay hands on a body whose
|
|
64
|
+
// owner was jacked into the Matrix and could not roll a defence.
|
|
65
|
+
//
|
|
66
|
+
// The refusal REASON is returned to the caller rather than swallowed
|
|
67
|
+
// -- NPC.act feeds it back to the model as a system note, so the
|
|
68
|
+
// boundary is something the fiction learns instead of something it
|
|
69
|
+
// grinds against.
|
|
70
|
+
if (declaring.getNpcPolicy() === 'authorized' && !this.scene.isHumanControlled(actor)) {
|
|
71
|
+
const verdict = authorizeNpcAction(this.scene, actor, verb, args);
|
|
72
|
+
if (!verdict.allowed) {
|
|
73
|
+
Logger.getInstance().write(`Refused "${commandString.trim()}" from ${actor.name}: ${verdict.reason}`);
|
|
74
|
+
return { args, refusal: verdict.reason ?? `That is not yours to do.` };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
52
77
|
return { command, args };
|
|
53
78
|
}
|
|
54
79
|
}
|
|
@@ -21,6 +21,31 @@ export class Command {
|
|
|
21
21
|
static isHumanOnly() {
|
|
22
22
|
return this.humanOnly;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* WHAT AN AI-DRIVEN ACTOR MAY DO WITH THIS VERB (aCErAfvEW7G23FWDM,
|
|
26
|
+
* reporter's ruling: "the only way to solve these is with engine
|
|
27
|
+
* enforcement against AI allowed actions").
|
|
28
|
+
*
|
|
29
|
+
* `humanOnly` above answers "is this the player's account", which is a
|
|
30
|
+
* narrower question than "is this the AI's to do". A verb that puts
|
|
31
|
+
* hands on somebody, moves them, or changes the world is not
|
|
32
|
+
* account-scoped at all -- an NPC is entitled to attack, and was
|
|
33
|
+
* equally entitled to attack a body whose owner was jacked into the
|
|
34
|
+
* Matrix and could not roll a defence.
|
|
35
|
+
*
|
|
36
|
+
* 'open' harmless in an NPC's hands -- look, say, the default.
|
|
37
|
+
* 'human-only' the player's alone; kept in lockstep with humanOnly.
|
|
38
|
+
* 'authorized' must pass utilities/npc-authorization.ts first.
|
|
39
|
+
*
|
|
40
|
+
* Declared on the command, enforced once in CommandRegistry.parse, and
|
|
41
|
+
* a test scans this directory for the world-touching verb that forgets.
|
|
42
|
+
*/
|
|
43
|
+
static npcPolicy = 'open';
|
|
44
|
+
static getNpcPolicy() {
|
|
45
|
+
// One source of truth: a verb that declares humanOnly IS human-only,
|
|
46
|
+
// whether or not it also remembered to say so here.
|
|
47
|
+
return this.humanOnly ? 'human-only' : this.npcPolicy;
|
|
48
|
+
}
|
|
24
49
|
static verb;
|
|
25
50
|
static description; // Make this static
|
|
26
51
|
actor;
|
|
@@ -132,7 +132,7 @@ export class CompileCommand extends Command {
|
|
|
132
132
|
const resist = rollPool(Math.max(1, actor.willpower + actor.resonance));
|
|
133
133
|
const fadingTaken = Math.max(0, fadingValue - resist.hits);
|
|
134
134
|
actor.performAction('goes still, eyes tracking code that is not there', 'threading a sprite');
|
|
135
|
-
this.scene.addWorldEvent(`${actor.name} compiled a sprite in ${actor.currentLocation.name}
|
|
135
|
+
this.scene.addWorldEvent(`${actor.name} compiled a sprite in ${actor.currentLocation.name}.`, { plane: 'matrix' });
|
|
136
136
|
this.logger.write(`CompileCommand: ${actor.name} ${typeKey} level ${level} -- ${mine.hits} vs ${sprite.hits}, tasks ${tasks}, fading ${fadingValue} (took ${fadingTaken}${overreach ? ' physical' : ''}).`);
|
|
137
137
|
const poolParts = [`Resonance ${actor.resonance}`, compiling > 0 ? `compiling ${compiling}` : `no compiling -1`];
|
|
138
138
|
if (actor.woundModifier < 0)
|
|
@@ -161,18 +161,18 @@ export class DecompileCommand extends Command {
|
|
|
161
161
|
const lines = [`You find ${target.name}'s thread and PULL AGAINST THE WEAVE:`];
|
|
162
162
|
if (net <= 0) {
|
|
163
163
|
lines.push(` Its code holds -- and now it KNOWS you, the touch traced straight back to your persona.`);
|
|
164
|
-
this.scene.addWorldEvent(`${actor.name} tried to decompile ${target.name} in ${room.name} -- and failed. It is alert
|
|
164
|
+
this.scene.addWorldEvent(`${actor.name} tried to decompile ${target.name} in ${room.name} -- and failed. It is alert.`, { plane: 'matrix' });
|
|
165
165
|
}
|
|
166
166
|
else {
|
|
167
167
|
const dmg = net + Math.floor(actor.resonance / 2);
|
|
168
168
|
target.takeDamage(dmg);
|
|
169
169
|
if (target.isDown()) {
|
|
170
|
-
this.scene.addWorldEvent(`${actor.name} decompiled ${target.name} in ${room.name} -- unraveled to static
|
|
170
|
+
this.scene.addWorldEvent(`${actor.name} decompiled ${target.name} in ${room.name} -- unraveled to static.`, { plane: 'matrix' });
|
|
171
171
|
this.scene.removeActor(target.name);
|
|
172
172
|
lines.push(` Thread by thread it comes APART -- ${dmg} boxes -- and ${target.name} unravels into clean static. Decompiled.`);
|
|
173
173
|
}
|
|
174
174
|
else {
|
|
175
|
-
this.scene.addWorldEvent(`${actor.name} tore at ${target.name}'s code in ${room.name}
|
|
175
|
+
this.scene.addWorldEvent(`${actor.name} tore at ${target.name}'s code in ${room.name}.`, { plane: 'matrix' });
|
|
176
176
|
lines.push(` Threads tear loose -- ${dmg} boxes: it holds together, barely, and it's coming for you.`);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -111,7 +111,7 @@ export class EditFileCommand extends Command {
|
|
|
111
111
|
room.inventory.removeItem(file.name);
|
|
112
112
|
room.plainSightItems.delete(file.name.toLowerCase());
|
|
113
113
|
this.logger.write(`${actor.name} deleted "${file.name}" from ${room.name}'s host: ${attempt.hits} v ${resist.hits}.`);
|
|
114
|
-
this.scene.addWorldEvent(`${file.name} was erased from ${host.name}'s archive
|
|
114
|
+
this.scene.addWorldEvent(`${file.name} was erased from ${host.name}'s archive.`, { plane: 'matrix' });
|
|
115
115
|
const banner = this.scene.checkErased?.(file.name, actor.name);
|
|
116
116
|
this.scene.updateStatus();
|
|
117
117
|
return [
|
|
@@ -4,6 +4,9 @@ import { sameSpot, spotOf, spotRefusal } from '../utilities/spots.js';
|
|
|
4
4
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
5
|
export class GiveCommand extends Command {
|
|
6
6
|
static verb = 'give';
|
|
7
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
8
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
9
|
+
static npcPolicy = 'authorized';
|
|
7
10
|
static description = 'Give an item or money to someone.';
|
|
8
11
|
async execute(args) {
|
|
9
12
|
if (!args)
|
|
@@ -5,6 +5,7 @@ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
|
5
5
|
import { physicalLimit, heldBy, holding, releaseGrapple } from '../utilities/grapple.js';
|
|
6
6
|
import { spotOf, ensureAtSpot } from '../utilities/spots.js';
|
|
7
7
|
import { billAction } from '../utilities/action-cost.js';
|
|
8
|
+
import { arenaOf } from '../utilities/combat-turn.js';
|
|
8
9
|
/**
|
|
9
10
|
* SUBDUING (SR5e p.195) -- "grapple"/"restrain"/"clinch": seize someone
|
|
10
11
|
* and hold them, dealing NO damage. The RAW shape, faithfully: a normal
|
|
@@ -20,6 +21,9 @@ import { billAction } from '../utilities/action-cost.js';
|
|
|
20
21
|
*/
|
|
21
22
|
export class GrappleCommand extends Command {
|
|
22
23
|
static verb = 'grapple';
|
|
24
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
25
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
26
|
+
static npcPolicy = 'authorized';
|
|
23
27
|
static description = 'Seize and restrain someone (SR5e Subduing, p.195) -- no damage, they can\'t move or fight until they break free ("struggle"). "release" lets them go. Also: "restrain", "clinch".';
|
|
24
28
|
async execute(args = []) {
|
|
25
29
|
const actor = this.actor;
|
|
@@ -113,6 +117,7 @@ export class GrappleCommand extends Command {
|
|
|
113
117
|
if (net <= 0) {
|
|
114
118
|
actor.performAction('grapples', `at ${target.name} -- and comes up with air`);
|
|
115
119
|
this.finishMeta(isPlayer, meta, lines);
|
|
120
|
+
await this.openFightOverTheGrab(target);
|
|
116
121
|
return `${crossNote}${lines.join('\n')}You lunge for ${target.name} -- they twist clear. Now they know exactly what you want.`;
|
|
117
122
|
}
|
|
118
123
|
// Strength + net hits must EXCEED the Physical limit (p.195).
|
|
@@ -122,6 +127,7 @@ export class GrappleCommand extends Command {
|
|
|
122
127
|
this.finishMeta(isPlayer, meta, lines);
|
|
123
128
|
if (grip <= limit) {
|
|
124
129
|
actor.performAction('grapples', `${target.name} -- a grip, but not a hold; they wrench loose`);
|
|
130
|
+
await this.openFightOverTheGrab(target);
|
|
125
131
|
return `${crossNote}${lines.join('\n')}You get hands on ${target.name}, but they're too solid to hold -- they wrench loose.`;
|
|
126
132
|
}
|
|
127
133
|
target.grappledBy = actor.name;
|
|
@@ -129,9 +135,52 @@ export class GrappleCommand extends Command {
|
|
|
129
135
|
actor.grappling = target.name;
|
|
130
136
|
actor.performAction('grapples', `${target.name} and locks them down -- held fast, unable to move`);
|
|
131
137
|
this.scene.addWorldEvent(`${actor.name} grappled and restrained ${target.name} in ${room.name}.`);
|
|
138
|
+
await this.openFightOverTheGrab(target);
|
|
132
139
|
this.scene.updateStatus();
|
|
133
140
|
return `${crossNote}${lines.join('\n')}You seize ${target.name} and lock the hold -- HELD FAST, no damage done. They can't move, flee, or fight until they break your grip${isPlayer ? `, and your hands are full until you "release"` : ''}.${hint(` (They escape only by beating ${net} hits on Unarmed + Strength.)`)}`;
|
|
134
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* SEIZING SOMEONE STARTS A FIGHT (aCErAfvEW7G23FWDM: "moving someone
|
|
144
|
+
* against their will is a hostile action, and should start some sort
|
|
145
|
+
* of combat").
|
|
146
|
+
*
|
|
147
|
+
* IT DID NOT, AND THAT WAS THE HOLE. Subduing is a melee attack in
|
|
148
|
+
* canon (p.195) and this command has always rolled it as one -- but
|
|
149
|
+
* `attack` opens a Combat Turn (attack.ts, Scene.startEncounter) and
|
|
150
|
+
* `grapple` never did. The only cost it paid was billAction, which is
|
|
151
|
+
* a NO-OP outside combat (action-cost.ts). So laying hands on someone
|
|
152
|
+
* in a quiet room cost nothing, started nothing, and left the target
|
|
153
|
+
* held with no turn in which to struggle -- the engine's most hostile
|
|
154
|
+
* act was also its cheapest.
|
|
155
|
+
*
|
|
156
|
+
* THE GRAB LANDS FIRST, then the fight opens. That is deliberate and
|
|
157
|
+
* it is the one place this differs from attack.ts, which opens the
|
|
158
|
+
* encounter BEFORE resolving so initiative can put someone faster
|
|
159
|
+
* ahead of the swing. A grapple is the opening act of a fight that
|
|
160
|
+
* did not exist yet -- there is no initiative to lose to, and
|
|
161
|
+
* re-plumbing this command through the phase loop to resolve a grab
|
|
162
|
+
* that has already been rolled would buy nothing but a second copy of
|
|
163
|
+
* executeInEncounter. The target answers on their phase, with
|
|
164
|
+
* "struggle", which is exactly what the turn now gives them.
|
|
165
|
+
*/
|
|
166
|
+
async openFightOverTheGrab(target) {
|
|
167
|
+
const actor = this.actor;
|
|
168
|
+
// Already fighting: the turn loop owns this and must not be
|
|
169
|
+
// re-opened underneath itself.
|
|
170
|
+
if (this.scene.encounterFor?.(actor))
|
|
171
|
+
return;
|
|
172
|
+
// Hands up is not a fight (see the surrendered branch above): a
|
|
173
|
+
// zip-tie on someone who already yielded starts nothing.
|
|
174
|
+
if (target.surrendered)
|
|
175
|
+
return;
|
|
176
|
+
if (target.isIncapacitated() || !actor.sharesCombatPlane(target))
|
|
177
|
+
return;
|
|
178
|
+
await this.scene.startEncounter(arenaOf(actor), {
|
|
179
|
+
aggressor: actor,
|
|
180
|
+
target,
|
|
181
|
+
ambush: actor.sneaking,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
135
184
|
/** NPC actors keep roll anatomy inline for their AI history; the
|
|
136
185
|
* player's goes to the Mechanics ticker (readability pass). */
|
|
137
186
|
finishMeta(isPlayer, meta, lines) {
|
|
@@ -240,7 +240,7 @@ export class HackCommand extends BypassCommand {
|
|
|
240
240
|
room.host?.spotted.add(this.actor.name);
|
|
241
241
|
this.actor.noteConditionChanged();
|
|
242
242
|
this.actor.sneaking = false;
|
|
243
|
-
this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} MADE ${this.actor.name} mid-intrusion -- alarm flagged, ice hunting
|
|
243
|
+
this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} MADE ${this.actor.name} mid-intrusion -- alarm flagged, ice hunting.`, { plane: 'matrix' });
|
|
244
244
|
this.scene.updateStatus();
|
|
245
245
|
return [
|
|
246
246
|
`The ice turns and LOOKS AT YOU -- your sleaze unravels mid-handshake.`,
|
|
@@ -324,7 +324,7 @@ export class HackCommand extends BypassCommand {
|
|
|
324
324
|
if (mode === 'attack') {
|
|
325
325
|
this.actor.sneaking = false;
|
|
326
326
|
room.hostAlert = true;
|
|
327
|
-
this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} takes a forced key -- alarm flagged, ice hunting
|
|
327
|
+
this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} takes a forced key -- alarm flagged, ice hunting.`, { plane: 'matrix' });
|
|
328
328
|
}
|
|
329
329
|
lines.push(...godLines);
|
|
330
330
|
this.scene.updateStatus();
|
|
@@ -439,7 +439,7 @@ export class HackCommand extends BypassCommand {
|
|
|
439
439
|
// knows it is under attack and starts hunting; what it does not
|
|
440
440
|
// know is whose hand did it.
|
|
441
441
|
room.hostAlert = true;
|
|
442
|
-
this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} was smashed open by brute force -- its security is down, alarms screaming
|
|
442
|
+
this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} was smashed open by brute force -- its security is down, alarms screaming.`, { plane: 'matrix' });
|
|
443
443
|
lines.push(` The host SCREAMED on the way down -- everything listening knows it fell, even if nothing saw your hand. Its ice is hunting now: it knows it was hit, not who hit it.`);
|
|
444
444
|
}
|
|
445
445
|
else {
|
|
@@ -637,7 +637,7 @@ export class HackCommand extends BypassCommand {
|
|
|
637
637
|
actor.sneaking = false;
|
|
638
638
|
if (host) {
|
|
639
639
|
host.hostAlert = true;
|
|
640
|
-
this.scene.addWorldEvent(`${device.name} flags a bad handshake -- ${hostLabel(host, { capital: true })} is awake
|
|
640
|
+
this.scene.addWorldEvent(`${device.name} flags a bad handshake -- ${hostLabel(host, { capital: true })} is awake.`, { plane: 'matrix' });
|
|
641
641
|
this.scene.updateStatus();
|
|
642
642
|
return [`${device.name} refuses the key and SAYS SO -- the node it answers to knows something is in here.${wanNote}`, ...godLines].join('\n');
|
|
643
643
|
}
|
|
@@ -9,6 +9,9 @@ import { AttackCommand } from './attack.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export class KillCommand extends AttackCommand {
|
|
11
11
|
static verb = 'kill';
|
|
12
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
13
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
14
|
+
static npcPolicy = 'authorized';
|
|
12
15
|
static description = 'Alias for "attack" -- resolves as real dice-pool combat, not an instant kill.';
|
|
13
16
|
}
|
|
14
17
|
//# sourceMappingURL=kill.js.map
|
|
@@ -23,6 +23,9 @@ import { serializeItem } from '../utilities/persistence.js';
|
|
|
23
23
|
*/
|
|
24
24
|
export class LeadCommand extends Command {
|
|
25
25
|
static verb = 'lead';
|
|
26
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
27
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
28
|
+
static npcPolicy = 'authorized';
|
|
26
29
|
static description = 'Leadership (SR5e p.141): "lead rally" (+initiative, whole party), "lead direct <companion>" (teamwork dice on their next attack), "lead inspire" (primes the party\'s next defense). Bare "lead" shows the party.';
|
|
27
30
|
leadPool() {
|
|
28
31
|
const skill = this.actor.skillRating('leadership');
|
|
@@ -45,6 +45,9 @@ const EMPTY_ROSTER_PHRASES = Object.values({
|
|
|
45
45
|
*/
|
|
46
46
|
export class OrderCommand extends Command {
|
|
47
47
|
static verb = 'order';
|
|
48
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
49
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
50
|
+
static npcPolicy = 'authorized';
|
|
48
51
|
static description = 'Command a companion: exact commands run instantly ("order spirit attack Krow"); anything else becomes a standing DIRECTIVE its AI pursues ("order agent to find Mr. Krow"). Spirit tasks cost a service.';
|
|
49
52
|
async execute(args = []) {
|
|
50
53
|
const game = this.game;
|
|
@@ -15,6 +15,9 @@ import { NPC } from '../models/npc.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export class PalmCommand extends Command {
|
|
17
17
|
static verb = 'palm';
|
|
18
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
19
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
20
|
+
static npcPolicy = 'authorized';
|
|
18
21
|
static description = 'Sleight of hand (Palming + Agility): lift an item without being seen -- opposed by the sharpest eyes in the room. Winning leaves no witnesses; losing means they watched your hand the whole way.';
|
|
19
22
|
async execute(args = []) {
|
|
20
23
|
const actor = this.actor;
|
|
@@ -10,6 +10,9 @@ import { AttackCommand } from './attack.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export class SubdueCommand extends AttackCommand {
|
|
12
12
|
static verb = 'subdue';
|
|
13
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
14
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
15
|
+
static npcPolicy = 'authorized';
|
|
13
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.';
|
|
14
17
|
async execute(args = []) {
|
|
15
18
|
// A chokehold needs hands and a neck: no grappling as a Matrix persona
|
|
@@ -8,6 +8,9 @@ import { Category } from '../types/shared/item-enum.js';
|
|
|
8
8
|
import { billAction } from '../utilities/action-cost.js';
|
|
9
9
|
export class TakeCommand extends Command {
|
|
10
10
|
static verb = 'take';
|
|
11
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
12
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
13
|
+
static npcPolicy = 'authorized';
|
|
11
14
|
static description = 'Pick up an item. "take all" sweeps everything here -- but only after you have searched the room.';
|
|
12
15
|
async execute(args) {
|
|
13
16
|
if (!args)
|
|
@@ -526,5 +526,56 @@
|
|
|
526
526
|
// months, while the web client rendered it perfectly. 134 tags are
|
|
527
527
|
// now {light-blue-fg}. The site's render-tags.ts maps that back
|
|
528
528
|
// to a real CSS keyword, so the pin must carry both halves.
|
|
529
|
-
|
|
529
|
+
// 1.58.0 (2026-09-15): THE AI PROPOSES, THE ENGINE DISPOSES
|
|
530
|
+
// (aCErAfvEW7G23FWDM, reporter: "the only way to solve these is with
|
|
531
|
+
// engine enforcement against AI allowed actions"). An NPC beat was
|
|
532
|
+
// free prose: any line of it beginning "[COMMAND] " went to the SAME
|
|
533
|
+
// registry the player types into with NO filter on the command text,
|
|
534
|
+
// and every other line was printed to the player as fact. So the
|
|
535
|
+
// engine could neither refuse an act nor even SEE one asserted.
|
|
536
|
+
// - WHAT AN NPC MAY DO. Command.npcPolicy, the same declarative shape
|
|
537
|
+
// as humanOnly, enforced once in CommandRegistry.parse and backed by
|
|
538
|
+
// utilities/npc-authorization.ts. A hands-on verb against a body
|
|
539
|
+
// whose owner is jacked in, projecting, or already down is refused
|
|
540
|
+
// -- SR5 resolves Subduing (p.195) as an OPPOSED melee attack, and
|
|
541
|
+
// there is nothing there to oppose. parse() grew a third outcome:
|
|
542
|
+
// null still means "no such verb", a REFUSAL carries a reason, and
|
|
543
|
+
// NPC.act feeds that reason back so the model hears the boundary.
|
|
544
|
+
// - WHAT A BEAT MAY CLAIM. utilities/narration-limits.ts: narration
|
|
545
|
+
// may do anything with the NPC's own body and nothing with anybody
|
|
546
|
+
// else's. A refused line is suppressed AND answered, so the next
|
|
547
|
+
// beat can commit to "[COMMAND] grapple X" and let the dice decide.
|
|
548
|
+
// - A GRAB IS A FIGHT. commands/grapple.ts never called startEncounter
|
|
549
|
+
// -- it billed through billAction, a no-op outside combat -- so the
|
|
550
|
+
// most hostile act in the engine was also its cheapest, and left the
|
|
551
|
+
// target held with no turn in which to struggle. Every outcome now
|
|
552
|
+
// opens a Combat Turn. `grapples` joins the PROVOCATION ladder, and
|
|
553
|
+
// turnHostile() is the runtime mirror standDown never had: `hostile`
|
|
554
|
+
// was written at BOOT and nowhere else, so an NPC who was not
|
|
555
|
+
// opposition when the scene was built had no path to becoming it.
|
|
556
|
+
// - WHAT AN NPC MAY KNOW. Seven blocks of the brief were scene-globals
|
|
557
|
+
// handed to everybody. World events now carry WHERE they happened
|
|
558
|
+
// and Matrix-plane events reach only the Matrix (a bartender could
|
|
559
|
+
// read "the host MADE the intruder -- ice hunting"); ephemerals get
|
|
560
|
+
// no grapevine at all; the run's payout goes to the parties to the
|
|
561
|
+
// deal. The raw, unparsed model reply is no longer written into the
|
|
562
|
+
// NPC's memory -- only what actually reached the world is.
|
|
563
|
+
// - NOBODY SAYS IT TWICE. utilities/beat-echo.ts, the engine's first
|
|
564
|
+
// dedupe of emitted text of any kind. Containment rather than
|
|
565
|
+
// Jaccard, with a lower bar when a speaker opens the same way twice;
|
|
566
|
+
// companion link traffic is never judged (two drones clearing two
|
|
567
|
+
// rooms say near-identical things on purpose).
|
|
568
|
+
// - FACTIONS, catalog-backed (site v20, kind 'faction'). factions.ts
|
|
569
|
+
// holds the rules; the rows say who exists. First rule reading them:
|
|
570
|
+
// a Johnson will not lay out terms in front of a badge she can SEE.
|
|
571
|
+
// An unknown faction is no opinion, so a catalog without the rows
|
|
572
|
+
// behaves exactly as this engine did before them.
|
|
573
|
+
// - NEW CATALOG LANE `hearing`: hearingRangeMeters read the flat
|
|
574
|
+
// `perception` pool, so CYBEREYES extended how far you could hear.
|
|
575
|
+
// Audio Enhancement I-III and Spatial Recognizer are rows at last
|
|
576
|
+
// (p.445, p.454; Chrome Flesh p.223).
|
|
577
|
+
// - BENCH: repro schema 2.6.0 adds npcs[].hostile. A fixture could not
|
|
578
|
+
// express a hostile PERSON -- only `ice` -- so no meat-side
|
|
579
|
+
// hostility report could be benched at all.
|
|
580
|
+
export const ENGINE_VERSION = '1.58.0';
|
|
530
581
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { factionFor } from './utilities/catalog.js';
|
|
2
|
+
/**
|
|
3
|
+
* THE SHADOW ECONOMY'S OWN SLUG. A Johnson, a fixer and a runner are
|
|
4
|
+
* not an organisation, but they are a SIDE -- the deniable one -- and
|
|
5
|
+
* the rules below need a name for it. Matches the catalog row.
|
|
6
|
+
*/
|
|
7
|
+
export const SHADOWS = 'shadows';
|
|
8
|
+
/**
|
|
9
|
+
* WHAT FACTION IS THIS ACTOR, if the engine can tell.
|
|
10
|
+
*
|
|
11
|
+
* An explicit `faction` on the NPC wins, always -- a seed or a
|
|
12
|
+
* generator that says so is the author speaking. Failing that, the one
|
|
13
|
+
* thing the engine already knew about strangers is read: the ephemeral
|
|
14
|
+
* KIND that spawned them (utilities/ephemeral.ts). A sweep officer is
|
|
15
|
+
* an 'authority' body and was already, in every way that mattered, the
|
|
16
|
+
* law; this just gives the engine the word for it.
|
|
17
|
+
*
|
|
18
|
+
* DERIVING IS DELIBERATELY SHALLOW. It maps the three spawn archetypes
|
|
19
|
+
* the engine already has and stops. Guessing a faction from a name or a
|
|
20
|
+
* description would be the kind of inference that looks clever in a
|
|
21
|
+
* demo and puts a gang tattoo on a bartender in play.
|
|
22
|
+
*/
|
|
23
|
+
export function factionOf(actor) {
|
|
24
|
+
const npc = actor;
|
|
25
|
+
if (npc.faction)
|
|
26
|
+
return npc.faction;
|
|
27
|
+
switch (npc.ephemeral?.kind) {
|
|
28
|
+
case 'authority': return 'knight-errant';
|
|
29
|
+
case 'mob': return 'ancients';
|
|
30
|
+
case 'bystander': return 'civilians';
|
|
31
|
+
default: return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* HOW `a` REGARDS `b`: -3 (blood) .. +3 (family), 0 for no opinion.
|
|
36
|
+
*
|
|
37
|
+
* ASYMMETRIC BY DESIGN. A gang can hate a corp that has never heard of
|
|
38
|
+
* it, so this reads the standing of the faction doing the JUDGING and
|
|
39
|
+
* never averages the two. Where a faction is silent about another, the
|
|
40
|
+
* SORTS answer instead -- the law and the shadows are structurally
|
|
41
|
+
* opposed whether or not anybody wrote a row about it, and two gangs
|
|
42
|
+
* are rivals until someone says otherwise.
|
|
43
|
+
*/
|
|
44
|
+
export function standingBetween(a, b) {
|
|
45
|
+
if (!a || !b)
|
|
46
|
+
return 0;
|
|
47
|
+
if (a === b)
|
|
48
|
+
return 3;
|
|
49
|
+
const rowA = factionFor(a);
|
|
50
|
+
const explicit = rowA?.standing?.[b];
|
|
51
|
+
if (typeof explicit === 'number')
|
|
52
|
+
return explicit;
|
|
53
|
+
// A DIVISION IS ITS PARENT'S. Knight Errant IS Ares Macrotechnology's
|
|
54
|
+
// security arm (p.38), so the two are one house unless a row says
|
|
55
|
+
// otherwise above -- and nobody writes "we like ourselves" into a
|
|
56
|
+
// standing table, which is exactly why this cannot be left to the
|
|
57
|
+
// declared numbers.
|
|
58
|
+
if (rowA?.parent === b)
|
|
59
|
+
return 3;
|
|
60
|
+
// And its opinion of a THIRD party is the house's, where it has not
|
|
61
|
+
// formed its own.
|
|
62
|
+
if (rowA?.parent) {
|
|
63
|
+
const inherited = factionFor(rowA.parent)?.standing?.[b];
|
|
64
|
+
if (typeof inherited === 'number')
|
|
65
|
+
return inherited;
|
|
66
|
+
}
|
|
67
|
+
const rowB = factionFor(b);
|
|
68
|
+
if (!rowA || !rowB)
|
|
69
|
+
return 0;
|
|
70
|
+
if (rowA.sort === 'law' && (rowB.sort === 'shadow' || rowB.sort === 'gang' || rowB.sort === 'syndicate'))
|
|
71
|
+
return -2;
|
|
72
|
+
if ((rowA.sort === 'shadow' || rowA.sort === 'gang' || rowA.sort === 'syndicate') && rowB.sort === 'law')
|
|
73
|
+
return -2;
|
|
74
|
+
if (rowA.sort === 'gang' && rowB.sort === 'gang')
|
|
75
|
+
return -1;
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* WOULD BUSINESS BE DISCUSSED IN FRONT OF THIS ACTOR?
|
|
80
|
+
*
|
|
81
|
+
* The Johnson's question, and the one the reported session turned on.
|
|
82
|
+
* Three ways to be safe to talk in front of, and a stranger with no
|
|
83
|
+
* faction at all is one of them -- an unknown face in a bar is the
|
|
84
|
+
* normal condition of a bar, and treating every unplaceable body as a
|
|
85
|
+
* threat would mean no deal was ever struck anywhere.
|
|
86
|
+
*/
|
|
87
|
+
export function wouldTalkBusinessNear(dealer, witness) {
|
|
88
|
+
const theirs = factionOf(witness);
|
|
89
|
+
if (!theirs)
|
|
90
|
+
return true;
|
|
91
|
+
const row = factionFor(theirs);
|
|
92
|
+
// A faction that keeps its mouth shut is not a problem to talk in
|
|
93
|
+
// front of, whatever it thinks of you: the syndicates and the gangs
|
|
94
|
+
// are IN the deniable economy, not auditing it.
|
|
95
|
+
if (row?.discreet)
|
|
96
|
+
return true;
|
|
97
|
+
return standingBetween(dealer ?? SHADOWS, theirs) > -2;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* THE FIRST BODY IN THIS ROOM THE DEAL CANNOT BE STRUCK IN FRONT OF,
|
|
101
|
+
* or undefined when the room will do.
|
|
102
|
+
*
|
|
103
|
+
* Lives here rather than in game.ts so it can be tested against a
|
|
104
|
+
* catalog the way every other faction rule is -- and because "who
|
|
105
|
+
* spoils a deal" is a faction question, not a Game one.
|
|
106
|
+
*
|
|
107
|
+
* SIGHT IS THE TEST, NOT EARSHOT, and getting that backwards was the
|
|
108
|
+
* first thing tried. Earshot is wrong twice over: a badge who cannot
|
|
109
|
+
* make out the WORDS can still make out the MEETING, and canHearSpeech
|
|
110
|
+
* deliberately refuses an unplaced listener -- which is precisely the
|
|
111
|
+
* patrol officer WORKING the room, the exact witness the report is
|
|
112
|
+
* about. The reporter's own words are about presence: "there wouldn't
|
|
113
|
+
* be a 'meet' if there was a police officer STANDING THERE."
|
|
114
|
+
*
|
|
115
|
+
* @param sees the engine's own "can this observer see that actor"
|
|
116
|
+
* (utilities/spots.ts wouldNotice), passed in so this
|
|
117
|
+
* module stays free of the spatial layer.
|
|
118
|
+
*/
|
|
119
|
+
export function businessWitness(client, others, sees) {
|
|
120
|
+
const dealer = factionOf(client) ?? SHADOWS;
|
|
121
|
+
for (const other of others) {
|
|
122
|
+
if (other === client)
|
|
123
|
+
continue;
|
|
124
|
+
if (other.isIncapacitated?.())
|
|
125
|
+
continue;
|
|
126
|
+
if (!client.canPerceive(other))
|
|
127
|
+
continue;
|
|
128
|
+
if (!sees(client, other))
|
|
129
|
+
continue;
|
|
130
|
+
if (!wouldTalkBusinessNear(dealer, other))
|
|
131
|
+
return other;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=factions.js.map
|
|
@@ -162,6 +162,23 @@ export function buildReproScene(fixture) {
|
|
|
162
162
|
throw new ReproFixtureError(`"${misplacedIce.name}" is ice, so it cannot also stand in a doorway, guard, circulate or carry a gun. `
|
|
163
163
|
+ `Ice lives in the host, not on the floor -- drop the meat-side flags, or drop "ice".`);
|
|
164
164
|
}
|
|
165
|
+
// HOSTILITY SAID TWICE IS DECORATION ONCE (aCErAfvEW7G23FWDM). Ice is
|
|
166
|
+
// opposition BY CONSTRUCTION -- scene-factory makes a matrix-plane
|
|
167
|
+
// actor hostile unless the seed says otherwise -- so a fixture
|
|
168
|
+
// declaring both is asserting the flag rather than the rule, which is
|
|
169
|
+
// the objection the `ice` comment in repro.ts raises and is right
|
|
170
|
+
// about. A street doc who opens fire on the patient is not a fixture
|
|
171
|
+
// either; it is two fixtures wearing one name.
|
|
172
|
+
const hostileIce = npcs.find(n => n.hostile && n.ice);
|
|
173
|
+
if (hostileIce) {
|
|
174
|
+
throw new ReproFixtureError(`"${hostileIce.name}" is asked to be both ice and hostile. Ice is opposition by construction -- `
|
|
175
|
+
+ `drop "hostile", or drop "ice" if you meant a hostile person.`);
|
|
176
|
+
}
|
|
177
|
+
const hostileDoc = npcs.find(n => n.hostile && n.doc);
|
|
178
|
+
if (hostileDoc) {
|
|
179
|
+
throw new ReproFixtureError(`"${hostileDoc.name}" is asked to be both a street doc and hostile. A doc who opens fire on the `
|
|
180
|
+
+ `patient is two fixtures -- split them, or pick one.`);
|
|
181
|
+
}
|
|
165
182
|
// A SEED HAS ONE PLANE. astral maps to 'astral' the same way ice maps
|
|
166
183
|
// to 'matrix' (npc-seed.ts) -- asking for both is asking the builder
|
|
167
184
|
// to pick a lie.
|
|
@@ -379,6 +396,13 @@ export function buildReproScene(fixture) {
|
|
|
379
396
|
// string, which would read as a spot whose name is nothing.
|
|
380
397
|
...(npc.circulating ? {} : { startSpot: inDoorway ? REPRO_DOORWAY_SPOT : REPRO_FLOOR_SPOT }),
|
|
381
398
|
guarding: npc.guarding === true,
|
|
399
|
+
// OPPOSITION, IN THE MEAT (aCErAfvEW7G23FWDM). Straight onto
|
|
400
|
+
// the seed's own `hostile`, which scene-factory reads and the
|
|
401
|
+
// hostile-contact ladder then drives by itself -- notice,
|
|
402
|
+
// contact, provocation, patience. Emitted only when asked, so
|
|
403
|
+
// a fixture that says nothing builds the same peaceable NPC it
|
|
404
|
+
// always did.
|
|
405
|
+
...(npc.hostile ? { hostile: true } : {}),
|
|
382
406
|
lifestyleChoices: [],
|
|
383
407
|
combat: {
|
|
384
408
|
body: 4, agility: 4, reaction: 4, combatSkill: 3, strength: 4,
|