@maka/maka-cli 5.146.0 → 5.148.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.146.0",
3
+ "version": "5.148.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.",
@@ -73,6 +73,14 @@ export class AdvanceCommand extends Command {
73
73
  this.actor.performAction('trains', skill);
74
74
  this.logger.write(`${actor.name} advanced ${skill} to ${next} (spent ${cost} karma, ${actor.karma} left).`);
75
75
  this.scene.updateStatus();
76
+ // A DURABLE CHANGE (fDswPCSf8tGhMPMZ6, same shape as vR8ajvm3wXdexxdF2:
77
+ // buy/sell learned this, advance never did). Karma spent and a skill
78
+ // raised is exactly the kind of change equip.ts already asks for a
79
+ // save over -- without it, a player who trains and quits before the
80
+ // next autosave beat reloads to a lower skill AND the karma still
81
+ // gone, and the web character sheet (reading the last saved doc)
82
+ // shows the stale rating.
83
+ this.game?.requestSave('advance');
76
84
  return `You drill ${skill} until it sticks: ${current} -> ${next}. (${cost} karma spent, ${actor.karma} left.)`;
77
85
  }
78
86
  trainAttribute(attr) {
@@ -111,6 +119,7 @@ export class AdvanceCommand extends Command {
111
119
  this.actor.performAction('trains', attr);
112
120
  this.logger.write(`${actor.name} advanced ${attr} to base ${attr === 'magic' ? actor.baseMagic : actor.baseResonance} (effective ${effective}; spent ${cost} karma, ${actor.karma} left).`);
113
121
  this.scene.updateStatus();
122
+ this.game?.requestSave('advance');
114
123
  return `The talent deepens: ${attr} ${base} -> ${base + 1}${actor.essenceBurn > 0 ? ` (effective ${effective} after the chrome's toll)` : ''}. (${cost} karma spent, ${actor.karma} left.)`;
115
124
  }
116
125
  const current = actor[attr];
@@ -136,6 +145,7 @@ export class AdvanceCommand extends Command {
136
145
  this.actor.performAction('trains', attr);
137
146
  this.logger.write(`${actor.name} advanced ${attr} to ${next} (spent ${cost} karma, ${actor.karma} left).`);
138
147
  this.scene.updateStatus();
148
+ this.game?.requestSave('advance');
139
149
  const trackNote = attr === 'body' ? ` Your physical track grows with it.` : attr === 'reaction' ? ` Your stun track grows with it.` : '';
140
150
  return `Weeks of drills compressed into downtime: ${attr} ${current} -> ${next}. (${cost} karma spent, ${actor.karma} left.)${trackNote}`;
141
151
  }
@@ -6,7 +6,7 @@ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
6
6
  import { describeFledTarget } from '../utilities/pursuit.js';
7
7
  import { shellsInRoom, resolveBodyAttack, crossPlaneRefusal } from '../utilities/planes.js';
8
8
  import { heldBy, holding } from '../utilities/grapple.js';
9
- import { spotOf, ensureAtSpot, ensureAtExit, canReach, spotDistanceMeters } from '../utilities/spots.js';
9
+ import { spotOf, ensureAtSpot, ensureAtExit, canReach, spotDistanceMeters, OPEN_FLOOR } from '../utilities/spots.js';
10
10
  import { resolveBarrierStrike } from '../utilities/barrier-combat.js';
11
11
  import { billAction, notYourPhase, phaseHint } from '../utilities/action-cost.js';
12
12
  import { spendMovementMeters } from '../utilities/movement-cost.js';
@@ -369,6 +369,18 @@ export class AttackCommand extends Command {
369
369
  // MELEE REACH: ground costs (p.161-162).
370
370
  if (isMelee) {
371
371
  const targetSpot = spotOf(target);
372
+ // An attacker with no spot of their own reads to canReach as
373
+ // "reaches everything" (the same compat rule that lets an unbound
374
+ // exit be worked from anywhere) -- fine for a location query, but
375
+ // for the ATTACKER in a reach check it skips ground-closing
376
+ // billing entirely: a hostile spawned mid-room, never having
377
+ // moved, could swing on a target the player had just paced away
378
+ // from in a fully gridded room (MotczCjebFNdHAWq7). Resolving
379
+ // an unset spot to the open floor first -- move.ts's own default
380
+ // -- gives canReach and spotDistanceMeters a real position to
381
+ // measure the reach/cost from instead of "unknown = anywhere".
382
+ if (spotOf(actor) === undefined)
383
+ actor.atSpot = OPEN_FLOOR;
372
384
  const mine = spotOf(actor);
373
385
  if (targetSpot !== undefined && !canReach(actor, targetSpot)) {
374
386
  const meters = mine !== undefined && room.ensureGrid() ? spotDistanceMeters(room, mine, targetSpot) : 0;
@@ -0,0 +1,54 @@
1
+ import { Command } from './command.js';
2
+ /**
3
+ * "there should be a command for counterspelling" (3vCH5uQvpKZdjzWYQ).
4
+ *
5
+ * There already is counterspelling -- it just isn't a command. RAW
6
+ * (p.294) is a STANDING PARRY: a Counterspelling-skilled defender's
7
+ * dice roll straight into the defense test the instant a hostile spell
8
+ * targets them, and a co-located party caster can extend theirs over a
9
+ * teammate the same way ("one source, no stacking" -- the better rating
10
+ * covers, they don't add). combat-exchange.ts already does exactly
11
+ * this, automatically, on every spell strike, and narrates it on the
12
+ * Defense line ("+N counterspell -- Name's cover") when it bites.
13
+ *
14
+ * So the report's actual gap is not mechanics, it's that typing
15
+ * "counterspell" got "I don't understand the command" -- a player
16
+ * reaching for a verb that (correctly, per RAW) does not exist. This
17
+ * command answers that reach with the truth instead of a parser error:
18
+ * what protection they're already carrying and why nothing needs to be
19
+ * typed to use it.
20
+ *
21
+ * ONE HONEST GAP: canon also lets a defender who has already spent
22
+ * their Free Action declare protection reactively as an Interrupt
23
+ * Action at Initiative -5 (p.294). This engine's Action Phase economy
24
+ * (utilities/action-cost.ts) has no off-phase Free Action today --
25
+ * `IBillOptions.anyPhase` is declared and reserved for exactly this,
26
+ * unused by anything yet -- so the reactive-declare half of spell
27
+ * defense isn't modeled. What's implemented (the standing parry, which
28
+ * is what actually answers "I have counterspelling, why didn't it
29
+ * help") works with no action spent at all, which is the more common
30
+ * case by far.
31
+ */
32
+ export class CounterspellCommand extends Command {
33
+ static verb = 'counterspell';
34
+ static description = 'Check your counterspelling cover. Nothing to spend: your Counterspelling skill (or a co-located party caster\'s, if theirs is better) automatically rolls into the defense the instant a hostile spell targets you or an ally.';
35
+ async execute(_args = []) {
36
+ const actor = this.actor;
37
+ if (actor.isAspected && !actor.canWorkSorcery()) {
38
+ return `Counterspelling is Sorcery, and your gift is aspected elsewhere -- you can see the weave, not turn it.`;
39
+ }
40
+ if (typeof actor.isCaster !== 'function' || !actor.isCaster()) {
41
+ return `Counterspelling takes the Art or a spell formula, grimoire, or focus on you -- you have none.`;
42
+ }
43
+ const rating = actor.skillRating('counterspelling');
44
+ if (rating <= 0) {
45
+ return `You have no Counterspelling trained -- "qualities" or "advance" at home if you want to pick it up.`;
46
+ }
47
+ return `Counterspelling isn't something you trigger -- it's a standing parry (SR5 p.294).`
48
+ + `\nEvery hostile spell that targets you, or a party member you're co-located with, automatically`
49
+ + `\nrolls your counterspelling ${rating} into their defense test -- no action spent. If more than one`
50
+ + `\ncaster in the room could cover the same target, the best single rating applies (it doesn't stack).`
51
+ + `\nWatch the Defense line for "+${rating} counterspell" when it bites.`;
52
+ }
53
+ }
54
+ //# sourceMappingURL=counterspell.js.map
@@ -57,7 +57,10 @@ export class DocwagonCommand extends Command {
57
57
  const surcharge = Math.ceil(def.price * check.surchargePct);
58
58
  const total = def.price + surcharge;
59
59
  if (this.actor.currency < total) {
60
- return `${reachable.name} taps the rate card: ${total} nuyen a year for ${tier}${surcharge > 0 ? ` (papers with a smell run extra)` : ''}. You're carrying ${this.actor.currency}.`;
60
+ return [
61
+ ...(check.lines[0] ? [check.lines[0]] : []),
62
+ `${reachable.name} taps the rate card: ${total} nuyen a year for ${tier}${surcharge > 0 ? ` (papers with a smell run extra)` : ''}. You're carrying ${this.actor.currency}.`,
63
+ ].join('\n');
61
64
  }
62
65
  const renewal = this.game.docwagon?.tier === tier;
63
66
  this.actor.adjustCurrency(-total);
@@ -161,7 +161,10 @@ export class InstallCommand extends Command {
161
161
  paymentLine = `You put the ${part.name} on the tray. The doc turns it over once, nods. "Part's good. Installation's on the house story-rate."`;
162
162
  }
163
163
  else if (actor.currency < aug.cost) {
164
- return `${aug.name} runs ${aug.cost} nuyen -- you're carrying ${actor.currency}. The doc doesn't do tabs on surgery.`;
164
+ return [
165
+ ...(identityLine ? [identityLine] : []),
166
+ `${aug.name} runs ${aug.cost} nuyen -- you're carrying ${actor.currency}. The doc doesn't do tabs on surgery.`,
167
+ ].join('\n');
165
168
  }
166
169
  else {
167
170
  actor.adjustCurrency(-aug.cost);
@@ -162,6 +162,24 @@ export function buildReproScene(fixture) {
162
162
  throw new ReproFixtureError(`"${misplacedIce.name}" is ice, so it cannot also stand in a doorway, guard, circulate or carry a gun. `
163
163
  + `Ice lives in the host, not on the floor -- drop the meat-side flags, or drop "ice".`);
164
164
  }
165
+ // A SEED HAS ONE PLANE. astral maps to 'astral' the same way ice maps
166
+ // to 'matrix' (npc-seed.ts) -- asking for both is asking the builder
167
+ // to pick a lie.
168
+ const bothPlanes = npcs.find(n => n.ice && n.astral);
169
+ if (bothPlanes) {
170
+ throw new ReproFixtureError(`"${bothPlanes.name}" is asked to be both ice and astral -- an actor has one plane, pick one.`);
171
+ }
172
+ // AN ASTRAL FORM CANNOT HOLD A DOOR SHUT OR CARRY A GUN. "guarding"
173
+ // means blocking meat movement, and "an astral presence has no body
174
+ // to block with" IS the fix this flag exists to bench -- a fixture
175
+ // asking for both would prove the opposite of what it claims. armed
176
+ // hands out a pistol and a commlink, and an astral form carries no
177
+ // physical object (SR5 p.301-302, Spirit Basics).
178
+ const contradictoryAstral = npcs.find(n => n.astral && (n.guarding || n.armed));
179
+ if (contradictoryAstral) {
180
+ throw new ReproFixtureError(`"${contradictoryAstral.name}" is astral, so it cannot guard a door or carry a gun -- `
181
+ + `an astral form has no body to block or hold anything with. Drop the flag, or drop "astral".`);
182
+ }
165
183
  const barrier = fixture.barrier;
166
184
  const direction = barrier?.sealsExit ?? Direction.NORTH;
167
185
  const startSide = fixture.start ?? 'barrier';
@@ -339,6 +357,12 @@ export function buildReproScene(fixture) {
339
357
  };
340
358
  }
341
359
  return {
360
+ // UNLIKE ice, astral does not move the NPC anywhere -- it keeps
361
+ // the ordinary placement below. The whole point of the fix this
362
+ // flag benches is that an astral actor stands right where a meat
363
+ // one would and still does not block (seatingIn is unchanged by
364
+ // the fix; only WHETHER it blocks changed, never WHERE it sits).
365
+ ...(npc.astral ? { plane: 'astral' } : {}),
342
366
  description: npc.concept,
343
367
  dialogue: [],
344
368
  player: {
@@ -170,6 +170,7 @@ import { SneakCommand } from './commands/sneak.js';
170
170
  import { PersuadeCommand } from './commands/persuade.js';
171
171
  import { BluffCommand } from './commands/bluff.js';
172
172
  import { DispelCommand } from './commands/dispel.js';
173
+ import { CounterspellCommand } from './commands/counterspell.js';
173
174
  import { BreachCommand } from './commands/breach.js';
174
175
  import { EmoteCommand } from './commands/emote.js';
175
176
  import { OverwatchCommand } from './commands/overwatch.js';
@@ -2648,6 +2649,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2648
2649
  CommandFactory.registerCommand('dispel', DispelCommand);
2649
2650
  CommandFactory.registerCommand('dispell', DispelCommand);
2650
2651
  CommandFactory.registerCommand('breach', BreachCommand);
2652
+ // "counterspell" is not an action to spend -- it explains the
2653
+ // standing parry combat-exchange.ts already applies automatically
2654
+ // (3vCH5uQvpKZdjzWYQ).
2655
+ CommandFactory.registerCommand('counterspell', CounterspellCommand);
2651
2656
  // Free-form roleplay ("me" is the classic MUD spelling).
2652
2657
  CommandFactory.registerCommand('emote', EmoteCommand);
2653
2658
  CommandFactory.registerCommand('me', EmoteCommand);
@@ -6051,7 +6056,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6051
6056
  // "players" folded back under crew after playtesting ("crew
6052
6057
  // players" / "crew street"); the bare verb still answers quietly.
6053
6058
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
6054
- { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense']] },
6059
+ { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
6055
6060
  { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6056
6061
  // The Emerged get their own shelf (player request: "there MUST be
6057
6062
  // resonance" -- matrix is the place, Resonance is the talent).
@@ -1840,8 +1840,9 @@ ${worldEventsSummary}
1840
1840
  ${wares.map(i => `- "${i.name}" (${priceOf(i)} nuyen)`).join('\n')}
1841
1841
  When a customer orders or agrees to buy, SERVE it in the same response: [COMMAND] give <item name> to <customer name>
1842
1842
  Serve EXACTLY the stock item whose name best matches what the customer asked for (they say "noodles", you have a "Noodle Bowl" -- that's the one). Substitute a different dish ONLY if the match is out of stock, and say so out loud when you do. The stock list above is the truth about what you have -- never claim a listed item is gone.
1843
- The till settles automatically on handoff (they're charged street price, or what they can cover). Quote the price when they order. Never serve someone who hasn't asked.`
1844
- : '';
1843
+ The till settles automatically on handoff (they're charged street price, or what they can cover). Quote the price when they order. Never serve someone who hasn't asked.
1844
+ NEVER claim a sale is complete from dialogue alone -- no "here you go," no till chirping, no describing them pocketing anything, unless this SAME response also issues the [COMMAND] give line above. A sale without that command simply never happened, and the customer is left owning nothing while believing they bought something.`
1845
+ : `YOUR SHOP STOCK: you have NOTHING owner-tagged to you on the shelf/counter right now -- the list above is empty. If a customer asks to buy something, you do not have it to sell: say so honestly (out of stock, don't carry it, check back later), or point them at where they might actually find it. NEVER narrate handing over an item, taking payment, or a till settling for anything that isn't real stock -- there is nothing here to [COMMAND] give, so nothing changes hands no matter how the conversation reads.`;
1845
1846
  // SWAG OVER THE LINE (source.ts; the shop-stock lesson's remote
1846
1847
  // twin): a contact with a TRADE can order anything in their domain
1847
1848
  // through the network -- but only the order system closes a deal.
@@ -20,8 +20,11 @@
20
20
  * would themselves have been a MINOR bump had a version already
21
21
  * existed. Read it as "schema, second edition", not "second edition
22
22
  * broke something".
23
+ *
24
+ * 2.4.0 adds IReproNpc.astral -- purely additive, older drafts still
25
+ * valid.
23
26
  */
24
- export const REPRO_SCHEMA_VERSION = '2.3.0';
27
+ export const REPRO_SCHEMA_VERSION = '2.4.0';
25
28
  /** THE LIVE REASON, from the field or the card's own words.
26
29
  *
27
30
  * `repro.live` is authoritative -- but the server's validator must
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.146.0",
3
+ "version": "5.148.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.",