@maka/maka-cli 5.166.0 → 5.169.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.
@@ -0,0 +1,51 @@
1
+ import { hint } from './hints.js';
2
+ import { spotOf, spotClimbSteps } from './spots.js';
3
+ import { METERS_PER_LEVEL } from './room-grid.js';
4
+ export function onTheWall(actor) {
5
+ const progress = actor.climbProgress;
6
+ if (!progress)
7
+ return undefined;
8
+ const room = actor.currentLocation;
9
+ const grid = room.ensureGrid();
10
+ const from = spotOf(actor);
11
+ if (!grid || from === undefined)
12
+ return undefined;
13
+ const fromZ = grid.spotCells.get(from)?.z;
14
+ const targetZ = grid.spotCells.get(progress.target)?.z;
15
+ // A bank toward a spot this grid cannot place, or one on the actor's
16
+ // own level, is stale -- it is not a wall anyone is on.
17
+ if (fromZ === undefined || targetZ === undefined || targetZ === fromZ)
18
+ return undefined;
19
+ const steps = spotClimbSteps(room, from, progress.target) ?? Math.abs(targetZ - fromZ);
20
+ const wallHeight = Math.max(steps, 1) * METERS_PER_LEVEL;
21
+ const travelled = Math.min(progress.meters, wallHeight);
22
+ const up = targetZ > fromZ;
23
+ return {
24
+ target: progress.target, from, travelled, wallHeight, up,
25
+ // Climbing UP, what you have travelled is your height. Climbing
26
+ // DOWN, travel is measured from the top, so height is what is left.
27
+ height: up ? travelled : wallHeight - travelled,
28
+ };
29
+ }
30
+ /** "about 1m" / "about a body length" -- the same rounding the climb
31
+ * ticker and the elevation bar use, so no two readouts disagree. */
32
+ export function heightPhrase(meters) {
33
+ return meters < 1 ? 'about a body length' : `about ${Math.round(meters)}m`;
34
+ }
35
+ /**
36
+ * The refusal every horizontal verb gives a runner who is part-way up
37
+ * a wall. Not a rule -- canon says nothing about acting mid-climb, and
38
+ * this invents nothing -- but a fact about where the body is: you do
39
+ * not cross a floor you are not standing on. Names both ways off the
40
+ * wall, because "you can't" with no way out is the false-impression
41
+ * shape this whole family of reports has been about.
42
+ */
43
+ export function midClimbRefusal(actor) {
44
+ const wall = onTheWall(actor);
45
+ if (!wall)
46
+ return undefined;
47
+ return wall.up
48
+ ? `You're partway up the wall toward the ${wall.target}, ${heightPhrase(wall.height)} off the ground -- not going anywhere sideways from here.${hint(` ("climb" keeps going; "descend" gets you back down to the ${wall.from} first.)`)}`
49
+ : `You're partway down the wall from the ${wall.from}, ${heightPhrase(wall.height)} above the ${wall.target} -- not going anywhere sideways from here.${hint(` ("descend" keeps going; "climb" gets you back up onto the ${wall.from} first.)`)}`;
50
+ }
51
+ //# sourceMappingURL=climb-state.js.map
@@ -92,6 +92,22 @@ export function parseMarkDeclaration(words) {
92
92
  export function deviceDefensePool(device) {
93
93
  return { label: 'Device Rating + Firewall', pool: Math.max(0, device.rating * 2) };
94
94
  }
95
+ /**
96
+ * A SLAVE ATTACKED THROUGH ITS MASTER (SR5 p.233, PANs and WANs):
97
+ * "Whenever a slaved device is called on to make a defense test, it
98
+ * uses either its own or its master's rating for each rating in the
99
+ * test ... whichever is higher in each instance." Per rating, not
100
+ * wholesale: the device's own Rating against the host's Rating, its
101
+ * own Firewall (which this engine reads as its rating, see above)
102
+ * against the host's Firewall. Only from the grid -- inside the host
103
+ * you are directly connected and the slave stands alone (p.233; Data
104
+ * Trails p.87-88), which is deviceDefensePool.
105
+ */
106
+ export function wanDefensePool(device, host) {
107
+ const rating = Math.max(device.rating, host.rating);
108
+ const firewall = Math.max(device.rating, host.attributes().firewall);
109
+ return { label: "Device Rating + Firewall (the master's where higher, p.233)", pool: Math.max(0, rating + firewall) };
110
+ }
95
111
  /**
96
112
  * A HOST'S OWN DEFENCE IS AN ENGINE RULING, NOT A CITATION, and this
97
113
  * comment is the label on it. Asked directly (2026-09-03), the rulebooks
@@ -1,5 +1,5 @@
1
1
  import { Category } from '../types/shared/item-enum.js';
2
- import { spotOf, canReach, isInCover, coverAvailableFor, actorDistanceMeters } from './spots.js';
2
+ import { inMeleeReach, isInCover, coverAvailableFor, actorDistanceMeters } from './spots.js';
3
3
  import { Logger } from './logger.js';
4
4
  /**
5
5
  * AN NPC'S ACTION PHASE, PLAYED BY RULE (SR5 p.163-167).
@@ -82,11 +82,15 @@ export async function runNpcActionPhase(enc, npc) {
82
82
  }
83
83
  const melee = !firearm || dry || weapon.jammed;
84
84
  if (melee) {
85
- const targetSpot = spotOf(target);
86
- if (!canReach(npc, targetSpot)) {
87
- const r = await npc.actInCombat(`move to ${targetSpot}`);
88
- logger.write(`Brain: ${npc.name} move to ${targetSpot} -> ${r}`);
89
- if (!canReach(npc, targetSpot)) {
85
+ // ARM'S REACH, NOT SPOT NAME (HBssKNQahWxGR95wB: "Ellis is able to
86
+ // attack me, however his position is several squares away"). The
87
+ // open floor is one spot across most of a room, so canReach said
88
+ // yes from five squares off and the swing landed. inMeleeReach asks
89
+ // the cells; "move to <person>" is the walk that closes them.
90
+ if (!inMeleeReach(npc, target)) {
91
+ const r = await npc.actInCombat(`move to ${target.name}`);
92
+ logger.write(`Brain: ${npc.name} move to ${target.name} -> ${r}`);
93
+ if (!inMeleeReach(npc, target)) {
90
94
  // Couldn't close this turn: get behind something and wait.
91
95
  tell(`${npc.name} closes on ${target.name} but can't cover the ground this turn.`);
92
96
  if (!isInCover(npc) && coverAvailableFor(npc) && (enc.budgetOf(npc)?.simple ?? 0) > 0) {
@@ -1,6 +1,7 @@
1
1
  import { archetypeGearDefs } from '../archetypes.js';
2
2
  import { siteRequest } from './cloud-saves.js';
3
3
  import { getAspect, ASPECTED_PRIORITY, MAGICAL_GROUP_SKILLS } from '../aspects.js';
4
+ import { skillsInGroup } from '../skill-groups.js';
4
5
  /**
5
6
  * A PREGEN GOES TO THE WEB TO BE BOUND (user, 2026-09-03): "if they choose
6
7
  * to bind the pre-gen character, I'd like the chargen sheet to open the
@@ -11,16 +12,25 @@ import { getAspect, ASPECTED_PRIORITY, MAGICAL_GROUP_SKILLS } from '../aspects.j
11
12
  * So binding a pregen no longer writes a bound save. The CLI creates a
12
13
  * chargen WORKSHEET through the site's own API (the same collection the
13
14
  * Forge wizard edits), fills it from the archetype -- attributes as spend
14
- * over the human minimums, the skills, the qualities, the chrome at
15
- * standard grade, the catalog slugs as gear lines, the spellbook and
16
- * powers, the name and gender -- and opens the wizard on it at Review.
17
- * The runner is born when the player finalizes there, exactly as a
18
- * web-built one is, and launch sources her from the cloud afterwards.
15
+ * over the human minimums, the skills and skill groups, the qualities
16
+ * with their ratings and picks, the chrome at standard grade, the catalog
17
+ * slugs as gear lines, the spellbook and powers, the name and gender --
18
+ * and opens the wizard on it at Review. The runner is born when the
19
+ * player finalizes there, exactly as a web-built one is, and launch
20
+ * sources her from the cloud afterwards.
19
21
  *
20
- * A pregen's spread is a spread, not a priority build, so the validator
21
- * will have things to say on arrival. That is the feature: the player
22
- * sees the ledger and settles it in the UI instead of the engine quietly
23
- * accepting a sheet no worksheet could produce.
22
+ * A PREGEN ARRIVES COMPLETE (user, 2026-09-11: "when a user chooses a
23
+ * premade archetype, they shouldn't need to figure out what's wrong
24
+ * with it -- it should just work"). Every archetype is a priority build
25
+ * (IArchetype.priorities) with every pool spent to the column and
26
+ * nothing the validator refuses; archetype-priorities.test.ts holds the
27
+ * CLI half of that contract and the site's chargen-archetypes suite runs
28
+ * the real validator over every one. Review is where the player may
29
+ * ADJUST, not where they finish someone else's homework. Until this
30
+ * date the patch dropped three fields the wizard reads -- `picks`,
31
+ * `qualityRatings`, `skillGroups` -- so the Decker arrived with an
32
+ * unanswered Codeslinger, every rated quality priced at rating 1, and
33
+ * no archetype could spend a single group point.
24
34
  *
25
35
  * The URL shape and the `open` query are CO-OWNED with the web side
26
36
  * (cli-login.tsx allowedNext, character-generator.tsx open(id)) -- do not
@@ -133,6 +143,24 @@ export function pregenWorksheetPatch(archetype, name, gender) {
133
143
  for (const member of MAGICAL_GROUP_SKILLS[aspect.group] ?? [])
134
144
  delete skills[member];
135
145
  }
146
+ /**
147
+ * BOUGHT GROUPS, THE SAME WAY (SR5 p.88, the skills column's second
148
+ * pool): a group the archetype declares is sent as a group, and its
149
+ * members come OUT of `skills` -- a group cannot be broken by rating a
150
+ * member individually on top, and the validator refuses exactly that
151
+ * ("covered by the X group"). The archetype's `combat.skills` keeps the
152
+ * expanded per-skill ratings for the local engine; the group is what
153
+ * the worksheet says about how they were paid for. Membership is read
154
+ * from skill-groups.ts itself, never copied here.
155
+ */
156
+ const skillGroups = {};
157
+ for (const [group, rating] of Object.entries(archetype.skillGroups ?? {})) {
158
+ if (!(rating > 0))
159
+ continue;
160
+ skillGroups[group] = rating;
161
+ for (const member of skillsInGroup(group))
162
+ delete skills[member];
163
+ }
136
164
  const gearSeen = new Set();
137
165
  const gear = [];
138
166
  for (const slug of archetype.gearSlugs ?? []) {
@@ -156,7 +184,10 @@ export function pregenWorksheetPatch(archetype, name, gender) {
156
184
  attributeSpend,
157
185
  specialSpend: { edge: spend(c.edge, HUMAN_EDGE_MIN), magic: magicPath === 'technomancer' || magicPath === 'mundane' ? 0 : Math.max(0, (c.magic ?? 0) - magicOffer), resonance: magicPath === 'technomancer' ? Math.max(0, (c.resonance ?? 0) - magicOffer) : 0 },
158
186
  skills,
187
+ ...(Object.keys(skillGroups).length ? { skillGroups } : {}),
159
188
  qualities: [...(archetype.qualities ?? [])],
189
+ ...(archetype.qualityRatings && Object.keys(archetype.qualityRatings).length ? { qualityRatings: { ...archetype.qualityRatings } } : {}),
190
+ ...(archetype.qualityPicks && Object.keys(archetype.qualityPicks).length ? { picks: { ...archetype.qualityPicks } } : {}),
160
191
  augmentations: (archetype.augs ?? []).map(key => ({ key, grade: 'standard' })),
161
192
  gear,
162
193
  lifestyle: 'Street',
@@ -203,7 +234,7 @@ export function pregenHandoffLines(url, name) {
203
234
  return {
204
235
  notice: `${name}'s sheet is waiting in the character generator: ${url}`,
205
236
  lines: [
206
- `Look it over there -- the ledger will say what a pregen leaves unsettled, and you can adjust before you finalize.`,
237
+ `It arrives complete and legal -- look it over there, adjust anything you like, then finalize.`,
207
238
  `When she is born, run "maka play" and she will be in your runners.`,
208
239
  ],
209
240
  };
@@ -1204,6 +1204,11 @@ export class SharedRunSession {
1204
1204
  sessionID: this.sessionID,
1205
1205
  query: inviteMatch[1].trim(),
1206
1206
  });
1207
+ // A mid-run seat needs the runner REACHABLE now (site ruling
1208
+ // 2026-09-11: a CLI alive on the account, or a browser logged
1209
+ // in); the server refuses with `offline` otherwise, so `online`
1210
+ // is always true past this point. The aside stays for a server
1211
+ // older than the rule.
1207
1212
  return `Invite sent to ${res.memberRunner} -- a seat at this table, mid-job.${res.online ? '' : ` (They're off the street -- it'll wait on their link.)`}`;
1208
1213
  }
1209
1214
  catch (err) {
@@ -1290,10 +1295,21 @@ export class SharedRunSession {
1290
1295
  return `You pull out of the run. The crew's voices fade off the link.`;
1291
1296
  }
1292
1297
  /** Quit-as-pause (ruling 2026-08-24): the seat survives, the run
1293
- * keeps rolling server-side, and the server marks us disconnected
1294
- * the moment this process's link closes. No leave call, no
1295
- * forfeiture -- "call taxi" rides back in. */
1298
+ * keeps rolling server-side, and the server marks us disconnected.
1299
+ * No leave call, no forfeiture -- "call taxi" rides back in.
1300
+ *
1301
+ * SAID OUT LOUD (2026-09-11): the server used to learn of this only
1302
+ * when the process's socket closed -- and this process does not
1303
+ * close it; the hub link stays up for invites and pages. So a runner
1304
+ * who stepped away sat `joined` on the doc with nobody at the
1305
+ * keyboard, and the site's sheet could not tell "at the table" from
1306
+ * "stepped away". sideQuest.stepAway is that one word; best effort,
1307
+ * and the socket-close path still stands behind it. */
1296
1308
  async stepAway() {
1309
+ try {
1310
+ await HubLink.call('sideQuest.stepAway', { sessionID: this.sessionID }, { timeoutMs: 5000 });
1311
+ }
1312
+ catch { /* the seat stays warm either way; the socket close says the same later */ }
1297
1313
  this.finish('');
1298
1314
  // THE OTHER HALF OF blankRunPanels. Entering a run clears the hub's
1299
1315
  // maps so the server's paint cannot be confused with them; stepping
@@ -803,6 +803,37 @@ export function actorCell(room, actor, seats) {
803
803
  * words for the test are "next to me in the room map".
804
804
  */
805
805
  export const ADJACENT_METERS = METERS_PER_CELL * 1.5;
806
+ /**
807
+ * WITHIN ARM'S REACH -- the melee question, asked of CELLS and not of
808
+ * spots (HBssKNQahWxGR95wB: "Ellis is able to attack me, however his
809
+ * position is several squares away from me, not in melee range").
810
+ *
811
+ * canReach answers by SPOT NAME, and that was right until bodies got
812
+ * their own cells (seatingIn, 2026-08-25): the open floor is one spot
813
+ * that covers most of a room, so two people "on the open floor" can
814
+ * be five squares apart and canReach still says yes. Every melee gate
815
+ * -- the player's attack, the NPC brain's "can I swing this phase" --
816
+ * read that yes and let the blow land across the room. The map drew
817
+ * the truth and the dice ignored it.
818
+ *
819
+ * Reach here is the engine's own atom: one cell, orthogonal or diagonal
820
+ * (ADJACENT_METERS -- see the note above on why that is not dressed as
821
+ * a canon number). Falls back to canReach wherever there is no cell to
822
+ * measure from, so a spotless or ungridded room keeps the compat rule.
823
+ */
824
+ export function inMeleeReach(actor, target) {
825
+ if (!canReach(actor, spotOf(target)))
826
+ return false;
827
+ const room = actor.currentLocation;
828
+ if (!room.ensureGrid() || target.currentLocation !== room)
829
+ return true;
830
+ const seats = seatingIn(room);
831
+ const a = actorCell(room, actor, seats);
832
+ const t = actorCell(room, target, seats);
833
+ if (!a || !t)
834
+ return true;
835
+ return distanceMeters(a, t) <= ADJACENT_METERS;
836
+ }
806
837
  /** Everything you have to walk AROUND rather than through: other
807
838
  * people's bodies and the furniture itself. findPath treats these as a
808
839
  * soft block (see its pass order) so they steer a route without ever
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.166.0",
3
+ "version": "5.169.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.",