@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
|
@@ -379,7 +379,7 @@ export class LookCommand extends Command {
|
|
|
379
379
|
const dim = this.scene.getActorsInRoom(far)
|
|
380
380
|
.find(a => a !== this.actor && a.name.toLowerCase() === target.toLowerCase() && this.actor.canPerceive(a));
|
|
381
381
|
if (dim)
|
|
382
|
-
return `${this.inspectPersona(dim)}\n{
|
|
382
|
+
return `${this.inspectPersona(dim)}\n{lightblue-fg} -- a far icon, ${far.name}${noiseNote(noise)}{/lightblue-fg}`;
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
// 3. THE HOST, as a thing you can name. gridIcons already renders it
|
|
@@ -396,8 +396,8 @@ export class LookCommand extends Command {
|
|
|
396
396
|
// `deck` reads out your own array.
|
|
397
397
|
const a = namedHost.hostAttributes();
|
|
398
398
|
return [
|
|
399
|
-
`{
|
|
400
|
-
`{
|
|
399
|
+
`{lightblue-fg}${hostIcon}{/lightblue-fg}`,
|
|
400
|
+
`{lightblue-fg} Attack ${a.attack} Sleaze ${a.sleaze} Data Processing ${a.dataProcessing} Firewall ${a.firewall}{/lightblue-fg}`,
|
|
401
401
|
].join('\n');
|
|
402
402
|
}
|
|
403
403
|
}
|
|
@@ -409,7 +409,7 @@ export class LookCommand extends Command {
|
|
|
409
409
|
if (matchesCameraName(target)) {
|
|
410
410
|
const eye = gridIcons(this.scene, this.actor, room).find(i => i.startsWith('◎'));
|
|
411
411
|
if (eye)
|
|
412
|
-
return `{
|
|
412
|
+
return `{lightblue-fg}${eye}{/lightblue-fg}`;
|
|
413
413
|
// Named correctly, and there is simply nothing here to name.
|
|
414
414
|
// Worth its own line: the old answer said the WORDS were wrong,
|
|
415
415
|
// which sent the player hunting for a synonym that never
|
|
@@ -525,7 +525,7 @@ export class LookCommand extends Command {
|
|
|
525
525
|
* there's a deck anywhere in the shape of it.
|
|
526
526
|
*/
|
|
527
527
|
inspectPersona(target) {
|
|
528
|
-
const lines = [`{
|
|
528
|
+
const lines = [`{lightblue-fg}◆ ${target.name} -- a persona on this grid.{/lightblue-fg}`];
|
|
529
529
|
const companion = this.scene.ownerGame?.companions.find(c => c.npc === target);
|
|
530
530
|
if (companion) {
|
|
531
531
|
lines.push(`Yours -- it goes where you go, and it answers when you speak.`);
|
|
@@ -93,7 +93,7 @@ export class MapCommand extends Command {
|
|
|
93
93
|
actor.insideHost
|
|
94
94
|
? `THE GRID -- INSIDE ${hostLabel(actor.insideHost, { capital: true }).toUpperCase()}`
|
|
95
95
|
: `THE GRID -- ${actor.currentGrid ? `on ${actor.currentGrid.name}, ` : ''}jacked in from ${body.name}`,
|
|
96
|
-
`{
|
|
96
|
+
`{lightblue-fg}Black flatland under a black sky. No streets out here -- only icons. The hosts hang overhead; ${actor.insideHost ? '"exit" puts you back under them' : '"go <host>" (or "go <number>") crosses into one you hold a mark on, from anywhere'}.{/lightblue-fg}`,
|
|
97
97
|
];
|
|
98
98
|
if (hosts.length === 0) {
|
|
99
99
|
lines.push(' Nothing overhead -- no host stands over this district.');
|
|
@@ -51,7 +51,7 @@ export class MarkCommand extends Command {
|
|
|
51
51
|
screen: this.screen,
|
|
52
52
|
game: this.game,
|
|
53
53
|
}).execute(args);
|
|
54
|
-
return `{
|
|
54
|
+
return `{lightblue-fg}Placing a mark IS ${action} -- running it (p.238/p.240).{/lightblue-fg}\n${result}`;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
//# sourceMappingURL=mark.js.map
|
|
@@ -163,6 +163,19 @@ export class RestCommand extends Command {
|
|
|
163
163
|
lines.push(` Bench work on the ${deck.name}: ${formatRoll(repairRoll)}`);
|
|
164
164
|
lines.push(` ${repaired} box${repaired === 1 ? '' : 'es'} repaired: ${deck.deckSummary()}${wasBricked && !deck.isBricked ? ' -- it boots again.' : ''}`);
|
|
165
165
|
}
|
|
166
|
+
// BRICKED GEAR (p.228 Repairing Matrix Damage): an hour with a
|
|
167
|
+
// toolkit and Hardware + Logic -- the rest is the hour, and every
|
|
168
|
+
// hit either clears boxes or halves the time, so by the end of a
|
|
169
|
+
// night's rest a bricked smartgun boots again. A reboot never does
|
|
170
|
+
// this; that is the difference between bricked and jammed.
|
|
171
|
+
const brickedGear = [
|
|
172
|
+
...actor.inventory.getAllItems(),
|
|
173
|
+
actor.equipment.rightHand?.weapon, actor.equipment.leftHand?.weapon,
|
|
174
|
+
].filter((i) => !!i && i.bricked);
|
|
175
|
+
for (const gear of new Set(brickedGear)) {
|
|
176
|
+
gear.bricked = false;
|
|
177
|
+
lines.push(` Bench work on the ${gear.name}: the burnt boards come out, and it powers up again.`);
|
|
178
|
+
}
|
|
166
179
|
for (const drone of damagedDrones) {
|
|
167
180
|
const wasWrecked = drone.isWrecked;
|
|
168
181
|
const repairRoll = rollPool(actor.logic + actor.skillRating('hardware') + actor.bonus('piloting'));
|
|
@@ -2,7 +2,9 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
3
|
import { planeTintItemName, emphasizeFind, objectiveMark, fileReach } from '../utilities/planes.js';
|
|
4
4
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
5
|
-
import { perceptionTest } from '../utilities/perception.js';
|
|
5
|
+
import { perceptionTest, matrixSearchTest, matrixSearchSeconds, MATRIX_SEARCH } from '../utilities/perception.js';
|
|
6
|
+
import { gridVicinity } from '../utilities/grid-reach.js';
|
|
7
|
+
import { hostLabel } from '../utilities/grid-names.js';
|
|
6
8
|
import { canReach, exitSpot, spotsActive, revealWithPerception, theSpot, sealedRouteTo, } from '../utilities/spots.js';
|
|
7
9
|
import { placeName } from '../utilities/log-style.js';
|
|
8
10
|
import { billAction, encounterOf } from '../utilities/action-cost.js';
|
|
@@ -135,6 +137,73 @@ ${coach}`;
|
|
|
135
137
|
export class SearchCommand extends Command {
|
|
136
138
|
static verb = 'search';
|
|
137
139
|
static description = 'Search for items in your area';
|
|
140
|
+
/**
|
|
141
|
+
* MATRIX SEARCH (SR5 p.241, player ruling 2026-09-13: "a true matrix
|
|
142
|
+
* search, not perception, revealing what is inside a host if it's
|
|
143
|
+
* cracked open").
|
|
144
|
+
*
|
|
145
|
+
* A Special action: Computer + Intuition [Data Processing] against a
|
|
146
|
+
* threshold from the Matrix Search Table, and it takes TIME -- one
|
|
147
|
+
* minute inside a host whatever the information, net hits over the
|
|
148
|
+
* threshold dividing it, a miss spending it all, a Browse program
|
|
149
|
+
* halving it (utilities/perception.ts). The time is real: it moves
|
|
150
|
+
* the persona's Overwatch clock back, so a slow search inside a host
|
|
151
|
+
* is what GOD bills for (utilities/overwatch.ts billTime, +2d6 per
|
|
152
|
+
* 15 minutes).
|
|
153
|
+
*
|
|
154
|
+
* WHAT IT FINDS. Inside a host, the host's archive: from then on the
|
|
155
|
+
* interior lists its files for this persona (Host.searchedBy,
|
|
156
|
+
* grid-view.ts). A CRACKED host spills them without the roll -- the
|
|
157
|
+
* seal is broken, there is nothing to find. Out on the grid it is the
|
|
158
|
+
* public-knowledge row: the loose data icons in reach.
|
|
159
|
+
*/
|
|
160
|
+
matrixSearch() {
|
|
161
|
+
const actor = this.actor;
|
|
162
|
+
const host = actor.hostInside;
|
|
163
|
+
const where = host ? host.over : gridVicinity(actor);
|
|
164
|
+
const row = host ? MATRIX_SEARCH.inHost : MATRIX_SEARCH.public;
|
|
165
|
+
const browse = actor.activeDeck?.hasProgram('browse') ?? false;
|
|
166
|
+
const { pool, limit } = matrixSearchTest(actor);
|
|
167
|
+
const roll = rollPool(pool, limit);
|
|
168
|
+
const seconds = matrixSearchSeconds(row.seconds, roll.hits, row.threshold, browse);
|
|
169
|
+
const isPlayer = this.scene.isHumanControlled(actor);
|
|
170
|
+
if (isPlayer) {
|
|
171
|
+
this.logger.meta(`Matrix Search (Computer + Intuition [Data Processing], threshold ${row.threshold}${browse ? ', Browse' : ''}): ${formatRoll(roll)} -- ${seconds}s`);
|
|
172
|
+
}
|
|
173
|
+
// THE MINUTES ARE GOD'S (p.231-232): Overwatch bills time on the
|
|
174
|
+
// grid, and a search is time. Winding the last tick back by the
|
|
175
|
+
// seconds spent is what makes the next illegal action pay for them.
|
|
176
|
+
if (actor.overwatchLastTick > 0)
|
|
177
|
+
actor.overwatchLastTick -= seconds * 1000;
|
|
178
|
+
actor.performAction('runs a Matrix Search', host ? `inside ${hostLabel(where)}` : `across ${where.name}'s grid`, { quiet: this.scene.isHumanControlled(actor) });
|
|
179
|
+
const files = where.offlineServer ? [] : where.inventory.getAllItems().filter(i => i.plane === 'matrix');
|
|
180
|
+
const clock = `(${seconds}s of Matrix time${browse ? ', Browse cutting it' : ''})`;
|
|
181
|
+
if (roll.hits < row.threshold) {
|
|
182
|
+
return host
|
|
183
|
+
? `You dig through ${hostLabel(where)}'s archive and the index keeps folding away from you -- nothing this pass. ${clock}${hint(` Try again; the host is not going anywhere.`)}`
|
|
184
|
+
: `You sweep ${where.name}'s grid for loose data and turn up nothing this pass. ${clock}`;
|
|
185
|
+
}
|
|
186
|
+
if (host) {
|
|
187
|
+
host.searchedBy.add(actor.name);
|
|
188
|
+
where.searchedBy.add(actor.name);
|
|
189
|
+
if (files.length === 0) {
|
|
190
|
+
return `The archive is indexed and EMPTY -- ${hostLabel(where)} keeps nothing worth the trip. ${clock}`;
|
|
191
|
+
}
|
|
192
|
+
const list = files.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
|
|
193
|
+
const seal = where.hostCracked
|
|
194
|
+
? hint(` ("take" them -- the seal is already broken.)`)
|
|
195
|
+
: hint(` (sealed in the host's data vault: "hack" the node to break the seal, then "take".)`);
|
|
196
|
+
return `The index gives up ${hostLabel(where)}'s archive: ${list}. ${clock}${seal}`;
|
|
197
|
+
}
|
|
198
|
+
where.searchedBy.add(actor.name);
|
|
199
|
+
const loose = where.hasNode ? [] : files;
|
|
200
|
+
if (loose.length === 0) {
|
|
201
|
+
const sealedElsewhere = files.length > 0 && where.hasNode;
|
|
202
|
+
return `Nothing loose on ${where.name}'s grid. ${clock}${sealedElsewhere ? hint(` (${hostLabel(where)} keeps its files INSIDE -- "enter" it and search from in there.)`) : ''}`;
|
|
203
|
+
}
|
|
204
|
+
const list = loose.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
|
|
205
|
+
return `Loose on ${where.name}'s grid: ${list}. ${clock}${hint(` ("take" rides them out on your deck.)`)}`;
|
|
206
|
+
}
|
|
138
207
|
async execute(_args) {
|
|
139
208
|
const room = this.actor.currentLocation;
|
|
140
209
|
// A MATRIX SEARCH IS MEASURED IN MINUTES (p.241: base time one
|
|
@@ -144,6 +213,8 @@ export class SearchCommand extends Command {
|
|
|
144
213
|
if (this.actor.plane === 'matrix' && encounterOf(this.scene, this.actor)) {
|
|
145
214
|
return `Not with the ice on you -- a Matrix Search takes minutes (p.241), and a Combat Turn is three seconds. Crash the ice or get out first.`;
|
|
146
215
|
}
|
|
216
|
+
if (this.actor.plane === 'matrix')
|
|
217
|
+
return this.matrixSearch();
|
|
147
218
|
// OBSERVE IN DETAIL (SR5 p.165): the Perception Test below is the
|
|
148
219
|
// Simple Action the book names, inside a Combat Turn.
|
|
149
220
|
const bill = billAction(this.scene, this.actor, 'simple', 'Observe in Detail');
|
|
@@ -155,17 +226,14 @@ export class SearchCommand extends Command {
|
|
|
155
226
|
// room stays uncased ("take all" stays gated). Barriers (puzzles) are
|
|
156
227
|
// in plain sight and show regardless -- you can't fail to notice the
|
|
157
228
|
// maglock you're staring at.
|
|
158
|
-
// On the GRID a search is Matrix Search (canon: Computer, Logic) --
|
|
159
|
-
// best-of with perception so pre-computer deckers lose nothing.
|
|
160
|
-
// A loaded Browse program is purpose-built for Matrix Search: +2.
|
|
161
|
-
const browse = this.actor.plane === 'matrix' && this.actor.activeDeck?.hasProgram('browse') ? 2 : 0;
|
|
162
229
|
// ACTIVE: this is the deliberate hunt, so it carries p.135-136's +3
|
|
163
230
|
// for "perceiver is specifically looking for it" -- the modifier
|
|
164
231
|
// that makes `search` mechanically different from `look` rather
|
|
165
232
|
// than only differently named (FNgptvRhbA5yMunbB). The pool and the
|
|
166
233
|
// bracket both live in utilities/perception.ts; look.ts rolls the
|
|
167
|
-
// same test without the +3.
|
|
168
|
-
|
|
234
|
+
// same test without the +3. The grid never comes through here: a
|
|
235
|
+
// persona's search is matrixSearch() above.
|
|
236
|
+
const { pool, limit } = perceptionTest(this.actor, { active: true });
|
|
169
237
|
const roll = rollPool(pool, limit);
|
|
170
238
|
// Roll anatomy to the Mechanics ticker for the player; an NPC's
|
|
171
239
|
// return string is its AI history, so IT keeps the roll inline
|
|
@@ -234,28 +302,7 @@ export class SearchCommand extends Command {
|
|
|
234
302
|
if (spotted.length > 0) {
|
|
235
303
|
description += `\n{yellow-fg}You pick ${spotted.map(a => a.name).join(', ')} out of the shadows -- ${spotted.length === 1 ? 'they were' : 'they were'} there the whole time.{/yellow-fg}`;
|
|
236
304
|
}
|
|
237
|
-
//
|
|
238
|
-
// vault state attached: sealed files need the host cracked first. A
|
|
239
|
-
// real session's decker found the paydata jacked-in, didn't realize it
|
|
240
|
-
// was takeable from the grid, and went hunting for it in the meat.
|
|
241
|
-
if (this.actor.plane === 'matrix' && found.some(i => i.plane === 'matrix')) {
|
|
242
|
-
description += room.hasNode && !room.offlineServer && !room.hostCracked
|
|
243
|
-
? `\n{cyan-fg}(the files sit sealed in the host's data vault -- "hack" the node to unseal them){/cyan-fg}`
|
|
244
|
-
: hint(`\n{cyan-fg}(data files are loot -- "take" them right here on the grid; they ride out on your deck){/cyan-fg}`);
|
|
245
|
-
}
|
|
246
|
-
// A persona sweeping the grid OUTSIDE a host: the files exist and
|
|
247
|
-
// this sweep cannot reach them. Say why, rather than reading empty
|
|
248
|
-
// -- a silent nothing is what made the old air-gap case feel broken.
|
|
249
|
-
if (this.actor.plane === 'matrix' && !filesInReach
|
|
250
|
-
&& room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
|
|
251
|
-
if (room.offlineServer) {
|
|
252
|
-
description += `\n{cyan-fg}There is a ${room.offlineServer} in this room and it is NOT on the Matrix -- nothing you can do from out here touches it.${hint(` Walk in and cable into it in the flesh.`)}{/cyan-fg}`;
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
const marked = (room.hostMarksBy.get(this.actor.name) ?? 0) > 0 || room.hostCracked;
|
|
256
|
-
description += `\n{cyan-fg}This sweep only covers the open grid. Whatever the host holds is INSIDE it, behind its wall (p.246).${hint(marked ? ` "enter" and search again.` : ` A mark is the door -- "mark" the host, then "enter".`)}{/cyan-fg}`;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
305
|
+
// The grid never reaches here: a persona's search is matrixSearch().
|
|
259
306
|
// The mirror hint: a meat-side search can't see matrix files, but it
|
|
260
307
|
// shouldn't pretend the room is empty when the good stuff is one
|
|
261
308
|
// "jack in" away.
|
|
@@ -266,8 +313,8 @@ export class SearchCommand extends Command {
|
|
|
266
313
|
// tells a player standing next to an offline server that it is
|
|
267
314
|
// there and what it wants from them.
|
|
268
315
|
description += room.offlineServer
|
|
269
|
-
? `\n{
|
|
270
|
-
: `\n{
|
|
316
|
+
? `\n{lightblue-fg}A ${room.offlineServer} sits here, dark and off the network -- no aerial, no marquee, nothing broadcasting. Whatever it holds comes out through a cable and no other way.${hint(` "jack in" RIGHT HERE to touch its files.`)}{/lightblue-fg}`
|
|
317
|
+
: `\n{lightblue-fg}The node here hums with locked files -- whatever's worth taking lives on the grid, inside the host.${hint(` "jack in", then "enter" the host.`)}{/lightblue-fg}`;
|
|
271
318
|
}
|
|
272
319
|
description += describeContainers(room, this.actor);
|
|
273
320
|
description += describeBarriers(room, this.actor);
|
|
@@ -49,7 +49,7 @@ export class SnoopCommand extends Command {
|
|
|
49
49
|
this.logger.write(`Snoop: ${actor.name} watching ${target.name} via camera feed.`);
|
|
50
50
|
const lines = [`CAMERA FEED: ${target.name.toUpperCase()}`];
|
|
51
51
|
if (!camerasLive(allRooms, target)) {
|
|
52
|
-
lines.push(`{
|
|
52
|
+
lines.push(`{lightblue-fg}(Security sees a loop of empty hallways -- these eyes answer only to you now.){/lightblue-fg}`);
|
|
53
53
|
}
|
|
54
54
|
const seen = this.scene.getActorsInRoom(target)
|
|
55
55
|
.filter(a => a !== actor && a.plane === 'meat' && !a.sneaking);
|
|
@@ -73,7 +73,7 @@ export class SpritesCommand extends Command {
|
|
|
73
73
|
const name = type?.name ?? set.spriteKey;
|
|
74
74
|
const isMine = mineType === set.spriteKey;
|
|
75
75
|
const powers = powersForSprite(set.spriteKey);
|
|
76
|
-
lines.push(`${name.toUpperCase()} SPRITE${isMine ? ' {
|
|
76
|
+
lines.push(`${name.toUpperCase()} SPRITE${isMine ? ' {lightblue-fg}(yours){/lightblue-fg}' : ''}${type ? ` -- ${type.blurb}` : ''}`);
|
|
77
77
|
lines.push(` Skills: ${skillsForSprite(set.spriteKey).join(', ')}${hint(` (${set.page})`)}`);
|
|
78
78
|
if (!detailed) {
|
|
79
79
|
// The names alone, which is the comparison view.
|
|
@@ -158,9 +158,9 @@ export class TapCommand extends Command {
|
|
|
158
158
|
}
|
|
159
159
|
this.logger.write(`${actor.name} now holds ${now} mark(s) on ${host.name} via the ${room.name} cameras.`);
|
|
160
160
|
const lines = [
|
|
161
|
-
`{
|
|
162
|
-
` {
|
|
163
|
-
` {
|
|
161
|
+
`{lightblue-fg}The filament finds the connector and the camera stops being a camera -- it's a device, and it's talking to you.{/lightblue-fg}`,
|
|
162
|
+
` {lightblue-fg}It never gets to hide behind ${hostLabel(host)}: a cable doesn't ask the network for permission, so the lens has to defend itself with what a lens has. It doesn't have much.{/lightblue-fg}`,
|
|
163
|
+
` {lightblue-fg}MARK PLACED -- and it lands on the host too. The camera answers to ${host.name}, and anything ${host.name} trusts to speak for it just handed you a key. [${now}/${MAX_MARKS}]{/lightblue-fg}`,
|
|
164
164
|
];
|
|
165
165
|
if (mode === 'attack') {
|
|
166
166
|
lines.push(` The housing is visibly forced -- the host FELT that one, even if nothing saw your face.`);
|
|
@@ -169,15 +169,15 @@ export class TapCommand extends Command {
|
|
|
169
169
|
lines.push(` Nothing logged, nothing flagged. You were never here.`);
|
|
170
170
|
}
|
|
171
171
|
if (host === room) {
|
|
172
|
-
lines.push(` {
|
|
172
|
+
lines.push(` {lightblue-fg}${now >= MAX_MARKS
|
|
173
173
|
? `The host thinks you belong now -- "hack" walks straight in.`
|
|
174
|
-
: `${hostLabel(host, { capital: true })} cracks easier with every key you hold${hint(` -- "jack in" and "hack", or come back for another lens`)}.`}{/
|
|
174
|
+
: `${hostLabel(host, { capital: true })} cracks easier with every key you hold${hint(` -- "jack in" and "hack", or come back for another lens`)}.`}{/lightblue-fg}`);
|
|
175
175
|
}
|
|
176
176
|
else {
|
|
177
|
-
lines.push(` {
|
|
177
|
+
lines.push(` {lightblue-fg}These lenses ride the site network, so the key fits ${hostLabel(host)}${hint(` -- "jack in", "go ${host.name}", "hack"`)}.{/lightblue-fg}`);
|
|
178
178
|
}
|
|
179
179
|
// A mark is all canSnoopFeeds ever wanted (surveillance.ts).
|
|
180
|
-
lines.push(` {
|
|
180
|
+
lines.push(` {lightblue-fg}And the eyes are half yours already${hint(` -- jacked in, "snoop <room>" rides any feed on this network`)}.{/lightblue-fg}`);
|
|
181
181
|
lines.push(...godLines);
|
|
182
182
|
this.scene.updateStatus();
|
|
183
183
|
return lines.join('\n');
|
|
@@ -67,7 +67,7 @@ export class ThreadCommand extends Command {
|
|
|
67
67
|
const lines = [`COMPLEX FORMS (thread <form> <level>)`];
|
|
68
68
|
for (const f of FORMS) {
|
|
69
69
|
const held = actor.sustainedForms.some(s => s.key === f.key);
|
|
70
|
-
lines.push(` ${f.name.padEnd(16)} ${f.kind === 'sustained' ? '[sustained]' : '[instant] '} ${f.blurb}${held ? ' {
|
|
70
|
+
lines.push(` ${f.name.padEnd(16)} ${f.kind === 'sustained' ? '[sustained]' : '[instant] '} ${f.blurb}${held ? ' {lightblue-fg}(HELD){/lightblue-fg}' : ''}`);
|
|
71
71
|
}
|
|
72
72
|
if (actor.sustainedForms.length > 0) {
|
|
73
73
|
lines.push('', ` Holding ${actor.sustainedForms.length} form${actor.sustainedForms.length === 1 ? '' : 's'} (${sustainTotal(actor.sustainedLedger().filter(w => w.kind === 'form'))}).${hint(` ("thread drop <form>" releases.)`)}`);
|
|
@@ -109,6 +109,7 @@ import { EndCallCommand } from './commands/end-call.js';
|
|
|
109
109
|
import { fuzzyPickName, guessVerb } from './utilities/fuzzy-match.js';
|
|
110
110
|
import { PickCommand } from './commands/pick.js';
|
|
111
111
|
import { HackCommand } from './commands/hack.js';
|
|
112
|
+
import { DisableCommand, BrickCommand } from './commands/disable.js';
|
|
112
113
|
import { CastCommand } from './commands/cast.js';
|
|
113
114
|
import { StanceCommand } from './commands/stance.js';
|
|
114
115
|
import { FollowCommand, UnfollowCommand } from './commands/follow.js';
|
|
@@ -2596,6 +2597,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2596
2597
|
CommandFactory.registerCommand('end-call', EndCallCommand);
|
|
2597
2598
|
CommandFactory.registerCommand('pick', PickCommand);
|
|
2598
2599
|
CommandFactory.registerCommand('hack', HackCommand);
|
|
2600
|
+
CommandFactory.registerCommand('disable', DisableCommand);
|
|
2601
|
+
CommandFactory.registerCommand('brick', BrickCommand);
|
|
2599
2602
|
CommandFactory.registerCommand('cast', CastCommand);
|
|
2600
2603
|
CommandFactory.registerCommand('stance', StanceCommand);
|
|
2601
2604
|
CommandFactory.registerCommand('surrender', SurrenderCommand);
|
|
@@ -5662,7 +5665,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5662
5665
|
// works the grid while you're back in the meat (the AR-drop exit).
|
|
5663
5666
|
const remoteAgent = this.companions.find(c => c.kind === 'agent');
|
|
5664
5667
|
if (remoteAgent && player.plane !== 'matrix') {
|
|
5665
|
-
lines.push(`{
|
|
5668
|
+
lines.push(`{lightblue-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''}${hint(' ("jack in" to rejoin it, "recall" to fold it home)')}{/lightblue-fg}`);
|
|
5666
5669
|
}
|
|
5667
5670
|
// Off-plane context: which reality you're in, your persona's Matrix
|
|
5668
5671
|
// track (cold-sim damage lands there), and where your empty body lies.
|
|
@@ -5681,7 +5684,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5681
5684
|
const track = jackedDeck
|
|
5682
5685
|
? ` Deck A${jackedDeck.attack} S${jackedDeck.sleaze} D${jackedDeck.dataProcessing} F${jackedDeck.firewall} ${jackedDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}${pgms}`
|
|
5683
5686
|
: ` (living persona -- damage is REAL stun)`;
|
|
5684
|
-
lines.push(`{
|
|
5687
|
+
lines.push(`{lightblue-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/lightblue-fg}`);
|
|
5685
5688
|
}
|
|
5686
5689
|
else if (player.plane === 'astral') {
|
|
5687
5690
|
lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
|
|
@@ -5773,7 +5776,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5773
5776
|
const dockW = mapWidth ?? 15;
|
|
5774
5777
|
const lines = hosts.length > 0
|
|
5775
5778
|
? hosts.map(h => hostDockLine(h, this.player, dockW))
|
|
5776
|
-
: ['{
|
|
5779
|
+
: ['{lightblue-fg}nothing overhead{/lightblue-fg}'];
|
|
5777
5780
|
lines.push(caption(this.player.insideHost ? `inside ${this.player.insideHost.name}` : `on the grid from ${gridVicinity(this.player).name}`, mapWidth));
|
|
5778
5781
|
this.setMapOverlay(lines);
|
|
5779
5782
|
if (onTheGrid) {
|
|
@@ -6275,7 +6278,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6275
6278
|
// players" / "crew street"); the bare verb still answers quietly.
|
|
6276
6279
|
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
|
|
6277
6280
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
|
|
6278
|
-
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
6281
|
+
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
6279
6282
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
6280
6283
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
6281
6284
|
// `sustain` shelves here and not under magic: the command is
|
|
@@ -126,6 +126,16 @@ export class Device {
|
|
|
126
126
|
structure;
|
|
127
127
|
armor;
|
|
128
128
|
_barrierDamage = 0;
|
|
129
|
+
/**
|
|
130
|
+
* THE MATRIX CONDITION MONITOR (p.228): 8 + (Device Rating / 2)
|
|
131
|
+
* boxes, resisted with Device Rating + Firewall, filled by a Data
|
|
132
|
+
* Spike (commands/disable.ts, "brick"). Full = BRICKED: "the device
|
|
133
|
+
* ceases functioning" -- a camera records nothing, a smartgun sparks
|
|
134
|
+
* and dies -- but "a lock stays locked" (p.228): bricking is never a
|
|
135
|
+
* way through a maglock. Repaired on the bench (rest.ts), never by a
|
|
136
|
+
* reboot. A ward is not electronics and has no monitor at all.
|
|
137
|
+
*/
|
|
138
|
+
_matrixDamage = 0;
|
|
129
139
|
/**
|
|
130
140
|
* THE LOCKSMITH EXTENDED TEST IN PROGRESS (p.48, p.359-360): picking a
|
|
131
141
|
* maglock by hand accumulates hits toward Rating x 2 across attempts,
|
|
@@ -207,6 +217,33 @@ export class Device {
|
|
|
207
217
|
* exact fault this codebase keeps having to close (see the gun in
|
|
208
218
|
* aroTags, AKfemBJJWJP99nx8K).
|
|
209
219
|
*/
|
|
220
|
+
// ---- The Matrix condition monitor (commands/disable.ts) ----
|
|
221
|
+
/** p.228: 8 + (Device Rating / 2), rounded up as the deck's is. 0 for
|
|
222
|
+
* a thing with no rating (a ward): nothing there to brick. */
|
|
223
|
+
get matrixMonitor() {
|
|
224
|
+
return this.rating > 0 ? 8 + Math.ceil(this.rating / 2) : 0;
|
|
225
|
+
}
|
|
226
|
+
get matrixDamage() {
|
|
227
|
+
return this._matrixDamage;
|
|
228
|
+
}
|
|
229
|
+
/** Returns boxes actually taken. */
|
|
230
|
+
takeMatrixDamage(boxes) {
|
|
231
|
+
if (this.matrixMonitor <= 0)
|
|
232
|
+
return 0;
|
|
233
|
+
const taken = Math.max(0, Math.floor(boxes));
|
|
234
|
+
this._matrixDamage = Math.min(this.matrixMonitor, this._matrixDamage + taken);
|
|
235
|
+
return taken;
|
|
236
|
+
}
|
|
237
|
+
get isBricked() {
|
|
238
|
+
return this.matrixMonitor > 0 && this._matrixDamage >= this.matrixMonitor;
|
|
239
|
+
}
|
|
240
|
+
/** Bench work (rest.ts): the whole monitor, at once. */
|
|
241
|
+
repairMatrix() {
|
|
242
|
+
this._matrixDamage = 0;
|
|
243
|
+
}
|
|
244
|
+
matrixSummary() {
|
|
245
|
+
return this.matrixMonitor > 0 ? `${this._matrixDamage}/${this.matrixMonitor} boxes` : '';
|
|
246
|
+
}
|
|
210
247
|
broadcastsAro() {
|
|
211
248
|
return this.wireless && this.rating > 0;
|
|
212
249
|
}
|
|
@@ -328,11 +365,13 @@ export class Device {
|
|
|
328
365
|
const text = hint(AFFORDANCE_FLAVOR[this.kind].hint);
|
|
329
366
|
return text.length > 0 ? text : undefined;
|
|
330
367
|
}
|
|
331
|
-
/** Persistence-only:
|
|
368
|
+
/** Persistence-only: flags in, nothing out. */
|
|
332
369
|
restoreState(state) {
|
|
333
370
|
this.open = state.open;
|
|
334
371
|
if (state.damage !== undefined)
|
|
335
372
|
this._barrierDamage = Math.max(0, Math.min(this.structure, Math.floor(state.damage)));
|
|
373
|
+
if (state.bricked !== undefined)
|
|
374
|
+
this._matrixDamage = state.bricked ? this.matrixMonitor : 0;
|
|
336
375
|
}
|
|
337
376
|
}
|
|
338
377
|
//# sourceMappingURL=device.js.map
|
|
@@ -41,6 +41,10 @@ export class Host {
|
|
|
41
41
|
marksBy = new Map();
|
|
42
42
|
/** player name -> marks the host holds on that player. */
|
|
43
43
|
marksOn = new Map();
|
|
44
|
+
/** Personas whose Matrix Search (p.241, commands/search.ts) turned up
|
|
45
|
+
* this host's files: the archive is listed for them from then on. A
|
|
46
|
+
* cracked host lists it for everyone. Session-only. */
|
|
47
|
+
searchedBy = new Set();
|
|
44
48
|
/** Walls up: the host holds a mark on an intruder and its ice is hunting. */
|
|
45
49
|
alert = false;
|
|
46
50
|
/** IC running now, by kind name. Names, not actors -- until increment 5. */
|
|
@@ -43,6 +43,12 @@ export class Item extends AbstractItem {
|
|
|
43
43
|
// A jammed firearm can't fire (fists until cleared); "reboot" clears
|
|
44
44
|
// every jammed device you carry, in the lull.
|
|
45
45
|
jammed = false;
|
|
46
|
+
// BRICKED (p.228, commands/disable.ts "brick"): a Data Spike filled the
|
|
47
|
+
// device's Matrix condition monitor. Dead electronics -- a smartgun
|
|
48
|
+
// "sparks, crackles, and smokes" and will not fire -- until bench
|
|
49
|
+
// work (rest.ts). A reboot does NOT clear it; that is the whole
|
|
50
|
+
// difference from `jammed`.
|
|
51
|
+
bricked = false;
|
|
46
52
|
_transferable;
|
|
47
53
|
// New currency-related fields
|
|
48
54
|
_currencyAmount = 0;
|
|
@@ -8,6 +8,7 @@ import { hint } from '../utilities/hints.js';
|
|
|
8
8
|
import { Category } from '../types/shared/item-enum.js';
|
|
9
9
|
import { Logger } from '../utilities/logger.js';
|
|
10
10
|
import { CALL_ICON, CALL_COLOR } from '../utilities/comm-style.js';
|
|
11
|
+
import { FEED_COLOR } from '../utilities/matrix-style.js';
|
|
11
12
|
// The Physical limit already lives here (subduing, p.195) -- imported
|
|
12
13
|
// rather than re-derived so meat combat and the grapple cannot drift
|
|
13
14
|
// into two different formulas for the same canon number.
|
|
@@ -2851,7 +2852,7 @@ export class Player extends AbstractPlayer {
|
|
|
2851
2852
|
// as before -- only the human's log line is dropped.
|
|
2852
2853
|
if (quiet)
|
|
2853
2854
|
return;
|
|
2854
|
-
this.logger.logWithColor(`${actor.name} ${verb} ${details}`,
|
|
2855
|
+
this.logger.logWithColor(`${actor.name} ${verb} ${details}`, FEED_COLOR);
|
|
2855
2856
|
}
|
|
2856
2857
|
}
|
|
2857
2858
|
performAction(verb, details, opts) {
|
|
@@ -469,6 +469,12 @@ export class Room extends AbstractRoom {
|
|
|
469
469
|
warded = false;
|
|
470
470
|
// See IRoomConstructorConfig.watched (utilities/surveillance.ts).
|
|
471
471
|
watched = false;
|
|
472
|
+
/** The cluster is LOOPED by Control Device (commands/disable.ts): the
|
|
473
|
+
* house sees empty hallways until the run ends. Session-only. */
|
|
474
|
+
camerasLooped = false;
|
|
475
|
+
/** The cluster is BRICKED by a Data Spike (p.228): dead lenses, until
|
|
476
|
+
* somebody's bench work nobody in this run is doing. Session-only. */
|
|
477
|
+
camerasBricked = false;
|
|
472
478
|
/**
|
|
473
479
|
* THE HOST STANDING OVER THIS ROOM, as an object (models/host.ts) --
|
|
474
480
|
* increment 1 of the Matrix position model. Every host field this class
|
|
@@ -256,7 +256,7 @@ export function aroBlock(scene, actor, room) {
|
|
|
256
256
|
const tags = aroTags(scene, actor, room);
|
|
257
257
|
if (tags.length === 0)
|
|
258
258
|
return '';
|
|
259
|
-
return `{
|
|
259
|
+
return `{lightblue-fg}AR OVERLAY:{/lightblue-fg}\n${tags.map(t => ` {lightblue-fg}${t}{/lightblue-fg}`).join('\n')}`;
|
|
260
260
|
}
|
|
261
261
|
/** Gear that talks to the grid on its own, lying LOOSE in the room.
|
|
262
262
|
* Weapons are absent here because a carried one is handled above, as
|
|
@@ -475,7 +475,7 @@ export class CombatEncounter {
|
|
|
475
475
|
// A persona has no metres to spend: the grid sees no distance.
|
|
476
476
|
const ground = this.arena.kind === 'room' ? `; ${this.movementLine(p.actor)}` : '';
|
|
477
477
|
const budgetLine = `Your Action Phase -- Combat Turn ${this.turn}, pass ${this.pass}, Initiative ${p.score}. ${this.budget.describe()}${ground}. "end turn" when you're done.`;
|
|
478
|
-
this.logger.log(`\n{
|
|
478
|
+
this.logger.log(`\n{yellow-fg}${budgetLine}{/yellow-fg}`, { actor: p.actor.name });
|
|
479
479
|
this.scene.updateStatus();
|
|
480
480
|
this.onHumanPhase?.(p.actor, this);
|
|
481
481
|
return;
|
|
@@ -742,7 +742,7 @@ export class CombatEncounter {
|
|
|
742
742
|
return undefined;
|
|
743
743
|
if (this.phaseActor === player && this.budget) {
|
|
744
744
|
const ground = this.arena.kind === 'room' ? ` · ${player.movementLeftMeters} m` : '';
|
|
745
|
-
return `{
|
|
745
|
+
return `{yellow-fg}⏱ YOUR PHASE{/yellow-fg} T${this.turn}/P${this.pass} Init ${p.score} · ${this.budget.describe()}${ground} · "end turn"`;
|
|
746
746
|
}
|
|
747
747
|
const who = this.phaseActor ? `${this.phaseActor.name} acting` : 'resolving';
|
|
748
748
|
return `⏱ COMBAT T${this.turn}/P${this.pass} Init ${p.score} · ${who}${p.fullDefenseTurn === this.turn ? ' · FULL DEFENSE' : ''}`;
|
|
@@ -106,6 +106,6 @@ export function hostDockLine(room, actor, width) {
|
|
|
106
106
|
const tag = room.hostCracked ? ' open' : marks > 0 ? ` ${marks}/3` : '';
|
|
107
107
|
const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}`;
|
|
108
108
|
const cut = [...raw].slice(0, Math.max(4, width)).join('');
|
|
109
|
-
return `{
|
|
109
|
+
return `{lightblue-fg}${cut}{/lightblue-fg}`;
|
|
110
110
|
}
|
|
111
111
|
//# sourceMappingURL=grid-reach.js.map
|