@maka/maka-cli 5.119.0 → 5.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +2 -2
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +31 -2
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.d.ts +10 -3
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +17 -4
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +1 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.d.ts +0 -7
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js +39 -3
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +28 -4
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/fit.d.ts +29 -0
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/fit.js +85 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/fit.js.map +1 -0
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +4 -4
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +16 -5
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +3 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js.map +1 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.d.ts +34 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +81 -12
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +5 -2
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  30. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +29 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +4 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js.map +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +7 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/game.js +12 -7
  37. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/models/device.d.ts +72 -0
  39. package/bundle/typescript/src/commands/game/sideQuest/models/device.js +89 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/models/device.js.map +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +134 -45
  42. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +448 -104
  43. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +2 -0
  45. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -5
  46. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +31 -3
  48. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +171 -47
  49. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  50. package/bundle/typescript/src/commands/game/sideQuest/types/repro.d.ts +8 -0
  51. package/bundle/typescript/src/commands/game/sideQuest/types/repro.js.map +1 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +4 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/types/seed/device-seed.d.ts +17 -0
  54. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +1 -1
  55. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js.map +1 -1
  56. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.d.ts +38 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +67 -0
  58. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js.map +1 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +279 -4
  60. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +221 -212
  61. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +24 -5
  63. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +50 -17
  64. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +6 -0
  66. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +28 -11
  67. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  68. package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +16 -1
  69. package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js.map +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js +3 -1
  71. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js.map +1 -1
  72. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +6 -2
  73. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.d.ts +19 -13
  77. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js +77 -34
  78. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js.map +1 -1
  79. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +2 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +22 -3
  81. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
  82. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ import { getSoft, getProgram } from '../utilities/programs.js';
2
+ import { catalogRowFor } from '../utilities/catalog.js';
1
3
  import { getQuality, resolveQualityKey } from '../qualities.js';
2
4
  import { isAspectKey } from '../aspects.js';
3
5
  import { hint } from '../utilities/hints.js';
@@ -49,6 +51,7 @@ export const SKILL_NAMES = [
49
51
  'alchemy', 'disenchanting', 'artificing',
50
52
  'aeronautics-mechanic', 'industrial-mechanic', 'automotive-mechanic', 'nautical-mechanic',
51
53
  'navigation', 'tracking', 'survival',
54
+ 'heavy-weapons', 'throwing-weapons', 'exotic-melee', 'exotic-ranged',
52
55
  ];
53
56
  /**
54
57
  * Skills that fall back to the legacy `combatSkill` attribute when no
@@ -61,8 +64,8 @@ export const COMBAT_SKILL_FALLBACK = new Set([
61
64
  // `combatSkill`, and after the split the skill it rolls depends on the
62
65
  // gun it happens to be holding. Listing one would leave every guard
63
66
  // with a shotgun rolling zero.
64
- 'automatics', 'pistols', 'longarms', 'archery',
65
- 'blades', 'unarmed', 'hacking',
67
+ 'automatics', 'pistols', 'longarms', 'archery', 'heavy-weapons', 'throwing-weapons',
68
+ 'blades', 'clubs', 'unarmed', 'exotic-melee', 'exotic-ranged', 'hacking',
66
69
  ]);
67
70
  /**
68
71
  * WHICH GUN ROLLS WHICH SKILL (SR5 p.130, p.132).
@@ -101,8 +104,13 @@ export const COMBAT_SKILL_FALLBACK = new Set([
101
104
  * (p.433 firearm accessory), a drone's armed mount -- do not apply to a
102
105
  * crossbow.
103
106
  */
104
- export const FIREARM_SKILLS = ['automatics', 'pistols', 'longarms'];
107
+ export const FIREARM_SKILLS = ['automatics', 'pistols', 'longarms', 'heavy-weapons'];
105
108
  export function firearmSkillFor(weapon) {
109
+ // THE ROW SAYS (catalog v13): an Item carries its weapon block's skill,
110
+ // and the regexes below are the fallback for a plain seed object or
111
+ // generated gear (Item.weaponSkill reads the same block first).
112
+ if (weapon.weaponSkill)
113
+ return weapon.weaponSkill;
106
114
  const hay = `${weapon.name} ${weapon.shape ?? ''}`.toLowerCase();
107
115
  if (/bow|crossbow|sling/.test(hay))
108
116
  return 'archery';
@@ -587,6 +595,10 @@ export class Player extends AbstractPlayer {
587
595
  }
588
596
  /** True for the one Electronics item that is a trode net (p.439-440). */
589
597
  static isTrodeNet(item) {
598
+ if (item.hasCapability('dni'))
599
+ return true;
600
+ if (item.row)
601
+ return false;
590
602
  return item.category === Category.Electronics && /\btrodes?\b/i.test(item.name);
591
603
  }
592
604
  /**
@@ -783,8 +795,10 @@ export class Player extends AbstractPlayer {
783
795
  panDefence(against = 'sleaze') {
784
796
  const master = this.getPanMaster();
785
797
  const silent = this.runningSilent ? 2 : 0;
786
- const scrub = master?.hasProgram('signal-scrub') ? 1 : 0;
787
- const sneak = master?.hasProgram('sneak') && this.runningSilent ? 2 : 0;
798
+ // What the running programs add (catalog v13): Signal Scrub always,
799
+ // Sneak only while silent -- the rows carry both, and the `only`.
800
+ const scrub = this.gearBonus('pan-defense');
801
+ const sneak = this.runningSilent ? this.gearBonus('pan-defense', { only: 'silent' }) - scrub : 0;
788
802
  const attributeName = against === 'attack' ? 'Willpower' : 'Intuition';
789
803
  const attribute = against === 'attack' ? this.willpower : this.intuition;
790
804
  const firewall = this.isTechnomancer()
@@ -887,7 +901,7 @@ export class Player extends AbstractPlayer {
887
901
  // SMARTLINK, so the bonus belongs to the pairing. Item.accuracy
888
902
  // stays the bare table value -- a gun on a table has no idea who
889
903
  // will pick it up -- and the shooter supplies the other half here.
890
- return acc + this.smartlinkAccuracyBonus(weapon);
904
+ return acc + Math.max(this.smartlinkAccuracyBonus(weapon), this.laserSightBonus(weapon));
891
905
  }
892
906
  return undefined;
893
907
  }
@@ -955,6 +969,13 @@ export class Player extends AbstractPlayer {
955
969
  // generated gear that carries the fiction but not the tag.
956
970
  const eyeCategory = (i) => i.category === Category.Glasses || i.category === Category.EyeCyberwere;
957
971
  const eyeWords = /goggles|glasses|contacts|cybereyes|monocle|visor/;
972
+ // THE ROW FIRST (catalog v13): a Smartlink fitted into the goggles
973
+ // grants the capability through Item.effects; the name pattern is
974
+ // the fallback for generated eyewear with no row.
975
+ if (onFace.hasCapability('smartlink'))
976
+ return 'gear';
977
+ if (onFace.row)
978
+ return undefined;
958
979
  const found = /smartlink/.test(hay(onFace))
959
980
  && (eyeCategory(onFace) || eyeWords.test(hay(onFace)));
960
981
  return found ? 'gear' : undefined;
@@ -976,10 +997,25 @@ export class Player extends AbstractPlayer {
976
997
  * never a die, and a die is never a limit.
977
998
  */
978
999
  smartlinkAccuracyBonus(weapon) {
979
- if (!weapon?.isFirearm?.())
1000
+ // A SMARTGUN, not any gun (catalog v13; p.433): the system is an
1001
+ // accessory -- built in (Predator V) or bolted on (smartgun-external).
1002
+ // Before v13 every firearm counted; that was the engine's shortcut,
1003
+ // not the book's rule.
1004
+ if (!weapon?.isFirearm?.() || !weapon.isSmartgun)
980
1005
  return 0;
981
1006
  return this.smartlinkTier() ? 2 : 0;
982
1007
  }
1008
+ /**
1009
+ * A LASER SIGHT's +1 Accuracy (p.432), built in or fitted -- and never
1010
+ * cumulative with a smartlink: getAttackLimit takes the larger of the two.
1011
+ */
1012
+ laserSightBonus(weapon) {
1013
+ if (!weapon?.isFirearm?.())
1014
+ return 0;
1015
+ if (weapon.hasBuiltIn('laser-sight'))
1016
+ return 1;
1017
+ return weapon.effects.reduce((n, e) => n + (e.type === 'limit' && e.property === 'accuracy' ? e.value : 0), 0);
1018
+ }
983
1019
  get matrixTaken() {
984
1020
  return this.getAnyCyberdeck()?.deckDamage ?? 0;
985
1021
  }
@@ -1178,6 +1214,88 @@ export class Player extends AbstractPlayer {
1178
1214
  this.magic = Math.max(0, this.baseMagic - burn);
1179
1215
  this.resonance = Math.max(0, this.baseResonance - burn);
1180
1216
  }
1217
+ // ==================== Gear effects (catalog v13) ====================
1218
+ // A catalog row says what it does (`effects`), and this is the one
1219
+ // place the engine reads that: what is worn (and fitted into it), what
1220
+ // is drawn, what is carried, what runs on the deck, what is stored on
1221
+ // a commlink. Same shape as augBonus / qualityBonus so a pool adds the
1222
+ // three the same way.
1223
+ /** Every effect that applies right now, with the rating it scales by. */
1224
+ applyingEffects() {
1225
+ const out = [];
1226
+ const take = (effects, defaultWhen, state, rating) => {
1227
+ for (const e of effects)
1228
+ if ((e.when ?? defaultWhen) === state)
1229
+ out.push({ effect: e, rating: rating ?? 1 });
1230
+ };
1231
+ for (const [groupName, group] of Object.entries(this._equipment)) {
1232
+ if (!group)
1233
+ continue;
1234
+ for (const [slot, item] of Object.entries(group)) {
1235
+ if (!item)
1236
+ continue;
1237
+ if (Player.isWeaponSlot(groupName, slot)) {
1238
+ if (this.weaponDrawn)
1239
+ take(item.effects, item.defaultWhen, 'held', item.ratingValue);
1240
+ continue;
1241
+ }
1242
+ take(item.effects, item.defaultWhen, 'worn', item.ratingValue);
1243
+ }
1244
+ }
1245
+ for (const item of this._inventory.getAllItems())
1246
+ take(item.effects, item.defaultWhen, 'carried', item.ratingValue);
1247
+ const runningOn = new Set();
1248
+ const deck = this.activeDeck ?? this.getCyberdeck();
1249
+ if (deck)
1250
+ runningOn.add(deck);
1251
+ const master = this.getPanMaster();
1252
+ if (master)
1253
+ runningOn.add(master);
1254
+ for (const device of runningOn) {
1255
+ for (const key of device.loadedPrograms) {
1256
+ const p = getProgram(key);
1257
+ const row = p ? catalogRowFor(`${p.name} Program`) ?? catalogRowFor(p.name) : undefined;
1258
+ if (row?.effects)
1259
+ take(row.effects, 'running', 'running', row.ratingValue);
1260
+ }
1261
+ }
1262
+ for (const device of [...this.commlinks(), ...this._inventory.getAllItems().filter(i => i.isCyberdeck())]) {
1263
+ for (const key of device.storedPrograms) {
1264
+ const soft = getSoft(key);
1265
+ const row = soft ? catalogRowFor(soft.name) : undefined;
1266
+ if (row?.effects)
1267
+ take(row.effects, 'stored', 'stored', row.ratingValue);
1268
+ }
1269
+ }
1270
+ return out;
1271
+ }
1272
+ sumEffects(type, property, only) {
1273
+ let total = 0;
1274
+ for (const { effect, rating } of this.applyingEffects()) {
1275
+ if (effect.type !== type || effect.property !== property)
1276
+ continue;
1277
+ if (effect.only !== undefined && effect.only !== only)
1278
+ continue;
1279
+ total += effect.value * (effect.scaleBy === 'rating' ? rating : 1);
1280
+ }
1281
+ return total;
1282
+ }
1283
+ /**
1284
+ * Dice the gear adds to a pool or a skill's tests (cf. augBonus,
1285
+ * qualityBonus): `kind` is a pool kind or a SkillName; `only` names a
1286
+ * narrowing an effect may carry ('haggle', 'silent', 'matrix-search').
1287
+ */
1288
+ gearBonus(kind, opts = {}) {
1289
+ return this.sumEffects('dice', kind, opts.only);
1290
+ }
1291
+ /** What the gear adds to a LIMIT (a Shopsoft's +1 Social on a haggle). */
1292
+ gearLimit(kind, opts = {}) {
1293
+ return this.sumEffects('limit', kind, opts.only);
1294
+ }
1295
+ /** A capability the gear grants (dni, smartlink, no-fog, low-light...), by key. */
1296
+ hasCapability(key) {
1297
+ return this.sumEffects('flag', key) > 0;
1298
+ }
1181
1299
  /**
1182
1300
  * The single tap every pool reads chrome through. Wired Reflexes tiers
1183
1301
  * don't stack -- the higher tier wins (II supersedes I) -- but the
@@ -1442,15 +1560,9 @@ export class Player extends AbstractPlayer {
1442
1560
  const weapon = this.weaponDrawn ? this.getCarriedWeapon() : null;
1443
1561
  if (!weapon)
1444
1562
  return 'unarmed';
1445
- if (weapon.isFirearm())
1446
- return firearmSkillFor(weapon);
1447
- // CLUBS vs BLADES (canon splits them): blunt instruments read as
1448
- // clubs by name/shape; everything else edged defaults to blades.
1449
- // Short tokens are boundary-anchored both sides -- "bat" once
1450
- // matched the tail of "Combat Knife".
1451
- return /club|\bbat\b|baton|staff|hammer|\bpipe\b|wrench|\bsap\b|\bmace\b|crowbar|cudgel/i.test(`${weapon.name} ${weapon.shape}`)
1452
- ? 'clubs'
1453
- : 'blades';
1563
+ // The weapon block names the skill (catalog v13); Item.weaponSkill
1564
+ // keeps the name/shape duck-typing for generated gear.
1565
+ return weapon.weaponSkill;
1454
1566
  }
1455
1567
  initializeEquipment() {
1456
1568
  return {
@@ -2318,15 +2430,18 @@ export class Player extends AbstractPlayer {
2318
2430
  // partly from ignoring armor. Quality still rides on top. A HOLSTERED
2319
2431
  // weapon counts for nothing -- until it's drawn (brandish, or the
2320
2432
  // auto-draw when an exchange opens), your hands are empty.
2321
- const strengthBase = Math.floor(this.strength / 2);
2433
+ // CANON NUMBERS (catalog v13, project owner 2026-09-03): the DV is the
2434
+ // book's -- a Predator's 8, a knife's Strength + 2 -- read off the
2435
+ // weapon block (Item.weaponDv / dvAttribute), with the class value as
2436
+ // the fallback for generated gear. The PRICE TIER NO LONGER ADDS DAMAGE
2437
+ // OR DICE: canon has no such thing, and a Legendary knife is a knife.
2438
+ // Unarmed is (STR)S (p.187); bone lacing's +2 rides augBonus.
2322
2439
  const weapon = this.weaponDrawn ? this.getCarriedWeapon() : null;
2323
2440
  if (!weapon) {
2324
- // Bone lacing turns the fists themselves into weapons.
2325
- return { name: 'bare fists', damage: 1 + strengthBase + this.augBonus('unarmed-damage'), poolBonus: 0, armorPen: 0 };
2441
+ return { name: 'bare fists', damage: this.strength + this.augBonus('unarmed-damage'), poolBonus: 0, armorPen: 0 };
2326
2442
  }
2327
- const bonus = weapon.qualityBonus;
2328
- const base = weapon.category === Category.MeleeWeapon ? weapon.meleeBase + strengthBase : weapon.rangedBase;
2329
- return { name: weapon.name, damage: base + bonus, poolBonus: bonus, armorPen: weapon.armorPen };
2443
+ const base = weapon.weaponDv + (weapon.dvAttribute === 'strength' ? this.strength : 0);
2444
+ return { name: weapon.name, damage: base, poolBonus: 0, armorPen: weapon.armorPen };
2330
2445
  }
2331
2446
  /** The weapon riding in a hand slot, drawn or holstered. */
2332
2447
  getCarriedWeapon() {
@@ -2378,18 +2493,16 @@ export class Player extends AbstractPlayer {
2378
2493
  for (const item of Object.values(group)) {
2379
2494
  if (!item || !Player.ARMOR_CATEGORIES.has(item.category))
2380
2495
  continue;
2381
- if (item.category === Category.Helmet) {
2382
- extras.push({ item, bonus: 1 });
2496
+ // CANON NUMBERS (catalog v13): the row's Armor rating -- a jacket's
2497
+ // 12, a helmet's +2 -- with the class value as the fallback.
2498
+ const bonus = item.armorBonus;
2499
+ if (bonus > 0) {
2500
+ extras.push({ item, bonus });
2383
2501
  continue;
2384
2502
  }
2385
- if (item.category === Category.Shield) {
2386
- extras.push({ item, bonus: 2 });
2503
+ const value = item.armorValue;
2504
+ if (value <= 0)
2387
2505
  continue;
2388
- }
2389
- if (item.category === Category.Boots || item.category === Category.Gloves
2390
- || item.category === Category.ArmArmor || item.category === Category.LegArmor)
2391
- continue;
2392
- const value = 1 + item.qualityBonus + (item.isHeavyArmor ? 1 : 0);
2393
2506
  if (value > best) {
2394
2507
  if (piece)
2395
2508
  ignored.push(piece);
@@ -2401,7 +2514,16 @@ export class Player extends AbstractPlayer {
2401
2514
  }
2402
2515
  }
2403
2516
  }
2404
- return { total: best + extras.reduce((n, e) => n + e.bonus, 0), piece, extras, ignored };
2517
+ // p.169: the bonus from "+" accessories is capped at Strength, and
2518
+ // every 2 full points over it costs -1 Agility and Reaction.
2519
+ const bonusRaw = extras.reduce((n, e) => n + e.bonus, 0);
2520
+ const bonusCapped = Math.min(bonusRaw, this.strength);
2521
+ const encumbrance = Math.floor(Math.max(0, bonusRaw - this.strength) / 2);
2522
+ return { total: best + bonusCapped, piece, extras, ignored, bonusCapped, encumbrance };
2523
+ }
2524
+ /** The p.169 encumbrance penalty on Agility and Reaction, as dice off those pools. */
2525
+ get encumbrancePenalty() {
2526
+ return this.armorBreakdown().encumbrance;
2405
2527
  }
2406
2528
  getAttackPool() {
2407
2529
  // Plane decides which attribute drives the fight: meat is muscle
@@ -2477,7 +2599,10 @@ export class Player extends AbstractPlayer {
2477
2599
  // weapon exercises a FIREARM skill at all. Archery is deliberately
2478
2600
  // not in that set -- a smartgun system is a firearm accessory
2479
2601
  // (p.433), and a crossbow has nothing to plug into.
2480
- const smartlink = FIREARM_SKILLS.includes(this.activeCombatSkill()) && this.weaponDrawn
2602
+ // AND THE GUN MUST BE A SMARTGUN (catalog v13, p.433): the wireless
2603
+ // bonus is the smartgun system talking to the smartlink.
2604
+ const drawnGun = this.weaponDrawn ? this.getCarriedWeapon() : null;
2605
+ const smartlink = drawnGun?.isSmartgun && FIREARM_SKILLS.includes(this.activeCombatSkill())
2481
2606
  ? (this.smartlinkTier() === 'gear' ? 1 : this.augBonus('firearms'))
2482
2607
  : 0;
2483
2608
  // RUNNING COSTS YOU (p.162 Running Modifiers): -2 dice on everything
@@ -2488,7 +2613,7 @@ export class Player extends AbstractPlayer {
2488
2613
  // sprinted across a room to reach someone arrived shooting exactly
2489
2614
  // as straight as one who strolled -- and the movement budget's whole
2490
2615
  // point is that ground has a price.
2491
- return Math.max(0, this.agility + this.skillRating(this.activeCombatSkill()) + adeptBonus + this.getWeaponProfile().poolBonus + this.augBonus('attack') + smartlink + this.woundModifier - this.sustainingPenalty - this.runningPenalty);
2616
+ return Math.max(0, this.agility + this.skillRating(this.activeCombatSkill()) + adeptBonus + this.getWeaponProfile().poolBonus + this.augBonus('attack') + this.gearBonus('attack') + this.gearBonus(this.activeCombatSkill()) + smartlink + this.woundModifier - this.sustainingPenalty - this.runningPenalty - this.encumbrancePenalty);
2492
2617
  }
2493
2618
  /**
2494
2619
  * The subdual pool: bare-hands work regardless of what's in the other
@@ -2535,7 +2660,7 @@ export class Player extends AbstractPlayer {
2535
2660
  // Improved Reflexes rides Reaction itself (p.309), so it lands here too.
2536
2661
  const adeptBonus = this.adeptDefenseBonus() + this.adeptReactionBonus();
2537
2662
  const grappled = this.grappledBy ? 2 : 0;
2538
- return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + adeptBonus + this.augBonus('defense') + this.woundModifier - this.sustainingPenalty - grappled);
2663
+ return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + adeptBonus + this.augBonus('defense') + this.gearBonus('defense') + this.woundModifier - this.sustainingPenalty - grappled - this.encumbrancePenalty);
2539
2664
  }
2540
2665
  getSoakPool() {
2541
2666
  // Matrix: firewall -- Logic plus the DECK's quality (your deck is your
@@ -2544,7 +2669,8 @@ export class Player extends AbstractPlayer {
2544
2669
  // meat left behind. Drone: the HULL soaks -- airframe body scaled by
2545
2670
  // build quality; the rider's armor is streets away with their body.
2546
2671
  if (this.plane === 'drone') {
2547
- return Math.max(0, 4 + 2 * (this.riggedDrone?.qualityBonus ?? 0));
2672
+ // Body + Armor (p.199), from the drone's vehicle block (catalog v13).
2673
+ return Math.max(0, (this.riggedDrone?.droneBody ?? 0) + (this.riggedDrone?.droneArmor ?? 0));
2548
2674
  }
2549
2675
  if (this.plane === 'matrix') {
2550
2676
  // MATRIX DAMAGE IS RESISTED WITH DEVICE RATING + FIREWALL
@@ -2561,13 +2687,13 @@ export class Player extends AbstractPlayer {
2561
2687
  }
2562
2688
  // A loaded Armor program hardens it further (+2, p.245 cited).
2563
2689
  const deck = this.activeDeck ?? this.getCyberdeck();
2564
- return Math.max(0, (deck?.deviceRating ?? 0) + this.matrixAttribute('firewall') + (deck?.hasProgram('armor') ? 2 : 0));
2690
+ return Math.max(0, (deck?.deviceRating ?? 0) + this.matrixAttribute('firewall') + this.gearBonus('matrix-soak'));
2565
2691
  }
2566
2692
  if (this.plane === 'astral') {
2567
2693
  return Math.max(0, this.willpower);
2568
2694
  }
2569
2695
  // Mystic Armor (p.309): the adept power is armor that was never worn.
2570
- return Math.max(0, this.body + this.getArmorValue() + this._sustainedArmor + this.adeptArmorBonus() + this.augBonus('soak') + this.qualityBonus('soak'));
2696
+ return Math.max(0, this.body + this.getArmorValue() + this._sustainedArmor + this.adeptArmorBonus() + this.augBonus('soak') + this.qualityBonus('soak') + this.gearBonus('soak'));
2571
2697
  }
2572
2698
  /**
2573
2699
  * THE INITIATIVE ATTRIBUTE -- what the dice are added TO (p.159,
@@ -3156,12 +3282,9 @@ export class Player extends AbstractPlayer {
3156
3282
  // whose quality feeds the drone pools, and whose destruction is a
3157
3283
  // DUMPSHOCK (see planes.ts leaveDrone).
3158
3284
  riggedDrone;
3159
- /** Whether the ridden drone carries a weapon mount (name/description duck-typing). */
3285
+ /** Whether the ridden drone carries a weapon mount (its vehicle block, catalog v13; the name for generated frames). */
3160
3286
  get droneArmed() {
3161
- const d = this.riggedDrone;
3162
- if (!d)
3163
- return false;
3164
- return /armed|combat|gun|roto|assault|sentry|weapon/i.test(`${d.name} ${d.description ?? ''}`);
3287
+ return this.riggedDrone?.hasWeaponMount ?? false;
3165
3288
  }
3166
3289
  // Dumpshock disorientation (SR5e p.229): -2 on everything for
3167
3290
  // (10 - Willpower) minutes after a forced dump, wall-clock -- the
@@ -3193,15 +3316,16 @@ export class Player extends AbstractPlayer {
3193
3316
  // "persuaded-by:<name>" marks who already made their play on this actor
3194
3317
  // this job. NPCs are rebuilt each scene, so it resets naturally.
3195
3318
  socialMarks = new Set();
3196
- /** The best Focus item on this actor, if any -- scales casting power. */
3319
+ /** The best Focus item on this actor, if any -- by Force (catalog v13: ratingValue). */
3197
3320
  getBestFocus() {
3198
3321
  return this._inventory.getAllItems()
3199
3322
  .filter(i => i.category === Category.Focus)
3200
- .sort((a, b) => b.qualityBonus - a.qualityBonus)[0];
3323
+ .sort((a, b) => (b.ratingValue ?? 0) - (a.ratingValue ?? 0))[0];
3201
3324
  }
3202
3325
  getMagicPool() {
3203
- const focus = this.getBestFocus();
3204
- const focusBonus = focus ? 1 + focus.qualityBonus : 0;
3326
+ // A focus adds its Force to the tests it is bonded for (p.318): the
3327
+ // row's effects, scaled by Force (catalog v13).
3328
+ const focusBonus = this.gearBonus('spellcasting');
3205
3329
  // SR-proper when the SPELLCASTING skill is rated: Magic + Spellcasting
3206
3330
  // for the Awakened, base + skill for a book-caster. Unrated falls back
3207
3331
  // to the legacy shapes (talent max(5, magic+2); scholar's flat 5), so