@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
|
@@ -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
|