@maka/maka-cli 5.200.0 → 5.202.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +165 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js +37 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +109 -11
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +41 -9
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +95 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +38 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +11 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +23 -3
- package/bundle/typescript/src/commands/game/sideQuest/npc-kits.js +295 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +48 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +87 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +17 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +25 -13
- package/package.json +1 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { augFor, itemFromCatalog, npcKitFor } from './utilities/catalog.js';
|
|
2
|
+
import { Category } from './types/shared/item-enum.js';
|
|
3
|
+
import { Logger } from './utilities/logger.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Tiers
|
|
6
|
+
// ============================================================================
|
|
7
|
+
/**
|
|
8
|
+
* WHICH KIT A SITE'S SECURITY TIER FIELDS. The slugs match the shipped
|
|
9
|
+
* rows (maka-cli.com full-catalog.json v21); a row that is missing
|
|
10
|
+
* resolves to no kit, per the file comment.
|
|
11
|
+
*/
|
|
12
|
+
export const KIT_FOR_TIER = {
|
|
13
|
+
0: 'thugs',
|
|
14
|
+
1: 'gangers',
|
|
15
|
+
2: 'corp-security',
|
|
16
|
+
3: 'police-patrol',
|
|
17
|
+
4: 'organized-crime',
|
|
18
|
+
5: 'elite-corp-security',
|
|
19
|
+
6: 'elite-special-forces',
|
|
20
|
+
};
|
|
21
|
+
export function kitSlugForTier(tier) {
|
|
22
|
+
return KIT_FOR_TIER[tier];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* THE MOST A JOB OF THIS SIZE PLAUSIBLY FIELDS. `campaignTier` is the
|
|
26
|
+
* job number (buildSkeletonPrompt's `tier`), NOT the security tier --
|
|
27
|
+
* the two words collide and this is the one place they meet. Job #1 is
|
|
28
|
+
* a warehouse, not an arcology: elite corporate security (PR 5) is a
|
|
29
|
+
* tier-3+ problem, and PR 6 special forces never come from a generated
|
|
30
|
+
* draft at all -- a hand-authored seed may declare them.
|
|
31
|
+
*/
|
|
32
|
+
export function maxSecurityTierFor(campaignTier) {
|
|
33
|
+
if (campaignTier <= 1)
|
|
34
|
+
return 2;
|
|
35
|
+
if (campaignTier === 2)
|
|
36
|
+
return 3;
|
|
37
|
+
return 5;
|
|
38
|
+
}
|
|
39
|
+
/** What a draft that declared nothing gets: a gang block for job #1, a
|
|
40
|
+
* corporate office for #2, a patrolled site after that. */
|
|
41
|
+
export function defaultSecurityTierFor(campaignTier) {
|
|
42
|
+
if (campaignTier <= 1)
|
|
43
|
+
return 1;
|
|
44
|
+
if (campaignTier === 2)
|
|
45
|
+
return 2;
|
|
46
|
+
return 3;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* CLAMP, NEVER REJECT. A model that wrote "securityTier": 9, or "high",
|
|
50
|
+
* or nothing, has still written a usable scene; failing the skeleton
|
|
51
|
+
* over it would spend a real generation retry on a number the engine
|
|
52
|
+
* can repair in one line. Same policy as the keyed-door and keyItem
|
|
53
|
+
* repairs in scene-factory.ts.
|
|
54
|
+
*/
|
|
55
|
+
export function clampSecurityTier(raw, campaignTier) {
|
|
56
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
57
|
+
const base = Number.isFinite(n) ? Math.round(n) : defaultSecurityTierFor(campaignTier);
|
|
58
|
+
return Math.max(0, Math.min(maxSecurityTierFor(campaignTier), base));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* ROUGHLY WHAT A RANK-AND-FILE GRUNT OF THIS TIER ROLLS, attack pool
|
|
62
|
+
* plus defense pool, read off the p.381-384 statblocks (Agility +
|
|
63
|
+
* weapon skill + smartlink/toner; Reaction + Intuition/2 + wired
|
|
64
|
+
* reflexes). The party's own figure (scene-chunks.ts partyStrength) is
|
|
65
|
+
* on the same scale, and a party that clears this by a margin draws
|
|
66
|
+
* the tier's lieutenant.
|
|
67
|
+
*/
|
|
68
|
+
export function expectedStrengthFor(securityTier) {
|
|
69
|
+
const table = { 0: 11, 1: 12, 2: 13, 3: 12, 4: 16, 5: 25, 6: 29 };
|
|
70
|
+
return table[Math.max(0, Math.min(6, securityTier))] ?? 12;
|
|
71
|
+
}
|
|
72
|
+
/** The margin past expectedStrengthFor at which a site posts its lieutenant. */
|
|
73
|
+
export const LIEUTENANT_MARGIN = 3;
|
|
74
|
+
export function partyDrawsLieutenant(partyStrength, securityTier) {
|
|
75
|
+
return partyStrength > expectedStrengthFor(securityTier) + LIEUTENANT_MARGIN;
|
|
76
|
+
}
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Group Edge and morale (p.379-380)
|
|
79
|
+
// ============================================================================
|
|
80
|
+
/** p.380: "a team of grunts has a Group Edge equal to its Professional Rating." */
|
|
81
|
+
export function groupEdgeFor(professionalRating) {
|
|
82
|
+
return Math.max(0, Math.min(6, Math.round(professionalRating)));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* p.379-380, verbatim in spirit: PR 0 "will flee if somebody in their
|
|
86
|
+
* group goes down"; PR 1-2 "retreat if more than a quarter of their
|
|
87
|
+
* team is taken out"; PR 3-4 "withdraw after casualties exceed half";
|
|
88
|
+
* PR 5-6 "will not break -- they fight to the last man".
|
|
89
|
+
*/
|
|
90
|
+
export function moraleBandFor(professionalRating) {
|
|
91
|
+
if (professionalRating <= 0)
|
|
92
|
+
return 'one-drops';
|
|
93
|
+
if (professionalRating <= 2)
|
|
94
|
+
return 'quarter';
|
|
95
|
+
if (professionalRating <= 4)
|
|
96
|
+
return 'half';
|
|
97
|
+
return 'never';
|
|
98
|
+
}
|
|
99
|
+
/** Does a team of `total` that has lost `casualties` break at this PR? */
|
|
100
|
+
export function moraleBreaks(professionalRating, casualties, total) {
|
|
101
|
+
if (total <= 0 || casualties <= 0)
|
|
102
|
+
return false;
|
|
103
|
+
switch (moraleBandFor(professionalRating)) {
|
|
104
|
+
case 'one-drops': return casualties >= 1;
|
|
105
|
+
case 'quarter': return casualties > total / 4;
|
|
106
|
+
case 'half': return casualties > total / 2;
|
|
107
|
+
case 'never': return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** The line the room hears when a team breaks, by how trained it was. */
|
|
111
|
+
export function moraleBreakLine(name, band) {
|
|
112
|
+
switch (band) {
|
|
113
|
+
case 'one-drops': return `${name} sees the body drop and wants no part of this -- hands up, backing off.`;
|
|
114
|
+
case 'quarter': return `${name} has seen enough of their own go down -- weapon lowered, backing out of it.`;
|
|
115
|
+
default: return `${name} calls it: too many down. Weapon down, hands where you can see them.`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Outfitting
|
|
120
|
+
// ============================================================================
|
|
121
|
+
/**
|
|
122
|
+
* WHAT AN NPC WEARS OR HOLDS, out of what a kit puts in its pack. The
|
|
123
|
+
* one list, shared by scene-factory's equip step and the ephemeral
|
|
124
|
+
* governor, so a guard built by a seed and a ganger spawned on a corner
|
|
125
|
+
* dress the same way. Weapons and armor only: a commlink stays in the
|
|
126
|
+
* pocket, and plot items (keys, chips) stay in inventory where "give"
|
|
127
|
+
* and the loot lane expect them.
|
|
128
|
+
*/
|
|
129
|
+
export const NPC_EQUIP_CATEGORIES = new Set([
|
|
130
|
+
Category.Weapon, Category.MeleeWeapon, Category.RangedWeapon,
|
|
131
|
+
Category.Armor, Category.BodyArmor, Category.ArmorClothing, Category.Shield,
|
|
132
|
+
Category.Helmet, Category.ArmArmor, Category.LegArmor, Category.Boots, Category.Gloves,
|
|
133
|
+
]);
|
|
134
|
+
const WEAPON_CATEGORIES = new Set([
|
|
135
|
+
Category.Weapon, Category.MeleeWeapon, Category.RangedWeapon,
|
|
136
|
+
]);
|
|
137
|
+
/**
|
|
138
|
+
* Slots every weapon and armor piece in the pack into the actor's
|
|
139
|
+
* equipment, FIRST WEAPON WINS. `equip` swaps, so an unguarded loop
|
|
140
|
+
* over a pack holding a pistol and a knife ends with the knife in hand
|
|
141
|
+
* and the pistol holstered -- the exact bug archetypes.ts:outfitArchetype
|
|
142
|
+
* documents for runners, reproduced here for everyone else. Armor
|
|
143
|
+
* layers (a helmet over a jacket), so only weapons are guarded.
|
|
144
|
+
*/
|
|
145
|
+
export function equipKitIntoHands(actor) {
|
|
146
|
+
for (const item of [...actor.inventory.getAllItems()]) {
|
|
147
|
+
if (!NPC_EQUIP_CATEGORIES.has(item.category))
|
|
148
|
+
continue;
|
|
149
|
+
if (WEAPON_CATEGORIES.has(item.category) && actor.getCarriedWeapon())
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
actor.equip(item);
|
|
153
|
+
}
|
|
154
|
+
catch { /* nothing to slot it into -- stays holstered */ }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* STAMP A KIT ONTO AN NPC. The NPC analogue of archetypes.ts
|
|
159
|
+
* outfitArchetype, minus the chargen ledger and the credstick.
|
|
160
|
+
*
|
|
161
|
+
* INTO THE PACK, NOT INTO THE HANDS. This adds gear to inventory and
|
|
162
|
+
* stops; equipping is the caller's step (scene-factory's Step 4.05 for
|
|
163
|
+
* a seeded NPC, equipKitIntoHands for an ephemeral), because the seeded
|
|
164
|
+
* path has an author to defer to: an item the seed put in this NPC's
|
|
165
|
+
* hands with `heldBy` is the author speaking, and the kit skips its
|
|
166
|
+
* own weapon rather than argue -- `respectCarried` below. Armor still
|
|
167
|
+
* layers.
|
|
168
|
+
*
|
|
169
|
+
* Idempotent on augs and qualities (hasAug / the Set), so a kit applied
|
|
170
|
+
* twice does not double-install wired reflexes.
|
|
171
|
+
*/
|
|
172
|
+
export function outfitNpcKit(npc, kit, opts = {}) {
|
|
173
|
+
const logger = Logger.getInstance();
|
|
174
|
+
const lt = opts.lieutenant ? kit.lieutenant : undefined;
|
|
175
|
+
const attrs = { ...kit.attributes, ...(lt?.attributes ?? {}) };
|
|
176
|
+
const skills = { ...kit.skills, ...(lt?.skills ?? {}) };
|
|
177
|
+
const gear = lt?.gear ?? kit.gear;
|
|
178
|
+
const ammo = lt?.ammo ?? kit.ammo;
|
|
179
|
+
const augs = [...kit.augs, ...(lt?.augs ?? [])];
|
|
180
|
+
const qualities = [...kit.qualities, ...(lt?.qualities ?? [])];
|
|
181
|
+
const spells = [...kit.spells, ...(lt?.spells ?? [])];
|
|
182
|
+
const adeptPowers = { ...(kit.adeptPowers ?? {}), ...(lt?.adeptPowers ?? {}) };
|
|
183
|
+
npc.kit = kit.key;
|
|
184
|
+
npc.professionalRating = kit.professionalRating;
|
|
185
|
+
if (opts.lieutenant)
|
|
186
|
+
npc.lieutenant = true;
|
|
187
|
+
// The faction feature (factions.ts) ships rows and rules and no seed
|
|
188
|
+
// wiring; a kit row knows who fields it, so the gap closes here. An
|
|
189
|
+
// author's explicit faction still wins.
|
|
190
|
+
if (kit.faction && !npc.faction)
|
|
191
|
+
npc.faction = kit.faction;
|
|
192
|
+
npc.body = attrs.body;
|
|
193
|
+
npc.agility = attrs.agility;
|
|
194
|
+
npc.reaction = attrs.reaction;
|
|
195
|
+
npc.strength = attrs.strength;
|
|
196
|
+
npc.willpower = attrs.willpower;
|
|
197
|
+
npc.logic = attrs.logic;
|
|
198
|
+
npc.intuition = attrs.intuition;
|
|
199
|
+
npc.charisma = attrs.charisma;
|
|
200
|
+
if (attrs.magic !== undefined)
|
|
201
|
+
npc.magic = attrs.magic;
|
|
202
|
+
if (attrs.resonance !== undefined)
|
|
203
|
+
npc.resonance = attrs.resonance;
|
|
204
|
+
if (attrs.adept !== undefined)
|
|
205
|
+
npc.adept = attrs.adept;
|
|
206
|
+
// Grunts have no Edge POOL of their own (p.380): the team shares one
|
|
207
|
+
// equal to its PR, held on the CombatEncounter. The RATING stays on
|
|
208
|
+
// the actor because Player.consumeEdgeBoost adds `this.edge` dice
|
|
209
|
+
// when a boost lands -- p.56's "add Edge to the dice pool" -- and a
|
|
210
|
+
// grunt's Edge for that purpose is the group's. edgeRemaining is 0
|
|
211
|
+
// so nothing reads a per-actor pool.
|
|
212
|
+
npc.edge = groupEdgeFor(kit.professionalRating + (opts.lieutenant ? 1 : 0));
|
|
213
|
+
npc.edgeRemaining = 0;
|
|
214
|
+
// THE KIT IS THE STATLINE: replace, never merge. A `combat.skills`
|
|
215
|
+
// block a model wrote for a guard is superseded by the book's, and a
|
|
216
|
+
// merge would keep a dead key like `firearms` alive beside the live ones.
|
|
217
|
+
npc.skills = { ...skills };
|
|
218
|
+
const top = Object.entries(skills).sort((a, b) => b[1] - a[1])[0];
|
|
219
|
+
if (top)
|
|
220
|
+
npc.combatSkill = top[1];
|
|
221
|
+
for (const key of qualities) {
|
|
222
|
+
if (!npc.qualities.has(key))
|
|
223
|
+
npc.addQuality(key);
|
|
224
|
+
}
|
|
225
|
+
for (const key of augs) {
|
|
226
|
+
const aug = augFor(key);
|
|
227
|
+
if (aug && !npc.hasAug(key))
|
|
228
|
+
npc.installAug(aug);
|
|
229
|
+
else if (!aug)
|
|
230
|
+
logger.write(`npc-kit ${kit.key}: augmentation "${key}" has no row -- skipped.`);
|
|
231
|
+
}
|
|
232
|
+
if (spells.length > 0)
|
|
233
|
+
npc.knownSpells = [...new Set([...(npc.knownSpells ?? []), ...spells])];
|
|
234
|
+
if (Object.keys(adeptPowers).length > 0) {
|
|
235
|
+
npc.adept = true;
|
|
236
|
+
npc.adeptPowers = { ...(npc.adeptPowers ?? {}), ...adeptPowers };
|
|
237
|
+
}
|
|
238
|
+
const carriesWeapon = opts.respectCarried
|
|
239
|
+
&& npc.inventory.getAllItems().some(i => WEAPON_CATEGORIES.has(i.category));
|
|
240
|
+
let firstGun;
|
|
241
|
+
for (const slug of gear) {
|
|
242
|
+
let item;
|
|
243
|
+
try {
|
|
244
|
+
item = itemFromCatalog(slug);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
// A missing gear row is a catalog problem, logged and survived:
|
|
248
|
+
// the guard is a little lighter, not absent. The kit suite walks
|
|
249
|
+
// every slug so this never reaches a player from a shipped row.
|
|
250
|
+
logger.write(`npc-kit ${kit.key}: ${err instanceof Error ? err.message : String(err)} -- skipped.`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (carriesWeapon && WEAPON_CATEGORIES.has(item.category))
|
|
254
|
+
continue;
|
|
255
|
+
try {
|
|
256
|
+
npc.addInventory(item);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
logger.write(`npc-kit ${kit.key}: could not give ${npc.name} ${item.name} (${err instanceof Error ? err.message : 'carry cap?'}).`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
item.owner = npc.name;
|
|
263
|
+
if (!firstGun && item.isFirearm())
|
|
264
|
+
firstGun = item;
|
|
265
|
+
}
|
|
266
|
+
// p.433-434: what is loaded shifts the gun's line -- this is how the
|
|
267
|
+
// PR 6 HK 227 gets its APDS AP -4 without the row lying about the gun.
|
|
268
|
+
if (ammo && firstGun) {
|
|
269
|
+
const round = itemFromCatalogQuiet(ammo);
|
|
270
|
+
if (round)
|
|
271
|
+
firstGun.loadedAmmo = round.name;
|
|
272
|
+
}
|
|
273
|
+
logger.write(`npc-kit: ${npc.name} outfitted as ${kit.key}${opts.lieutenant ? ' (lieutenant)' : ''} -- PR ${kit.professionalRating}, ${gear.length} piece(s).`);
|
|
274
|
+
}
|
|
275
|
+
function itemFromCatalogQuiet(slug) {
|
|
276
|
+
try {
|
|
277
|
+
return itemFromCatalog(slug);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Resolve-and-outfit in one call; a slug with no row is a no-op. Returns whether a kit landed. */
|
|
284
|
+
export function outfitNpcBySlug(npc, slug, opts = {}) {
|
|
285
|
+
if (!slug)
|
|
286
|
+
return false;
|
|
287
|
+
const kit = npcKitFor(slug);
|
|
288
|
+
if (!kit) {
|
|
289
|
+
Logger.getInstance().write(`npc-kit: no row for "${slug}" -- ${npc.name} keeps their attribute block.`);
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
outfitNpcKit(npc, kit, opts);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=npc-kits.js.map
|
|
@@ -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);
|
|
@@ -952,6 +952,40 @@ export function factionFor(key) {
|
|
|
952
952
|
export function catalogFactions() {
|
|
953
953
|
return listOfKind('faction', factionFor);
|
|
954
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* AN NPC KIT ROW, rebuilt as the engine reads it (catalog v21, "everyone
|
|
957
|
+
* is basically naked"). Same contract as factionFor: the rows live on
|
|
958
|
+
* maka-cli.com, an unknown slug answers undefined, and npc-kits.ts
|
|
959
|
+
* treats that as "no kit" -- a cached catalog from before v21 builds
|
|
960
|
+
* NPCs exactly as the engine always did.
|
|
961
|
+
*/
|
|
962
|
+
export function npcKitFor(key) {
|
|
963
|
+
return kindIndex('npc-kit', row => {
|
|
964
|
+
const k = row.npcKit;
|
|
965
|
+
return {
|
|
966
|
+
key: row.slug,
|
|
967
|
+
name: row.name,
|
|
968
|
+
description: row.description,
|
|
969
|
+
professionalRating: k.professionalRating,
|
|
970
|
+
...(k.faction !== undefined ? { faction: k.faction } : {}),
|
|
971
|
+
sort: k.sort,
|
|
972
|
+
attributes: { ...k.attributes },
|
|
973
|
+
skills: { ...k.skills },
|
|
974
|
+
gear: [...k.gear],
|
|
975
|
+
...(k.ammo !== undefined ? { ammo: k.ammo } : {}),
|
|
976
|
+
augs: [...(k.augs ?? [])],
|
|
977
|
+
qualities: [...(k.qualities ?? [])],
|
|
978
|
+
spells: [...(k.spells ?? [])],
|
|
979
|
+
...(k.adeptPowers !== undefined ? { adeptPowers: { ...k.adeptPowers } } : {}),
|
|
980
|
+
...(k.lieutenant !== undefined ? { lieutenant: { ...k.lieutenant } } : {}),
|
|
981
|
+
page: row.page ?? '',
|
|
982
|
+
};
|
|
983
|
+
}).get(key);
|
|
984
|
+
}
|
|
985
|
+
/** Every NPC kit the resolved catalog knows. */
|
|
986
|
+
export function catalogNpcKits() {
|
|
987
|
+
return listOfKind('npc-kit', npcKitFor);
|
|
988
|
+
}
|
|
955
989
|
/** Every metamagic and echo the resolved catalog knows. */
|
|
956
990
|
export function catalogMetamagics() {
|
|
957
991
|
return listOfKind('metamagic', metamagicFor);
|
|
@@ -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
|