@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
@@ -0,0 +1,632 @@
1
+ import { wallFixtureKindOf } from '../utilities/room-grid.js';
2
+ import { roomGridWire } from '../utilities/ways-out.js';
3
+ import { icTypeNamed, IC_TYPES, resolveICOrder } from '../utilities/ic.js';
4
+ import { deviceMaster } from '../utilities/wan.js';
5
+ import { isWatched, lensMaster, lensBroadcasts } from '../utilities/surveillance.js';
6
+ import { BIG_TEN } from '../utilities/grids.js';
7
+ import { roomAtCell } from '../utilities/district-plane.js';
8
+ import { kitSlugForTier } from '../npc-kits.js';
9
+ import { SceneSeedGenerator } from './scene-seed-generator.js';
10
+ import { VAULT_HOST_RATING } from './scene-chunks.js';
11
+ import { SIZE_SECURITY_BANDS, postsLieutenant, highestOpposingPool, payFor } from './job-order.js';
12
+ import { itemVocabulary } from './catalog-item-seed.js';
13
+ import { catalogRowFor } from '../utilities/catalog.js';
14
+ import { spiderRefusal, spidersIn } from '../utilities/spider.js';
15
+ import { NPC } from '../models/npc.js';
16
+ export { itemVocabulary };
17
+ /**
18
+ * THE BOOK'S NUMBERS, written over whatever the author put (SR5
19
+ * p.371-372, p.379-385). Works on a copy.
20
+ *
21
+ * - securityTier is clamped to the job size's Professional Rating band.
22
+ * - Every hostile (not the client) carries the tier's npc-kit, and its
23
+ * `combat` block is dropped: THE KIT IS THE STATLINE, as assembleScene
24
+ * rules for generated runs. From medium up one of them is posted as
25
+ * the lieutenant (p.380-381), preferring one in the prize's room.
26
+ * - The payout is payFor's, for `heads` runners.
27
+ *
28
+ * `heads` is the table's runner count -- known at launch; the editor's
29
+ * preview quotes it for one.
30
+ */
31
+ export function prepareAuthoredSeed(seed, heads = 1) {
32
+ const out = structuredClone(seed);
33
+ const notes = [];
34
+ const size = out.jobSize ?? 'small';
35
+ out.jobSize = size;
36
+ const band = SIZE_SECURITY_BANDS[size];
37
+ const declared = out.securityTier;
38
+ const tier = Math.max(band.min, Math.min(band.max, Number.isFinite(declared) ? Math.round(declared) : band.min));
39
+ if (declared !== tier)
40
+ notes.push(`securityTier ${declared ?? 'absent'} -> ${tier} (a ${size} job fields PR ${band.min === band.max ? band.min : `${band.min}-${band.max}`}).`);
41
+ out.securityTier = tier;
42
+ const npcs = (out.npcs ?? []).map(n => n.player);
43
+ const hostiles = npcs.filter(p => p.hostile === true && p.name !== out.client);
44
+ const winItem = (out.items ?? []).find(i => i.name === out.winCondition?.item);
45
+ const prizeRoom = out.winCondition?.target ?? winItem?.room;
46
+ const lieutenant = postsLieutenant(size) && hostiles.length > 0
47
+ ? (hostiles.find(p => prizeRoom !== undefined && p.startLocation === prizeRoom) ?? hostiles[0])
48
+ : undefined;
49
+ const kit = kitSlugForTier(tier);
50
+ for (const p of npcs) {
51
+ delete p.lieutenant;
52
+ if (!hostiles.includes(p))
53
+ continue;
54
+ // A SPIDER KEEPS THE AUTHOR'S STATLINE: no tier kit below the p.384
55
+ // elite lieutenant carries a deck or a Matrix skill, so stamping one
56
+ // would leave a decker who cannot hack (utilities/spider.ts). Until
57
+ // NPC sheets arrive, their `combat` block is what they hack with.
58
+ if (kit && !p.jackedInto) {
59
+ p.kit = kit;
60
+ delete p.combat;
61
+ }
62
+ if (p === lieutenant)
63
+ p.lieutenant = true;
64
+ }
65
+ // A data run's opposition is the vault host's IC (Host Rating x 2,
66
+ // p.247-248); a physical job's is the tier's statblock.
67
+ const vaultName = winItem?.plane === 'matrix' ? (out.winCondition?.target ?? winItem.room) : undefined;
68
+ const vault = vaultName ? (out.rooms ?? []).find(r => r.name === vaultName) : undefined;
69
+ const pool = vault ? 2 * (vault.hostRating ?? VAULT_HOST_RATING) : highestOpposingPool(tier, lieutenant !== undefined);
70
+ if (out.winCondition) {
71
+ const wrote = out.winCondition.payout;
72
+ const payout = payFor({
73
+ pool, heads: Math.max(1, heads), hostiles: hostiles.length, securityTier: tier,
74
+ dangerous: !!out.twist, wetwork: out.jobKind === 'assassination',
75
+ });
76
+ out.winCondition.payout = payout;
77
+ if (wrote !== undefined && wrote !== payout)
78
+ notes.push(`payout ${wrote} -> ${payout} (p.371).`);
79
+ }
80
+ return { seed: out, notes };
81
+ }
82
+ /** The rooms a fault's text names, longest name first so "Kettle Row
83
+ * Walk-Up" is not also read as "Kettle Row". */
84
+ function roomsNamed(message, seed) {
85
+ const names = (seed.rooms ?? []).map(r => r?.name).filter((n) => typeof n === 'string' && n.length > 0)
86
+ .sort((a, b) => b.length - a.length);
87
+ const found = [];
88
+ let rest = message;
89
+ for (const name of names) {
90
+ if (!rest.includes(name))
91
+ continue;
92
+ found.push(name);
93
+ rest = rest.split(name).join(' ');
94
+ }
95
+ return found;
96
+ }
97
+ /**
98
+ * WHAT AN AUTHOR'S ITEMS MUST BE (user 2026-10-06: "non-catalog items
99
+ * still need a category"). Authored runs only -- a generated seed's
100
+ * props are the generator's business. A hand-made item names a real
101
+ * engine Category; and a category the catalog stocks is not one a
102
+ * hand-made item may claim, because the catalog's row is the real thing
103
+ * (its stats, price and legality are the book's).
104
+ */
105
+ export function authoredItemFaults(seed) {
106
+ const vocab = itemVocabulary();
107
+ const byKey = new Map(vocab.categories.map(c => [c.key, c]));
108
+ const faults = [];
109
+ const rooms = new Map((seed.rooms ?? []).map(r => [r.name, r]));
110
+ for (const item of seed.items ?? []) {
111
+ const where = item.room ? [item.room] : [];
112
+ // AN ITEM LYING IN A ROOM LIES SOMEWHERE (user 2026-10-06: "I should
113
+ // be able to place items on the grid too"). Held items ride with their
114
+ // holder; paydata lives in the Matrix and has no floor.
115
+ const label = item.name ?? item.slug;
116
+ const room = item.room ? rooms.get(item.room) : undefined;
117
+ if (room && !item.heldBy && (item.plane ?? 'meat') === 'meat') {
118
+ if (!item.cell) {
119
+ faults.push({ check: 'coords', message: `"${label}" is not placed in "${room.name}" -- drag it onto its grid.`, rooms: where });
120
+ }
121
+ else if (room.footprint && (item.cell.x < 0 || item.cell.y < 0 || item.cell.x >= room.footprint.w || item.cell.y >= room.footprint.h)) {
122
+ faults.push({ check: 'coords', message: `"${label}" lies outside "${room.name}" (${item.cell.x},${item.cell.y}).`, rooms: where });
123
+ }
124
+ }
125
+ // HOLSTERED IS FOR WEAPONS AND ARMOR. A key is carried, never wielded.
126
+ if (item.equipped !== undefined) {
127
+ const key = item.slug ? catalogRowFor(item.slug)?.category : String(item.category);
128
+ if (key && byKey.get(key) && !byKey.get(key).equippable) {
129
+ faults.push({ check: 'items', message: `"${item.name ?? item.slug}" is not a weapon or armor -- it cannot be held in hand, worn or holstered.`, rooms: where });
130
+ }
131
+ }
132
+ // DATA LIVES ON A HOST, never in a pocket (scene-chunks' vault rule):
133
+ // a matrix-plane item sits in a room with a host, held by nobody.
134
+ if (item.plane === 'matrix') {
135
+ if (item.heldBy || !room?.hasNode) {
136
+ faults.push({ check: 'items', message: `"${label}" is host data but is not on a host -- put it on a room that has one.`, rooms: where });
137
+ }
138
+ }
139
+ // A FILE'S OWN DEFENCES (utilities/file-guard.ts) are for files: Edit
140
+ // File protects one (p.239) and a data bomb sits on one (p.242).
141
+ if (item.protection !== undefined || item.dataBomb !== undefined) {
142
+ if (item.plane !== 'matrix') {
143
+ faults.push({ check: 'items', message: `"${label}" is not host data -- only a file can be protected or carry a data bomb.`, rooms: where });
144
+ }
145
+ if (item.protection !== undefined && !(Number.isInteger(item.protection) && item.protection >= 1 && item.protection <= 12)) {
146
+ faults.push({ check: 'items', message: `"${label}"'s protection must be a whole rating from 1 to 12.`, rooms: where });
147
+ }
148
+ const bomb = item.dataBomb;
149
+ if (bomb && !(Number.isInteger(bomb.rating) && bomb.rating >= 1 && bomb.rating <= 12)) {
150
+ faults.push({ check: 'items', message: `"${label}"'s data bomb needs a whole rating from 1 to 12.`, rooms: where });
151
+ }
152
+ }
153
+ if (item.slug)
154
+ continue;
155
+ const cat = byKey.get(String(item.category));
156
+ if (!cat) {
157
+ faults.push({ check: 'items', message: `"${item.name}" has no real category ("${String(item.category ?? '')}") -- pick one.`, rooms: where });
158
+ }
159
+ else if (cat.catalog > 0) {
160
+ faults.push({ check: 'items', message: `"${item.name}" is a hand-made ${cat.label} -- the catalog stocks ${cat.catalog} of those; pick it from the catalog instead.`, rooms: where });
161
+ }
162
+ }
163
+ return faults;
164
+ }
165
+ /**
166
+ * FURNITURE IS WHERE THE AUTHOR PUTS IT (user 2026-10-06: "we shouldn't
167
+ * have the engine draw spots on its own"). In a hand-built run every spot
168
+ * carries its cells; one without them is a fault, not a cue for the grid
169
+ * to guess. Doors and windows are prose (wallFixtureKindOf) and need none.
170
+ */
171
+ export function authoredSpotFaults(seed) {
172
+ const faults = [];
173
+ for (const room of seed.rooms ?? []) {
174
+ for (const spot of room.spots ?? []) {
175
+ if (spot.cell || wallFixtureKindOf(spot.name))
176
+ continue;
177
+ faults.push({ check: 'coords', message: `"${spot.name}" in "${room.name}" is not placed -- draw it on the room's grid.`, rooms: [room.name] });
178
+ }
179
+ }
180
+ return faults;
181
+ }
182
+ /**
183
+ * EVERY BODY IS WHERE THE AUTHOR PUT IT. An NPC in the meat starts at a
184
+ * cell of its room (player.startCell); one without is a fault, and so is
185
+ * a cell outside the room's footprint. IC and spirits live on other
186
+ * planes and have no floor cell.
187
+ */
188
+ export function authoredNpcFaults(seed) {
189
+ const faults = [];
190
+ const rooms = new Map((seed.rooms ?? []).map(r => [r.name, r]));
191
+ for (const npc of seed.npcs ?? []) {
192
+ if ((npc.plane ?? 'meat') !== 'meat')
193
+ continue;
194
+ const p = npc.player;
195
+ const room = p.startLocation ? rooms.get(p.startLocation) : undefined;
196
+ const where = room ? [room.name] : [];
197
+ // A WATCHED DOOR is one of their own room's ways out (door-watch.ts).
198
+ const watches = p.watches;
199
+ if (room && watches && !(room.exits ?? []).some(e => String(e.direction).toLowerCase() === watches.toLowerCase())) {
200
+ faults.push({ check: 'coords', message: `${p.name} watches the ${watches} door of "${room.name}", which has none.`, rooms: where });
201
+ }
202
+ if (!room) {
203
+ faults.push({ check: 'coords', message: `${p.name} is in no room -- drag them onto a room's grid.`, rooms: where });
204
+ }
205
+ else if (!p.startCell) {
206
+ faults.push({ check: 'coords', message: `${p.name} is not placed in "${room.name}" -- drag them onto its grid.`, rooms: where });
207
+ }
208
+ else if (room.footprint && (p.startCell.x < 0 || p.startCell.y < 0
209
+ || p.startCell.x >= room.footprint.w || p.startCell.y >= room.footprint.h)) {
210
+ faults.push({ check: 'coords', message: `${p.name} stands outside "${room.name}" (${p.startCell.x},${p.startCell.y}).`, rooms: where });
211
+ }
212
+ }
213
+ return faults;
214
+ }
215
+ /**
216
+ * CAMERAS AND DEVICES WHERE THE AUTHOR PUT THEM. A lens hangs over a
217
+ * floor cell on the edge of its own wall; a placed device stands inside
218
+ * its room; anything slaved to a host names a room that has one.
219
+ */
220
+ export function authoredSecurityFaults(seed) {
221
+ const faults = [];
222
+ const hosts = new Set((seed.rooms ?? []).filter(r => r.hasNode).map(r => r.name));
223
+ for (const room of seed.rooms ?? []) {
224
+ const fp = room.footprint;
225
+ const where = [room.name];
226
+ for (const cam of room.cameras ?? []) {
227
+ const label = cam.name?.trim() || `the ${cam.wall} camera`;
228
+ if (!cam.cell) {
229
+ faults.push({ check: 'coords', message: `${label} in "${room.name}" is not hung -- put it on the room's grid.`, rooms: where });
230
+ }
231
+ else {
232
+ // North and west need no dimensions (row 0, column 0); south and
233
+ // east are read off the footprint when the room has one.
234
+ const c = cam.cell;
235
+ // OUTDOORS THERE IS NO WALL: a lens on a pole or a lamp-post stands
236
+ // on any cell of the area (user 2026-10-06, placing cameras on the
237
+ // ground and the strips from the plan).
238
+ const onWall = room.outdoors || (cam.wall === 'north' ? c.y === 0 : cam.wall === 'west' ? c.x === 0
239
+ : !fp || (cam.wall === 'south' ? c.y === fp.h - 1 : c.x === fp.w - 1));
240
+ const inside = c.x >= 0 && c.y >= 0 && (!fp || (c.x < fp.w && c.y < fp.h));
241
+ if (!inside || !onWall) {
242
+ faults.push({ check: 'coords', message: `${label} in "${room.name}" must hang on its ${cam.wall} wall (${c.x},${c.y} is not on it).`, rooms: where });
243
+ }
244
+ }
245
+ // EVERY CAMERA ANSWERS TO A HOST (user ruling 2026-10-06: "a camera
246
+ // can't exist on a room/pole with no host -- what's the point?").
247
+ // Its own link, else the room's, else the room's own host -- and a
248
+ // pole camera, failing those, the run's site host: the same default
249
+ // the engine wires (surveillance.ts cameraMasterHost) and the Matrix
250
+ // layer draws, so the editor never shows a link the audit denies.
251
+ const siteHost = room.outdoors && !cam.mountedOn ? (seed.rooms ?? []).find(r => r.hasNode)?.name : undefined;
252
+ // On a building's outside wall: that building's host.
253
+ const wallHost = cam.mountedOn ? seedBuildingHost(seed, cam.mountedOn) : undefined;
254
+ const master = cam.slavedTo ?? wallHost ?? room.camerasSlavedTo ?? (room.hasNode ? room.name : siteHost);
255
+ if (!master && cam.mountedOn) {
256
+ faults.push({ check: 'security', message: `${label} on the wall of "${cam.mountedOn}" answers to no host -- give "${cam.mountedOn}" a host, or slave the camera to one.`, rooms: [...where, cam.mountedOn] });
257
+ }
258
+ else if (!master) {
259
+ faults.push({ check: 'security', message: room.outdoors
260
+ ? `${label} in "${room.name}" is on a pole, and this run has no host for it to answer to -- give a building a host (police hosts are Host Rating 5+; below that it must broadcast).`
261
+ : `${label} in "${room.name}" answers to no host -- give the room a host, or slave the camera to one.`, rooms: where });
262
+ }
263
+ }
264
+ // AN OWNER IS SOMEONE: a room's owner names one of the run's NPCs.
265
+ if (room.owner && !(seed.npcs ?? []).some(n => n.player.name === room.owner)) {
266
+ faults.push({ check: 'security', message: `"${room.name}" is owned by "${room.owner}", who is not in the cast.`, rooms: where });
267
+ }
268
+ // A HOST HAS AN OWNER (user ruling 2026-10-06: "a room with a host
269
+ // must have an owner"). Someone runs it, knows its codes and answers
270
+ // for what is done to it (owner-guard.ts, NPC hostCodeNote).
271
+ if (room.hasNode && !room.owner) {
272
+ faults.push({ check: 'security', message: `"${room.name}" has a host but no owner -- pick who runs it (ROOM tab, "owner").`, rooms: where });
273
+ }
274
+ // THE HOST'S GRID is one the engine resolves (game.ts): public, local,
275
+ // or a Big Ten corp's global grid (p.233-234).
276
+ if (room.hostGrid && !gridVocabulary().includes(room.hostGrid)) {
277
+ faults.push({ check: 'security', message: `"${room.name}"'s host grid "${room.hostGrid}" is not a grid -- public, local, or a Big Ten global grid.`, rooms: where });
278
+ }
279
+ // THE IC LINEUP (Data Trails p.91-102): every program one the engine
280
+ // runs, each once. Canon lineups may be longer than the rating -- the
281
+ // cap is on programs running at once (p.247) -- so length is not a fault.
282
+ if (room.icOrder && room.icOrder.length > 0) {
283
+ if (!room.hasNode) {
284
+ faults.push({ check: 'security', message: `"${room.name}" lists IC but has no host.`, rooms: where });
285
+ }
286
+ const seen = new Set();
287
+ for (const n of room.icOrder) {
288
+ const t = icTypeNamed(n);
289
+ if (!t) {
290
+ faults.push({ check: 'security', message: `${n} (in "${room.name}"'s IC) is not one the engine runs yet.`, rooms: where });
291
+ }
292
+ else if (seen.has(t.name)) {
293
+ faults.push({ check: 'security', message: `${t.name} is listed twice in "${room.name}"'s IC -- a host runs one of each (p.247).`, rooms: where });
294
+ }
295
+ else {
296
+ seen.add(t.name);
297
+ }
298
+ }
299
+ }
300
+ if (room.camerasSlavedTo && !hosts.has(room.camerasSlavedTo)) {
301
+ faults.push({ check: 'security', message: `"${room.name}"'s cameras are slaved to "${room.camerasSlavedTo}", which has no host.`, rooms: where });
302
+ }
303
+ // ONE LENS, ITS OWN HOST (surveillance.ts lensMaster).
304
+ for (const cam of room.cameras ?? []) {
305
+ if (cam.slavedTo && !hosts.has(cam.slavedTo)) {
306
+ faults.push({ check: 'security', message: `The ${cam.name ?? `${cam.wall} camera`} in "${room.name}" is slaved to "${cam.slavedTo}", which has no host.`, rooms: where });
307
+ }
308
+ }
309
+ // NO LOCK BETWEEN TWO OUTDOOR SPACES (user 2026-10-06: "outside to
310
+ // outside links shouldn't have maglocks"): streets that meet share no
311
+ // wall, so there is no door to lock -- a device, or a keyed exit.
312
+ const outdoorTo = (dir) => {
313
+ if (!room.outdoors || !dir)
314
+ return undefined;
315
+ const exit = (room.exits ?? []).find(e => String(e.direction).toLowerCase() === dir.toLowerCase());
316
+ const to = exit ? (seed.rooms ?? []).find(r => r.name === exit.targetRoom) : undefined;
317
+ return to?.outdoors ? to : undefined;
318
+ };
319
+ for (const exit of room.exits ?? []) {
320
+ const to = outdoorTo(String(exit.direction));
321
+ if (to && exit.keyRequired) {
322
+ faults.push({ check: 'security', message: `"${room.name}" ${exit.direction} to "${to.name}" is open ground -- it cannot be locked.`, rooms: [room.name, to.name] });
323
+ }
324
+ }
325
+ for (const dev of room.devices ?? []) {
326
+ const to = dev.kind === 'lock' ? outdoorTo(dev.opensExit) : undefined;
327
+ if (to) {
328
+ faults.push({ check: 'security', message: `${dev.name} locks the way from "${room.name}" to "${to.name}", which is open ground -- there is no door to lock.`, rooms: [room.name, to.name] });
329
+ }
330
+ if (dev.slavedTo && !hosts.has(dev.slavedTo)) {
331
+ faults.push({ check: 'security', message: `${dev.name} is slaved to "${dev.slavedTo}", which has no host.`, rooms: where });
332
+ }
333
+ if (dev.cell && fp && (dev.cell.x < 0 || dev.cell.y < 0 || dev.cell.x >= fp.w || dev.cell.y >= fp.h)) {
334
+ faults.push({ check: 'coords', message: `${dev.name} stands outside "${room.name}" (${dev.cell.x},${dev.cell.y}).`, rooms: where });
335
+ }
336
+ }
337
+ }
338
+ return faults;
339
+ }
340
+ /**
341
+ * NOTHING STANDS IN A DOORWAY (user 2026-10-06: "it looks like I can draw
342
+ * furniture on a wall"). The grid claims every door's cell and the one
343
+ * cell it is reached from before anything else is placed; an author's
344
+ * furniture drawn over either seals the door. Read off the BUILT grid, so
345
+ * it is the engine's own door cells that are checked.
346
+ */
347
+ /**
348
+ * A DOOR OPENS ONTO WHAT IS ACTUALLY OUTSIDE IT (user 2026-10-06: "I should
349
+ * see an error if the exit of a building is to the master grid, but a
350
+ * street is on top of it"). Where a building's door to an outdoor area
351
+ * lands, read off the BUILT plane -- the engine's own door cell -- and the
352
+ * cell just past it must belong to the area the exit names (rooms over
353
+ * spaces over the ground, district-plane.ts roomAtCell). Otherwise the
354
+ * door opens onto some other area, and the exit should name that one.
355
+ */
356
+ /** The building a room belongs to's host, from the seed: its own, else the
357
+ * nearest through inside doors and stairs (surveillance.ts buildingHost). */
358
+ function seedBuildingHost(seed, roomName) {
359
+ const rooms = seed.rooms ?? [];
360
+ const byName = new Map(rooms.map(r => [r.name, r]));
361
+ const links = new Map();
362
+ const link = (a, b) => { (links.get(a) ?? links.set(a, new Set()).get(a)).add(b); };
363
+ for (const r of rooms)
364
+ for (const e of r.exits ?? []) {
365
+ link(r.name, e.targetRoom);
366
+ link(e.targetRoom, r.name);
367
+ }
368
+ const seen = new Set([roomName]);
369
+ let frontier = [roomName];
370
+ while (frontier.length > 0) {
371
+ const hit = frontier.find(n => byName.get(n)?.hasNode);
372
+ if (hit)
373
+ return hit;
374
+ const next = [];
375
+ for (const n of frontier)
376
+ for (const m of links.get(n) ?? []) {
377
+ if (seen.has(m) || byName.get(m)?.outdoors)
378
+ continue;
379
+ seen.add(m);
380
+ next.push(m);
381
+ }
382
+ frontier = next;
383
+ }
384
+ return undefined;
385
+ }
386
+ export function authoredDoorstepFaults(seed, built) {
387
+ const faults = [];
388
+ if (!built)
389
+ return faults;
390
+ const plane = built.ensurePlane();
391
+ const key = (n) => n.trim().toLowerCase();
392
+ const OUT = { north: [0, -1], south: [0, 1], west: [-1, 0], east: [1, 0] };
393
+ for (const room of seed.rooms ?? []) {
394
+ if (room.outdoors || !room.footprint)
395
+ continue;
396
+ const at = plane.placements.get(key(room.name));
397
+ if (!at)
398
+ continue;
399
+ for (const exit of room.exits ?? []) {
400
+ const dir = String(exit.direction).toLowerCase();
401
+ const step = OUT[dir];
402
+ const target = (seed.rooms ?? []).find(r => r.name === exit.targetRoom);
403
+ if (!step || !target?.outdoors)
404
+ continue;
405
+ const local = at.doors?.[dir];
406
+ if (!local)
407
+ continue;
408
+ // The door cell is in the wall ring; its doorstep one cell further out.
409
+ const ring = dir === 'north' ? { x: at.origin.x + local.x, y: at.origin.y - 1 }
410
+ : dir === 'south' ? { x: at.origin.x + local.x, y: at.origin.y + at.dims.y }
411
+ : dir === 'west' ? { x: at.origin.x - 1, y: at.origin.y + local.y }
412
+ : { x: at.origin.x + at.dims.x, y: at.origin.y + local.y };
413
+ const owner = roomAtCell(plane, { x: ring.x + step[0], y: ring.y + step[1], z: at.origin.z });
414
+ if (!owner || owner === key(target.name))
415
+ continue;
416
+ const ownerName = (seed.rooms ?? []).find(r => key(r.name) === owner)?.name ?? owner;
417
+ faults.push({
418
+ check: 'plan',
419
+ message: `${room.name}'s ${dir} door is linked to "${target.name}" but opens onto "${ownerName}" -- link it to "${ownerName}" (drag from the wall to it).`,
420
+ rooms: [room.name, ownerName],
421
+ });
422
+ }
423
+ }
424
+ return faults;
425
+ }
426
+ export function authoredDoorwayFaults(seed, built) {
427
+ const faults = [];
428
+ const INWARD = { north: [0, 1], south: [0, -1], west: [1, 0], east: [-1, 0] };
429
+ for (const seedRoom of seed.rooms ?? []) {
430
+ const placed = (seedRoom.spots ?? []).filter(s => s.cell);
431
+ if (placed.length === 0)
432
+ continue;
433
+ const grid = built?.getRoom(seedRoom.name)?.ensureGrid();
434
+ if (!grid)
435
+ continue;
436
+ const doorAt = new Map();
437
+ for (const [dir, c] of grid.exitCells.entries()) {
438
+ const d = String(dir);
439
+ if (!INWARD[d] || c.z !== 0)
440
+ continue;
441
+ doorAt.set(`${c.x},${c.y}`, d);
442
+ doorAt.set(`${c.x + INWARD[d][0]},${c.y + INWARD[d][1]}`, d);
443
+ }
444
+ for (const spot of placed) {
445
+ const w = Math.max(1, spot.size?.w ?? 1);
446
+ const h = Math.max(1, spot.size?.h ?? 1);
447
+ const hit = new Set();
448
+ for (let dx = 0; dx < w; dx++)
449
+ for (let dy = 0; dy < h; dy++) {
450
+ const d = doorAt.get(`${spot.cell.x + dx},${spot.cell.y + dy}`);
451
+ if (d)
452
+ hit.add(d);
453
+ }
454
+ for (const d of hit) {
455
+ faults.push({ check: 'coords', message: `"${spot.name}" blocks the ${d} door of "${seedRoom.name}" -- keep the doorway and the cell in front of it clear.`, rooms: [seedRoom.name] });
456
+ }
457
+ }
458
+ }
459
+ return faults;
460
+ }
461
+ /**
462
+ * SPIDERS (utilities/spider.ts): the host they ride exists, and they can
463
+ * actually jack in -- the BUILT NPC is asked the boot's own question
464
+ * (spiderRefusal), so a missing deck or DNI reads here exactly as the
465
+ * engine would refuse it.
466
+ */
467
+ export function authoredSpiderFaults(seed, built) {
468
+ const faults = [];
469
+ const rooms = new Map((seed.rooms ?? []).map(r => [r.name, r]));
470
+ for (const npc of seed.npcs ?? []) {
471
+ const p = npc.player;
472
+ if (!p.jackedInto)
473
+ continue;
474
+ const where = p.startLocation ? [p.startLocation] : [];
475
+ const host = rooms.get(p.jackedInto);
476
+ if (!host?.hasNode) {
477
+ faults.push({ check: 'matrix', message: `${p.name} is jacked into "${p.jackedInto}", which has no host.`, rooms: where });
478
+ continue;
479
+ }
480
+ const actor = built?.getActor(p.name);
481
+ if (actor instanceof NPC && actor.plane === 'meat') {
482
+ const why = spiderRefusal(actor, p.sim ?? 'cold');
483
+ if (why)
484
+ faults.push({ check: 'matrix', message: `${p.name} can't jack in: ${why}`, rooms: where });
485
+ }
486
+ }
487
+ return faults;
488
+ }
489
+ /**
490
+ * WARDS ARE A ROOM'S (user 2026-10-06; SR5 p.297-298, p.315-316): a
491
+ * warded room carries the ward's Force -- what pressing through and
492
+ * tearing at it are tested against -- and, if the author names one, the
493
+ * magician who raised it, who crosses it freely. A ward is never a
494
+ * device: that was the door-lock it used to be, and it stopped bodies
495
+ * that canon says walk through. Canon caps a ritual's Force nowhere in
496
+ * the ritual rules (p.296), so the audit asks only for a real Force.
497
+ */
498
+ export function authoredWardFaults(seed, built) {
499
+ const faults = [];
500
+ const cast = new Set((seed.npcs ?? []).map(n => n.player.name));
501
+ for (const room of seed.rooms ?? []) {
502
+ for (const d of room.devices ?? []) {
503
+ if (d.kind === 'ward') {
504
+ faults.push({ check: 'security', message: `"${d.name}" in "${room.name}" is a ward device -- a ward is the room's, not a thing in it. Remove it and ward the room it protects.`, rooms: [room.name] });
505
+ }
506
+ }
507
+ if (!room.warded)
508
+ continue;
509
+ const force = room.wardForce;
510
+ if (force === undefined || !Number.isInteger(force) || force < 1) {
511
+ faults.push({ check: 'security', message: `The ward over "${room.name}" needs a Force (a whole number, 1 or more) -- pressing through it is Magic + Charisma against Force x 2.`, rooms: [room.name] });
512
+ }
513
+ if (room.wardCreator !== undefined) {
514
+ if (!cast.has(room.wardCreator)) {
515
+ faults.push({ check: 'security', message: `The ward over "${room.name}" names ${room.wardCreator} as its creator, who is not in the cast.`, rooms: [room.name] });
516
+ }
517
+ else {
518
+ const actor = built?.getActor(room.wardCreator);
519
+ if (actor instanceof NPC && !actor.isAwakened()) {
520
+ faults.push({ check: 'security', message: `${room.wardCreator} can't have raised the ward over "${room.name}" -- a ward is a ritual, and they are mundane.`, rooms: [room.name] });
521
+ }
522
+ }
523
+ }
524
+ }
525
+ return faults;
526
+ }
527
+ /**
528
+ * THE GROUND AND THE SPACES ON IT (user rulings 2026-10-06): one ground,
529
+ * storey 0; outdoor spaces may overlap the ground but not each other on
530
+ * the same storey (rooms cut out of anything -- district-plane.ts
531
+ * roomAtCell ranks them).
532
+ */
533
+ export function authoredGroundFaults(seed) {
534
+ const faults = [];
535
+ const rooms = seed.rooms ?? [];
536
+ const grounds = rooms.filter(r => r.ground);
537
+ if (grounds.length > 1)
538
+ faults.push({ check: 'plan', message: `There can be only one ground -- ${grounds.map(r => `"${r.name}"`).join(', ')} are all marked as it.`, rooms: grounds.map(r => r.name) });
539
+ for (const g of grounds) {
540
+ if (!g.outdoors || (g.storey ?? 0) !== 0)
541
+ faults.push({ check: 'plan', message: `The ground "${g.name}" must be outdoors, on storey 0.`, rooms: [g.name] });
542
+ }
543
+ const spaces = rooms.filter(r => r.outdoors && !r.ground && r.footprint);
544
+ for (let i = 0; i < spaces.length; i++) {
545
+ for (let j = i + 1; j < spaces.length; j++) {
546
+ const a = spaces[i];
547
+ const b = spaces[j];
548
+ if ((a.storey ?? 0) !== (b.storey ?? 0))
549
+ continue;
550
+ const fa = a.footprint;
551
+ const fb = b.footprint;
552
+ if (fa.x < fb.x + fb.w && fb.x < fa.x + fa.w && fa.y < fb.y + fb.h && fb.y < fa.y + fa.h) {
553
+ faults.push({ check: 'plan', message: `"${a.name}" and "${b.name}" overlap -- two outdoor spaces may only overlap the ground, or each other on different storeys.`, rooms: [a.name, b.name] });
554
+ }
555
+ }
556
+ }
557
+ return faults;
558
+ }
559
+ export async function validateAuthoredRun(seed) {
560
+ const { faults, built } = await SceneSeedGenerator.auditSeed(prepareAuthoredSeed(seed).seed);
561
+ return [...authoredGroundFaults(seed), ...authoredItemFaults(seed), ...authoredSpotFaults(seed), ...authoredNpcFaults(seed), ...authoredSecurityFaults(seed), ...authoredDoorwayFaults(seed, built), ...authoredDoorstepFaults(seed, built), ...authoredSpiderFaults(seed, built), ...authoredWardFaults(seed, built), ...faults.map(f => ({ ...f, rooms: roomsNamed(f.message, seed) }))];
562
+ }
563
+ export function matrixWire(built) {
564
+ const rooms = built.getRooms();
565
+ const physical = Object.values(rooms).filter(r => r.roomType !== 'host');
566
+ const hosts = physical.filter(r => r.host).map(r => {
567
+ const host = r.host;
568
+ return {
569
+ room: r.name,
570
+ label: host.displayName ?? `${r.name.replace(/^the\s+/i, '')} host`,
571
+ rating: host.rating,
572
+ ...(host.gridSeed ? { grid: host.gridSeed } : {}),
573
+ files: host.files().map(f => f.name),
574
+ ic: resolveICOrder(host.icOrder).map(t => t.name),
575
+ icAuthored: (host.icOrder?.length ?? 0) > 0,
576
+ ...(r.owner ? { owner: r.owner } : {}),
577
+ // Who is jacked in and riding it (utilities/spider.ts).
578
+ spiders: built.allActors ? spidersIn(built, host).map(n => n.name) : [],
579
+ };
580
+ });
581
+ const links = [];
582
+ for (const r of physical) {
583
+ for (const d of r.devices ?? []) {
584
+ links.push({ kind: 'device', name: d.name, room: r.name, host: deviceMaster(rooms, r, d)?.name, wireless: d.wireless, silent: d.silent });
585
+ }
586
+ if (isWatched(r) && r.ensureCameras().length > 0) {
587
+ const byHost = new Map();
588
+ for (const c of r.ensureCameras()) {
589
+ const host = lensMaster(rooms, r, c)?.name;
590
+ byHost.set(host, [...(byHost.get(host) ?? []), c.name]);
591
+ }
592
+ for (const [host, lenses] of byHost) {
593
+ const n = lenses.length;
594
+ links.push({
595
+ kind: 'cameras', name: `${n} camera${n === 1 ? '' : 's'}`, room: r.name,
596
+ host, wireless: r.ensureCameras().some(c => lenses.includes(c.name) && lensBroadcasts(rooms, r, c)), silent: false, lenses,
597
+ });
598
+ }
599
+ }
600
+ }
601
+ return { hosts, links };
602
+ }
603
+ export async function previewAuthoredRun(seed, heads = 1) {
604
+ const prepared = prepareAuthoredSeed(seed, heads);
605
+ const { faults, built } = await SceneSeedGenerator.auditSeed(prepared.seed);
606
+ const rooms = {};
607
+ for (const room of Object.values(built?.getRooms() ?? {})) {
608
+ const wire = roomGridWire(room);
609
+ if (wire)
610
+ rooms[room.name] = wire;
611
+ }
612
+ return {
613
+ faults: [...authoredGroundFaults(seed), ...authoredItemFaults(seed), ...authoredSpotFaults(seed), ...authoredNpcFaults(seed), ...authoredSecurityFaults(seed), ...authoredDoorwayFaults(seed, built), ...authoredDoorstepFaults(seed, built), ...authoredSpiderFaults(seed, built), ...authoredWardFaults(seed, built), ...faults.map(f => ({ ...f, rooms: roomsNamed(f.message, seed) }))],
614
+ rooms,
615
+ ...(built ? { matrix: matrixWire(built) } : {}),
616
+ payout: prepared.seed.winCondition?.payout,
617
+ securityTier: prepared.seed.securityTier,
618
+ notes: prepared.notes,
619
+ };
620
+ }
621
+ /** THE IC THE ENGINE RUNS, in its default launch order -- the editor's
622
+ * lineup picker (an author may order any subset; Data Trails p.91-102). */
623
+ export function icVocabulary() {
624
+ return IC_TYPES.map(t => t.name);
625
+ }
626
+ /** THE GRIDS A HOST CAN SIT ON (SR5 p.233-234): the public grid, the
627
+ * district's local grid, or one of the Big Ten's global grids -- the
628
+ * room seed's hostGrid values, as game.ts resolves them. */
629
+ export function gridVocabulary() {
630
+ return ['public', 'local', ...BIG_TEN.map(c => `global:${c}`)];
631
+ }
632
+ //# sourceMappingURL=authored-run.js.map