@carmaclouds/core 2.3.1

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 (118) hide show
  1. package/dist/cache/CacheManager.d.ts.map +1 -0
  2. package/dist/cache/CacheManager.js +131 -0
  3. package/dist/cache/CacheManager.js.map +1 -0
  4. package/dist/index.d.ts +18 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +22 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/ir/index.d.ts +11 -0
  9. package/dist/ir/index.d.ts.map +1 -0
  10. package/dist/ir/index.js +9 -0
  11. package/dist/ir/index.js.map +1 -0
  12. package/dist/ir/normalize.d.ts +10 -0
  13. package/dist/ir/normalize.d.ts.map +1 -0
  14. package/dist/ir/normalize.js +207 -0
  15. package/dist/ir/normalize.js.map +1 -0
  16. package/dist/ir/persistence.d.ts +26 -0
  17. package/dist/ir/persistence.d.ts.map +1 -0
  18. package/dist/ir/persistence.js +21 -0
  19. package/dist/ir/persistence.js.map +1 -0
  20. package/dist/ir/sync.d.ts +12 -0
  21. package/dist/ir/sync.d.ts.map +1 -0
  22. package/dist/ir/sync.js +36 -0
  23. package/dist/ir/sync.js.map +1 -0
  24. package/dist/ir/types.d.ts +143 -0
  25. package/dist/ir/types.d.ts.map +1 -0
  26. package/dist/ir/types.js +13 -0
  27. package/dist/ir/types.js.map +1 -0
  28. package/dist/ir/views/dnd5e.d.ts +40 -0
  29. package/dist/ir/views/dnd5e.d.ts.map +1 -0
  30. package/dist/ir/views/dnd5e.js +50 -0
  31. package/dist/ir/views/dnd5e.js.map +1 -0
  32. package/dist/render/character.d.ts +19 -0
  33. package/dist/render/character.d.ts.map +1 -0
  34. package/dist/render/character.js +156 -0
  35. package/dist/render/character.js.map +1 -0
  36. package/dist/render/h.d.ts +27 -0
  37. package/dist/render/h.d.ts.map +1 -0
  38. package/dist/render/h.js +64 -0
  39. package/dist/render/h.js.map +1 -0
  40. package/dist/render/index.d.ts +11 -0
  41. package/dist/render/index.d.ts.map +1 -0
  42. package/dist/render/index.js +8 -0
  43. package/dist/render/index.js.map +1 -0
  44. package/dist/render/mount.d.ts +31 -0
  45. package/dist/render/mount.d.ts.map +1 -0
  46. package/dist/render/mount.js +63 -0
  47. package/dist/render/mount.js.map +1 -0
  48. package/dist/supabase/fields.d.ts.map +1 -0
  49. package/dist/supabase/fields.js +120 -0
  50. package/dist/supabase/fields.js.map +1 -0
  51. package/dist/types/character.d.ts.map +1 -0
  52. package/dist/types/character.js +5 -0
  53. package/dist/types/character.js.map +1 -0
  54. package/package.json +73 -0
  55. package/src/browser.js +51 -0
  56. package/src/cache/CacheManager.ts +174 -0
  57. package/src/common/browser-polyfill.js +319 -0
  58. package/src/common/debug.js +123 -0
  59. package/src/common/html-utils.js +134 -0
  60. package/src/common/theme-manager.js +265 -0
  61. package/src/index.ts +25 -0
  62. package/src/ir/__fixtures__/dnd5e-character.json +75962 -0
  63. package/src/ir/__fixtures__/non-dnd-character.json +14218 -0
  64. package/src/ir/index.ts +10 -0
  65. package/src/ir/normalize.ts +245 -0
  66. package/src/ir/persistence.ts +37 -0
  67. package/src/ir/sync.ts +49 -0
  68. package/src/ir/types.ts +161 -0
  69. package/src/ir/views/dnd5e.ts +94 -0
  70. package/src/lib/indexeddb-cache.js +320 -0
  71. package/src/modules/action-announcements.js +102 -0
  72. package/src/modules/action-display.js +1557 -0
  73. package/src/modules/action-executor.js +860 -0
  74. package/src/modules/action-filters.js +167 -0
  75. package/src/modules/action-options.js +117 -0
  76. package/src/modules/card-creator.js +142 -0
  77. package/src/modules/character-portrait.js +169 -0
  78. package/src/modules/character-trait-popups.js +959 -0
  79. package/src/modules/character-traits.js +814 -0
  80. package/src/modules/class-feature-edge-cases.js +1320 -0
  81. package/src/modules/color-utils.js +69 -0
  82. package/src/modules/combat-maneuver-edge-cases.js +660 -0
  83. package/src/modules/companions-manager.js +178 -0
  84. package/src/modules/concentration-tracker.js +178 -0
  85. package/src/modules/data-manager.js +514 -0
  86. package/src/modules/dice-roller.js +719 -0
  87. package/src/modules/effects-manager.js +743 -0
  88. package/src/modules/feature-modals.js +1264 -0
  89. package/src/modules/formula-resolver.js +444 -0
  90. package/src/modules/gm-mode.js +184 -0
  91. package/src/modules/health-modals.js +399 -0
  92. package/src/modules/hp-management.js +752 -0
  93. package/src/modules/inventory-manager.js +242 -0
  94. package/src/modules/macro-system.js +825 -0
  95. package/src/modules/notification-system.js +92 -0
  96. package/src/modules/racial-feature-edge-cases.js +746 -0
  97. package/src/modules/resource-manager.js +775 -0
  98. package/src/modules/sheet-builder.js +654 -0
  99. package/src/modules/spell-action-modals.js +583 -0
  100. package/src/modules/spell-cards.js +602 -0
  101. package/src/modules/spell-casting.js +723 -0
  102. package/src/modules/spell-display.js +314 -0
  103. package/src/modules/spell-edge-cases.js +509 -0
  104. package/src/modules/spell-macros.js +201 -0
  105. package/src/modules/spell-modals.js +1221 -0
  106. package/src/modules/spell-slots.js +224 -0
  107. package/src/modules/status-bar-bridge.js +101 -0
  108. package/src/modules/ui-utilities.js +284 -0
  109. package/src/modules/warlock-invocations.js +219 -0
  110. package/src/modules/window-management.js +211 -0
  111. package/src/render/character.ts +234 -0
  112. package/src/render/h.ts +74 -0
  113. package/src/render/index.ts +10 -0
  114. package/src/render/mount.ts +94 -0
  115. package/src/supabase/client.js +1383 -0
  116. package/src/supabase/config.js +60 -0
  117. package/src/supabase/fields.ts +129 -0
  118. package/src/types/character.ts +85 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Spell Display Module
3
+ *
4
+ * Handles spell list display, organization, and filtering.
5
+ * - Builds spell list organized by level
6
+ * - Applies filters (level, category, casting time, search)
7
+ * - Categorizes spells (damage/healing/utility)
8
+ *
9
+ * Loaded as a plain script (no ES6 modules) to export to window.
10
+ */
11
+
12
+ (function() {
13
+ 'use strict';
14
+
15
+ /**
16
+ * Build and display spells organized by level
17
+ * @param {HTMLElement} container - Container element for spells
18
+ * @param {Array} spells - Array of spell objects
19
+ */
20
+ function buildSpellsBySource(container, spells) {
21
+ if (typeof characterData === 'undefined' || !characterData) {
22
+ console.error('characterData not available');
23
+ return;
24
+ }
25
+
26
+ const debug = window.debug || console;
27
+ debug.log(`📚 buildSpellsBySource called with ${spells.length} spells`);
28
+ debug.log(`📚 Spell names: ${spells.map(s => s.name).join(', ')}`);
29
+
30
+ // Debug: Check for Eldritch Blast damageRolls
31
+ const eldritchBlast = spells.find(s => s.name && s.name.toLowerCase().includes('eldritch blast'));
32
+ if (eldritchBlast) {
33
+ console.log('⚡ ELDRITCH BLAST DATA IN POPUP:', {
34
+ name: eldritchBlast.name,
35
+ attackRoll: eldritchBlast.attackRoll,
36
+ damageRolls: eldritchBlast.damageRolls,
37
+ damageRollsLength: eldritchBlast.damageRolls ? eldritchBlast.damageRolls.length : 'undefined',
38
+ damageRollsJSON: JSON.stringify(eldritchBlast.damageRolls)
39
+ });
40
+ }
41
+
42
+ // Apply filters first
43
+ let filteredSpells = spells.filter(spell => {
44
+ // Filter out duplicate Divine Smite entries - keep only the main one
45
+ const spellName = (spell.name || '').toLowerCase();
46
+ if (spellName.includes('divine smite')) {
47
+ // Skip variants like "Divine Smite Level 1", "Divine Smite (Against Fiends, Critical) Level 1", etc.
48
+ // Keep only the base "Divine Smite" entry
49
+ if (spellName !== 'divine smite' && !spellName.match(/^divine smite$/)) {
50
+ debug.log(`⏭️ Filtering out duplicate Divine Smite spell: ${spell.name}`);
51
+ return false;
52
+ } else {
53
+ debug.log(`✅ Keeping main Divine Smite spell: ${spell.name}`);
54
+ }
55
+ }
56
+
57
+ // Filter by spell level
58
+ if (window.spellFilters && window.spellFilters.level !== 'all') {
59
+ const spellLevel = parseInt(spell.level) || 0;
60
+ if (spellLevel.toString() !== window.spellFilters.level) {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ // Filter by category
66
+ if (window.spellFilters && window.spellFilters.category !== 'all') {
67
+ const category = categorizeSpell(spell);
68
+ if (category !== window.spellFilters.category) {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ // Filter by casting time
74
+ if (window.spellFilters && window.spellFilters.castingTime !== 'all') {
75
+ const castingTime = (spell.castingTime || '').toLowerCase();
76
+ if (window.spellFilters.castingTime === 'action') {
77
+ // Match "action" but exclude "bonus action" and "reaction"
78
+ if (!castingTime.includes('action') || castingTime.includes('bonus') || castingTime.includes('reaction')) {
79
+ return false;
80
+ }
81
+ }
82
+ if (window.spellFilters.castingTime === 'bonus' && !castingTime.includes('bonus')) {
83
+ return false;
84
+ }
85
+ if (window.spellFilters.castingTime === 'reaction' && !castingTime.includes('reaction')) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ // Filter by search term
91
+ if (window.spellFilters && window.spellFilters.search) {
92
+ const searchLower = window.spellFilters.search;
93
+ const name = (spell.name || '').toLowerCase();
94
+ const desc = (spell.description || '').toLowerCase();
95
+ if (!name.includes(searchLower) && !desc.includes(searchLower)) {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ return true;
101
+ });
102
+
103
+ debug.log(`🔍 Filtered ${spells.length} spells to ${filteredSpells.length} spells`);
104
+
105
+ // Group spells by actual spell level (not source)
106
+ const spellsByLevel = {};
107
+
108
+ filteredSpells.forEach((spell, index) => {
109
+ // Add index to spell for tracking
110
+ spell.index = index;
111
+
112
+ // Use spell level for grouping
113
+ const spellLevel = parseInt(spell.level) || 0;
114
+ const levelKey = spellLevel === 0 ? 'Cantrips' : `Level ${spellLevel} Spells`;
115
+
116
+ if (!spellsByLevel[levelKey]) {
117
+ spellsByLevel[levelKey] = [];
118
+ }
119
+ spellsByLevel[levelKey].push(spell);
120
+ });
121
+
122
+ // Clear container
123
+ container.innerHTML = '';
124
+
125
+ // Sort by spell level (cantrips first, then 1-9)
126
+ const sortedLevels = Object.keys(spellsByLevel).sort((a, b) => {
127
+ if (a === 'Cantrips') return -1;
128
+ if (b === 'Cantrips') return 1;
129
+ return a.localeCompare(b, undefined, { numeric: true });
130
+ });
131
+
132
+ sortedLevels.forEach(levelKey => {
133
+ // Create level section
134
+ const levelSection = document.createElement('div');
135
+ levelSection.style.cssText = 'margin-bottom: 20px;';
136
+
137
+ const levelHeader = document.createElement('h4');
138
+ levelHeader.textContent = `📚 ${levelKey}`;
139
+ levelHeader.style.cssText = 'color: var(--text-primary); margin-bottom: 10px; padding: 5px; background: var(--bg-secondary, #ecf0f1); border-radius: 4px;';
140
+ levelSection.appendChild(levelHeader);
141
+
142
+ // Sort spells alphabetically within level
143
+ const sortedSpells = spellsByLevel[levelKey].sort((a, b) => {
144
+ return (a.name || '').localeCompare(b.name || '');
145
+ });
146
+
147
+ // Deduplicate spells by name and combine sources
148
+ const deduplicatedSpells = [];
149
+ const spellsByName = {};
150
+
151
+ debug.log(`📚 Deduplicating ${sortedSpells.length} spells in ${levelKey}`, sortedSpells.map(s => s.name));
152
+ sortedSpells.forEach(spell => {
153
+ const spellName = spell.name || 'Unnamed Spell';
154
+
155
+ if (!spellsByName[spellName]) {
156
+ // First occurrence of this spell
157
+ spellsByName[spellName] = spell;
158
+ deduplicatedSpells.push(spell);
159
+ debug.log(`📚 First occurrence: "${spellName}"`);
160
+ } else {
161
+ // Duplicate spell - combine sources
162
+ const existingSpell = spellsByName[spellName];
163
+ debug.log(`📚 Found duplicate: "${spellName}" - combining sources`);
164
+ if (spell.source && !existingSpell.source.includes(spell.source)) {
165
+ existingSpell.source += '; ' + spell.source;
166
+ debug.log(`📚 Combined duplicate spell "${spellName}": ${existingSpell.source}`);
167
+ }
168
+ }
169
+ });
170
+ debug.log(`📚 After deduplication: ${deduplicatedSpells.length} unique spells in ${levelKey}`, deduplicatedSpells.map(s => s.name));
171
+
172
+ // Add deduplicated spells
173
+ deduplicatedSpells.forEach(spell => {
174
+ if (typeof createSpellCard === 'function') {
175
+ const spellCard = createSpellCard(spell, spell.index);
176
+ levelSection.appendChild(spellCard);
177
+ } else {
178
+ debug.warn('createSpellCard function not available');
179
+ }
180
+ });
181
+
182
+ container.appendChild(levelSection);
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Rebuild spells with current filters
188
+ */
189
+ function rebuildSpells() {
190
+ if (!characterData || !characterData.spells) return;
191
+ const container = document.getElementById('spells-container');
192
+ buildSpellsBySource(container, characterData.spells);
193
+ }
194
+
195
+ /**
196
+ * Categorize a spell as damage, healing, or utility
197
+ * @param {object} spell - Spell object
198
+ * @returns {string} Category: 'damage', 'healing', or 'utility'
199
+ */
200
+ function categorizeSpell(spell) {
201
+ const spellName = (spell.name || '').toLowerCase();
202
+
203
+ // Check spell name for common healing keywords first (most reliable)
204
+ const healingKeywords = ['cure', 'heal', 'regenerat', 'revivif', 'raise', 'resurrection', 'aid', 'prayer of healing', 'mass healing'];
205
+ const isHealingSpell = healingKeywords.some(keyword => spellName.includes(keyword));
206
+
207
+ if (isHealingSpell) {
208
+ return 'healing';
209
+ }
210
+
211
+ // Use actual spell data instead of string matching in description
212
+ // Check damageRolls array to determine if it's damage or healing
213
+ if (spell.damageRolls && Array.isArray(spell.damageRolls) && spell.damageRolls.length > 0) {
214
+ // Check if any damage roll is healing
215
+ const hasHealing = spell.damageRolls.some(roll =>
216
+ roll.damageType && roll.damageType.toLowerCase() === 'healing'
217
+ );
218
+
219
+ // Check if any damage roll is actual damage (not healing)
220
+ const hasDamage = spell.damageRolls.some(roll =>
221
+ !roll.damageType || roll.damageType.toLowerCase() !== 'healing'
222
+ );
223
+
224
+ // Categorize based on what the spell actually does
225
+ if (hasHealing && !hasDamage) {
226
+ return 'healing';
227
+ } else if (hasDamage) {
228
+ return 'damage';
229
+ }
230
+ }
231
+
232
+ // Check legacy damage field with damageType
233
+ if (spell.damage && spell.damageType) {
234
+ if (spell.damageType.toLowerCase() === 'healing') {
235
+ return 'healing';
236
+ }
237
+ }
238
+
239
+ // Check for attack roll (attack spells are damage)
240
+ if (spell.attackRoll && spell.attackRoll !== '(none)') {
241
+ return 'damage';
242
+ }
243
+
244
+ // Everything else is utility (no damage rolls, no attack roll)
245
+ return 'utility';
246
+ }
247
+
248
+ /**
249
+ * Initialize spell filter event listeners
250
+ */
251
+ function initializeSpellFilters() {
252
+ // Initialize spell filters object
253
+ if (!window.spellFilters) {
254
+ window.spellFilters = {
255
+ level: 'all',
256
+ category: 'all',
257
+ castingTime: 'all',
258
+ search: ''
259
+ };
260
+ }
261
+
262
+ // Spell search filter
263
+ const spellsSearch = document.getElementById('spells-search');
264
+ if (spellsSearch) {
265
+ spellsSearch.addEventListener('input', (e) => {
266
+ window.spellFilters.search = e.target.value.toLowerCase();
267
+ rebuildSpells();
268
+ });
269
+ }
270
+
271
+ // Spell level filters
272
+ document.querySelectorAll('[data-type="spell-level"]').forEach(btn => {
273
+ btn.addEventListener('click', () => {
274
+ window.spellFilters.level = btn.dataset.filter;
275
+ document.querySelectorAll('[data-type="spell-level"]').forEach(b => b.classList.remove('active'));
276
+ btn.classList.add('active');
277
+ rebuildSpells();
278
+ });
279
+ });
280
+
281
+ // Spell category filters
282
+ document.querySelectorAll('[data-type="spell-category"]').forEach(btn => {
283
+ btn.addEventListener('click', () => {
284
+ window.spellFilters.category = btn.dataset.filter;
285
+ document.querySelectorAll('[data-type="spell-category"]').forEach(b => b.classList.remove('active'));
286
+ btn.classList.add('active');
287
+ rebuildSpells();
288
+ });
289
+ });
290
+
291
+ // Spell casting time filters
292
+ document.querySelectorAll('[data-type="spell-casting-time"]').forEach(btn => {
293
+ btn.addEventListener('click', () => {
294
+ window.spellFilters.castingTime = btn.dataset.filter;
295
+ document.querySelectorAll('[data-type="spell-casting-time"]').forEach(b => b.classList.remove('active'));
296
+ btn.classList.add('active');
297
+ rebuildSpells();
298
+ });
299
+ });
300
+
301
+ debug.log('✅ Spell filters initialized');
302
+ }
303
+
304
+ // Export functions to globalThis
305
+ Object.assign(globalThis, {
306
+ buildSpellsBySource,
307
+ rebuildSpells,
308
+ categorizeSpell,
309
+ initializeSpellFilters
310
+ });
311
+
312
+ console.log('✅ Spell Display module loaded');
313
+
314
+ })();