@maka/maka-cli 5.383.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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +89 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +206 -41
- package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +15 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +24 -9
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +8 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +22 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +7 -3
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +8 -6
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +87 -18
- package/bundle/typescript/src/commands/game/sideQuest/utilities/astral-move.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +102 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-geometry.js +253 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +32 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-glyphs.js +15 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-rulers.js +58 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +30 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +22 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +51 -11
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +24 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ways-out.js +34 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
180
|
-
line: 'sustained: hardened air
|
|
181
|
-
{ key: 'manabarrier', cast: 'manabarrier', how: 'cast manabarrier <direction>',
|
|
182
|
-
line: 'sustained: woven mana
|
|
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))
|
|
@@ -3334,5 +3334,26 @@
|
|
|
3334
3334
|
// (nfrBW27eRRxstJ8uC).
|
|
3335
3335
|
// - The HUD's holstered line drops its `-- "brandish"` tip
|
|
3336
3336
|
// (MkxamPxmsSyonR3wC).
|
|
3337
|
-
|
|
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';
|
|
3338
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';
|
|
@@ -9477,10 +9478,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
9477
9478
|
// the one that strands the team -- and both of them seal an exit
|
|
9478
9479
|
// that the caster cannot pass either.
|
|
9479
9480
|
if (player.sustainedBarrier) {
|
|
9480
|
-
heldSpells.push(`physical barrier (${player.sustainedBarrier.
|
|
9481
|
+
heldSpells.push(`physical barrier (${player.sustainedBarrier.shape} ${barrierSpan(player.sustainedBarrier)}, ${player.sustainedBarrier.roomName})`);
|
|
9481
9482
|
}
|
|
9482
9483
|
if (player.sustainedManaBarrier) {
|
|
9483
|
-
heldSpells.push(`mana barrier (${player.sustainedManaBarrier.
|
|
9484
|
+
heldSpells.push(`mana barrier (${player.sustainedManaBarrier.shape} ${barrierSpan(player.sustainedManaBarrier)}, ${player.sustainedManaBarrier.roomName})`);
|
|
9484
9485
|
}
|
|
9485
9486
|
// What this character holds on OTHERS -- the caster's burden, named
|
|
9486
9487
|
// by bearer so the mage knows whose armor is costing them two dice.
|
|
@@ -9721,6 +9722,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
9721
9722
|
// Whose eyes, and what counts as hostile -- so the district draws
|
|
9722
9723
|
// the same people, with the same marks, the room panel does.
|
|
9723
9724
|
viewer: this.player,
|
|
9725
|
+
rulers: true,
|
|
9724
9726
|
isOtherPlayer: a => this.scene.isHumanControlled(a),
|
|
9725
9727
|
isTableAlly: crewOfTable(this.scene.getPlayers()),
|
|
9726
9728
|
isHostile: a => hostileByStance(a) || enemyInFight(this.scene, this.player, a),
|
|
@@ -9753,6 +9755,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
9753
9755
|
const roomLines = renderRoomLayoutCompact(here, this.player, {
|
|
9754
9756
|
isTableAlly: crewOfTable(this.scene.getPlayers()),
|
|
9755
9757
|
boxWidth,
|
|
9758
|
+
// Rulers in PLANE coordinates -- the numbers "cast barrier x,y" takes.
|
|
9759
|
+
origin: here.planeFrame?.()?.origin,
|
|
9756
9760
|
isOtherPlayer: a => this.scene.isHumanControlled(a),
|
|
9757
9761
|
// Red is the stance flag OR whoever the Combat Turn says is
|
|
9758
9762
|
// fighting you (room-view.ts hostileByStance).
|
|
@@ -9767,7 +9771,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
9767
9771
|
// of its own. The GLYPHS moved to `map legend`, where there is
|
|
9768
9772
|
// room to say what they mean instead of abbreviating them into
|
|
9769
9773
|
// a strip nobody could read.
|
|
9770
|
-
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));
|
|
9771
9775
|
roomLines.push(caption(roomCaption(here, this.player), boxWidth));
|
|
9772
9776
|
}
|
|
9773
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
|
}
|
|
@@ -5244,10 +5244,11 @@ export class Player extends AbstractPlayer {
|
|
|
5244
5244
|
}
|
|
5245
5245
|
}
|
|
5246
5246
|
}
|
|
5247
|
-
/** The sustained PHYSICAL BARRIER spell
|
|
5248
|
-
*
|
|
5249
|
-
*
|
|
5250
|
-
* Session-only, like every
|
|
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. */
|
|
5251
5252
|
_sustainedBarrier;
|
|
5252
5253
|
get sustainedBarrier() { return this._sustainedBarrier; }
|
|
5253
5254
|
set sustainedBarrier(v) {
|
|
@@ -5259,8 +5260,9 @@ export class Player extends AbstractPlayer {
|
|
|
5259
5260
|
this.noteConditionChanged();
|
|
5260
5261
|
}
|
|
5261
5262
|
/** The MANA BARRIER twin (canon p.294, RAG-checked): blocks SPIRITS
|
|
5262
|
-
* and astral forms at
|
|
5263
|
-
* objects pass freely
|
|
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. */
|
|
5264
5266
|
_sustainedManaBarrier;
|
|
5265
5267
|
get sustainedManaBarrier() { return this._sustainedManaBarrier; }
|
|
5266
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
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* of the
|
|
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
|
-
|
|
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
|
|
1236
|
-
:
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stillWatching } from './surveillance.js';
|
|
2
2
|
import { rollPool, formatRoll } from './dice.js';
|
|
3
3
|
import { applyDeviceOpened } from './devices.js';
|
|
4
|
+
import { FACE_LABEL, allBreached, damageFace, penetrationBoxes, pierces } from './barrier-geometry.js';
|
|
4
5
|
export function resolveBarrierStrike(scene, logger, attacker, device, room, opts = {}) {
|
|
5
6
|
const world = [];
|
|
6
7
|
const meta = [];
|
|
@@ -148,4 +149,105 @@ export function resolveCameraStrike(scene, logger, attacker, camera, room, opts
|
|
|
148
149
|
return result;
|
|
149
150
|
}
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* HITTING A BARRIER SPELL (SR5 p.197-198 with p.294 / p.316; user
|
|
154
|
+
* rulings 2026-10-05). Same unopposed shape as a door, against ONE FACE:
|
|
155
|
+
*
|
|
156
|
+
* - The attack test is unopposed; its hits add to the DV.
|
|
157
|
+
* - p.197's text, not p.198's example: a DV that does not EXCEED the
|
|
158
|
+
* AP-modified Armor does nothing at all.
|
|
159
|
+
* - A gun is a PENETRATION weapon (p.198): the barrier takes 1 box,
|
|
160
|
+
* unresisted, and the rest of the shot is spent on the far side.
|
|
161
|
+
* It takes a lot of bullets to bring down a wall, which is the point.
|
|
162
|
+
* - Anything else resists with Structure + Armor; what gets through
|
|
163
|
+
* fills the face's track. Barriers ignore Stun.
|
|
164
|
+
* - A face whose track reaches Structure is breached: it stops
|
|
165
|
+
* blocking. Every face breached and the spell is over (p.294).
|
|
166
|
+
* - A mana barrier's caster knows at once it is under attack (p.316).
|
|
167
|
+
*
|
|
168
|
+
* The caller has already chosen the cell (and so the face) and checked
|
|
169
|
+
* the plane: a Physical Barrier is hit from the meat, a Mana Barrier
|
|
170
|
+
* only from the astral -- bullets go through a weave as if it were not
|
|
171
|
+
* there, because to them it is not.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveSpellBarrierStrike(scene, logger, attacker, barrier, face, room, opts = {}) {
|
|
174
|
+
const world = [];
|
|
175
|
+
const meta = [];
|
|
176
|
+
const done = (opened = false, collapsed = false) => ({ world, meta, opened, collapsed });
|
|
177
|
+
const what = `${barrier.caster}'s ${barrier.kind === 'mana' ? 'mana barrier' : 'barrier'} (${FACE_LABEL[face.id]})`;
|
|
178
|
+
const weaponItem = attacker.weaponDrawn ? attacker.getCarriedWeapon() : null;
|
|
179
|
+
const profile = attacker.getWeaponProfile();
|
|
180
|
+
if (weaponItem?.isFirearm() && (weaponItem.ammo <= 0 || weaponItem.jammed)) {
|
|
181
|
+
world.push(`${weaponItem.name} is ${weaponItem.jammed ? 'locked up' : 'dry'} -- nothing to put into ${what}.`);
|
|
182
|
+
return done();
|
|
183
|
+
}
|
|
184
|
+
const stun = weaponItem ? weaponItem.isStunWeapon() : !(attacker.hasAug('bone-lacing') || attacker.adept || attacker.hasKillingHands());
|
|
185
|
+
if (stun && attacker.plane === 'meat') {
|
|
186
|
+
world.push(`${weaponItem ? weaponItem.name : 'Your fists'} deal${weaponItem ? 's' : ''} Stun, and ${what} has nothing to stun -- barriers ignore it (p.198).`);
|
|
187
|
+
return done();
|
|
188
|
+
}
|
|
189
|
+
const firearm = attacker.plane === 'meat' && !!weaponItem?.isFirearm();
|
|
190
|
+
if (firearm)
|
|
191
|
+
weaponItem.fireRound();
|
|
192
|
+
attacker.sneaking = false;
|
|
193
|
+
const roll = rollPool(attacker.getAttackPool(), attacker.getAttackLimit(weaponItem));
|
|
194
|
+
meta.push(`Attack on ${what} (unopposed, p.197): ${formatRoll(roll)}`);
|
|
195
|
+
if (roll.criticalGlitch) {
|
|
196
|
+
world.push(`${attacker.name} goes for ${what} -- and it goes catastrophically wrong.`);
|
|
197
|
+
return noise(done());
|
|
198
|
+
}
|
|
199
|
+
const dv = profile.damage + roll.hits;
|
|
200
|
+
const ap = profile.armorPen ?? 0;
|
|
201
|
+
const modifiedArmor = Math.max(0, barrier.armor + ap);
|
|
202
|
+
const how = profile.name === 'bare fists' ? 'with bare fists' : `with ${profile.name}`;
|
|
203
|
+
meta.push(` DV ${profile.damage} + ${roll.hits} hit${roll.hits === 1 ? '' : 's'} = ${dv} v. Armor ${barrier.armor}${ap !== 0 ? ` (AP ${ap} -> ${modifiedArmor})` : ''}`);
|
|
204
|
+
if (!pierces(dv, barrier.armor, ap)) {
|
|
205
|
+
world.push(`${attacker.name} hits ${what} ${how} -- the shimmer swallows it. Too weak to break through (DV ${dv} against Armor ${modifiedArmor}, p.197).`);
|
|
206
|
+
return noise(done());
|
|
207
|
+
}
|
|
208
|
+
let dealt;
|
|
209
|
+
if (firearm) {
|
|
210
|
+
dealt = penetrationBoxes(1);
|
|
211
|
+
meta.push(` Penetration weapon (p.198): ${dealt} box to the barrier, unresisted -- the rest of the round is spent beyond it`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const soak = rollPool(barrier.structure + modifiedArmor);
|
|
215
|
+
dealt = Math.max(0, dv - soak.hits);
|
|
216
|
+
meta.push(` Resist: Structure ${barrier.structure} + Armor ${modifiedArmor}: ${formatRoll(soak)} -- ${dealt} box${dealt === 1 ? '' : 'es'} through`);
|
|
217
|
+
}
|
|
218
|
+
const breached = damageFace(barrier, face, dealt);
|
|
219
|
+
logger.write(`${attacker.name} dealt ${dealt} boxes to ${what} (${face.damage}/${barrier.structure}${face.breached ? ', breached' : ''}).`);
|
|
220
|
+
if (barrier.kind === 'mana') {
|
|
221
|
+
const caster = scene.getPlayers().find(p => p.name === barrier.caster);
|
|
222
|
+
if (caster && caster !== attacker)
|
|
223
|
+
logger.log(`Something is hammering your mana barrier in ${barrier.roomName} -- ${FACE_LABEL[face.id]}, ${face.breached ? 'breached' : `${face.damage}/${barrier.structure}`} (p.316).`, { actor: caster.name });
|
|
224
|
+
}
|
|
225
|
+
if (!breached) {
|
|
226
|
+
world.push(`${attacker.name} hits ${what} ${how} -- ${dealt > 0 ? `${dealt} box${dealt === 1 ? '' : 'es'}` : 'it holds, unmarked'}. (${face.damage}/${barrier.structure})`);
|
|
227
|
+
return noise(done());
|
|
228
|
+
}
|
|
229
|
+
world.push(`${attacker.name} breaks ${what} ${how} -- that face ${barrier.kind === 'mana' ? 'frays to nothing' : 'shatters like heat-haze'}, and the way through it is open.`);
|
|
230
|
+
if (allBreached(barrier)) {
|
|
231
|
+
const caster = scene.getPlayers().find(p => p.name === barrier.caster);
|
|
232
|
+
if (caster) {
|
|
233
|
+
if (barrier.kind === 'mana')
|
|
234
|
+
caster.sustainedManaBarrier = undefined;
|
|
235
|
+
else
|
|
236
|
+
caster.sustainedBarrier = undefined;
|
|
237
|
+
}
|
|
238
|
+
world.push(` The whole ${barrier.kind === 'mana' ? 'weave' : 'barrier'} collapses -- ${barrier.caster}'s spell is over (p.294).`);
|
|
239
|
+
scene.addWorldEvent(`${attacker.name} brought down ${barrier.caster}'s ${barrier.kind === 'mana' ? 'mana barrier' : 'force barrier'} in ${room.name}.`);
|
|
240
|
+
scene.updateStatus();
|
|
241
|
+
return noise(done(true, true));
|
|
242
|
+
}
|
|
243
|
+
scene.updateStatus();
|
|
244
|
+
return noise(done(true));
|
|
245
|
+
function noise(result) {
|
|
246
|
+
if (firearm) {
|
|
247
|
+
scene.addWorldEvent(`${attacker.name} opened fire on a force barrier in ${room.name} -- the shots carried.`);
|
|
248
|
+
opts.heat?.(1, 'shot at a force barrier');
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
151
253
|
//# sourceMappingURL=barrier-combat.js.map
|