@maka/maka-cli 5.199.0 → 5.201.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +139 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +115 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +19 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +48 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/presence-gate.js +27 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
- package/package.json +1 -1
|
@@ -621,6 +621,21 @@ export class Player extends AbstractPlayer {
|
|
|
621
621
|
// against me (hack.ts). My "reboot" wipes them all; a placer's own
|
|
622
622
|
// reboot/jack-out/convergence wipes just theirs (utilities/marks.ts).
|
|
623
623
|
panMarksBy = new Map();
|
|
624
|
+
/**
|
|
625
|
+
* COOKIE (SR5 p.256-257, utilities/sprite-power-actions.ts): sprite
|
|
626
|
+
* name -> net hits, which is the DEPTH the report carries when it ships
|
|
627
|
+
* home (one hit a bare outline, four or more a detailed report). The
|
|
628
|
+
* carrier does not know it is there; a Matrix Perception on them finds
|
|
629
|
+
* it. Session state, like the marks above -- a sprite's work lasts as
|
|
630
|
+
* long as the sprite.
|
|
631
|
+
*/
|
|
632
|
+
cookiedBy = new Map();
|
|
633
|
+
/**
|
|
634
|
+
* DIAGNOSTICS (p.257): the Teamwork bonus a machine sprite is holding
|
|
635
|
+
* on ONE device -- "it takes the sprite's entire attention", so there
|
|
636
|
+
* is at most one, and giving that sprite any other order drops it.
|
|
637
|
+
*/
|
|
638
|
+
spriteAssist;
|
|
624
639
|
// OVERWATCH SCORE (GOD, canon p.231-232, see utilities/overwatch.ts):
|
|
625
640
|
// evidence tally of this actor's illegal Matrix actions. Starts at 0
|
|
626
641
|
// on a fresh boot, grows with every Attack/Sleaze action (and a
|
|
@@ -704,6 +719,27 @@ export class Player extends AbstractPlayer {
|
|
|
704
719
|
return true;
|
|
705
720
|
return this.dniSource() !== null;
|
|
706
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* COULD THIS RUNNER RIDE THE GRID AT ALL -- the question commands/jack.ts
|
|
724
|
+
* answers in three separate refusals, asked once so ADVICE can be
|
|
725
|
+
* written for the right audience.
|
|
726
|
+
*
|
|
727
|
+
* Added for the loose-end nag (user 2026-09-15): the hub told every
|
|
728
|
+
* runner to "jack in, then hop" and erase the footage, including a
|
|
729
|
+
* street samurai with no deck and no datajack, who therefore got a
|
|
730
|
+
* consequence with no lever. A line that recommends a route has to
|
|
731
|
+
* know whether the reader can take it.
|
|
732
|
+
*
|
|
733
|
+
* Deliberately NOT the same as "may jack in right now" -- a bricked
|
|
734
|
+
* deck is a repair, not a disqualification, and this is about what the
|
|
735
|
+
* character can ever do rather than what they can do this minute.
|
|
736
|
+
*/
|
|
737
|
+
canReachMatrix() {
|
|
738
|
+
if (this.isTechnomancer())
|
|
739
|
+
return true;
|
|
740
|
+
const deck = this.getCyberdeck() ?? this.getAnyCyberdeck();
|
|
741
|
+
return deck !== undefined && this.hasDirectNeuralInterface();
|
|
742
|
+
}
|
|
707
743
|
/** Failed reads of a clue, by item name, for p.49's Trying Again
|
|
708
744
|
* (-2 per retry). In memory only: a save and reload is the "break"
|
|
709
745
|
* the rule says clears it. See utilities/clue-reading.ts. */
|
|
@@ -1173,6 +1209,14 @@ export class Player extends AbstractPlayer {
|
|
|
1173
1209
|
}
|
|
1174
1210
|
/** Routes Matrix combat damage into the jacked-in deck's hardware track. */
|
|
1175
1211
|
takeMatrixDamage(boxes) {
|
|
1212
|
+
// ELECTRON STORM ENDS WHEN ITS SPRITE IS HURT (SR5 p.257: "if the
|
|
1213
|
+
// sprite takes any Matrix damage, all of its electron storms end
|
|
1214
|
+
// immediately"). A sprite is an NPC, which is a Player, so the rule
|
|
1215
|
+
// lives on the one method every Matrix hit goes through rather than
|
|
1216
|
+
// in each thing that can deal one.
|
|
1217
|
+
const self = this;
|
|
1218
|
+
if (boxes > 0 && self.sustainedStorm)
|
|
1219
|
+
self.sustainedStorm = undefined;
|
|
1176
1220
|
const deck = this.getAnyCyberdeck();
|
|
1177
1221
|
const taken = deck ? deck.takeDeckDamage(boxes) : 0;
|
|
1178
1222
|
// DECK DAMAGE BEATS (web report 2026-09-02: "the damage to the deck is not moving in realtime"):
|
|
@@ -27,18 +27,28 @@
|
|
|
27
27
|
* sprite type, and the five types' assignments account for all nine
|
|
28
28
|
* exactly once -- the book's own list closes.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
* is wired to a die roll in
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
30
|
+
* THIS TABLE IS NOW THE CATALOGUE BEHIND REAL VERBS (2026-09-15). It
|
|
31
|
+
* shipped as reference only -- "no power below is wired to a die roll in
|
|
32
|
+
* this engine yet", and sprites.ts said so on the panel rather than
|
|
33
|
+
* implying otherwise. Eight of the nine are moves you can type now:
|
|
34
|
+
* utilities/sprite-power-actions.ts resolves them, keyed off the same
|
|
35
|
+
* five sprite keys, so the panel and the mechanics cannot drift.
|
|
36
|
+
*
|
|
37
|
+
* STABILITY IS DELIBERATELY STILL REFERENCE, and `runnable` below is how
|
|
38
|
+
* each entry says which it is. Stability demotes glitches, and glitches
|
|
39
|
+
* are read at sixteen scattered sites in this engine (rest, climbing,
|
|
40
|
+
* haggling, barrier combat, hiring...). Wiring it at three of them would
|
|
41
|
+
* produce a power that works where someone happened to look and silently
|
|
42
|
+
* does nothing everywhere else -- which is exactly the "wrong ability on
|
|
43
|
+
* a sprite is worse than a missing one" case (CLAUDE.md). It wants the
|
|
44
|
+
* glitch contract changed in one place first, and that is its own piece
|
|
45
|
+
* of work.
|
|
37
46
|
*/
|
|
38
47
|
/** All nine, alphabetical as the book lists them (p.256). */
|
|
39
48
|
export const SPRITE_POWERS = [
|
|
40
49
|
{
|
|
41
50
|
key: 'camouflage',
|
|
51
|
+
runnable: true,
|
|
42
52
|
name: 'Camouflage',
|
|
43
53
|
effect: 'Hides a file inside another file, invisible to Matrix searches. Only a Matrix Perception Test specifically hunting the hidden file will turn it up -- and the sprite has to make that test too, to get its own file back out.',
|
|
44
54
|
test: undefined,
|
|
@@ -46,6 +56,7 @@ export const SPRITE_POWERS = [
|
|
|
46
56
|
},
|
|
47
57
|
{
|
|
48
58
|
key: 'cookie',
|
|
59
|
+
runnable: true,
|
|
49
60
|
name: 'Cookie',
|
|
50
61
|
effect: 'Tags a persona with a silent tracking file rated at the sprite\'s Level. It logs every host entered, every program used, and who was talked to and when (not what was said). Net hits set the depth: one hit is a bare outline, four or more a detailed report. At a time set when it was placed the file ships itself back to the sprite -- and deletes itself instead if the sprite has left the Matrix. A Matrix Perception Test on the carrier finds it; unprotecting it and deleting it removes it.',
|
|
51
62
|
test: 'Hacking + Resonance [Sleaze] v. Intuition + Firewall',
|
|
@@ -53,6 +64,7 @@ export const SPRITE_POWERS = [
|
|
|
53
64
|
},
|
|
54
65
|
{
|
|
55
66
|
key: 'diagnostics',
|
|
67
|
+
runnable: true,
|
|
56
68
|
name: 'Diagnostics',
|
|
57
69
|
effect: 'Reads a device down to its seams and helps somebody use or repair it, as a Teamwork Test. Any hits give +1 limit, and each hit adds a die to their pool. It takes the sprite\'s whole attention -- the bonus lasts until it drops the power or does anything else.',
|
|
58
70
|
test: 'Simple Hardware + Level [Data Processing]',
|
|
@@ -60,6 +72,7 @@ export const SPRITE_POWERS = [
|
|
|
60
72
|
},
|
|
61
73
|
{
|
|
62
74
|
key: 'electron-storm',
|
|
75
|
+
runnable: true,
|
|
63
76
|
name: 'Electron Storm',
|
|
64
77
|
effect: 'Engulfs a persona in a sustained barrage of corrupting datastreams. On the first hit and on every action the sprite spends sustaining it, the target takes (Resonance) DV Matrix damage, resisted as normal, plus 2 points of noise. If the sprite takes any Matrix damage of its own, every storm it is sustaining ends at once.',
|
|
65
78
|
test: 'Cybercombat + Resonance [Attack] v. Intuition + Firewall',
|
|
@@ -67,6 +80,7 @@ export const SPRITE_POWERS = [
|
|
|
67
80
|
},
|
|
68
81
|
{
|
|
69
82
|
key: 'gremlins',
|
|
83
|
+
runnable: true,
|
|
70
84
|
name: 'Gremlins',
|
|
71
85
|
effect: 'Makes a device misbehave -- a jammed control, a looped signal, a faulty reading. Success is a glitch; four or more net hits is a critical one, and the device crashes, burns out, or bites its user.',
|
|
72
86
|
test: 'Hardware + Level [Attack] v. Device Rating + Firewall',
|
|
@@ -74,6 +88,7 @@ export const SPRITE_POWERS = [
|
|
|
74
88
|
},
|
|
75
89
|
{
|
|
76
90
|
key: 'hash',
|
|
91
|
+
runnable: true,
|
|
77
92
|
name: 'Hash',
|
|
78
93
|
effect: 'Seals a file with a Resonance algorithm only that sprite can undo, for up to (Level x 10) Combat Turns. Put the file down and it reverts to normal. Destroy the sprite while it still carries the file and the file is corrupted for good.',
|
|
79
94
|
test: undefined,
|
|
@@ -81,6 +96,7 @@ export const SPRITE_POWERS = [
|
|
|
81
96
|
},
|
|
82
97
|
{
|
|
83
98
|
key: 'stability',
|
|
99
|
+
runnable: false,
|
|
84
100
|
name: 'Stability',
|
|
85
101
|
effect: 'Steadies any persona or device the sprite holds a mark on: standard glitches are ignored outright and critical glitches drop to standard. It stops induced accidents too, Gremlins included -- which is why a machine sprite can both cause and prevent them.',
|
|
86
102
|
test: undefined,
|
|
@@ -88,6 +104,7 @@ export const SPRITE_POWERS = [
|
|
|
88
104
|
},
|
|
89
105
|
{
|
|
90
106
|
key: 'suppression',
|
|
107
|
+
runnable: true,
|
|
91
108
|
name: 'Suppression',
|
|
92
109
|
effect: 'Baffles a host into hesitating. A sprite using this inside a host delays any IC it launches by (Level / 2) Combat Turns, and delayed IC can neither act nor be targeted.',
|
|
93
110
|
test: undefined,
|
|
@@ -95,6 +112,7 @@ export const SPRITE_POWERS = [
|
|
|
95
112
|
},
|
|
96
113
|
{
|
|
97
114
|
key: 'watermark',
|
|
115
|
+
runnable: true,
|
|
98
116
|
name: 'Watermark',
|
|
99
117
|
effect: 'Marks an icon invisibly, readable only by Resonance-driven things -- a way to leave messages on Matrix objects that nobody else can see. A new watermark overwrites an old one, and it lasts as long as the icon does unless somebody erases the Matrix signature.',
|
|
100
118
|
test: undefined,
|
|
@@ -108,6 +126,29 @@ export const SPRITE_POWER_SETS = [
|
|
|
108
126
|
{ spriteKey: 'fault', powers: ['electron-storm'], skills: ['Computer', 'Cybercombat', 'Hacking'], page: 'p.259' },
|
|
109
127
|
{ spriteKey: 'machine', powers: ['diagnostics', 'gremlins', 'stability'], skills: ['Computer', 'Electronic Warfare', 'Hardware'], page: 'p.259' },
|
|
110
128
|
];
|
|
129
|
+
const MATRIX_BY_TYPE = {
|
|
130
|
+
data: l => ({ attack: l - 1, sleaze: l, dataProcessing: l + 4, firewall: l + 1 }),
|
|
131
|
+
fault: l => ({ attack: l + 3, sleaze: l, dataProcessing: l + 1, firewall: l + 2 }),
|
|
132
|
+
machine: l => ({ attack: l + 1, sleaze: l, dataProcessing: l + 3, firewall: l + 2 }),
|
|
133
|
+
crack: l => ({ attack: l, sleaze: l + 3, dataProcessing: l + 2, firewall: l + 1 }),
|
|
134
|
+
courier: l => ({ attack: l, sleaze: l + 3, dataProcessing: l + 1, firewall: l + 2 }),
|
|
135
|
+
};
|
|
136
|
+
export function spriteMatrix(spriteKey, level) {
|
|
137
|
+
const l = Math.max(1, level);
|
|
138
|
+
const build = MATRIX_BY_TYPE[spriteKey] ?? MATRIX_BY_TYPE.courier;
|
|
139
|
+
const attrs = build(l);
|
|
140
|
+
return {
|
|
141
|
+
attack: Math.max(0, attrs.attack),
|
|
142
|
+
sleaze: Math.max(0, attrs.sleaze),
|
|
143
|
+
dataProcessing: Math.max(0, attrs.dataProcessing),
|
|
144
|
+
firewall: Math.max(0, attrs.firewall),
|
|
145
|
+
resonance: l,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** A sprite's Matrix condition monitor: 8 + (Level / 2) boxes (p.254). */
|
|
149
|
+
export function spriteMatrixBoxes(level) {
|
|
150
|
+
return 8 + Math.floor(Math.max(1, level) / 2);
|
|
151
|
+
}
|
|
111
152
|
/** One power by key. */
|
|
112
153
|
export function spritePower(key) {
|
|
113
154
|
return SPRITE_POWERS.find(p => p.key === key);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAYING SOMEBODY ELSE TO SCRUB THE FOOTAGE (user 2026-09-15: "I'd like
|
|
3
|
+
* 'cleanup' jobs to allow hiring a crew. This is for situations where the
|
|
4
|
+
* runner (say, Jynx -- street samurai) cannot access the matrix").
|
|
5
|
+
*
|
|
6
|
+
* THE HOLE THIS FILLS. A run that ends with live cameras leaves an
|
|
7
|
+
* ILooseEnd, and the hub nags about it every homecoming -- "Doc Wu's
|
|
8
|
+
* Clinic still has your face from 'Ash and Ledger'" -- while the only
|
|
9
|
+
* route it advertises is closed to most runners. `hop` needs a persona
|
|
10
|
+
* (commands/hop.ts), `jack in` needs a cyberdeck AND a direct neural
|
|
11
|
+
* interface (commands/jack.ts), and the cleanup scene is deliberately
|
|
12
|
+
* crew-free: "NO CLIENT, NO PAYOUT, NO NPCS" (factories/cleanup-seed.ts).
|
|
13
|
+
* So a street samurai was handed a consequence with no lever, and a
|
|
14
|
+
* +1 Public Awareness on a timer.
|
|
15
|
+
*
|
|
16
|
+
* WHY AN ERRAND AND NOT A CREW SEAT. The playable scene stays exactly as
|
|
17
|
+
* it is. Hiring a shadow decker through a fixer is the SR5 idiom, it
|
|
18
|
+
* respects the no-payout design (you PAY here -- there is no client to
|
|
19
|
+
* bill), and it does not need enterCleanup un-wired from the player or
|
|
20
|
+
* a crew member taught to cross into a Matrix scene. What it costs is
|
|
21
|
+
* nuyen and a little control: you are not there, so you do not get to
|
|
22
|
+
* roll for it.
|
|
23
|
+
*
|
|
24
|
+
* IT RIDES THE LOOSE END ITSELF (ILooseEnd.hired), which is already on
|
|
25
|
+
* the save -- so a hired errand survives a reload with no new save field
|
|
26
|
+
* and no SAVE_VERSION bump, and settleLooseEnds resolves it at the next
|
|
27
|
+
* homecoming like everything else on that record.
|
|
28
|
+
*/
|
|
29
|
+
/** What a scrub costs before haggling. */
|
|
30
|
+
export function cleanupQuote(le) {
|
|
31
|
+
// A harder host is a harder job: the decker has to get through the same
|
|
32
|
+
// ice the runner would have. Rooms are how much footage there is to
|
|
33
|
+
// find. Round to something a fixer would actually say out loud.
|
|
34
|
+
const base = 1500 + (le.host.rating * 400) + (Math.max(1, le.rooms.length) * 250);
|
|
35
|
+
return Math.round(base / 100) * 100;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The decker's own test, rolled at the HOMECOMING rather than at hire
|
|
39
|
+
* time -- you paid for a result you do not get to watch.
|
|
40
|
+
*
|
|
41
|
+
* The pool is the fixer's reach (Connection) plus how much they care
|
|
42
|
+
* (Loyalty): a well-connected fixer knows a better decker, and one who
|
|
43
|
+
* likes you sends them sober. Against the host's rating, which is what
|
|
44
|
+
* the decker has to beat.
|
|
45
|
+
*
|
|
46
|
+
* LEVERAGE IS NOT A FAILURE. The file is gone either way; the difference
|
|
47
|
+
* is that somebody else now has a copy, which is a hook rather than a
|
|
48
|
+
* penalty -- the shadows run on favours owed.
|
|
49
|
+
*/
|
|
50
|
+
export function cleanupOutcome(netHits, criticalGlitch) {
|
|
51
|
+
if (criticalGlitch)
|
|
52
|
+
return 'botched';
|
|
53
|
+
if (netHits <= 0)
|
|
54
|
+
return 'botched';
|
|
55
|
+
return netHits >= 3 ? 'clean' : 'leverage';
|
|
56
|
+
}
|
|
57
|
+
/** The names a fixer's shadow decker answers to. Flavour, not mechanics. */
|
|
58
|
+
export const SHADOW_DECKERS = ['Static', 'Nyx', 'Palimpsest', 'Ghostwrite', 'Null', 'Sundog'];
|
|
59
|
+
export function pickShadowDecker(le) {
|
|
60
|
+
// Deterministic off the record's own id, so the quote and the job name
|
|
61
|
+
// the same person -- a fixer who offers "Static" does not send "Nyx".
|
|
62
|
+
let h = 0;
|
|
63
|
+
for (const ch of le.id)
|
|
64
|
+
h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
65
|
+
return SHADOW_DECKERS[h % SHADOW_DECKERS.length];
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=cleanup-errand.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { rollPool, formatRoll } from './dice.js';
|
|
2
|
+
import { fuzzyPickName } from './fuzzy-match.js';
|
|
3
|
+
import { cleanupQuote, pickShadowDecker } from './cleanup-errand.js';
|
|
4
|
+
import { hint } from './hints.js';
|
|
5
|
+
/**
|
|
6
|
+
* "ask <fixer> about cleanup" and "hire cleanup [<site>]" -- the two
|
|
7
|
+
* halves of paying somebody else to scrub your footage. The rules and
|
|
8
|
+
* pricing live in cleanup-errand.ts; this is the player-facing surface,
|
|
9
|
+
* kept out of both crew.ts and ask.ts because it belongs to neither.
|
|
10
|
+
*/
|
|
11
|
+
/** The best-placed contact to broker it: reach first, then how much they
|
|
12
|
+
* like you. A fixer with no reach knows no deckers. */
|
|
13
|
+
function brokerFor(game) {
|
|
14
|
+
let best;
|
|
15
|
+
for (const [name, c] of game.contacts.entries()) {
|
|
16
|
+
const score = c.connection * 2 + c.loyalty;
|
|
17
|
+
const bestScore = best ? best.connection * 2 + best.loyalty : -1;
|
|
18
|
+
if (score > bestScore)
|
|
19
|
+
best = { name, connection: c.connection, loyalty: c.loyalty };
|
|
20
|
+
}
|
|
21
|
+
return best;
|
|
22
|
+
}
|
|
23
|
+
/** The open loose end this request means -- named, or the freshest. */
|
|
24
|
+
function pickLooseEnd(game, query) {
|
|
25
|
+
const open = game.looseEnds.filter(le => !le.hired);
|
|
26
|
+
if (open.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (!query)
|
|
29
|
+
return open[open.length - 1];
|
|
30
|
+
const picked = fuzzyPickName(query, open.map(le => le.site));
|
|
31
|
+
return picked ? open.find(le => le.site === picked) : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* THE QUOTE (reached from "ask <contact> about cleanup"). No dice, no
|
|
35
|
+
* commitment, and no per-homecoming budget spent: this is a price, not
|
|
36
|
+
* legwork.
|
|
37
|
+
*/
|
|
38
|
+
export function cleanupQuoteFor(game, contactName, query = '') {
|
|
39
|
+
const le = pickLooseEnd(game, query);
|
|
40
|
+
if (!le) {
|
|
41
|
+
return game.looseEnds.length > 0
|
|
42
|
+
? `"Already got someone on that one," ${contactName} says. "Sit tight."`
|
|
43
|
+
: `"Nothing of yours is sitting on anybody's host right now," ${contactName} says. "Enjoy it."`;
|
|
44
|
+
}
|
|
45
|
+
const contact = game.contacts.get(contactName);
|
|
46
|
+
if (!contact)
|
|
47
|
+
return undefined;
|
|
48
|
+
const decker = pickShadowDecker(le);
|
|
49
|
+
const price = cleanupQuote(le);
|
|
50
|
+
return [
|
|
51
|
+
`"${le.site}?" ${contactName} thinks about it. "Got a kid calls themselves ${decker} who scrubs feeds. `,
|
|
52
|
+
`Rating ${le.host.rating} host, ${le.rooms.length} room${le.rooms.length === 1 ? '' : 's'} of it -- ${price} nuyen and your face was never there."`,
|
|
53
|
+
hint(` ("hire cleanup${game.looseEnds.filter(l => !l.hired).length > 1 ? ` ${le.site}` : ''}" to send them in. Negotiation trims the price; the job lands when you next come home.)`),
|
|
54
|
+
].join('');
|
|
55
|
+
}
|
|
56
|
+
/** THE HIRE: haggle, debit, stamp the record. The job itself resolves at
|
|
57
|
+
* the next homecoming (Game.settleHiredCleanup). */
|
|
58
|
+
export function hireCleanupErrand(actor, game, query, logger) {
|
|
59
|
+
const le = pickLooseEnd(game, query);
|
|
60
|
+
if (!le) {
|
|
61
|
+
const hiredAlready = game.looseEnds.some(le2 => le2.hired);
|
|
62
|
+
if (hiredAlready)
|
|
63
|
+
return `That one's already bought and paid for -- it lands when you next come home.`;
|
|
64
|
+
if (query)
|
|
65
|
+
return `Nothing of yours is sitting on a host called "${query}".`;
|
|
66
|
+
return `Nothing of yours is on anybody's host right now.${hint(` (A run that ends with the cameras still live leaves footage behind -- that is what this buys off.)`)}`;
|
|
67
|
+
}
|
|
68
|
+
const broker = brokerFor(game);
|
|
69
|
+
if (!broker) {
|
|
70
|
+
return `You don't know anybody who knows a decker.${hint(` (Contacts broker this -- trade with people, run their errands. "contacts" shows who talks to you.)`)}`;
|
|
71
|
+
}
|
|
72
|
+
const asking = cleanupQuote(le);
|
|
73
|
+
const decker = pickShadowDecker(le);
|
|
74
|
+
// ONE NEGOTIATION PASS, the same fence-counter school "hire" uses on a
|
|
75
|
+
// crew cut -- against the fixer's own Charisma-ish reach.
|
|
76
|
+
const negotiation = actor.skillRating('negotiation');
|
|
77
|
+
const mine = rollPool(Math.max(1, actor.charisma + (negotiation > 0 ? negotiation : -1)
|
|
78
|
+
+ actor.socialRep + actor.bonus('social') + actor.woundModifier - actor.sustainingPenalty));
|
|
79
|
+
const theirs = rollPool(Math.max(1, broker.connection + 2));
|
|
80
|
+
const net = mine.hits - theirs.hits;
|
|
81
|
+
// A critical glitch talks the price UP -- you argued badly and they
|
|
82
|
+
// remembered it (the shape haggle.ts and hire already use).
|
|
83
|
+
const price = mine.criticalGlitch
|
|
84
|
+
? Math.round(asking * 1.2 / 100) * 100
|
|
85
|
+
: Math.max(Math.round(asking * 0.6 / 100) * 100, asking - Math.max(0, net) * 200);
|
|
86
|
+
logger.meta(`Cleanup terms -- you (Cha + Negotiation): ${formatRoll(mine)}`);
|
|
87
|
+
logger.meta(` ${broker.name} (Connection ${broker.connection}): ${formatRoll(theirs)} -- ${price} nuyen (asked ${asking})`);
|
|
88
|
+
if (actor.currency < price) {
|
|
89
|
+
return `${broker.name} names ${price} nuyen for ${le.site}. You're carrying ${actor.currency}.${hint(` (Nobody scrubs a host on credit.)`)}`;
|
|
90
|
+
}
|
|
91
|
+
actor.adjustCurrency(-price);
|
|
92
|
+
game.hireCleanup(le, {
|
|
93
|
+
fixer: broker.name,
|
|
94
|
+
price,
|
|
95
|
+
decker,
|
|
96
|
+
// Banked now so a loyalty shift before the homecoming cannot
|
|
97
|
+
// retroactively change a job already bought.
|
|
98
|
+
pool: broker.connection + broker.loyalty,
|
|
99
|
+
});
|
|
100
|
+
actor.performAction('hires a scrub', `${decker} on ${le.site}'s feeds`);
|
|
101
|
+
game.requestSave?.('cleanup');
|
|
102
|
+
const haggled = price < asking
|
|
103
|
+
? `talked down from ${asking}`
|
|
104
|
+
: price > asking
|
|
105
|
+
? `talked UP from ${asking} -- they didn't like your tone`
|
|
106
|
+
: `at the asking price`;
|
|
107
|
+
return [
|
|
108
|
+
`{light-blue-fg}${broker.name} makes one call and hangs up. "${decker}'s on it."{/light-blue-fg}`,
|
|
109
|
+
`\nYou slide ${price} nuyen across (${haggled}). ${actor.currency} left.`,
|
|
110
|
+
hint(` (Nothing to watch -- you're not the one going in. ${decker} reports back when you next come home from a job.)`),
|
|
111
|
+
].join('');
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=cleanup-hire.js.map
|
|
@@ -246,7 +246,15 @@ export function droneReconNowhereLine(item) {
|
|
|
246
246
|
missile: `The ${item.name} climbs, circles once -- nowhere to go from here.`,
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
/**
|
|
250
|
+
* THE PROSE NO LONGER CARRIES THE DICE (wjFBY3zgyWzJP9adw sweep). This
|
|
251
|
+
* baked `Piloting: 8d6 -> 2 hits` into the world log while every other
|
|
252
|
+
* player test in the engine sends its anatomy to the Mechanics panel --
|
|
253
|
+
* the recon sweep was the one roll a rigger had to read out of a
|
|
254
|
+
* sentence. `rollText` is gone; commands/drone.ts emits the roll through
|
|
255
|
+
* logger.meta, and this line goes back to being prose.
|
|
256
|
+
*/
|
|
257
|
+
export function droneReconLaunchLine(item, ok) {
|
|
250
258
|
const off = pick(item, {
|
|
251
259
|
'flier-palm': `The ${item.name} whirs off your palm`,
|
|
252
260
|
flier: `The ${item.name} lifts off your hand`,
|
|
@@ -256,7 +264,7 @@ export function droneReconLaunchLine(item, rollText, ok) {
|
|
|
256
264
|
missile: `The ${item.name} kicks off its rail`,
|
|
257
265
|
});
|
|
258
266
|
if (!ok)
|
|
259
|
-
return
|
|
267
|
+
return off;
|
|
260
268
|
const threads = pick(item, {
|
|
261
269
|
'flier-palm': 'threads the gaps',
|
|
262
270
|
flier: 'threads the gaps',
|
|
@@ -265,7 +273,7 @@ export function droneReconLaunchLine(item, rollText, ok) {
|
|
|
265
273
|
anthro: 'goes to look',
|
|
266
274
|
missile: 'makes a fast pass',
|
|
267
275
|
});
|
|
268
|
-
return `${off} and ${threads}
|
|
276
|
+
return `${off} and ${threads}. Feed incoming:`;
|
|
269
277
|
}
|
|
270
278
|
export function droneReconReturnLine(item) {
|
|
271
279
|
return pick(item, {
|
|
@@ -77,6 +77,48 @@ export function hostLabel(room, opts = {}) {
|
|
|
77
77
|
const bare = room.name.replace(/^the\s+/i, '');
|
|
78
78
|
return `${opts.capital ? 'The' : 'the'} ${bare} host`;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* WHAT THE HOST RUNS ON, ONCE YOU HAVE PAID FOR IT (np6YBSPsdfAY7NcuZ,
|
|
82
|
+
* Maka: "data like this ... shouldn't be visible without a matrix search
|
|
83
|
+
* on the host").
|
|
84
|
+
*
|
|
85
|
+
* `Host.purpose` is seed content and the generator is told to fill it
|
|
86
|
+
* with "what this place runs on its host and so what is worth protecting
|
|
87
|
+
* inside it" (factories/scene-chunks.ts) -- so in practice it enumerates
|
|
88
|
+
* the archive: "patient intake logs, med supply orders, and the doc's
|
|
89
|
+
* private black book of who paid what for what". Four renderers printed
|
|
90
|
+
* that on the host's OUTSIDE icon with no roll, which handed a passer-by
|
|
91
|
+
* the inventory of a sealed host for free. The file NAMES were already
|
|
92
|
+
* gated (grid-view.ts, Host.searchedBy); only this phrase leaked.
|
|
93
|
+
*
|
|
94
|
+
* TWO TIERS, and both are rolls the engine already makes
|
|
95
|
+
* (commands/search.ts matrixSearch):
|
|
96
|
+
*
|
|
97
|
+
* tier 1 Room.searchedBy -- the threshold-1 public row out on the
|
|
98
|
+
* grid (MATRIX_SEARCH.public). What the place runs on its host
|
|
99
|
+
* is the kind of thing a public search turns up: canon has the
|
|
100
|
+
* icon look like the shop (p.219). This set is also flipped by
|
|
101
|
+
* a MEAT search of the room, and that is intended -- casing the
|
|
102
|
+
* clinic in the flesh is a fair way to learn it keeps a ledger.
|
|
103
|
+
* tier 2 Host.searchedBy -- the threshold-3 archive search from
|
|
104
|
+
* inside, which additionally NAMES the files (grid-view.ts).
|
|
105
|
+
*
|
|
106
|
+
* A cracked host spills it free, exactly as its files already do: the
|
|
107
|
+
* seal is broken, there is nothing left to hide behind.
|
|
108
|
+
*
|
|
109
|
+
* Returns undefined when the seed declared no purpose OR the persona has
|
|
110
|
+
* not earned it, so every caller can `??` it away.
|
|
111
|
+
*/
|
|
112
|
+
export function hostPurposeFor(room, actor) {
|
|
113
|
+
const host = room.host;
|
|
114
|
+
if (!host?.purpose)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (host.cracked)
|
|
117
|
+
return host.purpose;
|
|
118
|
+
return host.searchedBy.has(actor.name) || room.searchedBy.has(actor.name)
|
|
119
|
+
? host.purpose
|
|
120
|
+
: undefined;
|
|
121
|
+
}
|
|
80
122
|
export function hostNameHint(room) {
|
|
81
123
|
return room.hasNode ? `the host here answers to "host" or "${room.name}"` : '';
|
|
82
124
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hostConcealedFrom } from './jackpoint.js';
|
|
2
|
-
import { matchesHostName, hostLabel, hostAliases } from './grid-names.js';
|
|
2
|
+
import { matchesHostName, hostLabel, hostAliases, hostPurposeFor } from './grid-names.js';
|
|
3
3
|
import { fuzzyPickName, normalizeLoose } from './fuzzy-match.js';
|
|
4
4
|
import { MAX_MARKS } from './marks.js';
|
|
5
5
|
/**
|
|
@@ -193,10 +193,15 @@ export function hostLine(room, actor) {
|
|
|
193
193
|
// A haven shows no rating and no eyes (jackpoint.ts): it is not a
|
|
194
194
|
// thing you crack, so the numbers you would crack it with stay off.
|
|
195
195
|
const rating = room.hostSanctioned ? '' : ` [HR ${room.hostRating}]`;
|
|
196
|
-
// The host's business, where the seed declared one
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
196
|
+
// The host's business, where the seed declared one AND this persona has
|
|
197
|
+
// searched for it (hostPurposeFor, np6YBSPsdfAY7NcuZ). A grid map that
|
|
198
|
+
// tells a clinic's records from a club's door list is the map that lets
|
|
199
|
+
// a runner pick a target -- but it is a Matrix Search that draws it,
|
|
200
|
+
// not a glance. Map rows stay terse when it is unearned: the verb is
|
|
201
|
+
// taught on the host's own icon (grid-view.ts gridIcons), not once per
|
|
202
|
+
// row of a map that may list a dozen.
|
|
203
|
+
const found = hostPurposeFor(room, actor);
|
|
204
|
+
const purpose = found ? ` (${found})` : '';
|
|
200
205
|
return `${inside ? '▣' : '●'} ${hostLabel(room, { capital: true })}${rating}${purpose}${onGrid} -- ${standing}${alert}${over}${inside ? ' -- INSIDE' : ''}`;
|
|
201
206
|
}
|
|
202
207
|
/** The Map dock's one-line host: icon, name, rating, standing -- cut to the dock. */
|
|
@@ -2,7 +2,7 @@ import { sameHostSide } from '../models/player.js';
|
|
|
2
2
|
import { personasInside } from './matrix-roster.js';
|
|
3
3
|
import { hostsInReach, hostLine, roomsInReach, gridVicinity } from './grid-reach.js';
|
|
4
4
|
import { NPC } from '../models/npc.js';
|
|
5
|
-
import { hostLabel } from './grid-names.js';
|
|
5
|
+
import { hostLabel, hostPurposeFor } from './grid-names.js';
|
|
6
6
|
import { isLinked } from './comm-style.js';
|
|
7
7
|
import { isWatched, camerasLive, canSnoopFeeds } from './surveillance.js';
|
|
8
8
|
/**
|
|
@@ -65,10 +65,13 @@ export function gridIcons(scene, actor, room) {
|
|
|
65
65
|
// books never gave it -- boilerplate describing the opposite of what
|
|
66
66
|
// the place is. A member is standing in a room they belong to.
|
|
67
67
|
const hr = ` {bold}[HR ${room.hostRating}]{/bold}`;
|
|
68
|
-
// WHAT IT IS FOR rides the outside icon
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
68
|
+
// WHAT IT IS FOR rides the outside icon -- but only once a Matrix
|
|
69
|
+
// Search has turned it up (hostPurposeFor, np6YBSPsdfAY7NcuZ). The
|
|
70
|
+
// icon looking like the shop (p.219) is the NAME, which this line
|
|
71
|
+
// always prints; the seed's purpose phrase is an inventory of the
|
|
72
|
+
// archive, and handing that over for free is the report.
|
|
73
|
+
const found = hostPurposeFor(room, actor);
|
|
74
|
+
const purpose = found ? ` (${found})` : '';
|
|
72
75
|
if (room.hostSanctioned) {
|
|
73
76
|
icons.push(`● ${hostLabel(room, { capital: true })} -- three spheres turning over rooms that were never sold to anyone. You're on the list; nothing here is hunting.`);
|
|
74
77
|
}
|
|
@@ -82,7 +85,18 @@ export function gridIcons(scene, actor, room) {
|
|
|
82
85
|
// anyone inside it are on the far side of a wall (p.246). The
|
|
83
86
|
// marks line stays because a mark is the DOOR (p.239), so it is
|
|
84
87
|
// the one fact about the inside that belongs on the outside.
|
|
85
|
-
|
|
88
|
+
// The verb that EARNS the purpose phrase is offered only while it
|
|
89
|
+
// is still unearned -- a host that has already told you its
|
|
90
|
+
// business has nothing left to sell. Built as a list so a dropped
|
|
91
|
+
// entry cannot leave a dangling separator.
|
|
92
|
+
const ways = [
|
|
93
|
+
...(myMarks > 0 ? ['"enter" walks in on your mark'] : []),
|
|
94
|
+
'"hack" slips in on sleaze',
|
|
95
|
+
'"hack loud" smashes',
|
|
96
|
+
'"mark" softens it first',
|
|
97
|
+
...(found ? [] : ['"search" reads what it runs (p.241)']),
|
|
98
|
+
];
|
|
99
|
+
icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- sculpted ice geometry, sealed. Whatever it holds is inside it.${room.hostAlert ? ' Walls UP -- it holds a mark on an intruder.' : ''}${myMarks > 0 ? ` [your marks: ${myMarks}/3]` : ''} (${ways.join('; ')})`);
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
102
|
// WHERE THE HOST'S OWN ICON ENDS AND THE STREET BEGINS.
|
|
@@ -457,7 +471,11 @@ export function gridSculpt(actor, room) {
|
|
|
457
471
|
// sculpting is shown as that -- the purpose on the header, the
|
|
458
472
|
// sculpt where the boilerplate used to stand. The boilerplate stays
|
|
459
473
|
// for a hand-authored host that declared neither.
|
|
460
|
-
|
|
474
|
+
// The purpose is still the searched-for fact in here (hostPurposeFor):
|
|
475
|
+
// standing inside a host is not the same as having read its index.
|
|
476
|
+
// The SCULPT is not gated -- it is the architecture around you, and
|
|
477
|
+
// you are looking at it.
|
|
478
|
+
const purpose = hostPurposeFor(room, actor);
|
|
461
479
|
const sculpt = room.host?.sculpt;
|
|
462
480
|
return [
|
|
463
481
|
`{light-blue-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/light-blue-fg}`,
|
|
@@ -559,8 +577,11 @@ export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
|
559
577
|
const cut = (raw) => `{light-blue-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/light-blue-fg}`;
|
|
560
578
|
const lines = [];
|
|
561
579
|
lines.push(cut(`INSIDE ${host.name}`));
|
|
562
|
-
|
|
563
|
-
|
|
580
|
+
// Same gate as every other renderer (hostPurposeFor): the dock is a
|
|
581
|
+
// view, not a back door.
|
|
582
|
+
const purpose = hostPurposeFor(host.over, actor);
|
|
583
|
+
if (purpose)
|
|
584
|
+
lines.push(cut(purpose));
|
|
564
585
|
const marks = host.marksBy.get(actor.name) ?? 0;
|
|
565
586
|
lines.push(cut(host.sanctioned
|
|
566
587
|
? 'haven -- welcome here'
|
|
@@ -574,7 +595,13 @@ export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
|
574
595
|
continue;
|
|
575
596
|
lines.push(cut(`◆ ${a.name}`));
|
|
576
597
|
}
|
|
577
|
-
|
|
598
|
+
// THE COUNT IS PART OF THE ARCHIVE (np6YBSPsdfAY7NcuZ). The icon list
|
|
599
|
+
// above gates the file NAMES on a Matrix Search; this line quietly
|
|
600
|
+
// published how many there were regardless, which is most of what the
|
|
601
|
+
// roll was protecting -- "three files sealed" tells a runner whether
|
|
602
|
+
// the trip is worth it. Same gate, same set.
|
|
603
|
+
const indexed = host.cracked || host.searchedBy.has(actor.name);
|
|
604
|
+
const files = indexed ? host.files().length : 0;
|
|
578
605
|
if (files > 0)
|
|
579
606
|
lines.push(cut(`◇ ${files} file${files === 1 ? '' : 's'} ${host.cracked ? 'open' : 'sealed'}`));
|
|
580
607
|
return lines.slice(0, Math.max(1, rows));
|
|
@@ -47,6 +47,18 @@ async function tell(enc, scene, turn) {
|
|
|
47
47
|
*/
|
|
48
48
|
export async function hostTurnStart(scene, host, enc) {
|
|
49
49
|
const out = empty();
|
|
50
|
+
// SUPPRESSION (p.257, sprite-power-actions.ts): a sprite arguing with
|
|
51
|
+
// the host's launch paths delays what it brings up by (Level / 2)
|
|
52
|
+
// Combat Turns, and "delayed IC can't act or be targeted" -- which is
|
|
53
|
+
// exactly "the launch step does not happen this turn". Counted down
|
|
54
|
+
// here so the delay is spent whether or not the host wanted to launch.
|
|
55
|
+
if (host.icLaunchDelay > 0) {
|
|
56
|
+
host.icLaunchDelay -= 1;
|
|
57
|
+
out.lines.push(`{light-blue-fg}Something in ${host.name}'s architecture is arguing with itself -- whatever it meant to bring up does not arrive.{/light-blue-fg}`);
|
|
58
|
+
seatRunningIce(enc, host);
|
|
59
|
+
await tell(enc, scene, out);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
50
62
|
hostLaunchStep(host.over, scene, out);
|
|
51
63
|
seatRunningIce(enc, host);
|
|
52
64
|
await tell(enc, scene, out);
|
|
@@ -18,6 +18,16 @@ export function clearMarksPlacedBy(scene, placerName) {
|
|
|
18
18
|
for (const room of Object.values(scene.getRooms())) {
|
|
19
19
|
if (room.hostMarksBy.delete(placerName))
|
|
20
20
|
cleared++;
|
|
21
|
+
// A DEVICE'S MARKS ARE MARKS (f79LLqTerep4nKWcA sweep). Device.marksBy
|
|
22
|
+
// is the third ledger -- hack.ts writes it for every slaved and loose
|
|
23
|
+
// icon -- and no wipe had ever touched it, so the maglock you keyed
|
|
24
|
+
// last run still answered to a persona canon says was deleted on
|
|
25
|
+
// reboot (p.236). It also meant `disable` could drive a device on
|
|
26
|
+
// marks the sheet no longer showed you holding.
|
|
27
|
+
for (const device of room.allDevices()) {
|
|
28
|
+
if (device.marksBy.delete(placerName))
|
|
29
|
+
cleared++;
|
|
30
|
+
}
|
|
21
31
|
}
|
|
22
32
|
for (const a of scene.allActors) {
|
|
23
33
|
if (a.panMarksBy.delete(placerName))
|
|
@@ -71,6 +81,13 @@ export function describeMarks(scene, placerName) {
|
|
|
71
81
|
const n = room.hostMarksBy.get(placerName) ?? 0;
|
|
72
82
|
if (n > 0)
|
|
73
83
|
lines.push(`● ${hostLabel(room, { capital: true })} -- ${n} mark${n === 1 ? '' : 's'}${n >= MAX_MARKS ? ' (it thinks you belong)' : ''}`);
|
|
84
|
+
// Devices too -- the ledger `disable` reads to drive a maglock was
|
|
85
|
+
// invisible on the listing that is supposed to show what you hold.
|
|
86
|
+
for (const device of room.allDevices()) {
|
|
87
|
+
const d = device.marksBy.get(placerName) ?? 0;
|
|
88
|
+
if (d > 0)
|
|
89
|
+
lines.push(`▲ ${device.name} -- ${d} mark${d === 1 ? '' : 's'}`);
|
|
90
|
+
}
|
|
74
91
|
}
|
|
75
92
|
for (const a of scene.allActors) {
|
|
76
93
|
const n = a.panMarksBy.get(placerName) ?? 0;
|
|
@@ -92,6 +109,11 @@ export function marksPlaced(scene, placerName) {
|
|
|
92
109
|
const n = room.hostMarksBy.get(placerName) ?? 0;
|
|
93
110
|
if (n > 0)
|
|
94
111
|
out.push({ target: hostLabel(room, { capital: true }), count: n });
|
|
112
|
+
for (const device of room.allDevices()) {
|
|
113
|
+
const d = device.marksBy.get(placerName) ?? 0;
|
|
114
|
+
if (d > 0)
|
|
115
|
+
out.push({ target: device.name, count: d });
|
|
116
|
+
}
|
|
95
117
|
}
|
|
96
118
|
for (const a of scene.allActors) {
|
|
97
119
|
const n = a.panMarksBy.get(placerName) ?? 0;
|