@maka/maka-cli 5.120.0 → 5.122.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/archetypes.d.ts +72 -0
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +300 -61
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/aspects.js +11 -1
- package/bundle/typescript/src/commands/game/sideQuest/aspects.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +33 -0
- package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +31 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.d.ts +10 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +17 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.d.ts +0 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js +39 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +31 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/fit.d.ts +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/fit.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/fit.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +16 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +273 -28
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +4 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +16 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.d.ts +19 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +43 -227
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +31 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/pick.d.ts +34 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +81 -12
- package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +5 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +4 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.d.ts +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +50 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +45 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +34 -41
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +44 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +47 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +115 -11
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +17 -0
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +30 -2
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/device.d.ts +86 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js +103 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/device.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +140 -45
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +456 -104
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +113 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +289 -47
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/qualities.js +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +9 -9
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +6 -6
- package/bundle/typescript/src/commands/game/sideQuest/types/repro.d.ts +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/repro.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/device-seed.d.ts +17 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +23 -26
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.d.ts +38 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +279 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +221 -212
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +48 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +84 -17
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +52 -12
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.d.ts +32 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js +33 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +35 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.d.ts +121 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.js +141 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.d.ts +93 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js +201 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/range.d.ts +19 -13
- package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js +77 -34
- package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +91 -9
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +50 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +9 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +28 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +43 -3
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { Size, Category, Rating } from '../types/shared/item-enum.js';
|
|
2
|
-
import { PROGRAM_ATTR_MODS, findProgram, findSoft, getSoft } from '../utilities/programs.js';
|
|
3
|
-
import { catalogHullFor } from '../utilities/catalog.js';
|
|
2
|
+
import { PROGRAM_ATTR_MODS, findProgram, findSoft, getSoft, getProgram } from '../utilities/programs.js';
|
|
3
|
+
import { catalogHullFor, catalogRowFor } from '../utilities/catalog.js';
|
|
4
|
+
import { guessRangeClass } from '../utilities/range.js';
|
|
4
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
6
|
import { AbstractItem } from '../types/shared/abstracts.js';
|
|
7
|
+
/**
|
|
8
|
+
* What a running program adds to a Matrix attribute, from its catalog
|
|
9
|
+
* row's effects (v13). The program's key is the deck's word for it
|
|
10
|
+
* (utilities/programs.ts); the row is found by the program's name.
|
|
11
|
+
* PROGRAM_ATTR_MODS stands in when no row is loaded.
|
|
12
|
+
*/
|
|
13
|
+
export function programMatrixMod(key, attr) {
|
|
14
|
+
const program = getProgram(key);
|
|
15
|
+
const row = program ? catalogRowFor(`${program.name} Program`) ?? catalogRowFor(program.name) : undefined;
|
|
16
|
+
if (row?.effects) {
|
|
17
|
+
return row.effects.reduce((n, e) => n + (e.category === 'matrix' && e.property === attr ? e.value : 0), 0);
|
|
18
|
+
}
|
|
19
|
+
if (attr === 'damage')
|
|
20
|
+
return key === 'hammer' ? 2 : 0;
|
|
21
|
+
return PROGRAM_ATTR_MODS[key]?.[attr] ?? 0;
|
|
22
|
+
}
|
|
6
23
|
export class Item extends AbstractItem {
|
|
7
24
|
_id;
|
|
8
25
|
_owner;
|
|
@@ -191,26 +208,54 @@ export class Item extends AbstractItem {
|
|
|
191
208
|
/**
|
|
192
209
|
* STUN BY NATURE (SR5 p.170-171, p.424, RAG-checked 2026-09-02):
|
|
193
210
|
* electricity damage is Stun, so a taser, a stun baton or shock gloves
|
|
194
|
-
* never deal Physical.
|
|
211
|
+
* never deal Physical. THE ROW SAYS SO (catalog v13: weapon.damageType
|
|
212
|
+
* / weapon.electric); the name regex is the fallback for generated gear.
|
|
195
213
|
*/
|
|
196
214
|
isStunWeapon() {
|
|
215
|
+
const w = this.row?.weapon;
|
|
216
|
+
if (w)
|
|
217
|
+
return w.damageType === 'S' || w.electric === true;
|
|
197
218
|
return /taser|stun (baton|gun)|shock glove|pulsar|defiance ex|stunner/.test(this.weaponHay);
|
|
198
219
|
}
|
|
199
220
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
221
|
+
* WHAT THE LOADED ROUND DOES TO THE LINE (p.433-434): the ammunition
|
|
222
|
+
* row's block -- gel Stun at AP +1, stick-n-shock electric Stun at DV
|
|
223
|
+
* -2 / AP -5, APDS AP -4, explosive DV +1 / AP -1, flechette DV +2 /
|
|
224
|
+
* AP +5. Regular rounds shift nothing. Generated ammunition with no
|
|
225
|
+
* row keeps the two Stun names the engine always knew.
|
|
202
226
|
*/
|
|
203
|
-
|
|
204
|
-
|
|
227
|
+
loadedAmmoMods() {
|
|
228
|
+
if (!this.loadedAmmo)
|
|
229
|
+
return undefined;
|
|
230
|
+
const row = catalogRowFor(this.loadedAmmo);
|
|
231
|
+
if (row?.ammo)
|
|
232
|
+
return { ...row.ammo, kind: row.slug };
|
|
233
|
+
const a = this.loadedAmmo.toLowerCase();
|
|
205
234
|
if (/gel/.test(a))
|
|
206
|
-
return { kind: 'gel', dvMod: 0, apMod: 1 };
|
|
235
|
+
return { kind: 'gel-rounds', dvMod: 0, apMod: 1, damageType: 'S' };
|
|
207
236
|
if (/stick|shock/.test(a))
|
|
208
|
-
return { kind: 'stick-n-shock', dvMod: -2, apMod: -5 };
|
|
237
|
+
return { kind: 'stick-n-shock', dvMod: -2, apMod: -5, damageType: 'S', electric: true };
|
|
209
238
|
return undefined;
|
|
210
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* STUN BY AMMUNITION (p.434): the loaded round's block when it deals
|
|
242
|
+
* Stun. Kept as the pre-v13 shape for its callers (combat-exchange,
|
|
243
|
+
* the non-lethal tests): gel or stick-n-shock, with the DV/AP shift.
|
|
244
|
+
*/
|
|
245
|
+
stunAmmo() {
|
|
246
|
+
const mods = this.loadedAmmoMods();
|
|
247
|
+
if (!mods || mods.damageType !== 'S')
|
|
248
|
+
return undefined;
|
|
249
|
+
return { kind: mods.electric ? 'stick-n-shock' : 'gel', dvMod: mods.dvMod, apMod: mods.apMod };
|
|
250
|
+
}
|
|
211
251
|
/** Ammunition that would make this gun non-lethal (reload picks it under the non-lethal stance). */
|
|
212
252
|
static isStunAmmo(item) {
|
|
213
|
-
|
|
253
|
+
if (item.category !== Category.Ammunition)
|
|
254
|
+
return false;
|
|
255
|
+
const row = catalogRowFor(item.name);
|
|
256
|
+
if (row?.ammo)
|
|
257
|
+
return row.ammo.damageType === 'S';
|
|
258
|
+
return /gel|stick|shock/i.test(item.name);
|
|
214
259
|
}
|
|
215
260
|
get owner() {
|
|
216
261
|
return this._owner;
|
|
@@ -224,8 +269,24 @@ export class Item extends AbstractItem {
|
|
|
224
269
|
get transferable() {
|
|
225
270
|
return this._transferable;
|
|
226
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* SURGERY MAKES IT PART OF YOU (Chrome Flesh p.222, the headware
|
|
274
|
+
* cyberdeck housing). One-way on purpose: there is no uninstall for
|
|
275
|
+
* chrome anywhere else in this engine either.
|
|
276
|
+
*/
|
|
277
|
+
implantIntoBody() {
|
|
278
|
+
this._transferable = false;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* "Nothing special happens" is honest for most props, but confusing for
|
|
282
|
+
* gear whose NAME implies a specific action that lives at a different
|
|
283
|
+
* verb -- a Lockpick Set at a pickable door (user, 2026-09-03). The
|
|
284
|
+
* row's `useHint` (catalog v13) names that verb; absent it, the generic
|
|
285
|
+
* line stands, which is correct for gear that really has nothing to say.
|
|
286
|
+
*/
|
|
227
287
|
use() {
|
|
228
|
-
|
|
288
|
+
const hint = this.row?.useHint;
|
|
289
|
+
return hint ? `You turn the ${this.name} over. ${hint}` : `You use the ${this.name}. Nothing special happens.`;
|
|
229
290
|
}
|
|
230
291
|
/**
|
|
231
292
|
* How much this item's quality tier is worth in game mechanics: extra
|
|
@@ -244,45 +305,120 @@ export class Item extends AbstractItem {
|
|
|
244
305
|
default: return 0;
|
|
245
306
|
}
|
|
246
307
|
}
|
|
247
|
-
// =================
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
308
|
+
// ================= THE ROW BEHIND THE ITEM (catalog v13) =================
|
|
309
|
+
// An Item knows its name, category, size and price tier -- never its
|
|
310
|
+
// stat block. Since v13 the catalog row carries that block (weapon,
|
|
311
|
+
// wearable, device, vehicle, mod, ammo) and the row's effects, and
|
|
312
|
+
// every getter below reads the row FIRST. The fallbacks that follow
|
|
313
|
+
// are for generated street gear with no row, and they are the book's
|
|
314
|
+
// own class values (p.421-431, p.436-437) rather than the compressed
|
|
315
|
+
// scale the engine used to run: a generated heavy pistol and the
|
|
316
|
+
// catalog's Predator now hit on the same scale, which is the whole
|
|
317
|
+
// point of moving to canon numbers (project owner, 2026-09-03).
|
|
318
|
+
/** The catalog row, by exact slug or name (strict: catalogRowFor). */
|
|
319
|
+
get row() {
|
|
320
|
+
return catalogRowFor(this.name);
|
|
321
|
+
}
|
|
322
|
+
/** The SR5 Rating / Force / Capacity this thing carries: the row's, or
|
|
323
|
+
* the "(Rating N)" its name prints. Undefined for an unrated thing. */
|
|
324
|
+
get ratingValue() {
|
|
325
|
+
const fromRow = this.row?.ratingValue;
|
|
326
|
+
if (fromRow !== undefined)
|
|
327
|
+
return fromRow;
|
|
328
|
+
const m = /\((?:Rating|Force|Capacity|Level) (\d+)\)/i.exec(this.name);
|
|
329
|
+
return m ? Number(m[1]) : undefined;
|
|
330
|
+
}
|
|
255
331
|
get weaponHay() {
|
|
256
332
|
return `${this.name} ${this.description ?? ''}`.toLowerCase();
|
|
257
333
|
}
|
|
258
|
-
/**
|
|
259
|
-
get
|
|
260
|
-
|
|
261
|
-
if (/hold-?out|derringer/.test(hay))
|
|
262
|
-
return 2;
|
|
263
|
-
if (/light\s?fire|light pistol|streetline|colt america|taser/.test(hay))
|
|
264
|
-
return 3;
|
|
265
|
-
if (/sniper|longarm|barret/.test(hay))
|
|
266
|
-
return 6;
|
|
267
|
-
if (/assault|ak-?\d|alpha|\brifle\b|carbine/.test(hay))
|
|
268
|
-
return 5;
|
|
269
|
-
if (/shotgun|roomsweeper|defiance|scattergun/.test(hay))
|
|
270
|
-
return 5;
|
|
271
|
-
if (/smg|machine pistol|submachine|uzi|ingram/.test(hay))
|
|
272
|
-
return 4;
|
|
273
|
-
// Heavy pistols (Predator, Ultra-Power...) and unrecognized iron.
|
|
274
|
-
return 4;
|
|
334
|
+
/** The p.185 range class: the row's, else read off the name (utilities/range.ts guessRangeClass). */
|
|
335
|
+
get rangeClass() {
|
|
336
|
+
return this.row?.weapon?.range ?? guessRangeClass(this.weaponHay, this.category === Category.MeleeWeapon);
|
|
275
337
|
}
|
|
276
|
-
/**
|
|
277
|
-
get
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
return
|
|
281
|
-
if (
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
338
|
+
/** The skill this weapon is used with: the row's, else the class's. */
|
|
339
|
+
get weaponSkill() {
|
|
340
|
+
const fromRow = this.row?.weapon?.skill;
|
|
341
|
+
if (fromRow)
|
|
342
|
+
return fromRow;
|
|
343
|
+
if (this.category === Category.MeleeWeapon) {
|
|
344
|
+
return /club|\bbat\b|baton|staff|hammer|\bpipe\b|wrench|\bsap\b|\bmace\b|crowbar|cudgel/i.test(`${this.name} ${this.shape}`) ? 'clubs' : 'blades';
|
|
345
|
+
}
|
|
346
|
+
switch (this.rangeClass) {
|
|
347
|
+
case 'machine-pistol':
|
|
348
|
+
case 'smg':
|
|
349
|
+
case 'assault-rifle': return 'automatics';
|
|
350
|
+
case 'shotgun':
|
|
351
|
+
case 'sniper': return 'longarms';
|
|
352
|
+
case 'lmg':
|
|
353
|
+
case 'mmg': return 'heavy-weapons';
|
|
354
|
+
case 'bow':
|
|
355
|
+
case 'light-crossbow':
|
|
356
|
+
case 'medium-crossbow':
|
|
357
|
+
case 'heavy-crossbow': return 'archery';
|
|
358
|
+
case 'thrown-knife':
|
|
359
|
+
case 'grenade': return 'throwing-weapons';
|
|
360
|
+
default: return 'pistols';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* THE DV, the book's flat part (p.421-431): 8 for the Predator's "8P",
|
|
365
|
+
* 2 for a knife's "(STR+2)P" -- the Strength half is the wielder's and
|
|
366
|
+
* Player.getWeaponProfile adds it (dvAttribute says whether to). The
|
|
367
|
+
* fallback is the CLASS row of the same tables, un-compressed.
|
|
368
|
+
*/
|
|
369
|
+
get weaponDv() {
|
|
370
|
+
const w = this.row?.weapon;
|
|
371
|
+
if (w)
|
|
372
|
+
return w.dv;
|
|
373
|
+
if (this.category === Category.MeleeWeapon) {
|
|
374
|
+
if (this.isStunWeapon())
|
|
375
|
+
return 8; // shock gloves / stun baton class
|
|
376
|
+
const hay = this.weaponHay;
|
|
377
|
+
if (/knife|dagger|stiletto|shiv|razor|sap|baton/.test(hay))
|
|
378
|
+
return 2; // (STR+2)
|
|
379
|
+
return 3; // sword, katana, club, staff: (STR+3)
|
|
380
|
+
}
|
|
381
|
+
switch (this.rangeClass) {
|
|
382
|
+
case 'taser': return 7;
|
|
383
|
+
case 'holdout': return 6;
|
|
384
|
+
case 'light-pistol': return 6;
|
|
385
|
+
case 'machine-pistol': return 6;
|
|
386
|
+
case 'smg': return 7;
|
|
387
|
+
case 'shotgun': return 10;
|
|
388
|
+
case 'assault-rifle': return 10;
|
|
389
|
+
case 'sniper': return 13;
|
|
390
|
+
case 'lmg': return 9;
|
|
391
|
+
case 'mmg': return 10;
|
|
392
|
+
case 'medium-crossbow':
|
|
393
|
+
case 'light-crossbow':
|
|
394
|
+
case 'heavy-crossbow': return 7;
|
|
395
|
+
case 'grenade': return 16;
|
|
396
|
+
case 'thrown-knife': return 1;
|
|
397
|
+
default: return 8; // heavy pistols and unrecognised iron
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Which attribute the DV rides on: melee is (STR + dv) unless the row says otherwise. */
|
|
401
|
+
get dvAttribute() {
|
|
402
|
+
const w = this.row?.weapon;
|
|
403
|
+
if (w)
|
|
404
|
+
return w.dvAttribute;
|
|
405
|
+
if (this.category === Category.MeleeWeapon && !this.isStunWeapon())
|
|
406
|
+
return 'strength';
|
|
407
|
+
if (this.rangeClass === 'thrown-knife')
|
|
408
|
+
return 'strength';
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
/** Physical or Stun, before ammunition and stance have their say. */
|
|
412
|
+
get damageType() {
|
|
413
|
+
return this.row?.weapon?.damageType ?? (this.isStunWeapon() ? 'S' : 'P');
|
|
414
|
+
}
|
|
415
|
+
/** @deprecated v13 -- the DV is weaponDv; kept one release for its readers. */
|
|
416
|
+
get rangedBase() { return this.category === Category.MeleeWeapon ? 0 : this.weaponDv; }
|
|
417
|
+
/** @deprecated v13 -- the DV is weaponDv; kept one release for its readers. */
|
|
418
|
+
get meleeBase() { return this.category === Category.MeleeWeapon ? this.weaponDv : 0; }
|
|
419
|
+
/** Reach (p.421-423): the row's, else none. */
|
|
420
|
+
get reach() {
|
|
421
|
+
return this.row?.weapon?.reach ?? 0;
|
|
286
422
|
}
|
|
287
423
|
/**
|
|
288
424
|
* ACCURACY -- the LIMIT on an attack made with this weapon (p.46-47,
|
|
@@ -291,41 +427,25 @@ export class Item extends AbstractItem {
|
|
|
291
427
|
* inherent one, higher or lower (p.46-47), so this is not a floor or a
|
|
292
428
|
* cap on the Physical limit -- it is the limit.
|
|
293
429
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* The numbers are the book's tables (p.421-431), read through the same
|
|
302
|
-
* duck-typing school as rangedBase/armorPen so generated gear works
|
|
303
|
-
* unmodified. Class-based rather than per-weapon ON PURPOSE: the RAG
|
|
304
|
-
* could not produce standard-table rows for every gun this game ships
|
|
305
|
-
* (the shotgun table did not come back, and the one AK-97 figure it
|
|
306
|
-
* did return was off a Run Faster character sheet -- a MODIFIED
|
|
307
|
-
* weapon, not the table), so a per-weapon list would have meant
|
|
308
|
-
* inventing the gaps. Every value below is one the book gave up.
|
|
430
|
+
* THIS IS THE BARE NUMBER, NOT THE PARENTHETICAL. Tables write "Acc 5
|
|
431
|
+
* (7)": the 5 is the gun, and the 7 is the gun PLUS a shooter who owns
|
|
432
|
+
* a smartlink (p.433, +2 Accuracy with a smartgun system) -- see
|
|
433
|
+
* Player.smartlinkAccuracyBonus, which supplies the +2 in
|
|
434
|
+
* getAttackLimit. A laser sight's +1 rides the same way (fitted mods,
|
|
435
|
+
* Player.gearLimit), and never stacks with the smartlink (p.432).
|
|
309
436
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* on a table has no idea who will pick it up, so the pairing is asked
|
|
315
|
-
* of the shooter -- see Player.smartlinkAccuracyBonus, which supplies
|
|
316
|
-
* the +2 in getAttackLimit. fxPHSuE4nrH3gAGiy.
|
|
437
|
+
* 0 means "no Accuracy of its own": the table's own word 'physical'
|
|
438
|
+
* (shock gloves, throwing knives, grenades) and everything that is not
|
|
439
|
+
* a weapon. Callers fall back to the Physical limit, which is what an
|
|
440
|
+
* improvised or bare-handed attack uses (p.168).
|
|
317
441
|
*
|
|
318
|
-
* The
|
|
319
|
-
*
|
|
320
|
-
* modelled: the engine has no sight-fitting model, and inventing one
|
|
321
|
-
* to reuse the smartlink pairing would be guessing at a second rule.
|
|
322
|
-
*
|
|
323
|
-
* Note the wireless smartgun bonus is a DICE POOL bonus (+1/+2,
|
|
324
|
-
* p.433), NOT an Accuracy bonus -- getAttackPool already spends it as
|
|
325
|
-
* dice. Keeping those apart is the same discipline as the Matrix
|
|
326
|
-
* brackets: a limit is never a die, and a die is never a limit.
|
|
442
|
+
* The fallback ladder is the book's class values, read through the
|
|
443
|
+
* same duck-typing school as the DV so generated gear works.
|
|
327
444
|
*/
|
|
328
445
|
get accuracy() {
|
|
446
|
+
const w = this.row?.weapon;
|
|
447
|
+
if (w)
|
|
448
|
+
return typeof w.accuracy === 'number' ? w.accuracy : 0;
|
|
329
449
|
const hay = this.weaponHay;
|
|
330
450
|
if (this.category === Category.MeleeWeapon) {
|
|
331
451
|
if (/katana/.test(hay))
|
|
@@ -338,7 +458,9 @@ export class Item extends AbstractItem {
|
|
|
338
458
|
return 5;
|
|
339
459
|
if (/sap|whip|garrote/.test(hay))
|
|
340
460
|
return 5;
|
|
341
|
-
if (/
|
|
461
|
+
if (/staff/.test(hay))
|
|
462
|
+
return 6; // p.423
|
|
463
|
+
if (/club|bat\b|baton|pipe|crowbar|hammer|maul|mace|cudgel/.test(hay))
|
|
342
464
|
return 4;
|
|
343
465
|
return 5;
|
|
344
466
|
}
|
|
@@ -364,43 +486,263 @@ export class Item extends AbstractItem {
|
|
|
364
486
|
// Heavy pistols (Predator, Ultra-Power) and unrecognized iron.
|
|
365
487
|
return 5;
|
|
366
488
|
}
|
|
367
|
-
// Not a weapon: no Accuracy of its own. Callers fall back to the
|
|
368
|
-
// Physical limit, which is what an improvised or bare-handed
|
|
369
|
-
// attack uses (p.168).
|
|
370
489
|
return 0;
|
|
371
490
|
}
|
|
372
491
|
/**
|
|
373
|
-
* Armor penetration
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* (combat-exchange.ts).
|
|
492
|
+
* Armor penetration: how much the hit shifts the defender's ARMOR
|
|
493
|
+
* (p.169) -- negative eats it, a "+" (gel, flechette) adds to it. The
|
|
494
|
+
* row's AP, else the class value from the same tables, un-compressed.
|
|
377
495
|
*/
|
|
378
496
|
get armorPen() {
|
|
497
|
+
const w = this.row?.weapon;
|
|
498
|
+
if (w)
|
|
499
|
+
return w.ap;
|
|
379
500
|
const hay = this.weaponHay;
|
|
380
501
|
if (this.category === Category.MeleeWeapon) {
|
|
381
502
|
if (/knife|dagger|stiletto|katana/.test(hay))
|
|
503
|
+
return -3;
|
|
504
|
+
if (/sword|axe|machete|claymore|blade|pole\s?arm/.test(hay))
|
|
382
505
|
return -2;
|
|
383
|
-
if (/
|
|
384
|
-
return -
|
|
385
|
-
if (/club|bat\b|staff|pipe|crowbar|hammer|maul|mace|sap/.test(hay))
|
|
506
|
+
if (/taser|stun|shock/.test(hay))
|
|
507
|
+
return -5;
|
|
508
|
+
if (/club|bat\b|staff|pipe|crowbar|hammer|maul|mace|sap|baton/.test(hay))
|
|
386
509
|
return 0;
|
|
387
510
|
return -1;
|
|
388
511
|
}
|
|
389
512
|
if (this.category === Category.RangedWeapon || this.category === Category.Weapon) {
|
|
390
|
-
|
|
391
|
-
return -
|
|
392
|
-
|
|
393
|
-
return -
|
|
394
|
-
|
|
395
|
-
return
|
|
396
|
-
|
|
513
|
+
switch (this.rangeClass) {
|
|
514
|
+
case 'taser': return -5;
|
|
515
|
+
case 'sniper': return -4;
|
|
516
|
+
case 'assault-rifle': return -2;
|
|
517
|
+
case 'mmg': return -3;
|
|
518
|
+
case 'lmg': return -2;
|
|
519
|
+
case 'shotgun': return -1;
|
|
520
|
+
case 'holdout':
|
|
521
|
+
case 'light-pistol':
|
|
522
|
+
case 'machine-pistol':
|
|
523
|
+
case 'smg': return 0;
|
|
524
|
+
case 'medium-crossbow':
|
|
525
|
+
case 'light-crossbow':
|
|
526
|
+
case 'heavy-crossbow': return -2;
|
|
527
|
+
default: return -1; // heavy pistols (RAW -1)
|
|
528
|
+
}
|
|
397
529
|
}
|
|
398
530
|
return 0;
|
|
399
531
|
}
|
|
400
|
-
/** Heavier armor classes soak harder (RAW: jacket 12 vs clothing 6). */
|
|
532
|
+
/** Heavier armor classes soak harder (RAW: jacket 12 vs clothing 6). Fallback only. */
|
|
401
533
|
get isHeavyArmor() {
|
|
402
534
|
return /jacket|full body|milspec|mil-spec|plate|riot|ballistic|hardened/.test(this.weaponHay);
|
|
403
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* THE ARMOR RATING of a worn piece (p.436-437): the row's, else the
|
|
538
|
+
* class value -- jacket-class 12, a vest 9, armor clothing 6, plain
|
|
539
|
+
* clothing 0. Only the highest worn piece counts (Player.armorBreakdown).
|
|
540
|
+
*/
|
|
541
|
+
get armorValue() {
|
|
542
|
+
const w = this.row?.wearable;
|
|
543
|
+
if (w?.armor !== undefined)
|
|
544
|
+
return w.armor;
|
|
545
|
+
if (w?.armorBonus !== undefined)
|
|
546
|
+
return 0;
|
|
547
|
+
switch (this.category) {
|
|
548
|
+
case Category.Armor:
|
|
549
|
+
case Category.BodyArmor:
|
|
550
|
+
return this.isHeavyArmor ? 12 : 9;
|
|
551
|
+
case Category.ArmorClothing:
|
|
552
|
+
return this.isHeavyArmor ? 9 : 6;
|
|
553
|
+
default:
|
|
554
|
+
return 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** A "+" accessory's bonus (helmet +2, shield +6, p.437-438): the row's, else the class value. */
|
|
558
|
+
get armorBonus() {
|
|
559
|
+
const w = this.row?.wearable;
|
|
560
|
+
if (w?.armorBonus !== undefined)
|
|
561
|
+
return w.armorBonus;
|
|
562
|
+
if (w?.armor !== undefined)
|
|
563
|
+
return 0;
|
|
564
|
+
if (this.category === Category.Helmet)
|
|
565
|
+
return 2;
|
|
566
|
+
if (this.category === Category.Shield)
|
|
567
|
+
return 6;
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
// ---- Effects and capabilities (catalog v13) ----
|
|
571
|
+
/**
|
|
572
|
+
* WHAT THIS THING DOES, in the engine's vocabulary: the row's effects
|
|
573
|
+
* plus everything fitted into it (a smartlink in the goggles, a laser
|
|
574
|
+
* sight on the gun). A fitted mod's effects apply whenever the host
|
|
575
|
+
* does, so they are re-tagged with the host's own `when`.
|
|
576
|
+
*/
|
|
577
|
+
get effects() {
|
|
578
|
+
const own = this.row?.effects ?? [];
|
|
579
|
+
if (this.fitted.length === 0)
|
|
580
|
+
return own;
|
|
581
|
+
const hostWhen = this.defaultWhen;
|
|
582
|
+
const fromMods = this.fitted.flatMap(m => m.effects.map(e => e.when === 'fitted' ? { ...e, when: hostWhen } : e));
|
|
583
|
+
return [...own, ...fromMods];
|
|
584
|
+
}
|
|
585
|
+
/** When a row's effects apply by default: worn for a wearable, held for a weapon, carried otherwise. */
|
|
586
|
+
get defaultWhen() {
|
|
587
|
+
if (this.row?.weapon || this.category === Category.MeleeWeapon || this.isFirearm())
|
|
588
|
+
return 'held';
|
|
589
|
+
if (this.row?.wearable)
|
|
590
|
+
return 'worn';
|
|
591
|
+
return 'carried';
|
|
592
|
+
}
|
|
593
|
+
/** A flag the row (or a fitted mod) grants, by key; its value, or 0. */
|
|
594
|
+
capability(key) {
|
|
595
|
+
let total = 0;
|
|
596
|
+
for (const e of this.effects)
|
|
597
|
+
if (e.type === 'flag' && e.property === key)
|
|
598
|
+
total += e.value;
|
|
599
|
+
return total;
|
|
600
|
+
}
|
|
601
|
+
hasCapability(key) {
|
|
602
|
+
return this.capability(key) > 0;
|
|
603
|
+
}
|
|
604
|
+
// ---- Fitting (catalog v13; commands/fit.ts) ----
|
|
605
|
+
// Wearables hold mods by CAPACITY (goggles 4, helmet 6; a vision
|
|
606
|
+
// enhancement costs 1, p.443-445) and guns hold accessories by MOUNT
|
|
607
|
+
// (top / under / barrel, one accessory each, p.432). Built-in
|
|
608
|
+
// accessories occupy no mount. The fitted list rides the save.
|
|
609
|
+
fitted = [];
|
|
610
|
+
/** Capacity this thing offers to mods. */
|
|
611
|
+
get capacity() {
|
|
612
|
+
return this.row?.wearable?.capacity ?? this.row?.mod?.capacity ?? 0;
|
|
613
|
+
}
|
|
614
|
+
/** What kinds of mod it holds. */
|
|
615
|
+
get holds() {
|
|
616
|
+
const w = this.row?.wearable?.holds;
|
|
617
|
+
if (w)
|
|
618
|
+
return w;
|
|
619
|
+
return this.row?.mod?.capacity ? ['vision'] : [];
|
|
620
|
+
}
|
|
621
|
+
get capacityUsed() {
|
|
622
|
+
return this.fitted.reduce((n, m) => n + (m.row?.mod?.capacityCost ?? 0), 0);
|
|
623
|
+
}
|
|
624
|
+
/** The accessory seats a gun has: the row's, else the class rule (p.432). */
|
|
625
|
+
get mounts() {
|
|
626
|
+
const fromRow = this.row?.weapon?.mounts;
|
|
627
|
+
if (fromRow)
|
|
628
|
+
return fromRow;
|
|
629
|
+
if (!this.isFirearm())
|
|
630
|
+
return [];
|
|
631
|
+
switch (this.rangeClass) {
|
|
632
|
+
case 'holdout': return [];
|
|
633
|
+
case 'taser': return ['top'];
|
|
634
|
+
case 'light-pistol':
|
|
635
|
+
case 'heavy-pistol':
|
|
636
|
+
case 'machine-pistol':
|
|
637
|
+
case 'smg': return ['top', 'barrel'];
|
|
638
|
+
default: return ['top', 'under', 'barrel'];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/** Accessories the factory fitted (identifiers, see ICatalogWeapon.builtIn). */
|
|
642
|
+
get builtIn() {
|
|
643
|
+
return this.row?.weapon?.builtIn ?? [];
|
|
644
|
+
}
|
|
645
|
+
hasBuiltIn(id) {
|
|
646
|
+
return this.builtIn.includes(id);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* A SMARTGUN (p.433): a smartgun system built in or fitted. Before v13
|
|
650
|
+
* every firearm in the engine counted as one; canon makes it an
|
|
651
|
+
* accessory. Generated gear with no row is a smartgun only when it
|
|
652
|
+
* says so.
|
|
653
|
+
*/
|
|
654
|
+
get isSmartgun() {
|
|
655
|
+
if (!this.isFirearm())
|
|
656
|
+
return false;
|
|
657
|
+
if (this.row)
|
|
658
|
+
return this.hasBuiltIn('smartgun-internal') || this.fitted.some(m => m.hasCapability('smartgun'));
|
|
659
|
+
return /smartgun|smartlink/.test(this.weaponHay) || this.fitted.some(m => m.hasCapability('smartgun'));
|
|
660
|
+
}
|
|
661
|
+
/** Which mount a fitted accessory sits in: the first of its mounts still free when it was fitted. */
|
|
662
|
+
mountOf(mod) {
|
|
663
|
+
const taken = new Set();
|
|
664
|
+
for (const m of this.fitted) {
|
|
665
|
+
const wants = m.row?.mod?.mount ?? [];
|
|
666
|
+
const seat = wants.find(p => this.mounts.includes(p) && !taken.has(p));
|
|
667
|
+
if (seat)
|
|
668
|
+
taken.add(seat);
|
|
669
|
+
if (m === mod)
|
|
670
|
+
return seat;
|
|
671
|
+
}
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Why a mod will not fit, or null when it will. The message is the
|
|
676
|
+
* player's line, so it names the rule that refused.
|
|
677
|
+
*/
|
|
678
|
+
fitRefusal(mod) {
|
|
679
|
+
const spec = mod.row?.mod;
|
|
680
|
+
if (!spec)
|
|
681
|
+
return `${mod.name} is not something that fits into anything.`;
|
|
682
|
+
if (this.fitted.includes(mod))
|
|
683
|
+
return `${mod.name} is already fitted to ${this.name}.`;
|
|
684
|
+
if (spec.kind === 'weapon') {
|
|
685
|
+
if (!this.isFirearm())
|
|
686
|
+
return `${mod.name} mounts on a gun, and ${this.name} is not one.`;
|
|
687
|
+
const mounts = this.mounts;
|
|
688
|
+
if (mounts.length === 0)
|
|
689
|
+
return `${this.name} has no mounts -- nothing bolts onto it (p.432).`;
|
|
690
|
+
if (spec.excludes?.includes('shotgun') && this.rangeClass === 'shotgun')
|
|
691
|
+
return `${mod.name} will not fit a shotgun (p.432).`;
|
|
692
|
+
if (spec.excludes?.includes('revolver') && this.row?.weapon?.ammo?.feed === 'cy')
|
|
693
|
+
return `${mod.name} will not fit a revolver (p.432).`;
|
|
694
|
+
const taken = new Set(this.fitted.map(m => this.mountOf(m)).filter((p) => p !== undefined));
|
|
695
|
+
const free = (spec.mount ?? []).filter(p => mounts.includes(p) && !taken.has(p));
|
|
696
|
+
if (free.length === 0)
|
|
697
|
+
return `${this.name} has no free ${(spec.mount ?? []).join(' or ')} mount for ${mod.name}.`;
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
if (!this.holds.includes(spec.kind))
|
|
701
|
+
return `${this.name} does not take a ${spec.kind} enhancement.`;
|
|
702
|
+
const cost = spec.capacityCost ?? 0;
|
|
703
|
+
if (this.capacityUsed + cost > this.capacity)
|
|
704
|
+
return `${this.name} has ${this.capacity - this.capacityUsed} capacity left and ${mod.name} needs ${cost}.`;
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
/** Fits a mod; the refusal line, or null when it went in. */
|
|
708
|
+
fit(mod) {
|
|
709
|
+
const why = this.fitRefusal(mod);
|
|
710
|
+
if (why)
|
|
711
|
+
return why;
|
|
712
|
+
this.fitted.push(mod);
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
/** Takes a mod back out; false when it was not there. */
|
|
716
|
+
unfit(mod) {
|
|
717
|
+
const i = this.fitted.indexOf(mod);
|
|
718
|
+
if (i < 0)
|
|
719
|
+
return false;
|
|
720
|
+
this.fitted.splice(i, 1);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
/** The drone's stat block, when it has one. */
|
|
724
|
+
get vehicle() {
|
|
725
|
+
return this.row?.vehicle;
|
|
726
|
+
}
|
|
727
|
+
/** Pilot rating (p.199): the row's, else the price-tier fallback the engine always used. */
|
|
728
|
+
get pilotRating() {
|
|
729
|
+
return this.vehicle?.pilot ?? 2 + this.qualityBonus;
|
|
730
|
+
}
|
|
731
|
+
/** A drone's hull armor (p.199): the row's, else the tier fallback. */
|
|
732
|
+
get droneArmor() {
|
|
733
|
+
return this.vehicle?.armor ?? 2 * this.qualityBonus;
|
|
734
|
+
}
|
|
735
|
+
/** A drone's Body (p.199): the row's, else the tier fallback. */
|
|
736
|
+
get droneBody() {
|
|
737
|
+
return this.vehicle?.body ?? 2 + this.qualityBonus;
|
|
738
|
+
}
|
|
739
|
+
/** Does the drone carry a weapon mount: the row's, else the name says. */
|
|
740
|
+
get hasWeaponMount() {
|
|
741
|
+
const v = this.vehicle;
|
|
742
|
+
if (v)
|
|
743
|
+
return v.mount !== undefined;
|
|
744
|
+
return /armed|combat|gun|roto|assault|sentry|weapon/i.test(this.weaponHay);
|
|
745
|
+
}
|
|
404
746
|
// ================= Device condition monitor (cyberdecks) =================
|
|
405
747
|
// Matrix combat damage lands on HARDWARE: the deck you jacked in with
|
|
406
748
|
// (see combat-exchange.ts routing and Player.takeMatrixDamage). A full
|
|
@@ -411,9 +753,9 @@ export class Item extends AbstractItem {
|
|
|
411
753
|
isCyberdeck() {
|
|
412
754
|
return this.category === Category.Cyberdeck;
|
|
413
755
|
}
|
|
414
|
-
/**
|
|
756
|
+
/** The Matrix condition monitor (p.228): 8 + (Device Rating / 2), rounded up. */
|
|
415
757
|
get maxDeckBoxes() {
|
|
416
|
-
return 8 +
|
|
758
|
+
return 8 + Math.ceil(this.deviceRating / 2);
|
|
417
759
|
}
|
|
418
760
|
get deckDamage() {
|
|
419
761
|
return this._deckDamage;
|
|
@@ -425,8 +767,12 @@ export class Item extends AbstractItem {
|
|
|
425
767
|
// engine's quality tiers map onto the book's table exactly (Summit
|
|
426
768
|
// DR1 = Common ... Cyber-5 DR5 = Legendary), so generated street
|
|
427
769
|
// decks inherit real stats for free.
|
|
428
|
-
/**
|
|
770
|
+
/** Device Rating, NUMERIC (catalog v13): the row's, else the tier ladder
|
|
771
|
+
* (p.227: Summit 1, Chariot 2, Tsurugi 3, Cyber-5 5, Excalibur-class 6). */
|
|
429
772
|
get deviceRating() {
|
|
773
|
+
const fromRow = this.row?.device?.deviceRating;
|
|
774
|
+
if (fromRow !== undefined)
|
|
775
|
+
return fromRow;
|
|
430
776
|
switch (this.qualityBonus) {
|
|
431
777
|
case 0: return 1;
|
|
432
778
|
case 1: return 2;
|
|
@@ -435,8 +781,12 @@ export class Item extends AbstractItem {
|
|
|
435
781
|
default: return 6;
|
|
436
782
|
}
|
|
437
783
|
}
|
|
438
|
-
/** The p.227 attribute array (highest first), by Device Rating. */
|
|
784
|
+
/** The p.227 attribute array (highest first): the row's four numbers, else by Device Rating. */
|
|
439
785
|
get deckArray() {
|
|
786
|
+
const d = this.row?.device;
|
|
787
|
+
if (d && d.attack !== undefined && d.sleaze !== undefined && d.dataProcessing !== undefined && d.firewall !== undefined) {
|
|
788
|
+
return [d.attack, d.sleaze, d.dataProcessing, d.firewall];
|
|
789
|
+
}
|
|
440
790
|
switch (this.deviceRating) {
|
|
441
791
|
case 1: return [4, 3, 3, 1];
|
|
442
792
|
case 2: return [5, 4, 4, 2];
|
|
@@ -445,9 +795,9 @@ export class Item extends AbstractItem {
|
|
|
445
795
|
default: return [9, 8, 7, 6]; // Excalibur-class (canon-approximate)
|
|
446
796
|
}
|
|
447
797
|
}
|
|
448
|
-
/** How many programs run at once (p.227; storage is unlimited). */
|
|
798
|
+
/** How many programs run at once (p.227; storage is unlimited): the row's, else the Device Rating. */
|
|
449
799
|
get programSlots() {
|
|
450
|
-
return Math.min(6, this.deviceRating);
|
|
800
|
+
return this.row?.device?.programs ?? Math.min(6, this.deviceRating);
|
|
451
801
|
}
|
|
452
802
|
// Which array value sits in which attribute: indexes into deckArray
|
|
453
803
|
// for [attack, sleaze, dataProcessing, firewall]. Default boot config
|
|
@@ -520,7 +870,9 @@ export class Item extends AbstractItem {
|
|
|
520
870
|
}
|
|
521
871
|
attrValue(slot, key) {
|
|
522
872
|
const base = this.deckArray[this._deckOrder[slot]];
|
|
523
|
-
|
|
873
|
+
// What a RUNNING program adds (catalog v13: the program row's effects,
|
|
874
|
+
// `attribute`/`matrix`; PROGRAM_ATTR_MODS is the offline fallback).
|
|
875
|
+
const mods = this.loadedPrograms.reduce((sum, k) => sum + programMatrixMod(k, key), 0);
|
|
524
876
|
return base + mods;
|
|
525
877
|
}
|
|
526
878
|
get attack() { return this.attrValue(0, 'attack'); }
|
|
@@ -573,7 +925,7 @@ export class Item extends AbstractItem {
|
|
|
573
925
|
get matrixDamageBonus() {
|
|
574
926
|
if (!this.isCyberdeck())
|
|
575
927
|
return this.qualityBonus;
|
|
576
|
-
return Math.floor(this.attack / 2) +
|
|
928
|
+
return Math.floor(this.attack / 2) + this.loadedPrograms.reduce((sum, k) => sum + programMatrixMod(k, 'damage'), 0);
|
|
577
929
|
}
|
|
578
930
|
/** One-line stat readout: "A5 S4 D4 F2, DR2, 1/2 programs". */
|
|
579
931
|
deckStatLine() {
|