@maka/maka-cli 5.138.0 → 5.140.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.138.0",
3
+ "version": "5.140.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.",
@@ -0,0 +1,88 @@
1
+ import { Command } from './command.js';
2
+ import { hint } from '../utilities/hints.js';
3
+ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
4
+ import { NUDGES_TO_SKIP } from '../utilities/combat-turn.js';
5
+ /**
6
+ * NUDGE -- the table's answer to a runner who is holding the Action
7
+ * Phase and has gone quiet (project owner, 2026-09-07: "three nudges
8
+ * skips the player being nudged").
9
+ *
10
+ * Since the Combat Turn (utilities/combat-turn.ts) a human's phase
11
+ * waits for "end turn", and since the autopilot went (v5.136.0)
12
+ * nothing plays a phase for anyone. On a shared run that leaves one
13
+ * hole: a human who walks away mid-phase stalls the whole table. There
14
+ * is deliberately no clock -- a timer skips the player who was reading
15
+ * the rules -- so the escape is the table's: every nudge tells the
16
+ * nudged runner, in their own log, that people are waiting; the third
17
+ * nudge on one phase ends it for them, whatever they had left forfeit,
18
+ * exactly as "end turn" with nothing spent (SR5 p.158-159; the book
19
+ * gives no refund). The count is the phase's (CombatEncounter.noteNudge)
20
+ * and starts over the next time they act.
21
+ *
22
+ * Off the phase, or out of a fight, a nudge is a tap on the shoulder
23
+ * only the nudged runner sees. Human-only: an NPC acts by rule on its
24
+ * own phase and must never be able to skip a human's.
25
+ */
26
+ export class NudgeCommand extends Command {
27
+ static verb = 'nudge';
28
+ static humanOnly = true;
29
+ static description = 'Prod another runner on a shared run: "nudge <name>". If they are holding the Action Phase, that tells them the table is waiting -- the third nudge on one phase ends it for them, whatever is unspent forfeit, the same as "end turn" with nothing spent. Off the phase, or out of a fight, it is a tap on the shoulder only they see. "initiative" names who is holding the phase.';
30
+ /** The scene's other humans -- the only legal targets. */
31
+ others() {
32
+ return this.scene.getPlayers().filter(p => p !== this.actor);
33
+ }
34
+ async execute(args = []) {
35
+ if (!this.scene.isHumanControlled(this.actor))
36
+ return '';
37
+ const me = this.actor;
38
+ const query = (args ?? []).join(' ').trim();
39
+ const others = this.others();
40
+ if (others.length === 0) {
41
+ return `You're running this one alone -- there's nobody to nudge.${hint(` (Seat a table with "crew hire"; "nudge" is for a shared run.)`)}`;
42
+ }
43
+ if (!query) {
44
+ return `Nudge who? ${others.map(p => p.name).join(', ')}. Usage: "nudge <runner>".`;
45
+ }
46
+ if (query.toLowerCase() === me.name.toLowerCase()) {
47
+ return `Nudging yourself moves nothing. If it's your phase, act -- or "end turn".`;
48
+ }
49
+ const picked = fuzzyPickName(query, others.map(p => p.name));
50
+ const target = picked ? others.find(p => p.name === picked) : undefined;
51
+ if (!target) {
52
+ return `No runner on this job answers to "${query}". Out here with you: ${others.map(p => p.name).join(', ')}. (An NPC acts by rule on its own phase -- nothing to nudge there.)`;
53
+ }
54
+ if (this.scene.getKnockout?.(target.name)) {
55
+ return `${target.name} is out cold -- no nudge reaches them.`;
56
+ }
57
+ const enc = this.scene.encounterFor?.(target);
58
+ const count = enc?.noteNudge(target);
59
+ if (count === undefined) {
60
+ // A TAP ON THE SHOULDER, seen by them alone -- the same per-human
61
+ // scope beginPhase uses for the cyan "your Action Phase" line.
62
+ this.logger.log(`{yellow-fg}${me.name} nudges you.{/yellow-fg}`, { actor: target.name });
63
+ this.logger.write(`${me.name} nudged ${target.name} (no phase to count against).`);
64
+ const holder = enc?.phaseActor;
65
+ const aside = enc && holder && holder !== target
66
+ ? ` It isn't ${target.name}'s Action Phase -- ${holder === me ? `it's yours` : `${holder.name} is holding it`}.`
67
+ : '';
68
+ return `You nudge ${target.name}.${aside}`;
69
+ }
70
+ const room = target.currentLocation;
71
+ if (count >= NUDGES_TO_SKIP) {
72
+ // THE SKIP. Logged, not returned: the phases that follow announce
73
+ // themselves while this command is still running (end-turn.ts has
74
+ // the same note), and a return value would land under them.
75
+ this.logger.log(`{yellow-fg}${target.name}'s Action Phase is skipped -- nudged ${NUDGES_TO_SKIP} times; whatever they had left is forfeit.{/yellow-fg}`, { room: room.name });
76
+ this.logger.write(`${me.name}'s nudge #${count} skips ${target.name}'s Action Phase.`);
77
+ await enc.endPhase(target);
78
+ return '';
79
+ }
80
+ this.logger.log(`{yellow-fg}${me.name} nudges you -- the table is waiting on your Action Phase (${count}/${NUDGES_TO_SKIP}; ${NUDGES_TO_SKIP} and it is skipped). "end turn" when you're done.{/yellow-fg}`, { actor: target.name });
81
+ this.logger.log(`${me.name} nudges ${target.name} (${count}/${NUDGES_TO_SKIP}).`, { room: room.name });
82
+ this.logger.write(`${me.name} nudged ${target.name} (${count}/${NUDGES_TO_SKIP}).`);
83
+ // The room line above already reached the nudger when they share the
84
+ // room; from elsewhere they get the summary instead.
85
+ return me.currentLocation === room ? '' : `You nudge ${target.name} (${count}/${NUDGES_TO_SKIP}).`;
86
+ }
87
+ }
88
+ //# sourceMappingURL=nudge.js.map
@@ -384,5 +384,13 @@
384
384
  // arrival. During a live encounter the ephemeral governor, open-floor
385
385
  // re-seating and doorway yielding are frozen: nobody moves off the
386
386
  // player's keystrokes. bluff/persuade wins clear the hostile flag.
387
- export const ENGINE_VERSION = '1.42.0';
387
+ // 1.43.0 (2026-09-07): NUDGE. New verb in shared scenes: "nudge <runner>"
388
+ // tells a human holding the Action Phase that the table is waiting; the
389
+ // third nudge on one phase ends it for them (forfeit, as "end turn" with
390
+ // nothing spent). Off the phase it is a private tap on the shoulder.
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';
388
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';
@@ -80,6 +80,7 @@ import { InitiativeCommand } from './commands/initiative.js';
80
80
  import { AimCommand } from './commands/aim.js';
81
81
  import { DefendCommand } from './commands/defend.js';
82
82
  import { DelayCommand } from './commands/delay.js';
83
+ import { NudgeCommand } from './commands/nudge.js';
83
84
  import { SpellsCommand } from './commands/spells.js';
84
85
  import { InventoryCommand } from './commands/inv.js';
85
86
  import { EquipmentCommand } from './commands/equipment.js';
@@ -2553,6 +2554,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2553
2554
  CommandFactory.registerCommand('aim', AimCommand);
2554
2555
  CommandFactory.registerCommand('defend', DefendCommand);
2555
2556
  CommandFactory.registerCommand('delay', DelayCommand);
2557
+ CommandFactory.registerCommand('nudge', NudgeCommand);
2556
2558
  // 'train' used to alias advance; it belongs to crew instruction now
2557
2559
  // (commands/crew.ts) -- self-training is "advance".
2558
2560
  CommandFactory.registerCommand('advance', AdvanceCommand);
@@ -5621,6 +5623,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5621
5623
  isTableAlly: crewOfTable(this.scene.getPlayers()),
5622
5624
  boxWidth,
5623
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),
5624
5629
  });
5625
5630
  if (roomLines.length > 0) {
5626
5631
  roomLines.push(caption(roomCaption(here, this.player), boxWidth));
@@ -6040,7 +6045,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6040
6045
  static HELP_CATEGORIES = [
6041
6046
  { 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']] },
6042
6047
  { 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']] },
6043
- { 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']] },
6048
+ { title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
6044
6049
  // The party layer: everyone who walks (or flies, or manifests) at
6045
6050
  // your side answers to these.
6046
6051
  // "players" folded back under crew after playtesting ("crew
@@ -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()),
@@ -1,6 +1,71 @@
1
1
  import { rollInitiativeScore, formatInitiative, INITIATIVE_PASS_DROP, rollPool, formatRoll } from './dice.js';
2
2
  import { ActionBudget } from './action-budget.js';
3
3
  import { runNpcActionPhase } from './npc-combat-brain.js';
4
+ /**
5
+ * THE COMBAT TURN (SR5 p.158-161, RAG-checked 2026-09-06).
6
+ *
7
+ * This engine used to resolve a fight as EXCHANGES: one typed "attack"
8
+ * rolled a two-actor initiative, both sides struck, and a >10 gap bought
9
+ * one extra strike. That was the closest thing it had to the book's
10
+ * turn, and combat-exchange.ts said so. What it could never express was
11
+ * the thing the book is built on -- a sequence of Action Phases in
12
+ * which every participant spends a budget of actions, in an order the
13
+ * dice decided, until nobody has Initiative Score left.
14
+ *
15
+ * The sequence, as the book gives it:
16
+ * 1. ROLL INITIATIVE: Initiative attribute + Initiative Dice, summed
17
+ * (p.159). Highest acts first; ties break Edge, Reaction,
18
+ * Intuition, coin (p.159 "ERIC").
19
+ * 2-4. INITIATIVE PASS: each participant, highest score first, takes
20
+ * an ACTION PHASE -- two Simple Actions or one Complex, plus one
21
+ * Free (p.158). When everyone has acted, EVERY score drops by 10
22
+ * (p.159), and anyone still above zero goes again.
23
+ * 5. When nobody is above zero the Combat Turn ends and a new one
24
+ * begins at step 1 (p.159).
25
+ *
26
+ * Also carried here because the score lives here:
27
+ * - INTERRUPT ACTIONS (p.167-168) spend Initiative Score: Full
28
+ * Defense -10 for the Combat Turn, Block/Dodge/Parry -5 each for
29
+ * one test. Only affordable "if he has enough Initiative Score
30
+ * left" -- a score already at or below zero buys nothing (the p.191
31
+ * worked example).
32
+ * - WOUND MODIFIERS CHANGE THE SCORE AS THEY LAND (p.160 Changing
33
+ * Initiative): the difference is applied immediately, and it can
34
+ * reorder the pass. It never grants an extra action.
35
+ * - ENTERING LATE (p.160): roll as normal, then subtract 10 for every
36
+ * pass already gone.
37
+ * - DELAYING (p.161): an actor may hold their Action Phase and take
38
+ * it later in the same pass. Modelled as "act after everyone else
39
+ * this pass, keeping your score"; the pass-end -10 still applies.
40
+ * - SURPRISE (p.192): a fight opened on the unaware makes every other
41
+ * participant roll Reaction + Intuition (3); failure is -10 and no
42
+ * defense against, nor action against, the surprising side until
43
+ * their next Action Phase. +3 for anyone already alert.
44
+ * - MOVEMENT is a WHOLE-TURN total (p.161-162, Player.movedMetersThisTurn)
45
+ * and comes back at step 5, not per pass.
46
+ *
47
+ * SCOPE: one encounter per ROOM, on the physical planes (meat and a
48
+ * jumped-in drone). The Matrix keeps its own initiative structure
49
+ * (utilities/ic.ts maps IC to Combat Turns per player action) and
50
+ * astral combat still rides the exchange engine -- both are noted as
51
+ * gaps rather than folded in half-right.
52
+ *
53
+ * WHO DRIVES IT: a human's Action Phase waits for them to type actions
54
+ * and then "end turn" (commands/end-turn.ts). An NPC's phase is played
55
+ * by a deterministic brain (utilities/npc-combat-brain.ts) -- the LLM
56
+ * reflect chain is suspended for participants while the encounter runs,
57
+ * because a model answering "what next?" in a second or two cannot be
58
+ * sequenced against a dice-ordered pass, and was the reason NPCs used
59
+ * to shoot from between the beats of the player's own exchange.
60
+ */
61
+ /**
62
+ * Nudges on one Action Phase before the table ends it for the holder
63
+ * (commands/nudge.ts, owner ruling 2026-09-07). A ruling, not canon --
64
+ * the book has no idle player -- kept as a count rather than a clock so
65
+ * that a runner reading the rules is never skipped by a timer, only by
66
+ * three people saying so.
67
+ */
68
+ export const NUDGES_TO_SKIP = 3;
4
69
  export class CombatEncounter {
5
70
  scene;
6
71
  logger;
@@ -19,6 +84,10 @@ export class CombatEncounter {
19
84
  /** Whose Action Phase is live, and their budget. */
20
85
  phaseActor;
21
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;
22
91
  pairs = new Map();
23
92
  surrenderSeenAt = new Map();
24
93
  running = false;
@@ -41,13 +110,21 @@ export class CombatEncounter {
41
110
  async start(members, opts) {
42
111
  this.turn = 1;
43
112
  this.pass = 1;
113
+ this.openedBy = opts.aggressor;
114
+ this.openedOn = opts.target;
44
115
  for (const m of members)
45
116
  this.addParticipant(m);
46
117
  if (opts.target)
47
118
  this.noteAttack(opts.aggressor, opts.target);
48
119
  this.seedHostility();
49
120
  const metaLines = ['Initiative (SR5 p.159):'];
50
- 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
+ ];
51
128
  // SURPRISE (p.192): the ambushed side rolls Reaction + Intuition (3).
52
129
  // Failure: -10 Initiative and no defense against the surprisers
53
130
  // until their next Action Phase. The aggressor never rolls -- an
@@ -76,7 +153,7 @@ export class CombatEncounter {
76
153
  metaLines.push(` ${p.actor.name}: ${formatInitiative(p.roll)}${p.score !== p.roll.score ? ` -> ${p.score}` : ''}`);
77
154
  }
78
155
  worldLines.push(`Combat Turn ${this.turn} -- ${this.orderLine()}.`);
79
- 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(', ')}.`);
80
157
  await this.announce(worldLines, metaLines);
81
158
  await this.run();
82
159
  }
@@ -317,6 +394,7 @@ export class CombatEncounter {
317
394
  this.budget = new ActionBudget();
318
395
  p.surprised = false;
319
396
  p.delayed = false;
397
+ p.nudges = 0;
320
398
  p.actor.clearAim();
321
399
  // Recoil resets the moment an Action Phase passes without firing --
322
400
  // tracked by the phase, settled by attack.ts (p.175-176).
@@ -342,6 +420,19 @@ export class CombatEncounter {
342
420
  const walkLeft = Math.max(0, actor.walkRateMeters - actor.movedMetersThisTurn);
343
421
  return `${left} m of movement left this turn (${walkLeft} m at a walk)`;
344
422
  }
423
+ /**
424
+ * Another human nudged the phase holder (commands/nudge.ts): the
425
+ * running count for this phase, or undefined when `target` is not
426
+ * the one holding it. Bookkeeping only -- the verb owns the wording
427
+ * and calls endPhase() itself at NUDGES_TO_SKIP.
428
+ */
429
+ noteNudge(target) {
430
+ const p = this.participantOf(target);
431
+ if (!p || this.ended || this.phaseActor !== target)
432
+ return undefined;
433
+ p.nudges = (p.nudges ?? 0) + 1;
434
+ return p.nudges;
435
+ }
345
436
  /** The human typed "end turn" (or lost the phase -- fled, dropped). */
346
437
  async endPhase(actor) {
347
438
  const p = this.participantOf(actor);
@@ -545,6 +636,7 @@ export class CombatEncounter {
545
636
  : p.actor.surrendered ? 'surrendered'
546
637
  : p.acted ? 'acted' : p.delayed ? 'delaying' : p.score > 0 ? 'to act' : 'spent';
547
638
  const marks = [
639
+ p.actor === this.openedBy ? 'started it' : '',
548
640
  p.surprised ? 'surprised' : '',
549
641
  p.fullDefenseTurn === this.turn ? 'full defense' : '',
550
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.138.0",
3
+ "version": "5.140.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.",