@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,7 @@
1
1
  import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { capitalCase } from 'change-case';
4
- import { planeTintItemName, objectiveMark } from '../utilities/planes.js';
4
+ import { planeTintItemName, jobMark } from '../utilities/planes.js';
5
5
  import { dataDevices } from '../utilities/data-library.js';
6
6
  /**
7
7
  * On-demand inventory panel, replacing the always-on blessed sidebar box
@@ -76,7 +76,7 @@ export class InventoryCommand extends Command {
76
76
  const tinted = item.plane === 'meat'
77
77
  ? rarityTint(item.rating, planeTintItemName(item, 'meat'))
78
78
  : planeTintItemName(item, 'meat');
79
- let line = ` ${CARRIED_MARK}${objectiveMark(this.scene, item.name)}${tinted}`;
79
+ let line = ` ${CARRIED_MARK}${jobMark(this.scene, item)}${tinted}`;
80
80
  // x N for a stack, the same shape the currency tag below uses.
81
81
  if (item.quantity > 1)
82
82
  line += ` �${item.quantity}`;
@@ -130,7 +130,7 @@ export class InventoryCommand extends Command {
130
130
  if (libraries.length > 0) {
131
131
  lines.push('', `{light-blue-fg}On your devices:{/light-blue-fg}`);
132
132
  for (const device of libraries) {
133
- lines.push(` ${device.name}: ${device.files.map(f => `${objectiveMark(this.scene, f.name)}${planeTintItemName(f, 'meat')}`).join(', ')}`);
133
+ lines.push(` ${device.name}: ${device.files.map(f => `${jobMark(this.scene, f)}${planeTintItemName(f, 'meat')}`).join(', ')}`);
134
134
  }
135
135
  const legend = hint(`("transfer <file> to <device>" moves one; "transfer <file> to chip" writes it to physical media.)`);
136
136
  if (legend)
@@ -12,7 +12,7 @@ import { getPref, setPref } from '../utilities/prefs.js';
12
12
  import { roomMetresPerDot } from '../utilities/room-view.js';
13
13
  import { roomRasterRows } from '../utilities/room-grid.js';
14
14
  import { PLANE_METRES_PER_DOT, fogEye, fogName, fogOf } from '../utilities/map-view.js';
15
- import { PLAYER_GLYPH, OTHER_PLAYER_GLYPH, CREW_GLYPH, NEUTRAL_GLYPH, HOSTILE_GLYPH, DOWNED_GLYPH, DEAD_GLYPH, RIGGED_GLYPH, SPOT_GLYPH, CAMERA_GLYPH, CAMERA_COLOR, CAMERA_DEAD_COLOR, WALL_GLYPH, DOOR_GLYPH, FLOOR_GLYPH, FRONTIER_GLYPH, BLOCKED_GLYPH, UP_GLYPH, DOWN_GLYPH, } from '../utilities/map-glyphs.js';
15
+ import { PLAYER_GLYPH, OTHER_PLAYER_GLYPH, CREW_GLYPH, NEUTRAL_GLYPH, HOSTILE_GLYPH, DOWNED_GLYPH, DEAD_GLYPH, RIGGED_GLYPH, SPOT_GLYPH, CAMERA_GLYPH, CAMERA_COLOR, CAMERA_DEAD_COLOR, BARRIER_GLYPH, BARRIER_COLOR, MANA_BARRIER_COLOR, WALL_GLYPH, DOOR_GLYPH, FLOOR_GLYPH, FRONTIER_GLYPH, BLOCKED_GLYPH, UP_GLYPH, DOWN_GLYPH, } from '../utilities/map-glyphs.js';
16
16
  /**
17
17
  * The on-demand WAYS-ON list. Registered as both "map" and "exits";
18
18
  * since 2026-08-25 the drawn maps are permanently docked beside the log
@@ -41,7 +41,7 @@ export class MapCommand extends Command {
41
41
  if (sub === 'legend' || sub === 'key') {
42
42
  // Asked of the panel rather than guessed -- see Game.roomGridWidth.
43
43
  const availW = this.game?.roomGridWidth() ?? 19;
44
- const metres = roomMetresPerDot(room, availW, roomRasterRows());
44
+ const metres = roomMetresPerDot(room, availW, roomRasterRows(), room.planeFrame?.()?.origin);
45
45
  const out = [];
46
46
  out.push(`{bold}THE MAP KEY{/bold}`);
47
47
  out.push('');
@@ -81,6 +81,14 @@ export class MapCommand extends Command {
81
81
  out.push(` {${CAMERA_DEAD_COLOR}-fg}${CAMERA_GLYPH}{/${CAMERA_DEAD_COLOR}-fg} a smashed one`);
82
82
  out.push(` On the elevation bar, ${CAMERA_GLYPH} marks the height a camera hangs at.`);
83
83
  out.push('');
84
+ out.push(`{bold}Barriers{/bold}`);
85
+ out.push(` {${BARRIER_COLOR}-fg}${BARRIER_GLYPH}{/${BARRIER_COLOR}-fg} a force barrier -- nothing physical passes; "attack barrier" breaks a face`);
86
+ out.push(` {${MANA_BARRIER_COLOR}-fg}${BARRIER_GLYPH}{/${MANA_BARRIER_COLOR}-fg} a mana barrier, if you can see the astral -- it stops spirits and astral forms`);
87
+ out.push('');
88
+ out.push(`{bold}Coordinates{/bold}`);
89
+ out.push(` The numbers along the top and left edges are district x and y -- 2 m a cell, north is up.`);
90
+ out.push(` Your own x,y,z is under the Floor Plan. "cast barrier <x,y> <x,y>" raises a wall between two of them.`);
91
+ out.push('');
84
92
  out.push(`{bold}Scale{/bold}`);
85
93
  out.push(` Floor Plan: one dot is {white-fg}${metres} m{/white-fg} here.`);
86
94
  out.push(` Outside, the map runs at {white-fg}${PLANE_METRES_PER_DOT} m{/white-fg} a dot on the ground and scrolls with you;`);
@@ -45,6 +45,7 @@ import { onTheWall, midClimbRefusal } from '../utilities/climb-state.js';
45
45
  * verb that leaves a room.
46
46
  */
47
47
  export class MoveCommand extends Command {
48
+ static movesActor = true;
48
49
  static verb = 'walk';
49
50
  static description = 'Cross the room to a named spot or person: "move to bar", "move to Brick" (also "walk"/"approach"; "run to"/"sprint to" are the fast crossings). "move south 2" paces you across the room by cells instead -- use "go" to leave the room entirely. Bare "move" lists the room\'s spots. You must share a spot with someone to talk or trade with them.';
50
51
  /** Which movement mode this registration answers to (see subclasses). */
@@ -1200,7 +1201,10 @@ export class MoveCommand extends Command {
1200
1201
  // world simply stops. Naming it a wall would send the
1201
1202
  // player looking for a door.
1202
1203
  : blockedBy === 'edge' ? `the ground ends there`
1203
- : `there's a wall`;
1204
+ // A barrier spell, said as one: it is drawn, and it is
1205
+ // somebody's working (barrier-geometry.ts).
1206
+ : blockedBy === 'barrier' ? `a shimmer-wall of hardened air stands there${blocker ? ` -- ${blocker}'s barrier` : ''}`
1207
+ : `there's a wall`;
1204
1208
  const refusal = `You can't step ${direction} -- ${why}.${this.doorHint(room, cell, blockedBy)}`;
1205
1209
  // SAY IT ONCE (player screenshot 2026-09-21: twelve identical
1206
1210
  // "You can't step south -- there's a wall." filling the log).
@@ -1279,7 +1283,8 @@ export class MoveCommand extends Command {
1279
1283
  ? `Pulled up short after ${meters} m -- ${blockedBy === 'someone' ? 'someone is standing there'
1280
1284
  : blockedBy === 'furniture' ? 'furniture blocks the way'
1281
1285
  : blockedBy === 'terrain' ? 'the way is blocked'
1282
- : blockedBy === 'edge' ? 'the ground ends there' : 'the wall'}.`
1286
+ : blockedBy === 'edge' ? 'the ground ends there'
1287
+ : blockedBy === 'barrier' ? 'a force barrier stands there' : 'the wall'}.`
1283
1288
  : '';
1284
1289
  const arrived = actor.atSpot !== wasAt && actor.atSpot !== OPEN_FLOOR
1285
1290
  ? `You're at the ${actor.atSpot} now.`
@@ -1724,7 +1729,8 @@ export class MoveCommand extends Command {
1724
1729
  return driftRefusal(drift, direction);
1725
1730
  const stopped = drift.blockedBy === undefined ? ''
1726
1731
  : drift.blockedBy === 'ward' ? ` A ward stops you${drift.ward ? ` at ${drift.ward}` : ''}.`
1727
- : ' Then solid earth.';
1732
+ : drift.blockedBy === 'barrier' ? ` A mana barrier stops you${drift.ward ? ` -- ${drift.ward}'s working` : ''}.`
1733
+ : ' Then solid earth.';
1728
1734
  // Silent on a plain drift, like a meat step: the map moved, which
1729
1735
  // is the answer. Speak when you arrived over somewhere named, or
1730
1736
  // when something out here stopped you.
@@ -1,6 +1,6 @@
1
1
  import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
- import { planeTintItemName, emphasizeFind, objectiveMark, fileReach } from '../utilities/planes.js';
3
+ import { planeTintItemName, emphasizeFind, jobMark, fileReach } from '../utilities/planes.js';
4
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
5
5
  import { perceptionTest, matrixSearchTest, matrixSearchSeconds, MATRIX_SEARCH } from '../utilities/perception.js';
6
6
  import { gridVicinity } from '../utilities/grid-reach.js';
@@ -194,7 +194,7 @@ export class SearchCommand extends Command {
194
194
  if (files.length === 0) {
195
195
  return `The archive is indexed and EMPTY -- ${hostLabel(where)} keeps nothing worth the trip. ${clock}`;
196
196
  }
197
- const list = files.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
197
+ const list = files.map(i => `${jobMark(this.scene, i)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
198
198
  const seal = where.hostCracked
199
199
  ? hint(` ("take" them -- the seal is already broken.)`)
200
200
  : hint(` (sealed in the host's data vault: "hack" the node to break the seal, then "take".)`);
@@ -206,7 +206,7 @@ export class SearchCommand extends Command {
206
206
  const sealedElsewhere = files.length > 0 && where.hasNode;
207
207
  return `Nothing loose on ${where.name}'s grid. ${clock}${sealedElsewhere ? hint(` (${hostLabel(where)} keeps its files INSIDE -- "enter" it and search from in there.)`) : ''}`;
208
208
  }
209
- const list = loose.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
209
+ const list = loose.map(i => `${jobMark(this.scene, i)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
210
210
  return `Loose on ${where.name}'s grid: ${list}. ${clock}${hint(` ("take" rides them out on your deck.)`)}`;
211
211
  }
212
212
  async execute(_args) {
@@ -303,7 +303,7 @@ export class SearchCommand extends Command {
303
303
  // Plain lowercase for the log/AI history; the SCREEN list is tinted
304
304
  // per plane (see planeTintItemName) so paydata reads as data.
305
305
  const itemsList = found.map(i => i.name.toLowerCase()).join(', ');
306
- const displayList = found.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, this.actor.plane))}`).join(', ');
306
+ const displayList = found.map(i => `${jobMark(this.scene, i)}${emphasizeFind(planeTintItemName(i, this.actor.plane))}`).join(', ');
307
307
  this.logger.write(`SearchCommand: ${this.actor.name} found the following items: ${itemsList}`);
308
308
  this.actor.performAction('search', itemsList);
309
309
  description += `You search the area and find: ${displayList}`;
@@ -4,6 +4,7 @@ import { Category } from '../types/shared/item-enum.js';
4
4
  import { hint } from '../utilities/hints.js';
5
5
  import { getAspect } from '../aspects.js';
6
6
  import { spellCreationCap } from '../spells.js';
7
+ import { describeBarrier } from '../utilities/barrier-geometry.js';
7
8
  /**
8
9
  * The spellbook: how a player actually discovers what they can cast and
9
10
  * why. Especially load-bearing for INNATE casting -- an Awakened character
@@ -118,10 +119,10 @@ export class SpellsCommand extends Command {
118
119
  lines.push(`Sustaining on ${w.bearer}: ${w.key === 'reflexes' ? 'increase reflexes' : w.key} (${sustainBurden(w, ' to everything else')}).${hint(` Cast ${w.key} on ${w.bearer} again to release.`)}`);
119
120
  }
120
121
  if (actor.sustainedBarrier) {
121
- lines.push(`Sustained: force barrier sealing ${actor.sustainedBarrier.direction} in ${actor.sustainedBarrier.roomName}, Force ${actor.sustainedBarrier.force} (${burden('barrier')}).${hint(' Cast barrier again to release.')}`);
122
+ lines.push(`Sustained: force barrier -- ${describeBarrier(actor.sustainedBarrier)} (${burden('barrier')}).${hint(' "cast barrier off" releases it.')}`);
122
123
  }
123
124
  if (actor.sustainedManaBarrier) {
124
- lines.push(`Sustained: mana barrier sealing ${actor.sustainedManaBarrier.direction} in ${actor.sustainedManaBarrier.roomName}, Force ${actor.sustainedManaBarrier.force} (${burden('manabarrier')}).${hint(' Cast manabarrier again to release.')}`);
125
+ lines.push(`Sustained: mana barrier -- ${describeBarrier(actor.sustainedManaBarrier)} (${burden('manabarrier')}).${hint(' "cast manabarrier off" releases it.')}`);
125
126
  }
126
127
  if (actor.sustainedReflexes) {
127
128
  lines.push(`Sustained: increase reflexes, +2 initiative (${burden('reflexes')}).${hint(' Cast it again to release.')}`);
@@ -176,10 +177,10 @@ export class SpellsCommand extends Command {
176
177
  line: 'sustained: light slides around the bearer, +3 on sneak contests at -2 dice. Drain F-4.' },
177
178
  { key: 'reflexes', cast: 'increase reflexes', how: 'cast increase reflexes [on <ally>]',
178
179
  line: 'sustained: +2 initiative for the bearer at -2 dice. Drain F-3.' },
179
- { key: 'barrier', cast: 'barrier', how: 'cast barrier <direction>; cast barrier again to release',
180
- line: 'sustained: hardened air seals ONE exit of your room; nobody meat-side passes (you included) at -2 dice to you. Drain F-3.' },
181
- { key: 'manabarrier', cast: 'manabarrier', how: 'cast manabarrier <direction>',
182
- line: 'sustained: woven mana seals ONE exit against SPIRITS and astral forms; the living pass freely. Drain F-2.' },
180
+ { key: 'barrier', cast: 'barrier', how: 'cast barrier [F] <x,y> <x,y> (wall) | <x,y> (dome) | <direction> [cells] | at <someone> | (bare: a dome round you); cast barrier off',
181
+ line: 'sustained: hardened air -- a wall Force x 2 m long and Force m high, or a Force-metre dome. Armor/Structure = hits, a track per face; nobody meat-side passes (you included) at -2 dice to you. Drain F-1.' },
182
+ { key: 'manabarrier', cast: 'manabarrier', how: 'cast manabarrier [F] <x,y> <x,y> | <x,y> | <direction> [cells] | at <someone>; cast manabarrier off',
183
+ line: 'sustained: woven mana, same shapes -- stops SPIRITS and astral forms; the living pass freely, and so do you. Armor/Structure = Force. Drain F-2.' },
183
184
  ];
184
185
  for (const entry of book) {
185
186
  if (!actor.knowsSpell(entry.key))
@@ -153,7 +153,8 @@ export class TalkCommand extends Command {
153
153
  if (insideWithMe.includes(target)) {
154
154
  await speaker.hear(speaker, `(to ${target.name}) ${message}`);
155
155
  await target.hear(speaker, `(privately) ${message}`);
156
- return `You speak to ${target.name} inside the host: "${message}"`;
156
+ // The echo above IS the confirmation (see the meat branch below).
157
+ return '';
157
158
  }
158
159
  // "At" spots: conversation needs shared ground (see
159
160
  // utilities/spots.ts) -- the bartender doesn't lean across the room.
@@ -174,7 +175,12 @@ export class TalkCommand extends Command {
174
175
  // Face time with the run's CLIENT is THE MEET (or the wrap-up) --
175
176
  // see Game.onClientInteraction (terms / lump-sum settlement).
176
177
  const clientBeat = this.game?.onClientInteraction(target.name);
177
- return `You talk to ${target.name} privately: "${message}"${clientBeat ? `\n${clientBeat}` : ''}`;
178
+ // ONE LINE PER THING SAID (86LjwvK7xSbXhWXGi: "this repeated text can
179
+ // be removed: You talk to Mr. Vellum privately: ..."). The speaker's
180
+ // own hear() above already printed "Maka: (to Mr. Vellum) ...", so a
181
+ // reply quoting the same words back was the same sentence twice. The
182
+ // reply carries only what the echo cannot: the meet's beat.
183
+ return clientBeat ?? '';
178
184
  }
179
185
  }
180
186
  //# sourceMappingURL=talk.js.map
@@ -3319,5 +3319,41 @@
3319
3319
  // hub boot (reaching a save that already slid) and after a shared
3320
3320
  // run's homecoming. A move that cannot land now is retried next time;
3321
3321
  // a session with no districtSeedLoader never moves.
3322
- export const ENGINE_VERSION = '1.194.0';
3322
+ // 1.195.0 (2026-10-04): THE MEET, CLEANED UP (four live-table reports).
3323
+ // - The run's client holds his seat until the job is settled: his AI's
3324
+ // move/approach/run/sprint/fly/go/follow are refused with a reason
3325
+ // (npc-authorization.ts clientHoldsTheMeet; Command.movesActor), except
3326
+ // inside a fight (CWMrB2aNyL4hhrjyG).
3327
+ // - A body nudged onto an exact cell (shoulderAside, yieldDoorway) still
3328
+ // hears and is heard: earshot reads an exact cell as a place, so the
3329
+ // shoved Johnson is no longer deaf to `say` (xurFSiqNtRNbXA88A).
3330
+ // - talk-to/tell no longer quote the words back under the speaker's own
3331
+ // echo (86LjwvK7xSbXhWXGi).
3332
+ // - `inv`/`search` mark job leads with ☆ -- a page spelling a scene
3333
+ // device's code, or a device's own key -- beside the objective's ★
3334
+ // (nfrBW27eRRxstJ8uC).
3335
+ // - The HUD's holstered line drops its `-- "brandish"` tip
3336
+ // (MkxamPxmsSyonR3wC).
3337
+ // 1.196.0 (2026-10-05): BARRIER SPELLS ON THE DISTRICT PLANE (SR5 p.294,
3338
+ // p.197-198, p.315-316; utilities/barrier-geometry.ts).
3339
+ // - A barrier is cells, not a tag on an exit: `cast barrier [F] <x,y[,z]>
3340
+ // [<x,y[,z]>]` (a wall between two points, or a Force-metre dome round
3341
+ // one), `cast barrier [F] <dir> [cells]` (1x3 across the way), `cast
3342
+ // barrier [F] at <someone>`, bare `cast barrier` (a dome round you),
3343
+ // `cast barrier off`. Plane coordinates; LOS-gated; clipped by matter
3344
+ // (physical) or by earth/wards/other weaves (mana).
3345
+ // - Physical Barrier rolls for real: Armor and Structure = hits [Force].
3346
+ // Mana Barrier: Force. One damage track per FACE (a wall has one; a
3347
+ // dome N/E/S/W, plus a crown past one level). `attack barrier [x,y]`
3348
+ // breaks a face (p.197 text: DV must EXCEED Armor; guns do 1 box);
3349
+ // a gun fired through one loses a box to it. Both regenerate at the
3350
+ // Combat Turn boundary (mana at the end, physical at the start).
3351
+ // - Movement: findPath/stepFrom route round a physical barrier; astral
3352
+ // `move` now stops at a mana barrier (it drifted through); astral `go`
3353
+ // presses through on Magic + Charisma [Astral] v. Force x 2.
3354
+ // - Bare `cast <direction>` no longer unweaves a ward -- `dispel <dir>`.
3355
+ // - Wire: the room grid carries `barriers` (room-local cells; a mana
3356
+ // barrier only for eyes that can see it). Docks draw x/y rulers in
3357
+ // plane coordinates and the caption gives your x,y,z.
3358
+ export const ENGINE_VERSION = '1.196.0';
3323
3359
  //# sourceMappingURL=engine-version.js.map
@@ -149,6 +149,7 @@ import { ForceCommand } from './commands/force.js';
149
149
  import { CompleteCommand } from './commands/complete.js';
150
150
  import { DisableCommand, BrickCommand } from './commands/disable.js';
151
151
  import { CastCommand } from './commands/cast.js';
152
+ import { barrierSpan } from './utilities/barrier-geometry.js';
152
153
  import { FollowCommand, UnfollowCommand } from './commands/follow.js';
153
154
  import { BringCommand } from './commands/bring.js';
154
155
  import { SurrenderCommand } from './commands/surrender.js';
@@ -9273,8 +9274,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
9273
9274
  ? ` {red-fg}[JAMMED -- reboot]{/red-fg}`
9274
9275
  : weaponItem.ammo === 0 ? ` {red-fg}[DRY]{/red-fg}` : ` [${weaponItem.ammo} rds]`)
9275
9276
  : '';
9277
+ // NO VERB COACHING IN THE HUD (MkxamPxmsSyonR3wC: "get rid of this
9278
+ // line in the status bar: -- "brandish""). The status bar is state;
9279
+ // the `brandish` hint already rides the equip/holster replies and
9280
+ // `equipment`, where it answers the moment the question comes up.
9276
9281
  if (player.plane === 'meat' && weaponItem && !player.weaponDrawn) {
9277
- return `{white-fg}Holstered: ${weaponItem.name}${ammoNote}${hint(' -- "brandish"')}{/white-fg}`;
9282
+ return `{white-fg}Holstered: ${weaponItem.name}${ammoNote}{/white-fg}`;
9278
9283
  }
9279
9284
  // White, not gray: gray is ANSI bright-black and illegible on
9280
9285
  // stock dark palettes -- the same ruling that moved the HUD
@@ -9473,10 +9478,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
9473
9478
  // the one that strands the team -- and both of them seal an exit
9474
9479
  // that the caster cannot pass either.
9475
9480
  if (player.sustainedBarrier) {
9476
- heldSpells.push(`physical barrier (${player.sustainedBarrier.roomName} ${player.sustainedBarrier.direction})`);
9481
+ heldSpells.push(`physical barrier (${player.sustainedBarrier.shape} ${barrierSpan(player.sustainedBarrier)}, ${player.sustainedBarrier.roomName})`);
9477
9482
  }
9478
9483
  if (player.sustainedManaBarrier) {
9479
- heldSpells.push(`mana barrier (${player.sustainedManaBarrier.roomName} ${player.sustainedManaBarrier.direction})`);
9484
+ heldSpells.push(`mana barrier (${player.sustainedManaBarrier.shape} ${barrierSpan(player.sustainedManaBarrier)}, ${player.sustainedManaBarrier.roomName})`);
9480
9485
  }
9481
9486
  // What this character holds on OTHERS -- the caster's burden, named
9482
9487
  // by bearer so the mage knows whose armor is costing them two dice.
@@ -9717,6 +9722,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
9717
9722
  // Whose eyes, and what counts as hostile -- so the district draws
9718
9723
  // the same people, with the same marks, the room panel does.
9719
9724
  viewer: this.player,
9725
+ rulers: true,
9720
9726
  isOtherPlayer: a => this.scene.isHumanControlled(a),
9721
9727
  isTableAlly: crewOfTable(this.scene.getPlayers()),
9722
9728
  isHostile: a => hostileByStance(a) || enemyInFight(this.scene, this.player, a),
@@ -9749,6 +9755,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
9749
9755
  const roomLines = renderRoomLayoutCompact(here, this.player, {
9750
9756
  isTableAlly: crewOfTable(this.scene.getPlayers()),
9751
9757
  boxWidth,
9758
+ // Rulers in PLANE coordinates -- the numbers "cast barrier x,y" takes.
9759
+ origin: here.planeFrame?.()?.origin,
9752
9760
  isOtherPlayer: a => this.scene.isHumanControlled(a),
9753
9761
  // Red is the stance flag OR whoever the Combat Turn says is
9754
9762
  // fighting you (room-view.ts hostileByStance).
@@ -9763,7 +9771,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
9763
9771
  // of its own. The GLYPHS moved to `map legend`, where there is
9764
9772
  // room to say what they mean instead of abbreviating them into
9765
9773
  // a strip nobody could read.
9766
- roomLines.push(rightAlign(dockScale(roomMetresPerDot(here, this.roomGridWidth() ?? Math.max(6, (boxWidth ?? 27) - 8), roomRasterRows())), boxWidth));
9774
+ roomLines.push(rightAlign(dockScale(roomMetresPerDot(here, this.roomGridWidth() ?? Math.max(6, (boxWidth ?? 27) - 8), roomRasterRows(), here.planeFrame?.()?.origin)), boxWidth));
9767
9775
  roomLines.push(caption(roomCaption(here, this.player), boxWidth));
9768
9776
  }
9769
9777
  this.setRoomOverlay(roomLines);
@@ -564,6 +564,7 @@ function wireHeadless(game, ctx, logger) {
564
564
  arEyes: p.hasMatrixEyes(),
565
565
  at: game.scene.worldCellOf(p),
566
566
  viewer: p,
567
+ rulers: true,
567
568
  isOtherPlayer: (a) => game.scene.isHumanControlled(a),
568
569
  isTableAlly: crewOfTable(game.scene.getPlayers()),
569
570
  isHostile: (a) => hostileByStance(a) || enemyInFight(game.scene, p, a),
@@ -651,6 +652,7 @@ function wireHeadless(game, ctx, logger) {
651
652
  try {
652
653
  const roomLines = renderRoomLayoutCompact(here, p, {
653
654
  isOtherPlayer: a => game.scene.isHumanControlled(a),
655
+ origin: here.planeFrame?.()?.origin,
654
656
  isHostile: a => hostileByStance(a) || enemyInFight(game.scene, p, a),
655
657
  // The leader's crew are the TABLE's crew -- without this
656
658
  // every non-leader saw them as neutral bystanders.
@@ -671,7 +673,7 @@ function wireHeadless(game, ctx, logger) {
671
673
  // here anyway rather than asserted, since a decoration must
672
674
  // never take down the room push (same reasoning as the
673
675
  // try/catch this sits inside).
674
- const grid = roomGridWire(here);
676
+ const grid = roomGridWire(here, p);
675
677
  const data = grid ? { grid } : undefined;
676
678
  logger.system(roomLines.join('\n'), { actor: p.name }, 'room', data);
677
679
  }
@@ -2613,7 +2613,9 @@ staff, so codes, passphrases, and lock combinations are exactly the thing you we
2613
2613
  THEM to solve -- say so plainly when they ask, and send them to find it rather than
2614
2614
  handing them a number you'd be making up. If you genuinely don't know a thing, say so plainly and point at who might. You
2615
2615
  front half the pay at the meet and settle the rest on delivery (the engine moves the money
2616
- -- never haggle it yourself unless they invoke a negotiation).`
2616
+ -- never haggle it yourself unless they invoke a negotiation).
2617
+ You chose this venue and you HOLD YOUR SEAT in it: never move, walk, approach, follow or
2618
+ leave -- the runner comes to you. Turn, lean, gesture or speak instead.`
2617
2619
  : '';
2618
2620
  // ---------- prompt ----------
2619
2621
  const prompt = `
@@ -3180,6 +3180,10 @@ export class Player extends AbstractPlayer {
3180
3180
  * rather than leaving you drawn at the old one. NPCs keep re-deriving --
3181
3181
  * a picket or a crowd is placed by spot on purpose (NPC overrides).
3182
3182
  */
3183
+ /** An exact cell somebody PUT this body on -- a step, a shove, a
3184
+ * flight -- as opposed to a drift seat pinned only so it is drawn in
3185
+ * one place (pinDerivedSeat). earshot.ts reads the first as a place. */
3186
+ get placedOnCell() { return this._atCell !== undefined && !this._cellPinnedFromSeat; }
3183
3187
  get pinsDerivedSeat() { return true; }
3184
3188
  pinDerivedSeat(cell) {
3185
3189
  this.atCell = cell;
@@ -5240,10 +5244,11 @@ export class Player extends AbstractPlayer {
5240
5244
  }
5241
5245
  }
5242
5246
  }
5243
- /** The sustained PHYSICAL BARRIER spell (ruling 2026-08-24): seals
5244
- * one exit of one room while held -- go.ts and move refuse passage
5245
- * for everyone (the caster included; walls don't play favorites).
5246
- * Session-only, like every sustain. */
5247
+ /** The sustained PHYSICAL BARRIER spell: cells on the district plane
5248
+ * (utilities/barrier-geometry.ts, 2026-10-05 -- it used to seal one
5249
+ * exit). Nothing physical crosses a standing face, the caster
5250
+ * included; walls don't play favorites. Session-only, like every
5251
+ * sustain. */
5247
5252
  _sustainedBarrier;
5248
5253
  get sustainedBarrier() { return this._sustainedBarrier; }
5249
5254
  set sustainedBarrier(v) {
@@ -5255,8 +5260,9 @@ export class Player extends AbstractPlayer {
5255
5260
  this.noteConditionChanged();
5256
5261
  }
5257
5262
  /** The MANA BARRIER twin (canon p.294, RAG-checked): blocks SPIRITS
5258
- * and astral forms at the sealed exit; living mundanes and physical
5259
- * objects pass freely. Same lifecycle as sustainedBarrier. */
5263
+ * and astral forms at its cells; living mundanes and physical
5264
+ * objects pass freely, and so does its creator (p.315). Same
5265
+ * lifecycle as sustainedBarrier. */
5260
5266
  _sustainedManaBarrier;
5261
5267
  get sustainedManaBarrier() { return this._sustainedManaBarrier; }
5262
5268
  set sustainedManaBarrier(v) {
@@ -16,7 +16,7 @@ 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
- import { synthesizeGrid, attachExitCell, detachExitCell, hashFnv1a, mulberry32, } from '../utilities/room-grid.js';
19
+ import { synthesizeGrid, attachExitCell, detachExitCell, hashFnv1a, mulberry32, setCellBlocker, } from '../utilities/room-grid.js';
20
20
  /**
21
21
  * A CAMERA'S DEVICE RATING when the scene does not say (p.356, which
22
22
  * lists "Camera" at 1 -- the same table that gives the door lock the 2
@@ -560,6 +560,9 @@ export class Room extends AbstractRoom {
560
560
  levels: this.levels,
561
561
  terrainHints: this.terrainHints,
562
562
  });
563
+ // A BARRIER SPELL IS ASKED, NOT STORED (room-grid.ts CELL_BLOCKERS):
564
+ // its cells come and go while the grid lives, and the scene owns them.
565
+ setCellBlocker(this._grid, c => this.scene?.physicalBarrierAtLocal?.(this, c) ?? false);
563
566
  return this._grid;
564
567
  }
565
568
  /**
@@ -8,6 +8,12 @@ import { Direction } from '../types/shared/direction-enum.js';
8
8
  import { Door } from './door.js';
9
9
  import { dwellingDims } from '../utilities/dwellings.js';
10
10
  import { actorCell } from '../utilities/spots.js';
11
+ import { line3D } from '../utilities/room-grid.js';
12
+ import { faceAt } from '../utilities/barrier-geometry.js';
13
+ /** One step out through a doorway, per exit direction (north is -y). */
14
+ const BARRIER_STEP = {
15
+ north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0],
16
+ };
11
17
  import { packDistrict, worldCell, buildAuthoredPlane, partyLayout, planeFaults } from '../utilities/district-plane.js';
12
18
  import { Category, Size, Rating } from '../types/shared/item-enum.js';
13
19
  import { AbstractScene } from '../types/shared/abstracts.js';
@@ -1222,30 +1228,93 @@ export class Scene extends AbstractScene {
1222
1228
  return this.characterLocations.get(character.name);
1223
1229
  }
1224
1230
  /**
1225
- * A sustained PHYSICAL BARRIER spell (Player.sustainedBarrier)
1226
- * sealing this room's exit in `direction` -- checked from BOTH sides
1227
- * of the doorway (the wall is in the frame, not painted on one face).
1228
- * go.ts refuses traversal for every meat-plane actor while it holds.
1231
+ * EVERY BARRIER SPELL STANDING IN THIS SCENE (barrier-geometry.ts).
1232
+ * They live on the casters -- one of each kind apiece -- so this is a
1233
+ * walk of the players, the same place the old exit seal read them.
1229
1234
  */
1230
- barrierSealing(room, direction, kind = 'physical') {
1235
+ spellBarriers(kind) {
1236
+ const out = [];
1237
+ for (const p of this.getPlayers()) {
1238
+ if (p.sustainedBarrier && (!kind || kind === 'physical'))
1239
+ out.push(p.sustainedBarrier);
1240
+ if (p.sustainedManaBarrier && (!kind || kind === 'mana'))
1241
+ out.push(p.sustainedManaBarrier);
1242
+ }
1243
+ return out;
1244
+ }
1245
+ /**
1246
+ * The standing barrier face on this PLANE cell, if any. `passer` is
1247
+ * who is asking to cross: a mana barrier "does not affect its
1248
+ * creator, who can pass through at will" (p.315), so its own weaver
1249
+ * gets undefined. A physical wall plays no favourites.
1250
+ */
1251
+ barrierAt(c, kind, passer) {
1252
+ for (const barrier of this.spellBarriers(kind)) {
1253
+ if (kind === 'mana' && passer !== undefined && barrier.caster === passer)
1254
+ continue;
1255
+ const face = faceAt(barrier, c);
1256
+ if (face)
1257
+ return { barrier, face };
1258
+ }
1259
+ return undefined;
1260
+ }
1261
+ /**
1262
+ * The first standing barrier face a straight line crosses between two
1263
+ * PLANE cells, endpoints excluded -- what a shot at someone behind a
1264
+ * wall has to go through first (p.197-198).
1265
+ */
1266
+ barrierOnLine(a, b, kind) {
1267
+ if (this.spellBarriers(kind).length === 0)
1268
+ return undefined;
1269
+ for (const c of line3D(a, b).slice(1, -1)) {
1270
+ const hit = this.barrierAt(c, kind);
1271
+ if (hit)
1272
+ return hit;
1273
+ }
1274
+ return undefined;
1275
+ }
1276
+ /**
1277
+ * A ROOM-LOCAL cell under a physical barrier -- what Room.ensureGrid
1278
+ * registers with the grid, so findPath, stepFrom and seating all
1279
+ * route round the wall without knowing spells exist.
1280
+ */
1281
+ physicalBarrierAtLocal(room, local) {
1282
+ if (this.spellBarriers('physical').length === 0)
1283
+ return false;
1284
+ const c = worldCell(this.ensurePlane(), room.name, local);
1285
+ return !!c && !!this.barrierAt(c, 'physical');
1286
+ }
1287
+ /**
1288
+ * IS THIS EXIT WALLED OFF BY A BARRIER SPELL. Since 2026-10-05 a
1289
+ * barrier is cells, not a tag on an exit, so the question is whether a
1290
+ * standing face covers the doorway: the exit's own cell or the gap in
1291
+ * the wall beyond it -- from either side, because the far room's
1292
+ * doorway is the same hole.
1293
+ */
1294
+ barrierSealing(room, direction, kind = 'physical', passer) {
1295
+ if (this.spellBarriers(kind).length === 0)
1296
+ return undefined;
1297
+ const plane = this.ensurePlane();
1298
+ const cellsOf = (r, dir) => {
1299
+ const local = r.ensureGrid()?.exitCells.get(dir.toLowerCase());
1300
+ const at = local && worldCell(plane, r.name, local);
1301
+ if (!at)
1302
+ return [];
1303
+ const step = BARRIER_STEP[dir.toLowerCase()];
1304
+ return step ? [at, { x: at.x + step[0], y: at.y + step[1], z: at.z }] : [at];
1305
+ };
1231
1306
  const exit = room.exits.get(direction);
1232
1307
  // getOtherSide (the Room), not getOtherSideToName (a "To X" display
1233
1308
  // string -- a probe caught the far-side check silently never
1234
1309
  // matching on it).
1235
- const destName = !exit ? undefined
1236
- : typeof exit === 'string' ? exit
1237
- : exit.getOtherSide?.(room)?.name;
1310
+ const far = !exit || typeof exit === 'string' ? undefined
1311
+ : exit.getOtherSide?.(room);
1238
1312
  const opposite = room.getOppositeDirection(direction);
1239
- for (const p of this.getPlayers()) {
1240
- const b = kind === 'physical' ? p.sustainedBarrier : p.sustainedManaBarrier;
1241
- if (!b)
1242
- continue;
1243
- if (b.roomName === room.name && b.direction.toLowerCase() === direction.toLowerCase()) {
1244
- return { caster: p.name, force: b.force };
1245
- }
1246
- if (destName && b.roomName === destName && opposite && b.direction.toLowerCase() === String(opposite).toLowerCase()) {
1247
- return { caster: p.name, force: b.force };
1248
- }
1313
+ const cells = [...cellsOf(room, direction), ...(far && opposite ? cellsOf(far, String(opposite)) : [])];
1314
+ for (const c of cells) {
1315
+ const hit = this.barrierAt(c, kind, passer);
1316
+ if (hit)
1317
+ return { caster: hit.barrier.caster, force: hit.barrier.force };
1249
1318
  }
1250
1319
  return undefined;
1251
1320
  }
@@ -75,7 +75,7 @@ export function driftAstral(scene, actor, rooms, direction, count, untilOut = fa
75
75
  // the room it is trying to leave is large.
76
76
  const limit = untilOut ? 50 : count;
77
77
  while (taken < limit) {
78
- const step = astralStepFrom(plane, cell, direction, 1, byKey);
78
+ const step = astralStepFrom(plane, cell, direction, 1, byKey, c => scene.barrierAt?.(c, 'mana', actor.name)?.barrier.caster);
79
79
  if (step.taken === 0) {
80
80
  blockedBy = step.blockedBy;
81
81
  ward = step.ward;
@@ -193,6 +193,9 @@ export function driftRefusal(d, direction) {
193
193
  * nearest outdoor room and simply goes. The only things that stop a
194
194
  * form are the two canon gives: solid earth and a ward.
195
195
  */
196
+ if (d.blockedBy === 'barrier') {
197
+ 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
+ }
196
199
  if (d.blockedBy === 'ward') {
197
200
  return `A lattice of woven mana stands across the way ${direction}${d.ward ? ` -- ${d.ward}'s` : ''}. The astral does not pass.`;
198
201
  }