@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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +238 -146
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +27 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/climb.js +21 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +54 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +381 -255
- package/bundle/typescript/src/commands/game/sideQuest/utilities/climb-state.js +51 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.js +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +10 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js +41 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +19 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +31 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* The runner is born when the
|
|
18
|
-
* web-built one is, and launch
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
`
|
|
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
|
-
*
|
|
1295
|
-
*
|
|
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.
|
|
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.",
|