@maka/maka-cli 5.134.0 → 5.136.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.
Files changed (44) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/aim.js +60 -0
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +132 -3
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js +12 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +102 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/cover.js +24 -6
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/defend.js +74 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/delay.js +34 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +5 -0
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js +5 -0
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js +11 -2
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/end-turn.js +58 -0
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +6 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +14 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +15 -0
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/heal.js +6 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/initiative.js +20 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +42 -59
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/posture.js +23 -7
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +31 -10
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +6 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +16 -15
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +5 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js +10 -3
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +5 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js +6 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +8 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +18 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/game.js +35 -18
  30. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +3 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +21 -4
  32. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +44 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +50 -13
  34. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +102 -5
  35. package/bundle/typescript/src/commands/game/sideQuest/ui.js +12 -12
  36. package/bundle/typescript/src/commands/game/sideQuest/utilities/action-budget.js +87 -0
  37. package/bundle/typescript/src/commands/game/sideQuest/utilities/action-cost.js +66 -0
  38. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +101 -5
  39. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +571 -0
  40. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +20 -0
  41. package/bundle/typescript/src/commands/game/sideQuest/utilities/movement-cost.js +80 -0
  42. package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +122 -0
  43. package/package.json +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +0 -182
@@ -26,6 +26,9 @@ const MENU_VERBS = new Set([
26
26
  // The sprite database reads like the spellbook, so it rides the same
27
27
  // panel -- reference you consult, not a menu you spend from.
28
28
  'sprites',
29
+ // The initiative tracker (commands/initiative.ts) is a reference
30
+ // panel too: who acts when, this Combat Turn.
31
+ 'initiative', 'tracker', 'turn',
29
32
  ]);
30
33
  // Windows Terminal draws astral-plane emoji (📞 call icon, 📍 HUD pin,
31
34
  // 👥 companions, 💀) TWO columns wide, but blessed's 2015-era unicode
@@ -287,9 +290,10 @@ export async function main(playerName, sceneSeed, options) {
287
290
  game.updateInventory(game.player.inventory, capitalCase(game.scene.getCurrencyType()));
288
291
  game.updateExits(game.player.currentLocation);
289
292
  game.updateStatus();
290
- // Tab cycles the fight autopilot: manual -> guarded -> aggressive (see
291
- // utilities/auto-fight.ts; "stance" is the typed twin). Bound on the
292
- // input box because that's where focus lives during play.
293
+ // Tab flips the stance: lethal <-> non-lethal in the meat, Brute
294
+ // Force <-> Hack on the Fly on the Matrix (models/player.ts; "stance"
295
+ // is the typed twin). Bound on the input box because that's where
296
+ // focus lives during play.
293
297
  game.inputBox.key(['tab'], () => {
294
298
  // Blessed's textarea reader appends the literal '\t' to the box
295
299
  // value on this same keypress (listener order isn't guaranteed
@@ -315,7 +319,7 @@ export async function main(playerName, sceneSeed, options) {
315
319
  // bar"). game.player here is the LOCAL HUB player -- in a shared
316
320
  // run the runner on the table is a different object in a
317
321
  // different process, so cycling this one moved nothing the
318
- // autopilot reads and nothing the HUD paints (updateStatus
322
+ // engine reads and nothing the HUD paints (updateStatus
319
323
  // early-returns on remoteSession; the status box belongs to the
320
324
  // server's events). Send the typed twin instead and let the
321
325
  // engine cycle from the ACTOR's real stance.
@@ -323,10 +327,10 @@ export async function main(playerName, sceneSeed, options) {
323
327
  const res = await game.handleInput('stance next');
324
328
  if (!res)
325
329
  return;
326
- // white for manual, not gray -- gray is ANSI bright-black,
327
- // illegible on stock dark palettes (same fix as the HUD stance
328
- // in game.ts). The Matrix routes borrow the same temperature:
329
- // Brute Force is the loud one, Hack on the Fly the careful one.
330
+ // Red for the lethal routes, yellow for the careful ones (same
331
+ // palette as the HUD stance in game.ts): Brute Force is the loud
332
+ // one, Hack on the Fly the careful one. Never gray -- ANSI
333
+ // bright-black is illegible on stock dark palettes.
330
334
  const named = res.match(/(?:Matrix s|S)tance:\s*([\w-]+)/i)?.[1]?.toLowerCase();
331
335
  const c = named === 'lethal' || named === 'brute' ? 'red'
332
336
  : named === 'non-lethal' || named === 'non' || named === 'hack' ? 'yellow'
@@ -366,10 +370,6 @@ export async function main(playerName, sceneSeed, options) {
366
370
  // still a command you typed and will want to edit and retry.
367
371
  if (opts.recordHistory !== false)
368
372
  history.remember(input);
369
- // Typing takes over: any submitted command cancels a pending
370
- // auto-fight action (the interruptible beat -- see
371
- // utilities/auto-fight.ts). The next settled exchange re-arms it.
372
- game.autoFight.cancelPending();
373
373
  // Clear the box the moment the command is submitted, not after
374
374
  // it resolves -- a slow command (combat narration beats, an AI
375
375
  // round trip on a call) used to leave the typed text sitting in
@@ -0,0 +1,87 @@
1
+ export class ActionBudget {
2
+ /** Free Actions left this phase (one, p.158). */
3
+ free = 1;
4
+ /** Simple Actions left. A Complex Action spends both at once. */
5
+ simple = 2;
6
+ /** Whether an attack action has been taken this phase (p.164). */
7
+ attackTaken = false;
8
+ /** Everything spent so far, in order, for the tracker. */
9
+ spent = [];
10
+ /** A Complex Action needs the whole phase: both Simples unspent. */
11
+ get complexAvailable() {
12
+ return this.simple === 2;
13
+ }
14
+ /** Nothing left but to end the turn. */
15
+ get exhausted() {
16
+ return this.simple === 0 && this.free === 0;
17
+ }
18
+ /** The Simple/Complex half is spent -- only a Free Action remains. */
19
+ get actionsExhausted() {
20
+ return this.simple === 0;
21
+ }
22
+ /**
23
+ * Why this spend would be refused, or undefined when it is affordable.
24
+ * Checking never spends; spend() calls this first.
25
+ */
26
+ refusalFor(kind, attack = false) {
27
+ if (kind === 'free') {
28
+ if (this.free <= 0)
29
+ return `You've already used your Free Action this Action Phase (SR5 p.158 -- one per phase).`;
30
+ return undefined;
31
+ }
32
+ if (attack && this.attackTaken) {
33
+ return `You've already attacked this Action Phase -- only one of its actions can be an attack (SR5 p.164). "end turn" and go again next pass.`;
34
+ }
35
+ if (kind === 'simple') {
36
+ if (this.simple <= 0) {
37
+ return `You've spent both Simple Actions this Action Phase (SR5 p.163)${this.spentComplex() ? ' -- a Complex Action takes the whole phase (p.167)' : ''}. "end turn" to let the next combatant act.`;
38
+ }
39
+ return undefined;
40
+ }
41
+ // complex
42
+ if (!this.complexAvailable) {
43
+ return this.simple === 0
44
+ ? `Nothing left this Action Phase -- a Complex Action needs the whole phase (SR5 p.167). "end turn".`
45
+ : `A Complex Action takes the WHOLE Action Phase (SR5 p.167), and you've already spent a Simple Action. Use the other Simple, or "end turn".`;
46
+ }
47
+ return undefined;
48
+ }
49
+ /** Spends the action; returns the refusal instead when it cannot be paid. */
50
+ spend(kind, label, attack = false) {
51
+ const refusal = this.refusalFor(kind, attack);
52
+ if (refusal)
53
+ return refusal;
54
+ if (kind === 'free')
55
+ this.free -= 1;
56
+ else if (kind === 'simple')
57
+ this.simple -= 1;
58
+ else
59
+ this.simple = 0;
60
+ if (attack)
61
+ this.attackTaken = true;
62
+ this.spent.push({ kind, label, attack: attack || undefined });
63
+ return undefined;
64
+ }
65
+ spentComplex() {
66
+ return this.spent.some(s => s.kind === 'complex');
67
+ }
68
+ /** One line for the HUD: what is still on the table. */
69
+ describe() {
70
+ const parts = [];
71
+ if (this.simple === 2)
72
+ parts.push('2 Simple (or 1 Complex)');
73
+ else if (this.simple === 1)
74
+ parts.push('1 Simple');
75
+ else
76
+ parts.push('no Simple/Complex left');
77
+ parts.push(this.free > 0 ? '1 Free' : 'no Free');
78
+ return parts.join(' · ');
79
+ }
80
+ /** The phase's ledger, e.g. "Ready Weapon (Simple), Fire (Simple, attack)". */
81
+ ledger() {
82
+ if (this.spent.length === 0)
83
+ return 'nothing yet';
84
+ return this.spent.map(s => `${s.label} (${s.kind === 'free' ? 'Free' : s.kind === 'simple' ? 'Simple' : 'Complex'}${s.attack ? ', attack' : ''})`).join(', ');
85
+ }
86
+ }
87
+ //# sourceMappingURL=action-budget.js.map
@@ -0,0 +1,66 @@
1
+ import { hint } from './hints.js';
2
+ /** The encounter this actor is fighting in, if any. */
3
+ export function encounterOf(scene, actor) {
4
+ return scene.encounterFor?.(actor);
5
+ }
6
+ /**
7
+ * The refusal when it is not this actor's Action Phase -- the wording
8
+ * every verb shares, so a player learns one sentence.
9
+ */
10
+ export function notYourPhase(enc, actor) {
11
+ const who = enc.phaseActor;
12
+ if (who && who !== actor) {
13
+ return `It's ${who.name}'s Action Phase -- yours comes round at Initiative ${enc.scoreOf(actor)}.${hint(` ("initiative" shows the order.)`)}`;
14
+ }
15
+ return `The pass is resolving -- your Action Phase comes round at Initiative ${enc.scoreOf(actor)}.`;
16
+ }
17
+ /**
18
+ * Pays for an action, or explains why it cannot be paid. Undefined means
19
+ * "go ahead" -- out of combat, or in combat with the action affordable
20
+ * and now spent.
21
+ */
22
+ export function billAction(scene, actor, kind, label, opts = {}) {
23
+ const enc = encounterOf(scene, actor);
24
+ if (!enc || enc.ended)
25
+ return undefined;
26
+ if (!enc.isPhaseOf(actor))
27
+ return notYourPhase(enc, actor);
28
+ const budget = enc.budgetOf(actor);
29
+ if (!budget)
30
+ return notYourPhase(enc, actor);
31
+ const refusal = budget.spend(kind, label, opts.attack === true);
32
+ if (refusal)
33
+ return refusal;
34
+ // p.166: aiming is lost "if the character takes any other kind of
35
+ // action (including a Free Action) before attacking". An attack
36
+ // consumes the bonus at the roll (combat-exchange.ts), so it is left
37
+ // in place here for the strike to read.
38
+ if (!opts.aiming && !opts.attack)
39
+ actor.clearAim();
40
+ scene.updateStatus?.();
41
+ return undefined;
42
+ }
43
+ /**
44
+ * A verb that is refused outright mid-fight unless it is your phase --
45
+ * for things that are not actions at all but still cannot happen while
46
+ * someone else is acting (walking out of the room, say).
47
+ */
48
+ export function requirePhase(scene, actor) {
49
+ const enc = encounterOf(scene, actor);
50
+ if (!enc || enc.ended)
51
+ return undefined;
52
+ if (!enc.isPhaseOf(actor))
53
+ return notYourPhase(enc, actor);
54
+ return undefined;
55
+ }
56
+ /** What is left this phase, for a verb's trailing hint. */
57
+ export function phaseHint(scene, actor) {
58
+ const enc = encounterOf(scene, actor);
59
+ const budget = enc?.budgetOf(actor);
60
+ if (!enc || !budget)
61
+ return '';
62
+ return budget.actionsExhausted
63
+ ? hint(` (${budget.free > 0 ? 'A Free Action left, then ' : ''}"end turn".)`)
64
+ : hint(` (${budget.describe()} left.)`);
65
+ }
66
+ //# sourceMappingURL=action-cost.js.map
@@ -308,6 +308,8 @@ export class CombatExchange {
308
308
  if (lines.length > 0)
309
309
  this.logger.log(`\n${lines.join('\n')}`, fightScope);
310
310
  this.scene.updateStatus();
311
+ if (process.env.MAKA_NO_BEATS === '1')
312
+ return;
311
313
  await new Promise(resolve => setTimeout(resolve, CombatExchange.BEAT_MS));
312
314
  }
313
315
  /**
@@ -454,6 +456,94 @@ export class CombatExchange {
454
456
  * rebuild AI-history reports as [...world, ...meta] so an NPC's
455
457
  * memory of the fight keeps the dice.
456
458
  */
459
+ /**
460
+ * WHAT THE COMBAT TURN ADDS TO ONE STRIKE (RAG-checked 2026-09-06):
461
+ * - TAKE AIM (p.166): +1 per aim on the shot it was for, capped at
462
+ * half Willpower; consumed here whether or not it was for this
463
+ * target (aiming at one person then shooting another wastes it).
464
+ * - CHARGING (p.186): a running attacker closing into melee gets +4
465
+ * -- net +2 with the -2 running penalty already in the pool.
466
+ * - DEFENDER RUNNING (p.189 Defense Modifiers): +2 to defend
467
+ * against a ranged attack.
468
+ * - SURPRISE (p.192): a surprised defender gets no Defense Test at
469
+ * all against someone who is not.
470
+ * - FULL DEFENSE (p.191): +Willpower for the rest of the turn.
471
+ * - DODGE / BLOCK / PARRY (p.168): -5 Initiative each, +Gymnastics /
472
+ * +Unarmed / +weapon skill on this one test, by the defender's
473
+ * standing policy (commands/defend.ts), and only when the score
474
+ * can pay.
475
+ * Outside an encounter only the aim, charge and running lines apply --
476
+ * they are properties of the actors, not of the turn structure.
477
+ */
478
+ turnModifiers(attacker, defender, isMelee, strike, meta) {
479
+ let attack = 0;
480
+ let defense = 0;
481
+ if (attacker.aimBonus > 0 && !isMelee && !strike.isSpell) {
482
+ if (!attacker.aimTarget || attacker.aimTarget === defender.name) {
483
+ const aimed = Math.min(attacker.aimBonus, attacker.aimCap);
484
+ attack += aimed;
485
+ meta.push(` Aimed: +${aimed} (Take Aim, p.166)`);
486
+ }
487
+ else {
488
+ meta.push(` The aim on ${attacker.aimTarget} is wasted on ${defender.name}.`);
489
+ }
490
+ attacker.clearAim();
491
+ }
492
+ if (isMelee && !strike.isSpell && attacker.isRunningThisTurn) {
493
+ attack += 4;
494
+ meta.push(` Charging: +4 (p.186; the -2 for running is already in the pool)`);
495
+ }
496
+ if (!isMelee && defender.isRunningThisTurn) {
497
+ defense += 2;
498
+ meta.push(` ${defender.name} is running: +2 to defend (p.189)`);
499
+ }
500
+ const enc = this.scene.encounterFor?.(defender) ?? this.scene.encounterFor?.(attacker);
501
+ if (!enc)
502
+ return { attack, defense, noDefense: false };
503
+ if (enc.isSurprised(defender) && !enc.isSurprised(attacker)) {
504
+ meta.push(` ${defender.name} is SURPRISED -- no Defense Test (p.192).`);
505
+ return { attack, defense, noDefense: true };
506
+ }
507
+ if (enc.fullDefenseActive(defender)) {
508
+ defense += defender.willpower;
509
+ meta.push(` Full Defense: +${defender.willpower} Willpower (p.191)`);
510
+ }
511
+ const policy = defender.defensePolicy;
512
+ if (policy !== 'none' && (defender.plane === 'meat' || defender.plane === 'drone')) {
513
+ let skill = 0;
514
+ let label = '';
515
+ let source = '';
516
+ if (policy === 'dodge') {
517
+ skill = defender.skillRating('gymnastics');
518
+ label = 'Dodge';
519
+ source = 'Gymnastics';
520
+ }
521
+ else if (policy === 'block' && isMelee) {
522
+ skill = defender.skillRating('unarmed');
523
+ label = 'Block';
524
+ source = 'Unarmed Combat';
525
+ }
526
+ else if (policy === 'parry' && isMelee) {
527
+ const held = defender.weaponDrawn ? defender.getCarriedWeapon() : null;
528
+ if (held && !held.isFirearm()) {
529
+ skill = defender.skillRating(defender.activeCombatSkill());
530
+ label = 'Parry';
531
+ source = 'weapon skill';
532
+ }
533
+ }
534
+ if (label && skill > 0) {
535
+ const refusal = enc.spendInterrupt(defender, 5, label);
536
+ if (refusal) {
537
+ meta.push(` ${label} not taken: ${refusal}`);
538
+ }
539
+ else {
540
+ defense += skill;
541
+ meta.push(` ${label}: +${skill} (${source}, -5 Initiative, p.168)`);
542
+ }
543
+ }
544
+ }
545
+ return { attack, defense, noDefense: false };
546
+ }
457
547
  resolveStrike(attacker, defender, profile) {
458
548
  const strike = profile ?? this.weaponProfile(attacker, defender);
459
549
  const world = [];
@@ -564,10 +654,16 @@ export class CombatExchange {
564
654
  const attackLimit = strike.isSpell
565
655
  ? strike.limit
566
656
  : (strike.limit ?? attacker.getAttackLimit(strike.weaponItem));
567
- const attack = rollPool(strike.pool + directed + rangeMod + attackerFloor + targetDown, attackLimit);
568
- const defense = rollPool(defender.getDefensePool() + inspired + counterspell + coverDice + proneDefence);
569
657
  const header = `${attacker.name} attacks ${defender.name} ${strike.label}`;
570
658
  meta.push(`${header}:`);
659
+ // THE COMBAT TURN'S MODIFIERS (utilities/combat-turn.ts): Take Aim,
660
+ // charging, a running defender, surprise, Full Defense and the
661
+ // dodge/block/parry interrupts -- see turnModifiers below.
662
+ const turn = this.turnModifiers(attacker, defender, isMelee, strike, meta);
663
+ const attack = rollPool(strike.pool + directed + rangeMod + attackerFloor + targetDown + turn.attack, attackLimit);
664
+ const defense = turn.noDefense
665
+ ? rollPool(0)
666
+ : rollPool(defender.getDefensePool() + inspired + counterspell + coverDice + proneDefence + turn.defense);
571
667
  const rangeNote = weaponClass && spatial.meters > 0
572
668
  ? ` (${rangeMod !== 0 ? `${rangeMod} range` : `${rangeBracketName(spatial.meters, weaponClass)} range`} -- ~${Math.round(spatial.meters)} m)`
573
669
  : '';
@@ -899,8 +995,8 @@ export class CombatExchange {
899
995
  this.releasePlayerLocks();
900
996
  this.releaseExchange();
901
997
  this.scene.updateStatus();
902
- // The fight autopilot's trigger: a settled exchange involving the
903
- // player schedules their next auto action (see utilities/auto-fight.ts).
998
+ // A settled exchange: the scene lets a KO'd player's watch learn who
999
+ // is still fighting around them (Scene.notifyExchangeSettled).
904
1000
  this.scene.notifyExchangeSettled(this._combatants);
905
1001
  // A spirit whose services ran out departs once the dust settles
906
1002
  // (RAW p.302: the bargain is spent).
@@ -908,7 +1004,7 @@ export class CombatExchange {
908
1004
  if (this.scene.isHumanControlled(this.actor)) {
909
1005
  return '';
910
1006
  }
911
- if (this.playerWitnesses()) {
1007
+ if (this.playerWitnesses() && process.env.MAKA_NO_BEATS !== '1') {
912
1008
  await new Promise(resolve => setTimeout(resolve, CombatExchange.NPC_FOLLOWUP_PAUSE_MS));
913
1009
  }
914
1010
  return report.join('\n');