@maka/maka-cli 5.386.0 → 5.389.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.386.0",
3
+ "version": "5.389.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,10 +6,10 @@ import { rollPool, formatRoll, rollD6Total } 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, furnitureCells, standingOccupants } from '../utilities/spots.js';
9
+ import { spotOf, ensureAtSpot, furnitureCells, standingOccupants, visibleActorsIn, concealedFrom } from '../utilities/spots.js';
10
10
  import { key as gridKey, METERS_PER_CELL, distanceMeters, line3D } from '../utilities/room-grid.js';
11
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, FACE_LABEL, allBreached, damageFace, pierces } from '../utilities/barrier-geometry.js';
12
+ import { DIRECTIONAL_WALL_FORCE, DOME_FORCE, forceForWall, buildBarrier, clipFaces, formatCoord, isCoordToken, parseCoord, planDirectionalWall, planDome, planWall, FACE_LABEL, BARRIER_FOG, allBreached, damageFace, pierces } from '../utilities/barrier-geometry.js';
13
13
  import { NPC } from '../models/npc.js';
14
14
  import { spellFor, catalogSpells } from '../utilities/catalog.js';
15
15
  import { getAspect } from '../aspects.js';
@@ -91,6 +91,11 @@ const COMBAT_SPELL_FLAVOR = {
91
91
  * inherited from BypassCommand -- now gated on actually having magic gear,
92
92
  * instead of being a free-floating coin flip anyone could attempt.
93
93
  */
94
+ /**
95
+ * BLIND FIRE (p.178): "the same as the Total Darkness modifier", -6 -- the
96
+ * number p.197 gives for a target completely hidden from the attacker.
97
+ */
98
+ const BLIND_FIRE = -6;
94
99
  export class CastCommand extends BypassCommand {
95
100
  static verb = 'cast';
96
101
  /** Touches another actor: authorized per-act for AI-driven actors
@@ -99,6 +104,8 @@ export class CastCommand extends BypassCommand {
99
104
  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". Area spells too: "cast fireball 4 at 12,7" (a point) or "cast fireball 4 at <someone>" -- Force m around it, friend and foe. A ward is "dispel <direction>".';
100
105
  deviceKind = 'ward';
101
106
  verbName = 'cast';
107
+ /** The bolt in flight is at someone the caster cannot see (castCombatSpell). */
108
+ unseenTarget = false;
102
109
  noDeviceMessage = "There's nothing here to cast at.";
103
110
  /** Backlash drain when a ward-bypass fails -- resisted with Willpower,
104
111
  * like any drain. The nastiest drain in the game: forcing a hostile
@@ -520,9 +527,10 @@ export class CastCommand extends BypassCommand {
520
527
  }
521
528
  const armor = target.plane === 'meat' ? target.getArmorValue() : 0;
522
529
  const modifiedArmor = Math.max(0, armor - force);
530
+ const blind = this.unseenTarget ? BLIND_FIRE : 0;
523
531
  return {
524
- label: label(force) + (weave ? ` -- through a Force ${weave} mana barrier` : ''),
525
- pool: this.actor.getMagicPool() + this.actor.consumeEdgeBoost(),
532
+ label: label(force) + (weave ? ` -- through a Force ${weave} mana barrier` : '') + (blind ? ` -- thrown BLIND (${blind}, p.178)` : ''),
533
+ pool: Math.max(0, this.actor.getMagicPool() + this.actor.consumeEdgeBoost() + blind),
526
534
  damageBase: force,
527
535
  soakPool: Math.max(0, target.getSoakPool() - armor + modifiedArmor) + through,
528
536
  modifiedArmor: target.plane === 'meat' ? modifiedArmor : undefined,
@@ -623,6 +631,16 @@ export class CastCommand extends BypassCommand {
623
631
  if (target.isIncapacitated()) {
624
632
  return `${target.name} is already down. The fight is over.`;
625
633
  }
634
+ // LINE OF SIGHT (p.281, p.283). Seeing them is the one question
635
+ // visibleActorsIn answers for the map and `look`: on a plane you
636
+ // perceive and not hidden in cover. A DIRECT spell needs it -- it
637
+ // reaches through your own sight, with nothing to aim but that. An
638
+ // INDIRECT one only needs "a clear line of fire": you can throw it
639
+ // blind, at the Blind Fire penalty (p.178; -6, p.197).
640
+ this.unseenTarget = !visibleActorsIn(room, this.actor).includes(target);
641
+ if (this.unseenTarget && spellFor(spellKey)?.soak !== 'armor') {
642
+ return `You can't see ${target.name} -- a ${spellKey} reaches through your own eyes, and they have nothing to fix on (LOS, p.281).${hint(' (An indirect spell can be thrown blind, at -6.)')}`;
643
+ }
626
644
  // One exchange at a time per combatant, same as attack.ts: the player
627
645
  // is refused cleanly, an NPC caster waits out the exchange and re-runs
628
646
  // (see CombatExchange.waitForLull and the rationale in attack.ts).
@@ -980,13 +998,17 @@ export class CastCommand extends BypassCommand {
980
998
  this.scene.addWorldEvent(`${actor.name} ${flavor.act} ${aimedAt} in ${room.name}.`);
981
999
  const lines = [];
982
1000
  const meta = [];
983
- const pool = actor.getMagicPool() + actor.consumeEdgeBoost();
1001
+ // Seen through a Physical Barrier, the point is in Light Fog: -1 on
1002
+ // the casting test (p.294, p.175). An indirect ball meets the barrier
1003
+ // itself, below, so only a direct one pays this.
1004
+ const fog = spell.soak !== 'armor' && this.scene.barrierOnLine?.(from, aim, 'physical') ? BARRIER_FOG : 0;
1005
+ const pool = Math.max(0, actor.getMagicPool() + actor.consumeEdgeBoost() + fog);
984
1006
  const roll = rollPool(pool, force);
985
1007
  const header = `${actor.name} casts ${spellKey} at Force ${force}${overcast ? ' (OVERCAST)' : ''}, ${this.castingLabel()}, at ${aimedAt}`;
986
1008
  const hurt = [];
987
1009
  if (spell.soak !== 'armor') {
988
1010
  // ---- DIRECT: one test, opposed by each target it can see. ----
989
- meta.push(`${header}: casting ${formatRoll(roll)}`);
1011
+ meta.push(`${header}: casting ${formatRoll(roll)}${fog ? ` (${fog} through a force barrier, Light Fog p.175)` : ''}`);
990
1012
  lines.push(`${header} -- ${force} m of ${spell.type === 'mana' ? 'raw mana' : 'force'} around ${aimedAt}.`);
991
1013
  const caught = everyone.filter(a => {
992
1014
  const c = this.scene.worldCellOf?.(a);
@@ -994,7 +1016,12 @@ export class CastCommand extends BypassCommand {
994
1016
  return false;
995
1017
  // "...that you can see" (p.281): your own body counts; others must
996
1018
  // be perceived and in plain sight of you.
997
- return a === actor || (actor.canPerceive(a) && this.inSight(plane, from, c));
1019
+ // Hidden in cover is not seen either (visibleActorsIn's rule).
1020
+ if (a === actor)
1021
+ return true;
1022
+ if (!actor.canPerceive(a) || !this.inSight(plane, from, c))
1023
+ return false;
1024
+ return a.currentLocation !== room || !concealedFrom(room, actor, a);
998
1025
  });
999
1026
  if (caught.length === 0)
1000
1027
  lines.push(` The spell washes over empty ground.`);
@@ -43,6 +43,18 @@ const DRIFT_WAYS = new Set([
43
43
  Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST,
44
44
  Direction.UP, Direction.DOWN,
45
45
  ]);
46
+ /**
47
+ * IS THIS DOOR LOCKED TO SOMEONE LEAVING `from`. A tenant lock
48
+ * (Game.lockForeignResidences) is a flat's deadbolt: it keeps the street
49
+ * out and opens from inside, so nobody standing in the flat -- a save
50
+ * made there before the lock existed, a runner who breached in -- is
51
+ * shut in by it.
52
+ */
53
+ function lockedFrom(door, from) {
54
+ if (!door.checkIfLocked())
55
+ return false;
56
+ return !(door.tenantLocked && from.residenceTier !== undefined);
57
+ }
46
58
  export class GoCommand extends Command {
47
59
  /**
48
60
  * WHICH PLANE A COMPANION CAN HEEL ON, one rule per kind.
@@ -738,7 +750,7 @@ export class GoCommand extends Command {
738
750
  const way = wayOut(currentRoom, direction, here);
739
751
  const plane = way?.kind === 'seam' ? this.scene?.ensurePlane?.() : undefined;
740
752
  const at = plane?.placements.get(roomKey(currentRoom.name));
741
- const gated = exit instanceof Door && exit.checkIfLocked();
753
+ const gated = exit instanceof Door && lockedFrom(exit, currentRoom);
742
754
  if (way?.kind === 'seam' && at && here && !gated) {
743
755
  const world = { x: at.origin.x + way.door.x, y: at.origin.y + way.door.y, z: at.origin.z + way.door.z };
744
756
  const there = plane.placements.get(roomKey(way.target.name));
@@ -772,7 +784,7 @@ export class GoCommand extends Command {
772
784
  // crossExit). Asked BEFORE the walk: the bolt is visible from
773
785
  // across the room, and a runner told about it here is not made to
774
786
  // cross the floor to hear it.
775
- if (exit instanceof Door && exit.checkIfLocked() && this.actor.plane === 'meat') {
787
+ if (exit instanceof Door && lockedFrom(exit, currentRoom) && this.actor.plane === 'meat') {
776
788
  this.actor.performAction('is prevented by locked door', currentRoom.name);
777
789
  // NAME THE ROUTE (blocker 2026-08-27). This is FIRST CONTACT --
778
790
  // what a player types before they know a barrier is even there
@@ -815,7 +827,7 @@ export class GoCommand extends Command {
815
827
  */
816
828
  async crossExit(exit, direction, currentRoom, crossNote) {
817
829
  if (exit instanceof Door) {
818
- if (exit.checkIfLocked() && this.actor.plane === 'meat') {
830
+ if (lockedFrom(exit, currentRoom) && this.actor.plane === 'meat') {
819
831
  this.actor.performAction('is prevented by locked door', currentRoom.name);
820
832
  return `${crossNote}The door in the direction "${direction}" is locked.${lockedRouteHint(currentRoom, direction, exit)}`;
821
833
  }
@@ -109,7 +109,16 @@ export class RestCommand extends Command {
109
109
  // lifestyleChoices residence against the rooms the seed provides.
110
110
  // It answers "may someone of this tier live in this scene", never
111
111
  // "is this particular room your bedroom".
112
- const isResidence = room.roomType?.toLowerCase() === 'residence';
112
+ //
113
+ // AND A BED IS YOURS OR IT IS NOT A BED (kL23W3ufgsF5MZ2bt). "The
114
+ // roomType of _homeRoom and of nothing else" stopped being true when
115
+ // every district above Tacoma authored its own residence: a Street
116
+ // runner visiting Everett slept in Mill Court Flats on somebody
117
+ // else's lease. A residence a district authors FOR A TIER is the
118
+ // tenant's and nobody else's; any other Residence room (the runtime
119
+ // hole, a bench's `home` fixture) keeps the room-type answer.
120
+ const foreignFlat = room.residenceTier !== undefined && room !== this.game?.homeRoom;
121
+ const isResidence = room.roomType?.toLowerCase() === 'residence' && !foreignFlat;
113
122
  if (actor.stunTaken === 0 && actor.damageTaken === 0 && damagedDevices.length === 0 && damagedDrones.length === 0 && damagedImplants.length === 0) {
114
123
  // Nothing to heal -- but sleeping is its own point now (needs
115
124
  // system: edge refills only fed-and-slept, see Game.
@@ -83,6 +83,23 @@ export class TravelCommand extends Command {
83
83
  return `The driver takes the ${charge.paid} you have and doesn't ask twice.`;
84
84
  return `The fare takes its ${fare}.`;
85
85
  }
86
+ /**
87
+ * A CAB TAKES BODIES (6pdp3ELhsT8ihgtwH: "when using travel while
88
+ * projecting, my body is moved to the district, and am no longer
89
+ * projecting -- and when I return there is a duplicate body"). The
90
+ * ride moved the whole runner while the form was out, and bodyRoom
91
+ * kept a flesh in the room it had just left. call.ts's taxi has always
92
+ * refused off the meat plane; the ride across the sprawl is the same
93
+ * cab, so it asks the same question.
94
+ */
95
+ offPlaneRefusal() {
96
+ switch (this.actor.plane) {
97
+ case 'astral': return `Cabs take bodies, not ghosts -- "return" first.`;
98
+ case 'matrix': return `Cabs take bodies, not personas -- "jack out" first.`;
99
+ case 'drone': return `Your body is slumped where you left it and the frame can't hail a cab -- "jump out" first.`;
100
+ default: return undefined;
101
+ }
102
+ }
86
103
  async execute(args = []) {
87
104
  const game = this.game;
88
105
  // "travel to the safehouse" is "travel safehouse" (player 2026-09-25:
@@ -113,6 +130,9 @@ export class TravelCommand extends Command {
113
130
  if (!goingHome && destination === undefined) {
114
131
  return `Nowhere called "${wanted}" that a cab will take you.${hint(' ("travel" on its own lists the districts; "travel home" leaves.)')}`;
115
132
  }
133
+ const offPlane = this.offPlaneRefusal();
134
+ if (offPlane)
135
+ return offPlane;
116
136
  if (!game.onLeaveSpaceRequested)
117
137
  return `The door sticks -- the way home is not answering. Try again in a moment.`;
118
138
  if (inLiveCombat(this.scene, this.actor))
@@ -173,6 +193,9 @@ export class TravelCommand extends Command {
173
193
  return `No crew, no table -- seat a table or take a seat first.${hint(' ("crew invite <runner>", or accept an invite.)')}`;
174
194
  if (!this.scene.isHumanControlled(this.actor))
175
195
  return `The crew's table is for the crew, not the help.`;
196
+ const offPlane = this.offPlaneRefusal();
197
+ if (offPlane)
198
+ return offPlane;
176
199
  if (game.hasOpenRun)
177
200
  return `Not with a job left open -- finish it or walk out first.`;
178
201
  if (inLiveCombat(this.scene, this.actor))
@@ -251,6 +274,9 @@ export class TravelCommand extends Command {
251
274
  ? `You are home.`
252
275
  : `You are already in ${districts.find(d => d.id === targetId)?.label ?? targetId}.`;
253
276
  }
277
+ const offPlane = this.offPlaneRefusal();
278
+ if (offPlane)
279
+ return offPlane;
254
280
  // THE FARE IS READ OFF BOTH ENDS, so it has to be computed before the move
255
281
  // -- afterwards `districtId` is the destination and the origin is gone.
256
282
  const hereRating = game.districtPressure?.neighborhood;
@@ -3380,5 +3380,21 @@
3380
3380
  // - A mana barrier between adds its Force to defence/soak; a physical
3381
3381
  // barrier stops an indirect spell, which hits the face instead.
3382
3382
  // - Fireball drains F - 1 (catalog v36).
3383
- export const ENGINE_VERSION = '1.198.0';
3383
+ // 1.199.0 (2026-10-05): SPELLS NEED SIGHT (p.281, p.283, p.178). A
3384
+ // direct bolt at someone the caster cannot see -- off their plane or
3385
+ // hidden in cover (visibleActorsIn) -- is refused before any drain; an
3386
+ // indirect one flies blind at -6 (Blind Fire). Direct area spells pass
3387
+ // over anyone hidden in cover inside them.
3388
+ // 1.200.0 (2026-10-05): A PHYSICAL BARRIER IS LIGHT FOG (p.294): a spell
3389
+ // or ranged attack whose line crosses a standing face is at -1, the
3390
+ // Light Fog row of the Environmental Modifiers table (p.175; confirmed
3391
+ // against the rules index and SR5 references online). Melee is exempt.
3392
+ // 1.201.0 (2026-10-05): A CAB TAKES BODIES (6pdp3ELhsT8ihgtwH): `travel`
3393
+ // refuses a runner who is projecting, jacked in or jumped in, as `call
3394
+ // taxi` always has -- the ride moved the form and left a body behind.
3395
+ // SOMEBODY ELSE'S FLAT IS LOCKED (kL23W3ufgsF5MZ2bt): an authored
3396
+ // residence that is not the runner's home has its street door locked
3397
+ // (Game.lockForeignResidences; opens from inside), and `rest` counts
3398
+ // only the runner's own home as a bed.
3399
+ export const ENGINE_VERSION = '1.201.0';
3384
3400
  //# sourceMappingURL=engine-version.js.map
@@ -3522,6 +3522,7 @@ export default class Game {
3522
3522
  // cosmetics below, ask whether this district authors a building for
3523
3523
  // the tier now being paid for, and move house if it does.
3524
3524
  this.rebindHomeForTier();
3525
+ this.lockForeignResidences();
3525
3526
  /**
3526
3527
  * AN AUTHORED RESIDENCE IS THE SEED'S, NOT THE TIER TABLE'S.
3527
3528
  *
@@ -3634,6 +3635,50 @@ export default class Game {
3634
3635
  return Object.values(this.scene?.getRooms?.() ?? {})
3635
3636
  .find(r => r.residenceTier === this.lifestyle.name);
3636
3637
  }
3638
+ /**
3639
+ * SOMEBODY ELSE'S FLAT IS LOCKED (kL23W3ufgsF5MZ2bt: "I'm able to enter
3640
+ * and use an apartment for a lifestyle I don't pay for").
3641
+ *
3642
+ * Every district above Tacoma authors one residence, for its own tier,
3643
+ * and that room used to stand open to anyone on the street: a Street
3644
+ * runner visiting Everett walked into Mill Court Flats and slept in its
3645
+ * bed. The address is the lease's, so the door is locked to everyone
3646
+ * but its tenant -- and unlocked again the day the lease is theirs.
3647
+ * Only the locks this put on are ever lifted: a door a seed or a device
3648
+ * locked keeps its own reasons.
3649
+ *
3650
+ * Breaking in is still a way in (breach); this is a lock, not a wall.
3651
+ */
3652
+ lockForeignResidences() {
3653
+ const rooms = Object.values(this.scene?.getRooms?.() ?? {});
3654
+ for (const room of rooms) {
3655
+ if (room.residenceTier === undefined)
3656
+ continue;
3657
+ const ours = room === this._homeRoom;
3658
+ const doors = new Set();
3659
+ for (const r of rooms) {
3660
+ for (const ex of r.exits.values()) {
3661
+ if (ex instanceof Door && (ex.roomA === room || ex.roomB === room))
3662
+ doors.add(ex);
3663
+ }
3664
+ }
3665
+ for (const door of doors) {
3666
+ // YOUR OWN DOOR: lifted whether or not this session put the lock
3667
+ // on -- a hub overlay restores isLocked but not the flag, so a
3668
+ // flat locked on a visit and moved into later would otherwise
3669
+ // come back shut on its tenant. Keyed and device-held locks are
3670
+ // the seed's, and stay.
3671
+ if (ours && door.isLocked && !door.heldBy && (door.tenantLocked || !door.unlockItemOrSolution)) {
3672
+ door.isLocked = false;
3673
+ door.tenantLocked = false;
3674
+ }
3675
+ else if (!ours && !door.isLocked && !door.heldBy) {
3676
+ door.isLocked = true;
3677
+ door.tenantLocked = true;
3678
+ }
3679
+ }
3680
+ }
3681
+ }
3637
3682
  rebindHomeForTier() {
3638
3683
  const current = this._homeRoom;
3639
3684
  if (current === undefined)
@@ -5750,6 +5795,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5750
5795
  this.gangedRooms.clear();
5751
5796
  this.peopledRooms.clear();
5752
5797
  this.patrolledRooms.clear();
5798
+ // Arrived in a district with somebody else's flat on it (or your own).
5799
+ this.lockForeignResidences();
5753
5800
  const arrivalLines = this.unfoldCompanionsOnArrival();
5754
5801
  this.repaintForScene();
5755
5802
  this.refreshLogLabel();
@@ -6450,6 +6497,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6450
6497
  // everything stored since.
6451
6498
  const world = elsewhere ? this._hubArchive[here] : save.hub;
6452
6499
  applyHubOverlay(this.scene, world ? { ...world, home: save.hub.home } : save.hub, this.player, this._homeRoom, here);
6500
+ // The overlay restores door locks; whose flat is whose is decided now.
6501
+ this.lockForeignResidences();
6453
6502
  this._homeDistrictId = save.game.homeDistrictId;
6454
6503
  this._birthDistrictId = save.game.birthDistrictId;
6455
6504
  this._tier = save.game.tier;
@@ -8,6 +8,10 @@ export class Door extends AbstractDoor {
8
8
  unlockItemOrSolution;
9
9
  /** Set when a Device holds this door shut -- see IDoorHolder. */
10
10
  heldBy;
11
+ /** Locked because it is the street door of a residence that is not the
12
+ * runner's own (Game.lockForeignResidences) -- the lock that lifts the
13
+ * day the lease is theirs. */
14
+ tenantLocked;
11
15
  logger;
12
16
  isLockable;
13
17
  constructor(config) {
@@ -35,6 +35,11 @@ export function forceForWall(a, b) {
35
35
  }
36
36
  /** The directional wall is three cells across: Force 3. */
37
37
  export const DIRECTIONAL_WALL_FORCE = 3;
38
+ /**
39
+ * Seeing through a Physical Barrier: "equivalent to Light Fog" (p.294),
40
+ * the Light Fog row of the Environmental Modifiers table -- -1 (p.175).
41
+ */
42
+ export const BARRIER_FOG = -1;
38
43
  /** A dome with no Force named rises one full 3 m storey: Force 3. */
39
44
  export const DOME_FORCE = METERS_PER_LEVEL;
40
45
  /**
@@ -1,4 +1,4 @@
1
- import { allBreached, damageFace, penetrationBoxes, pierces } from './barrier-geometry.js';
1
+ import { allBreached, damageFace, penetrationBoxes, pierces, BARRIER_FOG } from './barrier-geometry.js';
2
2
  import { recoilCompensation, recoilPenalty } from './recoil.js';
3
3
  import { Category } from '../types/shared/item-enum.js';
4
4
  import { crashIC } from './ic-actors.js';
@@ -648,6 +648,20 @@ export class CombatExchange {
648
648
  const spatial = isMelee ? { meters: 0 } : this.spatialContext(attacker, defender);
649
649
  const weaponClass = strike.weaponItem?.isFirearm() ? classifyWeapon(strike.weaponItem) : undefined;
650
650
  const range = weaponClass ? rangePenalty(spatial.meters, weaponClass) : 0;
651
+ // THROUGH A PHYSICAL BARRIER (p.294: it is "equivalent to Light Fog"
652
+ // and impedes spellcasting only by visibility): the Light Fog row of
653
+ // the Environmental Modifiers table, -1 (p.175), on a ranged attack or
654
+ // a spell whose line crosses a standing face. Melee never reaches
655
+ // through one. Only the worst condition counts (p.173), and this is
656
+ // the only environmental condition the engine models, so it stands
657
+ // alone.
658
+ let fog = 0;
659
+ if (!isMelee) {
660
+ const a = this.scene.worldCellOf?.(attacker);
661
+ const d = this.scene.worldCellOf?.(defender);
662
+ if (a && d && this.scene.barrierOnLine?.(a, d, 'physical'))
663
+ fog = BARRIER_FOG;
664
+ }
651
665
  const rangeMod = range === 'out-of-range' ? -6 : range;
652
666
  // p.190 grants the bonus to a defender who "uses a Take Cover
653
667
  // action" -- so the dice follow the ACTION, not the address.
@@ -692,7 +706,7 @@ export class CombatExchange {
692
706
  // charging, a running defender, surprise, Full Defense and the
693
707
  // dodge/block/parry interrupts -- see turnModifiers below.
694
708
  const turn = this.turnModifiers(attacker, defender, isMelee, strike, meta);
695
- const attack = rollPool(strike.pool + directed + rangeMod + attackerFloor + targetDown + turn.attack + recoil, attackLimit);
709
+ const attack = rollPool(strike.pool + directed + rangeMod + attackerFloor + targetDown + turn.attack + recoil + fog, attackLimit);
696
710
  // A DIRECT SPELL IS NOT DODGED (p.283): the target opposes the
697
711
  // casting test with Body or Willpower, and only counterspelling (and
698
712
  // a teammate's inspiration) helps -- not cover, posture, surprise or
@@ -708,7 +722,7 @@ export class CombatExchange {
708
722
  const recoilNote = recoil < 0 && strike.weaponItem
709
723
  ? ` (${recoil} recoil -- round ${attacker.recoilRoundsFired} against RC ${recoilCompensation(attacker, strike.weaponItem)})`
710
724
  : '';
711
- meta.push(` Attack: ${formatRoll(attack)}${directed > 0 ? ` (+${directed} directed)` : ''}${rangeNote}${recoilNote}`);
725
+ meta.push(` Attack: ${formatRoll(attack)}${directed > 0 ? ` (+${directed} directed)` : ''}${rangeNote}${recoilNote}${fog ? ` (${fog} through a force barrier, Light Fog p.175)` : ''}`);
712
726
  const coverNote = coverDice > 0 ? ` (+${coverDice} cover -- the ${spotOf(defender)})` : '';
713
727
  meta.push(` Defense: ${formatRoll(defense)}${inspired > 0 ? ` (+${inspired} inspired)` : ''}${counterspell > 0 ? ` (+${counterspell} counterspell${counterspellSource ? ` -- ${counterspellSource}'s cover` : ''})` : ''}${coverNote}`);
714
728
  const dryWarning = strike.weaponItem?.isFirearm() && strike.weaponItem.ammo === 0
@@ -1643,6 +1643,10 @@ export function lockSeenFromOutside(building, side, door) {
1643
1643
  return ` ${device.name} holds it, and it is worked from inside.`;
1644
1644
  if (door.heldBy)
1645
1645
  return ` It's barred from ${door.heldBy.room} -- ${door.heldBy.device} is in there.`;
1646
+ // SOMEBODY'S FLAT (kL23W3ufgsF5MZ2bt): a residence that is not yours
1647
+ // is locked to you; Game.lockForeignResidences put the lock on.
1648
+ if (door.tenantLocked)
1649
+ return ` Somebody's flat -- the lease on it isn't yours, and neither is the key.`;
1646
1650
  if (door.unlockItemOrSolution)
1647
1651
  return ` It's keyed -- it takes ${door.unlockItemOrSolution}, from inside.`;
1648
1652
  return ` Bolted from the inside -- nothing on this side of it to work.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.386.0",
3
+ "version": "5.389.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.",