@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
@@ -6,9 +6,9 @@ import {
6
6
  isShieldArmor,
7
7
  isWeaponEntity,
8
8
  resolveEntityForEntry
9
- } from "../chunk-N6N74K6X.js";
10
- import "../chunk-4LZJXJJS.js";
11
- import "../chunk-LV4SNWZQ.js";
9
+ } from "../chunk-UBBKSYBY.js";
10
+ import "../chunk-WXWPK3LI.js";
11
+ import "../chunk-6X4KUDMU.js";
12
12
  export {
13
13
  defaultSlotForType,
14
14
  effectiveArmor,
@@ -1,3 +1,4 @@
1
+ import type { EntityRegistry } from "@archivist-gg/core";
1
2
  import type { Ability } from "@archivist-gg/dnd5e";
2
3
  import type { CharacterOverrides, KnownSpellEntry, ResolvedClass } from "./pc.types.js";
3
4
  import type { CasterType } from "@archivist-gg/dnd5e/class/class.types";
@@ -5,17 +6,19 @@ export interface SpellcastingProfile {
5
6
  ability: Ability;
6
7
  casterType: CasterType;
7
8
  preparation: "known" | "prepared";
8
- spellList: string;
9
+ /** null when neither block names a list (the converter's EK/AT). Read by the known-spell attribution guard
10
+ * (`attributeUnclassedSpell`, guard (i)) and carried to `SpellcastingClassInfo.spellList`. */
11
+ spellList: string | null;
9
12
  table: Record<number, {
10
13
  columns?: Record<string, string | number>;
11
14
  }>;
12
15
  }
13
16
  /**
14
- * Data-driven spellcasting profile for a resolved class. Reads the subclass
15
- * block if the subclass grants casting (e.g. Architect of Ruin), else the class
16
- * block. Known/cantrip columns come from whichever entity grants casting
17
- * (subclass table preferred, falling back to the class table). Returns null for
18
- * non-casters. No hardcoded class knowledge — everything comes from the data.
17
+ * Data-driven spellcasting profile for a resolved class, composed FIELD BY FIELD (R4-G1a D7): the caster type comes
18
+ * from the subclass block, else the class block, and without one the entity is not a caster (null: a subclass that
19
+ * only names a save ability grants no Spells tab). The table is the SUBCLASS table when the subclass block supplies
20
+ * the caster type, else the class table, else {}. Preparation is explicit, else inferred from a Prepared Spells /
21
+ * Spells Prepared column in the resolved table, else known. No hardcoded class knowledge.
19
22
  */
20
23
  export declare function resolveSpellcasting(rc: ResolvedClass): SpellcastingProfile | null;
21
24
  /** The ability a class actually casts with: a per-class override wins, else the
@@ -30,6 +33,44 @@ export interface NormalizedKnownSpell {
30
33
  alwaysPrepared: boolean;
31
34
  }
32
35
  export declare function normalizeKnownSpell(entry: KnownSpellEntry): NormalizedKnownSpell;
36
+ /** One caster class as the attribution rule reads it: a class slug and its profile's `spellList` (null when none is named).
37
+ * The slug is compared through `baseClassName` and also looks up the registered `class` document for its compendium, so it
38
+ * must be a registered class slug: the resolver passes the class-ref slug its own lookup used, the add drawer the resolved
39
+ * entity's `slug` (equal to the registered slug on every bundle and converter class document, and backfilled from it when a
40
+ * custom body omits it). */
41
+ export interface AttributionCaster {
42
+ classSlug: string;
43
+ spellList: string | null;
44
+ }
45
+ /** Every base class name a spell of ONE compendium lists in its `classes`, the compendium read off the registered spell
46
+ * document (`RegisteredEntity.compendium`). ONE walk of the spell bucket per registry fills every compendium's set at once,
47
+ * so each compendium is computed once; the walk repeats only when `registry.count()` changes (an entity registered under a
48
+ * new slug, or one removed). The core registry exposes no change generation, version or event, so the count is the only
49
+ * key. Known gap: re-registering an existing spell in place (same slug, same count; the plugin's `updateEntity` does this)
50
+ * with a different `classes` list is not seen until the count next moves. A compendium with no spells gets the empty set. */
51
+ export declare function listedSpellClassNames(registry: EntityRegistry, compendium: string): ReadonlySet<string>;
52
+ /**
53
+ * R4-G7 T8 RIDER-19 (F-PACT) and its fix rounds 1 and 2 (review C-1): the caster class an UN-CLASSED known spell belongs to.
54
+ * The ONE rule, read by the resolver (an entry with no `class:`) and by the Spells tab's add drawer (the `class:` it writes).
55
+ * The spell stays with the FIRST caster class (class-entry order) unless the first caster's own list is OBSERVABLE and does
56
+ * not name the spell, in which case it goes to the first LATER caster whose base class name the spell's `classes` names.
57
+ * Observable means (i) the first caster's profile names a list (`spellList` non-null; the converter's Arcane Trickster and
58
+ * Eldritch Knight name none, and their spells say `wizard`, never `rogue` / `fighter`) AND (ii) its base class name is
59
+ * listed on at least one spell of the first class document's OWN compendium (`RegisteredEntity.compendium` of the registered
60
+ * `class` document and of each spell document): the SRD 5e bundle lists `paladin` on no spell (DATA-SRD) while a real vault
61
+ * also holds SRD 2024 and converted books that do, so only the class's own book can speak for its list. A class document the
62
+ * registry does not hold, or a compendium with no spells, is unobservable. So a single caster, a spell the first caster's list
63
+ * names (Detect Magic on a Paladin / Warlock), a spell no caster names, and every spell of an unobservable first caster
64
+ * resolve exactly as before RIDER-19; Armor of Agathys (`classes: [warlock]`) on a Player's Handbook Paladin-first Paladin /
65
+ * Warlock goes to the Warlock. Null only when there is no caster class.
66
+ * Consequences a move carries: the spell's DC / attack become the new class's, it follows that class's Pact Magic routing,
67
+ * and a plain entry (no prepared flag) moved from a `known` caster to a `prepared` one is not castable until prepared.
68
+ * Known limit: a list EXTENSION on an observable first caster (a 2014 Fiend Warlock's patron-granted Burning Hands, Bard
69
+ * Magical Secrets, Divine Soul) is not visible in the spell's base-list `classes`, so such a spell moves to a later caster
70
+ * that names it; an explicit `class:` is the override. The class lookup and the registry walk for (ii) run only when a move
71
+ * is in question.
72
+ */
73
+ export declare function attributeUnclassedSpell(spellClasses: readonly unknown[] | undefined, casters: readonly AttributionCaster[], registry: EntityRegistry): string | null;
33
74
  export interface CasterClassInput {
34
75
  casterType: CasterType;
35
76
  level: number;
@@ -43,6 +84,16 @@ export interface DerivedSpellSlots {
43
84
  total: number;
44
85
  } | null;
45
86
  }
87
+ type SlotCaster = Exclude<CasterType, "pact">;
88
+ /** The single-class slot table for a progression. Exhaustive by construction: an unhandled member is a COMPILE
89
+ * error at the `never` assignment (a bare switch statement would fall through silently). */
90
+ export declare function slotTableFor(caster: SlotCaster): number[][];
91
+ /**
92
+ * Standard spell slots plus Pact Magic for a character's casting classes. A SINGLE regular caster reads its own
93
+ * progression table through `slotTableFor` (the artificer row included: level-1 slots, then the half-caster rows).
94
+ * TWO OR MORE pool into a multiclass caster level, `full + floor(half/2) + floor(third/3) + ceil(artificer/2)` on
95
+ * the SUMS, read off FULL_CASTER_SLOTS. Warlock levels never join that pool: they produce a separate pact entry.
96
+ */
46
97
  export declare function deriveSpellSlots(classes: CasterClassInput[]): DerivedSpellSlots;
47
98
  export interface LimitClassInput {
48
99
  classSlug: string;
@@ -57,4 +108,22 @@ export interface SpellLimit {
57
108
  cantripsKnown: number | null;
58
109
  preparedOrKnown: number | null;
59
110
  }
111
+ /** The converter spells the 2024 column two ways; the bundle uses the first. Serves the PREPARED branch of
112
+ * `computeSpellLimits` and `resolveSpellcasting`'s preparation inference (`hasPreparedColumn`). The KNOWN branch
113
+ * keeps its own candidates (the XPHB Sorcerer and Warlock are known casters whose count sits under
114
+ * "Prepared Spells"). */
115
+ export declare const PREPARED_COLUMNS: string[];
116
+ /** The level term of the prepared-count FALLBACK (used only when no table column supplies the count). Slots round
117
+ * the artificer progression UP; its prepared count rounds DOWN (TCE: "half your artificer level, rounded down").
118
+ * `third` is a DELTA from the shipped `else` arm, which used the whole `level`: reachable in principle now that
119
+ * `resolveSpellcasting` infers `prepared` for the XPHB Arcane Trickster, whose table supplies the count, so no
120
+ * shipped number moves (spec D6c, §8). */
121
+ export declare function preparedLevelTerm(caster: CasterType, level: number): number;
122
+ /**
123
+ * Cantrip and prepared/known counts per casting class. The PREPARED count is TABLE-FIRST (R4-G1a D6c): a
124
+ * PREPARED_COLUMNS cell on the class table row wins outright, and only a row without one falls back to the formula
125
+ * `max(1, abilityMod + preparedLevelTerm(casterType, level))`. The KNOWN count is read from the table alone and is
126
+ * null when the row carries no candidate column.
127
+ */
60
128
  export declare function computeSpellLimits(inputs: LimitClassInput[]): SpellLimit[];
129
+ export {};
@@ -1,18 +1,28 @@
1
1
  import {
2
+ PREPARED_COLUMNS,
3
+ attributeUnclassedSpell,
2
4
  computeSpellLimits,
3
5
  deriveSpellSlots,
4
6
  effectiveSpellcastingAbility,
7
+ listedSpellClassNames,
5
8
  normalizeKnownSpell,
6
- resolveSpellcasting
7
- } from "../chunk-6IRYTM5V.js";
8
- import "../chunk-5XUXXAQJ.js";
9
+ preparedLevelTerm,
10
+ resolveSpellcasting,
11
+ slotTableFor
12
+ } from "../chunk-DPUJDOKV.js";
13
+ import "../chunk-IUAAXXNO.js";
9
14
  import "../chunk-VTTA2ALZ.js";
10
- import "../chunk-NAEPSZAM.js";
11
- import "../chunk-WP2CX2JO.js";
15
+ import "../chunk-PDJSUH24.js";
16
+ import "../chunk-WELIKLZF.js";
12
17
  export {
18
+ PREPARED_COLUMNS,
19
+ attributeUnclassedSpell,
13
20
  computeSpellLimits,
14
21
  deriveSpellSlots,
15
22
  effectiveSpellcastingAbility,
23
+ listedSpellClassNames,
16
24
  normalizeKnownSpell,
17
- resolveSpellcasting
25
+ preparedLevelTerm,
26
+ resolveSpellcasting,
27
+ slotTableFor
18
28
  };
@@ -10,3 +10,26 @@ export declare function readTableColumn(table: Record<number, {
10
10
  export declare function numericColumnsAt(table: Record<number, {
11
11
  columns?: Record<string, string | number>;
12
12
  }> | undefined, level: number): Record<string, number>;
13
+ /** Class-table COLUMN names per `select-entity` choice id (R4-G5 §6.2, invariant 3): the DATA route that makes a
14
+ * levelled pick count GROW. MEASURED 2026-09-07 on both corpora: the Fighter 2024's `weapon-mastery` choice
15
+ * authors `count: 3` at levels 1 / 4 / 10 / 16 while the class table's "Weapon Mastery" column reads
16
+ * 3 / 4 / 5 / 6 (byte-identical between the converter document and the SRD 2024 bundle record); the Barbarian
17
+ * 2024's column reads 2 / 3 / 4 against an authored 2; Paladin / Ranger / Rogue 2024 carry `count: 2` and NO
18
+ * column, which is the correct RAW count. Read only INSIDE dnd5e (`buildDecisionLedger`). G7 handoff: the
19
+ * converter emits `count.column` on the choice itself and this table retires. */
20
+ export declare const COUNT_COLUMNS: Readonly<Record<string, string[]>>;
21
+ /** The column keys for a choice id, by OWN-property lookup (`derivePoolLayout`'s precedent, invariant 3). The
22
+ * guard is LOAD-BEARING, not defensive: `readTableColumn` above iterates its `keys` with `for ... of`, so a
23
+ * plain index on a choice id of `constructor` hands it the Object constructor FUNCTION and THROWS while the
24
+ * ledger is being built. Returns undefined for every id the table does not own. */
25
+ export declare function countColumnsFor(choiceId: string): string[] | undefined;
26
+ /** A DICE cell (`1d6`) of a (sub)class table column at `level`, or null when absent, non-string or not dice-shaped.
27
+ * `readTableColumn` / `numericColumnsAt` parse INTEGERS and would read `1d6` as 1 (R4-G6b §5.3). Own-property
28
+ * reads only: a key of `constructor` hands back the prototype's function otherwise. */
29
+ export declare function diceColumnAt(table: Record<number, {
30
+ columns?: Record<string, string | number>;
31
+ }> | undefined, level: number, key: string): string | null;
32
+ /** The class-table COLUMN that scales an unarmed strike's die, per FEATURE id slug (R4-G6b §5.4's synthetic; the
33
+ * `COUNT_COLUMNS` shape). Mirrors the G7 overlay entry this synthetic retires for SRD data; kept for homebrew. */
34
+ export declare const UNARMED_DIE_COLUMNS: Readonly<Record<string, string>>;
35
+ export declare function unarmedDieColumnFor(featureSlug: string): string | undefined;
@@ -1,8 +1,18 @@
1
1
  import {
2
+ COUNT_COLUMNS,
3
+ UNARMED_DIE_COLUMNS,
4
+ countColumnsFor,
5
+ diceColumnAt,
2
6
  numericColumnsAt,
3
- readTableColumn
4
- } from "../chunk-5XUXXAQJ.js";
7
+ readTableColumn,
8
+ unarmedDieColumnFor
9
+ } from "../chunk-IUAAXXNO.js";
5
10
  export {
11
+ COUNT_COLUMNS,
12
+ UNARMED_DIE_COLUMNS,
13
+ countColumnsFor,
14
+ diceColumnAt,
6
15
  numericColumnsAt,
7
- readTableColumn
16
+ readTableColumn,
17
+ unarmedDieColumnFor
8
18
  };
@@ -2,15 +2,18 @@ import type { Ability } from "../types/choice.js";
2
2
  import type { ArmorEntity } from "../armor/armor.types.js";
3
3
  import type { WeaponEntity } from "../weapon/weapon.types.js";
4
4
  import type { ItemEntity } from "../item/item.types.js";
5
- import type { SkillSlug, Feature } from "@archivist-gg/dnd5e";
5
+ import type { SkillSlug, Feature, Choice } from "@archivist-gg/dnd5e";
6
6
  import type { ClassEntity } from "@archivist-gg/dnd5e/class/class.types";
7
+ import type { CasterType } from "@archivist-gg/dnd5e/schemas/caster-type-schema";
7
8
  import type { RaceEntity } from "@archivist-gg/dnd5e/race/race.types";
8
9
  import type { SubclassEntity } from "@archivist-gg/dnd5e/subclass/subclass.types";
9
10
  import type { BackgroundEntity } from "@archivist-gg/dnd5e/background/background.types";
10
11
  import type { FeatEntity } from "@archivist-gg/dnd5e/feat/feat.types";
11
12
  import type { Spell } from "@archivist-gg/dnd5e/spell/spell.types";
12
13
  import type { OptionalFeatureEntity } from "@archivist-gg/dnd5e/types/optional-feature.types";
14
+ import type { PoolLayout } from "../types/selection-pool.js";
13
15
  import type { ConditionSlug } from "./conditions.constants.js";
16
+ import type { ResourceIndex } from "./pc.resources.js";
14
17
  /** A persisted decision value: entity slug / inline value (string), multi-select
15
18
  * slugs (string[]), or an ability-points allocation. Stale/odd legacy values
16
19
  * survive parsing (schema is permissive); readers narrow defensively. */
@@ -103,6 +106,11 @@ export interface KnownSpellObject {
103
106
  }
104
107
  export type KnownSpellEntry = string | KnownSpellObject;
105
108
  export type PassiveKind = "perception" | "investigation" | "insight";
109
+ /** The manual proficiency tri, in ONE place. The runtime twin is `proficiencyTri`
110
+ * (pc.schema.ts); `pc.recalc.ts` imports this name for the skills tri, and the plugin's
111
+ * `CharacterEditState.setToolProficiency` and `ProficiencyEditModal` import it for the
112
+ * tools tri (R4-G4 §9.3, UR1), so no consumer re-spells the union. */
113
+ export type ProficiencyTri = "none" | "proficient" | "expertise";
106
114
  export interface CharacterOverrides {
107
115
  scores?: Partial<Record<Ability, number>>;
108
116
  saves?: Partial<Record<Ability, {
@@ -111,7 +119,7 @@ export interface CharacterOverrides {
111
119
  }>>;
112
120
  skills?: Partial<Record<SkillSlug, {
113
121
  bonus: number;
114
- proficiency?: "none" | "proficient" | "expertise";
122
+ proficiency?: ProficiencyTri;
115
123
  }>>;
116
124
  passives?: Partial<{
117
125
  perception: number;
@@ -133,6 +141,50 @@ export interface CharacterOverrides {
133
141
  spellcasting_ability?: Ability;
134
142
  spell_slots?: Record<number, number>;
135
143
  attunement_limit?: number;
144
+ /** Manual proficiency edits, mirroring `characterOverridesShape` (pc.schema.ts). `remove`
145
+ * SUPPRESSES a rules-granted entry ("a dwarf who doesn't know dwarvish"). Both keys and EVERY
146
+ * leaf are optional with no defaults, so an untouched note stays byte-identical: every reader
147
+ * uses `?? []` (`?? {}` for the tools tri).
148
+ *
149
+ * `tools.proficiency` is the per-tool manual tri (R4-G4 §9.3, UR1); `languages` deliberately
150
+ * gets none. This line is the ONLY thing keeping the interface honest about it, and what it
151
+ * buys is TYPE VISIBILITY, not survival: the `NoOverridesKeyDrift` pair below compares
152
+ * TOP-LEVEL keys only, so a sibling added to the schema and not here ROUND-TRIPS at runtime
153
+ * (the plugin's `characterToYaml` deep-clones and dumps the RUNTIME object, and `parsePC`
154
+ * runs `characterSchema.safeParse`, which keeps a key the schema declares) while staying
155
+ * UNTYPED, so no consumer can read or write it without a cast. That is the same direction the
156
+ * parity block below states; the STRIPPED case is the other one, a key here that the schema
157
+ * lacks. */
158
+ languages?: {
159
+ add?: string[];
160
+ remove?: string[];
161
+ };
162
+ tools?: {
163
+ add?: string[];
164
+ remove?: string[];
165
+ proficiency?: Record<string, ProficiencyTri>;
166
+ };
167
+ /** Manual defense edits, mirroring `characterOverridesShape` (pc.schema.ts). SUPPRESSION-ONLY:
168
+ * the absent `add` channel is a DELIBERATE asymmetry with `languages`/`tools` above, not an
169
+ * oversight · do not "complete the pattern". The additive store is `character.defenses.*`,
170
+ * which already exists; a second additive channel would recreate the two-writers divergence
171
+ * R4-P5 exists to remove. Every level is optional with no defaults, so an untouched note stays
172
+ * byte-identical. The store has NO consumer as of this commit · Task 5 writes the first, and
173
+ * should read every level as possibly absent (`?? []`) and match with `toDefenseSlug`. */
174
+ defenses?: {
175
+ resistances?: {
176
+ remove?: string[];
177
+ };
178
+ immunities?: {
179
+ remove?: string[];
180
+ };
181
+ vulnerabilities?: {
182
+ remove?: string[];
183
+ };
184
+ condition_immunities?: {
185
+ remove?: string[];
186
+ };
187
+ };
136
188
  }
137
189
  export interface CharacterState {
138
190
  hp: {
@@ -236,6 +288,17 @@ export interface Character {
236
288
  };
237
289
  state: CharacterState;
238
290
  }
291
+ /** R4-G7 T8 RIDER-23 (F-FEATSRC) · the slot that granted a feat: a class's `choices[<level>].feat` pick, or a 2024
292
+ * background's origin feat. Its two members ARE the `class` and `background` sources below, so a renderer formats `via`
293
+ * through the arms it already has ("Fighter 4", "Background: Soldier"). */
294
+ export type FeatVia = {
295
+ kind: "class";
296
+ slug: string;
297
+ level: number;
298
+ } | {
299
+ kind: "background";
300
+ slug: string;
301
+ };
239
302
  export type FeatureSource = {
240
303
  kind: "class";
241
304
  slug: string;
@@ -253,6 +316,7 @@ export type FeatureSource = {
253
316
  } | {
254
317
  kind: "feat";
255
318
  slug: string;
319
+ via?: FeatVia;
256
320
  };
257
321
  export interface ResolvedFeature {
258
322
  feature: Feature;
@@ -270,6 +334,33 @@ export interface ResolvedFeature {
270
334
  renderSuppressed?: boolean;
271
335
  /** The feat's entire substance is a full ability-score improvement (a pure ASI). The DISPLAY layer hides these. */
272
336
  buildOnly?: boolean;
337
+ /** R4-G7 §7.1: EVERY LOWER copy the resolve-time fold collapsed into this wrapper, ASCENDING by level, with
338
+ * that copy's own card identity (`name`, and `description` = its `description` else its `entries` joined) and
339
+ * its `choices` when it carries any. A wrapper-only field, the `chosenInline` precedent: the shared registry
340
+ * `Feature` is never touched.
341
+ *
342
+ * The invariant every walker relies on: the TOP copy's own name, prose and choices stay on `feature` and are
343
+ * NEVER repeated here, so visiting `foldedFrom` (each entry at its own `level`) and then `feature` (at
344
+ * `source.level`) visits every folded copy EXACTLY ONCE, in ascending level order · the order the un-folded
345
+ * list had. That is what keeps "a level-N decision on a repeated feature stays a decision at level N" true,
346
+ * and what keeps the builder's per-level strip complete, while the sheet renders ONE row.
347
+ *
348
+ * Both readers live in `pc.decision-engine.ts`: `visitProficiencyChoices` (through
349
+ * `collectChosenProficiencies`) walks the entries that carry `choices` and nothing else, and
350
+ * `buildDecisionLedger` runs each entry at its own level through the SAME branch it runs the wrapper through
351
+ * (`emitCopy`): its authored choices when it has them, else `recognizeDecision`'s synthesized decision under
352
+ * the suppression filter, else the informational card, else nothing. Per-copy `name` and `description` are
353
+ * load-bearing, not decoration: they decide what the recognizer sees and what the row prints. MEASURED over
354
+ * both corpora, 63 of the 131 repeated-id families have a copy whose description differs from the top copy's
355
+ * and 1 has a differing name (an UPPER BOUND: it counts `ability-score-improvement` families, which are
356
+ * `buildOnly` and never fold; the non-ASI carriers are `artificer-specialist-feature`, `path-feature`,
357
+ * `bard-college-feature`, `expertise` and their siblings). Absent on an unfolded wrapper. */
358
+ foldedFrom?: {
359
+ level: number;
360
+ name: string;
361
+ description?: string;
362
+ choices?: Choice[];
363
+ }[];
273
364
  }
274
365
  export interface ResolvedSpell {
275
366
  entity: Spell;
@@ -286,6 +377,9 @@ export interface ResolvedSpell {
286
377
  /** Index of the originating equipment entry when this spell was granted by a
287
378
  * per-instance item (e.g. a Spell Scroll). Absent for class/feat/race spells. */
288
379
  entryIndex?: number;
380
+ /** True ONLY for a row from character.spells.known (removable / toggleable);
381
+ * absent on every grant · R4-G3b §5.2.7. */
382
+ persisted?: boolean;
289
383
  }
290
384
  export interface ResolvedClass {
291
385
  entity: ClassEntity | null;
@@ -296,6 +390,15 @@ export interface ResolvedClass {
296
390
  export interface ResolvedPoolEntry {
297
391
  slug: string;
298
392
  entity: OptionalFeatureEntity;
393
+ /** The compendium NAME the REGISTERED entity carries (R4-G5 §9.2.1), stamped at resolve time because `entity`
394
+ * is `e.data`, which carries none: MEASURED 2026-09-07 over the WHOLE converter output (226 documents under
395
+ * its `OptionalFeatures` folders) and the 7 bundle ones, 0 of 226 and 0 of 7 declare a `compendium` key
396
+ * INSIDE the fenced entity body, while 226 of 226 and 7 of 7 declare one in the note's FRONTMATTER, which
397
+ * core keeps on `RegisteredEntity`.
398
+ * The plugin's ONE visibility predicate reads it on BOTH surfaces (§3.2.2). Absent when the registry entry
399
+ * carries none, which in practice means a test double. The COLLAPSE does not read it: its identity is the
400
+ * slug PREFIX (§9.2.2). */
401
+ compendium?: string;
299
402
  }
300
403
  /** A materialized selection pool: count from the table column, prereq-filtered
301
404
  * candidates, player picks (from the choices ledger), and subclass auto-grants. */
@@ -311,6 +414,16 @@ export interface ResolvedPool {
311
414
  selected: ResolvedPoolEntry[];
312
415
  available: ResolvedPoolEntry[];
313
416
  grants: ResolvedPoolEntry[];
417
+ /** Derived at resolve time from the members' `rendering_hint` (pool-layout.ts),
418
+ * unconditionally: an authored `TabDecl.renders.layout` does not suppress the
419
+ * derivation, it OUTRANKS this field at the tab (§4.2.5). R4-G4 T5 LANDED that precedence in the
420
+ * plugin: `TabsContainer`'s dynamic-pool-tab loop is the ONE reader of this field (measured
421
+ * 2026-09-05 with `grep -rn "pool\.layout" packages/obsidian/src`, one line) and reads
422
+ * authored-then-derived-then-default before handing the string to the `PoolTab` constructor. */
423
+ layout?: PoolLayout;
424
+ /** The OWNED resource id the members spend (majority of `consumes.resource` among the ids the
425
+ * character owns); undefined when no member consumes an owned id (R4-G4 §4.2.3, Gate 0 B1). */
426
+ resource?: string;
314
427
  }
315
428
  export interface ResolvedCharacter {
316
429
  definition: Character;
@@ -318,10 +431,41 @@ export interface ResolvedCharacter {
318
431
  classes: ResolvedClass[];
319
432
  background: BackgroundEntity | null;
320
433
  feats: FeatEntity[];
434
+ /** The bare/full slug of the background's resolved origin feat (R4-G3b §8): stamped by the resolver OUTSIDE the
435
+ * feats de-dup guard, so it is set even when the same feat is ALSO a class-slot pick. Read by
436
+ * visitProficiencyChoices' origin arm; `resolved.feats` itself carries no provenance. */
437
+ originFeatSlug?: string;
321
438
  totalLevel: number;
322
439
  features: ResolvedFeature[];
323
440
  spells: ResolvedSpell[];
324
441
  pools: ResolvedPool[];
442
+ /** Every `resources[]` entry of every resolved feature, keyed by id, with the DECLARING feature
443
+ * stamped as owner, PLUS every pool pick's own `uses` with the POOL stamped as owner
444
+ * (R4-G4 §3.2.1 and §12). REQUIRED: the resolver's own literal sets it like `pools` and
445
+ * reassigns it after pools resolve. Cast test fixtures omit it. Its readers outside the
446
+ * resolver landed with R4-G4 T3, T4, T5 and T7b. Re-measured 2026-09-06 on the SHIPPED plugin
447
+ * tree: `grep -rn "resolved\.resources" packages/obsidian/src` returns TEN lines, of
448
+ * which SIX are read EXPRESSIONS and four are comment prose. The six expressions sit in six
449
+ * functions across five files: the module-private `resourceLevel`
450
+ * helper of `components/actions/feature-rows.ts` (T3, reading `?.owner`), `renderSpendControl`
451
+ * (`components/actions/spend-control.ts`, reading the entry for its `name` and `die`),
452
+ * `consumeCost` (`components/pool-tab.ts`, reading `?.name` for the Cost label), both T4,
453
+ * `renderPoolHead` (`components/pool-tab.ts`, T5, reading the entry for the tab head's `name`,
454
+ * `die`, `reset` and owner: §4.2.6), and T7b's two: `renderPickTracker`
455
+ * (`components/actions/pick-tracker.ts`, reading the entry for a pick's `reset` and `name`) and
456
+ * `seedFeatureUses` (`pc.resource-seed.ts`), which is the ONE expression that is not a
457
+ * `?.get(...)`: it walks `?.values()` for the entries whose `owner.kind` is `pool`, because a
458
+ * pick's `uses` reaches the seed through no other route. §3.2.1 names its consumers: T3's
459
+ * `resourceLevel(id, ctx)`
460
+ * die helper (§6.2.4), T4's spend control and Cost label (§3.2.2 / §3.2.4), T5's pool-tab
461
+ * head and T7b's tracker and seed (§12.2), all LANDED. T7's restore affordance has LANDED too and
462
+ * reads NOTHING here: its rest arm
463
+ * walks the index `computeRestPlan` re-derives and its card arm routes on `resolveRecovery(rec)`
464
+ * over the RAW `recovery[0]`.
465
+ * Every one of the six reaches it through `?.`, because the plugin's card / row / rest fixtures
466
+ * cast a `ResolvedCharacter` with no index. `computeRestPlan` never reads it at all, it re-derives
467
+ * the index. */
468
+ resources: ResourceIndex;
325
469
  /** 2024 Weapon Mastery: bare-normalized slugs of the weapons the character has
326
470
  * chosen mastery of, unioned across every class/level `weapon-mastery` pick
327
471
  * (e.g. "srd-2024_greatsword" → "greatsword"). Empty when the character has no
@@ -337,6 +481,10 @@ export interface ACTerm {
337
481
  source: string;
338
482
  amount: number;
339
483
  kind: "armor" | "shield" | "item" | "unarmored" | "override" | "dex" | "ability" | "feature";
484
+ /** Situational qualifier carried verbatim from the granting `ac-bonus` effect's `condition`
485
+ * (R4-G3a §3.2.5), e.g. "While wearing heavy armor". Absent = unconditional. Rendered as the
486
+ * AC-tooltip row's `title`; NEVER evaluated. */
487
+ condition?: string;
340
488
  }
341
489
  /** Structured max-HP math for the Max HP modal. Spec: P5 2026-07-21. */
342
490
  export interface HPBreakdown {
@@ -362,12 +510,19 @@ export interface HPBreakdown {
362
510
  /** An additive on-hit damage rider (dice or flat) applied to an attack, from a
363
511
  * feature (global — all weapon attacks) or a magic item (that weapon only).
364
512
  * `amount` is a dice/flat string ("2d6", "1d8", "2"); `damage_type` optional
365
- * (a bare manual override string carries its type inline); `source` is a human
366
- * label for the expand/tooltip. Rendered as its own damage chip on the row. */
513
+ * (a bare manual override string carries its type inline, and the schema's
514
+ * `chosen` sentinel is kept, never replaced by the row's type, RIDER-13); `source`
515
+ * is a human label for the expand/tooltip. `condition` is the source's own
516
+ * qualifier ("for a 1st-level spell slot"), CARRIED verbatim and never evaluated,
517
+ * from a feature effect (R4-G7 T8 RIDER-12) and from an item's `damage_riders`
518
+ * entry alike; absent when the source authors none. How the sheet prints a rider
519
+ * (a damage chip, or the row's caption) is the plugin renderer's policy, stated
520
+ * once at `weapons-table.ts` `renderWeaponRow`. */
367
521
  export interface DamageRider {
368
522
  amount: string;
369
523
  damage_type?: string;
370
524
  source?: string;
525
+ condition?: string;
371
526
  }
372
527
  export interface AttackRow {
373
528
  id: string;
@@ -400,6 +555,9 @@ export interface AttackRow {
400
555
  /** Action-economy cost for the Cost badge. Defaults to "action" at
401
556
  * render time when absent. Settable via entry.overrides.action. */
402
557
  actionCost?: "action" | "bonus-action" | "reaction" | "free" | "special";
558
+ /** The engine's always-present unarmed row (R4-G6b §5): never an equipment entry, never `equipped`, no `slotKey`.
559
+ * The weapons table gates its expand on this flag BEFORE the equipment lookup. Absent on every weapon row. */
560
+ unarmed?: true;
403
561
  /** When set, the weapon is being wielded one-handed but has a versatile
404
562
  * two-handed option. The Actions tab renders both lines stacked in the
405
563
  * damage cell instead of emitting a second row. */
@@ -476,12 +634,84 @@ export interface DerivedEquipment {
476
634
  * `condition` absent = always-on (not situational).
477
635
  */
478
636
  export interface RollModifierEntry {
479
- mode: "advantage" | "disadvantage";
637
+ mode: "advantage" | "disadvantage" | "reroll" | "add-d4";
480
638
  roll: "ability-check" | "saving-throw" | "attack";
481
639
  scope?: string;
482
640
  condition?: string;
483
641
  label: string;
484
642
  }
643
+ /**
644
+ * The two members' aliases, extracted from the entry's inline unions (R4-G3a §5.2). They are
645
+ * declared HERE and nowhere else: `pc/roll-tag-labels.ts`'s `Record<RollModifierMode, string>`
646
+ * tables and `pc/roll-scope.ts` import them, so widening the entry above is a COMPILE error in
647
+ * every label table that has not been extended (§14 row 8i).
648
+ */
649
+ export type RollModifierMode = RollModifierEntry["mode"];
650
+ export type RollKind = RollModifierEntry["roll"];
651
+ /** The closed `save-outcome` result vocabulary, extracted the same way (R4-G3a §5.3): how much
652
+ * of the effect still lands. `pc/roll-tag-labels.ts` types its OUTCOME glyph table
653
+ * `Record<SaveOutcome, string>`, so the union is spelled ONCE and a widened member is a compile
654
+ * error in the table rather than a silent missing glyph. */
655
+ export type SaveOutcome = SaveOutcomeEntry["on_success"];
656
+ /**
657
+ * A `save-outcome` effect as it leaves the fold (R4-G3a §5.3) · Evasion and its shape-mates
658
+ * ("on a failed save you take half damage, on a success none").
659
+ *
660
+ * `ability` ABSENT means every save, which is the save chip's existing "absent scope = every
661
+ * chip" idiom: the effect schema's `ability: "any"` maps to `undefined` at the fold rather than
662
+ * duplicating the entry six times. `appliesTo` is the effect's `applies_to` prose ("a spell"),
663
+ * carried for the tooltip only; `condition` is the situational qualifier, never evaluated.
664
+ */
665
+ export interface SaveOutcomeEntry {
666
+ ability?: Ability;
667
+ on_success: "none" | "half" | "full";
668
+ on_failure: SaveOutcomeEntry["on_success"];
669
+ appliesTo?: string;
670
+ condition?: string;
671
+ label: string;
672
+ }
673
+ /** Where a defense came from. Ordered weakest to strongest for the precedence rule in recalc. */
674
+ export type DefenseOrigin = "manual" | "equipment" | "grant";
675
+ /**
676
+ * One defense a feature effect grants, as it leaves the fold (R4-G3a §3.2.1).
677
+ *
678
+ * The four defense totals on FeatureEffectTotals are lists of these. `value` is the AUTHORED
679
+ * spelling, trimmed and never slugified · `toDefenseSlug` runs later, in pc.recalc.ts, which is
680
+ * why the fold's dedupe is a case-insensitive comparison rather than a slug lookup. `sources`
681
+ * holds the granting FEATURE NAMES, merged per value at the fold (two features granting the same
682
+ * immunity yield ONE grant with TWO sources · dropping the second would lose it before
683
+ * composeDefenseEntries ever saw it). `condition` is the first qualifier seen for that value,
684
+ * carried verbatim and NEVER evaluated.
685
+ */
686
+ export interface DefenseGrant {
687
+ value: string;
688
+ sources: string[];
689
+ condition?: string;
690
+ }
691
+ /**
692
+ * One entry in a derived defense bucket.
693
+ *
694
+ * `value` is ALWAYS `toDefenseSlug(raw)` · it is what every comparison keys on.
695
+ * `label` is the first-spelling-wins display string, which is what preserves an authored "Psychic".
696
+ * `origin` is the STRONGEST contributing source, not the first list to supply it.
697
+ *
698
+ * `sources` is GRANT-ORIGIN attribution only (R4-G3a §3.2.3): the names of the features whose
699
+ * effects granted this value, copied from the DefenseGrant. EQUIPMENT-origin attribution (the
700
+ * `pc.equipment.ts` half of R4-P5 §10.1 · archivist-obsidian/docs/superpowers/specs/
701
+ * 2026-08-05-r4-p5-defenses-design.md, the spec lives in the PLUGIN repo, not this one) stays
702
+ * DEFERRED, so an equipment-only entry carries no `sources` at all and the plugin's chip host
703
+ * must guard on emptiness rather than on origin alone.
704
+ *
705
+ * `condition` is the situational qualifier from the granting effect, carried verbatim for the
706
+ * chip tooltip and NEVER evaluated. Both fields are optional: a manual entry has neither.
707
+ */
708
+ export interface DefenseEntry {
709
+ value: string;
710
+ label: string;
711
+ origin: DefenseOrigin;
712
+ condition?: string;
713
+ sources?: string[];
714
+ }
485
715
  export interface DerivedStats {
486
716
  totalLevel: number;
487
717
  proficiencyBonus: number;
@@ -547,10 +777,10 @@ export interface DerivedStats {
547
777
  spellLimits: SpellLimitInfo[];
548
778
  warnings: string[];
549
779
  defenses: {
550
- resistances: string[];
551
- immunities: string[];
552
- vulnerabilities: string[];
553
- condition_immunities: string[];
780
+ resistances: DefenseEntry[];
781
+ immunities: DefenseEntry[];
782
+ vulnerabilities: DefenseEntry[];
783
+ condition_immunities: DefenseEntry[];
554
784
  };
555
785
  acBreakdown: ACTerm[];
556
786
  hpBreakdown: HPBreakdown;
@@ -573,10 +803,15 @@ export interface DerivedStats {
573
803
  attunementLimit: number;
574
804
  conditionEffects: ConditionEffects;
575
805
  /**
576
- * Structured advantage/disadvantage entries from `roll-modifier` effects.
577
- * Order-preserving pass-through (no dedupe/merge); rendered as ADV/DIS chips.
806
+ * Structured `roll-modifier` entries: four `mode` members since R4-G3a §6, not only advantage/disadvantage.
807
+ * Order-preserving pass-through (no dedupe/merge); rendered as ADV/DIS/RR/+D4 chips.
578
808
  */
579
809
  rollModifiers: RollModifierEntry[];
810
+ /**
811
+ * `save-outcome` entries (R4-G3a §5.3). Order-preserving pass-through; rendered as an outcome
812
+ * tag ("0/½") on the matching save chip, or on every save chip when `ability` is absent.
813
+ */
814
+ saveOutcomes: SaveOutcomeEntry[];
580
815
  }
581
816
  export interface SpellcastingClassInfo {
582
817
  classSlug: string;
@@ -585,8 +820,12 @@ export interface SpellcastingClassInfo {
585
820
  defaultAbility: Ability;
586
821
  saveDC: number;
587
822
  attackBonus: number;
588
- casterType: "full" | "half" | "third" | "pact";
823
+ /** The shared caster-type vocabulary; see schemas/caster-type-schema.ts (never spell the members out again). */
824
+ casterType: CasterType;
589
825
  preparation: "known" | "prepared";
826
+ /** The profile's `spellList` (null when neither block names one), so the add drawer can call the same
827
+ * `attributeUnclassedSpell` the resolver does. recalc always sets it; optional so hand-built rows read as unobservable. */
828
+ spellList?: string | null;
590
829
  }
591
830
  export interface SpellLimitInfo {
592
831
  classSlug: string;