@archivist-gg/dnd5e 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/background/background.codec.js +4 -4
  2. package/dist/background/background.parser.js +3 -3
  3. package/dist/background/background.schema.js +2 -2
  4. package/dist/{chunk-FFULAF2P.js → chunk-2BWAID3Y.js} +2 -2
  5. package/dist/{chunk-FUK7QSN5.js → chunk-42Y5IJKT.js} +1 -1
  6. package/dist/{chunk-537BHVSH.js → chunk-4JHOHWBI.js} +1 -1
  7. package/dist/{chunk-ZLG6FQFB.js → chunk-5WQD3WNZ.js} +16 -1
  8. package/dist/{chunk-FEOB24KP.js → chunk-73HOFQQ5.js} +1 -1
  9. package/dist/{chunk-4SSDYJS2.js → chunk-7VJ6BOA5.js} +5 -1
  10. package/dist/{chunk-DABBCYTT.js → chunk-CZPMTQOY.js} +1 -1
  11. package/dist/{chunk-DRNZJNJP.js → chunk-DZXSWSBP.js} +1 -1
  12. package/dist/{chunk-RVYYKYFL.js → chunk-GE4TLPWM.js} +1 -1
  13. package/dist/{chunk-4C2KOKLF.js → chunk-K6Q5OUGK.js} +12 -0
  14. package/dist/{chunk-PMADYMUI.js → chunk-L7E2UT5A.js} +1 -1
  15. package/dist/{chunk-CDW7R2QT.js → chunk-NXSWH3AE.js} +4 -1
  16. package/dist/{chunk-IDTDACM6.js → chunk-PHQRGDRQ.js} +1 -1
  17. package/dist/{chunk-BAXWWGFJ.js → chunk-Q5K6LWBZ.js} +1 -1
  18. package/dist/chunk-R5SQHP2X.js +1092 -0
  19. package/dist/{chunk-Z6GXV2GO.js → chunk-TLYDBT5F.js} +1 -1
  20. package/dist/{chunk-QQ5W2AZP.js → chunk-TQZB4OCJ.js} +67 -12
  21. package/dist/{chunk-A7T5L7SS.js → chunk-TUIFSVLI.js} +1 -1
  22. package/dist/{chunk-XURS6Q6Q.js → chunk-V6SOFUJW.js} +19 -5
  23. package/dist/{chunk-Z4TL2V7S.js → chunk-VTYUBINW.js} +1 -1
  24. package/dist/{chunk-B7UXKNLD.js → chunk-W7HWMMBF.js} +1 -1
  25. package/dist/{chunk-SSXBYF3P.js → chunk-XLSVNUTK.js} +1 -1
  26. package/dist/{chunk-GIOH5JB5.js → chunk-XTIQWSFM.js} +1 -1
  27. package/dist/{chunk-PKC77VZP.js → chunk-ZZGEVJKX.js} +1 -1
  28. package/dist/class/class.codec.js +5 -5
  29. package/dist/class/class.parser.js +4 -4
  30. package/dist/class/class.schema.js +3 -3
  31. package/dist/feat/feat.codec.js +4 -4
  32. package/dist/feat/feat.parser.js +3 -3
  33. package/dist/feat/feat.schema.js +2 -2
  34. package/dist/index.js +622 -118
  35. package/dist/item/item.actions-map.d.ts +10 -3
  36. package/dist/item/item.actions-map.js +13 -8
  37. package/dist/item/item.codec.js +3 -3
  38. package/dist/item/item.parser.js +2 -2
  39. package/dist/item/item.schema.d.ts +13 -0
  40. package/dist/item/item.schema.js +1 -1
  41. package/dist/item/item.types.d.ts +17 -0
  42. package/dist/pc/pc.decision-engine.js +8 -1
  43. package/dist/pc/pc.equipment.d.ts +2 -1
  44. package/dist/pc/pc.equipment.js +9 -1
  45. package/dist/pc/pc.feature-effects.d.ts +31 -9
  46. package/dist/pc/pc.feature-effects.js +11 -1
  47. package/dist/pc/pc.parser.js +1 -1
  48. package/dist/pc/pc.pools.js +8 -3
  49. package/dist/pc/pc.recalc.js +80 -62
  50. package/dist/pc/pc.resolver.d.ts +71 -1
  51. package/dist/pc/pc.resolver.js +21 -254
  52. package/dist/pc/pc.schema.d.ts +21 -0
  53. package/dist/pc/pc.schema.js +1 -1
  54. package/dist/pc/pc.types.d.ts +80 -1
  55. package/dist/race/race.codec.js +5 -5
  56. package/dist/race/race.parser.js +4 -4
  57. package/dist/race/race.schema.js +3 -3
  58. package/dist/schemas/choice-schema.js +1 -1
  59. package/dist/schemas/feature-schema.js +2 -2
  60. package/dist/subclass/subclass.codec.js +5 -5
  61. package/dist/subclass/subclass.parser.js +4 -4
  62. package/dist/subclass/subclass.schema.js +3 -3
  63. package/dist/types/choice.d.ts +5 -0
  64. package/dist/weapon/weapon-mastery.d.ts +12 -0
  65. package/package.json +1 -1
  66. package/dist/chunk-6PXE7T7C.js +0 -79
  67. package/dist/chunk-TJCNX6UZ.js +0 -521
@@ -2,12 +2,28 @@ import type { EntityRegistry } from "@archivist-gg/core";
2
2
  import type { RaceEntity } from "@archivist-gg/dnd5e/race/race.types";
3
3
  import type { BackgroundEntity } from "@archivist-gg/dnd5e/background/background.types";
4
4
  import type { FeatEntity } from "@archivist-gg/dnd5e/feat/feat.types";
5
- import type { Character, ResolvedCharacter, ResolvedClass, ResolvedFeature } from "./pc.types.js";
5
+ import type { Ability } from "@archivist-gg/dnd5e";
6
+ import type { Character, ChoiceValue, ResolvedCharacter, ResolvedClass, ResolvedFeature, ResolvedSpell } from "./pc.types.js";
6
7
  export interface ResolveResult {
7
8
  character: ResolvedCharacter;
8
9
  warnings: string[];
9
10
  }
10
11
  export declare function stripSlug(ref: string | null): string | null;
12
+ /**
13
+ * Resolve a background's `origin_feat` wikilink into a real FeatEntity + a display
14
+ * name, using ONLY the EntityRegistry (no DOM). Lifted from the builder's
15
+ * background-step so the resolver pipeline and the builder share ONE resolution
16
+ * (R2-m7, [[always-retire-shims-edit-all]]). Handles both path-style refs
17
+ * ("[[SRD 2024/Feats/Alert]]") and bare-slug refs ("[[my-feat]]"), plus the 2024
18
+ * parenthetical-variant refs ("[[SRD 2024/Feats/Magic Initiate (Cleric)]]") that
19
+ * resolve to the BASE feat ("magic-initiate") while keeping the full variant
20
+ * string as the display name. Returns null when the ref is empty or resolves to no
21
+ * feat (an unresolvable ref folds nothing into the pipeline).
22
+ */
23
+ export declare function resolveOriginFeat(entities: EntityRegistry, originFeatRef: string | null): {
24
+ feat: FeatEntity;
25
+ display: string;
26
+ } | null;
11
27
  export declare class PCResolver {
12
28
  private readonly entities;
13
29
  constructor(entities: EntityRegistry);
@@ -15,11 +31,65 @@ export declare class PCResolver {
15
31
  get registry(): EntityRegistry;
16
32
  resolve(character: Character): ResolveResult;
17
33
  }
34
+ /**
35
+ * 2024 Weapon Mastery: unions the persisted `weapon-mastery` picks across every
36
+ * class/level `ClassEntry.choices[level]["weapon-mastery"]`. Returns both the
37
+ * ORIGINAL full picked slugs (`full` — used for exact-match display-name lookup)
38
+ * and their BARE-normalized form (`bare` — e.g. "srd-2024_greatsword" →
39
+ * "greatsword"), each de-duplicated in first-seen order. Bare-authored picks
40
+ * normalize to themselves. Non-string / non-array picks are ignored.
41
+ */
42
+ export declare function collectChosenWeaponMasteries(character: Character): {
43
+ bare: string[];
44
+ full: string[];
45
+ };
18
46
  /**
19
47
  * Walks class.choices for `feat` entries and adds background feat slugs.
20
48
  * Returns bare slugs (no `[[ ]]`).
21
49
  */
22
50
  export declare function collectFeatSlugs(character: Character): string[];
51
+ /**
52
+ * Feat→spell application (3d). Walks a feat's OWN `choices` to find its spell
53
+ * picks and turns each into a ResolvedSpell. A feat's spells sit behind a
54
+ * `spell-list` select-inline whose CHOSEN branch nests `select-entity{spell}`
55
+ * picks; the chosen spellcasting ability comes from the feat's
56
+ * `spellcasting-ability` select-inline. `read(childId)` yields the persisted pick
57
+ * for a child choice id (origin feat: `origin_choices["background:feat:<id>"]`;
58
+ * class-slot feat: `choices[lvl]["feat:<id>"]`). Unresolvable spell slugs warn and
59
+ * are skipped, mirroring the known-spell loop. classSlug stays null: a feat spell
60
+ * is not owned by a class for DC/ability; the carried `ability` drives its casting.
61
+ */
62
+ export declare function collectFeatGrantedSpells(feat: FeatEntity, read: (childId: string) => ChoiceValue | undefined, entities: EntityRegistry, warnings: string[]): ResolvedSpell[];
63
+ /**
64
+ * Item→spell application (P4-T3). Walks the character's equipment for entries whose
65
+ * resolved item is a spell scroll (carries the T1 `scroll_level` marker) AND names a
66
+ * chosen spell via `overrides.spell`, turning each into a
67
+ * ResolvedSpell{ source:"item", classSlug:null, prepared:true, alwaysPrepared:true }.
68
+ * The casting `ability` is the character's OWN spellcasting ability when they have
69
+ * one (`ownAbility`, the first caster class's ability), else the per-instance
70
+ * `overrides.spell_ability`, else undefined (a no-ability scroll is left ability-less:
71
+ * the plugin surfaces it without a DC; an ability is NEVER fabricated here).
72
+ * `entryIndex` records the originating equipment index so the segmented dedupe keeps
73
+ * two scrolls of one spell (and a scroll of a class-known spell) as their own rows.
74
+ * Unresolvable spell slugs warn and are skipped, mirroring the known-spell + feat loops.
75
+ * In-memory only: no KnownSpellObject{source:"item"} is injected (its strict schema
76
+ * omits `ability`).
77
+ */
78
+ export declare function collectItemGrantedSpells(character: Character, ownAbility: Ability | null, entities: EntityRegistry, warnings: string[]): ResolvedSpell[];
79
+ /**
80
+ * Collapses duplicate resolved spells (3d Minor #2 carry-forward + P4-T3 segmented
81
+ * dedupe). A spell can be emitted more than once: it may be BOTH in
82
+ * `character.spells.known` AND a feat pick, or a feat may be taken as both origin +
83
+ * class-slot. A class-sourced copy owns a real DC via its `classSlug`, so it always
84
+ * wins over a feat copy of the same slug; otherwise first-seen wins. Insertion order
85
+ * is preserved (a later class copy replaces an earlier feat copy in place), so the
86
+ * Spells section never renders duplicate rows while a legitimately class-known copy is
87
+ * never lost. Item-source spells carry INSTANCE identity: they key by
88
+ * `slug + "#" + entryIndex`, so two scrolls of one spell stay two rows and a scroll of
89
+ * a class-known spell keeps its OWN item row, never collapsing into / being collapsed
90
+ * by a class/feat copy. The class/feat slug-only merge is unchanged.
91
+ */
92
+ export declare function dedupeResolvedSpells(spells: ResolvedSpell[]): ResolvedSpell[];
23
93
  /** Selected select-entity values (optional-features) and selected inline
24
94
  * options carrying effects[] become synthesized resolved features (SP2 Plan 3 §9).
25
95
  * The actions/resources engines consume these synthesized features, and the
@@ -1,267 +1,34 @@
1
1
  import {
2
- resolveAllPools
3
- } from "../chunk-6PXE7T7C.js";
4
- import {
5
- normalizeKnownSpell,
6
- resolveSpellcasting
7
- } from "../chunk-IXZXFFFW.js";
8
- import "../chunk-5XUXXAQJ.js";
9
- import "../chunk-TJCNX6UZ.js";
2
+ PCResolver,
3
+ collectChosenGrantedFeatures,
4
+ collectChosenWeaponMasteries,
5
+ collectFeatGrantedSpells,
6
+ collectFeatSlugs,
7
+ collectItemGrantedSpells,
8
+ collectResolvedFeatures,
9
+ dedupeResolvedSpells,
10
+ resolveOriginFeat,
11
+ stripSlug
12
+ } from "../chunk-R5SQHP2X.js";
10
13
  import "../chunk-BH6AMZVY.js";
14
+ import "../chunk-UC4FE5X3.js";
15
+ import "../chunk-IXZXFFFW.js";
16
+ import "../chunk-5XUXXAQJ.js";
11
17
  import "../chunk-JEYQU3JT.js";
18
+ import "../chunk-DC74TZNA.js";
19
+ import "../chunk-3TRGRSCH.js";
12
20
  import "../chunk-J2RYSZMQ.js";
13
21
  import "../chunk-NAEPSZAM.js";
14
22
  import "../chunk-WP2CX2JO.js";
15
-
16
- // src/pc/pc.resolver.ts
17
- var SLUG_RE = /^\[\[(.+?)\]\]$/;
18
- function stripSlug(ref) {
19
- if (!ref) return null;
20
- const m = ref.match(SLUG_RE);
21
- return m ? m[1] : ref;
22
- }
23
- var PCResolver = class {
24
- constructor(entities) {
25
- this.entities = entities;
26
- }
27
- /** Exposed so recalc() can apply equipment bonuses (Pass A / Pass B). */
28
- get registry() {
29
- return this.entities;
30
- }
31
- resolve(character) {
32
- var _a, _b, _c, _d, _e;
33
- const warnings = [];
34
- const lookup = (rawRef, type) => {
35
- const slug = stripSlug(rawRef);
36
- if (!slug) return null;
37
- const reg = this.entities.getByTypeAndSlug(type, slug);
38
- if (!reg) {
39
- warnings.push(`Slug [[${slug}]] not found in compendium as ${type}.`);
40
- return null;
41
- }
42
- const data = reg.data;
43
- return data.slug == null ? { ...data, slug: reg.slug } : data;
44
- };
45
- const race = lookup(character.race, "race");
46
- const background = lookup(character.background, "background");
47
- const classes = character.class.map((c) => ({
48
- entity: lookup(c.name, "class"),
49
- level: c.level,
50
- subclass: lookup(c.subclass, "subclass"),
51
- choices: c.choices
52
- }));
53
- const featSlugs = collectFeatSlugs(character);
54
- const feats = [];
55
- for (const slug of featSlugs) {
56
- const f = lookup(`[[${slug}]]`, "feat");
57
- if (f) feats.push(f);
58
- }
59
- const totalLevel = classes.reduce((sum, c) => sum + c.level, 0);
60
- const features = collectResolvedFeatures(race, classes, background, feats);
61
- const extraFeatures = collectChosenGrantedFeatures(character, classes, this.entities, race, background);
62
- features.push(...extraFeatures);
63
- const primaryCasterSlug = (_a = classes.map((c, i) => c.entity && resolveSpellcasting(c) ? stripSlug(character.class[i].name) : null).find((slug) => slug != null)) != null ? _a : null;
64
- const spells = [];
65
- for (const raw of (_b = character.spells.known) != null ? _b : []) {
66
- const n = normalizeKnownSpell(raw);
67
- const reg = this.entities.getByTypeAndSlug("spell", n.slug);
68
- if (!reg) {
69
- warnings.push(`Spell [[${n.slug}]] not found in compendium.`);
70
- continue;
71
- }
72
- const entity = reg.data;
73
- const isCantrip = ((_c = entity.level) != null ? _c : 0) === 0;
74
- const classSlug = (_d = n.classSlug) != null ? _d : primaryCasterSlug;
75
- const prep = isCantrip || n.alwaysPrepared ? true : (_e = n.preparedFlag) != null ? _e : false;
76
- spells.push({ entity, slug: n.slug, classSlug, source: n.source, prepared: prep, alwaysPrepared: n.alwaysPrepared });
77
- }
78
- const resolvedCharacter = {
79
- definition: character,
80
- race,
81
- classes,
82
- background,
83
- feats,
84
- totalLevel,
85
- features,
86
- spells,
87
- pools: [],
88
- state: character.state
89
- };
90
- resolvedCharacter.pools = resolveAllPools(resolvedCharacter, this.entities);
91
- return { character: resolvedCharacter, warnings };
92
- }
93
- };
94
- function collectFeatSlugs(character) {
95
- const slugs = /* @__PURE__ */ new Set();
96
- for (const c of character.class) {
97
- for (const [, choiceBlock] of Object.entries(c.choices)) {
98
- const feat = choiceBlock == null ? void 0 : choiceBlock.feat;
99
- if (typeof feat === "string") {
100
- const s = stripSlug(feat);
101
- if (s) slugs.add(s);
102
- }
103
- }
104
- }
105
- return [...slugs];
106
- }
107
- function collectChosenGrantedFeatures(character, classes, registry, race = null, background = null) {
108
- var _a, _b;
109
- const out = [];
110
- classes.forEach((c, i) => {
111
- var _a2, _b2, _c, _d;
112
- if (!c.entity) return;
113
- const entity = c.entity;
114
- const entry = character.class[i];
115
- if (!entry) return;
116
- for (const [lvlStr, atLevel] of Object.entries(entry.choices)) {
117
- const lvl = Number(lvlStr);
118
- if (!Number.isFinite(lvl) || lvl > entry.level) continue;
119
- const features = (_b2 = ((_a2 = entity.features_by_level) != null ? _a2 : {})[lvl]) != null ? _b2 : [];
120
- const subFeatures = c.subclass ? (_d = ((_c = c.subclass.features_by_level) != null ? _c : {})[lvl]) != null ? _d : [] : [];
121
- for (const feature of [...features, ...subFeatures]) {
122
- walkChoiceGrants(feature.choices, atLevel, (granted) => {
123
- out.push({ feature: granted, source: { kind: "class", slug: entity.slug, level: lvl } });
124
- }, registry);
125
- }
126
- }
127
- });
128
- const oc = (_a = character.origin_choices) != null ? _a : {};
129
- const originAt = (ns) => {
130
- const at = {};
131
- for (const [key, val] of Object.entries(oc)) {
132
- if (key.startsWith(`${ns}:`)) at[key.slice(ns.length + 1)] = val;
133
- }
134
- return at;
135
- };
136
- if (race) {
137
- const atRace = originAt("race");
138
- const emitRace = (granted) => {
139
- out.push({ feature: granted, source: { kind: "race", slug: race.slug } });
140
- };
141
- walkChoiceGrants(race.choices, atRace, emitRace, registry);
142
- for (const trait of (_b = race.traits) != null ? _b : []) {
143
- walkChoiceGrants(trait.choices, atRace, emitRace, registry);
144
- }
145
- }
146
- if (background) {
147
- const atBg = originAt("background");
148
- const emitBg = (granted) => {
149
- out.push({ feature: granted, source: { kind: "background", slug: background.slug } });
150
- };
151
- walkChoiceGrants(background.choices, atBg, emitBg, registry);
152
- if (background.feature) {
153
- walkChoiceGrants(background.feature.choices, atBg, emitBg, registry);
154
- }
155
- }
156
- return out;
157
- }
158
- function walkChoiceGrants(choices, atLevel, emit, registry) {
159
- var _a, _b, _c, _d;
160
- for (const ch of choices != null ? choices : []) {
161
- const sel = atLevel[ch.id];
162
- if (ch.kind === "select-entity" && ch.entity_type === "optional-feature") {
163
- const slugs = Array.isArray(sel) ? sel : typeof sel === "string" ? [sel] : [];
164
- for (const slug of slugs) {
165
- const reg = registry.getByTypeAndSlug("optional-feature", (_a = stripSlug(String(slug))) != null ? _a : String(slug));
166
- if (!reg) continue;
167
- const d = reg.data;
168
- emit({
169
- id: (_b = d.slug) != null ? _b : String(slug),
170
- name: (_c = d.name) != null ? _c : String(slug),
171
- description: d.description,
172
- effects: d.effects,
173
- ...d.action_cost ? { action: d.action_cost } : {}
174
- });
175
- }
176
- }
177
- if (ch.kind === "select-inline") {
178
- const branch = typeof sel === "string" ? ch.options.find((o) => o.value === sel) : void 0;
179
- if ((_d = branch == null ? void 0 : branch.effects) == null ? void 0 : _d.length) {
180
- emit({ id: `${ch.id}-${branch.value}`, name: branch.label, description: branch.description, effects: branch.effects });
181
- }
182
- if (branch == null ? void 0 : branch.choices) walkChoiceGrants(branch.choices, atLevel, emit, registry);
183
- }
184
- }
185
- }
186
- function collectResolvedFeatures(race, classes, background, feats) {
187
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
188
- const out = [];
189
- for (const c of classes) {
190
- if (!c.entity) continue;
191
- const slug = c.entity.slug;
192
- const byLevel = (_a = c.entity.features_by_level) != null ? _a : {};
193
- for (const [lvlStr, feats0] of Object.entries(byLevel)) {
194
- const lvl = parseInt(lvlStr, 10);
195
- if (Number.isNaN(lvl) || lvl > c.level) continue;
196
- for (const feat of feats0) {
197
- out.push({ feature: feat, source: { kind: "class", slug, level: lvl } });
198
- }
199
- }
200
- if ((_b = c.entity.resources) == null ? void 0 : _b.length) {
201
- out.push({
202
- feature: { name: c.entity.name, resources: c.entity.resources },
203
- source: { kind: "class", slug, level: 1 }
204
- });
205
- }
206
- if (c.subclass) {
207
- const sSlug = c.subclass.slug;
208
- const sByLevel = (_c = c.subclass.features_by_level) != null ? _c : {};
209
- for (const [lvlStr, feats0] of Object.entries(sByLevel)) {
210
- const lvl = parseInt(lvlStr, 10);
211
- if (Number.isNaN(lvl) || lvl > c.level) continue;
212
- for (const feat of feats0) {
213
- out.push({ feature: feat, source: { kind: "subclass", slug: sSlug, level: lvl } });
214
- }
215
- }
216
- if ((_d = c.subclass.resources) == null ? void 0 : _d.length) {
217
- out.push({
218
- feature: { name: c.subclass.name, resources: c.subclass.resources },
219
- source: { kind: "subclass", slug: sSlug, level: 1 }
220
- });
221
- }
222
- }
223
- }
224
- if (race) {
225
- const traits = (_e = race.traits) != null ? _e : [];
226
- for (const feat of traits) {
227
- out.push({ feature: feat, source: { kind: "race", slug: race.slug } });
228
- }
229
- }
230
- if (background) {
231
- const bgFeature = background.feature;
232
- if (bgFeature) {
233
- out.push({ feature: bgFeature, source: { kind: "background", slug: background.slug } });
234
- }
235
- }
236
- for (const feat of feats) {
237
- const bundled = (_f = feat.features) != null ? _f : [];
238
- const entityEffects = (_g = feat.effects) != null ? _g : [];
239
- if (bundled.length > 0) {
240
- bundled.forEach((f, i) => {
241
- var _a2;
242
- const feature = i === 0 && entityEffects.length > 0 ? { ...f, effects: [...(_a2 = f.effects) != null ? _a2 : [], ...entityEffects] } : f;
243
- out.push({ feature, source: { kind: "feat", slug: feat.slug } });
244
- });
245
- } else {
246
- const name = (_h = feat.name) != null ? _h : feat.slug;
247
- const description = (_i = feat.description) != null ? _i : "";
248
- out.push({
249
- feature: {
250
- name,
251
- description,
252
- ...feat.resources ? { resources: feat.resources } : {},
253
- ...entityEffects.length > 0 ? { effects: entityEffects } : {}
254
- },
255
- source: { kind: "feat", slug: feat.slug }
256
- });
257
- }
258
- }
259
- return out;
260
- }
261
23
  export {
262
24
  PCResolver,
263
25
  collectChosenGrantedFeatures,
26
+ collectChosenWeaponMasteries,
27
+ collectFeatGrantedSpells,
264
28
  collectFeatSlugs,
29
+ collectItemGrantedSpells,
265
30
  collectResolvedFeatures,
31
+ dedupeResolvedSpells,
32
+ resolveOriginFeat,
266
33
  stripSlug
267
34
  };
@@ -130,6 +130,19 @@ export declare const characterSchema: z.ZodObject<{
130
130
  special: "special";
131
131
  }>>;
132
132
  range: z.ZodOptional<z.ZodString>;
133
+ resist: z.ZodOptional<z.ZodArray<z.ZodString>>;
134
+ immune: z.ZodOptional<z.ZodArray<z.ZodString>>;
135
+ vulnerable: z.ZodOptional<z.ZodArray<z.ZodString>>;
136
+ condition_immune: z.ZodOptional<z.ZodArray<z.ZodString>>;
137
+ spell: z.ZodOptional<z.ZodString>;
138
+ spell_ability: z.ZodOptional<z.ZodEnum<{
139
+ str: "str";
140
+ dex: "dex";
141
+ con: "con";
142
+ int: "int";
143
+ wis: "wis";
144
+ cha: "cha";
145
+ }>>;
133
146
  }, z.core.$strict>>;
134
147
  state: z.ZodOptional<z.ZodObject<{
135
148
  charges: z.ZodOptional<z.ZodObject<{
@@ -216,6 +229,14 @@ export declare const characterSchema: z.ZodObject<{
216
229
  saveDC: z.ZodOptional<z.ZodNumber>;
217
230
  attackBonus: z.ZodOptional<z.ZodNumber>;
218
231
  }, z.core.$strip>>;
232
+ spellcasting_ability: z.ZodOptional<z.ZodEnum<{
233
+ str: "str";
234
+ dex: "dex";
235
+ con: "con";
236
+ int: "int";
237
+ wis: "wis";
238
+ cha: "cha";
239
+ }>>;
219
240
  spell_slots: z.ZodOptional<z.ZodRecord<z.ZodCoercedNumber<unknown>, z.ZodNumber>>;
220
241
  attunement_limit: z.ZodOptional<z.ZodNumber>;
221
242
  }, z.core.$strip>>;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  characterSchema
3
- } from "../chunk-ZLG6FQFB.js";
3
+ } from "../chunk-5WQD3WNZ.js";
4
4
  import "../chunk-TZK36BU7.js";
5
5
  export {
6
6
  characterSchema
@@ -35,6 +35,10 @@ export interface EquipmentEntryOverrides {
35
35
  immune?: string[];
36
36
  vulnerable?: string[];
37
37
  condition_immune?: string[];
38
+ /** Per-instance spell payload (e.g. the spell a Spell Scroll casts). */
39
+ spell?: string;
40
+ /** Ability governing this instance's spell save DC / attack when cast. */
41
+ spell_ability?: Ability;
38
42
  }
39
43
  export interface EquipmentEntryState {
40
44
  charges?: {
@@ -92,6 +96,10 @@ export interface KnownSpellObject {
92
96
  source?: "class" | "feat" | "item" | "race" | "domain";
93
97
  prepared?: boolean;
94
98
  always_prepared?: boolean;
99
+ /** The spell's own spellcasting ability (e.g. WIS for a Magic Initiate (Cleric)
100
+ * pick). Belongs on the persisted shape for symmetry with ResolvedSpell.ability;
101
+ * feat spells arrive via origin_choices, but a known-spell entry may carry it. */
102
+ ability?: Ability;
95
103
  }
96
104
  export type KnownSpellEntry = string | KnownSpellObject;
97
105
  export type PassiveKind = "perception" | "investigation" | "insight";
@@ -120,6 +128,10 @@ export interface CharacterOverrides {
120
128
  saveDC?: number;
121
129
  attackBonus?: number;
122
130
  };
131
+ /** Character-level spellcasting ability for spells that carry no ability of
132
+ * their own (e.g. a Spell Scroll cast by a non-caster): the DC-ability fallback
133
+ * after a per-instance override and the character's own class ability. */
134
+ spellcasting_ability?: Ability;
123
135
  spell_slots?: Record<number, number>;
124
136
  attunement_limit?: number;
125
137
  }
@@ -246,6 +258,19 @@ export type FeatureSource = {
246
258
  export interface ResolvedFeature {
247
259
  feature: Feature;
248
260
  source: FeatureSource;
261
+ /** #3: chosen select-inline option prose, folded onto the PARENT feature for
262
+ * render (a shallow-copy wrapper field — the shared registry entity is never
263
+ * mutated). One entry per select-inline choice with a recorded pick. */
264
+ chosenInline?: {
265
+ label: string;
266
+ description?: string;
267
+ }[];
268
+ /** #3: marks a synthesized chosen-option feature the renderer must SKIP (its
269
+ * prose shows on the parent via chosenInline). A wrapper-only flag —
270
+ * computeFeatureEffects ignores it, so the synthetic's effects still fold. */
271
+ renderSuppressed?: boolean;
272
+ /** The feat's entire substance is a full ability-score improvement (a pure ASI). The DISPLAY layer hides these. */
273
+ buildOnly?: boolean;
249
274
  }
250
275
  export interface ResolvedSpell {
251
276
  entity: Spell;
@@ -254,6 +279,14 @@ export interface ResolvedSpell {
254
279
  source: "class" | "feat" | "item" | "race" | "domain";
255
280
  prepared: boolean;
256
281
  alwaysPrepared: boolean;
282
+ /** The spell's OWN spellcasting ability. For a feat spell this carries the
283
+ * chosen ability (e.g. WIS for Magic Initiate (Cleric)) while classSlug stays
284
+ * null (a feat spell is not owned by a class for DC/ability). Absent/undefined
285
+ * for class spells, which derive their ability from classSlug. */
286
+ ability?: Ability | null;
287
+ /** Index of the originating equipment entry when this spell was granted by a
288
+ * per-instance item (e.g. a Spell Scroll). Absent for class/feat/race spells. */
289
+ entryIndex?: number;
257
290
  }
258
291
  export interface ResolvedClass {
259
292
  entity: ClassEntity | null;
@@ -290,6 +323,11 @@ export interface ResolvedCharacter {
290
323
  features: ResolvedFeature[];
291
324
  spells: ResolvedSpell[];
292
325
  pools: ResolvedPool[];
326
+ /** 2024 Weapon Mastery: bare-normalized slugs of the weapons the character has
327
+ * chosen mastery of, unioned across every class/level `weapon-mastery` pick
328
+ * (e.g. "srd-2024_greatsword" → "greatsword"). Empty when the character has no
329
+ * such pick. Populated by the resolver's collectChosenWeaponMasteries pass. */
330
+ weaponMasteries: string[];
293
331
  state: CharacterState;
294
332
  }
295
333
  export interface ProficiencySet {
@@ -301,6 +339,16 @@ export interface ACTerm {
301
339
  amount: number;
302
340
  kind: "armor" | "shield" | "item" | "unarmored" | "override" | "dex" | "ability" | "feature";
303
341
  }
342
+ /** An additive on-hit damage rider (dice or flat) applied to an attack, from a
343
+ * feature (global — all weapon attacks) or a magic item (that weapon only).
344
+ * `amount` is a dice/flat string ("2d6", "1d8", "2"); `damage_type` optional
345
+ * (a bare manual override string carries its type inline); `source` is a human
346
+ * label for the expand/tooltip. Rendered as its own damage chip on the row. */
347
+ export interface DamageRider {
348
+ amount: string;
349
+ damage_type?: string;
350
+ source?: string;
351
+ }
304
352
  export interface AttackRow {
305
353
  id: string;
306
354
  name: string;
@@ -308,7 +356,10 @@ export interface AttackRow {
308
356
  toHit: number;
309
357
  damageDice: string;
310
358
  damageType: string;
311
- extraDamage?: string;
359
+ /** Accumulated additive on-hit damage riders (feature-global + item-local +
360
+ * the migrated manual override), rendered as extra damage chips. Absent when
361
+ * none apply (so untouched rows are unchanged). */
362
+ damageRiders?: DamageRider[];
312
363
  properties: string[];
313
364
  proficient: boolean;
314
365
  breakdown: {
@@ -343,6 +394,24 @@ export interface AttackRow {
343
394
  * untouched attack rows are unchanged. The weapons table renders these joined
344
395
  * with " · " as a muted caption under the weapon name. */
345
396
  attackNotes?: string[];
397
+ /** 2024 Weapon Mastery for this attack, present only when the character has
398
+ * chosen mastery of this weapon AND is proficient with it AND the weapon
399
+ * declares a mastery property. `slug`/`label`/`description`/`gist` come from
400
+ * the mastery glossary (`gist` = a one-line effect summary for the Actions-tab
401
+ * mastery column, e.g. "on fail: Prone"); `derived` carries the computed
402
+ * number for the two numeric masteries (topple → Save DC, graze → On-miss
403
+ * damage) and is absent for the rest. Additive + display-only, absent
404
+ * (undefined) on untouched rows so nothing else changes. */
405
+ mastery?: {
406
+ slug: string;
407
+ label: string;
408
+ description: string;
409
+ gist?: string;
410
+ derived?: {
411
+ label: string;
412
+ value: number;
413
+ };
414
+ };
346
415
  }
347
416
  export interface AppliedBonuses {
348
417
  ability_bonuses: Partial<Record<Ability, number>>;
@@ -433,6 +502,16 @@ export interface DerivedStats {
433
502
  } | null;
434
503
  /** One entry per casting class (multiclass-aware). Empty for non-casters. */
435
504
  spellcastingClasses: SpellcastingClassInfo[];
505
+ /** Save DC / attack bonus for spells that carry their OWN spellcasting
506
+ * ability rather than a class (feat-granted spells, e.g. Magic Initiate).
507
+ * Keyed by that ability; populated for every ability a feat spell uses.
508
+ * Computed independently of class casters, so a NON-caster (empty
509
+ * spellcastingClasses / null spellcasting) still gets a real DC/attack.
510
+ * Empty object when the character has no such spell. */
511
+ abilitySpellcasting: Partial<Record<Ability, {
512
+ saveDC: number;
513
+ attackBonus: number;
514
+ }>>;
436
515
  /** Derived standard slot totals by spell level (before user override). */
437
516
  derivedSpellSlots: Record<number, number>;
438
517
  /** Warlock Pact Magic, or null. */
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  raceCodec
3
- } from "../chunk-PKC77VZP.js";
4
- import "../chunk-IDTDACM6.js";
5
- import "../chunk-FFULAF2P.js";
6
- import "../chunk-DABBCYTT.js";
3
+ } from "../chunk-ZZGEVJKX.js";
4
+ import "../chunk-PHQRGDRQ.js";
5
+ import "../chunk-2BWAID3Y.js";
6
+ import "../chunk-CZPMTQOY.js";
7
7
  import "../chunk-WYGD2UGN.js";
8
8
  import "../chunk-PZOWBI3B.js";
9
- import "../chunk-CDW7R2QT.js";
9
+ import "../chunk-NXSWH3AE.js";
10
10
  import "../chunk-NXR3BD2V.js";
11
11
  export {
12
12
  raceCodec
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  parseRace
3
- } from "../chunk-IDTDACM6.js";
4
- import "../chunk-FFULAF2P.js";
5
- import "../chunk-DABBCYTT.js";
3
+ } from "../chunk-PHQRGDRQ.js";
4
+ import "../chunk-2BWAID3Y.js";
5
+ import "../chunk-CZPMTQOY.js";
6
6
  import "../chunk-WYGD2UGN.js";
7
7
  import "../chunk-PZOWBI3B.js";
8
- import "../chunk-CDW7R2QT.js";
8
+ import "../chunk-NXSWH3AE.js";
9
9
  import "../chunk-NXR3BD2V.js";
10
10
  export {
11
11
  parseRace
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  raceEntitySchema
3
- } from "../chunk-FFULAF2P.js";
4
- import "../chunk-DABBCYTT.js";
3
+ } from "../chunk-2BWAID3Y.js";
4
+ import "../chunk-CZPMTQOY.js";
5
5
  import "../chunk-WYGD2UGN.js";
6
6
  import "../chunk-PZOWBI3B.js";
7
- import "../chunk-CDW7R2QT.js";
7
+ import "../chunk-NXSWH3AE.js";
8
8
  import "../chunk-NXR3BD2V.js";
9
9
  export {
10
10
  raceEntitySchema
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  choiceSchema
3
- } from "../chunk-CDW7R2QT.js";
3
+ } from "../chunk-NXSWH3AE.js";
4
4
  import "../chunk-NXR3BD2V.js";
5
5
  export {
6
6
  choiceSchema
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  featureSchema
3
- } from "../chunk-DABBCYTT.js";
3
+ } from "../chunk-CZPMTQOY.js";
4
4
  import "../chunk-WYGD2UGN.js";
5
5
  import "../chunk-PZOWBI3B.js";
6
- import "../chunk-CDW7R2QT.js";
6
+ import "../chunk-NXSWH3AE.js";
7
7
  import "../chunk-NXR3BD2V.js";
8
8
  export {
9
9
  featureSchema
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  subclassCodec
3
- } from "../chunk-SSXBYF3P.js";
4
- import "../chunk-PMADYMUI.js";
5
- import "../chunk-B7UXKNLD.js";
3
+ } from "../chunk-XLSVNUTK.js";
4
+ import "../chunk-L7E2UT5A.js";
5
+ import "../chunk-W7HWMMBF.js";
6
6
  import "../chunk-4TIJKN6O.js";
7
- import "../chunk-DABBCYTT.js";
7
+ import "../chunk-CZPMTQOY.js";
8
8
  import "../chunk-WYGD2UGN.js";
9
9
  import "../chunk-PZOWBI3B.js";
10
- import "../chunk-CDW7R2QT.js";
10
+ import "../chunk-NXSWH3AE.js";
11
11
  import "../chunk-NXR3BD2V.js";
12
12
  export {
13
13
  subclassCodec