@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.384.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -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 } from '../utilities/room-grid.js';
|
|
11
|
+
import { isSolid, isVoid, roomAtCell, wallsBetween } from '../utilities/district-plane.js';
|
|
12
|
+
import { 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
|
|
@@ -964,16 +973,31 @@ export class CastCommand extends BypassCommand {
|
|
|
964
973
|
return this.finishSpell(lines);
|
|
965
974
|
}
|
|
966
975
|
/**
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
* barrier
|
|
972
|
-
*
|
|
976
|
+
* THE BARRIER SPELLS, CAST AT THE DISTRICT PLANE (SR5 p.294; user
|
|
977
|
+
* spec 2026-10-05). Coordinates are plane cells -- the numbers on the
|
|
978
|
+
* map rulers -- and the shape follows from how many points you give:
|
|
979
|
+
*
|
|
980
|
+
* cast barrier [F] <x,y[,z]> <x,y[,z]> a wall between two points
|
|
981
|
+
* cast barrier [F] <x,y[,z]> a dome around one point
|
|
982
|
+
* cast barrier [F] <direction> [n] a 1x3 wall n cells that way
|
|
983
|
+
* cast barrier [F] at <someone> a dome around them
|
|
984
|
+
* cast barrier [F] a dome around yourself
|
|
985
|
+
* cast barrier off let it go (free)
|
|
986
|
+
*
|
|
987
|
+
* A wall is Force x 2 m long and Force m high; a dome is Force m in
|
|
988
|
+
* radius (barrier-geometry.ts). A Physical Barrier's Armor and
|
|
989
|
+
* Structure are its HITS; a Mana Barrier's are its Force (p.316). One
|
|
990
|
+
* of each per caster; casting again replaces the standing one, with a
|
|
991
|
+
* fresh roll and fresh drain. -2 sustaining like every held weave.
|
|
992
|
+
*
|
|
993
|
+
* Bare "cast north" no longer reaches a ward from here -- that is
|
|
994
|
+
* "dispel north" (user ruling 2026-10-05).
|
|
973
995
|
*/
|
|
974
996
|
async castBarrier(spellKey, force, overcast, rest) {
|
|
975
997
|
const actor = this.actor;
|
|
976
998
|
const mana = spellKey === 'manabarrier';
|
|
999
|
+
const kind = mana ? 'mana' : 'physical';
|
|
1000
|
+
const label = mana ? 'mana barrier' : 'barrier';
|
|
977
1001
|
if (actor.plane !== 'meat' && actor.plane !== 'astral') {
|
|
978
1002
|
return `Barriers are woven where the world is -- not from the grid.`;
|
|
979
1003
|
}
|
|
@@ -981,54 +1005,195 @@ export class CastCommand extends BypassCommand {
|
|
|
981
1005
|
return `A physical barrier needs physical hands -- come back to the meat.`;
|
|
982
1006
|
}
|
|
983
1007
|
const standing = mana ? actor.sustainedManaBarrier : actor.sustainedBarrier;
|
|
984
|
-
|
|
1008
|
+
const words = rest.map(w => w.trim()).filter(w => w.length > 0);
|
|
1009
|
+
if (words.length === 1 && /^(off|release|drop|end)$/i.test(words[0])) {
|
|
1010
|
+
if (!standing)
|
|
1011
|
+
return `You aren't holding a ${label}.`;
|
|
985
1012
|
if (mana)
|
|
986
1013
|
actor.sustainedManaBarrier = undefined;
|
|
987
1014
|
else
|
|
988
1015
|
actor.sustainedBarrier = undefined;
|
|
989
1016
|
this.scene.updateStatus();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
// Fall through: a direction was named -- re-seat the wall there.
|
|
1017
|
+
this.scene.updateExits?.(actor.currentLocation);
|
|
1018
|
+
actor.performAction(`releases ${label} spell`, `in ${standing.roomName}`);
|
|
1019
|
+
return this.finishSpell([`You let the ${label} go -- the ${mana ? 'weave unravels' : 'air unhardens'}, and the sustaining burden lifts.`]);
|
|
995
1020
|
}
|
|
996
1021
|
const room = actor.currentLocation;
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1022
|
+
const plane = this.scene.ensurePlane?.();
|
|
1023
|
+
const from = this.scene.worldCellOf?.(actor);
|
|
1024
|
+
if (!plane || !from) {
|
|
1025
|
+
return `There's no ground here to raise a ${label} on.`;
|
|
1000
1026
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1027
|
+
// WHAT SHAPE, AND WHERE.
|
|
1028
|
+
let plan;
|
|
1029
|
+
let around;
|
|
1030
|
+
let center;
|
|
1031
|
+
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>". The map rulers give x and y; you're at ${formatCoord(from)}.`;
|
|
1032
|
+
const unseen = (what) => `You can't see ${what} from here -- a barrier is cast at what you can see (LOS, p.294).`;
|
|
1033
|
+
if (words.length === 0) {
|
|
1034
|
+
center = from;
|
|
1035
|
+
around = 'you';
|
|
1036
|
+
plan = planDome(from, force);
|
|
1004
1037
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1038
|
+
else if (isCoordToken(words[0])) {
|
|
1039
|
+
if (words.length > 2 || (words.length === 2 && !isCoordToken(words[1])))
|
|
1040
|
+
return `Two points at most. ${usage}`;
|
|
1041
|
+
const a = parseCoord(words[0], from.z);
|
|
1042
|
+
if (words.length === 2) {
|
|
1043
|
+
const b = parseCoord(words[1], from.z);
|
|
1044
|
+
plan = planWall(a, b, force);
|
|
1045
|
+
if (plan.error)
|
|
1046
|
+
return plan.error;
|
|
1047
|
+
const hidden = [a, b].find(p => !this.inSight(plane, from, p));
|
|
1048
|
+
if (hidden)
|
|
1049
|
+
return unseen(formatCoord(hidden));
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
center = a;
|
|
1053
|
+
plan = planDome(a, force);
|
|
1054
|
+
if (!this.inSight(plane, from, a))
|
|
1055
|
+
return unseen(formatCoord(a));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
else if (this.isValidDirection(words[0].toLowerCase())) {
|
|
1059
|
+
if (words.length > 2 || (words.length === 2 && !/^\d+$/.test(words[1]))) {
|
|
1060
|
+
return `"cast ${spellKey} ${words[0]} [cells]" -- the distance is a whole number of cells.`;
|
|
1061
|
+
}
|
|
1062
|
+
const distance = words.length === 2 ? parseInt(words[1], 10) : 1;
|
|
1063
|
+
plan = planDirectionalWall(from, words[0].toLowerCase(), distance, force);
|
|
1064
|
+
if (plan.error)
|
|
1065
|
+
return plan.error;
|
|
1066
|
+
const run = plan.faces[0].cells;
|
|
1067
|
+
const mid = run[Math.floor(run.length / 2)];
|
|
1068
|
+
if (!this.inSight(plane, from, mid))
|
|
1069
|
+
return unseen(`${distance} cell${distance === 1 ? '' : 's'} ${words[0]}`);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
const lead = words[0].toLowerCase();
|
|
1073
|
+
const nameWords = lead === 'at' || lead === 'on' || lead === 'around' ? words.slice(1) : words;
|
|
1074
|
+
if (nameWords.length === 0)
|
|
1075
|
+
return usage;
|
|
1076
|
+
const candidates = this.scene.getActorsInRoom(room).filter(a => a === actor || ((a.plane === 'meat' || a.plane === actor.plane) && actor.canPerceive(a)));
|
|
1077
|
+
const picked = fuzzyPickName(nameWords.join(' '), candidates.map(a => a.name));
|
|
1078
|
+
const target = picked ? candidates.find(a => a.name === picked) : undefined;
|
|
1079
|
+
if (!target)
|
|
1080
|
+
return `You don't see "${nameWords.join(' ')}" here. ${usage}`;
|
|
1081
|
+
const at = this.scene.worldCellOf?.(target);
|
|
1082
|
+
if (!at)
|
|
1083
|
+
return `${target.name} isn't standing anywhere a dome could go round.`;
|
|
1084
|
+
if (target !== actor && !this.inSight(plane, from, at))
|
|
1085
|
+
return unseen(target.name);
|
|
1086
|
+
center = at;
|
|
1087
|
+
around = target === actor ? 'you' : target.name;
|
|
1088
|
+
plan = planDome(at, force);
|
|
1089
|
+
}
|
|
1090
|
+
if (plan.error)
|
|
1091
|
+
return plan.error;
|
|
1092
|
+
// CASTING IS A COMPLEX ACTION in a Combat Turn (p.167).
|
|
1093
|
+
const bill = billAction(this.scene, actor, 'complex', `Cast ${label}`);
|
|
1094
|
+
if (bill)
|
|
1095
|
+
return bill;
|
|
1096
|
+
// The standing one goes first: its cells must not clip the new one.
|
|
1097
|
+
if (standing) {
|
|
1098
|
+
if (mana)
|
|
1099
|
+
actor.sustainedManaBarrier = undefined;
|
|
1100
|
+
else
|
|
1101
|
+
actor.sustainedBarrier = undefined;
|
|
1102
|
+
}
|
|
1103
|
+
// WHAT IT CANNOT STAND IN (barrier-geometry clipFaces).
|
|
1104
|
+
const clipped = clipFaces(plan.faces, c => this.barrierCellOpen(plane, c, kind, center));
|
|
1105
|
+
if (clipped.faces.length === 0) {
|
|
1106
|
+
return `There's no room for a ${label} there -- every cell of it is ${mana ? 'earth or another weave' : 'wall, furniture or somebody'}.`;
|
|
1107
|
+
}
|
|
1108
|
+
// THE CASTING TEST: Magic + Spellcasting [Force] (p.281). Physical
|
|
1109
|
+
// Barrier: Armor and Structure 1 per hit (p.294). Mana Barrier: the
|
|
1110
|
+
// barrier's Armor and Structure are its Force (p.316).
|
|
1111
|
+
const roll = rollPool(actor.getMagicPool(), force);
|
|
1112
|
+
if (this.scene.isHumanControlled(actor))
|
|
1113
|
+
this.logger.meta(`${mana ? 'Mana Barrier' : 'Physical Barrier'} casting: ${formatRoll(roll)}`);
|
|
1009
1114
|
const spell = spellFor(spellKey);
|
|
1010
|
-
|
|
1115
|
+
if (!mana && roll.hits === 0) {
|
|
1116
|
+
actor.performAction(`casts ${label}`, 'and nothing holds');
|
|
1117
|
+
const lines = [
|
|
1118
|
+
`${actor.name} casts barrier at Force ${force}, ${this.castingLabel()}:`,
|
|
1119
|
+
` The air shivers and lets go -- no hits, no wall.`,
|
|
1120
|
+
...this.applyDrain(spell, force, overcast),
|
|
1121
|
+
];
|
|
1122
|
+
lines.push(...await this.drainCollapse(new CombatExchange(this.scene, this.logger, this.actor)));
|
|
1123
|
+
return this.finishSpell(lines);
|
|
1124
|
+
}
|
|
1125
|
+
const rating = mana ? force : roll.hits;
|
|
1126
|
+
const barrier = buildBarrier(kind, actor.name, force, rating, room.name, plan.shape, clipped.faces);
|
|
1011
1127
|
if (mana)
|
|
1012
|
-
actor.sustainedManaBarrier =
|
|
1128
|
+
actor.sustainedManaBarrier = barrier;
|
|
1013
1129
|
else
|
|
1014
|
-
actor.sustainedBarrier =
|
|
1015
|
-
|
|
1130
|
+
actor.sustainedBarrier = barrier;
|
|
1131
|
+
const cells = barrier.faces.reduce((n, f) => n + f.cells.length, 0);
|
|
1132
|
+
const first = barrier.faces[0].cells;
|
|
1133
|
+
const where = plan.shape === 'dome'
|
|
1134
|
+
? `a dome of ${force} m radius around ${around ?? formatCoord(center)}`
|
|
1135
|
+
: `a wall from ${formatCoord(first[0])} to ${formatCoord(first[first.length - 1])}`;
|
|
1136
|
+
actor.performAction(`casts ${label}`, where);
|
|
1016
1137
|
this.scene.addWorldEvent(mana
|
|
1017
|
-
? `${actor.name} wove a mana barrier
|
|
1018
|
-
: `${actor.name} raised a shimmering force barrier
|
|
1138
|
+
? `${actor.name} wove a mana barrier in ${room.name}.`
|
|
1139
|
+
: `${actor.name} raised a shimmering force barrier in ${room.name}.`);
|
|
1019
1140
|
this.scene.updateStatus();
|
|
1141
|
+
// The panels draw it (room-view / map-view): repaint them now.
|
|
1142
|
+
this.scene.updateExits?.(room);
|
|
1020
1143
|
const burden = sustainBurden(actor.sustainedLedger().find(w => w.key === spellKey), ' to everything else you do');
|
|
1144
|
+
const faces = barrier.faces.length > 1 ? ` ${barrier.faces.length} faces, each with its own damage track.` : '';
|
|
1021
1145
|
const lines = [
|
|
1022
|
-
`${actor.name} casts ${
|
|
1146
|
+
`${actor.name} casts ${label} at Force ${force}, ${this.castingLabel()}:`,
|
|
1147
|
+
...(this.scene.isHumanControlled(actor) ? [] : [` Casting: ${formatRoll(roll)}`]),
|
|
1023
1148
|
mana
|
|
1024
|
-
? ` A lattice of woven mana settles
|
|
1025
|
-
: ` The air itself hardens
|
|
1026
|
-
|
|
1149
|
+
? ` 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)}.`
|
|
1150
|
+
: ` 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}.`,
|
|
1151
|
+
...(plan.note ? [` ${plan.note}`] : []),
|
|
1152
|
+
...(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'}.`] : []),
|
|
1153
|
+
hint(` ("cast ${spellKey} off" lets it go; "dispel ${mana ? 'mana barrier' : 'barrier'}" unweaves it from either side.)`),
|
|
1027
1154
|
...this.applyDrain(spell, force, overcast),
|
|
1028
1155
|
];
|
|
1029
1156
|
lines.push(...await this.drainCollapse(new CombatExchange(this.scene, this.logger, this.actor)));
|
|
1030
1157
|
return this.finishSpell(lines);
|
|
1031
1158
|
}
|
|
1159
|
+
/** LINE OF SIGHT for an area spell's point (p.281): no wall between. */
|
|
1160
|
+
inSight(plane, from, to) {
|
|
1161
|
+
return wallsBetween(plane, from, to) === 0;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* CAN A BARRIER CELL STAND HERE. Physical: not in a wall, not in the
|
|
1165
|
+
* void, not in furniture or a body -- matter is already there. Mana:
|
|
1166
|
+
* the astral is "unimpeded by material objects" (p.313), so only earth,
|
|
1167
|
+
* a warded room you are not inside, and another mana barrier stop it.
|
|
1168
|
+
* The one a dome is raised around is inside it, never in its way.
|
|
1169
|
+
*/
|
|
1170
|
+
barrierCellOpen(plane, c, kind, center) {
|
|
1171
|
+
if (center && c.x === center.x && c.y === center.y && c.z === center.z)
|
|
1172
|
+
return true;
|
|
1173
|
+
if (isVoid(plane, c))
|
|
1174
|
+
return false;
|
|
1175
|
+
const ownerKey = roomAtCell(plane, c);
|
|
1176
|
+
const owner = ownerKey ? Object.values(this.scene.getRooms()).find(r => r.name.trim().toLowerCase() === ownerKey) : undefined;
|
|
1177
|
+
if (kind === 'mana') {
|
|
1178
|
+
if (owner?.warded && owner !== this.actor.currentLocation)
|
|
1179
|
+
return false;
|
|
1180
|
+
return !this.scene.barrierAt(c, 'mana');
|
|
1181
|
+
}
|
|
1182
|
+
if (isSolid(plane, c))
|
|
1183
|
+
return false;
|
|
1184
|
+
if (this.scene.barrierAt(c, 'physical'))
|
|
1185
|
+
return false;
|
|
1186
|
+
const place = ownerKey ? plane.placements.get(ownerKey) : undefined;
|
|
1187
|
+
if (!owner || !place)
|
|
1188
|
+
return true;
|
|
1189
|
+
const local = { x: c.x - place.origin.x, y: c.y - place.origin.y, z: c.z - place.origin.z };
|
|
1190
|
+
const lk = gridKey(local);
|
|
1191
|
+
if (furnitureCells(owner).has(lk))
|
|
1192
|
+
return false;
|
|
1193
|
+
if (standingOccupants(owner).has(lk))
|
|
1194
|
+
return false;
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1032
1197
|
/**
|
|
1033
1198
|
* The sustained utility pair (invisibility / increase reflexes), same
|
|
1034
1199
|
* 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 = '';
|
|
@@ -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;`);
|
|
@@ -1201,7 +1201,10 @@ export class MoveCommand extends Command {
|
|
|
1201
1201
|
// world simply stops. Naming it a wall would send the
|
|
1202
1202
|
// player looking for a door.
|
|
1203
1203
|
: blockedBy === 'edge' ? `the ground ends there`
|
|
1204
|
-
:
|
|
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`;
|
|
1205
1208
|
const refusal = `You can't step ${direction} -- ${why}.${this.doorHint(room, cell, blockedBy)}`;
|
|
1206
1209
|
// SAY IT ONCE (player screenshot 2026-09-21: twelve identical
|
|
1207
1210
|
// "You can't step south -- there's a wall." filling the log).
|
|
@@ -1280,7 +1283,8 @@ export class MoveCommand extends Command {
|
|
|
1280
1283
|
? `Pulled up short after ${meters} m -- ${blockedBy === 'someone' ? 'someone is standing there'
|
|
1281
1284
|
: blockedBy === 'furniture' ? 'furniture blocks the way'
|
|
1282
1285
|
: blockedBy === 'terrain' ? 'the way is blocked'
|
|
1283
|
-
: blockedBy === 'edge' ? 'the ground ends there'
|
|
1286
|
+
: blockedBy === 'edge' ? 'the ground ends there'
|
|
1287
|
+
: blockedBy === 'barrier' ? 'a force barrier stands there' : 'the wall'}.`
|
|
1284
1288
|
: '';
|
|
1285
1289
|
const arrived = actor.atSpot !== wasAt && actor.atSpot !== OPEN_FLOOR
|
|
1286
1290
|
? `You're at the ${actor.atSpot} now.`
|
|
@@ -1725,7 +1729,8 @@ export class MoveCommand extends Command {
|
|
|
1725
1729
|
return driftRefusal(drift, direction);
|
|
1726
1730
|
const stopped = drift.blockedBy === undefined ? ''
|
|
1727
1731
|
: drift.blockedBy === 'ward' ? ` A ward stops you${drift.ward ? ` at ${drift.ward}` : ''}.`
|
|
1728
|
-
: '
|
|
1732
|
+
: drift.blockedBy === 'barrier' ? ` A mana barrier stops you${drift.ward ? ` -- ${drift.ward}'s working` : ''}.`
|
|
1733
|
+
: ' Then solid earth.';
|
|
1729
1734
|
// Silent on a plain drift, like a meat step: the map moved, which
|
|
1730
1735
|
// is the answer. Speak when you arrived over somewhere named, or
|
|
1731
1736
|
// when something out here stopped you.
|