@maka/maka-cli 5.390.0 → 5.392.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +60 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +5 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +16 -150
- package/bundle/typescript/src/commands/game/sideQuest/commands/crack.js +76 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +42 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/disarm.js +64 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +23 -46
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +23 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +15 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +55 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +15 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/leap.js +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +40 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +16 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +17 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/authored-run.js +632 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/catalog-item-seed.js +78 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/job-order.js +7 -44
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +8 -10
- package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +30 -52
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +112 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +123 -3
- package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js +52 -4
- package/bundle/typescript/src/commands/game/sideQuest/game.js +17 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/camera.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js +14 -21
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +62 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +141 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +18 -11
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/astral-move.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/district-plane.js +109 -27
- package/bundle/typescript/src/commands/game/sideQuest/utilities/door-watch.js +70 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/falling.js +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/file-guard.js +120 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +23 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +29 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ledge-jump.js +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/owner-guard.js +77 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/path-run.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +66 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +8 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spider.js +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +20 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +193 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/wards.js +116 -0
- package/package.json +1 -1
|
@@ -6,6 +6,14 @@ export function doorwayKindOf(spotName) {
|
|
|
6
6
|
return 'lateral';
|
|
7
7
|
return undefined;
|
|
8
8
|
}
|
|
9
|
+
export function wallFixtureKindOf(spotName) {
|
|
10
|
+
const head = spotName.trim().toLowerCase().split(/\s+/).pop() ?? '';
|
|
11
|
+
if (/^(door|doors|doorway|gate|gates|entrance|entry|archway|airlock)$/.test(head))
|
|
12
|
+
return 'door';
|
|
13
|
+
if (/^(window|windows)$/.test(head))
|
|
14
|
+
return 'window';
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
9
17
|
export const METERS_PER_CELL = 2;
|
|
10
18
|
/**
|
|
11
19
|
* How tall one storey is. A CHOICE, not a citation -- SR5 never says how
|
|
@@ -237,8 +245,9 @@ export function mulberry32(seed) {
|
|
|
237
245
|
function cellExists(grid, c, levelCells) {
|
|
238
246
|
if (c.x < 0 || c.y < 0 || c.x >= grid.dims.x || c.y >= grid.dims.y)
|
|
239
247
|
return false;
|
|
248
|
+
// A HOLE in an outdoor area is a cell something else owns (GROUND_HOLES).
|
|
240
249
|
if (c.z === 0)
|
|
241
|
-
return
|
|
250
|
+
return !GROUND_HOLES.get(grid)?.has(`${c.x},${c.y}`);
|
|
242
251
|
return levelCells.get(c.z)?.has(`${c.x},${c.y}`) ?? false;
|
|
243
252
|
}
|
|
244
253
|
/** Whether `c` is a cell of this grid at all -- in bounds, and on a
|
|
@@ -392,10 +401,12 @@ export function distanceMeters(a, b) {
|
|
|
392
401
|
* time (the const is bound long before any consumer runs). */
|
|
393
402
|
export function levelFootprint(grid, z) {
|
|
394
403
|
if (z === 0) {
|
|
404
|
+
const holes = GROUND_HOLES.get(grid);
|
|
395
405
|
const out = [];
|
|
396
406
|
for (let x = 0; x < grid.dims.x; x++) {
|
|
397
407
|
for (let y = 0; y < grid.dims.y; y++)
|
|
398
|
-
|
|
408
|
+
if (!holes?.has(`${x},${y}`))
|
|
409
|
+
out.push({ x, y, z: 0 });
|
|
399
410
|
}
|
|
400
411
|
return out;
|
|
401
412
|
}
|
|
@@ -565,6 +576,8 @@ export function pathClimbSteps(grid, path) {
|
|
|
565
576
|
* cellExists needs but doesn't belong on the public IRoomGrid shape --
|
|
566
577
|
* associated by identity so callers never juggle it themselves. */
|
|
567
578
|
const LEVEL_CELLS = new WeakMap();
|
|
579
|
+
/** An outdoor area's holes, per grid (IGridSynthesisInput.holes). */
|
|
580
|
+
const GROUND_HOLES = new WeakMap();
|
|
568
581
|
const WALL_REGIONS = {
|
|
569
582
|
north: (d) => ({ x0: 0, x1: d.x - 1, y0: 0, y1: 0 }),
|
|
570
583
|
south: (d) => ({ x0: 0, x1: d.x - 1, y0: d.y - 1, y1: d.y - 1 }),
|
|
@@ -639,6 +652,8 @@ export function synthesizeGrid(input) {
|
|
|
639
652
|
exitCells: new Map(), terrain: new Map(),
|
|
640
653
|
};
|
|
641
654
|
LEVEL_CELLS.set(grid, levelCells);
|
|
655
|
+
if (input.holes && input.holes.size > 0)
|
|
656
|
+
GROUND_HOLES.set(grid, input.holes);
|
|
642
657
|
const exists = (c) => cellExists(grid, c, levelCells);
|
|
643
658
|
const freeCellNear = (region, z) => {
|
|
644
659
|
const candidates = [];
|
|
@@ -698,6 +713,11 @@ export function synthesizeGrid(input) {
|
|
|
698
713
|
// a room's width away from.
|
|
699
714
|
cell = nearestFreeOnWall(cell, wallSide, occupied, dims, exists);
|
|
700
715
|
}
|
|
716
|
+
else if (authored && exists({ x: authored.x, y: authored.y, z: 0 }) && !occupied.has(key({ x: authored.x, y: authored.y, z: 0 }))) {
|
|
717
|
+
// A STAIR WHERE THE AUTHOR PUT IT (exit seed `cell`), on the
|
|
718
|
+
// ground level of the room, unless something already stands there.
|
|
719
|
+
cell = { x: authored.x, y: authored.y, z: 0 };
|
|
720
|
+
}
|
|
701
721
|
else {
|
|
702
722
|
// up/down (and anything exotic): no natural wall -- an open
|
|
703
723
|
// z=0 cell near the centroid, same free-cell picker as unplaced
|
|
@@ -732,6 +752,28 @@ export function synthesizeGrid(input) {
|
|
|
732
752
|
if (exists(approach))
|
|
733
753
|
claim(approach);
|
|
734
754
|
}
|
|
755
|
+
// --- AUTHORED SPOTS: exactly where the author put them. ---
|
|
756
|
+
//
|
|
757
|
+
// Player-built runs (2026-10-06: "real coordinates are a must"). A spot
|
|
758
|
+
// that carries its own cells takes them -- anchor first, the rest its
|
|
759
|
+
// footprint -- before anything is chosen for it. No rng is drawn here,
|
|
760
|
+
// so a room with no authored spot runs every pass below exactly as
|
|
761
|
+
// before, cell for cell. Doors and their approaches were claimed first
|
|
762
|
+
// and stay won: an authored cell on one is the audit's to report, not
|
|
763
|
+
// this pass's to move.
|
|
764
|
+
const fixed = new Set();
|
|
765
|
+
for (const spot of input.spots) {
|
|
766
|
+
if (spot.perch || !spot.cells || spot.cells.length === 0)
|
|
767
|
+
continue;
|
|
768
|
+
const cells = spot.cells.filter(c => exists(c));
|
|
769
|
+
if (cells.length === 0)
|
|
770
|
+
continue;
|
|
771
|
+
for (const c of cells)
|
|
772
|
+
claim(c);
|
|
773
|
+
grid.spotCells.set(spot.name, cells[0]);
|
|
774
|
+
grid.spotFootprints.set(spot.name, cells);
|
|
775
|
+
fixed.add(spot.name);
|
|
776
|
+
}
|
|
735
777
|
// Seat each exit-serving spot, now that we know how many exits it
|
|
736
778
|
// serves. ONE exit: the spot IS that doorway, so it takes the cell
|
|
737
779
|
// (Mama Wu's "door" -> its east wall). SEVERAL: the spot is the hub
|
|
@@ -743,6 +785,8 @@ export function synthesizeGrid(input) {
|
|
|
743
785
|
// directions also come off the stalls"). The doorways stay on their
|
|
744
786
|
// own walls; the hub sits where all of them are plausibly reachable.
|
|
745
787
|
for (const [spotName, served] of exitCellsBySpot.entries()) {
|
|
788
|
+
if (fixed.has(spotName))
|
|
789
|
+
continue; // the author already put it somewhere
|
|
746
790
|
if (served.length === 1) {
|
|
747
791
|
// FURNITURE BESIDE THE DOOR, NOT IN IT (XanDMPmRrbJQ8ta8e,
|
|
748
792
|
// 2026-09-12: "the corner booth is right in front of the door
|
|
@@ -893,6 +937,18 @@ export function synthesizeGrid(input) {
|
|
|
893
937
|
...(input.keepClear ?? []).map(c => key({ x: c.x, y: c.y, z: 0 })),
|
|
894
938
|
]);
|
|
895
939
|
for (const [name, anchor] of grid.spotCells.entries()) {
|
|
940
|
+
// A DOOR OR WINDOW HAS NO FURNITURE (wallFixtureKindOf): an empty
|
|
941
|
+
// footprint, as a camera's perch has. The anchor stays claimed, so
|
|
942
|
+
// nothing grows onto it; it is simply floor. No rng is skipped --
|
|
943
|
+
// growFurniture draws none for a one-cell name -- so nothing else in
|
|
944
|
+
// the room moves.
|
|
945
|
+
if (wallFixtureKindOf(name)) {
|
|
946
|
+
grid.spotFootprints.set(name, []);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
// An authored spot already has the footprint its author drew.
|
|
950
|
+
if (fixed.has(name))
|
|
951
|
+
continue;
|
|
896
952
|
if (name === OPEN_FLOOR_NAME || exitKeys.has(key(anchor))) {
|
|
897
953
|
grid.spotFootprints.set(name, [anchor]);
|
|
898
954
|
continue;
|
|
@@ -1019,7 +1075,12 @@ function placePerches(grid, input, base, levelCells, occupied, furnitureKeys) {
|
|
|
1019
1075
|
const wanted = perch.place && SIDES.includes(perch.place) ? perch.place : undefined;
|
|
1020
1076
|
const order = wanted ? [wanted, ...SIDES.filter(s => s !== wanted)] : SIDES;
|
|
1021
1077
|
let cell;
|
|
1022
|
-
|
|
1078
|
+
// AN AUTHORED LENS hangs exactly where its author put it, before any
|
|
1079
|
+
// draw from this stream -- so authored perches move nothing else.
|
|
1080
|
+
const fixedAt = perch.cells?.[0];
|
|
1081
|
+
if (fixedAt && !taken.has(`${fixedAt.x},${fixedAt.y}`))
|
|
1082
|
+
cell = { x: fixedAt.x, y: fixedAt.y };
|
|
1083
|
+
for (const side of cell ? [] : order) {
|
|
1023
1084
|
const free = wallCells(side).filter(c => usable(c.x, c.y));
|
|
1024
1085
|
// Prefer clear of the claimed cells (an exit's approach, say) --
|
|
1025
1086
|
// standing room under the lens -- but take what there is.
|
|
@@ -1213,6 +1274,8 @@ export function serializeRoomGrid(grid, roomName, plane) {
|
|
|
1213
1274
|
vertical,
|
|
1214
1275
|
terrain,
|
|
1215
1276
|
levels,
|
|
1277
|
+
// The ground's missing cells: what other rooms own (GROUND_HOLES).
|
|
1278
|
+
...(GROUND_HOLES.get(grid) ? { holes: [...GROUND_HOLES.get(grid)].map(k => { const [x, y] = k.split(',').map(Number); return { x, y }; }) } : {}),
|
|
1216
1279
|
origin: plane?.origin,
|
|
1217
1280
|
walls: plane?.walls,
|
|
1218
1281
|
};
|
|
@@ -7,7 +7,7 @@ import { DRONE_COLOUR } from './drone-prose.js';
|
|
|
7
7
|
import { PLAYER_GLYPH, OTHER_PLAYER_GLYPH, OTHER_PLAYER_COLOR, NEUTRAL_GLYPH, HOSTILE_GLYPH, CREW_GLYPH, DEAD_GLYPH, DOWNED_GLYPH, SPOT_GLYPH, RIGGED_GLYPH, UP_GLYPH, CAMERA_GLYPH, CAMERA_COLOR, CAMERA_DEAD_COLOR, BARRIER_GLYPH, BARRIER_COLOR, MANA_BARRIER_COLOR, exitGlyph, GLYPH_RANK, } from './map-glyphs.js';
|
|
8
8
|
import { barriersWire } from './ways-out.js';
|
|
9
9
|
import { xRulerLine, yRulerLabel, yGutterWidth } from './map-rulers.js';
|
|
10
|
-
import { key, levelFootprint, dimsForSize, roomRasterRows, METERS_PER_LEVEL, METERS_PER_CELL, } from './room-grid.js';
|
|
10
|
+
import { key, levelFootprint, dimsForSize, roomRasterRows, METERS_PER_LEVEL, METERS_PER_CELL, wallFixtureKindOf, } from './room-grid.js';
|
|
11
11
|
/**
|
|
12
12
|
* WHO THIS PANEL DRAWS: everyone you can see, plus every ARO your AR is
|
|
13
13
|
* showing you (BJDoj9xwy5GmnHAmj: "I should always be able to spot an
|
|
@@ -209,6 +209,11 @@ function buildRenderMaps(grid, room, viewer) {
|
|
|
209
209
|
cellToSpot.set(key(c), name);
|
|
210
210
|
}
|
|
211
211
|
for (const [name, c] of grid.spotCells.entries()) {
|
|
212
|
+
// A door or window is not furniture (room-grid wallFixtureKindOf):
|
|
213
|
+
// its cell is plain floor, and a door that is an exit wears the
|
|
214
|
+
// exit's own arrow.
|
|
215
|
+
if (wallFixtureKindOf(name))
|
|
216
|
+
continue;
|
|
212
217
|
if (!cellToSpot.has(key(c)))
|
|
213
218
|
cellToSpot.set(key(c), name);
|
|
214
219
|
}
|
|
@@ -541,6 +546,8 @@ function diamondSpots(room, grid) {
|
|
|
541
546
|
return (room.spots ?? [])
|
|
542
547
|
// A perch draws as its lens (CAMERA_GLYPH), with a legend row of its own.
|
|
543
548
|
.filter(s => !isPerchSpot(s))
|
|
549
|
+
// ...and a door or window draws nothing, so it has no row to explain.
|
|
550
|
+
.filter(s => !wallFixtureKindOf(s.name))
|
|
544
551
|
.filter(s => {
|
|
545
552
|
const cell = grid.spotCells.get(s.name);
|
|
546
553
|
return cell !== undefined && !onExit.has(key(cell));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NPC } from '../models/npc.js';
|
|
2
|
+
/**
|
|
3
|
+
* SPIDERS: an NPC that starts the run jacked into a host (player-built
|
|
4
|
+
* runs, 2026-10-06; SR5 p.267-268, Data Trails p.168-169 -- marks and
|
|
5
|
+
* ownership belong to the persona). Not ice: a persona with a body, so
|
|
6
|
+
* NPC.isConstruct() leaves them out and they talk, hear and act like the
|
|
7
|
+
* person they are. The body slumps where they sat (planes.ts
|
|
8
|
+
* shellsInRoom) and is attackable there; one solid hit slams them back
|
|
9
|
+
* into it with dumpshock (p.229), exactly as for a runner.
|
|
10
|
+
*
|
|
11
|
+
* Seated at scene build, without the jack-in's narration: the run opens
|
|
12
|
+
* with them already in. The GATES are the jack-in's own (commands/
|
|
13
|
+
* jack.ts) -- a rig to ride, VR needs a DNI (p.222-223), a living
|
|
14
|
+
* persona is hot-sim only (p.250) -- in one function the factory and the
|
|
15
|
+
* authored audit both read, so the editor's fault and the boot agree.
|
|
16
|
+
*/
|
|
17
|
+
/** Why this NPC cannot be jacked in as asked, or undefined when they can. */
|
|
18
|
+
export function spiderRefusal(npc, mode) {
|
|
19
|
+
if (npc.plane !== 'meat')
|
|
20
|
+
return `${npc.name} is not in the meat to jack in from.`;
|
|
21
|
+
const rig = npc.chooseRig();
|
|
22
|
+
if (!rig) {
|
|
23
|
+
if (!npc.isTechnomancer())
|
|
24
|
+
return `${npc.name} carries no cyberdeck or commlink to jack in on (p.222).`;
|
|
25
|
+
if (mode === 'cold')
|
|
26
|
+
return `${npc.name} rides a living persona, which is hot-sim only (p.250).`;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (!npc.hasDirectNeuralInterface())
|
|
30
|
+
return `${npc.name} has no datajack or trode net -- VR needs a direct neural interface (p.222-223).`;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Seats the persona inside the host, the body slumped in its room. Returns the refusal when they cannot be. */
|
|
34
|
+
export function seatSpider(scene, npc, host, mode) {
|
|
35
|
+
const refusal = spiderRefusal(npc, mode);
|
|
36
|
+
if (refusal)
|
|
37
|
+
return refusal;
|
|
38
|
+
npc.bodyRoom = npc.currentLocation;
|
|
39
|
+
npc.plane = 'matrix';
|
|
40
|
+
npc.simMode = mode;
|
|
41
|
+
npc.setActiveRig(npc.chooseRig());
|
|
42
|
+
npc.matrixPosition = { kind: 'host', host };
|
|
43
|
+
scene.addWorldEvent?.(`${npc.name} is jacked into ${host.name}.`, { plane: 'matrix' });
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Every spider riding this host: NPC personas inside it with a body elsewhere. */
|
|
47
|
+
export function spidersIn(scene, host) {
|
|
48
|
+
return scene.allActors.filter((a) => a instanceof NPC && a.plane === 'matrix' && !!a.bodyRoom && a.icKind === undefined && a.hostInside === host);
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=spider.js.map
|
|
@@ -6,7 +6,7 @@ import { wayOut } from './ways-out.js';
|
|
|
6
6
|
import { significantWords, wordsAreCloseEnough } from './fuzzy-match.js';
|
|
7
7
|
import { hint } from './hints.js';
|
|
8
8
|
import { placeName } from './log-style.js';
|
|
9
|
-
import { findPath, distanceMeters, coverBetween, blockingRegionFor, pathClearing, pathClimbSteps, levelFootprint, key, isCellBlocked, pathSqueezes, METERS_PER_CELL, doorwayKindOf, cellOnGrid, } from './room-grid.js';
|
|
9
|
+
import { findPath, distanceMeters, coverBetween, blockingRegionFor, pathClearing, pathClimbSteps, levelFootprint, key, isCellBlocked, pathSqueezes, METERS_PER_CELL, doorwayKindOf, wallFixtureKindOf, cellOnGrid, } from './room-grid.js';
|
|
10
10
|
// The doorway vocabulary moved to room-grid.ts (the grid seats exit
|
|
11
11
|
// spots by it); every caller of spots.ts keeps its import.
|
|
12
12
|
export { doorwayKindOf };
|
|
@@ -908,7 +908,13 @@ export function stepFrom(room, mover, direction, count) {
|
|
|
908
908
|
if (walledBy !== undefined)
|
|
909
909
|
return { cell: cur, taken, blockedBy: 'barrier', blocker: walledBy };
|
|
910
910
|
if (!standable.has(k)) {
|
|
911
|
-
const
|
|
911
|
+
const inRect = next.x >= 0 && next.y >= 0 && next.x < grid.dims.x && next.y < grid.dims.y;
|
|
912
|
+
// A HOLE IS OFF THE EDGE TOO (room-grid.ts GROUND_HOLES): a cell of
|
|
913
|
+
// this area's rectangle that a building or another street owns. The
|
|
914
|
+
// plane decides it exactly as it decides past the rectangle -- a
|
|
915
|
+
// street's ground walks on (move.ts crossOutdoorSeam re-homes you),
|
|
916
|
+
// a building's floor or wall stops you.
|
|
917
|
+
const inBounds = inRect && (next.z !== 0 || cellOnGrid(grid, next));
|
|
912
918
|
// Off the edge of an OUTDOOR room is just more street, unless the
|
|
913
919
|
// plane says something is built there.
|
|
914
920
|
if (!inBounds && openOutside(next)) {
|
|
@@ -960,9 +966,13 @@ export function spotForCell(room, cell) {
|
|
|
960
966
|
return OPEN_FLOOR;
|
|
961
967
|
let best;
|
|
962
968
|
let bestDist = Infinity;
|
|
963
|
-
for (const [name,
|
|
969
|
+
for (const [name, footprint] of grid.spotFootprints.entries()) {
|
|
964
970
|
if (name === OPEN_FLOOR)
|
|
965
971
|
continue;
|
|
972
|
+
// A door or window fills no cell (room-grid wallFixtureKindOf), so its
|
|
973
|
+
// anchor stands in for it: step up to the window and you are AT it.
|
|
974
|
+
const anchor = footprint.length === 0 && wallFixtureKindOf(name) ? grid.spotCells.get(name) : undefined;
|
|
975
|
+
const cells = anchor ? [anchor] : footprint;
|
|
966
976
|
for (const c of cells) {
|
|
967
977
|
if (c.z !== cell.z)
|
|
968
978
|
continue;
|
|
@@ -2536,7 +2546,8 @@ export function describeSpotRoster(room, viewer) {
|
|
|
2536
2546
|
* - AN ASTRAL BARRIER. "Astral barriers block astral travel, and you
|
|
2537
2547
|
* might run into one before you realize that it's in your way" --
|
|
2538
2548
|
* Room.warded, the same flag the aura rules read. Both ways: a ward
|
|
2539
|
-
* keeps you out, and it keeps you in
|
|
2549
|
+
* keeps you out, and it keeps you in -- unless `wardStops` says this
|
|
2550
|
+
* form is its creator, or has just pressed through it (utilities/wards.ts).
|
|
2540
2551
|
*
|
|
2541
2552
|
* Returns WORLD cells, not room-local ones, because an astral form is
|
|
2542
2553
|
* not in a room in the sense a body is -- it is at a point on the
|
|
@@ -2546,7 +2557,10 @@ export function describeSpotRoster(room, viewer) {
|
|
|
2546
2557
|
export function astralStepFrom(plane, from, direction, count, roomAt,
|
|
2547
2558
|
/** Whose MANA BARRIER stands on a cell, if any (Scene.barrierAt --
|
|
2548
2559
|
* the creator's own weave already excluded, p.315). */
|
|
2549
|
-
manaBarrierAt
|
|
2560
|
+
manaBarrierAt,
|
|
2561
|
+
/** Does this warded room's ward stop THIS form (wards.ts wardHolds)?
|
|
2562
|
+
* Unset: every ward stops everyone. */
|
|
2563
|
+
wardStops) {
|
|
2550
2564
|
/**
|
|
2551
2565
|
* UP AND DOWN ARE DIRECTIONS OUT HERE (p.313-314: an astral form
|
|
2552
2566
|
* "can move in any direction" and is "not limited by gravity").
|
|
@@ -2567,7 +2581,7 @@ manaBarrierAt) {
|
|
|
2567
2581
|
if (owner === undefined)
|
|
2568
2582
|
return undefined;
|
|
2569
2583
|
const room = roomAt(owner);
|
|
2570
|
-
return room?.warded === true ? room : undefined;
|
|
2584
|
+
return room?.warded === true && (wardStops?.(room) ?? true) ? room : undefined;
|
|
2571
2585
|
};
|
|
2572
2586
|
let cur = from;
|
|
2573
2587
|
let taken = 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spotOf } from './spots.js';
|
|
2
|
+
import { wallsBetween } from './district-plane.js';
|
|
2
3
|
/**
|
|
3
4
|
* SURVEILLANCE (player go 2026-08-24; canon-shaped, values engine-
|
|
4
5
|
* adapted -- the RAG was down): facilities watch themselves. A room is
|
|
@@ -41,12 +42,70 @@ export function isWatched(room) {
|
|
|
41
42
|
export function cameraMasterHost(rooms, room) {
|
|
42
43
|
if (!isWatched(room))
|
|
43
44
|
return undefined;
|
|
45
|
+
// An author who wired these lenses to a named host is obeyed.
|
|
46
|
+
if (room.camerasSlavedTo) {
|
|
47
|
+
const named = rooms[room.camerasSlavedTo] ?? Object.values(rooms).find(r => r.name === room.camerasSlavedTo);
|
|
48
|
+
if (named?.hasNode)
|
|
49
|
+
return named;
|
|
50
|
+
}
|
|
44
51
|
if (room.hasNode)
|
|
45
52
|
return room;
|
|
46
53
|
// Deterministic pick, and the same set canSnoopFeeds treats as the
|
|
47
54
|
// site network -- whichever host you own, you own these feeds.
|
|
48
55
|
return Object.values(rooms).find(r => r.hasNode);
|
|
49
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* THE HOST ONE LENS IS SLAVED TO (user 2026-10-06: "I can have a camera
|
|
59
|
+
* outside every shop, and they should slave to the shop host, not the
|
|
60
|
+
* outdoor strip"). A lens an author wired to a named host answers to
|
|
61
|
+
* that host; every other lens answers to its room's master
|
|
62
|
+
* (cameraMasterHost). One room's lenses can therefore feed several hosts.
|
|
63
|
+
*/
|
|
64
|
+
export function lensMaster(rooms, room, cam) {
|
|
65
|
+
const named = (n) => (n ? (rooms[n] ?? Object.values(rooms).find(r => r.name === n)) : undefined);
|
|
66
|
+
const own = named(cam.masterHost);
|
|
67
|
+
if (own?.hasNode)
|
|
68
|
+
return own;
|
|
69
|
+
// BOLTED TO A BUILDING: that building's host.
|
|
70
|
+
const wall = named(cam.mountedOn);
|
|
71
|
+
const building = wall ? buildingHost(wall) : undefined;
|
|
72
|
+
if (building)
|
|
73
|
+
return building;
|
|
74
|
+
return cameraMasterHost(rooms, room);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* THE HOST OF THE BUILDING A ROOM BELONGS TO (user 2026-10-06: "the 2nd
|
|
78
|
+
* story room can have a camera attached to the outside wall, as the host
|
|
79
|
+
* is on the first floor"): the room's own, else the nearest one through
|
|
80
|
+
* its inside doors and stairs -- never across open ground, which is a
|
|
81
|
+
* different building. Undefined when the whole building has none.
|
|
82
|
+
*/
|
|
83
|
+
export function buildingHost(room) {
|
|
84
|
+
const seen = new Set([room]);
|
|
85
|
+
let frontier = [room];
|
|
86
|
+
while (frontier.length > 0) {
|
|
87
|
+
const hit = frontier.find(r => r.hasNode);
|
|
88
|
+
if (hit)
|
|
89
|
+
return hit;
|
|
90
|
+
const next = [];
|
|
91
|
+
for (const r of frontier) {
|
|
92
|
+
for (const exit of r.exits.values()) {
|
|
93
|
+
const other = typeof exit.getOtherSide === 'function'
|
|
94
|
+
? exit.getOtherSide(r) : undefined;
|
|
95
|
+
if (!other || seen.has(other) || other.outdoors)
|
|
96
|
+
continue;
|
|
97
|
+
seen.add(other);
|
|
98
|
+
next.push(other);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
frontier = next;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
/** An author named this lens's master -- on the lens or for its room. */
|
|
106
|
+
function explicitMaster(rooms, room, cam) {
|
|
107
|
+
return cam.masterHost || cam.mountedOn || room.camerasSlavedTo ? lensMaster(rooms, room, cam) : undefined;
|
|
108
|
+
}
|
|
50
109
|
/**
|
|
51
110
|
* DO THESE LENSES BROADCAST AT ALL (reporter's ruling 2026-09-17,
|
|
52
111
|
* eJANPpm7qCaCAJBJw)?
|
|
@@ -75,6 +134,12 @@ export function cameraMasterHost(rooms, room) {
|
|
|
75
134
|
export function camerasBroadcast(rooms, room) {
|
|
76
135
|
if (!isWatched(room))
|
|
77
136
|
return false;
|
|
137
|
+
if (room.camerasWireless !== undefined && !onPole(room))
|
|
138
|
+
return room.camerasWireless;
|
|
139
|
+
// Any lens that broadcasts (lensBroadcasts) puts the cluster on the air.
|
|
140
|
+
const cams = room.ensureCameras();
|
|
141
|
+
if (cams.length > 0)
|
|
142
|
+
return cams.some(c => lensBroadcasts(rooms, room, c));
|
|
78
143
|
if (room.camerasWireless !== undefined)
|
|
79
144
|
return room.camerasWireless;
|
|
80
145
|
return !cameraMasterHost(rooms, room);
|
|
@@ -175,7 +240,7 @@ export function describeCamera(rooms, room, cam, viewer) {
|
|
|
175
240
|
const where = `up under the ceiling on the ${cam.wall} wall, three metres off the floor`;
|
|
176
241
|
if (cam.destroyed)
|
|
177
242
|
return `The ${cam.name}, ${where}: smashed -- a cracked housing hanging off its cable. (${cam.summary()})`;
|
|
178
|
-
const knows = cam.knownOffBy.has(viewer.name) ||
|
|
243
|
+
const knows = cam.knownOffBy.has(viewer.name) || canSnoopLens(rooms, room, cam, viewer);
|
|
179
244
|
const state = knows && cam.bricked ? ' Bricked -- dead glass, though nothing shows it from down here.'
|
|
180
245
|
: knows && cam.looped ? ' Looped -- it records an empty hallway for the house.'
|
|
181
246
|
: cam.damage > 0 ? ' Chipped, still turning.' : '';
|
|
@@ -200,9 +265,26 @@ export function camerasLive(rooms, room) {
|
|
|
200
265
|
// 2026-10-01): looped by Control Device, bricked by a Data Spike
|
|
201
266
|
// (commands/disable.ts), or shot off the wall (commands/attack.ts).
|
|
202
267
|
// One lens still recording is one lens too many.
|
|
268
|
+
//
|
|
269
|
+
// AND EACH LENS ANSWERS TO ITS OWN MASTER (lensMaster): cracking the
|
|
270
|
+
// shop's host blinds the lens slaved to it and not its neighbour.
|
|
203
271
|
const cams = room.ensureCameras();
|
|
204
|
-
if (cams.length > 0
|
|
272
|
+
if (cams.length > 0)
|
|
273
|
+
return cams.some(c => lensLive(rooms, room, c));
|
|
274
|
+
return roomFeedsLive(rooms, room);
|
|
275
|
+
}
|
|
276
|
+
/** Is this one lens still recording for the house? */
|
|
277
|
+
export function lensLive(rooms, room, cam) {
|
|
278
|
+
if (!cam.working)
|
|
205
279
|
return false;
|
|
280
|
+
const master = explicitMaster(rooms, room, cam);
|
|
281
|
+
if (master)
|
|
282
|
+
return !master.hostCracked;
|
|
283
|
+
return roomFeedsLive(rooms, room);
|
|
284
|
+
}
|
|
285
|
+
/** The engine's default for lenses nobody wired: the room's own node, or
|
|
286
|
+
* -- a hostless watched room riding the site network -- any host. */
|
|
287
|
+
function roomFeedsLive(rooms, room) {
|
|
206
288
|
if (room.hasNode)
|
|
207
289
|
return !room.hostCracked;
|
|
208
290
|
const hosts = Object.values(rooms).filter(r => r.hasNode);
|
|
@@ -230,12 +312,16 @@ export function liveFootage(rooms, camSeen) {
|
|
|
230
312
|
const room = Object.values(rooms).find(r => r.name === name);
|
|
231
313
|
if (!room || !camerasLive(rooms, room))
|
|
232
314
|
continue;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
315
|
+
// Each live lens's footage is on ITS master (lensMaster).
|
|
316
|
+
const cams = room.ensureCameras().filter(c => lensLive(rooms, room, c));
|
|
317
|
+
const masters = new Set(cams.length > 0 ? cams.map(c => lensMaster(rooms, room, c)) : [cameraMasterHost(rooms, room)]);
|
|
318
|
+
for (const master of masters) {
|
|
319
|
+
if (!master)
|
|
320
|
+
continue;
|
|
321
|
+
const list = byHost.get(master) ?? [];
|
|
322
|
+
list.push(room);
|
|
323
|
+
byHost.set(master, list);
|
|
324
|
+
}
|
|
239
325
|
}
|
|
240
326
|
let best;
|
|
241
327
|
for (const [host, seen] of byHost) {
|
|
@@ -251,9 +337,108 @@ export function liveFootage(rooms, camSeen) {
|
|
|
251
337
|
export function canSnoopFeeds(rooms, room, viewer) {
|
|
252
338
|
if (!isWatched(room))
|
|
253
339
|
return false;
|
|
340
|
+
const cams = room.ensureCameras();
|
|
341
|
+
if (cams.length > 0)
|
|
342
|
+
return cams.some(c => canSnoopLens(rooms, room, c, viewer));
|
|
343
|
+
return canSnoopRoom(rooms, room, viewer);
|
|
344
|
+
}
|
|
345
|
+
/** This lens answers to the viewer: its master cracked or marked by them. */
|
|
346
|
+
export function canSnoopLens(rooms, room, cam, viewer) {
|
|
347
|
+
if (!isWatched(room))
|
|
348
|
+
return false;
|
|
349
|
+
const master = explicitMaster(rooms, room, cam);
|
|
350
|
+
if (master)
|
|
351
|
+
return master.hostCracked || (master.hostMarksBy.get(viewer.name) ?? 0) > 0;
|
|
352
|
+
return canSnoopRoom(rooms, room, viewer);
|
|
353
|
+
}
|
|
354
|
+
/** The engine's default: the room's own node, or any of the site's hosts. */
|
|
355
|
+
function canSnoopRoom(rooms, room, viewer) {
|
|
254
356
|
if (room.hasNode) {
|
|
255
357
|
return room.hostCracked || (room.hostMarksBy.get(viewer.name) ?? 0) > 0;
|
|
256
358
|
}
|
|
257
359
|
return Object.values(rooms).some(r => r.hasNode && (r.hostCracked || (r.hostMarksBy.get(viewer.name) ?? 0) > 0));
|
|
258
360
|
}
|
|
361
|
+
/** THE LOWEST HOST RATING THAT RUNS CABLE UNDER A STREET (user ruling
|
|
362
|
+
* 2026-10-06; SR5 p.247 puts "local police" at Host Rating 5-6). A
|
|
363
|
+
* pole camera -- one on an outdoor area, not on a building -- slaved to
|
|
364
|
+
* a host below it must broadcast: nobody else hardlines a lamp-post. */
|
|
365
|
+
export const POLE_HARDLINE_RATING = 5;
|
|
366
|
+
/** A lens on an outdoor area: on a pole, not on a building. */
|
|
367
|
+
export function onPole(room) {
|
|
368
|
+
return room.outdoors === true;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* DOES THIS ONE LENS BROADCAST? A pole camera on a host below
|
|
372
|
+
* POLE_HARDLINE_RATING must (it cannot be hardlined); otherwise the
|
|
373
|
+
* room's own switch, else the default -- slaved means dark.
|
|
374
|
+
*/
|
|
375
|
+
export function lensBroadcasts(rooms, room, cam) {
|
|
376
|
+
const master = lensMaster(rooms, room, cam);
|
|
377
|
+
// HARDLINED when it is bolted to its own host's building; otherwise an
|
|
378
|
+
// outdoor lens is cabled only by a police-grade host.
|
|
379
|
+
const wall = cam.mountedOn ? (rooms[cam.mountedOn] ?? Object.values(rooms).find(r => r.name === cam.mountedOn)) : undefined;
|
|
380
|
+
const hardlined = !!wall && !!master && buildingHost(wall) === master;
|
|
381
|
+
if (onPole(room) && master && !hardlined && master.hostRating < POLE_HARDLINE_RATING)
|
|
382
|
+
return true;
|
|
383
|
+
if (room.camerasWireless !== undefined)
|
|
384
|
+
return room.camerasWireless;
|
|
385
|
+
return !master;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* WHICH WORKING CAMERA SEES THIS ACTOR, IF ANY (user ruling 2026-10-06:
|
|
389
|
+
* "LOS only, no radius ... same rule everywhere. No LOS, no view").
|
|
390
|
+
* Canon gives a camera no range at all -- the sensor table's Camera row
|
|
391
|
+
* reads "--" (SR5 p.446) -- so there is none here: a live lens sees what
|
|
392
|
+
* it has a clear line to.
|
|
393
|
+
*
|
|
394
|
+
* - Indoors, a lens sees everyone in its own room.
|
|
395
|
+
* - Outdoors, a lens sees anyone on any outdoor area of the storey --
|
|
396
|
+
* its own included -- with no wall between them on the plane (district-plane.ts wallsBetween) --
|
|
397
|
+
* the pole on the corner watches the strip and the ground alike. A
|
|
398
|
+
* building's walls hide its inside, and it never sees out.
|
|
399
|
+
*
|
|
400
|
+
* Plain Invisibility is a mana illusion and never fools a camera (SR5
|
|
401
|
+
* p.289-291); Improved Invisibility, which would, is not in the engine.
|
|
402
|
+
*/
|
|
403
|
+
export function cameraSeeing(rooms, actor) {
|
|
404
|
+
const where = actor.bodyRoom ?? actor.currentLocation;
|
|
405
|
+
if (!where)
|
|
406
|
+
return undefined;
|
|
407
|
+
const ownRoom = () => {
|
|
408
|
+
for (const cam of isWatched(where) ? where.ensureCameras() : []) {
|
|
409
|
+
if (lensLive(rooms, where, cam))
|
|
410
|
+
return { room: where, cam };
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
};
|
|
414
|
+
if (!onPole(where))
|
|
415
|
+
return ownRoom();
|
|
416
|
+
// Nowhere on the plane to draw a line from (a body left behind, or a
|
|
417
|
+
// runner not yet stood on a cell): the area's own lenses, as before.
|
|
418
|
+
const plane = where.scene?.ensurePlane();
|
|
419
|
+
const mine = plane?.placements.get(where.name.trim().toLowerCase());
|
|
420
|
+
const at = actor.atCell;
|
|
421
|
+
if (!plane || !mine || !at || actor.bodyRoom)
|
|
422
|
+
return ownRoom();
|
|
423
|
+
const me = { x: mine.origin.x + at.x, y: mine.origin.y + at.y, z: mine.origin.z + at.z };
|
|
424
|
+
for (const room of Object.values(rooms)) {
|
|
425
|
+
if (!onPole(room) || !isWatched(room))
|
|
426
|
+
continue;
|
|
427
|
+
const there = plane.placements.get(room.name.trim().toLowerCase());
|
|
428
|
+
if (!there || there.origin.z !== me.z)
|
|
429
|
+
continue;
|
|
430
|
+
const grid = room.ensureGrid();
|
|
431
|
+
for (const cam of room.ensureCameras()) {
|
|
432
|
+
if (!lensLive(rooms, room, cam))
|
|
433
|
+
continue;
|
|
434
|
+
const perch = grid?.spotCells.get(cam.perch);
|
|
435
|
+
if (!perch)
|
|
436
|
+
continue;
|
|
437
|
+
const lens = { x: there.origin.x + perch.x, y: there.origin.y + perch.y, z: there.origin.z };
|
|
438
|
+
if (wallsBetween(plane, lens, me) === 0)
|
|
439
|
+
return { room, cam };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
259
444
|
//# sourceMappingURL=surveillance.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `use` was inventory-only. With the puzzle mechanic removed and its
|
|
5
5
|
* barriers becoming room-owned Devices, one verb now has to serve two
|
|
6
6
|
* populations that were never in the same namespace: the gear in your
|
|
7
|
-
* pack and the keypads
|
|
7
|
+
* pack and the keypads and grates bolted to the room.
|
|
8
8
|
*
|
|
9
9
|
* That makes resolution a LADDER, and the order is the whole design.
|
|
10
10
|
* Item lookup in this game is deliberately generous -- Inventory's
|
|
@@ -17,7 +17,7 @@ import { SPRITE_POWERS } from '../sprite-powers.js';
|
|
|
17
17
|
* leave them stuck on a word that works.
|
|
18
18
|
*/
|
|
19
19
|
export const HELP_CATEGORIES = [
|
|
20
|
-
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['fly'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
20
|
+
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['fly'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['leap'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
21
21
|
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['dumpster'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
|
|
22
22
|
{ title: 'combat', entries: [['attack'], ['kill'], ['quickdraw'], ['subdue', 'stun', 'knock'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
|
|
23
23
|
// The party layer: everyone who walks (or flies, or manifests) at
|
|
@@ -30,7 +30,7 @@ export const HELP_CATEGORIES = [
|
|
|
30
30
|
// entered like any other -- "mark jackpoint" takes its invitation
|
|
31
31
|
// (utilities/invitations.ts), "enter jackpoint" walks in. The bare word is still
|
|
32
32
|
// caught before the typo matcher (game.ts) so it cannot read as `jack`.
|
|
33
|
-
{ title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['upload'], ['transfer'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['wireless'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
33
|
+
{ title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['upload'], ['transfer'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['crack'], ['disarm'], ['pan'], ['wireless'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
34
34
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
35
35
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
36
36
|
// `sustain` shelves here and not under magic: the command is
|