@maka/maka-cli 5.151.0 → 5.153.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.151.0",
3
+ "version": "5.153.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.",
@@ -211,12 +211,15 @@ export class AttackCommand extends Command {
211
211
  }
212
212
  // A deliberately INITIATED attack with a dry gun is blocked with a
213
213
  // reload prompt -- the involuntary strikes (retaliation) auto-fall-back
214
- // to fists instead (see CombatExchange.weaponProfile).
214
+ // to fists instead (see CombatExchange.weaponProfile). Only a DRAWN
215
+ // gun is actually "in play" for this swing (2jbprbSYqF7EPXrkR) -- a
216
+ // holstered dry/jammed piece is not what's being attacked with, so it
217
+ // must not block a bare-fisted attack the player never asked to fail.
215
218
  const weaponItem = this.actor.equipment.rightHand?.weapon ?? this.actor.equipment.leftHand?.weapon;
216
- if (this.usesEquippedWeapon() && weaponItem?.isFirearm() && weaponItem.jammed) {
219
+ if (this.usesEquippedWeapon() && this.actor.weaponDrawn && weaponItem?.isFirearm() && weaponItem.jammed) {
217
220
  return `The ${weaponItem.name}'s smartlink is LOCKED -- someone cracked your PAN. "reboot" to clear it, or go in swinging.`;
218
221
  }
219
- if (this.usesEquippedWeapon() && weaponItem?.isFirearm() && weaponItem.ammo <= 0) {
222
+ if (this.usesEquippedWeapon() && this.actor.weaponDrawn && weaponItem?.isFirearm() && weaponItem.ammo <= 0) {
220
223
  return `Click -- the ${weaponItem.name} is dry. "reload" (needs ammunition), or go in swinging with something else.`;
221
224
  }
222
225
  // THE COMBAT TURN (utilities/combat-turn.ts): on the physical planes
@@ -360,11 +363,15 @@ export class AttackCommand extends Command {
360
363
  return notYourPhase(enc, actor);
361
364
  }
362
365
  enc.noteAttack(actor, target);
363
- // READY WEAPON (p.165) is an action, not a beat.
364
- if (this.usesEquippedWeapon() && weaponItem && !actor.weaponDrawn) {
365
- return `The ${weaponItem.name} is holstered -- "draw" it first (Ready Weapon, a Simple Action, SR5 p.165).${phaseHint(this.scene, actor)}`;
366
- }
367
- const isMelee = !this.usesEquippedWeapon() || !weaponItem?.isFirearm();
366
+ // A holstered weapon doesn't back this attack -- SR5 always allows a
367
+ // bare-handed strike, and weaponProfile() already resolves the punch's
368
+ // own pool/damage the moment the weapon isn't drawn (the character
369
+ // sheet advertises it: "Attack ... (bare fists) -- holstered"). Only a
370
+ // DRAWN weapon is what this swing is actually attacking WITH; want to
371
+ // shoot or swing the blade instead? READY WEAPON (p.165) is its own
372
+ // Simple Action -- "draw" first (2jbprbSYqF7EPXrkR).
373
+ const activeWeapon = this.usesEquippedWeapon() && actor.weaponDrawn ? weaponItem : undefined;
374
+ const isMelee = !activeWeapon || !activeWeapon.isFirearm();
368
375
  const report = [];
369
376
  // MELEE REACH: ground costs (p.161-162).
370
377
  if (isMelee) {
@@ -552,6 +552,33 @@ export default class Game {
552
552
  return `{yellow-fg}🔥 Heat: ears up all over the block{/yellow-fg}`;
553
553
  return `{yellow-fg}🔥 Heat: somebody heard something{/yellow-fg}`;
554
554
  }
555
+ /**
556
+ * THE SAME BAND, FOR AN NPC'S MOUTH (YE9G3Q5P85y5xcP7n: "Mr. Ibis said
557
+ * 'Quiet as promised' however, there's a status of: 🔥 Heat: sirens
558
+ * circling closer"). heatBand() paints the player's own HUD; nothing
559
+ * fed the run's actual heat into NPC.respondTo's prompt at all, so an
560
+ * NPC narrating "the coast is clear" had no way to know it wasn't --
561
+ * the same shape as payoutSummary's fix for an invented payout, here
562
+ * for an invented all-clear.
563
+ *
564
+ * DELIBERATELY NOT ITS OWN THRESHOLDS. heat-band-honesty.test.ts pins
565
+ * the top band's heat check to the ONE line inside heatBand(), on
566
+ * purpose: the HUD's promise and checkpointOnRideHome's verifierDice
567
+ * escalation share that number so they cannot drift apart. A second
568
+ * copy of the same comparison here for the AI prompt would be exactly
569
+ * the drift that guard exists to catch. So this reads heatBand()'s OWN
570
+ * text -- strip the color codes and the HUD's "🔥 Heat: " lede, and
571
+ * what is left is plain enough for a prompt -- rather than re-deriving
572
+ * the bands.
573
+ */
574
+ heatSummaryForDialogue() {
575
+ if (!this.onRun)
576
+ return undefined;
577
+ const band = this.heatBand();
578
+ if (!band)
579
+ return undefined;
580
+ return band.replace(/\{[^}]*\}/g, '').replace(/^🔥\s*Heat:\s*/, '');
581
+ }
555
582
  // THE FOR-HIRE BOARD: archetype-built candidates, fresh faces each
556
583
  // homecoming (the gig-pool rotation school). Not saved -- the street's
557
584
  // talent pool churns; whoever you HIRED is the persistent part.
@@ -1660,6 +1660,15 @@ ${worldEventsSummary}
1660
1660
  const payoutSummary = payout
1661
1661
  ? `The agreed payout for the current job is EXACTLY ${payout} nuyen, paid as ONE LUMP SUM on delivery -- there is no advance and no half up front; never speak as if part has already been paid. Any time you mention payment, reward, or what the job is worth, use exactly this figure -- never invent a different one. Payment transfers automatically when the job completes; you never need to hand it over.`
1662
1662
  : '';
1663
+ // HEAT IS MECHANICALLY REAL TOO (YE9G3Q5P85y5xcP7n: an NPC said
1664
+ // "Quiet as promised" while the player's own HUD read "Heat: sirens
1665
+ // circling closer"). Same shape as payoutSummary above -- nothing
1666
+ // told the model the run's actual heat, so a reassuring line had
1667
+ // nothing to check itself against.
1668
+ const heat = this._scene?.ownerGame?.heatSummaryForDialogue?.();
1669
+ const heatSummary = heat
1670
+ ? `The district's current HEAT on this run is ${heat}. If you would naturally reassure them that things are quiet, clean, or under control, that claim has to be TRUE against this -- never tell them the coast is clear, that nobody noticed, or that things went quiet if heat says otherwise. Speak honestly about how hot it actually is instead.`
1671
+ : '';
1663
1672
  // If nothing else has happened to/around this NPC yet this scene, this
1664
1673
  // is the first time anyone's reached out -- nudge toward volunteering
1665
1674
  // the job rather than waiting to be asked, which real playtesting
@@ -2103,6 +2112,7 @@ Your objective is to drive the story line.`}
2103
2112
  ${eph ? '' : objectiveSummary}
2104
2113
  ${dialogueSummary}
2105
2114
  ${eph ? '' : payoutSummary}
2115
+ ${eph ? '' : heatSummary}
2106
2116
 
2107
2117
  ${roomDesc}${spotBlock}${combatSpatialBlock}
2108
2118
  ${formattedExitsStr}
@@ -652,7 +652,37 @@ export class Player extends AbstractPlayer {
652
652
  return true;
653
653
  if (this.arOffline)
654
654
  return false;
655
- return !!this.getPanMaster();
655
+ if (!this.getPanMaster())
656
+ return false;
657
+ return this.hasARDisplay();
658
+ }
659
+ /**
660
+ * THE MISSING HALF OF AR (canon p.53-54/221-222, RAG-checked
661
+ * 2026-09-10, acBi46ZKKJCPyBKXb: "it looks like I'm able to see AR
662
+ * with the Hermes Ikon, however, I don't have a method to interface
663
+ * to it -- no trode, or commlink jack. I think this is a gap"). It
664
+ * was: "a commlink alone does not automatically give you AR -- the
665
+ * commlink processes and sends the information, but you still need a
666
+ * display device to receive it." hasMatrixEyes gated ONLY on a
667
+ * PAN master existing, never on having any way to actually SEE what
668
+ * it sends -- so a runner with a commlink in their pocket and nothing
669
+ * on their face or in their skull got the full overlay regardless.
670
+ *
671
+ * Two independent roads in, matching what the rulebook actually lists:
672
+ * a DNI (trodes/datajack/technomancer -- Player.hasDirectNeuralInterface,
673
+ * "you can use all your senses to view AR without any extra devices"),
674
+ * or a worn visual display (cybereyes, glasses, goggles, contacts --
675
+ * same head.eyes slot and same two categories smartlink detection
676
+ * already reads, Category.Glasses / Category.EyeCyberwere). Audio-only
677
+ * and haptic-only AROs have their own separate gear (earbuds/cyberear,
678
+ * AR gloves, p.222/439) and are not modelled here -- this predicate is
679
+ * specifically the VISUAL overlay hasMatrixEyes has always been about.
680
+ */
681
+ hasARDisplay() {
682
+ if (this.hasDirectNeuralInterface())
683
+ return true;
684
+ const onFace = this._equipment.head?.eyes;
685
+ return !!onFace && (onFace.category === Category.Glasses || onFace.category === Category.EyeCyberwere);
656
686
  }
657
687
  /**
658
688
  * A DIRECT NEURAL INTERFACE -- what VR needs and AR does not (SR5 core
@@ -592,10 +592,21 @@ export class Scene extends AbstractScene {
592
592
  this.logger.write(`Removed actor: ${name}`);
593
593
  // A fallen participant's Action Phase, if it was live, moves on;
594
594
  // the encounter itself re-checks who is still standing on its next
595
- // step (utilities/combat-turn.ts run).
595
+ // step (utilities/combat-turn.ts run). But run()'s own stillHostile()
596
+ // check only fires from ITS OWN loop, which never runs again while a
597
+ // human's Action Phase stays open (they can still move/loot before
598
+ // "end turn") -- so killing the last hostile mid-phase left the fight
599
+ // "still going" until the human explicitly ended their turn
600
+ // (5aqFRn2xPtRYDvNxR). Mirrors leave()'s own direct stillHostile()
601
+ // check for the same "nobody left to fight" shape.
596
602
  const enc = room ? this.encounterIn(room) : undefined;
597
- if (enc && enc.phaseActor === actor) {
598
- void enc.dropPhaseActor(actor);
603
+ if (enc && !enc.ended) {
604
+ if (enc.phaseActor === actor) {
605
+ void enc.dropPhaseActor(actor);
606
+ }
607
+ else if (!enc.stillHostile()) {
608
+ void enc.end('over');
609
+ }
599
610
  }
600
611
  // Tell the Game an actor left the world -- the companion roster
601
612
  // (Game.onActorRemoved) prunes shells and settles their bound
@@ -225,10 +225,17 @@ export function arStatusSlots(actor) {
225
225
  ? `{white-fg}◌ AR OFF{/white-fg}`
226
226
  : !actor.getPanMaster() && !actor.isTechnomancer()
227
227
  ? `{white-fg}◌ no AR rig{/white-fg}`
228
- // Off-plane, or any future state that puts the overlay down
229
- // without being one of the above. game.ts drops the slot
230
- // entirely while off-plane -- a persona has its own HUD line.
231
- : `{white-fg}◌ AR dark{/white-fg}`;
228
+ // A processor with nothing to paint it on (acBi46ZKKJCPyBKXb):
229
+ // a commlink sends the overlay, but nothing here receives it
230
+ // (Player.hasARDisplay -- no DNI, no worn glasses/goggles/
231
+ // cybereyes). Distinct from "no AR rig" -- the player HAS a
232
+ // rig, it just has nowhere to show up.
233
+ : !actor.hasARDisplay()
234
+ ? `{white-fg}◌ no display -- trodes or eyewear{/white-fg}`
235
+ // Off-plane, or any future state that puts the overlay down
236
+ // without being one of the above. game.ts drops the slot
237
+ // entirely while off-plane -- a persona has its own HUD line.
238
+ : `{white-fg}◌ AR dark{/white-fg}`;
232
239
  // The link slot asks for the LIVE LINK -- worn commlink or working
233
240
  // deck -- because that is what call.ts now asks for (p.223: a deck
234
241
  // performs all commlink functions). The old "AR ON | Link offline"
@@ -956,6 +956,15 @@ export class CombatExchange {
956
956
  // icon must drop to ☹ the moment they go down, not on the
957
957
  // player's next step.
958
958
  this.scene.updateExits(room);
959
+ // Same "combat sat there until 'end turn'" gap as removeActor
960
+ // below (5aqFRn2xPtRYDvNxR): a KO'd-not-killed last hostile also
961
+ // stops being present() (isIncapacitated), but nothing re-runs
962
+ // run()'s own stillHostile() check while the killer's own Action
963
+ // Phase stays open. End it directly, the same way leave() does.
964
+ const koEnc = this.scene.encounterFor?.(fallen);
965
+ if (koEnc && !koEnc.ended && koEnc.phaseActor !== fallen && !koEnc.stillHostile()) {
966
+ void koEnc.end('over');
967
+ }
959
968
  return ['', ...lines];
960
969
  }
961
970
  // A BODY ALWAYS FORMS (except for companion shells, which leave
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.151.0",
3
+ "version": "5.153.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.",