@maka/maka-cli 5.214.0 → 5.216.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/complete.js +60 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +53 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +10 -47
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/nameables.js +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/narration-limits.js +18 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/tab-cycle.js +100 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +99 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.216.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.",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { nameablesFor, whereAmI } from '../utilities/nameables.js';
|
|
3
|
+
import { playerVerbs, verbShelf, helpCategories } from '../utilities/verb-index.js';
|
|
3
4
|
import { normalizeLoose } from '../utilities/fuzzy-match.js';
|
|
4
5
|
/**
|
|
5
6
|
* TAB COMPLETION, ANSWERED BY THE ENGINE (f79LLqTerep4nKWcA: "How do I
|
|
@@ -71,6 +72,40 @@ export class CompleteCommand extends Command {
|
|
|
71
72
|
// long one out loud.
|
|
72
73
|
return all.filter(n => normalizeLoose(n.name).includes(want));
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* THE FIRST WORD IS A VERB, NOT A NAME (yaw2btNX8aNEEF9D3, Maka: "If
|
|
77
|
+
* someone presses 'tab' with nothing in the input box, the first list
|
|
78
|
+
* to reference is the 'command' list").
|
|
79
|
+
*
|
|
80
|
+
* Tab used to answer with NAMEABLES whatever was in the box, so an
|
|
81
|
+
* empty box dumped every icon and body in reach into the Mechanics
|
|
82
|
+
* panel -- the reporter's first sentence -- and a half-typed verb
|
|
83
|
+
* matched names by accident or nothing at all. But a command line has
|
|
84
|
+
* two halves and they have different vocabularies: before the first
|
|
85
|
+
* space you are choosing a VERB, after it you are naming a TARGET.
|
|
86
|
+
*
|
|
87
|
+
* The test is the space, not the emptiness: "ha" and "" are both the
|
|
88
|
+
* verb slot, "hack " and "hack ca" are both the target slot. That is
|
|
89
|
+
* also why `head` is what it is -- with no space there is no head, so
|
|
90
|
+
* a completed verb replaces the whole box.
|
|
91
|
+
*/
|
|
92
|
+
verbSlot(raw) {
|
|
93
|
+
return !raw.includes(' ');
|
|
94
|
+
}
|
|
95
|
+
/** The verb list, shaped like nameables so one renderer prints both. */
|
|
96
|
+
verbs() {
|
|
97
|
+
return playerVerbs().map(v => {
|
|
98
|
+
const shelf = verbShelf(v);
|
|
99
|
+
const primary = shelf?.primary;
|
|
100
|
+
return {
|
|
101
|
+
glyph: '›',
|
|
102
|
+
name: v,
|
|
103
|
+
what: shelf
|
|
104
|
+
? `${shelf.category}${primary && primary !== v ? ` -- same as "${primary}"` : ''}`
|
|
105
|
+
: 'a command',
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
74
109
|
async execute(args = []) {
|
|
75
110
|
const actor = this.actor;
|
|
76
111
|
// The client sends the box VERBATIM, so the words rebuild it exactly;
|
|
@@ -80,7 +115,20 @@ export class CompleteCommand extends Command {
|
|
|
80
115
|
// A VERB ALONE IS NOT A NAME. With nothing typed after the verb the
|
|
81
116
|
// player wants the roster, which is the `fragment === ''` case below
|
|
82
117
|
// and handled by matches() returning everything.
|
|
83
|
-
const
|
|
118
|
+
const onVerb = this.verbSlot(raw);
|
|
119
|
+
// AN EMPTY BOX IS NOT A QUERY, it is "what can I even do?", and the
|
|
120
|
+
// answer is not 180 verbs truncated at 12 -- that is the dump the
|
|
121
|
+
// report is about, wearing different clothes. The SHELVES are the
|
|
122
|
+
// command list at the altitude a person can read: a dozen lines,
|
|
123
|
+
// each one a place to look, and `help <shelf>` opens it.
|
|
124
|
+
if (onVerb && fragment.length === 0) {
|
|
125
|
+
const shelves = helpCategories();
|
|
126
|
+
return [
|
|
127
|
+
`Type a verb, or a letter or two and Tab again. The shelves:`,
|
|
128
|
+
...shelves.map(c => ` › ${c.title} -- ${c.entries.length} commands ("help ${c.title}")`),
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
131
|
+
const all = onVerb ? this.verbs() : nameablesFor(this.scene, actor, this.rooms);
|
|
84
132
|
if (all.length === 0) {
|
|
85
133
|
return `Nothing to name from ${whereAmI(actor)}.`;
|
|
86
134
|
}
|
|
@@ -90,6 +138,17 @@ export class CompleteCommand extends Command {
|
|
|
90
138
|
return `${COMPLETE_MARKER}${head}${only.name}`;
|
|
91
139
|
}
|
|
92
140
|
if (hits.length === 0) {
|
|
141
|
+
// The two slots miss for different reasons and the line has to say
|
|
142
|
+
// which: a verb that does not exist is a different problem from a
|
|
143
|
+
// name that is not HERE, and telling somebody "nothing at the
|
|
144
|
+
// Night Market answers to 'inevntory'" sends them looking round
|
|
145
|
+
// the room for their own pack.
|
|
146
|
+
if (onVerb) {
|
|
147
|
+
return [
|
|
148
|
+
`No command starts with "${fragment}". Try a shelf:`,
|
|
149
|
+
...helpCategories().map(c => ` › ${c.title} ("help ${c.title}")`),
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
93
152
|
return [
|
|
94
153
|
`Nothing at ${whereAmI(actor)} answers to "${fragment}". You could name:`,
|
|
95
154
|
...all.slice(0, 12).map(n => ` ${n.glyph} ${n.name} -- ${n.what}`),
|
|
@@ -1086,5 +1086,57 @@
|
|
|
1086
1086
|
// stay guarded...", "Suspicious system-note injection detected" --
|
|
1087
1087
|
// are all dated 2026-08-24 and predate isOutOfCharacter, which
|
|
1088
1088
|
// already catches them. Checked, not assumed.)
|
|
1089
|
-
|
|
1089
|
+
// 1.74.1 (2026-09-16): A PERSON MAY SAY WHAT THEY KNOW. 1.74.0's
|
|
1090
|
+
// INTERIOR rule leaked onto the SPEECH channel and gagged ordinary
|
|
1091
|
+
// dialogue -- an NPC saying "I know exactly what you mean" out loud
|
|
1092
|
+
// was refused as mind-reading. Caught before anyone played it, by
|
|
1093
|
+
// reading judgeNarration's other caller rather than by a report.
|
|
1094
|
+
// THE TRAP IS WORTH RECORDING. Dialogue has run through this judge
|
|
1095
|
+
// since KwiMKNMN2e8KBqTY7, and it was safe from the agency rules for
|
|
1096
|
+
// an INVISIBLE reason: that caller passes an empty cast, so there is
|
|
1097
|
+
// no subject to resolve and CONTACT/COMPELLED_MOVEMENT can never fire.
|
|
1098
|
+
// A real guarantee, nowhere stated. INTERIOR needs no subject, so it
|
|
1099
|
+
// walked straight through. judgeNarration now takes an explicit
|
|
1100
|
+
// `channel` ('beat' by default, 'speech' from the dialogue call) and
|
|
1101
|
+
// the beat-only rules are gated on it, so the next rule added here has
|
|
1102
|
+
// to decide which channel it belongs to instead of inheriting an
|
|
1103
|
+
// accident. The distinction is the rule itself: a character may SAY
|
|
1104
|
+
// what they know; they may not have it NARRATED at a player who cannot
|
|
1105
|
+
// hear it.
|
|
1106
|
+
// 1.75.0 (2026-09-16): TAB COMPLETES A VERB BEFORE THE SPACE AND A
|
|
1107
|
+
// TARGET AFTER IT (yaw2btNX8aNEEF9D3, Maka: "the 'tab' key shouldn't
|
|
1108
|
+
// SHOW all the things in the room in the Mechanics panel ... If
|
|
1109
|
+
// someone presses 'tab' with nothing in the input box, the first list
|
|
1110
|
+
// to reference is the 'command' list. Then, tab can cycle through
|
|
1111
|
+
// entities. Be careful though, if I can't percieve the thing, it
|
|
1112
|
+
// shouldn't be on the tab list."). Four asks, four changes.
|
|
1113
|
+
// - THE SPACE IS THE BOUNDARY. `complete` answered with NAMEABLES
|
|
1114
|
+
// whatever was in the box, so an empty box dumped every icon and
|
|
1115
|
+
// body in reach into the panel and a half-typed verb matched entity
|
|
1116
|
+
// names by accident. A command line has two halves with two
|
|
1117
|
+
// vocabularies; before the first space you are choosing a verb.
|
|
1118
|
+
// - AN EMPTY BOX GETS THE SHELVES, not 180 verbs truncated at 12 --
|
|
1119
|
+
// that is the same dump wearing different clothes. A dozen lines,
|
|
1120
|
+
// each a place to look, each with its "help <shelf>".
|
|
1121
|
+
// - THE VERB TABLE MOVED OUT OF game.ts to utilities/verb-index.ts, so
|
|
1122
|
+
// Tab and the help screen read ONE list. A second hand-maintained
|
|
1123
|
+
// copy of ~180 verbs is a copy that drifts. Aliases complete too:
|
|
1124
|
+
// somebody typing "inventor" is reaching for `inventory`, and it is
|
|
1125
|
+
// an alias of `inv`. Role-aware, so Tab never offers a play-tester
|
|
1126
|
+
// verb to an account the server will 403.
|
|
1127
|
+
// - TAB WAS A FREE SEARCH. nameablesFor read the room's WHOLE
|
|
1128
|
+
// inventory, so it offered loot nobody had found -- while `look` and
|
|
1129
|
+
// `take` filter the same set and would then refuse to show or hand
|
|
1130
|
+
// over the very name Tab had just typed for you. It now uses take.ts's
|
|
1131
|
+
// gate exactly: cased the room, or the thing arrived in the open in
|
|
1132
|
+
// front of you. Off-plane items go too, for take.ts's reason.
|
|
1133
|
+
// - AND TAB AGAIN WALKS THE LIST (utilities/tab-cycle.ts, CLI only for
|
|
1134
|
+
// now -- cycling is a property of an input box and only the CLI has
|
|
1135
|
+
// one). Pure model, so a test can press Tab four times without a
|
|
1136
|
+
// terminal. Continuation is an EXACT match on what the last press
|
|
1137
|
+
// wrote: one typed letter ends the walk and asks the engine again,
|
|
1138
|
+
// because that is what narrowing means. One candidate is not a
|
|
1139
|
+
// cycle -- a key that rewrites the box to what it already holds
|
|
1140
|
+
// looks broken.
|
|
1141
|
+
export const ENGINE_VERSION = '1.75.0';
|
|
1090
1142
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -60,7 +60,8 @@ import { NoteCommand } from './commands/note.js';
|
|
|
60
60
|
import { BacklogCommand } from './commands/backlog.js';
|
|
61
61
|
// Whether this account may actually USE the backlog -- gates what the
|
|
62
62
|
// help index offers, never what the verb accepts (item D).
|
|
63
|
-
import {
|
|
63
|
+
import { probePlaytesterStanding } from './utilities/playtester.js';
|
|
64
|
+
import { HELP_CATEGORIES, HIDDEN_VERBS, PLAYTESTER_VERBS, helpCategories } from './utilities/verb-index.js';
|
|
64
65
|
import { RolesCommand } from './commands/roles.js';
|
|
65
66
|
import { ReviewCommand } from './commands/review.js';
|
|
66
67
|
import { SinCommand } from './commands/sin.js';
|
|
@@ -6747,46 +6748,13 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6747
6748
|
* the renderer is ENTITLED to make -- the table was what lied.
|
|
6748
6749
|
* help-taxonomy.test.ts now holds that line for the whole table.
|
|
6749
6750
|
*/
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
// players" / "crew street"); the bare verb still answers quietly.
|
|
6758
|
-
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
|
|
6759
|
-
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
|
|
6760
|
-
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
6761
|
-
// The Emerged get their own shelf (player request: "there MUST be
|
|
6762
|
-
// resonance" -- matrix is the place, Resonance is the talent).
|
|
6763
|
-
// `sustain` shelves here and not under magic: the command is
|
|
6764
|
-
// technomancer-gated (a sprite task, SR5 p.256) and refuses anyone
|
|
6765
|
-
// else, so the Emerged shelf is where its audience looks.
|
|
6766
|
-
{ title: 'resonance', entries: [['compile'], ['decompile'], ['thread'], ['unravel'], ['register'], ['standby'], ['sustain'], ['sprites']] },
|
|
6767
|
-
{ title: 'social', entries: [['say'], ['shout'], ['emote', 'me'], ['tell', 'talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
6768
|
-
{ title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['garage'], ['pay']] },
|
|
6769
|
-
{ title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
|
|
6770
|
-
{ title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
6771
|
-
{ title: 'session', entries: [['help'], ['hints'], ['time', 'clock'], ['note'], ['backlog'], ['review'], ['roles'], ['clear'], ['quit', 'exit']] },
|
|
6772
|
-
];
|
|
6773
|
-
/** Registered but deliberately UNLISTED: reachable as subcommands or
|
|
6774
|
-
* muscle-memory ("players" folded back under crew after playtesting
|
|
6775
|
-
* -- "crew players" is the advertised path; the bare verb still
|
|
6776
|
-
* answers). Without this, the uncategorized sweep resurrects them
|
|
6777
|
-
* in an "other" bucket. */
|
|
6778
|
-
// 'first' and 'apply' exist ONLY to catch the two-word phrasings
|
|
6779
|
-
// ("first aid", "apply first aid") that used to die at the parser. As
|
|
6780
|
-
// bare words they mean nothing, so they are claimed but not advertised
|
|
6781
|
-
// -- `heal` is the verb the help screen teaches.
|
|
6782
|
-
// `complete` is what Tab sends (commands/complete.ts) -- a question the
|
|
6783
|
-
// input box asks, not a verb anyone types, so it stays out of help.
|
|
6784
|
-
static HIDDEN_VERBS = ['players', 'first', 'apply', 'complete'];
|
|
6785
|
-
/** Session verbs that only a play-tester (or admin) can actually use
|
|
6786
|
-
* -- the server role-gates both and 403s everyone else. Listed in
|
|
6787
|
-
* help ONLY for a confirmed play-tester; see helpCategories. `roles`
|
|
6788
|
-
* rides along because it is admin-only, a strictly narrower group. */
|
|
6789
|
-
static PLAYTESTER_VERBS = new Set(['backlog', 'review', 'roles']);
|
|
6751
|
+
// THE VERB TABLES MOVED TO utilities/verb-index.ts. Tab needs the
|
|
6752
|
+
// same list the help screen prints (yaw2btNX8aNEEF9D3), and two
|
|
6753
|
+
// hand-maintained copies of ~180 verbs is a copy that drifts. These
|
|
6754
|
+
// aliases keep every call site below reading as it did.
|
|
6755
|
+
static HELP_CATEGORIES = HELP_CATEGORIES;
|
|
6756
|
+
static HIDDEN_VERBS = HIDDEN_VERBS;
|
|
6757
|
+
static PLAYTESTER_VERBS = PLAYTESTER_VERBS;
|
|
6790
6758
|
/** Descriptions for the verbs handled inline in handleInput() rather than registered. */
|
|
6791
6759
|
static BUILTIN_DESCRIPTIONS = {
|
|
6792
6760
|
help: 'This overview. "help <category>" for one group with full descriptions; "help <command>" for one command.',
|
|
@@ -6810,12 +6778,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6810
6778
|
* and why the verb itself stays registered and usable regardless.
|
|
6811
6779
|
*/
|
|
6812
6780
|
helpCategories() {
|
|
6813
|
-
|
|
6814
|
-
return Game.HELP_CATEGORIES;
|
|
6815
|
-
return Game.HELP_CATEGORIES.map(c => c.title !== 'session' ? c : {
|
|
6816
|
-
...c,
|
|
6817
|
-
entries: c.entries.filter(e => !Game.PLAYTESTER_VERBS.has(e[0])),
|
|
6818
|
-
});
|
|
6781
|
+
return helpCategories();
|
|
6819
6782
|
}
|
|
6820
6783
|
getHelpMessage(args = []) {
|
|
6821
6784
|
const registered = Object.keys(CommandFactory.getAllRegisteredCommands());
|
|
@@ -2515,6 +2515,13 @@ Your objective is to drive the story line.`}
|
|
|
2515
2515
|
const placeVerdict = judgeNarration(said, this.name, [], {
|
|
2516
2516
|
here: this.currentLocation?.name,
|
|
2517
2517
|
worldRooms: Object.values(this._scene?.getRooms?.() ?? {}).map(r => r.name),
|
|
2518
|
+
// SAYS WHICH CHANNEL THIS IS, rather than relying on the empty
|
|
2519
|
+
// `others` above to keep the beat-only rules from firing. That
|
|
2520
|
+
// guarantee held for the agency rules, which need a subject to
|
|
2521
|
+
// resolve -- and broke the moment a rule arrived that does not
|
|
2522
|
+
// (INTERIOR), refusing an ordinary spoken "I know exactly what
|
|
2523
|
+
// you mean" as mind-reading. A person may SAY what they know.
|
|
2524
|
+
channel: 'speech',
|
|
2518
2525
|
});
|
|
2519
2526
|
if (placeVerdict.refused) {
|
|
2520
2527
|
this.logger.write(`${this.name} speech refused (claimed to be elsewhere): ${said.slice(0, 160)}`);
|
|
@@ -8,6 +8,7 @@ import { getArchetype, outfitArchetype } from './archetypes.js';
|
|
|
8
8
|
import { attachLineEditor, attachArrowMovement } from './utilities/line-editor.js';
|
|
9
9
|
import { attachInputHistory } from './utilities/input-history.js';
|
|
10
10
|
import { COMPLETE_MARKER } from './commands/complete.js';
|
|
11
|
+
import { startCycle, advance, continues } from './utilities/tab-cycle.js';
|
|
11
12
|
import { TERMINAL_RESET_SEQUENCE } from './utilities/terminal-reset.js';
|
|
12
13
|
import { readSave } from './utilities/persistence.js';
|
|
13
14
|
import { requireCatalog, CatalogUnavailableError } from './utilities/catalog.js';
|
|
@@ -328,6 +329,10 @@ export async function main(playerName, sceneSeed, options) {
|
|
|
328
329
|
// asked for in its place is a way to know which icon you are about
|
|
329
330
|
// to hit. Bound on the input box because that's where focus lives
|
|
330
331
|
// during play.
|
|
332
|
+
// The walk the previous Tab set up, if any. Lives out here so it
|
|
333
|
+
// survives between presses and nowhere else, which is the whole of
|
|
334
|
+
// its scope: no cycle outlives the input box it belongs to.
|
|
335
|
+
let tabCycle;
|
|
331
336
|
game.inputBox.key(['tab'], () => {
|
|
332
337
|
// Blessed's textarea reader appends the literal '\t' to the box
|
|
333
338
|
// value on this same keypress (listener order isn't guaranteed
|
|
@@ -355,9 +360,26 @@ export async function main(playerName, sceneSeed, options) {
|
|
|
355
360
|
// So: send the box, let the engine answer, on both run shapes.
|
|
356
361
|
void (async () => {
|
|
357
362
|
const box = game.inputBox.getValue().replace(/\t/g, '');
|
|
363
|
+
// TAB AGAIN TAKES THE NEXT ONE (yaw2btNX8aNEEF9D3, Maka: "Then,
|
|
364
|
+
// tab can cycle through entities"). No round trip and no second
|
|
365
|
+
// copy of the list in the panel: the names from the previous
|
|
366
|
+
// press are still good, and walking them is a property of this
|
|
367
|
+
// input box rather than a question for the engine. The model is
|
|
368
|
+
// in utilities/tab-cycle.ts, pure, so a test can press Tab four
|
|
369
|
+
// times without a terminal. `continues` is an exact match on
|
|
370
|
+
// what the last press wrote, so one typed letter ends the walk
|
|
371
|
+
// and asks the engine again -- which is what narrowing means.
|
|
372
|
+
if (continues(tabCycle, box)) {
|
|
373
|
+
const { box: filled, next } = advance(tabCycle);
|
|
374
|
+
tabCycle = next;
|
|
375
|
+
game.inputBox.setValue(filled);
|
|
376
|
+
screen.render();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
358
379
|
const res = await game.handleInput(`complete ${box}`);
|
|
359
380
|
if (!res)
|
|
360
381
|
return;
|
|
382
|
+
tabCycle = startCycle(box, res);
|
|
361
383
|
const lines = res.split('\n');
|
|
362
384
|
// THE MARKER LINE refills the box (see commands/complete.ts).
|
|
363
385
|
// Everything else is the candidate list, and goes to the ticker
|
|
@@ -365,12 +387,19 @@ export async function main(playerName, sceneSeed, options) {
|
|
|
365
387
|
if (lines[0]?.startsWith(COMPLETE_MARKER)) {
|
|
366
388
|
const filled = lines[0].slice(COMPLETE_MARKER.length);
|
|
367
389
|
game.inputBox.setValue(filled);
|
|
390
|
+
// The engine may have grown the box to the common head; the
|
|
391
|
+
// cycle has to compare against what is ACTUALLY there or the
|
|
392
|
+
// next press would look like a player keystroke.
|
|
393
|
+
if (tabCycle)
|
|
394
|
+
tabCycle = { ...tabCycle, box: filled };
|
|
368
395
|
lines.shift();
|
|
369
396
|
}
|
|
370
397
|
for (const l of lines) {
|
|
371
398
|
if (l.trim().length > 0)
|
|
372
399
|
gameLog.meta(l, 'white');
|
|
373
400
|
}
|
|
401
|
+
if (tabCycle)
|
|
402
|
+
gameLog.meta(` (Tab again walks these.)`, 'white');
|
|
374
403
|
screen.render();
|
|
375
404
|
})();
|
|
376
405
|
});
|
|
@@ -65,7 +65,26 @@ function meatNameables(actor) {
|
|
|
65
65
|
for (const device of room.openableDevices()) {
|
|
66
66
|
push(out, seen, { glyph: '▤', name: device.name, what: 'a device' });
|
|
67
67
|
}
|
|
68
|
+
// WHAT YOU HAVE ACTUALLY SEEN (yaw2btNX8aNEEF9D3, Maka: "if I can't
|
|
69
|
+
// percieve the thing, it shouldn't be on the tab list").
|
|
70
|
+
//
|
|
71
|
+
// This read the room's whole inventory, so Tab offered loot nobody had
|
|
72
|
+
// found yet -- and `look` (look.ts) and `take` (take.ts) both filter
|
|
73
|
+
// the same set and would have refused to show or hand over the very
|
|
74
|
+
// name Tab had just typed for you. Tab was a free `search`.
|
|
75
|
+
//
|
|
76
|
+
// The gate is take.ts's, verbatim in meaning: having cased the room
|
|
77
|
+
// reveals everything in it, and until then you know only what arrived
|
|
78
|
+
// in the open in front of you (Room.plainSightItems -- a dead NPC's
|
|
79
|
+
// scattered kit, a witnessed drop). Off-plane items are dropped for
|
|
80
|
+
// the same reason take.ts drops them: a matrix file on the floor is
|
|
81
|
+
// not a thing a body can pick up.
|
|
82
|
+
const cased = room.searchedBy.has(actor.name);
|
|
68
83
|
for (const item of room.getItemsForActor(actor)) {
|
|
84
|
+
if (item.plane !== actor.plane)
|
|
85
|
+
continue;
|
|
86
|
+
if (!cased && !room.plainSightItems.has(item.name.toLowerCase()))
|
|
87
|
+
continue;
|
|
69
88
|
push(out, seen, { glyph: '◇', name: item.name, what: 'on the ground' });
|
|
70
89
|
}
|
|
71
90
|
// WHAT YOU ARE CARRYING is nameable too -- "equip", "drop", "reload
|
|
@@ -309,6 +309,21 @@ function aimedAtAPerson(text, matches, subject) {
|
|
|
309
309
|
* @param opts.holding who this NPC already has a grapple on, if anyone --
|
|
310
310
|
* narration about a hold the dice already settled is a
|
|
311
311
|
* description, not a claim.
|
|
312
|
+
* @param opts.channel which half of the reply this is. 'beat' (the
|
|
313
|
+
* default) is exposition and gets every rule. 'speech'
|
|
314
|
+
* is a line of DIALOGUE and gets the place rule only.
|
|
315
|
+
*
|
|
316
|
+
* WHY THE CHANNEL IS A PARAMETER AND NOT AN ACCIDENT. Dialogue has been
|
|
317
|
+
* passing through here since KwiMKNMN2e8KBqTY7 ("Whisper ... says he
|
|
318
|
+
* is"), and it survived the agency rules only because its caller passes
|
|
319
|
+
* an EMPTY `others` -- with nobody to resolve as the subject, CONTACT
|
|
320
|
+
* and COMPELLED_MOVEMENT can never fire. That is a real guarantee but an
|
|
321
|
+
* invisible one, and the INTERIOR rule broke it the moment it was
|
|
322
|
+
* written: interiority needs no subject, so a perfectly ordinary spoken
|
|
323
|
+
* "I know exactly what you mean" was refused as mind-reading. A person
|
|
324
|
+
* is allowed to say what they know. They are not allowed to have it
|
|
325
|
+
* NARRATED at a player who cannot hear it -- which is the whole
|
|
326
|
+
* distinction, and it now has a name in the signature.
|
|
312
327
|
*/
|
|
313
328
|
export function judgeNarration(line, speaker, others, opts = {}) {
|
|
314
329
|
const t = line.replace(/^[\s│|>*-]+/, '').trim();
|
|
@@ -327,8 +342,9 @@ export function judgeNarration(line, speaker, others, opts = {}) {
|
|
|
327
342
|
}
|
|
328
343
|
}
|
|
329
344
|
// ALSO BEFORE the agency rules, and for the same reason: this is about
|
|
330
|
-
// the SPEAKER's own head, so there is no subject to resolve
|
|
331
|
-
|
|
345
|
+
// the SPEAKER's own head, so there is no subject to resolve -- which
|
|
346
|
+
// is exactly why it must be told to stay off the speech channel.
|
|
347
|
+
if (opts.channel !== 'speech' && INTERIOR.test(t)) {
|
|
332
348
|
return {
|
|
333
349
|
refused: true,
|
|
334
350
|
subject: speaker,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAB AGAIN TAKES THE NEXT ONE (yaw2btNX8aNEEF9D3, Maka: "Then, tab can
|
|
3
|
+
* cycle through entities").
|
|
4
|
+
*
|
|
5
|
+
* The engine answers a Tab with the candidates and, when they share a
|
|
6
|
+
* head, fills the box as far as they agree (commands/complete.ts). That
|
|
7
|
+
* is the shell behaviour everyone has in their fingers for the FIRST
|
|
8
|
+
* press. The second press is where a shell offers the list and a good
|
|
9
|
+
* one walks it, and the report asks for the walk.
|
|
10
|
+
*
|
|
11
|
+
* WHY A PURE MODEL AND NOT A FEW LINES IN ui.ts. The whole feature is
|
|
12
|
+
* "what counts as the same Tab?" -- and that is a state question with
|
|
13
|
+
* exactly the shape that rots in an event handler: a player who types a
|
|
14
|
+
* letter, or recalls a command from history, or clears the box, must
|
|
15
|
+
* start a NEW cycle, and a player who just presses Tab again must not.
|
|
16
|
+
* Wiring belongs in ui.ts; the decision belongs here, where a test can
|
|
17
|
+
* press Tab four times in a row without a terminal.
|
|
18
|
+
*
|
|
19
|
+
* DELIBERATELY CLIENT-SIDE, AND CLI-ONLY FOR NOW. The engine stays
|
|
20
|
+
* stateless -- it answers the same question the same way however many
|
|
21
|
+
* times it is asked, which is what lets three clients share it. Cycling
|
|
22
|
+
* is a property of an input box, and only the CLI has one of these.
|
|
23
|
+
*/
|
|
24
|
+
/** A name the engine offered, as printed in a candidate line. */
|
|
25
|
+
const CANDIDATE_LINE = /^\s{2}\S+\s+(.+?)\s+--\s/;
|
|
26
|
+
/**
|
|
27
|
+
* The names out of a `complete` reply.
|
|
28
|
+
*
|
|
29
|
+
* Reads the LIST lines, not the marker line: the marker is what the box
|
|
30
|
+
* already holds. A reply with no list (one match, or a refusal) yields
|
|
31
|
+
* nothing and ends any cycle, which is correct -- there is nothing to
|
|
32
|
+
* walk.
|
|
33
|
+
*/
|
|
34
|
+
export function candidatesIn(reply) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const line of reply.split('\n')) {
|
|
37
|
+
const m = CANDIDATE_LINE.exec(line);
|
|
38
|
+
if (m?.[1])
|
|
39
|
+
out.push(m[1]);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Split a box into the command so far and the fragment being typed --
|
|
45
|
+
* the SAME rule commands/complete.ts uses, deliberately duplicated
|
|
46
|
+
* rather than imported so the two can be compared by eye. A trailing
|
|
47
|
+
* space means an empty fragment.
|
|
48
|
+
*/
|
|
49
|
+
export function splitBox(raw) {
|
|
50
|
+
if (raw.length === 0 || raw.endsWith(' '))
|
|
51
|
+
return { head: raw, fragment: '' };
|
|
52
|
+
const cut = raw.lastIndexOf(' ');
|
|
53
|
+
return cut < 0 ? { head: '', fragment: raw } : { head: raw.slice(0, cut + 1), fragment: raw.slice(cut + 1) };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Begin a cycle from a reply, or return undefined when there is nothing
|
|
57
|
+
* to walk.
|
|
58
|
+
*
|
|
59
|
+
* ONE CANDIDATE IS NOT A CYCLE: the engine already filled the box with
|
|
60
|
+
* it, and a "cycle" of length one that rewrites the box on every press
|
|
61
|
+
* is just a key that appears to do nothing.
|
|
62
|
+
*/
|
|
63
|
+
export function startCycle(boxWhenPressed, reply) {
|
|
64
|
+
const names = candidatesIn(reply);
|
|
65
|
+
if (names.length < 2)
|
|
66
|
+
return undefined;
|
|
67
|
+
const { head } = splitBox(boxWhenPressed);
|
|
68
|
+
// `box` SEEDS TO THE BOX AS PRESSED, not to ''. The engine grows the
|
|
69
|
+
// box to the candidates' common head only when there IS one -- "hack
|
|
70
|
+
// s" with two s-names, but not "hack " with three unrelated ones. When
|
|
71
|
+
// it does not, nothing rewrites the box, so a cycle remembering ''
|
|
72
|
+
// would fail its own sameness test on the very next press and go back
|
|
73
|
+
// to the engine instead of walking. The caller overwrites this when a
|
|
74
|
+
// marker line did change the box.
|
|
75
|
+
return { head, names, index: -1, box: boxWhenPressed };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The next box contents, and the cycle to remember.
|
|
79
|
+
*
|
|
80
|
+
* Wraps at the end on purpose -- a walk you cannot get back to the start
|
|
81
|
+
* of is a walk you have to abandon by retyping.
|
|
82
|
+
*/
|
|
83
|
+
export function advance(cycle) {
|
|
84
|
+
const index = (cycle.index + 1) % cycle.names.length;
|
|
85
|
+
const box = `${cycle.head}${cycle.names[index]}`;
|
|
86
|
+
return { box, next: { ...cycle, index, box } };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Is this press a continuation of `cycle`?
|
|
90
|
+
*
|
|
91
|
+
* The box must be EXACTLY what the last press wrote. Not "starts with",
|
|
92
|
+
* not case-insensitive: a player who typed one more letter is narrowing
|
|
93
|
+
* their search and wants a fresh answer from the engine, and a cycle
|
|
94
|
+
* that swallowed that keystroke would be the most annoying possible
|
|
95
|
+
* version of this feature.
|
|
96
|
+
*/
|
|
97
|
+
export function continues(cycle, box) {
|
|
98
|
+
return cycle !== undefined && cycle.box === box && cycle.names.length > 1;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=tab-cycle.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { isPlaytester } from './playtester.js';
|
|
2
|
+
/**
|
|
3
|
+
* THE VERBS A PLAYER MAY TYPE, in one place.
|
|
4
|
+
*
|
|
5
|
+
* This table WAS a private static on Game, read only by the help screen.
|
|
6
|
+
* Tab needed the same list (yaw2btNX8aNEEF9D3, Maka: "If someone presses
|
|
7
|
+
* 'tab' with nothing in the input box, the first list to reference is
|
|
8
|
+
* the 'command' list"), and a second hand-maintained copy of ~180 verbs
|
|
9
|
+
* is a copy that drifts -- so it moved here and `help` reads it from
|
|
10
|
+
* here too. There is exactly one list.
|
|
11
|
+
*
|
|
12
|
+
* ALIASES ARE FIRST-CLASS. Each entry is [primary, ...aliases], and help
|
|
13
|
+
* prints them as "primary (alias, alias)". Tab must match all of them:
|
|
14
|
+
* somebody typing "inventor<Tab>" is reaching for `inventory`, which is
|
|
15
|
+
* an alias of `inv`, and a completer that only knew primaries would
|
|
16
|
+
* leave them stuck on a word that works.
|
|
17
|
+
*/
|
|
18
|
+
export const HELP_CATEGORIES = [
|
|
19
|
+
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
20
|
+
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
|
|
21
|
+
{ title: 'combat', entries: [['attack'], ['kill'], ['subdue', 'stun', 'knock'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
|
|
22
|
+
// The party layer: everyone who walks (or flies, or manifests) at
|
|
23
|
+
// your side answers to these.
|
|
24
|
+
// "players" folded back under crew after playtesting ("crew
|
|
25
|
+
// players" / "crew street"); the bare verb still answers quietly.
|
|
26
|
+
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
|
|
27
|
+
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
|
|
28
|
+
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
29
|
+
// The Emerged get their own shelf (player request: "there MUST be
|
|
30
|
+
// resonance" -- matrix is the place, Resonance is the talent).
|
|
31
|
+
// `sustain` shelves here and not under magic: the command is
|
|
32
|
+
// technomancer-gated (a sprite task, SR5 p.256) and refuses anyone
|
|
33
|
+
// else, so the Emerged shelf is where its audience looks.
|
|
34
|
+
{ title: 'resonance', entries: [['compile'], ['decompile'], ['thread'], ['unravel'], ['register'], ['standby'], ['sustain'], ['sprites']] },
|
|
35
|
+
{ title: 'social', entries: [['say'], ['shout'], ['emote', 'me'], ['tell', 'talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
36
|
+
{ title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['garage'], ['pay']] },
|
|
37
|
+
{ title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
|
|
38
|
+
{ title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
39
|
+
{ title: 'session', entries: [['help'], ['hints'], ['time', 'clock'], ['note'], ['backlog'], ['review'], ['roles'], ['clear'], ['quit', 'exit']] },
|
|
40
|
+
];
|
|
41
|
+
/** Registered but deliberately UNLISTED: reachable as subcommands or
|
|
42
|
+
* muscle-memory ("players" folded back under crew after playtesting
|
|
43
|
+
* -- "crew players" is the advertised path; the bare verb still
|
|
44
|
+
* answers). Without this, the uncategorized sweep resurrects them
|
|
45
|
+
* in an "other" bucket. */
|
|
46
|
+
// 'first' and 'apply' exist ONLY to catch the two-word phrasings
|
|
47
|
+
// ("first aid", "apply first aid") that used to die at the parser. As
|
|
48
|
+
// bare words they mean nothing, so they are claimed but not advertised
|
|
49
|
+
// -- `heal` is the verb the help screen teaches.
|
|
50
|
+
// `complete` is what Tab sends (commands/complete.ts) -- a question the
|
|
51
|
+
// input box asks, not a verb anyone types, so it stays out of help.
|
|
52
|
+
export const HIDDEN_VERBS = ['players', 'first', 'apply', 'complete'];
|
|
53
|
+
/** Session verbs that only a play-tester (or admin) can actually use
|
|
54
|
+
* -- the server role-gates both and 403s everyone else. Listed in
|
|
55
|
+
* help ONLY for a confirmed play-tester; see helpCategories. `roles`
|
|
56
|
+
* rides along because it is admin-only, a strictly narrower group. */
|
|
57
|
+
export const PLAYTESTER_VERBS = new Set(['backlog', 'review', 'roles']);
|
|
58
|
+
/**
|
|
59
|
+
* The help index as THIS account should see it (backlog wave 2, item
|
|
60
|
+
* D: "let's remove Seattle Backlog for non playtesters"). The backlog
|
|
61
|
+
* is role-gated on the server and 403s everyone else, so listing it
|
|
62
|
+
* for every player only advertises a refusal. Hidden unless the
|
|
63
|
+
* account is a CONFIRMED play-tester -- see utilities/playtester.ts
|
|
64
|
+
* for why "unknown" (offline, logged out) hides rather than shows,
|
|
65
|
+
* and why the verb itself stays registered and usable regardless.
|
|
66
|
+
*/
|
|
67
|
+
export function helpCategories() {
|
|
68
|
+
if (isPlaytester())
|
|
69
|
+
return HELP_CATEGORIES;
|
|
70
|
+
return HELP_CATEGORIES.map(c => c.title !== 'session' ? c : {
|
|
71
|
+
...c,
|
|
72
|
+
entries: c.entries.filter(e => !PLAYTESTER_VERBS.has(e[0])),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* EVERY WORD A PLAYER MAY TYPE AS A FIRST WORD, aliases included, in the
|
|
77
|
+
* order the help screen shelves them -- so an empty Tab reads like the
|
|
78
|
+
* help index rather than like an alphabetised dump.
|
|
79
|
+
*
|
|
80
|
+
* Role-aware through helpCategories(), for the reason given there: a
|
|
81
|
+
* completer that offers `backlog` to an account the server will 403 is
|
|
82
|
+
* advertising a refusal, and Tab is the worst place to learn about one.
|
|
83
|
+
*/
|
|
84
|
+
export function playerVerbs() {
|
|
85
|
+
return helpCategories().flatMap(c => c.entries.flat());
|
|
86
|
+
}
|
|
87
|
+
/** Which shelf a verb sits on, for the one-line hint Tab prints beside
|
|
88
|
+
* it. Falls back to the primary spelling for an alias, because "walk"
|
|
89
|
+
* is more useful annotated "moving -- see move" than not at all. */
|
|
90
|
+
export function verbShelf(verb) {
|
|
91
|
+
for (const c of HELP_CATEGORIES) {
|
|
92
|
+
for (const entry of c.entries) {
|
|
93
|
+
if (entry.includes(verb))
|
|
94
|
+
return { category: c.title, primary: entry[0] };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=verb-index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.216.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.",
|