@maka/maka-cli 5.210.0 → 5.212.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.210.0",
3
+ "version": "5.212.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,4 +1,5 @@
1
1
  import { Command } from './command.js';
2
+ import { billAction } from '../utilities/action-cost.js';
2
3
  import { roomsInReach, gridVicinity } from '../utilities/grid-reach.js';
3
4
  import { hint } from '../utilities/hints.js';
4
5
  import { Category } from '../types/shared/item-enum.js';
@@ -38,11 +39,15 @@ import { fileReach, planeTintItemName } from '../utilities/planes.js';
38
39
  * else's sprite is not a shelf you may take from -- that is a hack, and
39
40
  * hack.ts owns it.
40
41
  *
41
- * DELIBERATELY NOT A TEST. Pulling a file you already have reach on is
42
- * not an opposed action in this engine -- `take` does not roll for it
43
- * either, and getting past whatever GUARDED the file is what the host
44
- * crack and the mark already charged for. Adding dice here would price
45
- * the same obstacle twice.
42
+ * DELIBERATELY NOT A TEST -- BUT IT IS AN ACTION. Pulling a file you
43
+ * already have reach on is not an opposed action in this engine --
44
+ * `take` does not roll for it either, and getting past whatever GUARDED
45
+ * the file is what the host crack and the mark already charged for.
46
+ * Adding dice here would price the same obstacle twice.
47
+ *
48
+ * The TIME is a separate bill, and it was going unpaid: copying a file
49
+ * is Edit File, a COMPLEX ACTION (p.239). See the billAction call in
50
+ * execute() for what that was costing in a fight.
46
51
  */
47
52
  export class DownloadCommand extends Command {
48
53
  static verb = 'download';
@@ -116,6 +121,27 @@ export class DownloadCommand extends Command {
116
121
  if (wc?.type === 'erase' && wc.item.toLowerCase() === item.name.toLowerCase()) {
117
122
  return `Pulling ${item.name} down changes nothing -- the desk keeps its own. ${hint(`"erase ${item.name}" is what makes it go away (Edit File, p.239).`)}`;
118
123
  }
124
+ // COPYING A FILE IS EDIT FILE, AND EDIT FILE IS A COMPLEX ACTION
125
+ // (p.239: "Edit File allows you to create, change, COPY, delete, or
126
+ // protect any kind of file"). The no-dice ruling above stands -- the
127
+ // host crack already charged for the obstacle -- but "not a test"
128
+ // was silently read as "not an action", and in a Combat Turn that is
129
+ // a different claim entirely. Found in jfjjTmJ7NYwDaFiyB's own
130
+ // bench: walking into a rating-3 host wakes Patrol IC, `search` is
131
+ // correctly refused ("a Matrix Search takes minutes ... a Combat
132
+ // Turn is three seconds") and `download` then emptied the archive
133
+ // for FREE with the ice mid-turn on the runner. That makes the fight
134
+ // the report asked for optional -- stroll in, take the paydata, and
135
+ // the ice never mattered.
136
+ //
137
+ // billAction is a no-op outside an encounter, so the quiet case --
138
+ // an empty host, a file loose on the grid -- is untouched: still one
139
+ // verb, no roll, no friction. It bills only where an action economy
140
+ // exists to bill against, and a refusal returns BEFORE the item
141
+ // moves, so a runner out of actions keeps both the file and the turn.
142
+ const bill = billAction(this.scene, actor, 'complex', 'Edit File (copy)');
143
+ if (bill)
144
+ return bill;
119
145
  // THE CONVERSION, identical to take.ts's. Same two fields, because a
120
146
  // file that came down by one verb and a file that came down by the
121
147
  // other must be the same object afterwards -- a chip you can `give`.
@@ -101,16 +101,19 @@ export class EraseMarkCommand extends Command {
101
101
  };
102
102
  if (target.kind === 'self') {
103
103
  for (const [placer, count] of actor.panMarksBy.entries()) {
104
- if (count > 0)
105
- out.push({ name: placer, count, drop: () => dec(actor.panMarksBy, placer) });
104
+ if (count > 0) {
105
+ out.push({ name: placer, count, defence: this.defenceOfPersona(placer), drop: () => dec(actor.panMarksBy, placer) });
106
+ }
106
107
  }
107
108
  for (const room of Object.values(this.scene.getRooms())) {
108
109
  const host = room.host;
109
110
  const n = host?.marksOn.get(actor.name) ?? 0;
110
111
  if (n > 0 && host) {
112
+ const d = hostDefensePool(host);
111
113
  out.push({
112
114
  name: hostLabel(room),
113
115
  count: n,
116
+ defence: { label: `${d.label}, rating standing in for Willpower`, pool: d.pool },
114
117
  drop: () => {
115
118
  dec(host.marksOn, actor.name);
116
119
  // A key gone is a sighting gone: an icon it no longer
@@ -128,11 +131,27 @@ export class EraseMarkCommand extends Command {
128
131
  // Your own key is not a mark you "erase" -- p.239 is about reaching
129
132
  // into an icon to scrub somebody ELSE's recognition pattern.
130
133
  if (count > 0 && placer !== actor.name) {
131
- out.push({ name: placer, count, drop: () => dec(ledger, placer) });
134
+ out.push({ name: placer, count, defence: this.defenceOfPersona(placer), drop: () => dec(ledger, placer) });
132
135
  }
133
136
  }
134
137
  return out;
135
138
  }
139
+ /**
140
+ * A NAMED PLACER'S OWN DEFENCE. p.237-238: "when a defense test calls
141
+ * for a Mental attribute, use the owner's rating", and an unattended
142
+ * device falls back to its Device Rating. A placer who has left the
143
+ * scene cannot roll, so the mark comes off unopposed -- which is the
144
+ * honest reading of erasing a key nobody is behind any more.
145
+ */
146
+ defenceOfPersona(name) {
147
+ const who = this.scene.allActors.find(a => a.name === name);
148
+ if (!who)
149
+ return { label: 'nobody behind it any more', pool: 0 };
150
+ return {
151
+ label: 'Willpower + Firewall',
152
+ pool: Math.max(0, who.willpower + who.matrixAttribute('firewall')),
153
+ };
154
+ }
136
155
  async execute(args = []) {
137
156
  const actor = this.actor;
138
157
  if (actor.plane !== 'matrix') {
@@ -176,7 +195,9 @@ export class EraseMarkCommand extends Command {
176
195
  const pool = Math.max(1, actor.logic + (computer > 0 ? computer : -1)
177
196
  + penalty + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
178
197
  const limit = actor.matrixAttribute('attack');
179
- const defence = this.defenceFor(target);
198
+ // THE PLACER ROLLS, not the icon the key is written on (p.239, see
199
+ // IPlacer). `chosen` is the icon whose keys this action is scrubbing.
200
+ const defence = chosen.defence;
180
201
  const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
181
202
  const resist = rollPool(defence.pool);
182
203
  if (showsMechanics(this.scene, actor)) {
@@ -212,18 +233,6 @@ export class EraseMarkCommand extends Command {
212
233
  ...(god.length > 0 ? [`\n${god.join('\n')}`] : []),
213
234
  ].join('');
214
235
  }
215
- /** p.239: Willpower + Firewall. A host has no Willpower; its rating stands in. */
216
- defenceFor(target) {
217
- if (target.kind === 'host') {
218
- const d = hostDefensePool(target.room.host);
219
- return { label: `${d.label}, rating standing in for Willpower`, pool: d.pool };
220
- }
221
- const who = target.player;
222
- return {
223
- label: 'Willpower + Firewall',
224
- pool: Math.max(0, who.willpower + who.matrixAttribute('firewall')),
225
- };
226
- }
227
236
  remainingOn(target, actor) {
228
237
  if (target.kind !== 'self')
229
238
  return 0;
@@ -956,5 +956,58 @@
956
956
  // fatigue floor when recovery is refused, and the isDown() narrowing.
957
957
  // A shared JOB still has no hideout -- deliberate and unchanged: the
958
958
  // crew rode to the jobsite, and nobody's cot is stapled to it.
959
- export const ENGINE_VERSION = '1.70.0';
959
+ // 1.71.0 (2026-09-16): WHAT f79LLqTerep4nKWcA'S OWN BENCH PRINTED. The
960
+ // repro bench for this item ran clean and then said three things a
961
+ // reviewer would have caught, which is what a bench is for.
962
+ // - ERASE MARK WAS DEFENDED BY THE WRONG ICON (p.239, RAG-checked).
963
+ // The book's worked example is explicit: "You roll your Computer +
964
+ // Logic ... opposed by THE IC'S RATING (standing in for Willpower) +
965
+ // Firewall to erase its mark on your icon." 1.61.0's defenceFor()
966
+ // switched on the TARGET instead, so "unmark me" rolled the
967
+ // DECKER'S OWN Willpower + Firewall while the line said the host was
968
+ // resisting -- 8 dice where a rating-8 host owes 18, and a label
969
+ // naming an icon that was not in the test. The defence now rides
970
+ // each placer (IPlacer), because the placer is who the book has
971
+ // rolling; the marked icon only ever mattered for the three-mark
972
+ // gate. A placer who has left the scene rolls nothing, which is the
973
+ // honest reading of scrubbing a key nobody is behind any more.
974
+ // - "SEALED" SURVIVED IN TWO MORE RENDERERS, gridSculpt and the host
975
+ // icon, and printed two lines above a row rewritten in 1.68.0 to
976
+ // stop saying it. It read as English there rather than as the
977
+ // jargon token hostLine used -- but the reporter named the word, and
978
+ // a reviewer meeting it twice cannot be expected to know one was
979
+ // innocent. Both say what they mean now.
980
+ // - THE DIVIDER DIVIDED NOTHING. gridIcons splices a "-- and NEAR it
981
+ // ... --" heading after the host's own icon; gridIconBlock then
982
+ // pulls the host line out into its own section, leaving the heading
983
+ // at the top of ICONS IN REACH with nothing above it -- a bullet
984
+ // that reads like an icon you could act on. It is dropped when it
985
+ // lands first. (1.69.0 fixed the same heading leaking into the
986
+ // distance bands; this is the other half.)
987
+ // 1.72.0 (2026-09-16): COPYING A FILE IS AN ACTION, NOT JUST A VERB
988
+ // (jfjjTmJ7NYwDaFiyB, Ted: "the encrypted data chip was just laying
989
+ // there in the meat world...hardly a fun run for a decker that never
990
+ // needs to attack a host, get the data, download it, and get out").
991
+ // The generation half of that report shipped in 5.171.0 -- a
992
+ // data-shaped objective for a runner who can jack in is paydata on a
993
+ // host, refused at the skeleton. Building this item's bench on top of
994
+ // it is what exposed the rest: walk into a rating-3 host, Patrol IC
995
+ // opens a Combat Turn, "search" refuses itself correctly ("a Matrix
996
+ // Search takes minutes (p.241), and a Combat Turn is three seconds")
997
+ // -- and "download" then emptied the archive for FREE with the ice
998
+ // mid-turn on the runner. The fight the report asked for was
999
+ // optional: stroll in, take the paydata, the ice never mattered.
1000
+ // Copying a file IS Edit File, and Edit File IS a Complex Action
1001
+ // (p.239, RAG-checked: "Edit File allows you to create, change, COPY,
1002
+ // delete, or protect any kind of file"). download.ts now bills one.
1003
+ // The deliberate no-dice ruling it was built with stands untouched --
1004
+ // the host crack already charged for the obstacle, and dice here would
1005
+ // price it twice -- but "not a test" had been read as "not an action",
1006
+ // and in a Combat Turn those are different claims. billAction is a
1007
+ // no-op outside an encounter, so the quiet case (an empty host, a file
1008
+ // loose on the grid) is exactly as it was: one verb, no roll, no
1009
+ // friction. The refusal returns before the item moves, so a runner out
1010
+ // of actions keeps both the file and the turn, and listing what is in
1011
+ // reach ("download" bare) still costs nothing.
1012
+ export const ENGINE_VERSION = '1.72.0';
960
1013
  //# sourceMappingURL=engine-version.js.map
@@ -119,7 +119,7 @@ export function gridIcons(scene, actor, room) {
119
119
  room.host?.spotted.has(actor.name) ? 'and it can see you'
120
120
  : room.hostAlert ? 'and it knows someone is on the grid' : undefined,
121
121
  ].filter(Boolean).join(', ');
122
- icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- sculpted ice geometry, sealed. Whatever it holds is inside it.${standing.length > 0 ? ` ${standing.charAt(0).toUpperCase()}${standing.slice(1)}.` : ''} (${ways.join('; ')})`);
122
+ icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- sculpted ice geometry, closed all the way round. Whatever it holds is inside it.${standing.length > 0 ? ` ${standing.charAt(0).toUpperCase()}${standing.slice(1)}.` : ''} (${ways.join('; ')})`);
123
123
  }
124
124
  }
125
125
  // WHERE THE HOST'S OWN ICON ENDS AND THE STREET BEGINS.
@@ -521,7 +521,13 @@ export function gridSculpt(actor, room) {
521
521
  const flat = `{light-blue-fg}Black flatland under a black sky -- the icons are the only things out here.${grid ? ` Riding ${grid.name}${grid.userPenalty ? ' (-2 on everything you do, p.233)' : ''}.` : ''}{/light-blue-fg}`;
522
522
  if (!room.hasNode)
523
523
  return `${header}\n${flat}`;
524
- return `${header}\n${flat}\n{light-blue-fg}${hostLabel(room, { capital: true })} hangs directly overhead, sealed. What it holds is inside it.{/light-blue-fg}`;
524
+ // "SEALED" IS A WORD THE REPORTER NAMED (f79LLqTerep4nKWcA). It read
525
+ // as English here rather than as the jargon token hostLine used it
526
+ // as -- but it survived into this item's own repro bench two lines
527
+ // above a row that had just been rewritten to stop saying it, and a
528
+ // reviewer meeting the same word twice cannot be expected to know one
529
+ // of them was innocent. Says what it means instead.
530
+ return `${header}\n${flat}\n{light-blue-fg}${hostLabel(room, { capital: true })} hangs directly overhead -- a closed shape. Everything it holds is inside it, and you get in by putting a mark on it.{/light-blue-fg}`;
525
531
  }
526
532
  /**
527
533
  * The icons rendered as the standard cyan block, or the thin-grid line.
@@ -559,7 +565,18 @@ export function gridIconBlock(scene, actor, room, rooms) {
559
565
  out.push(hosts.length > 0
560
566
  ? `{light-blue-fg}HOSTS OVERHEAD:{/light-blue-fg}\n${hosts.map(h => ` {light-blue-fg}${hostLine(h, actor)}{/light-blue-fg}`).join('\n')}`
561
567
  : `{light-blue-fg}Nothing overhead -- no host stands over this district.{/light-blue-fg}`);
562
- const near = gridIcons(scene, actor, gridVicinity(actor)).filter(i => !isHostLine(i));
568
+ // THE DIVIDER DIVIDES NOTHING HERE. gridIcons puts the host's own icon
569
+ // first and splices a "-- and NEAR it ... --" heading after it; this
570
+ // block then pulls the host line OUT (it has its own HOSTS OVERHEAD
571
+ // section above), which left the heading standing at the top of the
572
+ // list with nothing above it to be divided from -- a bullet reading
573
+ // "-- and NEAR it, out on the open grid (not inside the host): --"
574
+ // as if it were an icon you could act on. Seen in this item's own
575
+ // repro bench (f79LLqTerep4nKWcA). It earns its place only when
576
+ // something survives above it.
577
+ const near = gridIcons(scene, actor, gridVicinity(actor))
578
+ .filter(i => !isHostLine(i))
579
+ .filter((i, idx) => !(idx === 0 && isNearDivider(i)));
563
580
  out.push(near.length > 0
564
581
  ? `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${near.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`
565
582
  : `{light-blue-fg}Nothing near -- background noise around your signal.{/light-blue-fg}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.210.0",
3
+ "version": "5.212.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.",