@maka/maka-cli 5.391.0 → 5.392.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/assense.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +60 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +5 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +16 -150
- package/bundle/typescript/src/commands/game/sideQuest/commands/crack.js +76 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +42 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/disarm.js +64 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +23 -46
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +23 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +15 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +55 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +15 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/leap.js +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +40 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +16 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +13 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/authored-run.js +632 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/catalog-item-seed.js +78 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/job-order.js +7 -44
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +8 -10
- package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +30 -52
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +112 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +123 -3
- package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js +52 -4
- package/bundle/typescript/src/commands/game/sideQuest/game.js +17 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/camera.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js +14 -21
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +62 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +141 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +18 -11
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/astral-move.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/district-plane.js +109 -27
- package/bundle/typescript/src/commands/game/sideQuest/utilities/door-watch.js +70 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/falling.js +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/file-guard.js +120 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +23 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +29 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ledge-jump.js +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/owner-guard.js +77 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/path-run.js +4 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +49 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spider.js +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +14 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +193 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/use-target.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/wards.js +116 -0
- package/package.json +1 -1
|
@@ -11,12 +11,26 @@ import * as fuzzyMatch from '../utilities/fuzzy-match.js';
|
|
|
11
11
|
import { sameHostSide } from './player.js';
|
|
12
12
|
import { AI } from '../../../../tools/ai/ai.class.js';
|
|
13
13
|
import { AbstractRoom } from '../types/shared/abstracts.js';
|
|
14
|
-
import { doorstepsOnto } from '../utilities/district-plane.js';
|
|
14
|
+
import { doorstepsOnto, holesOf } from '../utilities/district-plane.js';
|
|
15
15
|
import { dwellingDims } from '../utilities/dwellings.js';
|
|
16
16
|
import { spotsActive, isPerchSpot, actorDistanceMeters, voiceBandFor, voiceTag, doorwayKindOf, exitDoorwayKind } from '../utilities/spots.js';
|
|
17
17
|
import { canHearSpeech, canHearShout } from '../utilities/earshot.js';
|
|
18
18
|
import { freshCrowd, clampHeadcount, deriveOccupancy } from '../utilities/crowd.js';
|
|
19
19
|
import { synthesizeGrid, attachExitCell, detachExitCell, hashFnv1a, mulberry32, setCellBlocker, } from '../utilities/room-grid.js';
|
|
20
|
+
/** An authored spot's cells, anchor first: `size` cells east and south
|
|
21
|
+
* of `cell`, on the spot's level. */
|
|
22
|
+
export function authoredSpotCells(s) {
|
|
23
|
+
if (!s.cell)
|
|
24
|
+
return undefined;
|
|
25
|
+
const w = Math.max(1, Math.round(s.size?.w ?? 1));
|
|
26
|
+
const h = Math.max(1, Math.round(s.size?.h ?? 1));
|
|
27
|
+
const z = s.level ?? 0;
|
|
28
|
+
const cells = [];
|
|
29
|
+
for (let dy = 0; dy < h; dy++)
|
|
30
|
+
for (let dx = 0; dx < w; dx++)
|
|
31
|
+
cells.push({ x: s.cell.x + dx, y: s.cell.y + dy, z });
|
|
32
|
+
return cells;
|
|
33
|
+
}
|
|
20
34
|
/**
|
|
21
35
|
* A CAMERA'S DEVICE RATING when the scene does not say (p.356, which
|
|
22
36
|
* lists "Camera" at 1 -- the same table that gives the door lock the 2
|
|
@@ -24,6 +38,13 @@ import { synthesizeGrid, attachExitCell, detachExitCell, hashFnv1a, mulberry32,
|
|
|
24
38
|
* ordinary security devices at 2 and mil-spec at 4.
|
|
25
39
|
*/
|
|
26
40
|
export const DEFAULT_CAMERA_RATING = 1;
|
|
41
|
+
/**
|
|
42
|
+
* A WARD'S FORCE when a seed sets `warded` without one. Canon prices a
|
|
43
|
+
* ward by its Force and caps it nowhere in the ritual rules (p.296-298),
|
|
44
|
+
* so this is a house default for legacy seeds only -- the generator and
|
|
45
|
+
* the editor always write a Force (user ruling 2026-10-06).
|
|
46
|
+
*/
|
|
47
|
+
export const WARD_DEFAULT_FORCE = 4;
|
|
27
48
|
export class Room extends AbstractRoom {
|
|
28
49
|
logger = Logger.getInstance();
|
|
29
50
|
actors = new Set();
|
|
@@ -38,6 +59,10 @@ export class Room extends AbstractRoom {
|
|
|
38
59
|
* utilities/outdoors.ts (the taxi curb and the generator's
|
|
39
60
|
* start-room guarantee both go through it). */
|
|
40
61
|
outdoors;
|
|
62
|
+
/** The run's ground: the outdoor area under every other (seed `ground`). */
|
|
63
|
+
ground;
|
|
64
|
+
/** Storeys this room spans, from `storey` up (seed `storeys`); 1 unless tall. */
|
|
65
|
+
storeysSpanned = 1;
|
|
41
66
|
isStartRoom;
|
|
42
67
|
inventory = new Inventory();
|
|
43
68
|
supportedLifestyles;
|
|
@@ -100,6 +125,12 @@ export class Room extends AbstractRoom {
|
|
|
100
125
|
* to force either way per site.
|
|
101
126
|
*/
|
|
102
127
|
camerasWireless;
|
|
128
|
+
/** THE LENSES AN AUTHOR HUNG (IRoomSeedJson.cameras): when set,
|
|
129
|
+
* ensureCameras builds exactly these instead of deriving them. */
|
|
130
|
+
authoredCameras;
|
|
131
|
+
/** The room whose host these cameras are slaved to (surveillance.ts
|
|
132
|
+
* cameraMasterHost). Unset = this room's own host, else the site's. */
|
|
133
|
+
camerasSlavedTo;
|
|
103
134
|
/** Has this viewer pinned the cluster's icon? Inside the host these
|
|
104
135
|
* lenses are slaved to, there is nothing to pin: p.233 makes a
|
|
105
136
|
* persona in the WAN directly connected to everything on it, which is
|
|
@@ -292,6 +323,9 @@ export class Room extends AbstractRoom {
|
|
|
292
323
|
* authored plan, and by ensureGrid below for the room own extent. */
|
|
293
324
|
reachable;
|
|
294
325
|
footprint;
|
|
326
|
+
/** DOORS AND STAIRS WHERE THE AUTHOR PUT THEM (exit seed `cell`),
|
|
327
|
+
* room-local, by the direction this room declared them. */
|
|
328
|
+
authoredDoorCells;
|
|
295
329
|
storey;
|
|
296
330
|
planeOverride;
|
|
297
331
|
/**
|
|
@@ -468,6 +502,44 @@ export class Room extends AbstractRoom {
|
|
|
468
502
|
}
|
|
469
503
|
return bound;
|
|
470
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* WHERE A STAIR STANDS IN AN AUTHORED ROOM (user 2026-10-06: "like doors
|
|
507
|
+
* n/s/e/w, doors u/d need to match their counterpart"). The two ends of a
|
|
508
|
+
* stair are over each other: this room's own word if it gave one, else
|
|
509
|
+
* the other end's spot carried across the storeys, else the middle of
|
|
510
|
+
* where the two floors overlap -- the same answer from both ends, so up
|
|
511
|
+
* here lands on down there. Room-local, kept inside the room.
|
|
512
|
+
*/
|
|
513
|
+
stairCell(dir) {
|
|
514
|
+
const own = this.authoredDoorCells?.[dir];
|
|
515
|
+
if (own)
|
|
516
|
+
return own;
|
|
517
|
+
const fp = this.footprint;
|
|
518
|
+
const door = this.exits.get(dir);
|
|
519
|
+
const other = door instanceof Door ? door.getOtherSide(this) : undefined;
|
|
520
|
+
const ofp = other?.footprint;
|
|
521
|
+
if (!fp || !ofp)
|
|
522
|
+
return undefined;
|
|
523
|
+
const back = dir === Direction.UP ? Direction.DOWN : Direction.UP;
|
|
524
|
+
const theirs = other.authoredDoorCells?.[back];
|
|
525
|
+
let wx;
|
|
526
|
+
let wy;
|
|
527
|
+
if (theirs) {
|
|
528
|
+
wx = ofp.x + theirs.x;
|
|
529
|
+
wy = ofp.y + theirs.y;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
const x0 = Math.max(fp.x, ofp.x);
|
|
533
|
+
const x1 = Math.min(fp.x + fp.w, ofp.x + ofp.w);
|
|
534
|
+
const y0 = Math.max(fp.y, ofp.y);
|
|
535
|
+
const y1 = Math.min(fp.y + fp.h, ofp.y + ofp.h);
|
|
536
|
+
if (x1 <= x0 || y1 <= y0)
|
|
537
|
+
return undefined;
|
|
538
|
+
wx = Math.floor((x0 + x1 - 1) / 2);
|
|
539
|
+
wy = Math.floor((y0 + y1 - 1) / 2);
|
|
540
|
+
}
|
|
541
|
+
return { x: Math.max(0, Math.min(fp.w - 1, wx - fp.x)), y: Math.max(0, Math.min(fp.h - 1, wy - fp.y)) };
|
|
542
|
+
}
|
|
471
543
|
ensureGrid() {
|
|
472
544
|
// EVERY ROOM IS MATRIXED (player ruling 2026-08-25: "all rooms
|
|
473
545
|
// should be a matrixed grid"). This used to return undefined for a
|
|
@@ -496,8 +568,14 @@ export class Room extends AbstractRoom {
|
|
|
496
568
|
// A PACKED room that is one room of a building takes its doors
|
|
497
569
|
// from the building layout the same way (Scene.partyDoorsFor), so
|
|
498
570
|
// the gap in a shared wall is where both rooms stand their exit.
|
|
571
|
+
// ...and its STAIRS where the author put them (exit seed `cell`):
|
|
572
|
+
// up and down have no wall, so the room's own word is the cell.
|
|
499
573
|
doorCells: this.footprint
|
|
500
|
-
?
|
|
574
|
+
? {
|
|
575
|
+
...(this.scene?.ensurePlane().placements.get(this.name.trim().toLowerCase())?.doors ?? {}),
|
|
576
|
+
...(this.stairCell(Direction.UP) ? { [Direction.UP]: this.stairCell(Direction.UP) } : {}),
|
|
577
|
+
...(this.stairCell(Direction.DOWN) ? { [Direction.DOWN]: this.stairCell(Direction.DOWN) } : {}),
|
|
578
|
+
}
|
|
501
579
|
: this.outdoors || this.roomType === 'host' ? undefined : this.scene?.partyDoorsFor?.(this.name),
|
|
502
580
|
// NOTHING STANDS ON A DOORSTEP. A street holds no exit toward the
|
|
503
581
|
// buildings that front it -- their doors are places, not
|
|
@@ -506,6 +584,12 @@ export class Room extends AbstractRoom {
|
|
|
506
584
|
keepClear: this.outdoors && this.scene
|
|
507
585
|
? doorstepsOnto(this.scene.ensurePlane(), this.name)
|
|
508
586
|
: undefined,
|
|
587
|
+
// ROOMS ALWAYS WIN (user 2026-10-06): an authored outdoor area loses
|
|
588
|
+
// the cells a building -- or, for the ground, a street -- drawn over
|
|
589
|
+
// it owns (district-plane.ts holesOf). A packed street has none.
|
|
590
|
+
holes: this.outdoors && this.footprint && this.scene
|
|
591
|
+
? holesOf(this.scene.ensurePlane(), this.name)
|
|
592
|
+
: undefined,
|
|
509
593
|
// AN OUTDOOR ROOM HAS NO EXTENT OF ITS OWN. It is a region of the
|
|
510
594
|
// district plane, so it takes the cells the packer allotted it
|
|
511
595
|
// and its size class is only what decided how wide the street was
|
|
@@ -540,6 +624,9 @@ export class Room extends AbstractRoom {
|
|
|
540
624
|
name: s.name, place: s.place, level: s.level, tags: s.tags,
|
|
541
625
|
preferPerimeter: doorwayKindOf(s.name) === 'lateral',
|
|
542
626
|
perch: isPerchSpot(s) || undefined,
|
|
627
|
+
// A perch's authored cell is the FLOOR cell under the lens (z 0);
|
|
628
|
+
// placePerches hangs it a storey up from there.
|
|
629
|
+
cells: isPerchSpot(s) && s.cell ? [{ x: s.cell.x, y: s.cell.y, z: 0 }] : authoredSpotCells(s),
|
|
543
630
|
})),
|
|
544
631
|
// DECLARED bindings only (seed `exits[].atSpot`), plus the
|
|
545
632
|
// name-matched doorways, resolved below. The resolved exitSpot()
|
|
@@ -738,6 +825,13 @@ export class Room extends AbstractRoom {
|
|
|
738
825
|
* saved.
|
|
739
826
|
*/
|
|
740
827
|
warded = false;
|
|
828
|
+
/** The ward's Force (utilities/wards.ts); meaningful only while warded. */
|
|
829
|
+
wardForce = 0;
|
|
830
|
+
/** Who raised the ward -- passes it freely (p.315). */
|
|
831
|
+
wardCreator;
|
|
832
|
+
/** Structure boxes an astral attack has taken off the ward this
|
|
833
|
+
* Combat Turn; refilled at the end of every turn (p.316). */
|
|
834
|
+
wardDamage = 0;
|
|
741
835
|
// See IRoomConstructorConfig.watched (utilities/surveillance.ts).
|
|
742
836
|
watched = false;
|
|
743
837
|
/**
|
|
@@ -756,6 +850,23 @@ export class Room extends AbstractRoom {
|
|
|
756
850
|
ensureCameras() {
|
|
757
851
|
if (this._cameras.length > 0)
|
|
758
852
|
return this._cameras;
|
|
853
|
+
// EXACTLY THE LENSES THE AUTHOR HUNG, where they hung them.
|
|
854
|
+
if (this.authoredCameras && this.roomType !== 'host') {
|
|
855
|
+
const taken = new Set((this.spots ?? []).map(s => s.name.toLowerCase()));
|
|
856
|
+
for (const cam of this.authoredCameras) {
|
|
857
|
+
let name = (cam.name?.trim() || `${cam.wall} camera`).toLowerCase();
|
|
858
|
+
for (let i = 2; taken.has(name); i++)
|
|
859
|
+
name = `${cam.wall} camera ${i}`;
|
|
860
|
+
taken.add(name);
|
|
861
|
+
this._cameras.push(new Camera({ name, wall: cam.wall, rating: cam.rating ?? this.cameraRating, ...(cam.slavedTo ? { masterHost: cam.slavedTo } : {}), ...(cam.mountedOn ? { mountedOn: cam.mountedOn } : {}) }));
|
|
862
|
+
this.spots = [...(this.spots ?? []), {
|
|
863
|
+
name, place: cam.wall, level: 1, tags: ['perch'],
|
|
864
|
+
...(cam.cell ? { cell: { x: cam.cell.x, y: cam.cell.y } } : {}),
|
|
865
|
+
}];
|
|
866
|
+
}
|
|
867
|
+
this._grid = undefined;
|
|
868
|
+
return this._cameras;
|
|
869
|
+
}
|
|
759
870
|
// A host's own sculpted space is the Matrix, not a floor with walls.
|
|
760
871
|
if (!(this.watched || this.hasNode) || this.roomType === 'host')
|
|
761
872
|
return this._cameras;
|
|
@@ -942,6 +1053,8 @@ export class Room extends AbstractRoom {
|
|
|
942
1053
|
this.description = config.description;
|
|
943
1054
|
this.roomType = config.roomType;
|
|
944
1055
|
this.outdoors = config.outdoors;
|
|
1056
|
+
this.ground = config.ground === true ? true : undefined;
|
|
1057
|
+
this.storeysSpanned = Math.max(1, Math.min(4, Math.floor(config.storeys ?? 1)));
|
|
945
1058
|
this.hasNode = config.hasNode ?? false;
|
|
946
1059
|
// THE MIGRATION, AT THE ONE DOOR EVERY ROOM COMES THROUGH.
|
|
947
1060
|
//
|
|
@@ -966,6 +1079,19 @@ export class Room extends AbstractRoom {
|
|
|
966
1079
|
if (this.host) {
|
|
967
1080
|
// Blank from a seed reads as "none declared", never as an empty line.
|
|
968
1081
|
this.host.purpose = Host.phrase(config.hostPurpose);
|
|
1082
|
+
// A NAMED HOST: its own label, and its Matrix-side room wears it.
|
|
1083
|
+
// THE LINEUP, as the author ordered it (ic.ts resolveICOrder drops
|
|
1084
|
+
// names the engine does not model; the authored audit faults them).
|
|
1085
|
+
if (config.icOrder && config.icOrder.length > 0)
|
|
1086
|
+
this.host.icOrder = [...config.icOrder];
|
|
1087
|
+
if (config.icBricked)
|
|
1088
|
+
this.host.icBricked = config.icBricked;
|
|
1089
|
+
const named = Host.phrase(config.hostName);
|
|
1090
|
+
if (named) {
|
|
1091
|
+
this.host.displayName = named;
|
|
1092
|
+
if (this.host.room && this.host.room !== this)
|
|
1093
|
+
this.host.room.name = named;
|
|
1094
|
+
}
|
|
969
1095
|
this.host.sculpt = Host.phrase(config.hostSculpt);
|
|
970
1096
|
// A seeded quality implies the invitation; a bare `hostInvite`
|
|
971
1097
|
// is open to everyone who can see the host.
|
|
@@ -974,7 +1100,15 @@ export class Room extends AbstractRoom {
|
|
|
974
1100
|
}
|
|
975
1101
|
}
|
|
976
1102
|
this.warded = config.warded ?? false;
|
|
1103
|
+
this.wardForce = this.warded ? Math.max(1, Math.floor(config.wardForce ?? WARD_DEFAULT_FORCE)) : 0;
|
|
1104
|
+
this.wardCreator = this.warded ? config.wardCreator || undefined : undefined;
|
|
977
1105
|
this.watched = config.watched ?? false;
|
|
1106
|
+
this.authoredCameras = config.cameras && config.cameras.length > 0 ? config.cameras : undefined;
|
|
1107
|
+
if (this.authoredCameras)
|
|
1108
|
+
this.watched = true;
|
|
1109
|
+
if (config.camerasWireless !== undefined)
|
|
1110
|
+
this.camerasWireless = config.camerasWireless;
|
|
1111
|
+
this.camerasSlavedTo = config.camerasSlavedTo;
|
|
978
1112
|
this.exits = config.exits || new Map();
|
|
979
1113
|
this.inventory = config.inventory || new Inventory();
|
|
980
1114
|
this.isStartRoom = config.isStartRoom || false;
|
|
@@ -1292,7 +1426,11 @@ Keep it concise and dramatic.`;
|
|
|
1292
1426
|
* runtime-attached hideout, keep the symmetric wiring they have
|
|
1293
1427
|
* always had.
|
|
1294
1428
|
*/
|
|
1295
|
-
|
|
1429
|
+
// A SIDE DOOR only: a stair up onto a roof or down into a yard is one
|
|
1430
|
+
// flight with two ends, filed in both rooms like any other (user
|
|
1431
|
+
// 2026-10-06: "doors u/d need to match their counterpart").
|
|
1432
|
+
const lateral = direction !== Direction.UP && direction !== Direction.DOWN;
|
|
1433
|
+
const streetDoor = lateral && this.footprint !== undefined && isOutdoorsPlace(targetRoom) && !isOutdoorsPlace(this);
|
|
1296
1434
|
if (streetDoor) {
|
|
1297
1435
|
// No dedupe either: a building may open onto the same street
|
|
1298
1436
|
// twice (a front door and a side door), and each is its own Door.
|
|
@@ -1942,12 +1942,15 @@ export class Scene extends AbstractScene {
|
|
|
1942
1942
|
name: r.name,
|
|
1943
1943
|
size: r.size,
|
|
1944
1944
|
outdoors: r.outdoors,
|
|
1945
|
+
ground: r.ground,
|
|
1946
|
+
storeys: r.storeysSpanned > 1 ? r.storeysSpanned : undefined,
|
|
1945
1947
|
exits: [...r.exits.entries()].map(([direction, exit]) => ({
|
|
1946
1948
|
direction,
|
|
1947
1949
|
targetRoom: exit instanceof Door ? exit.getOtherSide(r).name : String(exit),
|
|
1948
1950
|
})),
|
|
1949
1951
|
footprint: r.footprint,
|
|
1950
1952
|
storey: r.storey,
|
|
1953
|
+
doorAt: r.footprint ? r.authoredDoorCells : undefined,
|
|
1951
1954
|
// A DWELLING BRINGS ITS OWN FLOOR (utilities/dwellings.ts), so the
|
|
1952
1955
|
// square the plane parks is the square the room grid builds.
|
|
1953
1956
|
dims: r.dwelling !== undefined ? dwellingDims(r.dwelling) : undefined,
|
|
@@ -13,13 +13,6 @@ export const AFFORDANCE_FLAVOR = {
|
|
|
13
13
|
failureMessage: `Your intrusion trips a countermeasure -- the system logs the attempt and pings whoever's watching.`,
|
|
14
14
|
hint: `This looks hackable -- try "hack" to skip finding the code (Logic + hacking). Fail, and the system pings whoever's watching.`,
|
|
15
15
|
},
|
|
16
|
-
ward: {
|
|
17
|
-
verb: 'cast',
|
|
18
|
-
verbs: ['cast', 'dispel'],
|
|
19
|
-
resists: `Your hands find nothing to work and your tools find nothing to bite -- the barrier isn't matter. It answers to the Art: "cast" at it, or "dispel" it.`,
|
|
20
|
-
failureMessage: `The weave slips through your fingers -- backlash crackles outward, and something out there felt it.`,
|
|
21
|
-
hint: `This could be unwoven magically -- try "dispel" (Magic + counterspelling) or "cast" (the casting pool). Fail, and the backlash BITES.`,
|
|
22
|
-
},
|
|
23
16
|
person: {
|
|
24
17
|
verb: 'bluff',
|
|
25
18
|
verbs: ['bluff', 'persuade', 'subdue'],
|
|
@@ -35,7 +28,9 @@ export function verbsFor(kind) {
|
|
|
35
28
|
}
|
|
36
29
|
/** WARD-SOUNDING NAMES, checked first: a magical barrier is rare and
|
|
37
30
|
* specific enough here that missing one costs less than a false
|
|
38
|
-
* positive stealing a genuine lock from `lock`.
|
|
31
|
+
* positive stealing a genuine lock from `lock`. A ward is not a device
|
|
32
|
+
* any more -- it is a room's astral barrier (utilities/wards.ts) -- so a
|
|
33
|
+
* device that reads as one is moved onto its room (isWardDevice). */
|
|
39
34
|
const WARD_HINTS = ['ward', 'seal', 'rune', 'glyph', 'weave', 'astral', 'spell', 'enchant', 'mystic'];
|
|
40
35
|
/** PHYSICAL-MECHANISM NAMES, checked second -- the actual fix
|
|
41
36
|
* (backlog 2uNPnc4L6jHaYEtnN / tuku6Z5XTtmNemkzd, 2026-09-08). A
|
|
@@ -64,13 +59,25 @@ const LOCK_HINTS = [
|
|
|
64
59
|
* description first.
|
|
65
60
|
*/
|
|
66
61
|
export function clampDeviceKind(rawKind, name, description) {
|
|
67
|
-
if (rawKind === 'lock' || rawKind === 'panel'
|
|
62
|
+
if (rawKind === 'lock' || rawKind === 'panel')
|
|
68
63
|
return rawKind;
|
|
69
64
|
const text = `${name} ${description ?? ''}`.toLowerCase();
|
|
70
|
-
if (WARD_HINTS.some(hint => text.includes(hint)))
|
|
71
|
-
return 'ward';
|
|
72
65
|
if (LOCK_HINTS.some(hint => text.includes(hint)))
|
|
73
66
|
return 'lock';
|
|
74
67
|
return 'panel';
|
|
75
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* IS THIS "DEVICE" REALLY A WARD (user 2026-10-06)? The retired kind, or
|
|
71
|
+
* an unknown kind whose name or description reads as one. Callers move
|
|
72
|
+
* it onto its room as an astral barrier (seed-migration.ts
|
|
73
|
+
* wardDevicesToRooms) instead of clamping it into a lock or a panel.
|
|
74
|
+
*/
|
|
75
|
+
export function isWardDevice(rawKind, name, description) {
|
|
76
|
+
if (rawKind === 'ward')
|
|
77
|
+
return true;
|
|
78
|
+
if (rawKind === 'lock' || rawKind === 'panel')
|
|
79
|
+
return false;
|
|
80
|
+
const text = `${name} ${description ?? ''}`.toLowerCase();
|
|
81
|
+
return WARD_HINTS.some(hint => text.includes(hint));
|
|
82
|
+
}
|
|
76
83
|
//# sourceMappingURL=affordances.js.map
|
|
@@ -62,6 +62,9 @@ export function alarmedStaff(scene, player) {
|
|
|
62
62
|
// Ice is not staff: a program answers to the host's dice
|
|
63
63
|
// (utilities/ic.ts), never to an alarm beat (NPC.isConstruct).
|
|
64
64
|
.filter(npc => !npc.isConstruct())
|
|
65
|
+
// Nor a spider's slumped body: the person is in the host
|
|
66
|
+
// (utilities/spider.ts), and the host's fight is how they answer.
|
|
67
|
+
.filter(npc => npc.plane === 'meat')
|
|
65
68
|
.filter(npc => !npc.isIncapacitated());
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
@@ -589,8 +589,10 @@ export function astralTags(scene, actor, room) {
|
|
|
589
589
|
// A ward is opaque astrally (SG p.107) -- the one thing that
|
|
590
590
|
// stops an aura. Cast over a PLACE, so it is the rooms that
|
|
591
591
|
// carry it, never the cells between.
|
|
592
|
-
|
|
593
|
-
|
|
592
|
+
// A ward is a SURFACE: it hides what is on its far side, never
|
|
593
|
+
// what shares the room with you (a body in your own warded room
|
|
594
|
+
// was going dark because its room was warded).
|
|
595
|
+
const wards = other === room ? 0 : (other.warded ? 1 : 0) + (room.warded ? 1 : 0);
|
|
594
596
|
if (!auraVisible(d, wards))
|
|
595
597
|
continue;
|
|
596
598
|
if (!castsAura(body))
|
|
@@ -4,6 +4,7 @@ import { isOutdoorsPlace } from './outdoors.js';
|
|
|
4
4
|
import { astralStepFrom, spotForCell } from './spots.js';
|
|
5
5
|
import { isOpenGround, isOpenAir } from './district-plane.js';
|
|
6
6
|
import { hint } from './hints.js';
|
|
7
|
+
import { spendWardPass, wardHolds } from './wards.js';
|
|
7
8
|
import { METERS_PER_CELL } from './room-grid.js';
|
|
8
9
|
/**
|
|
9
10
|
* THE OUTDOOR ROOM A PATCH OF UNOWNED GROUND BELONGS TO.
|
|
@@ -75,7 +76,7 @@ export function driftAstral(scene, actor, rooms, direction, count, untilOut = fa
|
|
|
75
76
|
// the room it is trying to leave is large.
|
|
76
77
|
const limit = untilOut ? 50 : count;
|
|
77
78
|
while (taken < limit) {
|
|
78
|
-
const step = astralStepFrom(plane, cell, direction, 1, byKey, c => scene.barrierAt?.(c, 'mana', actor.name)?.barrier.caster);
|
|
79
|
+
const step = astralStepFrom(plane, cell, direction, 1, byKey, c => scene.barrierAt?.(c, 'mana', actor.name)?.barrier.caster, r => wardHolds(r, actor));
|
|
79
80
|
if (step.taken === 0) {
|
|
80
81
|
blockedBy = step.blockedBy;
|
|
81
82
|
ward = step.ward;
|
|
@@ -178,6 +179,10 @@ export function driftAstral(scene, actor, rooms, direction, count, untilOut = fa
|
|
|
178
179
|
}
|
|
179
180
|
scene.updateStatus?.();
|
|
180
181
|
scene.updateExits?.(actor.currentLocation);
|
|
182
|
+
// A PRESSED-THROUGH WARD IS ONE CROSSING (wards.ts): the drift that
|
|
183
|
+
// made it spends the pass, so the way back out is pressed again.
|
|
184
|
+
if (taken > 0)
|
|
185
|
+
spendWardPass(actor);
|
|
181
186
|
return { cell, taken, arrived, blockedBy, ward, metres: taken * METERS_PER_CELL };
|
|
182
187
|
}
|
|
183
188
|
/** What stopped you, said the way the astral would say it. */
|
|
@@ -197,7 +202,7 @@ export function driftRefusal(d, direction) {
|
|
|
197
202
|
return `A mana barrier hangs across the way ${direction}${d.ward ? ` -- ${d.ward}'s working` : ''}: a hazy, solid sheet of woven mana. The astral does not pass (p.315-316).`;
|
|
198
203
|
}
|
|
199
204
|
if (d.blockedBy === 'ward') {
|
|
200
|
-
return `A
|
|
205
|
+
return `A ward stands across the way ${direction}${d.ward ? ` -- the one over ${d.ward}` : ''}: a hazy, solid lattice of woven mana. The astral does not pass${hint(` -- "go ${direction}" presses through it (Magic + Charisma v. its Force x 2, p.316), "attack ward" tears at it`)}.`;
|
|
201
206
|
}
|
|
202
207
|
// WHICH NOTHING IT IS depends on which way you tried. Downward the
|
|
203
208
|
// refusal is canon and worth saying out loud -- solid earth is the
|
|
@@ -7,7 +7,7 @@ export function resolveBarrierStrike(scene, logger, attacker, device, room, opts
|
|
|
7
7
|
const meta = [];
|
|
8
8
|
const done = (opened = false) => ({ world, meta, opened });
|
|
9
9
|
if (device.structure <= 0) {
|
|
10
|
-
world.push(`${device.name}
|
|
10
|
+
world.push(`${device.name} has no Structure -- there is nothing for a round to break.`);
|
|
11
11
|
return done();
|
|
12
12
|
}
|
|
13
13
|
if (device.isOpen() || device.isHoled) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mendWards } from './wards.js';
|
|
1
2
|
import { regenerate } from './barrier-geometry.js';
|
|
2
3
|
import { rollInitiativeScore, formatInitiative, INITIATIVE_PASS_DROP, rollPool, formatRoll } from './dice.js';
|
|
3
4
|
import { ActionBudget } from './action-budget.js';
|
|
@@ -823,6 +824,14 @@ export class CombatEncounter {
|
|
|
823
824
|
mended = true;
|
|
824
825
|
this.logger.log(`{cyan-fg}${barrier.caster}'s ${kind === 'mana' ? 'mana barrier' : 'barrier'} knits itself whole again -- Structure ${barrier.structure} (${kind === 'mana' ? 'p.316' : 'p.294'}).{/cyan-fg}`, { room: barrier.roomName });
|
|
825
826
|
}
|
|
827
|
+
// WARDS TOO (p.316): a ward is a mana barrier, whole again at the end
|
|
828
|
+
// of every Combat Turn -- tearing one down is a single turn's work.
|
|
829
|
+
if (kind === 'mana') {
|
|
830
|
+
for (const ward of mendWards(Object.values(this.scene.getRooms?.() ?? {}))) {
|
|
831
|
+
mended = true;
|
|
832
|
+
this.logger.log(`{cyan-fg}The ward over ${ward.name} knits itself whole again -- Structure ${ward.wardForce} (p.316).{/cyan-fg}`, { room: ward.name });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
826
835
|
if (mended) {
|
|
827
836
|
this.scene.updateStatus?.();
|
|
828
837
|
this.scene.updateExits?.(this.room);
|
|
@@ -626,7 +626,8 @@ export function planeFaults(plane, rooms) {
|
|
|
626
626
|
for (let j = i + 1; j < floors.length; j++) {
|
|
627
627
|
const [an, a] = floors[i];
|
|
628
628
|
const [bn, b] = floors[j];
|
|
629
|
-
|
|
629
|
+
// Spans that share a storey (a tall room reaches up through them).
|
|
630
|
+
if (a.origin.z + (a.height ?? 1) <= b.origin.z || b.origin.z + (b.height ?? 1) <= a.origin.z)
|
|
630
631
|
continue;
|
|
631
632
|
if (overlaps(a, b.origin.x - WALL_CELLS, b.origin.y - WALL_CELLS, b.dims.x + 2 * WALL_CELLS, b.dims.y + 2 * WALL_CELLS)) {
|
|
632
633
|
out.push(`${label(an)} and ${label(bn)} are drawn touching, with no wall between them.`);
|
|
@@ -991,9 +992,11 @@ function interiorMap(placements) {
|
|
|
991
992
|
for (const [name, at] of placements) {
|
|
992
993
|
if (at.outdoors)
|
|
993
994
|
continue;
|
|
994
|
-
for (let
|
|
995
|
-
for (let
|
|
996
|
-
|
|
995
|
+
for (let z = at.origin.z; z < at.origin.z + (at.height ?? 1); z++) {
|
|
996
|
+
for (let x = at.origin.x; x < at.origin.x + at.dims.x; x++) {
|
|
997
|
+
for (let y = at.origin.y; y < at.origin.y + at.dims.y; y++) {
|
|
998
|
+
interiorAt.set(`${x},${y},${z}`, name);
|
|
999
|
+
}
|
|
997
1000
|
}
|
|
998
1001
|
}
|
|
999
1002
|
}
|
|
@@ -1046,20 +1049,21 @@ function ringsAndDoorways(placements, graph) {
|
|
|
1046
1049
|
for (const [, at] of placements) {
|
|
1047
1050
|
if (at.outdoors)
|
|
1048
1051
|
continue;
|
|
1049
|
-
|
|
1050
|
-
for (let
|
|
1051
|
-
for (let
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1052
|
+
// A TALL ROOM IS WALLED ON EVERY STOREY IT SPANS (seed `storeys`).
|
|
1053
|
+
for (let z = at.origin.z; z < at.origin.z + (at.height ?? 1); z++)
|
|
1054
|
+
for (let x = at.origin.x - WALL_CELLS; x < at.origin.x + at.dims.x + WALL_CELLS; x++) {
|
|
1055
|
+
for (let y = at.origin.y - WALL_CELLS; y < at.origin.y + at.dims.y + WALL_CELLS; y++) {
|
|
1056
|
+
const inside = x >= at.origin.x && x < at.origin.x + at.dims.x
|
|
1057
|
+
&& y >= at.origin.y && y < at.origin.y + at.dims.y;
|
|
1058
|
+
if (inside)
|
|
1059
|
+
continue;
|
|
1060
|
+
// Never wall over someone else's floor: two buildings sharing a
|
|
1061
|
+
// gutter get one wall between them, not two overlapping rings.
|
|
1062
|
+
if (interiorAt.has(`${x},${y},${z}`))
|
|
1063
|
+
continue;
|
|
1064
|
+
walls.add(`${x},${y},${z}`);
|
|
1065
|
+
}
|
|
1061
1066
|
}
|
|
1062
|
-
}
|
|
1063
1067
|
}
|
|
1064
1068
|
// DOORWAYS: a gap in the wall between two rooms the exit graph joins
|
|
1065
1069
|
// laterally. Punched on the facing edge, nearest the neighbour, so
|
|
@@ -1124,7 +1128,27 @@ function overlaps(a, bx, by, bw, bh) {
|
|
|
1124
1128
|
* Room.ensureGrid takes its exit cell FROM the plane (the reverse of
|
|
1125
1129
|
* the packed path). One rule, no jitter, nothing to reconcile.
|
|
1126
1130
|
*/
|
|
1127
|
-
function authoredDoors(placements, graph) {
|
|
1131
|
+
function authoredDoors(placements, graph, rooms = []) {
|
|
1132
|
+
// WHERE AN AUTHOR MOVED A DOOR (user 2026-10-06: "move ... the door
|
|
1133
|
+
// placement, within reason based on wall area between the rooms"):
|
|
1134
|
+
// one plane coordinate along the shared wall per pair of rooms, from
|
|
1135
|
+
// whichever side declared it, so both halves of the door agree and it
|
|
1136
|
+
// stays one gap in one wall (planeFaults). Clamped below to the
|
|
1137
|
+
// overlap -- a door cannot open onto masonry.
|
|
1138
|
+
const pairKey = (a, b) => [a, b].sort().join('|');
|
|
1139
|
+
const along = new Map();
|
|
1140
|
+
for (const room of rooms) {
|
|
1141
|
+
const at = placements.get(norm(room.name));
|
|
1142
|
+
if (!at || !room.doorAt)
|
|
1143
|
+
continue;
|
|
1144
|
+
for (const link of graph.get(norm(room.name)) ?? []) {
|
|
1145
|
+
const lat = LATERAL[link.direction];
|
|
1146
|
+
const cell = room.doorAt[link.direction];
|
|
1147
|
+
if (!lat || !cell)
|
|
1148
|
+
continue;
|
|
1149
|
+
along.set(pairKey(norm(room.name), link.to), lat[0] !== 0 ? at.origin.y + cell.y : at.origin.x + cell.x);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1128
1152
|
for (const [name, at] of placements) {
|
|
1129
1153
|
if (at.outdoors)
|
|
1130
1154
|
continue;
|
|
@@ -1138,13 +1162,19 @@ function authoredDoors(placements, graph) {
|
|
|
1138
1162
|
// East/west wall: the door slides along Y.
|
|
1139
1163
|
const lo = Math.max(at.origin.y, other.origin.y);
|
|
1140
1164
|
const hi = Math.min(at.origin.y + at.dims.y, other.origin.y + other.dims.y);
|
|
1141
|
-
const
|
|
1165
|
+
const authored = along.get(pairKey(name, link.to));
|
|
1166
|
+
const midY = hi > lo
|
|
1167
|
+
? (authored !== undefined ? clamp(authored, lo, hi - 1) : Math.floor((lo + hi - 1) / 2))
|
|
1168
|
+
: at.origin.y + Math.floor(at.dims.y / 2);
|
|
1142
1169
|
doors[link.direction] = { x: 0, y: clamp(midY - at.origin.y, 0, at.dims.y - 1) };
|
|
1143
1170
|
}
|
|
1144
1171
|
else {
|
|
1145
1172
|
const lo = Math.max(at.origin.x, other.origin.x);
|
|
1146
1173
|
const hi = Math.min(at.origin.x + at.dims.x, other.origin.x + other.dims.x);
|
|
1147
|
-
const
|
|
1174
|
+
const authored = along.get(pairKey(name, link.to));
|
|
1175
|
+
const midX = hi > lo
|
|
1176
|
+
? (authored !== undefined ? clamp(authored, lo, hi - 1) : Math.floor((lo + hi - 1) / 2))
|
|
1177
|
+
: at.origin.x + Math.floor(at.dims.x / 2);
|
|
1148
1178
|
doors[link.direction] = { x: clamp(midX - at.origin.x, 0, at.dims.x - 1), y: 0 };
|
|
1149
1179
|
}
|
|
1150
1180
|
}
|
|
@@ -1168,6 +1198,10 @@ function parkUnplaced(rooms, placements, graph, byName, notes) {
|
|
|
1168
1198
|
for (const at of placements.values()) {
|
|
1169
1199
|
if (at.origin.z !== z)
|
|
1170
1200
|
continue;
|
|
1201
|
+
// THE GROUND IS WHERE THINGS PARK, not in their way: a room set
|
|
1202
|
+
// down on it cuts its own hole (roomAtCell).
|
|
1203
|
+
if (at.ground)
|
|
1204
|
+
continue;
|
|
1171
1205
|
// One clear cell all round, so two rings never fuse.
|
|
1172
1206
|
if (overlaps(at, x - 1, y - 1, w + 2, h + 2))
|
|
1173
1207
|
return false;
|
|
@@ -1258,11 +1292,13 @@ export function buildAuthoredPlane(rooms) {
|
|
|
1258
1292
|
origin: { x: fp.x, y: fp.y, z: room.storey ?? 0 },
|
|
1259
1293
|
dims: { x: Math.max(1, fp.w), y: Math.max(1, fp.h) },
|
|
1260
1294
|
outdoors: room.outdoors === true,
|
|
1295
|
+
...(room.ground ? { ground: true } : {}),
|
|
1296
|
+
...(room.storeys && room.storeys > 1 ? { height: room.storeys } : {}),
|
|
1261
1297
|
});
|
|
1262
1298
|
}
|
|
1263
1299
|
parkUnplaced(rooms, placements, graph, byName, notes);
|
|
1264
|
-
authoredDoors(placements, graph);
|
|
1265
|
-
const storeys = [...new Set([0, ...[...placements.values()].
|
|
1300
|
+
authoredDoors(placements, graph, rooms);
|
|
1301
|
+
const storeys = [...new Set([0, ...[...placements.values()].flatMap(p => Array.from({ length: p.height ?? 1 }, (_, i) => p.origin.z + i))])]
|
|
1266
1302
|
.sort((a, b) => a - b);
|
|
1267
1303
|
// The canvas is the extent worth DRAWING, not a fence -- the edges
|
|
1268
1304
|
// are open, so this only has to cover what exists.
|
|
@@ -1305,16 +1341,34 @@ export function worldCell(plane, room, local) {
|
|
|
1305
1341
|
return undefined;
|
|
1306
1342
|
return { x: at.origin.x + local.x, y: at.origin.y + local.y, z: at.origin.z + local.z };
|
|
1307
1343
|
}
|
|
1308
|
-
/**
|
|
1344
|
+
/**
|
|
1345
|
+
* Which room owns a plane cell, if any.
|
|
1346
|
+
*
|
|
1347
|
+
* ROOMS ALWAYS WIN (user rulings 2026-10-06): where rectangles overlap,
|
|
1348
|
+
* an indoor room owns the cell; else an outdoor space; the ground --
|
|
1349
|
+
* the area under everything -- only what nothing else covers. So a
|
|
1350
|
+
* building drawn on a street cuts its floor out of the street, and a
|
|
1351
|
+
* street drawn on the ground cuts its own out of the ground, whatever
|
|
1352
|
+
* order they were drawn or named in. A packed district never overlaps,
|
|
1353
|
+
* so it answers exactly as the first-match scan always did.
|
|
1354
|
+
*/
|
|
1309
1355
|
export function roomAtCell(plane, c) {
|
|
1356
|
+
let space;
|
|
1357
|
+
let ground;
|
|
1310
1358
|
for (const [name, at] of plane.placements) {
|
|
1311
|
-
if (at.origin.z
|
|
1359
|
+
if (c.z < at.origin.z || c.z >= at.origin.z + (at.height ?? 1))
|
|
1360
|
+
continue;
|
|
1361
|
+
if (c.x < at.origin.x || c.x >= at.origin.x + at.dims.x
|
|
1362
|
+
|| c.y < at.origin.y || c.y >= at.origin.y + at.dims.y)
|
|
1312
1363
|
continue;
|
|
1313
|
-
if (
|
|
1314
|
-
&& c.y >= at.origin.y && c.y < at.origin.y + at.dims.y)
|
|
1364
|
+
if (!at.outdoors)
|
|
1315
1365
|
return name;
|
|
1366
|
+
if (at.ground)
|
|
1367
|
+
ground ??= name;
|
|
1368
|
+
else
|
|
1369
|
+
space ??= name;
|
|
1316
1370
|
}
|
|
1317
|
-
return
|
|
1371
|
+
return space ?? ground;
|
|
1318
1372
|
}
|
|
1319
1373
|
/**
|
|
1320
1374
|
* Straight-line distance between two plane cells, in metres. The whole
|
|
@@ -1650,6 +1704,30 @@ export const planeSeed = hashFnv1a;
|
|
|
1650
1704
|
* side is the ground outside it. No side arithmetic and no exit graph:
|
|
1651
1705
|
* ask which of a doorway's four neighbours this street covers.
|
|
1652
1706
|
*/
|
|
1707
|
+
/**
|
|
1708
|
+
* THE CELLS OF AN OUTDOOR AREA'S RECTANGLE IT DOES NOT OWN, room-local
|
|
1709
|
+
* "x,y" (user rulings 2026-10-06: "rooms always win"; spaces are drawn
|
|
1710
|
+
* over the ground): another room's floor or a wall. Empty for anything
|
|
1711
|
+
* indoor, and for an area nothing is drawn over -- which is every packed
|
|
1712
|
+
* district, so their grids are untouched.
|
|
1713
|
+
*/
|
|
1714
|
+
export function holesOf(plane, room) {
|
|
1715
|
+
const holes = new Set();
|
|
1716
|
+
const me = norm(room);
|
|
1717
|
+
const at = plane.placements.get(me);
|
|
1718
|
+
if (!at?.outdoors)
|
|
1719
|
+
return holes;
|
|
1720
|
+
const z = at.origin.z;
|
|
1721
|
+
for (let lx = 0; lx < at.dims.x; lx++) {
|
|
1722
|
+
for (let ly = 0; ly < at.dims.y; ly++) {
|
|
1723
|
+
const x = at.origin.x + lx;
|
|
1724
|
+
const y = at.origin.y + ly;
|
|
1725
|
+
if (plane.walls.has(`${x},${y},${z}`) || roomAtCell(plane, { x, y, z }) !== me)
|
|
1726
|
+
holes.add(`${lx},${ly}`);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return holes;
|
|
1730
|
+
}
|
|
1653
1731
|
export function doorstepsOnto(plane, room) {
|
|
1654
1732
|
const at = plane.placements.get(norm(room));
|
|
1655
1733
|
if (at === undefined || !at.outdoors)
|
|
@@ -1667,6 +1745,10 @@ export function doorstepsOnto(plane, room) {
|
|
|
1667
1745
|
continue;
|
|
1668
1746
|
if (y < at.origin.y || y >= at.origin.y + at.dims.y)
|
|
1669
1747
|
continue;
|
|
1748
|
+
// Only the street's OWN ground: a building drawn inside it owns
|
|
1749
|
+
// the cells on its side of the doorway (roomAtCell).
|
|
1750
|
+
if (roomAtCell(plane, { x, y, z: at.origin.z }) !== norm(room))
|
|
1751
|
+
continue;
|
|
1670
1752
|
const local = `${x - at.origin.x},${y - at.origin.y}`;
|
|
1671
1753
|
if (seen.has(local))
|
|
1672
1754
|
continue;
|