@maka/maka-cli 5.390.0 → 5.392.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/assense.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +60 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +5 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +16 -150
- package/bundle/typescript/src/commands/game/sideQuest/commands/crack.js +76 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +42 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/disarm.js +64 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +23 -46
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +23 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +15 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +55 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +15 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/leap.js +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +40 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +16 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +17 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/authored-run.js +632 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/catalog-item-seed.js +78 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/job-order.js +7 -44
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +8 -10
- package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +30 -52
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +112 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +123 -3
- package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js +52 -4
- package/bundle/typescript/src/commands/game/sideQuest/game.js +17 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/camera.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js +14 -21
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +62 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +141 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +18 -11
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/astral-move.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/district-plane.js +109 -27
- package/bundle/typescript/src/commands/game/sideQuest/utilities/door-watch.js +70 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/falling.js +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/file-guard.js +120 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +23 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +29 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ledge-jump.js +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/owner-guard.js +77 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/path-run.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +66 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +8 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spider.js +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +20 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +193 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/wards.js +116 -0
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { gridActionModifier } from '../utilities/grids.js';
|
|
3
|
+
import { hint } from '../utilities/hints.js';
|
|
4
|
+
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
5
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
6
|
+
import { hostLabel } from '../utilities/grid-names.js';
|
|
7
|
+
import { detonate, pickHostFile } from '../utilities/file-guard.js';
|
|
8
|
+
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
9
|
+
/**
|
|
10
|
+
* DISARM DATA BOMB (SR5 p.239, RAG-checked 2026-10-06): Complex Action,
|
|
11
|
+
* Marks Required none, Software + Intuition [Firewall] v. Data Bomb
|
|
12
|
+
* Rating x 2. "If you get any net hits, the Data Bomb is removed and
|
|
13
|
+
* deleted. If you fail, the Data Bomb activates." A Firewall action
|
|
14
|
+
* (p.244) -- legal, so GOD does not count it.
|
|
15
|
+
*
|
|
16
|
+
* You disarm what you have FOUND: a bomb is detected with Matrix
|
|
17
|
+
* Perception (p.242), "look <file>" inside the host. Software cannot be
|
|
18
|
+
* defaulted (p.145, "Default: No").
|
|
19
|
+
*/
|
|
20
|
+
export class DisarmCommand extends Command {
|
|
21
|
+
static verb = 'disarm';
|
|
22
|
+
static description = 'Disarm Data Bomb (SR5 p.239, Complex Action): "disarm <file>" -- Software + Intuition [Firewall] v. the bomb\'s Rating x 2, once Matrix Perception ("look <file>" inside the host) has turned the bomb up. Any net hit deletes the bomb; failure sets it off. Legal.';
|
|
23
|
+
async execute(args = []) {
|
|
24
|
+
const actor = this.actor;
|
|
25
|
+
const found = pickHostFile(actor, args.join(' ').trim(), 'disarm');
|
|
26
|
+
if (typeof found === 'string')
|
|
27
|
+
return found;
|
|
28
|
+
const { host, file } = found;
|
|
29
|
+
if (!file.dataBomb || !file.bombSeenBy.has(actor.name)) {
|
|
30
|
+
return `You see no data bomb on ${file.name} to disarm. ${hint(`(Matrix Perception finds one: "look ${file.name}".)`)}`;
|
|
31
|
+
}
|
|
32
|
+
const software = actor.skillRating('software');
|
|
33
|
+
if (software <= 0) {
|
|
34
|
+
return `Disarming a data bomb is Software, and Software cannot be defaulted (p.145) -- without the skill there is no test to try.`;
|
|
35
|
+
}
|
|
36
|
+
const burned = actor.burnedAttributeRefusal('firewall');
|
|
37
|
+
if (burned)
|
|
38
|
+
return burned;
|
|
39
|
+
const bill = billAction(this.scene, actor, 'complex', 'Disarm Data Bomb');
|
|
40
|
+
if (bill)
|
|
41
|
+
return bill;
|
|
42
|
+
const rating = file.dataBomb.rating;
|
|
43
|
+
const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, actor.currentGrid);
|
|
44
|
+
const pool = Math.max(1, actor.intuition + software + actor.bonus('software')
|
|
45
|
+
+ actor.matrixActionPenalty + grid.total + actor.woundModifier - actor.sustainingPenalty);
|
|
46
|
+
const attempt = rollPool(pool, actor.matrixAttribute('firewall'), { gremlins: actor.deckGremlins });
|
|
47
|
+
const resist = rollPool(rating * 2);
|
|
48
|
+
if (showsMechanics(this.scene, actor)) {
|
|
49
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Disarm Data Bomb -- Software + Intuition [Firewall]: ${formatRoll(attempt)}`);
|
|
50
|
+
this.logger.meta(` v. Data Bomb ${rating} x 2: ${formatRoll(resist)}`);
|
|
51
|
+
}
|
|
52
|
+
actor.performAction('works at a data bomb', `on ${file.name} in ${hostLabel(host.over)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
53
|
+
if (attempt.hits <= resist.hits) {
|
|
54
|
+
this.logger.write(`${actor.name} failed to disarm the bomb on "${file.name}": ${attempt.hits} v ${resist.hits}.`);
|
|
55
|
+
return [`Your probe trips the trigger.`, ...detonate(this.scene, this.logger, actor, host, file).lines].join('\n');
|
|
56
|
+
}
|
|
57
|
+
file.dataBomb = undefined;
|
|
58
|
+
file.bombSeenBy.clear();
|
|
59
|
+
this.logger.write(`${actor.name} disarmed the bomb on "${file.name}": ${attempt.hits} v ${resist.hits}.`);
|
|
60
|
+
this.scene.updateStatus();
|
|
61
|
+
return `{light-blue-fg}You unpick the payload thread by thread -- the data bomb on ${file.name} deletes itself, harmless.{/light-blue-fg}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=disarm.js.map
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { CastCommand } from './cast.js';
|
|
2
|
-
import { BypassCommand } from './bypass-command.js';
|
|
3
2
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
4
3
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
4
|
import { billAction } from '../utilities/action-cost.js';
|
|
6
5
|
import { roomAtCell } from '../utilities/district-plane.js';
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* COUNTERSPELLING'S SECOND HALF (RAW p.295): "dispel <name>" unweaves a
|
|
8
|
+
* SUSTAINED SPELL off an actor in the room -- an enemy mage's
|
|
9
|
+
* invisibility, an armor weave, quickened reflexes, a barrier spell --
|
|
10
|
+
* Magic + counterspelling opposed by their grip on it.
|
|
11
|
+
*
|
|
12
|
+
* A WARD IS NOT DISPELLED (user 2026-10-06). It used to be: a ward was a
|
|
13
|
+
* door device this verb unweaved, with backlash. A ward is a ritual's
|
|
14
|
+
* astral barrier over a room (utilities/wards.ts), and canon gives no
|
|
15
|
+
* rule for dispelling one -- it is pressed through or attacked, from the
|
|
16
|
+
* astral (p.316).
|
|
17
17
|
*
|
|
18
18
|
* Registered under both spellings ("dispel" is the book's, "dispell" is
|
|
19
19
|
* how half the table types it).
|
|
@@ -30,8 +30,7 @@ const SUSTAINED_EFFECTS = [
|
|
|
30
30
|
// The barrier pair (ruling 2026-08-24; canon p.294). MANA first:
|
|
31
31
|
// find() takes the first word match, so "dispel mana barrier" lands
|
|
32
32
|
// here instead of on the physical entry's 'barrier'. With no
|
|
33
|
-
// spell-wall standing, these words
|
|
34
|
-
// route as before (holderless named effects return undefined below).
|
|
33
|
+
// spell-wall standing, these words find no holder (undefined below).
|
|
35
34
|
{ key: 'manabarrier', words: ['manabarrier', 'mana'], label: 'mana barrier',
|
|
36
35
|
has: p => !!p.sustainedManaBarrier, clear: p => { p.sustainedManaBarrier = undefined; } },
|
|
37
36
|
{ key: 'barrier', words: ['barrier', 'wall'], label: 'force barrier',
|
|
@@ -39,23 +38,7 @@ const SUSTAINED_EFFECTS = [
|
|
|
39
38
|
];
|
|
40
39
|
export class DispelCommand extends CastCommand {
|
|
41
40
|
static verb = 'dispel';
|
|
42
|
-
static description = 'Unweave
|
|
43
|
-
verbName = 'dispel';
|
|
44
|
-
noDeviceMessage = `There's no magical barrier here to dispel.`;
|
|
45
|
-
/**
|
|
46
|
-
* RAW dispelling (p.295): Magic + counterspelling -- the unweave is a
|
|
47
|
-
* parry, not a working, so the CASTING pool (cast's ward route) stays
|
|
48
|
-
* cast's. Unskilled follows the house -1 default; a gear-only caster
|
|
49
|
-
* (grimoire, no Magic) is down to scraps here -- their route is still
|
|
50
|
-
* "cast", which rolls the full casting pool.
|
|
51
|
-
*/
|
|
52
|
-
async determineOutcome() {
|
|
53
|
-
const roll = rollPool(this.dispelPool());
|
|
54
|
-
if (this.scene.isHumanControlled(this.actor)) {
|
|
55
|
-
this.logger.meta(`Counterspelling: ${formatRoll(roll)} (2+ to unweave)`);
|
|
56
|
-
}
|
|
57
|
-
return roll.hits >= 2;
|
|
58
|
-
}
|
|
41
|
+
static description = 'Unweave a SUSTAINED SPELL -- Magic + counterspelling, opposed by its caster\'s grip: "dispel <name>", "dispel invisibility on <name>", "dispel barrier". No backlash; retry freely. A ward is not dispelled -- press through it or attack it from the astral.';
|
|
59
42
|
dispelPool() {
|
|
60
43
|
const skill = this.actor.skillRating('counterspelling');
|
|
61
44
|
return Math.max(1, this.actor.magic + (skill > 0 ? skill : -1) + this.actor.woundModifier - this.actor.sustainingPenalty);
|
|
@@ -70,27 +53,27 @@ export class DispelCommand extends CastCommand {
|
|
|
70
53
|
return `Unweaving is Sorcery, and your gift is aspected to conjuring -- you can see the weave, not pull it.`;
|
|
71
54
|
}
|
|
72
55
|
if (!this.actor.isCaster()) {
|
|
73
|
-
return `Unweaving a
|
|
56
|
+
return `Unweaving a spell takes talent you don't have -- you'd need to be Awakened, or have a spell formula, grimoire, or focus on you.`;
|
|
57
|
+
}
|
|
58
|
+
if (!args || args.length === 0) {
|
|
59
|
+
return `Dispel what? "dispel <name>" strips a sustained spell off someone here.`;
|
|
74
60
|
}
|
|
75
61
|
// DISPELLING IS A COMPLEX ACTION (SR5 p.295) inside a Combat Turn.
|
|
76
62
|
const bill = billAction(this.scene, this.actor, 'complex', 'Dispel');
|
|
77
63
|
if (bill)
|
|
78
64
|
return bill;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return sustained;
|
|
65
|
+
const sustained = this.tryDispelSustained(args);
|
|
66
|
+
if (sustained !== undefined)
|
|
67
|
+
return sustained;
|
|
68
|
+
if (args.some(w => /^wards?$/i.test(w))) {
|
|
69
|
+
return `A ward is a ritual's astral barrier, not a sustained spell -- there is no grip on it to unweave. From the astral, "go" presses through it and "attack ward" tears at it (p.316).`;
|
|
85
70
|
}
|
|
86
|
-
|
|
87
|
-
// ever means a barrier or a sustained spell.
|
|
88
|
-
return BypassCommand.prototype.execute.call(this, args);
|
|
71
|
+
return `No sustained spell by that name here to unweave.`;
|
|
89
72
|
}
|
|
90
73
|
/**
|
|
91
74
|
* "dispel <name>" / "dispel invisibility on <name>": strip a sustained
|
|
92
75
|
* spell off an actor here. Returns undefined when the words name no
|
|
93
|
-
* actor in the room
|
|
76
|
+
* actor in the room, a message otherwise.
|
|
94
77
|
*/
|
|
95
78
|
tryDispelSustained(args) {
|
|
96
79
|
const lowered = args.map(w => w.toLowerCase());
|
|
@@ -143,12 +126,6 @@ export class DispelCommand extends CastCommand {
|
|
|
143
126
|
if ((target.sustainedForms?.length ?? 0) > 0) {
|
|
144
127
|
return `What's woven around ${target.name} isn't mana -- it's RESONANCE. Your counterspelling finds nothing to grip.`;
|
|
145
128
|
}
|
|
146
|
-
// A fuzzy actor hit holding nothing, with a warded barrier standing
|
|
147
|
-
// here and no effect named ("dispel ward" grazing an NPC called
|
|
148
|
-
// Warden): the word was probably aimed at the barrier -- yield.
|
|
149
|
-
const barriers = (room.openableDevices() ?? []).filter(d => d.kind === 'ward');
|
|
150
|
-
if (barriers.length > 0 && !named)
|
|
151
|
-
return undefined;
|
|
152
129
|
return target === this.actor
|
|
153
130
|
? `Nothing of yours is sustained right now.`
|
|
154
131
|
: `No sustained spell clings to ${target.name}.`;
|
|
@@ -9,6 +9,7 @@ import { hostLabel } from '../utilities/grid-names.js';
|
|
|
9
9
|
import { editFileBlocked, rollEditFile } from '../utilities/edit-file-test.js';
|
|
10
10
|
import { copyFile, deviceHas, fileOnto, findDataDevice, libraryDevice } from '../utilities/data-library.js';
|
|
11
11
|
import { NPC } from '../models/npc.js';
|
|
12
|
+
import { guardFile, splitPasscode } from '../utilities/file-guard.js';
|
|
12
13
|
/**
|
|
13
14
|
* "download" -- COPY A FILE OFF THE GRID INTO YOUR DEVICE'S LIBRARY.
|
|
14
15
|
*
|
|
@@ -50,7 +51,9 @@ export class DownloadCommand extends Command {
|
|
|
50
51
|
static description = 'Copy a file off the Matrix into your device\'s library -- "download <file>" (or "download <file> to <device>"). Edit File (SR5 p.239, a Complex Action): Computer + Logic [Data Processing] v. the host, with a mark on it -- no roll once the host is cracked, or for data loose on the grid or held by your own sprites. The host keeps its original. "inv" lists your libraries; "transfer" moves files between devices or onto a chip.';
|
|
51
52
|
async execute(args = []) {
|
|
52
53
|
const actor = this.actor;
|
|
53
|
-
|
|
54
|
+
// "download <file> with <passcode>" walks past its data bomb (p.242).
|
|
55
|
+
const { rest: named, passcode } = splitPasscode(args);
|
|
56
|
+
let words = named.join(' ').trim();
|
|
54
57
|
// "... to <device>": split on the LAST " to ", and only when the tail
|
|
55
58
|
// names a data device you carry -- a file called "Letter to Krow"
|
|
56
59
|
// still downloads whole.
|
|
@@ -106,7 +109,7 @@ export class DownloadCommand extends Command {
|
|
|
106
109
|
return elsewhere;
|
|
107
110
|
return `No file called "${words}" within reach.${hint(` ("download" on its own lists what is.)`)}`;
|
|
108
111
|
}
|
|
109
|
-
const out = this.pull(source, target);
|
|
112
|
+
const out = this.pull(source, target, passcode);
|
|
110
113
|
return out.text;
|
|
111
114
|
}
|
|
112
115
|
/**
|
|
@@ -186,7 +189,7 @@ export class DownloadCommand extends Command {
|
|
|
186
189
|
* only when a file actually landed -- take-all stops on a refusal that
|
|
187
190
|
* is about the ACTOR (out of actions), not the file.
|
|
188
191
|
*/
|
|
189
|
-
pull(source, target) {
|
|
192
|
+
pull(source, target, passcode) {
|
|
190
193
|
const actor = this.actor;
|
|
191
194
|
const item = source.item;
|
|
192
195
|
// A LOOSE END IS ERASED, NOT DOWNLOADED: the desk keeps its copy
|
|
@@ -245,12 +248,26 @@ export class DownloadCommand extends Command {
|
|
|
245
248
|
const bill = billAction(this.scene, actor, 'complex', 'Edit File (copy)');
|
|
246
249
|
if (bill)
|
|
247
250
|
return { ok: false, text: bill, stop: true };
|
|
251
|
+
// THE FILE'S OWN DEFENCES (utilities/file-guard.ts, p.239: a
|
|
252
|
+
// protected file's copy "automatically fails", and a bomb "goes off
|
|
253
|
+
// on you"). A cracked HOST is not a cracked FILE -- this gate stands
|
|
254
|
+
// whether or not the host roll does.
|
|
255
|
+
let blast = '';
|
|
256
|
+
if (host) {
|
|
257
|
+
const guard = guardFile(this.scene, this.logger, actor, host, item, 'copy', passcode);
|
|
258
|
+
if (guard.stop) {
|
|
259
|
+
actor.performAction('edits a file', `${item.name} in ${hostLabel(host.over)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
260
|
+
return { ok: false, text: guard.lines.join('\n') };
|
|
261
|
+
}
|
|
262
|
+
if (guard.lines.length > 0)
|
|
263
|
+
blast = `${guard.lines.join('\n')}\n`;
|
|
264
|
+
}
|
|
248
265
|
if (host && needsRoll) {
|
|
249
266
|
const roll = rollEditFile(this.scene, this.logger, actor, host, 'copy');
|
|
250
267
|
if (!roll.success) {
|
|
251
268
|
actor.performAction('edits a file', `${item.name} in ${hostLabel(host.over)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
252
269
|
this.logger.write(`${actor.name} failed to copy "${item.name}" from ${host.over.name}: ${roll.hits} v ${roll.defence}.`);
|
|
253
|
-
return { ok: false, text:
|
|
270
|
+
return { ok: false, text: `${blast}The copy stalls -- ${hostLabel(host.over)}'s checksum rejects the read and nothing comes down. A failed edit is not a failed sleaze; nobody upstairs noticed.${hint(` (Try again; each attempt is a Complex Action.)`)}` };
|
|
254
271
|
}
|
|
255
272
|
}
|
|
256
273
|
// THE MOVE. A sprite's file is already yours and moves; everything
|
|
@@ -281,11 +298,11 @@ export class DownloadCommand extends Command {
|
|
|
281
298
|
this.logger.write(`${actor.name} downloaded "${item.name}" (${source.kind}) ${onto ? `onto ${onto.name}` : 'into its own hands'}.`);
|
|
282
299
|
this.game?.requestSave('data');
|
|
283
300
|
this.scene.updateStatus();
|
|
284
|
-
const text = onto
|
|
301
|
+
const text = blast + (onto
|
|
285
302
|
? source.kind === 'sprite'
|
|
286
303
|
? `{light-blue-fg}${source.npc.name} opens its hands and the data pours out of them -- {bold}{underline}${item.name}{/underline}{/bold} is on your ${onto.name} now.{/light-blue-fg}`
|
|
287
304
|
: `{light-blue-fg}You copy {bold}{underline}${item.name}{/underline}{/bold} onto your ${onto.name}${host ? ` -- ${hostLabel(host.over)} keeps its original` : ''}.{/light-blue-fg}${hint(` ("give" or "call" hands it over straight from the device; "transfer ${item.name} to chip" writes it to physical media.)`)}`
|
|
288
|
-
: `{light-blue-fg}You take hold of ${item.name}.{/light-blue-fg}
|
|
305
|
+
: `{light-blue-fg}You take hold of ${item.name}.{/light-blue-fg}`);
|
|
289
306
|
// 'possess' objectives complete in the runner's hands; a device library
|
|
290
307
|
// is the runner's hands for data (scene.ts checkWinCondition).
|
|
291
308
|
if (this.scene.isHumanControlled(actor))
|
|
@@ -4,6 +4,7 @@ import { billAction } from '../utilities/action-cost.js';
|
|
|
4
4
|
import { editFileBlocked, rollEditFile } from '../utilities/edit-file-test.js';
|
|
5
5
|
import { hostLabel } from '../utilities/grid-names.js';
|
|
6
6
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
7
|
+
import { guardFile, splitPasscode } from '../utilities/file-guard.js';
|
|
7
8
|
/**
|
|
8
9
|
* EDIT FILE (SR5 p.239, RAG-checked 2026-09-14): a Complex Action, Marks
|
|
9
10
|
* Required 1, Computer + Logic [Data Processing] v. Intuition + Firewall.
|
|
@@ -87,7 +88,9 @@ export class EditFileCommand extends Command {
|
|
|
87
88
|
}
|
|
88
89
|
const room = host.over;
|
|
89
90
|
const files = host.files();
|
|
90
|
-
|
|
91
|
+
// "erase <file> with <passcode>" walks past its data bomb (p.242).
|
|
92
|
+
const { rest: named, passcode } = splitPasscode(rest);
|
|
93
|
+
const query = named.join(' ').trim();
|
|
91
94
|
if (!query) {
|
|
92
95
|
return files.length === 0
|
|
93
96
|
? `${hostLabel(room, { capital: true })} keeps nothing here to edit.`
|
|
@@ -119,20 +122,29 @@ export class EditFileCommand extends Command {
|
|
|
119
122
|
const bill = billAction(this.scene, actor, 'complex', 'Edit File');
|
|
120
123
|
if (bill)
|
|
121
124
|
return bill;
|
|
125
|
+
// THE FILE'S OWN DEFENCES (utilities/file-guard.ts): the attempt
|
|
126
|
+
// springs a bomb, and protection refuses the delete outright.
|
|
127
|
+
const guard = guardFile(this.scene, this.logger, actor, host, file, 'delete', passcode);
|
|
128
|
+
if (guard.stop) {
|
|
129
|
+
actor.performAction('edits a file', `${file.name} in ${hostLabel(room)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
130
|
+
return guard.lines.join('\n');
|
|
131
|
+
}
|
|
132
|
+
const blast = guard.lines.length > 0 ? `${guard.lines.join('\n')}
|
|
133
|
+
` : '';
|
|
122
134
|
// The roll is utilities/edit-file-test.ts, shared with download (copy)
|
|
123
135
|
// and upload (create) so the family cannot drift apart.
|
|
124
136
|
const attempt = rollEditFile(this.scene, this.logger, actor, host, 'delete');
|
|
125
137
|
actor.performAction('edits a file', `${file.name} in ${hostLabel(room)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
126
138
|
if (!attempt.success) {
|
|
127
139
|
this.logger.write(`${actor.name} failed to delete "${file.name}" in ${room.name}: ${attempt.hits} v ${attempt.defence}.`);
|
|
128
|
-
return
|
|
140
|
+
return `${blast}The file's checksum holds -- your edit rolls off it and the archive keeps ${file.name}. Nothing upstairs noticed: a failed edit is not a failed sleaze.${hint(` (Try again; each attempt is a Complex Action.)`)}`;
|
|
129
141
|
}
|
|
130
142
|
host.removeFile(file.name);
|
|
131
143
|
this.logger.write(`${actor.name} deleted "${file.name}" from ${room.name}'s host: ${attempt.hits} v ${attempt.defence}.`);
|
|
132
144
|
this.scene.addWorldEvent(`${file.name} was erased from ${host.name}'s archive.`, { plane: 'matrix' });
|
|
133
145
|
const banner = this.scene.checkErased?.(file.name, actor.name);
|
|
134
146
|
this.scene.updateStatus();
|
|
135
|
-
return [
|
|
147
|
+
return blast + [
|
|
136
148
|
`{light-blue-fg}${file.name} comes apart under your hand -- sectors zeroed, index entry gone, the host's own checksum rewritten to agree. ${hostLabel(room, { capital: true })} no longer has it.{/light-blue-fg}`,
|
|
137
149
|
...(banner ? [banner] : []),
|
|
138
150
|
].join('\n');
|
|
@@ -15,7 +15,7 @@ import { hint } from '../utilities/hints.js';
|
|
|
15
15
|
import { gridIconBlock } from '../utilities/grid-view.js';
|
|
16
16
|
import { requirePhase } from '../utilities/action-cost.js';
|
|
17
17
|
import { midClimbRefusal } from '../utilities/climb-state.js';
|
|
18
|
-
import {
|
|
18
|
+
import { cameraSeeing } from '../utilities/surveillance.js';
|
|
19
19
|
import { maybeSinSweep } from '../utilities/sin.js';
|
|
20
20
|
import { despawnEphemerals } from '../utilities/ephemeral.js';
|
|
21
21
|
import { maybeStreetGang } from '../utilities/street-gang.js';
|
|
@@ -25,9 +25,11 @@ import { Direction } from '../types/shared/direction-enum.js';
|
|
|
25
25
|
import { dragFollowers } from './follow.js';
|
|
26
26
|
import { escortedBy } from './bring.js';
|
|
27
27
|
import { NPC } from '../models/npc.js';
|
|
28
|
+
import { caughtAtDoor, springDoorWatch } from '../utilities/door-watch.js';
|
|
28
29
|
import { Command } from './command.js';
|
|
29
30
|
import { doorwayOf, streetDoors } from '../utilities/district-plane.js';
|
|
30
31
|
import { driftAstral, driftRefusal } from '../utilities/astral-move.js';
|
|
32
|
+
import { pressThroughWard, spendWardPass, wardBetween } from '../utilities/wards.js';
|
|
31
33
|
import { isOutdoorsPlace } from '../utilities/outdoors.js';
|
|
32
34
|
import { crowdLine } from '../utilities/crowd.js';
|
|
33
35
|
import { startPathRun, goalReached, routeMeters, planSegment } from '../utilities/path-run.js';
|
|
@@ -571,7 +573,21 @@ export class GoCommand extends Command {
|
|
|
571
573
|
*/
|
|
572
574
|
if (this.actor.plane === 'astral' && this.isValidDirection(direction)
|
|
573
575
|
&& DRIFT_WAYS.has(direction)) {
|
|
574
|
-
|
|
576
|
+
let drift = driftAstral(this.scene, this.actor, this.rooms ?? {}, direction, 1, true);
|
|
577
|
+
// A WARD IN THE WAY IS PRESSED, NOT JUST REFUSED (p.316): `go` is
|
|
578
|
+
// the verb that means to get there, so it makes the attempt.
|
|
579
|
+
if (drift !== undefined && drift.taken === 0 && drift.blockedBy === 'ward') {
|
|
580
|
+
const ward = Object.values(this.rooms ?? {}).find(r => r.name === drift.ward);
|
|
581
|
+
if (ward) {
|
|
582
|
+
const press = pressThroughWard(this.scene, this.logger, this.actor, ward);
|
|
583
|
+
if (this.scene.isHumanControlled(this.actor))
|
|
584
|
+
this.logger.meta(press.meta);
|
|
585
|
+
if (!press.through) {
|
|
586
|
+
return `The ward over ${ward.name} holds -- Force ${ward.wardForce}. You press against the lattice and it presses back; the astral does not pass.`;
|
|
587
|
+
}
|
|
588
|
+
drift = driftAstral(this.scene, this.actor, this.rooms ?? {}, direction, 1, true);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
575
591
|
if (drift !== undefined) {
|
|
576
592
|
if (drift.taken === 0)
|
|
577
593
|
return driftRefusal(drift, direction);
|
|
@@ -691,6 +707,21 @@ export class GoCommand extends Command {
|
|
|
691
707
|
// either one holds the way -- there is no route around a door the
|
|
692
708
|
// way there is around a table. Checked BEFORE the walk, so you're
|
|
693
709
|
// told who's in your way instead of crossing the room to find out.
|
|
710
|
+
// A GUARDED DOOR (utilities/door-watch.ts): whoever watches it -- or
|
|
711
|
+
// holds it, which watches it too -- and catches the runner going for
|
|
712
|
+
// it turns hostile, and the fight opens here; the move does not
|
|
713
|
+
// happen. A runner who slips past unseen carries on. Runners only:
|
|
714
|
+
// NPCs walking the site do not spring each other's watch.
|
|
715
|
+
if (!(this.actor instanceof NPC)) {
|
|
716
|
+
const watched = caughtAtDoor(currentRoom, direction, this.actor, this.scene.isHumanControlled(this.actor));
|
|
717
|
+
if (this.scene.isHumanControlled(this.actor))
|
|
718
|
+
for (const line of watched.lines)
|
|
719
|
+
this.logger.meta(line);
|
|
720
|
+
if (watched.npc) {
|
|
721
|
+
this.actor.performAction('is caught going for the door', `${watched.npc.name} watches the ${direction} door`);
|
|
722
|
+
return await springDoorWatch(this.scene, currentRoom, direction, this.actor, watched.npc);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
694
725
|
const blocker = doorwayBlocker(currentRoom, direction, this.actor);
|
|
695
726
|
if (blocker) {
|
|
696
727
|
this.actor.performAction('finds the way blocked', `${blocker.name} holds the ${direction} doorway`);
|
|
@@ -898,6 +929,7 @@ export class GoCommand extends Command {
|
|
|
898
929
|
this.actor.performAction('exits', `${currentRoom.name} to ${targetRoom.name}`);
|
|
899
930
|
this.actor.cameFrom = { room: currentRoom.name, back: GoCommand.opposite(travelled) };
|
|
900
931
|
this.actor.currentLocation = targetRoom;
|
|
932
|
+
spendWardPass(this.actor);
|
|
901
933
|
// "At" spots: an arrival stands at the room's entry (the setter
|
|
902
934
|
// cleared any stale spot), and the WITNESS event says where -- so
|
|
903
935
|
// an NPC's history reads "enters the bar (at the door)" and its AI
|
|
@@ -1106,7 +1138,7 @@ export class GoCommand extends Command {
|
|
|
1106
1138
|
let camLine = '';
|
|
1107
1139
|
if (this.game?.onRun && this.actor === this.scene.getPlayer()
|
|
1108
1140
|
&& this.actor.qualities.has('sinner-criminal')
|
|
1109
|
-
&&
|
|
1141
|
+
&& !!cameraSeeing(this.rooms ?? {}, this.actor)
|
|
1110
1142
|
&& !this.game.camPingedRooms.has(room.name)) {
|
|
1111
1143
|
this.game.camPingedRooms.add(room.name);
|
|
1112
1144
|
this.game.addHeat(1, 'facial rec: criminal SIN on camera', this.actor, { identity: true });
|
|
@@ -1237,21 +1269,27 @@ export class GoCommand extends Command {
|
|
|
1237
1269
|
const map = { north: 'south', south: 'north', east: 'west', west: 'east', up: 'down', down: 'up' };
|
|
1238
1270
|
return map[direction] ?? direction;
|
|
1239
1271
|
}
|
|
1240
|
-
/**
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1272
|
+
/**
|
|
1273
|
+
* WARDS STOP ASTRAL FORMS AND NOTHING ELSE (p.294, p.315), both ways --
|
|
1274
|
+
* into a warded room and out of one. Crossing one is a press (p.316):
|
|
1275
|
+
* Magic + Charisma [Astral] v. Force x 2, and a breach is something the
|
|
1276
|
+
* creator knows (utilities/wards.ts). Its creator crosses freely.
|
|
1277
|
+
*
|
|
1278
|
+
* No Masking slip any more: Masking hides an aura from Assensing and
|
|
1279
|
+
* crosses nothing (SG p.149). Fooling Wards (SG p.135) needs Flux or an
|
|
1280
|
+
* astral doppelganger, which this engine does not model.
|
|
1281
|
+
*/
|
|
1244
1282
|
checkWard(target) {
|
|
1245
|
-
if (this.actor.plane
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
return `A ward seals ${
|
|
1283
|
+
if (this.actor.plane !== 'astral')
|
|
1284
|
+
return null;
|
|
1285
|
+
const ward = wardBetween(this.actor.currentLocation, target, this.actor);
|
|
1286
|
+
if (!ward)
|
|
1287
|
+
return null;
|
|
1288
|
+
const press = pressThroughWard(this.scene, this.logger, this.actor, ward);
|
|
1289
|
+
if (this.scene.isHumanControlled(this.actor))
|
|
1290
|
+
this.logger.meta(press.meta);
|
|
1291
|
+
if (!press.through) {
|
|
1292
|
+
return `A ward seals ${ward.name} -- Force ${ward.wardForce}. You press against the lattice and it presses back; the astral does not pass${hint(` ("attack ward" tears at it instead)`)}.`;
|
|
1255
1293
|
}
|
|
1256
1294
|
return null;
|
|
1257
1295
|
}
|
|
@@ -16,6 +16,7 @@ import { MARKS_TO_CONTROL } from './disable.js';
|
|
|
16
16
|
import { nameableNames } from '../utilities/nameables.js';
|
|
17
17
|
import { sameHostSide } from '../models/player.js';
|
|
18
18
|
import { MAX_MARKS, markHackerFromDevice } from '../utilities/marks.js';
|
|
19
|
+
import { ownerInformed } from '../utilities/owner-guard.js';
|
|
19
20
|
import { actionRefusal, billAction } from '../utilities/action-cost.js';
|
|
20
21
|
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
21
22
|
import { declarationPenalty, parseMarkDeclaration, hostDefensePool, wanDefensePool, freeMatrixPerceptionHits, bruteForceMatrixDv, overwatchFromDefense, GO_BIG_QUALITY, deviceDefensePool } from '../utilities/matrix-intrusion.js';
|
|
@@ -323,9 +324,12 @@ export class HackCommand extends BypassCommand {
|
|
|
323
324
|
// exact moment they most need to know, since a blown Sleaze is
|
|
324
325
|
// what hands a mark to something that did not have one.
|
|
325
326
|
const marksOnMe = room.hostMarksOn.get(this.actor.name) ?? 0;
|
|
327
|
+
// ...and its OWNER is told (owner-guard.ts; p.236).
|
|
328
|
+
const told = ownerInformed(this.scene, room, this.actor, 'host-sleaze', hostLabel(room, { capital: true }));
|
|
326
329
|
return [
|
|
327
330
|
`${hostLabel(room, { capital: true })} turns and LOOKS AT YOU -- your Hack on the Fly unravels mid-handshake.`,
|
|
328
331
|
` It holds ${marksOnMe === 1 ? 'a mark' : `${marksOnMe} marks`} on your persona now, it can see you, and everything guarding this place knows someone is on the grid. ("unmark me" scrubs it; so do "reboot" and "jack out", p.242.)`,
|
|
332
|
+
...(told ? [` ${told}`] : []),
|
|
329
333
|
...godLines,
|
|
330
334
|
].join('\n');
|
|
331
335
|
}
|
|
@@ -410,6 +414,9 @@ export class HackCommand extends BypassCommand {
|
|
|
410
414
|
this.actor.sneaking = false;
|
|
411
415
|
room.hostAlert = true;
|
|
412
416
|
this.scene.addWorldEvent(`${hostLabel(room, { capital: true })} takes a forced key -- alarm flagged, ice hunting.`, { plane: 'matrix' });
|
|
417
|
+
const told = ownerInformed(this.scene, room, this.actor, 'attack-landed', hostLabel(room, { capital: true }));
|
|
418
|
+
if (told)
|
|
419
|
+
lines.push(` ${told}`);
|
|
413
420
|
}
|
|
414
421
|
lines.push(...godLines);
|
|
415
422
|
this.scene.updateStatus();
|
|
@@ -507,6 +514,9 @@ export class HackCommand extends BypassCommand {
|
|
|
507
514
|
// under attack and screams to its owner -- it just can't see
|
|
508
515
|
// WHOSE hand did it).
|
|
509
516
|
this.actor.sneaking = false;
|
|
517
|
+
const told = ownerInformed(this.scene, room, this.actor, 'attack-landed', hostLabel(room, { capital: true }));
|
|
518
|
+
if (told)
|
|
519
|
+
lines.push(` ${told}`);
|
|
510
520
|
// A SUCCESSFUL ATTACK ACTION WAKES THE HOST (p.236: the target
|
|
511
521
|
// "becomes aware that it is under attack" though it does not
|
|
512
522
|
// automatically spot you; p.247: a host that notices something
|
|
@@ -765,7 +775,8 @@ export class HackCommand extends BypassCommand {
|
|
|
765
775
|
actor.sneaking = false;
|
|
766
776
|
// ...and it marks you (p.236) -- the trace a blown Sleaze leaves.
|
|
767
777
|
const onYou = markHackerFromDevice(actor, device.name, host);
|
|
768
|
-
const
|
|
778
|
+
const toldOwner = ownerInformed(this.scene, Object.values(allRooms).find(r => r.devices.includes(device)), actor, 'device-sleaze', device.name);
|
|
779
|
+
const trace = ` It holds ${onYou} mark${onYou === 1 ? '' : 's'} on your deck now${hint(' ("mark" shows who holds you; a reboot or "unmark me" shakes it)')}.${toldOwner ? ` ${toldOwner}` : ''}`;
|
|
769
780
|
if (host) {
|
|
770
781
|
host.hostAlert = true;
|
|
771
782
|
this.scene.addWorldEvent(`${device.name} flags a bad handshake -- ${hostLabel(host, { capital: true })} is awake.`, { plane: 'matrix' });
|
|
@@ -810,6 +821,9 @@ export class HackCommand extends BypassCommand {
|
|
|
810
821
|
if (host)
|
|
811
822
|
host.hostAlert = true;
|
|
812
823
|
actor.sneaking = false;
|
|
824
|
+
const told = ownerInformed(this.scene, Object.values(allRooms).find(r => r.devices.includes(device)), actor, 'attack-landed', device.name);
|
|
825
|
+
if (told)
|
|
826
|
+
lines.push(` ${told}`);
|
|
813
827
|
}
|
|
814
828
|
else {
|
|
815
829
|
const perception = freeMatrixPerceptionHits(netHits);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { hint } from '../utilities/hints.js';
|
|
3
|
+
import { ledgeDirection, ledgeJump } from '../utilities/ledge-jump.js';
|
|
4
|
+
/**
|
|
5
|
+
* "leap off <direction>" (user ruling 2026-10-06): going over the edge of
|
|
6
|
+
* a balcony, catwalk or rooftop on purpose. The extra word is the safety
|
|
7
|
+
* catch -- a fall is never one mistyped direction away -- so a bare
|
|
8
|
+
* "leap north" is refused and told the full form. utilities/ledge-jump.ts
|
|
9
|
+
* does the drop and the landing.
|
|
10
|
+
*/
|
|
11
|
+
export class LeapCommand extends Command {
|
|
12
|
+
static verb = 'leap';
|
|
13
|
+
static description = 'Go over the edge of a balcony, catwalk or rooftop on purpose: "leap off <north|south|east|west>". You land on the outdoor ground below that spot and take the fall (SR5 p.172: more than 3 m is DV = metres, AP -4, Body + Armor).';
|
|
14
|
+
async execute(args = []) {
|
|
15
|
+
const [off, dirWord] = args.map(a => a.toLowerCase());
|
|
16
|
+
const dir = ledgeDirection(dirWord);
|
|
17
|
+
if (off !== 'off' || !dir) {
|
|
18
|
+
return `Leap where? ${hint(`"leap off <north|south|east|west>" -- the word off is on purpose: a drop is never one word away.`)}`;
|
|
19
|
+
}
|
|
20
|
+
return ledgeJump(this.scene, this.actor, dir);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=leap.js.map
|
|
@@ -507,6 +507,14 @@ export class LookCommand extends Command {
|
|
|
507
507
|
const onDevice = findDeviceFile(this.actor, target);
|
|
508
508
|
if (onDevice)
|
|
509
509
|
return this.describeItem(onDevice.file, target);
|
|
510
|
+
// A FILE IN THE ARCHIVE YOU STAND IN, once it is found: Matrix
|
|
511
|
+
// Perception on the file (p.241-242) -- what turns up a data bomb.
|
|
512
|
+
const inHost = this.actor.hostInside;
|
|
513
|
+
if (inHost && (inHost.searchedBy.has(this.actor.name) || inHost.over.hostCracked)) {
|
|
514
|
+
const file = inHost.files().find(f => f.name.toLowerCase() === target.toLowerCase());
|
|
515
|
+
if (file)
|
|
516
|
+
return this.perceiveFile(file, target);
|
|
517
|
+
}
|
|
510
518
|
const onGrid = room.inventory.getItem(target);
|
|
511
519
|
if (onGrid?.plane === 'matrix') {
|
|
512
520
|
const reach = fileReach(this.actor, room);
|
|
@@ -774,6 +782,38 @@ export class LookCommand extends Command {
|
|
|
774
782
|
}
|
|
775
783
|
/** The item's prose and stat block -- the same read for a thing in the
|
|
776
784
|
* bag, on the body, or on the floor. */
|
|
785
|
+
/**
|
|
786
|
+
* MATRIX PERCEPTION ON A HOST FILE (p.241; p.242: "a Data Bomb can be
|
|
787
|
+
* detected using Matrix Perception"). Computer + Intuition [Data
|
|
788
|
+
* Processing], a Complex Action; a bomb does not run silent, so it is
|
|
789
|
+
* not an opposed test -- one hit turns it up (p.235: each hit buys a
|
|
790
|
+
* piece of information). Protection needs no test: the file is a
|
|
791
|
+
* sealed block on its face, and the book's own refusal (p.239) tells
|
|
792
|
+
* anyone who tries.
|
|
793
|
+
*/
|
|
794
|
+
perceiveFile(file, itemName) {
|
|
795
|
+
const bill = billAction(this.scene, this.actor, 'complex', 'Matrix Perception');
|
|
796
|
+
if (bill)
|
|
797
|
+
return bill;
|
|
798
|
+
const lines = [this.describeItem(file, itemName)];
|
|
799
|
+
if (file.protection > 0)
|
|
800
|
+
lines.push(`{light-blue-fg} PROTECTED -- encrypted at rating ${file.protection}. "crack ${file.name}" breaks it (p.238).{/light-blue-fg}`);
|
|
801
|
+
if (file.dataBomb && !file.bombSeenBy.has(this.actor.name)) {
|
|
802
|
+
const computer = this.actor.skillRating('computer');
|
|
803
|
+
const pool = Math.max(1, this.actor.intuition + (computer > 0 ? computer : -1)
|
|
804
|
+
+ this.actor.matrixActionPenalty + this.actor.woundModifier - this.actor.sustainingPenalty);
|
|
805
|
+
const roll = rollPool(pool, this.actor.matrixAttribute('dataProcessing'), { gremlins: this.actor.deckGremlins });
|
|
806
|
+
if (showsMechanics(this.scene, this.actor)) {
|
|
807
|
+
this.logger.meta(`${mechanicsActorPrefix(this.scene, this.actor)}Matrix Perception on ${file.name} -- Computer + Intuition [Data Processing]: ${formatRoll(roll)}`);
|
|
808
|
+
}
|
|
809
|
+
if (roll.hits > 0)
|
|
810
|
+
file.bombSeenBy.add(this.actor.name);
|
|
811
|
+
}
|
|
812
|
+
if (file.dataBomb && file.bombSeenBy.has(this.actor.name)) {
|
|
813
|
+
lines.push(`{red-fg} DATA BOMB -- rating ${file.dataBomb.rating}${file.dataBomb.deletesFile ? ', set to take the file with it' : ''}. Touch it without the passcode and it goes off. "disarm ${file.name}" (p.239).{/red-fg}`);
|
|
814
|
+
}
|
|
815
|
+
return lines.join('\n');
|
|
816
|
+
}
|
|
777
817
|
describeItem(item, itemName) {
|
|
778
818
|
// A CODE ON THE PAGE IS A PERCEPTION TEST (Xxbwtgffkyq8uCHfR): a
|
|
779
819
|
// scrap of paper with a combination on it is read here, not by
|