@maka/maka-cli 5.7.0 → 5.8.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/deploy/ubuntu.sub.cmd.js +16 -31
- package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +11 -8
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +32 -14
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.d.ts +33 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +137 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +53 -12
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +12 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +23 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.d.ts +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +151 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.d.ts +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +112 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +81 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +367 -9
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +33 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +162 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +32 -5
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +15 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +111 -28
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +7 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +8 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.d.ts +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js +15 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/log-style.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js +49 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/logger.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
- package/bundle/typescript/src/commands/ssl.js +10 -15
- package/bundle/typescript/src/commands/ssl.js.map +1 -1
- package/bundle/typescript/src/templates/react/kit/mui/auth/password-reset/basic-password-reset-page.js.tsx +3 -4
- package/bundle/typescript/src/tools/ssl/ssl.class.d.ts +8 -0
- package/bundle/typescript/src/tools/ssl/ssl.class.js +53 -0
- package/bundle/typescript/src/tools/ssl/ssl.class.js.map +1 -1
- package/package.json +1 -1
|
@@ -56,6 +56,9 @@ import { AskCommand } from './commands/ask.js';
|
|
|
56
56
|
import { ContactsCommand } from './commands/contacts.js';
|
|
57
57
|
import { JumpCommand } from './commands/jump.js';
|
|
58
58
|
import { InitiateCommand } from './commands/initiate.js';
|
|
59
|
+
import { SummonCommand } from './commands/summon.js';
|
|
60
|
+
import { OrderCommand } from './commands/order.js';
|
|
61
|
+
import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
|
|
59
62
|
import { InstallCommand } from './commands/install.js';
|
|
60
63
|
import { BuyCommand } from './commands/buy.js';
|
|
61
64
|
import { SellCommand } from './commands/sell.js';
|
|
@@ -67,6 +70,7 @@ import { EatCommand } from './commands/eat.js';
|
|
|
67
70
|
import { DrinkCommand } from './commands/drink.js';
|
|
68
71
|
import { equipIntoEmptySlots } from './archetypes.js';
|
|
69
72
|
import { NPC } from './models/npc.js';
|
|
73
|
+
import { CommandRegistry } from './commands/command-registry.js';
|
|
70
74
|
import { HaggleCommand } from './commands/haggle.js';
|
|
71
75
|
import { SneakCommand } from './commands/sneak.js';
|
|
72
76
|
import { PersuadeCommand } from './commands/persuade.js';
|
|
@@ -76,6 +80,8 @@ import { CommandFactory } from './factories/command-factory.js';
|
|
|
76
80
|
import { SceneSynthesizer } from './factories/scene-factory.js';
|
|
77
81
|
import { SceneSeedGenerator, FIXER_NAME } from './factories/scene-seed-generator.js';
|
|
78
82
|
import { CALL_ICON, CALL_COLOR } from './utilities/comm-style.js';
|
|
83
|
+
import { AI } from '../../../tools/ai/ai.class.js';
|
|
84
|
+
import { BULLET } from './utilities/log-style.js';
|
|
79
85
|
export default class Game {
|
|
80
86
|
initialized;
|
|
81
87
|
static instance;
|
|
@@ -91,6 +97,7 @@ export default class Game {
|
|
|
91
97
|
// give the log the whole screen.
|
|
92
98
|
_screen;
|
|
93
99
|
_logBox;
|
|
100
|
+
_metaBox;
|
|
94
101
|
_statusBox;
|
|
95
102
|
_inputBox;
|
|
96
103
|
_useAi = false;
|
|
@@ -164,6 +171,227 @@ export default class Game {
|
|
|
164
171
|
}
|
|
165
172
|
Logger.getInstance().write(`Home turf: ${this.contacts.size} contacts seeded from the hub cast.`);
|
|
166
173
|
}
|
|
174
|
+
// ==================== COMPANIONS: the party layer ====================
|
|
175
|
+
// Every companion is a per-scene NPC SHELL plus persistent STATE, and
|
|
176
|
+
// scene transitions are covered by narration, never instance rebinding
|
|
177
|
+
// (design decision with the player): spirits fade (RAW p.300: the
|
|
178
|
+
// summons doesn't outlast the ride), drones fold to the pack, agents
|
|
179
|
+
// into the deck -- damage lives on the bound Item and redeploys free.
|
|
180
|
+
// Tenants: summoned spirits (summon.ts), matrix agents (agent-cmd.ts),
|
|
181
|
+
// autonomous drones (deploy in drone-deploy.ts). Future tenants
|
|
182
|
+
// (enlisted allies, anthroform frames) ride the same rail with
|
|
183
|
+
// different words ("takes their own ride and meets you there").
|
|
184
|
+
// Roster invariant: entries are LIVE SHELLS only -- dismissal and
|
|
185
|
+
// death both remove the entry; persistent state (the frame, the deck)
|
|
186
|
+
// lives on Items in the player's possession, never here.
|
|
187
|
+
companions = [];
|
|
188
|
+
// Drones deployed at travel time re-shell on arrival ("the rotors spin
|
|
189
|
+
// back up at the curb") -- tracked as their items across the swap.
|
|
190
|
+
_pendingRedeploys = [];
|
|
191
|
+
/**
|
|
192
|
+
* The one correct runtime NPC spawn (mirrors scene-factory's seed
|
|
193
|
+
* path): construct with a scene-bound registry, add to the scene
|
|
194
|
+
* (names must be unique -- addActor throws), place via
|
|
195
|
+
* setCharacterLocation (the ONLY path that binds room observers), and
|
|
196
|
+
* hand it the scene for its AI context.
|
|
197
|
+
*/
|
|
198
|
+
spawnCompanionShell(opts) {
|
|
199
|
+
if (this.scene.getActor(opts.name)) {
|
|
200
|
+
Logger.getInstance().write(`spawnCompanionShell: name "${opts.name}" already in scene -- refusing.`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const npc = new NPC({
|
|
204
|
+
dialog: [],
|
|
205
|
+
description: opts.description,
|
|
206
|
+
playerConstructorConfig: { playerName: opts.name, startLocation: opts.room, combat: opts.combat },
|
|
207
|
+
}, new CommandRegistry(this.scene));
|
|
208
|
+
npc.plane = opts.plane;
|
|
209
|
+
npc.allyOf = this.player.name;
|
|
210
|
+
npc.companionKind = opts.kind;
|
|
211
|
+
npc.boundDevice = opts.boundDevice;
|
|
212
|
+
this.scene.addActor(npc);
|
|
213
|
+
this.scene.setCharacterLocation(npc, opts.room.name);
|
|
214
|
+
npc.scene = this.scene;
|
|
215
|
+
this.companions.push({ npc, kind: opts.kind, services: opts.services, force: opts.force, boundDevice: opts.boundDevice });
|
|
216
|
+
Logger.getInstance().write(`Companion spawned: ${opts.name} (${opts.kind}, force ${opts.force}) in ${opts.room.name}.`);
|
|
217
|
+
return npc;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The autonomous-drone shell, shared by the deploy command and
|
|
221
|
+
* arrival re-shelling: the frame's dog-brain (Pilot = 2 + quality)
|
|
222
|
+
* stands in for every attribute it lacks (RAW p.269-271).
|
|
223
|
+
*/
|
|
224
|
+
// Cargo sling by frame class (player request: capacity enforced -- a
|
|
225
|
+
// palm-sized Fly-Spy is not a mule). Keyed by the frame Item's size.
|
|
226
|
+
static DRONE_CARGO_KG = {
|
|
227
|
+
tiny: 0.5, small: 2, medium: 8, large: 20, huge: 40,
|
|
228
|
+
};
|
|
229
|
+
deployDroneShell(item, room) {
|
|
230
|
+
const pilot = 2 + item.qualityBonus;
|
|
231
|
+
const armed = /armed|combat|gun|roto|assault|sentry|weapon/i.test(`${item.name} ${item.description ?? ''}`);
|
|
232
|
+
const shell = this.spawnCompanionShell({
|
|
233
|
+
name: `${item.name} (autopilot)`,
|
|
234
|
+
description: `${this.player.name}'s drone running its dog-brain autopilot: it follows them, watches, and ${armed ? 'shoots what threatens them' : 'has cameras where guns would go -- it never attacks'}.`,
|
|
235
|
+
kind: 'drone',
|
|
236
|
+
combat: { body: 2 + item.qualityBonus, agility: pilot, reaction: pilot, logic: pilot, intuition: pilot, willpower: pilot, strength: 1, charisma: 1, skills: { perception: pilot, ...(armed ? { firearms: pilot } : {}) } },
|
|
237
|
+
room, plane: 'meat', force: pilot, boundDevice: item,
|
|
238
|
+
});
|
|
239
|
+
// Take-command weight checks (Player.addInventory) enforce this cap
|
|
240
|
+
// for the shell exactly as they do for a runner.
|
|
241
|
+
if (shell)
|
|
242
|
+
shell.maxCarryingWeight = Game.DRONE_CARGO_KG[String(item.size)] ?? 2;
|
|
243
|
+
return shell;
|
|
244
|
+
}
|
|
245
|
+
/** Drops a companion's shell and its roster entry. */
|
|
246
|
+
dismissCompanion(entry, reason) {
|
|
247
|
+
if (this.scene.getActor(entry.npc.name) === entry.npc) {
|
|
248
|
+
// Cargo comes home with the shell (player request: fetch-and-carry
|
|
249
|
+
// runs -- a real session's drone took a credstick and had no way
|
|
250
|
+
// to hand it over). A deliberate dismissal passes whatever the
|
|
251
|
+
// shell carried to its master; without this, scene.removeActor
|
|
252
|
+
// dumps it on the floor wherever the shell happened to be.
|
|
253
|
+
const carried = entry.npc.inventory.getAllItems().map(i => i.name);
|
|
254
|
+
const delivered = carried.filter(n => entry.npc.inventory.transferItemTo(n, this.player.inventory));
|
|
255
|
+
if (delivered.length > 0) {
|
|
256
|
+
Logger.getInstance().logWithColor(`👥 From ${entry.npc.name}'s cargo, now yours: ${delivered.join(', ')}.`, 'green');
|
|
257
|
+
}
|
|
258
|
+
// Mark deliberate before removeActor fires onActorRemoved, so no
|
|
259
|
+
// wreckage is charged to a clean dismissal.
|
|
260
|
+
entry.npc.deathClaimed = true;
|
|
261
|
+
this.scene.removeActor(entry.npc.name);
|
|
262
|
+
}
|
|
263
|
+
const idx = this.companions.indexOf(entry);
|
|
264
|
+
if (idx >= 0)
|
|
265
|
+
this.companions.splice(idx, 1);
|
|
266
|
+
Logger.getInstance().write(`Companion dismissed: ${entry.npc.name} -- ${reason}`);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* An actor left the world (Scene.removeActor). For a companion shell
|
|
270
|
+
* that DIED (not a deliberate dismissal -- those set deathClaimed
|
|
271
|
+
* first and prune themselves), settle the wreckage: a drone's frame is
|
|
272
|
+
* wrecked where it fell, an agent derezzes (the deck already ate the
|
|
273
|
+
* damage), a spirit is banished outright.
|
|
274
|
+
*/
|
|
275
|
+
onActorRemoved(name) {
|
|
276
|
+
const entry = this.companions.find(c => c.npc.name === name);
|
|
277
|
+
if (!entry)
|
|
278
|
+
return;
|
|
279
|
+
const idx = this.companions.indexOf(entry);
|
|
280
|
+
if (idx >= 0)
|
|
281
|
+
this.companions.splice(idx, 1);
|
|
282
|
+
if (entry.kind === 'drone' && entry.boundDevice && !entry.npc.deathClaimed) {
|
|
283
|
+
entry.boundDevice.takeDroneDamage(entry.boundDevice.maxDroneBoxes);
|
|
284
|
+
Logger.getInstance().logWithColor(`👥 The ${entry.boundDevice.name} is WRECKED where it fell -- rest to repair the frame.`, 'yellow');
|
|
285
|
+
}
|
|
286
|
+
this.updateStatus();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Spirits with no services left depart once no exchange holds them
|
|
290
|
+
* (called from CombatExchange.finishExchange): the bargain is spent.
|
|
291
|
+
*/
|
|
292
|
+
settleSpiritServices() {
|
|
293
|
+
for (const entry of [...this.companions]) {
|
|
294
|
+
if (entry.kind !== 'spirit' || (entry.services ?? 0) > 0)
|
|
295
|
+
continue;
|
|
296
|
+
if (entry.npc.inExchange)
|
|
297
|
+
continue;
|
|
298
|
+
// The paid fight is fought to the END. Between exchanges of the
|
|
299
|
+
// same fight inExchange is briefly false -- a real session's
|
|
300
|
+
// spirit spent its last service to join a fight and was dismissed
|
|
301
|
+
// 3ms later, before a single blow. Same freshness window as the
|
|
302
|
+
// per-fight billing in combat-exchange.
|
|
303
|
+
if (entry.npc.combatOpponent && Date.now() - entry.npc.lastExchangeAt < 60_000)
|
|
304
|
+
continue;
|
|
305
|
+
Logger.getInstance().logWithColor(`👥 ${entry.npc.name}'s bargain is spent -- it bows once and thins into nothing.`, 'green');
|
|
306
|
+
this.dismissCompanion(entry, 'services exhausted');
|
|
307
|
+
this.updateStatus();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// ==================== Pursuit heartbeat ====================
|
|
311
|
+
// Companions are stimulus-driven like every NPC: a directive burst ends
|
|
312
|
+
// at the reflect cap ("Max recursion reached") and a shell alone in a
|
|
313
|
+
// distant room then NEVER acts again -- a real session's drone was
|
|
314
|
+
// ordered to "find Mr. Krow", walked three rooms, and froze mid-hunt.
|
|
315
|
+
// The heartbeat re-spurs every shell with an open objective on a slow
|
|
316
|
+
// tick until it reports DONE/FAILED (which clears the objective -- see
|
|
317
|
+
// NPC.relayToMaster) or is dismissed.
|
|
318
|
+
_pursuitTimer;
|
|
319
|
+
_pursuitInFlight = new Set();
|
|
320
|
+
static PURSUIT_TICK_MS = 30_000;
|
|
321
|
+
startPursuitHeartbeat() {
|
|
322
|
+
if (this._pursuitTimer)
|
|
323
|
+
return;
|
|
324
|
+
this._pursuitTimer = setInterval(() => this.spurCompanionPursuits(), Game.PURSUIT_TICK_MS);
|
|
325
|
+
// Never hold the process open on our account.
|
|
326
|
+
this._pursuitTimer.unref?.();
|
|
327
|
+
}
|
|
328
|
+
stopPursuitHeartbeat() {
|
|
329
|
+
if (this._pursuitTimer)
|
|
330
|
+
clearInterval(this._pursuitTimer);
|
|
331
|
+
this._pursuitTimer = undefined;
|
|
332
|
+
}
|
|
333
|
+
spurCompanionPursuits() {
|
|
334
|
+
// The tick doubles as the spent-bargain sweep: finishExchange's own
|
|
335
|
+
// settle call now skips spirits whose fight is still warm (see
|
|
336
|
+
// settleSpiritServices), so without this a victorious spirit would
|
|
337
|
+
// linger at zero services forever.
|
|
338
|
+
this.settleSpiritServices();
|
|
339
|
+
if (!AI.isConfigured())
|
|
340
|
+
return;
|
|
341
|
+
for (const entry of this.companions) {
|
|
342
|
+
const npc = entry.npc;
|
|
343
|
+
if (!npc.objective)
|
|
344
|
+
continue;
|
|
345
|
+
if (npc.isIncapacitated() || npc.inExchange || npc.waitingForLull)
|
|
346
|
+
continue;
|
|
347
|
+
// A stale shell (already removed from the world) gets no spur.
|
|
348
|
+
if (this.scene.getActor(npc.name) !== npc)
|
|
349
|
+
continue;
|
|
350
|
+
// One spur in flight per shell -- a slow AI burst must not stack
|
|
351
|
+
// with the next tick.
|
|
352
|
+
if (this._pursuitInFlight.has(npc))
|
|
353
|
+
continue;
|
|
354
|
+
this._pursuitInFlight.add(npc);
|
|
355
|
+
Logger.getInstance().write(`Pursuit heartbeat: spurring ${npc.name} -- ${npc.objective}`);
|
|
356
|
+
void Promise.resolve(npc.hear(this.player, `[System note: HEARTBEAT -- your standing directive is still open: "${npc.objective}". Continue it NOW with [COMMAND] actions -- several steps if needed. If it is complete, say a line starting "DONE: <result>"; if it cannot be done, say a line starting "FAILED: <why>". Do not go quiet.]`, { viaLink: true }))
|
|
357
|
+
.catch(err => Logger.getInstance().error(`Pursuit heartbeat failed for ${npc.name}: ${err}`))
|
|
358
|
+
.finally(() => this._pursuitInFlight.delete(npc));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** Pre-travel: every shell folds, each with its own goodbye. */
|
|
362
|
+
foldCompanionsForTravel() {
|
|
363
|
+
const lines = [];
|
|
364
|
+
for (const entry of [...this.companions]) {
|
|
365
|
+
if (entry.kind === 'spirit') {
|
|
366
|
+
lines.push(`Your bound spirit thins into the rain -- the summons doesn't outlast the ride.`);
|
|
367
|
+
}
|
|
368
|
+
else if (entry.kind === 'drone') {
|
|
369
|
+
lines.push(`The ${entry.boundDevice?.name ?? 'drone'} autopilots home to your hand for the ride.`);
|
|
370
|
+
if (entry.boundDevice)
|
|
371
|
+
this._pendingRedeploys.push(entry.boundDevice);
|
|
372
|
+
}
|
|
373
|
+
else if (entry.kind === 'agent') {
|
|
374
|
+
lines.push(`Your agent folds back into the deck.`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
lines.push(`${entry.npc.name} takes their own ride -- they'll meet you there.`);
|
|
378
|
+
}
|
|
379
|
+
this.dismissCompanion(entry, `travel: ${entry.kind} folds`);
|
|
380
|
+
}
|
|
381
|
+
return lines;
|
|
382
|
+
}
|
|
383
|
+
/** Post-arrival: re-shell what was deployed when you left (drones). */
|
|
384
|
+
unfoldCompanionsOnArrival() {
|
|
385
|
+
const lines = [];
|
|
386
|
+
for (const item of this._pendingRedeploys.splice(0)) {
|
|
387
|
+
if (item.isWrecked)
|
|
388
|
+
continue;
|
|
389
|
+
const shell = this.deployDroneShell(item, this.player.currentLocation);
|
|
390
|
+
if (shell)
|
|
391
|
+
lines.push(`The ${item.name}'s rotors spin back up at the curb.`);
|
|
392
|
+
}
|
|
393
|
+
return lines;
|
|
394
|
+
}
|
|
167
395
|
/**
|
|
168
396
|
* Records a real transaction with a hub NPC: first contact opens the
|
|
169
397
|
* relationship at Loyalty 1 (a safeguard -- the hub cast is pre-seeded
|
|
@@ -517,6 +745,12 @@ export default class Game {
|
|
|
517
745
|
CommandFactory.registerCommand('rig', JumpCommand);
|
|
518
746
|
CommandFactory.registerCommand('initiate', InitiateCommand);
|
|
519
747
|
CommandFactory.registerCommand('submerge', InitiateCommand);
|
|
748
|
+
CommandFactory.registerCommand('summon', SummonCommand);
|
|
749
|
+
CommandFactory.registerCommand('conjure', SummonCommand);
|
|
750
|
+
CommandFactory.registerCommand('order', OrderCommand);
|
|
751
|
+
CommandFactory.registerCommand('deploy', DeployCommand);
|
|
752
|
+
CommandFactory.registerCommand('recall', RecallCommand);
|
|
753
|
+
CommandFactory.registerCommand('agent', AgentCommand);
|
|
520
754
|
CommandFactory.registerCommand('install', InstallCommand);
|
|
521
755
|
CommandFactory.registerCommand('chrome', InstallCommand);
|
|
522
756
|
CommandFactory.registerCommand('buy', BuyCommand);
|
|
@@ -568,6 +802,9 @@ export default class Game {
|
|
|
568
802
|
get logBox() {
|
|
569
803
|
return this._logBox;
|
|
570
804
|
}
|
|
805
|
+
get metaBox() {
|
|
806
|
+
return this._metaBox;
|
|
807
|
+
}
|
|
571
808
|
get inputBox() {
|
|
572
809
|
return this._inputBox;
|
|
573
810
|
}
|
|
@@ -576,6 +813,7 @@ export default class Game {
|
|
|
576
813
|
this._logFilePath = options.logFilePath;
|
|
577
814
|
this.initInputBox(screen);
|
|
578
815
|
this.initLogBox(screen);
|
|
816
|
+
this.initMetaBox(screen);
|
|
579
817
|
this.initStatusBox(screen);
|
|
580
818
|
// No "Generating Scene... Please wait." box anymore: every path now
|
|
581
819
|
// arrives with REAL prose (hand-authored static seeds, chunk-authored
|
|
@@ -633,6 +871,7 @@ export default class Game {
|
|
|
633
871
|
// never swapped out.
|
|
634
872
|
equipIntoEmptySlots(this.player);
|
|
635
873
|
this.warmNpcAppearancesInBackground();
|
|
874
|
+
this.startPursuitHeartbeat();
|
|
636
875
|
this.initialized = true;
|
|
637
876
|
// Nudge the player toward "call" up front -- the fixer convention means
|
|
638
877
|
// the story deliberately doesn't narrate a completed briefing, so
|
|
@@ -731,6 +970,8 @@ export default class Game {
|
|
|
731
970
|
// Read the run's fate BEFORE the scene swap below discards it -- a
|
|
732
971
|
// walked-away job gets a different homecoming line than a wrapped one.
|
|
733
972
|
const wrappedRun = this.scene !== hub && this.scene.isCompleted();
|
|
973
|
+
// Companions fold for the ride home, same as the trip out.
|
|
974
|
+
const companionFoldLines = this.foldCompanionsForTravel();
|
|
734
975
|
// Settle cross-plane and combat state that must not leak into the hub
|
|
735
976
|
// (the run's rooms -- including a bodyRoom the player left behind --
|
|
736
977
|
// are about to be orphaned wholesale).
|
|
@@ -801,10 +1042,13 @@ export default class Game {
|
|
|
801
1042
|
this.updateExits(this.player.currentLocation);
|
|
802
1043
|
this.updateStatus();
|
|
803
1044
|
Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${paid} paid, ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
|
|
1045
|
+
// Drones spin back up beside the cot.
|
|
1046
|
+
const companionArrivalLines = this.unfoldCompanionsOnArrival();
|
|
1047
|
+
const companionBlock = [...companionFoldLines, ...companionArrivalLines];
|
|
804
1048
|
const fateLine = wrappedRun
|
|
805
1049
|
? `The run's wrapped and the street swallows you back up.`
|
|
806
1050
|
: `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
|
|
807
|
-
return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}. Call ${FIXER_NAME} when you're hungry for the next job.\n${rentLine}${comfortLine}${restockLine}`;
|
|
1051
|
+
return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}. Call ${FIXER_NAME} when you're hungry for the next job.\n${rentLine}${comfortLine}${restockLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
808
1052
|
}
|
|
809
1053
|
/**
|
|
810
1054
|
* The actual drafting work, split out from continueToNextScene() purely
|
|
@@ -868,6 +1112,9 @@ export default class Game {
|
|
|
868
1112
|
this.player.activeCallPartner.activeCallPartner = undefined;
|
|
869
1113
|
this.player.activeCallPartner = undefined;
|
|
870
1114
|
}
|
|
1115
|
+
// Companions fold for the ride (spirits depart, drones to the pack,
|
|
1116
|
+
// agents into the deck) -- their lines join the JOB banner below.
|
|
1117
|
+
const companionLines = this.foldCompanionsForTravel();
|
|
871
1118
|
// false, false: no starter kit (the player carries their own gear
|
|
872
1119
|
// forward), and no blocking AI-redressing pass either -- the chunked
|
|
873
1120
|
// rooms pass already wrote this scene's prose.
|
|
@@ -896,8 +1143,12 @@ export default class Game {
|
|
|
896
1143
|
this.updateStatus();
|
|
897
1144
|
// Fresh streets, no trail yet (see Player.cameFrom / the HUD crumb).
|
|
898
1145
|
this.player.cameFrom = undefined;
|
|
1146
|
+
// Deployed drones spin back up at the curb -- BEFORE appearance
|
|
1147
|
+
// warming so re-shelled companions get warmed with the cast.
|
|
1148
|
+
const arrivalLines = this.unfoldCompanionsOnArrival();
|
|
899
1149
|
this.warmNpcAppearancesInBackground();
|
|
900
|
-
|
|
1150
|
+
const companionBlock = [...companionLines, ...arrivalLines];
|
|
1151
|
+
return `\n=== JOB #${this._tier}: ${seed.name} ===\n${this.scene.story}${needsLines.length > 0 ? `\n${needsLines.join('\n')}` : ''}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
901
1152
|
}
|
|
902
1153
|
/**
|
|
903
1154
|
* The needs check at every job departure (SR5e p.56 / p.172, scaled to
|
|
@@ -991,6 +1242,7 @@ export default class Game {
|
|
|
991
1242
|
}
|
|
992
1243
|
}
|
|
993
1244
|
async close() {
|
|
1245
|
+
this.stopPursuitHeartbeat();
|
|
994
1246
|
this._inputBox.removeAllListeners(); // removes all event listeners
|
|
995
1247
|
this._inputBox.cancel(); // cancels any active input session
|
|
996
1248
|
this._inputBox.detach(); // optionally remove from screen
|
|
@@ -1047,13 +1299,17 @@ export default class Game {
|
|
|
1047
1299
|
id: 'logBox',
|
|
1048
1300
|
top: '0%',
|
|
1049
1301
|
left: '0%',
|
|
1050
|
-
|
|
1302
|
+
// COLUMNS, not rows (playtest note: a 3-row Mechanics strip made
|
|
1303
|
+
// combat rolls "near impossible to read" -- they scrolled out as
|
|
1304
|
+
// fast as they landed). The world log takes the left ~two-thirds;
|
|
1305
|
+
// the Mechanics column stands full-height beside it.
|
|
1306
|
+
width: '65%',
|
|
1051
1307
|
// Fixed-row footer: 8 rows of HUD (status box) + 3 rows of input.
|
|
1052
1308
|
height: '100%-11',
|
|
1053
1309
|
border: {
|
|
1054
1310
|
type: 'line'
|
|
1055
1311
|
},
|
|
1056
|
-
label: ' Game Log ',
|
|
1312
|
+
label: ' Game Log (PgUp/PgDn to scroll) ',
|
|
1057
1313
|
scrollable: true, // Enable scrolling
|
|
1058
1314
|
alwaysScroll: true,
|
|
1059
1315
|
scrollbar: {
|
|
@@ -1073,8 +1329,83 @@ export default class Game {
|
|
|
1073
1329
|
tags: true // Intercept tags
|
|
1074
1330
|
});
|
|
1075
1331
|
this._logBox = logBox;
|
|
1332
|
+
// Scrolling without focus (player report: "can't scroll the game log
|
|
1333
|
+
// anymore"): focus lives in the input box on every path, so the
|
|
1334
|
+
// box's own keys never fire, and the wheel died with mouse tracking.
|
|
1335
|
+
// SCREEN-level PgUp/PgDn scroll the log while you keep typing; while
|
|
1336
|
+
// you're up in history, followBottom stops every new line from
|
|
1337
|
+
// yanking the view back down (Logger.log and ui.ts honor it).
|
|
1338
|
+
// Paging back to the bottom -- or End -- re-engages the follow.
|
|
1339
|
+
logBox.followBottom = true;
|
|
1340
|
+
const page = () => Math.max(1, (typeof logBox.height === 'number' ? logBox.height : 20) - 3);
|
|
1341
|
+
const setFollow = (on) => {
|
|
1342
|
+
logBox.followBottom = on;
|
|
1343
|
+
logBox.setLabel(on ? ' Game Log (PgUp/PgDn to scroll) ' : ' Game Log -- SCROLLED (End to follow) ');
|
|
1344
|
+
};
|
|
1345
|
+
const atBottom = () => logBox.getScrollPerc() >= 100 || logBox.getScrollHeight() <= page() + 3;
|
|
1346
|
+
screen.key(['pageup'], () => {
|
|
1347
|
+
logBox.scroll(-page());
|
|
1348
|
+
setFollow(false);
|
|
1349
|
+
screen.render();
|
|
1350
|
+
});
|
|
1351
|
+
screen.key(['pagedown'], () => {
|
|
1352
|
+
logBox.scroll(page());
|
|
1353
|
+
if (atBottom())
|
|
1354
|
+
setFollow(true);
|
|
1355
|
+
screen.render();
|
|
1356
|
+
});
|
|
1357
|
+
screen.key(['end'], () => {
|
|
1358
|
+
logBox.scrollTo(logBox.getScrollHeight());
|
|
1359
|
+
setFollow(true);
|
|
1360
|
+
screen.render();
|
|
1361
|
+
});
|
|
1076
1362
|
screen.append(logBox);
|
|
1077
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Scroll the log to its tail -- unless the player has paged up to read
|
|
1366
|
+
* history (followBottom off), in which case their place is left alone.
|
|
1367
|
+
* Every "new line arrived" site goes through here.
|
|
1368
|
+
*/
|
|
1369
|
+
followLog() {
|
|
1370
|
+
const box = this._logBox;
|
|
1371
|
+
if (!box || box.followBottom === false)
|
|
1372
|
+
return;
|
|
1373
|
+
box.scrollTo(box.getScrollHeight());
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* The MECHANICS column (readability pass, player request: the log was
|
|
1377
|
+
* a "wall of text"): dice anatomy and resource arithmetic land here
|
|
1378
|
+
* (via Logger.meta) instead of the world log, so the narrative stays
|
|
1379
|
+
* prose. A full-height COLUMN beside the world log, not a bottom
|
|
1380
|
+
* strip -- the first playtest's 3-row ticker scrolled combat rolls
|
|
1381
|
+
* away faster than they could be read. De-emphasis is the BLUE border
|
|
1382
|
+
* -- gray is banned (ANSI bright-black, illegible on stock dark
|
|
1383
|
+
* palettes). Follow-only (no keys); the full interleaved record lives
|
|
1384
|
+
* in session.log. Roll lines wrap at the column width -- fine in a
|
|
1385
|
+
* tall column. NO mouse (see initInputBox): never arm mouse tracking.
|
|
1386
|
+
*/
|
|
1387
|
+
initMetaBox(screen) {
|
|
1388
|
+
const metaBox = blessed.box({
|
|
1389
|
+
id: 'metaBox',
|
|
1390
|
+
top: '0%',
|
|
1391
|
+
left: '65%',
|
|
1392
|
+
width: '35%',
|
|
1393
|
+
// Same height as the world log: HUD (8) + input (3) below both.
|
|
1394
|
+
height: '100%-11',
|
|
1395
|
+
border: {
|
|
1396
|
+
type: 'line'
|
|
1397
|
+
},
|
|
1398
|
+
style: {
|
|
1399
|
+
border: { fg: 'blue' }
|
|
1400
|
+
},
|
|
1401
|
+
label: ' Mechanics ',
|
|
1402
|
+
scrollable: true,
|
|
1403
|
+
alwaysScroll: true,
|
|
1404
|
+
tags: true,
|
|
1405
|
+
});
|
|
1406
|
+
this._metaBox = metaBox;
|
|
1407
|
+
screen.append(metaBox);
|
|
1408
|
+
}
|
|
1078
1409
|
/**
|
|
1079
1410
|
* The HUD: a fixed strip between the log and the input box for the
|
|
1080
1411
|
* important real-time state -- condition tracks, armed/link status,
|
|
@@ -1191,6 +1522,30 @@ export default class Game {
|
|
|
1191
1522
|
if (player.activeCallPartner) {
|
|
1192
1523
|
lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} On a call with ${player.activeCallPartner.name}{/${CALL_COLOR}-fg}`);
|
|
1193
1524
|
}
|
|
1525
|
+
// The party: one line per live companion shell, with the tracks that
|
|
1526
|
+
// matter for its kind. A spirit shows its BODY too (it's a full
|
|
1527
|
+
// Force-statted combatant and hits land on it -- player request:
|
|
1528
|
+
// the track was real but invisible); drones show hull, agents deck.
|
|
1529
|
+
for (const c of this.companions) {
|
|
1530
|
+
const track = c.kind === 'spirit'
|
|
1531
|
+
? `${c.services ?? 0} service${(c.services ?? 0) === 1 ? '' : 's'} ${c.npc.conditionSummary()}${c.npc.stunTaken > 0 ? ` stun ${c.npc.stunSummary()}` : ''}`
|
|
1532
|
+
: c.kind === 'drone' && c.boundDevice ? `hull ${c.boundDevice.droneSummary()}`
|
|
1533
|
+
: c.kind === 'agent' && c.boundDevice ? `deck ${c.boundDevice.deckSummary()}`
|
|
1534
|
+
: c.npc.conditionSummary();
|
|
1535
|
+
// Each kind speaks its own language (a real rigger session read a
|
|
1536
|
+
// generic "(F3)" as spell Force on a Fly-Spy): drones fly on a
|
|
1537
|
+
// PILOT rating, agents run at a RATING, and a spirit's Force is
|
|
1538
|
+
// already in its name.
|
|
1539
|
+
const grade = c.kind === 'drone' ? `Pilot ${c.force} -- ` : c.kind === 'agent' ? `Rating ${c.force} -- ` : '';
|
|
1540
|
+
lines.push(`{green-fg}👥 ${c.npc.name} -- ${grade}${track}{/green-fg}`);
|
|
1541
|
+
}
|
|
1542
|
+
// DECK-AR INDICATOR (player request): "Link online" is the
|
|
1543
|
+
// COMMLINK's AR overlay; this is the DECK's -- lit when an agent
|
|
1544
|
+
// works the grid while you're back in the meat (the AR-drop exit).
|
|
1545
|
+
const remoteAgent = this.companions.find(c => c.kind === 'agent');
|
|
1546
|
+
if (remoteAgent && player.plane !== 'matrix') {
|
|
1547
|
+
lines.push(`{cyan-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''} ("jack in" to rejoin it, "recall" to fold it home){/cyan-fg}`);
|
|
1548
|
+
}
|
|
1194
1549
|
// Off-plane context: which reality you're in, your persona's Matrix
|
|
1195
1550
|
// track (cold-sim damage lands there), and where your empty body lies.
|
|
1196
1551
|
if (player.plane === 'matrix') {
|
|
@@ -1298,7 +1653,10 @@ export default class Game {
|
|
|
1298
1653
|
return `I don't understand "${verb}" -- did you mean "${guess}"?`;
|
|
1299
1654
|
}
|
|
1300
1655
|
const corrected = await this.handleInput([guess, ...args].join(' '));
|
|
1301
|
-
|
|
1656
|
+
// The correction note is bookkeeping -- Mechanics ticker, not the
|
|
1657
|
+
// narrative (readability pass).
|
|
1658
|
+
Logger.getInstance().meta(`(read "${verb}" as "${guess}")`);
|
|
1659
|
+
return corrected;
|
|
1302
1660
|
}
|
|
1303
1661
|
return `I don't understand the command "${verb}".`;
|
|
1304
1662
|
}
|
|
@@ -1314,9 +1672,9 @@ export default class Game {
|
|
|
1314
1672
|
static HELP_CATEGORIES = [
|
|
1315
1673
|
{ title: 'moving', entries: [['look'], ['go'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
1316
1674
|
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
|
|
1317
|
-
{ title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['stance'], ['surrender'], ['edge'], ['rest']] },
|
|
1318
|
-
{ title: 'magic', entries: [['spells'], ['cast'], ['project'], ['return'], ['assense']] },
|
|
1319
|
-
{ title: 'matrix', entries: [['jack'], ['hack'], ['pan'], ['silent'], ['reboot'], ['drone'], ['jump', 'rig']] },
|
|
1675
|
+
{ title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['order'], ['stance'], ['surrender'], ['edge'], ['rest']] },
|
|
1676
|
+
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
|
|
1677
|
+
{ title: 'matrix', entries: [['jack'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['deploy'], ['recall'], ['jump', 'rig']] },
|
|
1320
1678
|
{ title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
1321
1679
|
{ title: 'street', entries: [['browse', 'buy'], ['sell'], ['work', 'gig'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
|
|
1322
1680
|
{ title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
|
|
@@ -1349,7 +1707,7 @@ export default class Game {
|
|
|
1349
1707
|
const category = Game.HELP_CATEGORIES.find(c => c.title === pickedCategory);
|
|
1350
1708
|
const lines = [`=== ${category.title.toUpperCase()} ===`];
|
|
1351
1709
|
for (const [primary, ...aliases] of category.entries) {
|
|
1352
|
-
lines.push(
|
|
1710
|
+
lines.push(`${BULLET}${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
|
|
1353
1711
|
}
|
|
1354
1712
|
return lines.join('\n');
|
|
1355
1713
|
}
|