@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
3
3
|
import { hint } from '../utilities/hints.js';
|
|
4
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
4
5
|
/**
|
|
5
6
|
* Conjuring (SR5e p.300-303): "summon <force>" calls a spirit -- an
|
|
6
7
|
* opposed Magic + conjuring test against the spirit's Force dice. NET
|
|
@@ -50,6 +51,10 @@ export class SummonCommand extends Command {
|
|
|
50
51
|
return `A Force ${force} spirit would wear YOU as the vessel -- twice your Magic (${actor.magic * 2}) is the hard ceiling.`;
|
|
51
52
|
}
|
|
52
53
|
const overcast = force > actor.magic;
|
|
54
|
+
// SUMMONING IS A COMPLEX ACTION (SR5 p.167) inside a Combat Turn.
|
|
55
|
+
const bill = billAction(this.scene, actor, 'complex', 'Summoning');
|
|
56
|
+
if (bill)
|
|
57
|
+
return bill;
|
|
53
58
|
// The opposed test (p.300): Magic + conjuring vs the spirit's Force.
|
|
54
59
|
const conjuring = actor.skillRating('conjuring');
|
|
55
60
|
const mine = rollPool(Math.max(1, actor.magic + (conjuring > 0 ? conjuring : -1) + actor.woundModifier - actor.sustainingPenalty));
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
2
3
|
/**
|
|
3
4
|
* Throwing your hands up, as a real mechanical act instead of just talk.
|
|
4
5
|
* The wound-aware AI guidance already nudges a losing NPC toward
|
|
5
|
-
* surrender -- this gives that nudge teeth: the flag it sets is what
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* surrender -- this gives that nudge teeth: the flag it sets is what the
|
|
7
|
+
* Combat Turn reads to decide whether anyone is still hostile
|
|
8
|
+
* (CombatEncounter.stillHostile) and what "restrain" holds without a
|
|
9
|
+
* roll (p.195). Surrendering also breaks the fight:
|
|
8
10
|
* both sides' combat memory clears, so walking away afterwards invites no
|
|
9
11
|
* parting shot. The flag is revoked the moment the surrendered actor
|
|
10
12
|
* initiates violence again (CombatExchange.beginExchange).
|
|
@@ -27,6 +29,11 @@ export class SurrenderCommand extends Command {
|
|
|
27
29
|
if (actor.inExchange) {
|
|
28
30
|
return `Lead is flying -- wait for the exchange to resolve, then surrender in the lull.`;
|
|
29
31
|
}
|
|
32
|
+
// Hands up is a gesture and a phrase -- a Free Action (SR5 p.164
|
|
33
|
+
// Gesture / Speak) in a Combat Turn.
|
|
34
|
+
const bill = billAction(this.scene, actor, 'free', 'Surrender');
|
|
35
|
+
if (bill)
|
|
36
|
+
return bill;
|
|
30
37
|
actor.surrendered = true;
|
|
31
38
|
// Break the fight both ways: no parting shot for walking away from a
|
|
32
39
|
// surrender, and the opponent's own combat memory of you clears too.
|
|
@@ -5,6 +5,7 @@ import { fileReach } from '../utilities/planes.js';
|
|
|
5
5
|
import { canReach, ensureAtSpot, sealedRouteTo } from '../utilities/spots.js';
|
|
6
6
|
import { hint } from '../utilities/hints.js';
|
|
7
7
|
import { Category } from '../types/shared/item-enum.js';
|
|
8
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
8
9
|
export class TakeCommand extends Command {
|
|
9
10
|
static verb = 'take';
|
|
10
11
|
static description = 'Pick up an item. "take all" sweeps everything here -- but only after you have searched the room.';
|
|
@@ -25,6 +26,10 @@ export class TakeCommand extends Command {
|
|
|
25
26
|
screen: this.screen, game: this.game,
|
|
26
27
|
}).execute();
|
|
27
28
|
}
|
|
29
|
+
// PICK UP OBJECT (SR5 p.165): a Simple Action in a Combat Turn.
|
|
30
|
+
const bill = billAction(this.scene, this.actor, 'simple', 'Pick Up Object');
|
|
31
|
+
if (bill)
|
|
32
|
+
return bill;
|
|
28
33
|
if (wanted === 'all' || wanted === 'everything') {
|
|
29
34
|
return this.takeAll();
|
|
30
35
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { dropActiveCall, CALL_ICON, CALL_COLOR } from '../utilities/comm-style.js';
|
|
3
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
3
4
|
export class UnequipCommand extends Command {
|
|
4
5
|
static verb = 'unequip';
|
|
5
6
|
static description = 'Unequip an item.';
|
|
6
7
|
async execute(args) {
|
|
8
|
+
// Taking gear off is a Simple Action's worth of hands (SR5 p.165
|
|
9
|
+
// Pick Up/Put Down Object) inside a Combat Turn.
|
|
10
|
+
const bill = billAction(this.scene, this.actor, 'simple', 'Unequip');
|
|
11
|
+
if (bill)
|
|
12
|
+
return bill;
|
|
7
13
|
const slotName = args.join(' ').toLowerCase();
|
|
8
14
|
// TAKING THE LINK OFF HANGS UP THE CALL (MgrjzDYTzY8sJdvkT: "when I
|
|
9
15
|
// unequipped my commlink, I went offline, but the call with deditri
|
|
@@ -6,6 +6,7 @@ import { ensureAtSpot, ensureAtExit } from '../utilities/spots.js';
|
|
|
6
6
|
import { applyDeviceOpened } from '../utilities/devices.js';
|
|
7
7
|
import { hint } from '../utilities/hints.js';
|
|
8
8
|
import { FIRST_AID_THRESHOLD, firstAidCap, firstAidRefusal, performFirstAid, } from '../utilities/first-aid.js';
|
|
9
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
9
10
|
export class UseCommand extends Command {
|
|
10
11
|
static verb = 'use';
|
|
11
12
|
static description = 'Use a thing: an item from your pack (a medkit, a stim patch), or a device in the room -- "use keypad 4471" offers a code, "use safe" tries it with what you are carrying. With one device here, a bare code works too.';
|
|
@@ -16,6 +17,13 @@ export class UseCommand extends Command {
|
|
|
16
17
|
preferredCategory;
|
|
17
18
|
verbNoun = 'use';
|
|
18
19
|
async execute(args) {
|
|
20
|
+
// USE SIMPLE DEVICE (SR5 p.166-167): a Simple Action in a Combat
|
|
21
|
+
// Turn -- a medkit's first aid is the Complex Action heal.ts charges,
|
|
22
|
+
// but the thing this verb does with one is "use the kit", which the
|
|
23
|
+
// book prices as a Simple Action of the device.
|
|
24
|
+
const bill = billAction(this.scene, this.actor, 'simple', 'Use Simple Device');
|
|
25
|
+
if (bill)
|
|
26
|
+
return bill;
|
|
19
27
|
return this.use(args);
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
@@ -359,5 +359,22 @@
|
|
|
359
359
|
// beside you (companion-heel.ts trailMaster) -- they used to heel only on
|
|
360
360
|
// a room change and sat in the doorway while you paced. "recall" judges
|
|
361
361
|
// "already at your side" by cells, not spot names.
|
|
362
|
-
|
|
362
|
+
// 1.40.0 (2026-09-06): THE COMBAT TURN (SR5 p.158-168). Physical-plane
|
|
363
|
+
// combat is a turn/pass loop per room (utilities/combat-turn.ts):
|
|
364
|
+
// initiative for everyone, Action Phases in order, -10 per pass, fresh
|
|
365
|
+
// rolls per Combat Turn. COMMAND SEMANTICS in shared scenes: "attack"
|
|
366
|
+
// is one strike on your own phase (no retaliation, no auto-draw); new
|
|
367
|
+
// verbs end-turn/done/pass, initiative, aim, defend, delay; draw,
|
|
368
|
+
// holster, cover, stand, reload, take, drop, equip, use, search,
|
|
369
|
+
// grapple, struggle, release, cast, summon, dispel, heal and movement
|
|
370
|
+
// are refused outside your Action Phase and spend its Free/Simple/
|
|
371
|
+
// Complex budget. NPC participants act by rule, not by the LLM chain.
|
|
372
|
+
// 1.41.0 (2026-09-07): THE STANCE IS INTENT, NOT AN AUTOPILOT. The fight
|
|
373
|
+
// autopilot (utilities/auto-fight.ts) is gone: a phase you commit with
|
|
374
|
+
// "end turn" is never played for you, in a hosted run or a local one.
|
|
375
|
+
// "stance" / Tab keep the half the rules read -- lethal or non-lethal,
|
|
376
|
+
// the damage type your next blow means (p.186 flat of the blade, stun
|
|
377
|
+
// rounds on reload). SAVE SHAPE: autoStance is lethal | non-lethal;
|
|
378
|
+
// "manual" loads as lethal.
|
|
379
|
+
export const ENGINE_VERSION = '1.41.0';
|
|
363
380
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -74,6 +74,11 @@ import { GiveCommand } from './commands/give.js';
|
|
|
74
74
|
import { ReadCommand } from './commands/read.js';
|
|
75
75
|
import { KillCommand } from './commands/kill.js';
|
|
76
76
|
import { AttackCommand } from './commands/attack.js';
|
|
77
|
+
import { EndTurnCommand } from './commands/end-turn.js';
|
|
78
|
+
import { InitiativeCommand } from './commands/initiative.js';
|
|
79
|
+
import { AimCommand } from './commands/aim.js';
|
|
80
|
+
import { DefendCommand } from './commands/defend.js';
|
|
81
|
+
import { DelayCommand } from './commands/delay.js';
|
|
77
82
|
import { SpellsCommand } from './commands/spells.js';
|
|
78
83
|
import { InventoryCommand } from './commands/inv.js';
|
|
79
84
|
import { EquipmentCommand } from './commands/equipment.js';
|
|
@@ -174,7 +179,6 @@ import { TimeCommand } from './commands/time.js';
|
|
|
174
179
|
// The HUD's clock -- read in the engine so a shared run reads the
|
|
175
180
|
// host's wall clock, never each client's own (item B).
|
|
176
181
|
import { clockTime, worldNow } from './utilities/world-clock.js';
|
|
177
|
-
import { AutoFight } from './utilities/auto-fight.js';
|
|
178
182
|
import { CommandFactory } from './factories/command-factory.js';
|
|
179
183
|
import { SceneSynthesizer } from './factories/scene-factory.js';
|
|
180
184
|
import { SceneSeedGenerator, FIXER_NAME, isFixerName } from './factories/scene-seed-generator.js';
|
|
@@ -190,9 +194,6 @@ export default class Game {
|
|
|
190
194
|
sceneSeed;
|
|
191
195
|
scene;
|
|
192
196
|
player;
|
|
193
|
-
// The fight autopilot (Tab / "stance" command) -- created once, reads
|
|
194
|
-
// live game state at fire time so it survives scene transitions.
|
|
195
|
-
autoFight = new AutoFight(this);
|
|
196
197
|
// Just three surfaces: a full-width log, a HUD strip, and the input box.
|
|
197
198
|
// The old always-on Inventory/Equipment/Map sidebar boxes became the
|
|
198
199
|
// on-demand inv/equipment/map commands (see commands/inv.ts et al.) to
|
|
@@ -2528,6 +2529,18 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2528
2529
|
// wanted to end a call -- the intent is unambiguous mid-call.
|
|
2529
2530
|
CommandFactory.registerCommand('end', EndCallCommand);
|
|
2530
2531
|
CommandFactory.registerCommand('hangup', EndCallCommand);
|
|
2532
|
+
// THE COMBAT TURN (utilities/combat-turn.ts). "end turn" arrives
|
|
2533
|
+
// through EndCallCommand ("end" is the hangup) and is handed here.
|
|
2534
|
+
CommandFactory.registerCommand('end-turn', EndTurnCommand);
|
|
2535
|
+
CommandFactory.registerCommand('endturn', EndTurnCommand);
|
|
2536
|
+
CommandFactory.registerCommand('done', EndTurnCommand);
|
|
2537
|
+
CommandFactory.registerCommand('pass', EndTurnCommand);
|
|
2538
|
+
CommandFactory.registerCommand('initiative', InitiativeCommand);
|
|
2539
|
+
CommandFactory.registerCommand('tracker', InitiativeCommand);
|
|
2540
|
+
CommandFactory.registerCommand('turn', InitiativeCommand);
|
|
2541
|
+
CommandFactory.registerCommand('aim', AimCommand);
|
|
2542
|
+
CommandFactory.registerCommand('defend', DefendCommand);
|
|
2543
|
+
CommandFactory.registerCommand('delay', DelayCommand);
|
|
2531
2544
|
// 'train' used to alias advance; it belongs to crew instruction now
|
|
2532
2545
|
// (commands/crew.ts) -- self-training is "advance".
|
|
2533
2546
|
CommandFactory.registerCommand('advance', AdvanceCommand);
|
|
@@ -3645,7 +3658,6 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3645
3658
|
this.player.inExchange = false;
|
|
3646
3659
|
this.player.combatOpponent = undefined;
|
|
3647
3660
|
this.player.activeCallPartner = undefined;
|
|
3648
|
-
this.autoFight.cancelPending();
|
|
3649
3661
|
}
|
|
3650
3662
|
/** The panel refresh both errand legs need. */
|
|
3651
3663
|
/**
|
|
@@ -3768,7 +3780,6 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3768
3780
|
this.player.inExchange = false;
|
|
3769
3781
|
this.player.combatOpponent = undefined;
|
|
3770
3782
|
this.player.activeCallPartner = undefined;
|
|
3771
|
-
this.autoFight.cancelPending();
|
|
3772
3783
|
this.scene = hub;
|
|
3773
3784
|
this.scene.addRoom(this._homeRoom);
|
|
3774
3785
|
this.connectHomeRoom(hub.determineStartRoom());
|
|
@@ -4600,7 +4611,6 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
4600
4611
|
if (this._presenceEquipTimer)
|
|
4601
4612
|
clearTimeout(this._presenceEquipTimer);
|
|
4602
4613
|
this.stopPursuitHeartbeat();
|
|
4603
|
-
this.autoFight.cancelPending();
|
|
4604
4614
|
for (const ko of this.scene?.playerKnockouts.values() ?? []) {
|
|
4605
4615
|
if (ko.timer)
|
|
4606
4616
|
clearTimeout(ko.timer);
|
|
@@ -5220,14 +5230,15 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5220
5230
|
}
|
|
5221
5231
|
statusLinesFor(player) {
|
|
5222
5232
|
const lines = [];
|
|
5223
|
-
// Line 1: the
|
|
5233
|
+
// Line 1: the stance -- lethal or non-lethal in the meat (the damage
|
|
5234
|
+
// type your next blow means; models/player.ts FightStance), Brute
|
|
5235
|
+
// Force or Hack on the Fly on the Matrix. It lives HERE -- the
|
|
5224
5236
|
// shortest HUD line -- because appended to the long readiness line it
|
|
5225
5237
|
// wrapped out of the box on narrower terminals and effectively only
|
|
5226
|
-
// ever showed in the Tab announcement. Always shown
|
|
5227
|
-
//
|
|
5228
|
-
//
|
|
5229
|
-
//
|
|
5230
|
-
// is illegible enough that the stance looked absent entirely.
|
|
5238
|
+
// ever showed in the Tab announcement. Always shown: the HUD is how
|
|
5239
|
+
// players discover Tab flips it at all. Red for the lethal routes,
|
|
5240
|
+
// yellow for the careful ones -- never gray, which is ANSI "bright
|
|
5241
|
+
// black" and illegible on a stock dark palette.
|
|
5231
5242
|
//
|
|
5232
5243
|
// WHERE YOU ARE MOVED OUT (player ruling 2026-08-25): the room name
|
|
5233
5244
|
// now captions the Map box and the "@ spot" captions the Room box,
|
|
@@ -5235,9 +5246,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5235
5246
|
// what two panels already show. The breadcrumb went with it -- the
|
|
5236
5247
|
// district map draws the way you came far better than a text trail.
|
|
5237
5248
|
// THE STANCE THE HUD SHOWS IS THE ONE IN FORCE (uMQAhaAysgaKpWFkn,
|
|
5238
|
-
// 2026-09-03). Jacked in, the fight
|
|
5239
|
-
//
|
|
5240
|
-
//
|
|
5249
|
+
// 2026-09-03). Jacked in, the fight stance is not what Tab moves
|
|
5250
|
+
// and not what your next action obeys -- the Matrix stance is
|
|
5251
|
+
// (Brute Force or Hack on the Fly). Showing the meat stance to
|
|
5241
5252
|
// a persona would be the same class of bug as the 2026-08-27
|
|
5242
5253
|
// report that the HUD did not follow Tab at all: a readout naming
|
|
5243
5254
|
// a setting the player cannot currently change.
|
|
@@ -5245,7 +5256,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5245
5256
|
const stanceWord = onMatrix ? MATRIX_STANCE_ACTION[player.matrixStance] : player.autoStance;
|
|
5246
5257
|
const stanceColor = onMatrix
|
|
5247
5258
|
? (player.matrixStance === 'attack' ? 'red' : 'yellow')
|
|
5248
|
-
: player.autoStance === 'lethal' ? 'red' :
|
|
5259
|
+
: player.autoStance === 'lethal' ? 'red' : 'yellow';
|
|
5249
5260
|
// WHAT'S IN YOUR HANDS rides alongside the stance (player ruling
|
|
5250
5261
|
// 2026-08-25). The two answer the same question -- how ready are you
|
|
5251
5262
|
// -- and the stance line was left short and lonely once the location
|
|
@@ -5305,6 +5316,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5305
5316
|
// throws, least of all on the panel a dying player is reading.
|
|
5306
5317
|
const docwagon = this.docwagonSlot(this.docwagonByPlayer?.get(player.name) ?? (player === this.player ? this.docwagon : undefined));
|
|
5307
5318
|
lines.push(`${armorNote}${arSlot} | ${link} | ${docwagon}`);
|
|
5319
|
+
// THE COMBAT TURN (utilities/combat-turn.ts): whose phase it is, the
|
|
5320
|
+
// score, and what this player has left to spend -- present only
|
|
5321
|
+
// while a fight is on, like every transient line below.
|
|
5322
|
+
const combatLine = this.scene?.encounterFor?.(player)?.hudLine(player);
|
|
5323
|
+
if (combatLine)
|
|
5324
|
+
lines.push(combatLine);
|
|
5308
5325
|
// Lines 4+: transient states, present only while active. Future
|
|
5309
5326
|
// contexts (astral projection, VR hot-sim) join here the same way.
|
|
5310
5327
|
// THE COST IS THE ENGINE'S. Every one of these lines printed a
|
|
@@ -5988,7 +6005,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5988
6005
|
static HELP_CATEGORIES = [
|
|
5989
6006
|
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
5990
6007
|
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
|
|
5991
|
-
{ title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
|
|
6008
|
+
{ title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
|
|
5992
6009
|
// The party layer: everyone who walks (or flies, or manifests) at
|
|
5993
6010
|
// your side answers to these.
|
|
5994
6011
|
// "players" folded back under crew after playtesting ("crew
|
|
@@ -82,7 +82,7 @@ export function savedPlayer(name, overrides = {}) {
|
|
|
82
82
|
},
|
|
83
83
|
edgeRemaining: 3,
|
|
84
84
|
haggledThisJob: false,
|
|
85
|
-
autoStance: '
|
|
85
|
+
autoStance: 'lethal',
|
|
86
86
|
visitedRooms: [],
|
|
87
87
|
inventory: [],
|
|
88
88
|
equipment: {},
|
|
@@ -244,6 +244,8 @@ export function paritySeed(overrides = {}) {
|
|
|
244
244
|
export async function bootSession(opts = {}) {
|
|
245
245
|
process.env.MAKA_NO_DDP = '1';
|
|
246
246
|
process.env.MAKA_NO_LORE = '1';
|
|
247
|
+
// Combat narration beats are pacing for a person; a harness has none.
|
|
248
|
+
process.env.MAKA_NO_BEATS = '1';
|
|
247
249
|
process.env.MAKA_NO_TELEMETRY = '1';
|
|
248
250
|
const events = opts.events;
|
|
249
251
|
const session = await createHeadlessSession({
|
|
@@ -95,12 +95,29 @@ export class Item extends AbstractItem {
|
|
|
95
95
|
break;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
// Firearms track ammunition:
|
|
99
|
-
//
|
|
98
|
+
// Firearms track ammunition: the row's magazine when it has one,
|
|
99
|
+
// the flat default otherwise, starting full. Melee weapons never
|
|
100
|
+
// touch it. See fireRound()/reload() and combat-exchange.
|
|
100
101
|
if (this.isFirearm()) {
|
|
101
|
-
this.ammo =
|
|
102
|
+
this.ammo = this.magazineCapacity;
|
|
102
103
|
}
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* ROUNDS IN A FULL MAGAZINE: the catalog row's "ammo" (p.421-431 --
|
|
107
|
+
* 15(c) for a Predator, 6(cy) for a revolver) when the row carries
|
|
108
|
+
* one, else the engine's flat default for generated gear.
|
|
109
|
+
*/
|
|
110
|
+
get magazineCapacity() {
|
|
111
|
+
const rounds = this.row?.weapon?.ammo?.rounds;
|
|
112
|
+
return rounds && rounds > 0 ? rounds : Item.MAGAZINE_CAPACITY;
|
|
113
|
+
}
|
|
114
|
+
/** How it reloads (p.163 Reloading Weapons): 'c' clip, 'cy' cylinder,
|
|
115
|
+
* 'm' internal magazine, 'b' break action, 'd' drum, 'belt', 'ml'
|
|
116
|
+
* muzzle loader. A clip is two Simple Actions; everything else a
|
|
117
|
+
* Complex. Unknown gear reloads like a clip. */
|
|
118
|
+
get reloadFeed() {
|
|
119
|
+
return this.row?.weapon?.ammo?.feed ?? 'c';
|
|
120
|
+
}
|
|
104
121
|
// ======================== Stacking ========================
|
|
105
122
|
/**
|
|
106
123
|
* WHAT FUNGIBLE MEANS HERE (user ruling): only CONSUMABLES stack.
|
|
@@ -200,7 +217,7 @@ export class Item extends AbstractItem {
|
|
|
200
217
|
loadedAmmo;
|
|
201
218
|
reload(ammoName) {
|
|
202
219
|
if (this.isFirearm()) {
|
|
203
|
-
this.ammo =
|
|
220
|
+
this.ammo = this.magazineCapacity;
|
|
204
221
|
if (ammoName !== undefined)
|
|
205
222
|
this.loadedAmmo = ammoName;
|
|
206
223
|
}
|
|
@@ -909,6 +909,38 @@ description text.`;
|
|
|
909
909
|
this.logger.write(`AI fallback for appearance (${this.name}): ${err}`);
|
|
910
910
|
}
|
|
911
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* IN A FIGHT, THE TURN LOOP OWNS THIS SHELL (utilities/combat-turn.ts).
|
|
914
|
+
* While this NPC is a participant in a running encounter, its LLM
|
|
915
|
+
* chain -- act, respondTo, recursiveReflect -- is suspended: the
|
|
916
|
+
* deterministic brain (utilities/npc-combat-brain.ts) plays its Action
|
|
917
|
+
* Phases in initiative order, and a model answering on its own clock
|
|
918
|
+
* would only be refused by the phase gate and grind to a halt on the
|
|
919
|
+
* refusals. Perception (see/hear) still records history, so the shell
|
|
920
|
+
* knows what happened when the chain resumes after the fight.
|
|
921
|
+
*/
|
|
922
|
+
combatPhaseLocked() {
|
|
923
|
+
return !!this._scene?.encounterFor?.(this);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* One verb, executed now, no reflection -- the brain's way of acting
|
|
927
|
+
* inside an Action Phase. Returns the command's own result so the
|
|
928
|
+
* brain can read a refusal, and files it into history like any act.
|
|
929
|
+
*/
|
|
930
|
+
async actInCombat(commandString) {
|
|
931
|
+
if (this.deathClaimed)
|
|
932
|
+
return '';
|
|
933
|
+
const parsed = await this.commandRegistry.parse(commandString, this);
|
|
934
|
+
if (!parsed) {
|
|
935
|
+
this.logger.error(`${this.name} (combat) tried to perform unknown command: ${commandString}`);
|
|
936
|
+
return '';
|
|
937
|
+
}
|
|
938
|
+
const result = await parsed.command.execute(parsed.args);
|
|
939
|
+
if (result)
|
|
940
|
+
this._addToHistory(result);
|
|
941
|
+
this.relayToMaster(commandString, result);
|
|
942
|
+
return result ?? '';
|
|
943
|
+
}
|
|
912
944
|
async act(commandString) {
|
|
913
945
|
// A removed actor must STOP: dismissal/death happens while an AI
|
|
914
946
|
// burst is still in flight, and without this latch a real session's
|
|
@@ -918,6 +950,10 @@ description text.`;
|
|
|
918
950
|
this.logger.write(`${this.name} is gone from the world -- dropping queued action "${commandString}".`);
|
|
919
951
|
return;
|
|
920
952
|
}
|
|
953
|
+
if (this.combatPhaseLocked()) {
|
|
954
|
+
this.logger.write(`${this.name} is in a Combat Turn -- the turn loop acts for them; dropping "${commandString}".`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
921
957
|
const parsed = await this.commandRegistry.parse(commandString, this);
|
|
922
958
|
if (parsed) {
|
|
923
959
|
// Capture the result of the command
|
|
@@ -1361,6 +1397,10 @@ description text.`;
|
|
|
1361
1397
|
this.logger.write(`${this.name} is gone from the world -- reflect chain ends.`);
|
|
1362
1398
|
return;
|
|
1363
1399
|
}
|
|
1400
|
+
if (this.combatPhaseLocked()) {
|
|
1401
|
+
this.logger.write(`${this.name} is in a Combat Turn -- reflect chain yields to the turn loop.`);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1364
1404
|
// THE GRIND BREAKER. Record what just happened, then refuse to keep
|
|
1365
1405
|
// going round on it. Deterministic on purpose: the prompt already
|
|
1366
1406
|
// asks for honesty about failure and got it -- what it lacked was
|
|
@@ -1538,6 +1578,10 @@ ${worldEventsSummary}
|
|
|
1538
1578
|
: [];
|
|
1539
1579
|
}
|
|
1540
1580
|
async respondTo(actor, message) {
|
|
1581
|
+
if (this.combatPhaseLocked()) {
|
|
1582
|
+
this.logger.write(`${this.name} is in a Combat Turn -- no AI reaction to "${message.slice(0, 40)}".`);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1541
1585
|
this.logger.write('Attempting to respond');
|
|
1542
1586
|
// A fresh stimulus earns at most one fresh speech (see the field doc)
|
|
1543
1587
|
// -- and a clean slate of attempts. Something new has happened, so
|
|
@@ -15,9 +15,17 @@ import { physicalLimit } from '../utilities/grapple.js';
|
|
|
15
15
|
import { astralLimit } from '../utilities/limits.js';
|
|
16
16
|
import { Inventory } from './inventory.js';
|
|
17
17
|
import { AbstractPlayer } from '../types/shared/abstracts.js';
|
|
18
|
-
export const STANCE_CYCLE = ['
|
|
18
|
+
export const STANCE_CYCLE = ['lethal', 'non-lethal'];
|
|
19
19
|
/** The old names, still accepted from saves and the stance command. */
|
|
20
|
-
export const STANCE_ALIASES = {
|
|
20
|
+
export const STANCE_ALIASES = {
|
|
21
|
+
guarded: 'non-lethal', aggressive: 'lethal', manual: 'lethal',
|
|
22
|
+
nonlethal: 'non-lethal', 'non lethal': 'non-lethal', stun: 'non-lethal', kill: 'lethal',
|
|
23
|
+
};
|
|
24
|
+
/** What each stance means, in one line -- the HUD's and the verb's. */
|
|
25
|
+
export const STANCE_DESCRIPTION = {
|
|
26
|
+
lethal: 'your weapons deal the damage they were sold to deal; "attack" and "kill" mean it',
|
|
27
|
+
'non-lethal': 'Stun wherever the gear allows -- stun rounds when you reload, the flat of the blade (p.186), bare hands -- and a lethal gun with lethal rounds says so before it fires',
|
|
28
|
+
};
|
|
21
29
|
export const MATRIX_STANCE_CYCLE = ['attack', 'sleaze'];
|
|
22
30
|
/** What each stance is CALLED in the book -- printed everywhere the
|
|
23
31
|
* engine used to say "loud"/"quiet", so the vocabulary teaches the
|
|
@@ -36,8 +44,8 @@ export function normalizeMatrixStance(v) {
|
|
|
36
44
|
return MATRIX_STANCE_ALIASES[k] ?? (MATRIX_STANCE_CYCLE.includes(k) ? k : 'attack');
|
|
37
45
|
}
|
|
38
46
|
export function normalizeStance(v) {
|
|
39
|
-
const k = (v ?? '
|
|
40
|
-
return STANCE_ALIASES[k] ?? (STANCE_CYCLE.includes(k) ? k : '
|
|
47
|
+
const k = (v ?? 'lethal').toLowerCase();
|
|
48
|
+
return STANCE_ALIASES[k] ?? (STANCE_CYCLE.includes(k) ? k : 'lethal');
|
|
41
49
|
}
|
|
42
50
|
/** Meat and a jumped-in drone share the physical world. */
|
|
43
51
|
export function isPhysicalPlane(plane) {
|
|
@@ -185,16 +193,12 @@ export class Player extends AbstractPlayer {
|
|
|
185
193
|
// session where an NPC's overlapping AI loops fired two exchanges at
|
|
186
194
|
// once and the target died TWICE (two kill events, two narrations).
|
|
187
195
|
inExchange = false;
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// guarded -- auto-fights, but finishes non-lethally (switches to
|
|
193
|
-
// subdue when the target is nearly down) and stands down
|
|
194
|
-
// if they surrender
|
|
195
|
-
// aggressive -- fights until they're dead; surrender means nothing
|
|
196
|
+
// The fight stance (see FightStance above): lethal or non-lethal, the
|
|
197
|
+
// damage type your next blow is meant to deal. Tab cycles it in the UI;
|
|
198
|
+
// "stance" sets it by name. Every action is still yours to type -- the
|
|
199
|
+
// Combat Turn hands you the phase and waits for "end turn".
|
|
196
200
|
// Accessor (live-sheet pass, 2026-09-01): stance is sheet-visible.
|
|
197
|
-
_autoStance = '
|
|
201
|
+
_autoStance = 'lethal';
|
|
198
202
|
get autoStance() { return this._autoStance; }
|
|
199
203
|
set autoStance(v) {
|
|
200
204
|
if (this._autoStance === v)
|
|
@@ -229,6 +233,39 @@ export class Player extends AbstractPlayer {
|
|
|
229
233
|
rallyInitBonus = 0;
|
|
230
234
|
directedDice = 0;
|
|
231
235
|
inspiredGuardDice = 0;
|
|
236
|
+
// ==================== THE ACTION PHASE (utilities/combat-turn.ts) ====
|
|
237
|
+
//
|
|
238
|
+
// TAKE AIM (SR5 p.166): +1 die per Simple Action spent aiming, up to
|
|
239
|
+
// half Willpower rounded up, on the NEXT attack -- and lost the moment
|
|
240
|
+
// "any other kind of action (including a Free Action)" is taken first.
|
|
241
|
+
// Cleared by action-cost.ts on every non-aim spend, by the encounter
|
|
242
|
+
// at every phase boundary, and consumed by the strike that uses it.
|
|
243
|
+
aimBonus = 0;
|
|
244
|
+
aimTarget;
|
|
245
|
+
clearAim() {
|
|
246
|
+
this.aimBonus = 0;
|
|
247
|
+
this.aimTarget = undefined;
|
|
248
|
+
}
|
|
249
|
+
/** p.166: the cap on sequential Take Aim bonuses. */
|
|
250
|
+
get aimCap() {
|
|
251
|
+
return Math.max(1, Math.ceil(this.willpower / 2));
|
|
252
|
+
}
|
|
253
|
+
// PROGRESSIVE RECOIL (p.175-176): the rounds fired keep counting across
|
|
254
|
+
// Action Phases and Combat Turns until a phase goes by without a shot.
|
|
255
|
+
// firedThisPhase is the encounter's evidence for that reset.
|
|
256
|
+
recoilRoundsFired = 0;
|
|
257
|
+
firedThisPhase = false;
|
|
258
|
+
/**
|
|
259
|
+
* THE STANDING ANSWER TO "SOMEONE IS SHOOTING AT YOU" (p.168, p.191).
|
|
260
|
+
* Interrupt actions are declared in response to an attack, outside
|
|
261
|
+
* the defender's own phase. A terminal cannot prompt mid-pass -- and
|
|
262
|
+
* a hosted table cannot wait on one player's keystroke -- so the
|
|
263
|
+
* player sets a policy ("defend dodge") that the engine applies when
|
|
264
|
+
* an attack comes in and the Initiative Score can pay for it. Full
|
|
265
|
+
* Defense is different: declared once, -10, and it lasts the turn
|
|
266
|
+
* (CombatEncounter.declareFullDefense).
|
|
267
|
+
*/
|
|
268
|
+
defensePolicy = 'none';
|
|
232
269
|
// Set by the "surrender" command: this actor has thrown their hands up.
|
|
233
270
|
// Cleared the moment they initiate violence again (see
|
|
234
271
|
// CombatExchange.beginExchange). Guarded auto-fight honors it;
|
|
@@ -6,6 +6,7 @@ import { hint } from '../utilities/hints.js';
|
|
|
6
6
|
import { Category, Size, Rating } from '../types/shared/item-enum.js';
|
|
7
7
|
import { AbstractScene } from '../types/shared/abstracts.js';
|
|
8
8
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
9
|
+
import { CombatEncounter } from '../utilities/combat-turn.js';
|
|
9
10
|
export class Scene extends AbstractScene {
|
|
10
11
|
story;
|
|
11
12
|
// SYMMETRIC PLAYERS: the scene holds a LIST of human players --
|
|
@@ -129,6 +130,95 @@ export class Scene extends AbstractScene {
|
|
|
129
130
|
// meaning -- begin/end act on the primary player, the query asks
|
|
130
131
|
// "is ANY human's exchange live".
|
|
131
132
|
_playerExchangesActive = new Set();
|
|
133
|
+
// ==================== COMBAT ENCOUNTERS (utilities/combat-turn.ts) ====
|
|
134
|
+
//
|
|
135
|
+
// One Combat Turn structure per ROOM: the fight in the loading bay and
|
|
136
|
+
// the fight in the office are two initiative orders, as they would be
|
|
137
|
+
// at a table. An encounter holds the participants, their scores and
|
|
138
|
+
// whose Action Phase is live; the scene only owns the map.
|
|
139
|
+
encounters = new Map();
|
|
140
|
+
/** The live fight in a room, if any. */
|
|
141
|
+
encounterIn(room) {
|
|
142
|
+
if (!room)
|
|
143
|
+
return undefined;
|
|
144
|
+
const enc = this.encounters.get(room);
|
|
145
|
+
return enc && !enc.ended ? enc : undefined;
|
|
146
|
+
}
|
|
147
|
+
/** The fight this actor is a participant in, if any. */
|
|
148
|
+
encounterFor(actor) {
|
|
149
|
+
const enc = this.encounterIn(actor.currentLocation);
|
|
150
|
+
return enc?.has(actor) ? enc : undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Who is in the fight when `aggressor` opens on `target`: the two of
|
|
154
|
+
* them, every human and bound shell in the room (the party fights as
|
|
155
|
+
* one), every NPC flagged hostile, and anyone already engaged with
|
|
156
|
+
* either. A bystander is scenery; a bartender is not opposition until
|
|
157
|
+
* someone makes them so (noteAttack pulls them in then).
|
|
158
|
+
*/
|
|
159
|
+
encounterMembers(room, aggressor, target) {
|
|
160
|
+
const out = new Set([aggressor]);
|
|
161
|
+
if (target)
|
|
162
|
+
out.add(target);
|
|
163
|
+
for (const a of room.getActors()) {
|
|
164
|
+
if (a === aggressor || a === target)
|
|
165
|
+
continue;
|
|
166
|
+
if (!a.sharesCombatPlane(aggressor) || a.isIncapacitated())
|
|
167
|
+
continue;
|
|
168
|
+
const npc = a;
|
|
169
|
+
if (npc.ephemeral?.kind === 'bystander')
|
|
170
|
+
continue;
|
|
171
|
+
const party = this.isHumanControlled(a) || !!npc.allyOf;
|
|
172
|
+
const hostile = !this.isHumanControlled(a) && npc.hostile === true && !npc.allyOf;
|
|
173
|
+
const engaged = !!a.combatOpponent && (a.combatOpponent === aggressor || a.combatOpponent === target);
|
|
174
|
+
if (party || hostile || engaged)
|
|
175
|
+
out.add(a);
|
|
176
|
+
}
|
|
177
|
+
return [...out];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Opens a fight in a room -- or renews the one already running there.
|
|
181
|
+
* Resolves once the first pass has run up to a human's Action Phase
|
|
182
|
+
* (or the fight has already ended).
|
|
183
|
+
*/
|
|
184
|
+
async startEncounter(room, opts) {
|
|
185
|
+
const existing = this.encounterIn(room);
|
|
186
|
+
if (existing) {
|
|
187
|
+
if (opts.target)
|
|
188
|
+
existing.noteAttack(opts.aggressor, opts.target);
|
|
189
|
+
else if (!existing.has(opts.aggressor))
|
|
190
|
+
existing.join(opts.aggressor);
|
|
191
|
+
return existing;
|
|
192
|
+
}
|
|
193
|
+
const enc = new CombatEncounter(this, this.logger, room);
|
|
194
|
+
this.encounters.set(room, enc);
|
|
195
|
+
await enc.start(this.encounterMembers(room, opts.aggressor, opts.target), opts);
|
|
196
|
+
return enc;
|
|
197
|
+
}
|
|
198
|
+
/** The encounter is over (called by the encounter itself). */
|
|
199
|
+
endEncounter(enc) {
|
|
200
|
+
if (this.encounters.get(enc.room) === enc)
|
|
201
|
+
this.encounters.delete(enc.room);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Someone walks into a room where a fight is on (p.160 entering
|
|
205
|
+
* combat late): the party and the hostile join it; anyone else stays
|
|
206
|
+
* out until they act.
|
|
207
|
+
*/
|
|
208
|
+
admitToEncounter(actor) {
|
|
209
|
+
const enc = this.encounterIn(actor.currentLocation);
|
|
210
|
+
if (!enc || enc.has(actor) || actor.isIncapacitated())
|
|
211
|
+
return undefined;
|
|
212
|
+
const npc = actor;
|
|
213
|
+
const party = this.isHumanControlled(actor) || !!npc.allyOf;
|
|
214
|
+
const hostile = !this.isHumanControlled(actor) && npc.hostile === true && !npc.allyOf;
|
|
215
|
+
if (!party && !hostile)
|
|
216
|
+
return undefined;
|
|
217
|
+
if (!enc.participants.some(p => p.actor.sharesCombatPlane(actor)))
|
|
218
|
+
return undefined;
|
|
219
|
+
enc.join(actor);
|
|
220
|
+
return enc;
|
|
221
|
+
}
|
|
132
222
|
beginPlayerExchange(playerName) {
|
|
133
223
|
this._playerExchangesActive.add(playerName ?? this.player.name);
|
|
134
224
|
}
|
|
@@ -141,13 +231,13 @@ export class Scene extends AbstractScene {
|
|
|
141
231
|
return this._playerExchangesActive.size > 0;
|
|
142
232
|
}
|
|
143
233
|
/**
|
|
144
|
-
* CombatExchange.finishExchange()
|
|
145
|
-
* utilities/auto-fight.ts
|
|
146
|
-
*
|
|
147
|
-
*
|
|
234
|
+
* CombatExchange.finishExchange() and CombatEncounter.closePhase() ->
|
|
235
|
+
* here. This used to arm the fight autopilot (utilities/auto-fight.ts,
|
|
236
|
+
* retired 2026-09-07 with the Combat Turn: a phase you commit with
|
|
237
|
+
* "end turn" is not one the game plays for you). What remains is the
|
|
238
|
+
* KO'd player's view of the fight still raging around them.
|
|
148
239
|
*/
|
|
149
240
|
notifyExchangeSettled(combatants) {
|
|
150
|
-
this.game?.autoFight?.onExchangeSettled?.(combatants);
|
|
151
241
|
// While a player lies KO'd, every settling exchange is a beat of
|
|
152
242
|
// the fight still raging around them: learn its hostiles, then see
|
|
153
243
|
// whether the room has settled (see maybeResolvePlayerKnockout).
|
|
@@ -491,6 +581,13 @@ export class Scene extends AbstractScene {
|
|
|
491
581
|
// map was never cleaned on removal, leaving a dead Room reference.
|
|
492
582
|
this.characterLocations.delete(name);
|
|
493
583
|
this.logger.write(`Removed actor: ${name}`);
|
|
584
|
+
// A fallen participant's Action Phase, if it was live, moves on;
|
|
585
|
+
// the encounter itself re-checks who is still standing on its next
|
|
586
|
+
// step (utilities/combat-turn.ts run).
|
|
587
|
+
const enc = room ? this.encounterIn(room) : undefined;
|
|
588
|
+
if (enc && enc.phaseActor === actor) {
|
|
589
|
+
void enc.dropPhaseActor(actor);
|
|
590
|
+
}
|
|
494
591
|
// Tell the Game an actor left the world -- the companion roster
|
|
495
592
|
// (Game.onActorRemoved) prunes shells and settles their bound
|
|
496
593
|
// devices (a wrecked frame, a bricked deck).
|