@maka/maka-cli 5.201.0 → 5.203.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/commands/compile.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +13 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +48 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js +37 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +109 -11
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +41 -9
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +18 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +11 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +23 -3
- package/bundle/typescript/src/commands/game/sideQuest/npc-kits.js +295 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +87 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +17 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +25 -13
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.203.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,6 +1,7 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
3
3
|
import { hint } from '../utilities/hints.js';
|
|
4
|
+
import { spriteMatrix } from '../sprite-powers.js';
|
|
4
5
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
6
|
/** Exported for game.ts: re-shelling a REGISTERED sprite after travel
|
|
6
7
|
* or a resumed save needs the same stat builder. */
|
|
@@ -161,6 +162,12 @@ export class CompileCommand extends Command {
|
|
|
161
162
|
// A sprite rides its compiler's persona: same grid, same host.
|
|
162
163
|
if (shell)
|
|
163
164
|
shell.matrixPosition = actor.matrixPosition;
|
|
165
|
+
// ...and carries its OWN Matrix array (p.254, p.259), which is what
|
|
166
|
+
// fills the [Sleaze]/[Attack] bracket on every roll it makes. It
|
|
167
|
+
// has no deck for the rig lookup to find, so without this its
|
|
168
|
+
// limits computed to 0 and rollPool refused them as "not a limit".
|
|
169
|
+
if (shell)
|
|
170
|
+
shell.spriteMatrixAttrs = spriteMatrix(typeKey, level);
|
|
164
171
|
if (shell && this.game) {
|
|
165
172
|
const entry = this.game.companions.find(c => c.npc === shell);
|
|
166
173
|
if (entry)
|
|
@@ -13,6 +13,17 @@ export class EraseMarkCommand extends Command {
|
|
|
13
13
|
static verb = 'unmark';
|
|
14
14
|
static description = 'Erase Mark (Complex Action, SR5 p.239): "unmark me" scrubs the marks others hold on your persona; "unmark <icon>" needs three marks on that icon first. Computer + Logic [Attack] v. Willpower + Firewall. An Attack action -- GOD counts it. Also "erase mark on <icon>".';
|
|
15
15
|
static npcPolicy = 'authorized';
|
|
16
|
+
/**
|
|
17
|
+
* A PLACER'S NAME STARTING A SENTENCE. `hostLabel` yields "the Approach
|
|
18
|
+
* host", lowercase and correct mid-clause -- and these lines open with
|
|
19
|
+
* it, so a bench read "the Approach host's key sloughs off...". The
|
|
20
|
+
* same class of blemish grid-names.ts records at length: cosmetic, and
|
|
21
|
+
* not harmless on a repro bench, where a reviewer meeting broken prose
|
|
22
|
+
* reasonably starts doubting the fixture.
|
|
23
|
+
*/
|
|
24
|
+
static opening(name) {
|
|
25
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
26
|
+
}
|
|
16
27
|
/** p.239: two marks at -4, three at -10; one is unpenalised. */
|
|
17
28
|
static penaltyFor(count) {
|
|
18
29
|
if (count >= 3)
|
|
@@ -180,7 +191,7 @@ export class EraseMarkCommand extends Command {
|
|
|
180
191
|
if (attempt.hits <= resist.hits) {
|
|
181
192
|
this.logger.write(`${actor.name} failed Erase Mark on ${label} (${chosen.name}): ${attempt.hits} v ${resist.hits}.`);
|
|
182
193
|
return [
|
|
183
|
-
`The key holds. ${chosen.name}'s mark is still written into ${label} and your scrub slides off it.`,
|
|
194
|
+
`The key holds. ${EraseMarkCommand.opening(chosen.name)}'s mark is still written into ${label} and your scrub slides off it.`,
|
|
184
195
|
hint(` (A Complex Action each try${take > 1 ? `; going for ${take} at once cost you ${penalty} dice` : ''}.)`),
|
|
185
196
|
...(god.length > 0 ? [`\n${god.join('\n')}`] : []),
|
|
186
197
|
].join('');
|
|
@@ -192,7 +203,7 @@ export class EraseMarkCommand extends Command {
|
|
|
192
203
|
this.logger.write(`${actor.name} erased ${take} of ${chosen.name}'s mark(s) on ${label}: ${attempt.hits} v ${resist.hits}.`);
|
|
193
204
|
const left = this.remainingOn(target, actor);
|
|
194
205
|
return [
|
|
195
|
-
`{light-blue-fg}${chosen.name}'s ${take > 1 ? `${take} keys slough` : 'key sloughs'} off ${label} -- the recognition pattern unwrites itself and ${target.kind === 'self' ? 'your icon stops answering to it' : 'the icon forgets it ever agreed'}.{/light-blue-fg}`,
|
|
206
|
+
`{light-blue-fg}${EraseMarkCommand.opening(chosen.name)}'s ${take > 1 ? `${take} keys slough` : 'key sloughs'} off ${label} -- the recognition pattern unwrites itself and ${target.kind === 'self' ? 'your icon stops answering to it' : 'the icon forgets it ever agreed'}.{/light-blue-fg}`,
|
|
196
207
|
target.kind === 'self'
|
|
197
208
|
? (left > 0
|
|
198
209
|
? `\n${left} mark${left === 1 ? '' : 's'} still on you.`
|
|
@@ -265,6 +265,16 @@ export class OrderCommand extends Command {
|
|
|
265
265
|
// Result strings that mean the atomic tier bounced -- collected from
|
|
266
266
|
// real refusals (go/attack/talk/take across sessions). The ’ variant
|
|
267
267
|
// covers commands that answer with a smart quote.
|
|
268
|
-
|
|
268
|
+
//
|
|
269
|
+
// THE MATRIX REFUSALS WERE MISSING, and a repro bench found it the day
|
|
270
|
+
// sprites started being billed at all (wjFBY3zgyWzJP9adw). Order a
|
|
271
|
+
// sprite at a maglock slaved to a host you hold no mark on and `hack`
|
|
272
|
+
// answers "a host's devices answer nobody who holds no mark on the
|
|
273
|
+
// host itself" -- a refusal, nothing rolled, nothing changed. It
|
|
274
|
+
// matched none of the phrases below, so the atomic tier read it as
|
|
275
|
+
// obedience and charged a task for it. Free before this only because
|
|
276
|
+
// no sprite was ever charged for anything; a spirit ordered into the
|
|
277
|
+
// same wall has always paid a service for nothing.
|
|
278
|
+
static ATOMIC_REFUSAL = /not a way to go|can['’]t go that way|isn['’]t here|don['’]t see|there is no item|too heavy|doesn['’]t know how|not valid|doesn['’]t seem to have|answer nobody who holds no mark|no cyberdeck|needs a MARK|hold none on|is not here to|nothing here answers to|finds no persona|finds no device/i;
|
|
269
279
|
}
|
|
270
280
|
//# sourceMappingURL=order.js.map
|
|
@@ -715,5 +715,52 @@
|
|
|
715
715
|
// somebody happened to look -- the "wrong ability on a sprite is
|
|
716
716
|
// worse than a missing one" case. It wants the glitch contract in one
|
|
717
717
|
// place first, and that is its own piece of work.
|
|
718
|
-
|
|
718
|
+
// 1.64.0 (2026-09-15): NPC KITS -- "everyone is basically naked."
|
|
719
|
+
// - NEW CATALOG KIND 'npc-kit' (v21): SR5 p.379-385's Professional
|
|
720
|
+
// Rating tiers 0-6 as rows, each the book's own statblock AND gear
|
|
721
|
+
// (npc-kits.ts reads them; catalog.ts npcKitFor). Before this every
|
|
722
|
+
// NPC was an attribute block: no armor (soak on Body alone), no
|
|
723
|
+
// weapon (bare fists, always melee).
|
|
724
|
+
// - SEED SHAPE: ISceneSeedJson.securityTier (the SITE's PR, never the
|
|
725
|
+
// campaign job number), IPlayerSeedJson.kit / lieutenant. The
|
|
726
|
+
// generator declares the tier from a closed 0-6 table, clamps it
|
|
727
|
+
// against the job number, and assigns a kit to every hostile
|
|
728
|
+
// deterministically -- the model never writes a slug.
|
|
729
|
+
// - THE PARTY NUDGE IS THE LIEUTENANT (p.380-381): a party that
|
|
730
|
+
// outguns the tier draws the printed lieutenant, and the group's PR
|
|
731
|
+
// and Group Edge read one higher. Never a kit from a tier above.
|
|
732
|
+
// - GROUP EDGE (p.380) on the CombatEncounter keyed by kit; MORALE
|
|
733
|
+
// (p.379-380) breaks a team to surrender at the book's casualty
|
|
734
|
+
// thresholds, PR 5-6 never. npc-combat-brain.ts is the gamemaster.
|
|
735
|
+
// - EPHEMERALS: gangers are the PR 1 kit, sweep officers the PR 3
|
|
736
|
+
// kit with a real Biometric Reader; bystanders stay unarmed.
|
|
737
|
+
// Scene.removeActor(name, {spill:false}) so a departing body takes
|
|
738
|
+
// its gun with it.
|
|
739
|
+
// - LOOT: worn armor is RUINED on a kill (Item.ruined -- rated 0,
|
|
740
|
+
// worth nothing, saved); guns and commlinks drop intact.
|
|
741
|
+
// - PURGED: the dead `firearms` skill key from the NPC prompt, the
|
|
742
|
+
// ganger constant and the seed doc; a seed still carrying it maps
|
|
743
|
+
// onto pistols/automatics/longarms at the boundary.
|
|
744
|
+
// 1.65.0 (2026-09-16): THREE THINGS THE FIRST SPRITE BENCH FOUND. The
|
|
745
|
+
// repro bench for wjFBY3zgyWzJP9adw made an ordered sprite's dice
|
|
746
|
+
// visible for the first time, and visible dice are readable dice.
|
|
747
|
+
// - A SPRITE'S MATRIX ARRAY IS ITS OWN (p.254, p.259). It carries no
|
|
748
|
+
// deck, so baseMatrixAttribute's rig lookup returned 0 and every
|
|
749
|
+
// bracketed sprite roll computed a limit of zero -- which rollPool
|
|
750
|
+
// refuses as "not a limit" and said so, out loud, in the
|
|
751
|
+
// transcript. commands/compile.ts now stamps the book's per-type
|
|
752
|
+
// table (sprite-powers.spriteMatrix) onto the shell, so [Sleaze],
|
|
753
|
+
// [Attack] and [Data Processing] finally have numbers in them.
|
|
754
|
+
// Only Resonance equals Level; the four attributes do not.
|
|
755
|
+
// - A REFUSED ORDER MUST NOT COST A TASK. order.ts decides the atomic
|
|
756
|
+
// tier bounced by matching the result against a phrase list, and
|
|
757
|
+
// the Matrix refusals were never in it -- so a sprite ordered at a
|
|
758
|
+
// maglock slaved to a host it holds no mark on was told no AND
|
|
759
|
+
// charged for it. Free before only because sprites were charged for
|
|
760
|
+
// nothing; a spirit walked into the same wall and paid a service.
|
|
761
|
+
// - Erase Mark opened two lines with a host's lowercase label ("the
|
|
762
|
+
// Approach host's key sloughs off..."). Cosmetic, and the same
|
|
763
|
+
// class of blemish grid-names.ts records at length, because a
|
|
764
|
+
// reviewer meeting broken prose on a bench doubts the fixture.
|
|
765
|
+
export const ENGINE_VERSION = '1.65.0';
|
|
719
766
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -28,6 +28,15 @@ export class NPCFactory {
|
|
|
28
28
|
npc.setAuthoredAppearance(json.appearance);
|
|
29
29
|
return npc;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* ONLY FOUR FIELDS CROSS HERE, on purpose. `startSpot`, `guarding`,
|
|
33
|
+
* `hostile`, `kit` and `lieutenant` are applied by scene-factory AFTER
|
|
34
|
+
* the NPC is placed (Steps 3 and 4.04), because each depends on
|
|
35
|
+
* something the constructor cannot see -- the room it stands in, the
|
|
36
|
+
* items heldBy already put in its hands. Adding one of them to this
|
|
37
|
+
* config would silently reorder it ahead of that. (`startingItems` is
|
|
38
|
+
* dropped too, and documented as ignored for NPCs in repro-scene.ts.)
|
|
39
|
+
*/
|
|
31
40
|
convertPlayerSeedToConstructorConfig(seed) {
|
|
32
41
|
if (!seed.name) {
|
|
33
42
|
throw new Error(`Cannot create NPC without a name`);
|
|
@@ -40,8 +49,35 @@ export class NPCFactory {
|
|
|
40
49
|
playerName: seed.name,
|
|
41
50
|
lifestyleChoices: seed.lifestyleChoices,
|
|
42
51
|
startLocation,
|
|
43
|
-
combat: seed.combat
|
|
52
|
+
combat: seed.combat ? { ...seed.combat, skills: splitLegacySkills(seed.combat.skills) } : undefined,
|
|
44
53
|
};
|
|
45
54
|
}
|
|
46
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A SEED STILL SPEAKING THE OLD SKILL NAMES (skills.ts, stage 3): the
|
|
58
|
+
* same group-purchase reading persistence.ts applies to saves -- a
|
|
59
|
+
* condensed `firearms: N` meant "good with guns, all of them", so it
|
|
60
|
+
* lands as Pistols, Automatics and Longarms at N (p.88) where none was
|
|
61
|
+
* set; `athletics: N` lands as Gymnastics, Running and Swimming. The
|
|
62
|
+
* retired keys are dropped so they cannot ride into `Player.skills` as
|
|
63
|
+
* dead entries. Mapped, never merely deleted: a "sharpshooter" the
|
|
64
|
+
* model wrote with the old key was a sharpshooter, not a nobody.
|
|
65
|
+
*/
|
|
66
|
+
export function splitLegacySkills(skills) {
|
|
67
|
+
if (!skills)
|
|
68
|
+
return undefined;
|
|
69
|
+
const out = { ...skills };
|
|
70
|
+
const spread = (legacy, onto) => {
|
|
71
|
+
const n = out[legacy];
|
|
72
|
+
if (n === undefined)
|
|
73
|
+
return;
|
|
74
|
+
if (n > 0)
|
|
75
|
+
for (const key of onto)
|
|
76
|
+
out[key] ??= n;
|
|
77
|
+
delete out[legacy];
|
|
78
|
+
};
|
|
79
|
+
spread('firearms', ['pistols', 'automatics', 'longarms']);
|
|
80
|
+
spread('athletics', ['gymnastics', 'running', 'swimming']);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
47
83
|
//# sourceMappingURL=npc-factory.js.map
|
|
@@ -4,6 +4,7 @@ import { Logger } from '../utilities/logger.js';
|
|
|
4
4
|
import { catalogAugmentations } from '../utilities/catalog.js';
|
|
5
5
|
import { isOutdoorsPlace } from '../utilities/outdoors.js';
|
|
6
6
|
import { clampDeviceKind } from '../utilities/affordances.js';
|
|
7
|
+
import { clampSecurityTier, expectedStrengthFor, kitSlugForTier, maxSecurityTierFor, partyDrawsLieutenant } from '../npc-kits.js';
|
|
7
8
|
/**
|
|
8
9
|
* CHUNKED scene generation (see SceneSeedGenerator.generate for the
|
|
9
10
|
* pipeline that drives this module). One monolithic whole-scene completion
|
|
@@ -195,6 +196,73 @@ function repairNote(previousError) {
|
|
|
195
196
|
export function partyHeadcount(crew) {
|
|
196
197
|
return (crew?.length ?? 0) + 1;
|
|
197
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* HOW HARD THIS TABLE HITS, per head, on the scale npc-kits.ts
|
|
201
|
+
* expectedStrengthFor uses for a grunt (attack pool + defense pool).
|
|
202
|
+
*
|
|
203
|
+
* REUSES THE NUMBERS THAT ALREADY EXIST rather than inventing a third
|
|
204
|
+
* formula: the leader's figure is exactly the "Combat calibration"
|
|
205
|
+
* line buildPlayerDossier prints (getAttackPool + getDefensePool), and
|
|
206
|
+
* a crew member's is the body + agility + logic + magic sum the hiring
|
|
207
|
+
* price already reads (Game.spawnCrew's asking cut), scaled onto the
|
|
208
|
+
* same axis. Averaged per head, because the lieutenant question is
|
|
209
|
+
* "does this party outclass the tier", not "how many of them are there"
|
|
210
|
+
* -- head count already scales the OPPOSITION COUNT (Scope, above).
|
|
211
|
+
*
|
|
212
|
+
* A dossier player that is not a real Player (the tests' stubs, a
|
|
213
|
+
* server-side draft with no engine handle) contributes nothing and the
|
|
214
|
+
* crew alone decides; no player and no crew is 0, which never draws a
|
|
215
|
+
* lieutenant.
|
|
216
|
+
*/
|
|
217
|
+
export function partyStrength(player, crew) {
|
|
218
|
+
const heads = [];
|
|
219
|
+
const p = player;
|
|
220
|
+
if (p && typeof p.getAttackPool === 'function' && typeof p.getDefensePool === 'function') {
|
|
221
|
+
heads.push(p.getAttackPool() + p.getDefensePool());
|
|
222
|
+
}
|
|
223
|
+
for (const m of crew ?? []) {
|
|
224
|
+
const c = m.combat;
|
|
225
|
+
heads.push(1.5 * ((c.body ?? 3) + (c.agility ?? 3) + (c.logic ?? 3) + (c.magic ?? 0)));
|
|
226
|
+
}
|
|
227
|
+
if (heads.length === 0)
|
|
228
|
+
return 0;
|
|
229
|
+
return heads.reduce((s, h) => s + h, 0) / heads.length;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* SETTLE WHO GUARDS THIS PLACE (npc-kits.ts, SR5 p.379-385). Called once
|
|
233
|
+
* per accepted skeleton, after validateSkeleton and before the detail
|
|
234
|
+
* passes: clamps the declared securityTier against the campaign job
|
|
235
|
+
* number (a job #1 does not draw elite corporate security however the
|
|
236
|
+
* model felt about it), and decides whether the site posts its
|
|
237
|
+
* lieutenant -- p.380-381's own mechanism for a tougher group, and the
|
|
238
|
+
* whole of how opposition scales to a strong party. The lieutenant is
|
|
239
|
+
* the hostile guarding the prize's room if there is one, else the first
|
|
240
|
+
* hostile. Returns the notes for the generation log.
|
|
241
|
+
*/
|
|
242
|
+
export function settleSecurity(skeleton, campaignTier, player, crew) {
|
|
243
|
+
const notes = [];
|
|
244
|
+
const declared = skeleton.securityTier;
|
|
245
|
+
const settled = clampSecurityTier(declared, campaignTier);
|
|
246
|
+
if (declared !== settled) {
|
|
247
|
+
notes.push(`securityTier ${declared === undefined ? 'absent' : JSON.stringify(declared)} -> ${settled} (job #${campaignTier} fields at most PR ${maxSecurityTierFor(campaignTier)}).`);
|
|
248
|
+
}
|
|
249
|
+
skeleton.securityTier = settled;
|
|
250
|
+
const hostiles = skeleton.npcs.filter(n => n.name !== skeleton.client && n.hostile === true);
|
|
251
|
+
if (hostiles.length === 0) {
|
|
252
|
+
delete skeleton.lieutenant;
|
|
253
|
+
return notes;
|
|
254
|
+
}
|
|
255
|
+
const strength = partyStrength(player, crew);
|
|
256
|
+
if (!partyDrawsLieutenant(strength, settled)) {
|
|
257
|
+
delete skeleton.lieutenant;
|
|
258
|
+
return notes;
|
|
259
|
+
}
|
|
260
|
+
const prizeRoom = skeleton.items.find(i => i.name === skeleton.winCondition?.item)?.room;
|
|
261
|
+
const pick = hostiles.find(n => prizeRoom !== undefined && n.startLocation === prizeRoom) ?? hostiles[0];
|
|
262
|
+
skeleton.lieutenant = pick.name;
|
|
263
|
+
notes.push(`party strength ${strength.toFixed(1)} outguns PR ${settled} (expects ~${expectedStrengthFor(settled)}) -- ${pick.name} is the lieutenant.`);
|
|
264
|
+
return notes;
|
|
265
|
+
}
|
|
198
266
|
function buildTeamBlock(crew) {
|
|
199
267
|
if (!crew || crew.length === 0)
|
|
200
268
|
return '';
|
|
@@ -478,7 +546,19 @@ ${JSON_ONLY}
|
|
|
478
546
|
// again more per job after that
|
|
479
547
|
"rewardItem"?: string // optional bonus: an items[].name with role "reward"
|
|
480
548
|
},
|
|
481
|
-
"client": string // the npcs[].name of the run's CLIENT -- see CLIENT below
|
|
549
|
+
"client": string, // the npcs[].name of the run's CLIENT -- see CLIENT below
|
|
550
|
+
"securityTier": 0|1|2|3|4|5|6 // WHO GUARDS THIS PLACE. Pick from the SITE, not the payout,
|
|
551
|
+
// and use EXACTLY one of these integers:
|
|
552
|
+
// 0 street thugs, squatters, a mall cop
|
|
553
|
+
// 1 a gang's block, street scum holding a corner
|
|
554
|
+
// 2 corporate security at an ordinary office or warehouse
|
|
555
|
+
// 3 a police patrol, a licensed private force
|
|
556
|
+
// 4 an organized-crime crew on its own ground
|
|
557
|
+
// 5 elite corporate security -- a real corp facility
|
|
558
|
+
// 6 elite special forces -- military; needs a real reason
|
|
559
|
+
// Every "hostile" npc is armed and armored by the engine
|
|
560
|
+
// from this tier's statblock; you write WHO they are, the
|
|
561
|
+
// engine writes WHAT they carry.
|
|
482
562
|
}
|
|
483
563
|
|
|
484
564
|
CLIENT: every run has a CLIENT -- "a Mr. Johnson", the employer's deniable face, the person
|
|
@@ -646,17 +726,22 @@ For EVERY npc in the skeleton, expand its "concept":
|
|
|
646
726
|
will refuse it and the player will burn a run trusting it. An NPC may say that a code
|
|
647
727
|
EXISTS, who holds it, or where it's written down -- never what it is. The engine hands
|
|
648
728
|
the real pass to whoever genuinely holds it, at runtime (see npc.ts).
|
|
649
|
-
- "combat": OPTIONAL attribute block -- omit for anyone who'd never fight
|
|
650
|
-
|
|
729
|
+
- "combat": OPTIONAL attribute block -- omit for anyone who'd never fight, and OMIT IT FOR
|
|
730
|
+
EVERY HOSTILE: the engine outfits opposition from the site's security tier (the book's
|
|
731
|
+
own Professional Rating statblocks, gear included), and a "combat" block on a hostile is
|
|
732
|
+
ignored. Write one only for a NON-hostile who might still end up in a fight (a bodyguard
|
|
733
|
+
who is not security, a bartender with a shotgun). Each attribute 1-8 (soft civilian 1-2,
|
|
734
|
+
street-level 3-4, hardened enforcer 5-6, elite 7-8):
|
|
651
735
|
{ "body", "agility", "reaction", "combatSkill", "strength"?, "willpower"?, "logic"?,
|
|
652
736
|
"intuition"?, "charisma"?, "magic"?, "resonance"?, "adept"?,
|
|
653
|
-
"skills"?: { "
|
|
654
|
-
"
|
|
655
|
-
"
|
|
656
|
-
"
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
737
|
+
"skills"?: { "pistols"?, "automatics"?, "longarms"?, "blades"?, "clubs"?, "unarmed"?, "sneaking"?,
|
|
738
|
+
"perception"?, "palming"?, "hacking"?, "cybercombat"?, "electronic-warfare"?, "hardware"?,
|
|
739
|
+
"locksmith"?, "computer"?, "negotiation"?, "con"?, "intimidation"?, "etiquette"?,
|
|
740
|
+
"leadership"?, "performance"?, "disguise"?, "medicine"?, "assensing"?, "counterspelling"?,
|
|
741
|
+
"conjuring"?, "gunnery"? } }
|
|
742
|
+
"magic" 3-6 ONLY for a genuinely Awakened concept. "skills" is optional flavor for defined
|
|
743
|
+
concepts (a sharpshooter's "longarms": 6). There is no generic gun skill: guns are "pistols",
|
|
744
|
+
"automatics" (SMGs, machine pistols, assault rifles) or "longarms" (rifles, shotguns).
|
|
660
745
|
|
|
661
746
|
${JSON_ONLY}
|
|
662
747
|
[ { "name": string, "description": string, "dialogue": string[], "combat"?: { ... } } ]
|
|
@@ -2605,9 +2690,11 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
2605
2690
|
// from the exit they hold, so moving one would leave `guarding: true`
|
|
2606
2691
|
// with nothing behind it. See factories/npc-spread.ts.
|
|
2607
2692
|
const spreadStations = spreadStationedNpcs(npcStation, new Map(rooms.map(r => [r.name, (r.spots ?? []).map(s => s.name)])), name => guardPost.has(name));
|
|
2693
|
+
const tierKit = skeleton.securityTier === undefined ? undefined : kitSlugForTier(skeleton.securityTier);
|
|
2608
2694
|
const npcs = skeleton.npcs.map(npc => {
|
|
2609
2695
|
const detail = npcDetail.get(npc.name);
|
|
2610
2696
|
const station = spreadStations.get(npc.name);
|
|
2697
|
+
const kit = npc.hostile === true && npc.name !== skeleton.client ? tierKit : undefined;
|
|
2611
2698
|
const roomSpots = (rooms.find(r => r.name === npc.startLocation)?.spots ?? []);
|
|
2612
2699
|
const startSpot = station && station.room === npc.startLocation && roomSpots.some(s => s.name === station.spot)
|
|
2613
2700
|
? station.spot
|
|
@@ -2630,8 +2717,18 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
2630
2717
|
// other would put the flag on every checkpoint clerk in the
|
|
2631
2718
|
// game, which is the failure this feature is built to avoid.
|
|
2632
2719
|
hostile: npc.hostile === true || undefined,
|
|
2720
|
+
// WHAT THEY CARRY (npc-kits.ts, catalog v21): every hostile is
|
|
2721
|
+
// outfitted from the site's security tier, deterministically --
|
|
2722
|
+
// the model never writes a kit slug, because a slug it invented
|
|
2723
|
+
// is a whole failure class (itemFromCatalog throws on one) and
|
|
2724
|
+
// the tier is the one number it was asked for. THE KIT IS THE
|
|
2725
|
+
// STATLINE: a `combat` block the model wrote for a guard is
|
|
2726
|
+
// dropped here, not merged, so a dead key cannot ride in beside
|
|
2727
|
+
// the book's numbers. Non-hostiles keep theirs.
|
|
2728
|
+
...(kit ? { kit } : {}),
|
|
2729
|
+
...(kit && skeleton.lieutenant === npc.name ? { lieutenant: true } : {}),
|
|
2633
2730
|
lifestyleChoices: [],
|
|
2634
|
-
combat: detail?.combat,
|
|
2731
|
+
combat: kit ? undefined : detail?.combat,
|
|
2635
2732
|
},
|
|
2636
2733
|
};
|
|
2637
2734
|
});
|
|
@@ -2772,6 +2869,7 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
2772
2869
|
completionMessage: skeleton.completionMessage,
|
|
2773
2870
|
},
|
|
2774
2871
|
client: skeleton.client,
|
|
2872
|
+
...(skeleton.securityTier !== undefined ? { securityTier: skeleton.securityTier } : {}),
|
|
2775
2873
|
};
|
|
2776
2874
|
}
|
|
2777
2875
|
/**
|
|
@@ -14,6 +14,7 @@ import { migrateLegacySeed } from './seed-migration.js';
|
|
|
14
14
|
import { Device } from '../models/device.js';
|
|
15
15
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
16
16
|
import { adoptSeededIce } from '../utilities/ic-actors.js';
|
|
17
|
+
import { NPC_EQUIP_CATEGORIES, outfitNpcBySlug } from '../npc-kits.js';
|
|
17
18
|
/**
|
|
18
19
|
* How many AI enrichment calls may be in flight at once.
|
|
19
20
|
*
|
|
@@ -512,27 +513,58 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
512
513
|
logger.error(`Scene "${json.name}": the door ${String(direction)} of ${room.name} was locked by "${wanted ?? '(nothing)'}", which no device holds and no item matches -- it ships unlocked rather than as a wall.`);
|
|
513
514
|
}
|
|
514
515
|
}
|
|
516
|
+
// Step 4.04: NPCs get their kit (npc-kits.ts, SR5 p.379-385, catalog
|
|
517
|
+
// v21). AFTER "heldBy" has landed (Step 4.5 above -- the numbering is
|
|
518
|
+
// historical, the source order is what runs) and BEFORE the equip
|
|
519
|
+
// step below, on purpose: an author who put a weapon in this NPC's
|
|
520
|
+
// hands with heldBy is the author speaking, and the kit yields its
|
|
521
|
+
// own weapon to it (respectCarried) while its armor still layers.
|
|
522
|
+
// The seed's `kit` is applied here rather than in npc-factory for
|
|
523
|
+
// the same reason startSpot/guarding/hostile are applied at Step 3
|
|
524
|
+
// instead of the constructor: the factory drops every field but four,
|
|
525
|
+
// and the ordering guarantee above only holds in THIS loop.
|
|
526
|
+
for (const npcJson of json.npcs ?? []) {
|
|
527
|
+
if (!npcJson.player.kit)
|
|
528
|
+
continue;
|
|
529
|
+
const npc = scene.getActor(npcJson.player.name);
|
|
530
|
+
if (!npc)
|
|
531
|
+
continue;
|
|
532
|
+
outfitNpcBySlug(npc, npcJson.player.kit, {
|
|
533
|
+
lieutenant: npcJson.player.lieutenant === true,
|
|
534
|
+
respectCarried: true,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
515
537
|
// Step 4.05: NPCs wear their gear. "heldBy" only puts items in an NPC's
|
|
516
538
|
// inventory, but combat pools (see Player.getWeaponProfile /
|
|
517
539
|
// getArmorValue) read from EQUIPPED slots -- without this, an enforcer
|
|
518
540
|
// "holding" an assault rifle and an armored jacket would fight
|
|
519
541
|
// bare-fisted in street clothes. Only weapon/armor categories are
|
|
520
|
-
// auto-equipped
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
542
|
+
// auto-equipped (NPC_EQUIP_CATEGORIES -- the one list, shared with the
|
|
543
|
+
// ephemeral governor): plot items (datachips, books, keys) stay in
|
|
544
|
+
// inventory where "give"/death-drops expect them. Failures are
|
|
545
|
+
// non-fatal -- a second weapon just stays holstered in inventory.
|
|
546
|
+
//
|
|
547
|
+
// FIRST WEAPON WINS (2026-09-15, the day kits gave every guard two).
|
|
548
|
+
// `equip` SWAPS, so the unguarded loop this used to be would have
|
|
549
|
+
// ended every PR 1 ganger holding the knife with the Browning
|
|
550
|
+
// holstered -- the exact bug archetypes.ts:outfitArchetype documents
|
|
551
|
+
// for runners. It never fired here only because no seeded NPC ever
|
|
552
|
+
// carried two weapons. Armor still layers, so only weapons are
|
|
553
|
+
// guarded.
|
|
554
|
+
const weaponCategories = new Set([Category.Weapon, Category.MeleeWeapon, Category.RangedWeapon]);
|
|
528
555
|
for (const npcJson of json.npcs ?? []) {
|
|
529
556
|
const npc = scene.getActor(npcJson.player.name);
|
|
530
557
|
if (!npc)
|
|
531
558
|
continue;
|
|
532
559
|
for (const item of [...npc.inventory.getAllItems()]) {
|
|
533
|
-
if (
|
|
560
|
+
if (!NPC_EQUIP_CATEGORIES.has(item.category))
|
|
561
|
+
continue;
|
|
562
|
+
if (weaponCategories.has(item.category) && npc.getCarriedWeapon())
|
|
563
|
+
continue;
|
|
564
|
+
try {
|
|
534
565
|
npc.equip(item);
|
|
535
566
|
}
|
|
567
|
+
catch { /* nothing to slot it into -- stays holstered */ }
|
|
536
568
|
}
|
|
537
569
|
}
|
|
538
570
|
// Step 4.1: Add Starting Items for the Player (skipped when continuing
|
|
@@ -9,7 +9,7 @@ import { GenerationCapture } from '../utilities/generation-capture.js';
|
|
|
9
9
|
import { fetchCanonContext } from '../utilities/canon-lore.js';
|
|
10
10
|
import { providerUnavailable } from '../utilities/draft-failure-note.js';
|
|
11
11
|
import { SceneSynthesizer } from './scene-factory.js';
|
|
12
|
-
import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, CHUNK_SCHEMAS, unwrapChunk, VAULT_HOST_RATING, } from './scene-chunks.js';
|
|
12
|
+
import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, settleSecurity, assembleScene, classifyRepair, CHUNK_SCHEMAS, unwrapChunk, VAULT_HOST_RATING, } from './scene-chunks.js';
|
|
13
13
|
// The fixer name convention and the player dossier live with the prompt
|
|
14
14
|
// builders now (see scene-chunks.ts); re-exported so the existing
|
|
15
15
|
// import sites (call.ts, game.ts) keep working unchanged.
|
|
@@ -241,6 +241,15 @@ export class SceneSeedGenerator {
|
|
|
241
241
|
// for a scaled opposition, and this is what makes the aim
|
|
242
242
|
// stick when the model quietly ignores it.
|
|
243
243
|
validateSkeleton(skeleton, player, crew);
|
|
244
|
+
// WHO GUARDS THIS PLACE (npc-kits.ts): clamp the declared tier
|
|
245
|
+
// against the job number and post the lieutenant if this table
|
|
246
|
+
// outguns it. After validation, so a rejected skeleton never
|
|
247
|
+
// has its security settled; before the detail passes, so the
|
|
248
|
+
// NPC prompt can be told which entries the kit will dress.
|
|
249
|
+
for (const note of settleSecurity(skeleton, tier, player, crew)) {
|
|
250
|
+
notes.push(note);
|
|
251
|
+
logger.write(`SceneSeedGenerator: ${note}`);
|
|
252
|
+
}
|
|
244
253
|
GenerationCapture.record({
|
|
245
254
|
stage: 'skeleton', outcome: 'accepted', pipeline, attempt,
|
|
246
255
|
prompt, reply, notes,
|
|
@@ -1759,7 +1759,11 @@ export default class Game {
|
|
|
1759
1759
|
name: `${item.name} (autopilot)`,
|
|
1760
1760
|
description: `${this.player.name}'s drone running its dog-brain autopilot: it follows them, watches, and ${armed ? 'shoots what threatens them' : 'has cameras where guns would go -- it never attacks'}.`,
|
|
1761
1761
|
kind: 'drone',
|
|
1762
|
-
|
|
1762
|
+
// `firearms` is not a skill (skills.ts, stage 3) and this shell
|
|
1763
|
+
// carried it as a dead key until 2026-09-15: an armed drone rolls
|
|
1764
|
+
// Gunnery (p.131, and what getAttackPool reads for a jumped-in
|
|
1765
|
+
// frame) and, for a mounted gun's own skill, the split three.
|
|
1766
|
+
combat: { body: item.droneBody, agility: pilot, reaction: pilot, logic: pilot, intuition: pilot, willpower: pilot, strength: 1, charisma: 1, skills: { perception: pilot, ...(armed ? { gunnery: pilot, automatics: pilot, pistols: pilot, longarms: pilot } : {}) } },
|
|
1763
1767
|
room, plane: 'meat', force: pilot, boundDevice: item,
|
|
1764
1768
|
});
|
|
1765
1769
|
// Take-command weight checks (Player.addInventory) enforce this cap
|
|
@@ -49,6 +49,21 @@ export class Item extends AbstractItem {
|
|
|
49
49
|
// work (rest.ts). A reboot does NOT clear it; that is the whole
|
|
50
50
|
// difference from `jammed`.
|
|
51
51
|
bricked = false;
|
|
52
|
+
/**
|
|
53
|
+
* RUINED (Room.layOutBody, catalog v21 NPC kits): armor that soaked
|
|
54
|
+
* the rounds that killed its wearer. Rated 0 (armorValue, armorBonus)
|
|
55
|
+
* and worth nothing (commerce.ts priceOf); it drops, so the runner
|
|
56
|
+
* sees what the guard was wearing, and it is not a free set of full
|
|
57
|
+
* body armor -- the plates are what stopped the rounds. Saved.
|
|
58
|
+
*/
|
|
59
|
+
ruined = false;
|
|
60
|
+
/** Mark this piece ruined, and say so on it. Idempotent. */
|
|
61
|
+
ruin(how) {
|
|
62
|
+
if (this.ruined)
|
|
63
|
+
return;
|
|
64
|
+
this.ruined = true;
|
|
65
|
+
this.description = `${this.description} RUINED -- ${how}.`;
|
|
66
|
+
}
|
|
52
67
|
/**
|
|
53
68
|
* SPRITE POWERS ON A FILE (SR5 p.256-257,
|
|
54
69
|
* utilities/sprite-power-actions.ts). Matrix-plane items only; all
|
|
@@ -581,6 +596,8 @@ export class Item extends AbstractItem {
|
|
|
581
596
|
* clothing 0. Only the highest worn piece counts (Player.armorBreakdown).
|
|
582
597
|
*/
|
|
583
598
|
get armorValue() {
|
|
599
|
+
if (this.ruined)
|
|
600
|
+
return 0;
|
|
584
601
|
const w = this.row?.wearable;
|
|
585
602
|
if (w?.armor !== undefined)
|
|
586
603
|
return w.armor;
|
|
@@ -598,6 +615,8 @@ export class Item extends AbstractItem {
|
|
|
598
615
|
}
|
|
599
616
|
/** A "+" accessory's bonus (helmet +2, shield +6, p.437-438): the row's, else the class value. */
|
|
600
617
|
get armorBonus() {
|
|
618
|
+
if (this.ruined)
|
|
619
|
+
return 0;
|
|
601
620
|
const w = this.row?.wearable;
|
|
602
621
|
if (w?.armorBonus !== undefined)
|
|
603
622
|
return w.armorBonus;
|
|
@@ -705,6 +705,18 @@ export class NPC extends Player {
|
|
|
705
705
|
* "nobody in particular", which every rule reads as no opinion.
|
|
706
706
|
*/
|
|
707
707
|
faction;
|
|
708
|
+
/**
|
|
709
|
+
* WHAT THIS GRUNT CARRIES AND HOW TRAINED THEY ARE (npc-kits.ts, SR5
|
|
710
|
+
* p.379-385): the kit row's slug, its Professional Rating, and
|
|
711
|
+
* whether this one is the team's lieutenant (p.380-381). Set by
|
|
712
|
+
* outfitNpcKit and read by the Combat Turn for Group Edge and morale.
|
|
713
|
+
* Absent on every NPC built from an attribute block alone, which is
|
|
714
|
+
* every NPC there was before catalog v21. Session state, never saved:
|
|
715
|
+
* a seeded NPC is re-outfitted from its seed on every synthesis.
|
|
716
|
+
*/
|
|
717
|
+
kit;
|
|
718
|
+
professionalRating;
|
|
719
|
+
lieutenant;
|
|
708
720
|
/**
|
|
709
721
|
* SET ONLY BY THE EPHEMERAL GOVERNOR (utilities/ephemeral.ts): this
|
|
710
722
|
* body is a passing face, not a character. Its presence trims the
|
|
@@ -630,6 +630,14 @@ export class Player extends AbstractPlayer {
|
|
|
630
630
|
* long as the sprite.
|
|
631
631
|
*/
|
|
632
632
|
cookiedBy = new Map();
|
|
633
|
+
/**
|
|
634
|
+
* A SPRITE'S FOUR MATRIX ATTRIBUTES (SR5 p.254, p.259), stamped on the
|
|
635
|
+
* shell by commands/compile.ts. A sprite carries no deck, so without
|
|
636
|
+
* this every bracketed sprite roll computed a limit of 0 -- which
|
|
637
|
+
* rollPool rightly refuses as "not a limit" rather than honouring.
|
|
638
|
+
* Only Resonance equals Level; the four attributes are per TYPE.
|
|
639
|
+
*/
|
|
640
|
+
spriteMatrixAttrs;
|
|
633
641
|
/**
|
|
634
642
|
* DIAGNOSTICS (p.257): the Teamwork bonus a machine sprite is holding
|
|
635
643
|
* on ONE device -- "it takes the sprite's entire attention", so there
|
|
@@ -907,6 +915,16 @@ export class Player extends AbstractPlayer {
|
|
|
907
915
|
baseMatrixAttribute(kind) {
|
|
908
916
|
if (this.icHost)
|
|
909
917
|
return this.icHost.attributes()[kind];
|
|
918
|
+
// A SPRITE'S ARRAY IS ITS OWN (SR5 p.254, p.259). It has no deck, so
|
|
919
|
+
// the rig lookup below returned 0 -- and a 0 reaching rollPool is
|
|
920
|
+
// refused as "not a limit", which is exactly what a repro bench
|
|
921
|
+
// caught the day an ordered sprite's dice first became visible
|
|
922
|
+
// (wjFBY3zgyWzJP9adw): the roll happened, unlimited, and said so.
|
|
923
|
+
// Set on the shell at compile time from sprite-powers.spriteMatrix,
|
|
924
|
+
// which carries the book's per-type table; only Resonance equals
|
|
925
|
+
// Level, the four attributes do not.
|
|
926
|
+
if (this.spriteMatrixAttrs)
|
|
927
|
+
return this.spriteMatrixAttrs[kind];
|
|
910
928
|
if (this.isTechnomancer()) {
|
|
911
929
|
switch (kind) {
|
|
912
930
|
case 'attack': return this.charisma;
|
|
@@ -124,9 +124,18 @@ export class Room extends AbstractRoom {
|
|
|
124
124
|
for (const [slot, item] of Object.entries(group)) {
|
|
125
125
|
if (!item)
|
|
126
126
|
continue;
|
|
127
|
-
|
|
127
|
+
const piece = item;
|
|
128
|
+
// WORN ARMOR COMES OFF RUINED (2026-09-15, with the NPC kits
|
|
129
|
+
// that put real armor on guards): the jacket stopped the rounds
|
|
130
|
+
// that killed the wearer, and that is what a jacket is for.
|
|
131
|
+
// Guns and commlinks drop intact -- a PR 5 kill is still the
|
|
132
|
+
// Ares Alpha, it is just not a free set of full body armor.
|
|
133
|
+
if (piece.armorValue > 0 || piece.armorBonus > 0) {
|
|
134
|
+
piece.ruin(`it took the rounds that dropped ${actor.name}`);
|
|
135
|
+
}
|
|
136
|
+
held.addItem(piece);
|
|
128
137
|
group[slot] = null;
|
|
129
|
-
this.logger.write(`${actor.name}'s ${slot} falls with the body in ${this.name}.`);
|
|
138
|
+
this.logger.write(`${actor.name}'s ${slot} falls with the body in ${this.name}${piece.ruined ? ' (ruined)' : ''}.`);
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
141
|
}
|