@maka/maka-cli 5.199.0 → 5.201.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/ask.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +139 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +115 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +48 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/presence-gate.js +27 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { hostLabel } from '../utilities/grid-names.js';
|
|
|
8
8
|
import { canReach, exitSpot, spotsActive, revealWithPerception, theSpot, sealedRouteTo, } from '../utilities/spots.js';
|
|
9
9
|
import { placeName } from '../utilities/log-style.js';
|
|
10
10
|
import { billAction, encounterOf } from '../utilities/action-cost.js';
|
|
11
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
11
12
|
/**
|
|
12
13
|
* Barrier rendering (2026-08-24 playtest: "I don't see THE THING I'm
|
|
13
14
|
* supposed to act on -- just a description... and I still see the
|
|
@@ -166,9 +167,8 @@ export class SearchCommand extends Command {
|
|
|
166
167
|
const { pool, limit } = matrixSearchTest(actor);
|
|
167
168
|
const roll = rollPool(pool, limit);
|
|
168
169
|
const seconds = matrixSearchSeconds(row.seconds, roll.hits, row.threshold, browse);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.logger.meta(`Matrix Search (Computer + Intuition [Data Processing], threshold ${row.threshold}${browse ? ', Browse' : ''}): ${formatRoll(roll)} -- ${seconds}s`);
|
|
170
|
+
if (showsMechanics(this.scene, actor)) {
|
|
171
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Matrix Search (Computer + Intuition [Data Processing], threshold ${row.threshold}${browse ? ', Browse' : ''}): ${formatRoll(roll)} -- ${seconds}s`);
|
|
172
172
|
}
|
|
173
173
|
// THE MINUTES ARE GOD'S (p.231-232): Overwatch bills time on the
|
|
174
174
|
// grid, and a search is time. Winding the last tick back by the
|
|
@@ -176,7 +176,12 @@ export class SearchCommand extends Command {
|
|
|
176
176
|
if (actor.overwatchLastTick > 0)
|
|
177
177
|
actor.overwatchLastTick -= seconds * 1000;
|
|
178
178
|
actor.performAction('runs a Matrix Search', host ? `inside ${hostLabel(where)}` : `across ${where.name}'s grid`, { quiet: this.scene.isHumanControlled(actor) });
|
|
179
|
-
|
|
179
|
+
// CAMOUFLAGE (p.256, sprite-power-actions.ts): a file a sprite folded
|
|
180
|
+
// into another file is "invisible to Matrix searches" -- only a Matrix
|
|
181
|
+
// Perception specifically hunting it turns it up. This is the search
|
|
182
|
+
// it is invisible to.
|
|
183
|
+
const files = where.offlineServer ? [] : where.inventory.getAllItems()
|
|
184
|
+
.filter(i => i.plane === 'matrix' && !i.concealedBy);
|
|
180
185
|
const clock = `(${seconds}s of Matrix time${browse ? ', Browse cutting it' : ''})`;
|
|
181
186
|
if (roll.hits < row.threshold) {
|
|
182
187
|
return host
|
|
@@ -210,8 +215,24 @@ export class SearchCommand extends Command {
|
|
|
210
215
|
// minute inside a host), not in Action Phases -- there is no cost to
|
|
211
216
|
// bill inside a Combat Turn, so it is refused there rather than
|
|
212
217
|
// priced by invention.
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
//
|
|
219
|
+
// A FINISHED FIGHT IS NOT A FIGHT. This asked `encounterOf(...)` and
|
|
220
|
+
// nothing else, while every other seam in action-cost.ts -- billAction,
|
|
221
|
+
// requirePhase, inLiveCombat -- pairs it with `!enc.ended`. An
|
|
222
|
+
// encounter object outlives the fight it ran, so a persona that had
|
|
223
|
+
// crashed the ice and watched the walls come down kept being told to
|
|
224
|
+
// crash the ice: the refusal named an exit the player had already
|
|
225
|
+
// taken. That is the reported "it says I can't do it with an IC
|
|
226
|
+
// there" (user, 2026-09-15), and the reason the answer to "is that
|
|
227
|
+
// correct?" is yes-but-not-here.
|
|
228
|
+
const enc = encounterOf(this.scene, this.actor);
|
|
229
|
+
if (this.actor.plane === 'matrix' && enc && !enc.ended) {
|
|
230
|
+
// Name the action that DOES fit in three seconds. The old line
|
|
231
|
+
// listed only ways to leave, which reads as "not now" when canon's
|
|
232
|
+
// answer is "not this verb" -- Matrix Perception is the Complex
|
|
233
|
+
// Action for looking at things mid-fight, and `look` is it
|
|
234
|
+
// (commands/look.ts bills exactly that).
|
|
235
|
+
return `Not with the ice on you -- a Matrix Search takes minutes (p.241), and a Combat Turn is three seconds. "look" is Matrix Perception (p.241), the Complex Action you CAN take right now. Otherwise crash the ice, "hide" from it (p.240), or get out first.`;
|
|
215
236
|
}
|
|
216
237
|
if (this.actor.plane === 'matrix')
|
|
217
238
|
return this.matrixSearch();
|
|
@@ -238,10 +259,15 @@ export class SearchCommand extends Command {
|
|
|
238
259
|
// Roll anatomy to the Mechanics ticker for the player; an NPC's
|
|
239
260
|
// return string is its AI history, so IT keeps the roll inline
|
|
240
261
|
// (readability pass).
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
262
|
+
// ONE PREDICATE FOR BOTH HALVES. The roll goes to the Mechanics panel
|
|
263
|
+
// OR into the return string (an NPC's AI history), never both -- so
|
|
264
|
+
// when the gate widened to companions (showsMechanics), the inline
|
|
265
|
+
// copy had to narrow by exactly the same amount or an ordered drone
|
|
266
|
+
// would print its Perception twice.
|
|
267
|
+
const toPanel = showsMechanics(this.scene, this.actor);
|
|
268
|
+
if (toPanel)
|
|
269
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, this.actor)}Perception: ${formatRoll(roll)}`);
|
|
270
|
+
const rollPrefix = toPanel ? '' : `Perception: ${formatRoll(roll)}\n`;
|
|
245
271
|
if (roll.hits === 0) {
|
|
246
272
|
return `${rollPrefix}You sweep the room and come up empty... you think.${describeContainers(room, this.actor)}${describeBarriers(room, this.actor)}`;
|
|
247
273
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { hint } from '../utilities/hints.js';
|
|
3
|
+
import { NPC } from '../models/npc.js';
|
|
4
|
+
import { SPRITE_POWERS, SPRITE_POWER_TASK_RULE, powersForSprite } from '../sprite-powers.js';
|
|
5
|
+
import { runSpritePower } from '../utilities/sprite-power-actions.js';
|
|
6
|
+
/**
|
|
7
|
+
* A SPRITE POWER AS A TYPED MOVE (user 2026-09-15).
|
|
8
|
+
*
|
|
9
|
+
* The verb surface is `order <sprite> <power> [target]` -- order.ts
|
|
10
|
+
* already parses "order <name> <command>" and hands the rest to
|
|
11
|
+
* NPC.tryRunCommand, so each power name registers as an ordinary verb
|
|
12
|
+
* that only a sprite can run. That keeps the task accounting, the
|
|
13
|
+
* companion feed and the Mechanics routing that "order" already does,
|
|
14
|
+
* instead of building a second way to tell a sprite to do something.
|
|
15
|
+
*
|
|
16
|
+
* ONE TASK PER USE (p.254, p.256 -- SPRITE_POWER_TASK_RULE). order.ts
|
|
17
|
+
* bills it on the success path, so a refusal here costs nothing. That is
|
|
18
|
+
* why every guard below returns before the resolver runs.
|
|
19
|
+
*/
|
|
20
|
+
export class SpritePowerCommand extends Command {
|
|
21
|
+
static verb = 'cookie';
|
|
22
|
+
static description = 'A sprite power (SR5 p.256-257). Order it: "order <sprite> <power> <target>". "sprites" lists which sprite has which.';
|
|
23
|
+
static npcPolicy = 'open';
|
|
24
|
+
/** Which power this registration is -- set per alias at registration. */
|
|
25
|
+
powerKey() {
|
|
26
|
+
return this.constructor.verb;
|
|
27
|
+
}
|
|
28
|
+
async execute(args = []) {
|
|
29
|
+
const actor = this.actor;
|
|
30
|
+
const key = this.powerKey();
|
|
31
|
+
// A PERSON CANNOT DO THESE. Sprite powers are the thing a sprite has
|
|
32
|
+
// that its technomancer does not (Sz7EmXnyRYK4RYqxt's whole point),
|
|
33
|
+
// so a player typing "cookie" is told where the verb lives rather
|
|
34
|
+
// than being quietly refused by a type check.
|
|
35
|
+
if (!(actor instanceof NPC)) {
|
|
36
|
+
const power = SPRITE_POWERS.find(p => p.key === key);
|
|
37
|
+
return `${power?.name ?? key} is a SPRITE power (${power?.page ?? 'p.256-257'}) -- not something a persona does. ${hint(`Order it: "order <sprite> ${key} <target>". "sprites" shows which sprite has which.`)}`;
|
|
38
|
+
}
|
|
39
|
+
const game = this.scene.ownerGame;
|
|
40
|
+
const entry = game?.companions?.find(c => c.npc === actor);
|
|
41
|
+
if (!entry || entry.kind !== 'sprite') {
|
|
42
|
+
return `Only a sprite runs a sprite power (p.256).`;
|
|
43
|
+
}
|
|
44
|
+
const spriteKey = entry.spriteType ?? '';
|
|
45
|
+
const master = this.scene.getPlayers?.()[0];
|
|
46
|
+
if (!master)
|
|
47
|
+
return `No one is holding this sprite's thread.`;
|
|
48
|
+
// The p.259 database decides who has what; the resolver re-checks,
|
|
49
|
+
// but naming the sprite's own list here is the useful refusal.
|
|
50
|
+
if (!powersForSprite(spriteKey).some(p => p.key === key)) {
|
|
51
|
+
const mine = powersForSprite(spriteKey).map(p => p.name).join(', ');
|
|
52
|
+
return `${actor.name} is a ${spriteKey} sprite -- no ${SPRITE_POWERS.find(p => p.key === key)?.name ?? key} (p.259). It can do: ${mine || 'nothing listed'}.`;
|
|
53
|
+
}
|
|
54
|
+
// DIAGNOSTICS TAKES THE SPRITE'S WHOLE ATTENTION (p.257): any other
|
|
55
|
+
// order drops the bonus it was holding. Cleared before the new power
|
|
56
|
+
// runs, so the sprite cannot be assisting and attacking at once.
|
|
57
|
+
if (master.spriteAssist?.sprite === actor.name && key !== 'diagnostics') {
|
|
58
|
+
master.spriteAssist = undefined;
|
|
59
|
+
}
|
|
60
|
+
const result = runSpritePower(key, {
|
|
61
|
+
scene: this.scene,
|
|
62
|
+
sprite: actor,
|
|
63
|
+
level: entry.force,
|
|
64
|
+
spriteKey,
|
|
65
|
+
master,
|
|
66
|
+
}, args.join(' '));
|
|
67
|
+
for (const line of result.meta)
|
|
68
|
+
this.logger.meta(line);
|
|
69
|
+
if (!result.spent)
|
|
70
|
+
return result.text;
|
|
71
|
+
this.scene.updateStatus?.();
|
|
72
|
+
return `${result.text}${hint(`\n${SPRITE_POWER_TASK_RULE}`)}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** One registered verb per power, so "order <sprite> gremlins <lock>" parses
|
|
76
|
+
* through the ordinary command table rather than a second dispatcher. */
|
|
77
|
+
export const SPRITE_POWER_COMMANDS = Object.fromEntries(SPRITE_POWERS.map(power => {
|
|
78
|
+
const cls = class extends SpritePowerCommand {
|
|
79
|
+
static verb = power.key;
|
|
80
|
+
static description = `${power.name} (${power.page}): ${power.effect}`;
|
|
81
|
+
powerKey() { return power.key; }
|
|
82
|
+
};
|
|
83
|
+
return [power.key, cls];
|
|
84
|
+
}));
|
|
85
|
+
//# sourceMappingURL=sprite-power.js.map
|
|
@@ -82,7 +82,14 @@ export class SpritesCommand extends Command {
|
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
84
|
for (const p of powers) {
|
|
85
|
-
|
|
85
|
+
// WHICH POWERS ARE MOVES (ISpritePower.runnable). Eight of the
|
|
86
|
+
// nine resolve; Stability is still catalogue, and the panel says
|
|
87
|
+
// so on the power itself rather than letting a player find out
|
|
88
|
+
// by typing it -- a wrong ability is worse than a missing one.
|
|
89
|
+
const how = p.runnable
|
|
90
|
+
? hint(` -- "order <sprite> ${p.key} <target>"`)
|
|
91
|
+
: ` {yellow-fg}(reference only -- not a move yet){/yellow-fg}`;
|
|
92
|
+
lines.push(` ${p.name}${hint(` (${p.page})`)}${how}`);
|
|
86
93
|
lines.push(` ${p.effect}`);
|
|
87
94
|
// The Test line is the sharpest thing on the panel for a player
|
|
88
95
|
// deciding between two types -- it says what the power rolls
|
|
@@ -95,8 +102,10 @@ export class SpritesCommand extends Command {
|
|
|
95
102
|
}
|
|
96
103
|
const footer = [
|
|
97
104
|
SPRITE_POWER_TASK_RULE,
|
|
98
|
-
// Said plainly, not buried: see the class comment.
|
|
99
|
-
|
|
105
|
+
// Said plainly, not buried: see the class comment. Eight of the
|
|
106
|
+
// nine became real moves on 2026-09-15; Stability did not, and the
|
|
107
|
+
// footer is not allowed to round that up.
|
|
108
|
+
`Order a power like any other command: "order <sprite> <power> <target>". Stability alone is still reference -- it is listed because canon has it, not because it does anything yet.`,
|
|
100
109
|
detailed ? `"sprites" lists all five.` : `"sprites <type>" for what each power actually does. "compile <type> <level>" calls one up.`,
|
|
101
110
|
].join(' ');
|
|
102
111
|
const tail = hint(footer);
|
|
@@ -5,6 +5,7 @@ import { accrueOverwatch } from '../utilities/overwatch.js';
|
|
|
5
5
|
import { MAX_MARKS } from '../utilities/marks.js';
|
|
6
6
|
import { isWatched, cameraMasterHost, directConnectionBlocker } from '../utilities/surveillance.js';
|
|
7
7
|
import { hint } from '../utilities/hints.js';
|
|
8
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
8
9
|
/**
|
|
9
10
|
* TAP -- THE DIRECT CONNECTION (backlog items 9 and 10, Niko, session
|
|
10
11
|
* 2026-08-27T02-07-12-404Z: "check the rules on this -- shouldn't I be
|
|
@@ -103,8 +104,8 @@ export class TapCommand extends Command {
|
|
|
103
104
|
const pool = Math.max(1, actor.logic + actor.skillRating('hacking') + actor.bonus('hacking') + silence + actor.woundModifier - actor.sustainingPenalty);
|
|
104
105
|
actor.performAction('cables into the cameras', `${room.name} -- a hand on the housing, a filament out of their deck`);
|
|
105
106
|
const roll = rollPool(pool, limit);
|
|
106
|
-
if (this.scene
|
|
107
|
-
this.logger.meta(
|
|
107
|
+
if (showsMechanics(this.scene, actor)) {
|
|
108
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Direct connection, camera (${mode === 'attack' ? 'brute force' : 'sleaze'}): ${formatRoll(roll)} (${threshold}+ -- its OWN ratings, not the host's)`);
|
|
108
109
|
}
|
|
109
110
|
this.logger.write(`${actor.name} direct-connected to ${room.name}'s cameras (${mode}): ${roll.hits} vs ${threshold}, master=${host.name}.`);
|
|
110
111
|
// GOD counts it either way (p.231-232) -- the same flat 2 a host
|
|
@@ -577,5 +577,143 @@
|
|
|
577
577
|
// - BENCH: repro schema 2.6.0 adds npcs[].hostile. A fixture could not
|
|
578
578
|
// express a hostile PERSON -- only `ice` -- so no meat-side
|
|
579
579
|
// hostility report could be benched at all.
|
|
580
|
-
|
|
580
|
+
// 1.59.0 (2026-09-15): A HOST'S BUSINESS IS SOMETHING YOU SEARCH FOR.
|
|
581
|
+
// - THE PURPOSE PHRASE IS ARCHIVE CONTENT (np6YBSPsdfAY7NcuZ). The
|
|
582
|
+
// generator is told to fill Host.purpose with "what is worth
|
|
583
|
+
// protecting inside it", so in practice it enumerates the vault --
|
|
584
|
+
// "patient intake logs, med supply orders, and the doc's private
|
|
585
|
+
// black book of who paid what for what" -- and four renderers
|
|
586
|
+
// printed that on a SEALED host's outside icon for free, while the
|
|
587
|
+
// file names beside it were correctly gated. Two tiers now, both
|
|
588
|
+
// rolls the engine already made: Room.searchedBy (the threshold-1
|
|
589
|
+
// public row, or a meat search of the room) reveals the business;
|
|
590
|
+
// Host.searchedBy (threshold 3, from inside) still names the files.
|
|
591
|
+
// A cracked host spills both. One gate, grid-names.hostPurposeFor,
|
|
592
|
+
// read by hostLine, gridIcons, gridSculpt and hostInteriorPanel.
|
|
593
|
+
// The dock's "N files sealed" counter was ungated too and is now.
|
|
594
|
+
// - SAVE SHAPE: ISavedRoomOverlay.hostSearchedBy (optional) persists
|
|
595
|
+
// Host.searchedBy, which was session-only. Survivable while it hid
|
|
596
|
+
// a file list; not once it gates the purpose, because the search
|
|
597
|
+
// that opens it spends Matrix minutes and those minutes wind the
|
|
598
|
+
// Overwatch clock. A reload was charging the player twice.
|
|
599
|
+
// - A FINISHED FIGHT IS NOT A FIGHT. Matrix Search's Combat Turn
|
|
600
|
+
// refusal asked `encounterOf(...)` alone, while billAction,
|
|
601
|
+
// requirePhase and inLiveCombat all pair it with `!enc.ended` -- so
|
|
602
|
+
// a persona that had already crashed the ice kept being told to
|
|
603
|
+
// crash the ice. The refusal is correct in a LIVE fight (base time
|
|
604
|
+
// one minute, p.241, against a three-second Combat Turn) and now
|
|
605
|
+
// also names the action that does fit: Matrix Perception, which is
|
|
606
|
+
// `look`.
|
|
607
|
+
// - Browse's catalogue blurb promised "+2 dice on Matrix Search" and
|
|
608
|
+
// no code ever granted them; it halves the base TIME (Splintered
|
|
609
|
+
// State p.54), which is what perception.ts has always done.
|
|
610
|
+
// 1.60.0 (2026-09-15): THE MECHANICS PANEL ASKS WHO IS WATCHING.
|
|
611
|
+
// - AN ORDERED COMPANION'S DICE WERE ROLLED AND THROWN AWAY
|
|
612
|
+
// (wjFBY3zgyWzJP9adw). `order <sprite> mark <lock>` runs the real
|
|
613
|
+
// verb with the sprite as actor and rolls Hack on the Fly exactly as
|
|
614
|
+
// a decker would -- then ~30 commands dropped the result on the
|
|
615
|
+
// floor, because the guard around every logger.meta asked
|
|
616
|
+
// `isHumanControlled(actor)`: is the ROLLER a human. The panel wants
|
|
617
|
+
// "is a human WATCHING", which combat-exchange has always asked
|
|
618
|
+
// (playerWitnesses) -- which is why a sprite's COMBAT rolls showed
|
|
619
|
+
// and its ordered mark did not. New utilities/mechanics-audience.ts:
|
|
620
|
+
// showsMechanics is true for a human OR a companion a human is
|
|
621
|
+
// running, and the line names the roller when it is not the player.
|
|
622
|
+
// Swept across hack, disable, edit-file, hide, jack, overwatch, tap,
|
|
623
|
+
// look and search -- sprites, spirits, agents and drones alike.
|
|
624
|
+
// Where a command forked meta-vs-inline on the old predicate, BOTH
|
|
625
|
+
// halves moved, so nothing prints twice.
|
|
626
|
+
// - A SPRITE'S TASKS ARE A BARGAIN TOO. order.ts billed services for
|
|
627
|
+
// spirits only, so an ordered sprite worked free and its ledger line
|
|
628
|
+
// never printed -- while combat-exchange has always charged both per
|
|
629
|
+
// fight. Canon is explicit (p.254, p.256): a task per order, and the
|
|
630
|
+
// refusal when the thread is spent now speaks sprite, not spirit.
|
|
631
|
+
// - The un-jumped drone recon's Piloting test wrote its dice into the
|
|
632
|
+
// PROSE line (drone-prose.droneReconLaunchLine took a rollText) --
|
|
633
|
+
// the one player roll in the engine you had to read out of a
|
|
634
|
+
// sentence. It goes to the panel; the prose is prose again.
|
|
635
|
+
// 1.61.0 (2026-09-15): ERASE MARK (f79LLqTerep4nKWcA). New verb
|
|
636
|
+
// "unmark", also reached as "erase mark[s] on <icon>" -- a Complex
|
|
637
|
+
// Action, Computer + Logic [Attack] v. Willpower + Firewall (p.239),
|
|
638
|
+
// an ATTACK action so GOD bills the defender's hits. Nothing removed a
|
|
639
|
+
// mark before this: the only paths were the three canon wipes in
|
|
640
|
+
// utilities/marks.ts (reboot, jack-out, convergence), and `erase` was
|
|
641
|
+
// Edit File's delete on a FILE.
|
|
642
|
+
// - THE THREE-MARK GATE is the book's: you need three marks on the
|
|
643
|
+
// icon you are scrubbing, not on whoever placed the mark. Two marks
|
|
644
|
+
// in one action at -4, three at -10, all from the same icon.
|
|
645
|
+
// - THE OWNER EXEMPTION IS AN ENGINE RULING, labelled as one at the
|
|
646
|
+
// command. Read strictly, p.239 would make a decker mark themselves
|
|
647
|
+
// three times before clearing an IC's key off their own persona --
|
|
648
|
+
// marks nothing in the engine or the book lets you place on
|
|
649
|
+
// yourself. The gate stops you reaching into someone ELSE's icon;
|
|
650
|
+
// on your own it is waived and the roll still stands. When you hold
|
|
651
|
+
// too few marks to reach another icon, the refusal names reboot and
|
|
652
|
+
// jack out, which do it for free.
|
|
653
|
+
// - "unmark me" sweeps BOTH ledgers -- a host's key lives in
|
|
654
|
+
// Host.marksOn, not your PAN ledger -- and clearing a host's last
|
|
655
|
+
// key un-spots you (Host.spotted): an icon it no longer recognises
|
|
656
|
+
// is one it has to find again.
|
|
657
|
+
// - TWO LEDGER BUGS ALONGSIDE. Device.marksBy is the third mark
|
|
658
|
+
// ledger and no wipe had ever touched it, so a maglock you keyed
|
|
659
|
+
// last run still answered to a persona canon deleted on reboot
|
|
660
|
+
// (p.236) -- and `disable` could drive it on marks the sheet no
|
|
661
|
+
// longer showed. It is cleared by clearMarksPlacedBy now, and
|
|
662
|
+
// listed by describeMarks/marksPlaced, which had both skipped it.
|
|
663
|
+
// 1.62.0 (2026-09-15): HIRING THE SCRUB. A runner who cannot reach the
|
|
664
|
+
// Matrix can now pay somebody who can. "ask <contact> about cleanup"
|
|
665
|
+
// quotes a price (no dice, no legwork budget spent); "hire cleanup
|
|
666
|
+
// [<site>]" haggles it once on Negotiation, debits the nuyen and
|
|
667
|
+
// stamps the loose end. The decker's own test rolls at the NEXT
|
|
668
|
+
// homecoming -- you paid for a result you do not get to watch.
|
|
669
|
+
// Clean, leverage (done, but they kept a copy) or botched (footage
|
|
670
|
+
// stays, grace clock keeps running, no refund).
|
|
671
|
+
// - WHY: a run leaves an ILooseEnd and the hub nagged EVERY runner to
|
|
672
|
+
// "jack in, then hop" and erase it -- a route that needs a cyberdeck
|
|
673
|
+
// and a direct neural interface. A street samurai was handed a
|
|
674
|
+
// consequence with no lever and a +1 Public Awareness on a timer.
|
|
675
|
+
// The playable cleanup scene is untouched; this is the other door.
|
|
676
|
+
// - SAVE SHAPE: ILooseEnd.hired (optional, additive -- no
|
|
677
|
+
// SAVE_VERSION bump). It rides the record that was already
|
|
678
|
+
// persisted, so a job already paid for survives a reload, and the
|
|
679
|
+
// fixer's Connection + Loyalty is BANKED at hire time so a loyalty
|
|
680
|
+
// shift in between cannot retroactively change it.
|
|
681
|
+
// - New Player.canReachMatrix(), the question commands/jack.ts answers
|
|
682
|
+
// in three separate refusals, asked once so the hub's advice can be
|
|
683
|
+
// written for the runner reading it.
|
|
684
|
+
// 1.63.0 (2026-09-15): SPRITE POWERS ARE MOVES. sprite-powers.ts has
|
|
685
|
+
// been a canon-complete, individually cited catalogue of all nine
|
|
686
|
+
// since Sz7EmXnyRYK4RYqxt -- and every one was a printed Test line
|
|
687
|
+
// wired to nothing. Eight of them resolve now
|
|
688
|
+
// (utilities/sprite-power-actions.ts), keyed off the same five sprite
|
|
689
|
+
// keys so the panel and the mechanics cannot drift. Ordered like any
|
|
690
|
+
// other command: "order <sprite> <power> <target>", one TASK per use
|
|
691
|
+
// (p.254/p.256), billed on success so a refusal is free.
|
|
692
|
+
// Cookie Hacking + Resonance [Sleaze] v. Intuition+Firewall
|
|
693
|
+
// Diagnostics Hardware + Level [Data Processing], Teamwork
|
|
694
|
+
// Electron Storm Cybercombat + Resonance [Attack], sustained
|
|
695
|
+
// Gremlins Hardware + Level [Attack] v. Device Rating+Firewall
|
|
696
|
+
// Camouflage / Hash / Suppression / Watermark -- state, no roll
|
|
697
|
+
// - NEW CANON DATA: a sprite's four Matrix attributes are PER TYPE
|
|
698
|
+
// (p.259) and only Resonance equals Level (p.254). The engine had no
|
|
699
|
+
// table, which is fine while a power is a printed string and not
|
|
700
|
+
// fine the moment it needs a [Sleaze] number -- a bracket is a LIMIT.
|
|
701
|
+
// spriteMatrix()/spriteMatrixBoxes() carry it. Fault sprites gain
|
|
702
|
+
// Cybercombat and machine sprites Hardware, the skills p.259 already
|
|
703
|
+
// listed on the panel and compile.ts never gave the shell.
|
|
704
|
+
// - CONSUMERS: Camouflage hides a file from Matrix Search
|
|
705
|
+
// (commands/search.ts); Hash blocks Edit File; Suppression delays the
|
|
706
|
+
// host's launch step (utilities/host-combat.ts hostTurnStart);
|
|
707
|
+
// Electron Storm ends on any Matrix damage to the sprite, enforced on
|
|
708
|
+
// Player.takeMatrixDamage -- the one method every Matrix hit goes
|
|
709
|
+
// through. New session state: Item.concealedBy/hashedBy/watermark,
|
|
710
|
+
// Host.icLaunchDelay, Player.cookiedBy/spriteAssist, NPC.sustainedStorm.
|
|
711
|
+
// - STABILITY IS DELIBERATELY NOT WIRED, and both the catalogue and the
|
|
712
|
+
// panel say so per-power (ISpritePower.runnable). It demotes
|
|
713
|
+
// glitches, and glitches are read at sixteen scattered sites in this
|
|
714
|
+
// engine; wiring it at three would be a power that works where
|
|
715
|
+
// somebody happened to look -- the "wrong ability on a sprite is
|
|
716
|
+
// worse than a missing one" case. It wants the glitch contract in one
|
|
717
|
+
// place first, and that is its own piece of work.
|
|
718
|
+
export const ENGINE_VERSION = '1.63.0';
|
|
581
719
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -12,6 +12,9 @@ import { hostInteriorPanel } from './utilities/grid-view.js';
|
|
|
12
12
|
import { DEFAULT_GRID_PROVIDER, PUBLIC_GRID, globalGrid, looseEndGrid } from './utilities/grids.js';
|
|
13
13
|
import { buildCleanupSeed } from './factories/cleanup-seed.js';
|
|
14
14
|
import { EditFileCommand, EraseCommand } from './commands/edit-file.js';
|
|
15
|
+
import { EraseMarkCommand } from './commands/erase-mark.js';
|
|
16
|
+
import { SPRITE_POWER_COMMANDS } from './commands/sprite-power.js';
|
|
17
|
+
import { cleanupOutcome } from './utilities/cleanup-errand.js';
|
|
15
18
|
import { hostsInReach, hostDockLine, gridVicinity } from './utilities/grid-reach.js';
|
|
16
19
|
import { nameThem, scrubHandles } from './utilities/identity.js';
|
|
17
20
|
import { dirname } from 'path';
|
|
@@ -24,7 +27,7 @@ import { SAVE_VERSION } from './types/save-file.js';
|
|
|
24
27
|
import { hubSeedRefOf } from './utilities/hub-seed.js';
|
|
25
28
|
import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, applyHomeBlock, writeSaveAtomic, deleteSave, restoreItem, readSave, serializeItem, saveSlug, spillPathFor, } from './utilities/persistence.js';
|
|
26
29
|
import { heartbeatPresence, heartbeatPresenceNow } from './utilities/social.js';
|
|
27
|
-
import { presenceBeatAllowed } from './utilities/presence-gate.js';
|
|
30
|
+
import { presenceBeatAllowed, presenceProcessOnTheStreet } from './utilities/presence-gate.js';
|
|
28
31
|
import { pushSave, burnCloudSaveFor, enqueueBoundPush, flushPushQueue, writeSpill, takeLastMemorial, authToken, clearSpill, hasPendingPush, } from './utilities/cloud-saves.js';
|
|
29
32
|
import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
|
|
30
33
|
import { isFence, LIFESTYLE_TIERS, LIFESTYLE_OPTIONS, canonicalLifestyleTier, canonicalLifestyleOption, lifestyleOptionAllowed, lifestyleTierFor } from './utilities/commerce.js';
|
|
@@ -590,6 +593,19 @@ export default class Game {
|
|
|
590
593
|
// `?? []`: hosted-homecoming.test.ts drives a Game off its prototype,
|
|
591
594
|
// where field initialisers never ran.
|
|
592
595
|
for (const le of this._looseEnds ?? []) {
|
|
596
|
+
// A HIRED SCRUB RESOLVES BEFORE THE DESK DOES (cleanup-errand.ts).
|
|
597
|
+
// You paid at the hub and did not get to watch; this is where the
|
|
598
|
+
// decker reports back, and it happens whatever grace is left --
|
|
599
|
+
// that is what the nuyen bought.
|
|
600
|
+
if (le.hired) {
|
|
601
|
+
const done = this.settleHiredCleanup(le);
|
|
602
|
+
lines.push(...done.lines);
|
|
603
|
+
// A botched scrub leaves the footage exactly where it was, and
|
|
604
|
+
// the grace clock keeps running -- you bought an attempt.
|
|
605
|
+
if (done.keep)
|
|
606
|
+
kept.push(done.record);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
593
609
|
const left = le.homecomingsLeft - 1;
|
|
594
610
|
if (left > 0) {
|
|
595
611
|
kept.push({ ...le, homecomingsLeft: left });
|
|
@@ -607,6 +623,60 @@ export default class Game {
|
|
|
607
623
|
}
|
|
608
624
|
return lines;
|
|
609
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* A HIRED SCRUB REPORTS BACK (utilities/cleanup-errand.ts). The
|
|
628
|
+
* decker's test is rolled HERE, not at hire time: you paid for a
|
|
629
|
+
* result you do not get to watch, and the delay is the point.
|
|
630
|
+
*
|
|
631
|
+
* The pool was banked on the record at hire (the fixer's Connection +
|
|
632
|
+
* Loyalty) so a loyalty shift in between cannot retroactively change a
|
|
633
|
+
* job already bought. It rolls against the host's rating -- the same
|
|
634
|
+
* ice the runner would have had to get through.
|
|
635
|
+
*/
|
|
636
|
+
settleHiredCleanup(le) {
|
|
637
|
+
const hired = le.hired;
|
|
638
|
+
const mine = rollPool(Math.max(1, hired.pool));
|
|
639
|
+
const host = rollPool(Math.max(1, le.host.rating));
|
|
640
|
+
const outcome = cleanupOutcome(mine.hits - host.hits, mine.criticalGlitch);
|
|
641
|
+
Logger.getInstance().write(`Cleanup errand "${le.id}": ${hired.decker} via ${hired.fixer}, ${mine.hits} v ${host.hits} -> ${outcome}.`);
|
|
642
|
+
Logger.getInstance().meta(`Cleanup errand -- ${hired.decker} (${hired.fixer}'s reach): ${formatRoll(mine)} v. ${le.site}'s host: ${formatRoll(host)}`);
|
|
643
|
+
if (outcome === 'botched') {
|
|
644
|
+
// The clock keeps running, and the hire is spent: the record goes
|
|
645
|
+
// back to being yours to deal with.
|
|
646
|
+
const left = le.homecomingsLeft - 1;
|
|
647
|
+
const stillOpen = left > 0;
|
|
648
|
+
if (!stillOpen) {
|
|
649
|
+
const stained = this.player.addPublicAwareness(`caught on the cameras at "${le.site}" during "${le.jobName}"`);
|
|
650
|
+
return {
|
|
651
|
+
lines: [`\n{red-fg}${hired.fixer} calls, short: "${hired.decker} got burned off ${le.site}'s host. Didn't get it." And then the desk reviews the feeds anyway.{/red-fg}${stained ? ` (+1 Public Awareness, now ${this.player.publicAwareness})` : ''}`],
|
|
652
|
+
keep: false,
|
|
653
|
+
record: le,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
lines: [`\n{yellow-fg}${hired.fixer} calls, short: "${hired.decker} got burned off ${le.site}'s host. Didn't get it -- and no, you don't get the nuyen back."{/yellow-fg}${hint(` (The footage is still up. Hire again, or "jack in" and "hop ${le.site}" yourself.)`)}`],
|
|
658
|
+
keep: true,
|
|
659
|
+
record: { ...le, homecomingsLeft: left, hired: undefined },
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const leverage = outcome === 'leverage';
|
|
663
|
+
return {
|
|
664
|
+
lines: [`\n{light-blue-fg}${hired.fixer} pings you two days on: "${le.site}'s feeds from '${le.jobName}' are a loop of an empty hallway. You were never there."{/light-blue-fg}${leverage ? `\n{yellow-fg}A beat later, a second message from a number you don't know: a single still of your face, and nothing else. ${hired.decker} kept a copy.{/yellow-fg}` : ''}`],
|
|
665
|
+
keep: false,
|
|
666
|
+
record: le,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* HIRE A SHADOW DECKER for an open loose end (commands/crew.ts,
|
|
671
|
+
* "hire cleanup"). Debits the nuyen and stamps the record; the job
|
|
672
|
+
* itself resolves at the next homecoming (settleHiredCleanup).
|
|
673
|
+
*/
|
|
674
|
+
hireCleanup(le, hired) {
|
|
675
|
+
const idx = this._looseEnds.findIndex(x => x.id === le.id);
|
|
676
|
+
if (idx >= 0)
|
|
677
|
+
this._looseEnds[idx] = { ...this._looseEnds[idx], hired };
|
|
678
|
+
Logger.getInstance().write(`Cleanup hired: ${hired.decker} via ${hired.fixer} for "${le.id}" at ${hired.price} nuyen.`);
|
|
679
|
+
}
|
|
610
680
|
/** The grids "hop" lists for the open loose ends -- from the hub only. */
|
|
611
681
|
looseEndGrids() {
|
|
612
682
|
if (!this.hasHub || this.scene !== this._hubScene)
|
|
@@ -823,6 +893,17 @@ export default class Game {
|
|
|
823
893
|
holdsLiveSeat() {
|
|
824
894
|
return this.ridingWith.length > 0 || this.crew.some(m => m.ghost?.liveOnly);
|
|
825
895
|
}
|
|
896
|
+
/** Everything the presence gate judges on, in one place -- so no
|
|
897
|
+
* caller can answer half of it for itself (TLKZKLQ4rJBCnDjrw). */
|
|
898
|
+
presenceFacts() {
|
|
899
|
+
return {
|
|
900
|
+
anonymous: this._anonymous,
|
|
901
|
+
bench: this._bench,
|
|
902
|
+
hostedHub: this._hostedHub,
|
|
903
|
+
atHub: this.scene === this._hubScene,
|
|
904
|
+
holdsLiveSeat: this.holdsLiveSeat(),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
826
907
|
beatPresence() {
|
|
827
908
|
if (!this.hasHub)
|
|
828
909
|
return;
|
|
@@ -841,8 +922,11 @@ export default class Game {
|
|
|
841
922
|
// reports, both sides of one table, twice in ten minutes.
|
|
842
923
|
//
|
|
843
924
|
// The rule itself lives in utilities/presence-gate.ts so it can be
|
|
844
|
-
// tested without a Game, a hub, or a socket
|
|
845
|
-
|
|
925
|
+
// tested without a Game, a hub, or a socket -- and it is the WHOLE
|
|
926
|
+
// rule, account half included. `returnToHub` calls this method
|
|
927
|
+
// directly at every homecoming, so a gate that only guarded the
|
|
928
|
+
// timer-arming path was no gate at all (TLKZKLQ4rJBCnDjrw).
|
|
929
|
+
if (!presenceBeatAllowed(this.presenceFacts()))
|
|
846
930
|
return;
|
|
847
931
|
void heartbeatPresence(this.presencePayload());
|
|
848
932
|
// The hub link rides the same pulse: a failed connect at boot must
|
|
@@ -861,14 +945,18 @@ export default class Game {
|
|
|
861
945
|
*/
|
|
862
946
|
async pushPresenceNow() {
|
|
863
947
|
// A hosted hub never dials the site from inside the site: the box's
|
|
864
|
-
// own `maka login`, if any, is not this player's.
|
|
865
|
-
|
|
948
|
+
// own `maka login`, if any, is not this player's. Same predicate the
|
|
949
|
+
// heartbeat uses -- one account rule, one place.
|
|
950
|
+
if (!presenceProcessOnTheStreet(this.presenceFacts()))
|
|
866
951
|
return 'offline';
|
|
867
952
|
return heartbeatPresenceNow(this.presencePayload());
|
|
868
953
|
}
|
|
869
954
|
startPresenceHeartbeat() {
|
|
870
|
-
|
|
871
|
-
|
|
955
|
+
// Account-less, a bench, or hosted on the site: never on the street,
|
|
956
|
+
// so don't even arm the timer. beatPresence checks this again -- it
|
|
957
|
+
// has to, because returnToHub beats without ever coming through here.
|
|
958
|
+
if (!presenceProcessOnTheStreet(this.presenceFacts()))
|
|
959
|
+
return;
|
|
872
960
|
if (this._presenceTimer)
|
|
873
961
|
return;
|
|
874
962
|
this.beatPresence();
|
|
@@ -2983,6 +3071,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2983
3071
|
CommandFactory.registerCommand('wipe', EraseCommand);
|
|
2984
3072
|
// MAtrix Recognition Keys (canon p.235-236).
|
|
2985
3073
|
CommandFactory.registerCommand('mark', MarkCommand);
|
|
3074
|
+
CommandFactory.registerCommand('unmark', EraseMarkCommand);
|
|
3075
|
+
// SPRITE POWERS (p.256-257): one verb per power, reached as
|
|
3076
|
+
// "order <sprite> <power> <target>" -- see commands/sprite-power.ts.
|
|
3077
|
+
for (const [key, cls] of Object.entries(SPRITE_POWER_COMMANDS)) {
|
|
3078
|
+
CommandFactory.registerCommand(key, cls);
|
|
3079
|
+
}
|
|
2986
3080
|
// Camera feeds you own (surveillance).
|
|
2987
3081
|
CommandFactory.registerCommand('snoop', SnoopCommand);
|
|
2988
3082
|
// The cable into a slaved camera (canon p.232/233) -- "splice" is
|
|
@@ -3365,8 +3459,20 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3365
3459
|
const where = this._resumedAt && this._resumedAt !== this._homeRoom
|
|
3366
3460
|
? `You're back at the ${this._resumedAt.name}${this.player.atSpot ? `, by the ${this.player.atSpot}` : ''} -- right where you left off.`
|
|
3367
3461
|
: 'The cot remembers your shape.';
|
|
3462
|
+
// THE NAG HAS TO NAME A ROUTE THIS RUNNER CAN TAKE (user
|
|
3463
|
+
// 2026-09-15). "jack in, then hop" is closed to anyone without a
|
|
3464
|
+
// deck and a direct neural interface -- which is most runners --
|
|
3465
|
+
// so a street samurai was told about a consequence and handed no
|
|
3466
|
+
// lever. The errand (utilities/cleanup-errand.ts) is that lever,
|
|
3467
|
+
// and it is the advice a non-decker gets.
|
|
3468
|
+
const hiredNote = this._looseEnds.some(le => le.hired)
|
|
3469
|
+
? ` Somebody is already on one of them.`
|
|
3470
|
+
: '';
|
|
3471
|
+
const route = this.player.canReachMatrix()
|
|
3472
|
+
? `"jack in", then "hop" -- the site's grid is listed while the footage is still up there. Or "ask <contact> about cleanup" to buy the scrub instead.`
|
|
3473
|
+
: `"ask <contact> about cleanup" -- you can't ride the grid, but somebody you know knows somebody who can.`;
|
|
3368
3474
|
const looseNote = this._looseEnds.length > 0
|
|
3369
|
-
? `\n\n{yellow-fg}${this._looseEnds.map(le => `${le.site} still has your face from "${le.jobName}".`).join(' ')}{/yellow-fg}${hint(` (
|
|
3475
|
+
? `\n\n{yellow-fg}${this._looseEnds.map(le => `${le.site} still has your face from "${le.jobName}".`).join(' ')}${hiredNote}{/yellow-fg}${hint(` (${route})`)}`
|
|
3370
3476
|
: '';
|
|
3371
3477
|
return `Welcome back, ${this.player.name}. ${where}\n ${this.scene.story}${pendingNote}${looseNote}`;
|
|
3372
3478
|
}
|
|
@@ -6629,7 +6735,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6629
6735
|
// players" / "crew street"); the bare verb still answers quietly.
|
|
6630
6736
|
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
|
|
6631
6737
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
|
|
6632
|
-
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['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']] },
|
|
6738
|
+
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['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']] },
|
|
6633
6739
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
6634
6740
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
6635
6741
|
// `sustain` shelves here and not under magic: the command is
|
|
@@ -77,6 +77,14 @@ export class Host {
|
|
|
77
77
|
/** Kinds crashed this turn; the host relaunches them next turn (p.355-356). */
|
|
78
78
|
relaunchQueue = [];
|
|
79
79
|
patrolCountdown;
|
|
80
|
+
/**
|
|
81
|
+
* SUPPRESSION (SR5 p.257, utilities/sprite-power-actions.ts): a sprite
|
|
82
|
+
* inside the host is arguing with its launch paths, so IC it brings up
|
|
83
|
+
* arrives (Level / 2) Combat Turns late -- and "delayed IC can't act or
|
|
84
|
+
* be targeted" while it waits. Counted down by hostTurnStart. Session
|
|
85
|
+
* state, like the rest of the fight.
|
|
86
|
+
*/
|
|
87
|
+
icLaunchDelay = 0;
|
|
80
88
|
/** A haven (jackpoint.ts): never hunts, shows no rating, forgets cracks. */
|
|
81
89
|
sanctioned = false;
|
|
82
90
|
/**
|
|
@@ -49,6 +49,25 @@ export class Item extends AbstractItem {
|
|
|
49
49
|
// work (rest.ts). A reboot does NOT clear it; that is the whole
|
|
50
50
|
// difference from `jammed`.
|
|
51
51
|
bricked = false;
|
|
52
|
+
/**
|
|
53
|
+
* SPRITE POWERS ON A FILE (SR5 p.256-257,
|
|
54
|
+
* utilities/sprite-power-actions.ts). Matrix-plane items only; all
|
|
55
|
+
* three are session state, like the marks and sightings they sit
|
|
56
|
+
* beside -- a sprite's work lasts as long as the sprite does.
|
|
57
|
+
*
|
|
58
|
+
* CAMOUFLAGE: folded inside another file, so a Matrix Search walks
|
|
59
|
+
* past it (commands/search.ts) and only a Matrix Perception hunting
|
|
60
|
+
* this file turns it up -- the sprite included.
|
|
61
|
+
*/
|
|
62
|
+
concealedBy;
|
|
63
|
+
/** HASH: sealed by a Resonance algorithm only this sprite can undo, for
|
|
64
|
+
* `hashTurns` Combat Turns. Nothing else edits or deletes it while it
|
|
65
|
+
* holds (commands/edit-file.ts). */
|
|
66
|
+
hashedBy;
|
|
67
|
+
hashTurns = 0;
|
|
68
|
+
/** WATERMARK: an invisible tag only Resonance-driven things read -- a
|
|
69
|
+
* technomancer or another sprite. A new one overwrites the old. */
|
|
70
|
+
watermark;
|
|
52
71
|
_transferable;
|
|
53
72
|
// New currency-related fields
|
|
54
73
|
_currencyAmount = 0;
|
|
@@ -735,6 +735,14 @@ export class NPC extends Player {
|
|
|
735
735
|
}
|
|
736
736
|
boundDevice;
|
|
737
737
|
companionKind;
|
|
738
|
+
/**
|
|
739
|
+
* ELECTRON STORM (SR5 p.257, utilities/sprite-power-actions.ts): the
|
|
740
|
+
* persona this sprite is holding a storm open on. It burns again every
|
|
741
|
+
* action the sprite spends sustaining it, and "if the sprite takes any
|
|
742
|
+
* Matrix damage, all of its electron storms end immediately" -- which
|
|
743
|
+
* is why this is cleared on damage rather than on a timer.
|
|
744
|
+
*/
|
|
745
|
+
sustainedStorm;
|
|
738
746
|
/**
|
|
739
747
|
* Runs one command AS this NPC through its own scene-bound registry --
|
|
740
748
|
* the deterministic control surface for "order <companion> <command>"
|