@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
|
@@ -3,12 +3,15 @@ import path from 'path';
|
|
|
3
3
|
import { AI } from '../../../../tools/ai/ai.class.js';
|
|
4
4
|
import { Player } from '../models/player.js';
|
|
5
5
|
import { Room } from '../models/room.js';
|
|
6
|
+
import { deviceMaster } from '../utilities/wan.js';
|
|
6
7
|
import { Logger } from '../utilities/logger.js';
|
|
7
|
-
import { clampDeviceKind } from '../utilities/affordances.js';
|
|
8
|
+
import { clampDeviceKind, isWardDevice } from '../utilities/affordances.js';
|
|
9
|
+
import { wardDevicesToRooms } from './seed-migration.js';
|
|
8
10
|
import { GenerationCapture } from '../utilities/generation-capture.js';
|
|
9
11
|
import { fetchCanonContext } from '../utilities/canon-lore.js';
|
|
10
12
|
import { providerUnavailable } from '../utilities/draft-failure-note.js';
|
|
11
13
|
import { SceneSynthesizer } from './scene-factory.js';
|
|
14
|
+
import { resolveCatalogItemSeed } from './catalog-item-seed.js';
|
|
12
15
|
import { gridDefects } from '../game-master/diagnose.js';
|
|
13
16
|
import { rollTwist, sizeForTier, pickJobKind, shapeFor } from './job-order.js';
|
|
14
17
|
import { chapterJobKind } from './operation.js';
|
|
@@ -467,6 +470,76 @@ export class SceneSeedGenerator {
|
|
|
467
470
|
this.validateRewardItemPlacement(candidate);
|
|
468
471
|
this.validateDevicePlacement(candidate);
|
|
469
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* EVERY FAULT, NOT THE FIRST (player-built runs, 2026-10-06). The same
|
|
475
|
+
* gate dryRunValidate runs, for a seed somebody is EDITING: an author
|
|
476
|
+
* fixes faults one pass at a time only if they can see them all, so
|
|
477
|
+
* each check here is caught and recorded instead of ending the audit.
|
|
478
|
+
*
|
|
479
|
+
* Works on a copy -- clampDeviceKinds repairs its input, and an
|
|
480
|
+
* editor's seed is the author's, not the validator's. Structure and
|
|
481
|
+
* synthesis still stop the audit: every check after them reads the
|
|
482
|
+
* built scene or assumes named rooms, and would only echo the first
|
|
483
|
+
* fault in another voice. The built Scene comes back for a caller
|
|
484
|
+
* that wants to draw it.
|
|
485
|
+
*/
|
|
486
|
+
static async auditSeed(seed, player) {
|
|
487
|
+
const candidate = structuredClone(seed);
|
|
488
|
+
const message = (err) => (err instanceof Error ? err.message : String(err));
|
|
489
|
+
// CATALOG SLUGS FIRST: a slug-only item has no name until its row is
|
|
490
|
+
// read, and the structure check below would call it nameless. Each
|
|
491
|
+
// bad slug is its own fault, and the item is left out of the build so
|
|
492
|
+
// the rest of the run is still checked.
|
|
493
|
+
const slugFaults = [];
|
|
494
|
+
candidate.items = (candidate.items ?? []).flatMap(item => {
|
|
495
|
+
try {
|
|
496
|
+
return [resolveCatalogItemSeed(item)];
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
slugFaults.push({ check: 'items', message: message(err) });
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
try {
|
|
504
|
+
this.validateBasicStructure(candidate);
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
return { faults: [...slugFaults, { check: 'structure', message: message(err) }] };
|
|
508
|
+
}
|
|
509
|
+
const nexus = new Room({ name: 'Nexus', description: 'A featureless room where lost souls drift.', roomType: 'void' });
|
|
510
|
+
const throwawayPlayer = new Player({ playerName: candidate?.player?.name || 'Maka', startLocation: nexus });
|
|
511
|
+
let built;
|
|
512
|
+
try {
|
|
513
|
+
built = await SceneSynthesizer.synthesizeFromJson(candidate, throwawayPlayer, false);
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
return { faults: [...slugFaults, { check: 'synthesis', message: message(err) }] };
|
|
517
|
+
}
|
|
518
|
+
const faults = [
|
|
519
|
+
...slugFaults,
|
|
520
|
+
...built.planeFaults().map(f => ({ check: 'plane', message: f })),
|
|
521
|
+
];
|
|
522
|
+
const checks = [
|
|
523
|
+
['clues', () => this.validateClueItems(candidate, built)],
|
|
524
|
+
['winCondition', () => this.validateWinCondition(candidate, player)],
|
|
525
|
+
['client', () => this.validateClient(candidate)],
|
|
526
|
+
['matrix', () => this.validateMatrixDefense(candidate)],
|
|
527
|
+
['startingItems', () => this.validateStartingItems(candidate, player)],
|
|
528
|
+
['devices', () => this.clampDeviceKinds(candidate)],
|
|
529
|
+
['doorKeys', () => this.validateDoorKeys(candidate)],
|
|
530
|
+
['rewardItem', () => this.validateRewardItemPlacement(candidate)],
|
|
531
|
+
['devicePlacement', () => this.validateDevicePlacement(candidate)],
|
|
532
|
+
];
|
|
533
|
+
for (const [check, run] of checks) {
|
|
534
|
+
try {
|
|
535
|
+
run();
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
faults.push({ check, message: message(err) });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return { faults, built };
|
|
542
|
+
}
|
|
470
543
|
/**
|
|
471
544
|
* The one structural guarantee every other check assumes: every room,
|
|
472
545
|
* NPC, and item actually has a real, non-blank "name". Nothing about the
|
|
@@ -632,6 +705,25 @@ export class SceneSeedGenerator {
|
|
|
632
705
|
edges.push({ a: room.name, b: exit.targetRoom, keyRequired: exit.keyRequired, puzzle: exit.puzzle });
|
|
633
706
|
}
|
|
634
707
|
}
|
|
708
|
+
// OUTDOOR SEAMS ARE WALKED, NOT DOORS (user rulings 2026-10-06: a space
|
|
709
|
+
// drawn on the ground joins it seamlessly, and spaces that touch join
|
|
710
|
+
// each other): two outdoor areas on one storey whose footprints
|
|
711
|
+
// overlap or meet edge to edge are open ground between them
|
|
712
|
+
// (move.ts crossOutdoorSeam), with no exit declared and none needed.
|
|
713
|
+
const outdoor = rooms.filter(r => r.outdoors && r.footprint);
|
|
714
|
+
for (let i = 0; i < outdoor.length; i++) {
|
|
715
|
+
for (let j = i + 1; j < outdoor.length; j++) {
|
|
716
|
+
const a = outdoor[i];
|
|
717
|
+
const b = outdoor[j];
|
|
718
|
+
if ((a.storey ?? 0) !== (b.storey ?? 0))
|
|
719
|
+
continue;
|
|
720
|
+
const fa = a.footprint;
|
|
721
|
+
const fb = b.footprint;
|
|
722
|
+
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) {
|
|
723
|
+
edges.push({ a: a.name, b: b.name });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
635
727
|
reachable.add(startRoom);
|
|
636
728
|
let grew = true;
|
|
637
729
|
while (grew) {
|
|
@@ -865,6 +957,18 @@ export class SceneSeedGenerator {
|
|
|
865
957
|
* SYSTEM", which is backwards for a physical mechanism.
|
|
866
958
|
*/
|
|
867
959
|
static clampDeviceKinds(candidate) {
|
|
960
|
+
// A WARD IS A ROOM'S (user 2026-10-06): anything written or read as
|
|
961
|
+
// one is moved onto the room it protects first, so the clamp below
|
|
962
|
+
// never turns it into a lock or a panel.
|
|
963
|
+
for (const room of candidate.rooms ?? []) {
|
|
964
|
+
for (const device of room.devices ?? []) {
|
|
965
|
+
if (isWardDevice(device.kind, device.name, device.description))
|
|
966
|
+
device.kind = 'ward';
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const moved = wardDevicesToRooms(candidate);
|
|
970
|
+
candidate.rooms = moved.rooms;
|
|
971
|
+
candidate.items = moved.items;
|
|
868
972
|
for (const room of candidate.rooms ?? []) {
|
|
869
973
|
for (const device of room.devices ?? []) {
|
|
870
974
|
const clamped = clampDeviceKind(device.kind, device.name, device.description);
|
|
@@ -950,8 +1054,24 @@ export class SceneSeedGenerator {
|
|
|
950
1054
|
* documentation -- finding it and seeing its name on screen already
|
|
951
1055
|
* reveals the answer. A Book clue is still required for anything else.
|
|
952
1056
|
*/
|
|
953
|
-
static validateClueItems(candidate) {
|
|
1057
|
+
static validateClueItems(candidate, built) {
|
|
954
1058
|
const items = candidate.items ?? [];
|
|
1059
|
+
// THE HOST'S OWNER KNOWS IT (user ruling 2026-10-06: "circumvented if
|
|
1060
|
+
// the keypad is connected to a host -> room -> room owner"). A panel
|
|
1061
|
+
// slaved to a host whose room has an owner in the cast has its code
|
|
1062
|
+
// in somebody's head (NPC hostCodeNote) -- social or violent, the way
|
|
1063
|
+
// to it exists. Asked of the BUILT scene, so the master is the
|
|
1064
|
+
// engine's own wiring (wan.ts deviceMaster, nearest-host default
|
|
1065
|
+
// included); without one -- generation -- the rule is unchanged.
|
|
1066
|
+
const knownToOwner = (roomName, deviceName) => {
|
|
1067
|
+
if (!built)
|
|
1068
|
+
return false;
|
|
1069
|
+
const rooms = built.getRooms();
|
|
1070
|
+
const room = Object.values(rooms).find(r => r.name === roomName);
|
|
1071
|
+
const device = room?.allDevices().find(d => d.name === deviceName);
|
|
1072
|
+
const master = room && device ? deviceMaster(rooms, room, device) : undefined;
|
|
1073
|
+
return !!master?.owner && !!built.getActor(master.owner);
|
|
1074
|
+
};
|
|
955
1075
|
for (const room of candidate.rooms ?? []) {
|
|
956
1076
|
for (const device of room.devices ?? []) {
|
|
957
1077
|
// ONLY A PANEL HAS A CODE TO WRITE DOWN. A lock's findable way
|
|
@@ -979,7 +1099,7 @@ export class SceneSeedGenerator {
|
|
|
979
1099
|
// playing, and so look like random flakiness. A code is
|
|
980
1100
|
// findable for anyone; the decker who did not need it loses
|
|
981
1101
|
// nothing by its existing.
|
|
982
|
-
if (!hasBookClue && !hasNamedItemClue) {
|
|
1102
|
+
if (!hasBookClue && !hasNamedItemClue && !knownToOwner(room.name, device.name)) {
|
|
983
1103
|
throw new Error(`Panel "${device.name}" has no discoverable clue: no Book-category item in a different room whose "details" contains the code "${device.code}", and no item elsewhere literally named "${device.code}". Every panel's code must be written down somewhere findable.`);
|
|
984
1104
|
}
|
|
985
1105
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Logger } from '../utilities/logger.js';
|
|
2
|
+
import { WARD_DEFAULT_FORCE } from '../models/room.js';
|
|
2
3
|
/**
|
|
3
4
|
* KIND, from the old bypassType (rule 4).
|
|
4
5
|
*
|
|
@@ -11,7 +12,9 @@ import { Logger } from '../utilities/logger.js';
|
|
|
11
12
|
function kindFor(bypassType) {
|
|
12
13
|
switch (bypassType) {
|
|
13
14
|
case 'lockpick': return 'lock';
|
|
14
|
-
|
|
15
|
+
// A spell barrier was a ward. It is written as the legacy ward
|
|
16
|
+
// device here and moved onto its room by wardDevicesToRooms below.
|
|
17
|
+
case 'spell': return LEGACY_WARD;
|
|
15
18
|
case 'hack': return 'panel';
|
|
16
19
|
// Absent, 'con', or anything unrecognised.
|
|
17
20
|
default: return 'panel';
|
|
@@ -39,6 +42,47 @@ function synthesiseCode(name) {
|
|
|
39
42
|
export function migrateLegacySeed(json) {
|
|
40
43
|
return migrateLegacySeedWithReport(json).seed;
|
|
41
44
|
}
|
|
45
|
+
/** The retired device kind, as old seeds and old models still write it. */
|
|
46
|
+
const LEGACY_WARD = 'ward';
|
|
47
|
+
/**
|
|
48
|
+
* A WARD IS A ROOM'S, NOT A DOOR'S (user 2026-10-06; SR5 p.294, p.297-298,
|
|
49
|
+
* p.315). The ward DEVICE stood on a door and locked it to bodies until a
|
|
50
|
+
* mage dispelled it -- but a ward is an astral barrier over a place, and
|
|
51
|
+
* it "does not restrict living beings or physical objects". So every
|
|
52
|
+
* legacy ward device becomes a warded ROOM:
|
|
53
|
+
* - on an exit, the room behind that door -- what it was protecting;
|
|
54
|
+
* - on no exit (a warded strongbox), the room it stood in.
|
|
55
|
+
* Its Force is the device's rating where it had one, else the default.
|
|
56
|
+
* The door it held is simply a door now. Whatever it granted is put on
|
|
57
|
+
* the floor of its room, so a prize behind a ward is not stranded.
|
|
58
|
+
* Pure and idempotent: a seed with no ward device comes back as itself.
|
|
59
|
+
*/
|
|
60
|
+
export function wardDevicesToRooms(json) {
|
|
61
|
+
const isWard = (d) => d.kind === LEGACY_WARD;
|
|
62
|
+
if (!(json.rooms ?? []).some(r => (r.devices ?? []).some(isWard)))
|
|
63
|
+
return json;
|
|
64
|
+
const log = Logger.getInstance();
|
|
65
|
+
const rooms = (json.rooms ?? []).map(r => ({ ...r, devices: [...(r.devices ?? [])] }));
|
|
66
|
+
const byName = new Map(rooms.map(r => [r.name, r]));
|
|
67
|
+
let items = json.items;
|
|
68
|
+
for (const room of rooms) {
|
|
69
|
+
const wards = room.devices.filter(isWard);
|
|
70
|
+
if (wards.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
room.devices = room.devices.filter(d => !isWard(d));
|
|
73
|
+
for (const d of wards) {
|
|
74
|
+
const exit = d.opensExit ? (room.exits ?? []).find(e => e.direction === d.opensExit) : undefined;
|
|
75
|
+
const warded = (exit ? byName.get(exit.targetRoom) : undefined) ?? room;
|
|
76
|
+
warded.warded = true;
|
|
77
|
+
warded.wardForce = Math.max(warded.wardForce ?? 0, d.rating !== undefined && d.rating > 0 ? Math.floor(d.rating) : WARD_DEFAULT_FORCE);
|
|
78
|
+
if (d.grantsItem) {
|
|
79
|
+
items = (items ?? []).map(i => i.name === d.grantsItem && !i.room && !i.heldBy ? { ...i, room: room.name } : i);
|
|
80
|
+
}
|
|
81
|
+
log.write(`seed-migration: ward device "${d.name}" -> ${warded.name} is WARDED (Force ${warded.wardForce}); a ward stops astral forms, not bodies.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { ...json, rooms, items };
|
|
85
|
+
}
|
|
42
86
|
/**
|
|
43
87
|
* THE HUB CARRIES NO HOSTS (ruling 2026-09-12 on N4erz3f63MiCZLkDE).
|
|
44
88
|
*
|
|
@@ -113,7 +157,7 @@ export function migrateLegacySeedWithReport(json) {
|
|
|
113
157
|
// Hub hosts first, so the puzzle pass below sees the rooms as they
|
|
114
158
|
// will be played. Both passes are pure; order only matters for the
|
|
115
159
|
// spread that follows.
|
|
116
|
-
json = stripHubHosts(json);
|
|
160
|
+
json = wardDevicesToRooms(stripHubHosts(json));
|
|
117
161
|
const puzzles = json.puzzles ?? [];
|
|
118
162
|
// IDEMPOTENCE: a seed already migrated has no puzzles left to move,
|
|
119
163
|
// so the whole pass is a copy. Checked first so a re-migrated seed
|
|
@@ -207,7 +251,9 @@ export function migrateLegacySeedWithReport(json) {
|
|
|
207
251
|
// MANDATORY (see device-seed.ts): an absent riddle used to render
|
|
208
252
|
// the literal string "undefined" into an NPC's prompt.
|
|
209
253
|
description: p.riddle || `${p.name} -- sealed.`,
|
|
210
|
-
|
|
254
|
+
// A legacy ward is written as one and moved onto its room at the
|
|
255
|
+
// end of this pass (wardDevicesToRooms).
|
|
256
|
+
kind: kind,
|
|
211
257
|
code,
|
|
212
258
|
keyItem: p.requiredItem,
|
|
213
259
|
grantsItem: p.rewardItem,
|
|
@@ -244,6 +290,8 @@ export function migrateLegacySeedWithReport(json) {
|
|
|
244
290
|
// dropping the content on the floor silently.
|
|
245
291
|
log.error(`seed-migration: "${p.name}" is placed in no room and guards ${guardedExits.length} exits -- cannot site it. Dropped.`);
|
|
246
292
|
}
|
|
247
|
-
|
|
293
|
+
// The puzzle pass writes spell barriers as legacy ward devices; they
|
|
294
|
+
// land on their rooms here, the same as any other seed's.
|
|
295
|
+
return { seed: wardDevicesToRooms({ ...json, rooms, npcs, puzzles: [] }), fates };
|
|
248
296
|
}
|
|
249
297
|
//# sourceMappingURL=seed-migration.js.map
|
|
@@ -20,6 +20,9 @@ import { ledSeatsNow, hostCannotAnswer, reasonOf, CREW_JOB_RIDE_LINE, CREW_JOB_A
|
|
|
20
20
|
import { isHostProcess } from './utilities/host-process.js';
|
|
21
21
|
import { buildCleanupSeed } from './factories/cleanup-seed.js';
|
|
22
22
|
import { EditFileCommand, EraseCommand } from './commands/edit-file.js';
|
|
23
|
+
import { LeapCommand } from './commands/leap.js';
|
|
24
|
+
import { CrackCommand } from './commands/crack.js';
|
|
25
|
+
import { DisarmCommand } from './commands/disarm.js';
|
|
23
26
|
import { EraseMarkCommand } from './commands/erase-mark.js';
|
|
24
27
|
import { SPRITE_POWER_COMMANDS } from './commands/sprite-power.js';
|
|
25
28
|
import { cleanupOutcome } from './utilities/cleanup-errand.js';
|
|
@@ -192,7 +195,7 @@ import { HintsCommand } from './commands/hints.js';
|
|
|
192
195
|
import { hint, setHints, terse } from './utilities/hints.js';
|
|
193
196
|
import { businessWitness } from './factions.js';
|
|
194
197
|
import { wouldNotice } from './utilities/spots.js';
|
|
195
|
-
import {
|
|
198
|
+
import { cameraSeeing, liveFootage } from './utilities/surveillance.js';
|
|
196
199
|
import { InstallCommand } from './commands/install.js';
|
|
197
200
|
import { BuyCommand, BrowseCommand } from './commands/buy.js';
|
|
198
201
|
import { SellCommand } from './commands/sell.js';
|
|
@@ -666,8 +669,11 @@ export default class Game {
|
|
|
666
669
|
this.scene.crimeLogged = true;
|
|
667
670
|
let billed = amount;
|
|
668
671
|
const who = actor ?? this.heatActor();
|
|
669
|
-
|
|
670
|
-
|
|
672
|
+
// A LENS THAT CAN SEE THEM (surveillance.ts cameraSeeing: line of
|
|
673
|
+
// sight, no range) -- the room whose camera it is keeps the footage.
|
|
674
|
+
const seen = cameraSeeing(this.scene.getRooms(), who);
|
|
675
|
+
const room = seen?.room;
|
|
676
|
+
if (room) {
|
|
671
677
|
billed *= 2;
|
|
672
678
|
if (!this._camSeenRooms.has(room.name)) {
|
|
673
679
|
this._camSeenRooms.add(room.name);
|
|
@@ -5284,6 +5290,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5284
5290
|
// Vertical travel by name (commands/climb.ts) -- shortcuts over
|
|
5285
5291
|
// move, which already crosses levels.
|
|
5286
5292
|
CommandFactory.registerCommand('climb', ClimbCommand);
|
|
5293
|
+
// Over a ledge on purpose (utilities/ledge-jump.ts).
|
|
5294
|
+
CommandFactory.registerCommand('leap', LeapCommand);
|
|
5287
5295
|
// The word a player actually typed (vkDz3cquad25GACpm). mantle is
|
|
5288
5296
|
// six edits from climb, so no amount of typo tolerance was ever
|
|
5289
5297
|
// going to find it -- a synonym has to be said out loud.
|
|
@@ -5365,6 +5373,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5365
5373
|
CommandFactory.registerCommand('edit', EditFileCommand);
|
|
5366
5374
|
CommandFactory.registerCommand('erase', EraseCommand);
|
|
5367
5375
|
CommandFactory.registerCommand('wipe', EraseCommand);
|
|
5376
|
+
// A file's own defences (p.238-242, utilities/file-guard.ts).
|
|
5377
|
+
CommandFactory.registerCommand('crack', CrackCommand);
|
|
5378
|
+
CommandFactory.registerCommand('disarm', DisarmCommand);
|
|
5368
5379
|
// MAtrix Recognition Keys (canon p.235-236).
|
|
5369
5380
|
CommandFactory.registerCommand('mark', MarkCommand);
|
|
5370
5381
|
CommandFactory.registerCommand('marks', MarkCommand);
|
|
@@ -10236,7 +10247,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
10236
10247
|
// guessVerb, which owns the ordering and the reason.
|
|
10237
10248
|
const guess = guessVerb(verb, knownVerbs);
|
|
10238
10249
|
if (guess && guess !== verb) {
|
|
10239
|
-
|
|
10250
|
+
// A LEAP IS NEVER GUESSED (user 2026-10-06: "don't attempt to auto
|
|
10251
|
+
// read this one"): a fall is one deliberate command, not a typo's.
|
|
10252
|
+
if (guess === 'quit' || guess === 'exit' || guess === 'leap') {
|
|
10240
10253
|
return out(`I don't understand "${verb}" -- did you mean "${guess}"?`);
|
|
10241
10254
|
}
|
|
10242
10255
|
const corrected = await this.runInputAs(actorName, [guess, ...args].join(' '));
|
|
@@ -49,8 +49,16 @@ export class Camera {
|
|
|
49
49
|
* dark without owning the host (surveillance.ts cameraStateNote).
|
|
50
50
|
* A smashed lens needs no knowing: anyone can see it. */
|
|
51
51
|
knownOffBy = new Set();
|
|
52
|
+
/** THE HOST THIS LENS IS SLAVED TO, when the author named one: a room
|
|
53
|
+
* with a node (surveillance.ts lensMaster). Unset = the room's master. */
|
|
54
|
+
masterHost;
|
|
55
|
+
/** The building an outdoor lens is bolted to (seed `mountedOn`): not a
|
|
56
|
+
* pole, and hardlined to that building's host. */
|
|
57
|
+
mountedOn;
|
|
52
58
|
constructor(cfg) {
|
|
53
59
|
this.name = cfg.name;
|
|
60
|
+
this.masterHost = cfg.masterHost;
|
|
61
|
+
this.mountedOn = cfg.mountedOn;
|
|
54
62
|
this.perch = cfg.name;
|
|
55
63
|
this.wall = cfg.wall;
|
|
56
64
|
this.rating = cfg.rating;
|
|
@@ -6,9 +6,8 @@ import { hint } from '../utilities/hints.js';
|
|
|
6
6
|
* which prices a door lock at 2, and p.234, where a device's Data
|
|
7
7
|
* Processing and Firewall are both simply its Device Rating).
|
|
8
8
|
*
|
|
9
|
-
* A
|
|
10
|
-
*
|
|
11
|
-
* broadcastsAro() answers false for it on the same grounds -- see
|
|
9
|
+
* A WARD IS NOT A DEVICE (user 2026-10-06): it is a room's astral
|
|
10
|
+
* barrier (utilities/wards.ts), with a Force and no Device Rating -- see
|
|
12
11
|
* kbWdrysBQSahg67cA, where the ask was "anything that can be HACKED must
|
|
13
12
|
* be a device", not "everything in the room is".
|
|
14
13
|
*/
|
|
@@ -21,8 +20,7 @@ export const DEFAULT_DEVICE_RATING = {
|
|
|
21
20
|
* hidden from view, such as locks and other security devices"; user
|
|
22
21
|
* ruling 2026-09-16 on what that covers -- "cameras, locks, sensors
|
|
23
22
|
* etc"). In this engine a 'lock' and a 'panel' ARE the security
|
|
24
|
-
* furniture: the maglock, the keypad, the safe.
|
|
25
|
-
* has no wireless mode to hide in.
|
|
23
|
+
* furniture: the maglock, the keypad, the safe.
|
|
26
24
|
*/
|
|
27
25
|
export const SILENT_BY_DEFAULT = new Set(['lock', 'panel']);
|
|
28
26
|
/**
|
|
@@ -57,8 +55,7 @@ export const WIRELESS_OFF_BY_DEFAULT = new Set(['lock', 'panel']);
|
|
|
57
55
|
* Structure / Armor. Fragile is standard glass; Reinforced is a security
|
|
58
56
|
* door, armored glass, Kevlar wallboard; Heavy is concrete or a metal
|
|
59
57
|
* beam. A device seed that says neither gets Reinforced -- the ordinary
|
|
60
|
-
* locked door -- or Heavy when its footprint is large.
|
|
61
|
-
* and has no Structure to shoot.
|
|
58
|
+
* locked door -- or Heavy when its footprint is large.
|
|
62
59
|
*/
|
|
63
60
|
export const BARRIER_TIERS = {
|
|
64
61
|
fragile: { structure: 1, armor: 2 },
|
|
@@ -66,8 +63,6 @@ export const BARRIER_TIERS = {
|
|
|
66
63
|
heavy: { structure: 12, armor: 20 },
|
|
67
64
|
};
|
|
68
65
|
export function barrierDefaultsFor(kind, footprint) {
|
|
69
|
-
if (kind === 'ward')
|
|
70
|
-
return { structure: 0, armor: 0 };
|
|
71
66
|
return footprint === 'large' ? BARRIER_TIERS.heavy : BARRIER_TIERS.reinforced;
|
|
72
67
|
}
|
|
73
68
|
/** A seed's rating, kept to the range the book uses (1-12, the same
|
|
@@ -108,7 +103,7 @@ export function clampDeviceRating(rating) {
|
|
|
108
103
|
* A CODE ONLY BELONGS ON A PANEL. User ruling, verbatim: "if it's a
|
|
109
104
|
* _mechanical_ lock, it has a key or can be picked. You can't talk at a
|
|
110
105
|
* _mechanical_ lock." A 'lock' therefore opens by key or by force, and
|
|
111
|
-
*
|
|
106
|
+
* has nothing to hear a phrase with.
|
|
112
107
|
* The migration enforces this by dropping the phrase off anything that
|
|
113
108
|
* isn't a panel.
|
|
114
109
|
*/
|
|
@@ -262,7 +257,7 @@ export class Device {
|
|
|
262
257
|
* ceases functioning" -- a camera records nothing, a smartgun sparks
|
|
263
258
|
* and dies -- but "a lock stays locked" (p.228): bricking is never a
|
|
264
259
|
* way through a maglock. Repaired on the bench (rest.ts), never by a
|
|
265
|
-
* reboot.
|
|
260
|
+
* reboot.
|
|
266
261
|
*/
|
|
267
262
|
_matrixDamage = 0;
|
|
268
263
|
/**
|
|
@@ -289,19 +284,17 @@ export class Device {
|
|
|
289
284
|
this.refuseMessage = config.refuseMessage || `Nothing happens.`;
|
|
290
285
|
this.rating = config.rating !== undefined ? clampDeviceRating(config.rating) : DEFAULT_DEVICE_RATING[config.kind] ?? 0;
|
|
291
286
|
const tier = barrierDefaultsFor(config.kind, config.footprint);
|
|
292
|
-
this.structure =
|
|
293
|
-
this.armor =
|
|
294
|
-
//
|
|
295
|
-
// because a seeded keypad nobody thought about is a keypad on the
|
|
287
|
+
this.structure = Math.max(0, Math.floor(config.structure ?? tier.structure));
|
|
288
|
+
this.armor = Math.max(0, Math.floor(config.armor ?? tier.armor));
|
|
289
|
+
// Wireless defaults ON, because a seeded keypad nobody thought about is a keypad on the
|
|
296
290
|
// grid -- which is the state the report says the game should model.
|
|
297
|
-
this.wireless = config.
|
|
291
|
+
this.wireless = config.wireless ?? true;
|
|
298
292
|
this.masterHost = config.masterHost;
|
|
299
293
|
// SECURITY IS SILENT BY DEFAULT (p.217-218: "locks and other
|
|
300
294
|
// security devices"). In this engine a 'lock' and a 'panel' ARE the
|
|
301
295
|
// security furniture -- the maglock, the keypad, the safe -- so both
|
|
302
|
-
// are born dark unless a seed says otherwise.
|
|
303
|
-
|
|
304
|
-
this.silent = config.kind === 'ward' ? false : (config.silent ?? SILENT_BY_DEFAULT.has(config.kind));
|
|
296
|
+
// are born dark unless a seed says otherwise.
|
|
297
|
+
this.silent = config.silent ?? SILENT_BY_DEFAULT.has(config.kind);
|
|
305
298
|
}
|
|
306
299
|
isOpen() {
|
|
307
300
|
return this.open;
|
|
@@ -335,7 +328,7 @@ export class Device {
|
|
|
335
328
|
* of those three sites.
|
|
336
329
|
*/
|
|
337
330
|
onHoled;
|
|
338
|
-
/** Boxes actually taken
|
|
331
|
+
/** Boxes actually taken. */
|
|
339
332
|
takeBarrierDamage(boxes) {
|
|
340
333
|
if (this.structure <= 0)
|
|
341
334
|
return 0;
|
|
@@ -372,7 +365,7 @@ export class Device {
|
|
|
372
365
|
*/
|
|
373
366
|
// ---- The Matrix condition monitor (commands/disable.ts) ----
|
|
374
367
|
/** p.228: 8 + (Device Rating / 2), rounded up as the deck's is. 0 for
|
|
375
|
-
* a thing with no rating
|
|
368
|
+
* a thing with no rating: nothing there to brick. */
|
|
376
369
|
get matrixMonitor() {
|
|
377
370
|
return this.rating > 0 ? 8 + Math.ceil(this.rating / 2) : 0;
|
|
378
371
|
}
|
|
@@ -165,6 +165,14 @@ export class Host {
|
|
|
165
165
|
* to say for itself is the report.
|
|
166
166
|
*/
|
|
167
167
|
purpose;
|
|
168
|
+
/** The author's IC launch order (room seed icOrder), names as written. */
|
|
169
|
+
icOrder;
|
|
170
|
+
/** Reboot a bricked program first, or continue down the line first. */
|
|
171
|
+
icBricked;
|
|
172
|
+
/** What the author called it (room seed hostName), when they did. The
|
|
173
|
+
* host's IDENTITY stays `name` (its room's); this is what it is CALLED
|
|
174
|
+
* -- grid-names.ts prints and accepts it. */
|
|
175
|
+
displayName;
|
|
168
176
|
sculpt;
|
|
169
177
|
constructor(over, opts = {}) {
|
|
170
178
|
this.over = over;
|
|
@@ -39,6 +39,10 @@ export class Item extends AbstractItem {
|
|
|
39
39
|
price;
|
|
40
40
|
// See IItemConstructorConfig.atSpot; take.ts gates reach on it.
|
|
41
41
|
atSpot;
|
|
42
|
+
/** WHERE IT LIES, room-local cells, when an author put it there
|
|
43
|
+
* (player-built runs, 2026-10-06; seed items[].cell). Reach still goes
|
|
44
|
+
* through atSpot, which is set from this cell; maps draw it here. */
|
|
45
|
+
atCell;
|
|
42
46
|
// PAN warfare (see commands/hack.ts): a cracked smart-device locks up.
|
|
43
47
|
// A jammed firearm can't fire (fists until cleared); "reboot" clears
|
|
44
48
|
// every jammed device you carry, in the lull.
|
|
@@ -112,6 +116,16 @@ export class Item extends AbstractItem {
|
|
|
112
116
|
* holds (commands/edit-file.ts). */
|
|
113
117
|
hashedBy;
|
|
114
118
|
hashTurns = 0;
|
|
119
|
+
/** PROTECTION (SR5 p.239): the rating Edit File's "protect" left on a
|
|
120
|
+
* file. It cannot be read, changed, deleted or copied until Crack File
|
|
121
|
+
* (p.238) breaks it -- utilities/file-guard.ts. 0 = unprotected. */
|
|
122
|
+
protection = 0;
|
|
123
|
+
/** A DATA BOMB (p.242): goes off on any read/edit/copy/delete without
|
|
124
|
+
* the passcode (utilities/file-guard.ts). One per file. */
|
|
125
|
+
dataBomb;
|
|
126
|
+
/** Personas whose Matrix Perception has turned up the bomb (p.242) --
|
|
127
|
+
* what Disarm Data Bomb (commands/disarm.ts) needs before it is tried. */
|
|
128
|
+
bombSeenBy = new Set();
|
|
115
129
|
/** WATERMARK: an invisible tag only Resonance-driven things read -- a
|
|
116
130
|
* technomancer or another sprite. A new one overwrites the old. */
|
|
117
131
|
watermark;
|
|
@@ -12,6 +12,7 @@ import { Door } from './door.js';
|
|
|
12
12
|
import { AI } from '../../../../tools/ai/ai.class.js';
|
|
13
13
|
import { CALL_ICON, CALL_COLOR } from '../utilities/comm-style.js';
|
|
14
14
|
import { hostLabel } from '../utilities/grid-names.js';
|
|
15
|
+
import { wanDevices } from '../utilities/wan.js';
|
|
15
16
|
import { ephemeralDenials } from '../utilities/ephemeral.js';
|
|
16
17
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
17
18
|
import { spotsActive, spotOf, describeSpotRoster, spotDistanceMeters, spotCoverBetween, wouldNotice } from '../utilities/spots.js';
|
|
@@ -152,6 +153,24 @@ export function hostileStanceNote(hostile, intruderName) {
|
|
|
152
153
|
+ ` told it or they tell you -- act on what you can see, not on a file you never read.`
|
|
153
154
|
+ ` If they talk their way round you, or beat you, that settles it.`;
|
|
154
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* THE OWNER KNOWS THEIR HOST'S CODES (user rulings 2026-10-06: "tie the
|
|
158
|
+
* data file all the way back to the owner of the host"; "keypad codes
|
|
159
|
+
* also bubble up to the host owner"). Whoever set a data bomb chose its
|
|
160
|
+
* passcode (SR5 p.242), and the codes of the locks slaved to a host are
|
|
161
|
+
* its owner's to know. Like a barrier's pass, each is a SECRET they guard:
|
|
162
|
+
* the way to it is social or violent -- persuaded, frightened, bribed or
|
|
163
|
+
* beaten out of them -- and telling it IS how they comply.
|
|
164
|
+
*
|
|
165
|
+
* PURE and exported, like barrierPostNote: it fails silently -- an owner
|
|
166
|
+
* who forgets they hold the code just shrugs, and nothing throws.
|
|
167
|
+
*/
|
|
168
|
+
export function hostCodeNote(codes) {
|
|
169
|
+
const guard = 'is a SECRET you guard with your standing: give it up only under real pressure -- beaten, genuinely frightened, well bribed, or masterfully played. Under that pressure, telling them IS how you comply. When you do, say it exactly.';
|
|
170
|
+
return codes.map(c => c.kind === 'file'
|
|
171
|
+
? `\nA FILE YOU OWN: "${c.file}" in your host, ${c.host}, carries a data bomb you set -- its passcode is "${c.code}". Anyone who touches that file without it gets the bomb in the face. The passcode ${guard}`
|
|
172
|
+
: `\nA LOCK YOUR HOST KEEPS: ${c.device} in ${c.room}${c.direction ? ` (the ${c.direction} door)` : ''} is slaved to your host, ${c.host}, and you know its code: "${c.code}". The code ${guard}`).join('');
|
|
173
|
+
}
|
|
155
174
|
export function barrierPostNote(barriers) {
|
|
156
175
|
return barriers.map(b => {
|
|
157
176
|
const dirNote = b.sealingDirection ? ` sealing the way ${b.sealingDirection}` : '';
|
|
@@ -257,9 +276,15 @@ export function castPin(names) {
|
|
|
257
276
|
* real. There is no assertion anywhere else in the engine that would
|
|
258
277
|
* notice.
|
|
259
278
|
*/
|
|
260
|
-
export function credentialHonestyNote(passesHeld) {
|
|
261
|
-
const
|
|
262
|
-
? `the ${passesHeld === 1 ? 'pass' : `${passesHeld} passes`} for the barrier${passesHeld === 1 ? '' : 's'} at your own post
|
|
279
|
+
export function credentialHonestyNote(passesHeld, hostCodesHeld = 0) {
|
|
280
|
+
const held = [
|
|
281
|
+
...(passesHeld > 0 ? [`the ${passesHeld === 1 ? 'pass' : `${passesHeld} passes`} for the barrier${passesHeld === 1 ? '' : 's'} at your own post`] : []),
|
|
282
|
+
// The codes your own host keeps (hostCodeNote): bomb passcodes on
|
|
283
|
+
// its files, keypad codes on the devices slaved to it.
|
|
284
|
+
...(hostCodesHeld > 0 ? [`the ${hostCodesHeld === 1 ? 'code' : `${hostCodesHeld} codes`} your own host keeps`] : []),
|
|
285
|
+
];
|
|
286
|
+
const known = held.length > 0
|
|
287
|
+
? `${held.join(' and ')}, named above, and NO others`
|
|
263
288
|
: `NO code, pass, or combination to anything -- you hold none`;
|
|
264
289
|
return `
|
|
265
290
|
CREDENTIALS YOU DO NOT HOLD: you know ${known}. You cannot deduce, guess, or
|
|
@@ -1087,9 +1112,6 @@ Available in-game commands (use with [COMMAND] prefix):
|
|
|
1087
1112
|
on yourself or an ally; its bearer gets +soak, and YOU carry -2 dice to all else
|
|
1088
1113
|
while you hold it; cast at them again to release). EVERY cast costs drain -- stun damage to YOU the caster; fill
|
|
1089
1114
|
your stun track and you're out cold. Never cast if you're mundane with no magic gear.
|
|
1090
|
-
- cast — bare "cast" attempts to magically bypass the ward/barrier in this room
|
|
1091
|
-
without knowing the answer (requires the same talent or gear; failing burns the
|
|
1092
|
-
route for good and the backlash hurts you badly)
|
|
1093
1115
|
- search — search the room for hidden things
|
|
1094
1116
|
- look — look around the current room
|
|
1095
1117
|
- look at <target> - looks at an item, player, or direction
|
|
@@ -1231,7 +1253,9 @@ description text.`;
|
|
|
1231
1253
|
* stimulus to wake.
|
|
1232
1254
|
*/
|
|
1233
1255
|
isConstruct() {
|
|
1234
|
-
|
|
1256
|
+
// A PERSONA WITH A BODY IS A PERSON (utilities/spider.ts): a spider
|
|
1257
|
+
// jacked in from a chair is on the matrix plane and is not ice.
|
|
1258
|
+
return this.icKind !== undefined || (this.plane === 'matrix' && !this.bodyRoom);
|
|
1235
1259
|
}
|
|
1236
1260
|
/** Every AI entry point asks this: a construct never runs the model,
|
|
1237
1261
|
* and anyone in a Combat Turn yields to the turn loop. */
|
|
@@ -2469,7 +2493,36 @@ An ORDER is the only way these goods change hands -- never a dialogue sale, neve
|
|
|
2469
2493
|
// Applies to EVERYONE, companions and the client included: a bound
|
|
2470
2494
|
// spirit inventing its master a code is the same bug wearing a
|
|
2471
2495
|
// leash.
|
|
2472
|
-
|
|
2496
|
+
// THE CODES THEIR OWN HOST KEEPS (hostCodeNote; user ruling
|
|
2497
|
+
// 2026-10-06: "keypad codes also bubble up to the host owner, same
|
|
2498
|
+
// as data bomb codes"). Read live off every host this NPC owns,
|
|
2499
|
+
// wherever they stand: the bomb passcodes on its files, and the code
|
|
2500
|
+
// of every still-shut device slaved to it (wan.ts wanDevices -- the
|
|
2501
|
+
// same wiring a hack through the host reaches). A spent bomb or an
|
|
2502
|
+
// opened lock drops out on its own; a lock already briefed as the
|
|
2503
|
+
// barrier at their own post is not said twice. Not for an ally shell
|
|
2504
|
+
// (the same caller decision as the barrier posts above).
|
|
2505
|
+
const hostCodes = [];
|
|
2506
|
+
if (!this.allyOf) {
|
|
2507
|
+
const rooms = this._scene?.getRooms?.() ?? {};
|
|
2508
|
+
const atPost = new Set(barrierPosts.map(b => b.name));
|
|
2509
|
+
for (const r of Object.values(rooms)) {
|
|
2510
|
+
if (r.owner !== this.name || !r.host)
|
|
2511
|
+
continue;
|
|
2512
|
+
const host = hostLabel(r);
|
|
2513
|
+
for (const f of r.host.files()) {
|
|
2514
|
+
if (f.dataBomb?.passcode)
|
|
2515
|
+
hostCodes.push({ kind: 'file', file: f.name, host, code: f.dataBomb.passcode });
|
|
2516
|
+
}
|
|
2517
|
+
for (const { device, room: at } of wanDevices(rooms, r)) {
|
|
2518
|
+
if (!device.code || device.open || atPost.has(device.name))
|
|
2519
|
+
continue;
|
|
2520
|
+
hostCodes.push({ kind: 'device', device: device.name, room: at.name, direction: device.opensExit ? String(device.opensExit) : undefined, host, code: device.code });
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
const hostCodeBlock = hostCodeNote(hostCodes);
|
|
2525
|
+
const credentialHonesty = credentialHonestyNote(passesHeldBy(barrierPosts), hostCodes.length);
|
|
2473
2526
|
const localKnowledge = localKnowledgeNote(room?.name);
|
|
2474
2527
|
// AN EPHEMERAL IS A PASSING FACE (7ALf6u7PvTg6TZSm7, see
|
|
2475
2528
|
// utilities/ephemeral.ts). The brief is trimmed hard everywhere
|
|
@@ -2664,7 +2717,7 @@ Your objective is to drive the story line.`}
|
|
|
2664
2717
|
${formattedExitsStr}
|
|
2665
2718
|
${roomInventoryDesc}
|
|
2666
2719
|
${eph ? '' : waresDesc}
|
|
2667
|
-
${eph ? '' : swagNote}${eph ? '' : clientNote}${hostileBlock}${picketBlock}${barrierBlock}${credentialHonesty}${localKnowledge}${ephDenials}
|
|
2720
|
+
${eph ? '' : swagNote}${eph ? '' : clientNote}${hostileBlock}${picketBlock}${barrierBlock}${hostCodeBlock}${credentialHonesty}${localKnowledge}${ephDenials}
|
|
2668
2721
|
${eph ? '' : familiarity}
|
|
2669
2722
|
${eph ? '' : ordersNote}
|
|
2670
2723
|
${droneNote}
|
|
@@ -298,6 +298,14 @@ export class Player extends AbstractPlayer {
|
|
|
298
298
|
* persuaded him while standing in the same doorway.
|
|
299
299
|
*/
|
|
300
300
|
guarding = false;
|
|
301
|
+
/**
|
|
302
|
+
* THE DOOR THIS ACTOR WATCHES -- a direction out of the room they stand
|
|
303
|
+
* in (utilities/door-watch.ts). Not a barrier: they let you by, until
|
|
304
|
+
* they notice you going for it -- then they turn hostile and the fight
|
|
305
|
+
* opens there (user ruling 2026-10-06, "guarded by"). A guarding actor
|
|
306
|
+
* in a doorway watches it too, without this being set.
|
|
307
|
+
*/
|
|
308
|
+
watching;
|
|
301
309
|
/**
|
|
302
310
|
* THIS ACTOR IS OPPOSITION (JjQdy4A4tzcQdPHno: "none of the NPCs ever
|
|
303
311
|
* seem to get hostile").
|
|
@@ -5381,6 +5389,9 @@ export class Player extends AbstractPlayer {
|
|
|
5381
5389
|
// Sneaking-vs-Perception rolls per witness (NPC.see). Broken by any
|
|
5382
5390
|
// loud act -- attacking, casting, speaking, brandishing, calling.
|
|
5383
5391
|
sneaking = false;
|
|
5392
|
+
/** The ward this astral form has PRESSED THROUGH and not yet crossed
|
|
5393
|
+
* (utilities/wards.ts pressThroughWard): one crossing, then spent. */
|
|
5394
|
+
wardPass;
|
|
5384
5395
|
// ==================== Jumped-in rigging (commands/jump.ts) ====================
|
|
5385
5396
|
// The drone currently being ridden while plane === 'drone': the item
|
|
5386
5397
|
// whose hull track (Item.droneDamage) eats what would hit the rider,
|