@maka/maka-cli 5.180.0 → 5.182.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/attack.js +17 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +8 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +69 -23
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +6 -4
- package/bundle/typescript/src/commands/game/sideQuest/game.js +47 -39
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +39 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +44 -17
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +91 -23
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +107 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-brain.js +84 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +84 -51
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +5 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.182.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.",
|
|
@@ -10,6 +10,7 @@ import { spotOf, ensureAtSpot, ensureAtExit, canReach, spotDistanceMeters, OPEN_
|
|
|
10
10
|
import { distanceMeters } from '../utilities/room-grid.js';
|
|
11
11
|
import { resolveBarrierStrike } from '../utilities/barrier-combat.js';
|
|
12
12
|
import { billAction, notYourPhase, phaseHint } from '../utilities/action-cost.js';
|
|
13
|
+
import { arenaOf, inArena } from '../utilities/combat-turn.js';
|
|
13
14
|
import { spendMovementMeters } from '../utilities/movement-cost.js';
|
|
14
15
|
/**
|
|
15
16
|
* Shadowrun-style dice-pool combat, one full exchange per command:
|
|
@@ -224,11 +225,12 @@ export class AttackCommand extends Command {
|
|
|
224
225
|
return `Click -- the ${weaponItem.name} is dry. "reload" (needs ammunition), or go in swinging with something else.`;
|
|
225
226
|
}
|
|
226
227
|
// THE COMBAT TURN (utilities/combat-turn.ts): on the physical planes
|
|
227
|
-
// a fight is a turn/pass loop and this attack is ONE
|
|
228
|
-
// actor's own Action Phase. The Matrix
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
228
|
+
// and the Matrix a fight is a turn/pass loop and this attack is ONE
|
|
229
|
+
// action in the actor's own Action Phase. The Matrix joined it on
|
|
230
|
+
// 2026-09-13 (utilities/host-combat.ts): a Data Spike used to roll a
|
|
231
|
+
// fresh two-actor initiative per command and print "FREEZES" every
|
|
232
|
+
// time. Only the astral still rides the older exchange below.
|
|
233
|
+
if (this.actor.plane === 'meat' || this.actor.plane === 'drone' || this.actor.plane === 'matrix') {
|
|
232
234
|
return await this.executeInEncounter(target, room, weaponItem ?? undefined, ambush);
|
|
233
235
|
}
|
|
234
236
|
const report = [];
|
|
@@ -340,8 +342,9 @@ export class AttackCommand extends Command {
|
|
|
340
342
|
const actor = this.actor;
|
|
341
343
|
const human = this.scene.isHumanControlled(actor);
|
|
342
344
|
let enc = this.scene.encounterFor?.(actor);
|
|
345
|
+
const arena = arenaOf(actor);
|
|
343
346
|
if (!enc) {
|
|
344
|
-
enc = await this.scene.startEncounter(
|
|
347
|
+
enc = await this.scene.startEncounter(arena, { aggressor: actor, target, ambush });
|
|
345
348
|
if (enc.ended) {
|
|
346
349
|
return human ? `` : `The fight was over before ${actor.name} got to act.`;
|
|
347
350
|
}
|
|
@@ -356,7 +359,7 @@ export class AttackCommand extends Command {
|
|
|
356
359
|
? `Initiative rolled -- ${who} ${enc.phaseActor ? 'acts' : 'act'} first. Your ${this.actionVerb() === 'attacks' ? 'attack' : 'move'} waits for your Action Phase.${hint(` ("initiative" shows the order.)`)}`
|
|
357
360
|
: `${actor.name} squares up -- initiative rolled; ${who} first.`;
|
|
358
361
|
}
|
|
359
|
-
if (target.isIncapacitated() || target
|
|
362
|
+
if (target.isIncapacitated() || !inArena(arena, target)) {
|
|
360
363
|
return human ? `${target.name} is no longer standing in front of you.` : `${target.name} is no longer a target.`;
|
|
361
364
|
}
|
|
362
365
|
}
|
|
@@ -372,7 +375,10 @@ export class AttackCommand extends Command {
|
|
|
372
375
|
// shoot or swing the blade instead? READY WEAPON (p.165) is its own
|
|
373
376
|
// Simple Action -- "draw" first (2jbprbSYqF7EPXrkR).
|
|
374
377
|
const activeWeapon = this.usesEquippedWeapon() && actor.weaponDrawn ? weaponItem : undefined;
|
|
375
|
-
|
|
378
|
+
// A DATA SPIKE (p.239) is neither a swing nor a shot: no ground to
|
|
379
|
+
// close, no recoil, a Complex Action. The grid sees no distance.
|
|
380
|
+
const onGrid = actor.plane === 'matrix';
|
|
381
|
+
const isMelee = !onGrid && (!activeWeapon || !activeWeapon.isFirearm());
|
|
376
382
|
const report = [];
|
|
377
383
|
// MELEE REACH: ground costs (p.161-162).
|
|
378
384
|
if (isMelee) {
|
|
@@ -428,8 +434,8 @@ export class AttackCommand extends Command {
|
|
|
428
434
|
}
|
|
429
435
|
}
|
|
430
436
|
// THE ACTION.
|
|
431
|
-
const label = isMelee ? (this.usesEquippedWeapon() ? 'Melee Attack' : 'Subdue') : 'Fire Weapon';
|
|
432
|
-
const bill = billAction(this.scene, actor, isMelee ? 'complex' : 'simple', label, { attack: true });
|
|
437
|
+
const label = onGrid ? 'Data Spike' : isMelee ? (this.usesEquippedWeapon() ? 'Melee Attack' : 'Subdue') : 'Fire Weapon';
|
|
438
|
+
const bill = billAction(this.scene, actor, onGrid || isMelee ? 'complex' : 'simple', label, { attack: true });
|
|
433
439
|
if (bill)
|
|
434
440
|
return bill;
|
|
435
441
|
const exchange = new CombatExchange(this.scene, this.logger, actor);
|
|
@@ -442,7 +448,7 @@ export class AttackCommand extends Command {
|
|
|
442
448
|
target.combatOpponent = target.combatOpponent ?? actor;
|
|
443
449
|
target.lastExchangeAt = now;
|
|
444
450
|
actor.surrendered = false;
|
|
445
|
-
if (!isMelee) {
|
|
451
|
+
if (!isMelee && !onGrid) {
|
|
446
452
|
actor.firedThisPhase = true;
|
|
447
453
|
actor.recoilRoundsFired += 1;
|
|
448
454
|
}
|
|
@@ -3,6 +3,7 @@ import { movePersonaWith } from '../utilities/matrix-roster.js';
|
|
|
3
3
|
import { resolveHostInReach, hostsInReach, gridVicinity, hostOver } from '../utilities/grid-reach.js';
|
|
4
4
|
import { hint } from '../utilities/hints.js';
|
|
5
5
|
import { hostLabel } from '../utilities/grid-names.js';
|
|
6
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
6
7
|
/**
|
|
7
8
|
* ENTER/EXIT HOST (SR5 p.239, Complex Action).
|
|
8
9
|
*
|
|
@@ -67,6 +68,12 @@ export class EnterHostCommand extends Command {
|
|
|
67
68
|
hint(`A host opens to anyone holding a MARK on it (p.239) -- "mark ${target.name}" softens it, or "hack" takes one by force.`),
|
|
68
69
|
].join('\n');
|
|
69
70
|
}
|
|
71
|
+
// A Complex Action (p.239) -- billed only inside a Combat Turn.
|
|
72
|
+
const bill = billAction(this.scene, actor, 'complex', 'Enter Host');
|
|
73
|
+
if (bill)
|
|
74
|
+
return bill;
|
|
75
|
+
// Crossing the wall leaves whatever fight was on this side of it.
|
|
76
|
+
this.scene.encounterFor?.(actor)?.leave(actor);
|
|
70
77
|
// THE PERSONA CROSSES, THE BODY STAYS (increment 2): position becomes
|
|
71
78
|
// the host, remembering the grid it came from (p.239: exit returns
|
|
72
79
|
// you there). currentLocation is never written -- it is the body's.
|
|
@@ -130,6 +137,13 @@ export class ExitHostCommand extends Command {
|
|
|
130
137
|
const by = [...actor.linkLockedBy].join(', ');
|
|
131
138
|
return `{red-fg}You reach for the door and it isn't there. LINK-LOCKED by ${by}: the connection is held open and Enter/Exit Host is closed to you (p.229). "jack out" still works -- it just costs you dumpshock.{/red-fg}`;
|
|
132
139
|
}
|
|
140
|
+
// A Complex Action (p.239), and the way out of the host's fight
|
|
141
|
+
// (utilities/host-combat.ts): a persona back on the grid is beyond
|
|
142
|
+
// the ice, which "does not operate out on the grid".
|
|
143
|
+
const bill = billAction(this.scene, actor, 'complex', 'Exit Host');
|
|
144
|
+
if (bill)
|
|
145
|
+
return bill;
|
|
146
|
+
this.scene.encounterFor?.(actor)?.leave(actor);
|
|
133
147
|
const host = actor.insideHost;
|
|
134
148
|
const from = actor.matrixPosition?.kind === 'host' ? actor.matrixPosition.from : undefined;
|
|
135
149
|
actor.matrixPosition = { kind: 'grid', grid: from };
|
|
@@ -12,6 +12,7 @@ import { matchesCameraName, hostLabel } from '../utilities/grid-names.js';
|
|
|
12
12
|
import { TapCommand } from './tap.js';
|
|
13
13
|
import { sameHostSide } from '../models/player.js';
|
|
14
14
|
import { MAX_MARKS } from '../utilities/marks.js';
|
|
15
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
15
16
|
import { declarationPenalty, parseMarkDeclaration, hostDefensePool, wanDefensePool, freeMatrixPerceptionHits, bruteForceMatrixDv, overwatchFromDefense, GO_BIG_QUALITY, deviceDefensePool } from '../utilities/matrix-intrusion.js';
|
|
16
17
|
/**
|
|
17
18
|
* MARKS A PAN INTRUSION MUST ALREADY HOLD before it can command the
|
|
@@ -74,6 +75,12 @@ export class HackCommand extends BypassCommand {
|
|
|
74
75
|
// here so a declaration never lands in a target name, the same way
|
|
75
76
|
// the mode words are.
|
|
76
77
|
const { marks: declaredMarks, rest } = parseMarkDeclaration(afterMode);
|
|
78
|
+
// BRUTE FORCE and HACK ON THE FLY are Complex Actions (p.238, p.240)
|
|
79
|
+
// -- billed only inside a Combat Turn, where one intrusion is the
|
|
80
|
+
// whole Action Phase.
|
|
81
|
+
const bill = billAction(this.scene, this.actor, 'complex', mode === 'sleaze' ? 'Hack on the Fly' : 'Brute Force');
|
|
82
|
+
if (bill)
|
|
83
|
+
return bill;
|
|
77
84
|
// PAN warfare: "hack <name>" cracks a meat actor's personal area
|
|
78
85
|
// network -- from inside the Matrix, or right here in AR with a
|
|
79
86
|
// working deck in hand (which is also how enemy deckers get YOU).
|
|
@@ -3,6 +3,7 @@ import { gridVicinity } from '../utilities/grid-reach.js';
|
|
|
3
3
|
import { enterMatrix, leaveMatrix } from '../utilities/planes.js';
|
|
4
4
|
import { hint } from '../utilities/hints.js';
|
|
5
5
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
6
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
6
7
|
/**
|
|
7
8
|
* The Matrix door, both directions: "jack in [hot|cold]" and "jack out".
|
|
8
9
|
* Needs a working CYBERDECK carried (a commlink is calls and AR only) --
|
|
@@ -97,6 +98,11 @@ export class JackCommand extends Command {
|
|
|
97
98
|
if (actor.inExchange) {
|
|
98
99
|
return `The ice has your persona locked in the exchange -- survive it first, then jack out in the lull.`;
|
|
99
100
|
}
|
|
101
|
+
// JACK OUT is a Simple Action (p.240) -- billed inside a Combat
|
|
102
|
+
// Turn; leaving the Matrix then leaves the fight (planes.ts).
|
|
103
|
+
const bill = billAction(this.scene, actor, 'simple', 'Jack Out');
|
|
104
|
+
if (bill)
|
|
105
|
+
return bill;
|
|
100
106
|
// LINK-LOCKED: leaving stops being a decision and becomes a test
|
|
101
107
|
// (p.229/p.240). Everything below is the Jack Out action.
|
|
102
108
|
if (actor.linkLockedBy.size > 0) {
|
|
@@ -17,6 +17,7 @@ import { BULLET } from '../utilities/log-style.js';
|
|
|
17
17
|
import { spotsActive, whoIsWhereLine, highlightSpots, highlightActors, canReach, sealedRouteTo } from '../utilities/spots.js';
|
|
18
18
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
19
19
|
import { findCatalogItem, describeEffect } from '../utilities/catalog.js';
|
|
20
|
+
import { billAction } from '../utilities/action-cost.js';
|
|
20
21
|
export class LookCommand extends Command {
|
|
21
22
|
static verb = 'look';
|
|
22
23
|
static description = 'Examine your surroundings';
|
|
@@ -301,6 +302,11 @@ export class LookCommand extends Command {
|
|
|
301
302
|
* match the grid map: ● host, ◆ persona, ▣ PAN, ◇ data.
|
|
302
303
|
*/
|
|
303
304
|
gridLook(room) {
|
|
305
|
+
// MATRIX PERCEPTION is a Complex Action (p.241) -- billed only inside
|
|
306
|
+
// a Combat Turn, where a look at the room costs the phase.
|
|
307
|
+
const bill = billAction(this.scene, this.actor, 'complex', 'Matrix Perception');
|
|
308
|
+
if (bill)
|
|
309
|
+
return bill;
|
|
304
310
|
// NO MEAT PROSE AT ALL (player ruling 2026-08-26: "looking in the
|
|
305
311
|
// matrix shouldn't show the meatworld"). The room's own description
|
|
306
312
|
// used to open this view "for orientation" -- but that description
|
|
@@ -5,7 +5,7 @@ import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
|
5
5
|
import { perceptionTest } from '../utilities/perception.js';
|
|
6
6
|
import { canReach, exitSpot, spotsActive, revealWithPerception, theSpot, sealedRouteTo, } from '../utilities/spots.js';
|
|
7
7
|
import { placeName } from '../utilities/log-style.js';
|
|
8
|
-
import { billAction } from '../utilities/action-cost.js';
|
|
8
|
+
import { billAction, encounterOf } from '../utilities/action-cost.js';
|
|
9
9
|
/**
|
|
10
10
|
* Barrier rendering (2026-08-24 playtest: "I don't see THE THING I'm
|
|
11
11
|
* supposed to act on -- just a description... and I still see the
|
|
@@ -137,6 +137,13 @@ export class SearchCommand extends Command {
|
|
|
137
137
|
static description = 'Search for items in your area';
|
|
138
138
|
async execute(_args) {
|
|
139
139
|
const room = this.actor.currentLocation;
|
|
140
|
+
// A MATRIX SEARCH IS MEASURED IN MINUTES (p.241: base time one
|
|
141
|
+
// minute inside a host), not in Action Phases -- there is no cost to
|
|
142
|
+
// bill inside a Combat Turn, so it is refused there rather than
|
|
143
|
+
// priced by invention.
|
|
144
|
+
if (this.actor.plane === 'matrix' && encounterOf(this.scene, this.actor)) {
|
|
145
|
+
return `Not with the ice on you -- a Matrix Search takes minutes (p.241), and a Combat Turn is three seconds. Crash the ice or get out first.`;
|
|
146
|
+
}
|
|
140
147
|
// OBSERVE IN DETAIL (SR5 p.165): the Perception Test below is the
|
|
141
148
|
// Simple Action the book names, inside a Combat Turn.
|
|
142
149
|
const bill = billAction(this.scene, this.actor, 'simple', 'Observe in Detail');
|
|
@@ -440,5 +440,14 @@
|
|
|
440
440
|
// 1.52.0 (2026-09-12): THE EMBEDDED HUB SEED IS GONE FROM SAVES. The one
|
|
441
441
|
// release of dual-writing is over: a save carries hubSeedRef only, and
|
|
442
442
|
// the site (sheet fallback) reads hubSeed on older docs alone.
|
|
443
|
-
|
|
443
|
+
// 1.53.0 (2026-09-13): THE MATRIX JOINS THE COMBAT TURN. A host fight is a
|
|
444
|
+
// CombatEncounter in a host arena (encounters are keyed by arena: room,
|
|
445
|
+
// host, or the open grid); the host launches one IC per Combat Turn at
|
|
446
|
+
// the turn's start, each program is a participant with 4D6 whose phase
|
|
447
|
+
// the ice brain plays, a Data Spike is the phase's Complex attack, and
|
|
448
|
+
// hack/enter/exit/look/jack out bill their canon action cost inside a
|
|
449
|
+
// Combat Turn. IC no longer act once per player command, never speak,
|
|
450
|
+
// and a failed IC attack damages the program (p.247). Ice NPCs are no
|
|
451
|
+
// longer generated; the vault host's rating is its guard.
|
|
452
|
+
export const ENGINE_VERSION = '1.53.0';
|
|
444
453
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -267,10 +267,13 @@ take ground. Build it accordingly:
|
|
|
267
267
|
Matrix presence, whose files open only to a runner whose BODY is in that room). Pick one
|
|
268
268
|
and commit to it -- the offline server is the harder, more interesting shape when the
|
|
269
269
|
fiction wants a vault you must walk into.
|
|
270
|
-
- THE OPPOSITION IS ICE, not muscle.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
- THE OPPOSITION IS THE HOST'S OWN ICE, not muscle. Give the vault host a "hostRating" of
|
|
271
|
+
5-8: the host launches its own intrusion countermeasures (Patrol, Probe, Killer...) at
|
|
272
|
+
that rating, so DO NOT write an NPC for the ice -- no "matrix"-plane npcs[] at all. The
|
|
273
|
+
NPCs are the client and the people who work the site. No guards patrolling meat
|
|
274
|
+
corridors, no gunfight in the lobby: a data run that ends in a firefight is a
|
|
275
|
+
smash-and-grab wearing the wrong name. A site can still be WATCHED (cameras) --
|
|
276
|
+
surveillance is pressure without being a gun.
|
|
274
277
|
- Keep it SHORT and tight. One site, one host, one file.
|
|
275
278
|
- It pays LESS than a full run of the same tier -- roughly half. The runner is trading pay
|
|
276
279
|
for a job that never puts a gun in their face, and the fixer prices it that way. Say so
|
|
@@ -407,8 +410,9 @@ ${JSON_ONLY}
|
|
|
407
410
|
// its description reads.
|
|
408
411
|
"npcs": [{
|
|
409
412
|
"name": string, "concept": string, "startLocation": string,
|
|
410
|
-
"plane"?: "meat"|"
|
|
411
|
-
|
|
413
|
+
"plane"?: "meat"|"astral", // "astral" = a spirit. Default meat. NEVER "matrix": a host's
|
|
414
|
+
// ice is not a person -- the host launches its own programs
|
|
415
|
+
// at its "hostRating", and an NPC written as ice is dropped.
|
|
412
416
|
"hostile"?: boolean // OPPOSITION: this one KNOWS the runner does not belong and will
|
|
413
417
|
// act on it -- a posted guard, a patrolling enforcer, a gang
|
|
414
418
|
// holding the room. Default false, and LEAVE IT FALSE unless the
|
|
@@ -418,9 +422,6 @@ ${JSON_ONLY}
|
|
|
418
422
|
// building, and marking them turns an ordinary scene into a
|
|
419
423
|
// brawl. If in doubt, leave it out -- a guard who stays calm is
|
|
420
424
|
// a missed beat, a hostile bartender is a broken scene.
|
|
421
|
-
// ICE NEEDS NO FLAG: a "matrix" NPC is intrusion countermeasures
|
|
422
|
-
// and is treated as opposition automatically. Write it only to
|
|
423
|
-
// say FALSE, for the rare tame construct.
|
|
424
425
|
}],
|
|
425
426
|
"devices": [{
|
|
426
427
|
"name": string, "concept": string, // what it physically IS (a maglock, a keypad, a
|
|
@@ -520,9 +521,10 @@ Hard structural rules (validated mechanically -- a violation is rejected):
|
|
|
520
521
|
skill can still take the shortcut -- pick the lock, hack the panel -- but the found way
|
|
521
522
|
must exist for a runner who has neither.
|
|
522
523
|
- The winCondition item must not be heldBy winCondition.toNpc. If a matrix-plane item is the
|
|
523
|
-
winCondition item, its room has hasNode true AND a "
|
|
524
|
-
|
|
525
|
-
|
|
524
|
+
winCondition item, its room has hasNode true AND a "hostRating" of at least 4 -- the host
|
|
525
|
+
defends its own files with its own ice; never write an NPC for that. If the runner can
|
|
526
|
+
jack in, a Datachip/Document winCondition item MUST be that matrix-plane item -- data for
|
|
527
|
+
a hacker lives on a host, never on a desk.
|
|
526
528
|
- ${isContinuation
|
|
527
529
|
? 'ZERO "starting"-role items -- the runner arrives with everything they own. 4-6'
|
|
528
530
|
: '3-5 "starting"-role items (basics the runner doesn\'t already own -- see the dossier), 4-6'}
|
|
@@ -652,8 +654,8 @@ For EVERY npc in the skeleton, expand its "concept":
|
|
|
652
654
|
"hacking"?, "cybercombat"?, "electronic-warfare"?, "hardware"?, "locksmith"?, "computer"?,
|
|
653
655
|
"negotiation"?, "con"?, "intimidation"?, "etiquette"?, "leadership"?, "performance"?, "disguise"?,
|
|
654
656
|
"medicine"?, "assensing"?, "counterspelling"?, "conjuring"?, "gunnery"? } }
|
|
655
|
-
"magic" 3-6 ONLY for a genuinely Awakened concept;
|
|
656
|
-
|
|
657
|
+
"magic" 3-6 ONLY for a genuinely Awakened concept; give combat blocks to anyone the story
|
|
658
|
+
expects in a firefight. "skills" is
|
|
657
659
|
optional flavor for defined concepts (a sharpshooter's "firearms": 6).
|
|
658
660
|
|
|
659
661
|
${JSON_ONLY}
|
|
@@ -886,6 +888,14 @@ const GENERIC_LOCK_WORDS = new Set([
|
|
|
886
888
|
'lock', 'maglock', 'door', 'gate', 'grate', 'hatch', 'safe', 'vault', 'cage', 'shutter', 'padlock', 'cabinet', 'locker',
|
|
887
889
|
'keycard', 'card', 'passkey', 'access', 'security', 'back', 'front', 'side', 'room', 'office', 'main', 'inner', 'outer',
|
|
888
890
|
]);
|
|
891
|
+
/**
|
|
892
|
+
* THE LEAST A VAULT HOST RATES. A host launches its own IC at its rating
|
|
893
|
+
* (p.247, one per Combat Turn up to the rating), so the rating is the
|
|
894
|
+
* whole defence of the paydata inside it; below this the run is a free
|
|
895
|
+
* walk. Four is the floor: a rating-3 default host fields three thin
|
|
896
|
+
* programs and no Killer.
|
|
897
|
+
*/
|
|
898
|
+
export const VAULT_HOST_RATING = 4;
|
|
889
899
|
export function validateSkeleton(skeleton, player, crew) {
|
|
890
900
|
const fail = (msg) => { throw new Error(`Skeleton invalid: ${msg}`); };
|
|
891
901
|
if (!Array.isArray(skeleton?.rooms) || skeleton.rooms.length === 0)
|
|
@@ -1107,11 +1117,18 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
1107
1117
|
// Worded to route to 'skeleton' (no "items[", no "Win condition item").
|
|
1108
1118
|
const winItem = skeleton.items.find(i => i.name === skeleton.winCondition.item);
|
|
1109
1119
|
if (winItem && isDataShaped(winItem.category) && winItem.plane !== 'matrix' && partyCanDeck(player)) {
|
|
1110
|
-
fail(`the winCondition item "${winItem.name}" is a ${winItem.category} on the meat plane, and this runner can jack in -- for a decker or technomancer, data is PAYDATA: give it "plane": "matrix", place it in a hasNode room (not heldBy anyone)
|
|
1120
|
+
fail(`the winCondition item "${winItem.name}" is a ${winItem.category} on the meat plane, and this runner can jack in -- for a decker or technomancer, data is PAYDATA: give it "plane": "matrix", place it in a hasNode room (not heldBy anyone) with a hostRating of ${VAULT_HOST_RATING} or more. A chip lying on a desk is no job for a hacker.`);
|
|
1111
1121
|
}
|
|
1112
1122
|
// The other half of the same rule, for ANY matrix-plane objective: the
|
|
1113
|
-
// schema says its room has hasNode and
|
|
1123
|
+
// schema says its room has hasNode and a rating that fields ice, and
|
|
1114
1124
|
// until now nothing at the skeleton checked it.
|
|
1125
|
+
//
|
|
1126
|
+
// THE HOST IS ITS OWN GUARD (player ruling 2026-09-13, "mute all
|
|
1127
|
+
// ice"): a vault used to demand a "matrix"-plane NPC -- a person
|
|
1128
|
+
// wearing an ice costume, with dialogue lines and a model behind it,
|
|
1129
|
+
// which is how four programs ended up bargaining over a chip in Vex's
|
|
1130
|
+
// log. Canon has none of that: the host launches its own IC at its
|
|
1131
|
+
// rating (p.247), so what a vault needs is a rating worth the name.
|
|
1115
1132
|
if (winItem?.plane === 'matrix') {
|
|
1116
1133
|
if (!winItem.room) {
|
|
1117
1134
|
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata but has no "room" -- data lives on a host, never in a pocket. Place it in a hasNode room.`);
|
|
@@ -1120,9 +1137,17 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
1120
1137
|
if (!vault?.hasNode) {
|
|
1121
1138
|
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in "${winItem.room}", which has no "hasNode": true -- a matrix file can only exist on a host. Mark that room hasNode.`);
|
|
1122
1139
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1140
|
+
else if ((vault.hostRating ?? 0) < VAULT_HOST_RATING) {
|
|
1141
|
+
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in hasNode room "${winItem.room}" whose hostRating is ${vault.hostRating ?? 'unset'} -- a vault defends itself with its own ice, so give that room a hostRating of ${VAULT_HOST_RATING} or more. An undefended vault is a free win.`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
// NO NPC IS ICE. The same ruling from the other side: a model that
|
|
1145
|
+
// still writes a "matrix"-plane person is writing a voice for a
|
|
1146
|
+
// program. normalizeSkeleton drops them with a note; this holds the
|
|
1147
|
+
// line for anything that reaches the validator by another road.
|
|
1148
|
+
for (const npc of skeleton.npcs) {
|
|
1149
|
+
if (npc.plane === 'matrix') {
|
|
1150
|
+
fail(`npc "${npc.name}" is "matrix"-plane -- ice is not a person. Remove it; the host over its room fields its own countermeasures at its hostRating.`);
|
|
1126
1151
|
}
|
|
1127
1152
|
}
|
|
1128
1153
|
// EVERY HOST HAS A PURPOSE AND HOLDS SOMETHING (N4erz3f63MiCZLkDE,
|
|
@@ -1153,10 +1178,9 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
1153
1178
|
if (!holds) {
|
|
1154
1179
|
fail(`room "${room.name}" is a host (${room.hostPurpose}) with NOTHING inside it -- no "matrix"-plane item placed in that room. A host holds what its business protects: put at least one matrix-plane item there (the ledger, the client list, the camera archive), or drop hasNode.`);
|
|
1155
1180
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1181
|
+
// "Ice stationed on it" used to be the third requirement. It is
|
|
1182
|
+
// now the host's own rating (see the vault rule above): a host
|
|
1183
|
+
// defends itself, and a person written as its ice is refused.
|
|
1160
1184
|
}
|
|
1161
1185
|
// Lock-and-key reachability. CRITICAL: edges are BIDIRECTIONAL -- the
|
|
1162
1186
|
// engine mirrors every declared exit into a shared two-way Door
|
|
@@ -1568,6 +1592,28 @@ export function normalizeSkeleton(skeleton) {
|
|
|
1568
1592
|
delete room.nodeAccess;
|
|
1569
1593
|
notes.push(`dropped the host over "${room.name}" -- it held no matrix-plane file, and a host with nothing in it should not be there.`);
|
|
1570
1594
|
}
|
|
1595
|
+
// ICE IS NOT A PERSON (player ruling 2026-09-13, "mute all ice"). The
|
|
1596
|
+
// prompt no longer asks for a "matrix"-plane NPC and validateSkeleton
|
|
1597
|
+
// refuses one; a model that writes one anyway is writing a voice for
|
|
1598
|
+
// a program, and the cheap repair is to drop it and let the host's
|
|
1599
|
+
// own rating do the guarding. The vault host gets that rating here
|
|
1600
|
+
// when the model forgot it -- clamp, don't burn a regeneration.
|
|
1601
|
+
const constructs = (skeleton.npcs ?? []).filter(n => n.plane === 'matrix');
|
|
1602
|
+
if (constructs.length > 0) {
|
|
1603
|
+
skeleton.npcs = skeleton.npcs.filter(n => n.plane !== 'matrix');
|
|
1604
|
+
for (const n of constructs) {
|
|
1605
|
+
notes.push(`normalized: dropped npc "${n.name}" -- it was "matrix"-plane ice, and a host fields its own countermeasures at its hostRating rather than a person in a costume.`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const winItem = (skeleton.items ?? []).find(i => i.name === skeleton.winCondition?.item);
|
|
1609
|
+
if (winItem?.plane === 'matrix' && winItem.room) {
|
|
1610
|
+
const vault = (skeleton.rooms ?? []).find(r => r.name === winItem.room);
|
|
1611
|
+
if (vault?.hasNode && (vault.hostRating ?? 0) < VAULT_HOST_RATING) {
|
|
1612
|
+
const was = vault.hostRating === undefined ? 'no hostRating' : `hostRating ${vault.hostRating}`;
|
|
1613
|
+
vault.hostRating = VAULT_HOST_RATING;
|
|
1614
|
+
notes.push(`normalized: the vault host over "${vault.name}" had ${was}; raised it to ${VAULT_HOST_RATING} so it fields ice worth the paydata.`);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1571
1617
|
// NOBODY MINDING THE STORE (playtest 2026-08-25: "the flooded
|
|
1572
1618
|
// undermarket could use some details -- it's just blank"). It was the
|
|
1573
1619
|
// district's only commerce room with neither a vendor nor stock: a
|
|
@@ -8,7 +8,7 @@ import { clampDeviceKind } from '../utilities/affordances.js';
|
|
|
8
8
|
import { GenerationCapture } from '../utilities/generation-capture.js';
|
|
9
9
|
import { fetchCanonContext } from '../utilities/canon-lore.js';
|
|
10
10
|
import { SceneSynthesizer } from './scene-factory.js';
|
|
11
|
-
import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, } from './scene-chunks.js';
|
|
11
|
+
import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, VAULT_HOST_RATING, } from './scene-chunks.js';
|
|
12
12
|
// The fixer name convention and the player dossier live with the prompt
|
|
13
13
|
// builders now (see scene-chunks.ts); re-exported so the existing
|
|
14
14
|
// import sites (call.ts, game.ts) keep working unchanged.
|
|
@@ -751,9 +751,11 @@ export class SceneSeedGenerator {
|
|
|
751
751
|
if (!room?.hasNode) {
|
|
752
752
|
throw new Error(`Matrix-plane win condition item "${wc.item}" is in room "${item.room}", which has no "hasNode": true -- matrix files can only exist on a host. Mark that room hasNode.`);
|
|
753
753
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
// THE HOST GUARDS ITSELF (2026-09-13): this used to demand an ice
|
|
755
|
+
// NPC; a host's countermeasures are its own programs at its rating
|
|
756
|
+
// (p.247), and normalizeSkeleton raises a vault to the floor.
|
|
757
|
+
if ((room.hostRating ?? 0) < VAULT_HOST_RATING) {
|
|
758
|
+
throw new Error(`Matrix-plane win condition item "${wc.item}" sits in hasNode room "${item.room}" whose hostRating is ${room.hostRating ?? 'unset'} -- a vault defends itself with its own ice; give that room a hostRating of ${VAULT_HOST_RATING} or more. An undefended data vault makes the run a free win.`);
|
|
757
759
|
}
|
|
758
760
|
}
|
|
759
761
|
/**
|
|
@@ -41,8 +41,9 @@ import { getPref } from './utilities/prefs.js';
|
|
|
41
41
|
import { rollPool, formatRoll } from './utilities/dice.js';
|
|
42
42
|
import { Direction } from './types/shared/direction-enum.js';
|
|
43
43
|
import { Logger } from './utilities/logger.js';
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
44
|
+
import { hostIsHunting, ambientPatrol } from './utilities/ic.js';
|
|
45
|
+
import { hostContactBeat } from './utilities/host-combat.js';
|
|
46
|
+
import { endPersona } from './utilities/planes.js';
|
|
46
47
|
import { CommandQueue } from './utilities/command-queue.js';
|
|
47
48
|
import { currentSession } from './utilities/session-context.js';
|
|
48
49
|
import { PlayersCommand } from './commands/players.js';
|
|
@@ -2482,7 +2483,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2482
2483
|
isParty: a => a !== player && this.scene.isHumanControlled(a),
|
|
2483
2484
|
// Whose Action Phase is live in the viewer's room
|
|
2484
2485
|
// (PYK6zCKhQcjRFwLSQ); undefined between phases and out of a fight.
|
|
2485
|
-
|
|
2486
|
+
// The viewer's own fight first -- a persona's is inside its host,
|
|
2487
|
+
// not in the room its body sits in.
|
|
2488
|
+
phaseActor: () => (this.scene.encounterFor(player) ?? this.scene.encounterIn(player.currentLocation))?.phaseActor?.name,
|
|
2486
2489
|
}))
|
|
2487
2490
|
.catch(() => undefined);
|
|
2488
2491
|
};
|
|
@@ -6044,7 +6047,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6044
6047
|
if (this.scene.isHumanControlled(actor))
|
|
6045
6048
|
this.scene.beat += 1;
|
|
6046
6049
|
const result = await command.execute(args);
|
|
6047
|
-
const
|
|
6050
|
+
const hosted = await this.withHostTurn(actor, result);
|
|
6051
|
+
const wrapped = await this.withHostileContact(actor, this.withEphemeralTurn(actor, this.withAlarmTurn(actor, hosted)));
|
|
6048
6052
|
// The IC turn above may have filled a track -- settle it now, not
|
|
6049
6053
|
// on the next keystroke.
|
|
6050
6054
|
const settled = await settleUnresolvedHarm(this.scene, actor);
|
|
@@ -6178,19 +6182,43 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6178
6182
|
const lines = await hostileContactBeat(this.scene, actor, this.scene.beat);
|
|
6179
6183
|
return lines.length === 0 ? result : `${result}\n\n${lines.join('\n')}`;
|
|
6180
6184
|
}
|
|
6181
|
-
|
|
6185
|
+
/**
|
|
6186
|
+
* THE HOST'S BEAT after every command a persona types inside one
|
|
6187
|
+
* (p.246-247: the ice acts on what is in the host).
|
|
6188
|
+
*
|
|
6189
|
+
* Before the host has made you, this is ambient Patrol on its clock
|
|
6190
|
+
* (utilities/ic.ts ambientPatrol) -- the per-command tick of that
|
|
6191
|
+
* clock is an engine mapping and its own comment says so. Once the
|
|
6192
|
+
* host is hunting, the fight is a Combat Turn in the host's arena
|
|
6193
|
+
* (utilities/host-combat.ts): the first beat opens it, and after
|
|
6194
|
+
* that the programs act in their OWN Action Phases, not on every
|
|
6195
|
+
* keystroke. The old shape -- every running program taking a swing
|
|
6196
|
+
* per player command, with its dice and lines appended to whatever
|
|
6197
|
+
* the command printed -- is gone; Vex's log had four programs and
|
|
6198
|
+
* twelve lines answering "look".
|
|
6199
|
+
*/
|
|
6200
|
+
async withHostTurn(actor, result) {
|
|
6182
6201
|
if (result === null)
|
|
6183
6202
|
return null;
|
|
6184
|
-
// The host's turn happens INSIDE it (p.246-247): the ice acts on what
|
|
6185
|
-
// is in the host, and the persona's position says whether it is.
|
|
6186
6203
|
const host = actor.hostInside;
|
|
6187
6204
|
if (!host || actor.plane !== 'matrix')
|
|
6188
6205
|
return result;
|
|
6189
|
-
|
|
6206
|
+
if (hostIsHunting(host.over, actor)) {
|
|
6207
|
+
if (this.scene.encounterInHost(host)?.has(actor))
|
|
6208
|
+
return result;
|
|
6209
|
+
// THE COMMAND'S OWN REPLY PRINTS FIRST. The encounter announces
|
|
6210
|
+
// itself while it opens (the aggressor, Surprise, the order), so
|
|
6211
|
+
// a result returned afterwards would land under all of that,
|
|
6212
|
+
// out of order -- the same trick hostile-contact.ts uses.
|
|
6213
|
+
if (result.length > 0 && this.scene.isHumanControlled(actor)) {
|
|
6214
|
+
Logger.getInstance().log(result, { actor: actor.name });
|
|
6215
|
+
}
|
|
6216
|
+
await hostContactBeat(this.scene, actor);
|
|
6217
|
+
return '';
|
|
6218
|
+
}
|
|
6219
|
+
const turn = ambientPatrol(host.over, actor);
|
|
6190
6220
|
if (turn.lines.length === 0 && turn.meta.length === 0)
|
|
6191
6221
|
return result;
|
|
6192
|
-
// Dice to the Mechanics pane, and only for a human -- the same gate
|
|
6193
|
-
// every other roll in this engine uses.
|
|
6194
6222
|
if (this.scene.isHumanControlled(actor)) {
|
|
6195
6223
|
const logger = Logger.getInstance();
|
|
6196
6224
|
for (const m of turn.meta)
|
|
@@ -6198,36 +6226,16 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6198
6226
|
}
|
|
6199
6227
|
for (const w of turn.world)
|
|
6200
6228
|
this.scene.addWorldEvent(w);
|
|
6201
|
-
// A deck bricked by IC owes the same forced dump a convergence does.
|
|
6202
|
-
// ic.ts reports it rather than performing it -- leaveMatrix needs the
|
|
6203
|
-
// Scene and that module deliberately takes none, exactly as hack.ts
|
|
6204
|
-
// splits the convergence hammer from the dump that follows it.
|
|
6205
|
-
if (turn.bricked) {
|
|
6206
|
-
turn.lines.push(...leaveMatrix(this.scene, actor, {
|
|
6207
|
-
forced: true,
|
|
6208
|
-
reason: `The host's ice burns your deck out from under you.`,
|
|
6209
|
-
}));
|
|
6210
|
-
}
|
|
6211
|
-
else if (turn.scrambled) {
|
|
6212
|
-
// SCRAMBLE IC (p.248): "you reboot immediately, taking dumpshock if
|
|
6213
|
-
// you were in VR." A forced exit through the same door, which is
|
|
6214
|
-
// what makes it a reboot rather than a graceful drop -- and the
|
|
6215
|
-
// reboot is also what hands the reducers' damage back.
|
|
6216
|
-
//
|
|
6217
|
-
// `else if` on purpose: a deck already bricked this turn has been
|
|
6218
|
-
// dumped, and dumping a persona twice would bill dumpshock twice.
|
|
6219
|
-
turn.lines.push(...leaveMatrix(this.scene, actor, {
|
|
6220
|
-
forced: true,
|
|
6221
|
-
reason: `Scramble IC rips the connection out at the root --`,
|
|
6222
|
-
}));
|
|
6223
|
-
}
|
|
6224
|
-
if (turn.traced) {
|
|
6225
|
-
// TRACK IC (p.249): the location goes to the authorities. Routed
|
|
6226
|
-
// into the heat the game already keeps for being made in the meat
|
|
6227
|
-
// world, rather than a second parallel notion of "they know".
|
|
6228
|
-
this.addHeat(4, `Track IC reported ${actor.name}'s location`);
|
|
6229
|
-
}
|
|
6230
6229
|
const body = turn.lines.join('\n');
|
|
6230
|
+
// Made on this very sweep: the fight opens on the same beat, after
|
|
6231
|
+
// the sighting has been read.
|
|
6232
|
+
if (hostIsHunting(host.over, actor)) {
|
|
6233
|
+
if (this.scene.isHumanControlled(actor)) {
|
|
6234
|
+
Logger.getInstance().log(result.length > 0 ? `${result}\n${body}` : body, { actor: actor.name });
|
|
6235
|
+
}
|
|
6236
|
+
await hostContactBeat(this.scene, actor);
|
|
6237
|
+
return '';
|
|
6238
|
+
}
|
|
6231
6239
|
return result.length > 0 ? `${result}\n${body}` : body;
|
|
6232
6240
|
}
|
|
6233
6241
|
/**
|