@archivist-gg/dnd5e 0.3.13 → 0.3.19

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 (122) hide show
  1. package/dist/background/background.codec.js +5 -5
  2. package/dist/background/background.parser.js +4 -4
  3. package/dist/background/background.schema.js +3 -3
  4. package/dist/chunk-3LWMFYKS.js +57 -0
  5. package/dist/{chunk-4SLOJXJ4.js → chunk-3SFGWFVQ.js} +1 -1
  6. package/dist/{chunk-Q6UOHCCT.js → chunk-3SIUAFDQ.js} +1 -1
  7. package/dist/chunk-4J6DS3EJ.js +30 -0
  8. package/dist/{chunk-RLGQ5PZZ.js → chunk-5JXFJ5AU.js} +11 -1
  9. package/dist/{chunk-4BZ47YJ4.js → chunk-64VNLMSG.js} +59 -16
  10. package/dist/{chunk-XY6X2APM.js → chunk-7Q2VPKCD.js} +17 -0
  11. package/dist/{chunk-Q5XTEYHD.js → chunk-7XJHI5H5.js} +16 -9
  12. package/dist/{chunk-FPGXFD2M.js → chunk-B2QHP6UF.js} +2 -2
  13. package/dist/{chunk-HJQNK3J5.js → chunk-B3DP42OM.js} +1 -4
  14. package/dist/{chunk-MC5YJ4PB.js → chunk-BJGOSM76.js} +1 -1
  15. package/dist/{chunk-VDPRBUU5.js → chunk-BJWSOQUC.js} +1 -1
  16. package/dist/{chunk-QEO7LYTG.js → chunk-EJD47JHL.js} +181 -93
  17. package/dist/{chunk-X7VZY4ON.js → chunk-FJYBLXV7.js} +2 -2
  18. package/dist/{chunk-QSBJ6NEM.js → chunk-FMUD3LDV.js} +1 -1
  19. package/dist/{chunk-LVX6TQ3R.js → chunk-FMYWORTP.js} +2 -2
  20. package/dist/{chunk-PX25R3DK.js → chunk-FVXLP7P3.js} +1 -1
  21. package/dist/{chunk-XJRNEQPD.js → chunk-GVWEFECG.js} +1 -1
  22. package/dist/{chunk-R2V5LN3I.js → chunk-HY2SRSVM.js} +1 -1
  23. package/dist/{chunk-ZMJQUZQH.js → chunk-JXWNXIIZ.js} +24 -0
  24. package/dist/chunk-KR6JF3BF.js +14 -0
  25. package/dist/{chunk-DPM6V7YX.js → chunk-LICYHPI3.js} +82 -6
  26. package/dist/{chunk-J53ERF6K.js → chunk-LNT64X7C.js} +1 -1
  27. package/dist/{chunk-RQNJXADQ.js → chunk-NKZMQV2C.js} +1 -1
  28. package/dist/{chunk-UE6LKRXQ.js → chunk-NTU2DXNG.js} +24 -1
  29. package/dist/{chunk-T5GZBKIQ.js → chunk-NXUY5XQN.js} +168 -33
  30. package/dist/{chunk-M2OWVAZG.js → chunk-NZO7M5GV.js} +1 -1
  31. package/dist/{chunk-BRGLDWUP.js → chunk-OOL4KMG2.js} +7 -3
  32. package/dist/{chunk-MWCCGPG3.js → chunk-OOSRUJBI.js} +3 -2
  33. package/dist/{chunk-H5PTKYXL.js → chunk-QA3YCOML.js} +1 -1
  34. package/dist/{chunk-MUIU7RZ3.js → chunk-QFWQKS3M.js} +1 -1
  35. package/dist/chunk-RD2MID3M.js +292 -0
  36. package/dist/chunk-TRBGC4ZG.js +112 -0
  37. package/dist/{chunk-PLOSP4XQ.js → chunk-UJCK5IW4.js} +11 -2
  38. package/dist/{chunk-5KGV6TF7.js → chunk-UNYR5GLI.js} +1 -1
  39. package/dist/{chunk-JNNNBEUH.js → chunk-VN5DLURA.js} +7 -2
  40. package/dist/{chunk-EL6QPF6I.js → chunk-VP5TIXDK.js} +1 -1
  41. package/dist/chunk-WOHX7YTH.js +165 -0
  42. package/dist/{chunk-G7ED5H2G.js → chunk-XTEFPUSN.js} +1 -1
  43. package/dist/{chunk-6ZY3U6BQ.js → chunk-ZOWKWMD2.js} +1 -1
  44. package/dist/class/class.codec.js +6 -6
  45. package/dist/class/class.parser.js +5 -5
  46. package/dist/class/class.schema.d.ts +8 -0
  47. package/dist/class/class.schema.js +4 -4
  48. package/dist/class/class.standard-array.d.ts +16 -0
  49. package/dist/class/class.standard-array.js +20 -0
  50. package/dist/class/class.types.d.ts +3 -0
  51. package/dist/dnd/amount-text.d.ts +42 -0
  52. package/dist/dnd/amount-text.js +15 -0
  53. package/dist/dnd/resource-words.d.ts +4 -0
  54. package/dist/dnd/resource-words.js +30 -0
  55. package/dist/entities/registry-revision.d.ts +25 -0
  56. package/dist/entities/registry-revision.js +10 -0
  57. package/dist/feat/feat.codec.js +5 -5
  58. package/dist/feat/feat.parser.js +4 -4
  59. package/dist/feat/feat.schema.d.ts +34 -0
  60. package/dist/feat/feat.schema.js +3 -3
  61. package/dist/index.js +4027 -383
  62. package/dist/monster/monster.parser.js +5 -5
  63. package/dist/monster/monster.schema.js +4 -4
  64. package/dist/optional-feature/optional-feature.codec.js +5 -4
  65. package/dist/optional-feature/optional-feature.parser.js +4 -3
  66. package/dist/optional-feature/optional-feature.schema.d.ts +35 -0
  67. package/dist/optional-feature/optional-feature.schema.js +3 -2
  68. package/dist/pc/pc.additional-spells.d.ts +16 -2
  69. package/dist/pc/pc.additional-spells.js +5 -2
  70. package/dist/pc/pc.amount-bindings.d.ts +41 -0
  71. package/dist/pc/pc.amount-bindings.js +28 -0
  72. package/dist/pc/pc.decision-engine.d.ts +11 -0
  73. package/dist/pc/pc.decision-engine.js +20 -16
  74. package/dist/pc/pc.entry-overrides.js +3 -2
  75. package/dist/pc/pc.equipment.js +7 -5
  76. package/dist/pc/pc.feature-effects.d.ts +21 -2
  77. package/dist/pc/pc.feature-effects.js +15 -2
  78. package/dist/pc/pc.infusions.d.ts +9 -1
  79. package/dist/pc/pc.infusions.js +3 -2
  80. package/dist/pc/pc.member-choices.d.ts +122 -0
  81. package/dist/pc/pc.member-choices.js +39 -0
  82. package/dist/pc/pc.parser.js +1 -1
  83. package/dist/pc/pc.pools.d.ts +7 -0
  84. package/dist/pc/pc.pools.js +5 -2
  85. package/dist/pc/pc.proficiencies.d.ts +14 -0
  86. package/dist/pc/pc.proficiencies.js +72 -22
  87. package/dist/pc/pc.proficiency-normalize.d.ts +14 -0
  88. package/dist/pc/pc.proficiency-normalize.js +9 -3
  89. package/dist/pc/pc.proficiency-query.js +7 -1
  90. package/dist/pc/pc.recalc.js +212 -99
  91. package/dist/pc/pc.resolver.d.ts +6 -2
  92. package/dist/pc/pc.resolver.js +16 -14
  93. package/dist/pc/pc.rest.js +4 -3
  94. package/dist/pc/pc.schema.d.ts +48 -2
  95. package/dist/pc/pc.schema.js +1 -1
  96. package/dist/pc/pc.spell-scope.d.ts +22 -0
  97. package/dist/pc/pc.tool-picks.js +2 -2
  98. package/dist/pc/pc.types.d.ts +110 -6
  99. package/dist/pc/pc.weapon-grant.d.ts +15 -0
  100. package/dist/pc/pool-layout.d.ts +3 -9
  101. package/dist/pc/pool-layout.js +1 -3
  102. package/dist/pc/resource-equivalents.d.ts +44 -0
  103. package/dist/pc/spell-source.js +4 -9
  104. package/dist/race/race.codec.js +6 -6
  105. package/dist/race/race.parser.js +5 -5
  106. package/dist/race/race.schema.js +4 -4
  107. package/dist/schemas/choice-schema.js +2 -2
  108. package/dist/schemas/feature-effect-schema.d.ts +34 -0
  109. package/dist/schemas/feature-effect-schema.js +1 -1
  110. package/dist/schemas/feature-schema.js +3 -3
  111. package/dist/spell/spell.codec.js +2 -2
  112. package/dist/spell/spell.parser.d.ts +6 -1
  113. package/dist/spell/spell.parser.js +5 -3
  114. package/dist/spell/spell.types.d.ts +10 -0
  115. package/dist/subclass/subclass.codec.js +6 -6
  116. package/dist/subclass/subclass.parser.js +5 -5
  117. package/dist/subclass/subclass.schema.js +4 -4
  118. package/dist/types/choice.d.ts +7 -2
  119. package/dist/types/feature-effect.d.ts +24 -0
  120. package/dist/types/optional-feature.types.d.ts +6 -1
  121. package/dist/types/selection-pool.d.ts +2 -2
  122. package/package.json +1 -1
@@ -4,12 +4,15 @@ import {
4
4
  import {
5
5
  warnOnce
6
6
  } from "./chunk-LVQLWRCU.js";
7
+ import {
8
+ memoByRevision
9
+ } from "./chunk-4J6DS3EJ.js";
7
10
  import {
8
11
  slugify
9
12
  } from "./chunk-ZCTUYGNV.js";
10
13
  import {
11
14
  mirrorSpellShapes
12
- } from "./chunk-XY6X2APM.js";
15
+ } from "./chunk-7Q2VPKCD.js";
13
16
  import {
14
17
  ABILITY_KEYS
15
18
  } from "./chunk-WELIKLZF.js";
@@ -41,6 +44,10 @@ function normalizeSpellRef(ref) {
41
44
  return out.length > 0 ? out : null;
42
45
  }
43
46
  function buildSpellNameIndex(entities) {
47
+ return memoByRevision(SPELL_NAME_INDEX, entities, () => spellNameIndexOf(entities));
48
+ }
49
+ var SPELL_NAME_INDEX = /* @__PURE__ */ new WeakMap();
50
+ function spellNameIndexOf(entities) {
44
51
  var _a;
45
52
  const byName = /* @__PURE__ */ new Map();
46
53
  for (const e of entities.search("", "spell", Number.POSITIVE_INFINITY)) {
@@ -95,20 +102,78 @@ var entryHasInSliceLeaf = (e, comparand, own) => readAbility(e.ability, own) !==
95
102
  const lv = e[b];
96
103
  return !!lv && typeof lv === "object" && !Array.isArray(lv) && Object.entries(lv).some(([k, v]) => (k === "_" || Number(k) <= comparand) && Array.isArray(v) && v.some((x) => typeof x === "string"));
97
104
  });
105
+ function chosenRaceOptionKeys(race, read) {
106
+ var _a;
107
+ const out = /* @__PURE__ */ new Set();
108
+ const r = race;
109
+ const lists = [r.choices, ...((_a = r.traits) != null ? _a : []).map((t) => t == null ? void 0 : t.choices)];
110
+ for (const list of lists) {
111
+ for (const c of Array.isArray(list) ? list : []) {
112
+ if ((c == null ? void 0 : c.kind) !== "select-inline" || typeof c.id !== "string" || RACE_STRUCTURAL_PSEUDO.has(c.id)) continue;
113
+ const v = read(c.id);
114
+ for (const value of Array.isArray(v) ? v : typeof v === "string" ? [v] : []) {
115
+ if (typeof value !== "string") continue;
116
+ out.add(slugify(value));
117
+ const opt = (Array.isArray(c.options) ? c.options : []).find((o) => (o == null ? void 0 : o.value) === value);
118
+ if (typeof (opt == null ? void 0 : opt.label) === "string") out.add(slugify(opt.label));
119
+ }
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ function innateLeaves(innate, comparand) {
125
+ if (!innate || typeof innate !== "object" || Array.isArray(innate)) return [];
126
+ const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : v && typeof v === "object" ? Object.values(v).flatMap(strings) : [];
127
+ return Object.entries(innate).filter(([k]) => k === "_" || Number(k) <= comparand).flatMap(([, v]) => strings(v));
128
+ }
129
+ function memberAtWillLeaves(innate, comparand, slug) {
130
+ if (!innate || typeof innate !== "object" || Array.isArray(innate)) return [];
131
+ const out = [];
132
+ for (const [k, v] of Object.entries(innate)) {
133
+ if (!(k === "_" || Number(k) <= comparand)) continue;
134
+ if (Array.isArray(v)) {
135
+ out.push(...v);
136
+ continue;
137
+ }
138
+ for (const [usage, list] of v && typeof v === "object" ? Object.entries(v) : []) {
139
+ if (usage === "will" && Array.isArray(list)) out.push(...list);
140
+ else warnOnce(`addspells:usage:${slug}:innate:${k}:${usage}`, `archivist: additional_spells on "${slug}" innate.${k}.${usage} is a usage economy (skipped; G8)`);
141
+ }
142
+ }
143
+ return out;
144
+ }
98
145
  function collectAdditionalSpells(args) {
146
+ var _a, _b;
99
147
  const { race, feats, classes, totalLevel, ownAbility, alreadyCollected, entities, warnings } = args;
148
+ const raceChoice = (_a = args.raceChoice) != null ? _a : (() => void 0);
100
149
  let index = null;
101
150
  const indexOf = () => index != null ? index : index = buildSpellNameIndex(entities);
102
151
  const out = [];
103
152
  const carriers = [];
104
153
  const asRaw = (x) => x == null ? void 0 : x.additional_spells;
105
154
  if (race && raceIsUngated(race)) carriers.push({ root: "race", slug: race.slug, edition: editionOf(race), entries: entriesOf(asRaw(race)), comparand: totalLevel, classSlug: null });
155
+ else if (race) {
156
+ const chosen = chosenRaceOptionKeys(race, raceChoice);
157
+ for (const e of entriesOf(asRaw(race))) {
158
+ if (typeof e.name === "string" && chosen.has(slugify(e.name))) {
159
+ carriers.push({ root: "race", slug: race.slug, edition: editionOf(race), entries: [e], comparand: totalLevel, classSlug: null });
160
+ }
161
+ }
162
+ }
106
163
  for (const f of feats) carriers.push({ root: "feat", slug: f.slug, edition: editionOf(f), entries: entriesOf(asRaw(f)), comparand: totalLevel, classSlug: null });
107
164
  for (const c of classes) {
108
165
  if (!c.entity) continue;
109
166
  carriers.push({ root: "class", slug: c.entity.slug, edition: editionOf(c.entity), entries: entriesOf(asRaw(c.entity)), comparand: c.level, classSlug: c.entity.slug });
110
167
  if (c.subclass) carriers.push({ root: "subclass", slug: c.subclass.slug, edition: editionOf(c.subclass), entries: entriesOf(asRaw(c.subclass)), comparand: c.level, classSlug: c.entity.slug });
111
168
  }
169
+ for (const pool of (_b = args.pools) != null ? _b : []) {
170
+ const c = classes[pool.classIndex];
171
+ if (!(c == null ? void 0 : c.entity)) continue;
172
+ for (const m of [...pool.selected, ...pool.grants]) {
173
+ if (m.itemBound === true) continue;
174
+ carriers.push({ root: "member", slug: m.slug, edition: editionOf(m.entity), entries: entriesOf(asRaw(m.entity)), comparand: c.level, classSlug: c.entity.slug });
175
+ }
176
+ }
112
177
  for (const car of carriers) {
113
178
  if (car.entries.length === 0) continue;
114
179
  if (car.entries.length > 1) {
@@ -116,12 +181,21 @@ function collectAdditionalSpells(args) {
116
181
  continue;
117
182
  }
118
183
  const entry = car.entries[0];
119
- const ability = readAbility(entry.ability, ownAbility);
184
+ let ability = readAbility(entry.ability, ownAbility);
185
+ if (ability === "choose" && car.root === "race") {
186
+ const pick = raceChoice("spellcasting-ability");
187
+ const allowed = entry.ability.choose;
188
+ ability = typeof pick === "string" && Array.isArray(allowed) && allowed.includes(pick) ? pick : "choose";
189
+ }
120
190
  if (ability === "choose") continue;
121
- for (const bucket of ["known", "prepared"]) {
191
+ const buckets = ["known", "prepared"].map((bucket) => {
122
192
  const levels = entry[bucket];
123
- if (!levels || typeof levels !== "object" || Array.isArray(levels)) continue;
124
- for (const [k, v] of Object.entries(levels)) {
193
+ return [bucket, !levels || typeof levels !== "object" || Array.isArray(levels) ? [] : Object.entries(levels)];
194
+ });
195
+ if (car.root === "race") buckets.push(["innate", [["_", innateLeaves(entry.innate, car.comparand)]]]);
196
+ if (car.root === "member") buckets.push(["at-will", [["_", memberAtWillLeaves(entry.innate, car.comparand, car.slug)]]]);
197
+ for (const [bucket, levelEntries] of buckets) {
198
+ for (const [k, v] of levelEntries) {
125
199
  if (!(k === "_" || Number(k) <= car.comparand)) continue;
126
200
  if (!Array.isArray(v)) {
127
201
  warnOnce(`addspells:usage:${car.slug}:${bucket}:${k}`, `archivist: additional_spells on "${car.slug}" ${bucket}.${k} is a usage economy (skipped; G8)`);
@@ -142,7 +216,8 @@ function collectAdditionalSpells(args) {
142
216
  source: car.root === "race" ? "race" : car.root === "feat" ? "feat" : "class",
143
217
  prepared: true,
144
218
  alwaysPrepared: true,
145
- ...ability ? { ability } : {}
219
+ ...ability ? { ability } : {},
220
+ ...bucket === "at-will" ? { atWill: true } : {}
146
221
  });
147
222
  }
148
223
  }
@@ -157,5 +232,6 @@ export {
157
232
  buildSpellNameIndex,
158
233
  resolveSpellByName,
159
234
  raceIsUngated,
235
+ chosenRaceOptionKeys,
160
236
  collectAdditionalSpells
161
237
  };
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-CJYCVNW2.js";
9
9
  import {
10
10
  featureSchema
11
- } from "./chunk-FPGXFD2M.js";
11
+ } from "./chunk-B2QHP6UF.js";
12
12
  import {
13
13
  resourceSchema
14
14
  } from "./chunk-ZJXSXVFP.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  backgroundEntitySchema
3
- } from "./chunk-MUIU7RZ3.js";
3
+ } from "./chunk-QFWQKS3M.js";
4
4
 
5
5
  // src/background/background.parser.ts
6
6
  import { parseYaml } from "@archivist-gg/core";
@@ -143,13 +143,16 @@ var characterOverridesShape = z.object({
143
143
  * `toProfSlug` output with the apostrophe RETAINED ("thieves'-tools"), matching the engine's
144
144
  * own tool vocabulary in types/choice.ts. The READER is wider: `computeEffectiveProficiencies`
145
145
  * normalises each key with `toProfSlug` on read, so a hand-typed "Thieves' Tools" also
146
- * matches; and the plugin's two writers (`CharacterEditState.setToolProficiency` and the
146
+ * matches; and the plugin's two writers (`CharacterEditState.setToolExpertise` and the
147
147
  * `addProficiency` clearance) match an existing key by slug too, so a key the note ALREADY
148
148
  * carries is written THROUGH in its own spelling rather than duplicated · nothing here
149
149
  * re-spells it (review M-4: "repaired in place" read as "normalised", which it is not).
150
150
  * `languages` gets no such sibling: languages have no tri (§9.3). */
151
151
  proficiency: z.record(z.string(), proficiencyTri).optional()
152
152
  }).optional(),
153
+ /** D4 A: weapon and armor proficiency edits, the `languages` shape. Same no-defaults rule. */
154
+ weapons: z.object({ add: z.array(z.string()).optional(), remove: z.array(z.string()).optional() }).optional(),
155
+ armor: z.object({ add: z.array(z.string()).optional(), remove: z.array(z.string()).optional() }).optional(),
153
156
  /** Manual defense edits. SUPPRESSION-ONLY, and the missing `add` channel is a DELIBERATE
154
157
  * asymmetry with `languages`/`tools` above · do not "complete the pattern". The additive
155
158
  * store is `character.defenses.*`, which already exists and works; a second additive
@@ -251,6 +254,25 @@ var knownSpellObjectSchema = z.object({
251
254
  always_prepared: z.boolean().optional()
252
255
  }).strict();
253
256
  var knownSpellEntrySchema = z.union([z.string().min(1), knownSpellObjectSchema]);
257
+ var sheetViewFilterSchema = z.object({
258
+ show: z.array(z.string().min(1)).optional(),
259
+ hide: z.array(z.string().min(1)).optional()
260
+ });
261
+ var sheetPoolViewSchema = z.object({
262
+ show: z.enum(["known", "granted", "not-known"]).optional().catch(void 0),
263
+ off: z.record(z.string(), z.array(z.string())).optional(),
264
+ folded: z.array(z.string().min(1)).optional()
265
+ });
266
+ var sheetStateSchema = z.object({
267
+ // D13 (D3-A): `body: wide` is the wide three-column sheet (it draws the saves as a section, like `saves: section`).
268
+ layout: z.object({
269
+ saves: z.enum(["section"]).optional().catch(void 0),
270
+ body: z.enum(["wide"]).optional().catch(void 0)
271
+ }).optional(),
272
+ favorites: z.array(z.string().min(1)).optional(),
273
+ filters: z.object({ actions: sheetViewFilterSchema.optional(), passive: sheetViewFilterSchema.optional() }).optional(),
274
+ pools: z.record(z.string(), sheetPoolViewSchema).optional()
275
+ });
254
276
  var characterSchema = z.object({
255
277
  name: z.string().min(1),
256
278
  edition: z.enum(["2014", "2024"]),
@@ -323,6 +345,7 @@ var characterSchema = z.object({
323
345
  vulnerabilities: z.array(z.string()).default([]),
324
346
  condition_immunities: z.array(z.string()).default([])
325
347
  }).partial().optional(),
348
+ sheet: sheetStateSchema.optional(),
326
349
  state: characterStateSchema
327
350
  });
328
351
 
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-W6GC6OKK.js";
4
4
  import {
5
5
  derivePoolLayout
6
- } from "./chunk-HJQNK3J5.js";
6
+ } from "./chunk-B3DP42OM.js";
7
7
  import {
8
8
  readTableColumn
9
9
  } from "./chunk-IUAAXXNO.js";
@@ -29,6 +29,10 @@ import {
29
29
  keptSlugFor,
30
30
  resolveBaseItem
31
31
  } from "./chunk-CM5CWO4K.js";
32
+ import {
33
+ bucketOf,
34
+ memoByRevision
35
+ } from "./chunk-4J6DS3EJ.js";
32
36
  import {
33
37
  bareEntitySlug,
34
38
  slugify,
@@ -41,29 +45,99 @@ import {
41
45
  isCanonicalDamageType
42
46
  } from "./chunk-PDJSUH24.js";
43
47
 
48
+ // src/pc/resource-equivalents.ts
49
+ var RESOURCE_EQUIVALENTS = [
50
+ { name: "Superiority Dice", ids: ["fighter:superiority-dice", "fighter-2024:superiority-dice"] },
51
+ { name: "Sorcery Points", ids: ["sorcerer:sorcery-point", "sorcerer:sorcery-points", "sorcerer-2024:sorcery-point"] },
52
+ { name: "Ki / Focus Points", ids: ["monk:ki", "monk:ki-points", "monk:focus-point", "monk:focus-points"] },
53
+ { name: "Channel Divinity (Cleric)", ids: ["cleric:channel-divinity", "cleric-2024:channel-divinity", "channel-divinity"] },
54
+ { name: "Channel Divinity (Paladin)", ids: ["paladin:channel-divinity", "paladin-2024:channel-divinity", "channel-divinity"] },
55
+ { name: "Bardic Inspiration", ids: ["bard:bardic-inspiration", "bard:bardic-die"] },
56
+ { name: "Rage", ids: ["barbarian:rage", "barbarian:rages", "barbarian-2024:rages"] },
57
+ { name: "Wild Shape", ids: ["druid:wild-shape", "druid-2024:wild-shape"] },
58
+ { name: "Soul Trinkets", ids: ["rogue:soul-trinket", "rogue-2024:soul-trinket"] }
59
+ ];
60
+ function equivalentResourceIds(id, groups = RESOURCE_EQUIVALENTS) {
61
+ const out = [];
62
+ for (const g of groups) {
63
+ if (!g.ids.includes(id)) continue;
64
+ for (const other of g.ids) if (other !== id && !out.includes(other)) out.push(other);
65
+ }
66
+ return out;
67
+ }
68
+ function ownedByClass(res, rc) {
69
+ var _a, _b;
70
+ const s = (_a = res == null ? void 0 : res.owner) == null ? void 0 : _a.source;
71
+ if (!s) return false;
72
+ if (s.kind === "class") return s.slug === ((_b = rc.entity) == null ? void 0 : _b.slug);
73
+ if (s.kind === "subclass") return !!rc.subclass && s.slug === rc.subclass.slug;
74
+ return false;
75
+ }
76
+ function payingResourceId(id, index, owner, groups = RESOURCE_EQUIVALENTS) {
77
+ const candidates = [id, ...equivalentResourceIds(id, groups)].filter((c) => index.has(c));
78
+ if (candidates.length === 0) return void 0;
79
+ if (owner) {
80
+ const own = candidates.find((c) => ownedByClass(index.get(c), owner));
81
+ if (own) return own;
82
+ }
83
+ return candidates[0];
84
+ }
85
+ function classOfSource(source, classes) {
86
+ var _a, _b;
87
+ if (source.kind === "class") return (_a = classes.find((c) => {
88
+ var _a2;
89
+ return ((_a2 = c.entity) == null ? void 0 : _a2.slug) === source.slug;
90
+ })) != null ? _a : null;
91
+ if (source.kind === "subclass") return (_b = classes.find((c) => {
92
+ var _a2;
93
+ return ((_a2 = c.subclass) == null ? void 0 : _a2.slug) === source.slug;
94
+ })) != null ? _b : null;
95
+ return null;
96
+ }
97
+ function bindFeatureConsumers(features, index, classes) {
98
+ return features.map((rf) => {
99
+ const consumes = rf.feature.consumes;
100
+ const id = consumes == null ? void 0 : consumes.resource;
101
+ if (!consumes || typeof id !== "string" || id.length === 0) return rf;
102
+ const payer = payingResourceId(id, index, classOfSource(rf.source, classes));
103
+ if (payer === void 0 || payer === id) return rf;
104
+ return { ...rf, feature: { ...rf.feature, consumes: { ...consumes, resource: payer } } };
105
+ });
106
+ }
107
+
44
108
  // src/pc/pc.pools.ts
109
+ var SORTED_OPTIONAL_FEATURES = /* @__PURE__ */ new WeakMap();
110
+ function sortedOptionalFeatures(registry) {
111
+ return memoByRevision(SORTED_OPTIONAL_FEATURES, registry, () => bucketOf(registry, "optional-feature").slice().sort((a, b) => a.name.localeCompare(b.name) || a.slug.localeCompare(b.slug)));
112
+ }
113
+ var SRD_OWNER_PREFIXES = /* @__PURE__ */ new Set(["srd-5e", "srd-2024"]);
45
114
  function levelPrereqMax(d) {
46
115
  var _a;
47
116
  return ((_a = d.prerequisites) != null ? _a : []).filter((p) => p.kind === "level").reduce((m, p) => Math.max(m, p.min), 0);
48
117
  }
49
- function resolvePool(rc, classIndex, pool, registry, index, edition) {
50
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
51
- const entity = rc.entity;
52
- const level = rc.level;
53
- const ownerBare = bareEntitySlug(entity.slug);
54
- const ownerPrefix = entity.slug.split("_")[0];
55
- const col = pool.count.column;
56
- const count = (_a = readTableColumn(entity.table, level, [col])) != null ? _a : 0;
118
+ function poolAnchorLevel(entity, pool) {
57
119
  let anchorLevel = 1;
58
120
  let columnSeen = false;
59
121
  for (let l = 1; l <= 20; l++) {
60
- const c = readTableColumn(entity.table, l, [col]);
122
+ const c = readTableColumn(entity.table, l, [pool.count.column]);
61
123
  if (c != null) columnSeen = true;
62
124
  if (c != null && c >= 1) {
63
125
  anchorLevel = l;
64
126
  break;
65
127
  }
66
128
  }
129
+ return { anchorLevel, columnSeen };
130
+ }
131
+ function resolvePool(rc, classIndex, pool, registry, index, edition) {
132
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
133
+ const entity = rc.entity;
134
+ const level = rc.level;
135
+ const ownerBare = bareEntitySlug(entity.slug);
136
+ const ownerPrefix = entity.slug.split("_")[0];
137
+ const ownEdition = SRD_OWNER_PREFIXES.has(ownerPrefix) ? entity.edition : void 0;
138
+ const col = pool.count.column;
139
+ const count = (_a = readTableColumn(entity.table, level, [col])) != null ? _a : 0;
140
+ const { anchorLevel, columnSeen } = poolAnchorLevel(entity, pool);
67
141
  if (!columnSeen) {
68
142
  warnOnce(
69
143
  `pool-column:${pool.id}`,
@@ -71,7 +145,7 @@ function resolvePool(rc, classIndex, pool, registry, index, edition) {
71
145
  );
72
146
  }
73
147
  const ft = pool.source.where.feature_type;
74
- const all = registry.search("", "optional-feature", Number.POSITIVE_INFINITY).slice().sort((a, b) => a.name.localeCompare(b.name) || a.slug.localeCompare(b.slug));
148
+ const all = sortedOptionalFeatures(registry);
75
149
  const entryOf = (slug, entity2, compendium) => ({ slug, entity: entity2, ...compendium ? { compendium } : {}, key: slug, param: null, itemBound: isItemBound(entity2) });
76
150
  const byBare = /* @__PURE__ */ new Map();
77
151
  const candidates = /* @__PURE__ */ new Map();
@@ -83,14 +157,15 @@ function resolvePool(rc, classIndex, pool, registry, index, edition) {
83
157
  if (d.feature_type !== ft) continue;
84
158
  if (!((_b = d.available_to) != null ? _b : []).some((l) => wikilinkTailSlug(l) === ownerBare)) continue;
85
159
  if (levelPrereqMax(d) > level) continue;
160
+ if (ownEdition && d.edition && d.edition !== ownEdition) continue;
86
161
  const group = candidates.get(bare);
87
162
  if (group) group.push(entry);
88
163
  else candidates.set(bare, [entry]);
89
164
  }
90
- const available = [];
165
+ const collapsed = [];
91
166
  for (const [bare, group] of candidates) {
92
167
  if (group.length === 1) {
93
- available.push(group[0]);
168
+ collapsed.push(group[0]);
94
169
  continue;
95
170
  }
96
171
  const owned = group.find((e) => e.slug.split("_")[0] === ownerPrefix);
@@ -100,7 +175,7 @@ function resolvePool(rc, classIndex, pool, registry, index, edition) {
100
175
  `Archivist: pool "${pool.id}" holds ${group.length} cross-edition entries for "${bare}" and none shares the owner's compendium prefix "${ownerPrefix}"; keeping "${group[0].slug}"`
101
176
  );
102
177
  }
103
- available.push(owned != null ? owned : group[0]);
178
+ collapsed.push(owned != null ? owned : group[0]);
104
179
  }
105
180
  const grantDecls = [
106
181
  ...(_d = (_c = rc.entity) == null ? void 0 : _c.pool_grants) != null ? _d : [],
@@ -114,6 +189,7 @@ function resolvePool(rc, classIndex, pool, registry, index, edition) {
114
189
  grantedBare.add(key);
115
190
  return true;
116
191
  });
192
+ const available = collapsed.filter((e) => !grantedBare.has(bareEntitySlug(e.slug)));
117
193
  const bySlugAvailable = new Map(available.map((e) => [e.slug, e]));
118
194
  const byBareAvailable = new Map(available.map((e) => [bareEntitySlug(e.slug), e]));
119
195
  const resolveStored = (s) => {
@@ -152,8 +228,9 @@ function resolvePool(rc, classIndex, pool, registry, index, edition) {
152
228
  const votes = /* @__PURE__ */ new Map();
153
229
  for (const e of members) {
154
230
  const id = (_h = e.entity.consumes) == null ? void 0 : _h.resource;
155
- if (!id || !index.has(id)) continue;
156
- votes.set(id, ((_i = votes.get(id)) != null ? _i : 0) + 1);
231
+ const payer = id ? payingResourceId(id, index, rc) : void 0;
232
+ if (!payer) continue;
233
+ votes.set(payer, ((_i = votes.get(payer)) != null ? _i : 0) + 1);
157
234
  }
158
235
  let resource;
159
236
  let bestN = 0;
@@ -161,7 +238,7 @@ function resolvePool(rc, classIndex, pool, registry, index, edition) {
161
238
  resource = id;
162
239
  bestN = n;
163
240
  }
164
- const bind = resource ? poolRebinder(rc, pool.id, resource, index) : (e) => e;
241
+ const bind = poolRebinder(rc, pool.id, resource, index);
165
242
  return {
166
243
  id: pool.id,
167
244
  label: pool.label,
@@ -184,14 +261,20 @@ function poolRebinder(rc, poolId, vote, index) {
184
261
  return (e) => {
185
262
  const consumes = e.entity.consumes;
186
263
  const id = consumes == null ? void 0 : consumes.resource;
187
- if (!consumes || typeof id !== "string" || id.length === 0 || index.has(id) || declared.has(id)) return e;
264
+ if (!consumes || typeof id !== "string" || id.length === 0) return e;
265
+ const payer = payingResourceId(id, index, rc);
266
+ if (payer === id) return e;
267
+ const target = payer != null ? payer : vote !== void 0 && !declared.has(id) ? vote : void 0;
268
+ if (target === void 0) return e;
188
269
  const done = copies.get(e.key);
189
270
  if (done) return done;
190
- warnOnce(
191
- `pool-rebind:${poolId}:${id}`,
192
- `Archivist: pool "${poolId}" members consume "${id}", which this character does not own; bound to the pool's resource "${vote}"`
193
- );
194
- const copy = { ...e, entity: { ...e.entity, consumes: { ...consumes, resource: vote } } };
271
+ if (payer === void 0) {
272
+ warnOnce(
273
+ `pool-rebind:${poolId}:${id}`,
274
+ `Archivist: pool "${poolId}" members consume "${id}", which this character does not own; bound to the pool's resource "${target}"`
275
+ );
276
+ }
277
+ const copy = { ...e, entity: { ...e.entity, consumes: { ...consumes, resource: target } } };
195
278
  copies.set(e.key, copy);
196
279
  return copy;
197
280
  };
@@ -281,13 +364,49 @@ function rowMatches(row, it) {
281
364
  if (c.variant) return c.variant.candidates.some((l) => bareEntitySlug(linkSlug(l)) === bareEntitySlug(it.slug));
282
365
  return !!c.filter && matchesFilter(it.data, c.filter);
283
366
  }
284
- var rowInstalledIn = (row, items) => {
285
- const v = row.creates.item ? void 0 : row.creates.variant;
286
- if (!v) return items.filter((it) => rowMatches(row, it));
287
- const bares = new Set(v.candidates.map((l) => bareEntitySlug(linkSlug(l))));
288
- return items.filter((it) => bares.has(bareEntitySlug(it.slug)));
289
- };
290
- var rowInstalled = (row, registry) => rowInstalledIn(row, itemsOf(registry));
367
+ function buildItemIndex(items) {
368
+ const lcType = [], lcRarity = [];
369
+ const byLcName = /* @__PURE__ */ new Map(), bySlug = /* @__PURE__ */ new Map(), byBare = /* @__PURE__ */ new Map();
370
+ const push = (m, k, i) => {
371
+ const l = m.get(k);
372
+ if (l) l.push(i);
373
+ else m.set(k, [i]);
374
+ };
375
+ items.forEach((it, i) => {
376
+ const data = it.data;
377
+ lcType.push(lc(data.type));
378
+ lcRarity.push(lc(data.rarity));
379
+ push(byLcName, lc(it.name), i);
380
+ if (!bySlug.has(it.slug)) bySlug.set(it.slug, i);
381
+ push(byBare, bareEntitySlug(it.slug), i);
382
+ });
383
+ return { items, lcType, lcRarity, byLcName, bySlug, byBare };
384
+ }
385
+ var ITEM_INDEX = /* @__PURE__ */ new WeakMap();
386
+ var itemIndexOf = (registry) => memoByRevision(ITEM_INDEX, registry, () => buildItemIndex(itemsOf(registry)));
387
+ var inBucketOrder = (ix, hits) => [...hits].sort((a, b) => a - b).map((i) => ix.items[i]);
388
+ function rowInstalledIn(row, ix) {
389
+ var _a, _b, _c;
390
+ const c = row.creates;
391
+ if (c.item) {
392
+ const hits = new Set((_a = ix.byLcName.get(lc(row.name))) != null ? _a : []);
393
+ const own = ix.bySlug.get(linkSlug(c.item));
394
+ if (own !== void 0) hits.add(own);
395
+ return inBucketOrder(ix, hits);
396
+ }
397
+ if (c.variant) {
398
+ const hits = /* @__PURE__ */ new Set();
399
+ for (const l of c.variant.candidates) for (const i of (_b = ix.byBare.get(bareEntitySlug(linkSlug(l)))) != null ? _b : []) hits.add(i);
400
+ return inBucketOrder(ix, hits);
401
+ }
402
+ const f = c.filter;
403
+ if (!f) return [];
404
+ const rarities = new Set(f.rarity.map(lc));
405
+ const types = f.types ? new Set(f.types.map(lc)) : null;
406
+ const excluded = new Set(((_c = f.exclude_types) != null ? _c : []).map(lc));
407
+ return ix.items.filter((it, i) => rarities.has(ix.lcRarity[i]) && (!types || types.has(ix.lcType[i])) && !excluded.has(ix.lcType[i]) && !(f.cursed === false && it.data.cursed === true));
408
+ }
409
+ var rowInstalled = (row, registry) => rowInstalledIn(row, itemIndexOf(registry));
291
410
  function candidateOrder(edition) {
292
411
  return (a, b) => {
293
412
  const ea = edition && lc(a.data.edition) === edition ? 0 : 1;
@@ -321,9 +440,10 @@ function candidatesFrom(installed, registry, opts) {
321
440
  }
322
441
  function resolveOptionRows(note, registry, opts = {}) {
323
442
  var _a;
324
- const items = itemsOf(registry);
443
+ const ix = itemIndexOf(registry);
325
444
  return ((_a = note.options) != null ? _a : []).map((row) => {
326
- const rowItems = rowInstalledIn(row, items);
445
+ var _a2;
446
+ const rowItems = rowInstalledIn(row, ix);
327
447
  const candidates = candidatesFrom(rowItems, registry, opts);
328
448
  const anyVisible = candidates.length > 0 || candidatesFrom(rowItems, registry, { ...opts, exclude: [] }).length > 0;
329
449
  const installed = rowItems.length > 0;
@@ -334,10 +454,23 @@ function resolveOptionRows(note, registry, opts = {}) {
334
454
  ...row.attunement !== void 0 ? { attunement: row.attunement } : {},
335
455
  arm: row.creates.item ? "item" : row.creates.variant ? "variant" : "filter",
336
456
  candidates,
337
- availability: anyVisible ? "installed" : installed ? "hidden-only" : "not-installed"
457
+ availability: anyVisible ? "installed" : installed ? "hidden-only" : "not-installed",
458
+ ...rowItemWords(row, candidates[0] ? ix.items[(_a2 = ix.bySlug.get(candidates[0])) != null ? _a2 : -1] : rowItems[0])
338
459
  };
339
460
  });
340
461
  }
462
+ function rowItemWords(row, item) {
463
+ var _a, _b, _c, _d;
464
+ const f = row.creates.filter;
465
+ if (f) return { type: ((_a = f.types) == null ? void 0 : _a[0]) ? lc(f.types[0]) : null, rarity: f.rarity[0] ? lc(f.rarity[0]) : null, charges: null };
466
+ const data = (_b = item == null ? void 0 : item.data) != null ? _b : {};
467
+ const max = (_c = data.charges) == null ? void 0 : _c.max;
468
+ return {
469
+ type: lc(data.type) || null,
470
+ rarity: lc(data.rarity) || null,
471
+ charges: typeof max === "number" && max > 0 ? { max, ...typeof ((_d = data.charges) == null ? void 0 : _d.recharge) === "string" ? { recharge: data.charges.recharge } : {} } : null
472
+ };
473
+ }
341
474
  function resolveCopyOption(note, param, ctx) {
342
475
  var _a, _b, _c;
343
476
  const rows = (_a = note.options) != null ? _a : [];
@@ -920,7 +1053,9 @@ export {
920
1053
  infusionsEndedByClassEdit,
921
1054
  reconcileClassEdit,
922
1055
  eligibleTargets,
1056
+ bindFeatureConsumers,
923
1057
  levelPrereqMax,
1058
+ poolAnchorLevel,
924
1059
  resolvePool,
925
1060
  resolveAllPools,
926
1061
  strandedPicks
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseSubclass
3
- } from "./chunk-5KGV6TF7.js";
3
+ } from "./chunk-UNYR5GLI.js";
4
4
 
5
5
  // src/subclass/subclass.codec.ts
6
6
  import yaml from "js-yaml";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  featureEffectSchema
3
- } from "./chunk-ZMJQUZQH.js";
3
+ } from "./chunk-JXWNXIIZ.js";
4
4
 
5
5
  // src/schemas/choice-schema.ts
6
6
  import { z } from "zod";
@@ -16,7 +16,9 @@ var entityFilterSchema = z.object({
16
16
  mundane: z.boolean().optional(),
17
17
  list: z.string().min(1).optional(),
18
18
  level: z.number().int().min(0).max(9).optional(),
19
- edition: z.string().min(1).optional()
19
+ edition: z.string().min(1).optional(),
20
+ // D2b-A (design §2.2 step 1): Pact of the Tome's "level 1 ritual spells".
21
+ ritual: z.boolean().optional()
20
22
  }).strict();
21
23
  var inlineOptionSchema = z.lazy(() => z.object({
22
24
  value: z.string().min(1),
@@ -40,7 +42,9 @@ var choiceSchema = z.lazy(() => z.discriminatedUnion("kind", [
40
42
  count: z.number().int().positive().optional(),
41
43
  entity_type: z.string().min(1),
42
44
  from: z.array(z.string().min(1)).nonempty().optional(),
43
- where: entityFilterSchema.optional()
45
+ where: entityFilterSchema.optional(),
46
+ // D2b-A: a pick that names a target (a blast's cantrip), never a grant.
47
+ target: z.literal(true).optional()
44
48
  }).strict(),
45
49
  z.object({
46
50
  kind: z.literal("select-proficiency"),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseSpell
3
- } from "./chunk-XY6X2APM.js";
3
+ } from "./chunk-7Q2VPKCD.js";
4
4
 
5
5
  // src/spell/spell.codec.ts
6
6
  import yaml from "js-yaml";
@@ -9,7 +9,8 @@ var spellCodec = {
9
9
  return parseSpell(doc.body);
10
10
  },
11
11
  serialize(spell) {
12
- return yaml.dump(spell, { lineWidth: -1, sortKeys: false });
12
+ const { material: _material, ...doc } = spell;
13
+ return yaml.dump(doc, { lineWidth: -1, sortKeys: false });
13
14
  }
14
15
  };
15
16
 
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-KU3YLF6D.js";
7
7
  import {
8
8
  toProfSlug
9
- } from "./chunk-RLGQ5PZZ.js";
9
+ } from "./chunk-5JXFJ5AU.js";
10
10
 
11
11
  // src/pc/pc.tool-picks.ts
12
12
  var TOOL_CATEGORIES = {
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-IN4M4X7P.js";
4
4
  import {
5
5
  choiceSchema
6
- } from "./chunk-BRGLDWUP.js";
6
+ } from "./chunk-OOL4KMG2.js";
7
7
  import {
8
8
  additionalSpellsEntrySchema,
9
9
  imageField