@maka/maka-cli 5.181.0 → 5.183.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/attack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +331 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +17 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +84 -30
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +51 -40
- 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 +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +44 -17
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +91 -23
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +30 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +107 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-brain.js +84 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +84 -51
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +5 -0
- 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 +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +4 -0
- package/package.json +1 -1
|
@@ -2,10 +2,12 @@ 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
|
-
import { billAction } from '../utilities/action-cost.js';
|
|
10
|
+
import { billAction, encounterOf } from '../utilities/action-cost.js';
|
|
9
11
|
/**
|
|
10
12
|
* Barrier rendering (2026-08-24 playtest: "I don't see THE THING I'm
|
|
11
13
|
* supposed to act on -- just a description... and I still see the
|
|
@@ -135,8 +137,84 @@ ${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`);
|
|
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;
|
|
209
|
+
// A MATRIX SEARCH IS MEASURED IN MINUTES (p.241: base time one
|
|
210
|
+
// minute inside a host), not in Action Phases -- there is no cost to
|
|
211
|
+
// bill inside a Combat Turn, so it is refused there rather than
|
|
212
|
+
// priced by invention.
|
|
213
|
+
if (this.actor.plane === 'matrix' && encounterOf(this.scene, this.actor)) {
|
|
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.`;
|
|
215
|
+
}
|
|
216
|
+
if (this.actor.plane === 'matrix')
|
|
217
|
+
return this.matrixSearch();
|
|
140
218
|
// OBSERVE IN DETAIL (SR5 p.165): the Perception Test below is the
|
|
141
219
|
// Simple Action the book names, inside a Combat Turn.
|
|
142
220
|
const bill = billAction(this.scene, this.actor, 'simple', 'Observe in Detail');
|
|
@@ -148,17 +226,14 @@ export class SearchCommand extends Command {
|
|
|
148
226
|
// room stays uncased ("take all" stays gated). Barriers (puzzles) are
|
|
149
227
|
// in plain sight and show regardless -- you can't fail to notice the
|
|
150
228
|
// maglock you're staring at.
|
|
151
|
-
// On the GRID a search is Matrix Search (canon: Computer, Logic) --
|
|
152
|
-
// best-of with perception so pre-computer deckers lose nothing.
|
|
153
|
-
// A loaded Browse program is purpose-built for Matrix Search: +2.
|
|
154
|
-
const browse = this.actor.plane === 'matrix' && this.actor.activeDeck?.hasProgram('browse') ? 2 : 0;
|
|
155
229
|
// ACTIVE: this is the deliberate hunt, so it carries p.135-136's +3
|
|
156
230
|
// for "perceiver is specifically looking for it" -- the modifier
|
|
157
231
|
// that makes `search` mechanically different from `look` rather
|
|
158
232
|
// than only differently named (FNgptvRhbA5yMunbB). The pool and the
|
|
159
233
|
// bracket both live in utilities/perception.ts; look.ts rolls the
|
|
160
|
-
// same test without the +3.
|
|
161
|
-
|
|
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 });
|
|
162
237
|
const roll = rollPool(pool, limit);
|
|
163
238
|
// Roll anatomy to the Mechanics ticker for the player; an NPC's
|
|
164
239
|
// return string is its AI history, so IT keeps the roll inline
|
|
@@ -227,28 +302,7 @@ export class SearchCommand extends Command {
|
|
|
227
302
|
if (spotted.length > 0) {
|
|
228
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}`;
|
|
229
304
|
}
|
|
230
|
-
//
|
|
231
|
-
// vault state attached: sealed files need the host cracked first. A
|
|
232
|
-
// real session's decker found the paydata jacked-in, didn't realize it
|
|
233
|
-
// was takeable from the grid, and went hunting for it in the meat.
|
|
234
|
-
if (this.actor.plane === 'matrix' && found.some(i => i.plane === 'matrix')) {
|
|
235
|
-
description += room.hasNode && !room.offlineServer && !room.hostCracked
|
|
236
|
-
? `\n{cyan-fg}(the files sit sealed in the host's data vault -- "hack" the node to unseal them){/cyan-fg}`
|
|
237
|
-
: hint(`\n{cyan-fg}(data files are loot -- "take" them right here on the grid; they ride out on your deck){/cyan-fg}`);
|
|
238
|
-
}
|
|
239
|
-
// A persona sweeping the grid OUTSIDE a host: the files exist and
|
|
240
|
-
// this sweep cannot reach them. Say why, rather than reading empty
|
|
241
|
-
// -- a silent nothing is what made the old air-gap case feel broken.
|
|
242
|
-
if (this.actor.plane === 'matrix' && !filesInReach
|
|
243
|
-
&& room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
|
|
244
|
-
if (room.offlineServer) {
|
|
245
|
-
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}`;
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
const marked = (room.hostMarksBy.get(this.actor.name) ?? 0) > 0 || room.hostCracked;
|
|
249
|
-
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}`;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
305
|
+
// The grid never reaches here: a persona's search is matrixSearch().
|
|
252
306
|
// The mirror hint: a meat-side search can't see matrix files, but it
|
|
253
307
|
// shouldn't pretend the room is empty when the good stuff is one
|
|
254
308
|
// "jack in" away.
|
|
@@ -440,5 +440,14 @@
|
|
|
440
440
|
// 1.52.0 (2026-09-12): THE EMBEDDED HUB SEED IS GONE FROM SAVES. The one
|
|
441
441
|
// release of dual-writing is over: a save carries hubSeedRef only, and
|
|
442
442
|
// the site (sheet fallback) reads hubSeed on older docs alone.
|
|
443
|
-
|
|
443
|
+
// 1.53.0 (2026-09-13): THE MATRIX JOINS THE COMBAT TURN. A host fight is a
|
|
444
|
+
// CombatEncounter in a host arena (encounters are keyed by arena: room,
|
|
445
|
+
// host, or the open grid); the host launches one IC per Combat Turn at
|
|
446
|
+
// the turn's start, each program is a participant with 4D6 whose phase
|
|
447
|
+
// the ice brain plays, a Data Spike is the phase's Complex attack, and
|
|
448
|
+
// hack/enter/exit/look/jack out bill their canon action cost inside a
|
|
449
|
+
// Combat Turn. IC no longer act once per player command, never speak,
|
|
450
|
+
// and a failed IC attack damages the program (p.247). Ice NPCs are no
|
|
451
|
+
// longer generated; the vault host's rating is its guard.
|
|
452
|
+
export const ENGINE_VERSION = '1.53.0';
|
|
444
453
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -41,8 +41,9 @@ import { getPref } from './utilities/prefs.js';
|
|
|
41
41
|
import { rollPool, formatRoll } from './utilities/dice.js';
|
|
42
42
|
import { Direction } from './types/shared/direction-enum.js';
|
|
43
43
|
import { Logger } from './utilities/logger.js';
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
44
|
+
import { hostIsHunting, ambientPatrol } from './utilities/ic.js';
|
|
45
|
+
import { hostContactBeat } from './utilities/host-combat.js';
|
|
46
|
+
import { endPersona } from './utilities/planes.js';
|
|
46
47
|
import { CommandQueue } from './utilities/command-queue.js';
|
|
47
48
|
import { currentSession } from './utilities/session-context.js';
|
|
48
49
|
import { PlayersCommand } from './commands/players.js';
|
|
@@ -108,6 +109,7 @@ import { EndCallCommand } from './commands/end-call.js';
|
|
|
108
109
|
import { fuzzyPickName, guessVerb } from './utilities/fuzzy-match.js';
|
|
109
110
|
import { PickCommand } from './commands/pick.js';
|
|
110
111
|
import { HackCommand } from './commands/hack.js';
|
|
112
|
+
import { DisableCommand, BrickCommand } from './commands/disable.js';
|
|
111
113
|
import { CastCommand } from './commands/cast.js';
|
|
112
114
|
import { StanceCommand } from './commands/stance.js';
|
|
113
115
|
import { FollowCommand, UnfollowCommand } from './commands/follow.js';
|
|
@@ -2482,7 +2484,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2482
2484
|
isParty: a => a !== player && this.scene.isHumanControlled(a),
|
|
2483
2485
|
// Whose Action Phase is live in the viewer's room
|
|
2484
2486
|
// (PYK6zCKhQcjRFwLSQ); undefined between phases and out of a fight.
|
|
2485
|
-
|
|
2487
|
+
// The viewer's own fight first -- a persona's is inside its host,
|
|
2488
|
+
// not in the room its body sits in.
|
|
2489
|
+
phaseActor: () => (this.scene.encounterFor(player) ?? this.scene.encounterIn(player.currentLocation))?.phaseActor?.name,
|
|
2486
2490
|
}))
|
|
2487
2491
|
.catch(() => undefined);
|
|
2488
2492
|
};
|
|
@@ -2593,6 +2597,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2593
2597
|
CommandFactory.registerCommand('end-call', EndCallCommand);
|
|
2594
2598
|
CommandFactory.registerCommand('pick', PickCommand);
|
|
2595
2599
|
CommandFactory.registerCommand('hack', HackCommand);
|
|
2600
|
+
CommandFactory.registerCommand('disable', DisableCommand);
|
|
2601
|
+
CommandFactory.registerCommand('brick', BrickCommand);
|
|
2596
2602
|
CommandFactory.registerCommand('cast', CastCommand);
|
|
2597
2603
|
CommandFactory.registerCommand('stance', StanceCommand);
|
|
2598
2604
|
CommandFactory.registerCommand('surrender', SurrenderCommand);
|
|
@@ -6044,7 +6050,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6044
6050
|
if (this.scene.isHumanControlled(actor))
|
|
6045
6051
|
this.scene.beat += 1;
|
|
6046
6052
|
const result = await command.execute(args);
|
|
6047
|
-
const
|
|
6053
|
+
const hosted = await this.withHostTurn(actor, result);
|
|
6054
|
+
const wrapped = await this.withHostileContact(actor, this.withEphemeralTurn(actor, this.withAlarmTurn(actor, hosted)));
|
|
6048
6055
|
// The IC turn above may have filled a track -- settle it now, not
|
|
6049
6056
|
// on the next keystroke.
|
|
6050
6057
|
const settled = await settleUnresolvedHarm(this.scene, actor);
|
|
@@ -6178,19 +6185,43 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6178
6185
|
const lines = await hostileContactBeat(this.scene, actor, this.scene.beat);
|
|
6179
6186
|
return lines.length === 0 ? result : `${result}\n\n${lines.join('\n')}`;
|
|
6180
6187
|
}
|
|
6181
|
-
|
|
6188
|
+
/**
|
|
6189
|
+
* THE HOST'S BEAT after every command a persona types inside one
|
|
6190
|
+
* (p.246-247: the ice acts on what is in the host).
|
|
6191
|
+
*
|
|
6192
|
+
* Before the host has made you, this is ambient Patrol on its clock
|
|
6193
|
+
* (utilities/ic.ts ambientPatrol) -- the per-command tick of that
|
|
6194
|
+
* clock is an engine mapping and its own comment says so. Once the
|
|
6195
|
+
* host is hunting, the fight is a Combat Turn in the host's arena
|
|
6196
|
+
* (utilities/host-combat.ts): the first beat opens it, and after
|
|
6197
|
+
* that the programs act in their OWN Action Phases, not on every
|
|
6198
|
+
* keystroke. The old shape -- every running program taking a swing
|
|
6199
|
+
* per player command, with its dice and lines appended to whatever
|
|
6200
|
+
* the command printed -- is gone; Vex's log had four programs and
|
|
6201
|
+
* twelve lines answering "look".
|
|
6202
|
+
*/
|
|
6203
|
+
async withHostTurn(actor, result) {
|
|
6182
6204
|
if (result === null)
|
|
6183
6205
|
return null;
|
|
6184
|
-
// The host's turn happens INSIDE it (p.246-247): the ice acts on what
|
|
6185
|
-
// is in the host, and the persona's position says whether it is.
|
|
6186
6206
|
const host = actor.hostInside;
|
|
6187
6207
|
if (!host || actor.plane !== 'matrix')
|
|
6188
6208
|
return result;
|
|
6189
|
-
|
|
6209
|
+
if (hostIsHunting(host.over, actor)) {
|
|
6210
|
+
if (this.scene.encounterInHost(host)?.has(actor))
|
|
6211
|
+
return result;
|
|
6212
|
+
// THE COMMAND'S OWN REPLY PRINTS FIRST. The encounter announces
|
|
6213
|
+
// itself while it opens (the aggressor, Surprise, the order), so
|
|
6214
|
+
// a result returned afterwards would land under all of that,
|
|
6215
|
+
// out of order -- the same trick hostile-contact.ts uses.
|
|
6216
|
+
if (result.length > 0 && this.scene.isHumanControlled(actor)) {
|
|
6217
|
+
Logger.getInstance().log(result, { actor: actor.name });
|
|
6218
|
+
}
|
|
6219
|
+
await hostContactBeat(this.scene, actor);
|
|
6220
|
+
return '';
|
|
6221
|
+
}
|
|
6222
|
+
const turn = ambientPatrol(host.over, actor);
|
|
6190
6223
|
if (turn.lines.length === 0 && turn.meta.length === 0)
|
|
6191
6224
|
return result;
|
|
6192
|
-
// Dice to the Mechanics pane, and only for a human -- the same gate
|
|
6193
|
-
// every other roll in this engine uses.
|
|
6194
6225
|
if (this.scene.isHumanControlled(actor)) {
|
|
6195
6226
|
const logger = Logger.getInstance();
|
|
6196
6227
|
for (const m of turn.meta)
|
|
@@ -6198,36 +6229,16 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6198
6229
|
}
|
|
6199
6230
|
for (const w of turn.world)
|
|
6200
6231
|
this.scene.addWorldEvent(w);
|
|
6201
|
-
// A deck bricked by IC owes the same forced dump a convergence does.
|
|
6202
|
-
// ic.ts reports it rather than performing it -- leaveMatrix needs the
|
|
6203
|
-
// Scene and that module deliberately takes none, exactly as hack.ts
|
|
6204
|
-
// splits the convergence hammer from the dump that follows it.
|
|
6205
|
-
if (turn.bricked) {
|
|
6206
|
-
turn.lines.push(...leaveMatrix(this.scene, actor, {
|
|
6207
|
-
forced: true,
|
|
6208
|
-
reason: `The host's ice burns your deck out from under you.`,
|
|
6209
|
-
}));
|
|
6210
|
-
}
|
|
6211
|
-
else if (turn.scrambled) {
|
|
6212
|
-
// SCRAMBLE IC (p.248): "you reboot immediately, taking dumpshock if
|
|
6213
|
-
// you were in VR." A forced exit through the same door, which is
|
|
6214
|
-
// what makes it a reboot rather than a graceful drop -- and the
|
|
6215
|
-
// reboot is also what hands the reducers' damage back.
|
|
6216
|
-
//
|
|
6217
|
-
// `else if` on purpose: a deck already bricked this turn has been
|
|
6218
|
-
// dumped, and dumping a persona twice would bill dumpshock twice.
|
|
6219
|
-
turn.lines.push(...leaveMatrix(this.scene, actor, {
|
|
6220
|
-
forced: true,
|
|
6221
|
-
reason: `Scramble IC rips the connection out at the root --`,
|
|
6222
|
-
}));
|
|
6223
|
-
}
|
|
6224
|
-
if (turn.traced) {
|
|
6225
|
-
// TRACK IC (p.249): the location goes to the authorities. Routed
|
|
6226
|
-
// into the heat the game already keeps for being made in the meat
|
|
6227
|
-
// world, rather than a second parallel notion of "they know".
|
|
6228
|
-
this.addHeat(4, `Track IC reported ${actor.name}'s location`);
|
|
6229
|
-
}
|
|
6230
6232
|
const body = turn.lines.join('\n');
|
|
6233
|
+
// Made on this very sweep: the fight opens on the same beat, after
|
|
6234
|
+
// the sighting has been read.
|
|
6235
|
+
if (hostIsHunting(host.over, actor)) {
|
|
6236
|
+
if (this.scene.isHumanControlled(actor)) {
|
|
6237
|
+
Logger.getInstance().log(result.length > 0 ? `${result}\n${body}` : body, { actor: actor.name });
|
|
6238
|
+
}
|
|
6239
|
+
await hostContactBeat(this.scene, actor);
|
|
6240
|
+
return '';
|
|
6241
|
+
}
|
|
6231
6242
|
return result.length > 0 ? `${result}\n${body}` : body;
|
|
6232
6243
|
}
|
|
6233
6244
|
/**
|
|
@@ -6267,7 +6278,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6267
6278
|
// players" / "crew street"); the bare verb still answers quietly.
|
|
6268
6279
|
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
|
|
6269
6280
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
|
|
6270
|
-
{ 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']] },
|
|
6271
6282
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
6272
6283
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
6273
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;
|
|
@@ -3554,6 +3554,12 @@ export class Player extends AbstractPlayer {
|
|
|
3554
3554
|
if (this.plane === 'drone')
|
|
3555
3555
|
return this.simMode === 'hot' ? 4 : 3;
|
|
3556
3556
|
if (this.plane === 'matrix') {
|
|
3557
|
+
// A HOST'S PROGRAM "should be treated as if it is in hot-sim, so
|
|
3558
|
+
// it gets a total of 4D6 Initiative Dice" (p.247). It has no
|
|
3559
|
+
// simMode of its own -- that field routes a visitor's damage into
|
|
3560
|
+
// a deck -- so the rule is read off the host it runs for.
|
|
3561
|
+
if (this.icHost)
|
|
3562
|
+
return 4;
|
|
3557
3563
|
if (this.simMode !== 'hot')
|
|
3558
3564
|
return 3;
|
|
3559
3565
|
// Overclocking echo (p.258): "an additional +1D6 while you're in
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Item, NPC } from './_index.js';
|
|
1
|
+
import { Room, Item, NPC } from './_index.js';
|
|
2
2
|
import { capitalCase } from 'change-case';
|
|
3
3
|
import { localGrid as makeLocalGrid } from '../utilities/grids.js';
|
|
4
4
|
import { Logger } from '../utilities/logger.js';
|
|
@@ -6,7 +6,7 @@ import { hint } from '../utilities/hints.js';
|
|
|
6
6
|
import { Category, Size, Rating } from '../types/shared/item-enum.js';
|
|
7
7
|
import { AbstractScene } from '../types/shared/abstracts.js';
|
|
8
8
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
9
|
-
import { CombatEncounter } from '../utilities/combat-turn.js';
|
|
9
|
+
import { CombatEncounter, arenaKey, arenaOf, inArena } from '../utilities/combat-turn.js';
|
|
10
10
|
export class Scene extends AbstractScene {
|
|
11
11
|
story;
|
|
12
12
|
// SYMMETRIC PLAYERS: the scene holds a LIST of human players --
|
|
@@ -136,13 +136,27 @@ export class Scene extends AbstractScene {
|
|
|
136
136
|
// the fight in the office are two initiative orders, as they would be
|
|
137
137
|
// at a table. An encounter holds the participants, their scores and
|
|
138
138
|
// whose Action Phase is live; the scene only owns the map.
|
|
139
|
+
//
|
|
140
|
+
// KEYED BY ARENA (utilities/combat-turn.ts, 2026-09-13): a room for
|
|
141
|
+
// the physical planes, a host for the personas inside it, the open
|
|
142
|
+
// grid for the rest. A persona's currentLocation is its body's room,
|
|
143
|
+
// so a room key could never find a decker's fight.
|
|
139
144
|
encounters = new Map();
|
|
140
|
-
|
|
145
|
+
encounterAt(arena) {
|
|
146
|
+
const enc = this.encounters.get(arenaKey(arena));
|
|
147
|
+
return enc && !enc.ended ? enc : undefined;
|
|
148
|
+
}
|
|
149
|
+
/** The live fight in a room (the physical arena), if any. */
|
|
141
150
|
encounterIn(room) {
|
|
142
151
|
if (!room)
|
|
143
152
|
return undefined;
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
return this.encounterAt({ kind: 'room', room });
|
|
154
|
+
}
|
|
155
|
+
/** The live fight inside a host, if any. */
|
|
156
|
+
encounterInHost(host) {
|
|
157
|
+
if (!host)
|
|
158
|
+
return undefined;
|
|
159
|
+
return this.encounterAt({ kind: 'host', host });
|
|
146
160
|
}
|
|
147
161
|
/**
|
|
148
162
|
* THE BEAT CLOCK: which human command the world is on. Game bumps it
|
|
@@ -155,7 +169,7 @@ export class Scene extends AbstractScene {
|
|
|
155
169
|
beat = 0;
|
|
156
170
|
/** The fight this actor is a participant in, if any. */
|
|
157
171
|
encounterFor(actor) {
|
|
158
|
-
const enc = this.
|
|
172
|
+
const enc = this.encounterAt(arenaOf(actor));
|
|
159
173
|
return enc?.has(actor) ? enc : undefined;
|
|
160
174
|
}
|
|
161
175
|
/**
|
|
@@ -165,11 +179,15 @@ export class Scene extends AbstractScene {
|
|
|
165
179
|
* either. A bystander is scenery; a bartender is not opposition until
|
|
166
180
|
* someone makes them so (noteAttack pulls them in then).
|
|
167
181
|
*/
|
|
168
|
-
encounterMembers(
|
|
182
|
+
encounterMembers(where, aggressor, target) {
|
|
183
|
+
const arena = where instanceof Room ? { kind: 'room', room: where } : where;
|
|
169
184
|
const out = new Set([aggressor]);
|
|
170
185
|
if (target)
|
|
171
186
|
out.add(target);
|
|
172
|
-
|
|
187
|
+
// The physical arena's roster is the room's; a host's or the grid's
|
|
188
|
+
// is every persona on that side of the wall (matrix-roster.ts).
|
|
189
|
+
const roster = arena.kind === 'room' ? arena.room.getActors() : this.allActors.filter(a => inArena(arena, a));
|
|
190
|
+
for (const a of roster) {
|
|
173
191
|
if (a === aggressor || a === target)
|
|
174
192
|
continue;
|
|
175
193
|
if (!a.sharesCombatPlane(aggressor) || a.isIncapacitated())
|
|
@@ -190,8 +208,9 @@ export class Scene extends AbstractScene {
|
|
|
190
208
|
* Resolves once the first pass has run up to a human's Action Phase
|
|
191
209
|
* (or the fight has already ended).
|
|
192
210
|
*/
|
|
193
|
-
async startEncounter(
|
|
194
|
-
const
|
|
211
|
+
async startEncounter(where, opts) {
|
|
212
|
+
const arena = where instanceof Room ? { kind: 'room', room: where } : where;
|
|
213
|
+
const existing = this.encounterAt(arena);
|
|
195
214
|
if (existing) {
|
|
196
215
|
if (opts.target)
|
|
197
216
|
existing.noteAttack(opts.aggressor, opts.target);
|
|
@@ -199,15 +218,21 @@ export class Scene extends AbstractScene {
|
|
|
199
218
|
existing.join(opts.aggressor);
|
|
200
219
|
return existing;
|
|
201
220
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
221
|
+
// The room under the arena: the fight's own for the physical planes,
|
|
222
|
+
// the host's room for a host, the aggressor's body for the grid.
|
|
223
|
+
const room = arena.kind === 'room' ? arena.room : arena.kind === 'host' ? arena.host.over : opts.aggressor.currentLocation;
|
|
224
|
+
const enc = new CombatEncounter(this, this.logger, room, arena);
|
|
225
|
+
enc.onNewTurn = opts.onNewTurn;
|
|
226
|
+
enc.keepAlive = opts.keepAlive;
|
|
227
|
+
this.encounters.set(arenaKey(arena), enc);
|
|
228
|
+
await enc.start(this.encounterMembers(arena, opts.aggressor, opts.target), opts);
|
|
205
229
|
return enc;
|
|
206
230
|
}
|
|
207
231
|
/** The encounter is over (called by the encounter itself). */
|
|
208
232
|
endEncounter(enc) {
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
const key = arenaKey(enc.arena);
|
|
234
|
+
if (this.encounters.get(key) === enc)
|
|
235
|
+
this.encounters.delete(key);
|
|
211
236
|
}
|
|
212
237
|
/**
|
|
213
238
|
* Someone walks into a room where a fight is on (p.160 entering
|
|
@@ -215,7 +240,7 @@ export class Scene extends AbstractScene {
|
|
|
215
240
|
* out until they act.
|
|
216
241
|
*/
|
|
217
242
|
admitToEncounter(actor) {
|
|
218
|
-
const enc = this.
|
|
243
|
+
const enc = this.encounterAt(arenaOf(actor));
|
|
219
244
|
if (!enc || enc.has(actor) || actor.isIncapacitated())
|
|
220
245
|
return undefined;
|
|
221
246
|
const npc = actor;
|
|
@@ -599,7 +624,9 @@ export class Scene extends AbstractScene {
|
|
|
599
624
|
// "still going" until the human explicitly ended their turn
|
|
600
625
|
// (5aqFRn2xPtRYDvNxR). Mirrors leave()'s own direct stillHostile()
|
|
601
626
|
// check for the same "nobody left to fight" shape.
|
|
602
|
-
|
|
627
|
+
// The actor's own arena first: a crashed IC's fight is inside its
|
|
628
|
+
// host, not in the room under it.
|
|
629
|
+
const enc = this.encounterFor(actor) ?? (room ? this.encounterIn(room) : undefined);
|
|
603
630
|
if (enc && !enc.ended) {
|
|
604
631
|
if (enc.phaseActor === actor) {
|
|
605
632
|
void enc.dropPhaseActor(actor);
|