@archivist-gg/dnd5e 0.1.1 → 0.3.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 (87) 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-353WXS6S.js +25 -0
  6. package/dist/{chunk-FUK7QSN5.js → chunk-42Y5IJKT.js} +1 -1
  7. package/dist/{chunk-TPZSPU4L.js → chunk-4JHOHWBI.js} +1 -1
  8. package/dist/{chunk-DC74TZNA.js → chunk-4LZJXJJS.js} +7 -2
  9. package/dist/chunk-5MZSB6OM.js +1089 -0
  10. package/dist/{chunk-FEOB24KP.js → chunk-73HOFQQ5.js} +1 -1
  11. package/dist/{chunk-YDPWD2T7.js → chunk-7VJ6BOA5.js} +4 -1
  12. package/dist/{chunk-ZLG6FQFB.js → chunk-A4WENOUQ.js} +21 -2
  13. package/dist/{chunk-GEJSJP6E.js → chunk-AIRFC5NQ.js} +3 -1
  14. package/dist/{chunk-DABBCYTT.js → chunk-CZPMTQOY.js} +1 -1
  15. package/dist/{chunk-DRNZJNJP.js → chunk-DZXSWSBP.js} +1 -1
  16. package/dist/{chunk-ADLTSHOR.js → chunk-EMOORSQ7.js} +59 -30
  17. package/dist/{chunk-RVYYKYFL.js → chunk-GE4TLPWM.js} +1 -1
  18. package/dist/{chunk-VLKIB33F.js → chunk-K6Q5OUGK.js} +6 -0
  19. package/dist/{chunk-PMADYMUI.js → chunk-L7E2UT5A.js} +1 -1
  20. package/dist/{chunk-3TRGRSCH.js → chunk-LV4SNWZQ.js} +7 -1
  21. package/dist/{chunk-IXZXFFFW.js → chunk-M2EMFD6S.js} +1 -1
  22. package/dist/{chunk-UC4FE5X3.js → chunk-N6N74K6X.js} +1 -1
  23. package/dist/{chunk-CDW7R2QT.js → chunk-NXSWH3AE.js} +4 -1
  24. package/dist/{chunk-IDTDACM6.js → chunk-PHQRGDRQ.js} +1 -1
  25. package/dist/{chunk-BAXWWGFJ.js → chunk-Q5K6LWBZ.js} +1 -1
  26. package/dist/{chunk-Z6GXV2GO.js → chunk-TLYDBT5F.js} +1 -1
  27. package/dist/{chunk-A7T5L7SS.js → chunk-TUIFSVLI.js} +1 -1
  28. package/dist/{chunk-J2RYSZMQ.js → chunk-VTTA2ALZ.js} +2 -2
  29. package/dist/{chunk-Z4TL2V7S.js → chunk-VTYUBINW.js} +1 -1
  30. package/dist/{chunk-B7UXKNLD.js → chunk-W7HWMMBF.js} +1 -1
  31. package/dist/{chunk-SSXBYF3P.js → chunk-XLSVNUTK.js} +1 -1
  32. package/dist/{chunk-GIOH5JB5.js → chunk-XTIQWSFM.js} +1 -1
  33. package/dist/{chunk-PKC77VZP.js → chunk-ZZGEVJKX.js} +1 -1
  34. package/dist/class/class.codec.js +5 -5
  35. package/dist/class/class.parser.js +4 -4
  36. package/dist/class/class.schema.js +3 -3
  37. package/dist/class/class.slug.d.ts +7 -5
  38. package/dist/class/class.slug.js +1 -1
  39. package/dist/entities/base-item-resolver.d.ts +9 -0
  40. package/dist/entities/base-item-resolver.js +6 -4
  41. package/dist/entities/slug.d.ts +7 -0
  42. package/dist/entities/slug.js +3 -1
  43. package/dist/feat/feat.codec.js +4 -4
  44. package/dist/feat/feat.parser.js +3 -3
  45. package/dist/feat/feat.schema.js +2 -2
  46. package/dist/index.js +4116 -3662
  47. package/dist/item/item.actions-map.d.ts +18 -10
  48. package/dist/item/item.actions-map.js +16 -12
  49. package/dist/item/item.codec.js +3 -3
  50. package/dist/item/item.parser.js +2 -2
  51. package/dist/item/item.schema.d.ts +3 -0
  52. package/dist/item/item.schema.js +1 -1
  53. package/dist/item/item.types.d.ts +8 -0
  54. package/dist/pc/pc.decision-engine.d.ts +0 -3
  55. package/dist/pc/pc.decision-engine.js +8 -3
  56. package/dist/pc/pc.equipment.js +5 -8
  57. package/dist/pc/pc.feature-effects.d.ts +5 -0
  58. package/dist/pc/pc.feature-effects.js +2 -4
  59. package/dist/pc/pc.parser.js +1 -1
  60. package/dist/pc/pc.pools.js +8 -3
  61. package/dist/pc/pc.recalc.d.ts +7 -0
  62. package/dist/pc/pc.recalc.js +94 -40
  63. package/dist/pc/pc.resolver.d.ts +59 -1
  64. package/dist/pc/pc.resolver.js +21 -343
  65. package/dist/pc/pc.rest.d.ts +1 -1
  66. package/dist/pc/pc.rest.js +20 -11
  67. package/dist/pc/pc.schema.d.ts +23 -0
  68. package/dist/pc/pc.schema.js +1 -1
  69. package/dist/pc/pc.slotting.js +3 -3
  70. package/dist/pc/pc.spellcasting.js +2 -2
  71. package/dist/pc/pc.types.d.ts +65 -5
  72. package/dist/race/race.codec.js +5 -5
  73. package/dist/race/race.parser.js +4 -4
  74. package/dist/race/race.schema.js +3 -3
  75. package/dist/schemas/choice-schema.js +1 -1
  76. package/dist/schemas/feature-schema.js +2 -2
  77. package/dist/spell/spell.access.js +1 -1
  78. package/dist/subclass/subclass.codec.js +5 -5
  79. package/dist/subclass/subclass.parser.js +4 -4
  80. package/dist/subclass/subclass.schema.js +3 -3
  81. package/dist/types/choice.d.ts +5 -0
  82. package/dist/weapon/weapon-mastery.d.ts +3 -0
  83. package/dist/weapon/weapon.classify.d.ts +21 -0
  84. package/dist/weapon/weapon.classify.js +7 -0
  85. package/package.json +1 -1
  86. package/dist/chunk-6PXE7T7C.js +0 -79
  87. package/dist/chunk-TJCNX6UZ.js +0 -521
@@ -0,0 +1,1089 @@
1
+ import {
2
+ ALL_SKILL_SLUGS
3
+ } from "./chunk-BH6AMZVY.js";
4
+ import {
5
+ normalizeKnownSpell,
6
+ resolveSpellcasting
7
+ } from "./chunk-M2EMFD6S.js";
8
+ import {
9
+ readTableColumn
10
+ } from "./chunk-5XUXXAQJ.js";
11
+ import {
12
+ recognizeDecision
13
+ } from "./chunk-JEYQU3JT.js";
14
+ import {
15
+ isItemEntity,
16
+ resolveEntityForEntry
17
+ } from "./chunk-N6N74K6X.js";
18
+ import {
19
+ bareEntitySlug
20
+ } from "./chunk-LV4SNWZQ.js";
21
+ import {
22
+ ABILITY_KEYS
23
+ } from "./chunk-WP2CX2JO.js";
24
+
25
+ // src/pc/pc.resolver.ts
26
+ var SLUG_RE = /^\[\[(.+?)\]\]$/;
27
+ function stripSlug(ref) {
28
+ if (!ref) return null;
29
+ const m = ref.match(SLUG_RE);
30
+ return m ? m[1] : ref;
31
+ }
32
+ function resolveOriginFeat(entities, originFeatRef) {
33
+ var _a, _b, _c;
34
+ if (!originFeatRef) return null;
35
+ const slug = wikilinkTailSlug(originFeatRef);
36
+ const feats = entities.search("", "feat", Number.POSITIVE_INFINITY);
37
+ const lookup = (s) => {
38
+ var _a2;
39
+ return (_a2 = feats.find((f) => f.slug === s)) != null ? _a2 : feats.find((f) => f.slug.endsWith(`_${s}`));
40
+ };
41
+ let reg = lookup(slug);
42
+ let variantName;
43
+ if (!reg) {
44
+ const rawTail = (_b = (_a = originFeatRef.replace(/^\[\[/, "").replace(/\]\]$/, "").split("/").pop()) == null ? void 0 : _a.trim()) != null ? _b : "";
45
+ const base = rawTail.replace(/\s*\([^()]*\)\s*$/, "").trim();
46
+ if (base && base !== rawTail) {
47
+ const baseReg = lookup(wikilinkTailSlug(`[[${base}]]`));
48
+ if (baseReg) {
49
+ reg = baseReg;
50
+ variantName = rawTail;
51
+ }
52
+ }
53
+ }
54
+ if (!reg) return null;
55
+ const data = reg.data;
56
+ const feat = data.slug == null ? { ...data, slug: reg.slug } : data;
57
+ return { feat, display: (_c = variantName != null ? variantName : feat.name) != null ? _c : slug };
58
+ }
59
+ var PCResolver = class {
60
+ constructor(entities) {
61
+ this.entities = entities;
62
+ }
63
+ /** Exposed so recalc() can apply equipment bonuses (Pass A / Pass B). */
64
+ get registry() {
65
+ return this.entities;
66
+ }
67
+ resolve(character) {
68
+ var _a, _b, _c, _d, _e, _f, _g, _h;
69
+ const warnings = [];
70
+ const lookup = (rawRef, type) => {
71
+ const slug = stripSlug(rawRef);
72
+ if (!slug) return null;
73
+ const reg = this.entities.getByTypeAndSlug(type, slug);
74
+ if (!reg) {
75
+ warnings.push(`Slug [[${slug}]] not found in compendium as ${type}.`);
76
+ return null;
77
+ }
78
+ const data = reg.data;
79
+ return data.slug == null ? { ...data, slug: reg.slug } : data;
80
+ };
81
+ const race = lookup(character.race, "race");
82
+ const background = lookup(character.background, "background");
83
+ const classes = character.class.map((c) => ({
84
+ entity: lookup(c.name, "class"),
85
+ level: c.level,
86
+ subclass: lookup(c.subclass, "subclass"),
87
+ choices: c.choices
88
+ }));
89
+ const featSlugs = collectFeatSlugs(character);
90
+ const feats = [];
91
+ for (const slug of featSlugs) {
92
+ const f = lookup(`[[${slug}]]`, "feat");
93
+ if (f) feats.push(f);
94
+ }
95
+ const originFeat = (background == null ? void 0 : background.origin_feat) ? resolveOriginFeat(this.entities, background.origin_feat) : null;
96
+ if (originFeat && !feats.some((f) => f.slug === originFeat.feat.slug)) {
97
+ feats.push(originFeat.feat);
98
+ }
99
+ const totalLevel = classes.reduce((sum, c) => sum + c.level, 0);
100
+ const features = collectResolvedFeatures(race, classes, background, feats);
101
+ const extraFeatures = collectChosenGrantedFeatures(character, classes, this.entities, race, background);
102
+ features.push(...extraFeatures);
103
+ const primaryCasterSlug = (_a = classes.map((c, i) => c.entity && resolveSpellcasting(c) ? stripSlug(character.class[i].name) : null).find((slug) => slug != null)) != null ? _a : null;
104
+ const spells = [];
105
+ for (const raw of (_b = character.spells.known) != null ? _b : []) {
106
+ const n = normalizeKnownSpell(raw);
107
+ const reg = this.entities.getByTypeAndSlug("spell", n.slug);
108
+ if (!reg) {
109
+ warnings.push(`Spell [[${n.slug}]] not found in compendium.`);
110
+ continue;
111
+ }
112
+ const entity = reg.data;
113
+ const isCantrip = ((_c = entity.level) != null ? _c : 0) === 0;
114
+ const classSlug = (_d = n.classSlug) != null ? _d : primaryCasterSlug;
115
+ const prep = isCantrip || n.alwaysPrepared ? true : (_e = n.preparedFlag) != null ? _e : false;
116
+ spells.push({ entity, slug: n.slug, classSlug, source: n.source, prepared: prep, alwaysPrepared: n.alwaysPrepared });
117
+ }
118
+ const oc = (_f = character.origin_choices) != null ? _f : {};
119
+ if (originFeat) {
120
+ const read = (childId) => oc[`background:feat:${childId}`];
121
+ spells.push(...collectFeatGrantedSpells(originFeat.feat, read, this.entities, warnings));
122
+ }
123
+ for (const entry of character.class) {
124
+ for (const atLevel of Object.values((_g = entry.choices) != null ? _g : {})) {
125
+ const featRef = atLevel.feat;
126
+ if (typeof featRef !== "string") continue;
127
+ const slug = stripSlug(featRef);
128
+ if (!slug) continue;
129
+ const reg = this.entities.getByTypeAndSlug("feat", slug);
130
+ if (!reg) continue;
131
+ const feat = reg.data;
132
+ const read = (childId) => atLevel[`feat:${childId}`];
133
+ spells.push(...collectFeatGrantedSpells(feat, read, this.entities, warnings));
134
+ }
135
+ }
136
+ const ownSpellcastingAbility = (_h = classes.map((c) => {
137
+ var _a2, _b2;
138
+ return (_b2 = (_a2 = resolveSpellcasting(c)) == null ? void 0 : _a2.ability) != null ? _b2 : null;
139
+ }).find((a) => a != null)) != null ? _h : null;
140
+ spells.push(...collectItemGrantedSpells(character, ownSpellcastingAbility, this.entities, warnings));
141
+ const chosenMasteries = collectChosenWeaponMasteries(character);
142
+ if (chosenMasteries.full.length > 0) {
143
+ const target = features.find(
144
+ (rf) => {
145
+ var _a2;
146
+ return rf.feature.id === "weapon-mastery" || ((_a2 = rf.feature.choices) == null ? void 0 : _a2.some(
147
+ (ch) => ch.kind === "select-entity" && ch.id === "weapon-mastery" && ch.entity_type === "weapon"
148
+ ));
149
+ }
150
+ );
151
+ if (target) {
152
+ const names = chosenMasteries.full.map(
153
+ (fullSlug) => {
154
+ var _a2, _b2;
155
+ return (_b2 = (_a2 = this.entities.getByTypeAndSlug("weapon", fullSlug)) == null ? void 0 : _a2.name) != null ? _b2 : fullSlug;
156
+ }
157
+ );
158
+ const entry = { label: "Mastered weapons", description: names.join(", ") };
159
+ target.chosenInline = target.chosenInline ? [...target.chosenInline, entry] : [entry];
160
+ }
161
+ }
162
+ const resolvedCharacter = {
163
+ definition: character,
164
+ race,
165
+ classes,
166
+ background,
167
+ feats,
168
+ totalLevel,
169
+ features,
170
+ spells: dedupeResolvedSpells(spells),
171
+ pools: [],
172
+ weaponMasteries: chosenMasteries.bare,
173
+ state: character.state
174
+ };
175
+ resolvedCharacter.pools = resolveAllPools(resolvedCharacter, this.entities);
176
+ return { character: resolvedCharacter, warnings };
177
+ }
178
+ };
179
+ function collectChosenWeaponMasteries(character) {
180
+ var _a, _b;
181
+ const full = /* @__PURE__ */ new Set();
182
+ for (const cls of (_a = character.class) != null ? _a : []) {
183
+ for (const lvl of Object.values((_b = cls.choices) != null ? _b : {})) {
184
+ const pick = lvl["weapon-mastery"];
185
+ if (Array.isArray(pick)) {
186
+ for (const s of pick) if (typeof s === "string") full.add(s);
187
+ }
188
+ }
189
+ }
190
+ return { full: [...full], bare: [...new Set([...full].map(bareEntitySlug))] };
191
+ }
192
+ function collectFeatSlugs(character) {
193
+ const slugs = /* @__PURE__ */ new Set();
194
+ for (const c of character.class) {
195
+ for (const [, choiceBlock] of Object.entries(c.choices)) {
196
+ const feat = choiceBlock == null ? void 0 : choiceBlock.feat;
197
+ if (typeof feat === "string") {
198
+ const s = stripSlug(feat);
199
+ if (s) slugs.add(s);
200
+ }
201
+ }
202
+ }
203
+ return [...slugs];
204
+ }
205
+ function collectFeatGrantedSpells(feat, read, entities, warnings) {
206
+ var _a;
207
+ const ability = readChosenSpellAbility(read);
208
+ const slugs = [];
209
+ collectSpellPickSlugs(feat.choices, read, slugs);
210
+ const out = [];
211
+ for (const rawSlug of slugs) {
212
+ const slug = (_a = stripSlug(rawSlug)) != null ? _a : rawSlug;
213
+ const reg = entities.getByTypeAndSlug("spell", slug);
214
+ if (!reg) {
215
+ warnings.push(`Feat spell [[${slug}]] not found in compendium.`);
216
+ continue;
217
+ }
218
+ const entity = reg.data;
219
+ out.push({ entity, slug, classSlug: null, source: "feat", prepared: true, alwaysPrepared: true, ability });
220
+ }
221
+ return out;
222
+ }
223
+ function collectItemGrantedSpells(character, ownAbility, entities, warnings) {
224
+ var _a;
225
+ const out = [];
226
+ ((_a = character.equipment) != null ? _a : []).forEach((entry, entryIndex) => {
227
+ var _a2, _b, _c, _d, _e, _f;
228
+ const spellRef = (_a2 = entry.overrides) == null ? void 0 : _a2.spell;
229
+ if (!spellRef) return;
230
+ const { entity: itemEntity } = resolveEntityForEntry(entry.item, entities);
231
+ const scrollLevel = isItemEntity(itemEntity) ? itemEntity.scroll_level : void 0;
232
+ if (scrollLevel == null) return;
233
+ const slug = (_b = stripSlug(spellRef)) != null ? _b : spellRef;
234
+ const reg = entities.getByTypeAndSlug("spell", slug);
235
+ if (!reg) {
236
+ warnings.push(`Scroll spell [[${slug}]] not found in compendium.`);
237
+ return;
238
+ }
239
+ const entity = reg.data;
240
+ const ability = (_f = (_d = (_c = entry.overrides) == null ? void 0 : _c.spell_ability) != null ? _d : ownAbility) != null ? _f : (_e = character.overrides) == null ? void 0 : _e.spellcasting_ability;
241
+ out.push({ entity, slug, classSlug: null, source: "item", prepared: true, alwaysPrepared: true, ability, entryIndex });
242
+ });
243
+ return out;
244
+ }
245
+ function dedupeResolvedSpells(spells) {
246
+ const byKey = /* @__PURE__ */ new Map();
247
+ for (const s of spells) {
248
+ const key = s.source === "item" ? `${s.slug}#${s.entryIndex}` : s.slug;
249
+ const existing = byKey.get(key);
250
+ if (!existing) {
251
+ byKey.set(key, s);
252
+ continue;
253
+ }
254
+ if (existing.source === "feat" && s.source !== "feat") byKey.set(key, s);
255
+ }
256
+ return [...byKey.values()];
257
+ }
258
+ function readChosenSpellAbility(read) {
259
+ const pick = read("spellcasting-ability");
260
+ return typeof pick === "string" && ABILITY_KEYS.includes(pick) ? pick : null;
261
+ }
262
+ function collectSpellPickSlugs(choices, read, out) {
263
+ for (const ch of choices != null ? choices : []) {
264
+ if (ch.kind === "select-entity" && ch.entity_type === "spell") {
265
+ const sel = read(ch.id);
266
+ const picks = Array.isArray(sel) ? sel : typeof sel === "string" ? [sel] : [];
267
+ for (const p of picks) if (typeof p === "string" && p.length > 0) out.push(p);
268
+ } else if (ch.kind === "select-inline") {
269
+ const sel = read(ch.id);
270
+ const branch = typeof sel === "string" ? ch.options.find((o) => o.value === sel) : void 0;
271
+ if (branch == null ? void 0 : branch.choices) collectSpellPickSlugs(branch.choices, read, out);
272
+ }
273
+ }
274
+ }
275
+ function collectChosenGrantedFeatures(character, classes, registry, race = null, background = null) {
276
+ var _a, _b;
277
+ const out = [];
278
+ classes.forEach((c, i) => {
279
+ var _a2, _b2, _c, _d;
280
+ if (!c.entity) return;
281
+ const entity = c.entity;
282
+ const entry = character.class[i];
283
+ if (!entry) return;
284
+ for (const [lvlStr, atLevel] of Object.entries(entry.choices)) {
285
+ const lvl = Number(lvlStr);
286
+ if (!Number.isFinite(lvl) || lvl > entry.level) continue;
287
+ const features = (_b2 = ((_a2 = entity.features_by_level) != null ? _a2 : {})[lvl]) != null ? _b2 : [];
288
+ const subFeatures = c.subclass ? (_d = ((_c = c.subclass.features_by_level) != null ? _c : {})[lvl]) != null ? _d : [] : [];
289
+ for (const feature of [...features, ...subFeatures]) {
290
+ walkChoiceGrants(feature.choices, atLevel, (granted, suppress) => {
291
+ out.push({
292
+ feature: granted,
293
+ source: { kind: "class", slug: entity.slug, level: lvl },
294
+ ...suppress ? { renderSuppressed: true } : {}
295
+ });
296
+ }, registry);
297
+ }
298
+ }
299
+ });
300
+ const oc = (_a = character.origin_choices) != null ? _a : {};
301
+ const originAt = (ns) => {
302
+ const at = {};
303
+ for (const [key, val] of Object.entries(oc)) {
304
+ if (key.startsWith(`${ns}:`)) at[key.slice(ns.length + 1)] = val;
305
+ }
306
+ return at;
307
+ };
308
+ if (race) {
309
+ const atRace = originAt("race");
310
+ const emitRace = (granted, suppress) => {
311
+ out.push({
312
+ feature: granted,
313
+ source: { kind: "race", slug: race.slug },
314
+ ...suppress ? { renderSuppressed: true } : {}
315
+ });
316
+ };
317
+ walkChoiceGrants(race.choices, atRace, emitRace, registry);
318
+ for (const trait of (_b = race.traits) != null ? _b : []) {
319
+ walkChoiceGrants(trait.choices, atRace, emitRace, registry);
320
+ }
321
+ }
322
+ if (background) {
323
+ const atBg = originAt("background");
324
+ const emitBg = (granted, suppress) => {
325
+ out.push({
326
+ feature: granted,
327
+ source: { kind: "background", slug: background.slug },
328
+ ...suppress ? { renderSuppressed: true } : {}
329
+ });
330
+ };
331
+ walkChoiceGrants(background.choices, atBg, emitBg, registry);
332
+ if (background.feature) {
333
+ walkChoiceGrants(background.feature.choices, atBg, emitBg, registry);
334
+ }
335
+ }
336
+ return out;
337
+ }
338
+ function walkChoiceGrants(choices, atLevel, emit, registry) {
339
+ var _a, _b, _c, _d, _e;
340
+ for (const ch of choices != null ? choices : []) {
341
+ const sel = atLevel[ch.id];
342
+ if (ch.kind === "select-entity" && ch.entity_type === "optional-feature") {
343
+ const slugs = Array.isArray(sel) ? sel : typeof sel === "string" ? [sel] : [];
344
+ for (const slug of slugs) {
345
+ const reg = registry.getByTypeAndSlug("optional-feature", (_a = stripSlug(String(slug))) != null ? _a : String(slug));
346
+ if (!reg) continue;
347
+ const d = reg.data;
348
+ emit({
349
+ id: (_b = d.slug) != null ? _b : String(slug),
350
+ name: (_c = d.name) != null ? _c : String(slug),
351
+ description: d.description,
352
+ effects: d.effects,
353
+ ...d.action_cost ? { action: d.action_cost } : {}
354
+ });
355
+ }
356
+ }
357
+ if (ch.kind === "select-inline") {
358
+ const branch = typeof sel === "string" ? ch.options.find((o) => o.value === sel) : void 0;
359
+ if ((_d = branch == null ? void 0 : branch.effects) == null ? void 0 : _d.length) {
360
+ const wc = (_e = branch.choices) == null ? void 0 : _e.find(
361
+ (c) => c.kind === "select-entity" && c.entity_type === "weapon"
362
+ );
363
+ const picked = wc ? atLevel == null ? void 0 : atLevel[wc.id] : void 0;
364
+ const effects = branch.effects.map(
365
+ (e) => e.kind === "weapon-ability" && e.weapons === "chosen" && typeof picked === "string" ? { ...e, weapons: [bareEntitySlug(picked)] } : e
366
+ );
367
+ emit({ id: `${ch.id}-${branch.value}`, name: branch.label, description: branch.description, effects }, true);
368
+ }
369
+ if (branch == null ? void 0 : branch.choices) walkChoiceGrants(branch.choices, atLevel, emit, registry);
370
+ }
371
+ }
372
+ }
373
+ function resolveChosenInline(feature, atLevel) {
374
+ var _a;
375
+ if (!atLevel) return void 0;
376
+ const picks = [];
377
+ for (const ch of (_a = feature.choices) != null ? _a : []) {
378
+ if (ch.kind !== "select-inline") continue;
379
+ const sel = atLevel[ch.id];
380
+ if (typeof sel !== "string") continue;
381
+ const opt = ch.options.find((o) => o.value === sel);
382
+ if (!opt) continue;
383
+ picks.push({ label: opt.label, description: opt.description });
384
+ }
385
+ return picks.length ? picks : void 0;
386
+ }
387
+ function isAsiSlotFeature(feature) {
388
+ var _a, _b, _c, _d;
389
+ return feature.id === "ability-score-improvement" && !((_a = feature.effects) == null ? void 0 : _a.length) && !((_b = feature.resources) == null ? void 0 : _b.length) && !feature.action && !((_c = feature.sub_features) == null ? void 0 : _c.length) && !((_d = feature.attacks) == null ? void 0 : _d.length);
390
+ }
391
+ function collectResolvedFeatures(race, classes, background, feats) {
392
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
393
+ const out = [];
394
+ for (const c of classes) {
395
+ if (!c.entity) continue;
396
+ const slug = c.entity.slug;
397
+ const byLevel = (_a = c.entity.features_by_level) != null ? _a : {};
398
+ for (const [lvlStr, feats0] of Object.entries(byLevel)) {
399
+ const lvl = parseInt(lvlStr, 10);
400
+ if (Number.isNaN(lvl) || lvl > c.level) continue;
401
+ for (const feat of feats0) {
402
+ const chosenInline = resolveChosenInline(feat, (_b = c.choices) == null ? void 0 : _b[lvl]);
403
+ out.push({
404
+ feature: feat,
405
+ source: { kind: "class", slug, level: lvl },
406
+ ...chosenInline ? { chosenInline } : {},
407
+ ...isAsiSlotFeature(feat) ? { buildOnly: true } : {}
408
+ });
409
+ }
410
+ }
411
+ if ((_c = c.entity.resources) == null ? void 0 : _c.length) {
412
+ out.push({
413
+ feature: { name: c.entity.name, resources: c.entity.resources },
414
+ source: { kind: "class", slug, level: 1 }
415
+ });
416
+ }
417
+ if (c.subclass) {
418
+ const sSlug = c.subclass.slug;
419
+ const sByLevel = (_d = c.subclass.features_by_level) != null ? _d : {};
420
+ for (const [lvlStr, feats0] of Object.entries(sByLevel)) {
421
+ const lvl = parseInt(lvlStr, 10);
422
+ if (Number.isNaN(lvl) || lvl > c.level) continue;
423
+ for (const feat of feats0) {
424
+ const chosenInline = resolveChosenInline(feat, (_e = c.choices) == null ? void 0 : _e[lvl]);
425
+ out.push({
426
+ feature: feat,
427
+ source: { kind: "subclass", slug: sSlug, level: lvl },
428
+ ...chosenInline ? { chosenInline } : {},
429
+ ...isAsiSlotFeature(feat) ? { buildOnly: true } : {}
430
+ });
431
+ }
432
+ }
433
+ if ((_f = c.subclass.resources) == null ? void 0 : _f.length) {
434
+ out.push({
435
+ feature: { name: c.subclass.name, resources: c.subclass.resources },
436
+ source: { kind: "subclass", slug: sSlug, level: 1 }
437
+ });
438
+ }
439
+ }
440
+ }
441
+ if (race) {
442
+ const traits = (_g = race.traits) != null ? _g : [];
443
+ for (const feat of traits) {
444
+ out.push({ feature: feat, source: { kind: "race", slug: race.slug } });
445
+ }
446
+ }
447
+ if (background) {
448
+ const bgFeature = background.feature;
449
+ if (bgFeature) {
450
+ out.push({ feature: bgFeature, source: { kind: "background", slug: background.slug } });
451
+ }
452
+ }
453
+ for (const feat of feats) {
454
+ const bundled = (_h = feat.features) != null ? _h : [];
455
+ const entityEffects = (_i = feat.effects) != null ? _i : [];
456
+ if (bundled.length > 0) {
457
+ bundled.forEach((f, i) => {
458
+ var _a2;
459
+ const feature = i === 0 && entityEffects.length > 0 ? { ...f, effects: [...(_a2 = f.effects) != null ? _a2 : [], ...entityEffects] } : f;
460
+ out.push({ feature, source: { kind: "feat", slug: feat.slug } });
461
+ });
462
+ } else {
463
+ const name = (_j = feat.name) != null ? _j : feat.slug;
464
+ const foldedDesc = [feat.description, ...(_k = feat.benefits) != null ? _k : []].filter(Boolean).join("\n\n") || void 0;
465
+ const buildOnly = ((_l = feat.choices) != null ? _l : []).some((c) => c.kind === "ability-points" && c.points >= 2) && !((_m = feat.effects) == null ? void 0 : _m.length) && !((_n = feat.resources) == null ? void 0 : _n.length) && bundled.length === 0;
466
+ out.push({
467
+ feature: {
468
+ name,
469
+ ...foldedDesc ? { description: foldedDesc } : {},
470
+ ...feat.resources ? { resources: feat.resources } : {},
471
+ ...entityEffects.length > 0 ? { effects: entityEffects } : {}
472
+ },
473
+ source: { kind: "feat", slug: feat.slug },
474
+ ...buildOnly ? { buildOnly: true } : {}
475
+ });
476
+ }
477
+ }
478
+ return out;
479
+ }
480
+
481
+ // src/pc/pc.decision-engine.ts
482
+ var warnedDegradedEquipment = /* @__PURE__ */ new Set();
483
+ function wikilinkTailSlug(link) {
484
+ var _a;
485
+ const inner = link.replace(/^\[\[/, "").replace(/\]\]$/, "");
486
+ const tail = (_a = inner.split("/").pop()) != null ? _a : inner;
487
+ return tail.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
488
+ }
489
+ function matchesFilter(e, where, ownerBare) {
490
+ var _a, _b, _c, _d, _e;
491
+ const d = e.data;
492
+ if (where.feature_type && d.feature_type !== where.feature_type) return false;
493
+ if (where.category && d.category !== where.category) return false;
494
+ if (where.list && !((_a = d.classes) != null ? _a : []).includes(where.list)) return false;
495
+ if (where.level !== void 0 && ((_b = d.level) != null ? _b : 0) !== where.level) return false;
496
+ if (where.edition && d.edition !== where.edition) return false;
497
+ if (where.weapon_category) {
498
+ const cat = ((_c = d.category) != null ? _c : "").toLowerCase();
499
+ if (!cat.startsWith(`${where.weapon_category}-`) && cat !== where.weapon_category) return false;
500
+ }
501
+ if (where.armor_category) {
502
+ if (((_d = d.category) != null ? _d : "").toLowerCase() !== where.armor_category) return false;
503
+ }
504
+ if (where.parent_class === "self") {
505
+ if (!d.parent_class || wikilinkTailSlug(d.parent_class) !== ownerBare) return false;
506
+ }
507
+ if (where.available_to === "self") {
508
+ const list = (_e = d.available_to) != null ? _e : [];
509
+ if (!list.some((l) => wikilinkTailSlug(l) === ownerBare)) return false;
510
+ }
511
+ return true;
512
+ }
513
+ var __matchesFilterForTest = matchesFilter;
514
+ function categoryToEntitySelect(category, id) {
515
+ const c = category.toLowerCase();
516
+ if (c === "shield") {
517
+ return { kind: "select-entity", id, label: "Choose a shield", count: 1, entity_type: "armor", where: { armor_category: "shield" } };
518
+ }
519
+ if (c.includes("armor")) {
520
+ const klass = c.includes("light") ? "light" : c.includes("medium") ? "medium" : c.includes("heavy") ? "heavy" : null;
521
+ return klass ? { kind: "select-entity", id, label: `Choose ${klass} armor`, count: 1, entity_type: "armor", where: { armor_category: klass } } : { kind: "select-entity", id, label: "Choose armor", count: 1, entity_type: "armor" };
522
+ }
523
+ const wc = c.includes("martial") ? "martial" : "simple";
524
+ return { kind: "select-entity", id, label: `Choose a ${c.replace(/-/g, " ")}`, count: 1, entity_type: "weapon", where: { weapon_category: wc } };
525
+ }
526
+ function enumerateOptions(choice, ctx, ownerBare) {
527
+ var _a, _b;
528
+ switch (choice.kind) {
529
+ case "select-inline":
530
+ return choice.options.map((o) => ({ value: o.value, label: o.label, description: o.description }));
531
+ case "select-entity": {
532
+ if (choice.from) {
533
+ const byBare = /* @__PURE__ */ new Map();
534
+ for (const e of ctx.registry.search("", choice.entity_type, Number.POSITIVE_INFINITY)) {
535
+ byBare.set(bareEntitySlug(e.slug), e);
536
+ }
537
+ return choice.from.map((slug) => {
538
+ var _a2;
539
+ const e = (_a2 = ctx.registry.getByTypeAndSlug(choice.entity_type, slug)) != null ? _a2 : byBare.get(slug);
540
+ return e ? { value: e.slug, label: e.name, entity: e } : { value: slug, label: slug, missing: true };
541
+ });
542
+ }
543
+ const all = ctx.registry.search("", choice.entity_type, Number.POSITIVE_INFINITY);
544
+ const filtered = choice.where ? all.filter((e) => matchesFilter(e, choice.where, ownerBare)) : all;
545
+ return filtered.map((e) => ({ value: e.slug, label: e.name, entity: e }));
546
+ }
547
+ case "select-proficiency": {
548
+ const pool = (_a = choice.from) != null ? _a : choice.domain === "skill" ? [...ALL_SKILL_SLUGS] : [];
549
+ return pool.map((v) => ({ value: v, label: v.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) }));
550
+ }
551
+ case "ability-points": {
552
+ const pool = (_b = choice.pool) != null ? _b : ["str", "dex", "con", "int", "wis", "cha"];
553
+ return pool.map((a) => ({ value: a, label: a.toUpperCase() }));
554
+ }
555
+ }
556
+ }
557
+ function selectionCount(choice, selected) {
558
+ if (selected === void 0) return 0;
559
+ if (choice.kind === "ability-points") {
560
+ if (typeof selected !== "object" || Array.isArray(selected)) return 0;
561
+ return Object.values(selected).reduce((s, v) => s + (typeof v === "number" ? v : 0), 0);
562
+ }
563
+ if (Array.isArray(selected)) return selected.length;
564
+ return typeof selected === "string" && selected.length > 0 ? 1 : 0;
565
+ }
566
+ function requiredCount(choice) {
567
+ var _a;
568
+ if (choice.kind === "ability-points") return choice.points;
569
+ if (choice.kind === "select-proficiency") return choice.count;
570
+ return (_a = choice.count) != null ? _a : 1;
571
+ }
572
+ function statusOf(choice, selected) {
573
+ const n = selectionCount(choice, selected);
574
+ const need = requiredCount(choice);
575
+ if (n === 0) return "unresolved";
576
+ return n >= need ? "resolved" : "partial";
577
+ }
578
+ function resolveEntityRef(ctx, entityType, value) {
579
+ if (typeof value !== "string" || value.length === 0) return void 0;
580
+ const raw = value.replace(/^\[\[/, "").replace(/\]\]$/, "");
581
+ const direct = ctx.registry.getByTypeAndSlug(entityType, raw);
582
+ if (direct) return direct;
583
+ for (const e of ctx.registry.search("", entityType, Number.POSITIVE_INFINITY)) {
584
+ if (e.slug === raw || bareEntitySlug(e.slug) === raw) return e;
585
+ }
586
+ return void 0;
587
+ }
588
+ function buildItem(choice, source, level, featureName, readValue, ctx, ownerBare, opts) {
589
+ var _a, _b, _c, _d;
590
+ const keyPrefix = (_a = opts == null ? void 0 : opts.keyPrefix) != null ? _a : "";
591
+ const expandFeatChildren = (_b = opts == null ? void 0 : opts.expandFeatChildren) != null ? _b : true;
592
+ const key = keyPrefix + choice.id;
593
+ const selected = readValue(key);
594
+ const item = {
595
+ key,
596
+ source,
597
+ level,
598
+ featureName,
599
+ choice,
600
+ description: opts == null ? void 0 : opts.description,
601
+ options: enumerateOptions(choice, ctx, ownerBare),
602
+ selected,
603
+ status: statusOf(choice, selected)
604
+ };
605
+ if (choice.kind === "select-inline" && typeof selected === "string") {
606
+ const branch = choice.options.find((o) => o.value === selected);
607
+ if ((_c = branch == null ? void 0 : branch.choices) == null ? void 0 : _c.length) {
608
+ item.children = branch.choices.map((c) => buildItem(c, source, level, featureName, readValue, ctx, ownerBare, { keyPrefix, expandFeatChildren }));
609
+ if (item.status === "resolved" && item.children.some((c) => c.status !== "resolved")) {
610
+ item.status = "partial";
611
+ }
612
+ }
613
+ }
614
+ if (choice.kind === "select-entity" && choice.entity_type === "feat" && expandFeatChildren && typeof selected === "string") {
615
+ const entity = resolveEntityRef(ctx, "feat", selected);
616
+ const rawChoices = (_d = entity == null ? void 0 : entity.data) == null ? void 0 : _d.choices;
617
+ const featChoices = Array.isArray(rawChoices) ? rawChoices : [];
618
+ if (featChoices.length) {
619
+ const childPrefix = `${keyPrefix}feat:`;
620
+ item.children = featChoices.map((c) => buildItem(
621
+ c,
622
+ source,
623
+ level,
624
+ featureName,
625
+ readValue,
626
+ ctx,
627
+ ownerBare,
628
+ { keyPrefix: childPrefix, expandFeatChildren: false }
629
+ ));
630
+ if (item.status === "resolved" && item.children.some((c) => c.status !== "resolved")) {
631
+ item.status = "partial";
632
+ }
633
+ }
634
+ }
635
+ return item;
636
+ }
637
+ function buildSubclassItem(choice, source, level, featureName, c, ctx, ownerBare, description) {
638
+ const selected = c.subclass ? c.subclass.slug : void 0;
639
+ return {
640
+ key: "subclass",
641
+ source,
642
+ level,
643
+ featureName,
644
+ description,
645
+ choice,
646
+ options: enumerateOptions(choice, ctx, ownerBare),
647
+ selected,
648
+ status: selected ? "resolved" : "unresolved"
649
+ };
650
+ }
651
+ function collectChosenProficiencies(resolved) {
652
+ var _a, _b;
653
+ const out = { skills: [], expertise: [], languages: [], tools: [] };
654
+ const apply = (choice, selected) => {
655
+ if (choice.kind !== "select-proficiency") return;
656
+ const vals = Array.isArray(selected) ? selected : typeof selected === "string" ? [selected] : [];
657
+ const pool = choice.from;
658
+ const valid = pool ? vals.filter((v) => pool.includes(v)) : vals;
659
+ const bucket = choice.domain === "skill" ? choice.expertise ? out.expertise : out.skills : choice.domain === "language" ? out.languages : choice.domain === "tool" ? out.tools : null;
660
+ if (bucket) {
661
+ for (const v of valid) if (!bucket.includes(v)) bucket.push(v);
662
+ }
663
+ };
664
+ const walk = (choices, read) => {
665
+ for (const ch of choices != null ? choices : []) {
666
+ apply(ch, read(ch.id));
667
+ if (ch.kind === "select-inline") {
668
+ const sel = read(ch.id);
669
+ const branch = typeof sel === "string" ? ch.options.find((o) => o.value === sel) : void 0;
670
+ if (branch == null ? void 0 : branch.choices) walk(branch.choices, read);
671
+ }
672
+ }
673
+ };
674
+ resolved.classes.forEach((c, i) => {
675
+ var _a2, _b2;
676
+ if (!c.entity) return;
677
+ const entity = c.entity;
678
+ const readAt = (lvl) => (id) => {
679
+ var _a3;
680
+ return (_a3 = c.choices[lvl]) == null ? void 0 : _a3[id];
681
+ };
682
+ if (i === 0 && ((_b2 = (_a2 = entity.skill_choices) == null ? void 0 : _a2.from) == null ? void 0 : _b2.length)) {
683
+ apply({
684
+ kind: "select-proficiency",
685
+ id: "skills",
686
+ count: entity.skill_choices.count,
687
+ domain: "skill",
688
+ from: entity.skill_choices.from
689
+ }, readAt(1)("skills"));
690
+ }
691
+ for (const rf of resolved.features) {
692
+ if (rf.source.kind !== "class" && rf.source.kind !== "subclass") continue;
693
+ const belongs = rf.source.kind === "class" ? rf.source.slug === entity.slug : c.subclass != null && rf.source.slug === c.subclass.slug;
694
+ if (!belongs) continue;
695
+ walk(rf.feature.choices, readAt(rf.source.level));
696
+ }
697
+ });
698
+ const oc = (_a = resolved.definition.origin_choices) != null ? _a : {};
699
+ const originRead = (ns) => (id) => oc[`${ns}:${id}`];
700
+ if (resolved.race) {
701
+ walk(resolved.race.choices, originRead("race"));
702
+ for (const t of (_b = resolved.race.traits) != null ? _b : []) walk(t.choices, originRead("race"));
703
+ }
704
+ if (resolved.background) {
705
+ walk(resolved.background.choices, originRead("background"));
706
+ if (resolved.background.feature) {
707
+ walk(resolved.background.feature.choices, originRead("background"));
708
+ }
709
+ }
710
+ return out;
711
+ }
712
+ function collectChosenAbilityPoints(resolved) {
713
+ var _a, _b, _c;
714
+ const oc = (_a = resolved.definition.origin_choices) != null ? _a : {};
715
+ const out = { race: {}, background: {} };
716
+ const fold = (ns, choices) => {
717
+ var _a2;
718
+ for (const ch of choices != null ? choices : []) {
719
+ if (ch.kind !== "ability-points") continue;
720
+ const raw = oc[`${ns}:${ch.id}`];
721
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
722
+ let left = ch.points;
723
+ for (const ab of ABILITY_KEYS) {
724
+ if (ch.pool && !ch.pool.includes(ab)) continue;
725
+ const v = raw[ab];
726
+ if (typeof v !== "number" || v <= 0 || left <= 0) continue;
727
+ const take = Math.min(v, ch.max_per, left);
728
+ out[ns][ab] = ((_a2 = out[ns][ab]) != null ? _a2 : 0) + take;
729
+ left -= take;
730
+ }
731
+ }
732
+ };
733
+ const race = resolved.race;
734
+ if (race) {
735
+ fold("race", race.choices);
736
+ for (const t of (_b = race.traits) != null ? _b : []) fold("race", t.choices);
737
+ }
738
+ const bg = resolved.background;
739
+ if (bg) {
740
+ fold("background", bg.choices);
741
+ fold("background", (_c = bg.feature) == null ? void 0 : _c.choices);
742
+ }
743
+ return out;
744
+ }
745
+ function buildDecisionLedger(resolved, ctx) {
746
+ var _a, _b, _c, _d, _e;
747
+ const classes = [];
748
+ resolved.classes.forEach((c, classIndex) => {
749
+ var _a2, _b2, _c2, _d2, _e2, _f, _g, _h, _i, _j, _k, _l, _m, _n;
750
+ if (!c.entity) return;
751
+ const entity = c.entity;
752
+ const ownerBare = bareEntitySlug(entity.slug);
753
+ const byLevel = /* @__PURE__ */ new Map();
754
+ const push = (lvl, item) => {
755
+ var _a3;
756
+ const arr = (_a3 = byLevel.get(lvl)) != null ? _a3 : [];
757
+ arr.push(item);
758
+ byLevel.set(lvl, arr);
759
+ };
760
+ const readAt = (lvl) => (id) => {
761
+ var _a3;
762
+ return (_a3 = c.choices[lvl]) == null ? void 0 : _a3[id];
763
+ };
764
+ if (classIndex === 0 && ((_b2 = (_a2 = entity.skill_choices) == null ? void 0 : _a2.from) == null ? void 0 : _b2.length)) {
765
+ const skillChoice = {
766
+ kind: "select-proficiency",
767
+ id: "skills",
768
+ label: "Skill Proficiencies",
769
+ count: entity.skill_choices.count,
770
+ domain: "skill",
771
+ from: entity.skill_choices.from
772
+ };
773
+ push(1, buildItem(
774
+ skillChoice,
775
+ { kind: "class", slug: entity.slug, level: 1 },
776
+ 1,
777
+ "Proficiencies",
778
+ readAt(1),
779
+ ctx,
780
+ ownerBare
781
+ ));
782
+ }
783
+ if (classIndex === 0) {
784
+ ((_c2 = entity.starting_equipment) != null ? _c2 : []).forEach((eq, i) => {
785
+ var _a3;
786
+ if (eq.kind !== "choice") return;
787
+ const options = (_a3 = eq.options) != null ? _a3 : [];
788
+ let degraded = false;
789
+ const ch = {
790
+ kind: "select-inline",
791
+ id: `equipment-${i}`,
792
+ label: "Starting Equipment",
793
+ count: 1,
794
+ options: options.map((opt, j) => {
795
+ var _a4;
796
+ const label = typeof opt === "string" ? opt : (_a4 = opt == null ? void 0 : opt.label) != null ? _a4 : "";
797
+ const grants = opt && typeof opt === "object" && Array.isArray(opt.grants) ? opt.grants : [];
798
+ if (typeof opt === "string" || !(opt && typeof opt === "object" && Array.isArray(opt.grants))) {
799
+ degraded = true;
800
+ }
801
+ return {
802
+ value: `option-${j}`,
803
+ label,
804
+ choices: grants.flatMap((g, k) => g && typeof g === "object" && "category" in g && g.category ? [categoryToEntitySelect(g.category, `equipment-${i}-opt-${j}-cat-${k}`)] : [])
805
+ };
806
+ })
807
+ };
808
+ if (degraded && !warnedDegradedEquipment.has(entity.slug)) {
809
+ warnedDegradedEquipment.add(entity.slug);
810
+ console.warn(
811
+ `[archivist] Starting equipment for "${entity.slug}" is in an outdated/unstructured format; its nested picks and seeding are skipped. Re-sync the compendium (delete _compendium.md and reload) to fix.`
812
+ );
813
+ }
814
+ push(1, buildItem(
815
+ ch,
816
+ { kind: "class", slug: entity.slug, level: 1 },
817
+ 1,
818
+ "Starting Equipment",
819
+ readAt(1),
820
+ ctx,
821
+ ownerBare
822
+ ));
823
+ });
824
+ }
825
+ let sawAuthoredSubclass = false;
826
+ for (const rf of resolved.features) {
827
+ const src = rf.source;
828
+ if (src.kind !== "class" && src.kind !== "subclass") continue;
829
+ const belongs = src.kind === "class" ? src.slug === entity.slug : c.subclass != null && src.slug === c.subclass.slug;
830
+ if (!belongs) continue;
831
+ const lvl = src.level;
832
+ let choices = rf.feature.choices;
833
+ if (!(choices == null ? void 0 : choices.length)) {
834
+ const recognized = recognizeDecision(rf.feature);
835
+ if (recognized === "informational") {
836
+ push(lvl, {
837
+ key: (_d2 = rf.feature.id) != null ? _d2 : rf.feature.name,
838
+ source: src,
839
+ level: lvl,
840
+ featureName: rf.feature.name,
841
+ description: (_f = rf.feature.description) != null ? _f : (_e2 = rf.feature.entries) == null ? void 0 : _e2.join("\n\n"),
842
+ choice: { kind: "select-inline", id: (_g = rf.feature.id) != null ? _g : rf.feature.name, options: [{ value: "_", label: "_" }] },
843
+ options: [],
844
+ selected: void 0,
845
+ status: "informational"
846
+ });
847
+ continue;
848
+ }
849
+ choices = recognized != null ? recognized : void 0;
850
+ }
851
+ if (!(choices == null ? void 0 : choices.length)) {
852
+ if (rf.feature.description || ((_h = rf.feature.entries) == null ? void 0 : _h.length)) {
853
+ push(lvl, {
854
+ key: (_i = rf.feature.id) != null ? _i : rf.feature.name,
855
+ source: src,
856
+ level: lvl,
857
+ featureName: rf.feature.name,
858
+ description: (_k = rf.feature.description) != null ? _k : (_j = rf.feature.entries) == null ? void 0 : _j.join("\n\n"),
859
+ choice: { kind: "select-inline", id: (_l = rf.feature.id) != null ? _l : rf.feature.name, options: [{ value: "_", label: "_" }] },
860
+ options: [],
861
+ selected: void 0,
862
+ status: "informational"
863
+ });
864
+ }
865
+ continue;
866
+ }
867
+ for (const ch of choices) {
868
+ if (ch.kind === "select-entity" && ch.entity_type === "subclass") {
869
+ sawAuthoredSubclass = true;
870
+ push(lvl, buildSubclassItem(ch, src, lvl, rf.feature.name, c, ctx, ownerBare, rf.feature.description));
871
+ continue;
872
+ }
873
+ push(lvl, buildItem(
874
+ ch,
875
+ src,
876
+ lvl,
877
+ rf.feature.name,
878
+ readAt(lvl),
879
+ ctx,
880
+ ownerBare,
881
+ { description: rf.feature.description }
882
+ ));
883
+ }
884
+ }
885
+ const subclassLevel = (_m = entity.subclass_level) != null ? _m : null;
886
+ if (subclassLevel != null && subclassLevel <= c.level && !sawAuthoredSubclass) {
887
+ const featureName = (_n = entity.subclass_feature_name) != null ? _n : "Subclass";
888
+ const synthChoice = {
889
+ kind: "select-entity",
890
+ id: "subclass",
891
+ label: featureName,
892
+ count: 1,
893
+ entity_type: "subclass",
894
+ where: { parent_class: "self" }
895
+ };
896
+ push(subclassLevel, buildSubclassItem(
897
+ synthChoice,
898
+ { kind: "class", slug: entity.slug, level: subclassLevel },
899
+ subclassLevel,
900
+ featureName,
901
+ c,
902
+ ctx,
903
+ ownerBare
904
+ ));
905
+ }
906
+ for (const pool of resolved.pools) {
907
+ if (pool.classIndex !== classIndex || pool.count < 1) continue;
908
+ const synth = {
909
+ kind: "select-entity",
910
+ id: pool.id,
911
+ label: pool.label,
912
+ count: pool.count,
913
+ entity_type: "optional-feature",
914
+ from: pool.available.map((e) => e.slug)
915
+ };
916
+ push(pool.anchorLevel, buildItem(
917
+ synth,
918
+ { kind: "class", slug: entity.slug, level: pool.anchorLevel },
919
+ pool.anchorLevel,
920
+ pool.label,
921
+ readAt(pool.anchorLevel),
922
+ ctx,
923
+ ownerBare
924
+ ));
925
+ }
926
+ const levels = [...byLevel.entries()].sort(([a], [b]) => a - b).map(([level, items]) => ({ level, items }));
927
+ classes.push({ classIndex, levels });
928
+ });
929
+ const origin = [];
930
+ const oc = (_a = resolved.definition.origin_choices) != null ? _a : {};
931
+ const originRead = (ns) => (id) => oc[`${ns}:${id}`];
932
+ const pushOrigin = (choices, ns, source, featureName, ownerBare, description) => {
933
+ for (const ch of choices != null ? choices : []) {
934
+ origin.push(buildItem(ch, source, 0, featureName, originRead(ns), ctx, ownerBare, { description }));
935
+ }
936
+ };
937
+ if (resolved.race) {
938
+ const bare = bareEntitySlug(resolved.race.slug);
939
+ pushOrigin(
940
+ resolved.race.choices,
941
+ "race",
942
+ { kind: "race", slug: resolved.race.slug },
943
+ (_b = resolved.race.name) != null ? _b : "Race",
944
+ bare,
945
+ resolved.race.description
946
+ );
947
+ for (const t of (_c = resolved.race.traits) != null ? _c : []) {
948
+ pushOrigin(
949
+ t.choices,
950
+ "race",
951
+ { kind: "race", slug: resolved.race.slug },
952
+ t.name,
953
+ bare,
954
+ t.description
955
+ );
956
+ }
957
+ }
958
+ if (resolved.background) {
959
+ const bare = bareEntitySlug(resolved.background.slug);
960
+ pushOrigin(
961
+ resolved.background.choices,
962
+ "background",
963
+ { kind: "background", slug: resolved.background.slug },
964
+ (_d = resolved.background.name) != null ? _d : "Background",
965
+ bare,
966
+ resolved.background.description
967
+ );
968
+ if (resolved.background.feature) {
969
+ pushOrigin(
970
+ resolved.background.feature.choices,
971
+ "background",
972
+ { kind: "background", slug: resolved.background.slug },
973
+ resolved.background.feature.name,
974
+ bare,
975
+ resolved.background.feature.description
976
+ );
977
+ }
978
+ if (resolved.background.origin_feat) {
979
+ const originFeat = resolveOriginFeat(
980
+ ctx.registry,
981
+ resolved.background.origin_feat
982
+ );
983
+ if (originFeat) {
984
+ const featSource = { kind: "feat", slug: originFeat.feat.slug };
985
+ const featBare = bareEntitySlug(originFeat.feat.slug);
986
+ for (const ch of (_e = originFeat.feat.choices) != null ? _e : []) {
987
+ origin.push(buildItem(
988
+ ch,
989
+ featSource,
990
+ 0,
991
+ originFeat.display,
992
+ originRead("background"),
993
+ ctx,
994
+ featBare,
995
+ { keyPrefix: "feat:" }
996
+ ));
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ return { classes, origin };
1002
+ }
1003
+
1004
+ // src/pc/pc.pools.ts
1005
+ function levelPrereqMax(d) {
1006
+ var _a;
1007
+ return ((_a = d.prerequisites) != null ? _a : []).filter((p) => p.kind === "level").reduce((m, p) => Math.max(m, p.min), 0);
1008
+ }
1009
+ function resolvePool(rc, classIndex, pool, registry) {
1010
+ var _a, _b, _c, _d, _e, _f, _g;
1011
+ const entity = rc.entity;
1012
+ const level = rc.level;
1013
+ const ownerBare = bareEntitySlug(entity.slug);
1014
+ const col = pool.count.column;
1015
+ const count = (_a = readTableColumn(entity.table, level, [col])) != null ? _a : 0;
1016
+ let anchorLevel = 1;
1017
+ for (let l = 1; l <= 20; l++) {
1018
+ const c = readTableColumn(entity.table, l, [col]);
1019
+ if (c != null && c >= 1) {
1020
+ anchorLevel = l;
1021
+ break;
1022
+ }
1023
+ }
1024
+ const ft = pool.source.where.feature_type;
1025
+ const all = registry.search("", "optional-feature", Number.POSITIVE_INFINITY);
1026
+ const byBare = /* @__PURE__ */ new Map();
1027
+ const available = [];
1028
+ for (const e of all) {
1029
+ const d = e.data;
1030
+ const entry = { slug: e.slug, entity: d };
1031
+ byBare.set(bareEntitySlug(e.slug), entry);
1032
+ if (d.feature_type !== ft) continue;
1033
+ if (!((_b = d.available_to) != null ? _b : []).some((l) => wikilinkTailSlug(l) === ownerBare)) continue;
1034
+ if (levelPrereqMax(d) > level) continue;
1035
+ available.push(entry);
1036
+ }
1037
+ const grantDecls = [
1038
+ ...(_d = (_c = rc.entity) == null ? void 0 : _c.pool_grants) != null ? _d : [],
1039
+ ...(_f = (_e = rc.subclass) == null ? void 0 : _e.pool_grants) != null ? _f : []
1040
+ ];
1041
+ const grantsRaw = grantDecls.filter((g) => g.pool === pool.id).flatMap((g) => g.grants).filter((g) => g.at_level <= level).map((g) => byBare.get(wikilinkTailSlug(g.feature))).filter((e) => e != null);
1042
+ const grantedBare = /* @__PURE__ */ new Set();
1043
+ const grants = grantsRaw.filter((e) => {
1044
+ const key = bareEntitySlug(e.slug);
1045
+ if (grantedBare.has(key)) return false;
1046
+ grantedBare.add(key);
1047
+ return true;
1048
+ });
1049
+ const raw = (_g = rc.choices[anchorLevel]) == null ? void 0 : _g[pool.id];
1050
+ const selectedSlugs = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
1051
+ const selected = selectedSlugs.map((s) => byBare.get(bareEntitySlug(s))).filter((e) => e != null).filter((e) => !grantedBare.has(bareEntitySlug(e.slug)));
1052
+ return { id: pool.id, label: pool.label, classIndex, count, anchorLevel, selected, available, grants };
1053
+ }
1054
+ function resolveAllPools(resolved, registry) {
1055
+ const out = [];
1056
+ resolved.classes.forEach((rc, classIndex) => {
1057
+ var _a, _b, _c;
1058
+ if (!rc.entity) return;
1059
+ const decls = [...(_a = rc.entity.selection_pools) != null ? _a : [], ...(_c = (_b = rc.subclass) == null ? void 0 : _b.selection_pools) != null ? _c : []];
1060
+ const seen = /* @__PURE__ */ new Set();
1061
+ for (const pool of decls) {
1062
+ if (seen.has(pool.id)) continue;
1063
+ seen.add(pool.id);
1064
+ out.push(resolvePool(rc, classIndex, pool, registry));
1065
+ }
1066
+ });
1067
+ return out;
1068
+ }
1069
+
1070
+ export {
1071
+ levelPrereqMax,
1072
+ resolvePool,
1073
+ resolveAllPools,
1074
+ stripSlug,
1075
+ resolveOriginFeat,
1076
+ PCResolver,
1077
+ collectChosenWeaponMasteries,
1078
+ collectFeatSlugs,
1079
+ collectFeatGrantedSpells,
1080
+ collectItemGrantedSpells,
1081
+ dedupeResolvedSpells,
1082
+ collectChosenGrantedFeatures,
1083
+ collectResolvedFeatures,
1084
+ wikilinkTailSlug,
1085
+ __matchesFilterForTest,
1086
+ collectChosenProficiencies,
1087
+ collectChosenAbilityPoints,
1088
+ buildDecisionLedger
1089
+ };