@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.
Files changed (67) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +1 -1
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +60 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +5 -2
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +8 -2
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +4 -2
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +16 -150
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/crack.js +76 -0
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +42 -6
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/disarm.js +64 -0
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +23 -46
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +23 -6
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +15 -3
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +55 -17
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +15 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/leap.js +23 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +40 -0
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +16 -15
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +13 -3
  21. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +13 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/factories/authored-run.js +632 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/factories/catalog-item-seed.js +78 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/factories/job-order.js +7 -44
  25. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +8 -10
  26. package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +12 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +30 -52
  28. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +112 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +123 -3
  30. package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js +52 -4
  31. package/bundle/typescript/src/commands/game/sideQuest/game.js +17 -4
  32. package/bundle/typescript/src/commands/game/sideQuest/models/camera.js +8 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/models/device.js +14 -21
  34. package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +14 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +62 -9
  37. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +11 -0
  38. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +141 -3
  39. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -0
  40. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +18 -11
  41. package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +3 -0
  42. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +4 -2
  43. package/bundle/typescript/src/commands/game/sideQuest/utilities/astral-move.js +7 -2
  44. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +1 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +9 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/utilities/district-plane.js +109 -27
  47. package/bundle/typescript/src/commands/game/sideQuest/utilities/door-watch.js +70 -0
  48. package/bundle/typescript/src/commands/game/sideQuest/utilities/falling.js +27 -0
  49. package/bundle/typescript/src/commands/game/sideQuest/utilities/file-guard.js +120 -0
  50. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +10 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +12 -3
  52. package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +23 -5
  53. package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +29 -2
  54. package/bundle/typescript/src/commands/game/sideQuest/utilities/ledge-jump.js +86 -0
  55. package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +2 -1
  56. package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +29 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/utilities/owner-guard.js +77 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/utilities/path-run.js +4 -2
  59. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +49 -3
  61. package/bundle/typescript/src/commands/game/sideQuest/utilities/spider.js +50 -0
  62. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +14 -4
  63. package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +193 -8
  64. package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.js +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +2 -2
  66. package/bundle/typescript/src/commands/game/sideQuest/utilities/wards.js +116 -0
  67. 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 true;
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
- out.push({ x, y, z: 0 });
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
- for (const side of order) {
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 inBounds = next.x >= 0 && next.y >= 0 && next.x < grid.dims.x && next.y < grid.dims.y;
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) || canSnoopFeeds(rooms, room, viewer);
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 && !cams.some(c => c.working))
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
- const master = cameraMasterHost(rooms, room);
234
- if (!master)
235
- continue;
236
- const list = byHost.get(master) ?? [];
237
- list.push(room);
238
- byHost.set(master, list);
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, grates and wards bolted to the room.
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.391.0",
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.",