@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
@@ -1,47 +1,99 @@
1
+ import {
2
+ collectChosenProficiencies,
3
+ collectProficiencyGrants,
4
+ computeEffectiveProficiencies
5
+ } from "../chunk-XQCSGNXE.js";
6
+ import "../chunk-UXPGW4TI.js";
7
+ import "../chunk-DPUJDOKV.js";
8
+ import "../chunk-O4SRD3J5.js";
9
+ import "../chunk-PP2UHLBA.js";
10
+ import "../chunk-IUAAXXNO.js";
11
+ import "../chunk-YHYTW5QA.js";
12
+ import "../chunk-MI7P3NX3.js";
13
+ import "../chunk-PVCEA4HZ.js";
14
+ import "../chunk-UBBKSYBY.js";
15
+ import "../chunk-JJ46T5XX.js";
16
+ import "../chunk-TOM66P2P.js";
17
+ import "../chunk-KU3YLF6D.js";
18
+ import {
19
+ proficiencyLabel,
20
+ toProfSlug
21
+ } from "../chunk-RLGQ5PZZ.js";
22
+ import "../chunk-LVQLWRCU.js";
23
+ import "../chunk-WXWPK3LI.js";
24
+ import "../chunk-6X4KUDMU.js";
25
+ import "../chunk-YBLDE5CG.js";
26
+ import "../chunk-EVMUJYUD.js";
27
+ import "../chunk-VTTA2ALZ.js";
28
+ import "../chunk-OZP6JFGT.js";
29
+ import "../chunk-VQAXTIBW.js";
30
+ import "../chunk-PDJSUH24.js";
31
+ import "../chunk-WELIKLZF.js";
32
+ import "../chunk-4U55VNC7.js";
33
+ import "../chunk-UA5KS2PL.js";
34
+
1
35
  // src/pc/pc.proficiencies.ts
36
+ function collectProficiencySources(resolved) {
37
+ const g = collectProficiencyGrants(resolved);
38
+ const chosen = collectChosenProficiencies(resolved);
39
+ return {
40
+ ...g,
41
+ chosenLanguages: chosen.languages,
42
+ chosenTools: chosen.tools,
43
+ chosenWeapons: chosen.weapons
44
+ };
45
+ }
2
46
  function aggregateProficiencies(resolved) {
3
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
4
- const armor = /* @__PURE__ */ new Set();
5
- const weapons = /* @__PURE__ */ new Set();
6
- const tools = /* @__PURE__ */ new Set();
7
- const languages = /* @__PURE__ */ new Set();
8
- for (const c of resolved.classes) {
9
- const prof = (_a = c.entity) == null ? void 0 : _a.proficiencies;
10
- if (!prof) continue;
11
- for (const a of (_b = prof.armor) != null ? _b : []) armor.add(prettyName(a));
12
- if ((_c = prof.weapons) == null ? void 0 : _c.fixed) for (const w of prof.weapons.fixed) weapons.add(prettyName(w));
13
- if ((_d = prof.weapons) == null ? void 0 : _d.categories) for (const w of prof.weapons.categories) weapons.add(prettyName(w));
14
- if ((_e = prof.tools) == null ? void 0 : _e.fixed) for (const t of prof.tools.fixed) tools.add(prettyName(t));
15
- }
16
- const raceLangs = (_f = resolved.race) == null ? void 0 : _f.languages;
17
- if (raceLangs == null ? void 0 : raceLangs.fixed) for (const l of raceLangs.fixed) languages.add(prettyName(l));
18
- const bg = resolved.background;
19
- if (bg) {
20
- for (const entry of (_g = bg.tool_proficiencies) != null ? _g : []) {
21
- if (entry.kind === "fixed") for (const t of entry.items) tools.add(prettyName(t));
22
- }
23
- for (const entry of (_h = bg.language_proficiencies) != null ? _h : []) {
24
- if (entry.kind === "fixed") for (const l of entry.languages) languages.add(prettyName(l));
47
+ const src = collectProficiencySources(resolved);
48
+ const effective = computeEffectiveProficiencies(resolved);
49
+ return {
50
+ // Walk order defines `sources` order (spec §4.1): class before feats, and
51
+ // for weapons the class `fixed` names before the class categories, matching
52
+ // the insertion order of the bucket this replaced, exactly.
53
+ //
54
+ // Effect buckets go LAST: first-seen wins the dedupe key, so an existing
55
+ // class or feat grant keeps its shipped spelling and label and the effect
56
+ // only appends its source name. Reordering these silently relabels rows.
57
+ //
58
+ // The chosen weapon PICKS are the second argument, so they are seeded after
59
+ // every grant bucket: a weapon both granted and picked stays ONE row that
60
+ // keeps origin `grant` and its granting source(s) (R4-G3b §7 F5 · the same
61
+ // grants-then-picks order computeEffectiveProficiencies uses for the
62
+ // language and tool buckets). Armor passes none.
63
+ armor: composeGrantEntries([src.classArmor, src.featArmor, src.effectArmor]),
64
+ weapons: composeGrantEntries([src.classWeaponFixed, src.classWeaponCategories, src.featWeapons, src.effectWeapons], src.chosenWeapons),
65
+ tools: effective.tools,
66
+ languages: effective.languages
67
+ };
68
+ }
69
+ function composeGrantEntries(buckets, picks = []) {
70
+ const byKey = /* @__PURE__ */ new Map();
71
+ for (const bucket of buckets) {
72
+ for (const g of bucket) {
73
+ const key = toProfSlug(g.value);
74
+ const existing = byKey.get(key);
75
+ if (!existing) {
76
+ byKey.set(key, {
77
+ value: g.value,
78
+ label: prettyName(g.value),
79
+ sources: [g.source],
80
+ origin: "grant"
81
+ });
82
+ continue;
83
+ }
84
+ if (!existing.sources.includes(g.source)) existing.sources.push(g.source);
25
85
  }
26
86
  }
27
- for (const f of resolved.feats) {
28
- const grants = f.proficiencies;
29
- if (!grants) continue;
30
- for (const a of (_i = grants.armor) != null ? _i : []) armor.add(prettyName(a));
31
- for (const w of (_j = grants.weapons) != null ? _j : []) weapons.add(prettyName(w));
32
- for (const t of (_k = grants.tools) != null ? _k : []) tools.add(prettyName(t));
33
- for (const l of (_l = grants.languages) != null ? _l : []) languages.add(prettyName(l));
87
+ for (const v of picks) {
88
+ const k = toProfSlug(v);
89
+ if (!byKey.has(k)) byKey.set(k, { value: v, label: prettyName(v), sources: [], origin: "pick" });
34
90
  }
35
- return {
36
- armor: [...armor].sort(),
37
- weapons: [...weapons].sort(),
38
- tools: [...tools].sort(),
39
- languages: [...languages].sort()
40
- };
91
+ return [...byKey.values()].sort((a, b) => a.label < b.label ? -1 : a.label > b.label ? 1 : 0);
41
92
  }
42
93
  function prettyName(slug) {
43
- return slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
94
+ return proficiencyLabel(slug);
44
95
  }
45
96
  export {
46
- aggregateProficiencies
97
+ aggregateProficiencies,
98
+ collectProficiencySources
47
99
  };
@@ -0,0 +1,58 @@
1
+ import type { ResolvedCharacter } from "./pc.types.js";
2
+ export type ProficiencyOrigin = "grant" | "pick" | "manual" | "custom";
3
+ export interface ProficiencyEntry {
4
+ /** Canonical slug when the value lands on the domain vocabulary, else the raw string. Spec §3.3. */
5
+ value: string;
6
+ /** Display form. Spec §3.3's first code block (languages/tools) or second (armor/weapons). */
7
+ label: string;
8
+ /** EVERY granting entity's display name, in walk order: class, race,
9
+ * background, feats, then feature EFFECTS last (R4-P3c · both composers order
10
+ * the effect bucket last so a first-seen entity grant keeps its own label). */
11
+ sources: string[];
12
+ origin: ProficiencyOrigin;
13
+ /** R4-G4 §9: tool expertise rides the grant channel (the ONE channel the panel
14
+ * and the modal read); the manual tri `overrides.tools.proficiency` (spec §9.3,
15
+ * Tier B) is its second writer, applied in `computeEffectiveProficiencies`.
16
+ * ABSENT rather than `false` on a plain entry. */
17
+ expertise?: boolean;
18
+ }
19
+ /** One granted value plus the display name of the entity that granted it.
20
+ * `expertise` is present (and `true`) only when the granting effect authored it
21
+ * (R4-G4 §9.2), so a plain grant keeps the exact two-key shape every pre-phase
22
+ * reader was written against. */
23
+ export interface ProficiencyGrant {
24
+ value: string;
25
+ source: string;
26
+ expertise?: boolean;
27
+ }
28
+ export interface ProficiencyGrants {
29
+ classArmor: ProficiencyGrant[];
30
+ classWeaponFixed: ProficiencyGrant[];
31
+ classWeaponCategories: ProficiencyGrant[];
32
+ classToolFixed: ProficiencyGrant[];
33
+ raceLangFixed: ProficiencyGrant[];
34
+ bgToolFixed: ProficiencyGrant[];
35
+ bgLangFixed: ProficiencyGrant[];
36
+ featArmor: ProficiencyGrant[];
37
+ featWeapons: ProficiencyGrant[];
38
+ featTools: ProficiencyGrant[];
39
+ featLanguages: ProficiencyGrant[];
40
+ effectArmor: ProficiencyGrant[];
41
+ effectWeapons: ProficiencyGrant[];
42
+ effectTools: ProficiencyGrant[];
43
+ effectLanguages: ProficiencyGrant[];
44
+ }
45
+ /** Every FIXED proficiency/language a character's entities grant, each paired
46
+ * with the display name of the entity that granted it. Walk order is
47
+ * class → race → background → feats, and that order defines `sources` ordering
48
+ * downstream (spec §4.1).
49
+ *
50
+ * This module is a LEAF on purpose: it must import nothing from the decision
51
+ * engine module, so that the engine can import it without a cycle. NOT enforced
52
+ * by any test or lint rule · a previous comment claimed a grep guard that has
53
+ * never existed. It is a convention, and this phase adds a feature-effects
54
+ * import that is cycle-free by inspection, not by tooling.
55
+ * The walk returns raw `{value, source}` buckets and nothing else · no
56
+ * humanizing, no sorting, no dedupe. Label composition stays with the
57
+ * module-private `prettyName` in `pc.proficiencies.ts` (spec §7.1). */
58
+ export declare function collectProficiencyGrants(resolved: ResolvedCharacter): ProficiencyGrants;
@@ -0,0 +1,29 @@
1
+ /** Normalized token-set key for matching class weapon DISPLAY names ("hand crossbows")
2
+ * against weapon-entity names ("Crossbow, hand"). EXACT equality on the key; verified
3
+ * collision-free across the SRD weapon set. Strip-"s" singularizer does not handle
4
+ * irregular plurals; no SRD weapon triggers that. */
5
+ export declare const normKey: (s: string) => string;
6
+ /** Canonical proficiency slug: lowercase, curly apostrophe folded to ASCII,
7
+ * whitespace collapsed to hyphens. Universally idempotent (one pass removes all
8
+ * whitespace) and the IDENTITY over ALL_SKILL_SLUGS / ALL_LANGUAGES / ALL_TOOLS,
9
+ * which are already canonical. The fold list is deliberately U+2019-only: no SRD
10
+ * tool name contains any other non-ASCII character. */
11
+ export declare const toProfSlug: (s: string) => string;
12
+ /** Apostrophe-safe title case: capitalizes only at start-of-string or after
13
+ * whitespace, never after `'` (so "smith's-tools" reads "Smith's Tools", not
14
+ * "Smith'S Tools"). Intentionally the same body as the plugin's humanizeSlug.
15
+ * The divergence that matters is one level up: prettyName is
16
+ * humanizeProficiency(toProfSlug(x)), and the U+2019 fold lives in toProfSlug.
17
+ * Do NOT "simplify" prettyName to a bare humanizer call · that reintroduces
18
+ * the duplicate-row defect (one proficiency rendering as two rows). */
19
+ export declare const humanizeProficiency: (s: string) => string;
20
+ /** The display label for one AUTHORED proficiency value that no vocabulary canonicalizes (R4-G7 T8 RIDER-14).
21
+ * The rule reads the value's SHAPE, never a word list:
22
+ * - a value with no whitespace is a slug ("thieves'-tools", "heavy"), and a phrase with no uppercase letter
23
+ * ("hand crossbows") carries no authored casing and shares its slug's `toProfSlug` key: both keep today's
24
+ * `humanizeProficiency(toProfSlug(raw))` (the fold and the apostrophe rule of the composition above);
25
+ * - a phrase that carries authored uppercase is PROSE ("Martial weapons that have the Light property",
26
+ * "... the 2H or H property") and prints as authored: U+2019 folded to ASCII, whitespace trimmed and collapsed,
27
+ * the first character capitalised, every other letter untouched. Title-casing it word by word lower-cased
28
+ * `2H` to `2h` and read "That Have The Light Property". */
29
+ export declare const proficiencyLabel: (raw: string) => string;
@@ -0,0 +1,12 @@
1
+ import {
2
+ humanizeProficiency,
3
+ normKey,
4
+ proficiencyLabel,
5
+ toProfSlug
6
+ } from "../chunk-RLGQ5PZZ.js";
7
+ export {
8
+ humanizeProficiency,
9
+ normKey,
10
+ proficiencyLabel,
11
+ toProfSlug
12
+ };
@@ -6,6 +6,19 @@ interface ProficienciesForQuery {
6
6
  weapons: ProficiencySet;
7
7
  tools: ProficiencySet;
8
8
  }
9
+ /** NOT the live weapon gate · that is `isWeaponSlugProficient` in
10
+ * `pc.equipment.ts`, reached from `computeAttacks`. The two are near-twins,
11
+ * NOT aliases: that function's docblock records where their matching diverges,
12
+ * so anything asserting runtime behaviour must call it, not this one. */
9
13
  export declare function isProficientWithWeapon(weapon: WeaponEntity, profs: ProficienciesForQuery): boolean;
14
+ /** No live armor gate exists at all · nothing in the product gates on armor
15
+ * proficiency today (`computeAppliedBonuses` takes `_profs` and ignores it),
16
+ * so this has no production caller either. Do NOT read that as "the
17
+ * `.specific` branch is dead": `normalizeArmorProf` in `pc.recalc.ts` routes
18
+ * an authored `armor: { specific: [...] }` block straight into it. No SRD
19
+ * entity uses that object form for armor yet, but `weapons:` and `tools:`
20
+ * already do in the same `proficiencies:` block, so it is one plausible
21
+ * keystroke away · and the function is published API besides. Trimming that
22
+ * branch could silently regress either. */
10
23
  export declare function isProficientWithArmor(armor: ArmorEntity, profs: ProficienciesForQuery): boolean;
11
24
  export {};
@@ -1,6 +1,10 @@
1
+ import {
2
+ normKey
3
+ } from "../chunk-RLGQ5PZZ.js";
4
+
1
5
  // src/pc/pc.proficiency-query.ts
2
6
  function isProficientWithWeapon(weapon, profs) {
3
- if (profs.weapons.specific.includes(weapon.slug)) return true;
7
+ if (profs.weapons.specific.some((e) => e === weapon.slug || normKey(e) === normKey(weapon.name))) return true;
4
8
  const baseCategory = weapon.category.split("-")[0];
5
9
  return profs.weapons.categories.includes(baseCategory);
6
10
  }
@@ -2,6 +2,8 @@ import type { Ability } from "@archivist-gg/dnd5e";
2
2
  import type { FeatEntity } from "@archivist-gg/dnd5e/feat/feat.types";
3
3
  import type { RaceEntity } from "@archivist-gg/dnd5e/race/race.types";
4
4
  import type { EntityRegistry } from "@archivist-gg/core";
5
+ import { type UnarmedStrikeSpec } from "./pc.equipment.js";
6
+ import { type FeatureEffectTotals } from "./pc.feature-effects.js";
5
7
  import type { ACTerm, DerivedStats, ProficiencySet, ResolvedCharacter, ResolvedClass, CharacterOverrides } from "./pc.types.js";
6
8
  /**
7
9
  * PHB "fixed average" value per hit die (per level beyond first). Called
@@ -28,18 +30,26 @@ export declare function flattenRaceAsi(race: RaceEntity | null): Partial<Record<
28
30
  * `abilityBonusBreakdown`'s `class` bucket so the two reads can never drift.
29
31
  *
30
32
  * Disjoint from chosen-feat ability-points (`choices[lvl]["feat:<id>"]`) and from
31
- * origin ability-points — no source double-counts.
33
+ * origin ability-points · no source double-counts.
34
+ *
35
+ * Since R4-P4 Decision B an `asi` that SHARES a level with a string `feat` key is
36
+ * discarded as an orphan rather than folded: that level took the feat branch, and
37
+ * the `asi` beside it is residue the picker never cleared on a branch switch.
38
+ * Folding both paid the same slot twice (Volker.md drew FOUR points from one L4).
32
39
  */
33
40
  export declare function collectClassAsiBranch(resolved: ResolvedCharacter): Partial<Record<Ability, number>>;
34
41
  /** Per-ability bonus provenance for the builder's obelisk captions:
35
42
  * species = fixed race ASI + subrace fixed ASI + race ability-points choices;
36
43
  * background = background ability-points choices;
37
- * class = legacy class ASI-BRANCH allocations (the L4 asi-or-feat → asi path);
44
+ * class = legacy class ASI-BRANCH allocations (the L4 asi-or-feat → asi path),
45
+ * MINUS any `asi` sharing a level with a string `feat` key, which
46
+ * collectClassAsiBranch discards as branch-switch residue (R4-P4 Dec. B),
47
+ * + the flat capstone fold when the caller threads the effect totals (R4-G3b §4);
38
48
  * feat = class chosen-feat ability-points (the L4 asi-or-feat → feat path) +
39
49
  * flat feat ability_bonuses (e.g. Athlete +1 STR). All already fold into
40
50
  * computeAbilityScores totals, so the caption must account for them or a
41
51
  * tile reads higher than the named sources explain (smoke r1/r5c). */
42
- export declare function abilityBonusBreakdown(resolved: ResolvedCharacter): Record<Ability, {
52
+ export declare function abilityBonusBreakdown(resolved: ResolvedCharacter, featureEffects?: FeatureEffectTotals): Record<Ability, {
43
53
  species: number;
44
54
  background: number;
45
55
  class: number;
@@ -60,21 +70,41 @@ export declare function hpLevelCount(classes: ResolvedClass[]): number;
60
70
  */
61
71
  export declare function multiclassMaxHP(classes: ResolvedClass[], conMod: number): number;
62
72
  /**
63
- * Default AC = 10 + DEX mod. If any class's features include an
64
- * "Unarmored Defense"–style feature with a structured flag, applies the
65
- * variant (Monk: +WIS; Barbarian: +CON). Falls back to feature-name
66
- * regex matching with a warning pushed onto `warnings`.
73
+ * The BEST APPLICABLE unarmoured formula (R4-G7 T6a, spec §4): the plain 10 + DEX mod, against every
74
+ * `unarmored-ac` effect that folds right now (its `base ?? 10` plus the modifier of every ability its
75
+ * OWN `abilities` list names), highest total winning. A class feature carrying an "Unarmored
76
+ * Defense"-style structured flag, or only the feature NAME (matched SILENTLY since R4-G6b §5.7), is the
77
+ * FALLBACK for a document that authors no effect at all (Monk: +WIS; Barbarian: +CON). `warnings` is
78
+ * retained for the exported signature and is no longer written to by this function or by
79
+ * `unarmoredACBreakdown`.
67
80
  */
68
81
  export declare function unarmoredAC(resolved: ResolvedCharacter, mods: Record<Ability, number>, warnings: string[]): number;
69
82
  /**
70
- * Same logic as `unarmoredAC` but also returns a structured breakdown of the
71
- * contributing terms (Base 10, DEX, optional class unarmored defense ability).
72
- * Used by recalc to assemble a richer acBreakdown when no armor is equipped.
83
+ * Same rule as `unarmoredAC` but also returns the WINNING formula's structured terms (its base, then one
84
+ * term per ability its `abilities` list names, in that list's order) and whether that formula admits a
85
+ * shield. Used by recalc to assemble a richer acBreakdown when no armor is equipped.
86
+ *
87
+ * `shieldBonus` is the equipped shield's contribution, read for the COMPARISON only: it is NOT part of
88
+ * the returned `total` or `terms`, because the shield is one of the additive terms recalc merges on the
89
+ * unarmoured path, and `shieldAllowed` is what tells that caller to keep it. A shield counts on a
90
+ * candidate only where its effect declares `allow_shield === true` (absent is NOT true: the SRD Monk
91
+ * omits the key and RAW allows the Monk no shield), and always on the plain 10 + DEX candidate, which is
92
+ * unarmoured-with-a-shield. Both fallback arms keep the shipped answer, shield included.
73
93
  */
74
- export declare function unarmoredACBreakdown(resolved: ResolvedCharacter, mods: Record<Ability, number>, warnings: string[]): {
94
+ export declare function unarmoredACBreakdown(resolved: ResolvedCharacter, mods: Record<Ability, number>, warnings: string[], shieldBonus?: number): {
75
95
  total: number;
76
96
  terms: ACTerm[];
97
+ shieldAllowed: boolean;
77
98
  };
99
+ /** R4-G6b §5.4: the die and ability set for the always-present Unarmed Strike row. Authored `unarmed-strike` effects
100
+ * first (a `{ column }` die is read from the GRANTING class's table at that class's level); then the SILENT synthetic:
101
+ * a class-sourced feature whose id slug `unarmedDieColumnFor` knows (`martial-arts`) with NO authored effect, on a
102
+ * class whose table carries that column with a dice value, contributes that die with DEX. The SHAPE is
103
+ * `decision-recognizer.ts`'s (a table-driven synthetic mirroring an overlay entry, retired when the data is
104
+ * authored: the G7 booking gives `overlay.schema.ts`'s class arm `effects` + `.strict()` and authors both Monks);
105
+ * the PLACEMENT is here because the builder ledger never feeds `derived.attacks`. Merge: the die with the highest
106
+ * average wins; the abilities are the union. Nothing writes onto a registry object. */
107
+ export declare function resolveUnarmedStrike(resolved: ResolvedCharacter): UnarmedStrikeSpec;
78
108
  /** Initiative = DEX mod + Alert (+5 in 2014). Extendable via feat flags. */
79
109
  export declare function initiativeBonus(dexMod: number, feats: FeatEntity[], edition: "2014" | "2024"): number;
80
110
  /** Speed = race.speed.walk + flat bonuses from feats (e.g., Mobile +10). */
@@ -83,14 +113,38 @@ export declare function speedFromRace(resolved: ResolvedCharacter): number;
83
113
  * Folds class-level CHOSEN-FEAT ability-points into per-ability totals.
84
114
  *
85
115
  * When the L4-style "asi or feat" decision resolves to a feat that carries
86
- * `ability-points` choices (e.g. Ability Score Improvement, the epic Boons),
87
- * the picker persists the allocation under the namespaced key
116
+ * `ability-points` choices (e.g. Ability Score Improvement), the picker
117
+ * persists the allocation under the namespaced key
88
118
  * `choices[lvl]["feat:" + choice.id]` (the engine's `buildItem` surfaces those
89
119
  * children; see pc.decision-engine.ts). This is disjoint from both the legacy
90
120
  * asi-BRANCH key (`choices[lvl].asi`) and origin ability-points, so no source
91
121
  * double-counts. The chosen feat entity is resolved from `resolved.feats`
92
122
  * (already looked up by the resolver) — no registry needed here.
93
123
  *
124
+ * The L19 Epic Boon folds through this SAME path, but only as of R4-P4's
125
+ * re-key (live in the SRD data from that phase's regeneration). This docblock
126
+ * earlier cited it as an existing example, which it was not. EVERY reader OF THE
127
+ * FEAT PICK keys on the LITERAL string `feat`, and there are THREE of them, not
128
+ * the two the R4-P4 spec §2.3 and task brief enumerate: this function
129
+ * (`block.feat`, below), `collectFeatPicks` (pc.resolver.ts) and
130
+ * `PCResolver.resolve`'s feat-to-spell application pass, the two
131
+ * `collectFeatGrantedSpells` calls (pc.resolver.ts), the one that
132
+ * makes a SPELL-granting boon work. Note the qualifier: this is NOT a claim that
133
+ * every reader of a saved class choice block keys on `feat`. Others read the very
134
+ * same block by OTHER keys · `collectClassAsiBranch` (above) reads the literal
135
+ * `.asi`, while `resolvePool` (pc.pools.ts) and `resolveChosenInline`
136
+ * (pc.resolver.ts) read by a DYNAMIC pool/choice id. As of R4-P4 Decision B
137
+ * `collectClassAsiBranch` also tests `block.feat`, but only to DETECT the feat
138
+ * branch, not to consume the pick. pc.decision-engine.ts is NOT a fourth:
139
+ * it reads generically by `choice.id`, which is exactly why re-keying works at
140
+ * all. srd-2024.yaml authored the boon's pick `id: epic-boon`, so an L19
141
+ * selection persisted under `choices[19]["epic-boon"]`, which no reader looks
142
+ * at: the boon resolved to no feat, granted no spells, and contributed no
143
+ * ability points at all. Re-keying the authored pick to `id: feat` is what
144
+ * makes it fold. No persisted GRANDCHILD key moved with it ·
145
+ * pc.decision-engine.ts builds `feat:<childId>` off a hardcoded prefix,
146
+ * independent of the parent choice's own id.
147
+ *
94
148
  * Clamps defensively (per-ability max_per, then stops at the points total in
95
149
  * ABILITY_KEYS order), mirroring collectChosenAbilityPoints.
96
150
  */
@@ -99,7 +153,8 @@ export declare function collectClassFeatAbilityPoints(resolved: ResolvedCharacte
99
153
  * Combines racial ASI (from race.ability_score_increases), feat ASI,
100
154
  * class-choice ASI (from classes[i].choices[lvl].asi), chosen-feat ASI
101
155
  * (from classes[i].choices[lvl]["feat:<id>"]), and user overrides.
102
- * Overrides win; ASI sources sum unconditionally.
156
+ * Overrides win; ASI sources sum unconditionally. The flat capstone fold is added
157
+ * in recalc's bonus loop, not here (R4-G3b §4).
103
158
  */
104
159
  export declare function computeAbilityScores(resolved: ResolvedCharacter, overrides: CharacterOverrides): Record<Ability, number>;
105
160
  export declare function computeProficiencies(resolved: ResolvedCharacter): {