@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,1557 @@
1
+ /**
2
+ * Action Display Module
3
+ *
4
+ * Handles rendering of actions and action cards.
5
+ * Loaded as a plain script (no ES6 modules) to export to window.
6
+ *
7
+ * Functions exported to globalThis:
8
+ * - buildActionsDisplay(container, actions)
9
+ * - decrementActionUses(action)
10
+ */
11
+
12
+ (function() {
13
+ 'use strict';
14
+
15
+ // ===== MAIN DISPLAY FUNCTION =====
16
+
17
+ function buildActionsDisplay(container, actions) {
18
+ // Clear container
19
+ container.innerHTML = '';
20
+
21
+ // Class feature toggle states
22
+ let sneakAttackEnabled = false;
23
+ let sneakAttackDamage = null;
24
+ let elementalWeaponEnabled = false;
25
+ let elementalWeaponDamage = null;
26
+
27
+ // DEBUG: Log all actions to see what we have
28
+ debug.log('🔍 buildActionsDisplay called with actions:', actions.map(a => ({ name: a.name, damage: a.damage, actionType: a.actionType })));
29
+ debug.log('🔍 Total actions received:', actions.length);
30
+
31
+ /**
32
+ * Normalize action name by removing common suffixes that indicate variants
33
+ * @param {string} name - The action name
34
+ * @returns {string} Normalized name for deduplication
35
+ */
36
+ function normalizeActionName(name) {
37
+ if (!name) return '';
38
+
39
+ // Remove common suffixes that indicate the same ability but free/different action type
40
+ const suffixPatterns = [
41
+ /\s*\(free\)$/i,
42
+ /\s*\(free action\)$/i,
43
+ /\s*\(bonus action\)$/i,
44
+ /\s*\(bonus\)$/i,
45
+ /\s*\(reaction\)$/i,
46
+ /\s*\(action\)$/i,
47
+ /\s*\(no spell slot\)$/i,
48
+ /\s*\(at will\)$/i
49
+ ];
50
+
51
+ let normalized = name.trim();
52
+ for (const pattern of suffixPatterns) {
53
+ normalized = normalized.replace(pattern, '');
54
+ }
55
+
56
+ return normalized.trim();
57
+ }
58
+
59
+ // Deduplicate actions by normalized name and combine sources (similar to spells)
60
+ const deduplicatedActions = [];
61
+ const actionsByNormalizedName = {};
62
+
63
+ // Sort actions by name for consistent processing
64
+ // Prefer base names without suffixes (shorter names first within same normalized group)
65
+ const sortedActions = [...actions].sort((a, b) => {
66
+ const normA = normalizeActionName(a.name || '');
67
+ const normB = normalizeActionName(b.name || '');
68
+
69
+ // First sort by normalized name
70
+ if (normA !== normB) {
71
+ return normA.localeCompare(normB);
72
+ }
73
+
74
+ // Within same normalized name, prefer shorter names (base versions)
75
+ return (a.name || '').length - (b.name || '').length;
76
+ });
77
+
78
+ sortedActions.forEach(action => {
79
+ const actionName = (action.name || '').trim();
80
+ const normalizedName = normalizeActionName(actionName);
81
+
82
+ if (!normalizedName) {
83
+ debug.log('⚠️ Skipping action with no name');
84
+ return;
85
+ }
86
+
87
+ if (!actionsByNormalizedName[normalizedName]) {
88
+ // First occurrence of this action (by normalized name)
89
+ actionsByNormalizedName[normalizedName] = action;
90
+ deduplicatedActions.push(action);
91
+ debug.log(`📝 First occurrence of action: "${actionName}" (normalized: "${normalizedName}")`);
92
+ } else {
93
+ // Duplicate action - combine sources and other properties
94
+ const existingAction = actionsByNormalizedName[normalizedName];
95
+
96
+ // Combine sources if they exist
97
+ if (action.source && !existingAction.source.includes(action.source)) {
98
+ existingAction.source = existingAction.source
99
+ ? existingAction.source + '; ' + action.source
100
+ : action.source;
101
+ debug.log(`📝 Combined duplicate action "${actionName}": ${existingAction.source}`);
102
+ }
103
+
104
+ // Combine descriptions if they exist and are different
105
+ if (action.description && action.description !== existingAction.description) {
106
+ existingAction.description = existingAction.description
107
+ ? existingAction.description + '\n\n' + action.description
108
+ : action.description;
109
+ debug.log(`📝 Combined descriptions for "${actionName}"`);
110
+ }
111
+
112
+ // Merge other useful properties
113
+ if (action.uses && !existingAction.uses) {
114
+ existingAction.uses = action.uses;
115
+ debug.log(`📝 Added uses to "${actionName}"`);
116
+ }
117
+
118
+ if (action.damage && !existingAction.damage) {
119
+ existingAction.damage = action.damage;
120
+ debug.log(`📝 Added damage to "${actionName}"`);
121
+ }
122
+
123
+ if (action.attackRoll && !existingAction.attackRoll) {
124
+ existingAction.attackRoll = action.attackRoll;
125
+ debug.log(`📝 Added attackRoll to "${actionName}"`);
126
+ }
127
+
128
+ debug.log(`🔄 Merged duplicate action: "${actionName}"`);
129
+ }
130
+ });
131
+
132
+ debug.log(`📊 Deduplicated ${actions.length} actions to ${deduplicatedActions.length} unique actions`);
133
+
134
+ // Apply filters
135
+ let filteredActions = deduplicatedActions.filter(action => {
136
+ const actionName = (action.name || '').toLowerCase();
137
+
138
+ // Filter out duplicate Divine Smite entries - keep only the main one
139
+ if (actionName.includes('divine smite')) {
140
+ // Skip variants like "Divine Smite Level 1", "Divine Smite (Against Fiends, Critical) Level 1", etc.
141
+ // Keep only the base "Divine Smite" entry
142
+ if (actionName !== 'divine smite' && !actionName.match(/^divine smite$/)) {
143
+ debug.log(`⏭️ Filtering out duplicate Divine Smite entry: ${action.name}`);
144
+ return false;
145
+ } else {
146
+ debug.log(`✅ Keeping main Divine Smite entry: ${action.name}`);
147
+ }
148
+ }
149
+
150
+ // Debug: Log all Lay on Hands related actions
151
+ if (actionName.includes('lay on hands')) {
152
+ const normalizedActionName = action.name.toLowerCase()
153
+ .replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
154
+ .replace(/\s+/g, ' ') // Normalize spaces
155
+ .trim();
156
+ const normalizedSearch = 'lay on hands: heal';
157
+
158
+ debug.log(`🔍 Found Lay on Hands action: "${action.name}"`);
159
+ debug.log(`🔍 Normalized action name: "${normalizedActionName}"`);
160
+ debug.log(`🔍 Normalized search term: "${normalizedSearch}"`);
161
+ debug.log(`🔍 Do they match? ${normalizedActionName === normalizedSearch}`);
162
+ debug.log(`🔍 Action object:`, action);
163
+ }
164
+
165
+ // Filter by action type
166
+ if (actionFilters.actionType !== 'all') {
167
+ const actionType = (action.actionType || '').toLowerCase();
168
+ if (actionType !== actionFilters.actionType) {
169
+ return false;
170
+ }
171
+ }
172
+
173
+ // Filter by category
174
+ if (actionFilters.category !== 'all') {
175
+ const category = categorizeAction(action);
176
+ if (category !== actionFilters.category) {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ // Filter by search term
182
+ if (actionFilters.search) {
183
+ const searchLower = actionFilters.search;
184
+ const name = (action.name || '').toLowerCase();
185
+ const desc = (action.description || '').toLowerCase();
186
+ if (!name.includes(searchLower) && !desc.includes(searchLower)) {
187
+ return false;
188
+ }
189
+ }
190
+
191
+ return true;
192
+ });
193
+
194
+ debug.log(`🔍 Filtered ${deduplicatedActions.length} actions to ${filteredActions.length} actions`);
195
+
196
+ // Check if character has Sneak Attack available (from DiceCloud)
197
+ // We only check if it EXISTS, not whether it's enabled on DiceCloud
198
+ // The toggle state on our sheet is independent and user-controlled
199
+ // Use flexible matching in case the name has slight variations
200
+ const sneakAttackAction = deduplicatedActions.find(a =>
201
+ a.name === 'Sneak Attack' ||
202
+ a.name.toLowerCase().includes('sneak attack')
203
+ );
204
+ debug.log('🎯 Sneak Attack search result:', sneakAttackAction);
205
+ if (sneakAttackAction && sneakAttackAction.damage) {
206
+ sneakAttackDamage = sneakAttackAction.damage;
207
+
208
+ // Resolve variables in the damage formula for display
209
+ const resolvedDamage = resolveVariablesInFormula(sneakAttackDamage);
210
+ debug.log(`🎯 Sneak Attack damage: "${sneakAttackDamage}" resolved to "${resolvedDamage}"`);
211
+
212
+ // Add toggle section at the top of actions
213
+ const toggleSection = document.createElement('div');
214
+ toggleSection.style.cssText = 'background: #2c3e50; color: white; padding: 10px; border-radius: 5px; margin-bottom: 10px; display: flex; align-items: center; gap: 10px;';
215
+
216
+ const toggleLabel = document.createElement('label');
217
+ toggleLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: bold;';
218
+
219
+ const checkbox = document.createElement('input');
220
+ checkbox.type = 'checkbox';
221
+ checkbox.id = 'sneak-attack-toggle';
222
+ checkbox.checked = sneakAttackEnabled; // Always starts false - IGNORES DiceCloud toggle state
223
+ checkbox.style.cssText = 'width: 18px; height: 18px; cursor: pointer;';
224
+ checkbox.addEventListener('change', (e) => {
225
+ sneakAttackEnabled = e.target.checked;
226
+ debug.log(`🎯 Sneak Attack toggle on our sheet: ${sneakAttackEnabled ? 'ON' : 'OFF'} (independent of DiceCloud)`);
227
+ });
228
+
229
+ const labelText = document.createElement('span');
230
+ labelText.textContent = `Add Sneak Attack (${resolvedDamage}) to weapon damage`;
231
+
232
+ toggleLabel.appendChild(checkbox);
233
+ toggleLabel.appendChild(labelText);
234
+ toggleSection.appendChild(toggleLabel);
235
+ container.appendChild(toggleSection);
236
+ }
237
+
238
+ // Check if character has Elemental Weapon spell prepared (check spells list)
239
+ // We only check if it EXISTS, the toggle is user-controlled
240
+ const hasElementalWeapon = characterData.spells && characterData.spells.some(s =>
241
+ s.name === 'Elemental Weapon' || (s.spell && s.spell.name === 'Elemental Weapon')
242
+ );
243
+
244
+ if (hasElementalWeapon) {
245
+ debug.log(`⚔️ Elemental Weapon spell found, adding toggle`);
246
+ // Set default elemental weapon damage (typically 1d4, but can vary by spell slot)
247
+ elementalWeaponDamage = '1d4';
248
+
249
+ // Add toggle section for Elemental Weapon
250
+ const elementalToggleSection = document.createElement('div');
251
+ elementalToggleSection.style.cssText = 'background: #8b4513; color: white; padding: 10px; border-radius: 5px; margin-bottom: 10px; display: flex; align-items: center; gap: 10px;';
252
+
253
+ const elementalToggleLabel = document.createElement('label');
254
+ elementalToggleLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: bold;';
255
+
256
+ const elementalCheckbox = document.createElement('input');
257
+ elementalCheckbox.type = 'checkbox';
258
+ elementalCheckbox.id = 'elemental-weapon-toggle';
259
+ elementalCheckbox.checked = elementalWeaponEnabled; // Always starts false
260
+ elementalCheckbox.style.cssText = 'width: 18px; height: 18px; cursor: pointer;';
261
+ elementalCheckbox.addEventListener('change', (e) => {
262
+ elementalWeaponEnabled = e.target.checked;
263
+ debug.log(`⚔️ Elemental Weapon toggle: ${elementalWeaponEnabled ? 'ON' : 'OFF'}`);
264
+ });
265
+
266
+ const elementalLabelText = document.createElement('span');
267
+ elementalLabelText.textContent = `Add Elemental Weapon (${elementalWeaponDamage}) to weapon damage`;
268
+
269
+ elementalToggleLabel.appendChild(elementalCheckbox);
270
+ elementalToggleLabel.appendChild(elementalLabelText);
271
+ elementalToggleSection.appendChild(elementalToggleLabel);
272
+ container.appendChild(elementalToggleSection);
273
+ }
274
+
275
+ // Check if character has Lucky feat
276
+ const hasLuckyFeat = characterData.features && characterData.features.some(f =>
277
+ f.name && f.name.toLowerCase().includes('lucky')
278
+ );
279
+
280
+ if (hasLuckyFeat) {
281
+ debug.log(`🎖️ Lucky feat found, adding action button`);
282
+
283
+ // Add action button for Lucky feat
284
+ const luckyActionSection = document.createElement('div');
285
+ luckyActionSection.style.cssText = 'background: #f39c12; color: white; padding: 12px; border-radius: 5px; margin-bottom: 10px;';
286
+
287
+ const luckyButton = document.createElement('button');
288
+ luckyButton.id = 'lucky-action-button';
289
+ luckyButton.style.cssText = `
290
+ background: #e67e22;
291
+ color: white;
292
+ border: none;
293
+ padding: 10px 16px;
294
+ border-radius: 5px;
295
+ cursor: pointer;
296
+ font-size: 14px;
297
+ font-weight: bold;
298
+ width: 100%;
299
+ transition: background 0.2s;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ gap: 8px;
304
+ `;
305
+ luckyButton.onmouseover = () => luckyButton.style.background = '#d35400';
306
+ luckyButton.onmouseout = () => luckyButton.style.background = '#e67e22';
307
+
308
+ // Update button text based on available luck points
309
+ const luckyResource = getLuckyResource();
310
+ const luckPointsAvailable = luckyResource ? luckyResource.current : 0;
311
+ luckyButton.innerHTML = `
312
+ <span style="font-size: 16px;">🎖️</span>
313
+ <span>Use Lucky Point (${luckPointsAvailable}/3)</span>
314
+ `;
315
+
316
+ luckyButton.addEventListener('click', () => {
317
+ const currentLuckyResource = getLuckyResource();
318
+ if (!currentLuckyResource || currentLuckyResource.current <= 0) {
319
+ showNotification('❌ No luck points available!', 'error');
320
+ return;
321
+ }
322
+
323
+ // Show simple Lucky modal like metamagic
324
+ showLuckyModal();
325
+ });
326
+
327
+ luckyActionSection.appendChild(luckyButton);
328
+ container.appendChild(luckyActionSection);
329
+ }
330
+
331
+ filteredActions.forEach((action, index) => {
332
+ // Skip rendering standalone Sneak Attack button if it exists
333
+ if ((action.name === 'Sneak Attack' || action.name.toLowerCase().includes('sneak attack')) && action.actionType === 'feature') {
334
+ debug.log('⏭️ Skipping standalone Sneak Attack button (using toggle instead)');
335
+ return;
336
+ }
337
+
338
+ // Clean up weapon damage to remove sneak attack if it was auto-added
339
+ // Pattern: remove any multi-dice formulas like "+3d6", "+4d6", etc. that come after the base damage
340
+ if (action.damage && action.attackRoll && sneakAttackDamage) {
341
+ // Remove the sneak attack damage pattern from weapon damage
342
+ const sneakPattern = new RegExp(`\\+?${sneakAttackDamage.replace(/[+\-]/g, '')}`, 'g');
343
+ const cleanedDamage = action.damage.replace(sneakPattern, '');
344
+ if (cleanedDamage !== action.damage) {
345
+ debug.log(`🧹 Cleaned weapon damage: "${action.damage}" -> "${cleanedDamage}"`);
346
+ action.damage = cleanedDamage;
347
+ }
348
+ }
349
+
350
+ const actionCard = document.createElement('div');
351
+ actionCard.className = 'action-card';
352
+
353
+ const actionHeader = document.createElement('div');
354
+ actionHeader.className = 'action-header';
355
+
356
+ const nameDiv = document.createElement('div');
357
+ nameDiv.className = 'action-name';
358
+
359
+ // Show uses if available
360
+ let nameText = action.name;
361
+
362
+ // Rename "Recover Spell Slot" to "Harness Divine Power" (Cleric feature)
363
+ if (nameText === 'Recover Spell Slot') {
364
+ nameText = 'Harness Divine Power';
365
+ }
366
+
367
+ if (action.uses) {
368
+ const usesTotal = action.uses.total || action.uses.value || action.uses;
369
+ // Prefer usesLeft from DiceCloud if available, otherwise calculate from usesUsed
370
+ const usesRemaining = action.usesLeft !== undefined ? action.usesLeft : (usesTotal - (action.usesUsed || 0));
371
+ nameText += ` <span class="uses-badge">${usesRemaining}/${usesTotal} uses</span>`;
372
+ }
373
+ nameDiv.innerHTML = nameText;
374
+
375
+ const buttonsDiv = document.createElement('div');
376
+ buttonsDiv.className = 'action-buttons';
377
+
378
+ // Get action options with edge case modifications
379
+ const actionOptionsResult = getActionOptions(action);
380
+ const actionOptions = actionOptionsResult.options;
381
+
382
+ // Check if this is a "utility only" action that should just announce
383
+ if (actionOptionsResult.skipNormalButtons) {
384
+ // Create simple action button for utility-only actions
385
+ const actionBtn = document.createElement('button');
386
+ actionBtn.className = 'action-btn';
387
+ actionBtn.textContent = '✨ Use';
388
+ actionBtn.style.cssText = `
389
+ background: #9b59b6;
390
+ color: white;
391
+ border: none;
392
+ padding: 8px 12px;
393
+ border-radius: 4px;
394
+ cursor: pointer;
395
+ font-size: 12px;
396
+ font-weight: bold;
397
+ `;
398
+ actionBtn.addEventListener('click', () => {
399
+ // Check for Divine Smite special handling
400
+ if (action.name.toLowerCase().includes('divine smite')) {
401
+ showDivineSmiteModal(action);
402
+ return;
403
+ }
404
+
405
+ // Check for Lay on Hands: Heal special handling
406
+ const normalizedActionName = action.name.toLowerCase()
407
+ .replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
408
+ .replace(/\s+/g, ' ') // Normalize spaces
409
+ .trim();
410
+ const normalizedSearch = 'lay on hands: heal';
411
+
412
+ if (normalizedActionName === normalizedSearch) {
413
+ debug.log(`💚 Lay on Hands: Heal action clicked: ${action.name}, showing custom modal`);
414
+ debug.log(`💚 Normalized match: "${normalizedActionName}" === "${normalizedSearch}"`);
415
+ const layOnHandsPool = getLayOnHandsResource();
416
+ if (layOnHandsPool) {
417
+ showLayOnHandsModal(layOnHandsPool);
418
+ } else {
419
+ showNotification('❌ No Lay on Hands pool resource found', 'error');
420
+ }
421
+ return;
422
+ }
423
+
424
+ // Fallback: Catch ANY Lay on Hands action for debugging
425
+ if (action.name.toLowerCase().includes('lay on hands')) {
426
+ debug.log(`🚨 FALLBACK: Caught Lay on Hands action: "${action.name}"`);
427
+ debug.log(`🚨 This action didn't match 'lay on hands: heal' but contains 'lay on hands'`);
428
+ debug.log(`🚨 Showing modal anyway for debugging`);
429
+ const layOnHandsPool = getLayOnHandsResource();
430
+ if (layOnHandsPool) {
431
+ showLayOnHandsModal(layOnHandsPool);
432
+ } else {
433
+ showNotification('❌ No Lay on Hands pool resource found', 'error');
434
+ }
435
+ return;
436
+ }
437
+
438
+ // Check and decrement uses BEFORE announcing (so announcement shows correct count)
439
+ if (action.uses && !decrementActionUses(action)) {
440
+ return; // No uses remaining
441
+ }
442
+
443
+ // Check and decrement other resources
444
+ if (!decrementActionResources(action)) {
445
+ return; // Not enough resources
446
+ }
447
+
448
+ // Announce the action with description AFTER decrements
449
+ announceAction(action);
450
+ });
451
+ buttonsDiv.appendChild(actionBtn);
452
+ } else {
453
+ // Create buttons for each action option
454
+ let actionAnnounced = false; // Track if action has been announced
455
+ actionOptions.forEach((option, optionIndex) => {
456
+ const actionBtn = document.createElement('button');
457
+ actionBtn.className = `${option.type}-btn`;
458
+
459
+ // Add edge case note if present
460
+ const edgeCaseNote = option.edgeCaseNote ? `<div style="font-size: 0.7em; color: #666; margin-top: 1px;">${option.edgeCaseNote}</div>` : '';
461
+ actionBtn.innerHTML = `${option.label}${edgeCaseNote}`;
462
+
463
+ actionBtn.style.cssText = `
464
+ background: ${option.color};
465
+ color: white;
466
+ border: none;
467
+ padding: 8px 12px;
468
+ border-radius: 4px;
469
+ cursor: pointer;
470
+ font-size: 12px;
471
+ font-weight: bold;
472
+ margin-right: 4px;
473
+ margin-bottom: 4px;
474
+ `;
475
+
476
+ actionBtn.addEventListener('click', () => {
477
+ // Check for Divine Smite special handling
478
+ if (action.name.toLowerCase().includes('divine smite')) {
479
+ showDivineSmiteModal(action);
480
+ return;
481
+ }
482
+
483
+ // Check for Lay on Hands: Heal special handling
484
+ const normalizedActionName = action.name.toLowerCase()
485
+ .replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
486
+ .replace(/\s+/g, ' ') // Normalize spaces
487
+ .trim();
488
+ const normalizedSearch = 'lay on hands: heal';
489
+
490
+ if (normalizedActionName === normalizedSearch) {
491
+ debug.log(`💚 Lay on Hands: Heal action clicked: ${action.name}, showing custom modal`);
492
+ debug.log(`💚 Normalized match: "${normalizedActionName}" === "${normalizedSearch}"`);
493
+ const layOnHandsPool = getLayOnHandsResource();
494
+ if (layOnHandsPool) {
495
+ showLayOnHandsModal(layOnHandsPool);
496
+ } else {
497
+ showNotification('❌ No Lay on Hands pool resource found', 'error');
498
+ }
499
+ return;
500
+ }
501
+
502
+ // Fallback: Catch ANY Lay on Hands action for debugging
503
+ if (action.name.toLowerCase().includes('lay on hands')) {
504
+ debug.log(`🚨 FALLBACK: Caught Lay on Hands action: "${action.name}"`);
505
+ debug.log(`🚨 This action didn't match 'lay on hands: heal' but contains 'lay on hands'`);
506
+ debug.log(`🚨 Showing modal anyway for debugging`);
507
+ const layOnHandsPool = getLayOnHandsResource();
508
+ if (layOnHandsPool) {
509
+ showLayOnHandsModal(layOnHandsPool);
510
+ } else {
511
+ showNotification('❌ No Lay on Hands pool resource found', 'error');
512
+ }
513
+ return;
514
+ }
515
+
516
+ // Announce the action on the FIRST button click only
517
+ if (!actionAnnounced) {
518
+ announceAction(action);
519
+ actionAnnounced = true;
520
+ }
521
+
522
+ // Handle different option types
523
+ if (option.type === 'attack') {
524
+ // Mark action as used for attacks
525
+ const actionType = action.actionType || 'action';
526
+ if (typeof window.markActionEconomyUsed === 'function') {
527
+ window.markActionEconomyUsed(actionType);
528
+ }
529
+
530
+ // Attack roll is just the d20 + modifiers, no damage dice
531
+ debug.log(`🎯 Attack button clicked for "${action.name}", formula: "${option.formula}"`);
532
+ console.log(`🎯 ATTACK DEBUG: Rolling attack for ${action.name} with formula ${option.formula}`);
533
+
534
+ try {
535
+ roll(`${action.name} Attack`, option.formula);
536
+ debug.log(`✅ Attack roll called successfully for "${action.name}"`);
537
+ } catch (error) {
538
+ debug.error(`❌ Error rolling attack for "${action.name}":`, error);
539
+ console.error('❌ ATTACK ERROR:', error);
540
+ showNotification(`❌ Error rolling attack: ${error.message}`, 'error');
541
+ }
542
+ } else if (option.type === 'healing' || option.type === 'temphp' || option.type === 'damage') {
543
+ // Check and decrement uses before rolling
544
+ if (action.uses && !decrementActionUses(action)) {
545
+ return; // No uses remaining
546
+ }
547
+
548
+ // Check and decrement Ki points if action costs Ki
549
+ const kiCost = getKiCostFromAction(action);
550
+ if (kiCost > 0) {
551
+ const kiResource = getKiPointsResource();
552
+ if (!kiResource) {
553
+ showNotification(`❌ No Ki Points resource found`, 'error');
554
+ return;
555
+ }
556
+ if (kiResource.current < kiCost) {
557
+ showNotification(`❌ Not enough Ki Points! Need ${kiCost}, have ${kiResource.current}`, 'error');
558
+ return;
559
+ }
560
+ kiResource.current -= kiCost;
561
+ saveCharacterData();
562
+ debug.log(`✨ Used ${kiCost} Ki points for ${action.name}. Remaining: ${kiResource.current}/${kiResource.max}`);
563
+ showNotification(`✨ ${action.name}! (${kiResource.current}/${kiResource.max} Ki left)`);
564
+ buildSheet(characterData); // Refresh display
565
+ }
566
+
567
+ // Check and decrement Sorcery Points if action costs them
568
+ const sorceryCost = getSorceryPointCostFromAction(action);
569
+ if (sorceryCost > 0) {
570
+ const sorceryResource = getSorceryPointsResource();
571
+ if (!sorceryResource) {
572
+ showNotification(`❌ No Sorcery Points resource found`, 'error');
573
+ return;
574
+ }
575
+ if (sorceryResource.current < sorceryCost) {
576
+ showNotification(`❌ Not enough Sorcery Points! Need ${sorceryCost}, have ${sorceryResource.current}`, 'error');
577
+ return;
578
+ }
579
+ sorceryResource.current -= sorceryCost;
580
+ saveCharacterData();
581
+ debug.log(`✨ Used ${sorceryCost} Sorcery Points for ${action.name}. Remaining: ${sorceryResource.current}/${sorceryResource.max}`);
582
+ showNotification(`✨ ${action.name}! (${sorceryResource.current}/${sorceryResource.max} SP left)`);
583
+ buildSheet(characterData); // Refresh display
584
+ }
585
+
586
+ // Check and decrement other resources (Wild Shape, Breath Weapon, etc.)
587
+ if (!decrementActionResources(action)) {
588
+ return; // Not enough resources
589
+ }
590
+
591
+ // Roll the damage/healing
592
+ const rollType = option.type === 'healing' ? 'Healing' : (option.type === 'temphp' ? 'Temp HP' : 'Damage');
593
+ let damageFormula = option.formula;
594
+
595
+ // Add Sneak Attack if toggle is enabled and this is a damage roll (not healing/temphp)
596
+ if (option.type === 'damage' && sneakAttackEnabled && sneakAttackDamage && action.attackRoll) {
597
+ damageFormula += `+${sneakAttackDamage}`;
598
+ debug.log(`🎯 Adding Sneak Attack to ${action.name} damage: ${damageFormula}`);
599
+ }
600
+
601
+ // Add Elemental Weapon if toggle is enabled and this is a damage roll
602
+ if (option.type === 'damage' && elementalWeaponEnabled && elementalWeaponDamage && action.attackRoll) {
603
+ damageFormula += `+${elementalWeaponDamage}`;
604
+ debug.log(`⚔️ Adding Elemental Weapon to ${action.name} damage: ${damageFormula}`);
605
+ }
606
+
607
+ roll(`${action.name} ${rollType}`, damageFormula);
608
+ }
609
+ });
610
+
611
+ buttonsDiv.appendChild(actionBtn);
612
+ });
613
+ }
614
+
615
+ // Add "Use" button for actions with no attack/damage options
616
+ // Show for any action that should be usable (has description OR is a valid action type)
617
+ if (actionOptions.length === 0 && !actionOptionsResult.skipNormalButtons) {
618
+ const useBtn = document.createElement('button');
619
+ useBtn.className = 'use-btn';
620
+ useBtn.textContent = '✨ Use';
621
+ useBtn.style.cssText = `
622
+ background: #9b59b6;
623
+ color: white;
624
+ border: none;
625
+ padding: 8px 12px;
626
+ border-radius: 4px;
627
+ cursor: pointer;
628
+ font-size: 12px;
629
+ font-weight: bold;
630
+ `;
631
+ useBtn.addEventListener('click', () => {
632
+ // Special handling for Divine Spark
633
+ if (action.name === 'Divine Spark') {
634
+ // Find Channel Divinity resource from the resources array
635
+ const channelDivinityResource = characterData.resources?.find(r =>
636
+ r.name === 'Channel Divinity' ||
637
+ r.variableName === 'channelDivinityCleric' ||
638
+ r.variableName === 'channelDivinityPaladin' ||
639
+ r.variableName === 'channelDivinity'
640
+ );
641
+
642
+ if (!channelDivinityResource) {
643
+ showNotification('❌ No Channel Divinity resource found', 'error');
644
+ return;
645
+ }
646
+
647
+ if (channelDivinityResource.current <= 0) {
648
+ showNotification('❌ No Channel Divinity uses remaining!', 'error');
649
+ return;
650
+ }
651
+
652
+ // Show the Divine Spark choice modal
653
+ showDivineSparkModal(action, channelDivinityResource);
654
+ return;
655
+ }
656
+
657
+ // Special handling for Harness Divine Power
658
+ if (action.name === 'Harness Divine Power' || action.name === 'Recover Spell Slot') {
659
+ // Find Channel Divinity resource from the resources array
660
+ const channelDivinityResource = characterData.resources?.find(r =>
661
+ r.name === 'Channel Divinity' ||
662
+ r.variableName === 'channelDivinityCleric' ||
663
+ r.variableName === 'channelDivinityPaladin' ||
664
+ r.variableName === 'channelDivinity'
665
+ );
666
+
667
+ if (!channelDivinityResource) {
668
+ showNotification('❌ No Channel Divinity resource found', 'error');
669
+ return;
670
+ }
671
+
672
+ if (channelDivinityResource.current <= 0) {
673
+ showNotification('❌ No Channel Divinity uses remaining!', 'error');
674
+ return;
675
+ }
676
+
677
+ // Show the Harness Divine Power choice modal
678
+ showHarnessDivinePowerModal(action, channelDivinityResource);
679
+ return;
680
+ }
681
+
682
+ // Special handling for Elemental Weapon
683
+ if (action.name === 'Elemental Weapon') {
684
+ // Show the Elemental Weapon choice modal
685
+ showElementalWeaponModal(action);
686
+ return;
687
+ }
688
+
689
+ // Special handling for Divine Intervention
690
+ if (action.name === 'Divine Intervention') {
691
+ // Show the Divine Intervention modal
692
+ showDivineInterventionModal(action);
693
+ return;
694
+ }
695
+
696
+ // Special handling for Starry Form (Stars Druid)
697
+ if (action.name && /starry form/i.test(action.name)) {
698
+ if (typeof showStarryFormModal === 'function') {
699
+ showStarryFormModal(action);
700
+ return;
701
+ }
702
+ }
703
+
704
+ // Special handling for Wild Shape
705
+ if (action.name === 'Wild Shape' || action.name === 'Combat Wild Shape') {
706
+ // Show the Wild Shape choice modal
707
+ showWildShapeModal(action);
708
+ return;
709
+ }
710
+
711
+ // Special handling for Shapechange
712
+ if (action.name === 'Shapechange') {
713
+ // Show the Shapechange choice modal
714
+ showShapechangeModal(action);
715
+ return;
716
+ }
717
+
718
+ // Special handling for True Polymorph
719
+ if (action.name === 'True Polymorph') {
720
+ // Show the True Polymorph choice modal
721
+ showTruePolymorphModal(action);
722
+ return;
723
+ }
724
+
725
+ // Special handling for Conjure Animals/Elementals/Fey/Celestial
726
+ if (action.name && (
727
+ action.name.includes('Conjure Animals') ||
728
+ action.name.includes('Conjure Elemental') ||
729
+ action.name.includes('Conjure Fey') ||
730
+ action.name.includes('Conjure Celestial')
731
+ )) {
732
+ // Show the Conjure choice modal
733
+ showConjureModal(action);
734
+ return;
735
+ }
736
+
737
+ // Special handling for Planar Binding
738
+ if (action.name === 'Planar Binding') {
739
+ // Show the Planar Binding choice modal
740
+ showPlanarBindingModal(action);
741
+ return;
742
+ }
743
+
744
+ // Special handling for Teleport
745
+ if (action.name === 'Teleport') {
746
+ // Show the Teleport choice modal
747
+ showTeleportModal(action);
748
+ return;
749
+ }
750
+
751
+ // Special handling for Word of Recall
752
+ if (action.name === 'Word of Recall') {
753
+ // Show the Word of Recall choice modal
754
+ showWordOfRecallModal(action);
755
+ return;
756
+ }
757
+
758
+ // Special handling for Contingency
759
+ if (action.name === 'Contingency') {
760
+ // Show the Contingency choice modal
761
+ showContingencyModal(action);
762
+ return;
763
+ }
764
+
765
+ // Special handling for Glyph of Warding
766
+ if (action.name === 'Glyph of Warding') {
767
+ // Show the Glyph of Warding choice modal
768
+ showGlyphOfWardingModal(action);
769
+ return;
770
+ }
771
+
772
+ // Special handling for Symbol
773
+ if (action.name === 'Symbol') {
774
+ // Show the Symbol choice modal
775
+ showSymbolModal(action);
776
+ return;
777
+ }
778
+
779
+ // Special handling for Programmed Illusion
780
+ if (action.name === 'Programmed Illusion') {
781
+ // Show the Programmed Illusion choice modal
782
+ showProgrammedIllusionModal(action);
783
+ return;
784
+ }
785
+
786
+ // Special handling for Sequester
787
+ if (action.name === 'Sequester') {
788
+ // Show the Sequester choice modal
789
+ showSequesterModal(action);
790
+ return;
791
+ }
792
+
793
+ // Special handling for Clone
794
+ if (action.name === 'Clone') {
795
+ // Show the Clone choice modal
796
+ showCloneModal(action);
797
+ return;
798
+ }
799
+
800
+ // Special handling for Astral Projection
801
+ if (action.name === 'Astral Projection') {
802
+ // Show the Astral Projection choice modal
803
+ showAstralProjectionModal(action);
804
+ return;
805
+ }
806
+
807
+ // Special handling for Etherealness
808
+ if (action.name === 'Etherealness') {
809
+ // Show the Etherealness choice modal
810
+ showEtherealnessModal(action);
811
+ return;
812
+ }
813
+
814
+ // Special handling for Magic Jar
815
+ if (action.name === 'Magic Jar') {
816
+ // Show the Magic Jar choice modal
817
+ showMagicJarModal(action);
818
+ return;
819
+ }
820
+
821
+ // Special handling for Imprisonment
822
+ if (action.name === 'Imprisonment') {
823
+ // Show the Imprisonment choice modal
824
+ showImprisonmentModal(action);
825
+ return;
826
+ }
827
+
828
+ // Special handling for Time Stop
829
+ if (action.name === 'Time Stop') {
830
+ // Show the Time Stop choice modal
831
+ showTimeStopModal(action);
832
+ return;
833
+ }
834
+
835
+ // Special handling for Mirage Arcane
836
+ if (action.name === 'Mirage Arcane') {
837
+ // Show the Mirage Arcane choice modal
838
+ showMirageArcaneModal(action);
839
+ return;
840
+ }
841
+
842
+ // Special handling for Forcecage
843
+ if (action.name === 'Forcecage') {
844
+ // Show the Forcecage choice modal
845
+ showForcecageModal(action);
846
+ return;
847
+ }
848
+
849
+ // Special handling for Maze
850
+ if (action.name === 'Maze') {
851
+ // Show the Maze choice modal
852
+ showMazeModal(action);
853
+ return;
854
+ }
855
+
856
+ // Special handling for Wish
857
+ if (action.name === 'Wish') {
858
+ // Show the Wish choice modal
859
+ showWishModal(action);
860
+ return;
861
+ }
862
+
863
+ // Special handling for Simulacrum
864
+ if (action.name === 'Simulacrum') {
865
+ // Show the Simulacrum choice modal
866
+ showSimulacrumModal(action);
867
+ return;
868
+ }
869
+
870
+ // Special handling for Gate
871
+ if (action.name === 'Gate') {
872
+ // Show the Gate choice modal
873
+ showGateModal(action);
874
+ return;
875
+ }
876
+
877
+ // Special handling for Legend Lore
878
+ if (action.name === 'Legend Lore') {
879
+ // Show the Legend Lore choice modal
880
+ showLegendLoreModal(action);
881
+ return;
882
+ }
883
+
884
+ // Special handling for Commune
885
+ if (action.name === 'Commune') {
886
+ // Show the Commune choice modal
887
+ showCommuneModal(action);
888
+ return;
889
+ }
890
+
891
+ // Special handling for Augury
892
+ if (action.name === 'Augury') {
893
+ // Show the Augury choice modal
894
+ showAuguryModal(action);
895
+ return;
896
+ }
897
+
898
+ // Special handling for Divination
899
+ if (action.name === 'Divination') {
900
+ // Show the Divination choice modal
901
+ showDivinationModal(action);
902
+ return;
903
+ }
904
+
905
+ // Special handling for Contact Other Plane
906
+ if (action.name === 'Contact Other Plane') {
907
+ // Show the Contact Other Plane choice modal
908
+ showContactOtherPlaneModal(action);
909
+ return;
910
+ }
911
+
912
+ // Special handling for Find the Path
913
+ if (action.name === 'Find the Path') {
914
+ // Show the Find the Path choice modal
915
+ showFindThePathModal(action);
916
+ return;
917
+ }
918
+
919
+ // Special handling for Speak with Dead
920
+ if (action.name === 'Speak with Dead') {
921
+ // Show the Speak with Dead choice modal
922
+ showSpeakWithDeadModal(action);
923
+ return;
924
+ }
925
+
926
+ // Special handling for Speak with Animals
927
+ if (action.name === 'Speak with Animals') {
928
+ // Show the Speak with Animals choice modal
929
+ showSpeakWithAnimalsModal(action);
930
+ return;
931
+ }
932
+
933
+ // Special handling for Speak with Plants
934
+ if (action.name === 'Speak with Plants') {
935
+ // Show the Speak with Plants choice modal
936
+ showSpeakWithPlantsModal(action);
937
+ return;
938
+ }
939
+
940
+ // Special handling for Zone of Truth
941
+ if (action.name === 'Zone of Truth') {
942
+ // Show the Zone of Truth choice modal
943
+ showZoneOfTruthModal(action);
944
+ return;
945
+ }
946
+
947
+ // Special handling for Sending
948
+ if (action.name === 'Sending') {
949
+ // Show the Sending choice modal
950
+ showSendingModal(action);
951
+ return;
952
+ }
953
+
954
+ // Special handling for Dream
955
+ if (action.name === 'Dream') {
956
+ // Show the Dream choice modal
957
+ showDreamModal(action);
958
+ return;
959
+ }
960
+
961
+ // Special handling for Scrying
962
+ if (action.name === 'Scrying') {
963
+ // Show the Scrying choice modal
964
+ showScryingModal(action);
965
+ return;
966
+ }
967
+
968
+ // Special handling for Dispel Evil and Good
969
+ if (action.name === 'Dispel Evil and Good') {
970
+ // Show the Dispel Evil and Good choice modal
971
+ showDispelEvilAndGoodModal(action);
972
+ return;
973
+ }
974
+
975
+ // Special handling for Freedom of Movement
976
+ if (action.name === 'Freedom of Movement') {
977
+ // Show the Freedom of Movement choice modal
978
+ showFreedomOfMovementModal(action);
979
+ return;
980
+ }
981
+
982
+ // Special handling for Nondetection
983
+ if (action.name === 'Nondetection') {
984
+ // Show the Nondetection choice modal
985
+ showNondetectionModal(action);
986
+ return;
987
+ }
988
+
989
+ // Special handling for Protection from Energy
990
+ if (action.name === 'Protection from Energy') {
991
+ // Show the Protection from Energy choice modal
992
+ showProtectionFromEnergyModal(action);
993
+ return;
994
+ }
995
+
996
+ // Special handling for Protection from Evil and Good
997
+ if (action.name === 'Protection from Evil and Good') {
998
+ // Show the Protection from Evil and Good choice modal
999
+ showProtectionFromEvilAndGoodModal(action);
1000
+ return;
1001
+ }
1002
+
1003
+ // Special handling for Sanctuary
1004
+ if (action.name === 'Sanctuary') {
1005
+ // Show the Sanctuary choice modal
1006
+ showSanctuaryModal(action);
1007
+ return;
1008
+ }
1009
+
1010
+ // Special handling for Silence
1011
+ if (action.name === 'Silence') {
1012
+ // Show the Silence choice modal
1013
+ showSilenceModal(action);
1014
+ return;
1015
+ }
1016
+
1017
+ // Special handling for Magic Circle
1018
+ if (action.name === 'Magic Circle') {
1019
+ // Show the Magic Circle choice modal
1020
+ showMagicCircleModal(action);
1021
+ return;
1022
+ }
1023
+
1024
+ // Special handling for Greater Restoration
1025
+ if (action.name === 'Greater Restoration') {
1026
+ // Show the Greater Restoration choice modal
1027
+ showGreaterRestorationModal(action);
1028
+ return;
1029
+ }
1030
+
1031
+ // Special handling for Remove Curse
1032
+ if (action.name === 'Remove Curse') {
1033
+ // Show the Remove Curse choice modal
1034
+ showRemoveCurseModal(action);
1035
+ return;
1036
+ }
1037
+
1038
+ // Special handling for Revivify
1039
+ if (action.name === 'Revivify') {
1040
+ // Show the Revivify choice modal
1041
+ showRevivifyModal(action);
1042
+ return;
1043
+ }
1044
+
1045
+ // Special handling for Raise Dead
1046
+ if (action.name === 'Raise Dead') {
1047
+ // Show the Raise Dead choice modal
1048
+ showRaiseDeadModal(action);
1049
+ return;
1050
+ }
1051
+
1052
+ // Special handling for Resurrection
1053
+ if (action.name === 'Resurrection') {
1054
+ // Show the Resurrection choice modal
1055
+ showResurrectionModal(action);
1056
+ return;
1057
+ }
1058
+
1059
+ // Special handling for True Resurrection
1060
+ if (action.name === 'True Resurrection') {
1061
+ // Show the True Resurrection choice modal
1062
+ showTrueResurrectionModal(action);
1063
+ return;
1064
+ }
1065
+
1066
+ // Special handling for Detect Magic
1067
+ if (action.name === 'Detect Magic') {
1068
+ // Show the Detect Magic choice modal
1069
+ showDetectMagicModal(action);
1070
+ return;
1071
+ }
1072
+
1073
+ // Special handling for Identify
1074
+ if (action.name === 'Identify') {
1075
+ // Show the Identify choice modal
1076
+ showIdentifyModal(action);
1077
+ return;
1078
+ }
1079
+
1080
+ // Special handling for Dispel Magic
1081
+ if (action.name === 'Dispel Magic') {
1082
+ // Show the Dispel Magic choice modal
1083
+ showDispelMagicModal(action);
1084
+ return;
1085
+ }
1086
+
1087
+ // Special handling for Feather Fall
1088
+ if (action.name === 'Feather Fall') {
1089
+ // Show the Feather Fall choice modal
1090
+ showFeatherFallModal(action);
1091
+ return;
1092
+ }
1093
+
1094
+ // Special handling for Hellish Rebuke
1095
+ if (action.name === 'Hellish Rebuke') {
1096
+ // Show the Hellish Rebuke choice modal
1097
+ showHellishRebukeModal(action);
1098
+ return;
1099
+ }
1100
+
1101
+ // Special handling for Shield
1102
+ if (action.name === 'Shield') {
1103
+ // Show the Shield choice modal
1104
+ showShieldModal(action);
1105
+ return;
1106
+ }
1107
+
1108
+ // Special handling for Absorb Elements
1109
+ if (action.name === 'Absorb Elements') {
1110
+ // Show the Absorb Elements choice modal
1111
+ showAbsorbElementsModal(action);
1112
+ return;
1113
+ }
1114
+
1115
+ // Special handling for Counterspell
1116
+ if (action.name === 'Counterspell') {
1117
+ // Show the Counterspell choice modal
1118
+ showCounterspellModal(action);
1119
+ return;
1120
+ }
1121
+
1122
+ // Special handling for Fire Shield
1123
+ if (action.name === 'Fire Shield') {
1124
+ // Show the Fire Shield choice modal
1125
+ showFireShieldModal(action);
1126
+ return;
1127
+ }
1128
+
1129
+ // Special handling for Armor of Agathys
1130
+ if (action.name === 'Armor of Agathys') {
1131
+ // Show the Armor of Agathys choice modal
1132
+ showArmorOfAgathysModal(action);
1133
+ return;
1134
+ }
1135
+
1136
+ // Special handling for Meld into Stone
1137
+ if (action.name === 'Meld into Stone') {
1138
+ // Show the Meld into Stone choice modal
1139
+ showMeldIntoStoneModal(action);
1140
+ return;
1141
+ }
1142
+
1143
+ // Special handling for Vampiric Touch
1144
+ if (action.name === 'Vampiric Touch') {
1145
+ // Show the Vampiric Touch choice modal
1146
+ showVampiricTouchModal(action);
1147
+ return;
1148
+ }
1149
+
1150
+ // Special handling for Life Transference
1151
+ if (action.name === 'Life Transference') {
1152
+ // Show the Life Transference choice modal
1153
+ showLifeTransferenceModal(action);
1154
+ return;
1155
+ }
1156
+
1157
+ // Special handling for Geas
1158
+ if (action.name === 'Geas') {
1159
+ // Show the Geas choice modal
1160
+ showGeasModal(action);
1161
+ return;
1162
+ }
1163
+
1164
+ // Special handling for Symbol
1165
+ if (action.name === 'Symbol') {
1166
+ // Show the Symbol choice modal
1167
+ showSymbolModal(action);
1168
+ return;
1169
+ }
1170
+
1171
+ // Special handling for Spiritual Weapon
1172
+ if (action.name === 'Spiritual Weapon') {
1173
+ // Show the Spiritual Weapon choice modal
1174
+ showSpiritualWeaponModal(action);
1175
+ return;
1176
+ }
1177
+
1178
+ // Special handling for Flaming Sphere
1179
+ if (action.name === 'Flaming Sphere') {
1180
+ // Show the Flaming Sphere choice modal
1181
+ showFlamingSphereModal(action);
1182
+ return;
1183
+ }
1184
+
1185
+ // Special handling for Bigby's Hand
1186
+ if (action.name === 'Bigby\'s Hand') {
1187
+ // Show the Bigby's Hand choice modal
1188
+ showBigbysHandModal(action);
1189
+ return;
1190
+ }
1191
+
1192
+ // Special handling for Animate Objects
1193
+ if (action.name === 'Animate Objects') {
1194
+ // Show the Animate Objects choice modal
1195
+ showAnimateObjectsModal(action);
1196
+ return;
1197
+ }
1198
+
1199
+ // Special handling for Moonbeam
1200
+ if (action.name === 'Moonbeam') {
1201
+ // Show the Moonbeam choice modal
1202
+ showMoonbeamModal(action);
1203
+ return;
1204
+ }
1205
+
1206
+ // Special handling for Healing Spirit
1207
+ if (action.name === 'Healing Spirit') {
1208
+ // Show the Healing Spirit choice modal
1209
+ showHealingSpiritModal(action);
1210
+ return;
1211
+ }
1212
+
1213
+ // Special handling for Bless
1214
+ if (action.name === 'Bless') {
1215
+ // Show the Bless choice modal
1216
+ showBlessModal(action);
1217
+ return;
1218
+ }
1219
+
1220
+ // Special handling for Bane
1221
+ if (action.name === 'Bane') {
1222
+ // Show the Bane choice modal
1223
+ showBaneModal(action);
1224
+ return;
1225
+ }
1226
+
1227
+ // Special handling for Guidance
1228
+ if (action.name === 'Guidance') {
1229
+ // Show the Guidance choice modal
1230
+ showGuidanceModal(action);
1231
+ return;
1232
+ }
1233
+
1234
+ // Special handling for Resistance
1235
+ if (action.name === 'Resistance') {
1236
+ // Show the Resistance choice modal
1237
+ showResistanceModal(action);
1238
+ return;
1239
+ }
1240
+
1241
+ // Special handling for Hex
1242
+ if (action.name === 'Hex') {
1243
+ // Show the Hex choice modal
1244
+ showHexModal(action);
1245
+ return;
1246
+ }
1247
+
1248
+ // Special handling for Hunter's Mark
1249
+ if (action.name === 'Hunter\'s Mark') {
1250
+ // Show the Hunter's Mark choice modal
1251
+ showHuntersMarkModal(action);
1252
+ return;
1253
+ }
1254
+
1255
+ // Special handling for Magic Missile
1256
+ if (action.name === 'Magic Missile') {
1257
+ // Show the Magic Missile choice modal
1258
+ showMagicMissileModal(action);
1259
+ return;
1260
+ }
1261
+
1262
+ // Special handling for Scorching Ray
1263
+ if (action.name === 'Scorching Ray') {
1264
+ // Show the Scorching Ray choice modal
1265
+ showScorchingRayModal(action);
1266
+ return;
1267
+ }
1268
+
1269
+ // Special handling for Aid
1270
+ if (action.name === 'Aid') {
1271
+ // Show the Aid choice modal
1272
+ showAidModal(action);
1273
+ return;
1274
+ }
1275
+
1276
+ // Note: Eldritch Blast uses standard attack/damage buttons, no special modal needed
1277
+
1278
+ // Special handling for Spirit Guardians
1279
+ if (action.name === 'Spirit Guardians') {
1280
+ // Show the Spirit Guardians choice modal
1281
+ showSpiritGuardiansModal(action);
1282
+ return;
1283
+ }
1284
+
1285
+ // Special handling for Cloud of Daggers
1286
+ if (action.name === 'Cloud of Daggers') {
1287
+ // Show the Cloud of Daggers choice modal
1288
+ showCloudOfDaggersModal(action);
1289
+ return;
1290
+ }
1291
+
1292
+ // Special handling for Spike Growth
1293
+ if (action.name === 'Spike Growth') {
1294
+ // Show the Spike Growth choice modal
1295
+ showSpikeGrowthModal(action);
1296
+ return;
1297
+ }
1298
+
1299
+ // Special handling for Wall of Fire
1300
+ if (action.name === 'Wall of Fire') {
1301
+ // Show the Wall of Fire choice modal
1302
+ showWallOfFireModal(action);
1303
+ return;
1304
+ }
1305
+
1306
+ // Special handling for Haste
1307
+ if (action.name === 'Haste') {
1308
+ // Show the Haste choice modal
1309
+ showHasteModal(action);
1310
+ return;
1311
+ }
1312
+
1313
+ // Special handling for Booming Blade
1314
+ if (action.name === 'Booming Blade') {
1315
+ // Show the Booming Blade choice modal
1316
+ showBoomingBladeModal(action);
1317
+ return;
1318
+ }
1319
+
1320
+ // Special handling for Green-Flame Blade
1321
+ if (action.name === 'Green-Flame Blade') {
1322
+ // Show the Green-Flame Blade choice modal
1323
+ showGreenFlameBladeModal(action);
1324
+ return;
1325
+ }
1326
+
1327
+ // Special handling for Chromatic Orb
1328
+ if (action.name === 'Chromatic Orb') {
1329
+ // Show the Chromatic Orb choice modal
1330
+ showChromaticOrbModal(action);
1331
+ return;
1332
+ }
1333
+
1334
+ // Special handling for Dragon's Breath
1335
+ if (action.name === 'Dragon\'s Breath') {
1336
+ // Show the Dragons Breath choice modal
1337
+ showDragonsBreathModal(action);
1338
+ return;
1339
+ }
1340
+
1341
+ // Special handling for Chaos Bolt
1342
+ if (action.name === 'Chaos Bolt') {
1343
+ // Show the Chaos Bolt choice modal
1344
+ showChaosBoltModal(action);
1345
+ return;
1346
+ }
1347
+
1348
+ // Special handling for Delayed Blast Fireball
1349
+ if (action.name === 'Delayed Blast Fireball') {
1350
+ // Show the Delayed Blast Fireball choice modal
1351
+ showDelayedBlastFireballModal(action);
1352
+ return;
1353
+ }
1354
+
1355
+ // Special handling for Polymorph
1356
+ if (action.name === 'Polymorph') {
1357
+ // Show the Polymorph choice modal
1358
+ showPolymorphModal(action);
1359
+ return;
1360
+ }
1361
+
1362
+ // Special handling for True Polymorph
1363
+ if (action.name === 'True Polymorph') {
1364
+ // Show the True Polymorph choice modal
1365
+ showTruePolymorphModal(action);
1366
+ return;
1367
+ }
1368
+
1369
+ // Default handling for other actions
1370
+
1371
+ // Check and decrement uses BEFORE announcing (so announcement shows correct count)
1372
+ if (action.uses && !decrementActionUses(action)) {
1373
+ return; // No uses remaining
1374
+ }
1375
+
1376
+ // Check and decrement Ki points if action costs Ki
1377
+ const kiCost = getKiCostFromAction(action);
1378
+ if (kiCost > 0) {
1379
+ const kiResource = getKiPointsResource();
1380
+ if (!kiResource) {
1381
+ showNotification(`❌ No Ki Points resource found`, 'error');
1382
+ return;
1383
+ }
1384
+ if (kiResource.current < kiCost) {
1385
+ showNotification(`❌ Not enough Ki Points! Need ${kiCost}, have ${kiResource.current}`, 'error');
1386
+ return;
1387
+ }
1388
+ kiResource.current -= kiCost;
1389
+ saveCharacterData();
1390
+ debug.log(`✨ Used ${kiCost} Ki points for ${action.name}. Remaining: ${kiResource.current}/${kiResource.max}`);
1391
+ showNotification(`✨ ${action.name}! (${kiResource.current}/${kiResource.max} Ki left)`);
1392
+ buildSheet(characterData); // Refresh display
1393
+ }
1394
+
1395
+ // Check and decrement Sorcery Points if action costs them
1396
+ const sorceryCost = getSorceryPointCostFromAction(action);
1397
+ if (sorceryCost > 0) {
1398
+ const sorceryResource = getSorceryPointsResource();
1399
+ if (!sorceryResource) {
1400
+ showNotification(`❌ No Sorcery Points resource found`, 'error');
1401
+ return;
1402
+ }
1403
+ if (sorceryResource.current < sorceryCost) {
1404
+ showNotification(`❌ Not enough Sorcery Points! Need ${sorceryCost}, have ${sorceryResource.current}`, 'error');
1405
+ return;
1406
+ }
1407
+ sorceryResource.current -= sorceryCost;
1408
+ saveCharacterData();
1409
+ debug.log(`✨ Used ${sorceryCost} Sorcery Points for ${action.name}. Remaining: ${sorceryResource.current}/${sorceryResource.max}`);
1410
+ showNotification(`✨ ${action.name}! (${sorceryResource.current}/${sorceryResource.max} SP left)`);
1411
+ buildSheet(characterData); // Refresh display
1412
+ }
1413
+
1414
+ // Check and decrement other resources (Wild Shape, Breath Weapon, etc.)
1415
+ if (!decrementActionResources(action)) {
1416
+ return; // Not enough resources
1417
+ }
1418
+
1419
+ // Announce the action AFTER all decrements (so announcement shows correct counts)
1420
+ announceAction(action);
1421
+
1422
+ // Mark action as used based on action type (auto-tracking during combat)
1423
+ const actionType = action.actionType || 'action';
1424
+ debug.log(`🎯 Action type for "${action.name}": "${actionType}"`);
1425
+
1426
+ // Call global markActionEconomyUsed if available (from popup-sheet.js)
1427
+ if (typeof window.markActionEconomyUsed === 'function') {
1428
+ window.markActionEconomyUsed(actionType);
1429
+ }
1430
+ });
1431
+ buttonsDiv.appendChild(useBtn);
1432
+ }
1433
+
1434
+ // Add Details button if there's any useful information to show
1435
+ const hasDetails = action.description || action.summary || action.damageType || action.attackRoll || action.damage || action.source || action.range;
1436
+ if (hasDetails) {
1437
+ const detailsBtn = document.createElement('button');
1438
+ detailsBtn.className = 'details-btn';
1439
+ detailsBtn.innerHTML = '▼ Details';
1440
+ detailsBtn.style.cssText = `
1441
+ background: #34495e;
1442
+ color: white;
1443
+ border: none;
1444
+ padding: 8px 12px;
1445
+ border-radius: 4px;
1446
+ cursor: pointer;
1447
+ font-size: 12px;
1448
+ font-weight: bold;
1449
+ margin-right: 4px;
1450
+ margin-bottom: 4px;
1451
+ `;
1452
+ detailsBtn.addEventListener('click', () => {
1453
+ const descDiv = actionCard.querySelector('.action-details');
1454
+ if (descDiv) {
1455
+ descDiv.style.display = descDiv.style.display === 'none' ? 'block' : 'none';
1456
+ detailsBtn.innerHTML = descDiv.style.display === 'none' ? '▼ Details' : '▲ Hide';
1457
+ }
1458
+ });
1459
+ buttonsDiv.appendChild(detailsBtn);
1460
+ }
1461
+
1462
+ // Append nameDiv and buttonsDiv to actionHeader
1463
+ actionHeader.appendChild(nameDiv);
1464
+ actionHeader.appendChild(buttonsDiv);
1465
+
1466
+ // Append actionHeader to actionCard
1467
+ actionCard.appendChild(actionHeader);
1468
+
1469
+ // Add details section if any useful information exists (hidden by default, toggled by Details button)
1470
+ if (hasDetails) {
1471
+ const detailsDiv = document.createElement('div');
1472
+ detailsDiv.className = 'action-details';
1473
+ detailsDiv.style.display = 'none'; // Hidden by default
1474
+
1475
+ let detailsHTML = '<div style="margin-top: 10px; padding: 10px; background: var(--bg-secondary, #f5f5f5); border-radius: 4px; font-size: 0.9em;">';
1476
+
1477
+ // Add summary if available
1478
+ if (action.summary) {
1479
+ const resolvedSummary = resolveVariablesInFormula(action.summary);
1480
+ detailsHTML += `<div style="margin-bottom: 8px;"><strong>Summary:</strong> ${resolvedSummary}</div>`;
1481
+ }
1482
+
1483
+ // Add description if available
1484
+ if (action.description) {
1485
+ const resolvedDescription = resolveVariablesInFormula(action.description);
1486
+ detailsHTML += `<div style="margin-bottom: 8px;">${resolvedDescription}</div>`;
1487
+ }
1488
+
1489
+ // Add action details
1490
+ const details = [];
1491
+ if (action.attackRoll) details.push(`<strong>Attack:</strong> ${action.attackRoll}`);
1492
+ if (action.damage) details.push(`<strong>Damage:</strong> ${action.damage}`);
1493
+ if (action.damageType) details.push(`<strong>Type:</strong> ${action.damageType}`);
1494
+ if (action.range) details.push(`<strong>Range:</strong> ${action.range}`);
1495
+ if (action.source) details.push(`<strong>Source:</strong> ${action.source}`);
1496
+
1497
+ if (details.length > 0) {
1498
+ detailsHTML += `<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 0.85em;">${details.join(' • ')}</div>`;
1499
+ }
1500
+
1501
+ detailsHTML += '</div>';
1502
+ detailsDiv.innerHTML = detailsHTML;
1503
+
1504
+ actionCard.appendChild(detailsDiv);
1505
+ }
1506
+
1507
+ container.appendChild(actionCard);
1508
+ });
1509
+ }
1510
+
1511
+ // Note: Inventory functions (rebuildInventory, buildInventoryDisplay, createInventoryCard)
1512
+ // are now provided by inventory-manager.js
1513
+
1514
+ // buildCompanionsDisplay is now in modules/companions-manager.js
1515
+
1516
+ function decrementActionUses(action) {
1517
+ if (!action.uses) {
1518
+ return true; // No uses to track, allow action
1519
+ }
1520
+
1521
+ const usesTotal = action.uses.total || action.uses.value || action.uses;
1522
+ const usesUsed = action.usesUsed || 0;
1523
+ const usesRemaining = action.usesLeft !== undefined ? action.usesLeft : (usesTotal - usesUsed);
1524
+
1525
+ if (usesRemaining <= 0) {
1526
+ showNotification(`❌ No uses remaining for ${action.name}`, 'error');
1527
+ return false;
1528
+ }
1529
+
1530
+ // Increment usesUsed and decrement usesLeft
1531
+ action.usesUsed = usesUsed + 1;
1532
+ if (action.usesLeft !== undefined) {
1533
+ action.usesLeft = usesRemaining - 1;
1534
+ }
1535
+ const newRemaining = action.usesLeft !== undefined ? action.usesLeft : (usesTotal - action.usesUsed);
1536
+
1537
+ // Update character data and save
1538
+ saveCharacterData();
1539
+
1540
+ // Show notification
1541
+ showNotification(`✅ Used ${action.name} (${newRemaining}/${usesTotal} remaining)`);
1542
+
1543
+ // Rebuild the actions display to show updated count
1544
+ const actionsContainer = document.getElementById('actions-container');
1545
+ buildActionsDisplay(actionsContainer, characterData.actions);
1546
+
1547
+ return true;
1548
+ }
1549
+
1550
+ // ===== EXPORTS =====
1551
+
1552
+ window.buildActionsDisplay = buildActionsDisplay;
1553
+ window.decrementActionUses = decrementActionUses;
1554
+
1555
+ console.log('✅ Action Display module loaded');
1556
+
1557
+ })();