@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,70 @@
|
|
|
1
|
+
import { NPC } from '../models/npc.js';
|
|
2
|
+
import { rollPool, formatRoll } from './dice.js';
|
|
3
|
+
import { wouldNotice, doorwayBlocker } from './spots.js';
|
|
4
|
+
import { turnHostile } from './hostile-contact.js';
|
|
5
|
+
/**
|
|
6
|
+
* A GUARDED DOOR (player-built runs, 2026-10-06). Two ways an NPC can own
|
|
7
|
+
* a doorway, and the author picks per exit:
|
|
8
|
+
*
|
|
9
|
+
* - HELD BY: a bouncer standing in it, physically opposing passage
|
|
10
|
+
* (Player.guarding + standing on the threshold; spots.ts holdsGround).
|
|
11
|
+
* - GUARDED BY: someone watching it from wherever they stand
|
|
12
|
+
* (Player.watching = the direction). They do not block -- but "will
|
|
13
|
+
* be hostile if you attempt to move past it".
|
|
14
|
+
*
|
|
15
|
+
* And "by association, something that is heldBy is also guardedBy": the
|
|
16
|
+
* bouncer in the doorway watches it too.
|
|
17
|
+
*
|
|
18
|
+
* CAUGHT IN THE ACT (user ruling): a watcher who NOTICES the runner going
|
|
19
|
+
* for the door turns hostile and the fight opens right there, before the
|
|
20
|
+
* runner leaves -- the move does not happen. A runner who is sneaking
|
|
21
|
+
* gets the same opposed test a sneaking arrival does (NPC.see: Agility +
|
|
22
|
+
* Sneaking vs Intuition + Perception, ties to the watcher); one who is
|
|
23
|
+
* not is noticed whenever the watcher would notice them at all
|
|
24
|
+
* (spots.ts wouldNotice).
|
|
25
|
+
*/
|
|
26
|
+
export function doorWatchers(room, direction, mover) {
|
|
27
|
+
const dir = String(direction).toLowerCase();
|
|
28
|
+
const held = doorwayBlocker(room, direction, mover);
|
|
29
|
+
return room.getActors()
|
|
30
|
+
.filter((a) => a instanceof NPC && a !== mover)
|
|
31
|
+
.filter(n => !n.isIncapacitated() && n.allyOf !== mover.name && !n.surrendered)
|
|
32
|
+
.filter(n => n === held || String(n.watching ?? '').toLowerCase() === dir);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The watcher (if any) who catches `mover` going for the door. Rolls the
|
|
36
|
+
* sneak contest per watcher, as an arrival does; the first to spot them
|
|
37
|
+
* is returned, with the lines the player should see.
|
|
38
|
+
*/
|
|
39
|
+
export function caughtAtDoor(room, direction, mover, isPlayer) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
for (const npc of doorWatchers(room, direction, mover)) {
|
|
42
|
+
if (!npc.canPerceive(mover))
|
|
43
|
+
continue;
|
|
44
|
+
if (mover.sneaking) {
|
|
45
|
+
const theirSkill = mover.skillRating('sneaking');
|
|
46
|
+
const sneak = rollPool(Math.max(1, mover.agility + (theirSkill > 0 ? theirSkill : -1) + mover.bonus('sneak') + mover.woundModifier));
|
|
47
|
+
const mySkill = npc.skillRating('perception');
|
|
48
|
+
const spot = rollPool(Math.max(1, npc.intuition + (mySkill > 0 ? mySkill : -1) + npc.woundModifier));
|
|
49
|
+
if (isPlayer)
|
|
50
|
+
lines.push(`Slip past ${npc.name}: ${formatRoll(sneak)} vs ${formatRoll(spot)} -- ${sneak.hits > spot.hits ? 'unseen.' : 'SPOTTED.'}`);
|
|
51
|
+
if (sneak.hits > spot.hits)
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
else if (!wouldNotice(npc, mover)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
return { npc, lines };
|
|
58
|
+
}
|
|
59
|
+
// Nobody caught them -- but a sneak past a watcher still shows its dice.
|
|
60
|
+
return { lines };
|
|
61
|
+
}
|
|
62
|
+
/** Caught: the watcher turns hostile and the fight opens, now. */
|
|
63
|
+
export async function springDoorWatch(scene, room, direction, mover, npc) {
|
|
64
|
+
turnHostile(npc, scene.beat ?? 0, `caught ${mover.name} going for the ${String(direction)} door of ${room.name}`);
|
|
65
|
+
if (!scene.encounterFor?.(mover)) {
|
|
66
|
+
await scene.startEncounter(room, { aggressor: npc, target: mover, ambush: false });
|
|
67
|
+
}
|
|
68
|
+
return `${npc.name} sees you go for the ${String(direction)} door -- and comes for you.`;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=door-watch.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { rollPool, formatRoll } from './dice.js';
|
|
2
|
+
import { fallDamageDVMeters } from './room-grid.js';
|
|
3
|
+
/** SR5 p.172: falling damage carries AP -4. */
|
|
4
|
+
export const FALL_AP = -4;
|
|
5
|
+
/**
|
|
6
|
+
* A LANDING (SR5 p.172, RAG-checked 2026-10-06): a fall of more than three
|
|
7
|
+
* metres deals Physical damage with a DV equal to the metres fallen, AP -4,
|
|
8
|
+
* resisted with Body + Armor. One rule for every way down -- a climb that
|
|
9
|
+
* slips, a rappel that runs, a leap off a ledge -- so none of them can be
|
|
10
|
+
* the one that forgets the AP.
|
|
11
|
+
*
|
|
12
|
+
* The AP takes up to four points of ARMOR out of the soak pool (it cannot
|
|
13
|
+
* cut into Body). Returns the boxes taken and the mechanics line; the
|
|
14
|
+
* caller writes its own prose.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveFall(actor, meters) {
|
|
17
|
+
const dv = fallDamageDVMeters(meters);
|
|
18
|
+
if (dv <= 0)
|
|
19
|
+
return { dv: 0, dealt: 0 };
|
|
20
|
+
const armorCut = Math.min(-FALL_AP, Math.max(0, actor.getArmorValue()));
|
|
21
|
+
const soak = rollPool(Math.max(0, actor.getSoakPool() - armorCut));
|
|
22
|
+
const dealt = Math.max(0, dv - soak.hits);
|
|
23
|
+
if (dealt > 0)
|
|
24
|
+
actor.takeDamage(dealt);
|
|
25
|
+
return { dv, dealt, meta: `Fall: DV ${dv}P (${Math.round(meters)}m, AP ${FALL_AP}) v. Body + Armor: ${formatRoll(soak)} -> ${dealt}` };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=falling.js.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { rollD6Total, rollPool, formatRoll } from './dice.js';
|
|
2
|
+
import { hint } from './hints.js';
|
|
3
|
+
import { noteHarm } from './harm-note.js';
|
|
4
|
+
import { leaveMatrix } from './planes.js';
|
|
5
|
+
import { hostLabel } from './grid-names.js';
|
|
6
|
+
import { fuzzyPickName } from './fuzzy-match.js';
|
|
7
|
+
import { showsMechanics, mechanicsActorPrefix } from './mechanics-audience.js';
|
|
8
|
+
/** Splits a trailing "with <passcode>" / "passcode <x>" off a verb's words. */
|
|
9
|
+
export function splitPasscode(words) {
|
|
10
|
+
const at = words.findIndex(w => /^(with|passcode|code)$/i.test(w));
|
|
11
|
+
if (at < 0 || at === words.length - 1)
|
|
12
|
+
return { rest: words };
|
|
13
|
+
return { rest: words.slice(0, at), passcode: words.slice(at + 1).join(' ') };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The gate every read/edit/copy/delete of a host file passes through,
|
|
17
|
+
* after the action is billed and before its roll. Fires an un-passcoded
|
|
18
|
+
* bomb, then refuses a protected file.
|
|
19
|
+
*/
|
|
20
|
+
export function guardFile(scene, logger, actor, host, file, touch, passcode) {
|
|
21
|
+
const lines = [];
|
|
22
|
+
const bomb = file.dataBomb;
|
|
23
|
+
// Spoken by its owner, typed by a runner: case, quotes and spacing are
|
|
24
|
+
// not the code (filePasscodeNote).
|
|
25
|
+
if (bomb && !(bomb.passcode && passcode && sameCode(passcode, bomb.passcode))) {
|
|
26
|
+
const blast = detonate(scene, logger, actor, host, file);
|
|
27
|
+
lines.push(...blast.lines);
|
|
28
|
+
if (blast.stop)
|
|
29
|
+
return { lines, stop: true };
|
|
30
|
+
}
|
|
31
|
+
if (file.protection && file.protection > 0) {
|
|
32
|
+
lines.push(`${file.name} is PROTECTED -- an encrypted file cannot be ${touch === 'copy' ? 'copied' : touch === 'delete' ? 'deleted' : touch === 'read' ? 'read' : 'changed'} until the protection is broken (p.239). ${hint(`"crack ${file.name}" (Crack File, p.238).`)}`);
|
|
33
|
+
return { lines, stop: true };
|
|
34
|
+
}
|
|
35
|
+
return { lines, stop: false };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* THE BOMB GOES OFF (p.242): (Rating)D6 Matrix damage, resisted with
|
|
39
|
+
* Device Rating + Firewall like any Matrix damage (p.228 -- getSoakPool
|
|
40
|
+
* is exactly that on the matrix plane); the file goes too if it was set
|
|
41
|
+
* to; then the bomb is gone. Shared by the file gate and a failed
|
|
42
|
+
* Disarm (p.239: "If you fail, the Data Bomb activates").
|
|
43
|
+
*/
|
|
44
|
+
export function detonate(scene, logger, actor, host, file) {
|
|
45
|
+
const bomb = file.dataBomb;
|
|
46
|
+
file.dataBomb = undefined;
|
|
47
|
+
file.bombSeenBy.clear();
|
|
48
|
+
const dv = rollD6Total(bomb.rating);
|
|
49
|
+
const soak = rollPool(actor.getSoakPool());
|
|
50
|
+
const dealt = Math.max(0, dv - soak.hits);
|
|
51
|
+
if (showsMechanics(scene, actor)) {
|
|
52
|
+
logger.meta(`${mechanicsActorPrefix(scene, actor)}Data Bomb ${bomb.rating} -- ${bomb.rating}D6 = ${dv} DV v. Device Rating + Firewall: ${formatRoll(soak)} -> ${dealt}`);
|
|
53
|
+
}
|
|
54
|
+
logger.write(`${actor.name} set off a Rating ${bomb.rating} data bomb on "${file.name}": ${dv} DV, ${dealt} through.`);
|
|
55
|
+
const lines = [`{red-fg}${file.name} was BOMBED -- the moment you touch it the payload goes off in your face.{/red-fg}`];
|
|
56
|
+
let stop = false;
|
|
57
|
+
if (dealt <= 0) {
|
|
58
|
+
lines.push(` {red-fg}Your firewall eats the blast.{/red-fg}`);
|
|
59
|
+
}
|
|
60
|
+
else if (actor.onLivingPersona()) {
|
|
61
|
+
// p.229: no Matrix Condition Monitor on a living persona -- stun.
|
|
62
|
+
noteHarm(actor, { by: 'a data bomb' });
|
|
63
|
+
actor.takeStun(dealt);
|
|
64
|
+
lines.push(` {red-fg}It tears through your living persona -- ${dealt} stun: ${actor.stunSummary()}{/red-fg}`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
actor.takeMatrixDamage(dealt);
|
|
68
|
+
lines.push(` {red-fg}It burns ${dealt} box${dealt === 1 ? '' : 'es'} off your deck: ${actor.matrixSummary()}{/red-fg}`);
|
|
69
|
+
if (actor.matrixCrashed) {
|
|
70
|
+
lines.push(` {red-fg}The deck BRICKS under it --{/red-fg}`);
|
|
71
|
+
lines.push(...leaveMatrix(scene, actor, { forced: true, reason: `The data bomb burns your deck out from under you.` }));
|
|
72
|
+
stop = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (bomb.deletesFile) {
|
|
76
|
+
host.removeFile(file.name);
|
|
77
|
+
scene.addWorldEvent(`${file.name} was destroyed by its own data bomb in ${host.name}'s archive.`, { plane: 'matrix' });
|
|
78
|
+
lines.push(` {red-fg}And the file goes with it -- ${file.name} is zeroed.{/red-fg}`);
|
|
79
|
+
// Gone is gone: an erase job's file is as erased by its own bomb.
|
|
80
|
+
const banner = scene.checkErased?.(file.name, actor.name);
|
|
81
|
+
if (banner)
|
|
82
|
+
lines.push(banner);
|
|
83
|
+
stop = true;
|
|
84
|
+
}
|
|
85
|
+
actor.noteConditionChanged();
|
|
86
|
+
scene.updateStatus();
|
|
87
|
+
return { lines, stop };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* THE FILE A VERB NAMES, from inside the host that keeps it -- the same
|
|
91
|
+
* gates edit-file.ts walks (a persona, inside, the archive found by a
|
|
92
|
+
* Matrix Search or opened by a crack). A string is the refusal.
|
|
93
|
+
*/
|
|
94
|
+
export function pickHostFile(actor, query, verb) {
|
|
95
|
+
if (actor.plane !== 'matrix')
|
|
96
|
+
return `Files are worked by a persona. ${hint(`"jack in" first.`)}`;
|
|
97
|
+
const host = actor.hostInside;
|
|
98
|
+
if (!host)
|
|
99
|
+
return `A host's files are worked from INSIDE it (p.246). ${hint(`"enter" the host that keeps it.`)}`;
|
|
100
|
+
const files = host.files();
|
|
101
|
+
if (!query) {
|
|
102
|
+
return files.length === 0
|
|
103
|
+
? `${hostLabel(host.over, { capital: true })} keeps no files.`
|
|
104
|
+
: `${verb} which file? ${hint(`("${verb} <file>". "search" lists the archive.)`)}`;
|
|
105
|
+
}
|
|
106
|
+
if (!host.searchedBy.has(actor.name) && !host.over.hostCracked) {
|
|
107
|
+
return `You haven't found ${hostLabel(host.over)}'s archive yet. ${hint(`"search" runs a Matrix Search inside the host (p.241).`)}`;
|
|
108
|
+
}
|
|
109
|
+
const picked = fuzzyPickName(query, files.map(f => f.name));
|
|
110
|
+
const file = picked ? files.find(f => f.name === picked) : undefined;
|
|
111
|
+
if (!file)
|
|
112
|
+
return `No file called "${query}" in ${hostLabel(host.over)}'s archive.`;
|
|
113
|
+
return { host, file };
|
|
114
|
+
}
|
|
115
|
+
/** Two passcodes match ignoring case, surrounding quotes and spacing. */
|
|
116
|
+
function sameCode(a, b) {
|
|
117
|
+
const norm = (s) => s.toLowerCase().replace(/["'`]/g, '').replace(/\s+/g, ' ').trim();
|
|
118
|
+
return norm(a) === norm(b);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=file-guard.js.map
|
|
@@ -29,10 +29,14 @@ import { normalizeLoose } from './fuzzy-match.js';
|
|
|
29
29
|
export function hostAliases(room) {
|
|
30
30
|
const name = normalizeLoose(room.name);
|
|
31
31
|
const bare = ['host', 'the host', 'node', 'the node', 'ice'];
|
|
32
|
+
// A NAMED HOST answers to its name too, with or without "the".
|
|
33
|
+
const named = room.host?.displayName ? normalizeLoose(room.host.displayName.replace(/^the\s+/i, '')) : '';
|
|
34
|
+
const own = named ? [named, `the ${named}`] : [];
|
|
32
35
|
if (name.length === 0)
|
|
33
|
-
return bare;
|
|
36
|
+
return [...bare, ...own];
|
|
34
37
|
return [
|
|
35
38
|
...bare,
|
|
39
|
+
...own,
|
|
36
40
|
// What the grid map calls it.
|
|
37
41
|
name,
|
|
38
42
|
// What look calls it, and what mark's own success line calls it.
|
|
@@ -74,6 +78,11 @@ export function matchesHostName(query, room) {
|
|
|
74
78
|
* hand-written article again.
|
|
75
79
|
*/
|
|
76
80
|
export function hostLabel(room, opts = {}) {
|
|
81
|
+
// A named host is called by its name ("the Kettle Row Grid"), not "the
|
|
82
|
+
// <room> host".
|
|
83
|
+
const named = room.host?.displayName?.replace(/^the\s+/i, '');
|
|
84
|
+
if (named)
|
|
85
|
+
return `${opts.capital ? 'The' : 'the'} ${named}`;
|
|
77
86
|
const bare = room.name.replace(/^the\s+/i, '');
|
|
78
87
|
return `${opts.capital ? 'The' : 'the'} ${bare} host`;
|
|
79
88
|
}
|
|
@@ -5,7 +5,7 @@ import { tableNoteLine } from './drop-in.js';
|
|
|
5
5
|
import { NPC } from '../models/npc.js';
|
|
6
6
|
import { hostLabel, hostPurposeFor } from './grid-names.js';
|
|
7
7
|
import { isLinked } from './comm-style.js';
|
|
8
|
-
import { isWatched, camerasLive, canSnoopFeeds, cameraMasterHost, camerasBroadcast, cameraStateNote } from './surveillance.js';
|
|
8
|
+
import { isWatched, camerasLive, canSnoopFeeds, cameraMasterHost, camerasBroadcast, cameraStateNote, lensLive, lensMaster } from './surveillance.js';
|
|
9
9
|
import { wanDevices } from './wan.js';
|
|
10
10
|
/**
|
|
11
11
|
* The grid's icon inventory for one room -- everything a persona can
|
|
@@ -374,8 +374,17 @@ export function gridIcons(scene, actor, room) {
|
|
|
374
374
|
// cable. It simply refused to say WHICH, in a district that has
|
|
375
375
|
// exactly one, so the reader was left to guess at a fact the
|
|
376
376
|
// engine had already handed them.
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
// EACH LIVE LENS'S OWN MASTER (surveillance.ts lensMaster): a strip's
|
|
378
|
+
// lenses can feed every shop along it.
|
|
379
|
+
const all = scene.getRooms();
|
|
380
|
+
const owners = [...new Set(room.ensureCameras().filter(c => lensLive(all, room, c)).map(c => lensMaster(all, room, c)))]
|
|
381
|
+
.filter((r) => !!r);
|
|
382
|
+
if (owners.length === 0) {
|
|
383
|
+
const fallback = cameraMasterHost(all, room);
|
|
384
|
+
if (fallback)
|
|
385
|
+
owners.push(fallback);
|
|
386
|
+
}
|
|
387
|
+
icons.push(`◎ Camera cluster ${cameraDr(room, actor.name)}-- live, feeding ${owners.length > 0 ? owners.map(o => hostLabel(o)).join(' and ') : `the site's host`}. Loud acts here travel. (own the host to loop them -- or walk in and "tap" a lens in the flesh: slaved to the host means a key to the host)`);
|
|
379
388
|
}
|
|
380
389
|
}
|
|
381
390
|
// THE DIVIDER (eJANPpm7qCaCAJBJw). Only when there is a host to be
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { hostIsHunting, hostLaunchStep } from './ic.js';
|
|
2
2
|
import { personasInside } from './matrix-roster.js';
|
|
3
|
+
import { spidersIn } from './spider.js';
|
|
4
|
+
import { turnHostile } from './hostile-contact.js';
|
|
3
5
|
/**
|
|
4
6
|
* THE HOST OWNS ITS FIGHT (2026-09-13, the Matrix joins the Combat Turn).
|
|
5
7
|
*
|
|
@@ -27,12 +29,28 @@ import { personasInside } from './matrix-roster.js';
|
|
|
27
29
|
* holds the Scene and the encounter.
|
|
28
30
|
*/
|
|
29
31
|
const empty = () => ({ lines: [], meta: [], world: [] });
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Every program the host has running as an actor, seated in the fight --
|
|
34
|
+
* and every SPIDER riding it (utilities/spider.ts). A host that is
|
|
35
|
+
* fighting an intruder has made one, and the deckers sitting in it are
|
|
36
|
+
* its security: they are told, and they turn on it (the owner-guard
|
|
37
|
+
* rule, owner-guard.ts, for the people inside the architecture).
|
|
38
|
+
*/
|
|
39
|
+
function seatRunningIce(enc, host, scene) {
|
|
32
40
|
for (const ice of host.iceActors.values()) {
|
|
33
41
|
if (!enc.has(ice) && !ice.isIncapacitated())
|
|
34
42
|
enc.join(ice);
|
|
35
43
|
}
|
|
44
|
+
if (!scene)
|
|
45
|
+
return;
|
|
46
|
+
for (const spider of spidersIn(scene, host)) {
|
|
47
|
+
if (spider.isIncapacitated() || spider.surrendered)
|
|
48
|
+
continue;
|
|
49
|
+
if (spider.hostile !== true)
|
|
50
|
+
turnHostile(spider, scene.beat ?? 0, `${host.name} is fighting an intruder`);
|
|
51
|
+
if (!enc.has(spider))
|
|
52
|
+
enc.join(spider);
|
|
53
|
+
}
|
|
36
54
|
}
|
|
37
55
|
/** Report one host step to the persona's watchers. */
|
|
38
56
|
async function tell(enc, scene, turn) {
|
|
@@ -55,12 +73,12 @@ export async function hostTurnStart(scene, host, enc) {
|
|
|
55
73
|
if (host.icLaunchDelay > 0) {
|
|
56
74
|
host.icLaunchDelay -= 1;
|
|
57
75
|
out.lines.push(`{light-blue-fg}Something in ${host.name}'s architecture is arguing with itself -- whatever it meant to bring up does not arrive.{/light-blue-fg}`);
|
|
58
|
-
seatRunningIce(enc, host);
|
|
76
|
+
seatRunningIce(enc, host, scene);
|
|
59
77
|
await tell(enc, scene, out);
|
|
60
78
|
return;
|
|
61
79
|
}
|
|
62
80
|
hostLaunchStep(host.over, scene, out);
|
|
63
|
-
seatRunningIce(enc, host);
|
|
81
|
+
seatRunningIce(enc, host, scene);
|
|
64
82
|
await tell(enc, scene, out);
|
|
65
83
|
}
|
|
66
84
|
/** The fight goes on while the host is alert and someone human is still
|
|
@@ -95,7 +113,7 @@ export async function openHostEncounter(scene, host, intruder) {
|
|
|
95
113
|
onNewTurn: e => hostTurnStart(scene, host, e),
|
|
96
114
|
keepAlive: () => hostKeepsFighting(scene, host),
|
|
97
115
|
});
|
|
98
|
-
seatRunningIce(enc, host);
|
|
116
|
+
seatRunningIce(enc, host, scene);
|
|
99
117
|
if (out.lines.length > 0)
|
|
100
118
|
await enc.announce(out.lines, out.meta);
|
|
101
119
|
return enc;
|
|
@@ -120,6 +120,27 @@ export const IC_TYPES = [
|
|
|
120
120
|
{ name: 'Track IC', kind: 'track', attacks: true, hurtByFailure: true, marksNeeded: 2 },
|
|
121
121
|
{ name: 'Scramble IC', kind: 'scramble', attacks: true, hurtByFailure: true, marksNeeded: 3 },
|
|
122
122
|
];
|
|
123
|
+
/**
|
|
124
|
+
* AN IC NAME AS AN AUTHOR WRITES IT -- "Probe", "probe ic", "Black IC" --
|
|
125
|
+
* to the engine's program, or undefined when the engine does not model
|
|
126
|
+
* it (Data Trails' Bloodhound, Tar Baby, Shocker, Crash).
|
|
127
|
+
*/
|
|
128
|
+
export function icTypeNamed(name) {
|
|
129
|
+
const n = name.trim().toLowerCase().replace(/\s+ic$/, '');
|
|
130
|
+
return IC_TYPES.find(t => t.name.toLowerCase().replace(/\s+ic$/, '') === n);
|
|
131
|
+
}
|
|
132
|
+
/** A host's launch order: the author's, resolved, else the default. */
|
|
133
|
+
export function resolveICOrder(order) {
|
|
134
|
+
if (!order || order.length === 0)
|
|
135
|
+
return IC_TYPES;
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const n of order) {
|
|
138
|
+
const t = icTypeNamed(n);
|
|
139
|
+
if (t && !out.includes(t))
|
|
140
|
+
out.push(t);
|
|
141
|
+
}
|
|
142
|
+
return out.length > 0 ? out : IC_TYPES;
|
|
143
|
+
}
|
|
123
144
|
const empty = () => ({ lines: [], meta: [], world: [] });
|
|
124
145
|
/**
|
|
125
146
|
* Does this host act at all this turn?
|
|
@@ -398,7 +419,12 @@ export function hostLaunchStep(room, scene, out) {
|
|
|
398
419
|
crashIC(scene, host, kind);
|
|
399
420
|
out.lines.push(` {red-fg}${kind} shears apart -- crashed. The host is already recompiling it.{/red-fg}`);
|
|
400
421
|
}
|
|
401
|
-
|
|
422
|
+
// THE BRICKED POLICY (Data Trails p.91-102): most hosts reboot a
|
|
423
|
+
// crashed program first; some continue down the line and reboot only
|
|
424
|
+
// once the lineup is spent.
|
|
425
|
+
const continueFirst = host.icBricked === 'continue'
|
|
426
|
+
&& resolveICOrder(host.icOrder).some(t => !room.runningIC.includes(t.name) && !host.relaunchQueue.includes(t.name));
|
|
427
|
+
if (!continueFirst && host.relaunchQueue.length > 0 && room.runningIC.length < room.hostRating) {
|
|
402
428
|
const kind = host.relaunchQueue.shift();
|
|
403
429
|
room.runningIC.push(kind);
|
|
404
430
|
if (scene)
|
|
@@ -409,7 +435,8 @@ export function hostLaunchStep(room, scene, out) {
|
|
|
409
435
|
}
|
|
410
436
|
}
|
|
411
437
|
if (room.runningIC.length < room.hostRating) {
|
|
412
|
-
|
|
438
|
+
// THE HOST'S OWN ORDER when its author wrote one (Data Trails p.91-102).
|
|
439
|
+
const next = resolveICOrder(host?.icOrder).find(t => !room.runningIC.includes(t.name) && !(host?.relaunchQueue.includes(t.name)));
|
|
413
440
|
if (next) {
|
|
414
441
|
room.runningIC.push(next.name);
|
|
415
442
|
if (host && scene)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { hint } from './hints.js';
|
|
2
|
+
import { roomAtCell, isVoid } from './district-plane.js';
|
|
3
|
+
import { cellOnGrid, METERS_PER_LEVEL } from './room-grid.js';
|
|
4
|
+
import { actorCell, spotForCell } from './spots.js';
|
|
5
|
+
import { resolveFall } from './falling.js';
|
|
6
|
+
import { Logger } from './logger.js';
|
|
7
|
+
const STEP = { north: [0, -1], south: [0, 1], west: [-1, 0], east: [1, 0] };
|
|
8
|
+
// Spelled out in full: a drop is never one letter away.
|
|
9
|
+
const WORD = { north: 'north', south: 'south', east: 'east', west: 'west' };
|
|
10
|
+
/** A compass word, spelled out ("leap off north"). */
|
|
11
|
+
export function ledgeDirection(word) {
|
|
12
|
+
return word ? WORD[word.toLowerCase()] : undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* OFF THE EDGE, ON PURPOSE (user rulings 2026-10-06): the ground is
|
|
16
|
+
* walkable everywhere, but an outdoor area on any other storey -- a
|
|
17
|
+
* balcony, a catwalk, a rooftop -- is a ledge: walking stops at its edge,
|
|
18
|
+
* and "leap off <direction>" (commands/leap.ts) goes over it. You come down on whatever outdoor
|
|
19
|
+
* ground lies below that spot and take the landing (SR5 p.172: more than
|
|
20
|
+
* three metres is DV = metres, AP -4, Body + Armor -- utilities/falling.ts).
|
|
21
|
+
* A building's roof is not a place you can land on until someone draws
|
|
22
|
+
* it as one, so a jump that would land there is refused, as is one over
|
|
23
|
+
* nothing at all.
|
|
24
|
+
*/
|
|
25
|
+
export function ledgeJump(scene, actor, dir) {
|
|
26
|
+
const room = actor.currentLocation;
|
|
27
|
+
if (actor.plane !== 'meat')
|
|
28
|
+
return `Your body has to be there to jump.`;
|
|
29
|
+
const plane = room?.scene?.ensurePlane();
|
|
30
|
+
const at = plane?.placements.get(room.name.trim().toLowerCase());
|
|
31
|
+
if (!room || !plane || !at || !room.outdoors)
|
|
32
|
+
return `There is no ledge to jump from in here.`;
|
|
33
|
+
if (at.origin.z === 0)
|
|
34
|
+
return `You are on the ground already -- walk ("go ${dir}").`;
|
|
35
|
+
const cell = actorCell(room, actor);
|
|
36
|
+
if (!cell)
|
|
37
|
+
return `You need to be standing somewhere first.`;
|
|
38
|
+
const step = STEP[dir];
|
|
39
|
+
const next = { x: cell.x + step[0], y: cell.y + step[1], z: 0 };
|
|
40
|
+
const grid = room.ensureGrid();
|
|
41
|
+
if (grid && cellOnGrid(grid, next))
|
|
42
|
+
return `The edge isn't that way -- there is more ${room.name} to walk on ("go ${dir}").`;
|
|
43
|
+
const world = { x: at.origin.x + next.x, y: at.origin.y + next.y, z: at.origin.z };
|
|
44
|
+
if (!isVoid(plane, world))
|
|
45
|
+
return `That is not a drop -- ${roomAtCell(plane, world) ? `walk ("go ${dir}")` : 'there is a wall there'}.`;
|
|
46
|
+
// DOWN TO WHATEVER IS UNDER THAT SPOT.
|
|
47
|
+
let landing;
|
|
48
|
+
let landZ = world.z;
|
|
49
|
+
for (let z = world.z - 1; z >= Math.min(0, ...plane.storeys); z--) {
|
|
50
|
+
if (plane.walls.has(`${world.x},${world.y},${z}`))
|
|
51
|
+
return `There is a wall straight below -- nothing to land on.`;
|
|
52
|
+
const owner = roomAtCell(plane, { x: world.x, y: world.y, z });
|
|
53
|
+
if (!owner)
|
|
54
|
+
continue;
|
|
55
|
+
const r = Object.values(scene.getRooms()).find(x => x.name.trim().toLowerCase() === owner);
|
|
56
|
+
if (!r?.outdoors)
|
|
57
|
+
return `Below that edge is ${r ? `${r.name}'s roof` : 'a roof'} -- nothing anyone has made a place to land on.`;
|
|
58
|
+
landing = r;
|
|
59
|
+
landZ = z;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (!landing)
|
|
63
|
+
return `There is nothing mapped below that edge to land on.`;
|
|
64
|
+
const there = plane.placements.get(landing.name.trim().toLowerCase());
|
|
65
|
+
const local = { x: world.x - there.origin.x, y: world.y - there.origin.y, z: 0 };
|
|
66
|
+
const below = landing.ensureGrid();
|
|
67
|
+
if (!below || !cellOnGrid(below, local))
|
|
68
|
+
return `Something stands right where you would come down -- not there.`;
|
|
69
|
+
const meters = (world.z - landZ) * METERS_PER_LEVEL;
|
|
70
|
+
actor.currentLocation = landing;
|
|
71
|
+
actor.atCell = local;
|
|
72
|
+
actor.atSpot = spotForCell(landing, local);
|
|
73
|
+
actor.visitedRooms.add(landing.name);
|
|
74
|
+
actor.performAction('jumps down', `from ${room.name} to ${landing.name}`);
|
|
75
|
+
const fall = resolveFall(actor, meters);
|
|
76
|
+
if (fall.meta)
|
|
77
|
+
Logger.getInstance().meta(fall.meta);
|
|
78
|
+
scene.updateStatus?.();
|
|
79
|
+
const landingLine = fall.dv <= 0
|
|
80
|
+
? `You land on ${landing.name} -- ${meters} m, and nothing worse than stinging soles.`
|
|
81
|
+
: fall.dealt > 0
|
|
82
|
+
? `You come down hard on ${landing.name} -- ${meters} m -- and the landing costs you ${fall.dealt} box${fall.dealt === 1 ? '' : 'es'}: ${actor.conditionSummary()}`
|
|
83
|
+
: `You come down hard on ${landing.name} -- ${meters} m -- and the armor eats it.`;
|
|
84
|
+
return `You go over the ${dir} edge of ${room.name}. ${landingLine}${hint(' ("look" to see where you are.)')}`;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=ledge-jump.js.map
|
|
@@ -538,7 +538,8 @@ export function collectPlaneBodies(input) {
|
|
|
538
538
|
* it is seeing the light they cast. What DOES stop it is the one
|
|
539
539
|
* thing canon calls opaque astrally -- a ward (SG p.107).
|
|
540
540
|
*/
|
|
541
|
-
|
|
541
|
+
// A surface, not a fog: nothing to cross inside your own room.
|
|
542
|
+
const wards = room === viewer.currentLocation ? 0 : (room.warded ? 1 : 0) + (viewer.currentLocation?.warded ? 1 : 0);
|
|
542
543
|
const auraLit = astral && at !== undefined
|
|
543
544
|
&& castsAura(actor)
|
|
544
545
|
&& auraVisible(distanceMeters(at, cell), wards);
|
|
@@ -34,6 +34,10 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
34
34
|
// weapon to draw, no ground to close -- one test, by type.
|
|
35
35
|
if (npc.icKind !== undefined)
|
|
36
36
|
return runIcActionPhase(enc, npc);
|
|
37
|
+
// A SPIDER FIGHTS AS A PERSONA (utilities/spider.ts): no gun, no
|
|
38
|
+
// ground -- the Data Spike a player types.
|
|
39
|
+
if (npc.plane === 'matrix')
|
|
40
|
+
return runPersonaActionPhase(enc, npc);
|
|
37
41
|
const logger = Logger.getInstance();
|
|
38
42
|
if (npc.isIncapacitated() || npc.surrendered)
|
|
39
43
|
return;
|
|
@@ -298,4 +302,29 @@ async function paceToward(npc, target) {
|
|
|
298
302
|
return;
|
|
299
303
|
}
|
|
300
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* A SPIDER'S ACTION PHASE (utilities/spider.ts): one Data Spike (p.239,
|
|
307
|
+
* a Complex Action) at the intruder it is already fighting, else the
|
|
308
|
+
* first in reach -- through `attack`, the verb a decker types, so the
|
|
309
|
+
* test, the bill and the refusals are a player's.
|
|
310
|
+
*/
|
|
311
|
+
async function runPersonaActionPhase(enc, npc) {
|
|
312
|
+
const logger = Logger.getInstance();
|
|
313
|
+
if (npc.isIncapacitated() || npc.surrendered)
|
|
314
|
+
return;
|
|
315
|
+
const enemies = enc.enemiesOf(npc);
|
|
316
|
+
if (enemies.length === 0)
|
|
317
|
+
return;
|
|
318
|
+
const target = npc.combatOpponent && enemies.includes(npc.combatOpponent) ? npc.combatOpponent : enemies[0];
|
|
319
|
+
npc.combatOpponent = target;
|
|
320
|
+
npc.lastExchangeAt = Date.now();
|
|
321
|
+
if (!enc.budgetOf(npc)?.complexAvailable)
|
|
322
|
+
return;
|
|
323
|
+
const r = await npc.actInCombat(`attack ${target.name}`);
|
|
324
|
+
logger.write(`Brain: ${npc.name} (persona) attack ${target.name} -> ${r.split('\n')[0]}`);
|
|
325
|
+
// The arena's own announce: a host's watchers are personas whose
|
|
326
|
+
// bodies are anywhere, which a room-scoped line would miss.
|
|
327
|
+
if (!enc.budgetOf(npc)?.attackTaken)
|
|
328
|
+
await enc.announce([`${npc.name}'s icon turns on ${target.name} and can't find the angle.`], []);
|
|
329
|
+
}
|
|
301
330
|
//# sourceMappingURL=npc-combat-brain.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NPC } from '../models/npc.js';
|
|
2
|
+
import { wouldNotice } from './spots.js';
|
|
3
|
+
import { turnHostile } from './hostile-contact.js';
|
|
4
|
+
/**
|
|
5
|
+
* A ROOM'S OWNER GUARDS WHAT IS THEIRS (user ruling 2026-10-06: "a room
|
|
6
|
+
* owner also guards all its security devices, and hosts. That is, hostile
|
|
7
|
+
* actions that the guarder notices caused them to react. This can include
|
|
8
|
+
* marks made on the attacker").
|
|
9
|
+
*
|
|
10
|
+
* Two ways the owner comes to know, and they are different facts:
|
|
11
|
+
*
|
|
12
|
+
* - INFORMED, through the Matrix. SR5 p.231 / p.236: on a FAILED SLEAZE
|
|
13
|
+
* action "your target immediately gets one free mark on you (or its
|
|
14
|
+
* owner does, if your target is a device) ... A device immediately
|
|
15
|
+
* informs its owner"; a slaved device's mark goes to the device's
|
|
16
|
+
* owner, not to its master (p.233). A SUCCESSFUL ATTACK action makes
|
|
17
|
+
* the target aware it is under attack, and it "will almost always alert
|
|
18
|
+
* its owner" (p.236). The owner need not be anywhere near.
|
|
19
|
+
* - WITNESSED, in the meat: picking, breaching or smashing their lock or
|
|
20
|
+
* their camera in front of them -- the same notice every act already
|
|
21
|
+
* gets (spots.ts wouldNotice).
|
|
22
|
+
*
|
|
23
|
+
* What the owner DOES on knowing is not in canon ("depending on the
|
|
24
|
+
* owner's preferences and the gamemaster's judgment", p.236) -- that is
|
|
25
|
+
* the user's ruling: they turn hostile (hostile-contact.ts turnHostile,
|
|
26
|
+
* the one writer of provoked hostility; the ladder opens the fight).
|
|
27
|
+
*/
|
|
28
|
+
/** The NPC who owns `room` (room seed `owner`), if they are about. */
|
|
29
|
+
export function ownerOf(scene, room) {
|
|
30
|
+
const name = room?.owner;
|
|
31
|
+
if (!scene || !name)
|
|
32
|
+
return undefined;
|
|
33
|
+
const actor = scene.getActor?.(name);
|
|
34
|
+
return actor instanceof NPC && !actor.isIncapacitated() ? actor : undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* THE OWNER IS INFORMED (Matrix). `device-sleaze`: the owner also takes the
|
|
38
|
+
* free mark on the hacker (p.231/p.236), held the way an NPC holds marks
|
|
39
|
+
* on a persona today (Player.panMarksBy, capped at 3 -- p.236). Either
|
|
40
|
+
* way they turn hostile. Returns the line the hacker should read, or
|
|
41
|
+
* undefined when the place has no owner.
|
|
42
|
+
*/
|
|
43
|
+
export function ownerInformed(scene, room, actor, how, what) {
|
|
44
|
+
const owner = ownerOf(scene, room);
|
|
45
|
+
if (!owner || owner === actor || owner.allyOf === actor.name)
|
|
46
|
+
return undefined;
|
|
47
|
+
// The free mark goes to the OWNER only when the target was a DEVICE
|
|
48
|
+
// (p.236 "or its owner does, if your target is a device"); a host keeps
|
|
49
|
+
// its own mark on you (hack.ts), and its owner is told.
|
|
50
|
+
if (how === 'device-sleaze') {
|
|
51
|
+
const held = actor.panMarksBy.get(owner.name) ?? 0;
|
|
52
|
+
if (held < 3)
|
|
53
|
+
actor.panMarksBy.set(owner.name, held + 1);
|
|
54
|
+
}
|
|
55
|
+
turnHostile(owner, scene?.beat ?? 0, how !== 'attack-landed'
|
|
56
|
+
? `${what} told its owner about ${actor.name}'s failed intrusion`
|
|
57
|
+
: `${what} told its owner it was under attack by ${actor.name}`);
|
|
58
|
+
return how === 'device-sleaze'
|
|
59
|
+
? `${what} tells its owner, ${owner.name} -- who now holds a mark on you, and knows your icon.`
|
|
60
|
+
: how === 'host-sleaze'
|
|
61
|
+
? `${what} tells its owner, ${owner.name}, that someone is in.`
|
|
62
|
+
: `${what} raises the alarm with its owner, ${owner.name}.`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* THE OWNER WITNESSES (meat): in the room, conscious, and noticing the
|
|
66
|
+
* actor -- then they turn hostile. Returns the line, or undefined.
|
|
67
|
+
*/
|
|
68
|
+
export function ownerWitnesses(scene, room, actor, what) {
|
|
69
|
+
const owner = ownerOf(scene, room);
|
|
70
|
+
if (!owner || owner.allyOf === actor.name)
|
|
71
|
+
return undefined;
|
|
72
|
+
if (owner.currentLocation !== room || !owner.canPerceive(actor) || !wouldNotice(owner, actor))
|
|
73
|
+
return undefined;
|
|
74
|
+
turnHostile(owner, scene?.beat ?? 0, `watched ${actor.name} go at ${what}`);
|
|
75
|
+
return `${owner.name} sees you go at ${what} -- theirs -- and turns on you.`;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=owner-guard.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Door } from '../models/door.js';
|
|
2
2
|
import { Direction } from '../types/shared/direction-enum.js';
|
|
3
|
-
import { key, levelFootprint, isCellBlocked, METERS_PER_CELL } from './room-grid.js';
|
|
3
|
+
import { key, levelFootprint, isCellBlocked, cellOnGrid, METERS_PER_CELL } from './room-grid.js';
|
|
4
4
|
import { actorCell, furnitureCells, standingOccupants, openGroundBeyond, approachCellFor } from './spots.js';
|
|
5
5
|
import { forgetLastStep } from './movement-cost.js';
|
|
6
6
|
import { worldNow } from './world-clock.js';
|
|
@@ -130,7 +130,9 @@ function walkable(room, actor, z) {
|
|
|
130
130
|
if (furniture.has(k))
|
|
131
131
|
return false;
|
|
132
132
|
const inBounds = c.x >= 0 && c.y >= 0 && c.x < grid.dims.x && c.y < grid.dims.y;
|
|
133
|
-
|
|
133
|
+
// A hole (another room's cell) is decided by the plane, like past
|
|
134
|
+
// the edge -- spots.ts stepFrom, the same rule.
|
|
135
|
+
if (inBounds && (z !== 0 || cellOnGrid(grid, c)))
|
|
134
136
|
return floor.has(k);
|
|
135
137
|
return outside(c);
|
|
136
138
|
};
|
|
@@ -274,7 +274,9 @@ export function leaveMatrix(scene, actor, opts) {
|
|
|
274
274
|
// deck stays live and the agent keeps its hunt; an idle agent (or any
|
|
275
275
|
// forced dump) is a clean jack-out reboot that folds it.
|
|
276
276
|
const game = scene.ownerGame;
|
|
277
|
-
|
|
277
|
+
// The table's agent rides a RUNNER's deck: a spider dumped out of its
|
|
278
|
+
// host (utilities/spider.ts) reboots its own deck, not theirs.
|
|
279
|
+
const agentEntry = scene.isHumanControlled?.(actor) === false ? undefined : game?.companions.find(c => c.kind === 'agent');
|
|
278
280
|
if (agentEntry) {
|
|
279
281
|
if (!opts?.forced && agentEntry.npc.objective) {
|
|
280
282
|
lines.push(`You drop to AR instead of a hard jack-out -- the deck stays live in your bag, your agent stays on the grid working its directive, and its status ghosts across the same AR overlay your link feeds you.`);
|