@maka/maka-cli 5.383.0 → 5.385.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 +276 -43
- 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 +31 -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 +277 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.385.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.",
|
|
@@ -6,9 +6,11 @@ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
|
6
6
|
import { describeFledTarget } from '../utilities/pursuit.js';
|
|
7
7
|
import { shellsInRoom, resolveBodyAttack, crossPlaneRefusal } from '../utilities/planes.js';
|
|
8
8
|
import { heldBy, holding } from '../utilities/grapple.js';
|
|
9
|
-
import { spotOf, ensureAtSpot, ensureAtExit, canReach, spotDistanceMeters, OPEN_FLOOR, inMeleeReach, actorCell, standingRoomNear, spotForCell } from '../utilities/spots.js';
|
|
9
|
+
import { spotOf, ensureAtSpot, ensureAtExit, canReach, spotDistanceMeters, OPEN_FLOOR, inMeleeReach, actorCell, standingRoomNear, spotForCell, ADJACENT_METERS } from '../utilities/spots.js';
|
|
10
10
|
import { distanceMeters } from '../utilities/room-grid.js';
|
|
11
|
-
import {
|
|
11
|
+
import { wallsBetween } from '../utilities/district-plane.js';
|
|
12
|
+
import { formatCoord, parseCoord } from '../utilities/barrier-geometry.js';
|
|
13
|
+
import { resolveBarrierStrike, resolveCameraStrike, resolveSpellBarrierStrike } from '../utilities/barrier-combat.js';
|
|
12
14
|
import { isWatched, cameraWithinReach, cameraOutOfReach } from '../utilities/surveillance.js';
|
|
13
15
|
import { matchesCameraName } from '../utilities/grid-names.js';
|
|
14
16
|
import { billAction, notYourPhase, phaseHint } from '../utilities/action-cost.js';
|
|
@@ -48,7 +50,7 @@ export class AttackCommand extends Command {
|
|
|
48
50
|
async readyForStrike() {
|
|
49
51
|
return {};
|
|
50
52
|
}
|
|
51
|
-
static description = 'Attack someone in the room -- dice-pool combat with your equipped weapon. They WILL hit back. "attack <door>" shoots a barrier apart instead (unopposed; its Armor and Structure decide, p.197).';
|
|
53
|
+
static description = 'Attack someone in the room -- dice-pool combat with your equipped weapon. They WILL hit back. "attack <door>" shoots a barrier apart instead (unopposed; its Armor and Structure decide, p.197). "attack barrier [x,y]" breaks a face of a barrier spell.';
|
|
52
54
|
async execute(args = []) {
|
|
53
55
|
if (!args || args.length === 0) {
|
|
54
56
|
return 'Who do you want to attack? Try: `attack <name>`';
|
|
@@ -159,6 +161,36 @@ export class AttackCommand extends Command {
|
|
|
159
161
|
return this.scene.isHumanControlled(this.actor) ? '' : lines.join('\n');
|
|
160
162
|
}
|
|
161
163
|
}
|
|
164
|
+
// A BARRIER SPELL (2026-10-05; p.197-198, p.294, p.316): "attack
|
|
165
|
+
// barrier" hits the nearest standing face you can see, "attack
|
|
166
|
+
// barrier 3,5" the face holding that cell. A Physical Barrier is
|
|
167
|
+
// matter to the meat; a Mana Barrier is only there for the astral.
|
|
168
|
+
const spellWall = this.spellBarrierTarget(targetName);
|
|
169
|
+
if (spellWall) {
|
|
170
|
+
if ('refusal' in spellWall)
|
|
171
|
+
return spellWall.refusal;
|
|
172
|
+
const held = this.actor.getCarriedWeapon();
|
|
173
|
+
if (this.scene.encounterFor?.(this.actor)) {
|
|
174
|
+
if (held && !this.actor.weaponDrawn) {
|
|
175
|
+
return `The ${held.name} is holstered -- "draw" it first (Ready Weapon, a Simple Action, SR5 p.165).`;
|
|
176
|
+
}
|
|
177
|
+
const bill = billAction(this.scene, this.actor, held?.isFirearm() ? 'simple' : 'complex', 'Attack Barrier', { attack: true });
|
|
178
|
+
if (bill)
|
|
179
|
+
return bill;
|
|
180
|
+
}
|
|
181
|
+
const drawn = this.scene.encounterFor?.(this.actor) ? null : this.actor.drawWeapon();
|
|
182
|
+
const strike = resolveSpellBarrierStrike(this.scene, this.logger, this.actor, spellWall.barrier, spellWall.face, room, {
|
|
183
|
+
heat: (amount, why) => this.game?.addHeat(amount, why),
|
|
184
|
+
});
|
|
185
|
+
const lines = [...(drawn ? [`${this.actor.name} draws ${drawn.name}.`] : []), ...strike.world];
|
|
186
|
+
this.actor.performAction('attacks', `${spellWall.barrier.caster}'s barrier`);
|
|
187
|
+
this.scene.updateStatus();
|
|
188
|
+
if (strike.opened)
|
|
189
|
+
this.scene.updateExits?.(room);
|
|
190
|
+
const exchange = new CombatExchange(this.scene, this.logger, this.actor);
|
|
191
|
+
await exchange.announce(lines, strike.meta);
|
|
192
|
+
return this.scene.isHumanControlled(this.actor) ? '' : lines.join('\n');
|
|
193
|
+
}
|
|
162
194
|
// A BARRIER (2026-09-03; SR5 p.197-198): "attack <door>" shoots it
|
|
163
195
|
// apart -- unopposed, AP against its Armor, Structure as its track.
|
|
164
196
|
// Named the way pick/breach name it: the device's name, or the exit
|
|
@@ -569,5 +601,59 @@ export class AttackCommand extends Command {
|
|
|
569
601
|
}
|
|
570
602
|
return null;
|
|
571
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* "barrier", "the force wall", "mana barrier 3,5" -> the standing face
|
|
606
|
+
* this actor would hit, or a refusal that says why not, or undefined
|
|
607
|
+
* when the words are not about a barrier spell at all (the door route
|
|
608
|
+
* and the "who?" answer come next).
|
|
609
|
+
*/
|
|
610
|
+
spellBarrierTarget(words) {
|
|
611
|
+
const m = /^(?:the\s+)?(?:(mana|force|physical)\s+)?(?:barrier|wall)(?:\s+(?:at\s+)?(-?\d+,-?\d+(?:,-?\d+)?))?$/i.exec(words.trim());
|
|
612
|
+
if (!m)
|
|
613
|
+
return undefined;
|
|
614
|
+
const kind = this.actor.plane === 'astral' ? 'mana' : 'physical';
|
|
615
|
+
const all = this.scene.spellBarriers?.(kind) ?? [];
|
|
616
|
+
if (all.length === 0) {
|
|
617
|
+
// A door called "the wall" is still the door route's to answer.
|
|
618
|
+
if (m[1]?.toLowerCase() === 'mana' && kind === 'physical' && (this.scene.spellBarriers?.('mana').length ?? 0) > 0) {
|
|
619
|
+
return { refusal: `A mana barrier is woven mana -- your ${this.actor.getCarriedWeapon()?.name ?? 'fists'} would pass through it as if it were not there. Only the astral can touch it.` };
|
|
620
|
+
}
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
const plane = this.scene.ensurePlane?.();
|
|
624
|
+
const from = this.scene.worldCellOf?.(this.actor);
|
|
625
|
+
if (!plane || !from)
|
|
626
|
+
return { refusal: `You can't place yourself well enough to aim at a barrier from here.` };
|
|
627
|
+
const melee = !this.actor.getCarriedWeapon()?.isFirearm();
|
|
628
|
+
const reachable = (c) => wallsBetween(plane, from, c) === 0 && (!melee || distanceMeters(from, c) <= ADJACENT_METERS);
|
|
629
|
+
if (m[2]) {
|
|
630
|
+
const at = parseCoord(m[2], from.z);
|
|
631
|
+
const hit = this.scene.barrierAt(at, kind);
|
|
632
|
+
if (!hit)
|
|
633
|
+
return { refusal: `There's no standing barrier at ${formatCoord(at)}.` };
|
|
634
|
+
if (!reachable(at)) {
|
|
635
|
+
return { refusal: melee ? `${formatCoord(at)} is out of arm's reach -- close in, or bring a gun.` : `You can't see ${formatCoord(at)} from here.` };
|
|
636
|
+
}
|
|
637
|
+
return hit;
|
|
638
|
+
}
|
|
639
|
+
let best;
|
|
640
|
+
for (const barrier of all) {
|
|
641
|
+
for (const face of barrier.faces) {
|
|
642
|
+
if (face.breached)
|
|
643
|
+
continue;
|
|
644
|
+
for (const c of face.cells) {
|
|
645
|
+
if (!reachable(c))
|
|
646
|
+
continue;
|
|
647
|
+
const d = distanceMeters(from, c);
|
|
648
|
+
if (!best || d < best.d)
|
|
649
|
+
best = { barrier, face, d };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (!best) {
|
|
654
|
+
return { refusal: melee ? `No barrier within arm's reach -- close in, or bring a gun.` : `No barrier face in sight from here.` };
|
|
655
|
+
}
|
|
656
|
+
return { barrier: best.barrier, face: best.face };
|
|
657
|
+
}
|
|
572
658
|
}
|
|
573
659
|
//# sourceMappingURL=attack.js.map
|
|
@@ -6,7 +6,10 @@ import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
|
6
6
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
7
7
|
import { describeFledTarget } from '../utilities/pursuit.js';
|
|
8
8
|
import { crossPlaneRefusal } from '../utilities/planes.js';
|
|
9
|
-
import { spotOf, ensureAtSpot } from '../utilities/spots.js';
|
|
9
|
+
import { spotOf, ensureAtSpot, furnitureCells, standingOccupants } from '../utilities/spots.js';
|
|
10
|
+
import { key as gridKey, METERS_PER_CELL } from '../utilities/room-grid.js';
|
|
11
|
+
import { isSolid, isVoid, roomAtCell, wallsBetween } from '../utilities/district-plane.js';
|
|
12
|
+
import { DIRECTIONAL_WALL_FORCE, DOME_FORCE, forceForWall, buildBarrier, clipFaces, formatCoord, isCoordToken, parseCoord, planDirectionalWall, planDome, planWall } from '../utilities/barrier-geometry.js';
|
|
10
13
|
import { NPC } from '../models/npc.js';
|
|
11
14
|
import { spellFor, catalogSpells } from '../utilities/catalog.js';
|
|
12
15
|
import { getAspect } from '../aspects.js';
|
|
@@ -93,7 +96,7 @@ export class CastCommand extends BypassCommand {
|
|
|
93
96
|
/** Touches another actor: authorized per-act for AI-driven actors
|
|
94
97
|
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
95
98
|
static npcPolicy = 'authorized';
|
|
96
|
-
static description = 'Cast a spell you know, at a FORCE you choose: "cast manabolt 8 at <target>" (default Force = your Magic; drain scales with it, and OVERCASTING past your talent makes the drain PHYSICAL). "spells" lists what you know.
|
|
99
|
+
static description = 'Cast a spell you know, at a FORCE you choose: "cast manabolt 8 at <target>" (default Force = your Magic; drain scales with it, and OVERCASTING past your talent makes the drain PHYSICAL). "spells" lists what you know. Barriers take map coordinates: "cast barrier 3 3,4 3,6" (a wall), "cast barrier 3 5,5" (a dome), "cast barrier east 2". A ward is "dispel <direction>".';
|
|
97
100
|
deviceKind = 'ward';
|
|
98
101
|
verbName = 'cast';
|
|
99
102
|
noDeviceMessage = "There's nothing here to cast at.";
|
|
@@ -205,16 +208,11 @@ export class CastCommand extends BypassCommand {
|
|
|
205
208
|
return this.castSpell(spellName, words.slice(1));
|
|
206
209
|
}
|
|
207
210
|
// An AMBIGUOUS fragment ("cast stun ...", now that stunbolt and
|
|
208
|
-
// stunball both exist)
|
|
209
|
-
// path and answer with something about barriers. Name the choices
|
|
210
|
-
// instead -- but only for someone who can actually cast, so "cast
|
|
211
|
-
// north" still routes to the bypass.
|
|
211
|
+
// stunball both exist) names the choices rather than guessing.
|
|
212
212
|
const near = this.spellsMatching(words?.[0]);
|
|
213
213
|
if (near.length > 1 && this.actor.isCaster()) {
|
|
214
214
|
return `Which one? ${near.join(', ')}.`;
|
|
215
215
|
}
|
|
216
|
-
// Ward-bypass path (BypassCommand). Now that casting is a real, gated
|
|
217
|
-
// ability, throwing magic at a ward requires the talent or the gear.
|
|
218
216
|
// ASPECTED TO ANOTHER GROUP (SR5 p.69): no Sorcery at all -- not even
|
|
219
217
|
// through a formula or focus, which stand in for the SKILL, not the gift.
|
|
220
218
|
if (this.actor.isAspected && !this.actor.canWorkSorcery()) {
|
|
@@ -223,7 +221,18 @@ export class CastCommand extends BypassCommand {
|
|
|
223
221
|
if (!this.actor.isCaster()) {
|
|
224
222
|
return `Casting takes talent you don't have -- you'd need to be Awakened, or have a spell formula, grimoire, or focus on you.`;
|
|
225
223
|
}
|
|
226
|
-
|
|
224
|
+
// "CAST <DIRECTION>" IS GONE (user ruling 2026-10-05): it used to
|
|
225
|
+
// unweave a ward on that exit, which is DISPEL's job and now only
|
|
226
|
+
// dispel's. A direction gets pointed at both verbs it might mean;
|
|
227
|
+
// anything else is not a spell this caster knows.
|
|
228
|
+
const first = words[0]?.toLowerCase();
|
|
229
|
+
if (!first) {
|
|
230
|
+
return `Cast what? "spells" lists what you know -- "cast <spell> [force] ...".`;
|
|
231
|
+
}
|
|
232
|
+
if (this.isValidDirection(first)) {
|
|
233
|
+
return `To unweave a ward that way, "dispel ${first}". To wall the way off, "cast barrier ${first} [cells]".`;
|
|
234
|
+
}
|
|
235
|
+
return `"${words.join(' ')}" isn't a spell you know. "spells" lists them; a ward is "dispel <it>".`;
|
|
227
236
|
}
|
|
228
237
|
/**
|
|
229
238
|
* ONE REFUSAL FOR THE ASPECT, wherever it is reached from -- the
|
|
@@ -298,6 +307,7 @@ export class CastCommand extends BypassCommand {
|
|
|
298
307
|
const spellDef = spellFor(spellName);
|
|
299
308
|
const safeForce = this.actor.magic > 0 ? this.actor.magic : 4;
|
|
300
309
|
let force = Math.max(1, -(spellDef?.drainOffset ?? -3));
|
|
310
|
+
let forceGiven = true;
|
|
301
311
|
if (rest[0]?.toLowerCase() === 'force' && /^\d+$/.test(rest[1] ?? '')) {
|
|
302
312
|
force = parseInt(rest[1], 10);
|
|
303
313
|
rest = rest.slice(2);
|
|
@@ -306,6 +316,9 @@ export class CastCommand extends BypassCommand {
|
|
|
306
316
|
force = parseInt(rest[0], 10);
|
|
307
317
|
rest = rest.slice(1);
|
|
308
318
|
}
|
|
319
|
+
else {
|
|
320
|
+
forceGiven = false;
|
|
321
|
+
}
|
|
309
322
|
if (force < 1) {
|
|
310
323
|
return `Force below 1 isn't a spell, it's a wish.`;
|
|
311
324
|
}
|
|
@@ -326,7 +339,7 @@ export class CastCommand extends BypassCommand {
|
|
|
326
339
|
case 'heal': return this.castHeal(force, overcast, rest);
|
|
327
340
|
case 'armor': return this.castArmor(force, overcast, rest);
|
|
328
341
|
case 'sustained-utility': return this.castSustainedUtility(spellName, force, overcast, rest);
|
|
329
|
-
case 'barrier': return this.castBarrier(spellName, force, overcast, rest);
|
|
342
|
+
case 'barrier': return this.castBarrier(spellName, force, overcast, rest, forceGiven ? undefined : safeForce);
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
/**
|
|
@@ -964,16 +977,34 @@ export class CastCommand extends BypassCommand {
|
|
|
964
977
|
return this.finishSpell(lines);
|
|
965
978
|
}
|
|
966
979
|
/**
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
* barrier
|
|
972
|
-
*
|
|
980
|
+
* THE BARRIER SPELLS, CAST AT THE DISTRICT PLANE (SR5 p.294; user
|
|
981
|
+
* spec 2026-10-05). Coordinates are plane cells -- the numbers on the
|
|
982
|
+
* map rulers -- and the shape follows from how many points you give:
|
|
983
|
+
*
|
|
984
|
+
* cast barrier [F] <x,y[,z]> <x,y[,z]> a wall between two points
|
|
985
|
+
* cast barrier [F] <x,y[,z]> a dome around one point
|
|
986
|
+
* cast barrier [F] <direction> [n] a 1x3 wall n cells that way
|
|
987
|
+
* cast barrier [F] at <someone> [size] a dome around them ("me"/"self": you);
|
|
988
|
+
* size = radius in cells = Force / 2
|
|
989
|
+
* cast barrier [F] a dome around yourself
|
|
990
|
+
* cast barrier off let it go (free)
|
|
991
|
+
*
|
|
992
|
+
* A wall is Force x 2 m long and Force m high; a dome is Force m in
|
|
993
|
+
* radius (barrier-geometry.ts). A Physical Barrier's Armor and
|
|
994
|
+
* Structure are its HITS; a Mana Barrier's are its Force (p.316). One
|
|
995
|
+
* of each per caster; casting again replaces the standing one, with a
|
|
996
|
+
* fresh roll and fresh drain. -2 sustaining like every held weave.
|
|
997
|
+
*
|
|
998
|
+
* Bare "cast north" no longer reaches a ward from here -- that is
|
|
999
|
+
* "dispel north" (user ruling 2026-10-05).
|
|
973
1000
|
*/
|
|
974
|
-
async castBarrier(spellKey, force, overcast, rest
|
|
1001
|
+
async castBarrier(spellKey, force, overcast, rest,
|
|
1002
|
+
/** Set when no Force was named: the talent a sized cast may reach. */
|
|
1003
|
+
autoUpTo) {
|
|
975
1004
|
const actor = this.actor;
|
|
976
1005
|
const mana = spellKey === 'manabarrier';
|
|
1006
|
+
const kind = mana ? 'mana' : 'physical';
|
|
1007
|
+
const label = mana ? 'mana barrier' : 'barrier';
|
|
977
1008
|
if (actor.plane !== 'meat' && actor.plane !== 'astral') {
|
|
978
1009
|
return `Barriers are woven where the world is -- not from the grid.`;
|
|
979
1010
|
}
|
|
@@ -981,54 +1012,256 @@ export class CastCommand extends BypassCommand {
|
|
|
981
1012
|
return `A physical barrier needs physical hands -- come back to the meat.`;
|
|
982
1013
|
}
|
|
983
1014
|
const standing = mana ? actor.sustainedManaBarrier : actor.sustainedBarrier;
|
|
984
|
-
|
|
1015
|
+
const words = rest.map(w => w.trim()).filter(w => w.length > 0);
|
|
1016
|
+
if (words.length === 1 && /^(off|release|drop|end)$/i.test(words[0])) {
|
|
1017
|
+
if (!standing)
|
|
1018
|
+
return `You aren't holding a ${label}.`;
|
|
985
1019
|
if (mana)
|
|
986
1020
|
actor.sustainedManaBarrier = undefined;
|
|
987
1021
|
else
|
|
988
1022
|
actor.sustainedBarrier = undefined;
|
|
989
1023
|
this.scene.updateStatus();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
// Fall through: a direction was named -- re-seat the wall there.
|
|
1024
|
+
this.scene.updateExits?.(actor.currentLocation);
|
|
1025
|
+
actor.performAction(`releases ${label} spell`, `in ${standing.roomName}`);
|
|
1026
|
+
return this.finishSpell([`You let the ${label} go -- the ${mana ? 'weave unravels' : 'air unhardens'}, and the sustaining burden lifts.`]);
|
|
995
1027
|
}
|
|
996
1028
|
const room = actor.currentLocation;
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1029
|
+
const plane = this.scene.ensurePlane?.();
|
|
1030
|
+
const from = this.scene.worldCellOf?.(actor);
|
|
1031
|
+
if (!plane || !from) {
|
|
1032
|
+
return `There's no ground here to raise a ${label} on.`;
|
|
1000
1033
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1034
|
+
// WHAT SHAPE, AND WHERE.
|
|
1035
|
+
let plan;
|
|
1036
|
+
let around;
|
|
1037
|
+
let center;
|
|
1038
|
+
const usage = `"cast ${spellKey} [force] <x,y> <x,y>" (a wall), "cast ${spellKey} [force] <x,y>" (a dome), "cast ${spellKey} [force] <direction> [cells]", or "cast ${spellKey} at <someone|me> [radius in cells]". Leave the Force off and it is sized for you. The map rulers give x and y; you're at ${formatCoord(from)}.`;
|
|
1039
|
+
const unseen = (what) => `You can't see ${what} from here -- a barrier is cast at what you can see (LOS, p.294).`;
|
|
1040
|
+
if (words.length === 0) {
|
|
1041
|
+
center = from;
|
|
1042
|
+
around = 'you';
|
|
1043
|
+
force = this.domeForce(force, autoUpTo);
|
|
1044
|
+
plan = planDome(from, force);
|
|
1004
1045
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1046
|
+
else if (isCoordToken(words[0])) {
|
|
1047
|
+
if (words.length > 2 || (words.length === 2 && !isCoordToken(words[1])))
|
|
1048
|
+
return `Two points at most. ${usage}`;
|
|
1049
|
+
const a = parseCoord(words[0], from.z);
|
|
1050
|
+
if (words.length === 2) {
|
|
1051
|
+
const b = parseCoord(words[1], from.z);
|
|
1052
|
+
const sized = this.sizeForce(spellKey, force, forceForWall(a, b), autoUpTo);
|
|
1053
|
+
if (typeof sized === 'string')
|
|
1054
|
+
return sized;
|
|
1055
|
+
force = sized;
|
|
1056
|
+
plan = planWall(a, b, force);
|
|
1057
|
+
if (plan.error)
|
|
1058
|
+
return plan.error;
|
|
1059
|
+
const hidden = [a, b].find(p => !this.inSight(plane, from, p));
|
|
1060
|
+
if (hidden)
|
|
1061
|
+
return unseen(formatCoord(hidden));
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
center = a;
|
|
1065
|
+
force = this.domeForce(force, autoUpTo);
|
|
1066
|
+
plan = planDome(a, force);
|
|
1067
|
+
if (!this.inSight(plane, from, a))
|
|
1068
|
+
return unseen(formatCoord(a));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
else if (this.isValidDirection(words[0].toLowerCase())) {
|
|
1072
|
+
if (words.length > 2 || (words.length === 2 && !/^\d+$/.test(words[1]))) {
|
|
1073
|
+
return `"cast ${spellKey} ${words[0]} [cells]" -- the distance is a whole number of cells.`;
|
|
1074
|
+
}
|
|
1075
|
+
const distance = words.length === 2 ? parseInt(words[1], 10) : 1;
|
|
1076
|
+
const sized = this.sizeForce(spellKey, force, DIRECTIONAL_WALL_FORCE, autoUpTo);
|
|
1077
|
+
if (typeof sized === 'string')
|
|
1078
|
+
return sized;
|
|
1079
|
+
force = sized;
|
|
1080
|
+
plan = planDirectionalWall(from, words[0].toLowerCase(), distance, force);
|
|
1081
|
+
if (plan.error)
|
|
1082
|
+
return plan.error;
|
|
1083
|
+
const run = plan.faces[0].cells;
|
|
1084
|
+
const mid = run[Math.floor(run.length / 2)];
|
|
1085
|
+
if (!this.inSight(plane, from, mid))
|
|
1086
|
+
return unseen(`${distance} cell${distance === 1 ? '' : 's'} ${words[0]}`);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
const lead = words[0].toLowerCase();
|
|
1090
|
+
let nameWords = lead === 'at' || lead === 'on' || lead === 'around' ? words.slice(1) : words;
|
|
1091
|
+
// "at <someone> [size]" (user 2026-10-05): a trailing number is the
|
|
1092
|
+
// dome's RADIUS in map cells. p.294 makes the radius the Force in
|
|
1093
|
+
// metres, so the size IS a Force -- two cells is Force 4.
|
|
1094
|
+
let size;
|
|
1095
|
+
if (nameWords.length > 1 && /^\d+$/.test(nameWords[nameWords.length - 1])) {
|
|
1096
|
+
size = parseInt(nameWords[nameWords.length - 1], 10);
|
|
1097
|
+
nameWords = nameWords.slice(0, -1);
|
|
1098
|
+
}
|
|
1099
|
+
if (nameWords.length === 0)
|
|
1100
|
+
return usage;
|
|
1101
|
+
// "me" / "self" / "myself" are the caster (user 2026-10-05).
|
|
1102
|
+
const selfWord = nameWords.length === 1 && /^(me|self|myself|yourself)$/i.test(nameWords[0]);
|
|
1103
|
+
const candidates = this.scene.getActorsInRoom(room).filter(a => a === actor || ((a.plane === 'meat' || a.plane === actor.plane) && actor.canPerceive(a)));
|
|
1104
|
+
const picked = selfWord ? actor.name : fuzzyPickName(nameWords.join(' '), candidates.map(a => a.name));
|
|
1105
|
+
const target = selfWord ? actor : picked ? candidates.find(a => a.name === picked) : undefined;
|
|
1106
|
+
if (!target)
|
|
1107
|
+
return `You don't see "${nameWords.join(' ')}" here. ${usage}`;
|
|
1108
|
+
const at = this.scene.worldCellOf?.(target);
|
|
1109
|
+
if (!at)
|
|
1110
|
+
return `${target.name} isn't standing anywhere a dome could go round.`;
|
|
1111
|
+
if (target !== actor && !this.inSight(plane, from, at))
|
|
1112
|
+
return unseen(target.name);
|
|
1113
|
+
center = at;
|
|
1114
|
+
around = target === actor ? 'you' : target.name;
|
|
1115
|
+
if (size !== undefined) {
|
|
1116
|
+
if (size < 1)
|
|
1117
|
+
return `A dome's size is its radius in cells, 1 or more.`;
|
|
1118
|
+
const needed = Math.ceil(size * METERS_PER_CELL);
|
|
1119
|
+
if (autoUpTo === undefined && needed !== force) {
|
|
1120
|
+
return `A dome's radius IS its Force in metres (p.294): ${size} cell${size === 1 ? '' : 's'} is Force ${needed}, not ${force}. Name one or the other.`;
|
|
1121
|
+
}
|
|
1122
|
+
const sized = this.sizeForce(spellKey, force, needed, autoUpTo, 'dome');
|
|
1123
|
+
if (typeof sized === 'string')
|
|
1124
|
+
return sized;
|
|
1125
|
+
force = sized;
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
force = this.domeForce(force, autoUpTo);
|
|
1129
|
+
}
|
|
1130
|
+
plan = planDome(at, force);
|
|
1131
|
+
}
|
|
1132
|
+
if (plan.error)
|
|
1133
|
+
return plan.error;
|
|
1134
|
+
// CASTING IS A COMPLEX ACTION in a Combat Turn (p.167).
|
|
1135
|
+
const bill = billAction(this.scene, actor, 'complex', `Cast ${label}`);
|
|
1136
|
+
if (bill)
|
|
1137
|
+
return bill;
|
|
1138
|
+
// The standing one goes first: its cells must not clip the new one.
|
|
1139
|
+
if (standing) {
|
|
1140
|
+
if (mana)
|
|
1141
|
+
actor.sustainedManaBarrier = undefined;
|
|
1142
|
+
else
|
|
1143
|
+
actor.sustainedBarrier = undefined;
|
|
1144
|
+
}
|
|
1145
|
+
// WHAT IT CANNOT STAND IN (barrier-geometry clipFaces).
|
|
1146
|
+
const clipped = clipFaces(plan.faces, c => this.barrierCellOpen(plane, c, kind, center));
|
|
1147
|
+
if (clipped.faces.length === 0) {
|
|
1148
|
+
return `There's no room for a ${label} there -- every cell of it is ${mana ? 'earth or another weave' : 'wall, furniture or somebody'}.`;
|
|
1149
|
+
}
|
|
1150
|
+
// THE CASTING TEST: Magic + Spellcasting [Force] (p.281). Physical
|
|
1151
|
+
// Barrier: Armor and Structure 1 per hit (p.294). Mana Barrier: the
|
|
1152
|
+
// barrier's Armor and Structure are its Force (p.316).
|
|
1153
|
+
const roll = rollPool(actor.getMagicPool(), force);
|
|
1154
|
+
if (this.scene.isHumanControlled(actor))
|
|
1155
|
+
this.logger.meta(`${mana ? 'Mana Barrier' : 'Physical Barrier'} casting: ${formatRoll(roll)}`);
|
|
1009
1156
|
const spell = spellFor(spellKey);
|
|
1010
|
-
|
|
1157
|
+
if (!mana && roll.hits === 0) {
|
|
1158
|
+
actor.performAction(`casts ${label}`, 'and nothing holds');
|
|
1159
|
+
const lines = [
|
|
1160
|
+
`${actor.name} casts barrier at Force ${force}, ${this.castingLabel()}:`,
|
|
1161
|
+
` The air shivers and lets go -- no hits, no wall.`,
|
|
1162
|
+
...this.applyDrain(spell, force, overcast),
|
|
1163
|
+
];
|
|
1164
|
+
lines.push(...await this.drainCollapse(new CombatExchange(this.scene, this.logger, this.actor)));
|
|
1165
|
+
return this.finishSpell(lines);
|
|
1166
|
+
}
|
|
1167
|
+
const rating = mana ? force : roll.hits;
|
|
1168
|
+
const barrier = buildBarrier(kind, actor.name, force, rating, room.name, plan.shape, clipped.faces);
|
|
1011
1169
|
if (mana)
|
|
1012
|
-
actor.sustainedManaBarrier =
|
|
1170
|
+
actor.sustainedManaBarrier = barrier;
|
|
1013
1171
|
else
|
|
1014
|
-
actor.sustainedBarrier =
|
|
1015
|
-
|
|
1172
|
+
actor.sustainedBarrier = barrier;
|
|
1173
|
+
const cells = barrier.faces.reduce((n, f) => n + f.cells.length, 0);
|
|
1174
|
+
const first = barrier.faces[0].cells;
|
|
1175
|
+
const where = plan.shape === 'dome'
|
|
1176
|
+
? `a dome of ${force} m radius around ${around ?? formatCoord(center)}`
|
|
1177
|
+
: `a wall from ${formatCoord(first[0])} to ${formatCoord(first[first.length - 1])}`;
|
|
1178
|
+
actor.performAction(`casts ${label}`, where);
|
|
1016
1179
|
this.scene.addWorldEvent(mana
|
|
1017
|
-
? `${actor.name} wove a mana barrier
|
|
1018
|
-
: `${actor.name} raised a shimmering force barrier
|
|
1180
|
+
? `${actor.name} wove a mana barrier in ${room.name}.`
|
|
1181
|
+
: `${actor.name} raised a shimmering force barrier in ${room.name}.`);
|
|
1019
1182
|
this.scene.updateStatus();
|
|
1183
|
+
// The panels draw it (room-view / map-view): repaint them now.
|
|
1184
|
+
this.scene.updateExits?.(room);
|
|
1020
1185
|
const burden = sustainBurden(actor.sustainedLedger().find(w => w.key === spellKey), ' to everything else you do');
|
|
1186
|
+
const faces = barrier.faces.length > 1 ? ` ${barrier.faces.length} faces, each with its own damage track.` : '';
|
|
1021
1187
|
const lines = [
|
|
1022
|
-
`${actor.name} casts ${
|
|
1188
|
+
`${actor.name} casts ${label} at Force ${force}, ${this.castingLabel()}:`,
|
|
1189
|
+
...(this.scene.isHumanControlled(actor) ? [] : [` Casting: ${formatRoll(roll)}`]),
|
|
1023
1190
|
mana
|
|
1024
|
-
? ` A lattice of woven mana settles
|
|
1025
|
-
: ` The air itself hardens
|
|
1026
|
-
|
|
1191
|
+
? ` A lattice of woven mana settles into ${where} -- ${cells} cell${cells === 1 ? '' : 's'}, invisible to meat eyes. Armor and Structure ${rating}.${faces} SPIRITS and astral forms stop at it; the living walk through without noticing, and so do you. ${burden.charAt(0).toUpperCase()}${burden.slice(1)}.`
|
|
1192
|
+
: ` The air itself hardens into ${where} -- ${cells} cell${cells === 1 ? '' : 's'} of heat-shimmer. Armor and Structure ${rating} (${roll.hits} hit${roll.hits === 1 ? '' : 's'}).${faces} NOTHING physical passes while you hold it (you included), and ${burden}.`,
|
|
1193
|
+
...(plan.note ? [` ${plan.note}`] : []),
|
|
1194
|
+
...(clipped.dropped > 0 ? [` ${clipped.dropped} cell${clipped.dropped === 1 ? '' : 's'} of it ${mana ? 'stop at earth or another weave' : 'meet wall, furniture or a body and stop there'}.`] : []),
|
|
1195
|
+
hint(` ("cast ${spellKey} off" lets it go; "dispel ${mana ? 'mana barrier' : 'barrier'}" unweaves it from either side.)`),
|
|
1027
1196
|
...this.applyDrain(spell, force, overcast),
|
|
1028
1197
|
];
|
|
1029
1198
|
lines.push(...await this.drainCollapse(new CombatExchange(this.scene, this.logger, this.actor)));
|
|
1030
1199
|
return this.finishSpell(lines);
|
|
1031
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* NO FORCE NAMED: SIZE IT TO THE SHAPE (user 2026-10-05). A wall gets
|
|
1203
|
+
* the least Force that raises it -- never less than the default, and
|
|
1204
|
+
* never past your talent: overcasting turns the drain PHYSICAL (p.281),
|
|
1205
|
+
* and that is a choice to make out loud, not one made for you.
|
|
1206
|
+
*/
|
|
1207
|
+
sizeForce(spellKey, force, needed, autoUpTo, what = 'wall') {
|
|
1208
|
+
// A wall only ever grows to fit; a sized dome IS its size.
|
|
1209
|
+
if (autoUpTo === undefined || (what === 'wall' && needed <= force))
|
|
1210
|
+
return force;
|
|
1211
|
+
if (needed > autoUpTo) {
|
|
1212
|
+
return `That ${what} needs Force ${needed}, past your talent (${autoUpTo}) -- name the Force to overcast it on purpose ("cast ${spellKey} ${needed} ..."; the drain lands PHYSICAL, p.281).`;
|
|
1213
|
+
}
|
|
1214
|
+
return needed;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* A DOME WITH NO FORCE NAMED stands one storey: Force 3, a dome 3 m
|
|
1218
|
+
* high (p.294: height = radius = Force m). Anything lower would not
|
|
1219
|
+
* cover the person it is raised around. Never past your talent -- a
|
|
1220
|
+
* caster with less Magic gets the tallest dome they can hold safely.
|
|
1221
|
+
*/
|
|
1222
|
+
domeForce(force, autoUpTo) {
|
|
1223
|
+
if (autoUpTo === undefined)
|
|
1224
|
+
return force;
|
|
1225
|
+
return Math.max(force, Math.min(DOME_FORCE, autoUpTo));
|
|
1226
|
+
}
|
|
1227
|
+
/** LINE OF SIGHT for an area spell's point (p.281): no wall between. */
|
|
1228
|
+
inSight(plane, from, to) {
|
|
1229
|
+
return wallsBetween(plane, from, to) === 0;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* CAN A BARRIER CELL STAND HERE. Physical: not in a wall, not in the
|
|
1233
|
+
* void, not in furniture or a body -- matter is already there. Mana:
|
|
1234
|
+
* the astral is "unimpeded by material objects" (p.313), so only earth,
|
|
1235
|
+
* a warded room you are not inside, and another mana barrier stop it.
|
|
1236
|
+
* The one a dome is raised around is inside it, never in its way.
|
|
1237
|
+
*/
|
|
1238
|
+
barrierCellOpen(plane, c, kind, center) {
|
|
1239
|
+
if (center && c.x === center.x && c.y === center.y && c.z === center.z)
|
|
1240
|
+
return true;
|
|
1241
|
+
if (isVoid(plane, c))
|
|
1242
|
+
return false;
|
|
1243
|
+
const ownerKey = roomAtCell(plane, c);
|
|
1244
|
+
const owner = ownerKey ? Object.values(this.scene.getRooms()).find(r => r.name.trim().toLowerCase() === ownerKey) : undefined;
|
|
1245
|
+
if (kind === 'mana') {
|
|
1246
|
+
if (owner?.warded && owner !== this.actor.currentLocation)
|
|
1247
|
+
return false;
|
|
1248
|
+
return !this.scene.barrierAt(c, 'mana');
|
|
1249
|
+
}
|
|
1250
|
+
if (isSolid(plane, c))
|
|
1251
|
+
return false;
|
|
1252
|
+
if (this.scene.barrierAt(c, 'physical'))
|
|
1253
|
+
return false;
|
|
1254
|
+
const place = ownerKey ? plane.placements.get(ownerKey) : undefined;
|
|
1255
|
+
if (!owner || !place)
|
|
1256
|
+
return true;
|
|
1257
|
+
const local = { x: c.x - place.origin.x, y: c.y - place.origin.y, z: c.z - place.origin.z };
|
|
1258
|
+
const lk = gridKey(local);
|
|
1259
|
+
if (furnitureCells(owner).has(lk))
|
|
1260
|
+
return false;
|
|
1261
|
+
if (standingOccupants(owner).has(lk))
|
|
1262
|
+
return false;
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1032
1265
|
/**
|
|
1033
1266
|
* The sustained utility pair (invisibility / increase reflexes), same
|
|
1034
1267
|
* toggle shape as the armor spell: the BEARER wears the effect, the
|
|
@@ -3,6 +3,7 @@ import { BypassCommand } from './bypass-command.js';
|
|
|
3
3
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
4
4
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
5
|
import { billAction } from '../utilities/action-cost.js';
|
|
6
|
+
import { roomAtCell } from '../utilities/district-plane.js';
|
|
6
7
|
/**
|
|
7
8
|
* The magical barrier's OWN verb (player ruling: "solve" was too broad --
|
|
8
9
|
* a ward wants "dispel", not a generic answer verb), and the second half
|
|
@@ -110,16 +111,23 @@ export class DispelCommand extends CastCommand {
|
|
|
110
111
|
return `More than one weave of ${named.label} here -- name whose: "dispel ${named.words[0]} on <name>".`;
|
|
111
112
|
}
|
|
112
113
|
// A barrier's WEAVE is here even when its weaver isn't: "dispel
|
|
113
|
-
// [mana] barrier"
|
|
114
|
-
//
|
|
114
|
+
// [mana] barrier" reaches the caster wherever they stand, for any
|
|
115
|
+
// barrier with a cell in this room or cast from it -- or across one
|
|
116
|
+
// of its doorways, from either side.
|
|
115
117
|
if (!target && (named?.key === 'barrier' || named?.key === 'manabarrier')) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const kind = named.key === 'manabarrier' ? 'mana' : 'physical';
|
|
119
|
+
const plane = this.scene.ensurePlane?.();
|
|
120
|
+
const here = room.name.trim().toLowerCase();
|
|
121
|
+
const standing = this.scene.spellBarriers?.(kind).find(b => b.roomName === room.name
|
|
122
|
+
|| (plane && b.faces.some(f => f.cells.some(c => roomAtCell(plane, c) === here))));
|
|
123
|
+
let caster = standing?.caster;
|
|
124
|
+
for (const direction of caster ? [] : room.exits.keys()) {
|
|
125
|
+
caster = this.scene.barrierSealing(room, direction, kind)?.caster;
|
|
126
|
+
if (caster)
|
|
120
127
|
break;
|
|
121
|
-
}
|
|
122
128
|
}
|
|
129
|
+
if (caster)
|
|
130
|
+
target = this.scene.getPlayers().find(p => p.name === caster);
|
|
123
131
|
}
|
|
124
132
|
if (!target)
|
|
125
133
|
return undefined;
|
|
@@ -5,6 +5,7 @@ import { resolveHostInReach } from '../utilities/grid-reach.js';
|
|
|
5
5
|
import { planeChangeHint, astralMovesLeft, astralCordWarning, astralCordParts } from '../utilities/planes.js';
|
|
6
6
|
import { CombatExchange } from '../utilities/combat-exchange.js';
|
|
7
7
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
8
|
+
import { astralLimit } from '../utilities/limits.js';
|
|
8
9
|
import { walkOut } from '../utilities/movement-cost.js';
|
|
9
10
|
import { frameOf, droneImmobile, droneImmobileLine } from '../utilities/drone-prose.js';
|
|
10
11
|
import { entrySpotFor, arrivalAt, ensureAtExit, spotsActive, highlightSpots, highlightActors, doorwayBlocker, doorwayFurniture, yieldDoorway, lockedRouteHint, lockSeenFromOutside, spotForCell, waysOutOf, actorCell } from '../utilities/spots.js';
|
|
@@ -636,11 +637,12 @@ export class GoCommand extends Command {
|
|
|
636
637
|
// back corner, and nothing said so). Walking out means walking to
|
|
637
638
|
// it first: free out of combat, refused mid-exchange -- the same
|
|
638
639
|
// rule pick/unlock already enforce on the same doors.
|
|
639
|
-
// SUSTAINED BARRIER SPELL
|
|
640
|
-
//
|
|
641
|
-
// NPCs, the caster too. Checked
|
|
642
|
-
// nobody strolls up to a wall they
|
|
643
|
-
// Astral and Matrix forms pass: the
|
|
640
|
+
// SUSTAINED BARRIER SPELL: a held force wall standing over this
|
|
641
|
+
// doorway (its cells, barrier-geometry.ts) seals it for EVERY
|
|
642
|
+
// meat-plane actor -- players, fleeing NPCs, the caster too. Checked
|
|
643
|
+
// before the walk-to-the-doorway so nobody strolls up to a wall they
|
|
644
|
+
// can see shimmering from here. Astral and Matrix forms pass: the
|
|
645
|
+
// spell hardens AIR.
|
|
644
646
|
if (this.actor.plane === 'meat') {
|
|
645
647
|
const seal = this.scene.barrierSealing(currentRoom, direction);
|
|
646
648
|
if (seal) {
|
|
@@ -649,12 +651,25 @@ export class GoCommand extends Command {
|
|
|
649
651
|
}
|
|
650
652
|
}
|
|
651
653
|
// The MANA BARRIER twin (canon p.294): spirits and astral forms
|
|
652
|
-
// stop at the weave; the living walk through it without noticing
|
|
654
|
+
// stop at the weave; the living walk through it without noticing,
|
|
655
|
+
// and so does its creator (p.315).
|
|
653
656
|
if (this.actor.plane === 'astral') {
|
|
654
|
-
const seal = this.scene.barrierSealing(currentRoom, direction, 'mana');
|
|
657
|
+
const seal = this.scene.barrierSealing(currentRoom, direction, 'mana', this.actor.name);
|
|
655
658
|
if (seal) {
|
|
656
|
-
|
|
657
|
-
|
|
659
|
+
// PRESSING THROUGH (p.316, "Getting Around Mana Barriers"):
|
|
660
|
+
// Magic + Charisma [Astral] v. the barrier's Force x 2. Any net
|
|
661
|
+
// hits and you are through; otherwise the weave holds you.
|
|
662
|
+
const press = rollPool(this.actor.magic + this.actor.charisma, astralLimit(this.actor));
|
|
663
|
+
const hold = rollPool(seal.force * 2);
|
|
664
|
+
const net = press.hits - hold.hits;
|
|
665
|
+
if (this.scene.isHumanControlled(this.actor)) {
|
|
666
|
+
this.logger.meta(`Press through the mana barrier (p.316): Magic + Charisma ${formatRoll(press)} v. Force x 2 ${formatRoll(hold)}`);
|
|
667
|
+
}
|
|
668
|
+
if (net <= 0) {
|
|
669
|
+
this.actor.performAction('is stopped by a mana barrier', `${direction} from ${currentRoom.name}`);
|
|
670
|
+
return `A lattice of woven mana seals the way ${direction} -- Force ${seal.force}, ${seal.caster}'s working. You press against it and it presses back; the astral does not pass.`;
|
|
671
|
+
}
|
|
672
|
+
this.actor.performAction('presses through a mana barrier', `${direction} from ${currentRoom.name}`);
|
|
658
673
|
}
|
|
659
674
|
}
|
|
660
675
|
let crossNote = '';
|