@archivist-gg/dnd5e 0.3.1 → 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 (259) 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 +26 -0
  10. package/dist/background/background.schema.js +5 -4
  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-6IRYTM5V.js → chunk-DPUJDOKV.js} +119 -28
  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-BIK4DFWK.js → chunk-HJ5EEZ47.js} +1 -1
  39. package/dist/{chunk-6NK2MUD3.js → chunk-HZA7OE7N.js} +9 -2
  40. package/dist/{chunk-TN6ZLTNU.js → chunk-I2NDWFDD.js} +51 -4
  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-SE7AZDR3.js → chunk-JRDPUDT5.js} +29 -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-5ZBE4CWV.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-D3CLOBE3.js → chunk-OSQFPGLM.js} +44 -8
  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 +2033 -6907
  122. package/dist/item/item.codec.js +4 -3
  123. package/dist/item/item.enrichment.js +4 -4
  124. package/dist/item/item.parser.js +3 -2
  125. package/dist/item/item.schema.d.ts +69 -3
  126. package/dist/item/item.schema.js +2 -1
  127. package/dist/item/item.types.d.ts +69 -3
  128. package/dist/monster/monster.codec.d.ts +7 -6
  129. package/dist/monster/monster.enrichment.js +4 -4
  130. package/dist/monster/monster.format.d.ts +135 -0
  131. package/dist/monster/monster.format.js +78 -0
  132. package/dist/monster/monster.parser.d.ts +6 -1
  133. package/dist/monster/monster.parser.js +10 -1
  134. package/dist/monster/monster.schema.d.ts +369 -0
  135. package/dist/monster/monster.schema.js +34 -0
  136. package/dist/monster/monster.types.d.ts +200 -14
  137. package/dist/optional-feature/optional-feature.codec.js +6 -5
  138. package/dist/optional-feature/optional-feature.parser.js +5 -4
  139. package/dist/optional-feature/optional-feature.schema.d.ts +176 -0
  140. package/dist/optional-feature/optional-feature.schema.js +4 -3
  141. package/dist/pack.d.ts +1 -1
  142. package/dist/pc/conditions.constants.d.ts +0 -1
  143. package/dist/pc/conditions.constants.js +1 -3
  144. package/dist/pc/decision-recognizer.js +1 -1
  145. package/dist/pc/pc.additional-spells.d.ts +47 -0
  146. package/dist/pc/pc.additional-spells.js +23 -0
  147. package/dist/pc/pc.asi-flatten.d.ts +18 -0
  148. package/dist/pc/pc.decision-engine.d.ts +105 -17
  149. package/dist/pc/pc.decision-engine.js +28 -12
  150. package/dist/pc/pc.defense-normalize.d.ts +11 -0
  151. package/dist/pc/pc.defense-normalize.js +6 -0
  152. package/dist/pc/pc.equipment.d.ts +30 -1
  153. package/dist/pc/pc.equipment.js +12 -8
  154. package/dist/pc/pc.feature-effects.d.ts +225 -11
  155. package/dist/pc/pc.feature-effects.js +21 -5
  156. package/dist/pc/pc.parser.js +2 -2
  157. package/dist/pc/pc.pools.d.ts +11 -3
  158. package/dist/pc/pc.pools.js +9 -13
  159. package/dist/pc/pc.proficiencies.d.ts +54 -29
  160. package/dist/pc/pc.proficiencies.js +74 -95
  161. package/dist/pc/pc.proficiency-grants.d.ts +58 -0
  162. package/dist/pc/pc.proficiency-normalize.d.ts +24 -0
  163. package/dist/pc/pc.proficiency-normalize.js +12 -0
  164. package/dist/pc/pc.proficiency-query.d.ts +13 -0
  165. package/dist/pc/pc.proficiency-query.js +1 -1
  166. package/dist/pc/pc.recalc.d.ts +69 -14
  167. package/dist/pc/pc.recalc.js +261 -122
  168. package/dist/pc/pc.resolver.d.ts +34 -8
  169. package/dist/pc/pc.resolver.js +27 -12
  170. package/dist/pc/pc.resource-seed.d.ts +3 -2
  171. package/dist/pc/pc.resource-seed.js +19 -17
  172. package/dist/pc/pc.resources.d.ts +87 -0
  173. package/dist/pc/pc.resources.js +21 -0
  174. package/dist/pc/pc.rest.d.ts +10 -1
  175. package/dist/pc/pc.rest.js +66 -12
  176. package/dist/pc/pc.schema.d.ts +139 -3
  177. package/dist/pc/pc.schema.js +4 -2
  178. package/dist/pc/pc.slotting.js +3 -3
  179. package/dist/pc/pc.spellcasting.d.ts +75 -6
  180. package/dist/pc/pc.spellcasting.js +16 -6
  181. package/dist/pc/pc.table-column.d.ts +23 -0
  182. package/dist/pc/pc.table-column.js +13 -3
  183. package/dist/pc/pc.types.d.ts +251 -12
  184. package/dist/pc/pool-layout.d.ts +47 -0
  185. package/dist/pc/pool-layout.js +16 -0
  186. package/dist/pc/roll-scope.d.ts +2 -0
  187. package/dist/pc/roll-scope.js +8 -0
  188. package/dist/pc/roll-tag-labels.d.ts +40 -0
  189. package/dist/pc/roll-tag-labels.js +17 -0
  190. package/dist/pc/spell-source.d.ts +21 -0
  191. package/dist/pc/spell-source.js +13 -0
  192. package/dist/race/race.codec.js +9 -7
  193. package/dist/race/race.parser.js +8 -6
  194. package/dist/race/race.schema.d.ts +51 -1
  195. package/dist/race/race.schema.js +8 -5
  196. package/dist/race/race.structural.d.ts +7 -0
  197. package/dist/race/race.structural.js +6 -0
  198. package/dist/race/race.types.d.ts +20 -0
  199. package/dist/schemas/attack-schema.d.ts +5 -0
  200. package/dist/schemas/caster-type-schema.d.ts +14 -0
  201. package/dist/schemas/caster-type-schema.js +6 -0
  202. package/dist/schemas/choice-schema.js +2 -2
  203. package/dist/schemas/entity-extras-schema.d.ts +53 -0
  204. package/dist/schemas/entity-extras-schema.js +10 -0
  205. package/dist/schemas/equipment-grant-schema.d.ts +2 -0
  206. package/dist/schemas/equipment-grant-schema.js +1 -1
  207. package/dist/schemas/feature-alias.d.ts +34 -0
  208. package/dist/schemas/feature-effect-schema.d.ts +124 -0
  209. package/dist/schemas/feature-effect-schema.js +1 -1
  210. package/dist/schemas/feature-schema.d.ts +8 -0
  211. package/dist/schemas/feature-schema.js +6 -4
  212. package/dist/schemas/resource-schema.d.ts +43 -0
  213. package/dist/schemas/resource-schema.js +1 -1
  214. package/dist/schemas/selection-pool-schema.d.ts +2 -0
  215. package/dist/schemas/selection-pool-schema.js +1 -1
  216. package/dist/spell/spell.access.js +23 -3
  217. package/dist/spell/spell.codec.js +4 -3
  218. package/dist/spell/spell.enrichment.js +4 -4
  219. package/dist/spell/spell.parser.d.ts +30 -0
  220. package/dist/spell/spell.parser.js +9 -2
  221. package/dist/spell/spell.scaling.d.ts +27 -6
  222. package/dist/spell/spell.scaling.js +25 -6
  223. package/dist/spell/spell.schema.d.ts +44 -2
  224. package/dist/spell/spell.schema.js +2 -1
  225. package/dist/spell/spell.types.d.ts +14 -0
  226. package/dist/subclass/subclass.codec.d.ts +1 -1
  227. package/dist/subclass/subclass.codec.js +11 -8
  228. package/dist/subclass/subclass.parser.js +10 -7
  229. package/dist/subclass/subclass.schema.d.ts +54 -7
  230. package/dist/subclass/subclass.schema.js +8 -6
  231. package/dist/subclass/subclass.types.d.ts +20 -2
  232. package/dist/types/choice.d.ts +53 -2
  233. package/dist/types/choice.js +13 -1
  234. package/dist/types/equipment-grant.d.ts +8 -1
  235. package/dist/types/feature-effect.d.ts +78 -23
  236. package/dist/types/feature.d.ts +70 -1
  237. package/dist/types/optional-feature.types.d.ts +28 -0
  238. package/dist/types/resource.d.ts +21 -1
  239. package/dist/types/selection-pool.d.ts +17 -2
  240. package/dist/weapon/weapon.classify.js +2 -2
  241. package/dist/weapon/weapon.codec.js +4 -3
  242. package/dist/weapon/weapon.parser.js +3 -2
  243. package/dist/weapon/weapon.schema.d.ts +4 -0
  244. package/dist/weapon/weapon.schema.js +2 -1
  245. package/dist/weapon/weapon.types.d.ts +8 -0
  246. package/package.json +2 -2
  247. package/dist/chunk-5XUXXAQJ.js +0 -30
  248. package/dist/chunk-7P65Q426.js +0 -158
  249. package/dist/chunk-7TEK5KK7.js +0 -49
  250. package/dist/chunk-AIRFC5NQ.js +0 -143
  251. package/dist/chunk-CZPMTQOY.js +0 -68
  252. package/dist/chunk-N2JE7BWL.js +0 -81
  253. package/dist/chunk-NXR3BD2V.js +0 -90
  254. package/dist/chunk-O5EYMKFH.js +0 -6
  255. package/dist/chunk-Q5K6LWBZ.js +0 -45
  256. package/dist/chunk-TNUFPIXI.js +0 -1110
  257. package/dist/chunk-TZK36BU7.js +0 -38
  258. package/dist/chunk-W7HWMMBF.js +0 -46
  259. package/dist/chunk-Y4I74YEG.js +0 -33
@@ -1,28 +1,32 @@
1
1
  import {
2
2
  readTableColumn
3
- } from "./chunk-5XUXXAQJ.js";
3
+ } from "./chunk-IUAAXXNO.js";
4
4
  import {
5
- bareSlug
5
+ bareSlug,
6
+ baseClassName
6
7
  } from "./chunk-VTTA2ALZ.js";
7
8
  import {
8
9
  abilityModifier
9
- } from "./chunk-NAEPSZAM.js";
10
+ } from "./chunk-PDJSUH24.js";
10
11
 
11
12
  // src/pc/pc.spellcasting.ts
13
+ function hasPreparedColumn(table) {
14
+ return Object.values(table).some((row) => PREPARED_COLUMNS.some((k) => {
15
+ var _a;
16
+ return ((_a = row == null ? void 0 : row.columns) == null ? void 0 : _a[k]) !== void 0;
17
+ }));
18
+ }
12
19
  function resolveSpellcasting(rc) {
13
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
20
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
14
21
  const sub = (_b = (_a = rc.subclass) == null ? void 0 : _a.spellcasting) != null ? _b : null;
15
22
  const cls = (_d = (_c = rc.entity) == null ? void 0 : _c.spellcasting) != null ? _d : null;
16
- const sc = sub != null ? sub : cls;
17
- if (!sc) return null;
18
- const table = (_i = (_h = sub ? (_e = rc.subclass) == null ? void 0 : _e.table : (_f = rc.entity) == null ? void 0 : _f.table) != null ? _h : (_g = rc.entity) == null ? void 0 : _g.table) != null ? _i : {};
19
- return {
20
- ability: sc.ability,
21
- casterType: sc.caster_type,
22
- preparation: sc.preparation,
23
- spellList: sc.spell_list,
24
- table
25
- };
23
+ const casterType = (_f = (_e = sub == null ? void 0 : sub.caster_type) != null ? _e : cls == null ? void 0 : cls.caster_type) != null ? _f : null;
24
+ const ability = (_h = (_g = sub == null ? void 0 : sub.ability) != null ? _g : cls == null ? void 0 : cls.ability) != null ? _h : null;
25
+ if (!casterType || !ability) return null;
26
+ const table = (_l = (_k = (sub == null ? void 0 : sub.caster_type) ? (_i = rc.subclass) == null ? void 0 : _i.table : void 0) != null ? _k : (_j = rc.entity) == null ? void 0 : _j.table) != null ? _l : {};
27
+ const preparation = (_n = (_m = sub == null ? void 0 : sub.preparation) != null ? _m : cls == null ? void 0 : cls.preparation) != null ? _n : hasPreparedColumn(table) ? "prepared" : "known";
28
+ const spellList = (_p = (_o = sub == null ? void 0 : sub.spell_list) != null ? _o : cls == null ? void 0 : cls.spell_list) != null ? _p : null;
29
+ return { ability, casterType, preparation, spellList, table };
26
30
  }
27
31
  function effectiveSpellcastingAbility(classSlug, dataAbility, overrides) {
28
32
  var _a, _b;
@@ -41,6 +45,43 @@ function normalizeKnownSpell(entry) {
41
45
  alwaysPrepared: (_b = entry.always_prepared) != null ? _b : false
42
46
  };
43
47
  }
48
+ function firstListedCasterClass(spellClasses, casterClassSlugs) {
49
+ var _a;
50
+ if (!Array.isArray(spellClasses) || spellClasses.length === 0) return null;
51
+ const listed = new Set(spellClasses.map((c) => baseClassName(String(c))));
52
+ return (_a = casterClassSlugs.find((slug) => listed.has(baseClassName(slug)))) != null ? _a : null;
53
+ }
54
+ var LISTED_CLASS_NAMES = /* @__PURE__ */ new WeakMap();
55
+ var NO_NAMES = /* @__PURE__ */ new Set();
56
+ function listedSpellClassNames(registry, compendium) {
57
+ var _a;
58
+ const count = registry.count();
59
+ let cached = LISTED_CLASS_NAMES.get(registry);
60
+ if (!cached || cached.count !== count) {
61
+ const byCompendium = /* @__PURE__ */ new Map();
62
+ for (const e of registry.search("", "spell", Number.POSITIVE_INFINITY)) {
63
+ const classes = e.data.classes;
64
+ if (!Array.isArray(classes)) continue;
65
+ let names = byCompendium.get(e.compendium);
66
+ if (!names) byCompendium.set(e.compendium, names = /* @__PURE__ */ new Set());
67
+ for (const c of classes) names.add(baseClassName(String(c)));
68
+ }
69
+ cached = { count, byCompendium };
70
+ LISTED_CLASS_NAMES.set(registry, cached);
71
+ }
72
+ return (_a = cached.byCompendium.get(compendium)) != null ? _a : NO_NAMES;
73
+ }
74
+ function attributeUnclassedSpell(spellClasses, casters, registry) {
75
+ var _a;
76
+ const first = casters[0];
77
+ if (!first) return null;
78
+ const listed = firstListedCasterClass(spellClasses, casters.map((c) => c.classSlug));
79
+ if (listed == null || listed === first.classSlug) return first.classSlug;
80
+ if (first.spellList == null) return first.classSlug;
81
+ const compendium = (_a = registry.getByTypeAndSlug("class", first.classSlug)) == null ? void 0 : _a.compendium;
82
+ if (compendium == null || !listedSpellClassNames(registry, compendium).has(baseClassName(first.classSlug))) return first.classSlug;
83
+ return listed;
84
+ }
44
85
  var FULL_CASTER_SLOTS = [
45
86
  /* 1 */
46
87
  [2, 0, 0, 0, 0, 0, 0, 0, 0],
@@ -209,6 +250,24 @@ var PACT_MAGIC = [
209
250
  /* 20 */
210
251
  { level: 5, total: 4 }
211
252
  ];
253
+ var ARTIFICER_CASTER_SLOTS = HALF_CASTER_SLOTS.map((row) => [...row]);
254
+ ARTIFICER_CASTER_SLOTS[0] = [2, 0, 0, 0, 0];
255
+ function slotTableFor(caster) {
256
+ switch (caster) {
257
+ case "full":
258
+ return FULL_CASTER_SLOTS;
259
+ case "half":
260
+ return HALF_CASTER_SLOTS;
261
+ case "third":
262
+ return THIRD_CASTER_SLOTS;
263
+ case "artificer":
264
+ return ARTIFICER_CASTER_SLOTS;
265
+ default: {
266
+ const _n = caster;
267
+ throw new Error(`unknown caster type ${String(_n)}`);
268
+ }
269
+ }
270
+ }
212
271
  function rowToRecord(row) {
213
272
  const out = {};
214
273
  row.forEach((n, i) => {
@@ -217,7 +276,7 @@ function rowToRecord(row) {
217
276
  return out;
218
277
  }
219
278
  function deriveSpellSlots(classes) {
220
- let fullLevels = 0, halfLevels = 0, thirdLevels = 0, warlockLevel = 0;
279
+ let fullLevels = 0, halfLevels = 0, thirdLevels = 0, artificerLevels = 0, warlockLevel = 0;
221
280
  const regular = [];
222
281
  for (const c of classes) {
223
282
  if (c.casterType === "pact") {
@@ -225,28 +284,55 @@ function deriveSpellSlots(classes) {
225
284
  continue;
226
285
  }
227
286
  regular.push({ caster: c.casterType, level: c.level });
228
- if (c.casterType === "full") fullLevels += c.level;
229
- else if (c.casterType === "half") halfLevels += c.level;
230
- else if (c.casterType === "third") thirdLevels += c.level;
287
+ switch (c.casterType) {
288
+ case "full":
289
+ fullLevels += c.level;
290
+ break;
291
+ case "half":
292
+ halfLevels += c.level;
293
+ break;
294
+ case "third":
295
+ thirdLevels += c.level;
296
+ break;
297
+ case "artificer":
298
+ artificerLevels += c.level;
299
+ break;
300
+ default: {
301
+ const _n = c.casterType;
302
+ throw new Error(`unknown caster type ${String(_n)}`);
303
+ }
304
+ }
231
305
  }
232
306
  let standard = {};
233
307
  if (regular.length === 1) {
234
308
  const only = regular[0];
309
+ const table = slotTableFor(only.caster);
235
310
  const idx = only.level - 1;
236
- if (only.caster === "half") {
237
- if (idx >= 0 && idx < HALF_CASTER_SLOTS.length) standard = rowToRecord(HALF_CASTER_SLOTS[idx]);
238
- } else if (only.caster === "third") {
239
- if (idx >= 0 && idx < THIRD_CASTER_SLOTS.length) standard = rowToRecord(THIRD_CASTER_SLOTS[idx]);
240
- } else {
241
- if (only.level >= 1 && only.level <= FULL_CASTER_SLOTS.length) standard = rowToRecord(FULL_CASTER_SLOTS[only.level - 1]);
242
- }
311
+ if (idx >= 0 && idx < table.length) standard = rowToRecord(table[idx]);
243
312
  } else if (regular.length >= 2) {
244
- const cl = fullLevels + Math.floor(halfLevels / 2) + Math.floor(thirdLevels / 3);
313
+ const cl = fullLevels + Math.floor(halfLevels / 2) + Math.floor(thirdLevels / 3) + Math.ceil(artificerLevels / 2);
245
314
  if (cl >= 1 && cl <= FULL_CASTER_SLOTS.length) standard = rowToRecord(FULL_CASTER_SLOTS[cl - 1]);
246
315
  }
247
316
  const pact = warlockLevel >= 1 && warlockLevel <= PACT_MAGIC.length ? PACT_MAGIC[warlockLevel - 1] : null;
248
317
  return { standard, pact };
249
318
  }
319
+ var PREPARED_COLUMNS = ["Prepared Spells", "Spells Prepared"];
320
+ function preparedLevelTerm(caster, level) {
321
+ switch (caster) {
322
+ case "full":
323
+ case "pact":
324
+ return level;
325
+ case "half":
326
+ case "artificer":
327
+ return Math.floor(level / 2);
328
+ case "third":
329
+ return Math.floor(level / 3);
330
+ default: {
331
+ const _n = caster;
332
+ throw new Error(`unknown caster type ${String(_n)}`);
333
+ }
334
+ }
335
+ }
250
336
  function computeSpellLimits(inputs) {
251
337
  const out = [];
252
338
  for (const i of inputs) {
@@ -255,8 +341,8 @@ function computeSpellLimits(inputs) {
255
341
  const mod = abilityModifier(i.abilityScore);
256
342
  let preparedOrKnown;
257
343
  if (p.preparation === "prepared") {
258
- const levelTerm = p.casterType === "half" ? Math.floor(i.level / 2) : i.level;
259
- preparedOrKnown = Math.max(1, mod + levelTerm);
344
+ const fromTable = readTableColumn(p.table, i.level, PREPARED_COLUMNS);
345
+ preparedOrKnown = fromTable !== null ? fromTable : Math.max(1, mod + preparedLevelTerm(p.casterType, i.level));
260
346
  } else {
261
347
  preparedOrKnown = readTableColumn(p.table, i.level, ["Spells Known", "Prepared Spells"]);
262
348
  }
@@ -269,6 +355,11 @@ export {
269
355
  resolveSpellcasting,
270
356
  effectiveSpellcastingAbility,
271
357
  normalizeKnownSpell,
358
+ listedSpellClassNames,
359
+ attributeUnclassedSpell,
360
+ slotTableFor,
272
361
  deriveSpellSlots,
362
+ PREPARED_COLUMNS,
363
+ preparedLevelTerm,
273
364
  computeSpellLimits
274
365
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseFeat
3
- } from "./chunk-XTIQWSFM.js";
3
+ } from "./chunk-D5U7GMPX.js";
4
4
 
5
5
  // src/feat/feat.codec.ts
6
6
  import yaml from "js-yaml";
@@ -0,0 +1,195 @@
1
+ // src/dnd/resource-formula.ts
2
+ var IDENTS = /* @__PURE__ */ new Set([
3
+ "level",
4
+ "class_level",
5
+ "prof",
6
+ "str_mod",
7
+ "dex_mod",
8
+ "con_mod",
9
+ "int_mod",
10
+ "wis_mod",
11
+ "cha_mod"
12
+ ]);
13
+ var AT_WILL_MAX = 999;
14
+ var FUNCS = /* @__PURE__ */ new Set(["ceil", "floor", "max", "min"]);
15
+ function tokenize(input) {
16
+ input = input.trim();
17
+ const tokens = [];
18
+ const re = /\s*('[^']*'|\{[a-z_]+\}|[a-z_]+|\d+|[+\-*/(),])/y;
19
+ let i = 0;
20
+ while (i < input.length) {
21
+ re.lastIndex = i;
22
+ const m = re.exec(input);
23
+ if (!m || m.index !== i) return null;
24
+ const raw = m[1];
25
+ i = re.lastIndex;
26
+ if (/^\d+$/.test(raw)) tokens.push({ kind: "num", value: parseInt(raw, 10) });
27
+ else if (raw.startsWith("'")) tokens.push({ kind: "str", value: raw.slice(1, -1) });
28
+ else if (raw === "(") tokens.push({ kind: "lparen" });
29
+ else if (raw === ")") tokens.push({ kind: "rparen" });
30
+ else if (raw === ",") tokens.push({ kind: "comma" });
31
+ else if (raw === "+" || raw === "-" || raw === "*" || raw === "/") tokens.push({ kind: "op", op: raw });
32
+ else {
33
+ const braced = raw.startsWith("{");
34
+ const name = braced ? raw.slice(1, -1) : raw;
35
+ if (!braced && FUNCS.has(name)) tokens.push({ kind: "fn", name });
36
+ else if (!braced && name === "column") tokens.push({ kind: "col" });
37
+ else if (IDENTS.has(name)) tokens.push({ kind: "id", name });
38
+ else return null;
39
+ }
40
+ }
41
+ return tokens.length > 0 ? tokens : null;
42
+ }
43
+ function evalTokens(tokens, b) {
44
+ let pos = 0;
45
+ const peek = () => tokens[pos];
46
+ const factor = () => {
47
+ var _a, _b, _c, _d, _e, _f, _g, _h;
48
+ const t = peek();
49
+ if (!t) return null;
50
+ if (t.kind === "num") {
51
+ pos++;
52
+ return t.value;
53
+ }
54
+ if (t.kind === "id") {
55
+ const val = b[t.name];
56
+ if (val === void 0) return null;
57
+ pos++;
58
+ return val;
59
+ }
60
+ if (t.kind === "fn") {
61
+ pos++;
62
+ if (((_a = peek()) == null ? void 0 : _a.kind) !== "lparen") return null;
63
+ pos++;
64
+ const args = [];
65
+ const first = expr();
66
+ if (first === null) return null;
67
+ args.push(first);
68
+ while (((_b = peek()) == null ? void 0 : _b.kind) === "comma") {
69
+ pos++;
70
+ const next = expr();
71
+ if (next === null) return null;
72
+ args.push(next);
73
+ }
74
+ if (((_c = peek()) == null ? void 0 : _c.kind) !== "rparen") return null;
75
+ pos++;
76
+ if (t.name === "ceil" || t.name === "floor") {
77
+ if (args.length !== 1) return null;
78
+ return t.name === "ceil" ? Math.ceil(args[0]) : Math.floor(args[0]);
79
+ }
80
+ if (args.length < 2) return null;
81
+ return t.name === "max" ? Math.max(...args) : Math.min(...args);
82
+ }
83
+ if (t.kind === "col") {
84
+ pos++;
85
+ if (((_d = peek()) == null ? void 0 : _d.kind) !== "lparen") return null;
86
+ pos++;
87
+ const s = peek();
88
+ if (!s || s.kind !== "str") return null;
89
+ pos++;
90
+ if (((_e = peek()) == null ? void 0 : _e.kind) !== "rparen") return null;
91
+ pos++;
92
+ return (_g = (_f = b.columns) == null ? void 0 : _f[s.value]) != null ? _g : 0;
93
+ }
94
+ if (t.kind === "lparen") {
95
+ pos++;
96
+ const inner = expr();
97
+ if (inner === null) return null;
98
+ if (((_h = peek()) == null ? void 0 : _h.kind) !== "rparen") return null;
99
+ pos++;
100
+ return inner;
101
+ }
102
+ return null;
103
+ };
104
+ const term = () => {
105
+ var _a;
106
+ let v = factor();
107
+ if (v === null) return null;
108
+ while (((_a = peek()) == null ? void 0 : _a.kind) === "op" && (peek().op === "*" || peek().op === "/")) {
109
+ const op = peek().op;
110
+ pos++;
111
+ const r = factor();
112
+ if (r === null) return null;
113
+ v = op === "*" ? v * r : v / r;
114
+ }
115
+ return v;
116
+ };
117
+ const expr = () => {
118
+ var _a;
119
+ let v = term();
120
+ if (v === null) return null;
121
+ while (((_a = peek()) == null ? void 0 : _a.kind) === "op" && (peek().op === "+" || peek().op === "-")) {
122
+ const op = peek().op;
123
+ pos++;
124
+ const r = term();
125
+ if (r === null) return null;
126
+ v = op === "+" ? v + r : v - r;
127
+ }
128
+ return v;
129
+ };
130
+ const result = expr();
131
+ if (result === null || pos !== tokens.length) return null;
132
+ return result;
133
+ }
134
+ var ZERO = {
135
+ level: 1,
136
+ class_level: 1,
137
+ prof: 2,
138
+ str_mod: 0,
139
+ dex_mod: 0,
140
+ con_mod: 0,
141
+ int_mod: 0,
142
+ wis_mod: 0,
143
+ cha_mod: 0,
144
+ columns: {}
145
+ };
146
+ function isValidMaxFormula(formula) {
147
+ const toks = tokenize(formula);
148
+ if (!toks) return false;
149
+ return evalTokens(toks, ZERO) !== null;
150
+ }
151
+ function evaluateMaxFormula(formula, bindings) {
152
+ const toks = tokenize(formula);
153
+ if (!toks) throw new Error(`invalid max_formula: ${JSON.stringify(formula)}`);
154
+ const v = evalTokens(toks, bindings);
155
+ if (v === null) throw new Error(`invalid max_formula: ${JSON.stringify(formula)}`);
156
+ return v;
157
+ }
158
+ function resolveMaxAt(totalLevel, resource) {
159
+ var _a;
160
+ let chosen = resource.max_formula;
161
+ let best = 0;
162
+ for (const step of (_a = resource.scales_at) != null ? _a : []) {
163
+ if (step.level <= totalLevel && step.level > best) {
164
+ best = step.level;
165
+ chosen = step.max;
166
+ }
167
+ }
168
+ return chosen;
169
+ }
170
+ function resolveMaxCountAt(level, resource, bindings) {
171
+ var _a;
172
+ const tryEval = (s) => {
173
+ const toks = tokenize(s);
174
+ return toks ? evalTokens(toks, bindings) : null;
175
+ };
176
+ let best = 0;
177
+ let chosen = null;
178
+ for (const step of (_a = resource.scales_at) != null ? _a : []) {
179
+ if (step.level > level || step.level <= best) continue;
180
+ const v = tryEval(step.max);
181
+ if (v === null) continue;
182
+ best = step.level;
183
+ chosen = v;
184
+ }
185
+ if (chosen !== null) return chosen;
186
+ return tryEval(resource.max_formula);
187
+ }
188
+
189
+ export {
190
+ AT_WILL_MAX,
191
+ isValidMaxFormula,
192
+ evaluateMaxFormula,
193
+ resolveMaxAt,
194
+ resolveMaxCountAt
195
+ };
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  featureSchema
3
- } from "./chunk-CZPMTQOY.js";
3
+ } from "./chunk-4FWNK4FD.js";
4
4
  import {
5
5
  choiceSchema
6
- } from "./chunk-NXSWH3AE.js";
6
+ } from "./chunk-T26ZVCL5.js";
7
+ import {
8
+ additionalSpellsEntrySchema,
9
+ imageField
10
+ } from "./chunk-UA5KS2PL.js";
7
11
 
8
12
  // src/race/race.schema.ts
9
13
  import { z } from "zod";
@@ -55,9 +59,31 @@ var raceEntitySchema = z.object({
55
59
  variant_label: z.string().min(1),
56
60
  traits: z.array(featureSchema),
57
61
  choices: z.array(choiceSchema).optional(),
58
- subspecies_of: z.string().regex(wikilinkRegex).optional()
62
+ subspecies_of: z.string().regex(wikilinkRegex).optional(),
63
+ rendering_hint: z.string().optional(),
64
+ has_fluff: z.boolean().optional(),
65
+ has_fluff_images: z.boolean().optional(),
66
+ creature_type: z.array(z.string().min(1)).optional(),
67
+ // guarded non-empty at the emitter (finding 24)
68
+ creature_type_tags: z.array(z.string().min(1)).optional(),
69
+ additional_spells: z.union([
70
+ z.array(additionalSpellsEntrySchema),
71
+ // converter form, 109 docs
72
+ additionalSpellsEntrySchema
73
+ // SHIPPED-BUNDLE bare-object form, 3 docs (§2.8)
74
+ ]).optional(),
75
+ // The four RAW 5etools PASSTHROUGHS (race-mapper.ts D13 block copies them VERBATIM, no shape
76
+ // guard — finding 24): today's variant set is a snapshot, not a contract (lineage is already
77
+ // string|null|boolean), and NO G row ever reads them. Maximally loose by ruling; the guard
78
+ // test §9.5 pins today's variant classes since the census goes blind under z.unknown().
79
+ trait_tags: z.unknown().optional(),
80
+ lineage: z.unknown().optional(),
81
+ height_and_weight: z.unknown().optional(),
82
+ sound_clip: z.unknown().optional(),
83
+ image: imageField
59
84
  });
60
85
 
61
86
  export {
87
+ fixedAsiSchema,
62
88
  raceEntitySchema
63
89
  };
@@ -1,3 +1,7 @@
1
+ import {
2
+ imageField
3
+ } from "./chunk-UA5KS2PL.js";
4
+
1
5
  // src/item/item.schema.ts
2
6
  import { z } from "zod";
3
7
  var tier1Conditions = [
@@ -41,6 +45,7 @@ var conditionalBonusSchema = z.object({
41
45
  when: z.array(conditionSchema)
42
46
  });
43
47
  var numberOrConditional = z.union([z.number().int(), conditionalBonusSchema]);
48
+ var abilityEnum = z.enum(["str", "dex", "con", "int", "wis", "cha"]);
44
49
  var bonusesSchema = z.object({
45
50
  ac: numberOrConditional.optional(),
46
51
  weapon_attack: numberOrConditional.optional(),
@@ -48,6 +53,12 @@ var bonusesSchema = z.object({
48
53
  spell_attack: numberOrConditional.optional(),
49
54
  spell_save_dc: numberOrConditional.optional(),
50
55
  saving_throws: numberOrConditional.optional(),
56
+ // Converter-measured (spec §4): 24 / 3 / 2 / 1 carriers. Same numberOrConditional
57
+ // shape as the six above, so a conditional arm is legal on these too.
58
+ spell_damage: numberOrConditional.optional(),
59
+ ability_check: numberOrConditional.optional(),
60
+ proficiency_bonus: numberOrConditional.optional(),
61
+ saving_throw_concentration: numberOrConditional.optional(),
51
62
  ability_scores: z.object({
52
63
  // partial: each ability is independently optional. z.record(enum, …) in
53
64
  // Zod v4 treats every enum key as required, which doesn't match the
@@ -67,7 +78,16 @@ var bonusesSchema = z.object({
67
78
  int: numberOrConditional.optional(),
68
79
  wis: numberOrConditional.optional(),
69
80
  cha: numberOrConditional.optional()
70
- }).optional()
81
+ }).optional(),
82
+ // "choose `count` of `from`, each +`amount`" (6 measured leaves). `amount` is
83
+ // OPTIONAL: Lost Laboratory of Kwalish/Deck of Several Things carries
84
+ // {from: [all six], count: 1} with NO amount, and a required `amount` would
85
+ // refuse that document outright.
86
+ choose: z.array(z.strictObject({
87
+ from: z.array(abilityEnum),
88
+ count: z.number().int().positive(),
89
+ amount: z.number().int().optional()
90
+ })).optional()
71
91
  }).optional(),
72
92
  speed: z.object({
73
93
  walk: numberOrConditional.optional(),
@@ -78,6 +98,9 @@ var bonusesSchema = z.object({
78
98
  });
79
99
  var chargesSchema = z.object({
80
100
  max: z.number().int(),
101
+ // Dice regained per recharge, e.g. "1d4 - 1" (68 carriers). Distinct from
102
+ // `recharge_amount`, which the canonical pipeline also emits as a string.
103
+ dice: z.string().optional(),
81
104
  recharge: z.string().optional(),
82
105
  recharge_amount: z.string().optional(),
83
106
  destroy_on_empty: z.object({
@@ -96,16 +119,37 @@ var attachedSpellsSchema = z.object({
96
119
  // Source canonical pipeline emits this for Necklace of Fireballs, Helm of
97
120
  // Brilliance, Candle of Invocation, Efreeti Bottle. Key is total available
98
121
  // uses (e.g. "9": ["fireball"] for the necklace's 9 beads).
99
- limited: z.record(z.string(), z.array(z.string())).optional()
122
+ limited: z.record(z.string(), z.array(z.string())).optional(),
123
+ // Converter-measured (spec §4): spells the item lists without a cost model (18),
124
+ // the casting ability for the item's attached spells (10), and ritual-only
125
+ // spells (1).
126
+ other: z.array(z.string()).optional(),
127
+ ability: z.string().optional(),
128
+ ritual: z.array(z.string()).optional()
100
129
  });
101
130
  var attunementTagSchema = z.union([
102
- z.object({ class: z.string(), subclass: z.string().optional() }),
131
+ z.object({
132
+ class: z.string(),
133
+ subclass: z.string().optional(),
134
+ alignment: z.union([z.string(), z.array(z.string())]).optional()
135
+ }),
103
136
  // alignment can be a single string OR an array of alignment-letter codes
104
137
  // (e.g. ["G"] for good, ["L", "G"] for lawful good).
105
- z.object({ alignment: z.union([z.string(), z.array(z.string())]) }),
106
- z.object({ race: z.string() }),
107
- z.object({ creature_type: z.string() }),
108
- z.object({ spellcasting: z.boolean() })
138
+ z.object({
139
+ alignment: z.union([z.string(), z.array(z.string())]),
140
+ race: z.string().optional()
141
+ }),
142
+ z.object({
143
+ race: z.string(),
144
+ alignment: z.union([z.string(), z.array(z.string())]).optional()
145
+ }),
146
+ z.object({ creature_type: z.string(), size: z.string().optional() }),
147
+ z.object({ spellcasting: z.boolean() }),
148
+ z.object({ background: z.string() }),
149
+ z.object({ psionics: z.boolean() }),
150
+ z.object({ int: z.number().int() }),
151
+ z.object({ skill_proficiency: z.array(z.string()) }),
152
+ z.object({ language_proficiency: z.array(z.string()) })
109
153
  ]);
110
154
  var attunementCanonicalSchema = z.object({
111
155
  required: z.boolean(),
@@ -122,15 +166,27 @@ var grantsSchema = z.object({
122
166
  blindsight: z.number().optional()
123
167
  }).optional()
124
168
  });
169
+ var containerCapacitySchema = z.strictObject({
170
+ weight: z.array(z.number()).optional(),
171
+ item: z.array(z.record(z.string(), z.number())).optional(),
172
+ weightless: z.boolean().optional(),
173
+ volume: z.array(z.number()).optional()
174
+ });
125
175
  var containerSchema = z.object({
126
176
  capacity_weight: z.number().optional(),
127
177
  weightless: z.boolean().optional(),
128
- pack_contents: z.array(z.string()).optional()
178
+ pack_contents: z.array(z.string()).optional(),
179
+ capacity: containerCapacitySchema.optional()
129
180
  });
130
181
  var lightSchema = z.object({
131
182
  bright_radius: z.number(),
132
183
  dim_radius: z.number()
133
184
  });
185
+ var lightEntrySchema = z.strictObject({
186
+ bright: z.number().optional(),
187
+ dim: z.number().optional(),
188
+ shape: z.string().optional()
189
+ });
134
190
  var itemEntitySchema = z.object({
135
191
  name: z.string().min(1),
136
192
  slug: z.string().optional(),
@@ -147,10 +203,12 @@ var itemEntitySchema = z.object({
147
203
  attunement: z.union([attunementCanonicalSchema, z.boolean(), z.string()]).optional(),
148
204
  grants: grantsSchema.optional(),
149
205
  container: containerSchema.optional(),
150
- light: lightSchema.optional(),
206
+ light: z.union([lightSchema, z.array(lightEntrySchema).nonempty()]).optional(),
151
207
  cursed: z.boolean().optional(),
152
208
  sentient: z.boolean().optional(),
153
- focus: z.union([z.boolean(), z.string()]).optional(),
209
+ // 54 docs carry a LIST of classes that may use the item as a focus
210
+ // (e.g. ["Druid", "Ranger"]); 26 carry a single string, 2 a boolean.
211
+ focus: z.union([z.boolean(), z.string(), z.array(z.string())]).optional(),
154
212
  tier: z.enum(["major", "minor"]).optional(),
155
213
  damage: z.union([
156
214
  z.object({
@@ -182,6 +240,24 @@ var itemEntitySchema = z.object({
182
240
  scroll_level: z.number().optional(),
183
241
  unidentified: z.boolean().optional(),
184
242
  masked_category: z.string().optional(),
243
+ // Converter-modelled top-level keys (spec §4). Declared here so they survive
244
+ // parse AND listed in the parser's KNOWN_KEYS so they are not additionally
245
+ // copied into `raw` — the schema and the raw-bag are independent gates.
246
+ // `modify_speed` (124 carriers): equal 101 (string-valued, e.g. {fly: "walk"}) ·
247
+ // static 20 · multiply 3 · bonus 1 (keyed "*").
248
+ modify_speed: z.strictObject({
249
+ equal: z.record(z.string(), z.string()).optional(),
250
+ static: z.record(z.string(), z.number()).optional(),
251
+ multiply: z.record(z.string(), z.number()).optional(),
252
+ bonus: z.record(z.string(), z.number()).optional()
253
+ }).optional(),
254
+ // Carried by all 6,046 converter item docs, ALWAYS as the empty string today:
255
+ // `.min(1)` is forbidden here.
256
+ rendering_hint: z.string().optional(),
257
+ has_fluff: z.boolean().optional(),
258
+ has_fluff_images: z.boolean().optional(),
259
+ // Already `.optional()` at its source — never append a second one.
260
+ image: imageField,
185
261
  raw: z.record(z.string(), z.unknown()).optional(),
186
262
  // Legacy fields preserved
187
263
  damage_dice: z.string().optional(),