@maka/maka-cli 5.182.0 → 5.184.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/delay.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +331 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-turn.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +11 -13
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +17 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +78 -31
- package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +7 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js +40 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +44 -18
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +50 -21
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +6 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +4 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.184.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -64,14 +64,14 @@ export class ArCommand extends Command {
|
|
|
64
64
|
return `Overlay back on -- over nothing. No commlink, no deck, no AR: you're reading the street with your eyes like it's 2050.`;
|
|
65
65
|
}
|
|
66
66
|
const tags = aroTags(this.scene, actor, actor.currentLocation);
|
|
67
|
-
const head = `{
|
|
67
|
+
const head = `{lightblue-fg}The overlay blinks back in.{/lightblue-fg}`;
|
|
68
68
|
// Turning AR back on while silent is the other moment the question
|
|
69
69
|
// lands -- you have just done the thing you were unsure was allowed.
|
|
70
70
|
const note = silentAndSeeingNote(actor);
|
|
71
71
|
if (tags.length === 0) {
|
|
72
|
-
return [head, `{
|
|
72
|
+
return [head, `{lightblue-fg}Nothing here is talking -- a dead patch of street.{/lightblue-fg}`, note].filter(Boolean).join('\n');
|
|
73
73
|
}
|
|
74
|
-
return [head, ...tags.map(t => ` {
|
|
74
|
+
return [head, ...tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`), note].filter(Boolean).join('\n');
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
@@ -105,11 +105,11 @@ export class ArosCommand extends Command {
|
|
|
105
105
|
const note = silentAndSeeingNote(actor);
|
|
106
106
|
const tags = aroTags(this.scene, actor, actor.currentLocation);
|
|
107
107
|
if (tags.length === 0) {
|
|
108
|
-
return [`{
|
|
108
|
+
return [`{lightblue-fg}Nothing here is talking -- a dead patch of street.{/lightblue-fg}`, note].filter(Boolean).join('\n');
|
|
109
109
|
}
|
|
110
110
|
return [
|
|
111
|
-
`{
|
|
112
|
-
...tags.map(t => ` {
|
|
111
|
+
`{lightblue-fg}AR OVERLAY:{/lightblue-fg}`,
|
|
112
|
+
...tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`),
|
|
113
113
|
note,
|
|
114
114
|
].filter(Boolean).join('\n');
|
|
115
115
|
}
|
|
@@ -218,6 +218,9 @@ export class AttackCommand extends Command {
|
|
|
218
218
|
// holstered dry/jammed piece is not what's being attacked with, so it
|
|
219
219
|
// must not block a bare-fisted attack the player never asked to fail.
|
|
220
220
|
const weaponItem = this.actor.equipment.rightHand?.weapon ?? this.actor.equipment.leftHand?.weapon;
|
|
221
|
+
if (this.usesEquippedWeapon() && this.actor.weaponDrawn && weaponItem?.isFirearm() && weaponItem.bricked) {
|
|
222
|
+
return `The ${weaponItem.name} is BRICKED -- its electronics burned out under a Data Spike (p.228). Bench work while you rest brings it back; for now, go in swinging.`;
|
|
223
|
+
}
|
|
221
224
|
if (this.usesEquippedWeapon() && this.actor.weaponDrawn && weaponItem?.isFirearm() && weaponItem.jammed) {
|
|
222
225
|
return `The ${weaponItem.name}'s smartlink is LOCKED -- someone cracked your PAN. "reboot" to clear it, or go in swinging.`;
|
|
223
226
|
}
|
|
@@ -25,7 +25,7 @@ export class DelayCommand extends Command {
|
|
|
25
25
|
return `Too late to delay -- you've already acted this phase (${budget.ledger()}). "end turn" instead.`;
|
|
26
26
|
}
|
|
27
27
|
if (this.scene.isHumanControlled(actor)) {
|
|
28
|
-
this.logger.log(`{
|
|
28
|
+
this.logger.log(`{yellow-fg}You hold your action, watching.{/yellow-fg}`, { actor: actor.name });
|
|
29
29
|
}
|
|
30
30
|
await enc.delayPhase(actor);
|
|
31
31
|
return this.scene.isHumanControlled(actor) ? '' : `${actor.name} holds their action.`;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { hint } from '../utilities/hints.js';
|
|
3
|
+
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
4
|
+
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
|
+
import { hostsInReach, hostOver, gridVicinity, roomsInReach } from '../utilities/grid-reach.js';
|
|
6
|
+
import { hostLabel, matchesCameraName } from '../utilities/grid-names.js';
|
|
7
|
+
import { isWatched, cameraMasterHost } from '../utilities/surveillance.js';
|
|
8
|
+
import { deviceDefensePool, wanDefensePool, hostDefensePool } from '../utilities/matrix-intrusion.js';
|
|
9
|
+
import { accrueOverwatch } from '../utilities/overwatch.js';
|
|
10
|
+
import { leaveMatrix } from '../utilities/planes.js';
|
|
11
|
+
import { applyDeviceOpened } from '../utilities/devices.js';
|
|
12
|
+
import { gridActionModifier, gridNote } from '../utilities/grids.js';
|
|
13
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
14
|
+
import { MAX_MARKS } from '../utilities/marks.js';
|
|
15
|
+
import { sameHostSide } from '../models/player.js';
|
|
16
|
+
/** The marks Control Device wants for a Simple device action (p.238). */
|
|
17
|
+
export const MARKS_TO_CONTROL = 2;
|
|
18
|
+
export class DisableCommand extends Command {
|
|
19
|
+
static verb = 'disable';
|
|
20
|
+
static description = 'Control Device (SR5 p.238): with 2 marks on a device, order it -- a maglock unlocks, a camera cluster loops, a smartgun\'s clip drops. Electronic Warfare + Intuition [Sleaze] v. Device Rating + Firewall. "disable loud <icon>" is a Data Spike instead (see "brick").';
|
|
21
|
+
/** The Data Spike route: `brick` always, `disable loud` by the word. */
|
|
22
|
+
way(args) {
|
|
23
|
+
const rest = [...args];
|
|
24
|
+
if (rest[0]?.toLowerCase() === 'loud' || rest[0]?.toLowerCase() === 'brick') {
|
|
25
|
+
rest.shift();
|
|
26
|
+
return { way: 'spike', rest };
|
|
27
|
+
}
|
|
28
|
+
if (rest[0]?.toLowerCase() === 'quiet')
|
|
29
|
+
rest.shift();
|
|
30
|
+
return { way: 'control', rest };
|
|
31
|
+
}
|
|
32
|
+
async execute(args = []) {
|
|
33
|
+
const actor = this.actor;
|
|
34
|
+
const { way, rest } = this.way(args);
|
|
35
|
+
const named = rest.join(' ').trim();
|
|
36
|
+
if (!named) {
|
|
37
|
+
return way === 'spike'
|
|
38
|
+
? `Brick what? Name the icon: "brick <device>", "brick cameras", "brick <person>" for their gun.`
|
|
39
|
+
: `Disable what? Name the icon: "disable <device>", "disable cameras", "disable <person>" for their gun.${hint(` (Control Device wants ${MARKS_TO_CONTROL} marks on it -- "hack <it>" first.)`)}`;
|
|
40
|
+
}
|
|
41
|
+
// REACH: riding the Matrix, a living persona, or AR with a working
|
|
42
|
+
// deck in hand -- the same gate a PAN intrusion uses (hack.ts).
|
|
43
|
+
if (actor.plane === 'astral')
|
|
44
|
+
return `The Matrix doesn't answer to spirits.`;
|
|
45
|
+
if (actor.plane !== 'matrix' && !actor.isTechnomancer() && !actor.getCyberdeck()) {
|
|
46
|
+
return `You'd need to be jacked in, or have a working cyberdeck in hand, to reach a device's icon.`;
|
|
47
|
+
}
|
|
48
|
+
const target = this.resolve(named);
|
|
49
|
+
if (typeof target === 'string')
|
|
50
|
+
return target;
|
|
51
|
+
const bill = billAction(this.scene, actor, 'complex', way === 'spike' ? 'Data Spike' : 'Control Device', { attack: way === 'spike' });
|
|
52
|
+
if (bill)
|
|
53
|
+
return bill;
|
|
54
|
+
return way === 'spike' ? this.dataSpike(target) : this.controlDevice(target);
|
|
55
|
+
}
|
|
56
|
+
// ------------------------------------------------------------ targets --
|
|
57
|
+
resolve(named) {
|
|
58
|
+
const actor = this.actor;
|
|
59
|
+
const raw = named.toLowerCase();
|
|
60
|
+
const rooms = this.rooms ?? this.scene.getRooms();
|
|
61
|
+
// 1. THE CAMERA CLUSTER of the room the persona is over / inside.
|
|
62
|
+
if (matchesCameraName(named)) {
|
|
63
|
+
const room = actor.insideHost ?? (actor.plane === 'matrix' ? gridVicinity(actor) : actor.currentLocation);
|
|
64
|
+
if (!isWatched(room))
|
|
65
|
+
return `No cameras on ${room.name} -- nothing to loop.`;
|
|
66
|
+
return { kind: 'camera', room, master: cameraMasterHost(rooms, room) };
|
|
67
|
+
}
|
|
68
|
+
// 2. A DEVICE ICON: inside the host, its WAN; from the grid, a slaved
|
|
69
|
+
// device you hold a host mark for, or a loose wireless one where
|
|
70
|
+
// the body stands. Mirrors hack.ts tryDeviceHack.
|
|
71
|
+
const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.name.toLowerCase().includes(raw)
|
|
72
|
+
|| (d.opensExit ?? '').toLowerCase() === raw
|
|
73
|
+
|| d.description.toLowerCase().includes(raw));
|
|
74
|
+
const inside = actor.insideHost;
|
|
75
|
+
if (inside) {
|
|
76
|
+
const matches = matching(inside);
|
|
77
|
+
if (matches.length === 1)
|
|
78
|
+
return { kind: 'device', device: matches[0], room: inside, host: inside, viaWan: false };
|
|
79
|
+
if (matches.length > 1)
|
|
80
|
+
return `More than one thing in here answers to "${named}": ${matches.map(d => d.name).join(', ')}.`;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const found = hostsInReach(rooms, actor).map(r => ({ room: r, matches: matching(r) })).filter(x => x.matches.length === 1);
|
|
84
|
+
if (found.length === 1) {
|
|
85
|
+
const host = found[0].room;
|
|
86
|
+
const device = found[0].matches[0];
|
|
87
|
+
if ((host.hostMarksBy.get(actor.name) ?? 0) < 1) {
|
|
88
|
+
return `${device.name} hangs off ${hostLabel(host)}'s WAN -- slaved, and a host's devices answer nobody who holds no mark on the host itself (p.233).${hint(` ("hack ${hostLabel(host)}" for a mark first.)`)}`;
|
|
89
|
+
}
|
|
90
|
+
return { kind: 'device', device, room: host, host, viaWan: true };
|
|
91
|
+
}
|
|
92
|
+
const here = actor.plane === 'matrix' ? gridVicinity(actor) : actor.currentLocation;
|
|
93
|
+
const loose = hostOver(here) ? [] : matching(here).filter(d => d.wireless);
|
|
94
|
+
if (loose.length === 1)
|
|
95
|
+
return { kind: 'device', device: loose[0], room: here, viaWan: false };
|
|
96
|
+
}
|
|
97
|
+
// 3. A PERSON'S GUN, broken out of their PAN (p.219). Same reach as
|
|
98
|
+
// a PAN intrusion: bodies on this side of the host wall.
|
|
99
|
+
const reach = (actor.plane === 'matrix' && !actor.insideHost)
|
|
100
|
+
? roomsInReach(rooms, actor)
|
|
101
|
+
: [{ room: actor.currentLocation, noise: 0 }];
|
|
102
|
+
const bodies = reach.flatMap(({ room: r }) => this.scene.getActorsInRoom(r)
|
|
103
|
+
.filter(a => a !== actor && a.plane === 'meat' && !a.isIncapacitated() && sameHostSide(actor, a)));
|
|
104
|
+
const picked = fuzzyPickName(named, bodies.map(a => a.name));
|
|
105
|
+
const owner = picked ? bodies.find(a => a.name === picked) : undefined;
|
|
106
|
+
if (owner) {
|
|
107
|
+
const gun = owner.equipment.rightHand?.weapon ?? owner.equipment.leftHand?.weapon;
|
|
108
|
+
if (!gun || !gun.isFirearm())
|
|
109
|
+
return `${owner.name} has no firearm on the grid to work -- nothing wireless in their hands.`;
|
|
110
|
+
return { kind: 'gun', owner, gun };
|
|
111
|
+
}
|
|
112
|
+
return `Nothing on the grid answers to "${named}".${hint(` ("look" lists the icons in reach.)`)}`;
|
|
113
|
+
}
|
|
114
|
+
marksOn(t) {
|
|
115
|
+
const me = this.actor.name;
|
|
116
|
+
if (t.kind === 'device')
|
|
117
|
+
return t.device.marksBy.get(me) ?? 0;
|
|
118
|
+
if (t.kind === 'camera')
|
|
119
|
+
return t.master?.hostMarksBy.get(me) ?? 0;
|
|
120
|
+
return t.owner.panMarksBy.get(me) ?? 0;
|
|
121
|
+
}
|
|
122
|
+
nameOf(t) {
|
|
123
|
+
return t.kind === 'device' ? t.device.name : t.kind === 'camera' ? `the camera cluster on ${t.room.name}` : `${t.owner.name}'s ${t.gun.name}`;
|
|
124
|
+
}
|
|
125
|
+
/** What defends: DR + Firewall for an unattended device (the master's
|
|
126
|
+
* where higher, seen from the grid), the host for its cameras, the
|
|
127
|
+
* owner's Intuition + Firewall for a gun on their PAN. */
|
|
128
|
+
defence(t) {
|
|
129
|
+
if (t.kind === 'device') {
|
|
130
|
+
return t.viaWan && t.host?.host ? wanDefensePool(t.device, t.host.host) : deviceDefensePool(t.device);
|
|
131
|
+
}
|
|
132
|
+
if (t.kind === 'camera') {
|
|
133
|
+
return t.master?.host ? hostDefensePool(t.master.host) : { label: 'Device Rating + Firewall', pool: 4 };
|
|
134
|
+
}
|
|
135
|
+
return { label: 'Intuition + Firewall', pool: t.owner.getPanDefensePool('sleaze') };
|
|
136
|
+
}
|
|
137
|
+
godBill(defenseHits, why) {
|
|
138
|
+
const bill = accrueOverwatch(this.scene, this.actor, defenseHits, why);
|
|
139
|
+
if (bill.converged && this.actor.plane === 'matrix') {
|
|
140
|
+
bill.lines.push(...leaveMatrix(this.scene, this.actor, { forced: true, reason: `GOD force-reboots your persona --` }));
|
|
141
|
+
}
|
|
142
|
+
return bill.lines;
|
|
143
|
+
}
|
|
144
|
+
/** The owner of the thing learns something went wrong. */
|
|
145
|
+
wakeOwner(t, made) {
|
|
146
|
+
const actor = this.actor;
|
|
147
|
+
actor.sneaking = false;
|
|
148
|
+
const host = t.kind === 'device' ? t.host : t.kind === 'camera' ? t.master : undefined;
|
|
149
|
+
if (host) {
|
|
150
|
+
host.hostAlert = true;
|
|
151
|
+
if (made)
|
|
152
|
+
host.hostMarksOn.set(actor.name, Math.min(MAX_MARKS, (host.hostMarksOn.get(actor.name) ?? 0) + 1));
|
|
153
|
+
}
|
|
154
|
+
if (t.kind === 'gun' && made) {
|
|
155
|
+
actor.panMarksBy.set(t.owner.name, Math.min(MAX_MARKS, (actor.panMarksBy.get(t.owner.name) ?? 0) + 1));
|
|
156
|
+
t.owner.performAction('notices a Matrix probe', 'their link flashing an intrusion warning');
|
|
157
|
+
}
|
|
158
|
+
this.scene.updateStatus();
|
|
159
|
+
}
|
|
160
|
+
// ------------------------------------------------------ Control Device --
|
|
161
|
+
controlDevice(t) {
|
|
162
|
+
const actor = this.actor;
|
|
163
|
+
const name = this.nameOf(t);
|
|
164
|
+
const marks = this.marksOn(t);
|
|
165
|
+
if (marks < MARKS_TO_CONTROL) {
|
|
166
|
+
const how = t.kind === 'device' ? `"hack ${t.device.name}"` : t.kind === 'camera' ? `"hack ${t.master ? hostLabel(t.master) : 'the host'}"` : `"hack ${t.owner.name}"`;
|
|
167
|
+
return `${name} takes orders from ${MARKS_TO_CONTROL} marks and you hold ${marks} (Control Device, p.238: a Simple device action wants two).${hint(` (${how} places them; or "brick" it -- no marks, but loud, and a bricked lock stays locked.)`)}`;
|
|
168
|
+
}
|
|
169
|
+
const burned = actor.burnedAttributeRefusal('sleaze');
|
|
170
|
+
if (burned)
|
|
171
|
+
return burned;
|
|
172
|
+
// ELECTRONIC WARFARE + INTUITION [SLEAZE] v. Intuition + Firewall
|
|
173
|
+
// (p.238), the no-test device action's test. Unskilled at -1 (p.130).
|
|
174
|
+
const ew = actor.skillRating('electronic-warfare');
|
|
175
|
+
const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, t.kind === 'device' ? t.host?.host?.grid : t.kind === 'camera' ? t.master?.host?.grid : undefined);
|
|
176
|
+
const pool = Math.max(1, actor.intuition + (ew > 0 ? ew : -1) + actor.bonus('hacking') + actor.bonus('matrix-sleaze')
|
|
177
|
+
+ actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty + grid.total);
|
|
178
|
+
const limit = actor.matrixAttribute('sleaze');
|
|
179
|
+
const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
180
|
+
const defence = this.defence(t);
|
|
181
|
+
const defenceRoll = rollPool(defence.pool);
|
|
182
|
+
const net = roll.hits - defenceRoll.hits;
|
|
183
|
+
actor.performAction('takes control of a device', name, { quiet: this.scene.isHumanControlled(actor) });
|
|
184
|
+
if (this.scene.isHumanControlled(actor)) {
|
|
185
|
+
this.logger.meta(`Control Device (Electronic Warfare + Intuition [Sleaze])${gridNote(grid)}: ${formatRoll(roll)}`);
|
|
186
|
+
this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
|
|
187
|
+
}
|
|
188
|
+
const godLines = this.godBill(defenceRoll.hits, 'Control Device (sleaze)');
|
|
189
|
+
if (net <= 0) {
|
|
190
|
+
// A blown Sleaze is noticed (p.236): the owner marks you and the
|
|
191
|
+
// host it hangs off is awake.
|
|
192
|
+
this.wakeOwner(t, true);
|
|
193
|
+
const who = t.kind === 'gun' ? `${t.owner.name}'s firewall holds a MARK on your persona now` : `whoever owns it has your icon on file now`;
|
|
194
|
+
return [`${name} refuses the order and SAYS SO -- ${who}.`, ...godLines].join('\n');
|
|
195
|
+
}
|
|
196
|
+
const lines = [];
|
|
197
|
+
if (t.kind === 'device') {
|
|
198
|
+
// THE ORDER: it opens. Same aftermath as a key or a forced door
|
|
199
|
+
// (utilities/devices.ts) -- the exit unseals in the meat.
|
|
200
|
+
const opened = t.device.force(true);
|
|
201
|
+
lines.push(`${t.device.name} takes the order as its owner's: ${opened.message}`);
|
|
202
|
+
lines.push(...applyDeviceOpened(this.scene, actor, t.room, t.device, 'disable'));
|
|
203
|
+
}
|
|
204
|
+
else if (t.kind === 'camera') {
|
|
205
|
+
t.room.camerasLooped = true;
|
|
206
|
+
lines.push(`The cluster on ${t.room.name} takes the order: every lens loops on an empty hallway. Security sees NOTHING there now.${hint(` ("snoop ${t.room.name}" rides the real feed.)`)}`);
|
|
207
|
+
this.scene.addWorldEvent(`An unseen hand looped the cameras on ${t.room.name}.`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
t.gun.jammed = true;
|
|
211
|
+
lines.push(`${t.owner.name}'s ${t.gun.name} takes the order: the smartlink seizes and the clip drops -- LOCKED until they reboot.`);
|
|
212
|
+
t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} locking up in their hand`);
|
|
213
|
+
this.scene.addWorldEvent(`An unseen hand seized ${t.owner.name}'s ${t.gun.name}.`);
|
|
214
|
+
}
|
|
215
|
+
this.scene.updateStatus();
|
|
216
|
+
return [...lines, ...godLines].join('\n');
|
|
217
|
+
}
|
|
218
|
+
// ----------------------------------------------------------- Data Spike --
|
|
219
|
+
dataSpike(t) {
|
|
220
|
+
const actor = this.actor;
|
|
221
|
+
const name = this.nameOf(t);
|
|
222
|
+
const burned = actor.burnedAttributeRefusal('attack');
|
|
223
|
+
if (burned)
|
|
224
|
+
return burned;
|
|
225
|
+
if (t.kind === 'device' && t.device.isBricked)
|
|
226
|
+
return `${t.device.name} is already bricked -- dead boards, nothing left to spike.`;
|
|
227
|
+
if (t.kind === 'camera' && t.room.camerasBricked)
|
|
228
|
+
return `The cluster on ${t.room.name} is already dead glass.`;
|
|
229
|
+
if (t.kind === 'gun' && t.gun.bricked)
|
|
230
|
+
return `${t.owner.name}'s ${t.gun.name} is already bricked.`;
|
|
231
|
+
// CYBERCOMBAT + LOGIC [ATTACK] v. Intuition + Firewall (p.239).
|
|
232
|
+
const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, t.kind === 'device' ? t.host?.host?.grid : t.kind === 'camera' ? t.master?.host?.grid : undefined);
|
|
233
|
+
const pool = Math.max(1, actor.getAttackPool() + actor.matrixActionPenalty + actor.bonus('matrix-attack') + grid.total);
|
|
234
|
+
const limit = actor.matrixAttribute('attack');
|
|
235
|
+
const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
236
|
+
const defence = this.defence(t);
|
|
237
|
+
const defenceRoll = rollPool(defence.pool);
|
|
238
|
+
const net = roll.hits - defenceRoll.hits;
|
|
239
|
+
actor.performAction('throws a Data Spike', name, { quiet: this.scene.isHumanControlled(actor) });
|
|
240
|
+
if (this.scene.isHumanControlled(actor)) {
|
|
241
|
+
this.logger.meta(`Data Spike (Cybercombat + Logic [Attack ${limit}])${gridNote(grid)}: ${formatRoll(roll)}`);
|
|
242
|
+
this.logger.meta(`${defence.label}: ${formatRoll(defenceRoll)} -- net ${net >= 0 ? '+' : ''}${net}`);
|
|
243
|
+
}
|
|
244
|
+
const godLines = this.godBill(defenceRoll.hits, 'Data Spike (attack)');
|
|
245
|
+
if (net <= 0) {
|
|
246
|
+
// A failed Attack action is never noticed (p.236), but the
|
|
247
|
+
// target's software rejects the code and sends it back: one
|
|
248
|
+
// unresistable box per net hit the defender got (Data Trails p.181).
|
|
249
|
+
const rebound = Math.max(0, -net);
|
|
250
|
+
const lines = [`${name} shrugs the spike off${rebound > 0 ? ' -- and sends it back' : ''}.`];
|
|
251
|
+
if (rebound > 0) {
|
|
252
|
+
if (actor.isTechnomancer()) {
|
|
253
|
+
actor.takeStun(rebound);
|
|
254
|
+
lines.push(` Rejected code burns back through your living persona -- ${rebound} stun: ${actor.stunSummary()}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
actor.takeMatrixDamage(rebound);
|
|
258
|
+
lines.push(` Rejected code burns back into your deck -- ${rebound} box${rebound === 1 ? '' : 'es'}, unresisted: ${actor.matrixSummary()}`);
|
|
259
|
+
if (actor.matrixCrashed)
|
|
260
|
+
lines.push(...leaveMatrix(this.scene, actor, { forced: true, reason: `Your own spike bricks the deck under you.` }));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
lines.push(` Nobody upstairs noticed -- a spike that doesn't land leaves no trace.`);
|
|
264
|
+
return [...lines, ...godLines].join('\n');
|
|
265
|
+
}
|
|
266
|
+
// DV = Attack + net hits, +2 per mark held (p.239); resisted with
|
|
267
|
+
// Device Rating + Firewall (p.228).
|
|
268
|
+
const marks = this.marksOn(t);
|
|
269
|
+
const dv = limit + net + 2 * marks;
|
|
270
|
+
const soak = rollPool(this.defence(t).pool);
|
|
271
|
+
const dealt = Math.max(0, dv - soak.hits);
|
|
272
|
+
if (this.scene.isHumanControlled(actor)) {
|
|
273
|
+
this.logger.meta(`Data Spike -- ${dv} DV (Attack ${limit} +${net} net${marks > 0 ? ` +${2 * marks} for ${marks} mark${marks === 1 ? '' : 's'}` : ''}) v. ${defence.label}: ${formatRoll(soak)} -> ${dealt}`);
|
|
274
|
+
}
|
|
275
|
+
// An Attack action is never subtle: the owner knows.
|
|
276
|
+
this.wakeOwner(t, false);
|
|
277
|
+
const lines = [];
|
|
278
|
+
if (dealt <= 0) {
|
|
279
|
+
lines.push(`${name} takes the spike and its firewall holds -- barely. It knows it was hit.`);
|
|
280
|
+
return [...lines, ...godLines].join('\n');
|
|
281
|
+
}
|
|
282
|
+
if (t.kind === 'device') {
|
|
283
|
+
t.device.takeMatrixDamage(dealt);
|
|
284
|
+
lines.push(`The spike lands on ${t.device.name} -- ${dealt} box${dealt === 1 ? '' : 'es'} (${t.device.matrixSummary()}).`);
|
|
285
|
+
if (t.device.isBricked) {
|
|
286
|
+
lines.push(t.device.kind === 'lock'
|
|
287
|
+
? ` ${t.device.name} BRICKS -- sparks, a pop, a smell of hot plastic. And the lock STAYS LOCKED (p.228): a dead maglock holds like a bolt.${hint(` (Marks and "disable" would have opened it; now it is "pick" or "breach".)`)}`
|
|
288
|
+
: ` ${t.device.name} BRICKS -- sparks, a pop, dead electronics. Whatever it was doing, it has stopped.`);
|
|
289
|
+
this.scene.addWorldEvent(`${actor.name} bricked ${t.device.name} in ${t.room.name} with a Data Spike.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (t.kind === 'camera') {
|
|
293
|
+
// The cluster has no Device of its own to hold a monitor; a hit
|
|
294
|
+
// that lands past the firewall kills it. Engine ruling.
|
|
295
|
+
t.room.camerasBricked = true;
|
|
296
|
+
lines.push(`The spike lands on the cluster -- ${dealt} box${dealt === 1 ? '' : 'es'} -- and every lens on ${t.room.name} goes to static and stays there. Dead glass.`);
|
|
297
|
+
this.scene.addWorldEvent(`${actor.name} bricked the cameras on ${t.room.name} with a Data Spike.`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// A gun is a Rating 2 device with a monitor of 9 (p.228, the
|
|
301
|
+
// book's own smartgun example): a spike that lands past its
|
|
302
|
+
// firewall for that much bricks it outright.
|
|
303
|
+
const monitor = 8 + 1;
|
|
304
|
+
if (dealt >= monitor) {
|
|
305
|
+
t.gun.bricked = true;
|
|
306
|
+
lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- it sparks, crackles and smokes in their hand. BRICKED: it will not fire again until somebody's bench work.`);
|
|
307
|
+
t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} smoking in their hand`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
t.gun.jammed = true;
|
|
311
|
+
lines.push(`The spike lands on ${t.owner.name}'s ${t.gun.name} -- ${dealt} box${dealt === 1 ? '' : 'es'}: the smartlink seizes and LOCKS. It boots again on a reboot; another spike like that bricks it.`);
|
|
312
|
+
t.owner.performAction('suffers a PAN crack', `their ${t.gun.name} locking up in their hand`);
|
|
313
|
+
}
|
|
314
|
+
this.scene.addWorldEvent(`${actor.name} spiked ${t.owner.name}'s ${t.gun.name}.`);
|
|
315
|
+
}
|
|
316
|
+
this.scene.updateStatus();
|
|
317
|
+
return [...lines, ...godLines].join('\n');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** `brick <icon>`: the Data Spike, by its street name. */
|
|
321
|
+
export class BrickCommand extends DisableCommand {
|
|
322
|
+
static verb = 'brick';
|
|
323
|
+
static description = 'Data Spike (SR5 p.239): Cybercombat + Logic [Attack] v. Device Rating + Firewall, no marks needed, never subtle. Fill a device\'s Matrix condition monitor (8 + DR/2, p.228) and it BRICKS -- a camera goes dead, a smartgun sparks and dies; a lock stays locked. A miss burns your own deck.';
|
|
324
|
+
way(args) {
|
|
325
|
+
const rest = [...args];
|
|
326
|
+
if (rest[0]?.toLowerCase() === 'loud')
|
|
327
|
+
rest.shift();
|
|
328
|
+
return { way: 'spike', rest };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=disable.js.map
|
|
@@ -92,7 +92,7 @@ export class DownloadCommand extends Command {
|
|
|
92
92
|
// is that nobody could find the file should say what it can see.
|
|
93
93
|
const query = args.join(' ').trim();
|
|
94
94
|
if (query.length === 0) {
|
|
95
|
-
const lines = [`{
|
|
95
|
+
const lines = [`{lightblue-fg}Within reach to pull down:{/lightblue-fg}`];
|
|
96
96
|
for (const i of loose)
|
|
97
97
|
lines.push(` ${planeTintItemName(i, actor.plane)} -- loose on the grid here`);
|
|
98
98
|
for (const c of carried)
|
|
@@ -134,11 +134,11 @@ export class DownloadCommand extends Command {
|
|
|
134
134
|
this.scene.updateStatus();
|
|
135
135
|
return fromSprite
|
|
136
136
|
? [
|
|
137
|
-
`{
|
|
137
|
+
`{lightblue-fg}${fromSprite.npc.name} opens its hands and the data pours out of them.{/lightblue-fg}`,
|
|
138
138
|
`You pull {bold}{underline}${item.name}{/underline}{/bold} down onto a chip -- meat-world cargo now.${hint(` (You can "give ${item.name}" to somebody standing in front of you.)`)}`,
|
|
139
139
|
].join('\n')
|
|
140
140
|
: [
|
|
141
|
-
`{
|
|
141
|
+
`{lightblue-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down off the grid -- written to a chip.{/lightblue-fg}`,
|
|
142
142
|
hint(`(Meat-world cargo now: carry it, stash it, or "give" it to somebody.)`),
|
|
143
143
|
].join('\n');
|
|
144
144
|
}
|
|
@@ -49,7 +49,7 @@ export class EndTurnCommand extends Command {
|
|
|
49
49
|
// Logged now rather than returned: the phases that follow announce
|
|
50
50
|
// themselves while this command is still running, and a return
|
|
51
51
|
// value would land under them, out of order.
|
|
52
|
-
this.logger.log(`{
|
|
52
|
+
this.logger.log(`{yellow-fg}You end your Action Phase${budget && budget.spent.length > 0 ? ` -- ${ledger}` : ' -- nothing spent'}.{/yellow-fg}`, { actor: actor.name });
|
|
53
53
|
}
|
|
54
54
|
await enc.endPhase(actor);
|
|
55
55
|
return this.scene.isHumanControlled(actor) ? '' : `${actor.name} ends their Action Phase.`;
|
|
@@ -64,7 +64,7 @@ export class EnterHostCommand extends Command {
|
|
|
64
64
|
const marks = target.hostMarksBy.get(actor.name) ?? 0;
|
|
65
65
|
if (marks < 1 && !target.hostCracked && !target.hostSanctioned) {
|
|
66
66
|
return [
|
|
67
|
-
`{
|
|
67
|
+
`{lightblue-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/lightblue-fg}`,
|
|
68
68
|
hint(`A host opens to anyone holding a MARK on it (p.239) -- "mark ${target.name}" softens it, or "hack" takes one by force.`),
|
|
69
69
|
].join('\n');
|
|
70
70
|
}
|
|
@@ -79,14 +79,15 @@ export class EnterHostCommand extends Command {
|
|
|
79
79
|
// you there). currentLocation is never written -- it is the body's.
|
|
80
80
|
actor.matrixPosition = { kind: 'host', host: target.host, from: actor.currentGrid };
|
|
81
81
|
movePersonaWith(this.game ?? undefined, actor);
|
|
82
|
-
actor.performAction('enters a host', `icon crossing into ${target.name}'s architecture
|
|
82
|
+
actor.performAction('enters a host', `icon crossing into ${target.name}'s architecture`, { quiet: this.scene.isHumanControlled(actor) });
|
|
83
83
|
this.logger.write(`${actor.name} entered the host at ${target.name}.`);
|
|
84
84
|
this.scene.updateStatus();
|
|
85
|
+
// ONE LINE (2026-09-13, "too much text"): the crossing, and the
|
|
86
|
+
// coaching behind the hints toggle.
|
|
85
87
|
return [
|
|
86
|
-
`{
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
].join('\n');
|
|
88
|
+
`{lightblue-fg}You cross into ${hostLabel(target, { capital: false })} -- the outside grid folds shut behind you.{/lightblue-fg}`,
|
|
89
|
+
hint(`(In here there is only what the host keeps. "look" reads it; "exit" puts you back on the grid.)`),
|
|
90
|
+
].filter(Boolean).join('\n');
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
93
|
* The host this verb means. Bare `enter` takes the one under your feet
|
|
@@ -109,14 +110,14 @@ export class EnterHostCommand extends Command {
|
|
|
109
110
|
const over = hostOver(gridVicinity(this.actor)) ?? hostOver(here);
|
|
110
111
|
if (over)
|
|
111
112
|
return over;
|
|
112
|
-
return `{
|
|
113
|
+
return `{lightblue-fg}Thin grid here -- no host stands over ${gridVicinity(this.actor).name}.{/lightblue-fg} ${hint(`Overhead: ${roster} -- "enter <name>" crosses into any of them from here.`)}`;
|
|
113
114
|
}
|
|
114
115
|
const host = resolveHostInReach(rooms, this.actor, named);
|
|
115
116
|
if (host)
|
|
116
117
|
return host;
|
|
117
118
|
const street = Object.values(rooms).find(r => r.name.toLowerCase() === named.toLowerCase());
|
|
118
119
|
if (street)
|
|
119
|
-
return `{
|
|
120
|
+
return `{lightblue-fg}${street.name} has no host over it -- nothing to enter. Overhead: ${roster}.{/lightblue-fg}`;
|
|
120
121
|
return `No host called "${named}" overhead. ${hint(`Overhead: ${roster}.`)}`;
|
|
121
122
|
}
|
|
122
123
|
}
|
|
@@ -148,13 +149,10 @@ export class ExitHostCommand extends Command {
|
|
|
148
149
|
const from = actor.matrixPosition?.kind === 'host' ? actor.matrixPosition.from : undefined;
|
|
149
150
|
actor.matrixPosition = { kind: 'grid', grid: from };
|
|
150
151
|
movePersonaWith(this.game ?? undefined, actor);
|
|
151
|
-
actor.performAction('exits a host', `icon surfacing out of ${host.name}'s architecture
|
|
152
|
+
actor.performAction('exits a host', `icon surfacing out of ${host.name}'s architecture`, { quiet: this.scene.isHumanControlled(actor) });
|
|
152
153
|
this.logger.write(`${actor.name} exited the host at ${host.name}.`);
|
|
153
154
|
this.scene.updateStatus();
|
|
154
|
-
return
|
|
155
|
-
`{cyan-fg}You step back out of ${hostLabel(host, { capital: false })} and onto the grid -- your body still sits in ${actor.currentLocation.name}.{/cyan-fg}`,
|
|
156
|
-
`{cyan-fg}The host seals behind you: one icon again, whatever you saw in there now on the other side of its wall.{/cyan-fg}`,
|
|
157
|
-
].join('\n');
|
|
155
|
+
return `{lightblue-fg}You step back out of ${hostLabel(host, { capital: false })} onto the grid; the host seals behind you. Your body still sits in ${actor.currentLocation.name}.{/lightblue-fg}`;
|
|
158
156
|
}
|
|
159
157
|
}
|
|
160
158
|
//# sourceMappingURL=enter-host.js.map
|
|
@@ -173,7 +173,7 @@ export class HackCommand extends BypassCommand {
|
|
|
173
173
|
const burned = this.actor.burnedAttributeRefusal(mode === 'attack' ? 'attack' : 'sleaze');
|
|
174
174
|
if (burned)
|
|
175
175
|
return burned;
|
|
176
|
-
this.actor.performAction('hacks the host', room.name);
|
|
176
|
+
this.actor.performAction('hacks the host', room.name, { quiet: this.scene.isHumanControlled(this.actor) });
|
|
177
177
|
const attempt = await this.hostIntrusionRoll(mode, declared, room);
|
|
178
178
|
const success = attempt.success;
|
|
179
179
|
// GOD BILLS THE DEFENDER'S HITS (p.231-232): "your OS increases by
|
|
@@ -276,7 +276,7 @@ export class HackCommand extends BypassCommand {
|
|
|
276
276
|
if (marksNow >= 1)
|
|
277
277
|
lines.push(` One mark is the door: "enter" walks your persona inside (p.239).`);
|
|
278
278
|
if (perception > 0)
|
|
279
|
-
lines.push(` {
|
|
279
|
+
lines.push(` {lightblue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/lightblue-fg}`);
|
|
280
280
|
if (forcedDv > 0)
|
|
281
281
|
lines.push(` The forcing burns ${forcedDv} DV into the architecture -- a host has no boards to brick, so it only announces you louder.`);
|
|
282
282
|
lines.push(` ${MAX_MARKS - marksNow} more and its architecture stops asking who you are.${hint(` ("hack for ${MAX_MARKS - marksNow}" reaches for the rest at once -- ${declarationPenalty(MAX_MARKS - marksNow, this.actor.hasQuality(GO_BIG_QUALITY))} dice.)`)}`);
|
|
@@ -340,13 +340,13 @@ export class HackCommand extends BypassCommand {
|
|
|
340
340
|
lines.push(` ${iconLocks.length} ${iconLocks.length === 1 ? 'lock hangs' : 'locks hang'} off it, still sealed -- each its own icon behind its own rating: ${iconLocks.map(d => d.name).join(', ')}.${hint(` ("hack <lock>" from in here keys one; the host's key is not the door's.)`)}`);
|
|
341
341
|
}
|
|
342
342
|
if (vaultedFiles.length > 0) {
|
|
343
|
-
lines.push(` {
|
|
343
|
+
lines.push(` {lightblue-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/lightblue-fg}`);
|
|
344
344
|
}
|
|
345
345
|
// AND WHAT THE CRACK DID NOT REACH: a wired site's data sits on an
|
|
346
346
|
// air-gapped box the host never held (p.233) -- say so, or the next
|
|
347
347
|
// "take" reads as a contradiction.
|
|
348
348
|
if (room.offlineServer && room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
|
|
349
|
-
lines.push(` {
|
|
349
|
+
lines.push(` {lightblue-fg}Nothing on the grid side of the vault -- whatever this place keeps sits on the ${room.offlineServer}, an air-gapped box with no Matrix presence. Your BODY has to be in ${room.name} to cable in (p.233).{/lightblue-fg}`);
|
|
350
350
|
}
|
|
351
351
|
// THE FEEDS ARE A REAL GAIN AND WERE NEVER MENTIONED
|
|
352
352
|
// (BpR5cjhYuvgCyDSZc, 7aNWnDSHeZMYmS2De: "what have I gained?").
|
|
@@ -540,7 +540,7 @@ export class HackCommand extends BypassCommand {
|
|
|
540
540
|
? Math.max(1, actor.getAttackPool() + silence + actor.bonus('matrix-attack') + declarePenalty)
|
|
541
541
|
: Math.max(1, actor.logic + actor.skillRating('hacking') + actor.bonus('hacking') + actor.bonus('matrix-sleaze')
|
|
542
542
|
+ silence + actor.woundModifier - actor.sustainingPenalty + declarePenalty);
|
|
543
|
-
actor.performAction('works a slaved icon', device.name);
|
|
543
|
+
actor.performAction('works a slaved icon', device.name, { quiet: this.scene.isHumanControlled(actor) });
|
|
544
544
|
const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
545
545
|
// From the grid the slave borrows its master's numbers where they are
|
|
546
546
|
// higher (p.233); inside, it stands alone (Data Trails p.87-88).
|
|
@@ -606,18 +606,18 @@ export class HackCommand extends BypassCommand {
|
|
|
606
606
|
else {
|
|
607
607
|
const perception = freeMatrixPerceptionHits(netHits);
|
|
608
608
|
if (perception > 0)
|
|
609
|
-
lines.push(` {
|
|
609
|
+
lines.push(` {lightblue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what it guards (p.240).{/lightblue-fg}`);
|
|
610
610
|
}
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
611
|
+
// TWO MARKS COMMAND IT (p.238 Control Device: 1 mark for a Free
|
|
612
|
+
// action, 2 for a Simple, 3 for a Complex -- and unlocking a maglock
|
|
613
|
+
// is the book's own example of the no-test device action, a Simple
|
|
614
|
+
// "Use Simple Device", p.165). The verb is `disable`
|
|
615
|
+
// (commands/disable.ts): the key is turned from in here, the door
|
|
616
|
+
// swings out there. One mark short says so.
|
|
617
|
+
if (!device.isOpen()) {
|
|
618
|
+
lines.push(now >= 2
|
|
619
|
+
? ` That is enough to command it: ${hint(`"disable ${device.name}" (Control Device, p.238) ${device.opensExit ? `opens the ${device.opensExit} way` : `works it`} from right here.`)}`
|
|
620
|
+
: ` One more mark and it takes orders.${hint(` ("hack ${device.name}" again, then "disable ${device.name}" -- Control Device wants 2 marks for a Simple device action, p.238.)`)}`);
|
|
621
621
|
}
|
|
622
622
|
this.scene.updateStatus();
|
|
623
623
|
return [...lines, ...godLines].join('\n');
|
|
@@ -765,7 +765,7 @@ export class HackCommand extends BypassCommand {
|
|
|
765
765
|
// Intuition + Firewall against a sleaze, Willpower + Firewall
|
|
766
766
|
// against a smash (p.240 / p.238), and no limit on either.
|
|
767
767
|
const defense = rollPool(target.getPanDefensePool(mode === 'attack' ? 'attack' : 'sleaze'));
|
|
768
|
-
actor.performAction('runs a PAN intrusion', target.name);
|
|
768
|
+
actor.performAction('runs a PAN intrusion', target.name, { quiet: this.scene.isHumanControlled(actor) });
|
|
769
769
|
this.logger.write(`${actor.name} PAN-hacks ${target.name}: ${attack.hits} vs ${defense.hits} (silent=${target.runningSilent}).`);
|
|
770
770
|
// Dice anatomy to the Mechanics ticker (readability pass) --
|
|
771
771
|
// player-gated like every roll site: an NPC's probe isn't your dice.
|
|
@@ -53,7 +53,7 @@ export class HopCommand extends Command {
|
|
|
53
53
|
actor.performAction('hops grids', `persona blinking across to ${target.name}`);
|
|
54
54
|
this.logger.write(`${actor.name} hopped to ${target.key} (legal, ${tier}).`);
|
|
55
55
|
this.scene.updateStatus();
|
|
56
|
-
return `{
|
|
56
|
+
return `{lightblue-fg}The sky changes texture and you're on ${target.name}.{/lightblue-fg}${target.userPenalty ? ` ${hint(`Public grid: -2 on everything you do out here.`)}` : ''}`;
|
|
57
57
|
}
|
|
58
58
|
// ILLEGAL HOP (p.240): Hack on the Fly against the grid itself.
|
|
59
59
|
const burned = actor.burnedAttributeRefusal('sleaze');
|
|
@@ -87,7 +87,7 @@ export class HopCommand extends Command {
|
|
|
87
87
|
this.logger.write(`${actor.name} hopped to ${target.key} (illegal, ${attempt.hits} v ${defence.hits}).`);
|
|
88
88
|
this.scene.updateStatus();
|
|
89
89
|
return [
|
|
90
|
-
`{
|
|
90
|
+
`{lightblue-fg}You slip through a seam nobody sold you and come up on ${target.name}.{/lightblue-fg}`,
|
|
91
91
|
...bill.lines,
|
|
92
92
|
].join('\n');
|
|
93
93
|
}
|