@maka/maka-cli 5.390.0 → 5.392.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +60 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +5 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +16 -150
- package/bundle/typescript/src/commands/game/sideQuest/commands/crack.js +76 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +42 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/disarm.js +64 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +23 -46
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +23 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +15 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +55 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +15 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/leap.js +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +40 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +16 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +17 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/authored-run.js +632 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/catalog-item-seed.js +78 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/job-order.js +7 -44
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +8 -10
- package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +30 -52
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +112 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +123 -3
- package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js +52 -4
- package/bundle/typescript/src/commands/game/sideQuest/game.js +17 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/camera.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js +14 -21
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +62 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +141 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +18 -11
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/astral-move.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/district-plane.js +109 -27
- package/bundle/typescript/src/commands/game/sideQuest/utilities/door-watch.js +70 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/falling.js +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/file-guard.js +120 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +23 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +29 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ledge-jump.js +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/owner-guard.js +77 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/path-run.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +66 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +8 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spider.js +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +20 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +193 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/wards.js +116 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Category, Size, Rating } from '../types/shared/item-enum.js';
|
|
2
|
+
import { catalogGear, catalogGearDef, catalogRowFor } from '../utilities/catalog.js';
|
|
3
|
+
import { NPC_EQUIP_CATEGORIES } from '../npc-kits.js';
|
|
4
|
+
/**
|
|
5
|
+
* A SEED ITEM BY CATALOG SLUG (player-built runs, 2026-10-06: "sourcing
|
|
6
|
+
* real catalog-backed loot"). `slug` names a gear row on the site's
|
|
7
|
+
* catalog; the item takes that row's NAME and its physical fields, so
|
|
8
|
+
* the catalog's stats -- a weapon's DV and AP, a jacket's armor, a
|
|
9
|
+
* deck's attributes, read by name through catalogRowFor -- reach it, and
|
|
10
|
+
* its price, availability and legality are the book's. What the seed
|
|
11
|
+
* still decides is where it is (room / heldBy / atSpot / owner) and how
|
|
12
|
+
* it is carried.
|
|
13
|
+
*
|
|
14
|
+
* EXACT, never fuzzy: catalogGearDef falls back to findCatalogItem, which
|
|
15
|
+
* is right for a player typing "predator" at a counter and wrong for an
|
|
16
|
+
* author's saved run -- a near miss would silently swap the gun.
|
|
17
|
+
*
|
|
18
|
+
* Throws on an unknown slug, so synthesis (and so the editor's audit)
|
|
19
|
+
* reports it rather than shipping an empty-handed guard.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveCatalogItemSeed(seed) {
|
|
22
|
+
if (!seed.slug)
|
|
23
|
+
return seed;
|
|
24
|
+
const row = catalogRowFor(seed.slug);
|
|
25
|
+
const def = row && row.slug === seed.slug ? catalogGearDef(row.slug) : undefined;
|
|
26
|
+
if (!def) {
|
|
27
|
+
throw new Error(`items: "${seed.slug}" is not a catalog gear slug -- pick the item from the catalog.`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...seed,
|
|
31
|
+
name: def.name,
|
|
32
|
+
description: seed.description?.trim() ? seed.description : def.description,
|
|
33
|
+
category: enumKey(Category, def.category),
|
|
34
|
+
size: enumKey(Size, def.size),
|
|
35
|
+
rating: enumKey(Rating, def.rating),
|
|
36
|
+
shape: def.shape,
|
|
37
|
+
color: def.color,
|
|
38
|
+
texture: def.texture,
|
|
39
|
+
weight: def.weight,
|
|
40
|
+
...(def.price !== undefined ? { price: def.price } : {}),
|
|
41
|
+
// A DECK WITH NO CONFIG IS A DECK WITH A ZEROED ARRAY (repro-scene's
|
|
42
|
+
// catalogSeedFor has the same rule): the identity order unless the
|
|
43
|
+
// author set one (p.227).
|
|
44
|
+
...(def.category === Category.Cyberdeck && !seed.deckOrder ? { deckOrder: [0, 1, 2, 3] } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function enumKey(e, value) {
|
|
48
|
+
const entries = e;
|
|
49
|
+
return Object.keys(entries).find(k => entries[k] === value) ?? 'Unknown';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* THE ITEM VOCABULARY, for an editor's dropdowns: every Category, Size
|
|
53
|
+
* and Rating key the engine accepts, and which categories the catalog
|
|
54
|
+
* actually stocks. A category with rows is one a hand-made item may not
|
|
55
|
+
* claim (the catalog's row is the real thing); one without -- a Key, a
|
|
56
|
+
* Document, a Badge -- is exactly what a hand-made item is for.
|
|
57
|
+
*/
|
|
58
|
+
export function itemVocabulary() {
|
|
59
|
+
const stocked = new Map();
|
|
60
|
+
for (const row of catalogGear()) {
|
|
61
|
+
const key = String(row.category);
|
|
62
|
+
stocked.set(key, (stocked.get(key) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
const list = (e) => Object.entries(e)
|
|
65
|
+
.filter(([k]) => Number.isNaN(Number(k)))
|
|
66
|
+
.map(([key, label]) => ({ key, label }));
|
|
67
|
+
return {
|
|
68
|
+
categories: list(Category).map(c => ({
|
|
69
|
+
...c,
|
|
70
|
+
catalog: stocked.get(c.key) ?? 0,
|
|
71
|
+
equippable: NPC_EQUIP_CATEGORIES.has(c.label),
|
|
72
|
+
weapon: c.label === Category.Weapon || c.label === Category.MeleeWeapon || c.label === Category.RangedWeapon,
|
|
73
|
+
})),
|
|
74
|
+
sizes: list(Size),
|
|
75
|
+
ratings: list(Rating),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=catalog-item-seed.js.map
|
|
@@ -241,31 +241,27 @@ export function parseJobSize(said) {
|
|
|
241
241
|
}
|
|
242
242
|
// ============================ Requirements ============================
|
|
243
243
|
/**
|
|
244
|
-
* WHAT THIS SITE DEMANDS, read off its structure
|
|
245
|
-
*
|
|
244
|
+
* WHAT THIS SITE DEMANDS, read off its structure -- what no findable
|
|
245
|
+
* clue or key supplies:
|
|
246
246
|
* - 'decker': the objective is a file on a host. Only a persona
|
|
247
247
|
* reaches it (a cyberdeck or a living one).
|
|
248
|
-
* - 'mage': a ward stands on every route from the curb to the prize.
|
|
249
|
-
* Nothing but the Art unweaves a ward.
|
|
250
248
|
* Locks and panels never count: every one has a key or a clue on the
|
|
251
249
|
* near side (the unconditional clue rule), so any runner gets through.
|
|
250
|
+
* Nor do wards, any more (user 2026-10-06): a ward is an astral barrier
|
|
251
|
+
* over a room, and bodies walk through it (p.294, p.315) -- there was a
|
|
252
|
+
* 'mage' requirement while a ward was a door only the Art opened.
|
|
252
253
|
*/
|
|
253
254
|
export function requirementsFor(skeleton) {
|
|
254
255
|
const needs = [];
|
|
255
256
|
const win = skeleton.items.find(i => i.name === skeleton.winCondition?.item);
|
|
256
257
|
if (win?.plane === 'matrix')
|
|
257
258
|
needs.push('decker');
|
|
258
|
-
const prizeRoom = objectiveRoomFor(skeleton);
|
|
259
|
-
const start = skeleton.rooms.find(r => r.isStartRoom)?.name;
|
|
260
|
-
if (prizeRoom && start && !reachableAvoidingWards(skeleton, start, prizeRoom)) {
|
|
261
|
-
needs.push('mage');
|
|
262
|
-
}
|
|
263
259
|
return needs;
|
|
264
260
|
}
|
|
265
261
|
/**
|
|
266
262
|
* WHERE THE OBJECTIVE STANDS: the prize item's room (or its holder's),
|
|
267
263
|
* the mark's post, or the target device's room -- whichever the job
|
|
268
|
-
* has. The one answer the lieutenant post
|
|
264
|
+
* has. The one answer the lieutenant post and the pay's
|
|
269
265
|
* "prize room" all read, so a kind added later changes it in one place.
|
|
270
266
|
*/
|
|
271
267
|
export function objectiveRoomFor(skeleton) {
|
|
@@ -286,45 +282,12 @@ export function objectiveRoomFor(skeleton) {
|
|
|
286
282
|
}
|
|
287
283
|
return undefined;
|
|
288
284
|
}
|
|
289
|
-
/** Is `to` reachable from `from` without crossing an exit a ward holds?
|
|
290
|
-
* Edges are two-way, as the engine builds them. A prize room that is
|
|
291
|
-
* unreachable even WITH wards is the seed validator's problem, not a
|
|
292
|
-
* mage's -- so it answers true rather than blaming the Art. */
|
|
293
|
-
function reachableAvoidingWards(skeleton, from, to) {
|
|
294
|
-
const wards = new Set((skeleton.devices ?? []).filter(d => d.kind === 'ward').map(d => d.name));
|
|
295
|
-
const walk = (skipWards) => {
|
|
296
|
-
const seen = new Set([from]);
|
|
297
|
-
const queue = [from];
|
|
298
|
-
while (queue.length) {
|
|
299
|
-
const here = queue.shift();
|
|
300
|
-
if (here === to)
|
|
301
|
-
return true;
|
|
302
|
-
for (const room of skeleton.rooms) {
|
|
303
|
-
for (const exit of room.exits ?? []) {
|
|
304
|
-
if (room.name !== here && exit.targetRoom !== here)
|
|
305
|
-
continue;
|
|
306
|
-
if (skipWards && exit.device && wards.has(exit.device))
|
|
307
|
-
continue;
|
|
308
|
-
const next = room.name === here ? exit.targetRoom : room.name;
|
|
309
|
-
if (!seen.has(next)) {
|
|
310
|
-
seen.add(next);
|
|
311
|
-
queue.push(next);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return false;
|
|
317
|
-
};
|
|
318
|
-
if (!walk(false))
|
|
319
|
-
return true;
|
|
320
|
-
return walk(true);
|
|
321
|
-
}
|
|
322
285
|
/** How Krow says it. Plainly (user ruling): the job's requirement, and
|
|
323
286
|
* that a crew member covers it. */
|
|
324
287
|
export function requirementLine(needs) {
|
|
325
288
|
if (needs.length === 0)
|
|
326
289
|
return '';
|
|
327
|
-
const words = needs.map(
|
|
290
|
+
const words = needs.map(() => 'a decker');
|
|
328
291
|
const list = words.length === 1 ? words[0] : `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`;
|
|
329
292
|
return `Straight talk: this one needs ${list}. If that's not you, put one on the crew before you ride.`;
|
|
330
293
|
}
|
|
@@ -64,12 +64,10 @@ export const REPRO_FLOOR_SPOT = 'back wall';
|
|
|
64
64
|
const DEFAULT_DEVICE_NAME = {
|
|
65
65
|
lock: 'Maglock',
|
|
66
66
|
panel: 'Keypad',
|
|
67
|
-
ward: 'Chalk Ward',
|
|
68
67
|
};
|
|
69
68
|
const DEFAULT_DEVICE_DESCRIPTION = {
|
|
70
69
|
lock: 'A heavy maglock, seated in the frame and holding the door shut.',
|
|
71
70
|
panel: 'A keypad on a short steel stalk, its display waiting for a code.',
|
|
72
|
-
ward: 'A ward drawn in chalk and old ash across the whole doorway, humming against your teeth.',
|
|
73
71
|
};
|
|
74
72
|
const OPPOSITE = {
|
|
75
73
|
[Direction.NORTH]: Direction.SOUTH,
|
|
@@ -79,9 +77,6 @@ const OPPOSITE = {
|
|
|
79
77
|
[Direction.UP]: Direction.DOWN,
|
|
80
78
|
[Direction.DOWN]: Direction.UP,
|
|
81
79
|
};
|
|
82
|
-
/** Archetypes that can work a ward. `dispel` needs the Art; nothing
|
|
83
|
-
* else in the game does. */
|
|
84
|
-
const AWAKENED = new Set(['Adept', 'Combat Mage', 'Street Shaman']);
|
|
85
80
|
/** Bounds. A repro is a bench: more than this and it is a scene, and a
|
|
86
81
|
* scene is a place a reproduction gets lost in. */
|
|
87
82
|
const MAX_NPCS = 4;
|
|
@@ -258,21 +253,24 @@ export function buildReproScene(fixture) {
|
|
|
258
253
|
}
|
|
259
254
|
if (barrier && !plainDoor) {
|
|
260
255
|
// INVARIANT 1: a code belongs to a panel and nowhere else. The
|
|
261
|
-
// Device constructor DROPS it on a lock
|
|
256
|
+
// Device constructor DROPS it on a lock, silently and
|
|
262
257
|
// correctly -- that is the runtime honouring "you can't talk at a
|
|
263
258
|
// mechanical lock". Inheriting that drop here would produce a bench
|
|
264
259
|
// whose stated credential does nothing, and the reviewer would read
|
|
265
260
|
// the refusal as the bug still being present.
|
|
266
261
|
if (barrier.code && barrier.kind !== 'panel') {
|
|
267
|
-
throw new ReproFixtureError(`A ${barrier.kind} has no code -- only a panel does. Give it a keyItem, or let it be picked
|
|
262
|
+
throw new ReproFixtureError(`A ${barrier.kind} has no code -- only a panel does. Give it a keyItem, or let it be picked.`);
|
|
263
|
+
}
|
|
264
|
+
// A WARD IS NOT A DOOR (user 2026-10-06): it is a room's astral
|
|
265
|
+
// barrier and stops no body (utilities/wards.ts). A spec still asking
|
|
266
|
+
// for one on a doorway is describing a game that no longer exists.
|
|
267
|
+
if (barrier.kind === 'ward') {
|
|
268
|
+
throw new ReproFixtureError('A ward is not a door barrier any more -- it is an astral barrier over a room, and bodies walk through it. Use a "lock" or a "panel" here.');
|
|
268
269
|
}
|
|
269
270
|
// A panel with no code cannot be opened the way a panel opens.
|
|
270
271
|
if (barrier.kind === 'panel' && !barrier.code && !barrier.keyItem && !barrier.open) {
|
|
271
272
|
warnings.push('This panel has neither a code nor a key, so the only way through it is force.');
|
|
272
273
|
}
|
|
273
|
-
if (barrier.kind === 'ward' && !AWAKENED.has(archetypeName)) {
|
|
274
|
-
warnings.push(`A ward answers to the Art, and ${archetypeName} is mundane -- fine if the refusal is the point, otherwise use Combat Mage.`);
|
|
275
|
-
}
|
|
276
274
|
device = {
|
|
277
275
|
name: barrier.name ?? DEFAULT_DEVICE_NAME[barrier.kind],
|
|
278
276
|
description: DEFAULT_DEVICE_DESCRIPTION[barrier.kind],
|
|
@@ -16,6 +16,8 @@ export class RoomFactory {
|
|
|
16
16
|
isStartRoom: roomJson.isStartRoom,
|
|
17
17
|
roomType: roomJson.roomType,
|
|
18
18
|
outdoors: roomJson.outdoors,
|
|
19
|
+
ground: roomJson.ground,
|
|
20
|
+
storeys: roomJson.storeys,
|
|
19
21
|
tags: roomJson.tags ?? [],
|
|
20
22
|
hasNode: roomJson.hasNode,
|
|
21
23
|
nodeAccess: roomJson.nodeAccess,
|
|
@@ -25,6 +27,9 @@ export class RoomFactory {
|
|
|
25
27
|
hostRating: roomJson.hostRating,
|
|
26
28
|
hostGrid: roomJson.hostGrid,
|
|
27
29
|
hostPurpose: roomJson.hostPurpose,
|
|
30
|
+
hostName: roomJson.hostName,
|
|
31
|
+
icOrder: roomJson.icOrder,
|
|
32
|
+
icBricked: roomJson.icBricked,
|
|
28
33
|
hostSculpt: roomJson.hostSculpt,
|
|
29
34
|
// THE INVITATION (p.240; Host.invite). Eaten by this whitelist on
|
|
30
35
|
// 5.243.0's first live walk: the served hub carried hostInvite,
|
|
@@ -35,7 +40,14 @@ export class RoomFactory {
|
|
|
35
40
|
hostInvite: roomJson.hostInvite,
|
|
36
41
|
hostInviteQuality: roomJson.hostInviteQuality,
|
|
37
42
|
warded: roomJson.warded,
|
|
43
|
+
wardForce: roomJson.wardForce,
|
|
44
|
+
wardCreator: roomJson.wardCreator,
|
|
38
45
|
watched: roomJson.watched,
|
|
46
|
+
// Authored lenses (player-built runs) -- named here or this
|
|
47
|
+
// whitelist eats them, as it ate `owner` and `hostInvite`.
|
|
48
|
+
cameras: roomJson.cameras,
|
|
49
|
+
camerasWireless: roomJson.camerasWireless,
|
|
50
|
+
camerasSlavedTo: roomJson.camerasSlavedTo,
|
|
39
51
|
// The crowd (utilities/crowd.ts) -- named here or this whitelist eats it.
|
|
40
52
|
occupancy: roomJson.occupancy,
|
|
41
53
|
// "At" spots ride through -- this whitelist SILENTLY DROPS any
|
|
@@ -4,7 +4,8 @@ import { Logger } from '../utilities/logger.js';
|
|
|
4
4
|
import { catalogAugmentations } from '../utilities/catalog.js';
|
|
5
5
|
import { isOutdoorsPlace } from '../utilities/outdoors.js';
|
|
6
6
|
import { exitContradictions } from '../utilities/district-plane.js';
|
|
7
|
-
import { clampDeviceKind } from '../utilities/affordances.js';
|
|
7
|
+
import { clampDeviceKind, isWardDevice } from '../utilities/affordances.js';
|
|
8
|
+
import { wardDevicesToRooms } from './seed-migration.js';
|
|
8
9
|
import { kitSlugForTier } from '../npc-kits.js';
|
|
9
10
|
import { SIZE_SECURITY_BANDS, SIZE_HOST_BANDS, scopeFor, postsLieutenant, sizeForTier, highestOpposingPool, payFor, rollTwist, requirementsFor, DATA_RUN_SHARE, objectiveRoomFor, isDataCourier, courierMustBeUnseen, } from './job-order.js';
|
|
10
11
|
export { DATA_RUN_SHARE } from './job-order.js';
|
|
@@ -464,8 +465,9 @@ routes, and the site should carry a third where it fits -- pick from:
|
|
|
464
465
|
bribed, or put down;
|
|
465
466
|
* the BACK WAY: a lock or panel on a side door with its key or clue findable elsewhere;
|
|
466
467
|
* the MATRIX WAY: a hasNode host whose maglocks and cameras a persona can take over;
|
|
467
|
-
* the ASTRAL WAY: a
|
|
468
|
-
|
|
468
|
+
* the ASTRAL WAY: a warded room ("warded" + "wardForce") -- an astral barrier over the
|
|
469
|
+
whole room that keeps projecting mages and spirits out but stops NO body -- and the
|
|
470
|
+
rooms around it a projecting mage can scout freely;
|
|
469
471
|
* the QUIET WAY: a route past sleeping or distracted staff, a service corridor, a roof.
|
|
470
472
|
Never gate the prize behind ONE chokepoint. Never build a route ONLY a decker or ONLY a
|
|
471
473
|
mage could take unless another route exists for everyone else -- but DO build those
|
|
@@ -577,7 +579,12 @@ ${JSON_ONLY}
|
|
|
577
579
|
// corporate, low-level government. 9-10 regional corporate, secure
|
|
578
580
|
// sites. 11-12 megacorporate HQ, military command. Rate it for the
|
|
579
581
|
// place it actually is; omit it and the host is treated as a 3.
|
|
580
|
-
"warded"?: boolean, //
|
|
582
|
+
"warded"?: boolean, // a WARD over this whole room: an astral barrier that stops
|
|
583
|
+
// projecting mages and astral spirits going in OR out. It stops
|
|
584
|
+
// NO body and no physical object -- runners walk through it.
|
|
585
|
+
// Corporate sites buy them as a cheap contracted service.
|
|
586
|
+
"wardForce"?: number, // REQUIRED with warded: the ward's Force. 2-3 a small business,
|
|
587
|
+
// 4-6 a corporate site, 7+ a megacorporate vault.
|
|
581
588
|
"watched"?: boolean, // security cameras cover this room (feeds ride the site's hosts;
|
|
582
589
|
// hasNode rooms are always watched implicitly). Use for lobbies,
|
|
583
590
|
// corridors, and secure floors in corp/guarded locations -- loud
|
|
@@ -632,13 +639,14 @@ ${JSON_ONLY}
|
|
|
632
639
|
}],
|
|
633
640
|
"devices": [{
|
|
634
641
|
"name": string, "concept": string, // what it physically IS (a maglock, a keypad, a
|
|
635
|
-
// scanner
|
|
642
|
+
// scanner) -- a THING, never a person, never a ward
|
|
643
|
+
// (a ward is a ROOM's "warded", not a device)
|
|
636
644
|
"room": string, // REQUIRED. A device always stands in ONE room -- the one you must be
|
|
637
645
|
// in to reach it. If it holds a door, declare that exit FROM THIS
|
|
638
646
|
// SAME ROOM with "device": "<this name>".
|
|
639
|
-
"kind": "lock"|"panel"
|
|
640
|
-
// key. panel = an interface: a code, or hacked.
|
|
641
|
-
//
|
|
647
|
+
"kind": "lock"|"panel", // lock = a mechanism: picked, breached, or opened by its
|
|
648
|
+
// key. panel = an interface: a code, or hacked. EXACTLY these
|
|
649
|
+
// lowercase strings.
|
|
642
650
|
// A device is always a THING. A doorman or a checkpoint guard is
|
|
643
651
|
// NOT a device -- write them into npcs[] and name them in that
|
|
644
652
|
// exit's "guard"; people are got past socially, not picked.
|
|
@@ -987,7 +995,7 @@ export function buildDevicesPrompt(skeleton, previousError) {
|
|
|
987
995
|
return '';
|
|
988
996
|
return `
|
|
989
997
|
You are writing the DEVICES for a Shadowrun text adventure. A device is a THING that is
|
|
990
|
-
shut and can be opened -- a lock
|
|
998
|
+
shut and can be opened -- a lock or a panel. Never a person.
|
|
991
999
|
|
|
992
1000
|
${skeletonContext(skeleton)}
|
|
993
1001
|
|
|
@@ -1010,9 +1018,8 @@ fixed, as is its "kind"):
|
|
|
1010
1018
|
* Otherwise (an intercom, a voice-gate, a spoken passphrase, a name, a sign reading
|
|
1011
1019
|
"password") -- ALL LOWERCASE, 2-5 plain words, no punctuation.
|
|
1012
1020
|
Either way a concrete literal thing, never wordplay.
|
|
1013
|
-
OMIT "code" for a "lock"
|
|
1014
|
-
|
|
1015
|
-
promise the game cannot keep.
|
|
1021
|
+
OMIT "code" for a "lock". You cannot talk at a mechanical lock -- it has nothing to hear
|
|
1022
|
+
a phrase with, so a code on one is a promise the game cannot keep.
|
|
1016
1023
|
- "openMessage" / "refuseMessage": one sentence each, in-fiction.
|
|
1017
1024
|
- "guardsExit": ONLY when the concept is physically TERRAIN blocking a passage -- rubble, a
|
|
1018
1025
|
collapsed rack, a security shutter across a walkway -- name the exact "direction" of one
|
|
@@ -1173,35 +1180,6 @@ export function unwrapChunk(parsed, label) {
|
|
|
1173
1180
|
throw new Error(`The "${label}" chunk came back as neither an array nor a { "${CHUNK_ENVELOPE_KEY}": [...] } object.`);
|
|
1174
1181
|
}
|
|
1175
1182
|
// ========================= Parse / validate / assemble ====================
|
|
1176
|
-
/**
|
|
1177
|
-
* Can anyone at this table unweave a WARD?
|
|
1178
|
-
*
|
|
1179
|
-
* The last surviving branch of what used to be canBypassUnaided, which
|
|
1180
|
-
* asked the same question of all three barrier kinds. The other two are
|
|
1181
|
-
* gone because their answers stopped mattering (ruling 2026-08-27):
|
|
1182
|
-
*
|
|
1183
|
-
* A PANEL takes a code, and a code is FINDABLE. Write it down and any
|
|
1184
|
-
* runner can type it; a decker who did not need it loses nothing.
|
|
1185
|
-
* A LOCK takes a key, and a key is FINDABLE. Place it and any runner
|
|
1186
|
-
* can carry it to the door; picking becomes the SHORTCUT for someone
|
|
1187
|
-
* with locksmith, not the requirement.
|
|
1188
|
-
*
|
|
1189
|
-
* So for those two the clue/key rule is UNCONDITIONAL now, and the
|
|
1190
|
-
* character-dependent waiver is gone with the condition it rested on.
|
|
1191
|
-
*
|
|
1192
|
-
* A WARD is irreducible. There is no findable answer to a ward; it takes
|
|
1193
|
-
* Magic. A mundane team cannot dispel one however much is written down,
|
|
1194
|
-
* so a ward generated for a party with nobody awakened is an unopenable
|
|
1195
|
-
* barrier no clue can rescue. That is where the character dependence is
|
|
1196
|
-
* REAL rather than incidental, and it is the only place this survives.
|
|
1197
|
-
*
|
|
1198
|
-
* Named for what it now is: a one-case function still called
|
|
1199
|
-
* canBypassUnaided would attract the other two cases back.
|
|
1200
|
-
*/
|
|
1201
|
-
export function partyCanDispel(player) {
|
|
1202
|
-
// Adepts are awakened but cannot cast, so they cannot unweave a ward.
|
|
1203
|
-
return !!player && player.isAwakened() && !player.adept;
|
|
1204
|
-
}
|
|
1205
1183
|
/**
|
|
1206
1184
|
* CAN THIS RUNNER WORK THE GRID AT ALL (ruoedrMTA6kCRNmEN, user ruling:
|
|
1207
1185
|
* data runs are offered to deck-capable runners only)?
|
|
@@ -1487,8 +1465,6 @@ export function validateSkeleton(skeleton, _player, _crew, order) {
|
|
|
1487
1465
|
fail(`device "${device.name}" is the demolition target and also holds a door -- the target is a thing to wreck, not a way through. Give the door its own device.`);
|
|
1488
1466
|
if (device.grantsItem)
|
|
1489
1467
|
fail(`device "${device.name}" is the demolition target and "grantsItem" -- the target opens onto nothing; drop grantsItem.`);
|
|
1490
|
-
if ((device.kind ?? 'panel') === 'ward')
|
|
1491
|
-
fail(`device "${device.name}" is the demolition target and a ward -- a ward is not matter and cannot be wrecked. Make it a "panel" or a "lock".`);
|
|
1492
1468
|
continue;
|
|
1493
1469
|
}
|
|
1494
1470
|
// EVERY WAY THROUGH IS FINDABLE, and it no longer depends on who is
|
|
@@ -1549,10 +1525,6 @@ export function validateSkeleton(skeleton, _player, _crew, order) {
|
|
|
1549
1525
|
fail(`lock "${device.name}" ${holdsDoor ? 'holds a door' : 'holds a prize'} and no "key"-role item opens it -- add one with "forDevice": "${device.name}", so a runner without locksmith has a way through.`);
|
|
1550
1526
|
}
|
|
1551
1527
|
}
|
|
1552
|
-
// A WARD has no findable answer, and that is now allowed (ruling
|
|
1553
|
-
// 2026-09-24): the prompt asks for a second route past any ward,
|
|
1554
|
-
// and job-order.ts requirementsFor tells Krow to say "you'll need a
|
|
1555
|
-
// mage" when a ward stands on every route to the prize.
|
|
1556
1528
|
}
|
|
1557
1529
|
if (skeleton.winCondition.item !== undefined && !itemNames.has(skeleton.winCondition.item))
|
|
1558
1530
|
fail(`winCondition.item "${skeleton.winCondition.item}" is not in items.`);
|
|
@@ -2235,8 +2207,7 @@ export function normalizeSkeleton(skeleton) {
|
|
|
2235
2207
|
// protected from being converted into an unplaced reward.
|
|
2236
2208
|
//
|
|
2237
2209
|
// PANELS ONLY. A lock's findable way through is its key, not a
|
|
2238
|
-
// written code
|
|
2239
|
-
// refused outright at validation when nobody can dispel). Synthesis
|
|
2210
|
+
// written code. Synthesis
|
|
2240
2211
|
// is unconditional for panels now -- there is no waiver left to
|
|
2241
2212
|
// second-guess, which is what made the old version's log lines need
|
|
2242
2213
|
// to explain which kind of failure they were.
|
|
@@ -2880,6 +2851,9 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
2880
2851
|
hostPurpose: room.hasNode && typeof room.hostPurpose === 'string' ? room.hostPurpose : undefined,
|
|
2881
2852
|
hostSculpt: room.hasNode && typeof detail?.hostSculpt === 'string' ? detail.hostSculpt : undefined,
|
|
2882
2853
|
warded: room.warded,
|
|
2854
|
+
// A ward always has a Force; a bad or missing one takes the default
|
|
2855
|
+
// at load (Room WARD_DEFAULT_FORCE) rather than failing the draft.
|
|
2856
|
+
wardForce: room.warded && typeof room.wardForce === 'number' && Number.isFinite(room.wardForce) && room.wardForce >= 1 ? Math.floor(room.wardForce) : undefined,
|
|
2883
2857
|
watched: room.watched,
|
|
2884
2858
|
// A headcount or nothing: a bad number falls back to the derived one.
|
|
2885
2859
|
occupancy: typeof room.occupancy === 'number' && Number.isFinite(room.occupancy) && room.occupancy >= 0 ? Math.round(room.occupancy) : undefined,
|
|
@@ -3169,7 +3143,11 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
3169
3143
|
// why, and for the incident (a "tunnel grate" clamped to `panel`
|
|
3170
3144
|
// and rendered hack-only) that put a name/description-aware
|
|
3171
3145
|
// fallback here instead of a blind default to `panel`.
|
|
3172
|
-
|
|
3146
|
+
// A WARD WRITTEN AS A DEVICE stays one here, only to be moved onto
|
|
3147
|
+
// its room by wardDevicesToRooms at the end of assembly.
|
|
3148
|
+
const kind = isWardDevice(device.kind, device.name, detail?.description ?? device.concept)
|
|
3149
|
+
? 'ward'
|
|
3150
|
+
: clampDeviceKind(device.kind, device.name, detail?.description ?? device.concept);
|
|
3173
3151
|
// A CODE ONLY BELONGS ON A PANEL -- enforced here as well as in the
|
|
3174
3152
|
// prompt, because the prompt is advice and this is not. You cannot
|
|
3175
3153
|
// talk at a mechanical lock.
|
|
@@ -3190,7 +3168,7 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
3190
3168
|
};
|
|
3191
3169
|
});
|
|
3192
3170
|
const startRoom = skeleton.rooms.find(r => r.isStartRoom)?.name ?? skeleton.rooms[0]?.name ?? '';
|
|
3193
|
-
return {
|
|
3171
|
+
return wardDevicesToRooms({
|
|
3194
3172
|
name: skeleton.name,
|
|
3195
3173
|
story: skeleton.story,
|
|
3196
3174
|
currency: { type: 'nuyen', authorityRoomTags: ['Bank'] },
|
|
@@ -3254,7 +3232,7 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
3254
3232
|
...(skeleton.securityTier !== undefined ? { securityTier: skeleton.securityTier } : {}),
|
|
3255
3233
|
...(skeleton.neighborhood !== undefined ? { neighborhood: skeleton.neighborhood } : {}),
|
|
3256
3234
|
...(skeleton.lawContract ? { lawContract: skeleton.lawContract } : {}),
|
|
3257
|
-
};
|
|
3235
|
+
});
|
|
3258
3236
|
}
|
|
3259
3237
|
/**
|
|
3260
3238
|
* Maps a validation failure to the chunk that owns the broken content, so
|
|
@@ -5,6 +5,7 @@ import { Door } from '../models/door.js';
|
|
|
5
5
|
import { RoomFactory } from './room-factory.js';
|
|
6
6
|
import { NPCFactory } from './npc-factory.js';
|
|
7
7
|
import { ItemFactory } from './item-factory.js';
|
|
8
|
+
import { resolveCatalogItemSeed } from './catalog-item-seed.js';
|
|
8
9
|
/** The heaviest a job's prize may be: a Strength 1 runner carries 30 kg
|
|
9
10
|
* (player.ts maxCarryingWeight), and they still need room for gear. */
|
|
10
11
|
export const OBJECTIVE_MAX_WEIGHT_KG = 15;
|
|
@@ -17,6 +18,9 @@ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
|
17
18
|
import { adoptSeededIce } from '../utilities/ic-actors.js';
|
|
18
19
|
import { NPC_EQUIP_CATEGORIES, outfitNpcBySlug } from '../npc-kits.js';
|
|
19
20
|
import { seatHostOwner } from '../utilities/matrix-roster.js';
|
|
21
|
+
import { seatSpider } from '../utilities/spider.js';
|
|
22
|
+
import { cellOnGrid } from '../utilities/room-grid.js';
|
|
23
|
+
import { spotForCell } from '../utilities/spots.js';
|
|
20
24
|
/**
|
|
21
25
|
* How many AI enrichment calls may be in flight at once.
|
|
22
26
|
*
|
|
@@ -123,6 +127,31 @@ export class SceneSynthesizer {
|
|
|
123
127
|
// that has already been migrated (and saved, and reloaded) passes
|
|
124
128
|
// through untouched.
|
|
125
129
|
json = migrateLegacySeed(json);
|
|
130
|
+
// A CATALOG SLUG IS THE ROW ITSELF (catalog-item-seed.ts): resolved
|
|
131
|
+
// once, here, so every later step that matches items by name -- heldBy,
|
|
132
|
+
// ownership, the win condition, keys -- sees the catalog's name. A
|
|
133
|
+
// seed with no slugs passes through untouched.
|
|
134
|
+
if ((json.items ?? []).some(i => i.slug)) {
|
|
135
|
+
json = { ...json, items: json.items.map(resolveCatalogItemSeed) };
|
|
136
|
+
}
|
|
137
|
+
// A DEVICE WITH A CELL STANDS THERE (device-seed.ts cell): it gets a
|
|
138
|
+
// one-cell spot of its own name, and is `at` it. A seed with no
|
|
139
|
+
// placed device passes through untouched.
|
|
140
|
+
if ((json.rooms ?? []).some(r => (r.devices ?? []).some(d => d.cell && !d.atSpot))) {
|
|
141
|
+
json = {
|
|
142
|
+
...json,
|
|
143
|
+
rooms: json.rooms.map(r => {
|
|
144
|
+
const placed = (r.devices ?? []).filter(d => d.cell && !d.atSpot);
|
|
145
|
+
if (placed.length === 0)
|
|
146
|
+
return r;
|
|
147
|
+
return {
|
|
148
|
+
...r,
|
|
149
|
+
spots: [...(r.spots ?? []), ...placed.map(d => ({ name: d.name, cell: d.cell, size: { w: 1, h: 1 } }))],
|
|
150
|
+
devices: (r.devices ?? []).map(d => (d.cell && !d.atSpot ? { ...d, atSpot: d.name } : d)),
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
126
155
|
// Step 1: Create the Scene using the JSON’s room configuration
|
|
127
156
|
logger.write(`Initilizing Scene`);
|
|
128
157
|
const scene = new Scene({
|
|
@@ -187,6 +216,12 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
187
216
|
if (exit.atSpot) {
|
|
188
217
|
scene.getRoom(roomJson.name)?.exitSpots.set(exit.direction.toLowerCase(), exit.atSpot);
|
|
189
218
|
}
|
|
219
|
+
// An authored door or stair position (exit seed `cell`).
|
|
220
|
+
if (exit.cell) {
|
|
221
|
+
const declaring = scene.getRoom(roomJson.name);
|
|
222
|
+
if (declaring)
|
|
223
|
+
declaring.authoredDoorCells = { ...(declaring.authoredDoorCells ?? {}), [exit.direction.toLowerCase()]: { x: exit.cell.x, y: exit.cell.y } };
|
|
224
|
+
}
|
|
190
225
|
}
|
|
191
226
|
}
|
|
192
227
|
beat('exits');
|
|
@@ -217,6 +252,11 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
217
252
|
if (npcJson.player.guarding) {
|
|
218
253
|
npc.guarding = true;
|
|
219
254
|
}
|
|
255
|
+
// ...and a WATCHED one is guarded without being blocked (utilities/
|
|
256
|
+
// door-watch.ts): they turn on anyone caught going for it.
|
|
257
|
+
if (npcJson.player.watches) {
|
|
258
|
+
npc.watching = String(npcJson.player.watches).toLowerCase();
|
|
259
|
+
}
|
|
220
260
|
// A MERCHANT'S TRADE (IMerchantSeed): what they sell is authored,
|
|
221
261
|
// not read off whatever happens to lie in their room.
|
|
222
262
|
if (npcJson.merchant) {
|
|
@@ -290,8 +330,18 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
290
330
|
// Step 4: Add Items
|
|
291
331
|
const itemFactory = new ItemFactory();
|
|
292
332
|
const itemMap = new Map();
|
|
333
|
+
/** Weapons and armor an author asked to keep holstered / packed
|
|
334
|
+
* (IItemSeedJson.equipped === false) -- read at Step 4.05. */
|
|
335
|
+
const carryAs = new Map();
|
|
293
336
|
for (const itemJson of json.items ?? []) {
|
|
294
337
|
const item = await itemFactory.createItemFromJson(itemJson);
|
|
338
|
+
if (typeof itemJson.equipped === 'boolean')
|
|
339
|
+
carryAs.set(item, itemJson.equipped);
|
|
340
|
+
// A FILE'S OWN DEFENCES (utilities/file-guard.ts), authored only.
|
|
341
|
+
if (itemJson.protection && itemJson.protection > 0)
|
|
342
|
+
item.protection = itemJson.protection;
|
|
343
|
+
if (itemJson.dataBomb && itemJson.dataBomb.rating > 0)
|
|
344
|
+
item.dataBomb = { ...itemJson.dataBomb };
|
|
295
345
|
// THE PRIZE HAS TO BE CARRIABLE (KSfzQzuHTx7kHjohi, 2026-09-12:
|
|
296
346
|
// "I shouldn't have an item I need to return, that I can't carry
|
|
297
347
|
// -- this case is too heavy"). The seed's items are written by a
|
|
@@ -556,6 +606,48 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
556
606
|
logger.error(`Scene "${json.name}": the door ${String(direction)} of ${room.name} was locked by "${wanted ?? '(nothing)'}", which no device holds and no item matches -- it ships unlocked rather than as a wall.`);
|
|
557
607
|
}
|
|
558
608
|
}
|
|
609
|
+
// Step 4.96: AUTHORED START CELLS (player-built runs, 2026-10-06). An
|
|
610
|
+
// NPC with `startCell` stands exactly there. HERE, not at Step 3 where
|
|
611
|
+
// they were placed: spotForCell builds the room's grid, and a grid
|
|
612
|
+
// built before the device step above (4.9) would be frozen without
|
|
613
|
+
// the rubble its terrain hints add. The spot comes from `startSpot`
|
|
614
|
+
// when the author gave one, otherwise from whatever the cell touches.
|
|
615
|
+
for (const npcJson of json.npcs ?? []) {
|
|
616
|
+
const at = npcJson.player.startCell;
|
|
617
|
+
if (!at)
|
|
618
|
+
continue;
|
|
619
|
+
const npc = scene.getActor(npcJson.player.name);
|
|
620
|
+
const room = npc?.currentLocation;
|
|
621
|
+
if (!npc || !room?.ensureGrid())
|
|
622
|
+
continue;
|
|
623
|
+
const cell = { x: Math.round(at.x), y: Math.round(at.y), z: Math.round(at.z ?? 0) };
|
|
624
|
+
if (!cellOnGrid(room.ensureGrid(), cell)) {
|
|
625
|
+
logger.error(`Scene "${json.name}": ${npc.name}'s startCell ${cell.x},${cell.y} is off ${room.name}'s grid -- left to their spot.`);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
npc.atSpot = npcJson.player.startSpot ?? spotForCell(room, cell);
|
|
629
|
+
npc.atCell = cell;
|
|
630
|
+
}
|
|
631
|
+
// ...and items lying where the author put them (items[].cell). An
|
|
632
|
+
// item may sit ON furniture -- the file on the desk -- so the cell is
|
|
633
|
+
// not checked against footprints; reach goes through the spot it
|
|
634
|
+
// touches, exactly as for a body standing there.
|
|
635
|
+
for (const itemJson of json.items ?? []) {
|
|
636
|
+
const at = itemJson.cell;
|
|
637
|
+
if (!at || !itemJson.room)
|
|
638
|
+
continue;
|
|
639
|
+
const item = itemMap.get(itemJson.name);
|
|
640
|
+
const room = scene.getRoom(itemJson.room);
|
|
641
|
+
if (!item || !room?.ensureGrid())
|
|
642
|
+
continue;
|
|
643
|
+
const cell = { x: Math.round(at.x), y: Math.round(at.y), z: Math.round(at.z ?? 0) };
|
|
644
|
+
if (!cellOnGrid(room.ensureGrid(), cell)) {
|
|
645
|
+
logger.error(`Scene "${json.name}": "${item.name}" cell ${cell.x},${cell.y} is off ${room.name}'s grid -- left unplaced.`);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
item.atCell = cell;
|
|
649
|
+
item.atSpot = itemJson.atSpot ?? spotForCell(room, cell);
|
|
650
|
+
}
|
|
559
651
|
// Step 4.04: NPCs get their kit (npc-kits.ts, SR5 p.379-385, catalog
|
|
560
652
|
// v21). AFTER "heldBy" has landed (Step 4.5 above -- the numbering is
|
|
561
653
|
// historical, the source order is what runs) and BEFORE the equip
|
|
@@ -600,8 +692,12 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
600
692
|
if (!npc)
|
|
601
693
|
continue;
|
|
602
694
|
for (const item of [...npc.inventory.getAllItems()]) {
|
|
695
|
+
// Only weapons and armor are ever equipped -- a key is carried,
|
|
696
|
+
// never wielded -- and an author may keep one of those holstered.
|
|
603
697
|
if (!NPC_EQUIP_CATEGORIES.has(item.category))
|
|
604
698
|
continue;
|
|
699
|
+
if (carryAs.get(item) === false)
|
|
700
|
+
continue;
|
|
605
701
|
if (weaponCategories.has(item.category) && npc.getCarriedWeapon())
|
|
606
702
|
continue;
|
|
607
703
|
try {
|
|
@@ -728,6 +824,22 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
728
824
|
if (seated)
|
|
729
825
|
logger.write(`"${seated.name}" works the host over "${room.name}" -- his icon is inside it.`);
|
|
730
826
|
}
|
|
827
|
+
// Step 7c: SPIDERS (utilities/spider.ts). After 7b, so a host owner
|
|
828
|
+
// who is also its spider ends up in VR rather than AR; after 4.96 and
|
|
829
|
+
// the gear steps, so the body slumps at its authored cell with its
|
|
830
|
+
// deck in hand. A refusal leaves them in the meat -- the authored
|
|
831
|
+
// audit reports the same refusal as a fault.
|
|
832
|
+
for (const npcJson of json.npcs ?? []) {
|
|
833
|
+
const into = npcJson.player.jackedInto;
|
|
834
|
+
if (!into)
|
|
835
|
+
continue;
|
|
836
|
+
const npc = scene.getActor(npcJson.player.name);
|
|
837
|
+
const host = scene.getRoom(into)?.host;
|
|
838
|
+
if (!npc || !host)
|
|
839
|
+
continue;
|
|
840
|
+
const refused = seatSpider(scene, npc, host, npcJson.player.sim ?? 'cold');
|
|
841
|
+
logger.write(refused ? `Spider "${npc.name}" stays in the meat: ${refused}` : `"${npc.name}" is jacked into ${host.name} (${npc.simMode}-sim).`);
|
|
842
|
+
}
|
|
731
843
|
// JACKPOINT, IN EVERY SCENE (qgyKnJLSeoxMf23j8). A standing host has
|
|
732
844
|
// to be reachable from wherever the runner is, and Scene.getRooms()
|
|
733
845
|
// is the whole reachable universe -- so the haven is added to each
|