@iamjameslennon/ddb-mcp 2.7.1 → 2.9.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 (101) hide show
  1. package/README.md +2 -0
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +19 -4
  4. package/dist/browser.js.map +1 -1
  5. package/dist/index.js +58 -38
  6. package/dist/index.js.map +1 -1
  7. package/dist/open5e.d.ts +2 -0
  8. package/dist/open5e.d.ts.map +1 -1
  9. package/dist/open5e.js +4 -0
  10. package/dist/open5e.js.map +1 -1
  11. package/dist/tools/campaign.js +1 -1
  12. package/dist/tools/campaign.js.map +1 -1
  13. package/dist/tools/character/ac.d.ts +20 -0
  14. package/dist/tools/character/ac.d.ts.map +1 -0
  15. package/dist/tools/character/ac.js +83 -0
  16. package/dist/tools/character/ac.js.map +1 -0
  17. package/dist/tools/character/actions.d.ts +22 -0
  18. package/dist/tools/character/actions.d.ts.map +1 -0
  19. package/dist/tools/character/actions.js +110 -0
  20. package/dist/tools/character/actions.js.map +1 -0
  21. package/dist/tools/character/core.d.ts +15 -0
  22. package/dist/tools/character/core.d.ts.map +1 -0
  23. package/dist/tools/character/core.js +129 -0
  24. package/dist/tools/character/core.js.map +1 -0
  25. package/dist/tools/character/defenses.d.ts +16 -0
  26. package/dist/tools/character/defenses.d.ts.map +1 -0
  27. package/dist/tools/character/defenses.js +27 -0
  28. package/dist/tools/character/defenses.js.map +1 -0
  29. package/dist/tools/character/definition.d.ts +14 -0
  30. package/dist/tools/character/definition.d.ts.map +1 -0
  31. package/dist/tools/character/definition.js +205 -0
  32. package/dist/tools/character/definition.js.map +1 -0
  33. package/dist/tools/character/features.d.ts +26 -0
  34. package/dist/tools/character/features.d.ts.map +1 -0
  35. package/dist/tools/character/features.js +107 -0
  36. package/dist/tools/character/features.js.map +1 -0
  37. package/dist/tools/character/helpers.d.ts +57 -0
  38. package/dist/tools/character/helpers.d.ts.map +1 -0
  39. package/dist/tools/character/helpers.js +80 -0
  40. package/dist/tools/character/helpers.js.map +1 -0
  41. package/dist/tools/character/identity.d.ts +19 -0
  42. package/dist/tools/character/identity.d.ts.map +1 -0
  43. package/dist/tools/character/identity.js +84 -0
  44. package/dist/tools/character/identity.js.map +1 -0
  45. package/dist/tools/character/inventory.d.ts +21 -0
  46. package/dist/tools/character/inventory.d.ts.map +1 -0
  47. package/dist/tools/character/inventory.js +61 -0
  48. package/dist/tools/character/inventory.js.map +1 -0
  49. package/dist/tools/character/notes.d.ts +20 -0
  50. package/dist/tools/character/notes.d.ts.map +1 -0
  51. package/dist/tools/character/notes.js +63 -0
  52. package/dist/tools/character/notes.js.map +1 -0
  53. package/dist/tools/character/parse.d.ts +13 -0
  54. package/dist/tools/character/parse.d.ts.map +1 -0
  55. package/dist/tools/character/parse.js +82 -0
  56. package/dist/tools/character/parse.js.map +1 -0
  57. package/dist/tools/character/spells.d.ts +26 -0
  58. package/dist/tools/character/spells.d.ts.map +1 -0
  59. package/dist/tools/character/spells.js +237 -0
  60. package/dist/tools/character/spells.js.map +1 -0
  61. package/dist/tools/character/stats.d.ts +33 -0
  62. package/dist/tools/character/stats.d.ts.map +1 -0
  63. package/dist/tools/character/stats.js +372 -0
  64. package/dist/tools/character/stats.js.map +1 -0
  65. package/dist/tools/character/templates.d.ts +22 -0
  66. package/dist/tools/character/templates.d.ts.map +1 -0
  67. package/dist/tools/character/templates.js +61 -0
  68. package/dist/tools/character/templates.js.map +1 -0
  69. package/dist/tools/character/types.d.ts +133 -0
  70. package/dist/tools/character/types.d.ts.map +1 -0
  71. package/dist/tools/character/types.js +12 -0
  72. package/dist/tools/character/types.js.map +1 -0
  73. package/dist/tools/character/vitals.d.ts +34 -0
  74. package/dist/tools/character/vitals.d.ts.map +1 -0
  75. package/dist/tools/character/vitals.js +198 -0
  76. package/dist/tools/character/vitals.js.map +1 -0
  77. package/dist/tools/character/weapons.d.ts +13 -0
  78. package/dist/tools/character/weapons.d.ts.map +1 -0
  79. package/dist/tools/character/weapons.js +79 -0
  80. package/dist/tools/character/weapons.js.map +1 -0
  81. package/dist/tools/character.d.ts +15 -3
  82. package/dist/tools/character.d.ts.map +1 -1
  83. package/dist/tools/character.js +28 -1182
  84. package/dist/tools/character.js.map +1 -1
  85. package/dist/tools/library.d.ts +32 -4
  86. package/dist/tools/library.d.ts.map +1 -1
  87. package/dist/tools/library.js +181 -86
  88. package/dist/tools/library.js.map +1 -1
  89. package/dist/tools/monster.d.ts +2 -0
  90. package/dist/tools/monster.d.ts.map +1 -1
  91. package/dist/tools/monster.js +4 -0
  92. package/dist/tools/monster.js.map +1 -1
  93. package/dist/tools/reference.d.ts +5 -0
  94. package/dist/tools/reference.d.ts.map +1 -1
  95. package/dist/tools/reference.js +76 -69
  96. package/dist/tools/reference.js.map +1 -1
  97. package/dist/tools/search.d.ts +1 -2
  98. package/dist/tools/search.d.ts.map +1 -1
  99. package/dist/tools/search.js +36 -59
  100. package/dist/tools/search.js.map +1 -1
  101. package/package.json +2 -1
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Identity domain — name, race (with 2024 variant detection), class line,
3
+ * background, XP, inspiration. Owns the header block.
4
+ *
5
+ * Phase 3 of the character.ts refactor — see docs/character-refactor.md.
6
+ */
7
+ import { arr, num, obj, str } from "./helpers.js";
8
+ /**
9
+ * 2024 races store the sub-selection (Elven Lineage, Fiendish Legacy, Giant
10
+ * Ancestry, Gnomish Lineage, …) in `char.options.race[]` rather than in the
11
+ * subRaceShortName field that 2014 characters use. The option name encodes
12
+ * the chosen variant — e.g. "Wood Elf Lineage", "Infernal Legacy",
13
+ * "Stone's Endurance (Stone Giant)". Pattern-match the suffix so this is
14
+ * generic across every 2024 race that follows the same naming convention.
15
+ *
16
+ * Note: 2024 Aasimar Celestial Revelation is *not* a creation-time choice —
17
+ * the player picks one of Heavenly Wings / Inner Radiance / Necrotic Shroud
18
+ * each time they transform, and `char.options.race` is empty for Aasimar.
19
+ * Header correctly shows just "Aasimar" with no parenthetical.
20
+ */
21
+ function detectRaceVariant(char) {
22
+ for (const opt of arr(obj(char.options).race)) {
23
+ const name = str(obj(opt.definition).name).trim();
24
+ if (!name)
25
+ continue;
26
+ // "Wood Elf Lineage" / "High Elf Lineage" / "Drow Lineage"
27
+ // / "Forest Gnome Lineage" / "Rock Gnome Lineage" / …
28
+ let m = name.match(/^(.+?)\s+Lineage$/);
29
+ if (m)
30
+ return m[1];
31
+ // "Infernal Legacy" / "Abyssal Legacy" / "Chthonic Legacy"
32
+ m = name.match(/^(.+?)\s+Legacy$/);
33
+ if (m)
34
+ return m[1];
35
+ // Giant Ancestry: "Stone's Endurance (Stone Giant)" etc.
36
+ m = name.match(/\(([A-Za-z]+)\s+Giant\)$/);
37
+ if (m)
38
+ return m[1];
39
+ }
40
+ return null;
41
+ }
42
+ function deriveRace(char) {
43
+ const base = str(obj(char.race).fullName || obj(char.race).baseName);
44
+ const variant = detectRaceVariant(char);
45
+ if (!variant)
46
+ return base;
47
+ // Avoid "Wood Elf (Wood Elf)" if the base race name already mentions the
48
+ // variant (defensive — happens when fullName has been pre-decorated).
49
+ if (base.toLowerCase().includes(variant.toLowerCase()))
50
+ return base;
51
+ return `${base} (${variant})`;
52
+ }
53
+ function buildClassLine(classes) {
54
+ return classes.map(c => {
55
+ const def = obj(c.definition);
56
+ const sub = obj(c.subclassDefinition);
57
+ const lvl = num(c.level);
58
+ return sub.name ? `${def.name} (${sub.name}) ${lvl}` : `${def.name} ${lvl}`;
59
+ }).join(" / ");
60
+ }
61
+ export function computeIdentity(core) {
62
+ const { char, classes, totalLevel } = core;
63
+ return {
64
+ charName: str(char.name),
65
+ race: deriveRace(char),
66
+ classLine: buildClassLine(classes),
67
+ totalLevel,
68
+ background: str(obj(obj(char.background).definition).name),
69
+ xp: num(char.currentXp),
70
+ inspiration: !!char.inspiration,
71
+ };
72
+ }
73
+ export function formatHeaderBlock(i) {
74
+ return [
75
+ `═══════════════════════════════════════`,
76
+ ` ${i.charName}`,
77
+ ` ${i.race} | ${i.classLine} | Level ${i.totalLevel}`,
78
+ ` Background: ${i.background || "—"} | XP: ${i.xp}`,
79
+ ` Inspiration: ${i.inspiration ? "Yes" : "No"}`,
80
+ `═══════════════════════════════════════`,
81
+ ``,
82
+ ];
83
+ }
84
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../../src/tools/character/identity.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAalD;;;;;;;;;;;;GAYG;AACH,SAAS,iBAAiB,CAAC,IAAc;IACvC,KAAK,MAAM,GAAG,IAAI,GAAG,CAA0B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,2DAA2D;QAC3D,uDAAuD;QACvD,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACxC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACnB,2DAA2D;QAC3D,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACnC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACnB,yDAAyD;QACzD,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC3C,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,UAAU,CAAC,IAAc;IAChC,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,yEAAyE;IACzE,sEAAsE;IACtE,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACpE,OAAO,GAAG,IAAI,KAAK,OAAO,GAAG,CAAC;AAChC,CAAC;AAED,SAAS,cAAc,CAAC,OAA8B;IACpD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACrB,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACzB,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;IAC9E,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAe;IAC7C,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAC3C,OAAO;QACL,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;QACxB,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC;QACtB,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC;QAClC,UAAU;QACV,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;QAC1D,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;QACvB,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW;KAChC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,CAAW;IAC3C,OAAO;QACL,yCAAyC;QACzC,KAAK,CAAC,CAAC,QAAQ,EAAE;QACjB,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,UAAU,EAAE;QACtD,iBAAiB,CAAC,CAAC,UAAU,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,EAAE;QACpD,kBAAkB,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE;QAChD,yCAAyC;QACzC,EAAE;KACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Inventory domain — equipped armor, carried (non-weapon) items, attunement
3
+ * count, and currency. Owns the inventory block.
4
+ *
5
+ * Phase 8 of the character.ts refactor — see docs/character-refactor.md.
6
+ *
7
+ * Weapons are intentionally excluded — they're surfaced in the ACTIONS
8
+ * block (Phase 6a). Equipped armor is shown separately with its AC value;
9
+ * everything else non-weapon is rolled into a single comma-separated
10
+ * INVENTORY line with ×N consolidation.
11
+ */
12
+ import type { CoreStats } from "./types.js";
13
+ export interface Inventory {
14
+ equippedNonWeapons: string[];
15
+ inventoryLine: string;
16
+ attuned: number;
17
+ currencyLine: string;
18
+ }
19
+ export declare function computeInventory(core: CoreStats): Inventory;
20
+ export declare function formatInventoryBlock(inv: Inventory): string[];
21
+ //# sourceMappingURL=inventory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../../src/tools/character/inventory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAY,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,WAAW,SAAS;IACxB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAUD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CA2B3D;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,CAU7D"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Inventory domain — equipped armor, carried (non-weapon) items, attunement
3
+ * count, and currency. Owns the inventory block.
4
+ *
5
+ * Phase 8 of the character.ts refactor — see docs/character-refactor.md.
6
+ *
7
+ * Weapons are intentionally excluded — they're surfaced in the ACTIONS
8
+ * block (Phase 6a). Equipped armor is shown separately with its AC value;
9
+ * everything else non-weapon is rolled into a single comma-separated
10
+ * INVENTORY line with ×N consolidation.
11
+ */
12
+ import { num, obj, str } from "./helpers.js";
13
+ function buildCurrencyLine(char) {
14
+ const currencies = obj(char.currencies);
15
+ return ["pp", "gp", "ep", "sp", "cp"]
16
+ .map(c => `${num(currencies[c])}${c}`)
17
+ .filter(c => !c.startsWith("0"))
18
+ .join(", ") || "none";
19
+ }
20
+ export function computeInventory(core) {
21
+ const { char, inventory } = core;
22
+ const equippedNonWeapons = [];
23
+ const carriedItems = new Map();
24
+ let attuned = 0;
25
+ for (const i of inventory) {
26
+ const def = obj(i.definition);
27
+ const iName = str(def.name);
28
+ const filterType = str(def.filterType);
29
+ const qty = num(i.quantity) || 1;
30
+ if (i.isAttuned)
31
+ attuned++;
32
+ if (i.equipped && filterType === "Armor") {
33
+ const ac2 = num(def.armorClass);
34
+ equippedNonWeapons.push(`${iName}${ac2 ? ` (AC ${ac2})` : ""}`);
35
+ }
36
+ else if (filterType !== "Weapon") {
37
+ carriedItems.set(iName, (carriedItems.get(iName) ?? 0) + qty);
38
+ }
39
+ }
40
+ const inventoryLine = [...carriedItems.entries()]
41
+ .map(([n, q]) => q > 1 ? `${n} ×${q}` : n)
42
+ .join(", ");
43
+ return {
44
+ equippedNonWeapons,
45
+ inventoryLine,
46
+ attuned,
47
+ currencyLine: buildCurrencyLine(char),
48
+ };
49
+ }
50
+ export function formatInventoryBlock(inv) {
51
+ return [
52
+ ...(inv.equippedNonWeapons.length
53
+ ? [`EQUIPPED`, ...inv.equippedNonWeapons.map(e => ` ${e}`), ``]
54
+ : []),
55
+ ...(inv.inventoryLine ? [`INVENTORY`, ` ${inv.inventoryLine}`, ``] : []),
56
+ `ATTUNEMENT: ${inv.attuned}/3 slots used`,
57
+ ``,
58
+ `CURRENCY: ${inv.currencyLine}`,
59
+ ];
60
+ }
61
+ //# sourceMappingURL=inventory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inventory.js","sourceRoot":"","sources":["../../../src/tools/character/inventory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAU7C,SAAS,iBAAiB,CAAC,IAAc;IACvC,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACxC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;SAClC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;SACrC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;SAC/B,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAe;IAC9C,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IACjC,MAAM,kBAAkB,GAAa,EAAE,CAAC;IACxC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACvC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,QAAQ,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC;YACzC,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAChC,kBAAkB,CAAC,IAAI,CAAC,GAAG,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClE,CAAC;aAAM,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;YACnC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IACD,MAAM,aAAa,GAAG,CAAC,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC;SAC9C,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;SACzC,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO;QACL,kBAAkB;QAClB,aAAa;QACb,OAAO;QACP,YAAY,EAAE,iBAAiB,CAAC,IAAI,CAAC;KACtC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,GAAc;IACjD,OAAO;QACL,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,MAAM;YAC/B,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAChE,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC,aAAa,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,eAAe,GAAG,CAAC,OAAO,eAAe;QACzC,EAAE;QACF,aAAa,GAAG,CAAC,YAAY,EAAE;KAChC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Notes domain — personality traits, backstory, allies/organisations, and
3
+ * additional possessions/notes. Owns the notes block.
4
+ *
5
+ * Phase 8 of the character.ts refactor — see docs/character-refactor.md.
6
+ *
7
+ * Uses the full `stripHtml` from `../../utils.js` (which decodes HTML
8
+ * entities) rather than the lightweight helpers version — backstory and
9
+ * traits frequently contain entities like `’` and `—`.
10
+ */
11
+ import type { CoreStats } from "./types.js";
12
+ export interface Notes {
13
+ traitLines: string[];
14
+ backstoryText: string | null;
15
+ allyLines: string[];
16
+ extraLines: string[];
17
+ }
18
+ export declare function computeNotes(core: CoreStats): Notes;
19
+ export declare function formatNotesBlock(n: Notes): string[];
20
+ //# sourceMappingURL=notes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notes.d.ts","sourceRoot":"","sources":["../../../src/tools/character/notes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,KAAK;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAYD,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CA6BnD;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,CAWnD"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Notes domain — personality traits, backstory, allies/organisations, and
3
+ * additional possessions/notes. Owns the notes block.
4
+ *
5
+ * Phase 8 of the character.ts refactor — see docs/character-refactor.md.
6
+ *
7
+ * Uses the full `stripHtml` from `../../utils.js` (which decodes HTML
8
+ * entities) rather than the lightweight helpers version — backstory and
9
+ * traits frequently contain entities like `’` and `—`.
10
+ */
11
+ import { stripHtml } from "../../utils.js";
12
+ import { obj, str } from "./helpers.js";
13
+ const field = (v) => {
14
+ const s = stripHtml(str(v)).trim();
15
+ return s || null;
16
+ };
17
+ const labelled = (label, pad, v) => {
18
+ const t = field(v);
19
+ return t ? ` ${label.padEnd(pad)}${t}` : null;
20
+ };
21
+ export function computeNotes(core) {
22
+ const { char } = core;
23
+ const traits = obj(char.traits);
24
+ const notes = obj(char.notes);
25
+ const traitLines = [
26
+ labelled("Traits:", 13, traits.personalityTraits),
27
+ labelled("Ideals:", 13, traits.ideals),
28
+ labelled("Bonds:", 13, traits.bonds),
29
+ labelled("Flaws:", 13, traits.flaws),
30
+ labelled("Appearance:", 13, traits.appearance),
31
+ ].filter((l) => l !== null);
32
+ const allyLines = [
33
+ labelled("Allies:", 15, notes.allies),
34
+ labelled("Organisations:", 15, notes.organizations),
35
+ ].filter((l) => l !== null);
36
+ const extraLines = [
37
+ labelled("Possessions:", 13, notes.personalPossessions),
38
+ labelled("Other:", 13, notes.otherNotes),
39
+ ].filter((l) => l !== null);
40
+ return {
41
+ traitLines,
42
+ backstoryText: field(notes.backstory),
43
+ allyLines,
44
+ extraLines,
45
+ };
46
+ }
47
+ export function formatNotesBlock(n) {
48
+ const hasAny = n.traitLines.length || n.backstoryText || n.allyLines.length || n.extraLines.length;
49
+ if (!hasAny) {
50
+ return ["No notes or backstory have been recorded for this character."];
51
+ }
52
+ const out = [];
53
+ if (n.traitLines.length)
54
+ out.push("PERSONALITY", ...n.traitLines, "");
55
+ if (n.backstoryText)
56
+ out.push("BACKSTORY", ` ${n.backstoryText}`, "");
57
+ if (n.allyLines.length)
58
+ out.push("ALLIES & ORGANISATIONS", ...n.allyLines, "");
59
+ if (n.extraLines.length)
60
+ out.push("ADDITIONAL NOTES", ...n.extraLines, "");
61
+ return out;
62
+ }
63
+ //# sourceMappingURL=notes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notes.js","sourceRoot":"","sources":["../../../src/tools/character/notes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `parseCharacterData(raw, sections)` — the orchestrator.
3
+ *
4
+ * Phase 9 of the character.ts refactor — see docs/character-refactor.md.
5
+ *
6
+ * Every domain (identity, vitals, ac, stats, defenses, features, actions,
7
+ * spells, inventory, notes) computed once via `computeCoreStats`, then
8
+ * routed through `sections` to pick which blocks to render. No business
9
+ * logic lives here — only the section → block routing.
10
+ */
11
+ import type { CharData, ParseSection } from "./types.js";
12
+ export declare function parseCharacterData(raw: CharData, sections?: ParseSection): string;
13
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../src/tools/character/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAYzD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,QAAQ,EACb,QAAQ,GAAE,YAAqB,GAC9B,MAAM,CAkER"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `parseCharacterData(raw, sections)` — the orchestrator.
3
+ *
4
+ * Phase 9 of the character.ts refactor — see docs/character-refactor.md.
5
+ *
6
+ * Every domain (identity, vitals, ac, stats, defenses, features, actions,
7
+ * spells, inventory, notes) computed once via `computeCoreStats`, then
8
+ * routed through `sections` to pick which blocks to render. No business
9
+ * logic lives here — only the section → block routing.
10
+ */
11
+ import { addCharacterSpellsToCompendium } from "../reference.js";
12
+ import { computeCoreStats } from "./core.js";
13
+ import { computeIdentity, formatHeaderBlock } from "./identity.js";
14
+ import { computeVitals, formatVitalsBlock } from "./vitals.js";
15
+ import { computeAc } from "./ac.js";
16
+ import { computeStats, formatStatsBlock } from "./stats.js";
17
+ import { computeDefenses, formatDefensesBlock } from "./defenses.js";
18
+ import { computeFeatures, formatFeaturesBlock } from "./features.js";
19
+ import { computeActions, formatCombatBlock } from "./actions.js";
20
+ import { computeSpells, formatSpellsBlock, formatConcentrationBlock } from "./spells.js";
21
+ import { computeInventory, formatInventoryBlock } from "./inventory.js";
22
+ import { computeNotes, formatNotesBlock } from "./notes.js";
23
+ export function parseCharacterData(raw, sections = "full") {
24
+ const char = (raw?.data ?? raw);
25
+ // Supplement spell compendium with this character's chosen spells (cantrips etc.)
26
+ addCharacterSpellsToCompendium(char);
27
+ // computeCoreStats produces every value used across multiple sections.
28
+ // After phases 3–8 every domain consumes `core` directly; only profBonus
29
+ // is still needed inline (threaded into vitals for the formatter's prof line).
30
+ const core = computeCoreStats(char);
31
+ const { profBonus } = core;
32
+ const identity = computeIdentity(core);
33
+ const vitals = computeVitals(core);
34
+ const ac = computeAc(core);
35
+ const stats = computeStats(core);
36
+ const defenses = computeDefenses(core);
37
+ const features = computeFeatures(core);
38
+ const actions = computeActions(core);
39
+ const spells = computeSpells(core);
40
+ const inv = computeInventory(core);
41
+ const notes = computeNotes(core);
42
+ const headerBlock = formatHeaderBlock(identity);
43
+ const vitalsBlock = formatVitalsBlock(vitals, ac, profBonus);
44
+ const statsBlock = formatStatsBlock(stats);
45
+ const defensesBlock = formatDefensesBlock(defenses);
46
+ const featuresBlock = formatFeaturesBlock(features);
47
+ const combatBlock = formatCombatBlock(actions);
48
+ const spellsBlock = formatSpellsBlock(spells);
49
+ const concentrationBlock = formatConcentrationBlock(spells);
50
+ const inventoryBlock = formatInventoryBlock(inv);
51
+ const notesBlock = formatNotesBlock(notes);
52
+ const out = [...headerBlock];
53
+ switch (sections) {
54
+ case "summary":
55
+ out.push(...vitalsBlock, ...statsBlock);
56
+ break;
57
+ case "combat":
58
+ out.push(...vitalsBlock, ...statsBlock, ...defensesBlock, ...combatBlock);
59
+ break;
60
+ case "spells":
61
+ out.push(...(spellsBlock.length ? spellsBlock : ["No spellcasting on this character."]));
62
+ break;
63
+ case "inventory":
64
+ out.push(...inventoryBlock);
65
+ break;
66
+ case "features":
67
+ out.push(...featuresBlock);
68
+ break;
69
+ case "concentration":
70
+ out.push(...concentrationBlock);
71
+ break;
72
+ case "notes":
73
+ out.push(...notesBlock);
74
+ break;
75
+ case "full":
76
+ default:
77
+ out.push(...vitalsBlock, ...statsBlock, ...defensesBlock, ...featuresBlock, ...combatBlock, ...spellsBlock, ...inventoryBlock, ...notesBlock);
78
+ break;
79
+ }
80
+ return out.join("\n");
81
+ }
82
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/tools/character/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Spells domain — spellcasting stats, spell slots, the prepared/known spell
3
+ * listing (with cross-source duplicate detection), and the concentration
4
+ * block. Owns the SPELLS block and the CONCENTRATION SPELLS block.
5
+ *
6
+ * Phase 7 of the character.ts refactor — see docs/character-refactor.md.
7
+ *
8
+ * Concentration detection delegates to `isConcentrationSpell` in
9
+ * `./reference.ts` (compendium-backed) and falls back to the per-spell
10
+ * `concentration` flag. Spellbook (Wizard) prepared/ritual gate is applied
11
+ * to leveled spells but not cantrips.
12
+ *
13
+ * Output is pre-formatted strings to preserve byte-for-byte parity with
14
+ * the original inline code.
15
+ */
16
+ import type { CoreStats } from "./types.js";
17
+ export interface Spells {
18
+ spellcastingLines: string[];
19
+ slotLines: string[];
20
+ spellSections: string[];
21
+ concentrationByLevel: Map<number, string[]>;
22
+ }
23
+ export declare function computeSpells(core: CoreStats): Spells;
24
+ export declare function formatSpellsBlock(s: Spells): string[];
25
+ export declare function formatConcentrationBlock(s: Spells): string[];
26
+ //# sourceMappingURL=spells.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spells.d.ts","sourceRoot":"","sources":["../../../src/tools/character/spells.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAwB,SAAS,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,WAAW,MAAM;IACrB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC7C;AA2MD,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAQrD;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAMrD;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAc5D"}
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Spells domain — spellcasting stats, spell slots, the prepared/known spell
3
+ * listing (with cross-source duplicate detection), and the concentration
4
+ * block. Owns the SPELLS block and the CONCENTRATION SPELLS block.
5
+ *
6
+ * Phase 7 of the character.ts refactor — see docs/character-refactor.md.
7
+ *
8
+ * Concentration detection delegates to `isConcentrationSpell` in
9
+ * `./reference.ts` (compendium-backed) and falls back to the per-spell
10
+ * `concentration` flag. Spellbook (Wizard) prepared/ritual gate is applied
11
+ * to leveled spells but not cantrips.
12
+ *
13
+ * Output is pre-formatted strings to preserve byte-for-byte parity with
14
+ * the original inline code.
15
+ */
16
+ import { isConcentrationSpell } from "../reference.js";
17
+ import { arr, num, obj, signed, statNames, str } from "./helpers.js";
18
+ const SOURCE_LABELS = {
19
+ race: "Racial Trait", class: "Class Feature", background: "Background", feat: "Feat", item: "Item",
20
+ };
21
+ const SLOT_ORDINAL = ["", "1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th"];
22
+ const slotOrdinal = (lvl) => SLOT_ORDINAL[lvl] ?? `${lvl}th`;
23
+ // ── Spellcasting summary (per spellcasting class) ────────────────────────────
24
+ function buildSpellcastingLines(classes, statMods, profBonus) {
25
+ // spellCastingAbilityId: 1=STR 2=DEX 3=CON 4=INT 5=WIS 6=CHA
26
+ const lines = [];
27
+ for (const c of classes) {
28
+ const def = obj(c.definition);
29
+ const subDef = obj(c.subclassDefinition);
30
+ const classCasts = def.canCastSpells === true;
31
+ const subclassCasts = subDef.canCastSpells === true;
32
+ if (!classCasts && !subclassCasts)
33
+ continue;
34
+ const abilityId = num(classCasts ? def.spellCastingAbilityId : subDef.spellCastingAbilityId);
35
+ if (!abilityId)
36
+ continue;
37
+ const className = classCasts ? str(def.name) : `${str(def.name)} (${str(subDef.name)})`;
38
+ const abilityMod = statMods[abilityId - 1];
39
+ const spellAttack = abilityMod + profBonus;
40
+ const saveDc = 8 + abilityMod + profBonus;
41
+ lines.push(` ${className}: ${statNames[abilityId - 1]} Spell Attack: ${signed(spellAttack)} Save DC: ${saveDc}`);
42
+ }
43
+ return lines;
44
+ }
45
+ // ── Spell slots (max from levelSpellSlots, used from char.spellSlots) ────────
46
+ function buildSlotLines(char, classes) {
47
+ // char.spellSlots only tracks used counts; max slots come from the class's
48
+ // levelSpellSlots progression table: levelSpellSlots[classLevel][slotLevel-1]
49
+ const spellSlotUsed = {};
50
+ for (const s of arr(char.spellSlots)) {
51
+ spellSlotUsed[num(s.level)] = num(s.used);
52
+ }
53
+ const slotMax = {};
54
+ for (const c of classes) {
55
+ // Only compute slots for classes/subclasses that actually grant spellcasting.
56
+ // Non-spellcasting base classes (Barbarian, Rogue, Monk, etc.) have canCastSpells: false
57
+ // but still carry non-empty levelSpellSlots tables — skip those.
58
+ // Spellcasting subclasses (Arcane Trickster, Eldritch Knight) set canCastSpells on
59
+ // the subclassDefinition instead, so check both.
60
+ const classCasts = obj(c.definition).canCastSpells === true;
61
+ const subclassCasts = obj(c.subclassDefinition).canCastSpells === true;
62
+ if (!classCasts && !subclassCasts)
63
+ continue;
64
+ const spellRules = obj(obj(c.definition).spellRules);
65
+ const rawTable = spellRules.levelSpellSlots;
66
+ const table = Array.isArray(rawTable) ? rawTable : [];
67
+ const lvl = num(c.level);
68
+ const row = table[lvl] ?? [];
69
+ for (let i = 0; i < row.length; i++) {
70
+ if (row[i] > 0)
71
+ slotMax[i + 1] = (slotMax[i + 1] ?? 0) + row[i];
72
+ }
73
+ }
74
+ return Object.entries(slotMax)
75
+ .sort(([a], [b]) => Number(a) - Number(b))
76
+ .map(([lvl, max]) => {
77
+ const used = spellSlotUsed[Number(lvl)] ?? 0;
78
+ return ` Level ${lvl}: ${max - used}/${max}`;
79
+ });
80
+ }
81
+ // ── Spell listing (per class + per non-class source + duplicate warnings) ────
82
+ // Pre-seeds seenSpellIds during the per-class walk so duplicate detection
83
+ // across sources is consistent regardless of section order.
84
+ function buildSpellSections(char, classes) {
85
+ const sections = [];
86
+ const classSpells = arr(char.classSpells);
87
+ const seenSpellIds = new Map(); // spellId → first source label
88
+ for (const cs of classSpells) {
89
+ // Try characterClassId first; fall back to id/classId for 2024-rules format
90
+ const classEntry = classes.find(c => c.id === cs.characterClassId ||
91
+ c.id === cs.id ||
92
+ c.id === cs.classId);
93
+ const className = str(obj(classEntry?.definition ?? {}).name);
94
+ const isSpellbook = className === "Wizard";
95
+ // spells may be under cs.spells or cs.classSpells (2024 format variation)
96
+ const allSpells = arr(cs.spells).length > 0
97
+ ? arr(cs.spells)
98
+ : arr(cs.classSpells);
99
+ const cantrips = allSpells
100
+ .filter(s => num(obj(s.definition).level) === 0)
101
+ .map(s => str(obj(s.definition).name));
102
+ const leveled = allSpells
103
+ .filter(s => {
104
+ if (num(obj(s.definition).level) === 0)
105
+ return false;
106
+ if (isSpellbook)
107
+ return s.prepared === true || obj(s.definition).ritual === true;
108
+ return true;
109
+ })
110
+ .map(s => {
111
+ const def = obj(s.definition);
112
+ const ritual = isSpellbook && def.ritual ? " [ritual]" : "";
113
+ return `${str(def.name)} (L${num(def.level)}${ritual})`;
114
+ });
115
+ if (cantrips.length)
116
+ sections.push(` Cantrips: ${cantrips.join(", ")}`);
117
+ if (leveled.length)
118
+ sections.push(` Spells: ${leveled.join(", ")}`);
119
+ // Pre-seed duplicate detection in the same pass
120
+ for (const s of allSpells) {
121
+ const spellId = num(obj(s.definition).id);
122
+ if (spellId && !seenSpellIds.has(spellId))
123
+ seenSpellIds.set(spellId, "Spells");
124
+ }
125
+ }
126
+ const spellsObj = obj(char.spells);
127
+ const duplicateWarnings = [];
128
+ for (const [key, label] of Object.entries(SOURCE_LABELS)) {
129
+ const spellList = arr(spellsObj[key]);
130
+ if (!spellList.length)
131
+ continue;
132
+ const names = [...new Set(spellList
133
+ .filter(s => {
134
+ const def = obj(s.definition);
135
+ const spellId = num(def.id);
136
+ if (!spellId)
137
+ return true;
138
+ if (seenSpellIds.has(spellId)) {
139
+ const firstLabel = seenSpellIds.get(spellId);
140
+ const spellName = str(def.name);
141
+ const lvl = num(def.level);
142
+ const spellStr = lvl === 0 ? spellName : `${spellName} (L${lvl})`;
143
+ duplicateWarnings.push(` • ${spellStr} — already granted by ${firstLabel}, also in ${label}`);
144
+ return false;
145
+ }
146
+ seenSpellIds.set(spellId, label);
147
+ return true;
148
+ })
149
+ .map(s => {
150
+ const def = obj(s.definition);
151
+ const n = str(def.name);
152
+ return n ? (num(def.level) === 0 ? n : `${n} (L${num(def.level)})`) : "";
153
+ })
154
+ .filter(n => n.length > 0))];
155
+ if (names.length)
156
+ sections.push(` From ${label}: ${names.join(", ")}`);
157
+ }
158
+ if (duplicateWarnings.length) {
159
+ sections.push(` ⚠ Duplicate spell grants detected — the following spells are already`, ` provided by an earlier source; the extra grant may be a wasted choice:`, ...duplicateWarnings);
160
+ }
161
+ return sections;
162
+ }
163
+ // ── Concentration spell collection ───────────────────────────────────────────
164
+ // Collects all available/prepared spells, filters to concentration:true,
165
+ // groups by spell level. Spellbook gate is applied to leveled spells but
166
+ // not cantrips (cantrips don't go through prepared/known lists).
167
+ function buildConcentrationByLevel(char, classes) {
168
+ const concByLevel = new Map();
169
+ const addConcSpell = (s) => {
170
+ const def = obj(s.definition);
171
+ const name = str(def.name);
172
+ if (!name)
173
+ return;
174
+ const level = num(def.level);
175
+ const fromCompendium = isConcentrationSpell(name);
176
+ const isConc = fromCompendium !== null ? fromCompendium : def.concentration === true;
177
+ if (!isConc)
178
+ return;
179
+ const bucket = concByLevel.get(level) ?? [];
180
+ if (!bucket.includes(name)) {
181
+ bucket.push(name);
182
+ concByLevel.set(level, bucket);
183
+ }
184
+ };
185
+ for (const cs of arr(char.classSpells)) {
186
+ const classEntry = classes.find(c => c.id === cs.characterClassId || c.id === cs.id || c.id === cs.classId);
187
+ const isSpellbook = str(obj(classEntry?.definition ?? {}).name) === "Wizard";
188
+ const allSp = arr(cs.spells).length > 0
189
+ ? arr(cs.spells)
190
+ : arr(cs.classSpells);
191
+ for (const s of allSp) {
192
+ const def = obj(s.definition);
193
+ if (num(def.level) > 0 && isSpellbook && !(s.prepared === true || def.ritual === true))
194
+ continue;
195
+ addConcSpell(s);
196
+ }
197
+ }
198
+ for (const spellList of Object.values(obj(char.spells))) {
199
+ for (const s of arr(spellList))
200
+ addConcSpell(s);
201
+ }
202
+ return concByLevel;
203
+ }
204
+ // ── Public API ───────────────────────────────────────────────────────────────
205
+ export function computeSpells(core) {
206
+ const { char, classes, statMods, profBonus } = core;
207
+ return {
208
+ spellcastingLines: buildSpellcastingLines(classes, statMods, profBonus),
209
+ slotLines: buildSlotLines(char, classes),
210
+ spellSections: buildSpellSections(char, classes),
211
+ concentrationByLevel: buildConcentrationByLevel(char, classes),
212
+ };
213
+ }
214
+ export function formatSpellsBlock(s) {
215
+ return [
216
+ ...(s.spellcastingLines.length ? [`SPELLCASTING`, ...s.spellcastingLines, ``] : []),
217
+ ...(s.slotLines.length ? [`SPELL SLOTS`, ...s.slotLines, ``] : []),
218
+ ...(s.spellSections.length ? [`SPELLS`, ...s.spellSections, ``] : []),
219
+ ];
220
+ }
221
+ export function formatConcentrationBlock(s) {
222
+ if (s.concentrationByLevel.size === 0) {
223
+ return ["This character has no concentration spells prepared."];
224
+ }
225
+ const out = ["CONCENTRATION SPELLS"];
226
+ for (const lvl of [...s.concentrationByLevel.keys()].sort((a, b) => a - b)) {
227
+ out.push(lvl === 0 ? " Cantrips (no slot required):" : ` Level ${lvl}:`);
228
+ for (const name of s.concentrationByLevel.get(lvl)) {
229
+ out.push(` • ${name}${lvl > 0 ? ` [${slotOrdinal(lvl)}-level slot]` : ""}`);
230
+ }
231
+ }
232
+ out.push("");
233
+ if (s.slotLines.length)
234
+ out.push("SPELL SLOTS", ...s.slotLines);
235
+ return out;
236
+ }
237
+ //# sourceMappingURL=spells.js.map