@maka/maka-cli 5.197.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/ar.js +6 -6
- 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/download.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +5 -5
- 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 +8 -8
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
- 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/look.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
- 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/say.js +19 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/shout.js +69 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
- 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/commands/talk.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +75 -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 +66 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +165 -14
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +90 -57
- 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/ar.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 +153 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hostile-contact.js +48 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +1 -1
- 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/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
- 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.",
|
|
@@ -64,14 +64,14 @@ export class ArCommand extends Command {
|
|
|
64
64
|
return `Overlay back on -- over nothing. No commlink, no deck, no AR: you're reading the street with your eyes like it's 2050.`;
|
|
65
65
|
}
|
|
66
66
|
const tags = aroTags(this.scene, actor, actor.currentLocation);
|
|
67
|
-
const head = `{
|
|
67
|
+
const head = `{light-blue-fg}The overlay blinks back in.{/light-blue-fg}`;
|
|
68
68
|
// Turning AR back on while silent is the other moment the question
|
|
69
69
|
// lands -- you have just done the thing you were unsure was allowed.
|
|
70
70
|
const note = silentAndSeeingNote(actor);
|
|
71
71
|
if (tags.length === 0) {
|
|
72
|
-
return [head, `{
|
|
72
|
+
return [head, `{light-blue-fg}Nothing here is talking -- a dead patch of street.{/light-blue-fg}`, note].filter(Boolean).join('\n');
|
|
73
73
|
}
|
|
74
|
-
return [head, ...tags.map(t => ` {
|
|
74
|
+
return [head, ...tags.map(t => ` {light-blue-fg}${t}{/light-blue-fg}`), note].filter(Boolean).join('\n');
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
@@ -105,11 +105,11 @@ export class ArosCommand extends Command {
|
|
|
105
105
|
const note = silentAndSeeingNote(actor);
|
|
106
106
|
const tags = aroTags(this.scene, actor, actor.currentLocation);
|
|
107
107
|
if (tags.length === 0) {
|
|
108
|
-
return [`{
|
|
108
|
+
return [`{light-blue-fg}Nothing here is talking -- a dead patch of street.{/light-blue-fg}`, note].filter(Boolean).join('\n');
|
|
109
109
|
}
|
|
110
110
|
return [
|
|
111
|
-
`{
|
|
112
|
-
...tags.map(t => ` {
|
|
111
|
+
`{light-blue-fg}AR OVERLAY:{/light-blue-fg}`,
|
|
112
|
+
...tags.map(t => ` {light-blue-fg}${t}{/light-blue-fg}`),
|
|
113
113
|
note,
|
|
114
114
|
].filter(Boolean).join('\n');
|
|
115
115
|
}
|
|
@@ -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
|
}
|
|
@@ -92,7 +92,7 @@ export class DownloadCommand extends Command {
|
|
|
92
92
|
// is that nobody could find the file should say what it can see.
|
|
93
93
|
const query = args.join(' ').trim();
|
|
94
94
|
if (query.length === 0) {
|
|
95
|
-
const lines = [`{
|
|
95
|
+
const lines = [`{light-blue-fg}Within reach to pull down:{/light-blue-fg}`];
|
|
96
96
|
for (const i of loose)
|
|
97
97
|
lines.push(` ${planeTintItemName(i, actor.plane)} -- loose on the grid here`);
|
|
98
98
|
for (const c of carried)
|
|
@@ -140,11 +140,11 @@ export class DownloadCommand extends Command {
|
|
|
140
140
|
this.scene.updateStatus();
|
|
141
141
|
return fromSprite
|
|
142
142
|
? [
|
|
143
|
-
`{
|
|
143
|
+
`{light-blue-fg}${fromSprite.npc.name} opens its hands and the data pours out of them.{/light-blue-fg}`,
|
|
144
144
|
`You pull {bold}{underline}${item.name}{/underline}{/bold} down onto a chip -- meat-world cargo now.${hint(` (You can "give ${item.name}" to somebody standing in front of you.)`)}`,
|
|
145
145
|
].join('\n')
|
|
146
146
|
: [
|
|
147
|
-
`{
|
|
147
|
+
`{light-blue-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down off the grid -- written to a chip.{/light-blue-fg}`,
|
|
148
148
|
hint(`(Meat-world cargo now: carry it, stash it, or "give" it to somebody.)`),
|
|
149
149
|
].join('\n');
|
|
150
150
|
}
|
|
@@ -111,11 +111,11 @@ 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 [
|
|
118
|
-
`{
|
|
118
|
+
`{light-blue-fg}${file.name} comes apart under your hand -- sectors zeroed, index entry gone, the host's own checksum rewritten to agree. ${hostLabel(room, { capital: true })} no longer has it.{/light-blue-fg}`,
|
|
119
119
|
...(banner ? [banner] : []),
|
|
120
120
|
].join('\n');
|
|
121
121
|
}
|
|
@@ -64,7 +64,7 @@ export class EnterHostCommand extends Command {
|
|
|
64
64
|
const marks = target.hostMarksBy.get(actor.name) ?? 0;
|
|
65
65
|
if (marks < 1 && !target.hostCracked && !target.hostSanctioned) {
|
|
66
66
|
return [
|
|
67
|
-
`{
|
|
67
|
+
`{light-blue-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/light-blue-fg}`,
|
|
68
68
|
hint(`A host opens to anyone holding a MARK on it (p.239) -- "mark ${target.name}" softens it, or "hack" takes one by force.`),
|
|
69
69
|
].join('\n');
|
|
70
70
|
}
|
|
@@ -85,7 +85,7 @@ export class EnterHostCommand extends Command {
|
|
|
85
85
|
// ONE LINE (2026-09-13, "too much text"): the crossing, and the
|
|
86
86
|
// coaching behind the hints toggle.
|
|
87
87
|
return [
|
|
88
|
-
`{
|
|
88
|
+
`{light-blue-fg}You cross into ${hostLabel(target, { capital: false })} -- the outside grid folds shut behind you.{/light-blue-fg}`,
|
|
89
89
|
hint(`(In here there is only what the host keeps. "look" reads it; "exit" puts you back on the grid.)`),
|
|
90
90
|
].filter(Boolean).join('\n');
|
|
91
91
|
}
|
|
@@ -110,14 +110,14 @@ export class EnterHostCommand extends Command {
|
|
|
110
110
|
const over = hostOver(gridVicinity(this.actor)) ?? hostOver(here);
|
|
111
111
|
if (over)
|
|
112
112
|
return over;
|
|
113
|
-
return `{
|
|
113
|
+
return `{light-blue-fg}Thin grid here -- no host stands over ${gridVicinity(this.actor).name}.{/light-blue-fg} ${hint(`Overhead: ${roster} -- "enter <name>" crosses into any of them from here.`)}`;
|
|
114
114
|
}
|
|
115
115
|
const host = resolveHostInReach(rooms, this.actor, named);
|
|
116
116
|
if (host)
|
|
117
117
|
return host;
|
|
118
118
|
const street = Object.values(rooms).find(r => r.name.toLowerCase() === named.toLowerCase());
|
|
119
119
|
if (street)
|
|
120
|
-
return `{
|
|
120
|
+
return `{light-blue-fg}${street.name} has no host over it -- nothing to enter. Overhead: ${roster}.{/light-blue-fg}`;
|
|
121
121
|
return `No host called "${named}" overhead. ${hint(`Overhead: ${roster}.`)}`;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -152,7 +152,7 @@ export class ExitHostCommand extends Command {
|
|
|
152
152
|
actor.performAction('exits a host', `icon surfacing out of ${host.name}'s architecture`, { quiet: this.scene.isHumanControlled(actor) });
|
|
153
153
|
this.logger.write(`${actor.name} exited the host at ${host.name}.`);
|
|
154
154
|
this.scene.updateStatus();
|
|
155
|
-
return `{
|
|
155
|
+
return `{light-blue-fg}You step back out of ${hostLabel(host, { capital: false })} onto the grid; the host seals behind you. Your body still sits in ${actor.currentLocation.name}.{/light-blue-fg}`;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
//# sourceMappingURL=enter-host.js.map
|
|
@@ -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.`,
|
|
@@ -317,14 +317,14 @@ export class HackCommand extends BypassCommand {
|
|
|
317
317
|
if (marksNow >= 1)
|
|
318
318
|
lines.push(` One mark is the door: "enter" walks your persona inside (p.239).`);
|
|
319
319
|
if (perception > 0)
|
|
320
|
-
lines.push(` {
|
|
320
|
+
lines.push(` {light-blue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/light-blue-fg}`);
|
|
321
321
|
if (forcedDv > 0)
|
|
322
322
|
lines.push(` The forcing burns ${forcedDv} DV into the architecture -- a host has no boards to brick, so it only announces you louder.`);
|
|
323
323
|
lines.push(` ${MAX_MARKS - marksNow} more and its architecture stops asking who you are.${hint(` ("hack for ${MAX_MARKS - marksNow}" reaches for the rest at once -- ${declarationPenalty(MAX_MARKS - marksNow, this.actor.hasQuality(GO_BIG_QUALITY))} dice.)`)}`);
|
|
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();
|
|
@@ -388,13 +388,13 @@ export class HackCommand extends BypassCommand {
|
|
|
388
388
|
lines.push(` ${iconLocks.length} ${iconLocks.length === 1 ? 'lock hangs' : 'locks hang'} off it, still sealed -- each its own icon behind its own rating: ${iconLocks.map(d => d.name).join(', ')}.${hint(` ("hack <lock>" from in here keys one; the host's key is not the door's.)`)}`);
|
|
389
389
|
}
|
|
390
390
|
if (vaultedFiles.length > 0) {
|
|
391
|
-
lines.push(` {
|
|
391
|
+
lines.push(` {light-blue-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/light-blue-fg}`);
|
|
392
392
|
}
|
|
393
393
|
// AND WHAT THE CRACK DID NOT REACH: a wired site's data sits on an
|
|
394
394
|
// air-gapped box the host never held (p.233) -- say so, or the next
|
|
395
395
|
// "take" reads as a contradiction.
|
|
396
396
|
if (room.offlineServer && room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
|
|
397
|
-
lines.push(` {
|
|
397
|
+
lines.push(` {light-blue-fg}Nothing on the grid side of the vault -- whatever this place keeps sits on the ${room.offlineServer}, an air-gapped box with no Matrix presence. Your BODY has to be in ${room.name} to cable in (p.233).{/light-blue-fg}`);
|
|
398
398
|
}
|
|
399
399
|
// THE FEEDS ARE A REAL GAIN AND WERE NEVER MENTIONED
|
|
400
400
|
// (BpR5cjhYuvgCyDSZc, 7aNWnDSHeZMYmS2De: "what have I gained?").
|
|
@@ -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
|
}
|
|
@@ -683,7 +683,7 @@ export class HackCommand extends BypassCommand {
|
|
|
683
683
|
else {
|
|
684
684
|
const perception = freeMatrixPerceptionHits(netHits);
|
|
685
685
|
if (perception > 0)
|
|
686
|
-
lines.push(` {
|
|
686
|
+
lines.push(` {light-blue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what it guards (p.240).{/light-blue-fg}`);
|
|
687
687
|
}
|
|
688
688
|
// TWO MARKS COMMAND IT (p.238 Control Device: 1 mark for a Free
|
|
689
689
|
// action, 2 for a Simple, 3 for a Complex -- and unlocking a maglock
|
|
@@ -101,7 +101,7 @@ export class HideCommand extends Command {
|
|
|
101
101
|
this.scene.addWorldEvent(`${host.name}'s ice lost ${actor.name} in the traffic.`);
|
|
102
102
|
this.scene.updateStatus();
|
|
103
103
|
return [
|
|
104
|
-
`{
|
|
104
|
+
`{light-blue-fg}You drop into the host's own traffic and become one more packet in a million. ${hostLabel(room, { capital: true })} loses you.{/light-blue-fg}`,
|
|
105
105
|
` Its ice has nothing to swing at -- but the host is still awake, and Patrol is looking every action now.${hint(` ("silent" keeps a sweep from seeing you for free; "search" is possible while it looks.)`)}`,
|
|
106
106
|
...god.lines,
|
|
107
107
|
].join('\n');
|
|
@@ -71,7 +71,7 @@ export class HopCommand extends Command {
|
|
|
71
71
|
actor.performAction('hops grids', `persona blinking across to ${target.name}`);
|
|
72
72
|
this.logger.write(`${actor.name} hopped to ${target.key} (legal, ${tier}).`);
|
|
73
73
|
this.scene.updateStatus();
|
|
74
|
-
return `{
|
|
74
|
+
return `{light-blue-fg}The sky changes texture and you're on ${target.name}.{/light-blue-fg}${target.userPenalty ? ` ${hint(`Public grid: -2 on everything you do out here.`)}` : ''}${await land()}`;
|
|
75
75
|
}
|
|
76
76
|
// ILLEGAL HOP (p.240): Hack on the Fly against the grid itself.
|
|
77
77
|
const burned = actor.burnedAttributeRefusal('sleaze');
|
|
@@ -105,7 +105,7 @@ export class HopCommand extends Command {
|
|
|
105
105
|
this.logger.write(`${actor.name} hopped to ${target.key} (illegal, ${attempt.hits} v ${defence.hits}).`);
|
|
106
106
|
this.scene.updateStatus();
|
|
107
107
|
return [
|
|
108
|
-
`{
|
|
108
|
+
`{light-blue-fg}You slip through a seam nobody sold you and come up on ${target.name}.{/light-blue-fg}`,
|
|
109
109
|
...bill.lines,
|
|
110
110
|
...(isLooseEnd ? [await land()] : []),
|
|
111
111
|
].join('\n');
|
|
@@ -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');
|
|
@@ -387,7 +387,7 @@ export class LookCommand extends Command {
|
|
|
387
387
|
const dim = this.scene.getActorsInRoom(far)
|
|
388
388
|
.find(a => a !== this.actor && a.name.toLowerCase() === target.toLowerCase() && this.actor.canPerceive(a));
|
|
389
389
|
if (dim)
|
|
390
|
-
return `${this.inspectPersona(dim)}\n{
|
|
390
|
+
return `${this.inspectPersona(dim)}\n{light-blue-fg} -- a far icon, ${far.name}${noiseNote(noise)}{/light-blue-fg}`;
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
// 3. THE HOST, as a thing you can name. gridIcons already renders it
|
|
@@ -404,8 +404,8 @@ export class LookCommand extends Command {
|
|
|
404
404
|
// `deck` reads out your own array.
|
|
405
405
|
const a = namedHost.hostAttributes();
|
|
406
406
|
return [
|
|
407
|
-
`{
|
|
408
|
-
`{
|
|
407
|
+
`{light-blue-fg}${hostIcon}{/light-blue-fg}`,
|
|
408
|
+
`{light-blue-fg} Attack ${a.attack} Sleaze ${a.sleaze} Data Processing ${a.dataProcessing} Firewall ${a.firewall}{/light-blue-fg}`,
|
|
409
409
|
].join('\n');
|
|
410
410
|
}
|
|
411
411
|
}
|
|
@@ -417,7 +417,7 @@ export class LookCommand extends Command {
|
|
|
417
417
|
if (matchesCameraName(target)) {
|
|
418
418
|
const eye = gridIcons(this.scene, this.actor, room).find(i => i.startsWith('◎'));
|
|
419
419
|
if (eye)
|
|
420
|
-
return `{
|
|
420
|
+
return `{light-blue-fg}${eye}{/light-blue-fg}`;
|
|
421
421
|
// Named correctly, and there is simply nothing here to name.
|
|
422
422
|
// Worth its own line: the old answer said the WORDS were wrong,
|
|
423
423
|
// which sent the player hunting for a synonym that never
|
|
@@ -533,7 +533,7 @@ export class LookCommand extends Command {
|
|
|
533
533
|
* there's a deck anywhere in the shape of it.
|
|
534
534
|
*/
|
|
535
535
|
inspectPersona(target) {
|
|
536
|
-
const lines = [`{
|
|
536
|
+
const lines = [`{light-blue-fg}◆ ${target.name} -- a persona on this grid.{/light-blue-fg}`];
|
|
537
537
|
const companion = this.scene.ownerGame?.companions.find(c => c.npc === target);
|
|
538
538
|
if (companion) {
|
|
539
539
|
lines.push(`Yours -- it goes where you go, and it answers when you speak.`);
|
|
@@ -93,7 +93,7 @@ export class MapCommand extends Command {
|
|
|
93
93
|
actor.insideHost
|
|
94
94
|
? `THE GRID -- INSIDE ${hostLabel(actor.insideHost, { capital: true }).toUpperCase()}`
|
|
95
95
|
: `THE GRID -- ${actor.currentGrid ? `on ${actor.currentGrid.name}, ` : ''}jacked in from ${body.name}`,
|
|
96
|
-
`{
|
|
96
|
+
`{light-blue-fg}Black flatland under a black sky. No streets out here -- only icons. The hosts hang overhead; ${actor.insideHost ? '"exit" puts you back under them' : '"go <host>" (or "go <number>") crosses into one you hold a mark on, from anywhere'}.{/light-blue-fg}`,
|
|
97
97
|
];
|
|
98
98
|
if (hosts.length === 0) {
|
|
99
99
|
lines.push(' Nothing overhead -- no host stands over this district.');
|
|
@@ -51,7 +51,7 @@ export class MarkCommand extends Command {
|
|
|
51
51
|
screen: this.screen,
|
|
52
52
|
game: this.game,
|
|
53
53
|
}).execute(args);
|
|
54
|
-
return `{
|
|
54
|
+
return `{light-blue-fg}Placing a mark IS ${action} -- running it (p.238/p.240).{/light-blue-fg}\n${result}`;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
//# sourceMappingURL=mark.js.map
|
|
@@ -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;
|
|
@@ -87,6 +87,7 @@ export class SayCommand extends Command {
|
|
|
87
87
|
}
|
|
88
88
|
const message = args?.join(' ');
|
|
89
89
|
const currentPlayer = this.actor;
|
|
90
|
+
let farOff = "";
|
|
90
91
|
const scene = this.context.scene;
|
|
91
92
|
// Speaking out loud ends a sneak (see sneak.ts).
|
|
92
93
|
currentPlayer.sneaking = false;
|
|
@@ -245,8 +246,23 @@ export class SayCommand extends Command {
|
|
|
245
246
|
// (matrix-roster.ts); a body's reach the room, as always.
|
|
246
247
|
if (currentPlayer.plane === 'matrix')
|
|
247
248
|
speakOnTheGrid(this.scene, currentPlayer, message);
|
|
248
|
-
else
|
|
249
|
-
currentPlayer.currentLocation.say(currentPlayer, message);
|
|
249
|
+
else {
|
|
250
|
+
const heard = currentPlayer.currentLocation.say(currentPlayer, message);
|
|
251
|
+
// NOBODY CLOSE ENOUGH is now a thing that can happen, and has to
|
|
252
|
+
// be said (en3p6eCpiRuqsTSKm). Speech used to escalate to a
|
|
253
|
+
// room-wide shout the moment nobody shared your spot, so this
|
|
254
|
+
// case did not exist; with `say` bounded by each listener's
|
|
255
|
+
// hearing range it does, and a player whose words reached nobody
|
|
256
|
+
// would otherwise see their own echo and assume they landed.
|
|
257
|
+
//
|
|
258
|
+
// Only when there was somebody TO miss: an empty room is not a
|
|
259
|
+
// failure to be heard, it is an empty room, and nagging about it
|
|
260
|
+
// every time a runner mutters to themselves would be noise.
|
|
261
|
+
const others = currentPlayer.currentLocation.getActors().filter(a => a !== currentPlayer);
|
|
262
|
+
if ((heard ?? []).length === 0 && others.length > 0 && !(this.actor instanceof NPC)) {
|
|
263
|
+
farOff = `\n Nobody's close enough to catch that.${hint(` ("shout" carries the whole room; "move to <place>" gets you near someone.)`)}`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
250
266
|
// Talking in the CLIENT's presence is the meet (a real session
|
|
251
267
|
// walked up to Ms. Veil, spoke with plain "say", and the advance
|
|
252
268
|
// never triggered -- only talk-to/call/give were hooked). The
|
|
@@ -294,7 +310,7 @@ export class SayCommand extends Command {
|
|
|
294
310
|
? `Your persona speaks and the grid carries it: ${heard.join(', ')} ${heard.length === 1 ? 'is' : 'are'} on this side of the architecture and hears you. Bodies in the room hear nothing -- you have no voice out there.`
|
|
295
311
|
: `Your words go out over the grid and find no persona to land on. Anyone here in the FLESH hears nothing -- a PAN icon is a commlink in somebody's pocket, not a person you can talk to. ("call <name>" reaches a body.)`;
|
|
296
312
|
}
|
|
297
|
-
return
|
|
313
|
+
return farOff;
|
|
298
314
|
}
|
|
299
315
|
/**
|
|
300
316
|
* "say private" -- the answer to "can anyone hear me right now",
|