@maka/maka-cli 5.135.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.135.0",
3
+ "version": "5.136.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.",
@@ -1,16 +1,19 @@
1
1
  import { Command } from './command.js';
2
- import { STANCE_CYCLE, STANCE_ALIASES, MATRIX_STANCE_CYCLE, MATRIX_STANCE_ALIASES, MATRIX_STANCE_ACTION } from '../models/player.js';
3
- import { AutoFight } from '../utilities/auto-fight.js';
2
+ import { STANCE_CYCLE, STANCE_ALIASES, STANCE_DESCRIPTION, MATRIX_STANCE_CYCLE, MATRIX_STANCE_ALIASES, MATRIX_STANCE_ACTION } from '../models/player.js';
4
3
  import { fuzzyPickName } from '../utilities/fuzzy-match.js';
5
4
  /**
6
- * The typed twin of the Tab key: set (or inspect) the fight-or-flight
7
- * autopilot temperature. Tab cycles manual -> guarded -> aggressive in the
8
- * UI; this command jumps straight to one by name, typo-tolerant. The full
9
- * semantics live in utilities/auto-fight.ts.
5
+ * The typed twin of the Tab key: set (or inspect) the stance. On either
6
+ * plane the stance is what your next action DOES -- in the meat, the
7
+ * damage type your blow is meant to deal (lethal / non-lethal, see
8
+ * FightStance in models/player.ts); on the Matrix, which intrusion
9
+ * action a hack is (Brute Force / Hack on the Fly). It never decides
10
+ * whether you act: since the Combat Turn (2026-09-06) every phase is
11
+ * yours to play and "end turn" to commit, and the fight autopilot this
12
+ * verb used to set was retired with it (2026-09-07).
10
13
  */
11
14
  export class StanceCommand extends Command {
12
15
  static verb = 'stance';
13
- static description = 'Set your stance. ON THE MATRIX it picks which intrusion action you use -- attack (Brute Force, p.238) or sleaze (Hack on the Fly, p.240); Tab flips it, and "deck config" still sets the array. IN THE MEAT it sets your fight autopilot. You ALWAYS counter when struck; the stance decides who presses: manual (you type it), non-lethal (auto, Stun where the gear allows, subdues, honors surrender), lethal (auto, to the death). Tab cycles.';
16
+ static description = 'Set your stance -- what your next action does, never whether you take it. ON THE MATRIX it picks the intrusion action: attack (Brute Force, p.238) or sleaze (Hack on the Fly, p.240); "deck config" still sets the array. IN THE MEAT it picks the damage type you mean: lethal (the gear as sold) or non-lethal (Stun wherever the gear allows -- stun rounds when you reload, the flat of the blade at Accuracy 3 per p.186, bare hands; a lethal gun with lethal rounds warns you it stays lethal). Tab flips it.';
14
17
  /**
15
18
  * ON THE MATRIX, TAB FLIPS THE INTRUSION ACTION instead of the fight
16
19
  * autopilot (mjmcee, uMQAhaAysgaKpWFkn). One verb and one key, two
@@ -40,11 +43,11 @@ export class StanceCommand extends Command {
40
43
  if (onMatrix)
41
44
  return this.matrixLines(actor);
42
45
  const lines = [
43
- `Stance: ${actor.autoStance.toUpperCase()} -- ${AutoFight.describe(actor.autoStance)}.`,
46
+ `Stance: ${actor.autoStance.toUpperCase()} -- ${STANCE_DESCRIPTION[actor.autoStance]}.`,
44
47
  '',
45
- ...STANCE_CYCLE.map(s => ` ${s.padEnd(10)} -- ${AutoFight.describe(s)}`),
48
+ ...STANCE_CYCLE.map(s => ` ${s.padEnd(10)} -- ${STANCE_DESCRIPTION[s]}`),
46
49
  '',
47
- `Set with "stance <name>", or Tab to cycle. Flight is never automated -- running is always your call.`,
50
+ `Set with "stance <name>", or Tab to flip. The stance is what a blow does, not who throws it -- every action is yours to type, and "end turn" commits the phase.`,
48
51
  ];
49
52
  return lines.join('\n');
50
53
  }
@@ -72,10 +75,9 @@ export class StanceCommand extends Command {
72
75
  const cur = STANCE_CYCLE.indexOf(actor.autoStance);
73
76
  const nextStance = STANCE_CYCLE[(cur + 1) % STANCE_CYCLE.length];
74
77
  actor.autoStance = nextStance;
75
- this.game?.autoFight?.cancelPending();
76
78
  this.scene.updateStatus();
77
79
  this.logger.write(`${actor.name} cycled stance to ${nextStance}.`);
78
- return `Stance: ${nextStance.toUpperCase()} -- ${AutoFight.describe(nextStance)}.`;
80
+ return `Stance: ${nextStance.toUpperCase()} -- ${STANCE_DESCRIPTION[nextStance]}.`;
79
81
  }
80
82
  // A Matrix route by name, from either plane's vocabulary -- so
81
83
  // "stance sleaze" works while jacked in and reads as a typo
@@ -100,13 +102,12 @@ export class StanceCommand extends Command {
100
102
  }
101
103
  const stance = picked;
102
104
  if (stance === actor.autoStance) {
103
- return `Already ${stance.toUpperCase()} -- ${AutoFight.describe(stance)}.`;
105
+ return `Already ${stance.toUpperCase()} -- ${STANCE_DESCRIPTION[stance]}.`;
104
106
  }
105
107
  actor.autoStance = stance;
106
- this.game?.autoFight?.cancelPending();
107
108
  this.scene.updateStatus();
108
109
  this.logger.write(`${actor.name} set stance to ${stance}.`);
109
- return `Stance: ${stance.toUpperCase()} -- ${AutoFight.describe(stance)}.`;
110
+ return `Stance: ${stance.toUpperCase()} -- ${STANCE_DESCRIPTION[stance]}.`;
110
111
  }
111
112
  }
112
113
  //# sourceMappingURL=stance.js.map
@@ -3,9 +3,10 @@ import { billAction } from '../utilities/action-cost.js';
3
3
  /**
4
4
  * Throwing your hands up, as a real mechanical act instead of just talk.
5
5
  * The wound-aware AI guidance already nudges a losing NPC toward
6
- * surrender -- this gives that nudge teeth: the flag it sets is what a
7
- * GUARDED auto-fighter honors by holding fire (see utilities/auto-fight.ts;
8
- * an AGGRESSIVE one doesn't have to). Surrendering also breaks the fight:
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:
9
10
  * both sides' combat memory clears, so walking away afterwards invites no
10
11
  * parting shot. The flag is revoked the moment the surrendered actor
11
12
  * initiates violence again (CombatExchange.beginExchange).
@@ -369,5 +369,12 @@
369
369
  // grapple, struggle, release, cast, summon, dispel, heal and movement
370
370
  // are refused outside your Action Phase and spend its Free/Simple/
371
371
  // Complex budget. NPC participants act by rule, not by the LLM chain.
372
- export const ENGINE_VERSION = '1.40.0';
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';
373
380
  //# sourceMappingURL=engine-version.js.map
@@ -179,7 +179,6 @@ import { TimeCommand } from './commands/time.js';
179
179
  // The HUD's clock -- read in the engine so a shared run reads the
180
180
  // host's wall clock, never each client's own (item B).
181
181
  import { clockTime, worldNow } from './utilities/world-clock.js';
182
- import { AutoFight } from './utilities/auto-fight.js';
183
182
  import { CommandFactory } from './factories/command-factory.js';
184
183
  import { SceneSynthesizer } from './factories/scene-factory.js';
185
184
  import { SceneSeedGenerator, FIXER_NAME, isFixerName } from './factories/scene-seed-generator.js';
@@ -195,9 +194,6 @@ export default class Game {
195
194
  sceneSeed;
196
195
  scene;
197
196
  player;
198
- // The fight autopilot (Tab / "stance" command) -- created once, reads
199
- // live game state at fire time so it survives scene transitions.
200
- autoFight = new AutoFight(this);
201
197
  // Just three surfaces: a full-width log, a HUD strip, and the input box.
202
198
  // The old always-on Inventory/Equipment/Map sidebar boxes became the
203
199
  // on-demand inv/equipment/map commands (see commands/inv.ts et al.) to
@@ -3662,7 +3658,6 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3662
3658
  this.player.inExchange = false;
3663
3659
  this.player.combatOpponent = undefined;
3664
3660
  this.player.activeCallPartner = undefined;
3665
- this.autoFight.cancelPending();
3666
3661
  }
3667
3662
  /** The panel refresh both errand legs need. */
3668
3663
  /**
@@ -3785,7 +3780,6 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3785
3780
  this.player.inExchange = false;
3786
3781
  this.player.combatOpponent = undefined;
3787
3782
  this.player.activeCallPartner = undefined;
3788
- this.autoFight.cancelPending();
3789
3783
  this.scene = hub;
3790
3784
  this.scene.addRoom(this._homeRoom);
3791
3785
  this.connectHomeRoom(hub.determineStartRoom());
@@ -4617,7 +4611,6 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4617
4611
  if (this._presenceEquipTimer)
4618
4612
  clearTimeout(this._presenceEquipTimer);
4619
4613
  this.stopPursuitHeartbeat();
4620
- this.autoFight.cancelPending();
4621
4614
  for (const ko of this.scene?.playerKnockouts.values() ?? []) {
4622
4615
  if (ko.timer)
4623
4616
  clearTimeout(ko.timer);
@@ -5237,14 +5230,15 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5237
5230
  }
5238
5231
  statusLinesFor(player) {
5239
5232
  const lines = [];
5240
- // Line 1: the fight autopilot mode. The stance lives HERE -- 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
5241
5236
  // shortest HUD line -- because appended to the long readiness line it
5242
5237
  // wrapped out of the box on narrower terminals and effectively only
5243
- // ever showed in the Tab announcement. Always shown, even MANUAL: the
5244
- // HUD is how players discover Tab cycles it at all (see
5245
- // utilities/auto-fight.ts). MANUAL is white, not gray -- blessed's
5246
- // gray is ANSI "bright black", which on a stock dark terminal palette
5247
- // 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.
5248
5242
  //
5249
5243
  // WHERE YOU ARE MOVED OUT (player ruling 2026-08-25): the room name
5250
5244
  // now captions the Map box and the "@ spot" captions the Room box,
@@ -5252,9 +5246,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5252
5246
  // what two panels already show. The breadcrumb went with it -- the
5253
5247
  // district map draws the way you came far better than a text trail.
5254
5248
  // THE STANCE THE HUD SHOWS IS THE ONE IN FORCE (uMQAhaAysgaKpWFkn,
5255
- // 2026-09-03). Jacked in, the fight autopilot is not what Tab
5256
- // moves and not what your next action obeys -- the Matrix stance
5257
- // is (Brute Force or Hack on the Fly). Showing the meat stance to
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
5258
5252
  // a persona would be the same class of bug as the 2026-08-27
5259
5253
  // report that the HUD did not follow Tab at all: a readout naming
5260
5254
  // a setting the player cannot currently change.
@@ -5262,7 +5256,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5262
5256
  const stanceWord = onMatrix ? MATRIX_STANCE_ACTION[player.matrixStance] : player.autoStance;
5263
5257
  const stanceColor = onMatrix
5264
5258
  ? (player.matrixStance === 'attack' ? 'red' : 'yellow')
5265
- : player.autoStance === 'lethal' ? 'red' : player.autoStance === 'non-lethal' ? 'yellow' : 'white';
5259
+ : player.autoStance === 'lethal' ? 'red' : 'yellow';
5266
5260
  // WHAT'S IN YOUR HANDS rides alongside the stance (player ruling
5267
5261
  // 2026-08-25). The two answer the same question -- how ready are you
5268
5262
  // -- and the stance line was left short and lonely once the location
@@ -82,7 +82,7 @@ export function savedPlayer(name, overrides = {}) {
82
82
  },
83
83
  edgeRemaining: 3,
84
84
  haggledThisJob: false,
85
- autoStance: 'manual',
85
+ autoStance: 'lethal',
86
86
  visitedRooms: [],
87
87
  inventory: [],
88
88
  equipment: {},
@@ -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 = ['manual', 'non-lethal', 'lethal'];
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 = { guarded: 'non-lethal', aggressive: 'lethal', nonlethal: 'non-lethal', 'non lethal': 'non-lethal' };
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 ?? 'manual').toLowerCase();
40
- return STANCE_ALIASES[k] ?? (STANCE_CYCLE.includes(k) ? k : 'manual');
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
- // Fight-or-flight autopilot (see utilities/auto-fight.ts): the FIGHT
189
- // half runs automatically at the set temperature, the flight stays
190
- // manual -- always. Tab cycles it in the UI; "stance" sets it by name.
191
- // manual -- today's behavior, you type every swing
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 = 'manual';
201
+ _autoStance = 'lethal';
198
202
  get autoStance() { return this._autoStance; }
199
203
  set autoStance(v) {
200
204
  if (this._autoStance === v)
@@ -192,7 +192,6 @@ export class Scene extends AbstractScene {
192
192
  }
193
193
  const enc = new CombatEncounter(this, this.logger, room);
194
194
  this.encounters.set(room, enc);
195
- enc.onHumanPhase = (p, e) => this.game?.autoFight?.onPhaseBegins?.(p, e);
196
195
  await enc.start(this.encounterMembers(room, opts.aggressor, opts.target), opts);
197
196
  return enc;
198
197
  }
@@ -232,13 +231,13 @@ export class Scene extends AbstractScene {
232
231
  return this._playerExchangesActive.size > 0;
233
232
  }
234
233
  /**
235
- * CombatExchange.finishExchange() -> here -> the fight autopilot (see
236
- * utilities/auto-fight.ts). Optional-chained like updateStatus: harmless
237
- * before the Game reference is wired, or under a partial test stub, or
238
- * for NPC-vs-NPC fights (the autopilot ignores those itself).
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.
239
239
  */
240
240
  notifyExchangeSettled(combatants) {
241
- this.game?.autoFight?.onExchangeSettled?.(combatants);
242
241
  // While a player lies KO'd, every settling exchange is a beat of
243
242
  // the fight still raging around them: learn its hostiles, then see
244
243
  // whether the room has settled (see maybeResolvePlayerKnockout).
@@ -290,9 +290,10 @@ export async function main(playerName, sceneSeed, options) {
290
290
  game.updateInventory(game.player.inventory, capitalCase(game.scene.getCurrencyType()));
291
291
  game.updateExits(game.player.currentLocation);
292
292
  game.updateStatus();
293
- // Tab cycles the fight autopilot: manual -> guarded -> aggressive (see
294
- // utilities/auto-fight.ts; "stance" is the typed twin). Bound on the
295
- // 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.
296
297
  game.inputBox.key(['tab'], () => {
297
298
  // Blessed's textarea reader appends the literal '\t' to the box
298
299
  // value on this same keypress (listener order isn't guaranteed
@@ -318,7 +319,7 @@ export async function main(playerName, sceneSeed, options) {
318
319
  // bar"). game.player here is the LOCAL HUB player -- in a shared
319
320
  // run the runner on the table is a different object in a
320
321
  // different process, so cycling this one moved nothing the
321
- // autopilot reads and nothing the HUD paints (updateStatus
322
+ // engine reads and nothing the HUD paints (updateStatus
322
323
  // early-returns on remoteSession; the status box belongs to the
323
324
  // server's events). Send the typed twin instead and let the
324
325
  // engine cycle from the ACTOR's real stance.
@@ -326,10 +327,10 @@ export async function main(playerName, sceneSeed, options) {
326
327
  const res = await game.handleInput('stance next');
327
328
  if (!res)
328
329
  return;
329
- // white for manual, not gray -- gray is ANSI bright-black,
330
- // illegible on stock dark palettes (same fix as the HUD stance
331
- // in game.ts). The Matrix routes borrow the same temperature:
332
- // 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.
333
334
  const named = res.match(/(?:Matrix s|S)tance:\s*([\w-]+)/i)?.[1]?.toLowerCase();
334
335
  const c = named === 'lethal' || named === 'brute' ? 'red'
335
336
  : named === 'non-lethal' || named === 'non' || named === 'hack' ? 'yellow'
@@ -369,10 +370,6 @@ export async function main(playerName, sceneSeed, options) {
369
370
  // still a command you typed and will want to edit and retry.
370
371
  if (opts.recordHistory !== false)
371
372
  history.remember(input);
372
- // Typing takes over: any submitted command cancels a pending
373
- // auto-fight action (the interruptible beat -- see
374
- // utilities/auto-fight.ts). The next settled exchange re-arms it.
375
- game.autoFight.cancelPending();
376
373
  // Clear the box the moment the command is submitted, not after
377
374
  // it resolves -- a slow command (combat narration beats, an AI
378
375
  // round trip on a call) used to leave the typed text sitting in
@@ -995,8 +995,8 @@ export class CombatExchange {
995
995
  this.releasePlayerLocks();
996
996
  this.releaseExchange();
997
997
  this.scene.updateStatus();
998
- // The fight autopilot's trigger: a settled exchange involving the
999
- // 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).
1000
1000
  this.scene.notifyExchangeSettled(this._combatants);
1001
1001
  // A spirit whose services ran out departs once the dust settles
1002
1002
  // (RAW p.302: the bargain is spent).
@@ -22,7 +22,10 @@ export class CombatEncounter {
22
22
  pairs = new Map();
23
23
  surrenderSeenAt = new Map();
24
24
  running = false;
25
- /** Every human phase that began, for the autopilot hook. */
25
+ /** Every human phase that began -- a seam for tests and for any future
26
+ * phase clock; nothing plays the phase for a human (the fight
27
+ * autopilot was retired 2026-09-07: a phase you commit with "end
28
+ * turn" is yours to play). */
26
29
  onHumanPhase;
27
30
  constructor(scene, logger, room) {
28
31
  this.scene = scene;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.135.0",
3
+ "version": "5.136.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.",
@@ -1,240 +0,0 @@
1
- import { Logger } from './logger.js';
2
- /**
3
- * Fight-or-flight autopilot -- the FIGHT half. The player sets a
4
- * temperature (Tab in the UI, or the "stance" command) and the game keeps
5
- * swinging for them; flight stays manual, always, because stopping YOUR
6
- * swings never stops THEIRS -- the only real defense is the door, and
7
- * that's the player's call.
8
- *
9
- * manual -- counters only. Struck, you strike back (the exchange
10
- * engine answers every swing in EVERY stance -- see
11
- * CombatExchange.resolveStrike's two-way resolution);
12
- * but pressing the fight, chasing, or finishing is yours
13
- * to type. (A real session read "manual" as fully
14
- * passive, saw the built-in counters, and concluded the
15
- * stances were redundant -- they never were; the old
16
- * descriptions were wrong.)
17
- * guarded -- keeps swinging FOR you between exchanges, with a
18
- * conscience: switches to subdue when the target is
19
- * nearly down, and stands down the moment they surrender
20
- * aggressive -- keeps swinging until they're dead; surrender means
21
- * nothing
22
- *
23
- * Wiring: CombatExchange.finishExchange() -> Scene.notifyExchangeSettled()
24
- * -> onExchangeSettled() here. If the player was a combatant and the
25
- * stance says fight, the next action is scheduled a short, interruptible
26
- * beat later (REENGAGE_DELAY_MS). Submitting ANY command in the UI cancels
27
- * the pending action and takes over (see ui.ts) -- the beat exists exactly
28
- * so fleeing and healing stay possible mid-autofight. Everything is
29
- * revalidated at fire time: the fight may have ended, moved, or been
30
- * surrendered out of during the delay.
31
- *
32
- * Auto actions lead with whatever hits hardest: a caster whose manabolt
33
- * out-damages their equipped weapon throws the bolt (and eats the drain);
34
- * everyone else uses the gun/blade/fists they're holding. A dry gun's
35
- * refusal (reload prompt) is surfaced to the player and auto simply waits
36
- * for them to deal with it -- reloading is a decision, not a reflex.
37
- */
38
- export class AutoFight {
39
- game;
40
- static REENGAGE_DELAY_MS = 2000;
41
- // One pending timer per HUMAN player (symmetric MP: each human's
42
- // autopilot runs independently).
43
- pending = new Map();
44
- constructor(game) {
45
- this.game = game;
46
- }
47
- /** Pending auto actions, dropped. No name = all of them (the local
48
- * player typed, or the stance changed -- ui.ts's call). */
49
- cancelPending(playerName) {
50
- if (playerName === undefined) {
51
- for (const t of this.pending.values())
52
- clearTimeout(t);
53
- this.pending.clear();
54
- return;
55
- }
56
- const t = this.pending.get(playerName);
57
- if (t) {
58
- clearTimeout(t);
59
- this.pending.delete(playerName);
60
- }
61
- }
62
- /**
63
- * Called after every combat exchange resolves (player-involved or not).
64
- * Schedules the next action for every HUMAN combatant whose stance
65
- * says fight.
66
- */
67
- onExchangeSettled(combatants) {
68
- for (const player of this.game.scene.getPlayers()) {
69
- if (!combatants.includes(player))
70
- continue;
71
- if (player.autoStance === 'manual')
72
- continue;
73
- // A COMBAT TURN schedules from the phase (onPhaseBegins), never
74
- // from a settled strike -- acting between phases is the thing the
75
- // turn structure exists to stop.
76
- if (this.game.scene.encounterFor?.(player))
77
- continue;
78
- const target = combatants.find(c => c !== player);
79
- if (!target)
80
- continue;
81
- this.cancelPending(player.name);
82
- const t = setTimeout(() => {
83
- this.pending.delete(player.name);
84
- void this.fire(player, target);
85
- }, AutoFight.REENGAGE_DELAY_MS);
86
- this.pending.set(player.name, t);
87
- }
88
- }
89
- /**
90
- * A COMBAT TURN handed this human their Action Phase
91
- * (utilities/combat-turn.ts). At any stance but manual the autopilot
92
- * plays the phase after the same interruptible beat: draw if
93
- * holstered, the stance's strike, then "end turn". Typing anything
94
- * cancels it and takes over, exactly as between exchanges.
95
- */
96
- onPhaseBegins(player, enc) {
97
- if (player.autoStance === 'manual')
98
- return;
99
- this.cancelPending(player.name);
100
- const t = setTimeout(() => {
101
- this.pending.delete(player.name);
102
- void this.playPhase(player, enc);
103
- }, AutoFight.REENGAGE_DELAY_MS);
104
- this.pending.set(player.name, t);
105
- }
106
- async playPhase(player, enc) {
107
- const logger = Logger.getInstance();
108
- if (player.autoStance === 'manual' || !enc.isPhaseOf(player) || player.isIncapacitated())
109
- return;
110
- const run = async (cmd) => {
111
- if (!enc.isPhaseOf(player))
112
- return;
113
- logger.meta(`⚔ [${player.autoStance}] ${cmd}`, 'cyan');
114
- const result = (await this.game.handleInputAs(player.name, cmd)).text;
115
- if (result)
116
- logger.log(result);
117
- };
118
- const target = enc.enemiesOf(player)[0];
119
- if (!target) {
120
- await run('end-turn');
121
- return;
122
- }
123
- if (player.autoStance === 'non-lethal' && target.surrendered) {
124
- if (player.plane === 'meat' && target.plane === 'meat'
125
- && player.grappling !== target.name && !target.grappledBy) {
126
- await run(`restrain ${target.name}`);
127
- }
128
- else {
129
- logger.logWithColor(`⚔ ${target.name} has surrendered -- you hold fire. (non-lethal)`, 'yellow');
130
- }
131
- await run('end-turn');
132
- this.game.updateStatus();
133
- return;
134
- }
135
- const weapon = player.getCarriedWeapon();
136
- if (weapon && !player.weaponDrawn && player.plane === 'meat')
137
- await run('draw');
138
- await run(this.chooseAction(player, target));
139
- await run('end-turn');
140
- this.game.updateStatus();
141
- }
142
- /**
143
- * The beat elapsed uninterrupted -- act, if the fight still stands.
144
- * Every guard here re-checks live state: the target may have died, fled,
145
- * surrendered, or started another exchange during the delay.
146
- */
147
- async fire(player, target) {
148
- const logger = Logger.getInstance();
149
- if (player.autoStance === 'manual')
150
- return;
151
- if (player.isIncapacitated() || target.isIncapacitated())
152
- return;
153
- if (player.currentLocation !== target.currentLocation)
154
- return;
155
- // Cross-plane fights don't exist: a dump or a return mid-beat (see
156
- // utilities/planes.ts) ends the engagement, autopilot included.
157
- if (player.plane !== target.plane)
158
- return;
159
- // A fresh exchange is already in flight -- when IT settles,
160
- // onExchangeSettled reschedules us. No waiting here.
161
- if (player.inExchange || target.inExchange)
162
- return;
163
- // Guarded honors a surrender -- but doesn't trust it: the final
164
- // action is a RESTRAIN (player request; SR5e Subduing p.195, the
165
- // surrendered branch is an automatic no-roll hold). Hands-up
166
- // prisoners get zip-tied, not taken at their word. One-shot: an
167
- // already-held target (by anyone) means truly stand down, and off
168
- // the meat plane there are no hands to hold with.
169
- if (player.autoStance === 'non-lethal' && target.surrendered) {
170
- if (player.plane === 'meat' && target.plane === 'meat'
171
- && player.grappling !== target.name && !target.grappledBy) {
172
- const cmd = `restrain ${target.name}`;
173
- logger.meta(`⚔ [non-lethal] ${cmd}`, 'cyan');
174
- const result = (await this.game.handleInputAs(player.name, cmd)).text;
175
- if (result) {
176
- logger.log(result);
177
- }
178
- this.game.updateStatus();
179
- return;
180
- }
181
- logger.logWithColor(`⚔ ${target.name} has surrendered -- you hold fire. (non-lethal)`, 'yellow');
182
- this.game.updateStatus();
183
- return;
184
- }
185
- const cmd = this.chooseAction(player, target);
186
- // Cyan, not gray: the autopilot announcing it acted must be legible on
187
- // stock dark palettes (gray = ANSI bright-black, near-invisible there).
188
- // Mechanics ticker, not the narrative -- the strike it triggers tells
189
- // the story in the world log (readability pass).
190
- logger.meta(`⚔ [${player.autoStance}] ${cmd}`, 'cyan');
191
- const result = (await this.game.handleInputAs(player.name, cmd)).text;
192
- // Player-initiated exchanges self-announce ('' result); anything else
193
- // (a dry-gun reload prompt, a refusal) is information the player needs.
194
- if (result) {
195
- logger.log(result);
196
- }
197
- this.game.updateStatus();
198
- }
199
- /**
200
- * Lead with what hits hardest -- except Guarded near the end, which
201
- * finishes non-lethally: when the target is within a few boxes of death,
202
- * the next auto action is a subdue (stun damage, bare hands) instead of
203
- * another killing blow.
204
- */
205
- chooseAction(player, target) {
206
- // Subduing takes real hands (meat plane only); a guarded decker
207
- // "killing" ice is just crashing a program, and astral entities go
208
- // down by force -- no non-lethal switch off the meat plane.
209
- if (player.autoStance === 'non-lethal' && player.plane === 'meat') {
210
- if (target.maxConditionBoxes - target.damageTaken <= 3)
211
- return `subdue ${target.name}`;
212
- // A gun that can only kill is not what this stance reaches for:
213
- // no stun rounds loaded means hands, not lead (p.434).
214
- const gun = player.weaponDrawn ? player.getCarriedWeapon() : null;
215
- if (gun?.isFirearm() && !gun.isStunWeapon() && !gun.stunAmmo())
216
- return `subdue ${target.name}`;
217
- }
218
- // Magic doesn't compile in the Matrix (see cast.ts); everywhere else,
219
- // lead with the harder hit. The autopilot casts at the mage's FULL
220
- // safe Force (bolt damage = F - 2, drain billed, never overcast) --
221
- // a bare "cast manabolt" would use the drain-neutral default (F3,
222
- // damage 1: the Force pass silently gutted the old comparison).
223
- // Same safe ceiling as cast.ts: a book-taught mundane works at 4.
224
- const boltForce = player.magic > 0 ? player.magic : 4;
225
- const boltDamage = Math.max(1, boltForce - 2);
226
- if (player.plane !== 'matrix' && player.isCaster() && boltDamage > player.getWeaponProfile().damage) {
227
- return `cast manabolt ${boltForce} at ${target.name}`;
228
- }
229
- return `attack ${target.name}`;
230
- }
231
- /** One-line HUD/announce description for a stance. */
232
- static describe(stance) {
233
- switch (stance) {
234
- case 'manual': return 'counters when struck (every stance does) -- but pressing the fight is yours to type';
235
- case 'non-lethal': return 'presses the fight for you with Stun wherever the gear allows (stun rounds, the flat of the blade, bare hands), subdues when they are nearly down, and RESTRAINS them when they surrender';
236
- case 'lethal': return 'presses the fight to the death; surrender means nothing';
237
- }
238
- }
239
- }
240
- //# sourceMappingURL=auto-fight.js.map