@maka/maka-cli 5.164.0 → 5.166.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.164.0",
3
+ "version": "5.166.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.",
@@ -28,9 +28,25 @@ export async function runNpcActionPhase(enc, npc) {
28
28
  const logger = Logger.getInstance();
29
29
  if (npc.isIncapacitated() || npc.surrendered)
30
30
  return;
31
+ // THE ROOM SEES THE PHASE (aCjJiDDJM9jGZJeER, web run 2026-09-11: "I
32
+ // can't see Hammerhead Joe doing any combat" -- a hostile NPC). The
33
+ // verbs below answer the ACTOR: their return text is second person,
34
+ // goes into the NPC's own history and to a shell's master, and to
35
+ // nobody else. Only an attack that connects narrates through the
36
+ // exchange. So a phase spent readying a gun, reloading, closing the
37
+ // distance or taking cover -- most of a melee NPC's phases -- read to
38
+ // the player as "Joe acts (Initiative 9)." followed by nothing, which
39
+ // is the report. One third-person line per action, room-scoped, the
40
+ // same scope the exchange itself announces on.
41
+ const room = npc.currentLocation;
42
+ const tell = (line) => {
43
+ if (room)
44
+ logger.log(line, { room: room.name });
45
+ };
31
46
  const enemies = enc.enemiesOf(npc);
32
47
  if (enemies.length === 0) {
33
48
  logger.write(`Brain: ${npc.name} has nobody to fight -- phase forfeited.`);
49
+ tell(`${npc.name} looks for a target and finds none.`);
34
50
  return;
35
51
  }
36
52
  const target = pickTarget(npc, enemies);
@@ -49,8 +65,11 @@ export async function runNpcActionPhase(enc, npc) {
49
65
  if (weapon && !npc.weaponDrawn) {
50
66
  const r = await npc.actInCombat('draw');
51
67
  logger.write(`Brain: ${npc.name} draw -> ${r}`);
52
- if (!npc.weaponDrawn)
68
+ if (!npc.weaponDrawn) {
69
+ tell(`${npc.name} fumbles for ${weapon.name} and comes up empty-handed.`);
53
70
  return; // refused -- nothing else will go better
71
+ }
72
+ tell(`${npc.name} readies ${weapon.name}.`);
54
73
  continue;
55
74
  }
56
75
  // 2. A dry gun: reload if the pack allows, else it is a club.
@@ -58,6 +77,7 @@ export async function runNpcActionPhase(enc, npc) {
58
77
  if (dry && !weapon.jammed && hasAmmo(npc)) {
59
78
  const r = await npc.actInCombat('reload');
60
79
  logger.write(`Brain: ${npc.name} reload -> ${r}`);
80
+ tell(`${npc.name} reloads ${weapon.name}.`);
61
81
  return; // a reload is the phase (p.163 Reloading Weapons)
62
82
  }
63
83
  const melee = !firearm || dry || weapon.jammed;
@@ -68,16 +88,22 @@ export async function runNpcActionPhase(enc, npc) {
68
88
  logger.write(`Brain: ${npc.name} move to ${targetSpot} -> ${r}`);
69
89
  if (!canReach(npc, targetSpot)) {
70
90
  // Couldn't close this turn: get behind something and wait.
91
+ tell(`${npc.name} closes on ${target.name} but can't cover the ground this turn.`);
71
92
  if (!isInCover(npc) && coverAvailableFor(npc) && (enc.budgetOf(npc)?.simple ?? 0) > 0) {
72
93
  await npc.actInCombat('cover');
94
+ if (isInCover(npc))
95
+ tell(`${npc.name} ducks behind cover.`);
73
96
  }
74
97
  return;
75
98
  }
99
+ tell(`${npc.name} closes on ${target.name}.`);
76
100
  }
77
101
  const b = enc.budgetOf(npc);
78
102
  if (b?.complexAvailable) {
79
103
  const r = await npc.actInCombat(`attack ${target.name}`);
80
104
  logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
105
+ if (!enc.budgetOf(npc)?.attackTaken)
106
+ tell(`${npc.name} goes for ${target.name} and can't make it count.`);
81
107
  }
82
108
  return;
83
109
  }
@@ -87,13 +113,18 @@ export async function runNpcActionPhase(enc, npc) {
87
113
  logger.write(`Brain: ${npc.name} cover -> ${r}`);
88
114
  if (!isInCover(npc))
89
115
  return;
116
+ tell(`${npc.name} ducks behind cover.`);
90
117
  continue;
91
118
  }
92
119
  if (!budget.attackTaken && budget.simple >= 1) {
93
120
  const r = await npc.actInCombat(`attack ${target.name}`);
94
121
  logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
95
- if (!enc.budgetOf(npc)?.attackTaken)
122
+ if (!enc.budgetOf(npc)?.attackTaken) {
123
+ // The shot was refused (range, line, a jam): the room should
124
+ // see the attempt, not silence.
125
+ tell(`${npc.name} lines up on ${target.name} and the shot doesn't come.`);
96
126
  return; // refused: don't loop on it
127
+ }
97
128
  continue;
98
129
  }
99
130
  return;
@@ -156,6 +156,70 @@ export function readReproFile(candidate) {
156
156
  * accepts a sink array, so per step we take the meta events emitted
157
157
  * since the last step and print them under the output they explain.
158
158
  */
159
+ /** The bench's actor. One name, used for the seat and for the
160
+ * visibility filter below. */
161
+ export const REPRO_ACTOR = 'Tester';
162
+ const stripTags = (s) => s.replace(/\{\/?[a-z-]+\}/g, '');
163
+ /**
164
+ * ONE STEP OF TRANSCRIPT, pure: the command's return string, then what
165
+ * the actor HEARD, then the dice, then the panels.
166
+ *
167
+ * THE LOG CHANNEL IS PART OF THE TRANSCRIPT, and it was missing
168
+ * (aCjJiDDJM9jGZJeER, 2026-09-11). A command's return string is what the
169
+ * engine says to the one who typed it; everything a room is told -- an
170
+ * NPC drawing, closing, taking cover, a companion's line, the exchange
171
+ * narration itself -- goes out as `log` events scoped to the room and
172
+ * resolved to the players standing in it. The dry run printed the reply
173
+ * and the dice and dropped every one of those, so a bench whose whole
174
+ * point is "the room now sees Joe's Action Phase" showed Joe's roll in
175
+ * the mechanics block and NOT the line the fix added -- the same "a
176
+ * bench that cannot display its own point" shape this renderer has been
177
+ * extended for twice already (mechanics, then panels).
178
+ *
179
+ * Only what THE ACTOR can see: an event resolved to other players is
180
+ * not on this transcript, because the bench sits one runner at one
181
+ * table and a line they never saw is not evidence of anything.
182
+ */
183
+ export function renderStepLines(step, text, fresh, actor = REPRO_ACTOR) {
184
+ const lines = [];
185
+ lines.push(`> ${step}`);
186
+ lines.push(stripTags(text ?? '(no output)'));
187
+ const heard = fresh.filter(e => e.channel === 'log'
188
+ && (e.visibleTo === 'all' || e.visibleTo.includes(actor)));
189
+ for (const h of heard) {
190
+ const body = stripTags(h.text).replace(/\s+$/gm, '');
191
+ if (body.trim().length === 0)
192
+ continue;
193
+ for (const row of body.split('\n'))
194
+ lines.push(` [heard] ${row}`);
195
+ }
196
+ for (const m of fresh.filter(e => e.channel === 'meta')) {
197
+ lines.push(` [mechanics] ${stripTags(m.text)}`);
198
+ }
199
+ // THE DOCKED PANELS ARE PART OF THE TRANSCRIPT TOO, for exactly
200
+ // the reason the mechanics ticker above is.
201
+ //
202
+ // The map and room boxes are a THIRD channel: not the command's
203
+ // return string, not the meta ticker, but per-player `system`
204
+ // events. A bench whose subject is a panel -- "the NPC I can see
205
+ // is not on the room map", an elevation bar, a district caption --
206
+ // dry-ran green while showing none of what it exists to show,
207
+ // which is the same "a bench that cannot display its own point"
208
+ // failure this function was already extended once to fix.
209
+ //
210
+ // Printed only when the panel has something to say: an empty
211
+ // event is the jacked-into-the-matrix contract, and blank
212
+ // [room]/[map] blocks on every step would bury the steps.
213
+ for (const p of fresh.filter(e => e.channel === 'system' && (e.kind === 'map' || e.kind === 'room'))) {
214
+ const body = stripTags(p.text ?? '').replace(/\s+$/gm, '');
215
+ if (body.trim().length === 0)
216
+ continue;
217
+ for (const row of body.split('\n'))
218
+ lines.push(` [${p.kind}] ${row}`);
219
+ }
220
+ lines.push('');
221
+ return lines;
222
+ }
159
223
  export async function dryRunRepro(repro) {
160
224
  const [{ buildReproScene, reproArchetypeName }, { bootSession, saveForArchetype }] = await Promise.all([
161
225
  import('../factories/repro-scene.js'),
@@ -170,10 +234,9 @@ export async function dryRunRepro(repro) {
170
234
  const events = [];
171
235
  const session = await bootSession({
172
236
  seed: built.seed,
173
- players: [saveForArchetype('Tester', reproArchetypeName(fixture))],
237
+ players: [saveForArchetype(REPRO_ACTOR, reproArchetypeName(fixture))],
174
238
  events,
175
239
  });
176
- const strip = (s) => s.replace(/\{\/?[a-z-]+\}/g, '');
177
240
  const lines = [];
178
241
  // THE FIRST PAINT IS NOT PART OF STEP ONE. bootSession pushes every
179
242
  // player's panels the moment the session comes up (B7NNNesy5gvRSbZpp),
@@ -183,40 +246,13 @@ export async function dryRunRepro(repro) {
183
246
  let drained = events.length;
184
247
  try {
185
248
  for (const step of repro.steps) {
186
- const { text } = await session.command('Tester', step);
187
- lines.push(`> ${step}`);
188
- lines.push(strip(text ?? '(no output)'));
249
+ const { text } = await session.command(REPRO_ACTOR, step);
189
250
  // Only what THIS step produced: the cursor advances past
190
- // everything seen, including log events, so a step that emits no
191
- // dice prints no Mechanics block rather than repeating the last
192
- // one that did.
251
+ // everything seen, so a step that emits no dice prints no
252
+ // Mechanics block rather than repeating the last one that did.
193
253
  const fresh = events.slice(drained);
194
254
  drained = events.length;
195
- for (const m of fresh.filter(e => e.channel === 'meta')) {
196
- lines.push(` [mechanics] ${strip(m.text)}`);
197
- }
198
- // THE DOCKED PANELS ARE PART OF THE TRANSCRIPT TOO, for exactly
199
- // the reason the mechanics ticker above is.
200
- //
201
- // The map and room boxes are a THIRD channel: not the command's
202
- // return string, not the meta ticker, but per-player `system`
203
- // events. A bench whose subject is a panel -- "the NPC I can see
204
- // is not on the room map", an elevation bar, a district caption --
205
- // dry-ran green while showing none of what it exists to show,
206
- // which is the same "a bench that cannot display its own point"
207
- // failure this function was already extended once to fix.
208
- //
209
- // Printed only when the panel has something to say: an empty
210
- // event is the jacked-into-the-matrix contract, and blank
211
- // [room]/[map] blocks on every step would bury the steps.
212
- for (const p of fresh.filter(e => e.channel === 'system' && (e.kind === 'map' || e.kind === 'room'))) {
213
- const body = strip(p.text ?? '').replace(/\s+$/gm, '');
214
- if (body.trim().length === 0)
215
- continue;
216
- for (const row of body.split('\n'))
217
- lines.push(` [${p.kind}] ${row}`);
218
- }
219
- lines.push('');
255
+ lines.push(...renderStepLines(step, text, fresh));
220
256
  }
221
257
  }
222
258
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.164.0",
3
+ "version": "5.166.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.",