@maka/maka-cli 5.131.0 → 5.133.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.131.0",
3
+ "version": "5.133.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.",
@@ -5,7 +5,8 @@ import { agentRatingFromName } from '../utilities/programs.js';
5
5
  import { resonanceAction } from '../resonance-actions.js';
6
6
  import { FIREARM_SKILLS } from '../models/player.js';
7
7
  import { ownedDrones, droneListLines } from '../utilities/owned-drones.js';
8
- import { droneLaunchAction, droneLaunchLine, droneRecallLine, droneWreckFragment } from '../utilities/drone-prose.js';
8
+ import { droneLaunchAction, droneLaunchLine, droneRecallLine, droneStowLine, droneHandHeld, droneWreckFragment } from '../utilities/drone-prose.js';
9
+ import { HolsterCommand } from './brandish.js';
9
10
  /**
10
11
  * The rigger's autonomous frames (SR5e p.269-271): "deploy <drone>" puts
11
12
  * a carried frame in the air under its own DOG-BRAIN -- no control rig
@@ -70,7 +71,7 @@ export class DeployCommand extends Command {
70
71
  // every catalog frame.
71
72
  return [
72
73
  `${droneLaunchLine(drone, from)} -- dog-brain running, Pilot ${drone.pilotRating}.`,
73
- ` It follows where you walk${armed ? ` and its mount tracks what threatens you` : ` -- cameras only; it will not fight`}. Hull ${drone.droneSummary()}.${hint(` ("order ${drone.name.toLowerCase()} ..." to direct it, "recall" to bring it home, "jump" to take the stick yourself, wherever it flies.)`)}`,
74
+ ` It follows where you walk${armed ? ` and its mount tracks what threatens you` : ` -- cameras only; it will not fight`}. Hull ${drone.droneSummary()}.${hint(` ("order ${drone.name.toLowerCase()} ..." to direct it, "recall" to bring it to your side, "stow" to pack it away, "jump" to take the stick yourself, wherever it flies.)`)}`,
74
75
  ].join('\n');
75
76
  }
76
77
  }
@@ -137,15 +138,66 @@ export class AgentCommand extends Command {
137
138
  ].join('\n');
138
139
  }
139
140
  }
140
- /** "recall": fold a deployed autonomous drone back to hand, spin a
141
- * remote agent down into its deck, or work CALL/DISMISS SPRITE -- see
142
- * the sprite branch below. */
141
+ function pickDeployedDrone(game, args) {
142
+ if (!game || args.length === 0)
143
+ return undefined;
144
+ const drones = game.companions.filter(c => c.kind === 'drone');
145
+ const names = drones.map(c => c.boundDevice?.name ?? c.npc.name);
146
+ const picked = fuzzyPickName(args.join(' '), names);
147
+ return picked ? drones[names.indexOf(picked)] : undefined;
148
+ }
149
+ /**
150
+ * "stow <drone>": fold a deployed frame back into your pack -- what
151
+ * "recall" used to do before the 2026-09-06 ruling made recall "return
152
+ * to me". Hand-held frames only (drone-prose.ts droneHandHeld): a Steel
153
+ * Lynx is not a pocket job and stays deployed ("store" parks it in the
154
+ * garage at the hideout). The verb was already the holster's ("stow"
155
+ * = holster your weapon) and still is: only a deployed frame the words
156
+ * name -- or a bare "stow" with a frame out and no steel drawn -- goes
157
+ * to the drone; everything else holsters exactly as before.
158
+ */
159
+ export class StowCommand extends Command {
160
+ static verb = 'stow';
161
+ static description = 'Stow a deployed drone back into your pack ("stow <drone>"; frames too big to carry stay out), or holster your drawn weapon.';
162
+ async execute(args = []) {
163
+ const game = this.game;
164
+ const drones = game?.companions.filter(c => c.kind === 'drone') ?? [];
165
+ const named = pickDeployedDrone(game, args);
166
+ const bareForDrone = args.length === 0 && drones.length > 0 && !this.actor.weaponDrawn;
167
+ const entry = named ?? (bareForDrone ? drones[0] : undefined);
168
+ if (!entry) {
169
+ return new HolsterCommand({ actor: this.actor, scene: this.scene, rooms: this.rooms, game: this.game }).execute(args);
170
+ }
171
+ const item = entry.boundDevice;
172
+ const name = item?.name ?? entry.npc.name;
173
+ if (this.actor.plane !== 'meat') {
174
+ return `Stowing is a hands job -- come back to your body.`;
175
+ }
176
+ if (item && !droneHandHeld(item)) {
177
+ return `The ${name} is not a pocket job -- it stays deployed.${game?.homeRoom ? ` At the hideout, "store ${name.toLowerCase()}" parks it in the garage.` : ''} "recall" brings it to your side.`;
178
+ }
179
+ if (entry.npc.currentLocation !== this.actor.currentLocation) {
180
+ return `The ${name} isn't here to stow -- it's in ${entry.npc.currentLocation.name}. "recall" it first.`;
181
+ }
182
+ if (entry.npc.inExchange) {
183
+ return `The ${name} is fighting for its life -- let the exchange settle before folding it.`;
184
+ }
185
+ game.dismissCompanion(entry, 'stowed');
186
+ this.actor.performAction('stows', name);
187
+ this.logger.write(`StowCommand: ${this.actor.name} stowed ${name}.`);
188
+ this.scene.updateStatus();
189
+ return item ? droneStowLine(item) : `The ${name} folds into your pack.`;
190
+ }
191
+ }
192
+ /** "recall": bring a deployed drone to your side (it stays deployed and
193
+ * follows again -- "stow" is the fold), spin a remote agent down into
194
+ * its deck, or work CALL/DISMISS SPRITE -- see the sprite branch below. */
143
195
  export class RecallCommand extends Command {
144
196
  static verb = 'recall';
145
- static description = `Recall your deployed drone to your hand, fold a running agent back into its deck, or call a registered sprite out of the Resonance. "${resonanceAction('call-dismiss-sprite')?.name}" (${resonanceAction('call-dismiss-sprite')?.actionType}, SR5 p.250): "recall <name>" also sends a standing registered sprite back -- which releases every task it still owes you. To park one cheaply instead, "standby" spends a single task (p.256).`;
197
+ static description = `Recall your deployed drone to your side -- it comes to you and follows again ("stow <drone>" folds it into your pack) -- fold a running agent back into its deck, or call a registered sprite out of the Resonance. "${resonanceAction('call-dismiss-sprite')?.name}" (${resonanceAction('call-dismiss-sprite')?.actionType}, SR5 p.250): "recall <name>" also sends a standing registered sprite back -- which releases every task it still owes you. To park one cheaply instead, "standby" spends a single task (p.256).`;
146
198
  async execute(args = []) {
147
199
  const game = this.game;
148
- const entry = game?.companions.find(c => c.kind === 'drone');
200
+ const entry = pickDeployedDrone(game, args) ?? game?.companions.find(c => c.kind === 'drone');
149
201
  if (!entry) {
150
202
  // No drone aloft -- but an agent on the grid answers to the same
151
203
  // word (a real session tried every phrasing but the bare "agent"
@@ -163,11 +215,23 @@ export class RecallCommand extends Command {
163
215
  return sprite;
164
216
  return `Nothing of yours is in the air.`;
165
217
  }
218
+ // RECALL IS "RETURN TO ME" (player ruling 2026-09-06, after a
219
+ // playtest: recall folded the frame into the hand and "it
220
+ // disappeared, which doesn't make sense -- it should just return to
221
+ // me and begin following me again"). The frame comes to your side,
222
+ // stays deployed, and is back on heel; "stow" is the fold.
166
223
  const name = entry.boundDevice?.name ?? entry.npc.name;
167
- game.dismissCompanion(entry, 'recalled');
224
+ if (entry.npc.inExchange) {
225
+ return `The ${name} is fighting for its life -- let the exchange settle before calling it off.`;
226
+ }
227
+ const { moved } = game.heelCompanion(entry);
228
+ if (!moved)
229
+ return `The ${name} is already at your side.`;
230
+ entry.npc.performAction('returns', `to ${this.actor.name}'s side`);
168
231
  this.actor.performAction('recalls', name);
232
+ this.logger.write(`RecallCommand: ${name} recalled to ${this.actor.name}'s side in ${this.actor.currentLocation.name}.`);
169
233
  this.scene.updateStatus();
170
- return entry.boundDevice ? droneRecallLine(entry.boundDevice) : `The ${name} comes back to hand.`;
234
+ return entry.boundDevice ? droneRecallLine(entry.boundDevice) : `The ${name} comes to your side and follows again.`;
171
235
  }
172
236
  /**
173
237
  * CALL/DISMISS SPRITE (SR5 p.250, Simple Action), on this verb rather
@@ -50,6 +50,23 @@ export class GoCommand extends Command {
50
50
  sprite: (plane) => plane === 'matrix',
51
51
  ally: () => false
52
52
  };
53
+ /**
54
+ * Does this companion follow the master's step? Hired crew never do
55
+ * (ruling 2026-08-25, above); a shell whose plane can't keep up does
56
+ * not; one that is down does not; and a drone HOLDING POSITION -- the
57
+ * frame a rigger jumped out of and left where it was (rigger pass
58
+ * 2026-09-06) -- waits there until "recall" brings it to heel. Pulled
59
+ * out of the loop so the rule is one function a test can ask.
60
+ */
61
+ static heels(c, plane) {
62
+ if (c.kind === 'ally')
63
+ return false;
64
+ if (c.holding)
65
+ return false;
66
+ if (c.npc.isIncapacitated())
67
+ return false;
68
+ return GoCommand.HEELS_ON[c.kind](plane);
69
+ }
53
70
  static verb = 'go';
54
71
  static description = 'Move in the specified direction ("go back" retraces your last move). Fleeing someone you just fought hands them one free parting shot. Jacked in, the grid sees no distance: "go <any room>" jumps your persona straight there ("map" lights the icons). Inside a host there is no distance to cross at all -- "exit" first.';
55
72
  async execute(args) {
@@ -470,10 +487,7 @@ export class GoCommand extends Command {
470
487
  // with their own feet: they hold whatever ground they're on
471
488
  // until they decide to move or you "order" them to, which is
472
489
  // also what makes sending one ahead to scout mean anything.
473
- if (c.kind === 'ally')
474
- continue;
475
- const canFollow = GoCommand.HEELS_ON[c.kind](this.actor.plane);
476
- if (!canFollow || c.npc.currentLocation === dest || c.npc.isIncapacitated())
490
+ if (!GoCommand.heels(c, this.actor.plane) || c.npc.currentLocation === dest)
477
491
  continue;
478
492
  c.npc.currentLocation = dest;
479
493
  c.npc.atSpot = this.actor.atSpot; // at their master's heel = at their spot
@@ -16,7 +16,7 @@ import { droneWreckFragment } from '../utilities/drone-prose.js';
16
16
  */
17
17
  export class JumpCommand extends Command {
18
18
  static verb = 'jump';
19
- static description = 'Jump INTO a drone (control rig required): fly it, see through it, fight with its mount. A deployed frame is seized wherever it flies. "jump hot" for hot-sim; "jump" again returns to your body and leaves the frame out on its dog-brain where you left it ("recall" brings it home).';
19
+ static description = 'Jump INTO a drone (control rig required): fly it, see through it, fight with its mount. A deployed frame is seized wherever it flies. "jump hot" for hot-sim; "jump" again returns to your body and leaves the frame out on its dog-brain where you left it ("recall" brings it to your side, "stow" packs it away).';
20
20
  async execute(args = []) {
21
21
  const actor = this.actor;
22
22
  // Already riding: this is the jump OUT.
@@ -38,10 +38,23 @@ export class JumpCommand extends Command {
38
38
  // (utilities/owned-drones.ts): a frame parked at home jumps from
39
39
  // home and stays parked while you ride it.
40
40
  const owned = ownedDrones(actor, this.game?.homeRoom, { reach: 'here' });
41
- const drones = owned.map(d => d.item);
41
+ // ...AND EVERY DEPLOYED FRAME, WHEREVER IT FLIES (transcript
42
+ // 2026-09-06: a Direktionssekretar deployed from the garage could
43
+ // not be jumped into once the rigger left the hideout -- "Nothing
44
+ // to jump into -- no drone in your pack" -- while this verb's own
45
+ // description promised seizure "wherever it flies"). Jumping in
46
+ // rides the link, not your hands: the hands rule above is for
47
+ // frames still stowed; a frame already out is in reach by definition.
48
+ const deployed = (this.game?.companions ?? [])
49
+ .filter(c => c.kind === 'drone' && c.boundDevice && !owned.some(d => d.item === c.boundDevice));
50
+ const drones = [...owned.map(d => d.item), ...deployed.map(c => c.boundDevice)];
51
+ const listing = [
52
+ ...droneListLines(owned),
53
+ ...deployed.map(c => ` • ${c.boundDevice.name.padEnd(26)} deployed -- ${c.npc.currentLocation.name}${c.boundDevice.maxDroneBoxes > 0 ? ` hull ${c.boundDevice.droneSummary()}` : ''}`),
54
+ ];
42
55
  const atHome = this.game?.homeRoom !== undefined && actor.currentLocation === this.game.homeRoom;
43
56
  if (drones.length === 0) {
44
- return `Nothing to jump into -- no drone in your pack${atHome ? ' or your garage' : ''}.${hint(` (Ratchet's Circuit Bazaar stocks airframes.)`)}`;
57
+ return `Nothing to jump into -- no drone in your pack${atHome ? ' or your garage' : ''}, none deployed.${hint(` (Ratchet's Circuit Bazaar stocks airframes.)`)}`;
45
58
  }
46
59
  // "jump hot" / "jump <drone> hot": strip the mode word, pick the frame.
47
60
  const words = args.map(w => w.toLowerCase());
@@ -52,11 +65,11 @@ export class JumpCommand extends Command {
52
65
  const picked = fuzzyPickName(nameWords.join(' '), drones.map(d => d.name));
53
66
  drone = picked ? drones.find(d => d.name === picked) : undefined;
54
67
  if (!drone) {
55
- return [`No drone by that name in reach. Yours:`, ...droneListLines(owned)].join('\n');
68
+ return [`No drone by that name in reach. Yours:`, ...listing].join('\n');
56
69
  }
57
70
  }
58
71
  if (!drone) {
59
- return [`Which airframe? ("jump <name>", add "hot" for hot-sim)`, ...droneListLines(owned)].join('\n');
72
+ return [`Which airframe? ("jump <name>", add "hot" for hot-sim)`, ...listing].join('\n');
60
73
  }
61
74
  if (drone.isWrecked) {
62
75
  return `The ${drone.name} is a wreck -- ${droneWreckFragment(drone)} -- and a wreck jumps nowhere. It repairs while you rest somewhere safe.`;
@@ -7,7 +7,7 @@ import { Direction } from '../types/shared/direction-enum.js';
7
7
  import { Door } from '../models/door.js';
8
8
  import { fuzzyPickName } from '../utilities/fuzzy-match.js';
9
9
  import { heldBy, holding, physicalLimit } from '../utilities/grapple.js';
10
- import { spotsActive, resolveSpot, spotOf, describeSpotRoster, OPEN_FLOOR, pathBetweenSpots, spotDistanceMeters, spotClimbSteps, blockingRegion, terrainRefusal, reseatOpenFloor, routeForMove, stepFrom, spotForCell, actorCell, standingRoomNear, furnitureCells, approachCellFor, exitSpot, concealedFrom, revealActor, onTheFloor } from '../utilities/spots.js';
10
+ import { spotsActive, resolveSpot, spotOf, describeSpotRoster, OPEN_FLOOR, pathBetweenSpots, spotDistanceMeters, spotClimbSteps, blockingRegion, terrainRefusal, reseatOpenFloor, routeForMove, stepFrom, spotForCell, actorCell, standingRoomNear, yieldSquareToMaster, furnitureCells, approachCellFor, exitSpot, concealedFrom, revealActor, onTheFloor } from '../utilities/spots.js';
11
11
  // fallDamageDVMeters, not fallDamageDV: a climb that fell short drops
12
12
  // you from where you GOT to, which is a distance in metres, not a whole
13
13
  // storey. The storey-based wrapper still exists for callers that really
@@ -759,6 +759,8 @@ export class MoveCommand extends Command {
759
759
  const wasAt = actor.atSpot;
760
760
  actor.atCell = cell;
761
761
  actor.atSpot = spotForCell(room, cell);
762
+ // A shell of yours on that square steps aside (spots.ts heelsTo).
763
+ yieldSquareToMaster(room, actor);
762
764
  actor.performAction('steps', `${taken} ${taken === 1 ? 'pace' : 'paces'} ${direction}`);
763
765
  this.scene.updateStatus();
764
766
  this.scene.updateExits(room);
@@ -2,7 +2,7 @@ import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { fuzzyPickName, significantWords, normalizeLoose, wordsAreCloseEnough } from '../utilities/fuzzy-match.js';
4
4
  import { AI } from '../../../../tools/ai/ai.class.js';
5
- import { DRONE_COLOUR, DRONE_ICON, droneRecallLine } from '../utilities/drone-prose.js';
5
+ import { DRONE_COLOUR, DRONE_ICON, droneRecallLine, droneStowLine, droneHandHeld } from '../utilities/drone-prose.js';
6
6
  /**
7
7
  * What "you have nobody" says, one phrase per COMPANION KIND.
8
8
  *
@@ -107,8 +107,20 @@ export class OrderCommand extends Command {
107
107
  // body." (a real session's agent couldn't be brought home by any
108
108
  // natural phrasing: "order agent return", "return to deck"...).
109
109
  // ("return fire" is exempt -- that's a shooting order, not a fold.)
110
+ // A DRONE ORDERED TO COME comes -- and stays deployed, back on heel
111
+ // (player ruling 2026-09-06: recall is "return to me"). "fold" /
112
+ // "stow" is the pack, handled with the other kinds below.
113
+ if (entry.kind === 'drone'
114
+ && /^(return(?!\s+fire)|recall|come(\s+(back|home|here|to\s+me))?|follow(\s+me)?|heel|stand\s+down|go\s+home)\b/i.test(cmd)) {
115
+ const { moved } = game.heelCompanion(entry);
116
+ this.scene.updateStatus();
117
+ const line = !moved
118
+ ? `${entry.npc.name} is already at your heel.`
119
+ : entry.boundDevice ? droneRecallLine(entry.boundDevice) : `${entry.npc.name} comes to your side.`;
120
+ return `{${feed.color}-fg}${feed.icon} ${line}{/${feed.color}-fg}`;
121
+ }
110
122
  if (entry.kind !== 'ally'
111
- && /^(return(?!\s+fire)|recall|come\s+(back|home)|fold|dismiss|stand\s+down|go\s+home)\b/i.test(cmd)) {
123
+ && /^(return(?!\s+fire)|recall|come\s+(back|home)|fold|dismiss|stand\s+down|go\s+home|stow|pack)\b/i.test(cmd)) {
112
124
  // A SPRITE ORDERED HOME IS DISMISSED, NOT DESTROYED, and it is not
113
125
  // an agent. This branch had no sprite case at all: the ternary
114
126
  // fell through to the AGENT wording, so a technomancer who told
@@ -135,13 +147,21 @@ export class OrderCommand extends Command {
135
147
  : `${feed.icon} ${name} unravels into clean static -- an unregistered thread has no Resonance to wait in.`;
136
148
  return `{${feed.color}-fg}${line}{/${feed.color}-fg}`;
137
149
  }
150
+ // A frame you cannot carry has no pack to fold into; one across
151
+ // the district has to be recalled first -- stowing is hands-on.
152
+ if (entry.kind === 'drone' && entry.boundDevice && !droneHandHeld(entry.boundDevice)) {
153
+ return `{${feed.color}-fg}${feed.icon} The ${entry.boundDevice.name} is not a pocket job -- it stays deployed. "recall" brings it to your side.{/${feed.color}-fg}`;
154
+ }
155
+ if (entry.kind === 'drone' && entry.npc.currentLocation !== this.actor.currentLocation) {
156
+ return `{${feed.color}-fg}${feed.icon} The ${entry.boundDevice?.name ?? entry.npc.name} isn't here to stow -- "recall" it first.{/${feed.color}-fg}`;
157
+ }
138
158
  const foldLine = entry.kind === 'spirit'
139
159
  ? `${feed.icon} You speak the release. The spirit inclines its head -- the bargain closes${(entry.services ?? 0) > 0 ? `, ${entry.services} unspent service${entry.services === 1 ? '' : 's'} forfeit` : ''} -- and it thins back to its own plane.`
140
160
  : entry.kind === 'drone'
141
- // The same words "recall" speaks (drone-prose.ts), classed by
161
+ // The same words "stow" speaks (drone-prose.ts), classed by
142
162
  // how the frame moves -- this line used to be a verbatim copy
143
163
  // of companions.ts's, and both said rotors to a Steel Lynx.
144
- ? `${feed.icon} ${entry.boundDevice ? droneRecallLine(entry.boundDevice) : `The ${entry.npc.name} comes back to hand.`}`
164
+ ? `${feed.icon} ${entry.boundDevice ? droneStowLine(entry.boundDevice) : `The ${entry.npc.name} folds into your pack.`}`
145
165
  : this.actor.plane === 'matrix'
146
166
  ? `${feed.icon} Your agent's icon collapses back into your persona -- folded into the deck.`
147
167
  : `${feed.icon} The deck spins its agent down -- the DECK IN AR light dies as the icon folds home.`;
@@ -336,5 +336,22 @@
336
336
  // under its own power; a locked door stops every frame but a palm-flier
337
337
  // on the recon sweep. Two tables on different versions disagree about
338
338
  // where a frame is after a jump-out.
339
- export const ENGINE_VERSION = '1.36.0';
339
+ // 1.37.0 (2026-09-06): RECALL IS "RETURN TO ME"; STOW PUTS IT AWAY. COMMAND
340
+ // SEMANTICS in shared scenes: "recall <drone>" now brings a deployed frame
341
+ // to your side and back on heel, still deployed (it used to fold the frame
342
+ // into your hand -- playtest: "it disappeared, which doesn't make sense");
343
+ // "stow <drone>" is the fold (hand-held frames only; the holster meaning
344
+ // of "stow" is untouched), and "order <drone> come/return/follow" heels
345
+ // while "order <drone> fold/stow" packs. A frame you jump out of HOLDS
346
+ // its position (Game.companions.holding) instead of teleporting to your
347
+ // heel on your next step; recall clears the hold. HUD shows "holding at".
348
+ // 1.38.0 (2026-09-06): YOUR OWN SHELL IS NEVER IN YOUR WAY. COMMAND SEMANTICS
349
+ // in shared scenes: a companion shell (drone, spirit, sprite, agent) at its
350
+ // master's heel no longer blocks the master's "move <direction>" -- it
351
+ // yields the square (spots.ts heelsTo / yieldSquareToMaster); hired crew
352
+ // still hold theirs. "jump" reaches every DEPLOYED frame wherever it flies,
353
+ // garage-born or not (it used to answer "no drone in your pack" away from
354
+ // the hideout). An NPC with no dialogue and no AI no longer greets you
355
+ // with "<name>: undefined".
356
+ export const ENGINE_VERSION = '1.38.0';
340
357
  //# sourceMappingURL=engine-version.js.map
@@ -132,7 +132,7 @@ import { SustainCommand } from './commands/sustain.js';
132
132
  import { AbandonCommand } from './commands/abandon.js';
133
133
  import { UnravelCommand } from './commands/unravel.js';
134
134
  import { OrderCommand } from './commands/order.js';
135
- import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
135
+ import { DeployCommand, AgentCommand, RecallCommand, StowCommand } from './commands/companions.js';
136
136
  import { DeckCommand, LoadProgramCommand, UnloadProgramCommand } from './commands/deck.js';
137
137
  import { getProgram } from './utilities/programs.js';
138
138
  import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
@@ -183,6 +183,7 @@ import { AI } from '../../../tools/ai/ai.class.js';
183
183
  import { BULLET } from './utilities/log-style.js';
184
184
  import { spotOf, resolveSpot } from './utilities/spots.js';
185
185
  import { DRONE_COLOUR, DRONE_ICON, droneFoldForTravelLine, droneUnfoldAtCurbLine } from './utilities/drone-prose.js';
186
+ import { bringToHeel } from './utilities/companion-heel.js';
186
187
  export default class Game {
187
188
  initialized;
188
189
  static instance;
@@ -1445,6 +1446,17 @@ export default class Game {
1445
1446
  shell.maxCarryingWeight = Game.DRONE_CARGO_KG[item.size] ?? 2;
1446
1447
  return shell;
1447
1448
  }
1449
+ /**
1450
+ * BRING A COMPANION TO HEEL (player ruling 2026-09-06: "recall" is
1451
+ * "return to me" -- the frame comes to your side and follows again;
1452
+ * putting it away is "stow"). The shell crosses to the master's room
1453
+ * and spot and any HOLDING flag clears, so the next "go" drags it
1454
+ * along like any companion. Returns whether it had anywhere to come
1455
+ * from.
1456
+ */
1457
+ heelCompanion(entry) {
1458
+ return bringToHeel(entry, this.player);
1459
+ }
1448
1460
  /** Drops a companion's shell and its roster entry. */
1449
1461
  dismissCompanion(entry, reason) {
1450
1462
  if (this.scene.getActor(entry.npc.name) === entry.npc) {
@@ -2508,7 +2520,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2508
2520
  CommandFactory.registerCommand('brandish', BrandishCommand);
2509
2521
  CommandFactory.registerCommand('draw', BrandishCommand);
2510
2522
  CommandFactory.registerCommand('holster', HolsterCommand);
2511
- CommandFactory.registerCommand('stow', HolsterCommand);
2523
+ // "stow" is the holster's word AND the drone fold's (2026-09-06):
2524
+ // StowCommand takes a deployed frame the words name and hands
2525
+ // everything else to HolsterCommand unchanged.
2526
+ CommandFactory.registerCommand('stow', StowCommand);
2512
2527
  // "end" and "hangup" both bounced in real sessions when players
2513
2528
  // wanted to end a call -- the intent is unambiguous mid-call.
2514
2529
  CommandFactory.registerCommand('end', EndCallCommand);
@@ -5420,7 +5435,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5420
5435
  // PILOT rating, agents run at a RATING, and a spirit's Force is
5421
5436
  // already in its name.
5422
5437
  const grade = c.kind === 'drone' ? `Pilot ${c.force} -- ` : c.kind === 'agent' ? `Rating ${c.force} -- ` : c.kind === 'sprite' ? `Level ${c.force} -- ` : '';
5423
- lines.push(`{green-fg}👥 ${c.npc.name} -- ${grade}${track}{/green-fg}`);
5438
+ // A frame left on its dog-brain says where it waits.
5439
+ const hold = c.holding ? ` -- holding at ${c.npc.currentLocation.name}` : '';
5440
+ lines.push(`{green-fg}👥 ${c.npc.name} -- ${grade}${track}${hold}{/green-fg}`);
5424
5441
  }
5425
5442
  // DECK-AR INDICATOR (player request): "Link online" is the
5426
5443
  // COMMLINK's AR overlay; this is the DECK's -- lit when an agent
@@ -5970,13 +5987,13 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5970
5987
  */
5971
5988
  static HELP_CATEGORIES = [
5972
5989
  { 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']] },
5973
- { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
5990
+ { 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']] },
5974
5991
  { title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
5975
5992
  // The party layer: everyone who walks (or flies, or manifests) at
5976
5993
  // your side answers to these.
5977
5994
  // "players" folded back under crew after playtesting ("crew
5978
5995
  // players" / "crew street"); the bare verb still answers quietly.
5979
- { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
5996
+ { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
5980
5997
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense']] },
5981
5998
  { 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']] },
5982
5999
  // The Emerged get their own shelf (player request: "there MUST be
@@ -1546,7 +1546,13 @@ ${worldEventsSummary}
1546
1546
  this._triedThisStimulus = [];
1547
1547
  if (!AI.isConfigured()) {
1548
1548
  this.logger.write(`No AI available`);
1549
- await actor.hear(this, this.speak());
1549
+ // A shell with no dialogue -- every companion is built with
1550
+ // `dialog: []` -- has nothing to say, and used to say so as
1551
+ // "<name>: undefined" (transcript 2026-09-06: a drone waking as
1552
+ // its rigger walked in). Silence is the right line.
1553
+ const line = this.speak();
1554
+ if (line)
1555
+ await actor.hear(this, line);
1550
1556
  return;
1551
1557
  }
1552
1558
  // ---------- helpers ----------
@@ -2679,6 +2679,10 @@ export class Player extends AbstractPlayer {
2679
2679
  const overLink = opts?.viaLink || this.activeCallPartner === actor;
2680
2680
  if (!overLink && !this.canPerceive(actor))
2681
2681
  return;
2682
+ // Nothing said is nothing heard: a caller passing an empty line
2683
+ // (an NPC with no dialogue, AI off) must not print "<name>: undefined".
2684
+ if (!message)
2685
+ return;
2682
2686
  this.logger.write(`${this.name} heard ${actor.name}`);
2683
2687
  // A voice arriving over an active CALL from someone who isn't in the
2684
2688
  // room wears the comm dressing (player note: Mr. Johnson -- who lives
@@ -0,0 +1,22 @@
1
+ /**
2
+ * BRING A COMPANION TO HEEL (player ruling 2026-09-06: "recall" is
3
+ * "return to me" -- the frame comes to your side and follows again;
4
+ * putting it away is "stow"). The shell crosses to the master's room
5
+ * and spot, its cell is released so seating finds it a place beside
6
+ * them, and any HOLDING flag clears so the next "go" drags it along
7
+ * like any companion. Returns whether it had anywhere to come from.
8
+ *
9
+ * One function, used by Game.heelCompanion and by the tests that pin
10
+ * recall, so a mock Game and the real one cannot disagree about it.
11
+ */
12
+ export function bringToHeel(entry, master) {
13
+ const here = master.currentLocation;
14
+ const moved = entry.npc.currentLocation !== here || entry.npc.atSpot !== master.atSpot || !!entry.holding;
15
+ if (entry.npc.currentLocation !== here)
16
+ entry.npc.currentLocation = here;
17
+ entry.npc.atSpot = master.atSpot;
18
+ entry.npc.atCell = undefined;
19
+ entry.holding = false;
20
+ return { moved };
21
+ }
22
+ //# sourceMappingURL=companion-heel.js.map
@@ -113,21 +113,42 @@ export function droneLaunchLine(item, from) {
113
113
  missile: `The ${name} kicks off its rail and holds a tight circle overhead`,
114
114
  });
115
115
  }
116
- /** Recall / "order <drone> home": the frame comes back to you. */
116
+ /**
117
+ * Recall / "order <drone> come": the frame comes to your side and is
118
+ * back at heel, still deployed (player ruling 2026-09-06: "recall is
119
+ * just return to me"; putting it away is "stow" -- droneStowLine).
120
+ */
117
121
  export function droneRecallLine(item) {
118
122
  const name = item.name;
119
123
  const back = pick(item, {
120
- 'flier-palm': `The ${name} loops back and settles onto your palm, rotors stilling.`,
121
- flier: `The ${name} banks once and settles back onto your hand, rotors folding.`,
122
- ground: `The ${name} rolls back to your side and powers down.`,
123
- walker: `The ${name} picks its way back and folds its legs into your hand.`,
124
- anthro: `The ${name} walks back and powers down at your side.`,
125
- missile: `The ${name} comes around and settles onto its rail, turbine spooling down.`,
124
+ 'flier-palm': `The ${name} loops back and hangs at your shoulder, rotors whining.`,
125
+ flier: `The ${name} banks once and settles into a hover at your shoulder.`,
126
+ ground: `The ${name} rolls back to your side and idles, motors ticking.`,
127
+ walker: `The ${name} picks its way back and settles at your feet.`,
128
+ anthro: `The ${name} walks back and falls in at your side.`,
129
+ missile: `The ${name} comes around and picks up station overhead.`,
130
+ });
131
+ const dents = item.droneDamage > 0
132
+ ? ` The frame carries its dents (${item.droneSummary()})${hint(' -- rest repairs it')}.`
133
+ : '';
134
+ return `${back} It follows where you walk again.${dents}`;
135
+ }
136
+ /** Stow: the frame folds back into your pack -- hand-held frames only
137
+ * (droneHandHeld); a frame you cannot carry stays deployed. */
138
+ export function droneStowLine(item) {
139
+ const name = item.name;
140
+ const away = pick(item, {
141
+ 'flier-palm': `The ${name} settles onto your palm, rotors stilling, and goes back in your pocket.`,
142
+ flier: `The ${name} settles back onto your hand, rotors folding, and goes into the pack.`,
143
+ ground: `The ${name} powers down and you sling it.`,
144
+ walker: `The ${name} folds its legs into your hand and goes into the pack.`,
145
+ anthro: `The ${name} climbs into the pack and goes still.`,
146
+ missile: `The ${name} settles onto its rail, turbine spooling down.`,
126
147
  });
127
148
  const dents = item.droneDamage > 0
128
149
  ? ` The frame carries its dents (${item.droneSummary()})${hint(' -- rest repairs it')}.`
129
150
  : '';
130
- return `${back}${dents}`;
151
+ return `${away}${dents}`;
131
152
  }
132
153
  /** Pre-travel: how the frame gets to the cab, or does not need one. */
133
154
  export function droneFoldForTravelLine(item) {
@@ -212,7 +233,7 @@ export function droneJumpOutLine(item, frameRoom, bodyRoom) {
212
233
  missile: 'holds its circle overhead',
213
234
  });
214
235
  const where = frameRoom === bodyRoom ? 'where you left it' : `in ${frameRoom.name}`;
215
- return `You're back in your body in ${bodyRoom.name}; the ${item.name} ${holds} ${where} -- dog-brain on the stick.${hint(' ("recall" brings it home.)')}`;
236
+ return `You're back in your body in ${bodyRoom.name}; the ${item.name} ${holds} ${where} -- dog-brain on the stick, holding until you "recall" it.${hint(' ("recall" brings it to your side; "stow" packs it away.)')}`;
216
237
  }
217
238
  /** The recon sweep ("drone"): nowhere to go, launch, and return. */
218
239
  export function droneReconNowhereLine(item) {
@@ -298,7 +298,7 @@ export function enterDrone(scene, actor, drone, mode) {
298
298
  : ` Cold-sim: hits on the hull sting through the link as stun, half strength.`;
299
299
  return [
300
300
  `The rig takes hold and the world snaps to sensor-sight -- you ARE the ${drone.name} now, ${droneJumpInFragment(drone)}, your body slumped and empty in ${actor.bodyRoom.name}.${hotWarning}${hullNote}`,
301
- hint(`Move with "go" (you fly the meat world; locked doors still stop an airframe), "look" through the sensors, "jump" again to drop back into your body -- the frame stays out on its dog-brain where you leave it; "recall" brings it home.`),
301
+ hint(`Move with "go" (you fly the meat world; locked doors still stop an airframe), "look" through the sensors, "jump" again to drop back into your body -- the frame stays out on its dog-brain where you leave it; "recall" brings it to your side, "stow" packs it away.`),
302
302
  ].filter(l => l.length > 0);
303
303
  }
304
304
  /**
@@ -361,6 +361,12 @@ export function leaveDrone(scene, actor, opts) {
361
361
  if (shell) {
362
362
  shell.atSpot = frameSpot;
363
363
  shell.atCell = frameCell;
364
+ // It HOLDS there (Game.companions.holding): a frame you left in
365
+ // the next room must not teleport to your heel on your next step
366
+ // -- "recall" fetches it.
367
+ const entry = game.companions.find(c => c.npc === shell);
368
+ if (entry)
369
+ entry.holding = true;
364
370
  Logger.getInstance().write(`${drone.name} re-shelled on its dog-brain in ${frameRoom.name} after ${actor.name} jumped out.`);
365
371
  }
366
372
  lines.push(shell && drone
@@ -858,6 +858,15 @@ export function standingOccupants(room, mover) {
858
858
  const offPlane = new Set(room.getActors()
859
859
  .filter(a => !onTheFloor(a) && a.bodyRoom !== room)
860
860
  .map(a => a.name));
861
+ // YOUR OWN SHELL IS NEVER IN YOUR WAY (transcript 2026-09-06: a drone
862
+ // at heel sat on its master's next square -- "You can't step east --
863
+ // S-K Direktionssekretar (autopilot)'s in the way", a dozen times --
864
+ // and read as the frame refusing to follow). A companion shell heels
865
+ // AT its master's spot by construction (go.ts), so of course it is
866
+ // standing where the master wants to go next; it yields instead
867
+ // (yieldSquareToMaster, called by the step). Hired crew are people
868
+ // (ruling 2026-08-25) and still hold their square.
869
+ const heels = new Set(mover ? room.getActors().filter(a => heelsTo(a, mover)).map(a => a.name) : []);
861
870
  for (const [name, c] of seatingIn(room).entries()) {
862
871
  if (mover && name === mover.name)
863
872
  continue;
@@ -865,10 +874,40 @@ export function standingOccupants(room, mover) {
865
874
  continue;
866
875
  if (offPlane.has(name))
867
876
  continue;
877
+ if (heels.has(name))
878
+ continue;
868
879
  out.set(key(c), name);
869
880
  }
870
881
  return out;
871
882
  }
883
+ /** Is `a` a companion SHELL of `master` -- a drone, spirit, sprite or
884
+ * agent riding their bond? Hired crew (kind 'ally') are people and
885
+ * answer no. */
886
+ export function heelsTo(a, master) {
887
+ const shell = a;
888
+ return shell.allyOf === master.name && shell.companionKind !== undefined && shell.companionKind !== 'ally';
889
+ }
890
+ /**
891
+ * After `master` steps onto a square one of their own shells was
892
+ * holding, the shell gives it up: its cell is released so seatingIn
893
+ * finds it the nearest free square at the master's spot. Returns the
894
+ * shells that moved. */
895
+ export function yieldSquareToMaster(room, master) {
896
+ const here = master.atCell ? key(master.atCell) : undefined;
897
+ if (!here)
898
+ return [];
899
+ const moved = [];
900
+ for (const a of room.getActors()) {
901
+ if (!heelsTo(a, master))
902
+ continue;
903
+ if (!a.atCell || key(a.atCell) !== here)
904
+ continue;
905
+ a.atCell = undefined;
906
+ a.atSpot = master.atSpot;
907
+ moved.push(a);
908
+ }
909
+ return moved;
910
+ }
872
911
  /** The cell `mover` would end up in on arriving at `spotName` -- their
873
912
  * seat around that fixture, with everyone else's seats already taken.
874
913
  * This, not the fixture's anchor, is where movement actually paths to:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.131.0",
3
+ "version": "5.133.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.",