@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.
Files changed (44) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +2 -0
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +2 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +9 -0
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +10 -3
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +49 -45
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.d.ts +18 -0
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +161 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js +11 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js.map +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +5 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +25 -0
  21. package/bundle/typescript/src/commands/game/sideQuest/game.js +134 -4
  22. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +1 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +11 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +1 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +1 -1
  30. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +6 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +6 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.d.ts +16 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +59 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js.map +1 -0
  37. package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.d.ts +20 -0
  38. package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +50 -0
  39. package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js.map +1 -0
  40. package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +6 -0
  41. package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js.map +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +4 -1
  43. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  44. 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
- if (jammedGear.length === 0 && !hadArDown && !hadOverwatch) {
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":"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"}
1
+ {"version":3,"file":"reboot.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/commands/reboot.ts"],"names":[],"mappings":"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"}
@@ -16,4 +16,4 @@
16
16
  * No import.meta here (engine-graph law) -- this file is loaded by the
17
17
  * server.
18
18
  */
19
- export declare const ENGINE_VERSION = "1.7.0";
19
+ export declare const ENGINE_VERSION = "1.9.0";
@@ -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
- export const ENGINE_VERSION = '1.7.0';
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 save reflects
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']] },