@maka/maka-cli 5.139.0 → 5.141.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.139.0",
3
+ "version": "5.141.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.",
@@ -4,15 +4,10 @@ import { Log } from '../../tools/log/log.class.js';
4
4
  * Prompts for which environment to target when --env wasn't given.
5
5
  */
6
6
  export async function promptForEnv(cfg, opts = {}) {
7
- const { message = 'Which environment?', excludeLocal = false } = opts;
8
- let configs = cfg.getAppProjectConfigs();
9
- if (excludeLocal) {
10
- configs = configs.filter(config => config !== 'local');
11
- }
7
+ const { message = 'Which environment?' } = opts;
8
+ const configs = cfg.getAppProjectConfigs();
12
9
  if (configs.length === 0) {
13
- Log.error(excludeLocal
14
- ? 'No configuration other than local found. Consider running "maka g:config production" first.'
15
- : 'No configurations found. Consider running "maka g:config local" first.');
10
+ Log.error('No configurations found. Consider running "maka g:config local" first.');
16
11
  process.exit(1);
17
12
  }
18
13
  if (configs.length === 1) {
@@ -34,7 +34,7 @@ Command.create({
34
34
  ]
35
35
  }, async function (args, opts) {
36
36
  try {
37
- const env = opts.env ? this.cfg.getEnvFromShorthand(opts.env) : await promptForEnv(this.cfg, { excludeLocal: true });
37
+ const env = opts.env ? this.cfg.getEnvFromShorthand(opts.env) : await promptForEnv(this.cfg);
38
38
  if (!this.cfg.checkConfigExists(env)) {
39
39
  Log.error(`No configuration: ${env}, consider running "maka g:config ${env}"`);
40
40
  process.exit(1);
@@ -388,5 +388,9 @@
388
388
  // tells a human holding the Action Phase that the table is waiting; the
389
389
  // third nudge on one phase ends it for them (forfeit, as "end turn" with
390
390
  // nothing spent). Off the phase it is a private tap on the shoulder.
391
- export const ENGINE_VERSION = '1.43.0';
391
+ // 1.44.0 (2026-09-07): WHO STARTED IT. The fight's first line names the
392
+ // aggressor and their target; the world event carries it; "initiative"
393
+ // marks them. Hostiles are red on the room map by stance or by live
394
+ // enmity, not by the retired exchange flag.
395
+ export const ENGINE_VERSION = '1.44.0';
392
396
  //# sourceMappingURL=engine-version.js.map
@@ -35,7 +35,7 @@ import { hostileContactBeat } from './utilities/hostile-contact.js';
35
35
  import { requestAlarmBeat, newAlarmBeatLatch, ALARM_STIMULUS } from './utilities/alarmed-staff.js';
36
36
  import { renderMapViewport, mapCaption } from './utilities/map-view.js';
37
37
  import { tallestRoomRows } from './utilities/room-grid.js';
38
- import { renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
38
+ import { hostileByStance, enemyInFight, renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
39
39
  import { getPref } from './utilities/prefs.js';
40
40
  import { rollPool, formatRoll } from './utilities/dice.js';
41
41
  import { Direction } from './types/shared/direction-enum.js';
@@ -5623,6 +5623,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5623
5623
  isTableAlly: crewOfTable(this.scene.getPlayers()),
5624
5624
  boxWidth,
5625
5625
  isOtherPlayer: a => this.scene.isHumanControlled(a),
5626
+ // Red is the stance flag OR whoever the Combat Turn says is
5627
+ // fighting you (room-view.ts hostileByStance).
5628
+ isHostile: a => hostileByStance(a) || enemyInFight(this.scene, this.player, a),
5626
5629
  });
5627
5630
  if (roomLines.length > 0) {
5628
5631
  roomLines.push(caption(roomCaption(here, this.player), boxWidth));
@@ -17,7 +17,7 @@ import { Logger } from './utilities/logger.js';
17
17
  import { currentSession, runInSession } from './utilities/session-context.js';
18
18
  import { restorePlayer, serializePlayer } from './utilities/persistence.js';
19
19
  import { renderMapViewport, mapCaption } from './utilities/map-view.js';
20
- import { renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
20
+ import { hostileByStance, enemyInFight, renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
21
21
  import { serializeRoomGrid } from './utilities/room-grid.js';
22
22
  import { entrySpotFor, spotOf } from './utilities/spots.js';
23
23
  import { equipIntoEmptySlots } from './archetypes.js';
@@ -319,6 +319,7 @@ export async function createHeadlessSession(opts) {
319
319
  try {
320
320
  const roomLines = renderRoomLayoutCompact(here, p, {
321
321
  isOtherPlayer: a => game.scene.isHumanControlled(a),
322
+ isHostile: a => hostileByStance(a) || enemyInFight(game.scene, p, a),
322
323
  // The leader's crew are the TABLE's crew -- without this
323
324
  // every non-leader saw them as neutral bystanders.
324
325
  isTableAlly: crewOfTable(game.scene.getPlayers()),
@@ -84,6 +84,10 @@ export class CombatEncounter {
84
84
  /** Whose Action Phase is live, and their budget. */
85
85
  phaseActor;
86
86
  budget;
87
+ /** Who opened the fight, and on whom (playtest 2026-09-07: "nothing
88
+ * tells me who the aggressor is"). The tracker marks them. */
89
+ openedBy;
90
+ openedOn;
87
91
  pairs = new Map();
88
92
  surrenderSeenAt = new Map();
89
93
  running = false;
@@ -106,13 +110,21 @@ export class CombatEncounter {
106
110
  async start(members, opts) {
107
111
  this.turn = 1;
108
112
  this.pass = 1;
113
+ this.openedBy = opts.aggressor;
114
+ this.openedOn = opts.target;
109
115
  for (const m of members)
110
116
  this.addParticipant(m);
111
117
  if (opts.target)
112
118
  this.noteAttack(opts.aggressor, opts.target);
113
119
  this.seedHostility();
114
120
  const metaLines = ['Initiative (SR5 p.159):'];
115
- const worldLines = [];
121
+ // WHO STARTED IT is the first thing the room hears (playtest
122
+ // 2026-09-07): before the Surprise results and before the order.
123
+ const worldLines = [
124
+ opts.target
125
+ ? `{red-fg}⚔ ${opts.aggressor.name} starts it -- opening on ${opts.target.name}.{/red-fg}`
126
+ : `{red-fg}⚔ ${opts.aggressor.name} starts it.{/red-fg}`,
127
+ ];
116
128
  // SURPRISE (p.192): the ambushed side rolls Reaction + Intuition (3).
117
129
  // Failure: -10 Initiative and no defense against the surprisers
118
130
  // until their next Action Phase. The aggressor never rolls -- an
@@ -141,7 +153,7 @@ export class CombatEncounter {
141
153
  metaLines.push(` ${p.actor.name}: ${formatInitiative(p.roll)}${p.score !== p.roll.score ? ` -> ${p.score}` : ''}`);
142
154
  }
143
155
  worldLines.push(`Combat Turn ${this.turn} -- ${this.orderLine()}.`);
144
- this.scene.addWorldEvent(`A fight broke out in ${this.room.name}: ${this.participants.map(p => p.actor.name).join(', ')}.`);
156
+ this.scene.addWorldEvent(`A fight broke out in ${this.room.name} -- ${opts.aggressor.name} opened${opts.target ? ` on ${opts.target.name}` : ''}. In it: ${this.participants.map(p => p.actor.name).join(', ')}.`);
145
157
  await this.announce(worldLines, metaLines);
146
158
  await this.run();
147
159
  }
@@ -624,6 +636,7 @@ export class CombatEncounter {
624
636
  : p.actor.surrendered ? 'surrendered'
625
637
  : p.acted ? 'acted' : p.delayed ? 'delaying' : p.score > 0 ? 'to act' : 'spent';
626
638
  const marks = [
639
+ p.actor === this.openedBy ? 'started it' : '',
627
640
  p.surprised ? 'surprised' : '',
628
641
  p.fullDefenseTurn === this.turn ? 'full defense' : '',
629
642
  p.actor.aimBonus > 0 ? `aiming +${p.actor.aimBonus}` : '',
@@ -98,7 +98,12 @@ async function open(scene, room, npc, player, why) {
98
98
  : why === 'contact'
99
99
  ? `{yellow-fg}${npc.name} is done looking at you.{/yellow-fg}`
100
100
  : `{yellow-fg}${npc.name} has waited long enough.{/yellow-fg}`;
101
+ // Logged to the room BEFORE the encounter opens, not returned after
102
+ // it: the fight announces itself ("⚔ X starts it -- opening on Y",
103
+ // Surprise, the order) while startEncounter runs, and a returned line
104
+ // would print under all of that, out of order.
105
+ Logger.getInstance().log(line, { room: room.name });
101
106
  await scene.startEncounter(room, { aggressor: npc, target: player, ambush: false });
102
- return [line];
107
+ return [];
103
108
  }
104
109
  //# sourceMappingURL=hostile-contact.js.map
@@ -276,7 +276,33 @@ export function crewOfTable(humans) {
276
276
  return !!master && seated.has(master);
277
277
  };
278
278
  }
279
- function actorGlyph(a, viewer, isOtherPlayer, isTableAlly) {
279
+ /**
280
+ * RED MEANS HOSTILE (playtest 2026-09-07: "the icons on the map should
281
+ * be red if they are hostile"). Until then the sword was keyed to
282
+ * `inExchange`, which only the old two-actor exchange ever set -- the
283
+ * Combat Turn (combat-turn.ts) never touches it, so a ganger standing
284
+ * over you drew as a white local. The stance flag (Player.hostile: set
285
+ * only by a seed or a scene that means it, cleared when talked round)
286
+ * is the structural answer that needs no Scene; the exchange case stays
287
+ * for the astral/Matrix fights still on that engine. A caller with a
288
+ * Scene adds live enmity through `isHostile` (game.ts, headless.ts).
289
+ */
290
+ export function hostileByStance(a) {
291
+ if (a.surrendered)
292
+ return false;
293
+ const npc = a;
294
+ return (npc.hostile === true && !npc.allyOf) || a.inExchange;
295
+ }
296
+ /**
297
+ * Live enmity, for a caller that holds the Scene: `a` is in a running
298
+ * Combat Turn the viewer is also in, on the other side. Typed
299
+ * structurally so this file stays free of the Scene import.
300
+ */
301
+ export function enemyInFight(scene, viewer, a) {
302
+ const enc = scene.encounterFor?.(a);
303
+ return !!enc && enc.has(viewer) && enc.enemiesOf(viewer).includes(a);
304
+ }
305
+ function actorGlyph(a, viewer, isOtherPlayer, isTableAlly, isHostile) {
280
306
  // Still an actor, so still alive -- the dead are Room.bodies, drawn
281
307
  // separately as skulls. This is someone face-down and revivable.
282
308
  if (a.isIncapacitated())
@@ -313,7 +339,7 @@ function actorGlyph(a, viewer, isOtherPlayer, isTableAlly) {
313
339
  if (isTableAlly?.(a) ?? a.allyOf === viewer.name) {
314
340
  return { ch: CREW_GLYPH, color: 'green' };
315
341
  }
316
- if (a.inExchange && !a.surrendered)
342
+ if (isHostile?.(a) ?? hostileByStance(a))
317
343
  return { ch: HOSTILE_GLYPH, color: 'red' };
318
344
  return { ch: NEUTRAL_GLYPH, color: 'white' };
319
345
  }
@@ -321,18 +347,18 @@ function actorGlyph(a, viewer, isOtherPlayer, isTableAlly) {
321
347
  * The glyph for one cell, in priority order: people, then terrain, then
322
348
  * vertical links, then exits, then the spot itself.
323
349
  */
324
- function glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly) {
350
+ function glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile) {
325
351
  const k = key(c);
326
352
  const occ = maps.occupants.get(k);
327
353
  if (occ && occ.length > 0) {
328
354
  if (occ.includes(viewer))
329
- return actorGlyph(viewer, viewer, isOtherPlayer, isTableAlly);
355
+ return actorGlyph(viewer, viewer, isOtherPlayer, isTableAlly, isHostile);
330
356
  if (occ.length === 1)
331
- return actorGlyph(occ[0], viewer, isOtherPlayer, isTableAlly);
357
+ return actorGlyph(occ[0], viewer, isOtherPlayer, isTableAlly, isHostile);
332
358
  // A crowded cell shows the most urgent icon standing in it: a fight
333
359
  // outranks a runner outranks a bystander, and a body never hides
334
360
  // someone still upright.
335
- const glyphs = occ.map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly));
361
+ const glyphs = occ.map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly, isHostile));
336
362
  return glyphs.find(g => g.ch === HOSTILE_GLYPH)
337
363
  ?? glyphs.find(g => g.ch === PLAYER_GLYPH)
338
364
  // A fellow runner outranks a bystander for the same reason you
@@ -398,7 +424,7 @@ function levelPlansFor(room) {
398
424
  * glyph used. Returns [] for a spotless room (no grid ever synthesizes
399
425
  * -- see Room.ensureGrid).
400
426
  */
401
- export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
427
+ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly, isHostile) {
402
428
  const grid = room.ensureGrid();
403
429
  if (!grid)
404
430
  return [];
@@ -419,7 +445,7 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
419
445
  let row = '';
420
446
  for (let x = x0; x <= x1; x++) {
421
447
  const c = { x, y, z: lvl.z };
422
- row += exists.has(key(c)) ? cell(glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly)) : ' ';
448
+ row += exists.has(key(c)) ? cell(glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile)) : ' ';
423
449
  }
424
450
  rows.push(row.trimEnd());
425
451
  }
@@ -483,7 +509,7 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
483
509
  }
484
510
  // People, once, only for the icons actually standing in the room --
485
511
  // a key to four kinds of person in an empty bar is just noise.
486
- const present = new Set(visibleActorsIn(room, viewer).map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly).ch));
512
+ const present = new Set(visibleActorsIn(room, viewer).map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly, isHostile).ch));
487
513
  const who = [
488
514
  // "you" leads the key: on a party job it is the answer to the
489
515
  // question that sent the player to the legend in the first place.
@@ -508,7 +534,7 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
508
534
  present.has(OTHER_PLAYER_GLYPH) ? `{${OTHER_PLAYER_COLOR}-fg}${OTHER_PLAYER_GLYPH}{/${OTHER_PLAYER_COLOR}-fg} runner` : '',
509
535
  present.has(CREW_GLYPH) ? `{green-fg}${CREW_GLYPH}{/green-fg} yours` : '',
510
536
  present.has(NEUTRAL_GLYPH) ? `{white-fg}${NEUTRAL_GLYPH}{/white-fg} local` : '',
511
- present.has(HOSTILE_GLYPH) ? `{red-fg}${HOSTILE_GLYPH}{/red-fg} fighting` : '',
537
+ present.has(HOSTILE_GLYPH) ? `{red-fg}${HOSTILE_GLYPH}{/red-fg} hostile` : '',
512
538
  // The knocked-out mark was missing from this key entirely -- the one
513
539
  // glyph on the map a player is most likely to need explained.
514
540
  present.has(DOWNED_GLYPH) ? `{yellow-fg}${DOWNED_GLYPH}{/yellow-fg} out cold` : '',
@@ -712,7 +738,7 @@ function renderShellCompact(room, viewer, availW, viewH, boxWidth) {
712
738
  export function renderRoomLayoutCompact(room, viewer, opts = {}) {
713
739
  // Default to the TALLEST room class, not a fixed 9 -- see
714
740
  // room-grid.ts tallestRoomRows for why that number is derived.
715
- const { viewH = tallestRoomRows(), boxWidth, isOtherPlayer, isTableAlly } = opts;
741
+ const { viewH = tallestRoomRows(), boxWidth, isOtherPlayer, isTableAlly, isHostile } = opts;
716
742
  // Width left for the grid once the right-justified elevation bar and
717
743
  // its gap are reserved.
718
744
  const availW = Math.max(6, (boxWidth ?? 27) - ELEVATION_WIDTH - 1);
@@ -735,7 +761,7 @@ export function renderRoomLayoutCompact(room, viewer, opts = {}) {
735
761
  const row = [];
736
762
  for (let x = x0; x <= x1; x++) {
737
763
  const c = { x, y, z };
738
- row.push(exists.has(key(c)) ? glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly) : undefined);
764
+ row.push(exists.has(key(c)) ? glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile) : undefined);
739
765
  }
740
766
  cells.push(row);
741
767
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.139.0",
3
+ "version": "5.141.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.",