@archivist-gg/dnd5e 0.3.0 → 0.3.4

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 (260) hide show
  1. package/LICENSES/SRD.md +25 -6
  2. package/dist/armor/armor.codec.js +4 -3
  3. package/dist/armor/armor.parser.js +3 -2
  4. package/dist/armor/armor.schema.d.ts +4 -0
  5. package/dist/armor/armor.schema.js +2 -1
  6. package/dist/armor/armor.types.d.ts +8 -0
  7. package/dist/background/background.codec.js +7 -6
  8. package/dist/background/background.parser.js +6 -5
  9. package/dist/background/background.schema.d.ts +34 -0
  10. package/dist/background/background.schema.js +9 -6
  11. package/dist/background/background.types.d.ts +22 -0
  12. package/dist/{chunk-4TIJKN6O.js → chunk-23LZLBJK.js} +1 -1
  13. package/dist/{chunk-AFKTJXRA.js → chunk-2DFENQIJ.js} +18 -0
  14. package/dist/chunk-2FRUSVZS.js +489 -0
  15. package/dist/chunk-4FWNK4FD.js +159 -0
  16. package/dist/chunk-4U55VNC7.js +22 -0
  17. package/dist/{chunk-4JHOHWBI.js → chunk-4XTNPNQB.js} +1 -1
  18. package/dist/{chunk-HCP3GMHS.js → chunk-5N2GW72F.js} +15 -2
  19. package/dist/{chunk-42Y5IJKT.js → chunk-5U5FFFA3.js} +5 -1
  20. package/dist/{chunk-4F6AIZ36.js → chunk-5XMQPTHD.js} +1 -1
  21. package/dist/{chunk-GJOI2GHP.js → chunk-6GNXYAYP.js} +1 -1
  22. package/dist/chunk-6PKV5V2H.js +178 -0
  23. package/dist/{chunk-66XXW2Z2.js → chunk-6PXTNCBP.js} +2 -2
  24. package/dist/chunk-6X2NV665.js +33 -0
  25. package/dist/{chunk-LV4SNWZQ.js → chunk-6X4KUDMU.js} +8 -1
  26. package/dist/{chunk-ZMR77DDL.js → chunk-AW3EHF3L.js} +1 -1
  27. package/dist/chunk-CJYCVNW2.js +7 -0
  28. package/dist/chunk-COEOOQX2.js +133 -0
  29. package/dist/{chunk-XTIQWSFM.js → chunk-D5U7GMPX.js} +1 -1
  30. package/dist/{chunk-RAWJTA45.js → chunk-DEEHHLAP.js} +1 -1
  31. package/dist/chunk-DPUJDOKV.js +365 -0
  32. package/dist/{chunk-73HOFQQ5.js → chunk-ESS46GR4.js} +1 -1
  33. package/dist/chunk-EVMUJYUD.js +195 -0
  34. package/dist/{chunk-2BWAID3Y.js → chunk-FDBWYMZK.js} +29 -3
  35. package/dist/{chunk-K6Q5OUGK.js → chunk-GFZ7PITJ.js} +86 -10
  36. package/dist/chunk-H7KBLR4X.js +81 -0
  37. package/dist/{chunk-L7E2UT5A.js → chunk-HB7XZXDK.js} +5 -1
  38. package/dist/{chunk-VTYUBINW.js → chunk-HJ5EEZ47.js} +1 -1
  39. package/dist/{chunk-6NK2MUD3.js → chunk-HZA7OE7N.js} +9 -2
  40. package/dist/{chunk-A4WENOUQ.js → chunk-I2NDWFDD.js} +52 -8
  41. package/dist/{chunk-RSDRMH54.js → chunk-IMZFOOVL.js} +1 -1
  42. package/dist/{chunk-3DZ74AKK.js → chunk-IN4M4X7P.js} +8 -2
  43. package/dist/chunk-IUAAXXNO.js +58 -0
  44. package/dist/{chunk-XJNECUPT.js → chunk-JC5UNIVM.js} +19 -0
  45. package/dist/chunk-JJ46T5XX.js +297 -0
  46. package/dist/{chunk-TUIFSVLI.js → chunk-JRDPUDT5.js} +30 -3
  47. package/dist/{chunk-24GFQU7T.js → chunk-JSZ4H6AQ.js} +1 -1
  48. package/dist/chunk-KU3YLF6D.js +106 -0
  49. package/dist/{chunk-5INR4NSL.js → chunk-LDDCNPHD.js} +1 -1
  50. package/dist/{chunk-DZXSWSBP.js → chunk-LMBAEIP6.js} +1 -1
  51. package/dist/chunk-LVQLWRCU.js +15 -0
  52. package/dist/chunk-MI7P3NX3.js +161 -0
  53. package/dist/chunk-O4SRD3J5.js +165 -0
  54. package/dist/{chunk-EMOORSQ7.js → chunk-OSQFPGLM.js} +45 -6
  55. package/dist/chunk-OZP6JFGT.js +162 -0
  56. package/dist/{chunk-NAEPSZAM.js → chunk-PDJSUH24.js} +24 -2
  57. package/dist/chunk-PP2UHLBA.js +50 -0
  58. package/dist/chunk-PVCEA4HZ.js +6 -0
  59. package/dist/{chunk-XLSVNUTK.js → chunk-R3VAKWKW.js} +1 -1
  60. package/dist/chunk-RIXI5D3R.js +89 -0
  61. package/dist/chunk-RLGQ5PZZ.js +16 -0
  62. package/dist/chunk-S7REMWW4.js +21 -0
  63. package/dist/{chunk-7VJ6BOA5.js → chunk-SMLXZUBH.js} +9 -1
  64. package/dist/{chunk-NXSWH3AE.js → chunk-T26ZVCL5.js} +1 -1
  65. package/dist/chunk-TOM66P2P.js +38 -0
  66. package/dist/chunk-UA5KS2PL.js +26 -0
  67. package/dist/{chunk-N6N74K6X.js → chunk-UBBKSYBY.js} +1 -1
  68. package/dist/{chunk-353WXS6S.js → chunk-UIRMA5KM.js} +1 -1
  69. package/dist/{chunk-GE4TLPWM.js → chunk-US5IKYCW.js} +1 -1
  70. package/dist/chunk-UXPGW4TI.js +101 -0
  71. package/dist/chunk-VQAXTIBW.js +100 -0
  72. package/dist/{chunk-WP2CX2JO.js → chunk-WELIKLZF.js} +22 -1
  73. package/dist/chunk-WPWOHGPI.js +172 -0
  74. package/dist/{chunk-GZ46OVGZ.js → chunk-WSVMLNTO.js} +1 -1
  75. package/dist/{chunk-4LZJXJJS.js → chunk-WXWPK3LI.js} +6 -2
  76. package/dist/{chunk-ZZGEVJKX.js → chunk-XBQMCEI2.js} +1 -1
  77. package/dist/{chunk-PHQRGDRQ.js → chunk-XGOFP7AQ.js} +5 -1
  78. package/dist/chunk-XJ2F2WO6.js +8 -0
  79. package/dist/chunk-XQCSGNXE.js +1580 -0
  80. package/dist/chunk-YBLDE5CG.js +42 -0
  81. package/dist/{chunk-JEYQU3JT.js → chunk-YHYTW5QA.js} +2 -18
  82. package/dist/{chunk-TLYDBT5F.js → chunk-YJJTISBN.js} +64 -7
  83. package/dist/{chunk-PZOWBI3B.js → chunk-YLL2QXY2.js} +7 -2
  84. package/dist/{chunk-MDEJHMKK.js → chunk-ZONJ66NE.js} +26 -5
  85. package/dist/class/class.codec.js +12 -9
  86. package/dist/class/class.parser.js +11 -8
  87. package/dist/class/class.schema.d.ts +55 -3
  88. package/dist/class/class.schema.js +9 -7
  89. package/dist/class/class.types.d.ts +48 -3
  90. package/dist/condition/condition.codec.d.ts +9 -0
  91. package/dist/condition/condition.entity-type.d.ts +2 -0
  92. package/dist/condition/condition.parser.d.ts +13 -0
  93. package/dist/condition/condition.parser.js +7 -0
  94. package/dist/condition/condition.schema.d.ts +29 -0
  95. package/dist/condition/condition.types.d.ts +15 -0
  96. package/dist/condition/condition.types.js +0 -0
  97. package/dist/dnd/constants.d.ts +22 -0
  98. package/dist/dnd/constants.js +9 -3
  99. package/dist/dnd/formula-tags.js +2 -2
  100. package/dist/dnd/math.d.ts +26 -0
  101. package/dist/dnd/math.js +12 -2
  102. package/dist/dnd/prose-tags.js +1 -1
  103. package/dist/dnd/resource-formula.d.ts +36 -10
  104. package/dist/dnd/resource-formula.js +10 -155
  105. package/dist/dnd/srd-tag-converter.js +3 -3
  106. package/dist/dnd/tag-grammar.js +1 -1
  107. package/dist/dnd/warn-once.d.ts +2 -0
  108. package/dist/dnd/warn-once.js +8 -0
  109. package/dist/entities/base-item-resolver.js +2 -2
  110. package/dist/entities/slug.d.ts +13 -0
  111. package/dist/entities/slug.js +5 -3
  112. package/dist/entities/wikilink-display.d.ts +3 -0
  113. package/dist/entities/wikilink-display.js +10 -0
  114. package/dist/feat/feat.category-codes.d.ts +32 -0
  115. package/dist/feat/feat.category-codes.js +14 -0
  116. package/dist/feat/feat.codec.js +7 -5
  117. package/dist/feat/feat.parser.js +6 -4
  118. package/dist/feat/feat.schema.d.ts +230 -0
  119. package/dist/feat/feat.schema.js +5 -3
  120. package/dist/feat/feat.types.d.ts +37 -1
  121. package/dist/index.js +2120 -6923
  122. package/dist/item/item.ai-schema.d.ts +1 -1
  123. package/dist/item/item.codec.js +4 -3
  124. package/dist/item/item.enrichment.js +4 -4
  125. package/dist/item/item.parser.js +3 -2
  126. package/dist/item/item.schema.d.ts +69 -3
  127. package/dist/item/item.schema.js +2 -1
  128. package/dist/item/item.types.d.ts +69 -3
  129. package/dist/monster/monster.codec.d.ts +7 -6
  130. package/dist/monster/monster.enrichment.js +4 -4
  131. package/dist/monster/monster.format.d.ts +135 -0
  132. package/dist/monster/monster.format.js +78 -0
  133. package/dist/monster/monster.parser.d.ts +6 -1
  134. package/dist/monster/monster.parser.js +10 -1
  135. package/dist/monster/monster.schema.d.ts +369 -0
  136. package/dist/monster/monster.schema.js +34 -0
  137. package/dist/monster/monster.types.d.ts +200 -14
  138. package/dist/optional-feature/optional-feature.codec.js +6 -5
  139. package/dist/optional-feature/optional-feature.parser.js +5 -4
  140. package/dist/optional-feature/optional-feature.schema.d.ts +176 -0
  141. package/dist/optional-feature/optional-feature.schema.js +4 -3
  142. package/dist/pack.d.ts +1 -1
  143. package/dist/pc/conditions.constants.d.ts +0 -1
  144. package/dist/pc/conditions.constants.js +1 -3
  145. package/dist/pc/decision-recognizer.js +1 -1
  146. package/dist/pc/pc.additional-spells.d.ts +47 -0
  147. package/dist/pc/pc.additional-spells.js +23 -0
  148. package/dist/pc/pc.asi-flatten.d.ts +18 -0
  149. package/dist/pc/pc.decision-engine.d.ts +106 -5
  150. package/dist/pc/pc.decision-engine.js +28 -10
  151. package/dist/pc/pc.defense-normalize.d.ts +11 -0
  152. package/dist/pc/pc.defense-normalize.js +6 -0
  153. package/dist/pc/pc.equipment.d.ts +30 -1
  154. package/dist/pc/pc.equipment.js +12 -7
  155. package/dist/pc/pc.feature-effects.d.ts +225 -11
  156. package/dist/pc/pc.feature-effects.js +21 -5
  157. package/dist/pc/pc.parser.js +2 -2
  158. package/dist/pc/pc.pools.d.ts +11 -3
  159. package/dist/pc/pc.pools.js +9 -13
  160. package/dist/pc/pc.proficiencies.d.ts +60 -4
  161. package/dist/pc/pc.proficiencies.js +89 -37
  162. package/dist/pc/pc.proficiency-grants.d.ts +58 -0
  163. package/dist/pc/pc.proficiency-normalize.d.ts +29 -0
  164. package/dist/pc/pc.proficiency-normalize.js +12 -0
  165. package/dist/pc/pc.proficiency-query.d.ts +13 -0
  166. package/dist/pc/pc.proficiency-query.js +5 -1
  167. package/dist/pc/pc.recalc.d.ts +69 -14
  168. package/dist/pc/pc.recalc.js +291 -142
  169. package/dist/pc/pc.resolver.d.ts +34 -8
  170. package/dist/pc/pc.resolver.js +27 -12
  171. package/dist/pc/pc.resource-seed.d.ts +3 -2
  172. package/dist/pc/pc.resource-seed.js +19 -17
  173. package/dist/pc/pc.resources.d.ts +87 -0
  174. package/dist/pc/pc.resources.js +21 -0
  175. package/dist/pc/pc.rest.d.ts +10 -1
  176. package/dist/pc/pc.rest.js +66 -12
  177. package/dist/pc/pc.schema.d.ts +147 -7
  178. package/dist/pc/pc.schema.js +4 -2
  179. package/dist/pc/pc.slotting.js +3 -3
  180. package/dist/pc/pc.spellcasting.d.ts +80 -7
  181. package/dist/pc/pc.spellcasting.js +18 -6
  182. package/dist/pc/pc.table-column.d.ts +23 -0
  183. package/dist/pc/pc.table-column.js +13 -3
  184. package/dist/pc/pc.types.d.ts +253 -16
  185. package/dist/pc/pool-layout.d.ts +47 -0
  186. package/dist/pc/pool-layout.js +16 -0
  187. package/dist/pc/roll-scope.d.ts +2 -0
  188. package/dist/pc/roll-scope.js +8 -0
  189. package/dist/pc/roll-tag-labels.d.ts +40 -0
  190. package/dist/pc/roll-tag-labels.js +17 -0
  191. package/dist/pc/spell-source.d.ts +21 -0
  192. package/dist/pc/spell-source.js +13 -0
  193. package/dist/race/race.codec.js +9 -7
  194. package/dist/race/race.parser.js +8 -6
  195. package/dist/race/race.schema.d.ts +51 -1
  196. package/dist/race/race.schema.js +8 -5
  197. package/dist/race/race.structural.d.ts +7 -0
  198. package/dist/race/race.structural.js +6 -0
  199. package/dist/race/race.types.d.ts +20 -0
  200. package/dist/schemas/attack-schema.d.ts +5 -0
  201. package/dist/schemas/caster-type-schema.d.ts +14 -0
  202. package/dist/schemas/caster-type-schema.js +6 -0
  203. package/dist/schemas/choice-schema.js +2 -2
  204. package/dist/schemas/entity-extras-schema.d.ts +53 -0
  205. package/dist/schemas/entity-extras-schema.js +10 -0
  206. package/dist/schemas/equipment-grant-schema.d.ts +2 -0
  207. package/dist/schemas/equipment-grant-schema.js +1 -1
  208. package/dist/schemas/feature-alias.d.ts +34 -0
  209. package/dist/schemas/feature-effect-schema.d.ts +124 -0
  210. package/dist/schemas/feature-effect-schema.js +1 -1
  211. package/dist/schemas/feature-schema.d.ts +8 -0
  212. package/dist/schemas/feature-schema.js +6 -4
  213. package/dist/schemas/resource-schema.d.ts +43 -0
  214. package/dist/schemas/resource-schema.js +1 -1
  215. package/dist/schemas/selection-pool-schema.d.ts +2 -0
  216. package/dist/schemas/selection-pool-schema.js +1 -1
  217. package/dist/spell/spell.access.js +23 -3
  218. package/dist/spell/spell.codec.js +4 -3
  219. package/dist/spell/spell.enrichment.js +4 -4
  220. package/dist/spell/spell.parser.d.ts +30 -0
  221. package/dist/spell/spell.parser.js +9 -2
  222. package/dist/spell/spell.scaling.d.ts +27 -6
  223. package/dist/spell/spell.scaling.js +25 -6
  224. package/dist/spell/spell.schema.d.ts +44 -2
  225. package/dist/spell/spell.schema.js +2 -1
  226. package/dist/spell/spell.types.d.ts +14 -0
  227. package/dist/subclass/subclass.codec.d.ts +1 -1
  228. package/dist/subclass/subclass.codec.js +11 -8
  229. package/dist/subclass/subclass.parser.js +10 -7
  230. package/dist/subclass/subclass.schema.d.ts +54 -7
  231. package/dist/subclass/subclass.schema.js +8 -6
  232. package/dist/subclass/subclass.types.d.ts +20 -2
  233. package/dist/types/choice.d.ts +56 -0
  234. package/dist/types/choice.js +21 -3
  235. package/dist/types/equipment-grant.d.ts +8 -1
  236. package/dist/types/feature-effect.d.ts +78 -23
  237. package/dist/types/feature.d.ts +70 -1
  238. package/dist/types/optional-feature.types.d.ts +28 -0
  239. package/dist/types/resource.d.ts +21 -1
  240. package/dist/types/selection-pool.d.ts +17 -2
  241. package/dist/weapon/weapon.classify.js +2 -2
  242. package/dist/weapon/weapon.codec.js +4 -3
  243. package/dist/weapon/weapon.parser.js +3 -2
  244. package/dist/weapon/weapon.schema.d.ts +4 -0
  245. package/dist/weapon/weapon.schema.js +2 -1
  246. package/dist/weapon/weapon.types.d.ts +8 -0
  247. package/package.json +2 -2
  248. package/dist/chunk-5MZSB6OM.js +0 -1089
  249. package/dist/chunk-5XUXXAQJ.js +0 -30
  250. package/dist/chunk-7P65Q426.js +0 -158
  251. package/dist/chunk-AIRFC5NQ.js +0 -143
  252. package/dist/chunk-BH6AMZVY.js +0 -25
  253. package/dist/chunk-CZPMTQOY.js +0 -68
  254. package/dist/chunk-M2EMFD6S.js +0 -269
  255. package/dist/chunk-N2JE7BWL.js +0 -81
  256. package/dist/chunk-NXR3BD2V.js +0 -90
  257. package/dist/chunk-Q5K6LWBZ.js +0 -45
  258. package/dist/chunk-TZK36BU7.js +0 -38
  259. package/dist/chunk-W7HWMMBF.js +0 -46
  260. package/dist/chunk-Y4I74YEG.js +0 -33
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Canonical key for a defense value (damage type or condition).
3
+ *
4
+ * Deliberately SIMPLER than `toProfSlug` (pc.proficiency-normalize.ts): damage types and conditions carry
5
+ * no apostrophes and no hyphens, so there is no U+2019 fold and no space-to-hyphen step. Do NOT reuse
6
+ * `normKey` · it token-sorts and singularizes for weapon display names and is actively wrong here.
7
+ *
8
+ * The whole point of this function is that ONE normalizer serves every reader and writer on the defenses
9
+ * path. Adding a second is the bug R4-P5 exists to remove.
10
+ */
11
+ export declare function toDefenseSlug(s: string): string;
@@ -0,0 +1,6 @@
1
+ import {
2
+ toDefenseSlug
3
+ } from "../chunk-XJ2F2WO6.js";
4
+ export {
5
+ toDefenseSlug
6
+ };
@@ -1,6 +1,7 @@
1
1
  import type { EntityRegistry } from "@archivist-gg/core";
2
2
  import type { Ability } from "@archivist-gg/dnd5e";
3
- import type { AppliedBonuses, DerivedEquipment, ResolvedCharacter, ProficiencySet } from "./pc.types.js";
3
+ import type { WeaponEntity } from "@archivist-gg/dnd5e/weapon/weapon.types";
4
+ import type { AppliedBonuses, AttackRow, DerivedEquipment, ResolvedCharacter, ProficiencySet } from "./pc.types.js";
4
5
  import type { WeaponAbilityOverride } from "./pc.feature-effects.js";
5
6
  interface ProficienciesForQuery {
6
7
  armor: ProficiencySet;
@@ -9,5 +10,33 @@ interface ProficienciesForQuery {
9
10
  }
10
11
  export declare function emptyAppliedBonuses(): AppliedBonuses;
11
12
  export declare function computeAppliedBonuses(resolved: ResolvedCharacter, _profs: ProficienciesForQuery, registry: EntityRegistry, warnings: string[]): AppliedBonuses;
13
+ /** The LIVE weapon-proficiency gate · the one the attack pipeline calls at
14
+ * `computeAttacks` below. Exported for the overlay effect-slug guard
15
+ * (`tests/srd-canonical/overlay-effect-slugs.test.ts`), which validates
16
+ * authored `proficiency_type: weapon` values by asking this function whether
17
+ * any SRD weapon comes out proficient. Note `pc.proficiency-query.ts`'s
18
+ * `isProficientWithWeapon` is a near-twin with NO production callers · it
19
+ * differs on the third arm (it compares only the base category, this one also
20
+ * accepts the full hyphenated form), so guards wanting runtime fidelity must
21
+ * use THIS one. */
22
+ export declare function isWeaponSlugProficient(weapon: WeaponEntity, weaponSlug: string, profs: ProficienciesForQuery): boolean;
23
+ /** The die and ability set an `unarmed-strike` effect (or its synthetic) resolved for this character (R4-G6b §5.1). */
24
+ export interface UnarmedStrikeSpec {
25
+ dice?: string;
26
+ abilities?: Ability[];
27
+ }
28
+ /** The always-present Unarmed Strike row (R4-G6b §5.1): 1 + the modifier bludgeoning, 5 ft, always proficient. With a
29
+ * DIE the damage string reproduces the OUTPUT of `buildAttackRow`'s `formatDice` (a closure over `dmgFlat` that cannot
30
+ * be called): the flat part's `^\+0$` strip equals the whole-string `\+0$` strip for every modifier, so a die reads
31
+ * `1d6-1`. With NO die the base is the number 1, and a signed suffix on a number is unevaluated arithmetic (`1-1`,
32
+ * `1+3`), so the flat base prints EVALUATED and floored at 0: `String(Math.max(0, 1 + mod))` (R4-G7 T8 RIDER-10; the
33
+ * user's Q-3 "1 + STR" is the formula, the evaluated number its display). The
34
+ * ability is STR unless the spec lists others; the highest modifier wins and a LATER candidate wins a tie, so a
35
+ * listed DEX beats STR on a tie (the shipped finesse idiom `mods.dex >= mods.str`). Called from recalc, which owns
36
+ * the walk that produces `spec`; never from `computeAttacks`, so the row is present with or without a registry.
37
+ * The `subLabel` is an ENGINE string, printed as it ships: the sheet humanizes a weapon's category tokens, and
38
+ * this row carries no weapon, so `Unarmed` is capitalised here beside `Unarmed Strike`, `5 ft` and `bludgeoning`
39
+ * rather than in a renderer (R4-G6b live rider R-1). */
40
+ export declare function buildUnarmedRow(mods: Record<Ability, number>, proficiencyBonus: number, spec?: UnarmedStrikeSpec): AttackRow;
12
41
  export declare function computeSlotsAndAttacks(resolved: ResolvedCharacter, mods: Record<Ability, number>, profs: ProficienciesForQuery, registry: EntityRegistry, warnings: string[], proficiencyBonus: number, weaponAbilities?: WeaponAbilityOverride[], weaponMasteries?: string[]): DerivedEquipment;
13
42
  export {};
@@ -1,16 +1,21 @@
1
1
  import {
2
+ buildUnarmedRow,
2
3
  computeAppliedBonuses,
3
4
  computeSlotsAndAttacks,
4
- emptyAppliedBonuses
5
- } from "../chunk-EMOORSQ7.js";
6
- import "../chunk-353WXS6S.js";
7
- import "../chunk-N6N74K6X.js";
5
+ emptyAppliedBonuses,
6
+ isWeaponSlugProficient
7
+ } from "../chunk-OSQFPGLM.js";
8
+ import "../chunk-UIRMA5KM.js";
9
+ import "../chunk-UBBKSYBY.js";
10
+ import "../chunk-RLGQ5PZZ.js";
8
11
  import "../chunk-LWFTHJIE.js";
9
12
  import "../chunk-DNAJRMNN.js";
10
- import "../chunk-4LZJXJJS.js";
11
- import "../chunk-LV4SNWZQ.js";
13
+ import "../chunk-WXWPK3LI.js";
14
+ import "../chunk-6X4KUDMU.js";
12
15
  export {
16
+ buildUnarmedRow,
13
17
  computeAppliedBonuses,
14
18
  computeSlotsAndAttacks,
15
- emptyAppliedBonuses
19
+ emptyAppliedBonuses,
20
+ isWeaponSlugProficient
16
21
  };
@@ -1,6 +1,6 @@
1
- import type { SenseType } from "@archivist-gg/dnd5e/types/feature-effect";
1
+ import type { FeatureEffect, SenseType } from "@archivist-gg/dnd5e/types/feature-effect";
2
2
  import type { Ability } from "@archivist-gg/dnd5e";
3
- import type { DamageRider, ResolvedFeature, RollModifierEntry } from "./pc.types.js";
3
+ import type { DamageRider, DefenseGrant, FeatureSource, ResolvedCharacter, ResolvedFeature, RollModifierEntry, SaveOutcomeEntry } from "./pc.types.js";
4
4
  /**
5
5
  * A melee-attack ability override from a `weapon-ability` effect. `weaponSlugs`
6
6
  * (bare, namespace-stripped) scopes the override to matching weapon types only;
@@ -35,27 +35,84 @@ export interface FeatureEffectTotals {
35
35
  * a "base speed becomes 60" feature). Max across all set effects; 0 = none.
36
36
  * recalc applies it as Math.max(set, race + additive bonuses) so it never
37
37
  * lowers an already-higher speed and is independent of the additive bonus.
38
+ * A `scales_at` progression resolves BEFORE the max, at the effect's own source level (R4-G7 §7.3).
38
39
  */
39
40
  speed_walk_set: number;
41
+ /** Flat ability-score bumps from `ability-score-increase` effects whose `abilities` is a FIXED LIST (the three level-20
42
+ * capstones). `chosen` arms are the ASI SLOT's second encoding and MUST NOT fold (the synthesized feat decision pays
43
+ * them; a flat fold double-counts). No cap: `max` is declared and unread (user ruling 2026-09-03). */
44
+ ability_bonus: Partial<Record<Ability, number>>;
40
45
  /** Max range per sense type granted by effects; 0 = none for that type. */
41
46
  senses: Record<SenseType, number>;
42
- /** One term per ac-bonus effect, labeled with the owning feature's name. */
47
+ /** One term per ac-bonus effect, labeled with the owning feature's name. `condition` is the
48
+ * effect's situational qualifier, carried verbatim onto the ACTerm and never evaluated. */
43
49
  ac_terms: {
44
50
  value: number;
45
51
  requires_armor: boolean;
46
52
  label: string;
53
+ condition?: string;
47
54
  }[];
48
- resistances: string[];
49
- condition_immunities: string[];
50
55
  /**
51
- * skills are kebab-case lowercase slugs (matching skill slugs). armor/weapons
52
- * are lowercase CATEGORY words ("heavy"/"shield", "simple"/"martial") — the
53
- * same form class/race/feat grants use; recalc folds them into the matcher's
54
- * `.categories` bucket, NOT `.specific` (which is per-item slugs). tools/languages
55
- * keep their display spelling; saves are canonical ability keys.
56
+ * The four defense buckets a feature effect can grant, as DefenseGrant ENTRIES (R4-G3a §3.2.1).
57
+ * Deduped by value case-insensitively, first spelling wins, trimmed · exactly the contract
58
+ * `pushUnique` had when these were `string[]` · but the SECOND granting feature's NAME is merged
59
+ * into `sources` rather than discarded. `immunity` and `vulnerability` fold REGARDLESS of
60
+ * `condition` (following `resistance`: dropping a real immunity is worse than showing it
61
+ * unqualified); `immune-condition` keeps its `while` skip. Values stay in the AUTHORED spelling ·
62
+ * toDefenseSlug runs in pc.recalc.ts, where composeDefenseEntries turns these into DefenseEntry.
63
+ */
64
+ resistances: DefenseGrant[];
65
+ immunities: DefenseGrant[];
66
+ vulnerabilities: DefenseGrant[];
67
+ condition_immunities: DefenseGrant[];
68
+ /**
69
+ * skills, tools and languages are canonical slugs (toProfSlug: lowercase, U+2019
70
+ * folded to ASCII, whitespace collapsed to hyphens). armor and weapons entries
71
+ * are only `.toLowerCase()`d, NOT slugified, so an authored value keeps its
72
+ * spaces. recalc folds armor into armor.categories ONLY; it folds every weapons
73
+ * value into weapons.categories and additionally routes a non-category value
74
+ * into weapons.specific. saves are canonical ability keys, and are the one
75
+ * bucket a closed vocabulary genuinely guarantees: normalizeAbility returns null
76
+ * for anything else and the effect is dropped.
77
+ *
78
+ * What is INTENDED and what is ENFORCED differ here, and the gap is silent.
79
+ * INTENDED: skills/tools/languages land on ALL_SKILL_SLUGS / ALL_TOOLS /
80
+ * ALL_LANGUAGES, over which toProfSlug is the identity; armor values are
81
+ * CATEGORY words ("heavy"/"shield"); weapons values are a category word
82
+ * ("simple"/"martial", or an entity-level "martial-melee" form) or an authored
83
+ * weapon NAME. ENFORCED at runtime: nothing. feature-effect-schema's proficiency
84
+ * arm is `value: z.string().min(1)`, an OPEN string, and classifyProficiencyEffect
85
+ * does no membership test. The only guard is
86
+ * tests/srd-canonical/overlay-effect-slugs.test.ts, and it sees OVERLAY-AUTHORED
87
+ * data only · there skill/tool/language are checked against those three lists and
88
+ * weapon against the live gate, while armor and saving-throw are unchecked even
89
+ * there. Vault homebrew reaching this at runtime is checked by nothing.
90
+ *
91
+ * An off-vocabulary value does not crash and does not disappear · it becomes a
92
+ * value that can never coincide with the canonical one. Measured: an authored
93
+ * `{proficiency_type:"tool", value:"Playing Card Set"}` normalizes to
94
+ * "playing-card-set", which is NOT in ALL_TOOLS (that list carries the 2024
95
+ * spelling "playing-cards"). It still folds, and still renders as its own row
96
+ * labeled "Playing Card Set", but everything keyed on the canonical slug · the
97
+ * first-seen-wins dedupe in computeEffectiveProficiencies, an
98
+ * `overrides.tools.remove` suppression, the builder's already-satisfied
99
+ * exclusion · misses it, so the character ends up able to hold both.
100
+ *
101
+ * `skillExpertise` is NOT a seventh bucket · it is a SUBSET of `skills`, in the
102
+ * same canonical slug form, carrying the values whose effect set
103
+ * `expertise: true`. An expertise skill is pushed to BOTH, so every consumer
104
+ * that only knows about proficiency keeps working and recalc's `expSet` gains
105
+ * one source. SKILLS ONLY (spec §7.2): a tool or language effect carrying
106
+ * `expertise: true` folds as a plain proficiency and never lands here. It is
107
+ * not lost · since R4-G4 §9.2 the authored flag travels the GRANT channel
108
+ * instead (`EffectProficiencyGrant.expertise` -> `ProficiencyGrant.expertise`
109
+ * -> `ProficiencyEntry.expertise`), which is the channel the sheet panel and
110
+ * the proficiency modal read. Routing it here as well would put a value in the
111
+ * skill-expertise set that no skill key can ever match.
56
112
  */
57
113
  proficiencies: {
58
114
  skills: string[];
115
+ skillExpertise: string[];
59
116
  tools: string[];
60
117
  languages: string[];
61
118
  saves: Ability[];
@@ -78,6 +135,12 @@ export interface FeatureEffectTotals {
78
135
  * labeled with the owning feature's name.
79
136
  */
80
137
  rollModifiers: RollModifierEntry[];
138
+ /**
139
+ * Order-preserving list of `save-outcome` entries (R4-G3a §5.3), labeled with the owning
140
+ * feature's name. Pass-through (no dedupe/merge); `ability: "any"` becomes an ABSENT ability,
141
+ * which the save chip reads as "every save".
142
+ */
143
+ saveOutcomes: SaveOutcomeEntry[];
81
144
  /**
82
145
  * Lowest weapon-attack crit threshold (natural roll that scores a critical
83
146
  * hit) granted by `crit-range` effects. Folds via Math.min from init 20, so
@@ -86,10 +149,11 @@ export interface FeatureEffectTotals {
86
149
  */
87
150
  critRange: number;
88
151
  /**
89
- * Max extra attacks per Attack action granted by `extra-attack` effects.
152
+ * Max EXTRA attacks per Attack action granted by `extra-attack` effects, never the total:
90
153
  * Non-stacking (D&D Extra Attack features don't stack): folds via Math.max
91
154
  * from init 0, so 0 = no extra attacks. recalc maps this onto
92
155
  * DerivedStats.attacksPerAction as `1 + extraAttack`.
156
+ * A `scales_at` progression resolves BEFORE the max, at the effect's own source level (R4-G7 §7.3).
93
157
  */
94
158
  extraAttack: number;
95
159
  /**
@@ -115,8 +179,158 @@ export declare function emptyFeatureEffectTotals(): FeatureEffectTotals;
115
179
  * is OFF by default, so callers passing no opts see activatable features fold to
116
180
  * nothing (correct — a buff is off until toggled). Non-activatable features fold
117
181
  * unconditionally regardless of opts.
182
+ *
183
+ * R4-G7 §7.3: `levelFor` answers "at what level does an effect from THIS source resolve", which is the rule
184
+ * `resourceLevelFor` (pc.resources.ts) already applies to a resource's own `scales_at`: the character's level
185
+ * in the granting class for a `class` source, the level in the class owning the subclass for a `subclass`
186
+ * source, the total level otherwise. Both production callers pass exactly that (dnd5e `pc.recalc.ts` and the
187
+ * plugin's builder `abilities-step.ts`); a caller that passes none leaves every `scales_at` unresolved and
188
+ * every effect at its BASE value, which is what a fixture with no character behind it should read.
118
189
  */
119
190
  export interface FeatureEffectsOpts {
120
191
  activeBuffs?: Set<string>;
192
+ levelFor?: (source: FeatureSource) => number;
121
193
  }
194
+ /** Does this feature's effects fold right now? An activatable feature folds ONLY
195
+ * while its id is in the active set; a buff is off until toggled. Shared by
196
+ * computeFeatureEffects and the display-side proficiency collector so the fold
197
+ * and the display can never disagree about boons (spec fence F3). */
198
+ export declare function foldsNow(rf: ResolvedFeature, activeBuffs: Set<string>): boolean;
199
+ /** The ONE assembly of everything whose effects fold: authored features plus
200
+ * pool-granted and pool-selected boons, with the active-buff set.
201
+ *
202
+ * Returning BOTH halves is load-bearing: a caller handed only the feature list
203
+ * would drop activeBuffs and computeFeatureEffects would skip every activatable
204
+ * boon · exactly the display/gate divergence this helper exists to close.
205
+ *
206
+ * The guards defend against CAST-BUILT FIXTURES, not against the type: `features`,
207
+ * `pools` and `state` are all non-optional on ResolvedCharacter, but `tests/` is
208
+ * typechecked by nothing. Only fixtures that actually reach this function count,
209
+ * and today that is THREE entry points: recalc, the display-side walk
210
+ * (computeEffectiveProficiencies → collectProficiencyGrants), and this
211
+ * function's own direct tests. All three guards are LIVE. Counts below were
212
+ * measured by deleting each guard and running the suite · re-derive them the
213
+ * same way rather than trusting the numbers, which drift as tests are added:
214
+ * · `pools ?? []` is exercised by `emptyResolved()` in
215
+ * tests/pc-recalc-feature-effects.test.ts, which omits pools and IS fed to
216
+ * recalc, and by `dwarf()` in tests/pc-proficiency-effective.test.ts. This
217
+ * guard predates the extraction; it was already here. Deleting it: 149 tests
218
+ * red across 13 files.
219
+ * · `state?.` is exercised by the "survives a cast-built fixture with no pools
220
+ * and no state" case in tests/pc-feature-effects.test.ts, which calls this
221
+ * function directly with `{ features: [...] } as never`, and again by
222
+ * `dwarf()`. Deleting it: 11 red across 2 files.
223
+ * · `features ?? []` is exercised by `dwarf()` in
224
+ * tests/pc-proficiency-effective.test.ts, which omits `features` and reaches
225
+ * here through the display walk. Deleting it: 7 red, all in that file.
226
+ * ⚠️ This bullet used to read "NOT exercised today, and is the one kept
227
+ * purely for the display-side callers". That was TRUE when written and was
228
+ * falsified LATER IN THIS SAME PHASE by the tasks that wired the display
229
+ * path, because nobody re-read it. A guard's own justification is the thing
230
+ * most likely to go stale under you · re-run the deletion before restating
231
+ * any of these three.
232
+ * Do not "simplify" any of the three away. */
233
+ export declare function assembleEffectFeatures(resolved: ResolvedCharacter): {
234
+ features: ResolvedFeature[];
235
+ activeBuffs: Set<string>;
236
+ };
237
+ /** Spec R4-G1a D2. Whether an effect changes the character it is written on. `subject` is absent on every SRD
238
+ * effect (=== "self"); the converter emits "self" today and will emit other creatures later (Hound of Ill Omen
239
+ * imposes disadvantage on a TARGET). A non-self effect never folds onto the PC: not here, not in
240
+ * collectProficiencyEffectGrants, not in unarmoredACBreakdown, not in recalc's weapon-ability scan. Its FEATURE
241
+ * still resolves and renders (it is not buildOnly); R4-G3a §4 gives it a row-local caption.
242
+ *
243
+ * R4-G3a §4.4: a subject that is neither absent nor "self" warns ONCE per distinct subject string
244
+ * (`warnOnce`, keyed on the subject). The converter's vocabulary here is open, so an unexpected
245
+ * spelling ("Self", "target creature") silently stops folding today; the warning names it without
246
+ * changing the answer. Never a refusal: the return value is unchanged. */
247
+ export declare function foldsOnSelf(eff: {
248
+ subject?: string;
249
+ }): boolean;
250
+ /** The effects of a feature that fold on the character: every reader of `feature.effects` for a derived stat goes through this. */
251
+ export declare function selfEffectsOf(f: {
252
+ effects?: FeatureEffect[];
253
+ }): FeatureEffect[];
122
254
  export declare function computeFeatureEffects(features: ResolvedFeature[], opts?: FeatureEffectsOpts): FeatureEffectTotals;
255
+ /** The ONE interpreter of `kind: "proficiency"` effects (spec fence F3).
256
+ *
257
+ * Two consumers with different needs, so the return carries BOTH forms:
258
+ * `applyEffect` pushes `value` (normalized) into FeatureEffectTotals, while the
259
+ * display collector reads `raw` for armor/weapons · composeGrantEntries stores
260
+ * the raw authored string as ProficiencyEntry.value (spec §4.8 #1) and computes
261
+ * its dedupe key separately. Tools/languages read `value`.
262
+ *
263
+ * `saves` is discriminated because FeatureEffectTotals.proficiencies.saves is
264
+ * Ability[], not string[] (fence F1). A null return means "not a proficiency
265
+ * effect, or an unresolvable ability".
266
+ *
267
+ * `expertise` is REQUIRED and always a boolean, never absent: the authored key
268
+ * is optional, so folding it here once means no consumer has to tell
269
+ * `undefined` from `false`. It is reported for EVERY bucket · deciding that
270
+ * only skills act on it belongs to the fold (spec §7.2), not to the
271
+ * classification. */
272
+ export type ProficiencyClassification = {
273
+ bucket: "skills" | "tools" | "languages" | "armor" | "weapons";
274
+ value: string;
275
+ raw: string;
276
+ expertise: boolean;
277
+ } | {
278
+ bucket: "saves";
279
+ value: Ability;
280
+ raw: string;
281
+ expertise: boolean;
282
+ };
283
+ export declare function classifyProficiencyEffect(eff: FeatureEffect): ProficiencyClassification | null;
284
+ /** One proficiency an effect grants, tagged with the source it came from.
285
+ *
286
+ * BOTH forms are carried because the two display consumers need different ones
287
+ * and, since the tool/language canonicalization landed, they no longer coincide:
288
+ * armor/weapons read `raw` (composeGrantEntries stores the raw authored string
289
+ * as ProficiencyEntry.value and keys its dedupe on toProfSlug separately), while
290
+ * tools/languages read `value`, the canonical slug the effective-proficiency
291
+ * matcher matches its vocabulary and suppressions on. Reaching for the wrong
292
+ * field is silently wrong, not harmlessly identical.
293
+ *
294
+ * `sourceSlug` is the source's slug VERBATIM (namespace and all), because the
295
+ * consumer resolves it against the resolved character's entities to get a
296
+ * display name. */
297
+ export interface EffectProficiencyGrant {
298
+ value: string;
299
+ raw: string;
300
+ sourceKind: FeatureSource["kind"];
301
+ sourceSlug: string;
302
+ /**
303
+ * Set (to `true`) only when the granting effect carried `expertise: true`, and
304
+ * ABSENT otherwise, so a plain grant keeps the exact four-key shape the two
305
+ * display consumers already read. Reported for every bucket, unlike the fold,
306
+ * which routes skills only (spec §7.2). CONSUMED since R4-G4 §9.2 by
307
+ * `toGrants` in pc.proficiency-grants.ts, which spreads it onto
308
+ * `ProficiencyGrant` for every bucket; the tools display is the one reader
309
+ * today, and it reads this rather than re-deriving it from the raw effects.
310
+ */
311
+ expertise?: boolean;
312
+ }
313
+ export type EffectProficiencyGrants = Record<"skills" | "tools" | "languages" | "saves" | "armor" | "weapons", EffectProficiencyGrant[]>;
314
+ /** Every proficiency an effect grants, tagged with the FeatureSource it came from.
315
+ *
316
+ * Deliberately NOT deduped: composeGrantEntries collects distinct SOURCES per
317
+ * value, so collapsing on value alone here would silently drop the second
318
+ * granting entity and blank half the provenance.
319
+ *
320
+ * `skills` and `saves` are collected because classifyProficiencyEffect's bucket
321
+ * union has SIX members and this function indexes the record by it · drop either
322
+ * key and `out[c.bucket].push(...)` is a type error, and a hand-built record
323
+ * missing one crashes on the first such effect. They have NO display consumer:
324
+ * effect-granted skills already reach the sheet via recalc's skill union and
325
+ * saves via its save set, and the Proficiencies panel has only four rows. No
326
+ * test asserts either bucket, and `toGrants` in pc.proficiency-grants.ts reads
327
+ * only the other four. That is deliberate, not an oversight.
328
+ *
329
+ * Reads the same three rules as the fold, and NOTHING else: foldsNow decides
330
+ * what is active, selfEffectsOf decides whose numbers an effect changes and
331
+ * classifyProficiencyEffect decides what a proficiency effect means (spec fence
332
+ * F3), so the display can never disagree with the engine.
333
+ *
334
+ * Reads only effects that fold on self (`selfEffectsOf`), the same predicate as
335
+ * the fold, so a non-self proficiency is neither granted nor displayed. */
336
+ export declare function collectProficiencyEffectGrants(features: ResolvedFeature[], activeBuffs: Set<string>): EffectProficiencyGrants;
@@ -1,10 +1,26 @@
1
1
  import {
2
+ assembleEffectFeatures,
3
+ classifyProficiencyEffect,
4
+ collectProficiencyEffectGrants,
2
5
  computeFeatureEffects,
3
- emptyFeatureEffectTotals
4
- } from "../chunk-AIRFC5NQ.js";
5
- import "../chunk-LV4SNWZQ.js";
6
- import "../chunk-WP2CX2JO.js";
6
+ emptyFeatureEffectTotals,
7
+ foldsNow,
8
+ foldsOnSelf,
9
+ selfEffectsOf
10
+ } from "../chunk-JJ46T5XX.js";
11
+ import "../chunk-TOM66P2P.js";
12
+ import "../chunk-KU3YLF6D.js";
13
+ import "../chunk-RLGQ5PZZ.js";
14
+ import "../chunk-LVQLWRCU.js";
15
+ import "../chunk-6X4KUDMU.js";
16
+ import "../chunk-WELIKLZF.js";
7
17
  export {
18
+ assembleEffectFeatures,
19
+ classifyProficiencyEffect,
20
+ collectProficiencyEffectGrants,
8
21
  computeFeatureEffects,
9
- emptyFeatureEffectTotals
22
+ emptyFeatureEffectTotals,
23
+ foldsNow,
24
+ foldsOnSelf,
25
+ selfEffectsOf
10
26
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  characterSchema
3
- } from "../chunk-A4WENOUQ.js";
4
- import "../chunk-TZK36BU7.js";
3
+ } from "../chunk-I2NDWFDD.js";
4
+ import "../chunk-S7REMWW4.js";
5
5
 
6
6
  // src/pc/pc.parser.ts
7
7
  import * as yaml from "js-yaml";
@@ -1,12 +1,20 @@
1
1
  import type { RegisteredEntity } from "@archivist-gg/core";
2
- import type { ResolvedCharacter, ResolvedClass, ResolvedPool } from "./pc.types.js";
2
+ import type { ResolvedCharacter, ResolvedClass, ResolvedPool, ResolvedPoolEntry } from "./pc.types.js";
3
3
  import type { SelectionPool } from "@archivist-gg/dnd5e/types/selection-pool";
4
4
  import type { OptionalFeatureEntity } from "@archivist-gg/dnd5e/types/optional-feature.types";
5
+ import type { ResourceIndex } from "./pc.resources.js";
5
6
  /** Structural subset of EntityRegistry used here (lets tests pass a fake). */
6
7
  export interface PoolRegistry {
7
8
  search(query: string, entityType: string, limit: number): RegisteredEntity[];
8
9
  getByTypeAndSlug(entityType: string, slug: string): RegisteredEntity | undefined;
9
10
  }
10
11
  export declare function levelPrereqMax(d: OptionalFeatureEntity): number;
11
- export declare function resolvePool(rc: ResolvedClass, classIndex: number, pool: SelectionPool, registry: PoolRegistry): ResolvedPool;
12
- export declare function resolveAllPools(resolved: ResolvedCharacter, registry: PoolRegistry): ResolvedPool[];
12
+ export declare function resolvePool(rc: ResolvedClass, classIndex: number, pool: SelectionPool, registry: PoolRegistry, index: ResourceIndex): ResolvedPool;
13
+ export declare function resolveAllPools(resolved: ResolvedCharacter, registry: PoolRegistry, index: ResourceIndex): ResolvedPool[];
14
+ /** The stored picks that survived resolution but are NOT in the collapsed `available` list, compared by FULL
15
+ * slug: exactly the rule the plugin's `strandedSelections` used before T3 retires it (R4-G5 §3.2.3). Because
16
+ * §9.2.3's resolution rewrites a collapsed twin to its survivor, only a GENUINE prerequisite-failing pick (the
17
+ * one resolved by step 3 from the full scan) can land here, and it keeps its entity so the builder strip and the
18
+ * sheet can both dress it "prerequisite unmet". A granted feature is already excluded from `selected`, so a
19
+ * grant never appears here. Pure over the resolved pool: it reads nothing the resolver did not compute. */
20
+ export declare function strandedPicks(pool: ResolvedPool): ResolvedPoolEntry[];
@@ -1,20 +1,16 @@
1
1
  import {
2
2
  levelPrereqMax,
3
3
  resolveAllPools,
4
- resolvePool
5
- } from "../chunk-5MZSB6OM.js";
6
- import "../chunk-BH6AMZVY.js";
7
- import "../chunk-M2EMFD6S.js";
8
- import "../chunk-5XUXXAQJ.js";
9
- import "../chunk-JEYQU3JT.js";
10
- import "../chunk-N6N74K6X.js";
11
- import "../chunk-4LZJXJJS.js";
12
- import "../chunk-LV4SNWZQ.js";
13
- import "../chunk-VTTA2ALZ.js";
14
- import "../chunk-NAEPSZAM.js";
15
- import "../chunk-WP2CX2JO.js";
4
+ resolvePool,
5
+ strandedPicks
6
+ } from "../chunk-O4SRD3J5.js";
7
+ import "../chunk-PP2UHLBA.js";
8
+ import "../chunk-IUAAXXNO.js";
9
+ import "../chunk-LVQLWRCU.js";
10
+ import "../chunk-6X4KUDMU.js";
16
11
  export {
17
12
  levelPrereqMax,
18
13
  resolveAllPools,
19
- resolvePool
14
+ resolvePool,
15
+ strandedPicks
20
16
  };
@@ -1,8 +1,64 @@
1
1
  import type { ResolvedCharacter } from "./pc.types.js";
2
+ import { type ProficiencyEntry, type ProficiencyGrant } from "./pc.proficiency-grants.js";
3
+ /** Re-exported for the plugin: both types are DECLARED in the leaf so that
4
+ * pc.decision-engine can reference them without importing this module, which
5
+ * would close a cycle (spec §4.3). */
6
+ export type { ProficiencyEntry, ProficiencyOrigin } from "./pc.proficiency-grants.js";
7
+ /** Every bucket carries provenance (spec §7.1). `languages`/`tools` ARE
8
+ * {@link computeEffectiveProficiencies}' output, unmodified · `armor`/`weapons`
9
+ * are composed below from the raw grant walk, because they have no vocabulary
10
+ * constant to canonicalize against (spec §3.3's second rule) · and for `weapons`
11
+ * that grant walk is followed by the chosen `select-entity{weapon}` picks
12
+ * (R4-G3b §7 F5), which land with origin `pick`. */
2
13
  export interface ProficiencyAggregate {
3
- armor: string[];
4
- weapons: string[];
5
- tools: string[];
6
- languages: string[];
14
+ armor: ProficiencyEntry[];
15
+ weapons: ProficiencyEntry[];
16
+ tools: ProficiencyEntry[];
17
+ languages: ProficiencyEntry[];
7
18
  }
19
+ /** Normalized intermediate of every proficiency/language source the DISPLAY path
20
+ * reads, in one place. Among the ENTITY branches only the class one is a live
21
+ * proficiency source today (race/bg/feat `.proficiencies` object fields do not
22
+ * exist on those entities); race languages, bg tool/lang, and feat grants read
23
+ * their real fields, and the four `effect*` buckets carry what a feature's
24
+ * `kind: "proficiency"` effects grant · that is the live path a 2014 race trait
25
+ * actually uses (R4-P3c). Chosen picks come from the single decision walk in
26
+ * pc.decision-engine so display can never diverge from the persisted picks.
27
+ *
28
+ * Every grant bucket carries `{value, source}` rather than a bare value: the
29
+ * walk holds the granting entity and the aggregate needs it to fill
30
+ * `ProficiencyEntry.sources` for armor and weapons (spec §7.1). The
31
+ * language/tool grant buckets are still surfaced here for completeness, but
32
+ * the aggregate no longer reads them · languages and tools come from
33
+ * computeEffectiveProficiencies, which does its own grant walk so that
34
+ * suppressions and manual adds can never be skipped by a second composer.
35
+ *
36
+ * These names are RE-DECLARED by hand rather than inherited: this interface
37
+ * does not `extends ProficiencyGrants`, so a bucket added to the grant walk
38
+ * stays invisible here until it is added here too. `collectProficiencySources`
39
+ * spreads the grants object, so an undeclared bucket still flows at RUNTIME ·
40
+ * the compiler is the only thing that notices, and only where the field is
41
+ * read. Nothing iterates these objects with Object.keys/values/entries, so a
42
+ * declaration gap is inert rather than corrupting; it is still a gap. */
43
+ export interface ProficiencySources {
44
+ classArmor: ProficiencyGrant[];
45
+ classWeaponFixed: ProficiencyGrant[];
46
+ classWeaponCategories: ProficiencyGrant[];
47
+ classToolFixed: ProficiencyGrant[];
48
+ raceLangFixed: ProficiencyGrant[];
49
+ bgToolFixed: ProficiencyGrant[];
50
+ bgLangFixed: ProficiencyGrant[];
51
+ featArmor: ProficiencyGrant[];
52
+ featWeapons: ProficiencyGrant[];
53
+ featTools: ProficiencyGrant[];
54
+ featLanguages: ProficiencyGrant[];
55
+ effectArmor: ProficiencyGrant[];
56
+ effectWeapons: ProficiencyGrant[];
57
+ effectTools: ProficiencyGrant[];
58
+ effectLanguages: ProficiencyGrant[];
59
+ chosenLanguages: string[];
60
+ chosenTools: string[];
61
+ chosenWeapons: string[];
62
+ }
63
+ export declare function collectProficiencySources(resolved: ResolvedCharacter): ProficiencySources;
8
64
  export declare function aggregateProficiencies(resolved: ResolvedCharacter): ProficiencyAggregate;