@maka/maka-cli 5.391.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 +13 -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 +49 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spider.js +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +14 -4
- 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
|
@@ -245,8 +245,9 @@ export function mulberry32(seed) {
|
|
|
245
245
|
function cellExists(grid, c, levelCells) {
|
|
246
246
|
if (c.x < 0 || c.y < 0 || c.x >= grid.dims.x || c.y >= grid.dims.y)
|
|
247
247
|
return false;
|
|
248
|
+
// A HOLE in an outdoor area is a cell something else owns (GROUND_HOLES).
|
|
248
249
|
if (c.z === 0)
|
|
249
|
-
return
|
|
250
|
+
return !GROUND_HOLES.get(grid)?.has(`${c.x},${c.y}`);
|
|
250
251
|
return levelCells.get(c.z)?.has(`${c.x},${c.y}`) ?? false;
|
|
251
252
|
}
|
|
252
253
|
/** Whether `c` is a cell of this grid at all -- in bounds, and on a
|
|
@@ -400,10 +401,12 @@ export function distanceMeters(a, b) {
|
|
|
400
401
|
* time (the const is bound long before any consumer runs). */
|
|
401
402
|
export function levelFootprint(grid, z) {
|
|
402
403
|
if (z === 0) {
|
|
404
|
+
const holes = GROUND_HOLES.get(grid);
|
|
403
405
|
const out = [];
|
|
404
406
|
for (let x = 0; x < grid.dims.x; x++) {
|
|
405
407
|
for (let y = 0; y < grid.dims.y; y++)
|
|
406
|
-
|
|
408
|
+
if (!holes?.has(`${x},${y}`))
|
|
409
|
+
out.push({ x, y, z: 0 });
|
|
407
410
|
}
|
|
408
411
|
return out;
|
|
409
412
|
}
|
|
@@ -573,6 +576,8 @@ export function pathClimbSteps(grid, path) {
|
|
|
573
576
|
* cellExists needs but doesn't belong on the public IRoomGrid shape --
|
|
574
577
|
* associated by identity so callers never juggle it themselves. */
|
|
575
578
|
const LEVEL_CELLS = new WeakMap();
|
|
579
|
+
/** An outdoor area's holes, per grid (IGridSynthesisInput.holes). */
|
|
580
|
+
const GROUND_HOLES = new WeakMap();
|
|
576
581
|
const WALL_REGIONS = {
|
|
577
582
|
north: (d) => ({ x0: 0, x1: d.x - 1, y0: 0, y1: 0 }),
|
|
578
583
|
south: (d) => ({ x0: 0, x1: d.x - 1, y0: d.y - 1, y1: d.y - 1 }),
|
|
@@ -647,6 +652,8 @@ export function synthesizeGrid(input) {
|
|
|
647
652
|
exitCells: new Map(), terrain: new Map(),
|
|
648
653
|
};
|
|
649
654
|
LEVEL_CELLS.set(grid, levelCells);
|
|
655
|
+
if (input.holes && input.holes.size > 0)
|
|
656
|
+
GROUND_HOLES.set(grid, input.holes);
|
|
650
657
|
const exists = (c) => cellExists(grid, c, levelCells);
|
|
651
658
|
const freeCellNear = (region, z) => {
|
|
652
659
|
const candidates = [];
|
|
@@ -706,6 +713,11 @@ export function synthesizeGrid(input) {
|
|
|
706
713
|
// a room's width away from.
|
|
707
714
|
cell = nearestFreeOnWall(cell, wallSide, occupied, dims, exists);
|
|
708
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
|
+
}
|
|
709
721
|
else {
|
|
710
722
|
// up/down (and anything exotic): no natural wall -- an open
|
|
711
723
|
// z=0 cell near the centroid, same free-cell picker as unplaced
|
|
@@ -740,6 +752,28 @@ export function synthesizeGrid(input) {
|
|
|
740
752
|
if (exists(approach))
|
|
741
753
|
claim(approach);
|
|
742
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
|
+
}
|
|
743
777
|
// Seat each exit-serving spot, now that we know how many exits it
|
|
744
778
|
// serves. ONE exit: the spot IS that doorway, so it takes the cell
|
|
745
779
|
// (Mama Wu's "door" -> its east wall). SEVERAL: the spot is the hub
|
|
@@ -751,6 +785,8 @@ export function synthesizeGrid(input) {
|
|
|
751
785
|
// directions also come off the stalls"). The doorways stay on their
|
|
752
786
|
// own walls; the hub sits where all of them are plausibly reachable.
|
|
753
787
|
for (const [spotName, served] of exitCellsBySpot.entries()) {
|
|
788
|
+
if (fixed.has(spotName))
|
|
789
|
+
continue; // the author already put it somewhere
|
|
754
790
|
if (served.length === 1) {
|
|
755
791
|
// FURNITURE BESIDE THE DOOR, NOT IN IT (XanDMPmRrbJQ8ta8e,
|
|
756
792
|
// 2026-09-12: "the corner booth is right in front of the door
|
|
@@ -910,6 +946,9 @@ export function synthesizeGrid(input) {
|
|
|
910
946
|
grid.spotFootprints.set(name, []);
|
|
911
947
|
continue;
|
|
912
948
|
}
|
|
949
|
+
// An authored spot already has the footprint its author drew.
|
|
950
|
+
if (fixed.has(name))
|
|
951
|
+
continue;
|
|
913
952
|
if (name === OPEN_FLOOR_NAME || exitKeys.has(key(anchor))) {
|
|
914
953
|
grid.spotFootprints.set(name, [anchor]);
|
|
915
954
|
continue;
|
|
@@ -1036,7 +1075,12 @@ function placePerches(grid, input, base, levelCells, occupied, furnitureKeys) {
|
|
|
1036
1075
|
const wanted = perch.place && SIDES.includes(perch.place) ? perch.place : undefined;
|
|
1037
1076
|
const order = wanted ? [wanted, ...SIDES.filter(s => s !== wanted)] : SIDES;
|
|
1038
1077
|
let cell;
|
|
1039
|
-
|
|
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) {
|
|
1040
1084
|
const free = wallCells(side).filter(c => usable(c.x, c.y));
|
|
1041
1085
|
// Prefer clear of the claimed cells (an exit's approach, say) --
|
|
1042
1086
|
// standing room under the lens -- but take what there is.
|
|
@@ -1230,6 +1274,8 @@ export function serializeRoomGrid(grid, roomName, plane) {
|
|
|
1230
1274
|
vertical,
|
|
1231
1275
|
terrain,
|
|
1232
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 }; }) } : {}),
|
|
1233
1279
|
origin: plane?.origin,
|
|
1234
1280
|
walls: plane?.walls,
|
|
1235
1281
|
};
|
|
@@ -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
|
|
@@ -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)) {
|
|
@@ -2540,7 +2546,8 @@ export function describeSpotRoster(room, viewer) {
|
|
|
2540
2546
|
* - AN ASTRAL BARRIER. "Astral barriers block astral travel, and you
|
|
2541
2547
|
* might run into one before you realize that it's in your way" --
|
|
2542
2548
|
* Room.warded, the same flag the aura rules read. Both ways: a ward
|
|
2543
|
-
* 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).
|
|
2544
2551
|
*
|
|
2545
2552
|
* Returns WORLD cells, not room-local ones, because an astral form is
|
|
2546
2553
|
* not in a room in the sense a body is -- it is at a point on the
|
|
@@ -2550,7 +2557,10 @@ export function describeSpotRoster(room, viewer) {
|
|
|
2550
2557
|
export function astralStepFrom(plane, from, direction, count, roomAt,
|
|
2551
2558
|
/** Whose MANA BARRIER stands on a cell, if any (Scene.barrierAt --
|
|
2552
2559
|
* the creator's own weave already excluded, p.315). */
|
|
2553
|
-
manaBarrierAt
|
|
2560
|
+
manaBarrierAt,
|
|
2561
|
+
/** Does this warded room's ward stop THIS form (wards.ts wardHolds)?
|
|
2562
|
+
* Unset: every ward stops everyone. */
|
|
2563
|
+
wardStops) {
|
|
2554
2564
|
/**
|
|
2555
2565
|
* UP AND DOWN ARE DIRECTIONS OUT HERE (p.313-314: an astral form
|
|
2556
2566
|
* "can move in any direction" and is "not limited by gravity").
|
|
@@ -2571,7 +2581,7 @@ manaBarrierAt) {
|
|
|
2571
2581
|
if (owner === undefined)
|
|
2572
2582
|
return undefined;
|
|
2573
2583
|
const room = roomAt(owner);
|
|
2574
|
-
return room?.warded === true ? room : undefined;
|
|
2584
|
+
return room?.warded === true && (wardStops?.(room) ?? true) ? room : undefined;
|
|
2575
2585
|
};
|
|
2576
2586
|
let cur = from;
|
|
2577
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
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { rollPool, formatRoll } from './dice.js';
|
|
2
|
+
import { astralLimit } from './limits.js';
|
|
3
|
+
import { pierces } from './barrier-geometry.js';
|
|
4
|
+
/** Does this room's ward stop this form? Not its creator, and not the
|
|
5
|
+
* form holding a pass it just pressed for. */
|
|
6
|
+
export function wardHolds(room, actor) {
|
|
7
|
+
if (!room?.warded)
|
|
8
|
+
return false;
|
|
9
|
+
if (room.wardCreator !== undefined && room.wardCreator === actor.name)
|
|
10
|
+
return false;
|
|
11
|
+
return actor.wardPass !== room.name;
|
|
12
|
+
}
|
|
13
|
+
/** The ward an astral form crosses going from one room to another --
|
|
14
|
+
* entering `to`'s, or leaving `from`'s -- or undefined. */
|
|
15
|
+
export function wardBetween(from, to, actor) {
|
|
16
|
+
if (from === to)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (wardHolds(to, actor))
|
|
19
|
+
return to;
|
|
20
|
+
if (wardHolds(from, actor))
|
|
21
|
+
return from;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* PRESS THROUGH (p.316): Magic + Charisma [Astral] v. Force x 2. Any net
|
|
26
|
+
* hits and the form is through -- it holds a pass for THIS ward until
|
|
27
|
+
* the crossing is made. A breach is something the creator knows (p.357).
|
|
28
|
+
*/
|
|
29
|
+
export function pressThroughWard(scene, logger, actor, ward) {
|
|
30
|
+
const press = rollPool(actor.magic + actor.charisma, astralLimit(actor));
|
|
31
|
+
const hold = rollPool(ward.wardForce * 2);
|
|
32
|
+
const meta = `Press through the ward (p.316): Magic + Charisma ${formatRoll(press)} v. Force ${ward.wardForce} x 2 ${formatRoll(hold)}`;
|
|
33
|
+
if (press.hits - hold.hits <= 0) {
|
|
34
|
+
actor.performAction('is stopped by a ward', ward.name);
|
|
35
|
+
return { through: false, meta };
|
|
36
|
+
}
|
|
37
|
+
actor.wardPass = ward.name;
|
|
38
|
+
actor.performAction('presses through a ward', ward.name);
|
|
39
|
+
alertWard(scene, logger, ward, actor, 'breached');
|
|
40
|
+
return { through: true, meta };
|
|
41
|
+
}
|
|
42
|
+
/** Spend the pass once the crossing it bought is made. */
|
|
43
|
+
export function spendWardPass(actor) {
|
|
44
|
+
actor.wardPass = undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* THE CREATOR KNOWS (p.316, p.357): "will know when it has been
|
|
48
|
+
* breached, attacked, or destroyed and will inform whoever is to be
|
|
49
|
+
* contacted in the protected organization". The site goes on alert the
|
|
50
|
+
* way a tripped device puts it there; a creator on site hears it.
|
|
51
|
+
*/
|
|
52
|
+
export function alertWard(scene, logger, ward, actor, how) {
|
|
53
|
+
scene.raiseAlarm(`the ward over ${ward.name} was ${how}`);
|
|
54
|
+
scene.addWorldEvent(`The ward over ${ward.name} was ${how} -- whoever raised it knows.`);
|
|
55
|
+
const creator = ward.wardCreator ? scene.getPlayers().find(p => p.name === ward.wardCreator) : undefined;
|
|
56
|
+
if (creator && creator !== actor) {
|
|
57
|
+
logger?.log(`Your ward over ${ward.name} has been ${how}${how === 'attacked' ? ` (${ward.wardDamage}/${ward.wardForce})` : ''} (p.316).`, { actor: creator.name });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* HITTING A WARD from the astral (p.316, "Getting Around Mana Barriers"):
|
|
62
|
+
* the same unopposed shape as a mana barrier spell -- the attack's hits
|
|
63
|
+
* add to the DV, a DV that does not exceed Armor does nothing (p.197),
|
|
64
|
+
* the rest is resisted with Structure + Armor, both equal to the Force.
|
|
65
|
+
* Structure filled is a ward torn down, for the rest of the run.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveWardStrike(scene, logger, attacker, ward) {
|
|
68
|
+
const world = [];
|
|
69
|
+
const meta = [];
|
|
70
|
+
const force = ward.wardForce;
|
|
71
|
+
const profile = attacker.getWeaponProfile();
|
|
72
|
+
const roll = rollPool(attacker.getAttackPool(), attacker.getAttackLimit(null));
|
|
73
|
+
meta.push(`Attack on the ward over ${ward.name} (unopposed, p.197): ${formatRoll(roll)}`);
|
|
74
|
+
attacker.sneaking = false;
|
|
75
|
+
if (roll.criticalGlitch) {
|
|
76
|
+
world.push(`${attacker.name} goes for the ward over ${ward.name} -- and it goes catastrophically wrong.`);
|
|
77
|
+
return { world, meta, destroyed: false };
|
|
78
|
+
}
|
|
79
|
+
const dv = profile.damage + roll.hits;
|
|
80
|
+
const ap = profile.armorPen ?? 0;
|
|
81
|
+
meta.push(` DV ${profile.damage} + ${roll.hits} = ${dv} v. Armor ${force} (Force, p.316)`);
|
|
82
|
+
if (!pierces(dv, force, ap)) {
|
|
83
|
+
world.push(`${attacker.name} strikes the ward over ${ward.name} -- the lattice drinks it. Too weak to mark it (DV ${dv} against Armor ${force}, p.197).`);
|
|
84
|
+
alertWard(scene, logger, ward, attacker, 'attacked');
|
|
85
|
+
return { world, meta, destroyed: false };
|
|
86
|
+
}
|
|
87
|
+
const armor = Math.max(0, force + ap);
|
|
88
|
+
const soak = rollPool(force + armor);
|
|
89
|
+
const dealt = Math.max(0, dv - soak.hits);
|
|
90
|
+
meta.push(` Resist: Structure ${force} + Armor ${armor}: ${formatRoll(soak)} -- ${dealt} box${dealt === 1 ? '' : 'es'} through`);
|
|
91
|
+
ward.wardDamage += dealt;
|
|
92
|
+
logger.write(`${attacker.name} dealt ${dealt} boxes to the ward over ${ward.name} (${ward.wardDamage}/${force}).`);
|
|
93
|
+
if (ward.wardDamage < force) {
|
|
94
|
+
world.push(`${attacker.name} strikes the ward over ${ward.name} -- ${dealt > 0 ? `${dealt} box${dealt === 1 ? '' : 'es'}` : 'it holds, unmarked'}. (${ward.wardDamage}/${force})`);
|
|
95
|
+
alertWard(scene, logger, ward, attacker, 'attacked');
|
|
96
|
+
return { world, meta, destroyed: false };
|
|
97
|
+
}
|
|
98
|
+
ward.warded = false;
|
|
99
|
+
ward.wardDamage = 0;
|
|
100
|
+
world.push(`${attacker.name} tears the ward over ${ward.name} apart -- the lattice frays to nothing, and the astral there is open ground.`);
|
|
101
|
+
alertWard(scene, logger, ward, attacker, 'destroyed');
|
|
102
|
+
scene.addWorldEvent(`${attacker.name} brought down the ward over ${ward.name}.`);
|
|
103
|
+
return { world, meta, destroyed: true };
|
|
104
|
+
}
|
|
105
|
+
/** End of a Combat Turn: every standing ward is whole again (p.316). */
|
|
106
|
+
export function mendWards(rooms) {
|
|
107
|
+
const mended = [];
|
|
108
|
+
for (const r of rooms) {
|
|
109
|
+
if (!r.warded || r.wardDamage <= 0)
|
|
110
|
+
continue;
|
|
111
|
+
r.wardDamage = 0;
|
|
112
|
+
mended.push(r);
|
|
113
|
+
}
|
|
114
|
+
return mended;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=wards.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.392.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|