@maka/maka-cli 5.200.0 → 5.202.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/ask.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +165 -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 +95 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +38 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -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/sprite-powers.js +48 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -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/drone-prose.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +17 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +25 -13
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -12,6 +12,9 @@ import { hostInteriorPanel } from './utilities/grid-view.js';
|
|
|
12
12
|
import { DEFAULT_GRID_PROVIDER, PUBLIC_GRID, globalGrid, looseEndGrid } from './utilities/grids.js';
|
|
13
13
|
import { buildCleanupSeed } from './factories/cleanup-seed.js';
|
|
14
14
|
import { EditFileCommand, EraseCommand } from './commands/edit-file.js';
|
|
15
|
+
import { EraseMarkCommand } from './commands/erase-mark.js';
|
|
16
|
+
import { SPRITE_POWER_COMMANDS } from './commands/sprite-power.js';
|
|
17
|
+
import { cleanupOutcome } from './utilities/cleanup-errand.js';
|
|
15
18
|
import { hostsInReach, hostDockLine, gridVicinity } from './utilities/grid-reach.js';
|
|
16
19
|
import { nameThem, scrubHandles } from './utilities/identity.js';
|
|
17
20
|
import { dirname } from 'path';
|
|
@@ -590,6 +593,19 @@ export default class Game {
|
|
|
590
593
|
// `?? []`: hosted-homecoming.test.ts drives a Game off its prototype,
|
|
591
594
|
// where field initialisers never ran.
|
|
592
595
|
for (const le of this._looseEnds ?? []) {
|
|
596
|
+
// A HIRED SCRUB RESOLVES BEFORE THE DESK DOES (cleanup-errand.ts).
|
|
597
|
+
// You paid at the hub and did not get to watch; this is where the
|
|
598
|
+
// decker reports back, and it happens whatever grace is left --
|
|
599
|
+
// that is what the nuyen bought.
|
|
600
|
+
if (le.hired) {
|
|
601
|
+
const done = this.settleHiredCleanup(le);
|
|
602
|
+
lines.push(...done.lines);
|
|
603
|
+
// A botched scrub leaves the footage exactly where it was, and
|
|
604
|
+
// the grace clock keeps running -- you bought an attempt.
|
|
605
|
+
if (done.keep)
|
|
606
|
+
kept.push(done.record);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
593
609
|
const left = le.homecomingsLeft - 1;
|
|
594
610
|
if (left > 0) {
|
|
595
611
|
kept.push({ ...le, homecomingsLeft: left });
|
|
@@ -607,6 +623,60 @@ export default class Game {
|
|
|
607
623
|
}
|
|
608
624
|
return lines;
|
|
609
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* A HIRED SCRUB REPORTS BACK (utilities/cleanup-errand.ts). The
|
|
628
|
+
* decker's test is rolled HERE, not at hire time: you paid for a
|
|
629
|
+
* result you do not get to watch, and the delay is the point.
|
|
630
|
+
*
|
|
631
|
+
* The pool was banked on the record at hire (the fixer's Connection +
|
|
632
|
+
* Loyalty) so a loyalty shift in between cannot retroactively change a
|
|
633
|
+
* job already bought. It rolls against the host's rating -- the same
|
|
634
|
+
* ice the runner would have had to get through.
|
|
635
|
+
*/
|
|
636
|
+
settleHiredCleanup(le) {
|
|
637
|
+
const hired = le.hired;
|
|
638
|
+
const mine = rollPool(Math.max(1, hired.pool));
|
|
639
|
+
const host = rollPool(Math.max(1, le.host.rating));
|
|
640
|
+
const outcome = cleanupOutcome(mine.hits - host.hits, mine.criticalGlitch);
|
|
641
|
+
Logger.getInstance().write(`Cleanup errand "${le.id}": ${hired.decker} via ${hired.fixer}, ${mine.hits} v ${host.hits} -> ${outcome}.`);
|
|
642
|
+
Logger.getInstance().meta(`Cleanup errand -- ${hired.decker} (${hired.fixer}'s reach): ${formatRoll(mine)} v. ${le.site}'s host: ${formatRoll(host)}`);
|
|
643
|
+
if (outcome === 'botched') {
|
|
644
|
+
// The clock keeps running, and the hire is spent: the record goes
|
|
645
|
+
// back to being yours to deal with.
|
|
646
|
+
const left = le.homecomingsLeft - 1;
|
|
647
|
+
const stillOpen = left > 0;
|
|
648
|
+
if (!stillOpen) {
|
|
649
|
+
const stained = this.player.addPublicAwareness(`caught on the cameras at "${le.site}" during "${le.jobName}"`);
|
|
650
|
+
return {
|
|
651
|
+
lines: [`\n{red-fg}${hired.fixer} calls, short: "${hired.decker} got burned off ${le.site}'s host. Didn't get it." And then the desk reviews the feeds anyway.{/red-fg}${stained ? ` (+1 Public Awareness, now ${this.player.publicAwareness})` : ''}`],
|
|
652
|
+
keep: false,
|
|
653
|
+
record: le,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
lines: [`\n{yellow-fg}${hired.fixer} calls, short: "${hired.decker} got burned off ${le.site}'s host. Didn't get it -- and no, you don't get the nuyen back."{/yellow-fg}${hint(` (The footage is still up. Hire again, or "jack in" and "hop ${le.site}" yourself.)`)}`],
|
|
658
|
+
keep: true,
|
|
659
|
+
record: { ...le, homecomingsLeft: left, hired: undefined },
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const leverage = outcome === 'leverage';
|
|
663
|
+
return {
|
|
664
|
+
lines: [`\n{light-blue-fg}${hired.fixer} pings you two days on: "${le.site}'s feeds from '${le.jobName}' are a loop of an empty hallway. You were never there."{/light-blue-fg}${leverage ? `\n{yellow-fg}A beat later, a second message from a number you don't know: a single still of your face, and nothing else. ${hired.decker} kept a copy.{/yellow-fg}` : ''}`],
|
|
665
|
+
keep: false,
|
|
666
|
+
record: le,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* HIRE A SHADOW DECKER for an open loose end (commands/crew.ts,
|
|
671
|
+
* "hire cleanup"). Debits the nuyen and stamps the record; the job
|
|
672
|
+
* itself resolves at the next homecoming (settleHiredCleanup).
|
|
673
|
+
*/
|
|
674
|
+
hireCleanup(le, hired) {
|
|
675
|
+
const idx = this._looseEnds.findIndex(x => x.id === le.id);
|
|
676
|
+
if (idx >= 0)
|
|
677
|
+
this._looseEnds[idx] = { ...this._looseEnds[idx], hired };
|
|
678
|
+
Logger.getInstance().write(`Cleanup hired: ${hired.decker} via ${hired.fixer} for "${le.id}" at ${hired.price} nuyen.`);
|
|
679
|
+
}
|
|
610
680
|
/** The grids "hop" lists for the open loose ends -- from the hub only. */
|
|
611
681
|
looseEndGrids() {
|
|
612
682
|
if (!this.hasHub || this.scene !== this._hubScene)
|
|
@@ -1689,7 +1759,11 @@ export default class Game {
|
|
|
1689
1759
|
name: `${item.name} (autopilot)`,
|
|
1690
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'}.`,
|
|
1691
1761
|
kind: 'drone',
|
|
1692
|
-
|
|
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 } : {}) } },
|
|
1693
1767
|
room, plane: 'meat', force: pilot, boundDevice: item,
|
|
1694
1768
|
});
|
|
1695
1769
|
// Take-command weight checks (Player.addInventory) enforce this cap
|
|
@@ -3001,6 +3075,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3001
3075
|
CommandFactory.registerCommand('wipe', EraseCommand);
|
|
3002
3076
|
// MAtrix Recognition Keys (canon p.235-236).
|
|
3003
3077
|
CommandFactory.registerCommand('mark', MarkCommand);
|
|
3078
|
+
CommandFactory.registerCommand('unmark', EraseMarkCommand);
|
|
3079
|
+
// SPRITE POWERS (p.256-257): one verb per power, reached as
|
|
3080
|
+
// "order <sprite> <power> <target>" -- see commands/sprite-power.ts.
|
|
3081
|
+
for (const [key, cls] of Object.entries(SPRITE_POWER_COMMANDS)) {
|
|
3082
|
+
CommandFactory.registerCommand(key, cls);
|
|
3083
|
+
}
|
|
3004
3084
|
// Camera feeds you own (surveillance).
|
|
3005
3085
|
CommandFactory.registerCommand('snoop', SnoopCommand);
|
|
3006
3086
|
// The cable into a slaved camera (canon p.232/233) -- "splice" is
|
|
@@ -3383,8 +3463,20 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3383
3463
|
const where = this._resumedAt && this._resumedAt !== this._homeRoom
|
|
3384
3464
|
? `You're back at the ${this._resumedAt.name}${this.player.atSpot ? `, by the ${this.player.atSpot}` : ''} -- right where you left off.`
|
|
3385
3465
|
: 'The cot remembers your shape.';
|
|
3466
|
+
// THE NAG HAS TO NAME A ROUTE THIS RUNNER CAN TAKE (user
|
|
3467
|
+
// 2026-09-15). "jack in, then hop" is closed to anyone without a
|
|
3468
|
+
// deck and a direct neural interface -- which is most runners --
|
|
3469
|
+
// so a street samurai was told about a consequence and handed no
|
|
3470
|
+
// lever. The errand (utilities/cleanup-errand.ts) is that lever,
|
|
3471
|
+
// and it is the advice a non-decker gets.
|
|
3472
|
+
const hiredNote = this._looseEnds.some(le => le.hired)
|
|
3473
|
+
? ` Somebody is already on one of them.`
|
|
3474
|
+
: '';
|
|
3475
|
+
const route = this.player.canReachMatrix()
|
|
3476
|
+
? `"jack in", then "hop" -- the site's grid is listed while the footage is still up there. Or "ask <contact> about cleanup" to buy the scrub instead.`
|
|
3477
|
+
: `"ask <contact> about cleanup" -- you can't ride the grid, but somebody you know knows somebody who can.`;
|
|
3386
3478
|
const looseNote = this._looseEnds.length > 0
|
|
3387
|
-
? `\n\n{yellow-fg}${this._looseEnds.map(le => `${le.site} still has your face from "${le.jobName}".`).join(' ')}{/yellow-fg}${hint(` (
|
|
3479
|
+
? `\n\n{yellow-fg}${this._looseEnds.map(le => `${le.site} still has your face from "${le.jobName}".`).join(' ')}${hiredNote}{/yellow-fg}${hint(` (${route})`)}`
|
|
3388
3480
|
: '';
|
|
3389
3481
|
return `Welcome back, ${this.player.name}. ${where}\n ${this.scene.story}${pendingNote}${looseNote}`;
|
|
3390
3482
|
}
|
|
@@ -6647,7 +6739,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6647
6739
|
// players" / "crew street"); the bare verb still answers quietly.
|
|
6648
6740
|
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
|
|
6649
6741
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
|
|
6650
|
-
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
6742
|
+
{ title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
6651
6743
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
6652
6744
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
6653
6745
|
// `sustain` shelves here and not under magic: the command is
|
|
@@ -77,6 +77,14 @@ export class Host {
|
|
|
77
77
|
/** Kinds crashed this turn; the host relaunches them next turn (p.355-356). */
|
|
78
78
|
relaunchQueue = [];
|
|
79
79
|
patrolCountdown;
|
|
80
|
+
/**
|
|
81
|
+
* SUPPRESSION (SR5 p.257, utilities/sprite-power-actions.ts): a sprite
|
|
82
|
+
* inside the host is arguing with its launch paths, so IC it brings up
|
|
83
|
+
* arrives (Level / 2) Combat Turns late -- and "delayed IC can't act or
|
|
84
|
+
* be targeted" while it waits. Counted down by hostTurnStart. Session
|
|
85
|
+
* state, like the rest of the fight.
|
|
86
|
+
*/
|
|
87
|
+
icLaunchDelay = 0;
|
|
80
88
|
/** A haven (jackpoint.ts): never hunts, shows no rating, forgets cracks. */
|
|
81
89
|
sanctioned = false;
|
|
82
90
|
/**
|
|
@@ -49,6 +49,40 @@ 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
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* SPRITE POWERS ON A FILE (SR5 p.256-257,
|
|
69
|
+
* utilities/sprite-power-actions.ts). Matrix-plane items only; all
|
|
70
|
+
* three are session state, like the marks and sightings they sit
|
|
71
|
+
* beside -- a sprite's work lasts as long as the sprite does.
|
|
72
|
+
*
|
|
73
|
+
* CAMOUFLAGE: folded inside another file, so a Matrix Search walks
|
|
74
|
+
* past it (commands/search.ts) and only a Matrix Perception hunting
|
|
75
|
+
* this file turns it up -- the sprite included.
|
|
76
|
+
*/
|
|
77
|
+
concealedBy;
|
|
78
|
+
/** HASH: sealed by a Resonance algorithm only this sprite can undo, for
|
|
79
|
+
* `hashTurns` Combat Turns. Nothing else edits or deletes it while it
|
|
80
|
+
* holds (commands/edit-file.ts). */
|
|
81
|
+
hashedBy;
|
|
82
|
+
hashTurns = 0;
|
|
83
|
+
/** WATERMARK: an invisible tag only Resonance-driven things read -- a
|
|
84
|
+
* technomancer or another sprite. A new one overwrites the old. */
|
|
85
|
+
watermark;
|
|
52
86
|
_transferable;
|
|
53
87
|
// New currency-related fields
|
|
54
88
|
_currencyAmount = 0;
|
|
@@ -562,6 +596,8 @@ export class Item extends AbstractItem {
|
|
|
562
596
|
* clothing 0. Only the highest worn piece counts (Player.armorBreakdown).
|
|
563
597
|
*/
|
|
564
598
|
get armorValue() {
|
|
599
|
+
if (this.ruined)
|
|
600
|
+
return 0;
|
|
565
601
|
const w = this.row?.wearable;
|
|
566
602
|
if (w?.armor !== undefined)
|
|
567
603
|
return w.armor;
|
|
@@ -579,6 +615,8 @@ export class Item extends AbstractItem {
|
|
|
579
615
|
}
|
|
580
616
|
/** A "+" accessory's bonus (helmet +2, shield +6, p.437-438): the row's, else the class value. */
|
|
581
617
|
get armorBonus() {
|
|
618
|
+
if (this.ruined)
|
|
619
|
+
return 0;
|
|
582
620
|
const w = this.row?.wearable;
|
|
583
621
|
if (w?.armorBonus !== undefined)
|
|
584
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
|
|
@@ -735,6 +747,14 @@ export class NPC extends Player {
|
|
|
735
747
|
}
|
|
736
748
|
boundDevice;
|
|
737
749
|
companionKind;
|
|
750
|
+
/**
|
|
751
|
+
* ELECTRON STORM (SR5 p.257, utilities/sprite-power-actions.ts): the
|
|
752
|
+
* persona this sprite is holding a storm open on. It burns again every
|
|
753
|
+
* action the sprite spends sustaining it, and "if the sprite takes any
|
|
754
|
+
* Matrix damage, all of its electron storms end immediately" -- which
|
|
755
|
+
* is why this is cleared on damage rather than on a timer.
|
|
756
|
+
*/
|
|
757
|
+
sustainedStorm;
|
|
738
758
|
/**
|
|
739
759
|
* Runs one command AS this NPC through its own scene-bound registry --
|
|
740
760
|
* the deterministic control surface for "order <companion> <command>"
|
|
@@ -621,6 +621,21 @@ export class Player extends AbstractPlayer {
|
|
|
621
621
|
// against me (hack.ts). My "reboot" wipes them all; a placer's own
|
|
622
622
|
// reboot/jack-out/convergence wipes just theirs (utilities/marks.ts).
|
|
623
623
|
panMarksBy = new Map();
|
|
624
|
+
/**
|
|
625
|
+
* COOKIE (SR5 p.256-257, utilities/sprite-power-actions.ts): sprite
|
|
626
|
+
* name -> net hits, which is the DEPTH the report carries when it ships
|
|
627
|
+
* home (one hit a bare outline, four or more a detailed report). The
|
|
628
|
+
* carrier does not know it is there; a Matrix Perception on them finds
|
|
629
|
+
* it. Session state, like the marks above -- a sprite's work lasts as
|
|
630
|
+
* long as the sprite.
|
|
631
|
+
*/
|
|
632
|
+
cookiedBy = new Map();
|
|
633
|
+
/**
|
|
634
|
+
* DIAGNOSTICS (p.257): the Teamwork bonus a machine sprite is holding
|
|
635
|
+
* on ONE device -- "it takes the sprite's entire attention", so there
|
|
636
|
+
* is at most one, and giving that sprite any other order drops it.
|
|
637
|
+
*/
|
|
638
|
+
spriteAssist;
|
|
624
639
|
// OVERWATCH SCORE (GOD, canon p.231-232, see utilities/overwatch.ts):
|
|
625
640
|
// evidence tally of this actor's illegal Matrix actions. Starts at 0
|
|
626
641
|
// on a fresh boot, grows with every Attack/Sleaze action (and a
|
|
@@ -704,6 +719,27 @@ export class Player extends AbstractPlayer {
|
|
|
704
719
|
return true;
|
|
705
720
|
return this.dniSource() !== null;
|
|
706
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* COULD THIS RUNNER RIDE THE GRID AT ALL -- the question commands/jack.ts
|
|
724
|
+
* answers in three separate refusals, asked once so ADVICE can be
|
|
725
|
+
* written for the right audience.
|
|
726
|
+
*
|
|
727
|
+
* Added for the loose-end nag (user 2026-09-15): the hub told every
|
|
728
|
+
* runner to "jack in, then hop" and erase the footage, including a
|
|
729
|
+
* street samurai with no deck and no datajack, who therefore got a
|
|
730
|
+
* consequence with no lever. A line that recommends a route has to
|
|
731
|
+
* know whether the reader can take it.
|
|
732
|
+
*
|
|
733
|
+
* Deliberately NOT the same as "may jack in right now" -- a bricked
|
|
734
|
+
* deck is a repair, not a disqualification, and this is about what the
|
|
735
|
+
* character can ever do rather than what they can do this minute.
|
|
736
|
+
*/
|
|
737
|
+
canReachMatrix() {
|
|
738
|
+
if (this.isTechnomancer())
|
|
739
|
+
return true;
|
|
740
|
+
const deck = this.getCyberdeck() ?? this.getAnyCyberdeck();
|
|
741
|
+
return deck !== undefined && this.hasDirectNeuralInterface();
|
|
742
|
+
}
|
|
707
743
|
/** Failed reads of a clue, by item name, for p.49's Trying Again
|
|
708
744
|
* (-2 per retry). In memory only: a save and reload is the "break"
|
|
709
745
|
* the rule says clears it. See utilities/clue-reading.ts. */
|
|
@@ -1173,6 +1209,14 @@ export class Player extends AbstractPlayer {
|
|
|
1173
1209
|
}
|
|
1174
1210
|
/** Routes Matrix combat damage into the jacked-in deck's hardware track. */
|
|
1175
1211
|
takeMatrixDamage(boxes) {
|
|
1212
|
+
// ELECTRON STORM ENDS WHEN ITS SPRITE IS HURT (SR5 p.257: "if the
|
|
1213
|
+
// sprite takes any Matrix damage, all of its electron storms end
|
|
1214
|
+
// immediately"). A sprite is an NPC, which is a Player, so the rule
|
|
1215
|
+
// lives on the one method every Matrix hit goes through rather than
|
|
1216
|
+
// in each thing that can deal one.
|
|
1217
|
+
const self = this;
|
|
1218
|
+
if (boxes > 0 && self.sustainedStorm)
|
|
1219
|
+
self.sustainedStorm = undefined;
|
|
1176
1220
|
const deck = this.getAnyCyberdeck();
|
|
1177
1221
|
const taken = deck ? deck.takeDeckDamage(boxes) : 0;
|
|
1178
1222
|
// DECK DAMAGE BEATS (web report 2026-09-02: "the damage to the deck is not moving in realtime"):
|
|
@@ -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
|
}
|
|
@@ -628,7 +628,23 @@ export class Scene extends AbstractScene {
|
|
|
628
628
|
this.logger.write(`Actor with name ${actor.name} has been added to the scene.`);
|
|
629
629
|
this.actors.set(actor.name, actor);
|
|
630
630
|
}
|
|
631
|
-
|
|
631
|
+
/**
|
|
632
|
+
* Take an actor out of the world.
|
|
633
|
+
*
|
|
634
|
+
* `spill` (default true) is whether what they carried lands on the
|
|
635
|
+
* floor. TRUE IS A BODY: a kill has already been stripped by
|
|
636
|
+
* Room.layOutBody, and anything left is the leftover a corpse drops.
|
|
637
|
+
* FALSE IS A DEPARTURE (utilities/ephemeral.ts despawnEphemerals): a
|
|
638
|
+
* ganger who walks off their corner, an officer whose sweep moved on,
|
|
639
|
+
* takes their gun with them. Before kits (catalog v21) ephemerals
|
|
640
|
+
* carried nothing so the distinction cost nothing; the day they got
|
|
641
|
+
* a Browning and an armor vest, walking away from a street corner
|
|
642
|
+
* rained both onto the pavement, per ganger, per block -- and the
|
|
643
|
+
* governor's founding denial is that an ephemeral cannot create
|
|
644
|
+
* obligations. A gun the world has to keep track of after the body
|
|
645
|
+
* is gone is exactly that.
|
|
646
|
+
*/
|
|
647
|
+
removeActor(name, opts = {}) {
|
|
632
648
|
const actor = this.actors.get(name);
|
|
633
649
|
// Removal is cleanup, not an assertion: a concurrent kill resolution
|
|
634
650
|
// (or any double-remove) finding the actor already gone is a no-op,
|
|
@@ -639,6 +655,10 @@ export class Scene extends AbstractScene {
|
|
|
639
655
|
return;
|
|
640
656
|
}
|
|
641
657
|
const room = actor.currentLocation;
|
|
658
|
+
const spill = opts.spill !== false;
|
|
659
|
+
if (!spill && room) {
|
|
660
|
+
this.logger.write(`removeActor: ${actor.name} leaves ${room.name} with everything they carried.`);
|
|
661
|
+
}
|
|
642
662
|
// One consolidated observer line for the whole spill, not one stilted
|
|
643
663
|
// "on death drops" line per item -- and everything that falls is IN
|
|
644
664
|
// PLAIN SIGHT (see Room.plainSightItems): the player just watched it
|
|
@@ -649,7 +669,7 @@ export class Scene extends AbstractScene {
|
|
|
649
669
|
// "counter" that didn't exist in the room (the note had ridden a
|
|
650
670
|
// crew member's pack from a different scene entirely).
|
|
651
671
|
const fallSpot = actor.atSpot;
|
|
652
|
-
if (room && actor.inventory) {
|
|
672
|
+
if (spill && room && actor.inventory) {
|
|
653
673
|
const items = actor.inventory.getAllItems();
|
|
654
674
|
for (const item of items) {
|
|
655
675
|
const didDrop = actor.inventory.transferItemTo(item.name, room.inventory);
|
|
@@ -666,7 +686,7 @@ export class Scene extends AbstractScene {
|
|
|
666
686
|
// holstered pistol are exactly the loot a runner expects to strip.
|
|
667
687
|
// Without this, everything an NPC had equipped would silently vanish
|
|
668
688
|
// from the world on death.
|
|
669
|
-
if (room && actor.equipment) {
|
|
689
|
+
if (spill && room && actor.equipment) {
|
|
670
690
|
for (const group of Object.values(actor.equipment)) {
|
|
671
691
|
if (!group)
|
|
672
692
|
continue;
|