@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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT A BEAT MAY CLAIM (aCErAfvEW7G23FWDM).
|
|
3
|
+
*
|
|
4
|
+
* THE LINE THIS EXISTS FOR, from session 2026-09-15T14-45-01-617Z at
|
|
5
|
+
* 11:05:43, three seconds after the player typed "jack in":
|
|
6
|
+
*
|
|
7
|
+
* "Ms. Quill plants a hand on Ted's shoulder, not gentle, steering
|
|
8
|
+
* him toward the door before Kessler decides to make this paperwork."
|
|
9
|
+
*
|
|
10
|
+
* Nothing happened. No dice were rolled, no defence was offered, no
|
|
11
|
+
* Combat Turn opened, and Ted -- who was on the Ares grid with his body
|
|
12
|
+
* slumped in a bar -- had no way to answer any of it. The sentence was
|
|
13
|
+
* EXPOSITION: a non-command line of a model reply, printed to the
|
|
14
|
+
* player verbatim (models/npc.ts). The engine never saw an act at all,
|
|
15
|
+
* which is why no rule caught it.
|
|
16
|
+
*
|
|
17
|
+
* THE REPORTER'S OWN RULE, which is the one implemented here: "'plants
|
|
18
|
+
* hand on Ted's shoulder' is an emote, and that's fine, but moving
|
|
19
|
+
* someone against their will is a hostile action, and should start some
|
|
20
|
+
* sort of combat."
|
|
21
|
+
*
|
|
22
|
+
* So the boundary is not violence and it is not touch. It is AGENCY
|
|
23
|
+
* OVER SOMEBODY ELSE'S BODY. A beat may do anything it likes with the
|
|
24
|
+
* NPC's own body, mood, tone, props and surroundings -- that is the
|
|
25
|
+
* writing, and the writing is good. It may not move, hold, or lay hands
|
|
26
|
+
* on another actor, because those have mechanics (SR5 p.195 Subduing,
|
|
27
|
+
* resolved as an opposed Unarmed melee attack) and prose is not allowed
|
|
28
|
+
* to skip them.
|
|
29
|
+
*
|
|
30
|
+
* WHAT HAPPENS TO A CAUGHT LINE. It is suppressed and the NPC is told
|
|
31
|
+
* why, in the same breath, so the next beat can COMMIT to the act as a
|
|
32
|
+
* command instead of asserting it as a fact -- where the authorization
|
|
33
|
+
* gate, the dice and the Combat Turn all get their say. Refusing
|
|
34
|
+
* silently would just produce a mute NPC and a confused model.
|
|
35
|
+
*
|
|
36
|
+
* WHY A PREDICATE AND NOT A MODEL INSTRUCTION. The prompt has told the
|
|
37
|
+
* model to behave for a long time; it says "Do NOT repeat, rephrase, or
|
|
38
|
+
* re-ask" and the same session has an NPC delivering one hook twice in
|
|
39
|
+
* two phrasings. An instruction is advice. This is the engine.
|
|
40
|
+
*
|
|
41
|
+
* Exported and PURE for the same reason isOutOfCharacter is: it fails
|
|
42
|
+
* silently -- a pattern that stops matching does not throw, the line
|
|
43
|
+
* simply prints, and the only detector is a player watching their
|
|
44
|
+
* Johnson march them out of a bar they were not standing in.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* PUTTING HANDS ON SOMEBODY. Deliberately anchored on the ACT, not on
|
|
48
|
+
* the body part: "a hand on their shoulder" and "a hand on their arm"
|
|
49
|
+
* are the same claim, and enumerating anatomy is how a list like this
|
|
50
|
+
* rots.
|
|
51
|
+
*
|
|
52
|
+
* `grabs|seizes|...` carry their inflections because a beat is written
|
|
53
|
+
* in the present tense but a reflect line may not be.
|
|
54
|
+
*/
|
|
55
|
+
const CONTACT = new RegExp([
|
|
56
|
+
// "plants/lays/claps/puts/sets a hand on", and the bare "a hand on".
|
|
57
|
+
String.raw `\b(?:plant|lay|lays|clap|put|puts|set|sets|place|places|rest|rests|drop|drops)\w*\s+(?:a|her|his|their|one)\s+hands?\s+(?:on|against|to)\b`,
|
|
58
|
+
// "...hand closes around", whoever the hand belongs to. No determiner
|
|
59
|
+
// required: the fiction writes "Quill's hand closes around Ted's arm"
|
|
60
|
+
// as readily as "a hand closes", and both are the same claim.
|
|
61
|
+
String.raw `\bhands?\s+(?:lands?|closes?|clamps?|settles?|falls?|tightens?|locks?)\s+(?:on|around|over|about)\b`,
|
|
62
|
+
// Seizing and holding.
|
|
63
|
+
String.raw `\b(?:grab|grabs|grabbed|grabbing|seiz\w+|grip\w*|clutch\w*|clamp\w*|catch\w*|snatch\w*|takes?\s+hold|gets?\s+(?:a\s+)?(?:hand|grip|hold))\b`,
|
|
64
|
+
String.raw `\b(?:restrain\w*|pin\w*|wrestl\w*|manhandl\w*|hold\w*\s+(?:down|back|still))\b`,
|
|
65
|
+
// "holds Maka pinned", where the person sits BETWEEN the verb and the
|
|
66
|
+
// word that makes it a hold. The aimed-at test reads forward from a
|
|
67
|
+
// match, so the name has to be inside the match for this shape --
|
|
68
|
+
// found by sweeping the transcripts, where it is how a continuing
|
|
69
|
+
// grapple is always written.
|
|
70
|
+
String.raw `\bhold\w*\s+\w+(?:'s|’s)?\s*(?:pinned|down|still|fast|against|in place)\b`,
|
|
71
|
+
// Striking, shoving -- these ARE attacks and have their own verbs.
|
|
72
|
+
String.raw `\b(?:shov\w+|push\w*|punch\w*|slap\w*|strik\w+|kick\w*|slam\w*|throw\w*)\b`,
|
|
73
|
+
// "takes/catches them by the arm" -- the classic escort grip.
|
|
74
|
+
String.raw `\b(?:take|takes|took|catch|catches|caught|hook|hooks)\s+\w+(?:'s|’s)?\s*\w*\s+by\s+the\s+\w+`,
|
|
75
|
+
].join('|'), 'i');
|
|
76
|
+
/**
|
|
77
|
+
* MOVING SOMEBODY WHO DID NOT CHOOSE TO MOVE. The engine has no
|
|
78
|
+
* compelled-movement mechanic at all -- nothing anywhere writes another
|
|
79
|
+
* actor's position, and `follow` is opt-in with a leash that breaks the
|
|
80
|
+
* moment you are grappled. So this is not "use the other verb", it is
|
|
81
|
+
* "this cannot happen": the canon answer to wanting someone somewhere
|
|
82
|
+
* else is to hold them (p.195) and let them decide.
|
|
83
|
+
*/
|
|
84
|
+
const COMPELLED_MOVEMENT = new RegExp([
|
|
85
|
+
// INFLECTED FORMS ONLY for the ones whose stem is also a common NOUN.
|
|
86
|
+
// `bundl\w+` matched the item "Street Ammo Bundle" and turned a vendor
|
|
87
|
+
// receipt into an abduction; `herd` and `hustle` are nouns too. Same
|
|
88
|
+
// lesson as `tow` below, found the same way -- by sweeping the gate
|
|
89
|
+
// over every transcript on disk rather than by reading the pattern.
|
|
90
|
+
String.raw `\b(?:steer\w*|guid\w+|shepherd\w*|usher\w*|escort\w*|march\w*|propel\w*|herd(?:s|ed|ing)|bundl(?:es|ed|ing)|hustl(?:es|ed|ing)|hurr(?:y|ies|ied|ying))\b`,
|
|
91
|
+
// `tow` is spelled out rather than stemmed: `tow\w*` matches TOWARD,
|
|
92
|
+
// which appears in half the good lines in any transcript and quietly
|
|
93
|
+
// turned "slides a cred stick across the table toward Ted" into an
|
|
94
|
+
// abduction. A stem is only safe when no innocent word starts with it.
|
|
95
|
+
String.raw `\b(?:drag\w*|haul\w*|pull\w*|tug\w*|yank\w*|tows?|towed|towing|carr(?:y|ies|ied|ying)|lift\w*)\b`,
|
|
96
|
+
String.raw `\b(?:walk\w*|mov\w+|lead\w*|tak\w+)\s+\w+\s+(?:toward|towards|to|out|off|away|through|into|back)\b`,
|
|
97
|
+
].join('|'), 'i');
|
|
98
|
+
const ALLOWED = { refused: false };
|
|
99
|
+
/**
|
|
100
|
+
* THE WORDS OF A NAME THAT CAN STAND FOR IT.
|
|
101
|
+
*
|
|
102
|
+
* "Ms. Quill" is abbreviated to "Quill" in every transcript, so a name
|
|
103
|
+
* has to match on its parts -- but ONLY the parts that identify
|
|
104
|
+
* somebody. A scene may legitimately hold an NPC called "The Drowned
|
|
105
|
+
* Choir" or a bar called "The Blind Eye", and letting "The" stand for
|
|
106
|
+
* either turns every "the cup" in the room into a person. Found by
|
|
107
|
+
* sweeping the gate over every transcript on disk: it was the last
|
|
108
|
+
* false positive left standing.
|
|
109
|
+
*/
|
|
110
|
+
const NOT_A_NAME = /^(?:the|and|of|an|mr|ms|mrs|dr|de|la|le|van|von)\.?$/i;
|
|
111
|
+
function nameForms(name) {
|
|
112
|
+
const parts = name.split(/\s+/).filter(p => p.length > 2 && !NOT_A_NAME.test(p));
|
|
113
|
+
return [name, ...parts].map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A possessive or object mention of somebody who is NOT the speaker.
|
|
117
|
+
*
|
|
118
|
+
* Names, not pronouns, and that is the whole reason this is tractable.
|
|
119
|
+
* A beat that says "steering him toward the door" with no name in it
|
|
120
|
+
* has no referent the engine can check, so it is left alone -- the
|
|
121
|
+
* reported line is caught because it says "Ted's shoulder" and
|
|
122
|
+
* "steering him" in ONE sentence, which is how these are actually
|
|
123
|
+
* written. Chasing pronouns across sentences would cost far more
|
|
124
|
+
* false positives than it would catch real ones.
|
|
125
|
+
*/
|
|
126
|
+
function subjectIn(line, others) {
|
|
127
|
+
for (const name of others) {
|
|
128
|
+
if (!name)
|
|
129
|
+
continue;
|
|
130
|
+
const forms = nameForms(name);
|
|
131
|
+
const re = new RegExp(`(?:^|[^\\w])(?:${forms.join('|')})(?:'s|’s)?(?:[^\\w]|$)`, 'i');
|
|
132
|
+
if (re.test(line))
|
|
133
|
+
return name;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* DOES THE VERB LAND ON THIS PERSON?
|
|
139
|
+
*
|
|
140
|
+
* THE FALSE POSITIVES THIS EXISTS FOR, every one a verbatim line from a
|
|
141
|
+
* real session:
|
|
142
|
+
* "Ms. Veil lifts the cup, watches Jynx over its rim."
|
|
143
|
+
* "Jynx holds the camera out; Mr. Ibis snatches it up with both hands."
|
|
144
|
+
* "Rhen pushes off the wall, inching toward the doorway where Vek stands."
|
|
145
|
+
* "Brick's cyber-arms whir, the ledger clutched in his hands."
|
|
146
|
+
* Each has a contact verb AND somebody else's name, and each is good
|
|
147
|
+
* writing about an OBJECT or about the speaker's own body. A name
|
|
148
|
+
* somewhere in the sentence is not enough.
|
|
149
|
+
*
|
|
150
|
+
* Approximate and deliberately so: what follows a verb within a few
|
|
151
|
+
* words is its object far more often than not, and the alternative is
|
|
152
|
+
* a parser. Two shapes count as aimed at somebody:
|
|
153
|
+
*
|
|
154
|
+
* 1. the name (or an object pronoun) inside the next ~30 characters
|
|
155
|
+
* -- "grabs Ted", "steers him toward the door", "holds Maka pinned";
|
|
156
|
+
* 2. the possessive-plus-body-part form the hand patterns produce --
|
|
157
|
+
* "a hand on Ted's shoulder", where the name follows a preposition.
|
|
158
|
+
*
|
|
159
|
+
* "lifts the cup", "snatches it up" and "pushes off the wall" match
|
|
160
|
+
* neither, which is the entire point.
|
|
161
|
+
*/
|
|
162
|
+
function aimedAtAPerson(text, matches, subject) {
|
|
163
|
+
const target = new RegExp(`(?:${nameForms(subject).join('|')})|\\b(?:him|her|them|themselves)\\b`, 'i');
|
|
164
|
+
for (const m of matches) {
|
|
165
|
+
if (!m || m.index === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
// THE MATCH ITSELF COUNTS, because some patterns swallow their own
|
|
168
|
+
// object: the compelled-movement rule is written as
|
|
169
|
+
// "walk|move|lead|take <something> toward", so in "walks Ted toward
|
|
170
|
+
// the door" the name is INSIDE the match and everything after it is
|
|
171
|
+
// furniture.
|
|
172
|
+
//
|
|
173
|
+
// SIXTEEN CHARACTERS, about three words, and the number is doing
|
|
174
|
+
// real work rather than being round. At thirty, "Ms. Veil lifts the
|
|
175
|
+
// cup, watches Jynx over its rim" reaches Jynx and a lovely line
|
|
176
|
+
// dies; at sixteen it does not, while "grabs Ted by the collar" and
|
|
177
|
+
// "steering him toward the door" both still land.
|
|
178
|
+
const end = m.index + m[0].length;
|
|
179
|
+
if (target.test(m[0] + text.slice(end, end + 16)))
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* MAY THIS BEAT SAY THIS?
|
|
186
|
+
*
|
|
187
|
+
* @param line one exposition line of a model reply
|
|
188
|
+
* @param speaker the NPC whose beat it is
|
|
189
|
+
* @param others every other actor present, by name
|
|
190
|
+
* @param opts.holding who this NPC already has a grapple on, if anyone --
|
|
191
|
+
* narration about a hold the dice already settled is a
|
|
192
|
+
* description, not a claim.
|
|
193
|
+
*/
|
|
194
|
+
export function judgeNarration(line, speaker, others, opts = {}) {
|
|
195
|
+
const t = line.replace(/^[\s│|>*-]+/, '').trim();
|
|
196
|
+
if (t.length === 0)
|
|
197
|
+
return ALLOWED;
|
|
198
|
+
const contactAt = t.match(CONTACT);
|
|
199
|
+
const movedAt = t.match(COMPELLED_MOVEMENT);
|
|
200
|
+
if (!contactAt && !movedAt)
|
|
201
|
+
return ALLOWED;
|
|
202
|
+
// WHO IS THE VERB AIMED AT -- asked of EVERY candidate rather than
|
|
203
|
+
// asked once of whoever is named first.
|
|
204
|
+
//
|
|
205
|
+
// Found by sweeping the transcripts: "Brick holds Maka pinned against
|
|
206
|
+
// the grate, his scarred face grim as he glances at Patches' slumped
|
|
207
|
+
// form" names two people. Resolving the subject first picked Patches
|
|
208
|
+
// (merely mentioned), then asked whether the verb was aimed at
|
|
209
|
+
// Patches, decided no, and let a real hold through. The question has
|
|
210
|
+
// to be asked per candidate or the answer depends on word order.
|
|
211
|
+
const subject = others
|
|
212
|
+
.filter(n => n && n !== speaker)
|
|
213
|
+
.find(n => subjectIn(t, [n]) && aimedAtAPerson(t, [contactAt, movedAt], n));
|
|
214
|
+
if (!subject)
|
|
215
|
+
return ALLOWED;
|
|
216
|
+
// ALREADY HOLDING THEM. If the engine says this NPC has a grapple on
|
|
217
|
+
// that target, then "Brick holds Maka pinned against the grate" is
|
|
218
|
+
// not a claim -- it is a DESCRIPTION of a state the dice already
|
|
219
|
+
// settled, and refusing it would gag the fiction about the one thing
|
|
220
|
+
// that IS mechanically true. Measured against every transcript on
|
|
221
|
+
// disk: without this, three of the nineteen refusals were an NPC
|
|
222
|
+
// narrating its own real grapple.
|
|
223
|
+
if (opts.holding && opts.holding === subject)
|
|
224
|
+
return ALLOWED;
|
|
225
|
+
// WHICH REFUSAL, because they are different rules with different
|
|
226
|
+
// answers. Contact has a verb and a rule behind it and the NPC should
|
|
227
|
+
// be told to use them. Compelled movement has NEITHER -- no mechanic
|
|
228
|
+
// exists for moving a body that did not choose to move -- so the note
|
|
229
|
+
// says the true thing rather than naming a verb that will not work.
|
|
230
|
+
const note = contactAt
|
|
231
|
+
? `NARRATION CANNOT PUT HANDS ON ${subject.toUpperCase()}. Your last beat described doing it anyway, so it did not happen and the player was not shown it. Laying hold of someone is an Unarmed attack they get to defend against (Subduing, p.195) and it starts a fight. If you mean it, commit to it: "[COMMAND] grapple ${subject}". If you do not, describe your own body instead -- what your hands do short of touching them is yours to narrate.`
|
|
232
|
+
: `YOU CANNOT MOVE ${subject.toUpperCase()}. Your last beat described steering or carrying them somewhere, so it did not happen and the player was not shown it. Where a runner's body goes is theirs alone -- there is no way for you to walk them anywhere. Hold them where they stand if you must ("[COMMAND] grapple ${subject}", which starts a fight), ask them to come, or let them stay.`;
|
|
233
|
+
return { refused: true, subject, note };
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=narration-limits.js.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { fuzzyPickName } from './fuzzy-match.js';
|
|
2
|
+
const ALLOWED = { allowed: true };
|
|
3
|
+
/**
|
|
4
|
+
* VERBS THAT PUT HANDS ON SOMEONE. The distinction that matters is not
|
|
5
|
+
* "is this violent" but "does this require the target's body to be
|
|
6
|
+
* there and able to answer for itself". Subduing (SR5 p.195) is the
|
|
7
|
+
* canonical one: it deals no damage at all and is still an Unarmed
|
|
8
|
+
* melee attack the target gets to defend against.
|
|
9
|
+
*/
|
|
10
|
+
const CONTACT_VERBS = new Set([
|
|
11
|
+
'attack', 'kill', 'subdue', 'grapple', 'restrain', 'clinch', 'struggle',
|
|
12
|
+
'palm', 'pick',
|
|
13
|
+
]);
|
|
14
|
+
/** Verbs whose first argument names another actor rather than an item
|
|
15
|
+
* or a direction. Anything not listed here is authorized on the actor
|
|
16
|
+
* alone -- we never guess a target out of an item name. */
|
|
17
|
+
const ACTOR_TARGET_VERBS = new Set([
|
|
18
|
+
...CONTACT_VERBS,
|
|
19
|
+
'cast', 'lead', 'order', 'give', 'talk-to', 'tell', 'treat', 'follow',
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* The target named in the arguments, if this verb names one and the
|
|
23
|
+
* name resolves to somebody in the room. Deliberately the same fuzzy
|
|
24
|
+
* resolution the combat verbs use (utilities/fuzzy-match.ts), so a
|
|
25
|
+
* refusal and an execution disagree about who was meant exactly never.
|
|
26
|
+
*
|
|
27
|
+
* Returns undefined when no target is named OR when the name does not
|
|
28
|
+
* resolve -- an unresolvable name is the COMMAND's business to complain
|
|
29
|
+
* about ("You don't see X here"), not this module's. Refusing it here
|
|
30
|
+
* would replace a specific, useful refusal with a vague one.
|
|
31
|
+
*/
|
|
32
|
+
function namedTarget(scene, actor, verb, args) {
|
|
33
|
+
if (!ACTOR_TARGET_VERBS.has(verb))
|
|
34
|
+
return undefined;
|
|
35
|
+
const words = args.filter(w => !/^(the|a|an|at|to|with|from)$/i.test(w));
|
|
36
|
+
if (words.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const room = actor.currentLocation;
|
|
39
|
+
if (!room)
|
|
40
|
+
return undefined;
|
|
41
|
+
// EVERY actor in the room, not the perceivable ones: this function
|
|
42
|
+
// answers "who did it mean", and the perception rule below is what
|
|
43
|
+
// answers "may it". Filtering by perception here would turn "you
|
|
44
|
+
// cannot see them" into a silent no-target and lose the reason.
|
|
45
|
+
const candidates = scene.getActorsInRoom(room).filter(a => a !== actor);
|
|
46
|
+
const picked = fuzzyPickName(words.join(' '), candidates.map(a => a.name));
|
|
47
|
+
return picked ? candidates.find(a => a.name === picked) : undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* MAY THIS AI-DRIVEN ACTOR RUN THIS COMMAND, RIGHT NOW.
|
|
51
|
+
*
|
|
52
|
+
* Called from CommandRegistry.parse for any command declaring
|
|
53
|
+
* npcPolicy 'authorized', and only for actors the scene does not
|
|
54
|
+
* consider human-controlled.
|
|
55
|
+
*/
|
|
56
|
+
export function authorizeNpcAction(scene, actor, verb, args) {
|
|
57
|
+
const target = namedTarget(scene, actor, verb, args);
|
|
58
|
+
if (!target)
|
|
59
|
+
return ALLOWED;
|
|
60
|
+
const contact = CONTACT_VERBS.has(verb);
|
|
61
|
+
// ORDER MATTERS HERE, and it is a writing decision as much as a rules
|
|
62
|
+
// one. A jacked-in runner fails BOTH tests below -- cross-plane
|
|
63
|
+
// perception and body presence -- and the perception rule would fire
|
|
64
|
+
// first if it were written first, refusing with "you cannot perceive
|
|
65
|
+
// them". That is true of the PERSONA and useless about the BODY,
|
|
66
|
+
// which is lying right there in the room and is what the act was
|
|
67
|
+
// reaching for. For a hands-on verb the absent-body reason is the one
|
|
68
|
+
// that tells the fiction something it can use.
|
|
69
|
+
if (contact && !isPresentInBody(target)) {
|
|
70
|
+
return {
|
|
71
|
+
allowed: false,
|
|
72
|
+
reason: `You can't lay hands on ${target.name}: they are not present in their body, and there is nobody home to resist you. Describe what you see, or wait for them to come back.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// YOU CANNOT ACT ON WHAT YOU CANNOT PERCEIVE. The engine's single
|
|
76
|
+
// cross-plane rule (Player.canPerceive) already gates hear() and
|
|
77
|
+
// see(), so an NPC never even WAKES for someone on another plane --
|
|
78
|
+
// but a beat woken by something else could still name them in a
|
|
79
|
+
// command, and did.
|
|
80
|
+
if (!actor.canPerceive(target)) {
|
|
81
|
+
return {
|
|
82
|
+
allowed: false,
|
|
83
|
+
reason: `You can't act on ${target.name} -- they are not something you can perceive from where you stand.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!contact)
|
|
87
|
+
return ALLOWED;
|
|
88
|
+
// LAYING HANDS ON SOMEONE WHO CANNOT ANSWER.
|
|
89
|
+
//
|
|
90
|
+
// This is the rung the reported session actually broke, and the
|
|
91
|
+
// reporter named the rule themselves: an emote is fine, "but moving
|
|
92
|
+
// someone against their will is a hostile action, and should start
|
|
93
|
+
// some sort of combat". Ms. Quill planted a hand on Ted's shoulder
|
|
94
|
+
// and steered him toward the door while Ted was JACKED IN -- a body
|
|
95
|
+
// slumped in a bar with nobody home to roll a defence.
|
|
96
|
+
//
|
|
97
|
+
// SR5 has no rule for this because SR5 assumes a person is present to
|
|
98
|
+
// defend: Subduing (p.195) is resolved as an opposed melee attack,
|
|
99
|
+
// and there is nothing to oppose. Rather than invent a defence pool
|
|
100
|
+
// for an empty body, the engine refuses the act. A runner's unattended
|
|
101
|
+
// body is not a prop the fiction may move.
|
|
102
|
+
if (target.isIncapacitated()) {
|
|
103
|
+
return {
|
|
104
|
+
allowed: false,
|
|
105
|
+
reason: `You can't -- ${target.name} is already down and cannot defend themselves.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return ALLOWED;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Is this actor home? A decker jacked into the Matrix, a magician
|
|
112
|
+
* projecting astrally and a rigger jumped into a drone all leave a body
|
|
113
|
+
* behind that is still listed in the room.
|
|
114
|
+
*
|
|
115
|
+
* `drone` is deliberately NOT here: a rigger jumped into an airframe is
|
|
116
|
+
* a physical machine in the meat world with its own defence, and
|
|
117
|
+
* grabbing one is an ordinary act of violence the engine can resolve.
|
|
118
|
+
*/
|
|
119
|
+
function isPresentInBody(actor) {
|
|
120
|
+
return actor.plane === 'meat' || actor.plane === 'drone';
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=npc-authorization.js.map
|
|
@@ -58,10 +58,10 @@ export function accrueOverwatch(scene, actor, defenseHits, why) {
|
|
|
58
58
|
// The grid's built-in warning system (p.221): subtle ripples, once
|
|
59
59
|
// per band -- the only free hint the player ever gets.
|
|
60
60
|
if (before < 30 && actor.overwatchScore >= 30) {
|
|
61
|
-
return { lines: [`{
|
|
61
|
+
return { lines: [`{light-blue-fg}The grid RIPPLES -- hard, close, and looking. Something vast has nearly found the thread you're hanging by. ("reboot" is a clean slate.){/light-blue-fg}`], converged: false };
|
|
62
62
|
}
|
|
63
63
|
if (before < 20 && actor.overwatchScore >= 20) {
|
|
64
|
-
return { lines: [`{
|
|
64
|
+
return { lines: [`{light-blue-fg}A ripple crosses the grid -- subtle, wrong, deliberate. Somewhere above, an eye has started counting your fingerprints.{/light-blue-fg}`], converged: false };
|
|
65
65
|
}
|
|
66
66
|
return { lines: [], converged: false };
|
|
67
67
|
}
|
|
@@ -454,7 +454,7 @@ export function fileReach(actor, room) {
|
|
|
454
454
|
*/
|
|
455
455
|
export function planeTintItemName(item, viewerPlane) {
|
|
456
456
|
if (item.plane === 'matrix') {
|
|
457
|
-
return `{
|
|
457
|
+
return `{light-blue-fg}${item.name} [data]{/light-blue-fg}`;
|
|
458
458
|
}
|
|
459
459
|
if (item.plane === 'astral') {
|
|
460
460
|
return `{magenta-fg}${item.name} [astral]{/magenta-fg}`;
|
package/package.json
CHANGED
|
@@ -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.",
|