@maka/maka-cli 5.382.0 → 5.384.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.
Files changed (38) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +89 -3
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +206 -41
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.js +9 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/command.js +10 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +15 -7
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/follow.js +1 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +25 -9
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +3 -3
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +10 -2
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +9 -3
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +4 -4
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +7 -6
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +8 -2
  15. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +37 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/game.js +12 -4
  17. package/bundle/typescript/src/commands/game/sideQuest/headless.js +3 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +3 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +12 -6
  20. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +4 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +87 -18
  22. package/bundle/typescript/src/commands/game/sideQuest/utilities/astral-move.js +4 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +102 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-geometry.js +253 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +32 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +25 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +1 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +26 -4
  29. package/bundle/typescript/src/commands/game/sideQuest/utilities/map-glyphs.js +15 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/utilities/map-rulers.js +58 -0
  31. package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +30 -4
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-authorization.js +27 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +38 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +22 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +51 -11
  36. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +24 -2
  37. package/bundle/typescript/src/commands/game/sideQuest/utilities/ways-out.js +34 -2
  38. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { capitalCase } from 'change-case';
2
2
  import { Door } from '../models/door.js';
3
3
  import { Direction } from '../types/shared/direction-enum.js';
4
- import { FRONTIER_GLYPH, BLOCKED_GLYPH, UP_GLYPH, DOWN_GLYPH, PLAYER_GLYPH, WALL_GLYPH, DOOR_GLYPH, FLOOR_GLYPH, VOID_GLYPH, TERRAIN_GLYPH, OUTSIDE_GROUND_COLOR, GLYPH_RANK, OFF_EDGE_GLYPH, } from './map-glyphs.js';
4
+ import { FRONTIER_GLYPH, BLOCKED_GLYPH, UP_GLYPH, DOWN_GLYPH, PLAYER_GLYPH, WALL_GLYPH, DOOR_GLYPH, FLOOR_GLYPH, VOID_GLYPH, TERRAIN_GLYPH, OUTSIDE_GROUND_COLOR, GLYPH_RANK, OFF_EDGE_GLYPH, BARRIER_GLYPH, BARRIER_COLOR, MANA_BARRIER_COLOR, } from './map-glyphs.js';
5
+ import { xRulerLine, yRulerLabel } from './map-rulers.js';
5
6
  import { roomAtCell, isVoid, wallsBetween, districtBearing, isOpenAir } from './district-plane.js';
6
7
  import { distanceMeters, METERS_PER_LEVEL } from './room-grid.js';
7
8
  import { isOutdoorsPlace } from './outdoors.js';
@@ -713,8 +714,13 @@ function renderPlaneViewport(plane, rooms, here, eyes, viewW, viewH) {
713
714
  */
714
715
  const MARGIN = 2;
715
716
  const cam = eyes?.at;
716
- const wantW = Math.max(1, viewW);
717
- const wantH = Math.max(1, viewH);
717
+ // The rulers come out of the panel, not on top of it: a fixed gutter
718
+ // (three characters -- the plane is 60 rows deep -- and a space) and
719
+ // one row for the x labels.
720
+ const RULER_GUTTER = 3;
721
+ const rulers = eyes?.rulers === true;
722
+ const wantW = Math.max(1, viewW - (rulers ? RULER_GUTTER + 1 : 0));
723
+ const wantH = Math.max(1, viewH - (rulers ? 1 : 0));
718
724
  /**
719
725
  * ONE CELL, ONE DOT -- 2 m, the same as the room panel.
720
726
  *
@@ -1068,6 +1074,21 @@ function renderPlaneViewport(plane, rooms, here, eyes, viewW, viewH) {
1068
1074
  labels.set(at2, ch);
1069
1075
  });
1070
1076
  }
1077
+ // BARRIER SPELLS (barrier-geometry.ts): a Physical Barrier for every
1078
+ // eye, a Mana Barrier only for its weaver or an eye on the astral.
1079
+ const spellCells = new Map();
1080
+ const seer = eyes?.viewer;
1081
+ const seesAstral = !!seer && (seer.plane === 'astral' || seer.astralPerceiving);
1082
+ for (const b of here.scene?.spellBarriers?.() ?? []) {
1083
+ if (b.kind === 'mana' && !seesAstral && b.caster !== seer?.name)
1084
+ continue;
1085
+ for (const f of b.faces) {
1086
+ if (f.breached)
1087
+ continue;
1088
+ for (const c of f.cells)
1089
+ spellCells.set(`${c.x},${c.y},${c.z}`, b.kind);
1090
+ }
1091
+ }
1071
1092
  const out = [];
1072
1093
  let openStyle = '';
1073
1094
  for (let ry = 0; ry < rows; ry++) {
@@ -1195,6 +1216,9 @@ function renderPlaneViewport(plane, rooms, here, eyes, viewW, viewH) {
1195
1216
  * not internal detail. The floorplan is what the Room panel
1196
1217
  * is for, one zoom in.
1197
1218
  */
1219
+ const spell = spellCells.get(key);
1220
+ if (spell)
1221
+ take(BARRIER_GLYPH, GLYPH_RANK.barrier, spell === 'mana' ? MANA_BARRIER_COLOR : BARRIER_COLOR);
1198
1222
  const owner = roomAtCell(plane, { x, y, z });
1199
1223
  const indoors = owner !== undefined && !plane.placements.get(owner).outdoors;
1200
1224
  const ring = plane.walls.has(key) || (zoomedOut && plane.doorways.has(key) && wallOwner.has(key));
@@ -1300,8 +1324,10 @@ function renderPlaneViewport(plane, rooms, here, eyes, viewW, viewH) {
1300
1324
  openStyle = '';
1301
1325
  // NOT trimmed: a filled row is the point, and trailing ground is
1302
1326
  // still ground.
1303
- out.push(line);
1327
+ out.push(rulers ? `${yRulerLabel(y0 + Math.floor((ry * spanY) / rows), RULER_GUTTER)} ${line}` : line);
1304
1328
  }
1329
+ if (rulers)
1330
+ out.unshift(`${' '.repeat(RULER_GUTTER + 1)}${xRulerLine(x0, cols, cellsPerDot)}`);
1305
1331
  return out;
1306
1332
  }
1307
1333
  /**
@@ -126,6 +126,33 @@ export function authorizeNpcAction(scene, actor, verb, args) {
126
126
  }
127
127
  return ALLOWED;
128
128
  }
129
+ /**
130
+ * THE JOHNSON HOLDS THE MEET (CWMrB2aNyL4hhrjyG: "when at the meet, the
131
+ * johnson shouldn't just keep moving around the room").
132
+ *
133
+ * The run's client picks the venue and waits in it (Run Faster's meet,
134
+ * p.197-198: the Johnson sets the place, the runners come to them). His
135
+ * AI read every beat as a chance to stage himself -- "Mr. Lacquer moves
136
+ * to the open floor", "moves to the counter", prod 2026-10-04 -- so the
137
+ * man the runner came to see wandered the room for the whole briefing.
138
+ * Prose can still have him turn, lean, or look at the door; the engine
139
+ * keeps his body where the meet put it until the job is settled, and his
140
+ * leaving after the settlement is the engine's own beat (game.ts
141
+ * "Johnsons don't linger"), not his AI's.
142
+ *
143
+ * A FIGHT IS NOT A MEET: inside a Combat Turn the turn loop moves him,
144
+ * and a Johnson ducking for cover is not this rule's business.
145
+ *
146
+ * Returns a refusal reason, or undefined when he may move.
147
+ */
148
+ export function clientHoldsTheMeet(scene, actor) {
149
+ const client = scene.ownerGame?.currentClient;
150
+ if (!client || client.name !== actor.name || client.reported || client.dead)
151
+ return undefined;
152
+ if (actor.inExchange || scene.encounterIn?.(actor.currentLocation))
153
+ return undefined;
154
+ return `You hold your seat for the meet -- the runner came to you, and you stay where you are until the job is settled. Turn, gesture, or speak instead of walking.`;
155
+ }
129
156
  /**
130
157
  * Is this actor home? A decker jacked into the Matrix, a magician
131
158
  * projecting astrally and a rigger jumped into a drone all leave a body
@@ -7,6 +7,7 @@ import { fuzzyPickName } from './fuzzy-match.js';
7
7
  import { resetOverwatch } from './overwatch.js';
8
8
  import { rebootWipe } from './marks.js';
9
9
  import { hint } from './hints.js';
10
+ import { codesOnPage } from './clue-reading.js';
10
11
  import { actorCell } from './spots.js';
11
12
  import { announceGameOver } from './game-over.js';
12
13
  import { holdAtBrink } from './not-dead-yet.js';
@@ -624,6 +625,43 @@ export function isObjectiveItem(scene, itemName) {
624
625
  export function objectiveMark(scene, itemName) {
625
626
  return isObjectiveItem(scene, itemName) ? `{yellow-fg}${OBJECTIVE_GLYPH}{/yellow-fg} ` : '';
626
627
  }
628
+ /**
629
+ * PART OF THE JOB, NOT THE PRIZE (nfrBW27eRRxstJ8uC: "the 'Lot Catalogue
630
+ * Proof Sheet' isn't marked in my inventory as quest specific").
631
+ *
632
+ * The star is the objective alone, so a page the job's route runs
633
+ * through -- the proof sheet with the vault shutter's code pencilled in
634
+ * its margin -- sat unmarked among the loot, and the runner could not
635
+ * tell it from a souvenir. A LEAD is an item this scene's barriers
636
+ * answer to, read off the same facts the engine itself opens them with:
637
+ * - a page that spells one of the scene's device codes (clue-reading.ts
638
+ * codesOnPage -- the fact ensureClueDetails guarantees is written), or
639
+ * - the key a device declares as its own (Device.keyItem).
640
+ * Nothing else: a lead the engine cannot point to would be a guess.
641
+ *
642
+ * It marks the item, never the code -- whether the runner can READ the
643
+ * number off the page is still the Perception Test (clue-reading.ts).
644
+ * Hub scenes have no win condition and mark nothing, like the star.
645
+ */
646
+ export const LEAD_GLYPH = '☆';
647
+ export function isJobLead(scene, item) {
648
+ if (!scene?.getWinCondition?.())
649
+ return false;
650
+ const name = item.name.trim().toLowerCase();
651
+ for (const room of Object.values(scene.getRooms())) {
652
+ for (const device of room.devices ?? []) {
653
+ if (device.keyItem?.name.trim().toLowerCase() === name)
654
+ return true;
655
+ }
656
+ }
657
+ return codesOnPage(scene, item).length > 0;
658
+ }
659
+ /** `★ ` for the objective, `☆ ` for a lead (isJobLead), '' otherwise. */
660
+ export function jobMark(scene, item) {
661
+ if (isObjectiveItem(scene, item.name))
662
+ return objectiveMark(scene, item.name);
663
+ return isJobLead(scene, item) ? `{yellow-fg}${LEAD_GLYPH}{/yellow-fg} ` : '';
664
+ }
627
665
  /**
628
666
  * The "you can see them, but you can't touch them" refusal. When a target
629
667
  * name matches someone in the room on a plane the actor perceives but
@@ -248,8 +248,23 @@ function cellExists(grid, c, levelCells) {
248
248
  export function cellOnGrid(grid, c) {
249
249
  return cellExists(grid, c, LEVEL_CELLS.get(grid) ?? new Map());
250
250
  }
251
- /** Whether `c` sits inside an uncleared BLOCKING terrain region. */
251
+ /**
252
+ * SOMETHING ELSE THAT SEALS A CELL, asked live: a sustained Physical
253
+ * Barrier (Scene.physicalBarrierAtLocal, registered by Room.ensureGrid).
254
+ * A spell comes and goes many times over a grid's life, so it is a
255
+ * question the grid asks rather than terrain baked into it -- and a
256
+ * rebuilt grid cannot lose a wall that was never stored on it. Keyed by
257
+ * grid identity like LEVEL_CELLS: per room, never module-wide (several
258
+ * scenes share this process on the site).
259
+ */
260
+ const CELL_BLOCKERS = new WeakMap();
261
+ export function setCellBlocker(grid, blocked) {
262
+ CELL_BLOCKERS.set(grid, blocked);
263
+ }
264
+ /** Whether `c` sits inside an uncleared BLOCKING terrain region, or under a standing barrier spell. */
252
265
  function isBlocked(grid, c) {
266
+ if (CELL_BLOCKERS.get(grid)?.(c))
267
+ return true;
253
268
  const k = key(c);
254
269
  for (const region of grid.terrain.values()) {
255
270
  if (!region.blocking || region.cleared)
@@ -482,6 +497,9 @@ export function pathClearing(grid, from, to, region) {
482
497
  const trial = { ...grid, terrain: new Map(grid.terrain) };
483
498
  trial.terrain.set(region.id, { ...region, cleared: true });
484
499
  LEVEL_CELLS.set(trial, LEVEL_CELLS.get(grid) ?? new Map());
500
+ const blocker = CELL_BLOCKERS.get(grid);
501
+ if (blocker)
502
+ CELL_BLOCKERS.set(trial, blocker);
485
503
  return findPath(trial, from, to);
486
504
  }
487
505
  /**
@@ -503,6 +521,9 @@ export function pathWithRegionsCleared(grid, from, to, regionIds) {
503
521
  trial.terrain.set(id, { ...region, cleared: true });
504
522
  }
505
523
  LEVEL_CELLS.set(trial, LEVEL_CELLS.get(grid) ?? new Map());
524
+ const blocker = CELL_BLOCKERS.get(grid);
525
+ if (blocker)
526
+ CELL_BLOCKERS.set(trial, blocker);
506
527
  return findPath(trial, from, to);
507
528
  }
508
529
  /** SR5 p.172 (RAG-verified): DV = meters fallen, only beyond 3m; AP-4
@@ -4,7 +4,9 @@ import { Direction } from '../types/shared/direction-enum.js';
4
4
  import { OPEN_FLOOR, spotOf, visibleActorsIn, exitSpot, seatingIn, theSpot, isPerchSpot } from './spots.js';
5
5
  import { placeName } from './log-style.js';
6
6
  import { DRONE_COLOUR } from './drone-prose.js';
7
- import { PLAYER_GLYPH, OTHER_PLAYER_GLYPH, OTHER_PLAYER_COLOR, NEUTRAL_GLYPH, HOSTILE_GLYPH, CREW_GLYPH, DEAD_GLYPH, DOWNED_GLYPH, SPOT_GLYPH, RIGGED_GLYPH, UP_GLYPH, CAMERA_GLYPH, CAMERA_COLOR, CAMERA_DEAD_COLOR, exitGlyph, GLYPH_RANK, } from './map-glyphs.js';
7
+ import { PLAYER_GLYPH, OTHER_PLAYER_GLYPH, OTHER_PLAYER_COLOR, NEUTRAL_GLYPH, HOSTILE_GLYPH, CREW_GLYPH, DEAD_GLYPH, DOWNED_GLYPH, SPOT_GLYPH, RIGGED_GLYPH, UP_GLYPH, CAMERA_GLYPH, CAMERA_COLOR, CAMERA_DEAD_COLOR, BARRIER_GLYPH, BARRIER_COLOR, MANA_BARRIER_COLOR, exitGlyph, GLYPH_RANK, } from './map-glyphs.js';
8
+ import { barriersWire } from './ways-out.js';
9
+ import { xRulerLine, yRulerLabel, yGutterWidth } from './map-rulers.js';
8
10
  import { key, levelFootprint, dimsForSize, roomRasterRows, METERS_PER_LEVEL, METERS_PER_CELL, } from './room-grid.js';
9
11
  /**
10
12
  * WHO THIS PANEL DRAWS: everyone you can see, plus every ARO your AR is
@@ -276,7 +278,16 @@ function buildRenderMaps(grid, room, viewer) {
276
278
  if (c)
277
279
  cameraAt.set(`${c.x},${c.y}`, { cam, z: c.z });
278
280
  }
279
- return { cellToSpot, cellToExit, verticalAt, cellTerrain, occupants, corpses, slumpedBody, cameraAt };
281
+ const barrierAt = new Map();
282
+ for (const b of barriersWire(room, viewer)) {
283
+ for (const f of b.faces) {
284
+ if (f.breached)
285
+ continue;
286
+ for (const c of f.cells)
287
+ barrierAt.set(key(c), b.kind);
288
+ }
289
+ }
290
+ return { cellToSpot, cellToExit, verticalAt, cellTerrain, occupants, corpses, slumpedBody, cameraAt, barrierAt };
280
291
  }
281
292
  function paint(g) {
282
293
  const body = g.inverse ? `{inverse}${g.ch}{/inverse}` : g.ch;
@@ -484,6 +495,9 @@ function glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile) {
484
495
  const lens = maps.cameraAt.get(`${c.x},${c.y}`);
485
496
  if (lens && lens.z >= c.z)
486
497
  return cameraGlyph(lens.cam);
498
+ const wall = maps.barrierAt.get(k);
499
+ if (wall)
500
+ return { ch: BARRIER_GLYPH, color: wall === 'mana' ? MANA_BARRIER_COLOR : BARRIER_COLOR, rank: GLYPH_RANK.barrier };
487
501
  const terrain = maps.cellTerrain.get(k);
488
502
  if (terrain)
489
503
  return terrain.blocking ? { ch: '▓', color: 'red', rank: GLYPH_RANK.terrainBlocking } : { ch: '▒', color: 'yellow', rank: GLYPH_RANK.terrainCover };
@@ -552,7 +566,9 @@ function levelPlansFor(room) {
552
566
  * glyph used. Returns [] for a spotless room (no grid ever synthesizes
553
567
  * -- see Room.ensureGrid).
554
568
  */
555
- export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly, isHostile) {
569
+ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly, isHostile,
570
+ /** The room's PLANE origin: rulers in plane coordinates (map-rulers.ts). */
571
+ origin) {
556
572
  const grid = room.ensureGrid();
557
573
  if (!grid)
558
574
  return [];
@@ -569,8 +585,11 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly, isHos
569
585
  const exists = new Set(footprint.map(key));
570
586
  if (levelPlans.length > 1)
571
587
  rows.push(`{bold}${lvl.label}{/bold}`);
588
+ const gutter = origin ? yGutterWidth(origin.y + y0, y1 - y0 + 1) + 1 : 0;
589
+ if (origin)
590
+ rows.push(`${' '.repeat(gutter)}${xRulerLine(origin.x + x0, x1 - x0 + 1)}`);
572
591
  for (let y = y0; y <= y1; y++) {
573
- let row = '';
592
+ let row = origin ? `${yRulerLabel(origin.y + y, gutter - 1)} ` : '';
574
593
  for (let x = x0; x <= x1; x++) {
575
594
  const c = { x, y, z: lvl.z };
576
595
  row += exists.has(key(c)) ? cell(glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile)) : ' ';
@@ -589,6 +608,11 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly, isHos
589
608
  // that reads as a keyword in the description must read as one here.
590
609
  legend.push(`{yellow-fg}${SPOT_GLYPH}{/yellow-fg} places: ${places.map(placeName).join(', ')}`);
591
610
  }
611
+ const kinds = new Set(maps.barrierAt.values());
612
+ if (kinds.has('physical'))
613
+ legend.push(`{${BARRIER_COLOR}-fg}${BARRIER_GLYPH}{/${BARRIER_COLOR}-fg} force barrier -- nothing physical passes; "attack barrier" to break a face`);
614
+ if (kinds.has('mana'))
615
+ legend.push(`{${MANA_BARRIER_COLOR}-fg}${BARRIER_GLYPH}{/${MANA_BARRIER_COLOR}-fg} mana barrier -- stops the astral and spirits; the living walk through`);
592
616
  for (const dir of grid.exitCells.keys()) {
593
617
  // Name the spot you actually stand in to leave this way -- read
594
618
  // from the same resolver movement uses, not from cell coincidence:
@@ -716,10 +740,13 @@ export function rightAlign(text, width) {
716
740
  const pad = Math.max(0, width - visibleWidth(text));
717
741
  return `${' '.repeat(pad)}${text}`;
718
742
  }
719
- export function roomMetresPerDot(room, availW, viewH) {
743
+ export function roomMetresPerDot(room, availW, viewH, origin) {
720
744
  const grid = room.ensureGrid();
721
745
  const dims = grid ? { x: grid.dims.x, y: grid.dims.y } : dimsForSize(room.size);
722
- return frameScale(dims.x, dims.y, availW, viewH) * METERS_PER_CELL;
746
+ // The rulers come out of the same raster (frameCells), so the scale
747
+ // read-out has to subtract them too or it would claim a finer dot.
748
+ const gutter = origin ? yGutterWidth(origin.y, dims.y) + 1 : 0;
749
+ return frameScale(dims.x, dims.y, availW - gutter, origin ? viewH - 1 : viewH) * METERS_PER_CELL;
723
750
  }
724
751
  export function frameScale(width, height, availW, viewH) {
725
752
  // One column a dot -- see cell(). The /2 this used to carry was the
@@ -758,15 +785,22 @@ export function frameScale(width, height, availW, viewH) {
758
785
  * The block is centred inside a fixed `viewH` rows so the box height
759
786
  * never jumps between rooms either.
760
787
  */
761
- function frameCells(cells, availW, viewH) {
788
+ function frameCells(cells, availW, viewH,
789
+ /** PLANE coordinates of cells[0][0]: draw the x ruler above and the y
790
+ * gutter beside (map-rulers.ts). Both come out of the same raster, so
791
+ * the box never grows -- the room scales a little harder instead. */
792
+ axes) {
762
793
  const height = cells.length;
763
794
  const width = Math.max(...cells.map(r => r.length), 0);
764
- const scale = frameScale(width, height, availW, viewH);
795
+ const gutter = axes ? yGutterWidth(axes.y, height) + 1 : 0;
796
+ const cols = availW - gutter;
797
+ const rows = axes ? viewH - 1 : viewH;
798
+ const scale = frameScale(width, height, cols, rows);
765
799
  const outW = Math.ceil(width / scale);
766
800
  const outH = Math.ceil(height / scale);
767
801
  const body = [];
768
802
  for (let ry = 0; ry < outH; ry++) {
769
- let line = '';
803
+ let line = axes ? `${yRulerLabel(axes.y + ry * scale, gutter - 1)} ` : '';
770
804
  for (let rx = 0; rx < outW; rx++) {
771
805
  // The highest-ranked thing in the block wins the dot -- see
772
806
  // IGlyph.rank. At scale 1 this is exactly the old behaviour,
@@ -783,6 +817,8 @@ function frameCells(cells, availW, viewH) {
783
817
  }
784
818
  body.push(line.trimEnd());
785
819
  }
820
+ if (axes)
821
+ body.unshift(`${' '.repeat(gutter)}${xRulerLine(axes.x, outW, scale)}`);
786
822
  // Vertically centre into a fixed height: a short room shouldn't sit
787
823
  // jammed against the top while a tall one fills the box.
788
824
  const pad = Math.max(0, viewH - body.length);
@@ -968,7 +1004,7 @@ export function renderRoomLayoutCompact(room, viewer, opts = {}) {
968
1004
  // room is SCALED onto it (frameCells), so this bounds the picture
969
1005
  // rather than the room: an outdoor room takes its extent from the
970
1006
  // district plane and can be far deeper than any size class.
971
- const { viewH = roomRasterRows(), boxWidth, isOtherPlayer, isTableAlly, isHostile } = opts;
1007
+ const { viewH = roomRasterRows(), boxWidth, isOtherPlayer, isTableAlly, isHostile, origin } = opts;
972
1008
  // Width left for the grid once the right-justified elevation bar and
973
1009
  // its gap are reserved.
974
1010
  const availW = Math.max(6, (boxWidth ?? 27) - ELEVATION_WIDTH - 1);
@@ -1023,7 +1059,7 @@ export function renderRoomLayoutCompact(room, viewer, opts = {}) {
1023
1059
  const lenses = new Map();
1024
1060
  for (const { cam, z: lz } of maps.cameraAt.values())
1025
1061
  lenses.set(lz, [...(lenses.get(lz) ?? []), cam]);
1026
- return withElevation(frameCells(cells, availW, viewH),
1062
+ return withElevation(frameCells(cells, availW, viewH, origin ? { x: origin.x + x0, y: origin.y + y0 } : undefined),
1027
1063
  // Circulating has no level to claim, same as it has no cell.
1028
1064
  elevationColumn(grid, viewerCell ? z : undefined, viewH, partialMeters, lenses), boxWidth);
1029
1065
  }
@@ -1099,6 +1135,10 @@ export function roomCaption(room, viewer) {
1099
1135
  const base = spot ? `at ${spot}` : '(moving through)';
1100
1136
  const { above, below } = actorsOnOtherLevels(room, viewer);
1101
1137
  const notes = [];
1138
+ // WHERE, IN THE RULERS' NUMBERS -- what "cast barrier x,y" is aimed with.
1139
+ const here = room.scene?.worldCellOf?.(viewer);
1140
+ if (here)
1141
+ notes.push(`${here.x},${here.y},${here.z}`);
1102
1142
  if (above > 0)
1103
1143
  notes.push(`+${above} above`);
1104
1144
  if (below > 0)
@@ -1,6 +1,6 @@
1
1
  import { droneImmobile } from './drone-prose.js';
2
2
  import { Direction } from '../types/shared/direction-enum.js';
3
- import { isOpenGround, isVoid, roomAtCell, streetDoors } from './district-plane.js';
3
+ import { isOpenGround, isVoid, roomAtCell, streetDoors, worldCell } from './district-plane.js';
4
4
  import { isOutdoorsPlace } from './outdoors.js';
5
5
  import { wayOut } from './ways-out.js';
6
6
  import { significantWords, wordsAreCloseEnough } from './fuzzy-match.js';
@@ -889,11 +889,24 @@ export function stepFrom(room, mover, direction, count) {
889
889
  const openOutside = openGroundBeyond(room);
890
890
  const furniture = furnitureCells(room);
891
891
  const bodies = standingOccupants(room, mover);
892
+ // A BARRIER SPELL is its own word (barrier-geometry.ts): it is drawn,
893
+ // it shimmers, and "the way is blocked" would hide whose working it is.
894
+ // Asked on the plane, so it stops a step off an outdoor room's edge too.
895
+ const scenePlane = room.scene?.spellBarriers?.('physical').length ? room.scene.ensurePlane() : undefined;
896
+ const barrierHere = (local) => {
897
+ if (!scenePlane || !room.scene)
898
+ return undefined;
899
+ const world = worldCell(scenePlane, room.name, local);
900
+ return world ? room.scene.barrierAt(world, 'physical')?.barrier.caster : undefined;
901
+ };
892
902
  let cur = start;
893
903
  let taken = 0;
894
904
  for (let i = 0; i < count; i++) {
895
905
  const next = { x: cur.x + step[0], y: cur.y + step[1], z: cur.z };
896
906
  const k = key(next);
907
+ const walledBy = barrierHere(next);
908
+ if (walledBy !== undefined)
909
+ return { cell: cur, taken, blockedBy: 'barrier', blocker: walledBy };
897
910
  if (!standable.has(k)) {
898
911
  const inBounds = next.x >= 0 && next.y >= 0 && next.x < grid.dims.x && next.y < grid.dims.y;
899
912
  // Off the edge of an OUTDOOR room is just more street, unless the
@@ -2526,7 +2539,10 @@ export function describeSpotRoster(room, viewer) {
2526
2539
  * district, and the room it happens to be over is a label the caller
2527
2540
  * re-derives.
2528
2541
  */
2529
- export function astralStepFrom(plane, from, direction, count, roomAt) {
2542
+ export function astralStepFrom(plane, from, direction, count, roomAt,
2543
+ /** Whose MANA BARRIER stands on a cell, if any (Scene.barrierAt --
2544
+ * the creator's own weave already excluded, p.315). */
2545
+ manaBarrierAt) {
2530
2546
  /**
2531
2547
  * UP AND DOWN ARE DIRECTIONS OUT HERE (p.313-314: an astral form
2532
2548
  * "can move in any direction" and is "not limited by gravity").
@@ -2564,6 +2580,12 @@ export function astralStepFrom(plane, from, direction, count, roomAt) {
2564
2580
  if (here !== there) {
2565
2581
  return { cell: cur, taken, blockedBy: 'ward', ward: (there ?? here)?.name };
2566
2582
  }
2583
+ // A MANA BARRIER SPELL: "solid, hazily opaque walls that stop astral
2584
+ // movement" (p.315-316). `go` always refused one; a `move` drift
2585
+ // used to float straight through it, because this walk never asked.
2586
+ const weaver = manaBarrierAt?.(next);
2587
+ if (weaver !== undefined)
2588
+ return { cell: cur, taken, blockedBy: 'barrier', ward: weaver };
2567
2589
  cur = next;
2568
2590
  taken++;
2569
2591
  }
@@ -71,7 +71,7 @@ export function wayOut(room, direction, from) {
71
71
  * (map-view.ts serializePlane); here a street keeps only what the plane
72
72
  * could not place.
73
73
  */
74
- export function roomGridWire(room) {
74
+ export function roomGridWire(room, viewer) {
75
75
  const grid = room.ensureGrid();
76
76
  if (!grid)
77
77
  return undefined;
@@ -83,9 +83,41 @@ export function roomGridWire(room) {
83
83
  damage: cam.damage, structure: cam.structure, destroyed: cam.destroyed,
84
84
  }] : [];
85
85
  });
86
- const wire = cameras.length > 0 ? { ...base, cameras } : base;
86
+ const withCams = cameras.length > 0 ? { ...base, cameras } : base;
87
+ const barriers = barriersWire(room, viewer);
88
+ const wire = barriers.length > 0 ? { ...withCams, barriers } : withCams;
87
89
  if (!isOutdoorsPlace(room))
88
90
  return wire;
89
91
  return { ...wire, doors: wire.doors.filter(d => wayOut(room, d.dir)?.kind === 'grid') };
90
92
  }
93
+ /**
94
+ * The barrier spells a viewer can see around this room, in its local
95
+ * cells: every standing Physical Barrier with a cell within one of the
96
+ * footprint (its wall ring included), and a Mana Barrier only for its
97
+ * weaver or an eye on the astral (p.294: "invisible to the physical
98
+ * plane"). Breached faces ride along marked, so a client can draw the
99
+ * gap rather than lose the face.
100
+ */
101
+ export function barriersWire(room, viewer) {
102
+ const scene = room.scene;
103
+ const all = scene?.spellBarriers?.() ?? [];
104
+ if (!scene || all.length === 0)
105
+ return [];
106
+ const frame = scene.ensurePlane().placements.get(roomKey(room.name));
107
+ if (!frame)
108
+ return [];
109
+ const seesAstral = !!viewer && (viewer.plane === 'astral' || viewer.astralPerceiving);
110
+ const near = (c) => c.x >= frame.origin.x - 1 && c.x <= frame.origin.x + frame.dims.x
111
+ && c.y >= frame.origin.y - 1 && c.y <= frame.origin.y + frame.dims.y;
112
+ return all
113
+ .filter(b => b.kind === 'physical' || seesAstral || b.caster === viewer?.name)
114
+ .filter(b => b.faces.some(f => f.cells.some(near)))
115
+ .map(b => ({
116
+ kind: b.kind, caster: b.caster, shape: b.shape, force: b.force, structure: b.structure,
117
+ faces: b.faces.map(f => ({
118
+ id: f.id, damage: f.damage, breached: f.breached,
119
+ cells: f.cells.map(c => ({ x: c.x - frame.origin.x, y: c.y - frame.origin.y, z: c.z - frame.origin.z })),
120
+ })),
121
+ }));
122
+ }
91
123
  //# sourceMappingURL=ways-out.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.382.0",
3
+ "version": "5.384.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",