@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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/aim.js +60 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +132 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js +12 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +102 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cover.js +24 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/defend.js +74 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/delay.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js +11 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-turn.js +58 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +15 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/heal.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiative.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +42 -59
- package/bundle/typescript/src/commands/game/sideQuest/commands/posture.js +23 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +31 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +16 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js +10 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +18 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +35 -18
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +21 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +50 -13
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +102 -5
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +12 -12
- package/bundle/typescript/src/commands/game/sideQuest/utilities/action-budget.js +87 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/action-cost.js +66 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +101 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +571 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/movement-cost.js +80 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +122 -0
- package/package.json +1 -1
- 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
|
|
291
|
-
//
|
|
292
|
-
// input box because that's where
|
|
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
|
-
//
|
|
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
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
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
|
-
//
|
|
903
|
-
//
|
|
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');
|