@maka/maka-cli 5.381.1 → 5.383.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.381.1",
3
+ "version": "5.383.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,7 +1,7 @@
1
1
  // src/commands/command-registry.ts
2
2
  import { CommandFactory } from '../factories/command-factory.js';
3
3
  import { Logger } from '../utilities/logger.js';
4
- import { authorizeNpcAction } from '../utilities/npc-authorization.js';
4
+ import { authorizeNpcAction, clientHoldsTheMeet } from '../utilities/npc-authorization.js';
5
5
  export class CommandRegistry {
6
6
  scene;
7
7
  constructor(scene) {
@@ -74,6 +74,14 @@ export class CommandRegistry {
74
74
  return { args, refusal: verdict.reason ?? `That is not yours to do.` };
75
75
  }
76
76
  }
77
+ // The meet's Johnson keeps his seat (npc-authorization.ts).
78
+ if (declaring.isMovement() && !this.scene.isHumanControlled(actor)) {
79
+ const held = clientHoldsTheMeet(this.scene, actor);
80
+ if (held) {
81
+ Logger.getInstance().write(`Refused "${commandString.trim()}" from ${actor.name}: the client keeps his seat.`);
82
+ return { args, refusal: held };
83
+ }
84
+ }
77
85
  return { command, args };
78
86
  }
79
87
  }
@@ -46,6 +46,16 @@ export class Command {
46
46
  // whether or not it also remembered to say so here.
47
47
  return this.humanOnly ? 'human-only' : this.npcPolicy;
48
48
  }
49
+ /**
50
+ * THIS VERB MOVES THE ACTOR'S OWN BODY -- across the room (move, run,
51
+ * sprint, fly), out of it (go) or after someone (follow). Read by
52
+ * CommandRegistry.parse for AI-driven actors only: a Johnson holding a
53
+ * meet keeps his seat (npc-authorization.ts clientHoldsTheMeet).
54
+ */
55
+ static movesActor = false;
56
+ static isMovement() {
57
+ return this.movesActor;
58
+ }
49
59
  static verb;
50
60
  static description; // Make this static
51
61
  actor;
@@ -124,6 +124,7 @@ export function dragFollowers(scene, mover, origin, logger) {
124
124
  }
125
125
  }
126
126
  export class FollowCommand extends Command {
127
+ static movesActor = true;
127
128
  static verb = 'follow';
128
129
  static description = 'Tail another runner: "follow <name>" and their room changes take you with them. Bare "follow" (or "unfollow") peels off. It breaks on its own if you get caught in a fight, fall behind, or they leave the run. Same room, same plane, real players only -- NPC companions already heel by nature ("order" sends them).';
129
130
  /** The scene's other humans -- the only legal targets. */
@@ -96,6 +96,7 @@ export class GoCommand extends Command {
96
96
  return false;
97
97
  return GoCommand.HEELS_ON[c.kind](plane);
98
98
  }
99
+ static movesActor = true;
99
100
  static verb = 'go';
100
101
  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.';
101
102
  /** See MoveCommand.execute: going through a door is a walk too, and
@@ -1,7 +1,7 @@
1
1
  import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { capitalCase } from 'change-case';
4
- import { planeTintItemName, objectiveMark } from '../utilities/planes.js';
4
+ import { planeTintItemName, jobMark } from '../utilities/planes.js';
5
5
  import { dataDevices } from '../utilities/data-library.js';
6
6
  /**
7
7
  * On-demand inventory panel, replacing the always-on blessed sidebar box
@@ -76,7 +76,7 @@ export class InventoryCommand extends Command {
76
76
  const tinted = item.plane === 'meat'
77
77
  ? rarityTint(item.rating, planeTintItemName(item, 'meat'))
78
78
  : planeTintItemName(item, 'meat');
79
- let line = ` ${CARRIED_MARK}${objectiveMark(this.scene, item.name)}${tinted}`;
79
+ let line = ` ${CARRIED_MARK}${jobMark(this.scene, item)}${tinted}`;
80
80
  // x N for a stack, the same shape the currency tag below uses.
81
81
  if (item.quantity > 1)
82
82
  line += ` �${item.quantity}`;
@@ -130,7 +130,7 @@ export class InventoryCommand extends Command {
130
130
  if (libraries.length > 0) {
131
131
  lines.push('', `{light-blue-fg}On your devices:{/light-blue-fg}`);
132
132
  for (const device of libraries) {
133
- lines.push(` ${device.name}: ${device.files.map(f => `${objectiveMark(this.scene, f.name)}${planeTintItemName(f, 'meat')}`).join(', ')}`);
133
+ lines.push(` ${device.name}: ${device.files.map(f => `${jobMark(this.scene, f)}${planeTintItemName(f, 'meat')}`).join(', ')}`);
134
134
  }
135
135
  const legend = hint(`("transfer <file> to <device>" moves one; "transfer <file> to chip" writes it to physical media.)`);
136
136
  if (legend)
@@ -45,6 +45,7 @@ import { onTheWall, midClimbRefusal } from '../utilities/climb-state.js';
45
45
  * verb that leaves a room.
46
46
  */
47
47
  export class MoveCommand extends Command {
48
+ static movesActor = true;
48
49
  static verb = 'walk';
49
50
  static description = 'Cross the room to a named spot or person: "move to bar", "move to Brick" (also "walk"/"approach"; "run to"/"sprint to" are the fast crossings). "move south 2" paces you across the room by cells instead -- use "go" to leave the room entirely. Bare "move" lists the room\'s spots. You must share a spot with someone to talk or trade with them.';
50
51
  /** Which movement mode this registration answers to (see subclasses). */
@@ -1,6 +1,6 @@
1
1
  import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
- import { planeTintItemName, emphasizeFind, objectiveMark, fileReach } from '../utilities/planes.js';
3
+ import { planeTintItemName, emphasizeFind, jobMark, fileReach } from '../utilities/planes.js';
4
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
5
5
  import { perceptionTest, matrixSearchTest, matrixSearchSeconds, MATRIX_SEARCH } from '../utilities/perception.js';
6
6
  import { gridVicinity } from '../utilities/grid-reach.js';
@@ -194,7 +194,7 @@ export class SearchCommand extends Command {
194
194
  if (files.length === 0) {
195
195
  return `The archive is indexed and EMPTY -- ${hostLabel(where)} keeps nothing worth the trip. ${clock}`;
196
196
  }
197
- const list = files.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
197
+ const list = files.map(i => `${jobMark(this.scene, i)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
198
198
  const seal = where.hostCracked
199
199
  ? hint(` ("take" them -- the seal is already broken.)`)
200
200
  : hint(` (sealed in the host's data vault: "hack" the node to break the seal, then "take".)`);
@@ -206,7 +206,7 @@ export class SearchCommand extends Command {
206
206
  const sealedElsewhere = files.length > 0 && where.hasNode;
207
207
  return `Nothing loose on ${where.name}'s grid. ${clock}${sealedElsewhere ? hint(` (${hostLabel(where)} keeps its files INSIDE -- "enter" it and search from in there.)`) : ''}`;
208
208
  }
209
- const list = loose.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
209
+ const list = loose.map(i => `${jobMark(this.scene, i)}${emphasizeFind(planeTintItemName(i, actor.plane))}`).join(', ');
210
210
  return `Loose on ${where.name}'s grid: ${list}. ${clock}${hint(` ("take" rides them out on your deck.)`)}`;
211
211
  }
212
212
  async execute(_args) {
@@ -303,7 +303,7 @@ export class SearchCommand extends Command {
303
303
  // Plain lowercase for the log/AI history; the SCREEN list is tinted
304
304
  // per plane (see planeTintItemName) so paydata reads as data.
305
305
  const itemsList = found.map(i => i.name.toLowerCase()).join(', ');
306
- const displayList = found.map(i => `${objectiveMark(this.scene, i.name)}${emphasizeFind(planeTintItemName(i, this.actor.plane))}`).join(', ');
306
+ const displayList = found.map(i => `${jobMark(this.scene, i)}${emphasizeFind(planeTintItemName(i, this.actor.plane))}`).join(', ');
307
307
  this.logger.write(`SearchCommand: ${this.actor.name} found the following items: ${itemsList}`);
308
308
  this.actor.performAction('search', itemsList);
309
309
  description += `You search the area and find: ${displayList}`;
@@ -153,7 +153,8 @@ export class TalkCommand extends Command {
153
153
  if (insideWithMe.includes(target)) {
154
154
  await speaker.hear(speaker, `(to ${target.name}) ${message}`);
155
155
  await target.hear(speaker, `(privately) ${message}`);
156
- return `You speak to ${target.name} inside the host: "${message}"`;
156
+ // The echo above IS the confirmation (see the meat branch below).
157
+ return '';
157
158
  }
158
159
  // "At" spots: conversation needs shared ground (see
159
160
  // utilities/spots.ts) -- the bartender doesn't lean across the room.
@@ -174,7 +175,12 @@ export class TalkCommand extends Command {
174
175
  // Face time with the run's CLIENT is THE MEET (or the wrap-up) --
175
176
  // see Game.onClientInteraction (terms / lump-sum settlement).
176
177
  const clientBeat = this.game?.onClientInteraction(target.name);
177
- return `You talk to ${target.name} privately: "${message}"${clientBeat ? `\n${clientBeat}` : ''}`;
178
+ // ONE LINE PER THING SAID (86LjwvK7xSbXhWXGi: "this repeated text can
179
+ // be removed: You talk to Mr. Vellum privately: ..."). The speaker's
180
+ // own hear() above already printed "Maka: (to Mr. Vellum) ...", so a
181
+ // reply quoting the same words back was the same sentence twice. The
182
+ // reply carries only what the echo cannot: the meet's beat.
183
+ return clientBeat ?? '';
178
184
  }
179
185
  }
180
186
  //# sourceMappingURL=talk.js.map
@@ -3308,5 +3308,31 @@
3308
3308
  // at boot -- and the reputation game over is awaited before the
3309
3309
  // command's reply, so the banner is that step's outcome on every
3310
3310
  // client and in a dry run's transcript.
3311
- export const ENGINE_VERSION = '1.193.1';
3311
+ // 1.194.0 (2026-10-04): THE DISTRICT FOLLOWS THE TIER (nqRsvbN6gvKHFKfW9;
3312
+ // ruling: "a runner should end up in a district that fits their
3313
+ // lifestyle"). A Middle runner who slid to Low on unpaid rent woke in
3314
+ // Everett in a stitched-in hole named after Auburn's walk-up.
3315
+ // - A tier whose home is authored in ANOTHER district no longer builds a
3316
+ // hole here (rebindHomeForTier).
3317
+ // - Game.settleAddress moves the runner to hubIdForTier's district as
3318
+ // home, binding that district's own building, after the ride home, at
3319
+ // hub boot (reaching a save that already slid) and after a shared
3320
+ // run's homecoming. A move that cannot land now is retried next time;
3321
+ // a session with no districtSeedLoader never moves.
3322
+ // 1.195.0 (2026-10-04): THE MEET, CLEANED UP (four live-table reports).
3323
+ // - The run's client holds his seat until the job is settled: his AI's
3324
+ // move/approach/run/sprint/fly/go/follow are refused with a reason
3325
+ // (npc-authorization.ts clientHoldsTheMeet; Command.movesActor), except
3326
+ // inside a fight (CWMrB2aNyL4hhrjyG).
3327
+ // - A body nudged onto an exact cell (shoulderAside, yieldDoorway) still
3328
+ // hears and is heard: earshot reads an exact cell as a place, so the
3329
+ // shoved Johnson is no longer deaf to `say` (xurFSiqNtRNbXA88A).
3330
+ // - talk-to/tell no longer quote the words back under the speaker's own
3331
+ // echo (86LjwvK7xSbXhWXGi).
3332
+ // - `inv`/`search` mark job leads with ☆ -- a page spelling a scene
3333
+ // device's code, or a device's own key -- beside the objective's ★
3334
+ // (nfrBW27eRRxstJ8uC).
3335
+ // - The HUD's holstered line drops its `-- "brandish"` tip
3336
+ // (MkxamPxmsSyonR3wC).
3337
+ export const ENGINE_VERSION = '1.195.0';
3312
3338
  //# sourceMappingURL=engine-version.js.map
@@ -42,7 +42,7 @@ import { pickedUpLine } from './utilities/takeover.js';
42
42
  import { presenceBeatAllowed, presenceProcessOnTheStreet } from './utilities/presence-gate.js';
43
43
  import { pushSave, burnCloudSaveFor, enqueueBoundPush, flushPushQueue, writeSpill, takeLastMemorial, authToken, clearSpill, hasPendingPush, getCloudSessionId, setLeasedHandler, } from './utilities/cloud-saves.js';
44
44
  import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
45
- import { isFence, knownDistricts, LIFESTYLE_TIERS, LIFESTYLE_OPTIONS, canonicalLifestyleTier, canonicalLifestyleOption, lifestyleOptionAllowed, lifestyleTierFor } from './utilities/commerce.js';
45
+ import { isFence, knownDistricts, LIFESTYLE_TIERS, LIFESTYLE_OPTIONS, canonicalLifestyleTier, canonicalLifestyleOption, lifestyleOptionAllowed, lifestyleTierFor, hubIdForTier } from './utilities/commerce.js';
46
46
  import { reconcileGarage, findDumpster, DUMPSTER_NAME } from './models/home-containers.js';
47
47
  import { ownedDrones } from './utilities/owned-drones.js';
48
48
  import { attachBrandMark, attachBrandMarkBottomRight } from './utilities/brand-mark.js';
@@ -3645,6 +3645,15 @@ export default class Game {
3645
3645
  // ordinary Street/Squatter case, and the only no-op worth naming.
3646
3646
  if (authored === undefined && current.residenceTier === undefined)
3647
3647
  return;
3648
+ // THE TIER'S ADDRESS IS IN ANOTHER DISTRICT (nqRsvbN6gvKHFKfW9: a Middle
3649
+ // runner slid to Low on unpaid rent and woke in Everett, in a stitched-in
3650
+ // hole named after Auburn's walk-up). An authored home with no authored
3651
+ // successor HERE means the runner is moving out, not down the hall:
3652
+ // settleAddress takes them to the district that fits, and that district's
3653
+ // own building is bound on arrival. Building a hole here would only leave
3654
+ // a counterfeit of somebody else's address standing on this street.
3655
+ if (authored === undefined)
3656
+ return;
3648
3657
  const next = authored ?? this.buildRuntimeHome();
3649
3658
  this.moveStash(current, next);
3650
3659
  // THE OLD HOLE IS CODE-OWNED, so it leaves with the lease -- and
@@ -5751,6 +5760,44 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5751
5760
  }
5752
5761
  return { ok: true, name: seed.name, firstVisit };
5753
5762
  }
5763
+ /**
5764
+ * LIVE WHERE THE RENT SAYS (user ruling 2026-10-04, nqRsvbN6gvKHFKfW9: "a
5765
+ * runner should end up in a district that fits their lifestyle").
5766
+ *
5767
+ * The lifestyle desk moves house itself. A tier that SLID on unpaid rent
5768
+ * (settleLifestyleUpkeep) cannot: every homecoming is synchronous and a
5769
+ * district move is not. So each async door a runner comes home through --
5770
+ * the ride home, the hub boot, a shared run's write-back -- calls this
5771
+ * afterwards, and the boot call also reaches a save that slid before this
5772
+ * shipped.
5773
+ *
5774
+ * Never refuses loudly. A move that cannot happen now (no network, a job
5775
+ * standing open) leaves the runner where they are, logged, and the next
5776
+ * homecoming or boot tries again. A visit is left alone: the runner is not
5777
+ * at home, and home is what this is about.
5778
+ */
5779
+ async settleAddress() {
5780
+ if (!this.hasHub || !this._hubScene || this.scene !== this._hubScene || this._crewSpace || this.isVisiting)
5781
+ return undefined;
5782
+ // NO LOADER, NOWHERE TO GO: a terminal and a site-hosted hub both say how
5783
+ // to fetch a district (districtSeedLoader). A harness that names none
5784
+ // booted exactly the one district it means to test, and must not be
5785
+ // carried off to a network fetch because its fixture's tier lives elsewhere.
5786
+ if (!this.districtSeedLoader)
5787
+ return undefined;
5788
+ const here = this.districtId;
5789
+ const want = hubIdForTier(this.lifestyleTier);
5790
+ if (here === undefined || want === undefined || here === want)
5791
+ return undefined;
5792
+ const leaving = this._hubSeed?.name ?? here;
5793
+ const moved = await this.moveToDistrict(want, { asHome: true });
5794
+ if (!moved.ok) {
5795
+ Logger.getInstance().write(`Address: ${this.lifestyleTier} lives in ${want}, runner still in ${here} -- move deferred (${moved.why}).`);
5796
+ return undefined;
5797
+ }
5798
+ this.requestSave('move');
5799
+ return `${leaving} is past what a ${this.lifestyleTier} lifestyle pays for. The landlord's drone has your things at the curb by morning, and a cab takes you to ${moved.name} -- ${this._homeRoom.name} is home now. The footlocker came with you; the neighbourhood did not.`;
5800
+ }
5754
5801
  /**
5755
5802
  * Wires a fresh, always-unlocked path between the persistent home base
5756
5803
  * and wherever the player is starting the current job from. Clears the
@@ -6195,6 +6242,11 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6195
6242
  const owed = this.applyHostedHomecoming();
6196
6243
  if (owed)
6197
6244
  Logger.getInstance().logWithColor(owed, 'green');
6245
+ // A tier that slid -- at that homecoming, or in a session that ended
6246
+ // before the move could land -- moves house before the runner wakes.
6247
+ const moved = await this.settleAddress();
6248
+ if (moved)
6249
+ Logger.getInstance().logWithColor(moved, 'green');
6198
6250
  this.requestSave('hub-arrival');
6199
6251
  }
6200
6252
  // A REGISTERED sprite from the save re-shells at the cot (the
@@ -6961,7 +7013,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6961
7013
  return `You're inside a loose end's host -- "exit" or "jack out" is what ends it.`;
6962
7014
  }
6963
7015
  if (this._hubScene && this.scene !== this._hubScene) {
6964
- return this.returnToHub();
7016
+ const home = this.returnToHub();
7017
+ // A tier that slid at the door moves house (settleAddress).
7018
+ const moved = await this.settleAddress();
7019
+ return moved ? `${home}\n${moved}` : home;
6965
7020
  }
6966
7021
  const gate = this.canDraftNextScene();
6967
7022
  if (!gate.allowed) {
@@ -7426,14 +7481,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
7426
7481
  const lapse = dropped.length > 0
7427
7482
  ? ` The garage lease goes with it -- the shutter comes down${sealedIn.length > 0 ? ` with ${sealedIn.join(', ')} still inside` : ''}.`
7428
7483
  : '';
7429
- // THE ADDRESS IS NOT GIVEN UP HERE, and that is deliberate. The tier
7430
- // slides -- you cannot afford this standard of living -- but nothing
7431
- // relocates the runner, because returnToHub is synchronous and a
7432
- // district move is async. They keep the street and owe the tab, and
7433
- // the street's own Neighborhood rating still governs what walks past
7434
- // (ISceneSeedJson.neighborhood). Moving out is something the player
7435
- // does, with "lifestyle".
7436
- rentLine = `The landlord's drone pings before the door even shuts: ${paid > 0 ? `${paid} paid, ` : ''}${this.rentDebt} nuyen still owed.${shedLine} Your ${tier.name} arrangement lapses -- you're living ${this.lifestyleTier} now.${lapse}${hint(` ("pay" the tab, "lifestyle" to move somewhere you can afford.)`)}`;
7484
+ // THE ADDRESS GOES WITH THE TIER (user ruling 2026-10-04, on
7485
+ // nqRsvbN6gvKHFKfW9: "a runner should end up in a district that fits
7486
+ // their lifestyle"). Not here, though: this is synchronous and a
7487
+ // district move is async, so every homecoming's async caller follows
7488
+ // up with settleAddress, which does the moving.
7489
+ rentLine = `The landlord's drone pings before the door even shuts: ${paid > 0 ? `${paid} paid, ` : ''}${this.rentDebt} nuyen still owed.${shedLine} Your ${tier.name} arrangement lapses -- you're living ${this.lifestyleTier} now.${lapse}${hint(` ("pay" the tab when you're flush.)`)}`;
7437
7490
  }
7438
7491
  else {
7439
7492
  rentLine = `Even the doorway costs something: ${this.rentDebt} nuyen on the tab.${shedLine}${hint(` ("pay" when you're flush.)`)}`;
@@ -9220,8 +9273,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
9220
9273
  ? ` {red-fg}[JAMMED -- reboot]{/red-fg}`
9221
9274
  : weaponItem.ammo === 0 ? ` {red-fg}[DRY]{/red-fg}` : ` [${weaponItem.ammo} rds]`)
9222
9275
  : '';
9276
+ // NO VERB COACHING IN THE HUD (MkxamPxmsSyonR3wC: "get rid of this
9277
+ // line in the status bar: -- "brandish""). The status bar is state;
9278
+ // the `brandish` hint already rides the equip/holster replies and
9279
+ // `equipment`, where it answers the moment the question comes up.
9223
9280
  if (player.plane === 'meat' && weaponItem && !player.weaponDrawn) {
9224
- return `{white-fg}Holstered: ${weaponItem.name}${ammoNote}${hint(' -- "brandish"')}{/white-fg}`;
9281
+ return `{white-fg}Holstered: ${weaponItem.name}${ammoNote}{/white-fg}`;
9225
9282
  }
9226
9283
  // White, not gray: gray is ANSI bright-black and illegible on
9227
9284
  // stock dark palettes -- the same ruling that moved the HUD
@@ -2613,7 +2613,9 @@ staff, so codes, passphrases, and lock combinations are exactly the thing you we
2613
2613
  THEM to solve -- say so plainly when they ask, and send them to find it rather than
2614
2614
  handing them a number you'd be making up. If you genuinely don't know a thing, say so plainly and point at who might. You
2615
2615
  front half the pay at the meet and settle the rest on delivery (the engine moves the money
2616
- -- never haggle it yourself unless they invoke a negotiation).`
2616
+ -- never haggle it yourself unless they invoke a negotiation).
2617
+ You chose this venue and you HOLD YOUR SEAT in it: never move, walk, approach, follow or
2618
+ leave -- the runner comes to you. Turn, lean, gesture or speak instead.`
2617
2619
  : '';
2618
2620
  // ---------- prompt ----------
2619
2621
  const prompt = `
@@ -3180,6 +3180,10 @@ export class Player extends AbstractPlayer {
3180
3180
  * rather than leaving you drawn at the old one. NPCs keep re-deriving --
3181
3181
  * a picket or a crowd is placed by spot on purpose (NPC overrides).
3182
3182
  */
3183
+ /** An exact cell somebody PUT this body on -- a step, a shove, a
3184
+ * flight -- as opposed to a drift seat pinned only so it is drawn in
3185
+ * one place (pinDerivedSeat). earshot.ts reads the first as a place. */
3186
+ get placedOnCell() { return this._atCell !== undefined && !this._cellPinnedFromSeat; }
3183
3187
  get pinsDerivedSeat() { return true; }
3184
3188
  pinDerivedSeat(cell) {
3185
3189
  this.atCell = cell;
@@ -1,4 +1,4 @@
1
- import { sameSpot, spotOf, spotsActive, isPerchSpot, OPEN_FLOOR, CONVERSATION_M, actorDistanceMeters } from './spots.js';
1
+ import { sameSpot, spotOf, spotsActive, isPerchSpot, OPEN_FLOOR, CONVERSATION_M, actorDistanceMeters, onTheFloor } from './spots.js';
2
2
  /**
3
3
  * WHO CAN OVERHEAR A MURMUR (playtest, Deditri: "a private place for
4
4
  * calls"). A call is audible to the room, and that is correct and
@@ -123,6 +123,28 @@ export function hearingRangeMeters(listener) {
123
123
  const bonus = listener.augBonus('hearing') + listener.qualityBonus('perception');
124
124
  return Math.max(SPEECH_METERS, SPEECH_METERS + bonus);
125
125
  }
126
+ /**
127
+ * HAS A PLACE IN THE ROOM: a named spot, OR an exact cell on the floor.
128
+ *
129
+ * THE SECOND HALF IS THE FIX (xurFSiqNtRNbXA88A: "I'm getting 'Nobody's
130
+ * close enough to catch that', but I'm right next to Mr. Vellum"). The
131
+ * runner stepped into Vellum's square; the excuse-me rule (spots.ts
132
+ * shoulderAside) moved him one cell over and, as it and yieldDoorway both
133
+ * do on purpose, left him on that EXACT cell with no spot name. This
134
+ * module read "no spot name" as "circulating", and a circulating listener
135
+ * is passing through and hears no murmur -- so a Johnson drawn one square
136
+ * from the runner was deaf to everything said to him for the rest of the
137
+ * meet. An exact cell is the most precise position the engine has
138
+ * (seatingIn seats it first), and distance is measured from it.
139
+ *
140
+ * A DRIFT SEAT IS STILL NOT A PLACE: a circulating runner's derived seat
141
+ * is pinned to atCell only so it is drawn in one spot (Player.
142
+ * pinDerivedSeat), and placedOnCell says no to it -- the Kessler rule
143
+ * below stands (speech-range.test.ts).
144
+ */
145
+ function standsSomewhere(actor) {
146
+ return spotOf(actor) !== undefined || (onTheFloor(actor) && actor.placedOnCell);
147
+ }
126
148
  /**
127
149
  * CAN THIS LISTENER HEAR ORDINARY SPEECH FROM THERE -- the whole of the
128
150
  * `say` rule, in one place so the verb, the NPC fan-out and any future
@@ -176,7 +198,7 @@ export function canHearShout(speaker, listener) {
176
198
  const room = speaker.currentLocation;
177
199
  if (!spotsActive(room))
178
200
  return true;
179
- if (spotOf(speaker) === undefined || spotOf(listener) === undefined)
201
+ if (!standsSomewhere(speaker) || !standsSomewhere(listener))
180
202
  return true;
181
203
  const reach = SHOUT_METERS + (hearingRangeMeters(listener) - SPEECH_METERS);
182
204
  return actorDistanceMeters(room, speaker, listener) <= reach;
@@ -186,10 +208,10 @@ export function canHearSpeech(speaker, listener) {
186
208
  if (!spotsActive(room))
187
209
  return true;
188
210
  // The speaker is unplaced: they are moving among everyone.
189
- if (spotOf(speaker) === undefined)
211
+ if (!standsSomewhere(speaker))
190
212
  return true;
191
213
  // The listener is unplaced: they are passing through, not standing here.
192
- if (spotOf(listener) === undefined)
214
+ if (!standsSomewhere(listener))
193
215
  return false;
194
216
  return actorDistanceMeters(room, speaker, listener) <= hearingRangeMeters(listener);
195
217
  }
@@ -126,6 +126,33 @@ export function authorizeNpcAction(scene, actor, verb, args) {
126
126
  }
127
127
  return ALLOWED;
128
128
  }
129
+ /**
130
+ * THE JOHNSON HOLDS THE MEET (CWMrB2aNyL4hhrjyG: "when at the meet, the
131
+ * johnson shouldn't just keep moving around the room").
132
+ *
133
+ * The run's client picks the venue and waits in it (Run Faster's meet,
134
+ * p.197-198: the Johnson sets the place, the runners come to them). His
135
+ * AI read every beat as a chance to stage himself -- "Mr. Lacquer moves
136
+ * to the open floor", "moves to the counter", prod 2026-10-04 -- so the
137
+ * man the runner came to see wandered the room for the whole briefing.
138
+ * Prose can still have him turn, lean, or look at the door; the engine
139
+ * keeps his body where the meet put it until the job is settled, and his
140
+ * leaving after the settlement is the engine's own beat (game.ts
141
+ * "Johnsons don't linger"), not his AI's.
142
+ *
143
+ * A FIGHT IS NOT A MEET: inside a Combat Turn the turn loop moves him,
144
+ * and a Johnson ducking for cover is not this rule's business.
145
+ *
146
+ * Returns a refusal reason, or undefined when he may move.
147
+ */
148
+ export function clientHoldsTheMeet(scene, actor) {
149
+ const client = scene.ownerGame?.currentClient;
150
+ if (!client || client.name !== actor.name || client.reported || client.dead)
151
+ return undefined;
152
+ if (actor.inExchange || scene.encounterIn?.(actor.currentLocation))
153
+ return undefined;
154
+ return `You hold your seat for the meet -- the runner came to you, and you stay where you are until the job is settled. Turn, gesture, or speak instead of walking.`;
155
+ }
129
156
  /**
130
157
  * Is this actor home? A decker jacked into the Matrix, a magician
131
158
  * projecting astrally and a rigger jumped into a drone all leave a body
@@ -7,6 +7,7 @@ import { fuzzyPickName } from './fuzzy-match.js';
7
7
  import { resetOverwatch } from './overwatch.js';
8
8
  import { rebootWipe } from './marks.js';
9
9
  import { hint } from './hints.js';
10
+ import { codesOnPage } from './clue-reading.js';
10
11
  import { actorCell } from './spots.js';
11
12
  import { announceGameOver } from './game-over.js';
12
13
  import { holdAtBrink } from './not-dead-yet.js';
@@ -624,6 +625,43 @@ export function isObjectiveItem(scene, itemName) {
624
625
  export function objectiveMark(scene, itemName) {
625
626
  return isObjectiveItem(scene, itemName) ? `{yellow-fg}${OBJECTIVE_GLYPH}{/yellow-fg} ` : '';
626
627
  }
628
+ /**
629
+ * PART OF THE JOB, NOT THE PRIZE (nfrBW27eRRxstJ8uC: "the 'Lot Catalogue
630
+ * Proof Sheet' isn't marked in my inventory as quest specific").
631
+ *
632
+ * The star is the objective alone, so a page the job's route runs
633
+ * through -- the proof sheet with the vault shutter's code pencilled in
634
+ * its margin -- sat unmarked among the loot, and the runner could not
635
+ * tell it from a souvenir. A LEAD is an item this scene's barriers
636
+ * answer to, read off the same facts the engine itself opens them with:
637
+ * - a page that spells one of the scene's device codes (clue-reading.ts
638
+ * codesOnPage -- the fact ensureClueDetails guarantees is written), or
639
+ * - the key a device declares as its own (Device.keyItem).
640
+ * Nothing else: a lead the engine cannot point to would be a guess.
641
+ *
642
+ * It marks the item, never the code -- whether the runner can READ the
643
+ * number off the page is still the Perception Test (clue-reading.ts).
644
+ * Hub scenes have no win condition and mark nothing, like the star.
645
+ */
646
+ export const LEAD_GLYPH = '☆';
647
+ export function isJobLead(scene, item) {
648
+ if (!scene?.getWinCondition?.())
649
+ return false;
650
+ const name = item.name.trim().toLowerCase();
651
+ for (const room of Object.values(scene.getRooms())) {
652
+ for (const device of room.devices ?? []) {
653
+ if (device.keyItem?.name.trim().toLowerCase() === name)
654
+ return true;
655
+ }
656
+ }
657
+ return codesOnPage(scene, item).length > 0;
658
+ }
659
+ /** `★ ` for the objective, `☆ ` for a lead (isJobLead), '' otherwise. */
660
+ export function jobMark(scene, item) {
661
+ if (isObjectiveItem(scene, item.name))
662
+ return objectiveMark(scene, item.name);
663
+ return isJobLead(scene, item) ? `{yellow-fg}${LEAD_GLYPH}{/yellow-fg} ` : '';
664
+ }
627
665
  /**
628
666
  * The "you can see them, but you can't touch them" refusal. When a target
629
667
  * name matches someone in the room on a plane the actor perceives but
@@ -1996,6 +1996,10 @@ export class SharedRunSession {
1996
1996
  const lines = g.applyHostedHomecoming(owed);
1997
1997
  if (lines)
1998
1998
  Logger.getInstance().logWithColor(lines, 'green');
1999
+ // A tier that slid at that homecoming moves house (settleAddress).
2000
+ const moved = await g.settleAddress();
2001
+ if (moved)
2002
+ Logger.getInstance().logWithColor(moved, 'green');
1999
2003
  }
2000
2004
  }
2001
2005
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.381.1",
3
+ "version": "5.383.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.",