@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,959 @@
1
+ /**
2
+ * Character Trait Popups Module
3
+ *
4
+ * Handles all character trait popup UI (Halfling Luck, Lucky, Elven Accuracy, etc.).
5
+ * Loaded as a plain script (no ES6 modules) to export to globalThis.
6
+ *
7
+ * Functions exported to globalThis:
8
+ * - getPopupThemeColors()
9
+ * - showHalflingLuckPopup(rollData)
10
+ * - showLuckyPopup(rollData)
11
+ * - showTraitChoicePopup(rollData)
12
+ * - showWildMagicSurgePopup(d100Roll, effect)
13
+ * - showBardicInspirationPopup(rollData)
14
+ * - showElvenAccuracyPopup(rollData)
15
+ * - performHalflingReroll(originalRollData)
16
+ * - performLuckyReroll(originalRollData)
17
+ * - performBardicInspirationRoll(rollData)
18
+ * - performElvenAccuracyReroll(originalRollData)
19
+ */
20
+
21
+ (function() {
22
+ 'use strict';
23
+
24
+ // ===== HELPER FUNCTIONS =====
25
+
26
+ /**
27
+ * Get theme-aware colors for popups
28
+ * @returns {Object} Color scheme based on current theme
29
+ */
30
+ function getPopupThemeColors() {
31
+ const isDarkMode = document.documentElement.classList.contains('theme-dark') ||
32
+ document.documentElement.getAttribute('data-theme') === 'dark';
33
+
34
+ return {
35
+ background: isDarkMode ? '#2d2d2d' : '#ffffff',
36
+ text: isDarkMode ? '#e0e0e0' : '#333333',
37
+ heading: isDarkMode ? '#ffffff' : '#2D8B83',
38
+ border: isDarkMode ? '#444444' : '#f0f8ff',
39
+ borderAccent: isDarkMode ? '#2D8B83' : '#2D8B83',
40
+ infoBox: isDarkMode ? '#1a1a1a' : '#f0f8ff',
41
+ infoText: isDarkMode ? '#b0b0b0' : '#666666'
42
+ };
43
+ }
44
+
45
+ // ===== HALFLING LUCK POPUP =====
46
+
47
+ /**
48
+ * Show Halfling Luck popup when rolling a natural 1
49
+ * @param {Object} rollData - Roll information
50
+ */
51
+ function showHalflingLuckPopup(rollData) {
52
+ debug.log('🍀 Halfling Luck popup called with:', rollData);
53
+
54
+ // Check if document.body exists
55
+ if (!document.body) {
56
+ debug.error('❌ document.body not available for Halfling Luck popup');
57
+ showNotification('🍀 Halfling Luck triggered! (Popup failed to display)', 'info');
58
+ return;
59
+ }
60
+
61
+ debug.log('🍀 Creating popup overlay...');
62
+
63
+ // Get theme-aware colors
64
+ const colors = getPopupThemeColors();
65
+
66
+ // Create popup overlay
67
+ const popupOverlay = document.createElement('div');
68
+ popupOverlay.style.cssText = `
69
+ position: fixed;
70
+ top: 0;
71
+ left: 0;
72
+ width: 100%;
73
+ height: 100%;
74
+ background: rgba(0, 0, 0, 0.8);
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ z-index: 10000;
79
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
80
+ `;
81
+
82
+ // Create popup content
83
+ const popupContent = document.createElement('div');
84
+ popupContent.style.cssText = `
85
+ background: ${colors.background};
86
+ border-radius: 12px;
87
+ padding: 24px;
88
+ max-width: 400px;
89
+ width: 90%;
90
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
91
+ text-align: center;
92
+ `;
93
+
94
+ debug.log('🍀 Setting popup content HTML...');
95
+
96
+ popupContent.innerHTML = `
97
+ <div style="font-size: 24px; margin-bottom: 16px;">🍀</div>
98
+ <h2 style="margin: 0 0 8px 0; color: ${colors.heading};">Halfling Luck!</h2>
99
+ <p style="margin: 0 0 16px 0; color: ${colors.text};">
100
+ You rolled a natural 1! As a Halfling, you can reroll this d20.
101
+ </p>
102
+ <div style="margin: 0 0 16px 0; padding: 12px; background: ${colors.infoBox}; border-radius: 8px; border-left: 4px solid ${colors.borderAccent}; color: ${colors.text};">
103
+ <strong>Original Roll:</strong> ${rollData.rollName}<br>
104
+ <strong>Result:</strong> ${rollData.baseRoll} (natural 1)<br>
105
+ <strong>Total:</strong> ${rollData.rollResult}
106
+ </div>
107
+ <div style="display: flex; gap: 12px; justify-content: center;">
108
+ <button id="halflingRerollBtn" style="
109
+ background: #2D8B83;
110
+ color: white;
111
+ border: none;
112
+ padding: 12px 24px;
113
+ border-radius: 8px;
114
+ cursor: pointer;
115
+ font-weight: bold;
116
+ font-size: 14px;
117
+ ">🎲 Reroll</button>
118
+ <button id="halflingKeepBtn" style="
119
+ background: #e74c3c;
120
+ color: white;
121
+ border: none;
122
+ padding: 12px 24px;
123
+ border-radius: 8px;
124
+ cursor: pointer;
125
+ font-weight: bold;
126
+ font-size: 14px;
127
+ ">Keep Roll</button>
128
+ </div>
129
+ `;
130
+
131
+ debug.log('🍀 Appending popup to document.body...');
132
+
133
+ popupOverlay.appendChild(popupContent);
134
+ document.body.appendChild(popupOverlay);
135
+
136
+ // Add event listeners
137
+ document.getElementById('halflingRerollBtn').addEventListener('click', () => {
138
+ debug.log('🍀 User chose to reroll');
139
+ performHalflingReroll(rollData);
140
+ document.body.removeChild(popupOverlay);
141
+ });
142
+
143
+ document.getElementById('halflingKeepBtn').addEventListener('click', () => {
144
+ debug.log('🍀 User chose to keep roll');
145
+ document.body.removeChild(popupOverlay);
146
+ });
147
+
148
+ // Close on overlay click
149
+ popupOverlay.addEventListener('click', (e) => {
150
+ if (e.target === popupOverlay) {
151
+ debug.log('🍀 User closed popup');
152
+ document.body.removeChild(popupOverlay);
153
+ }
154
+ });
155
+
156
+ debug.log('🍀 Halfling Luck popup displayed');
157
+ }
158
+
159
+ /**
160
+ * Perform Halfling Luck reroll
161
+ * @param {Object} originalRollData - Original roll data
162
+ */
163
+ function performHalflingReroll(originalRollData) {
164
+ debug.log('🍀 Performing Halfling reroll for:', originalRollData);
165
+
166
+ // Extract the base formula (remove any modifiers)
167
+ const formula = originalRollData.rollType;
168
+ const baseFormula = formula.split('+')[0]; // Get just the d20 part
169
+
170
+ // Create a new roll with just the d20
171
+ const rerollData = {
172
+ name: `🍀 ${originalRollData.rollName} (Halfling Luck)`,
173
+ formula: baseFormula,
174
+ color: '#2D8B83',
175
+ characterName: characterData.name
176
+ };
177
+
178
+ debug.log('🍀 Reroll data:', rerollData);
179
+
180
+ // TODO: Add Owlbear Rodeo integration for Halfling Luck rerolls
181
+ showNotification('🍀 Halfling Luck reroll initiated!', 'success');
182
+ }
183
+
184
+ // ===== LUCKY FEAT POPUP =====
185
+
186
+ /**
187
+ * Show Lucky Feat popup
188
+ * @param {Object} rollData - Roll information
189
+ */
190
+ function showLuckyPopup(rollData) {
191
+ debug.log('🎖️ Lucky popup called with:', rollData);
192
+
193
+ // Check if document.body exists
194
+ if (!document.body) {
195
+ debug.error('❌ document.body not available for Lucky popup');
196
+ showNotification('🎖️ Lucky triggered! (Popup failed to display)', 'info');
197
+ return;
198
+ }
199
+
200
+ debug.log('🎖️ Creating Lucky popup overlay...');
201
+
202
+ // Get theme-aware colors
203
+ const colors = getPopupThemeColors();
204
+
205
+ // Create popup overlay
206
+ const popupOverlay = document.createElement('div');
207
+ popupOverlay.style.cssText = `
208
+ position: fixed;
209
+ top: 0;
210
+ left: 0;
211
+ right: 0;
212
+ bottom: 0;
213
+ background: rgba(0, 0, 0, 0.8);
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ z-index: 10000;
218
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
219
+ `;
220
+
221
+ // Create popup content
222
+ const popupContent = document.createElement('div');
223
+ popupContent.style.cssText = `
224
+ background: ${colors.background};
225
+ border-radius: 12px;
226
+ padding: 24px;
227
+ max-width: 400px;
228
+ width: 90%;
229
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
230
+ text-align: center;
231
+ `;
232
+
233
+ debug.log('🎖️ Setting Lucky popup content HTML...');
234
+
235
+ popupContent.innerHTML = `
236
+ <div style="font-size: 24px; margin-bottom: 16px;">🎖️</div>
237
+ <h2 style="margin: 0 0 8px 0; color: #f39c12;">Lucky Feat!</h2>
238
+ <p style="margin: 0 0 16px 0; color: ${colors.text};">
239
+ You rolled a ${rollData.baseRoll}! You have ${rollData.luckPointsRemaining} luck points remaining.
240
+ </p>
241
+ <div style="margin: 0 0 16px 0; padding: 12px; background: ${colors.infoBox}; border-radius: 8px; border-left: 4px solid #f39c12; color: ${colors.text};">
242
+ <strong>Original Roll:</strong> ${rollData.rollName}<br>
243
+ <strong>Result:</strong> ${rollData.baseRoll}<br>
244
+ <strong>Luck Points:</strong> ${rollData.luckPointsRemaining}/3
245
+ </div>
246
+ <div style="display: flex; gap: 12px; justify-content: center;">
247
+ <button id="luckyRerollBtn" style="
248
+ background: #f39c12;
249
+ color: white;
250
+ border: none;
251
+ padding: 12px 24px;
252
+ border-radius: 8px;
253
+ cursor: pointer;
254
+ font-size: 16px;
255
+ font-weight: bold;
256
+ transition: background 0.2s;
257
+ ">
258
+ 🎲 Reroll (Use Luck Point)
259
+ </button>
260
+ <button id="luckyKeepBtn" style="
261
+ background: #95a5a6;
262
+ color: white;
263
+ border: none;
264
+ padding: 12px 24px;
265
+ border-radius: 8px;
266
+ cursor: pointer;
267
+ font-size: 16px;
268
+ font-weight: bold;
269
+ transition: background 0.2s;
270
+ ">
271
+ Keep Roll
272
+ </button>
273
+ </div>
274
+ `;
275
+
276
+ popupOverlay.appendChild(popupContent);
277
+ document.body.appendChild(popupOverlay);
278
+
279
+ debug.log('🎖️ Appending Lucky popup to document.body...');
280
+
281
+ // Add event listeners
282
+ const rerollBtn = document.getElementById('luckyRerollBtn');
283
+ const keepBtn = document.getElementById('luckyKeepBtn');
284
+
285
+ // Add hover effects via event listeners (CSP-compliant)
286
+ rerollBtn.addEventListener('mouseenter', () => rerollBtn.style.background = '#e67e22');
287
+ rerollBtn.addEventListener('mouseleave', () => rerollBtn.style.background = '#f39c12');
288
+ keepBtn.addEventListener('mouseenter', () => keepBtn.style.background = '#7f8c8d');
289
+ keepBtn.addEventListener('mouseleave', () => keepBtn.style.background = '#95a5a6');
290
+
291
+ rerollBtn.addEventListener('click', () => {
292
+ if (useLuckyPoint()) {
293
+ performLuckyReroll(rollData);
294
+ popupOverlay.remove();
295
+ } else {
296
+ alert('No luck points available!');
297
+ }
298
+ });
299
+
300
+ keepBtn.addEventListener('click', () => {
301
+ popupOverlay.remove();
302
+ });
303
+
304
+ // Close on overlay click
305
+ popupOverlay.addEventListener('click', (e) => {
306
+ if (e.target === popupOverlay) {
307
+ popupOverlay.remove();
308
+ }
309
+ });
310
+
311
+ debug.log('🎖️ Lucky popup displayed');
312
+ }
313
+
314
+ /**
315
+ * Perform Lucky reroll
316
+ * @param {Object} originalRollData - Original roll data
317
+ */
318
+ function performLuckyReroll(originalRollData) {
319
+ debug.log('🎖️ Performing Lucky reroll for:', originalRollData);
320
+
321
+ // Extract base formula (remove modifiers for the reroll)
322
+ const baseFormula = originalRollData.rollType.replace(/[+-]\d+$/i, '');
323
+
324
+ // Create a new roll with just the d20
325
+ const rerollData = {
326
+ name: `🎖️ ${originalRollData.rollName} (Lucky Reroll)`,
327
+ formula: baseFormula,
328
+ color: '#f39c12',
329
+ characterName: characterData.name
330
+ };
331
+
332
+ // TODO: Add Owlbear Rodeo integration for Lucky rerolls
333
+ showNotification('🎖️ Lucky reroll initiated!', 'success');
334
+ }
335
+
336
+ // ===== TRAIT CHOICE POPUP =====
337
+
338
+ /**
339
+ * Unified Trait Choice Popup (when multiple traits apply)
340
+ * @param {Object} rollData - Roll information with multiple traits
341
+ */
342
+ function showTraitChoicePopup(rollData) {
343
+ debug.log('🎯 Trait choice popup called with:', rollData);
344
+
345
+ // Check if document.body exists
346
+ if (!document.body) {
347
+ debug.error('❌ document.body not available for trait choice popup');
348
+ showNotification('🎯 Trait choice triggered! (Popup failed to display)', 'info');
349
+ return;
350
+ }
351
+
352
+ debug.log('🎯 Creating trait choice overlay...');
353
+
354
+ // Get theme-aware colors
355
+ const colors = getPopupThemeColors();
356
+
357
+ // Create popup overlay
358
+ const popupOverlay = document.createElement('div');
359
+ popupOverlay.style.cssText = `
360
+ position: fixed;
361
+ top: 0;
362
+ left: 0;
363
+ right: 0;
364
+ bottom: 0;
365
+ background: rgba(0, 0, 0, 0.8);
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ z-index: 10000;
370
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
371
+ `;
372
+
373
+ // Create popup content
374
+ const popupContent = document.createElement('div');
375
+ popupContent.style.cssText = `
376
+ background: ${colors.background};
377
+ border-radius: 12px;
378
+ padding: 24px;
379
+ max-width: 450px;
380
+ width: 90%;
381
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
382
+ text-align: center;
383
+ `;
384
+
385
+ // Build trait options HTML
386
+ let traitOptionsHTML = '';
387
+ const allTraits = [...rollData.racialTraits, ...rollData.featTraits];
388
+
389
+ allTraits.forEach((trait, index) => {
390
+ let icon = '';
391
+ let color = '';
392
+ let description = '';
393
+
394
+ if (trait.name === 'Halfling Luck') {
395
+ icon = '🍀';
396
+ color = '#2D8B83';
397
+ description = 'Reroll natural 1s (must use new roll)';
398
+ } else if (trait.name === 'Lucky') {
399
+ icon = '🎖️';
400
+ color = '#f39c12';
401
+ const luckyResource = getLuckyResource();
402
+ description = `Reroll any roll (${luckyResource?.current || 0}/3 points left)`;
403
+ }
404
+
405
+ traitOptionsHTML += `
406
+ <button class="trait-option-btn" data-trait-index="${index}" data-trait-color="${color}" style="
407
+ background: ${color};
408
+ color: white;
409
+ border: none;
410
+ padding: 16px;
411
+ border-radius: 8px;
412
+ cursor: pointer;
413
+ font-size: 16px;
414
+ font-weight: bold;
415
+ margin: 8px 0;
416
+ transition: transform 0.2s, background 0.2s;
417
+ width: 100%;
418
+ display: flex;
419
+ align-items: center;
420
+ justify-content: center;
421
+ gap: 12px;
422
+ ">
423
+ <span style="font-size: 20px;">${icon}</span>
424
+ <div style="text-align: left;">
425
+ <div style="font-weight: bold;">${trait.name}</div>
426
+ <div style="font-size: 12px; opacity: 0.9;">${description}</div>
427
+ </div>
428
+ </button>
429
+ `;
430
+ });
431
+
432
+ debug.log('🎯 Setting trait choice popup content HTML...');
433
+
434
+ popupContent.innerHTML = `
435
+ <div style="font-size: 24px; margin-bottom: 16px;">🎯</div>
436
+ <h2 style="margin: 0 0 8px 0; color: ${colors.heading};">Multiple Traits Available!</h2>
437
+ <p style="margin: 0 0 16px 0; color: ${colors.text};">
438
+ You rolled a ${rollData.baseRoll}! Choose which trait to use:
439
+ </p>
440
+ <div style="margin: 0 0 16px 0; padding: 12px; background: ${colors.infoBox}; border-radius: 8px; border-left: 4px solid #3498db; color: ${colors.text};">
441
+ <strong>Original Roll:</strong> ${rollData.rollName}<br>
442
+ <strong>Result:</strong> ${rollData.baseRoll}<br>
443
+ <strong>Total:</strong> ${rollData.rollResult}
444
+ </div>
445
+ <div style="display: flex; flex-direction: column; gap: 8px;">
446
+ ${traitOptionsHTML}
447
+ </div>
448
+ <button id="cancelTraitBtn" style="
449
+ background: #95a5a6;
450
+ color: white;
451
+ border: none;
452
+ padding: 12px 24px;
453
+ border-radius: 8px;
454
+ cursor: pointer;
455
+ font-size: 14px;
456
+ font-weight: bold;
457
+ margin-top: 8px;
458
+ transition: background 0.2s;
459
+ ">
460
+ Keep Original Roll
461
+ </button>
462
+ `;
463
+
464
+ popupOverlay.appendChild(popupContent);
465
+ document.body.appendChild(popupOverlay);
466
+
467
+ debug.log('🎯 Appending trait choice popup to document.body...');
468
+
469
+ // Add event listeners
470
+ const traitButtons = document.querySelectorAll('.trait-option-btn');
471
+ const cancelBtn = document.getElementById('cancelTraitBtn');
472
+
473
+ // Add hover effects for trait buttons (CSP-compliant)
474
+ traitButtons.forEach((btn, index) => {
475
+ const originalColor = btn.dataset.traitColor;
476
+
477
+ btn.addEventListener('mouseenter', () => {
478
+ btn.style.transform = 'translateY(-2px)';
479
+ btn.style.background = originalColor + 'dd';
480
+ });
481
+
482
+ btn.addEventListener('mouseleave', () => {
483
+ btn.style.transform = 'translateY(0)';
484
+ btn.style.background = originalColor;
485
+ });
486
+
487
+ btn.addEventListener('click', () => {
488
+ const trait = allTraits[index];
489
+ debug.log(`🎯 User chose trait: ${trait.name}`);
490
+
491
+ popupOverlay.remove();
492
+
493
+ // Execute the chosen trait's action
494
+ if (trait.name === 'Halfling Luck') {
495
+ showHalflingLuckPopup({
496
+ rollResult: rollData.baseRoll,
497
+ baseRoll: rollData.baseRoll,
498
+ rollType: rollData.rollType,
499
+ rollName: rollData.rollName
500
+ });
501
+ } else if (trait.name === 'Lucky') {
502
+ const luckyResource = getLuckyResource();
503
+ showLuckyPopup({
504
+ rollResult: rollData.baseRoll,
505
+ baseRoll: rollData.baseRoll,
506
+ rollType: rollData.rollType,
507
+ rollName: rollData.rollName,
508
+ luckPointsRemaining: luckyResource?.current || 0
509
+ });
510
+ }
511
+ });
512
+ });
513
+
514
+ // Add hover effects for cancel button (CSP-compliant)
515
+ cancelBtn.addEventListener('mouseenter', () => cancelBtn.style.background = '#7f8c8d');
516
+ cancelBtn.addEventListener('mouseleave', () => cancelBtn.style.background = '#95a5a6');
517
+
518
+ cancelBtn.addEventListener('click', () => {
519
+ popupOverlay.remove();
520
+ });
521
+
522
+ // Close on overlay click
523
+ popupOverlay.addEventListener('click', (e) => {
524
+ if (e.target === popupOverlay) {
525
+ popupOverlay.remove();
526
+ }
527
+ });
528
+
529
+ debug.log('🎯 Trait choice popup displayed');
530
+ }
531
+
532
+ // ===== WILD MAGIC SURGE POPUP =====
533
+
534
+ /**
535
+ * Show Wild Magic Surge popup
536
+ * @param {number} d100Roll - d100 roll result
537
+ * @param {string} effect - Wild magic effect description
538
+ */
539
+ function showWildMagicSurgePopup(d100Roll, effect) {
540
+ debug.log('🌀 Wild Magic Surge popup called with:', d100Roll, effect);
541
+
542
+ if (!document.body) {
543
+ debug.error('❌ document.body not available for Wild Magic Surge popup');
544
+ showNotification(`🌀 Wild Magic Surge! d100: ${d100Roll}`, 'warning');
545
+ return;
546
+ }
547
+
548
+ const colors = getPopupThemeColors();
549
+
550
+ // Create popup overlay
551
+ const popupOverlay = document.createElement('div');
552
+ popupOverlay.style.cssText = `
553
+ position: fixed;
554
+ top: 0;
555
+ left: 0;
556
+ width: 100%;
557
+ height: 100%;
558
+ background: rgba(0, 0, 0, 0.8);
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ z-index: 10000;
563
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
564
+ `;
565
+
566
+ // Create popup content
567
+ const popupContent = document.createElement('div');
568
+ popupContent.style.cssText = `
569
+ background: ${colors.background};
570
+ border-radius: 12px;
571
+ padding: 24px;
572
+ max-width: 500px;
573
+ width: 90%;
574
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
575
+ text-align: center;
576
+ `;
577
+
578
+ popupContent.innerHTML = `
579
+ <div style="font-size: 32px; margin-bottom: 16px;">🌀</div>
580
+ <h2 style="margin: 0 0 8px 0; color: #9b59b6;">Wild Magic Surge!</h2>
581
+ <p style="margin: 0 0 16px 0; color: ${colors.text};">
582
+ Your spell triggers a wild magic surge!
583
+ </p>
584
+ <div style="margin: 0 0 16px 0; padding: 16px; background: ${colors.infoBox}; border-radius: 8px; border-left: 4px solid #9b59b6; color: ${colors.text}; text-align: left;">
585
+ <div style="text-align: center; font-weight: bold; font-size: 18px; margin-bottom: 12px; color: #9b59b6;">
586
+ d100 Roll: ${d100Roll}
587
+ </div>
588
+ <div style="font-size: 14px; line-height: 1.6;">
589
+ ${effect}
590
+ </div>
591
+ </div>
592
+ <button id="closeWildMagicBtn" style="
593
+ background: #9b59b6;
594
+ color: white;
595
+ border: none;
596
+ padding: 12px 32px;
597
+ border-radius: 8px;
598
+ cursor: pointer;
599
+ font-weight: bold;
600
+ font-size: 14px;
601
+ transition: background 0.2s;
602
+ ">Got it!</button>
603
+ `;
604
+
605
+ popupOverlay.appendChild(popupContent);
606
+ document.body.appendChild(popupOverlay);
607
+
608
+ // Add event listeners
609
+ const closeBtn = document.getElementById('closeWildMagicBtn');
610
+
611
+ closeBtn.addEventListener('mouseenter', () => closeBtn.style.background = '#8e44ad');
612
+ closeBtn.addEventListener('mouseleave', () => closeBtn.style.background = '#9b59b6');
613
+
614
+ closeBtn.addEventListener('click', () => {
615
+ document.body.removeChild(popupOverlay);
616
+ });
617
+
618
+ // Close on overlay click
619
+ popupOverlay.addEventListener('click', (e) => {
620
+ if (e.target === popupOverlay) {
621
+ document.body.removeChild(popupOverlay);
622
+ }
623
+ });
624
+
625
+ // TODO: Add Owlbear Rodeo integration for Wild Magic Surge announcements
626
+
627
+ debug.log('🌀 Wild Magic Surge popup displayed');
628
+ }
629
+
630
+ // ===== BARDIC INSPIRATION POPUP =====
631
+
632
+ /**
633
+ * Show Bardic Inspiration popup
634
+ * @param {Object} rollData - Roll information
635
+ */
636
+ function showBardicInspirationPopup(rollData) {
637
+ debug.log('🎵 Bardic Inspiration popup called with:', rollData);
638
+
639
+ // Check if document.body exists
640
+ if (!document.body) {
641
+ debug.error('❌ document.body not available for Bardic Inspiration popup');
642
+ showNotification('🎵 Bardic Inspiration available! (Popup failed to display)', 'info');
643
+ return;
644
+ }
645
+
646
+ debug.log('🎵 Creating Bardic Inspiration popup overlay...');
647
+
648
+ // Get theme-aware colors
649
+ const colors = getPopupThemeColors();
650
+
651
+ // Create popup overlay
652
+ const popupOverlay = document.createElement('div');
653
+ popupOverlay.style.cssText = `
654
+ position: fixed;
655
+ top: 0;
656
+ left: 0;
657
+ width: 100%;
658
+ height: 100%;
659
+ background: rgba(0, 0, 0, 0.8);
660
+ display: flex;
661
+ align-items: center;
662
+ justify-content: center;
663
+ z-index: 10000;
664
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
665
+ `;
666
+
667
+ // Create popup content
668
+ const popupContent = document.createElement('div');
669
+ popupContent.style.cssText = `
670
+ background: ${colors.background};
671
+ border-radius: 12px;
672
+ padding: 24px;
673
+ max-width: 450px;
674
+ width: 90%;
675
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
676
+ text-align: center;
677
+ `;
678
+
679
+ debug.log('🎵 Setting Bardic Inspiration popup content HTML...');
680
+
681
+ popupContent.innerHTML = `
682
+ <div style="font-size: 32px; margin-bottom: 16px;">🎵</div>
683
+ <h2 style="margin: 0 0 8px 0; color: ${colors.heading};">Bardic Inspiration!</h2>
684
+ <p style="margin: 0 0 16px 0; color: ${colors.text};">
685
+ Add a <strong>${rollData.inspirationDie}</strong> to this roll?
686
+ </p>
687
+ <div style="margin: 0 0 16px 0; padding: 12px; background: ${colors.infoBox}; border-radius: 8px; border-left: 4px solid #9b59b6; color: ${colors.text};">
688
+ <strong>Current Roll:</strong> ${rollData.rollName}<br>
689
+ <strong>Base Result:</strong> ${rollData.baseRoll}<br>
690
+ <strong>Inspiration Die:</strong> ${rollData.inspirationDie}<br>
691
+ <strong>Uses Left:</strong> ${rollData.usesRemaining}
692
+ </div>
693
+ <div style="margin-bottom: 16px; padding: 12px; background: ${colors.infoBox}; border-radius: 8px; color: ${colors.text}; font-size: 13px; text-align: left;">
694
+ <strong>💡 How it works:</strong><br>
695
+ • Roll the inspiration die and add it to your total<br>
696
+ • Can be used on ability checks, attack rolls, or saves<br>
697
+ • Only one inspiration die can be used per roll
698
+ </div>
699
+ <div style="display: flex; gap: 12px; justify-content: center;">
700
+ <button id="bardicUseBtn" style="
701
+ background: #9b59b6;
702
+ color: white;
703
+ border: none;
704
+ padding: 12px 24px;
705
+ border-radius: 8px;
706
+ cursor: pointer;
707
+ font-weight: bold;
708
+ font-size: 14px;
709
+ transition: background 0.2s;
710
+ ">🎲 Use Inspiration</button>
711
+ <button id="bardicDeclineBtn" style="
712
+ background: #7f8c8d;
713
+ color: white;
714
+ border: none;
715
+ padding: 12px 24px;
716
+ border-radius: 8px;
717
+ cursor: pointer;
718
+ font-weight: bold;
719
+ font-size: 14px;
720
+ transition: background 0.2s;
721
+ ">Decline</button>
722
+ </div>
723
+ `;
724
+
725
+ debug.log('🎵 Appending Bardic Inspiration popup to document.body...');
726
+
727
+ popupOverlay.appendChild(popupContent);
728
+ document.body.appendChild(popupOverlay);
729
+
730
+ // Add hover effects
731
+ const useBtn = document.getElementById('bardicUseBtn');
732
+ const declineBtn = document.getElementById('bardicDeclineBtn');
733
+
734
+ useBtn.addEventListener('mouseenter', () => {
735
+ useBtn.style.background = '#8e44ad';
736
+ });
737
+ useBtn.addEventListener('mouseleave', () => {
738
+ useBtn.style.background = '#9b59b6';
739
+ });
740
+
741
+ declineBtn.addEventListener('mouseenter', () => {
742
+ declineBtn.style.background = '#95a5a6';
743
+ });
744
+ declineBtn.addEventListener('mouseleave', () => {
745
+ declineBtn.style.background = '#7f8c8d';
746
+ });
747
+
748
+ // Add event listeners
749
+ useBtn.addEventListener('click', () => {
750
+ debug.log('🎵 User chose to use Bardic Inspiration');
751
+ performBardicInspirationRoll(rollData);
752
+ document.body.removeChild(popupOverlay);
753
+ });
754
+
755
+ declineBtn.addEventListener('click', () => {
756
+ debug.log('🎵 User declined Bardic Inspiration');
757
+ showNotification('Bardic Inspiration declined', 'info');
758
+ document.body.removeChild(popupOverlay);
759
+ });
760
+
761
+ // Close on overlay click
762
+ popupOverlay.addEventListener('click', (e) => {
763
+ if (e.target === popupOverlay) {
764
+ debug.log('🎵 User closed Bardic Inspiration popup');
765
+ document.body.removeChild(popupOverlay);
766
+ }
767
+ });
768
+
769
+ debug.log('🎵 Bardic Inspiration popup displayed');
770
+ }
771
+
772
+ /**
773
+ * Perform Bardic Inspiration roll
774
+ * @param {Object} rollData - Roll information
775
+ */
776
+ function performBardicInspirationRoll(rollData) {
777
+ debug.log('🎵 Performing Bardic Inspiration roll with data:', rollData);
778
+
779
+ // Use one Bardic Inspiration use
780
+ const success = useBardicInspiration();
781
+ if (!success) {
782
+ debug.error('❌ Failed to use Bardic Inspiration (no uses left?)');
783
+ showNotification('❌ Failed to use Bardic Inspiration', 'error');
784
+ return;
785
+ }
786
+
787
+ // Roll the inspiration die
788
+ const dieSize = parseInt(rollData.inspirationDie.substring(1)); // "d6" -> 6
789
+ const inspirationRoll = Math.floor(Math.random() * dieSize) + 1;
790
+
791
+ debug.log(`🎵 Rolled ${rollData.inspirationDie}: ${inspirationRoll}`);
792
+
793
+ // Create the roll message
794
+ const inspirationMessage = `/roll ${rollData.inspirationDie}`;
795
+ const chatMessage = `🎵 Bardic Inspiration for ${rollData.rollName}: [[${inspirationRoll}]] (${rollData.inspirationDie})`;
796
+
797
+ // Show notification
798
+ showNotification(`🎵 Bardic Inspiration: +${inspirationRoll}!`, 'success');
799
+
800
+ // TODO: Add Owlbear Rodeo integration for Bardic Inspiration rolls
801
+
802
+ debug.log('🎵 Bardic Inspiration roll complete');
803
+ }
804
+
805
+ // ===== ELVEN ACCURACY POPUP =====
806
+
807
+ /**
808
+ * Show Elven Accuracy popup
809
+ * @param {Object} rollData - Roll information
810
+ */
811
+ function showElvenAccuracyPopup(rollData) {
812
+ debug.log('🧝 Elven Accuracy popup called with:', rollData);
813
+
814
+ if (!document.body) {
815
+ debug.error('❌ document.body not available for Elven Accuracy popup');
816
+ showNotification('🧝 Elven Accuracy triggered!', 'info');
817
+ return;
818
+ }
819
+
820
+ const colors = getPopupThemeColors();
821
+
822
+ // Create popup overlay
823
+ const popupOverlay = document.createElement('div');
824
+ popupOverlay.style.cssText = `
825
+ position: fixed;
826
+ top: 0;
827
+ left: 0;
828
+ width: 100%;
829
+ height: 100%;
830
+ background: rgba(0, 0, 0, 0.8);
831
+ display: flex;
832
+ align-items: center;
833
+ justify-content: center;
834
+ z-index: 10000;
835
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
836
+ `;
837
+
838
+ // Create popup content
839
+ const popupContent = document.createElement('div');
840
+ popupContent.style.cssText = `
841
+ background: ${colors.background};
842
+ border-radius: 12px;
843
+ padding: 24px;
844
+ max-width: 400px;
845
+ width: 90%;
846
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
847
+ text-align: center;
848
+ `;
849
+
850
+ popupContent.innerHTML = `
851
+ <div style="font-size: 24px; margin-bottom: 16px;">🧝</div>
852
+ <h2 style="margin: 0 0 8px 0; color: #27ae60;">Elven Accuracy!</h2>
853
+ <p style="margin: 0 0 16px 0; color: ${colors.text};">
854
+ You have advantage! Would you like to reroll the lower die?
855
+ </p>
856
+ <div style="margin: 0 0 16px 0; padding: 12px; background: ${colors.infoBox}; border-radius: 8px; border-left: 4px solid #27ae60; color: ${colors.text};">
857
+ <strong>Roll:</strong> ${rollData.rollName}<br>
858
+ <strong>Type:</strong> Advantage attack roll
859
+ </div>
860
+ <div style="display: flex; gap: 12px; justify-content: center;">
861
+ <button id="elvenRerollBtn" style="
862
+ background: #27ae60;
863
+ color: white;
864
+ border: none;
865
+ padding: 12px 24px;
866
+ border-radius: 8px;
867
+ cursor: pointer;
868
+ font-weight: bold;
869
+ font-size: 14px;
870
+ ">🎲 Reroll Lower Die</button>
871
+ <button id="elvenKeepBtn" style="
872
+ background: #95a5a6;
873
+ color: white;
874
+ border: none;
875
+ padding: 12px 24px;
876
+ border-radius: 8px;
877
+ cursor: pointer;
878
+ font-weight: bold;
879
+ font-size: 14px;
880
+ ">Keep Rolls</button>
881
+ </div>
882
+ `;
883
+
884
+ popupOverlay.appendChild(popupContent);
885
+ document.body.appendChild(popupOverlay);
886
+
887
+ // Add event listeners
888
+ const rerollBtn = document.getElementById('elvenRerollBtn');
889
+ const keepBtn = document.getElementById('elvenKeepBtn');
890
+
891
+ rerollBtn.addEventListener('mouseenter', () => rerollBtn.style.background = '#229954');
892
+ rerollBtn.addEventListener('mouseleave', () => rerollBtn.style.background = '#27ae60');
893
+ keepBtn.addEventListener('mouseenter', () => keepBtn.style.background = '#7f8c8d');
894
+ keepBtn.addEventListener('mouseleave', () => keepBtn.style.background = '#95a5a6');
895
+
896
+ rerollBtn.addEventListener('click', () => {
897
+ debug.log('🧝 User chose to reroll with Elven Accuracy');
898
+ performElvenAccuracyReroll(rollData);
899
+ document.body.removeChild(popupOverlay);
900
+ });
901
+
902
+ keepBtn.addEventListener('click', () => {
903
+ debug.log('🧝 User chose to keep original advantage rolls');
904
+ document.body.removeChild(popupOverlay);
905
+ });
906
+
907
+ // Close on overlay click
908
+ popupOverlay.addEventListener('click', (e) => {
909
+ if (e.target === popupOverlay) {
910
+ document.body.removeChild(popupOverlay);
911
+ }
912
+ });
913
+
914
+ debug.log('🧝 Elven Accuracy popup displayed');
915
+ }
916
+
917
+ /**
918
+ * Perform Elven Accuracy reroll
919
+ * @param {Object} originalRollData - Original roll data
920
+ */
921
+ function performElvenAccuracyReroll(originalRollData) {
922
+ debug.log('🧝 Performing Elven Accuracy reroll for:', originalRollData);
923
+
924
+ // Roll a third d20
925
+ const thirdRoll = Math.floor(Math.random() * 20) + 1;
926
+
927
+ // Create reroll announcement
928
+ const rerollData = {
929
+ name: `🧝 ${originalRollData.rollName} (Elven Accuracy - 3rd die)`,
930
+ formula: '1d20',
931
+ color: '#27ae60',
932
+ characterName: characterData.name
933
+ };
934
+
935
+ debug.log('🧝 Third die roll:', thirdRoll);
936
+
937
+ // TODO: Add Owlbear Rodeo integration for Elven Accuracy rerolls
938
+
939
+ showNotification(`🧝 Elven Accuracy! Third die: ${thirdRoll}`, 'success');
940
+ }
941
+
942
+ // ===== EXPORTS =====
943
+
944
+ // Export functions to globalThis
945
+ globalThis.getPopupThemeColors = getPopupThemeColors;
946
+ globalThis.showHalflingLuckPopup = showHalflingLuckPopup;
947
+ globalThis.showLuckyPopup = showLuckyPopup;
948
+ globalThis.showTraitChoicePopup = showTraitChoicePopup;
949
+ globalThis.showWildMagicSurgePopup = showWildMagicSurgePopup;
950
+ globalThis.showBardicInspirationPopup = showBardicInspirationPopup;
951
+ globalThis.showElvenAccuracyPopup = showElvenAccuracyPopup;
952
+ globalThis.performHalflingReroll = performHalflingReroll;
953
+ globalThis.performLuckyReroll = performLuckyReroll;
954
+ globalThis.performBardicInspirationRoll = performBardicInspirationRoll;
955
+ globalThis.performElvenAccuracyReroll = performElvenAccuracyReroll;
956
+
957
+ debug.log('✅ Character Trait Popups module loaded');
958
+
959
+ })();