@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.
Files changed (147) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.d.ts +72 -0
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +300 -61
  4. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/aspects.js +11 -1
  6. package/bundle/typescript/src/commands/game/sideQuest/aspects.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +33 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +85 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +2 -2
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +31 -2
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +8 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.d.ts +10 -3
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +17 -4
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.d.ts +0 -7
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js +39 -3
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +31 -5
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/fit.d.ts +29 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/fit.js +85 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/fit.js.map +1 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.d.ts +16 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +273 -28
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +4 -4
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +16 -5
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.d.ts +19 -10
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +43 -227
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +31 -3
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +3 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js.map +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.d.ts +34 -1
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js +81 -12
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/pick.js.map +1 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +5 -0
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +5 -2
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +4 -0
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +4 -6
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.d.ts +10 -0
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +50 -2
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -1
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js +27 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js.map +1 -1
  58. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  59. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +45 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +3 -2
  62. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +34 -41
  63. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +4 -0
  65. package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js.map +1 -1
  66. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +44 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
  68. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +47 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/game.js +115 -11
  70. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  71. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.d.ts +17 -0
  72. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +30 -2
  73. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/models/device.d.ts +86 -0
  75. package/bundle/typescript/src/commands/game/sideQuest/models/device.js +103 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/models/device.js.map +1 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +140 -45
  78. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +456 -104
  79. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  80. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +2 -0
  81. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +7 -5
  82. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  83. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +113 -4
  84. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +289 -47
  85. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  86. package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +4 -0
  87. package/bundle/typescript/src/commands/game/sideQuest/qualities.js +16 -0
  88. package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +9 -9
  90. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +6 -6
  91. package/bundle/typescript/src/commands/game/sideQuest/types/repro.d.ts +8 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/types/repro.js.map +1 -1
  93. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +9 -2
  94. package/bundle/typescript/src/commands/game/sideQuest/types/seed/device-seed.d.ts +17 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/ui.js +23 -26
  96. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  97. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js +1 -1
  98. package/bundle/typescript/src/commands/game/sideQuest/utilities/affordances.js.map +1 -1
  99. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.d.ts +38 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js +67 -0
  101. package/bundle/typescript/src/commands/game/sideQuest/utilities/barrier-combat.js.map +1 -0
  102. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +279 -4
  103. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +221 -212
  104. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
  105. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +48 -5
  106. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +84 -17
  107. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
  108. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +9 -0
  109. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +8 -0
  110. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
  111. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.d.ts +6 -0
  112. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +52 -12
  113. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  114. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.d.ts +32 -0
  115. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js +33 -0
  116. package/bundle/typescript/src/commands/game/sideQuest/utilities/comm-style.js.map +1 -1
  117. package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +10 -1
  118. package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js.map +1 -1
  119. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +5 -0
  120. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +35 -1
  121. package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -1
  122. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js +3 -1
  123. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js.map +1 -1
  124. package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.d.ts +121 -0
  125. package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.js +141 -0
  126. package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.js.map +1 -0
  127. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +11 -3
  128. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  129. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
  130. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  131. package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.d.ts +93 -0
  132. package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js +201 -0
  133. package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js.map +1 -0
  134. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.d.ts +19 -13
  135. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js +77 -34
  136. package/bundle/typescript/src/commands/game/sideQuest/utilities/range.js.map +1 -1
  137. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +91 -9
  138. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js.map +1 -1
  139. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.d.ts +16 -0
  140. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +50 -3
  141. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  142. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +9 -1
  143. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +28 -4
  144. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
  145. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +43 -3
  146. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  147. 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';
@@ -15,6 +17,23 @@ import { AbstractPlayer } from '../types/shared/abstracts.js';
15
17
  export const STANCE_CYCLE = ['manual', 'non-lethal', 'lethal'];
16
18
  /** The old names, still accepted from saves and the stance command. */
17
19
  export const STANCE_ALIASES = { guarded: 'non-lethal', aggressive: 'lethal', nonlethal: 'non-lethal', 'non lethal': 'non-lethal' };
20
+ export const MATRIX_STANCE_CYCLE = ['attack', 'sleaze'];
21
+ /** What each stance is CALLED in the book -- printed everywhere the
22
+ * engine used to say "loud"/"quiet", so the vocabulary teaches the
23
+ * real action names (the whole point of uMQAhaAysgaKpWFkn). */
24
+ export const MATRIX_STANCE_ACTION = {
25
+ attack: 'Brute Force',
26
+ sleaze: 'Hack on the Fly',
27
+ };
28
+ /** The words a player may type, including the engine's older ones. */
29
+ export const MATRIX_STANCE_ALIASES = {
30
+ loud: 'attack', force: 'attack', brute: 'attack', 'brute force': 'attack', bf: 'attack',
31
+ quiet: 'sleaze', sneak: 'sleaze', 'hack on the fly': 'sleaze', hotf: 'sleaze', fly: 'sleaze',
32
+ };
33
+ export function normalizeMatrixStance(v) {
34
+ const k = (v ?? 'attack').toLowerCase();
35
+ return MATRIX_STANCE_ALIASES[k] ?? (MATRIX_STANCE_CYCLE.includes(k) ? k : 'attack');
36
+ }
18
37
  export function normalizeStance(v) {
19
38
  const k = (v ?? 'manual').toLowerCase();
20
39
  return STANCE_ALIASES[k] ?? (STANCE_CYCLE.includes(k) ? k : 'manual');
@@ -49,6 +68,7 @@ export const SKILL_NAMES = [
49
68
  'alchemy', 'disenchanting', 'artificing',
50
69
  'aeronautics-mechanic', 'industrial-mechanic', 'automotive-mechanic', 'nautical-mechanic',
51
70
  'navigation', 'tracking', 'survival',
71
+ 'heavy-weapons', 'throwing-weapons', 'exotic-melee', 'exotic-ranged',
52
72
  ];
53
73
  /**
54
74
  * Skills that fall back to the legacy `combatSkill` attribute when no
@@ -61,8 +81,8 @@ export const COMBAT_SKILL_FALLBACK = new Set([
61
81
  // `combatSkill`, and after the split the skill it rolls depends on the
62
82
  // gun it happens to be holding. Listing one would leave every guard
63
83
  // with a shotgun rolling zero.
64
- 'automatics', 'pistols', 'longarms', 'archery',
65
- 'blades', 'unarmed', 'hacking',
84
+ 'automatics', 'pistols', 'longarms', 'archery', 'heavy-weapons', 'throwing-weapons',
85
+ 'blades', 'clubs', 'unarmed', 'exotic-melee', 'exotic-ranged', 'hacking',
66
86
  ]);
67
87
  /**
68
88
  * WHICH GUN ROLLS WHICH SKILL (SR5 p.130, p.132).
@@ -101,8 +121,13 @@ export const COMBAT_SKILL_FALLBACK = new Set([
101
121
  * (p.433 firearm accessory), a drone's armed mount -- do not apply to a
102
122
  * crossbow.
103
123
  */
104
- export const FIREARM_SKILLS = ['automatics', 'pistols', 'longarms'];
124
+ export const FIREARM_SKILLS = ['automatics', 'pistols', 'longarms', 'heavy-weapons'];
105
125
  export function firearmSkillFor(weapon) {
126
+ // THE ROW SAYS (catalog v13): an Item carries its weapon block's skill,
127
+ // and the regexes below are the fallback for a plain seed object or
128
+ // generated gear (Item.weaponSkill reads the same block first).
129
+ if (weapon.weaponSkill)
130
+ return weapon.weaponSkill;
106
131
  const hay = `${weapon.name} ${weapon.shape ?? ''}`.toLowerCase();
107
132
  if (/bow|crossbow|sling/.test(hay))
108
133
  return 'archery';
@@ -168,6 +193,16 @@ export class Player extends AbstractPlayer {
168
193
  this._autoStance = v;
169
194
  this.noteConditionChanged();
170
195
  }
196
+ /** Which of the book's two intrusion actions the next hack uses
197
+ * (see MatrixStance). Sheet-visible, like its meat-world twin. */
198
+ _matrixStance = 'attack';
199
+ get matrixStance() { return this._matrixStance; }
200
+ set matrixStance(v) {
201
+ if (this._matrixStance === v)
202
+ return;
203
+ this._matrixStance = v;
204
+ this.noteConditionChanged();
205
+ }
171
206
  // SUBDUING (RAW p.195, the "restrain" ask from a real session's runaway
172
207
  // NPC): a grapple links two actors BY NAME -- grappledBy/grappling are
173
208
  // the two ends, grappleThreshold the escape test's bar (the original
@@ -587,6 +622,10 @@ export class Player extends AbstractPlayer {
587
622
  }
588
623
  /** True for the one Electronics item that is a trode net (p.439-440). */
589
624
  static isTrodeNet(item) {
625
+ if (item.hasCapability('dni'))
626
+ return true;
627
+ if (item.row)
628
+ return false;
590
629
  return item.category === Category.Electronics && /\btrodes?\b/i.test(item.name);
591
630
  }
592
631
  /**
@@ -783,8 +822,10 @@ export class Player extends AbstractPlayer {
783
822
  panDefence(against = 'sleaze') {
784
823
  const master = this.getPanMaster();
785
824
  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;
825
+ // What the running programs add (catalog v13): Signal Scrub always,
826
+ // Sneak only while silent -- the rows carry both, and the `only`.
827
+ const scrub = this.gearBonus('pan-defense');
828
+ const sneak = this.runningSilent ? this.gearBonus('pan-defense', { only: 'silent' }) - scrub : 0;
788
829
  const attributeName = against === 'attack' ? 'Willpower' : 'Intuition';
789
830
  const attribute = against === 'attack' ? this.willpower : this.intuition;
790
831
  const firewall = this.isTechnomancer()
@@ -887,7 +928,7 @@ export class Player extends AbstractPlayer {
887
928
  // SMARTLINK, so the bonus belongs to the pairing. Item.accuracy
888
929
  // stays the bare table value -- a gun on a table has no idea who
889
930
  // will pick it up -- and the shooter supplies the other half here.
890
- return acc + this.smartlinkAccuracyBonus(weapon);
931
+ return acc + Math.max(this.smartlinkAccuracyBonus(weapon), this.laserSightBonus(weapon));
891
932
  }
892
933
  return undefined;
893
934
  }
@@ -955,6 +996,13 @@ export class Player extends AbstractPlayer {
955
996
  // generated gear that carries the fiction but not the tag.
956
997
  const eyeCategory = (i) => i.category === Category.Glasses || i.category === Category.EyeCyberwere;
957
998
  const eyeWords = /goggles|glasses|contacts|cybereyes|monocle|visor/;
999
+ // THE ROW FIRST (catalog v13): a Smartlink fitted into the goggles
1000
+ // grants the capability through Item.effects; the name pattern is
1001
+ // the fallback for generated eyewear with no row.
1002
+ if (onFace.hasCapability('smartlink'))
1003
+ return 'gear';
1004
+ if (onFace.row)
1005
+ return undefined;
958
1006
  const found = /smartlink/.test(hay(onFace))
959
1007
  && (eyeCategory(onFace) || eyeWords.test(hay(onFace)));
960
1008
  return found ? 'gear' : undefined;
@@ -976,10 +1024,25 @@ export class Player extends AbstractPlayer {
976
1024
  * never a die, and a die is never a limit.
977
1025
  */
978
1026
  smartlinkAccuracyBonus(weapon) {
979
- if (!weapon?.isFirearm?.())
1027
+ // A SMARTGUN, not any gun (catalog v13; p.433): the system is an
1028
+ // accessory -- built in (Predator V) or bolted on (smartgun-external).
1029
+ // Before v13 every firearm counted; that was the engine's shortcut,
1030
+ // not the book's rule.
1031
+ if (!weapon?.isFirearm?.() || !weapon.isSmartgun)
980
1032
  return 0;
981
1033
  return this.smartlinkTier() ? 2 : 0;
982
1034
  }
1035
+ /**
1036
+ * A LASER SIGHT's +1 Accuracy (p.432), built in or fitted -- and never
1037
+ * cumulative with a smartlink: getAttackLimit takes the larger of the two.
1038
+ */
1039
+ laserSightBonus(weapon) {
1040
+ if (!weapon?.isFirearm?.())
1041
+ return 0;
1042
+ if (weapon.hasBuiltIn('laser-sight'))
1043
+ return 1;
1044
+ return weapon.effects.reduce((n, e) => n + (e.type === 'limit' && e.property === 'accuracy' ? e.value : 0), 0);
1045
+ }
983
1046
  get matrixTaken() {
984
1047
  return this.getAnyCyberdeck()?.deckDamage ?? 0;
985
1048
  }
@@ -1156,6 +1219,53 @@ export class Player extends AbstractPlayer {
1156
1219
  hasAug(key) {
1157
1220
  return this.augmentations.has(key);
1158
1221
  }
1222
+ /**
1223
+ * THE WEAPON BUILT INTO YOUR HANDS (core p.458, xHarA3TNgxZXtZwgp).
1224
+ * Canon routes these through Unarmed Combat and the Physical limit,
1225
+ * so they REPLACE the bare-fists profile rather than sitting in a
1226
+ * hand slot -- see getWeaponProfile and utilities/combat-exchange.ts,
1227
+ * the two places that compute a punch.
1228
+ *
1229
+ * A drawn weapon always wins: chrome in the palm does not stop you
1230
+ * shooting. Mutually exclusive by row (excludes), so at most one is
1231
+ * ever installed and the find is the answer rather than a contest.
1232
+ */
1233
+ implantWeapon() {
1234
+ for (const key of this.augmentations) {
1235
+ const w = getAug(key)?.implantWeapon;
1236
+ if (!w)
1237
+ continue;
1238
+ // A shock hand out of charges is a hand (p.458).
1239
+ if (w.charges !== undefined && this.implantCharges <= 0)
1240
+ return undefined;
1241
+ return w;
1242
+ }
1243
+ return undefined;
1244
+ }
1245
+ /**
1246
+ * Jolts left in a shock hand. Ten per hand in the book, recharging
1247
+ * one per ten seconds while plugged in -- there is no wall clock here
1248
+ * for a hand to be plugged into, so the trickle is compressed to a
1249
+ * refill on rest (commands/rest.ts). Session state: a fresh install
1250
+ * arrives charged.
1251
+ */
1252
+ implantCharges = 0;
1253
+ /** Spends one jolt. Returns what is left. */
1254
+ spendImplantCharge() {
1255
+ if (this.implantCharges > 0)
1256
+ this.implantCharges--;
1257
+ return this.implantCharges;
1258
+ }
1259
+ /** Back to full -- a rest, or the moment the chrome goes in. */
1260
+ refillImplantCharges() {
1261
+ for (const key of this.augmentations) {
1262
+ const w = getAug(key)?.implantWeapon;
1263
+ if (w?.charges !== undefined) {
1264
+ this.implantCharges = w.charges;
1265
+ return;
1266
+ }
1267
+ }
1268
+ }
1159
1269
  /** Full essence points lost, rounded up -- the toll Magic/Resonance pay. */
1160
1270
  get essenceBurn() {
1161
1271
  return Math.ceil(Player.ESSENCE_MAX - this.essence - 1e-9);
@@ -1177,6 +1287,95 @@ export class Player extends AbstractPlayer {
1177
1287
  const burn = this.essenceBurn;
1178
1288
  this.magic = Math.max(0, this.baseMagic - burn);
1179
1289
  this.resonance = Math.max(0, this.baseResonance - burn);
1290
+ // A shock hand wakes up charged (p.458: ten per hand).
1291
+ this.refillImplantCharges();
1292
+ // A HOUSING TAKES THE DECK YOU BROUGHT (Chrome Flesh p.222). Nothing
1293
+ // is granted -- the book charges 5,000 PLUS the deck, so this moves
1294
+ // the one you own inside your skull and it stops being liftable.
1295
+ if (getAug(aug.key)?.implantsCarriedDeck)
1296
+ this.getAnyCyberdeck()?.implantIntoBody();
1297
+ }
1298
+ // ==================== Gear effects (catalog v13) ====================
1299
+ // A catalog row says what it does (`effects`), and this is the one
1300
+ // place the engine reads that: what is worn (and fitted into it), what
1301
+ // is drawn, what is carried, what runs on the deck, what is stored on
1302
+ // a commlink. Same shape as augBonus / qualityBonus so a pool adds the
1303
+ // three the same way.
1304
+ /** Every effect that applies right now, with the rating it scales by. */
1305
+ applyingEffects() {
1306
+ const out = [];
1307
+ const take = (effects, defaultWhen, state, rating) => {
1308
+ for (const e of effects)
1309
+ if ((e.when ?? defaultWhen) === state)
1310
+ out.push({ effect: e, rating: rating ?? 1 });
1311
+ };
1312
+ for (const [groupName, group] of Object.entries(this._equipment)) {
1313
+ if (!group)
1314
+ continue;
1315
+ for (const [slot, item] of Object.entries(group)) {
1316
+ if (!item)
1317
+ continue;
1318
+ if (Player.isWeaponSlot(groupName, slot)) {
1319
+ if (this.weaponDrawn)
1320
+ take(item.effects, item.defaultWhen, 'held', item.ratingValue);
1321
+ continue;
1322
+ }
1323
+ take(item.effects, item.defaultWhen, 'worn', item.ratingValue);
1324
+ }
1325
+ }
1326
+ for (const item of this._inventory.getAllItems())
1327
+ take(item.effects, item.defaultWhen, 'carried', item.ratingValue);
1328
+ const runningOn = new Set();
1329
+ const deck = this.activeDeck ?? this.getCyberdeck();
1330
+ if (deck)
1331
+ runningOn.add(deck);
1332
+ const master = this.getPanMaster();
1333
+ if (master)
1334
+ runningOn.add(master);
1335
+ for (const device of runningOn) {
1336
+ for (const key of device.loadedPrograms) {
1337
+ const p = getProgram(key);
1338
+ const row = p ? catalogRowFor(`${p.name} Program`) ?? catalogRowFor(p.name) : undefined;
1339
+ if (row?.effects)
1340
+ take(row.effects, 'running', 'running', row.ratingValue);
1341
+ }
1342
+ }
1343
+ for (const device of [...this.commlinks(), ...this._inventory.getAllItems().filter(i => i.isCyberdeck())]) {
1344
+ for (const key of device.storedPrograms) {
1345
+ const soft = getSoft(key);
1346
+ const row = soft ? catalogRowFor(soft.name) : undefined;
1347
+ if (row?.effects)
1348
+ take(row.effects, 'stored', 'stored', row.ratingValue);
1349
+ }
1350
+ }
1351
+ return out;
1352
+ }
1353
+ sumEffects(type, property, only) {
1354
+ let total = 0;
1355
+ for (const { effect, rating } of this.applyingEffects()) {
1356
+ if (effect.type !== type || effect.property !== property)
1357
+ continue;
1358
+ if (effect.only !== undefined && effect.only !== only)
1359
+ continue;
1360
+ total += effect.value * (effect.scaleBy === 'rating' ? rating : 1);
1361
+ }
1362
+ return total;
1363
+ }
1364
+ /**
1365
+ * Dice the gear adds to a pool or a skill's tests (cf. augBonus,
1366
+ * qualityBonus): `kind` is a pool kind or a SkillName; `only` names a
1367
+ * narrowing an effect may carry ('haggle', 'silent', 'matrix-search').
1368
+ */
1369
+ gearBonus(kind, opts = {}) {
1370
+ return this.sumEffects('dice', kind, opts.only);
1371
+ }
1372
+ /** What the gear adds to a LIMIT (a Shopsoft's +1 Social on a haggle). */
1373
+ gearLimit(kind, opts = {}) {
1374
+ return this.sumEffects('limit', kind, opts.only);
1375
+ }
1376
+ /** A capability the gear grants (dni, smartlink, no-fog, low-light...), by key. */
1377
+ hasCapability(key) {
1378
+ return this.sumEffects('flag', key) > 0;
1180
1379
  }
1181
1380
  /**
1182
1381
  * The single tap every pool reads chrome through. Wired Reflexes tiers
@@ -1442,15 +1641,9 @@ export class Player extends AbstractPlayer {
1442
1641
  const weapon = this.weaponDrawn ? this.getCarriedWeapon() : null;
1443
1642
  if (!weapon)
1444
1643
  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';
1644
+ // The weapon block names the skill (catalog v13); Item.weaponSkill
1645
+ // keeps the name/shape duck-typing for generated gear.
1646
+ return weapon.weaponSkill;
1454
1647
  }
1455
1648
  initializeEquipment() {
1456
1649
  return {
@@ -1883,6 +2076,28 @@ export class Player extends AbstractPlayer {
1883
2076
  static isWeaponSlot(group, slot) {
1884
2077
  return (group === 'rightHand' || group === 'leftHand') && slot === 'weapon';
1885
2078
  }
2079
+ /**
2080
+ * WHERE A THING ACTUALLY IS RIGHT NOW, which for a weapon is not a
2081
+ * property of the slot (user report, 2026-09-03: "when looking at
2082
+ * equipment, and I have a Katana Holstered, I should not see Right
2083
+ * hand Katana (weapon hand) -- it's not in hand, it's in holster").
2084
+ *
2085
+ * SLOT_WORDS is static, so it can only ever say where a weapon lives
2086
+ * when drawn. The weapon slot is really two places -- the hand and the
2087
+ * holster -- and `weaponDrawn` is which one. Everything else in the
2088
+ * game already knew: the HUD prints "Holstered:", equip prints "You
2089
+ * slide it into your holster", and the equipment screen's own top line
2090
+ * prints HOLSTERED. Only the worn listing underneath still read the
2091
+ * static word, so one screen contradicted itself.
2092
+ *
2093
+ * Instance method, not static, precisely because the answer depends on
2094
+ * the actor rather than the slot.
2095
+ */
2096
+ wornSlotWord(group, slot) {
2097
+ if (Player.isWeaponSlot(group, slot) && !this.weaponDrawn)
2098
+ return 'holstered';
2099
+ return Player.slotWord(group, slot);
2100
+ }
1886
2101
  unequip(input) {
1887
2102
  const slotInfo = this.determineEquipmentSlot(input);
1888
2103
  if (slotInfo) {
@@ -2318,15 +2533,33 @@ export class Player extends AbstractPlayer {
2318
2533
  // partly from ignoring armor. Quality still rides on top. A HOLSTERED
2319
2534
  // weapon counts for nothing -- until it's drawn (brandish, or the
2320
2535
  // auto-draw when an exchange opens), your hands are empty.
2321
- const strengthBase = Math.floor(this.strength / 2);
2536
+ // CANON NUMBERS (catalog v13, project owner 2026-09-03): the DV is the
2537
+ // book's -- a Predator's 8, a knife's Strength + 2 -- read off the
2538
+ // weapon block (Item.weaponDv / dvAttribute), with the class value as
2539
+ // the fallback for generated gear. The PRICE TIER NO LONGER ADDS DAMAGE
2540
+ // OR DICE: canon has no such thing, and a Legendary knife is a knife.
2541
+ // Unarmed is (STR)S (p.187); bone lacing's +2 rides augBonus.
2322
2542
  const weapon = this.weaponDrawn ? this.getCarriedWeapon() : null;
2323
2543
  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 };
2544
+ // CHROME IN THE HANDS REPLACES THE FISTS (core p.458,
2545
+ // xHarA3TNgxZXtZwgp): cyber melee weapons use Unarmed Combat and
2546
+ // the Physical limit, so they arrive here rather than in a hand
2547
+ // slot. SURFACE ONE OF TWO -- utilities/combat-exchange.ts computes
2548
+ // this same punch independently, and both read implantWeapon.
2549
+ const implant = this.implantWeapon();
2550
+ if (implant) {
2551
+ const aug = [...this.augmentations].map(k => getAug(k)).find(a => a?.implantWeapon);
2552
+ return {
2553
+ name: aug?.name?.toLowerCase() ?? 'implanted blades',
2554
+ damage: implant.dv + (implant.dvAttribute === 'strength' ? this.strength : 0),
2555
+ poolBonus: 0,
2556
+ armorPen: implant.ap,
2557
+ };
2558
+ }
2559
+ return { name: 'bare fists', damage: this.strength + this.augBonus('unarmed-damage'), poolBonus: 0, armorPen: 0 };
2326
2560
  }
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 };
2561
+ const base = weapon.weaponDv + (weapon.dvAttribute === 'strength' ? this.strength : 0);
2562
+ return { name: weapon.name, damage: base, poolBonus: 0, armorPen: weapon.armorPen };
2330
2563
  }
2331
2564
  /** The weapon riding in a hand slot, drawn or holstered. */
2332
2565
  getCarriedWeapon() {
@@ -2378,18 +2611,16 @@ export class Player extends AbstractPlayer {
2378
2611
  for (const item of Object.values(group)) {
2379
2612
  if (!item || !Player.ARMOR_CATEGORIES.has(item.category))
2380
2613
  continue;
2381
- if (item.category === Category.Helmet) {
2382
- extras.push({ item, bonus: 1 });
2383
- continue;
2384
- }
2385
- if (item.category === Category.Shield) {
2386
- extras.push({ item, bonus: 2 });
2614
+ // CANON NUMBERS (catalog v13): the row's Armor rating -- a jacket's
2615
+ // 12, a helmet's +2 -- with the class value as the fallback.
2616
+ const bonus = item.armorBonus;
2617
+ if (bonus > 0) {
2618
+ extras.push({ item, bonus });
2387
2619
  continue;
2388
2620
  }
2389
- if (item.category === Category.Boots || item.category === Category.Gloves
2390
- || item.category === Category.ArmArmor || item.category === Category.LegArmor)
2621
+ const value = item.armorValue;
2622
+ if (value <= 0)
2391
2623
  continue;
2392
- const value = 1 + item.qualityBonus + (item.isHeavyArmor ? 1 : 0);
2393
2624
  if (value > best) {
2394
2625
  if (piece)
2395
2626
  ignored.push(piece);
@@ -2401,7 +2632,16 @@ export class Player extends AbstractPlayer {
2401
2632
  }
2402
2633
  }
2403
2634
  }
2404
- return { total: best + extras.reduce((n, e) => n + e.bonus, 0), piece, extras, ignored };
2635
+ // p.169: the bonus from "+" accessories is capped at Strength, and
2636
+ // every 2 full points over it costs -1 Agility and Reaction.
2637
+ const bonusRaw = extras.reduce((n, e) => n + e.bonus, 0);
2638
+ const bonusCapped = Math.min(bonusRaw, this.strength);
2639
+ const encumbrance = Math.floor(Math.max(0, bonusRaw - this.strength) / 2);
2640
+ return { total: best + bonusCapped, piece, extras, ignored, bonusCapped, encumbrance };
2641
+ }
2642
+ /** The p.169 encumbrance penalty on Agility and Reaction, as dice off those pools. */
2643
+ get encumbrancePenalty() {
2644
+ return this.armorBreakdown().encumbrance;
2405
2645
  }
2406
2646
  getAttackPool() {
2407
2647
  // Plane decides which attribute drives the fight: meat is muscle
@@ -2477,7 +2717,10 @@ export class Player extends AbstractPlayer {
2477
2717
  // weapon exercises a FIREARM skill at all. Archery is deliberately
2478
2718
  // not in that set -- a smartgun system is a firearm accessory
2479
2719
  // (p.433), and a crossbow has nothing to plug into.
2480
- const smartlink = FIREARM_SKILLS.includes(this.activeCombatSkill()) && this.weaponDrawn
2720
+ // AND THE GUN MUST BE A SMARTGUN (catalog v13, p.433): the wireless
2721
+ // bonus is the smartgun system talking to the smartlink.
2722
+ const drawnGun = this.weaponDrawn ? this.getCarriedWeapon() : null;
2723
+ const smartlink = drawnGun?.isSmartgun && FIREARM_SKILLS.includes(this.activeCombatSkill())
2481
2724
  ? (this.smartlinkTier() === 'gear' ? 1 : this.augBonus('firearms'))
2482
2725
  : 0;
2483
2726
  // RUNNING COSTS YOU (p.162 Running Modifiers): -2 dice on everything
@@ -2488,7 +2731,7 @@ export class Player extends AbstractPlayer {
2488
2731
  // sprinted across a room to reach someone arrived shooting exactly
2489
2732
  // as straight as one who strolled -- and the movement budget's whole
2490
2733
  // 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);
2734
+ 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
2735
  }
2493
2736
  /**
2494
2737
  * The subdual pool: bare-hands work regardless of what's in the other
@@ -2535,7 +2778,7 @@ export class Player extends AbstractPlayer {
2535
2778
  // Improved Reflexes rides Reaction itself (p.309), so it lands here too.
2536
2779
  const adeptBonus = this.adeptDefenseBonus() + this.adeptReactionBonus();
2537
2780
  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);
2781
+ 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
2782
  }
2540
2783
  getSoakPool() {
2541
2784
  // Matrix: firewall -- Logic plus the DECK's quality (your deck is your
@@ -2544,7 +2787,8 @@ export class Player extends AbstractPlayer {
2544
2787
  // meat left behind. Drone: the HULL soaks -- airframe body scaled by
2545
2788
  // build quality; the rider's armor is streets away with their body.
2546
2789
  if (this.plane === 'drone') {
2547
- return Math.max(0, 4 + 2 * (this.riggedDrone?.qualityBonus ?? 0));
2790
+ // Body + Armor (p.199), from the drone's vehicle block (catalog v13).
2791
+ return Math.max(0, (this.riggedDrone?.droneBody ?? 0) + (this.riggedDrone?.droneArmor ?? 0));
2548
2792
  }
2549
2793
  if (this.plane === 'matrix') {
2550
2794
  // MATRIX DAMAGE IS RESISTED WITH DEVICE RATING + FIREWALL
@@ -2561,13 +2805,13 @@ export class Player extends AbstractPlayer {
2561
2805
  }
2562
2806
  // A loaded Armor program hardens it further (+2, p.245 cited).
2563
2807
  const deck = this.activeDeck ?? this.getCyberdeck();
2564
- return Math.max(0, (deck?.deviceRating ?? 0) + this.matrixAttribute('firewall') + (deck?.hasProgram('armor') ? 2 : 0));
2808
+ return Math.max(0, (deck?.deviceRating ?? 0) + this.matrixAttribute('firewall') + this.gearBonus('matrix-soak'));
2565
2809
  }
2566
2810
  if (this.plane === 'astral') {
2567
2811
  return Math.max(0, this.willpower);
2568
2812
  }
2569
2813
  // 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'));
2814
+ return Math.max(0, this.body + this.getArmorValue() + this._sustainedArmor + this.adeptArmorBonus() + this.augBonus('soak') + this.qualityBonus('soak') + this.gearBonus('soak'));
2571
2815
  }
2572
2816
  /**
2573
2817
  * THE INITIATIVE ATTRIBUTE -- what the dice are added TO (p.159,
@@ -3156,12 +3400,9 @@ export class Player extends AbstractPlayer {
3156
3400
  // whose quality feeds the drone pools, and whose destruction is a
3157
3401
  // DUMPSHOCK (see planes.ts leaveDrone).
3158
3402
  riggedDrone;
3159
- /** Whether the ridden drone carries a weapon mount (name/description duck-typing). */
3403
+ /** Whether the ridden drone carries a weapon mount (its vehicle block, catalog v13; the name for generated frames). */
3160
3404
  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 ?? ''}`);
3405
+ return this.riggedDrone?.hasWeaponMount ?? false;
3165
3406
  }
3166
3407
  // Dumpshock disorientation (SR5e p.229): -2 on everything for
3167
3408
  // (10 - Willpower) minutes after a forced dump, wall-clock -- the
@@ -3193,15 +3434,16 @@ export class Player extends AbstractPlayer {
3193
3434
  // "persuaded-by:<name>" marks who already made their play on this actor
3194
3435
  // this job. NPCs are rebuilt each scene, so it resets naturally.
3195
3436
  socialMarks = new Set();
3196
- /** The best Focus item on this actor, if any -- scales casting power. */
3437
+ /** The best Focus item on this actor, if any -- by Force (catalog v13: ratingValue). */
3197
3438
  getBestFocus() {
3198
3439
  return this._inventory.getAllItems()
3199
3440
  .filter(i => i.category === Category.Focus)
3200
- .sort((a, b) => b.qualityBonus - a.qualityBonus)[0];
3441
+ .sort((a, b) => (b.ratingValue ?? 0) - (a.ratingValue ?? 0))[0];
3201
3442
  }
3202
3443
  getMagicPool() {
3203
- const focus = this.getBestFocus();
3204
- const focusBonus = focus ? 1 + focus.qualityBonus : 0;
3444
+ // A focus adds its Force to the tests it is bonded for (p.318): the
3445
+ // row's effects, scaled by Force (catalog v13).
3446
+ const focusBonus = this.gearBonus('spellcasting');
3205
3447
  // SR-proper when the SPELLCASTING skill is rated: Magic + Spellcasting
3206
3448
  // for the Awakened, base + skill for a book-caster. Unrated falls back
3207
3449
  // to the legacy shapes (talent max(5, magic+2); scholar's flat 5), so