@maka/maka-cli 5.386.0 → 5.388.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.388.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.`);
@@ -3380,5 +3380,14 @@
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
+ export const ENGINE_VERSION = '1.200.0';
3384
3393
  //# sourceMappingURL=engine-version.js.map
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.386.0",
3
+ "version": "5.388.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.",