@maka/maka-cli 5.63.0 → 5.64.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 +29 -8
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.d.ts +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +86 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +29 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +22 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jobs.js +16 -9
- package/bundle/typescript/src/commands/game/sideQuest/commands/jobs.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/journal.d.ts +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/journal.js +136 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/journal.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.d.ts +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +99 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/players.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/players.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +27 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +30 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.d.ts +47 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +91 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/time.d.ts +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/time.js +61 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/time.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +45 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +150 -9
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +104 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +257 -11
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +78 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +188 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless.d.ts +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +54 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +62 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +121 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.d.ts +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js +28 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/puzzle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +67 -7
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +116 -19
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.d.ts +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +26 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-dump.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-dump.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.d.ts +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.d.ts +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +30 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/playtester.d.ts +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/playtester.js +43 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/playtester.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.d.ts +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +42 -28
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +66 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +45 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +41 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/world-clock.d.ts +83 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/world-clock.js +106 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/world-clock.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import blessed from 'blessed';
|
|
2
2
|
import { dirname } from 'path';
|
|
3
3
|
import { capitalCase } from 'change-case';
|
|
4
|
-
import { Category } from './types/shared/item-enum.js';
|
|
4
|
+
import { Category, Size, Rating } from './types/shared/item-enum.js';
|
|
5
5
|
import { Item } from './models/item.js';
|
|
6
6
|
import { Room } from './models/_index.js';
|
|
7
7
|
import { Door } from './models/door.js';
|
|
@@ -27,6 +27,9 @@ import { TakeCommand } from './commands/take.js';
|
|
|
27
27
|
import { LootCommand } from './commands/loot.js';
|
|
28
28
|
import { NoteCommand } from './commands/note.js';
|
|
29
29
|
import { BacklogCommand } from './commands/backlog.js';
|
|
30
|
+
// Whether this account may actually USE the backlog -- gates what the
|
|
31
|
+
// help index offers, never what the verb accepts (item D).
|
|
32
|
+
import { isPlaytester, probePlaytesterStanding } from './utilities/playtester.js';
|
|
30
33
|
import { RolesCommand } from './commands/roles.js';
|
|
31
34
|
import { SinCommand } from './commands/sin.js';
|
|
32
35
|
import { GoCommand } from './commands/go.js';
|
|
@@ -109,6 +112,7 @@ import { SellCommand } from './commands/sell.js';
|
|
|
109
112
|
import { PayCommand } from './commands/pay.js';
|
|
110
113
|
import { LifestyleCommand } from './commands/lifestyle.js';
|
|
111
114
|
import { JobsCommand } from './commands/jobs.js';
|
|
115
|
+
import { JournalCommand } from './commands/journal.js';
|
|
112
116
|
import { TreatCommand } from './commands/treat.js';
|
|
113
117
|
import { WorkCommand } from './commands/work.js';
|
|
114
118
|
import { EatCommand } from './commands/eat.js';
|
|
@@ -127,6 +131,10 @@ import { OverwatchCommand } from './commands/overwatch.js';
|
|
|
127
131
|
import { MarkCommand } from './commands/mark.js';
|
|
128
132
|
import { SnoopCommand } from './commands/snoop.js';
|
|
129
133
|
import { TapCommand } from './commands/tap.js';
|
|
134
|
+
import { TimeCommand } from './commands/time.js';
|
|
135
|
+
// The HUD's clock -- read in the engine so a shared run reads the
|
|
136
|
+
// host's wall clock, never each client's own (item B).
|
|
137
|
+
import { clockTime, worldNow } from './utilities/world-clock.js';
|
|
130
138
|
import { AutoFight } from './utilities/auto-fight.js';
|
|
131
139
|
import { CommandFactory } from './factories/command-factory.js';
|
|
132
140
|
import { SceneSynthesizer } from './factories/scene-factory.js';
|
|
@@ -156,6 +164,12 @@ export default class Game {
|
|
|
156
164
|
_quitArmed = false;
|
|
157
165
|
_escArmedAt = 0;
|
|
158
166
|
_statusBox;
|
|
167
|
+
/** The clock gutter left of the prompt, its ticker, and the face it
|
|
168
|
+
* last painted -- compared so a repaint only happens on the minute
|
|
169
|
+
* rollover (see paintClock). */
|
|
170
|
+
_clockBox;
|
|
171
|
+
_clockTimer;
|
|
172
|
+
_clockFace = '';
|
|
159
173
|
_inputBox;
|
|
160
174
|
/** The floating icon-minimap (map-overlay wave 2026-08-24). Optional:
|
|
161
175
|
* headless sessions never build widgets. */
|
|
@@ -172,6 +186,24 @@ export default class Game {
|
|
|
172
186
|
_anonymous = false;
|
|
173
187
|
_logFilePath = '';
|
|
174
188
|
_tier = 1;
|
|
189
|
+
/**
|
|
190
|
+
* The run's difficulty tier. `activateRunScene` sets it from the tier
|
|
191
|
+
* the scene was generated at; a HEADLESS boot never calls that, so a
|
|
192
|
+
* shared run sat at the default 1 no matter what it was actually
|
|
193
|
+
* generated as (audit 2026-08-27).
|
|
194
|
+
*
|
|
195
|
+
* That does not sit still: the stored tier rides into the save
|
|
196
|
+
* snapshot and `continueToNextScene` reads `_tier + 1` for the next
|
|
197
|
+
* draft, so an under-recorded tier propagates forward into content
|
|
198
|
+
* that has not been generated yet. Exposed so the headless boot can
|
|
199
|
+
* state the tier it was handed instead of guessing.
|
|
200
|
+
*/
|
|
201
|
+
get tier() {
|
|
202
|
+
return this._tier;
|
|
203
|
+
}
|
|
204
|
+
set tier(value) {
|
|
205
|
+
this._tier = value;
|
|
206
|
+
}
|
|
175
207
|
// Guards against two overlapping continueToNextScene() calls -- found via
|
|
176
208
|
// a real session where calling the fixer twice in a row (nothing stopped
|
|
177
209
|
// it, and a 20-40+ second draft invites a second, impatient call) fired
|
|
@@ -865,6 +897,80 @@ export default class Game {
|
|
|
865
897
|
* setCharacterLocation (the ONLY path that binds room observers), and
|
|
866
898
|
* hand it the scene for its AI context.
|
|
867
899
|
*/
|
|
900
|
+
/**
|
|
901
|
+
* THE FIXER IS REACHABLE FROM THE FIELD (player go 2026-08-27: "on a
|
|
902
|
+
* multi-crew session, I should still be able to TALK to Mr. Krow if I
|
|
903
|
+
* need something").
|
|
904
|
+
*
|
|
905
|
+
* A shared run executes on a server whose Game has no hub scene, so
|
|
906
|
+
* `hubCast()` is empty and NOBODY from home is a call candidate --
|
|
907
|
+
* including the fixer. Solo mid-run they are (see call.ts's own note
|
|
908
|
+
* about "call Ratchet" bouncing while the roster promised a phone
|
|
909
|
+
* icon), so the same fiction behaved differently by mode, invisibly.
|
|
910
|
+
*
|
|
911
|
+
* The ruling that keeps the ROSTER home is untouched, because the
|
|
912
|
+
* roster and the fixer are different KINDS of thing. The roster is
|
|
913
|
+
* per-player relationship state -- your loyalty, your favours, your
|
|
914
|
+
* ledger -- which is not merely awkward on a shared server but
|
|
915
|
+
* meaningless there, since four players have four different Mama Wus.
|
|
916
|
+
* The fixer is world furniture: one fixed identity, the same Krow for
|
|
917
|
+
* everyone at the table, carrying no per-player state. A shared-run
|
|
918
|
+
* Krow is well-defined in a way a shared-run roster is not, so
|
|
919
|
+
* answering yes here costs nothing that ruling was protecting.
|
|
920
|
+
*
|
|
921
|
+
* Deliberately NOT the whole hub cast: that needs per-player contact
|
|
922
|
+
* identity on the server and would reopen the ruling.
|
|
923
|
+
*
|
|
924
|
+
* He is built lazily and kept OFF the scene -- he is a voice on a
|
|
925
|
+
* commlink, not a body in the room, so he must never appear in "look",
|
|
926
|
+
* be attacked, or be looted. Returns undefined in any session that
|
|
927
|
+
* already has a real one (a hub session, or a static scene that ships
|
|
928
|
+
* him as a cast member), so there is only ever one Krow.
|
|
929
|
+
*/
|
|
930
|
+
remoteFixer() {
|
|
931
|
+
if (this.hasHub || !this.scene)
|
|
932
|
+
return undefined;
|
|
933
|
+
if (this.scene.allActors.some(a => isFixerName(a.name)))
|
|
934
|
+
return undefined;
|
|
935
|
+
if (this._remoteFixer)
|
|
936
|
+
return this._remoteFixer;
|
|
937
|
+
// His own private "room": never added to the scene, so nothing can
|
|
938
|
+
// walk into it. It exists because an NPC needs somewhere to stand
|
|
939
|
+
// for its own AI context to resolve.
|
|
940
|
+
const line = new Room({
|
|
941
|
+
name: 'On The Line',
|
|
942
|
+
description: 'Somewhere across the sprawl, a man takes a call he was expecting.',
|
|
943
|
+
roomType: 'void',
|
|
944
|
+
});
|
|
945
|
+
const npc = new NPC({
|
|
946
|
+
dialog: [
|
|
947
|
+
`Talk to me, chummer -- you're on the clock.`,
|
|
948
|
+
`I broker the work. What happens on site is your business.`,
|
|
949
|
+
],
|
|
950
|
+
description: `The fixer who put this job together. He is not on site and never will be -- he brokers, he does not run. Reachable by commlink because that is the whole point of a fixer. He knows the JOB exists and who hired for it; he does NOT know the inside of the place, the codes, or what the runners are looking at right now.`,
|
|
951
|
+
playerConstructorConfig: {
|
|
952
|
+
playerName: FIXER_NAME,
|
|
953
|
+
startLocation: line,
|
|
954
|
+
combat: { body: 3, agility: 3, reaction: 3, willpower: 4, logic: 4, intuition: 4, charisma: 5, strength: 3, edge: 3 },
|
|
955
|
+
},
|
|
956
|
+
}, new CommandRegistry(this.scene));
|
|
957
|
+
// A fixer with no commlink is a contradiction, and call.ts gates on
|
|
958
|
+
// carrying one (hasCommlink) before it will connect.
|
|
959
|
+
// NOTE the enum VALUES here. A runtime Item validates against the
|
|
960
|
+
// enum's values ('tiny'); a SAVED item uses the enum KEYS ('Tiny').
|
|
961
|
+
// The two conventions are inverses and both are load-bearing.
|
|
962
|
+
npc.inventory.addItem(new Item({
|
|
963
|
+
name: 'Fixer\'s Commlink',
|
|
964
|
+
description: 'The number everyone has and nobody sells.',
|
|
965
|
+
size: Size.Tiny, category: Category.Commlink, shape: 'Rectangle',
|
|
966
|
+
color: 'Black', texture: 'Matte', rating: Rating.Common, weight: 0.1,
|
|
967
|
+
}));
|
|
968
|
+
npc.scene = this.scene;
|
|
969
|
+
this._remoteFixer = npc;
|
|
970
|
+
Logger.getInstance().write(`Remote fixer materialised for a hubless session: ${FIXER_NAME}.`);
|
|
971
|
+
return npc;
|
|
972
|
+
}
|
|
973
|
+
_remoteFixer;
|
|
868
974
|
spawnCompanionShell(opts) {
|
|
869
975
|
if (this.scene.getActor(opts.name)) {
|
|
870
976
|
Logger.getInstance().write(`spawnCompanionShell: name "${opts.name}" already in scene -- refusing.`);
|
|
@@ -1466,6 +1572,24 @@ export default class Game {
|
|
|
1466
1572
|
// state never is (runs are disposable); reconstructed fresh by
|
|
1467
1573
|
// activateRunScene, settled into the roster by returnToHub.
|
|
1468
1574
|
currentClient;
|
|
1575
|
+
/**
|
|
1576
|
+
* WHEN THE RUN STARTED -- the one fact the journal cannot derive.
|
|
1577
|
+
*
|
|
1578
|
+
* commands/journal.ts renders current state rather than recording
|
|
1579
|
+
* events, deliberately: a journal that logs what happened can drift
|
|
1580
|
+
* from the world it describes, and a derived one cannot disagree with
|
|
1581
|
+
* the thing it is reading. Client, pay and objective all already exist
|
|
1582
|
+
* elsewhere and are read live. Elapsed time is the exception -- its
|
|
1583
|
+
* ORIGIN is not recoverable from any current state -- so this is the
|
|
1584
|
+
* single new field the feature adds, said out loud rather than grown
|
|
1585
|
+
* quietly into a record.
|
|
1586
|
+
*
|
|
1587
|
+
* Not saved, and does not need to be: like currentClient above, mid-run
|
|
1588
|
+
* state never persists (runs are disposable -- a quit resumes to the
|
|
1589
|
+
* hub with the job back on offer), so there is no save-schema change
|
|
1590
|
+
* here. Stamped by activateRunScene, cleared by settleClientReputation.
|
|
1591
|
+
*/
|
|
1592
|
+
runStartedAt;
|
|
1469
1593
|
// Taxi warn-once (player ruling): leaving with the pay unclaimed
|
|
1470
1594
|
// arms this; the second "call taxi" forfeits. Mirrors _quitArmed.
|
|
1471
1595
|
forfeitArmed = false;
|
|
@@ -1541,6 +1665,7 @@ export default class Game {
|
|
|
1541
1665
|
settleClientReputation(wrappedRun) {
|
|
1542
1666
|
const client = this.currentClient;
|
|
1543
1667
|
this.currentClient = undefined;
|
|
1668
|
+
this.runStartedAt = undefined;
|
|
1544
1669
|
this.forfeitArmed = false;
|
|
1545
1670
|
if (!client)
|
|
1546
1671
|
return '';
|
|
@@ -1799,6 +1924,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
1799
1924
|
CommandFactory.registerCommand('drink', DrinkCommand);
|
|
1800
1925
|
CommandFactory.registerCommand('lifestyle', LifestyleCommand);
|
|
1801
1926
|
CommandFactory.registerCommand('jobs', JobsCommand);
|
|
1927
|
+
CommandFactory.registerCommand('journal', JournalCommand);
|
|
1802
1928
|
CommandFactory.registerCommand('treat', TreatCommand);
|
|
1803
1929
|
CommandFactory.registerCommand('work', WorkCommand);
|
|
1804
1930
|
CommandFactory.registerCommand('gig', WorkCommand);
|
|
@@ -1827,6 +1953,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
1827
1953
|
// "jackpoint" are what players reach for when they mean the same act.
|
|
1828
1954
|
CommandFactory.registerCommand('tap', TapCommand);
|
|
1829
1955
|
CommandFactory.registerCommand('splice', TapCommand);
|
|
1956
|
+
// The wall clock (item B). "clock" too -- half of us type that.
|
|
1957
|
+
CommandFactory.registerCommand('time', TimeCommand);
|
|
1958
|
+
CommandFactory.registerCommand('clock', TimeCommand);
|
|
1830
1959
|
}
|
|
1831
1960
|
determineStartRoom() {
|
|
1832
1961
|
return this.scene.determineStartRoom();
|
|
@@ -1928,6 +2057,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
1928
2057
|
this._saveFilePath = options.saveFilePath;
|
|
1929
2058
|
this._owner = options.owner;
|
|
1930
2059
|
this._anonymous = options.anonymousRunner === true;
|
|
2060
|
+
// Ask the server, once, whether this account is a play-tester, so
|
|
2061
|
+
// the help index can stop advertising a backlog that will only 403
|
|
2062
|
+
// (item D). Fire-and-forget on purpose: it never blocks the boot,
|
|
2063
|
+
// never throws, and an unanswered probe just leaves the menu entry
|
|
2064
|
+
// hidden while the verb stays usable. See utilities/playtester.ts.
|
|
2065
|
+
void probePlaytesterStanding({ anonymous: this._anonymous });
|
|
1931
2066
|
// ONE loud notice the moment the AI provider refuses on ACCOUNT
|
|
1932
2067
|
// grounds (empty credits, bad key). Without it a dead key mutes
|
|
1933
2068
|
// every NPC and makes every job "fall through" invisibly -- a real
|
|
@@ -2041,6 +2176,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2041
2176
|
equipIntoEmptySlots(this.player);
|
|
2042
2177
|
this.warmNpcAppearancesInBackground();
|
|
2043
2178
|
this.startPursuitHeartbeat();
|
|
2179
|
+
this.startClockTicker();
|
|
2044
2180
|
// The banner learns where we are only after the hub/scene bookkeeping
|
|
2045
2181
|
// above settles.
|
|
2046
2182
|
this.refreshLogLabel();
|
|
@@ -2761,7 +2897,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2761
2897
|
// playtest note: being yanked into the run the instant generation
|
|
2762
2898
|
// finished was jarring). The player invokes it on their own time:
|
|
2763
2899
|
// "call Mr. Johnson" delivers the offer (haggle window included --
|
|
2764
|
-
// see call.ts), and "
|
|
2900
|
+
// see call.ts), and "call taxi" actually goes (launchPendingRun).
|
|
2765
2901
|
if (this.hasHub) {
|
|
2766
2902
|
// The mid-generation race (see draftedForParty above): if the live
|
|
2767
2903
|
// party changed while Krow was on the phone, this seed was quoted
|
|
@@ -2795,6 +2931,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2795
2931
|
this.camPingedRooms.clear();
|
|
2796
2932
|
this.sweptRooms.clear();
|
|
2797
2933
|
this.sceneSeed = seed;
|
|
2934
|
+
// The clock starts when the run does (commands/journal.ts).
|
|
2935
|
+
this.runStartedAt = worldNow();
|
|
2798
2936
|
// A call partner from the old scene wouldn't exist in the new one --
|
|
2799
2937
|
// hang up before moving on, rather than leave a dangling reference.
|
|
2800
2938
|
if (this.player.activeCallPartner) {
|
|
@@ -3007,6 +3145,45 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3007
3145
|
});
|
|
3008
3146
|
}
|
|
3009
3147
|
}
|
|
3148
|
+
/**
|
|
3149
|
+
* THE CLOCK TICKER (playtest 2026-08-27). The gutter rendered once,
|
|
3150
|
+
* whenever the pane happened to paint, and then showed that moment
|
|
3151
|
+
* forever. Parked beside a live prompt it is under the player's eye
|
|
3152
|
+
* for the whole run, so a stale one is worse than none at all -- and
|
|
3153
|
+
* the move made that matter MORE, not less.
|
|
3154
|
+
*
|
|
3155
|
+
* Repaints on the MINUTE ROLLOVER, not on every tick: it wakes each
|
|
3156
|
+
* second but only touches the screen when the rendered string
|
|
3157
|
+
* actually changes. A forced once-a-second render would flicker the
|
|
3158
|
+
* whole terminal and cost more than the feature is worth.
|
|
3159
|
+
*/
|
|
3160
|
+
paintClock() {
|
|
3161
|
+
if (!this._clockBox)
|
|
3162
|
+
return;
|
|
3163
|
+
const face = clockTime();
|
|
3164
|
+
if (face === this._clockFace)
|
|
3165
|
+
return;
|
|
3166
|
+
this._clockFace = face;
|
|
3167
|
+
this._clockBox.setContent(`{center}{cyan-fg}${face}{/cyan-fg}{/center}`);
|
|
3168
|
+
this._screen?.render();
|
|
3169
|
+
}
|
|
3170
|
+
startClockTicker() {
|
|
3171
|
+
// No gutter, nothing to repaint. Headless sessions have no screen,
|
|
3172
|
+
// and a server hosting many of them should not spin one interval
|
|
3173
|
+
// per session to call a function that early-returns.
|
|
3174
|
+
if (!this._clockBox || this._clockTimer)
|
|
3175
|
+
return;
|
|
3176
|
+
this._clockTimer = setInterval(() => this.paintClock(), 1000);
|
|
3177
|
+
// Never hold the process open for a clock: a session that is
|
|
3178
|
+
// otherwise finished should exit, not linger for the next minute.
|
|
3179
|
+
this._clockTimer.unref?.();
|
|
3180
|
+
}
|
|
3181
|
+
stopClockTicker() {
|
|
3182
|
+
if (this._clockTimer) {
|
|
3183
|
+
clearInterval(this._clockTimer);
|
|
3184
|
+
this._clockTimer = undefined;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3010
3187
|
stopPresenceHeartbeat() {
|
|
3011
3188
|
if (this._presenceTimer) {
|
|
3012
3189
|
clearInterval(this._presenceTimer);
|
|
@@ -3020,6 +3197,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3020
3197
|
*/
|
|
3021
3198
|
async dispose() {
|
|
3022
3199
|
this.stopPresenceHeartbeat();
|
|
3200
|
+
// A stray interval outliving a closed shared session would be a
|
|
3201
|
+
// nasty thing to find later, so it retires with the rest of them.
|
|
3202
|
+
this.stopClockTicker();
|
|
3023
3203
|
if (this._presenceEquipTimer)
|
|
3024
3204
|
clearTimeout(this._presenceEquipTimer);
|
|
3025
3205
|
this.stopPursuitHeartbeat();
|
|
@@ -3033,10 +3213,39 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3033
3213
|
}
|
|
3034
3214
|
initInputBox(screen) {
|
|
3035
3215
|
// Create an input box for user commands
|
|
3036
|
-
|
|
3216
|
+
// THE CLOCK GUTTER (playtest 2026-08-27: "if it makes sense, the
|
|
3217
|
+
// clock can be to the left of the input box"). A fixed-width pane
|
|
3218
|
+
// at left:0 with the input narrowed to match.
|
|
3219
|
+
//
|
|
3220
|
+
// This is also where the truncation bug goes to die rather than
|
|
3221
|
+
// move house. The clock used to be the LAST field on the Armor
|
|
3222
|
+
// line, so it was the first thing clipped when that pane got tight
|
|
3223
|
+
// -- which is why the player could not find it. A dedicated
|
|
3224
|
+
// fixed-width gutter cannot be squeezed by its neighbours: the
|
|
3225
|
+
// input field yields the columns instead, and long input already
|
|
3226
|
+
// scrolls horizontally. The cost is CLOCK_GUTTER_W columns of
|
|
3227
|
+
// typing room.
|
|
3228
|
+
//
|
|
3229
|
+
// A sibling widget does not touch the textbox's internal cursor --
|
|
3230
|
+
// it just gets a narrower width -- so there is no cursor maths here.
|
|
3231
|
+
const clockBox = blessed.box({
|
|
3232
|
+
id: 'clockBox',
|
|
3037
3233
|
bottom: 0,
|
|
3038
3234
|
left: 0,
|
|
3039
|
-
width:
|
|
3235
|
+
width: Game.CLOCK_GUTTER_W,
|
|
3236
|
+
height: 3,
|
|
3237
|
+
tags: true,
|
|
3238
|
+
border: { type: 'line' },
|
|
3239
|
+
style: { border: { fg: 'cyan' } },
|
|
3240
|
+
content: '',
|
|
3241
|
+
});
|
|
3242
|
+
this._clockBox = clockBox;
|
|
3243
|
+
screen.append(clockBox);
|
|
3244
|
+
this.paintClock();
|
|
3245
|
+
const inputBox = blessed.textbox({
|
|
3246
|
+
bottom: 0,
|
|
3247
|
+
left: Game.CLOCK_GUTTER_W,
|
|
3248
|
+
width: `100%-${Game.CLOCK_GUTTER_W}`,
|
|
3040
3249
|
height: 3,
|
|
3041
3250
|
tags: true,
|
|
3042
3251
|
border: { type: 'line' },
|
|
@@ -3307,6 +3516,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3307
3516
|
* the inner width from it to right-justify the elevation bar. */
|
|
3308
3517
|
static ROOM_DOCK_PCT = 0.23;
|
|
3309
3518
|
/** Must track the mapBox's `width` below -- used to trim its caption. */
|
|
3519
|
+
/** Columns the clock gutter takes from the input field. Wide enough
|
|
3520
|
+
* for "HH:MM" inside a border; adjustable if it costs too much
|
|
3521
|
+
* typing room in play. */
|
|
3522
|
+
static CLOCK_GUTTER_W = 8;
|
|
3310
3523
|
static MAP_DOCK_PCT = 0.12;
|
|
3311
3524
|
initMapBox(screen) {
|
|
3312
3525
|
const mapBox = blessed.box({
|
|
@@ -3554,6 +3767,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3554
3767
|
// decide whether calls land while you are away from your body.
|
|
3555
3768
|
const { overlay, link } = arStatusSlots(player);
|
|
3556
3769
|
const arSlot = player.plane === 'meat' ? ` | ${overlay}` : '';
|
|
3770
|
+
// THE WALL CLOCK rides the readiness line (item B). Rendered HERE,
|
|
3771
|
+
// in the engine, which in a shared run is the server -- so every
|
|
3772
|
+
// player's HUD shows the host's clock and "after midnight" is one
|
|
3773
|
+
// moment for the whole table. utilities/world-clock.ts has the why.
|
|
3774
|
+
// Time only, not the date: the date is what "time" is for.
|
|
3775
|
+
// The clock LEFT this line for the gutter beside the prompt. It was
|
|
3776
|
+
// the last field here, so it was the first thing clipped whenever
|
|
3777
|
+
// the pane got tight -- the reason the player could not find it.
|
|
3557
3778
|
lines.push(`${armorNote}${arSlot} | ${link}`);
|
|
3558
3779
|
// Lines 4+: transient states, present only while active. Future
|
|
3559
3780
|
// contexts (astral projection, VR hot-sim) join here the same way.
|
|
@@ -3913,10 +4134,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3913
4134
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
3914
4135
|
{ title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['register']] },
|
|
3915
4136
|
{ title: 'social', entries: [['say'], ['emote', 'me'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
3916
|
-
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
4137
|
+
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
3917
4138
|
{ title: 'barriers', entries: [['solve'], ['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
|
|
3918
4139
|
{ title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
3919
|
-
{ title: 'session', entries: [['help'], ['hints'], ['note'], ['backlog'], ['roles'], ['clear'], ['quit', 'exit']] },
|
|
4140
|
+
{ title: 'session', entries: [['help'], ['hints'], ['time', 'clock'], ['note'], ['backlog'], ['roles'], ['clear'], ['quit', 'exit']] },
|
|
3920
4141
|
];
|
|
3921
4142
|
/** Registered but deliberately UNLISTED: reachable as subcommands or
|
|
3922
4143
|
* muscle-memory ("players" folded back under crew after playtesting
|
|
@@ -3924,6 +4145,11 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3924
4145
|
* answers). Without this, the uncategorized sweep resurrects them
|
|
3925
4146
|
* in an "other" bucket. */
|
|
3926
4147
|
static HIDDEN_VERBS = ['players'];
|
|
4148
|
+
/** Session verbs that only a play-tester (or admin) can actually use
|
|
4149
|
+
* -- the server role-gates both and 403s everyone else. Listed in
|
|
4150
|
+
* help ONLY for a confirmed play-tester; see helpCategories. `roles`
|
|
4151
|
+
* rides along because it is admin-only, a strictly narrower group. */
|
|
4152
|
+
static PLAYTESTER_VERBS = new Set(['backlog', 'roles']);
|
|
3927
4153
|
/** Descriptions for the verbs handled inline in handleInput() rather than registered. */
|
|
3928
4154
|
static BUILTIN_DESCRIPTIONS = {
|
|
3929
4155
|
help: 'This overview. "help <category>" for one group with full descriptions; "help <command>" for one command.',
|
|
@@ -3937,17 +4163,37 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3937
4163
|
const CommandClass = CommandFactory.getAllRegisteredCommands()[verb];
|
|
3938
4164
|
return CommandClass?.getDescription() ?? 'No description available.';
|
|
3939
4165
|
}
|
|
4166
|
+
/**
|
|
4167
|
+
* The help index as THIS account should see it (backlog wave 2, item
|
|
4168
|
+
* D: "let's remove Seattle Backlog for non playtesters"). The backlog
|
|
4169
|
+
* is role-gated on the server and 403s everyone else, so listing it
|
|
4170
|
+
* for every player only advertises a refusal. Hidden unless the
|
|
4171
|
+
* account is a CONFIRMED play-tester -- see utilities/playtester.ts
|
|
4172
|
+
* for why "unknown" (offline, logged out) hides rather than shows,
|
|
4173
|
+
* and why the verb itself stays registered and usable regardless.
|
|
4174
|
+
*/
|
|
4175
|
+
helpCategories() {
|
|
4176
|
+
if (isPlaytester())
|
|
4177
|
+
return Game.HELP_CATEGORIES;
|
|
4178
|
+
return Game.HELP_CATEGORIES.map(c => c.title !== 'session' ? c : {
|
|
4179
|
+
...c,
|
|
4180
|
+
entries: c.entries.filter(e => !Game.PLAYTESTER_VERBS.has(e[0])),
|
|
4181
|
+
});
|
|
4182
|
+
}
|
|
3940
4183
|
getHelpMessage(args = []) {
|
|
3941
4184
|
const registered = Object.keys(CommandFactory.getAllRegisteredCommands());
|
|
4185
|
+
const helpCategories = this.helpCategories();
|
|
4186
|
+
// The uncategorized sweep must not resurrect what helpCategories
|
|
4187
|
+
// just hid, so the restricted verbs stay "covered" either way.
|
|
3942
4188
|
const covered = new Set([...Game.HELP_CATEGORIES.flatMap(c => c.entries.flat()), ...Game.HIDDEN_VERBS]);
|
|
3943
4189
|
const uncategorized = registered.filter(v => !covered.has(v));
|
|
3944
4190
|
const query = args.join(' ').trim().toLowerCase();
|
|
3945
4191
|
// "help <category>": that group, with full descriptions.
|
|
3946
4192
|
if (query) {
|
|
3947
|
-
const categoryNames =
|
|
4193
|
+
const categoryNames = helpCategories.map(c => c.title);
|
|
3948
4194
|
const pickedCategory = fuzzyPickName(query, categoryNames);
|
|
3949
4195
|
if (pickedCategory) {
|
|
3950
|
-
const category =
|
|
4196
|
+
const category = helpCategories.find(c => c.title === pickedCategory);
|
|
3951
4197
|
const lines = [`${category.title.toUpperCase()}`];
|
|
3952
4198
|
for (const [primary, ...aliases] of category.entries) {
|
|
3953
4199
|
lines.push(`${BULLET}${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
|
|
@@ -3962,7 +4208,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3962
4208
|
const pickedVerb = fuzzyPickName(words[0], [...covered, ...uncategorized])
|
|
3963
4209
|
?? fuzzyPickName(query, [...covered, ...uncategorized]);
|
|
3964
4210
|
if (pickedVerb) {
|
|
3965
|
-
const entry =
|
|
4211
|
+
const entry = helpCategories.flatMap(c => c.entries).find(e => e.includes(pickedVerb));
|
|
3966
4212
|
const primary = entry?.[0] ?? pickedVerb;
|
|
3967
4213
|
const aliases = entry?.slice(1) ?? [];
|
|
3968
4214
|
const CommandClass = CommandFactory.getAllRegisteredCommands()[primary];
|
|
@@ -4018,7 +4264,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
4018
4264
|
// line outgrows the Mechanics column (help renders there now --
|
|
4019
4265
|
// blessed's mid-list wrap lost the left-column alignment).
|
|
4020
4266
|
const lines = ['COMMANDS'];
|
|
4021
|
-
const pad = Math.max(...
|
|
4267
|
+
const pad = Math.max(...helpCategories.map(c => c.title.length));
|
|
4022
4268
|
const pushWrapped = (title, verbs) => {
|
|
4023
4269
|
const rows = [];
|
|
4024
4270
|
let cur = '';
|
|
@@ -4037,7 +4283,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
4037
4283
|
for (const r of rows.slice(1))
|
|
4038
4284
|
lines.push(`${' '.repeat(pad)} ${r}`);
|
|
4039
4285
|
};
|
|
4040
|
-
for (const category of
|
|
4286
|
+
for (const category of helpCategories) {
|
|
4041
4287
|
pushWrapped(category.title, category.entries.map(e => e[0]));
|
|
4042
4288
|
}
|
|
4043
4289
|
if (uncategorized.length > 0) {
|