@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
@@ -21,21 +21,29 @@ export declare const ITEM_ACTIONS: Record<string, ItemAction>;
21
21
  * (`srd-5e_wand-of-fireballs`, `srd-2024_wand-of-fireballs`,
22
22
  * `homebrew_wand-of-fireballs`).
23
23
  *
24
- * Compendium-prefixed slugs follow the shape `<prefix>_<name-slug>` where
25
- * both prefix and name-slug are kebab-case (slugify strips underscores).
26
- * The first underscore is therefore the unambiguous prefix separator: we
27
- * try the bare slug after the first underscore first, then fall back to
28
- * the original slug for legacy bare-slug callers.
24
+ * Type-namespaced slugs follow the shape `<prefix>_<entity_type>_<name-slug>`
25
+ * (all kebab-case; slugify never emits `_`), so a slug splits on `_` into
26
+ * exactly 3 parts and the bare name is `parts.slice(2).join("_")`. This is
27
+ * arity-robust: it also recovers the bare name from a legacy 2-part
28
+ * `<prefix>_<name>` slug and an already-bare name. `ITEM_ACTIONS` is keyed by
29
+ * bare name.
29
30
  *
30
- * Returns `undefined` if no map entry matches either form. Does not crash
31
- * on slugs without an underscore — those are treated as already-bare.
31
+ * Returns `undefined` if no map entry matches. Does not crash on slugs
32
+ * without an underscore — those are treated as already-bare.
32
33
  */
33
34
  export declare function findItemAction(slug: string): ItemAction | undefined;
34
35
  /**
35
36
  * Resolve the ItemAction for an equipped entry.
36
- * Priority: entry.overrides (action + range) merged onto curated map.
37
- * Returns null when neither source supplies an action cost.
37
+ * Priority: per-instance override > curated map > `itemType==="potion"`
38
+ * bonus-action default > null.
39
+ *
40
+ * `itemType` is the entity's `data.type` (from the registry, statically
41
+ * `unknown`); per the 2024 SRD, drinking/administering a potion is a Bonus
42
+ * Action, so an uncurated `type==="potion"` item still surfaces (curated oils
43
+ * mis-typed `"potion"` are pinned to "action" so this default cannot mislabel
44
+ * them). Returns null when no source supplies a cost (non-potion uncurated with
45
+ * no override).
38
46
  *
39
47
  * Accepts both bare and compendium-prefixed slugs (see `findItemAction`).
40
48
  */
41
- export declare function resolveItemAction(slug: string, entry: EquipmentEntry): ItemAction | null;
49
+ export declare function resolveItemAction(slug: string, entry: EquipmentEntry, itemType?: unknown): ItemAction | null;
@@ -46,30 +46,34 @@ var ITEM_ACTIONS = {
46
46
  // Magic weapons with activation actions (surface in items table only via override; weapons table is primary)
47
47
  "sun-blade": { cost: "free", range: "self" },
48
48
  // Potions / consumables
49
- "potion-of-healing": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
50
- "potion-of-greater-healing": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
51
- "potion-of-superior-healing": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
52
- "potion-of-supreme-healing": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
49
+ // 2024 healing potions are a Bonus Action to drink/administer (curation still
50
+ // carries range/max_charges/recovery; the type=potion default covers uncurated ones).
51
+ "potion-of-healing": { cost: "bonus-action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
52
+ "potion-of-greater-healing": { cost: "bonus-action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
53
+ "potion-of-superior-healing": { cost: "bonus-action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
54
+ "potion-of-supreme-healing": { cost: "bonus-action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
53
55
  "alchemists-fire": { cost: "action", range: "20 ft.", max_charges: 1, recovery: { amount: "0", reset: "special" } },
54
56
  "holy-water": { cost: "action", range: "20 ft.", max_charges: 1, recovery: { amount: "0", reset: "special" } },
57
+ // Oils are APPLIED (minutes), not drunk: curated to "action" so the blanket
58
+ // type=potion → bonus-action default never mislabels these mis-typed items.
55
59
  "oil-of-sharpness": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
60
+ "oil-of-etherealness": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
61
+ "oil-of-slipperiness": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } },
56
62
  "dust-of-disappearance": { cost: "action", range: "self", max_charges: 1, recovery: { amount: "0", reset: "special" } }
57
63
  };
58
64
  function findItemAction(slug) {
59
65
  if (slug in ITEM_ACTIONS) return ITEM_ACTIONS[slug];
60
- const underscore = slug.indexOf("_");
61
- if (underscore < 0) return void 0;
62
- const bare = slug.slice(underscore + 1);
63
- return ITEM_ACTIONS[bare];
66
+ const p = slug.split("_");
67
+ const bare = p.length >= 3 ? p.slice(2).join("_") : p[p.length - 1];
68
+ return bare === slug ? void 0 : ITEM_ACTIONS[bare];
64
69
  }
65
- function resolveItemAction(slug, entry) {
66
- var _a;
70
+ function resolveItemAction(slug, entry, itemType) {
71
+ var _a, _b;
67
72
  const curated = (_a = findItemAction(slug)) != null ? _a : null;
68
73
  const override = entry.overrides;
69
74
  const overrideCost = override == null ? void 0 : override.action;
70
75
  const overrideRange = override == null ? void 0 : override.range;
71
- if (!curated && !overrideCost) return null;
72
- const cost = overrideCost != null ? overrideCost : curated == null ? void 0 : curated.cost;
76
+ const cost = (_b = overrideCost != null ? overrideCost : curated == null ? void 0 : curated.cost) != null ? _b : itemType === "potion" ? "bonus-action" : void 0;
73
77
  if (!cost) return null;
74
78
  return {
75
79
  cost,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  itemCodec
3
- } from "../chunk-TPZSPU4L.js";
4
- import "../chunk-YDPWD2T7.js";
5
- import "../chunk-VLKIB33F.js";
3
+ } from "../chunk-4JHOHWBI.js";
4
+ import "../chunk-7VJ6BOA5.js";
5
+ import "../chunk-K6Q5OUGK.js";
6
6
  export {
7
7
  itemCodec
8
8
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseItem
3
- } from "../chunk-YDPWD2T7.js";
4
- import "../chunk-VLKIB33F.js";
3
+ } from "../chunk-7VJ6BOA5.js";
4
+ import "../chunk-K6Q5OUGK.js";
5
5
  export {
6
6
  parseItem
7
7
  };
@@ -175,6 +175,9 @@ export declare const itemEntitySchema: z.ZodObject<{
175
175
  }>>;
176
176
  condition: z.ZodOptional<z.ZodString>;
177
177
  }, z.core.$strip>>>;
178
+ scroll_level: z.ZodOptional<z.ZodNumber>;
179
+ unidentified: z.ZodOptional<z.ZodBoolean>;
180
+ masked_category: z.ZodOptional<z.ZodString>;
178
181
  raw: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
179
182
  damage_dice: z.ZodOptional<z.ZodString>;
180
183
  damage_type: z.ZodOptional<z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  conditionSchema,
3
3
  itemEntitySchema
4
- } from "../chunk-VLKIB33F.js";
4
+ } from "../chunk-K6Q5OUGK.js";
5
5
  export {
6
6
  conditionSchema,
7
7
  itemEntitySchema
@@ -106,6 +106,14 @@ export interface ItemEntity {
106
106
  applies_to?: "weapon" | "spell" | "all";
107
107
  condition?: string;
108
108
  }>;
109
+ /** Spell-scroll casting level (e.g. 3 for a "Spell Scroll (3rd Level)"). */
110
+ scroll_level?: number;
111
+ /** Marks an item the player has not yet identified: the sheet shows the
112
+ * masked_category label instead of the item's true identity. */
113
+ unidentified?: boolean;
114
+ /** Generic category label shown for an unidentified item (e.g. "potion",
115
+ * "scroll", "wand") in place of its real name. */
116
+ masked_category?: string;
109
117
  raw?: Record<string, unknown>;
110
118
  damage_dice?: string;
111
119
  damage_type?: string;
@@ -68,9 +68,6 @@ export interface DecisionContext {
68
68
  }
69
69
  /** "[[SRD 2024/Classes/Fighter]]" → "fighter" (tail segment, slugified). */
70
70
  export declare function wikilinkTailSlug(link: string): string;
71
- /** "srd-2024_fighter" → "fighter". Tolerates a nullish slug (degrades to "") so a
72
- * malformed entity can't hard-crash the builder; the resolver backfills real slugs. */
73
- export declare function bareEntitySlug(slug: string | null | undefined): string;
74
71
  declare function matchesFilter(e: RegisteredEntity, where: EntityFilter, ownerBare: string): boolean;
75
72
  /** Test-only export of {@link matchesFilter} (Task B2). */
76
73
  export declare const __matchesFilterForTest: typeof matchesFilter;
@@ -1,17 +1,22 @@
1
1
  import {
2
2
  __matchesFilterForTest,
3
- bareEntitySlug,
4
3
  buildDecisionLedger,
5
4
  collectChosenAbilityPoints,
6
5
  collectChosenProficiencies,
7
6
  wikilinkTailSlug
8
- } from "../chunk-TJCNX6UZ.js";
7
+ } from "../chunk-5MZSB6OM.js";
9
8
  import "../chunk-BH6AMZVY.js";
9
+ import "../chunk-M2EMFD6S.js";
10
+ import "../chunk-5XUXXAQJ.js";
10
11
  import "../chunk-JEYQU3JT.js";
12
+ import "../chunk-N6N74K6X.js";
13
+ import "../chunk-4LZJXJJS.js";
14
+ import "../chunk-LV4SNWZQ.js";
15
+ import "../chunk-VTTA2ALZ.js";
16
+ import "../chunk-NAEPSZAM.js";
11
17
  import "../chunk-WP2CX2JO.js";
12
18
  export {
13
19
  __matchesFilterForTest,
14
- bareEntitySlug,
15
20
  buildDecisionLedger,
16
21
  collectChosenAbilityPoints,
17
22
  collectChosenProficiencies,
@@ -2,16 +2,13 @@ import {
2
2
  computeAppliedBonuses,
3
3
  computeSlotsAndAttacks,
4
4
  emptyAppliedBonuses
5
- } from "../chunk-ADLTSHOR.js";
6
- import "../chunk-UC4FE5X3.js";
7
- import "../chunk-TJCNX6UZ.js";
8
- import "../chunk-BH6AMZVY.js";
9
- import "../chunk-JEYQU3JT.js";
5
+ } from "../chunk-EMOORSQ7.js";
6
+ import "../chunk-353WXS6S.js";
7
+ import "../chunk-N6N74K6X.js";
10
8
  import "../chunk-LWFTHJIE.js";
11
9
  import "../chunk-DNAJRMNN.js";
12
- import "../chunk-DC74TZNA.js";
13
- import "../chunk-3TRGRSCH.js";
14
- import "../chunk-WP2CX2JO.js";
10
+ import "../chunk-4LZJXJJS.js";
11
+ import "../chunk-LV4SNWZQ.js";
15
12
  export {
16
13
  computeAppliedBonuses,
17
14
  computeSlotsAndAttacks,
@@ -24,6 +24,11 @@ export interface WeaponAbilityOverride {
24
24
  export interface FeatureEffectTotals {
25
25
  initiative_bonus: number;
26
26
  hp_per_level_bonus: number;
27
+ /** One term per hp-per-level-bonus effect, labeled with the owning feature's name. */
28
+ hp_per_level_terms: {
29
+ label: string;
30
+ value: number;
31
+ }[];
27
32
  speed_walk_bonus: number;
28
33
  /**
29
34
  * Absolute walk-speed FLOOR from `speed-bonus` effects with `set:true` (e.g.
@@ -1,10 +1,8 @@
1
1
  import {
2
2
  computeFeatureEffects,
3
3
  emptyFeatureEffectTotals
4
- } from "../chunk-GEJSJP6E.js";
5
- import "../chunk-TJCNX6UZ.js";
6
- import "../chunk-BH6AMZVY.js";
7
- import "../chunk-JEYQU3JT.js";
4
+ } from "../chunk-AIRFC5NQ.js";
5
+ import "../chunk-LV4SNWZQ.js";
8
6
  import "../chunk-WP2CX2JO.js";
9
7
  export {
10
8
  computeFeatureEffects,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  characterSchema
3
- } from "../chunk-ZLG6FQFB.js";
3
+ } from "../chunk-A4WENOUQ.js";
4
4
  import "../chunk-TZK36BU7.js";
5
5
 
6
6
  // src/pc/pc.parser.ts
@@ -2,11 +2,16 @@ import {
2
2
  levelPrereqMax,
3
3
  resolveAllPools,
4
4
  resolvePool
5
- } from "../chunk-6PXE7T7C.js";
6
- import "../chunk-5XUXXAQJ.js";
7
- import "../chunk-TJCNX6UZ.js";
5
+ } from "../chunk-5MZSB6OM.js";
8
6
  import "../chunk-BH6AMZVY.js";
7
+ import "../chunk-M2EMFD6S.js";
8
+ import "../chunk-5XUXXAQJ.js";
9
9
  import "../chunk-JEYQU3JT.js";
10
+ import "../chunk-N6N74K6X.js";
11
+ import "../chunk-4LZJXJJS.js";
12
+ import "../chunk-LV4SNWZQ.js";
13
+ import "../chunk-VTTA2ALZ.js";
14
+ import "../chunk-NAEPSZAM.js";
10
15
  import "../chunk-WP2CX2JO.js";
11
16
  export {
12
17
  levelPrereqMax,
@@ -45,11 +45,18 @@ export declare function abilityBonusBreakdown(resolved: ResolvedCharacter): Reco
45
45
  class: number;
46
46
  feat: number;
47
47
  }>;
48
+ /** Dice-only portion of PC max HP: first VALID level anywhere = max(die),
49
+ * every later valid level = PHB average. Skip rule identical to hpLevelCount. */
50
+ export declare function hitDiceAverageSum(classes: ResolvedClass[]): number;
51
+ /** Count of levels that contribute to HP (valid class entries only). */
52
+ export declare function hpLevelCount(classes: ResolvedClass[]): number;
48
53
  /**
49
54
  * PC-oriented multiclass HP.
50
55
  * - First class's first level: max(hit_die) + conMod.
51
56
  * - Every subsequent level (in any class): phbAverageForDie(thatLevel'sClass) + conMod.
52
57
  * CON mod contributes once per total level.
58
+ * Decomposed (P5 T2) into hitDiceAverageSum (dice-only) + hpLevelCount (level count),
59
+ * so a later popup can render the two components separately.
53
60
  */
54
61
  export declare function multiclassMaxHP(classes: ResolvedClass[], conMod: number): number;
55
62
  /**
@@ -1,32 +1,33 @@
1
+ import {
2
+ computeConditionEffects
3
+ } from "../chunk-R2ZV233X.js";
4
+ import {
5
+ collectChosenAbilityPoints,
6
+ collectChosenProficiencies
7
+ } from "../chunk-5MZSB6OM.js";
8
+ import "../chunk-BH6AMZVY.js";
1
9
  import {
2
10
  computeSpellLimits,
3
11
  deriveSpellSlots,
4
12
  resolveSpellcasting
5
- } from "../chunk-IXZXFFFW.js";
13
+ } from "../chunk-M2EMFD6S.js";
6
14
  import "../chunk-5XUXXAQJ.js";
7
- import {
8
- computeConditionEffects
9
- } from "../chunk-R2ZV233X.js";
15
+ import "../chunk-JEYQU3JT.js";
10
16
  import {
11
17
  computeAppliedBonuses,
12
18
  computeSlotsAndAttacks,
13
19
  emptyAppliedBonuses
14
- } from "../chunk-ADLTSHOR.js";
15
- import "../chunk-UC4FE5X3.js";
20
+ } from "../chunk-EMOORSQ7.js";
21
+ import "../chunk-353WXS6S.js";
22
+ import "../chunk-N6N74K6X.js";
16
23
  import {
17
24
  computeFeatureEffects
18
- } from "../chunk-GEJSJP6E.js";
19
- import {
20
- collectChosenAbilityPoints,
21
- collectChosenProficiencies
22
- } from "../chunk-TJCNX6UZ.js";
23
- import "../chunk-BH6AMZVY.js";
24
- import "../chunk-JEYQU3JT.js";
25
+ } from "../chunk-AIRFC5NQ.js";
25
26
  import "../chunk-LWFTHJIE.js";
26
27
  import "../chunk-DNAJRMNN.js";
27
- import "../chunk-DC74TZNA.js";
28
- import "../chunk-3TRGRSCH.js";
29
- import "../chunk-J2RYSZMQ.js";
28
+ import "../chunk-4LZJXJJS.js";
29
+ import "../chunk-LV4SNWZQ.js";
30
+ import "../chunk-VTTA2ALZ.js";
30
31
  import {
31
32
  abilityModifier,
32
33
  attackBonus,
@@ -117,7 +118,7 @@ function abilityBonusBreakdown(resolved) {
117
118
  }
118
119
  return out;
119
120
  }
120
- function multiclassMaxHP(classes, conMod) {
121
+ function hitDiceAverageSum(classes) {
121
122
  var _a;
122
123
  let total = 0;
123
124
  let firstLevelCounted = false;
@@ -126,14 +127,27 @@ function multiclassMaxHP(classes, conMod) {
126
127
  if (!c.entity || die == null || c.level < 1) continue;
127
128
  for (let lvl = 1; lvl <= c.level; lvl++) {
128
129
  if (!firstLevelCounted) {
129
- total += die + conMod;
130
+ total += die;
130
131
  firstLevelCounted = true;
131
132
  } else {
132
- total += Math.floor(die / 2) + 1 + conMod;
133
+ total += Math.floor(die / 2) + 1;
133
134
  }
134
135
  }
135
136
  }
136
- return Math.max(1, total);
137
+ return total;
138
+ }
139
+ function hpLevelCount(classes) {
140
+ var _a;
141
+ let count = 0;
142
+ for (const c of classes) {
143
+ const die = parseDieSize((_a = c.entity) == null ? void 0 : _a.hit_die);
144
+ if (!c.entity || die == null || c.level < 1) continue;
145
+ count += c.level;
146
+ }
147
+ return count;
148
+ }
149
+ function multiclassMaxHP(classes, conMod) {
150
+ return Math.max(1, hitDiceAverageSum(classes) + conMod * hpLevelCount(classes));
137
151
  }
138
152
  function unarmoredAC(resolved, mods, warnings) {
139
153
  return unarmoredACBreakdown(resolved, mods, warnings).total;
@@ -362,7 +376,7 @@ function computeProficiencies(resolved) {
362
376
  };
363
377
  }
364
378
  function recalc(resolved, registry) {
365
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W;
379
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa;
366
380
  const warnings = [];
367
381
  const overrides = (_a = resolved.definition.overrides) != null ? _a : {};
368
382
  const chosenProfs = collectChosenProficiencies(resolved);
@@ -484,9 +498,36 @@ function recalc(resolved, registry) {
484
498
  investigation: (_t = (_s = overrides.passives) == null ? void 0 : _s.investigation) != null ? _t : passive(scores.int, investigationTri, proficiencyBonus),
485
499
  insight: (_v = (_u = overrides.passives) == null ? void 0 : _u.insight) != null ? _v : passive(scores.wis, insightTri, proficiencyBonus)
486
500
  };
487
- const hpMaxDerived = multiclassMaxHP(resolved.classes, mods.con) + featureEffects.hp_per_level_bonus * totalLevel;
488
- const hpMaxAfterConditions = Math.floor(hpMaxDerived * conditionEffects.hp_max_multiplier);
489
- const hpMax = (_x = (_w = overrides.hp) == null ? void 0 : _w.max) != null ? _x : hpMaxAfterConditions;
501
+ const hpRolled = (_x = (_w = overrides.hp) == null ? void 0 : _w.rolled) != null ? _x : null;
502
+ const hpModifier = (_z = (_y = overrides.hp) == null ? void 0 : _y.modifier) != null ? _z : null;
503
+ const averageDiceSum = hitDiceAverageSum(resolved.classes);
504
+ const conLevels = hpLevelCount(resolved.classes);
505
+ const diceSum = hpRolled != null ? hpRolled : averageDiceSum;
506
+ const diceConRaw = diceSum + mods.con * conLevels;
507
+ const diceConClamped = Math.max(1, diceConRaw);
508
+ const hpMaxDerived = diceConClamped + featureEffects.hp_per_level_bonus * totalLevel + (hpModifier != null ? hpModifier : 0);
509
+ const hpMaxAfterConditions = Math.max(0, Math.floor(hpMaxDerived * conditionEffects.hp_max_multiplier));
510
+ const hpMax = (_B = (_A = overrides.hp) == null ? void 0 : _A.max) != null ? _B : hpMaxAfterConditions;
511
+ const hpBreakdown = {
512
+ diceSum,
513
+ diceSource: hpRolled != null ? "rolled" : "average",
514
+ averageDiceSum,
515
+ conMod: mods.con,
516
+ conLevels,
517
+ clampApplied: diceConClamped !== diceConRaw,
518
+ perLevelTerms: featureEffects.hp_per_level_terms.map((t) => ({
519
+ label: t.label,
520
+ perLevel: t.value,
521
+ levels: totalLevel,
522
+ total: t.value * totalLevel
523
+ })),
524
+ modifier: hpModifier,
525
+ exhaustionMultiplier: conditionEffects.hp_max_multiplier,
526
+ exhaustionLevel: conditionEffects.exhaustion_level,
527
+ derivedMax: hpMaxAfterConditions,
528
+ override: (_D = (_C = overrides.hp) == null ? void 0 : _C.max) != null ? _D : null,
529
+ final: hpMax
530
+ };
490
531
  const featureAcTermsFor = (hasArmor) => featureEffects.ac_terms.filter((t) => !t.requires_armor || hasArmor).map((t) => ({ source: t.label, amount: t.value, kind: "feature" }));
491
532
  const sumTerms = (terms) => terms.reduce((s, t) => s + t.amount, 0);
492
533
  const weaponAbilities = [...featureEffects.weaponAbilities];
@@ -511,7 +552,7 @@ function recalc(resolved, registry) {
511
552
  let acBreakdownDerived = [];
512
553
  let acInformationalDerived = [];
513
554
  if (registry) {
514
- derivedEquipment = computeSlotsAndAttacks(resolved, mods, profsForApply, registry, warnings, proficiencyBonus, weaponAbilities, (_y = resolved.weaponMasteries) != null ? _y : []);
555
+ derivedEquipment = computeSlotsAndAttacks(resolved, mods, profsForApply, registry, warnings, proficiencyBonus, weaponAbilities, (_E = resolved.weaponMasteries) != null ? _E : []);
515
556
  if (derivedEquipment.equippedSlots.armor) {
516
557
  const featTerms = featureAcTermsFor(true);
517
558
  acDerived = derivedEquipment.ac + sumTerms(featTerms);
@@ -534,16 +575,16 @@ function recalc(resolved, registry) {
534
575
  acDerived = total + sumTerms(featTerms);
535
576
  acBreakdownDerived = [...terms, ...featTerms];
536
577
  }
537
- const ac = (_z = overrides.ac) != null ? _z : acDerived;
578
+ const ac = (_F = overrides.ac) != null ? _F : acDerived;
538
579
  const additiveSpeed = speedFromRace(resolved) + applied.speed_bonuses.walk + featureEffects.speed_walk_bonus;
539
580
  const baseSpeed = Math.max(featureEffects.speed_walk_set, additiveSpeed);
540
581
  const adjustedSpeed = baseSpeed * conditionEffects.speed_multiplier - conditionEffects.speed_reduction_ft;
541
582
  const conditionSpeed = conditionEffects.speed_floor_zero ? 0 : Math.max(0, Math.floor(adjustedSpeed));
542
- const speed = (_A = overrides.speed) != null ? _A : conditionSpeed;
583
+ const speed = (_G = overrides.speed) != null ? _G : conditionSpeed;
543
584
  if (!resolved.race) warnings.push("No race resolved; speed defaulted to 30.");
544
- const init = (_B = overrides.initiative) != null ? _B : initiativeBonus(mods.dex, resolved.feats, resolved.definition.edition) + featureEffects.initiative_bonus;
585
+ const init = (_H = overrides.initiative) != null ? _H : initiativeBonus(mods.dex, resolved.feats, resolved.definition.edition) + featureEffects.initiative_bonus;
545
586
  const senses = {
546
- darkvision: Math.max((_E = (_D = (_C = resolved.race) == null ? void 0 : _C.vision) == null ? void 0 : _D.darkvision) != null ? _E : 0, featureEffects.senses.darkvision, applied.senses.darkvision),
587
+ darkvision: Math.max((_K = (_J = (_I = resolved.race) == null ? void 0 : _I.vision) == null ? void 0 : _J.darkvision) != null ? _K : 0, featureEffects.senses.darkvision, applied.senses.darkvision),
547
588
  blindsight: Math.max(featureEffects.senses.blindsight, applied.senses.blindsight),
548
589
  tremorsense: Math.max(featureEffects.senses.tremorsense, applied.senses.tremorsense),
549
590
  truesight: Math.max(featureEffects.senses.truesight, applied.senses.truesight)
@@ -561,8 +602,8 @@ function recalc(resolved, registry) {
561
602
  classSlug: c.entity.slug,
562
603
  className: c.entity.name,
563
604
  ability: profile.ability,
564
- saveDC: (_G = (_F = overrides.spellcasting) == null ? void 0 : _F.saveDC) != null ? _G : dc,
565
- attackBonus: (_I = (_H = overrides.spellcasting) == null ? void 0 : _H.attackBonus) != null ? _I : atk,
605
+ saveDC: (_M = (_L = overrides.spellcasting) == null ? void 0 : _L.saveDC) != null ? _M : dc,
606
+ attackBonus: (_O = (_N = overrides.spellcasting) == null ? void 0 : _N.attackBonus) != null ? _O : atk,
566
607
  casterType: profile.casterType,
567
608
  preparation: profile.preparation
568
609
  });
@@ -574,24 +615,33 @@ function recalc(resolved, registry) {
574
615
  saveDC: spellcastingClasses[0].saveDC,
575
616
  attackBonus: spellcastingClasses[0].attackBonus
576
617
  } : null;
618
+ const abilitySpellcasting = {};
619
+ for (const s of resolved.spells) {
620
+ const ab = s.ability;
621
+ if (!ab || abilitySpellcasting[ab]) continue;
622
+ abilitySpellcasting[ab] = {
623
+ saveDC: saveDC(scores[ab], proficiencyBonus) + applied.spell_save_dc,
624
+ attackBonus: attackBonus(scores[ab], proficiencyBonus) + applied.spell_attack
625
+ };
626
+ }
577
627
  const derivedSlots = deriveSpellSlots(slotInputs);
578
628
  const spellLimits = computeSpellLimits(limitInputs);
579
629
  const defenses = {
580
630
  resistances: dedupeDefenseList([
581
- (_K = (_J = resolved.definition.defenses) == null ? void 0 : _J.resistances) != null ? _K : [],
631
+ (_Q = (_P = resolved.definition.defenses) == null ? void 0 : _P.resistances) != null ? _Q : [],
582
632
  applied.defenses.resistances,
583
633
  featureEffects.resistances
584
634
  ]),
585
635
  immunities: dedupeDefenseList([
586
- (_M = (_L = resolved.definition.defenses) == null ? void 0 : _L.immunities) != null ? _M : [],
636
+ (_S = (_R = resolved.definition.defenses) == null ? void 0 : _R.immunities) != null ? _S : [],
587
637
  applied.defenses.immunities
588
638
  ]),
589
639
  vulnerabilities: dedupeDefenseList([
590
- (_O = (_N = resolved.definition.defenses) == null ? void 0 : _N.vulnerabilities) != null ? _O : [],
640
+ (_U = (_T = resolved.definition.defenses) == null ? void 0 : _T.vulnerabilities) != null ? _U : [],
591
641
  applied.defenses.vulnerabilities
592
642
  ]),
593
643
  condition_immunities: dedupeDefenseList([
594
- (_Q = (_P = resolved.definition.defenses) == null ? void 0 : _P.condition_immunities) != null ? _Q : [],
644
+ (_W = (_V = resolved.definition.defenses) == null ? void 0 : _V.condition_immunities) != null ? _W : [],
595
645
  applied.defenses.condition_immunities,
596
646
  featureEffects.condition_immunities
597
647
  ])
@@ -611,11 +661,13 @@ function recalc(resolved, registry) {
611
661
  current: resolved.state.hp.current,
612
662
  temp: resolved.state.hp.temp
613
663
  },
664
+ hpBreakdown,
614
665
  ac,
615
666
  speed,
616
667
  initiative: init,
617
668
  spellcasting,
618
669
  spellcastingClasses,
670
+ abilitySpellcasting,
619
671
  derivedSpellSlots: derivedSlots.standard,
620
672
  pactMagic: derivedSlots.pact,
621
673
  spellLimits,
@@ -633,7 +685,7 @@ function recalc(resolved, registry) {
633
685
  // untouched rows keep `critRange`/`attackNotes` ABSENT (not 20 / not []).
634
686
  // crit-range: folded weapon crit threshold, only when an effect lowered it.
635
687
  // attackNotes: reroll-damage / attack-rule captions, only when non-empty.
636
- attacks: ((_R = derivedEquipment == null ? void 0 : derivedEquipment.attacks) != null ? _R : []).map((a) => {
688
+ attacks: ((_X = derivedEquipment == null ? void 0 : derivedEquipment.attacks) != null ? _X : []).map((a) => {
637
689
  var _a2;
638
690
  const riders = [...(_a2 = a.damageRiders) != null ? _a2 : [], ...featureEffects.damageBonuses];
639
691
  return {
@@ -644,10 +696,10 @@ function recalc(resolved, registry) {
644
696
  ...riders.length ? { damageRiders: riders } : {}
645
697
  };
646
698
  }),
647
- equippedSlots: (_S = derivedEquipment == null ? void 0 : derivedEquipment.equippedSlots) != null ? _S : {},
648
- carriedWeight: (_T = derivedEquipment == null ? void 0 : derivedEquipment.carriedWeight) != null ? _T : 0,
649
- attunementUsed: (_U = derivedEquipment == null ? void 0 : derivedEquipment.attunementUsed) != null ? _U : 0,
650
- attunementLimit: (_W = derivedEquipment == null ? void 0 : derivedEquipment.attunementLimit) != null ? _W : (_V = overrides.attunement_limit) != null ? _V : 3,
699
+ equippedSlots: (_Y = derivedEquipment == null ? void 0 : derivedEquipment.equippedSlots) != null ? _Y : {},
700
+ carriedWeight: (_Z = derivedEquipment == null ? void 0 : derivedEquipment.carriedWeight) != null ? _Z : 0,
701
+ attunementUsed: (__ = derivedEquipment == null ? void 0 : derivedEquipment.attunementUsed) != null ? __ : 0,
702
+ attunementLimit: (_aa = derivedEquipment == null ? void 0 : derivedEquipment.attunementLimit) != null ? _aa : (_$ = overrides.attunement_limit) != null ? _$ : 3,
651
703
  conditionEffects,
652
704
  rollModifiers: featureEffects.rollModifiers
653
705
  };
@@ -659,6 +711,8 @@ export {
659
711
  computeAbilityScores,
660
712
  computeProficiencies,
661
713
  flattenRaceAsi,
714
+ hitDiceAverageSum,
715
+ hpLevelCount,
662
716
  initiativeBonus,
663
717
  multiclassMaxHP,
664
718
  parseDieSize,
@@ -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);
@@ -32,6 +48,48 @@ export declare function collectChosenWeaponMasteries(character: Character): {
32
48
  * Returns bare slugs (no `[[ ]]`).
33
49
  */
34
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[];
35
93
  /** Selected select-entity values (optional-features) and selected inline
36
94
  * options carrying effects[] become synthesized resolved features (SP2 Plan 3 §9).
37
95
  * The actions/resources engines consume these synthesized features, and the