@maka/maka-cli 5.215.0 → 5.217.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 +61 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +10 -47
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +56 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/nameables.js +19 -0
- 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.217.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}`),
|
|
@@ -1103,5 +1103,65 @@
|
|
|
1103
1103
|
// accident. The distinction is the rule itself: a character may SAY
|
|
1104
1104
|
// what they know; they may not have it NARRATED at a player who cannot
|
|
1105
1105
|
// hear it.
|
|
1106
|
-
|
|
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
|
+
// 1.76.0 (2026-09-16): THE WAN IS DRAWN AS A TREE (RXBaewsjGDZqqW8ba,
|
|
1142
|
+
// Maka: "the heiarchy is confusing when using 'look' in the matrix.
|
|
1143
|
+
// What 'host' does camera cluster attach to? it says 'live, feeding
|
|
1144
|
+
// the site's host'. If we're allowed to know which host it's attached
|
|
1145
|
+
// to, then let's make this a 'tree' view"). The list was flat: a host
|
|
1146
|
+
// in one section, the devices hanging off it in another, nothing
|
|
1147
|
+
// joining them, and the only clue a phrase that named no host. The
|
|
1148
|
+
// structure was in the model all along and only the rendering hid it.
|
|
1149
|
+
// - HOSTS OVERHEAD now indents a host's slaves beneath it with box
|
|
1150
|
+
// drawing, and ICONS IN REACH holds only what answers to nobody.
|
|
1151
|
+
// - The camera line NAMES its master, from the same cameraMasterHost
|
|
1152
|
+
// tap.ts uses to decide what is on the other end of the cable.
|
|
1153
|
+
// - WHAT COUNTS AS A SLAVE is p.233's rule, not "everything nearby":
|
|
1154
|
+
// "the slaves must be devices and the master must be a host". So the
|
|
1155
|
+
// room's devices and its camera cluster branch off the host; a
|
|
1156
|
+
// person's commlink is THEIR PAN and a persona is nobody's slave,
|
|
1157
|
+
// and both stay at top level.
|
|
1158
|
+
// - "IF WE'RE ALLOWED TO KNOW" IS THE REPORTER'S OWN CONDITIONAL, and
|
|
1159
|
+
// it was RAG-checked rather than assumed. Matrix Perception does NOT
|
|
1160
|
+
// list a device's master among what a hit buys you (p.235), so this
|
|
1161
|
+
// must not become a way to learn one -- and it is not. The engine
|
|
1162
|
+
// already disclosed that these icons answered to "the site's host",
|
|
1163
|
+
// and a room with a node has exactly one; the tree says the same
|
|
1164
|
+
// thing in a shape that can be read. A watched room with NO node
|
|
1165
|
+
// still names no master, pinned as a test.
|
|
1166
|
+
export const ENGINE_VERSION = '1.76.0';
|
|
1107
1167
|
//# 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());
|
|
@@ -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
|
});
|
|
@@ -4,7 +4,7 @@ import { hostsInReach, hostLine, roomsInReach, gridVicinity } from './grid-reach
|
|
|
4
4
|
import { NPC } from '../models/npc.js';
|
|
5
5
|
import { hostLabel, hostPurposeFor } from './grid-names.js';
|
|
6
6
|
import { isLinked } from './comm-style.js';
|
|
7
|
-
import { isWatched, camerasLive, canSnoopFeeds } from './surveillance.js';
|
|
7
|
+
import { isWatched, camerasLive, canSnoopFeeds, cameraMasterHost } from './surveillance.js';
|
|
8
8
|
/**
|
|
9
9
|
* The grid's icon inventory for one room -- everything a persona can
|
|
10
10
|
* ACT on, each with its verb (player rulings 2026-08-23/24: "it's hard
|
|
@@ -277,7 +277,16 @@ export function gridIcons(scene, actor, room) {
|
|
|
277
277
|
// catch is that a persona cannot hold a cable -- so this reads as
|
|
278
278
|
// something your BODY could do, which is the price the rule
|
|
279
279
|
// charges. See commands/tap.ts.
|
|
280
|
-
|
|
280
|
+
// NAME THE MASTER (RXBaewsjGDZqqW8ba, Maka: "What 'host' does
|
|
281
|
+
// camera cluster attach to? it says 'live, feeding the site's
|
|
282
|
+
// host'"). Nothing new is disclosed -- the line already said these
|
|
283
|
+
// lenses answer to a host, and cameraMasterHost is the same
|
|
284
|
+
// function tap.ts uses to decide what is on the other end of the
|
|
285
|
+
// cable. It simply refused to say WHICH, in a district that has
|
|
286
|
+
// exactly one, so the reader was left to guess at a fact the
|
|
287
|
+
// engine had already handed them.
|
|
288
|
+
const owner = cameraMasterHost(scene.getRooms(), room);
|
|
289
|
+
icons.push(`◎ Camera cluster -- live, feeding ${owner ? hostLabel(owner) : `the site's host`}. Loud acts here travel. (own the host to loop them -- or walk in and "tap" a lens in the flesh: slaved to the host means a key to the host)`);
|
|
281
290
|
}
|
|
282
291
|
}
|
|
283
292
|
// THE DIVIDER (eJANPpm7qCaCAJBJw). Only when there is a host to be
|
|
@@ -562,9 +571,6 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
562
571
|
}
|
|
563
572
|
const out = [];
|
|
564
573
|
const hosts = hostsInReach(rooms, actor);
|
|
565
|
-
out.push(hosts.length > 0
|
|
566
|
-
? `{light-blue-fg}HOSTS OVERHEAD:{/light-blue-fg}\n${hosts.map(h => ` {light-blue-fg}${hostLine(h, actor)}{/light-blue-fg}`).join('\n')}`
|
|
567
|
-
: `{light-blue-fg}Nothing overhead -- no host stands over this district.{/light-blue-fg}`);
|
|
568
574
|
// THE DIVIDER DIVIDES NOTHING HERE. gridIcons puts the host's own icon
|
|
569
575
|
// first and splices a "-- and NEAR it ... --" heading after it; this
|
|
570
576
|
// block then pulls the host line OUT (it has its own HOSTS OVERHEAD
|
|
@@ -577,9 +583,51 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
577
583
|
const near = gridIcons(scene, actor, gridVicinity(actor))
|
|
578
584
|
.filter(i => !isHostLine(i))
|
|
579
585
|
.filter((i, idx) => !(idx === 0 && isNearDivider(i)));
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
586
|
+
// THE WAN IS DRAWN AS A TREE (RXBaewsjGDZqqW8ba, Maka: "the heiarchy
|
|
587
|
+
// is confusing when using 'look' in the matrix. What 'host' does
|
|
588
|
+
// camera cluster attach to? ... let's make this a 'tree' view").
|
|
589
|
+
//
|
|
590
|
+
// The list was flat, so a host and the devices hanging off it sat in
|
|
591
|
+
// two separate sections with nothing joining them, and the only clue
|
|
592
|
+
// was the phrase "the site's host" -- which names no host. The
|
|
593
|
+
// structure was in the model the whole time and only the rendering
|
|
594
|
+
// hid it.
|
|
595
|
+
//
|
|
596
|
+
// WHAT IS ACTUALLY SLAVED, and the line is not "everything nearby":
|
|
597
|
+
// a WAN's slaves are DEVICES (p.233 -- "the slaves must be devices and
|
|
598
|
+
// the master must be a host"), which here means the room's own
|
|
599
|
+
// devices and its camera cluster. A person's commlink is their PAN,
|
|
600
|
+
// not the host's, and a persona is nobody's slave; those stay at top
|
|
601
|
+
// level where they belong.
|
|
602
|
+
//
|
|
603
|
+
// AND NOTHING NEW IS DISCLOSED. Checked against the books rather than
|
|
604
|
+
// assumed: Matrix Perception does NOT list a device's master among
|
|
605
|
+
// what a hit buys you (p.235), so this must not become a way to learn
|
|
606
|
+
// one. It doesn't -- the engine already told you these icons answered
|
|
607
|
+
// to "the site's host", and a room with a node has exactly one. The
|
|
608
|
+
// tree says the same thing in a shape that can be read; it does not
|
|
609
|
+
// reach for a relationship the player did not already have.
|
|
610
|
+
const hereRoom = gridVicinity(actor);
|
|
611
|
+
const master = hosts.find(h => h === hereRoom);
|
|
612
|
+
const isSlave = (i) => i.startsWith('▤') || i.startsWith('◎');
|
|
613
|
+
const slaved = master ? near.filter(isSlave) : [];
|
|
614
|
+
const loose = master ? near.filter(i => !isSlave(i)) : near;
|
|
615
|
+
out.push(hosts.length > 0
|
|
616
|
+
? `{light-blue-fg}HOSTS OVERHEAD:{/light-blue-fg}\n${hosts.map(h => {
|
|
617
|
+
const line = ` {light-blue-fg}${hostLine(h, actor)}{/light-blue-fg}`;
|
|
618
|
+
if (h !== master || slaved.length === 0)
|
|
619
|
+
return line;
|
|
620
|
+
// Box-drawing, so the last child closes the branch and the eye can
|
|
621
|
+
// see where the WAN ends.
|
|
622
|
+
const kids = slaved.map((i, idx) => ` {light-blue-fg}${idx === slaved.length - 1 ? '└─' : '├─'} ${i}{/light-blue-fg}`);
|
|
623
|
+
return [line, ...kids].join('\n');
|
|
624
|
+
}).join('\n')}`
|
|
625
|
+
: `{light-blue-fg}Nothing overhead -- no host stands over this district.{/light-blue-fg}`);
|
|
626
|
+
out.push(loose.length > 0
|
|
627
|
+
? `{light-blue-fg}ICONS IN REACH${slaved.length > 0 ? ' -- answering to nobody up there' : ''}:{/light-blue-fg}\n${loose.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`
|
|
628
|
+
: slaved.length > 0
|
|
629
|
+
? `{light-blue-fg}Nothing else near -- everything around you hangs off the host.{/light-blue-fg}`
|
|
630
|
+
: `{light-blue-fg}Nothing near -- background noise around your signal.{/light-blue-fg}`);
|
|
583
631
|
// FARTHER OUT, WITHOUT AN ADDRESS (SR5 p.235, p.243): Matrix Perception
|
|
584
632
|
// never tells you where a device physically sits -- that is Trace Icon,
|
|
585
633
|
// two marks and an opposed test. So far icons are listed by the noise
|
|
@@ -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
|
|
@@ -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.217.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.",
|