@maka/maka-cli 5.22.0 → 5.24.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/breach.js +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +10 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +49 -45
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.d.ts +18 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +161 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +134 -4
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +59 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.d.ts +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +50 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
3
|
+
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
4
|
+
import { accrueOverwatch } from '../utilities/overwatch.js';
|
|
5
|
+
import { leaveMatrix, canReachHost } from '../utilities/planes.js';
|
|
6
|
+
import { isLinked } from '../utilities/comm-style.js';
|
|
7
|
+
import { describeMarks, MAX_MARKS } from '../utilities/marks.js';
|
|
8
|
+
import { hint } from '../utilities/hints.js';
|
|
9
|
+
/**
|
|
10
|
+
* MARK (canon p.235-238, player go 2026-08-24): place a MAtrix
|
|
11
|
+
* Recognition Key on an icon -- the room's host, or a meat presence's
|
|
12
|
+
* PAN. The patient decker's whole craft: each mark SOFTENS the target
|
|
13
|
+
* for you (hack.ts -- hosts crack at a lower threshold, PAN intrusions
|
|
14
|
+
* gain +2 dice per mark; at ${MAX_MARKS} marks a host simply thinks
|
|
15
|
+
* you belong). Same Attack/Sleaze fork as hack: bare "mark" rides
|
|
16
|
+
* Sleaze (fail = you're NOTICED); "mark loud <target>" is Brute Force
|
|
17
|
+
* (fail = unnoticed, the firewall bites). Same GOD bill either way.
|
|
18
|
+
* Marks die with your persona: reboot, jack-out, or convergence wipes
|
|
19
|
+
* every key you placed; a target's reboot shakes yours off it.
|
|
20
|
+
*/
|
|
21
|
+
export class MarkCommand extends Command {
|
|
22
|
+
static verb = 'mark';
|
|
23
|
+
static description = 'Place a MARK on an icon -- "mark" (the room\'s host) or "mark <name>" (a PAN). Each mark softens that target for YOUR hacks (+2 dice per mark on PANs; hosts crack easier, and at 3 marks open like you belong). Sleaze by default (fail = noticed); "mark loud" brute-forces (fail = unnoticed, the firewall bites). Bare "mark list" shows your standing keys. Marks die with your persona\'s reboot.';
|
|
24
|
+
async execute(args = []) {
|
|
25
|
+
const actor = this.actor;
|
|
26
|
+
if (actor.plane !== 'matrix') {
|
|
27
|
+
const deck = actor.getCyberdeck();
|
|
28
|
+
if (!actor.isTechnomancer() && !deck) {
|
|
29
|
+
return `Marking an icon takes a live persona -- jack in, or put a working cyberdeck in hand.`;
|
|
30
|
+
}
|
|
31
|
+
if (actor.plane === 'astral')
|
|
32
|
+
return `The Matrix doesn't answer to spirits.`;
|
|
33
|
+
}
|
|
34
|
+
// Mode fork (shared vocabulary with hack.ts).
|
|
35
|
+
const words = [...args].map(w => w.toLowerCase());
|
|
36
|
+
let mode = 'sleaze';
|
|
37
|
+
const loudIdx = words.findIndex(w => /^(loud|force|brute|attack)$/.test(w));
|
|
38
|
+
if (loudIdx >= 0) {
|
|
39
|
+
mode = 'attack';
|
|
40
|
+
words.splice(loudIdx, 1);
|
|
41
|
+
}
|
|
42
|
+
const quietIdx = words.findIndex(w => /^(quiet|sleaze|sneak)$/.test(w));
|
|
43
|
+
if (quietIdx >= 0)
|
|
44
|
+
words.splice(quietIdx, 1);
|
|
45
|
+
if (words[0] === 'list' || words[0] === 'mine') {
|
|
46
|
+
const standing = describeMarks(this.scene, actor.name);
|
|
47
|
+
return standing.length > 0
|
|
48
|
+
? `=== YOUR MARKS ===\n${standing.map(l => ` ${l}`).join('\n')}`
|
|
49
|
+
: `No standing marks -- your persona holds no keys right now.`;
|
|
50
|
+
}
|
|
51
|
+
const room = actor.currentLocation;
|
|
52
|
+
const query = words.join(' ').trim();
|
|
53
|
+
// Resolve: a named PAN first, else the room's host.
|
|
54
|
+
let target;
|
|
55
|
+
if (query.length > 0) {
|
|
56
|
+
const candidates = this.scene.getActorsInRoom(room)
|
|
57
|
+
.filter(a => a !== actor && a.plane === 'meat' && !a.isIncapacitated() && isLinked(a));
|
|
58
|
+
const picked = fuzzyPickName(query, candidates.map(a => a.name));
|
|
59
|
+
const hit = picked ? candidates.find(a => a.name === picked) : undefined;
|
|
60
|
+
if (hit)
|
|
61
|
+
target = { kind: 'pan', actor: hit };
|
|
62
|
+
else if (/^(host|node|the host)$/.test(query))
|
|
63
|
+
target = room.hasNode ? { kind: 'host' } : undefined;
|
|
64
|
+
if (!target)
|
|
65
|
+
return `Nothing here answers to "${query}" -- no broadcasting PAN, and ${room.hasNode ? 'the host goes by "host"' : 'no host on this stretch of grid'}.`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (!room.hasNode)
|
|
69
|
+
return `Thin grid out here -- no host to mark. ("mark <name>" keys a PAN instead.)`;
|
|
70
|
+
target = { kind: 'host' };
|
|
71
|
+
}
|
|
72
|
+
if (target.kind === 'host') {
|
|
73
|
+
if (!canReachHost(actor, room))
|
|
74
|
+
return `The host here is AIR-GAPPED -- no wireless surface to hang a mark on. Hard line only.`;
|
|
75
|
+
if (room.hostCracked)
|
|
76
|
+
return `The host hangs cracked open -- marks are for doors that are still closed.`;
|
|
77
|
+
const mine = room.hostMarksBy.get(actor.name) ?? 0;
|
|
78
|
+
if (mine >= MAX_MARKS)
|
|
79
|
+
return `You already hold ${MAX_MARKS} marks here -- the host thinks you belong. ("hack" walks in.)`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const mine = target.actor.panMarksBy.get(actor.name) ?? 0;
|
|
83
|
+
if (mine >= MAX_MARKS)
|
|
84
|
+
return `You already hold ${MAX_MARKS} marks on ${target.actor.name}'s PAN -- it can't get softer.`;
|
|
85
|
+
}
|
|
86
|
+
// The roll: hack.ts's pools, threshold 2 (a mark is the lighter
|
|
87
|
+
// touch -- the payoff is deferred, not the difficulty).
|
|
88
|
+
let pool;
|
|
89
|
+
if (mode === 'attack') {
|
|
90
|
+
pool = Math.max(1, actor.plane === 'matrix' ? actor.getAttackPool()
|
|
91
|
+
: actor.logic + actor.skillRating('hacking') + (actor.getCyberdeck()?.matrixEdge('attack') ?? 0) + actor.woundModifier);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const sleazeEdge = actor.isTechnomancer()
|
|
95
|
+
? Math.floor(actor.resonance / 2)
|
|
96
|
+
: (actor.activeDeck ?? actor.getCyberdeck())?.matrixEdge('sleaze') ?? 0;
|
|
97
|
+
pool = Math.max(1, actor.logic + actor.skillRating('hacking') + sleazeEdge + actor.qualityBonus('hacking') + actor.woundModifier - actor.sustainingPenalty);
|
|
98
|
+
}
|
|
99
|
+
const roll = rollPool(pool);
|
|
100
|
+
const targetLabel = target.kind === 'host' ? `the ${room.name} host` : `${target.actor.name}'s PAN`;
|
|
101
|
+
if (this.scene.isHumanControlled(actor)) {
|
|
102
|
+
this.logger.meta(`Mark on ${targetLabel} (${mode === 'attack' ? 'brute force' : 'sleaze'}): ${formatRoll(roll)} (2+ to place)`);
|
|
103
|
+
}
|
|
104
|
+
actor.performAction('works an icon over', `fingers tracing ${targetLabel}`);
|
|
105
|
+
// GOD bills the attempt like any illegal action.
|
|
106
|
+
const bill = accrueOverwatch(this.scene, actor, 2, `mark on ${targetLabel} (${mode})`);
|
|
107
|
+
const godLines = bill.lines;
|
|
108
|
+
if (bill.converged && actor.plane === 'matrix') {
|
|
109
|
+
godLines.push(...leaveMatrix(this.scene, actor, { forced: true, reason: `GOD force-reboots your persona --` }));
|
|
110
|
+
return [`The mark never lands --`, ...godLines].join('\n');
|
|
111
|
+
}
|
|
112
|
+
if (roll.hits < 2) {
|
|
113
|
+
if (mode === 'attack') {
|
|
114
|
+
// Canon: a failed Attack goes unnoticed; the firewall chews you.
|
|
115
|
+
const dealt = Math.max(0, 2 - rollPool(actor.getSoakPool()).hits);
|
|
116
|
+
const lines = [`The forced key shatters against the firewall${dealt > 0 ? ` -- and it chews back` : ``}.`];
|
|
117
|
+
if (dealt > 0) {
|
|
118
|
+
if (actor.simMode === 'hot' || actor.isTechnomancer()) {
|
|
119
|
+
actor.takeStun(dealt);
|
|
120
|
+
lines.push(` Feedback burns -- ${dealt} stun (biofeedback): ${actor.stunSummary()}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
actor.takeMatrixDamage(dealt);
|
|
124
|
+
lines.push(` Your deck's boards sizzle -- ${dealt} box${dealt === 1 ? '' : 'es'}: ${actor.matrixSummary()}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
lines.push(` Nobody noticed -- a key that never turned leaves no trace.`, ...godLines);
|
|
128
|
+
this.scene.updateStatus();
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
// Canon: a failed Sleaze is NOTICED.
|
|
132
|
+
if (target.kind === 'host') {
|
|
133
|
+
room.hostAlert = true;
|
|
134
|
+
this.scene.addWorldEvent(`The ${room.name} host MADE ${actor.name} planting a mark -- alarm flagged, ice hunting.`);
|
|
135
|
+
return [`The key slips -- and the ice turns to LOOK. The host holds a mark on YOU now: walls up, and everything guarding this place knows.`, ...godLines].join('\n');
|
|
136
|
+
}
|
|
137
|
+
target.actor.performAction('notices a Matrix probe', 'their link flashing an intrusion warning');
|
|
138
|
+
return [`The key slips off ${target.actor.name}'s PAN -- and their link just pinged them a probe warning.`, ...godLines].join('\n');
|
|
139
|
+
}
|
|
140
|
+
// Placed.
|
|
141
|
+
if (target.kind === 'host') {
|
|
142
|
+
const now = (room.hostMarksBy.get(actor.name) ?? 0) + 1;
|
|
143
|
+
room.hostMarksBy.set(actor.name, now);
|
|
144
|
+
this.logger.write(`Mark: ${actor.name} -> host ${room.name} (${now}/${MAX_MARKS}, ${mode}).`);
|
|
145
|
+
return [
|
|
146
|
+
now >= MAX_MARKS
|
|
147
|
+
? `The ${now}${now === 3 ? 'rd' : 'th'} key settles into the host's lattice -- and the whole geometry RELAXES around your persona. As far as this host is concerned, you BELONG.${hint(` ("hack" walks in like staff.)`)}`
|
|
148
|
+
: `The key settles into the host's lattice -- ${now} mark${now === 1 ? '' : 's'} riding it now. Every one makes your crack easier.`,
|
|
149
|
+
...godLines,
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
const now = (target.actor.panMarksBy.get(actor.name) ?? 0) + 1;
|
|
153
|
+
target.actor.panMarksBy.set(actor.name, now);
|
|
154
|
+
this.logger.write(`Mark: ${actor.name} -> PAN ${target.actor.name} (${now}/${MAX_MARKS}, ${mode}).`);
|
|
155
|
+
return [
|
|
156
|
+
`The key settles onto ${target.actor.name}'s PAN unnoticed -- ${now} mark${now === 1 ? '' : 's'} riding it (+${now * 2} dice on your intrusions).${now >= MAX_MARKS ? ` Their network answers to you as much as to them.` : ``}`,
|
|
157
|
+
...godLines,
|
|
158
|
+
].join('\n');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=mark.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mark.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/commands/mark.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { resetOverwatch } from '../utilities/overwatch.js';
|
|
3
|
+
import { clearMarksOn, clearMarksPlacedBy } from '../utilities/marks.js';
|
|
3
4
|
/**
|
|
4
5
|
* The answer to a cracked PAN (see hack.ts): reboot your network. Clears
|
|
5
6
|
* every jammed device you carry and brings a hacked-dark AR overlay back
|
|
@@ -31,7 +32,8 @@ export class RebootCommand extends Command {
|
|
|
31
32
|
}
|
|
32
33
|
const hadArDown = actor.arOffline;
|
|
33
34
|
const hadOverwatch = actor.overwatchScore > 0;
|
|
34
|
-
|
|
35
|
+
const marksOnMe = actor.panMarksBy.size;
|
|
36
|
+
if (jammedGear.length === 0 && !hadArDown && !hadOverwatch && marksOnMe === 0) {
|
|
35
37
|
return `Your PAN is clean -- nothing needs a reboot.`;
|
|
36
38
|
}
|
|
37
39
|
for (const item of jammedGear) {
|
|
@@ -54,6 +56,14 @@ export class RebootCommand extends Command {
|
|
|
54
56
|
resetOverwatch(actor, 'PAN reboot');
|
|
55
57
|
lines.push(` • Overwatch tally ZEROED -- whatever GOD was counting, it belonged to a persona that no longer exists`);
|
|
56
58
|
}
|
|
59
|
+
// Marks die on both sides of a reboot (canon p.236): every key
|
|
60
|
+
// hanging on YOUR network shakes off, and every key YOU placed
|
|
61
|
+
// belonged to the persona that just ceased to exist.
|
|
62
|
+
if (marksOnMe > 0) {
|
|
63
|
+
clearMarksOn(actor);
|
|
64
|
+
lines.push(` • ${marksOnMe} foreign mark${marksOnMe === 1 ? '' : 's'} shaken off your PAN -- whoever held keys to your network doesn't anymore`);
|
|
65
|
+
}
|
|
66
|
+
clearMarksPlacedBy(this.scene, actor.name);
|
|
57
67
|
return lines.join('\n');
|
|
58
68
|
}
|
|
59
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reboot.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/commands/reboot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAY,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"reboot.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/commands/reboot.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,5 +29,9 @@
|
|
|
29
29
|
// hack forks Attack/Sleaze (canon p.236/238/240).
|
|
30
30
|
// 1.7.0 (2026-08-23): GOD -- Overwatch Score on illegal actions,
|
|
31
31
|
// convergence at 40, overwatch check verb, reboot/jack-out resets.
|
|
32
|
-
|
|
32
|
+
// 1.8.0 (2026-08-24): SINs & getting caught -- run heat, the KE
|
|
33
|
+
// checkpoint on the ride home, fake SINs in the catalog (v4).
|
|
34
|
+
// 1.9.0 (2026-08-24): marks (MAtrix Recognition Keys) -- the mark
|
|
35
|
+
// verb, softened hacks, reboot/convergence wipes.
|
|
36
|
+
export const ENGINE_VERSION = '1.9.0';
|
|
33
37
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../../../../../src/commands/game/sideQuest/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,sEAAsE;AACtE,mEAAmE;AACnE,oEAAoE;AACpE,sEAAsE;AACtE,6CAA6C;AAC7C,iEAAiE;AACjE,qCAAqC;AACrC,oEAAoE;AACpE,6DAA6D;AAC7D,uEAAuE;AACvE,kDAAkD;AAClD,iEAAiE;AACjE,mEAAmE;AACnE,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../../../../../src/commands/game/sideQuest/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,sEAAsE;AACtE,mEAAmE;AACnE,oEAAoE;AACpE,sEAAsE;AACtE,6CAA6C;AAC7C,iEAAiE;AACjE,qCAAqC;AACrC,oEAAoE;AACpE,6DAA6D;AAC7D,uEAAuE;AACvE,kDAAkD;AAClD,iEAAiE;AACjE,mEAAmE;AACnE,gEAAgE;AAChE,8DAA8D;AAC9D,kEAAkE;AAClE,kDAAkD;AAClD,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC"}
|
|
@@ -95,6 +95,13 @@ export default class Game {
|
|
|
95
95
|
* standing quote. Returns the line to show, or ''. */
|
|
96
96
|
scrapPendingRunForCrewChange(): string;
|
|
97
97
|
performedThisCycle: boolean;
|
|
98
|
+
private _runHeat;
|
|
99
|
+
private static readonly CHECKPOINT_HEAT;
|
|
100
|
+
/** Bill the district's attention. No-op at the hub or in classic
|
|
101
|
+
* (hubless) scenes -- only runs have a ride home to be caught on. */
|
|
102
|
+
addHeat(amount: number, why: string): void;
|
|
103
|
+
/** The HUD's read on the district (bands, never the number). */
|
|
104
|
+
private heatBand;
|
|
98
105
|
crewForHire: ICrewMember[];
|
|
99
106
|
private _presenceTimer?;
|
|
100
107
|
private static readonly PRESENCE_TICK_MS;
|
|
@@ -572,6 +579,24 @@ export default class Game {
|
|
|
572
579
|
* open combat bookkeeping) is settled on the way home.
|
|
573
580
|
*/
|
|
574
581
|
private returnToHub;
|
|
582
|
+
/**
|
|
583
|
+
* KNIGHT ERRANT CHECKPOINT (SINs & getting caught, ruling 2026-08-24;
|
|
584
|
+
* canon p.363-368, verification values engine-adapted -- the RAG was
|
|
585
|
+
* down): past CHECKPOINT_HEAT, the ride home hits the wire and the
|
|
586
|
+
* SIN mechanic resolves. A CRIMINAL SIN broadcasts before the wand
|
|
587
|
+
* lifts -- shakedown, citation, notoriety. Otherwise your best FAKE
|
|
588
|
+
* SIN is tested: the scanner rolls its dice (R3 wand, R4 on the
|
|
589
|
+
* hottest nights) and BEATS the fake's rating to burn it -- burned
|
|
590
|
+
* means gone from every database at once, then processing. No SIN at
|
|
591
|
+
* all: fast-talk (Charisma + con) or zip ties. Heat always resets --
|
|
592
|
+
* the night ends either way.
|
|
593
|
+
*/
|
|
594
|
+
private checkpointOnRideHome;
|
|
595
|
+
/** Processing: prints, retinas, a fee -- and for the SINless, the
|
|
596
|
+
* street's cruelest joke: they ISSUE you a SIN. A criminal one.
|
|
597
|
+
* Catch-and-release always (arrest never ends a career by fiat --
|
|
598
|
+
* the record it leaves does the long-term damage). */
|
|
599
|
+
private processArrest;
|
|
575
600
|
/**
|
|
576
601
|
* The actual drafting work, split out from continueToNextScene() purely
|
|
577
602
|
* so that method can wrap it in the try/finally that clears
|
|
@@ -106,6 +106,7 @@ import { DispelCommand } from './commands/dispel.js';
|
|
|
106
106
|
import { BreachCommand } from './commands/breach.js';
|
|
107
107
|
import { EmoteCommand } from './commands/emote.js';
|
|
108
108
|
import { OverwatchCommand } from './commands/overwatch.js';
|
|
109
|
+
import { MarkCommand } from './commands/mark.js';
|
|
109
110
|
import { AutoFight } from './utilities/auto-fight.js';
|
|
110
111
|
import { CommandFactory } from './factories/command-factory.js';
|
|
111
112
|
import { SceneSynthesizer } from './factories/scene-factory.js';
|
|
@@ -237,6 +238,35 @@ export default class Game {
|
|
|
237
238
|
// cycle, reset at homecoming like a contact's legwork. Deliberately
|
|
238
239
|
// NOT saved -- a relaunch is as good as a homecoming for new material.
|
|
239
240
|
performedThisCycle = false;
|
|
241
|
+
// HEAT (SINs & getting caught, ruling 2026-08-24): the meatspace
|
|
242
|
+
// Overwatch Score. Loud acts during a RUN bill it -- alarms, breach
|
|
243
|
+
// crashes, bodies dropped, GOD burning your location -- and the ride
|
|
244
|
+
// home is where the night catches up: past CHECKPOINT_HEAT the taxi
|
|
245
|
+
// hits a Knight Errant checkpoint and the SIN mechanic resolves
|
|
246
|
+
// (checkpointOnRideHome). Hub life never accrues heat (home turf),
|
|
247
|
+
// and a fresh run starts cold. Not saved: heat is a per-run debt.
|
|
248
|
+
_runHeat = 0;
|
|
249
|
+
static CHECKPOINT_HEAT = 6;
|
|
250
|
+
/** Bill the district's attention. No-op at the hub or in classic
|
|
251
|
+
* (hubless) scenes -- only runs have a ride home to be caught on. */
|
|
252
|
+
addHeat(amount, why) {
|
|
253
|
+
if (!this.hasHub || this.scene === this._hubScene)
|
|
254
|
+
return;
|
|
255
|
+
this._runHeat += amount;
|
|
256
|
+
Logger.getInstance().write(`Heat: +${amount} (${why}) -> ${this._runHeat}.`);
|
|
257
|
+
}
|
|
258
|
+
/** The HUD's read on the district (bands, never the number). */
|
|
259
|
+
heatBand() {
|
|
260
|
+
if (this._runHeat <= 0)
|
|
261
|
+
return undefined;
|
|
262
|
+
if (this._runHeat >= 12)
|
|
263
|
+
return `{red-fg}🔥 Heat: HTR assembling somewhere close{/red-fg}`;
|
|
264
|
+
if (this._runHeat >= Game.CHECKPOINT_HEAT)
|
|
265
|
+
return `{red-fg}🔥 Heat: sirens circling closer{/red-fg}`;
|
|
266
|
+
if (this._runHeat >= 3)
|
|
267
|
+
return `{yellow-fg}🔥 Heat: ears up all over the block{/yellow-fg}`;
|
|
268
|
+
return `{yellow-fg}🔥 Heat: somebody heard something{/yellow-fg}`;
|
|
269
|
+
}
|
|
240
270
|
// THE FOR-HIRE BOARD: archetype-built candidates, fresh faces each
|
|
241
271
|
// homecoming (the gig-pool rotation school). Not saved -- the street's
|
|
242
272
|
// talent pool churns; whoever you HIRED is the persistent part.
|
|
@@ -1668,6 +1698,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
1668
1698
|
// GOD's tally ("os" for the tabletop shorthand).
|
|
1669
1699
|
CommandFactory.registerCommand('overwatch', OverwatchCommand);
|
|
1670
1700
|
CommandFactory.registerCommand('os', OverwatchCommand);
|
|
1701
|
+
// MAtrix Recognition Keys (canon p.235-236).
|
|
1702
|
+
CommandFactory.registerCommand('mark', MarkCommand);
|
|
1671
1703
|
}
|
|
1672
1704
|
determineStartRoom() {
|
|
1673
1705
|
return this.scene.determineStartRoom();
|
|
@@ -2303,14 +2335,104 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2303
2335
|
deliveryLine = `\nWaiting with your gear: ${delivered.join(', ')}.`;
|
|
2304
2336
|
}
|
|
2305
2337
|
}
|
|
2338
|
+
// The night catches up on the ride (SINs & getting caught): a hot
|
|
2339
|
+
// run means the wire between there and home. Settled AFTER the
|
|
2340
|
+
// payout (the fee comes out of tonight's take) and BEFORE the save
|
|
2341
|
+
// below -- a burned fake or an issued SIN must survive a quit.
|
|
2342
|
+
const checkpointBlock = this.checkpointOnRideHome();
|
|
2306
2343
|
// The homecoming save: everything above (payout, rent, restock,
|
|
2307
|
-
// contacts reset, deliveries) settled first, so the
|
|
2308
|
-
// the hub as the player will find it.
|
|
2344
|
+
// contacts reset, deliveries, the checkpoint) settled first, so the
|
|
2345
|
+
// save reflects the hub as the player will find it.
|
|
2309
2346
|
this.requestSave('homecoming');
|
|
2310
2347
|
const fateLine = wrappedRun
|
|
2311
2348
|
? `The run's wrapped and the street swallows you back up.`
|
|
2312
2349
|
: `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
|
|
2313
|
-
return `\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}${clientLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}${allyLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
2350
|
+
return `${checkpointBlock ? `\n${checkpointBlock}\n` : ''}\n=== ${this._hubName.toUpperCase()} ===\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}${clientLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}${allyLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* KNIGHT ERRANT CHECKPOINT (SINs & getting caught, ruling 2026-08-24;
|
|
2354
|
+
* canon p.363-368, verification values engine-adapted -- the RAG was
|
|
2355
|
+
* down): past CHECKPOINT_HEAT, the ride home hits the wire and the
|
|
2356
|
+
* SIN mechanic resolves. A CRIMINAL SIN broadcasts before the wand
|
|
2357
|
+
* lifts -- shakedown, citation, notoriety. Otherwise your best FAKE
|
|
2358
|
+
* SIN is tested: the scanner rolls its dice (R3 wand, R4 on the
|
|
2359
|
+
* hottest nights) and BEATS the fake's rating to burn it -- burned
|
|
2360
|
+
* means gone from every database at once, then processing. No SIN at
|
|
2361
|
+
* all: fast-talk (Charisma + con) or zip ties. Heat always resets --
|
|
2362
|
+
* the night ends either way.
|
|
2363
|
+
*/
|
|
2364
|
+
checkpointOnRideHome() {
|
|
2365
|
+
const heat = this._runHeat;
|
|
2366
|
+
this._runHeat = 0;
|
|
2367
|
+
if (heat < Game.CHECKPOINT_HEAT)
|
|
2368
|
+
return '';
|
|
2369
|
+
const p = this.player;
|
|
2370
|
+
const lines = [
|
|
2371
|
+
``,
|
|
2372
|
+
`{red-fg}Halfway home, the traffic folds around a KNIGHT ERRANT CHECKPOINT -- light bars, a drone hovering, a scanner wand. The night you just had is all over the district feeds.{/red-fg}`,
|
|
2373
|
+
];
|
|
2374
|
+
if (p.qualities.has('sinner-criminal')) {
|
|
2375
|
+
const fine = Math.min(p.currency, 500 + heat * 100);
|
|
2376
|
+
if (fine > 0)
|
|
2377
|
+
p.adjustCurrency(-fine);
|
|
2378
|
+
p.addNotoriety('swept up at a KE checkpoint on a hot night');
|
|
2379
|
+
lines.push(`Your CRIMINAL SIN broadcasts before the wand even lifts -- they know the face, the file, the record. On a night this hot, that means the full treatment.`);
|
|
2380
|
+
lines.push(` Cited and released: ${fine > 0 ? `${fine} nuyen lighter` : `your sticks came up empty, so the citation rides your file instead`}, and the record grows. (Notoriety.)`);
|
|
2381
|
+
return lines.join('\n');
|
|
2382
|
+
}
|
|
2383
|
+
const verifierDice = heat >= 12 ? 8 : 6;
|
|
2384
|
+
const fakes = p.inventory.getAllItems()
|
|
2385
|
+
.map(i => ({ item: i, m: /^Fake SIN \(Rating (\d)\)$/.exec(i.name) }))
|
|
2386
|
+
.filter(x => x.m !== null)
|
|
2387
|
+
.map(x => ({ item: x.item, rating: parseInt(x.m[1], 10) }))
|
|
2388
|
+
.sort((a, b) => b.rating - a.rating);
|
|
2389
|
+
if (fakes.length > 0) {
|
|
2390
|
+
const fake = fakes[0];
|
|
2391
|
+
const scan = rollPool(verifierDice);
|
|
2392
|
+
Logger.getInstance().meta(`SIN verification (KE scanner, ${verifierDice}d6): ${formatRoll(scan)} (beats Rating ${fake.rating} to burn the fake)`);
|
|
2393
|
+
if (scan.hits <= fake.rating) {
|
|
2394
|
+
lines.push(`You hand over the Rating ${fake.rating} fake and hold a bored face. The wand chews on it... green light. "Move along."`);
|
|
2395
|
+
return lines.join('\n');
|
|
2396
|
+
}
|
|
2397
|
+
p.inventory.removeItem(fake.item.name);
|
|
2398
|
+
lines.push(`You hand over the Rating ${fake.rating} fake. The wand chews... and SPITS. "This SIN is flagged as of now." The fake is BURNED -- worthless in every database at once.`);
|
|
2399
|
+
lines.push(this.processArrest(heat));
|
|
2400
|
+
return lines.join('\n');
|
|
2401
|
+
}
|
|
2402
|
+
const con = p.skillRating('con');
|
|
2403
|
+
const talk = rollPool(Math.max(1, p.charisma + (con > 0 ? con : -1) + p.woundModifier));
|
|
2404
|
+
const need = heat >= 12 ? 4 : 3;
|
|
2405
|
+
Logger.getInstance().meta(`Fast talk at the wire (Charisma + con): ${formatRoll(talk)} (${need}+ slips through)`);
|
|
2406
|
+
if (talk.hits >= need) {
|
|
2407
|
+
lines.push(`No SIN at all -- so you become somebody's cousin, somebody's designated driver, a story so boring the officer waves you through mid-sentence.`);
|
|
2408
|
+
return lines.join('\n');
|
|
2409
|
+
}
|
|
2410
|
+
lines.push(`No SIN, and no story that holds. Zip ties.`);
|
|
2411
|
+
lines.push(this.processArrest(heat));
|
|
2412
|
+
return lines.join('\n');
|
|
2413
|
+
}
|
|
2414
|
+
/** Processing: prints, retinas, a fee -- and for the SINless, the
|
|
2415
|
+
* street's cruelest joke: they ISSUE you a SIN. A criminal one.
|
|
2416
|
+
* Catch-and-release always (arrest never ends a career by fiat --
|
|
2417
|
+
* the record it leaves does the long-term damage). */
|
|
2418
|
+
processArrest(heat) {
|
|
2419
|
+
const p = this.player;
|
|
2420
|
+
const lines = [];
|
|
2421
|
+
const fine = Math.min(p.currency, 1000 + heat * 100);
|
|
2422
|
+
if (fine > 0)
|
|
2423
|
+
p.adjustCurrency(-fine);
|
|
2424
|
+
lines.push(`Processing: prints, retinas, a genome swab, every camera in the room.${fine > 0 ? ` The "administrative fee" is ${fine} nuyen.` : ` Your sticks come up empty; the fee rides your new file.`}`);
|
|
2425
|
+
if (!p.qualities.has('sinner-criminal') && !p.qualities.has('sinner-national') && !p.qualities.has('sinner-corp')) {
|
|
2426
|
+
p.addQuality('sinner-criminal');
|
|
2427
|
+
lines.push(`{red-fg}And the worst of it: they ISSUE you a SIN. A CRIMINAL SIN -- broadcast always, 15% of every payout withheld, forever in the system. Get caught being nobody, become somebody with a record.{/red-fg}`);
|
|
2428
|
+
}
|
|
2429
|
+
else {
|
|
2430
|
+
lines.push(`The record deepens. (Notoriety.)`);
|
|
2431
|
+
}
|
|
2432
|
+
p.addNotoriety('arrested on the ride home');
|
|
2433
|
+
lines.push(`They cut you loose before dawn -- catch-and-release. Holding a runner costs more than the paperwork is worth.`);
|
|
2434
|
+
Logger.getInstance().write(`Checkpoint arrest: fine ${fine}, heat ${heat}, criminal SIN ${p.qualities.has('sinner-criminal')}.`);
|
|
2435
|
+
return lines.join('\n');
|
|
2314
2436
|
}
|
|
2315
2437
|
/**
|
|
2316
2438
|
* The actual drafting work, split out from continueToNextScene() purely
|
|
@@ -2433,6 +2555,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2433
2555
|
*/
|
|
2434
2556
|
async activateRunScene(seed, tier, opts) {
|
|
2435
2557
|
this._tier = tier;
|
|
2558
|
+
// A fresh run starts COLD -- heat is a per-night debt.
|
|
2559
|
+
this._runHeat = 0;
|
|
2436
2560
|
this.sceneSeed = seed;
|
|
2437
2561
|
// A call partner from the old scene wouldn't exist in the new one --
|
|
2438
2562
|
// hang up before moving on, rather than leave a dangling reference.
|
|
@@ -2977,6 +3101,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2977
3101
|
const stunColor = trackColor(player.stunTaken, player.maxStunBoxes);
|
|
2978
3102
|
lines.push(`{${physColor}-fg}Physical ${player.conditionSummary()}{/${physColor}-fg} ` +
|
|
2979
3103
|
`{${stunColor}-fg}Stun ${player.stunSummary()}{/${stunColor}-fg}${woundNote}`);
|
|
3104
|
+
// HEAT (SINs & getting caught): the district's attention, in bands
|
|
3105
|
+
// -- the number stays the GM's. Absent entirely while cold, so hub
|
|
3106
|
+
// life never shows it.
|
|
3107
|
+
const heat = this.heatBand();
|
|
3108
|
+
if (heat && player === this.player)
|
|
3109
|
+
lines.push(heat);
|
|
2980
3110
|
// Line 3: readiness -- armed state, armor, and whether the commlink is
|
|
2981
3111
|
// actually live (equipped), the recurring "why can't I call anyone"
|
|
2982
3112
|
// friction surfaced at a glance.
|
|
@@ -3279,7 +3409,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3279
3409
|
// players" / "crew street"); the bare verb still answers quietly.
|
|
3280
3410
|
{ title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
|
|
3281
3411
|
{ title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
|
|
3282
|
-
{ title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['overwatch', 'os'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
3412
|
+
{ title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['overwatch', 'os'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
|
|
3283
3413
|
// The Emerged get their own shelf (player request: "there MUST be
|
|
3284
3414
|
// resonance" -- matrix is the place, Resonance is the talent).
|
|
3285
3415
|
{ title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['register']] },
|