@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,825 @@
1
+ /**
2
+ * Macro System Module
3
+ *
4
+ * Handles custom roll macros - creation, storage, execution, and settings UI.
5
+ * Includes the full settings modal with tabs for macros and preferences.
6
+ *
7
+ * Loaded as a plain script (no ES6 modules) to export to globalThis.
8
+ *
9
+ * Functions exported to globalThis:
10
+ * - loadCustomMacros()
11
+ * - saveAllCustomMacros()
12
+ * - addCustomMacro(name, formula, description)
13
+ * - deleteCustomMacro(macroId)
14
+ * - executeMacro(macro)
15
+ * - updateMacrosDisplay()
16
+ * - showSettingsModal()
17
+ * - showAddMacroModal()
18
+ * - updateMacrosDisplayInSettings()
19
+ * - initCustomMacros()
20
+ * - initSettingsButton()
21
+ *
22
+ * State variables exported to globalThis:
23
+ * - customMacros
24
+ */
25
+
26
+ (function() {
27
+ 'use strict';
28
+
29
+ // ===== STATE =====
30
+
31
+ // Setting to show/hide custom macro buttons on spells
32
+ let showCustomMacroButtons = false;
33
+
34
+ let customMacros = [];
35
+
36
+ /**
37
+ * Load custom macros from storage
38
+ */
39
+ async function loadCustomMacros() {
40
+ try {
41
+ const result = await browserAPI.storage.local.get(['customMacros']);
42
+ customMacros = result.customMacros || [];
43
+ debug.log(`🎲 Loaded ${customMacros.length} custom macros`);
44
+ updateMacrosDisplay();
45
+ } catch (error) {
46
+ debug.error('❌ Failed to load custom macros:', error);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Save all custom macros to storage
52
+ */
53
+ function saveAllCustomMacros() {
54
+ browserAPI.storage.local.set({ customMacros });
55
+ debug.log(`💾 Saved ${customMacros.length} custom macros`);
56
+ }
57
+
58
+ /**
59
+ * Add a new custom macro
60
+ */
61
+ function addCustomMacro(name, formula, description = '') {
62
+ const macro = {
63
+ id: Date.now().toString(),
64
+ name,
65
+ formula,
66
+ description,
67
+ createdAt: Date.now()
68
+ };
69
+
70
+ customMacros.push(macro);
71
+ saveAllCustomMacros();
72
+ updateMacrosDisplay();
73
+
74
+ debug.log(`✅ Added custom macro: ${name} (${formula})`);
75
+ return macro;
76
+ }
77
+
78
+ /**
79
+ * Delete a custom macro
80
+ */
81
+ function deleteCustomMacro(macroId) {
82
+ customMacros = customMacros.filter(m => m.id !== macroId);
83
+ saveAllCustomMacros();
84
+ updateMacrosDisplay();
85
+ debug.log(`🗑️ Deleted macro: ${macroId}`);
86
+ }
87
+
88
+ /**
89
+ * Execute a custom macro (roll the formula)
90
+ */
91
+ function executeMacro(macro) {
92
+ debug.log(`🎲 Executing macro: ${macro.name} (${macro.formula})`);
93
+
94
+ // Use the existing roll announcement system
95
+ announceAction(
96
+ macro.name,
97
+ macro.formula,
98
+ '',
99
+ macro.description || `Custom macro: ${macro.formula}`
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Update the macros display in the UI
105
+ */
106
+ function updateMacrosDisplay() {
107
+ const container = document.getElementById('custom-macros-container');
108
+ if (!container) return;
109
+
110
+ if (customMacros.length === 0) {
111
+ container.innerHTML = '<p style="text-align: center; color: #888; padding: 15px;">No custom macros yet. Click "Add Macro" to create one.</p>';
112
+ return;
113
+ }
114
+
115
+ container.innerHTML = customMacros.map(macro => {
116
+ // Validate macro properties to prevent undefined display
117
+ const macroName = macro.name || 'Unnamed Macro';
118
+ const macroFormula = macro.formula || '';
119
+ const macroDescription = macro.description || '';
120
+
121
+ return `
122
+ <div class="macro-item" style="
123
+ background: var(--bg-secondary);
124
+ border: 2px solid var(--border-color);
125
+ border-radius: 8px;
126
+ padding: 12px;
127
+ margin-bottom: 10px;
128
+ display: flex;
129
+ justify-content: space-between;
130
+ align-items: center;
131
+ transition: all 0.2s;
132
+ ">
133
+ <div style="flex: 1;">
134
+ <div style="font-weight: bold; color: var(--text-primary); margin-bottom: 4px;">
135
+ ${macroName}
136
+ </div>
137
+ <div style="font-family: monospace; color: var(--accent-info); font-size: 0.9em; margin-bottom: 4px;">
138
+ ${macroFormula}
139
+ </div>
140
+ ${macroDescription ? `<div style="font-size: 0.85em; color: var(--text-secondary);">${macroDescription}</div>` : ''}
141
+ </div>
142
+ <div style="display: flex; gap: 8px;">
143
+ <button class="macro-roll-btn" data-macro-id="${macro.id}" style="
144
+ background: var(--accent-primary);
145
+ color: white;
146
+ border: none;
147
+ padding: 8px 16px;
148
+ border-radius: 6px;
149
+ cursor: pointer;
150
+ font-weight: bold;
151
+ transition: all 0.2s;
152
+ ">
153
+ 🎲 Roll
154
+ </button>
155
+ <button class="macro-delete-btn" data-macro-id="${macro.id}" style="
156
+ background: var(--accent-danger);
157
+ color: white;
158
+ border: none;
159
+ padding: 8px 12px;
160
+ border-radius: 6px;
161
+ cursor: pointer;
162
+ transition: all 0.2s;
163
+ ">
164
+ 🗑️
165
+ </button>
166
+ </div>
167
+ </div>
168
+ `;
169
+ }).join('');
170
+
171
+ // Add event listeners
172
+ container.querySelectorAll('.macro-roll-btn').forEach(btn => {
173
+ btn.addEventListener('click', () => {
174
+ const macroId = btn.dataset.macroId;
175
+ const macro = customMacros.find(m => m.id === macroId);
176
+ if (macro) executeMacro(macro);
177
+ });
178
+ });
179
+
180
+ container.querySelectorAll('.macro-delete-btn').forEach(btn => {
181
+ btn.addEventListener('click', () => {
182
+ const macroId = btn.dataset.macroId;
183
+ const macro = customMacros.find(m => m.id === macroId);
184
+ if (macro && confirm(`Delete macro "${macro.name}"?`)) {
185
+ deleteCustomMacro(macroId);
186
+ }
187
+ });
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Show settings modal with tabs
193
+ */
194
+ function showSettingsModal() {
195
+ // Remove existing modal if any
196
+ const existingModal = document.getElementById('settings-modal');
197
+ if (existingModal) {
198
+ document.body.removeChild(existingModal);
199
+ }
200
+
201
+ const modal = document.createElement('div');
202
+ modal.id = 'settings-modal';
203
+ modal.style.cssText = `
204
+ position: fixed;
205
+ top: 0;
206
+ left: 0;
207
+ width: 100%;
208
+ height: 100%;
209
+ background: rgba(0, 0, 0, 0.7);
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ z-index: 10000;
214
+ `;
215
+
216
+ modal.innerHTML = `
217
+ <div style="
218
+ background: var(--bg-secondary);
219
+ border: 2px solid var(--border-color);
220
+ border-radius: 12px;
221
+ max-width: 700px;
222
+ width: 90%;
223
+ max-height: 80vh;
224
+ display: flex;
225
+ flex-direction: column;
226
+ overflow: hidden;
227
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
228
+ ">
229
+ <!-- Header -->
230
+ <div style="
231
+ padding: 20px;
232
+ border-bottom: 2px solid var(--border-color);
233
+ display: flex;
234
+ justify-content: space-between;
235
+ align-items: center;
236
+ ">
237
+ <h3 style="margin: 0; color: var(--text-primary);">⚙️ Settings</h3>
238
+ <button id="settings-close-btn" style="
239
+ background: var(--accent-danger);
240
+ color: white;
241
+ border: none;
242
+ padding: 6px 12px;
243
+ border-radius: 6px;
244
+ cursor: pointer;
245
+ font-weight: bold;
246
+ ">✕</button>
247
+ </div>
248
+
249
+ <!-- Tabs -->
250
+ <div style="
251
+ display: flex;
252
+ border-bottom: 2px solid var(--border-color);
253
+ background: var(--bg-tertiary);
254
+ ">
255
+ <button class="settings-tab active" data-tab="theme" style="
256
+ flex: 1;
257
+ padding: 12px;
258
+ background: transparent;
259
+ border: none;
260
+ cursor: pointer;
261
+ font-weight: bold;
262
+ color: var(--text-secondary);
263
+ transition: all 0.2s;
264
+ ">🎨 Theme</button>
265
+ <button class="settings-tab" data-tab="macros" style="
266
+ flex: 1;
267
+ padding: 12px;
268
+ background: transparent;
269
+ border: none;
270
+ cursor: pointer;
271
+ font-weight: bold;
272
+ color: var(--text-secondary);
273
+ transition: all 0.2s;
274
+ ">🎲 Custom Macros</button>
275
+ <button class="settings-tab" data-tab="gm" style="
276
+ flex: 1;
277
+ padding: 12px;
278
+ background: transparent;
279
+ border: none;
280
+ cursor: pointer;
281
+ font-weight: bold;
282
+ color: var(--text-secondary);
283
+ transition: all 0.2s;
284
+ ">👑 GM Integration</button>
285
+ </div>
286
+
287
+ <!-- Content -->
288
+ <div style="
289
+ flex: 1;
290
+ overflow-y: auto;
291
+ padding: 20px;
292
+ ">
293
+ <!-- Theme Tab -->
294
+ <div id="theme-tab-content" class="settings-tab-content">
295
+ <h4 style="margin: 0 0 15px 0; color: var(--text-primary);">Choose Theme</h4>
296
+ <div style="display: flex; gap: 15px; margin-bottom: 20px;">
297
+ <button class="theme-option" data-theme="light" style="
298
+ flex: 1;
299
+ padding: 20px;
300
+ background: var(--bg-primary);
301
+ border: 3px solid var(--border-color);
302
+ border-radius: 8px;
303
+ cursor: pointer;
304
+ transition: all 0.2s;
305
+ display: flex;
306
+ flex-direction: column;
307
+ align-items: center;
308
+ gap: 8px;
309
+ ">
310
+ <span style="font-size: 2em;">☀️</span>
311
+ <span style="font-weight: bold; color: var(--text-primary);">Light</span>
312
+ </button>
313
+ <button class="theme-option" data-theme="dark" style="
314
+ flex: 1;
315
+ padding: 20px;
316
+ background: var(--bg-primary);
317
+ border: 3px solid var(--border-color);
318
+ border-radius: 8px;
319
+ cursor: pointer;
320
+ transition: all 0.2s;
321
+ display: flex;
322
+ flex-direction: column;
323
+ align-items: center;
324
+ gap: 8px;
325
+ ">
326
+ <span style="font-size: 2em;">🌙</span>
327
+ <span style="font-weight: bold; color: var(--text-primary);">Dark</span>
328
+ </button>
329
+ <button class="theme-option active" data-theme="system" style="
330
+ flex: 1;
331
+ padding: 20px;
332
+ background: var(--bg-primary);
333
+ border: 3px solid var(--accent-primary);
334
+ border-radius: 8px;
335
+ cursor: pointer;
336
+ transition: all 0.2s;
337
+ display: flex;
338
+ flex-direction: column;
339
+ align-items: center;
340
+ gap: 8px;
341
+ ">
342
+ <span style="font-size: 2em;">💻</span>
343
+ <span style="font-weight: bold; color: var(--text-primary);">System</span>
344
+ </button>
345
+ </div>
346
+ <p style="color: var(--text-secondary); font-size: 0.9em; margin: 0;">
347
+ System theme automatically matches your operating system's appearance settings.
348
+ </p>
349
+ </div>
350
+
351
+ <!-- Macros Tab -->
352
+ <div id="macros-tab-content" class="settings-tab-content" style="display: none;">
353
+ <!-- Setting: Show gear buttons on spells -->
354
+ <div style="background: var(--bg-tertiary); padding: 12px; border-radius: 8px; margin-bottom: 15px;">
355
+ <label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
356
+ <input type="checkbox" id="show-macro-buttons-setting" style="width: 18px; height: 18px;" ${showCustomMacroButtons ? 'checked' : ''}>
357
+ <span style="color: var(--text-primary); font-weight: bold;">Show ⚙️ macro buttons on spells</span>
358
+ </label>
359
+ <p style="margin: 8px 0 0 28px; color: var(--text-secondary); font-size: 0.85em;">
360
+ When enabled, shows a gear button on each spell to configure custom Roll20 macros.
361
+ </p>
362
+ </div>
363
+
364
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
365
+ <h4 style="margin: 0; color: var(--text-primary);">Your Custom Macros</h4>
366
+ <button id="add-macro-btn-settings" style="
367
+ padding: 8px 16px;
368
+ background: var(--accent-primary);
369
+ color: white;
370
+ border: none;
371
+ border-radius: 6px;
372
+ cursor: pointer;
373
+ font-weight: bold;
374
+ ">➕ Add Macro</button>
375
+ </div>
376
+ <div id="custom-macros-container-settings"></div>
377
+ </div>
378
+
379
+ <!-- GM Integration Tab -->
380
+ <div id="gm-tab-content" class="settings-tab-content" style="display: none;">
381
+ <h4 style="margin: 0 0 15px 0; color: var(--text-primary);">👑 GM Integration</h4>
382
+
383
+ <!-- DiceCloud Sync Section -->
384
+ <div id="dicecloud-sync-section" style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px; margin-bottom: 20px; display: none;">
385
+ <h5 style="margin: 0 0 10px 0; color: var(--text-primary);">🔄 DiceCloud Sync</h5>
386
+ <p style="margin: 0 0 15px 0; color: var(--text-secondary); font-size: 0.9em;">
387
+ Manually sync all character data to DiceCloud. This updates HP, spell slots, Channel Divinity, class resources, and all other tracked values.
388
+ </p>
389
+ <button id="manual-sync-btn" style="
390
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
391
+ color: white;
392
+ border: none;
393
+ border-radius: 8px;
394
+ padding: 12px 24px;
395
+ font-size: 1em;
396
+ font-weight: bold;
397
+ cursor: pointer;
398
+ transition: all 0.3s ease;
399
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
400
+ width: 100%;
401
+ ">
402
+ 🔄 Sync to DiceCloud Now
403
+ </button>
404
+ <div id="sync-status" style="margin-top: 10px; padding: 8px; border-radius: 6px; font-size: 0.9em; display: none;"></div>
405
+ </div>
406
+
407
+ <div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px; margin-bottom: 20px;">
408
+ <h5 style="margin: 0 0 10px 0; color: var(--text-primary);">Share Character with GM</h5>
409
+ <p style="margin: 0 0 15px 0; color: var(--text-secondary); font-size: 0.9em;">
410
+ Share your complete character sheet with the Game Master. This sends all your character data including abilities, skills, actions, spells, and equipment.
411
+ </p>
412
+ <button id="show-to-gm-btn" class="show-to-gm-btn" style="
413
+ background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
414
+ color: white;
415
+ border: none;
416
+ border-radius: 8px;
417
+ padding: 12px 24px;
418
+ font-size: 1em;
419
+ font-weight: bold;
420
+ cursor: pointer;
421
+ transition: all 0.3s ease;
422
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
423
+ width: 100%;
424
+ ">
425
+ 👑 Share Character with GM
426
+ </button>
427
+ </div>
428
+
429
+ <div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px; margin-bottom: 20px;">
430
+ <h5 style="margin: 0 0 10px 0; color: var(--text-primary);">How It Works</h5>
431
+ <ul style="margin: 0; padding-left: 20px; color: var(--text-secondary); font-size: 0.9em;">
432
+ <li>Click the button above to share your character</li>
433
+ <li>Your character data appears in the Roll20 chat</li>
434
+ <li>The GM receives your complete character sheet</li>
435
+ <li>GM can view your stats, actions, and spells</li>
436
+ <li>Helps GM track party composition and abilities</li>
437
+ </ul>
438
+ </div>
439
+
440
+ <div style="background: var(--bg-tertiary); padding: 16px; border-radius: 8px;">
441
+ <h5 style="margin: 0 0 10px 0; color: var(--text-primary);">Privacy Note</h5>
442
+ <p style="margin: 0; color: var(--text-secondary); font-size: 0.9em;">
443
+ Only share character data when requested by your GM. The shared data includes your complete character sheet for game management purposes.
444
+ </p>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ `;
450
+
451
+ document.body.appendChild(modal);
452
+
453
+ // Set up tab switching
454
+ modal.querySelectorAll('.settings-tab').forEach(tab => {
455
+ tab.addEventListener('click', () => {
456
+ const tabName = tab.dataset.tab;
457
+
458
+ // Update tab buttons
459
+ modal.querySelectorAll('.settings-tab').forEach(t => {
460
+ t.classList.remove('active');
461
+ t.style.color = 'var(--text-secondary)';
462
+ t.style.background = 'transparent';
463
+ });
464
+ tab.classList.add('active');
465
+ tab.style.color = 'var(--text-primary)';
466
+ tab.style.background = 'var(--bg-secondary)';
467
+
468
+ // Update content
469
+ modal.querySelectorAll('.settings-tab-content').forEach(content => {
470
+ content.style.display = 'none';
471
+ });
472
+ modal.querySelector(`#${tabName}-tab-content`).style.display = 'block';
473
+ });
474
+ });
475
+
476
+ // Set up theme options
477
+ const currentTheme = typeof ThemeManager !== 'undefined' ? ThemeManager.getCurrentTheme() : 'light';
478
+ modal.querySelectorAll('.theme-option').forEach(option => {
479
+ if (option.dataset.theme === currentTheme) {
480
+ option.style.borderColor = 'var(--accent-primary)';
481
+ option.classList.add('active');
482
+ }
483
+
484
+ option.addEventListener('click', () => {
485
+ const theme = option.dataset.theme;
486
+ if (typeof ThemeManager !== 'undefined') {
487
+ ThemeManager.setTheme(theme);
488
+ }
489
+
490
+ // Update active state
491
+ modal.querySelectorAll('.theme-option').forEach(opt => {
492
+ opt.style.borderColor = 'var(--border-color)';
493
+ opt.classList.remove('active');
494
+ });
495
+ option.style.borderColor = 'var(--accent-primary)';
496
+ option.classList.add('active');
497
+ });
498
+ });
499
+
500
+ // Set up GM share button
501
+ const showToGMBtn = modal.querySelector('#show-to-gm-btn');
502
+ if (showToGMBtn) {
503
+ showToGMBtn.addEventListener('click', () => {
504
+ // Call the global shareCharacterWithGM function if it exists
505
+ if (typeof shareCharacterWithGM === 'function') {
506
+ shareCharacterWithGM();
507
+ // Close the settings modal after sharing
508
+ document.body.removeChild(modal);
509
+ } else {
510
+ debug.error('❌ shareCharacterWithGM function not available');
511
+ }
512
+ });
513
+ }
514
+
515
+ // Load macros into settings
516
+ updateMacrosDisplayInSettings();
517
+
518
+ // Initialize Show to GM button in settings
519
+ initShowToGM();
520
+
521
+ // Initialize manual DiceCloud sync button (experimental builds only)
522
+ initManualSyncButton();
523
+
524
+ // Set up add macro button
525
+ modal.querySelector('#add-macro-btn-settings').addEventListener('click', () => {
526
+ showAddMacroModal();
527
+ });
528
+
529
+ // Set up show macro buttons checkbox
530
+ const macroButtonsCheckbox = modal.querySelector('#show-macro-buttons-setting');
531
+ if (macroButtonsCheckbox) {
532
+ macroButtonsCheckbox.addEventListener('change', (e) => {
533
+ showCustomMacroButtons = e.target.checked;
534
+ localStorage.setItem('showCustomMacroButtons', showCustomMacroButtons ? 'true' : 'false');
535
+ debug.log(`⚙️ Custom macro buttons ${showCustomMacroButtons ? 'enabled' : 'disabled'}`);
536
+ // Rebuild spells to show/hide gear buttons
537
+ rebuildSpells();
538
+ });
539
+ }
540
+
541
+ // Close button
542
+ modal.querySelector('#settings-close-btn').addEventListener('click', () => {
543
+ document.body.removeChild(modal);
544
+ });
545
+
546
+ // Close on background click
547
+ modal.addEventListener('click', (e) => {
548
+ if (e.target === modal) {
549
+ document.body.removeChild(modal);
550
+ }
551
+ });
552
+ }
553
+
554
+ /**
555
+ * Show add macro modal (simplified version for settings)
556
+ */
557
+ function showAddMacroModal() {
558
+ const modal = document.createElement('div');
559
+ modal.id = 'add-macro-modal';
560
+ modal.style.cssText = `
561
+ position: fixed;
562
+ top: 0;
563
+ left: 0;
564
+ width: 100%;
565
+ height: 100%;
566
+ background: rgba(0, 0, 0, 0.8);
567
+ display: flex;
568
+ align-items: center;
569
+ justify-content: center;
570
+ z-index: 10001;
571
+ `;
572
+
573
+ modal.innerHTML = `
574
+ <div style="
575
+ background: var(--bg-secondary);
576
+ border: 2px solid var(--border-color);
577
+ border-radius: 12px;
578
+ padding: 24px;
579
+ max-width: 500px;
580
+ width: 90%;
581
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
582
+ ">
583
+ <h3 style="margin: 0 0 20px 0; color: var(--text-primary);">🎲 Add Custom Macro</h3>
584
+
585
+ <div style="margin-bottom: 16px;">
586
+ <label style="display: block; margin-bottom: 6px; font-weight: bold; color: var(--text-primary);">
587
+ Macro Name
588
+ </label>
589
+ <input type="text" id="macro-name-input" placeholder="e.g., Sneak Attack" style="
590
+ width: 100%;
591
+ padding: 10px;
592
+ border: 2px solid var(--border-color);
593
+ border-radius: 6px;
594
+ font-size: 14px;
595
+ background: var(--bg-primary);
596
+ color: var(--text-primary);
597
+ ">
598
+ </div>
599
+
600
+ <div style="margin-bottom: 16px;">
601
+ <label style="display: block; margin-bottom: 6px; font-weight: bold; color: var(--text-primary);">
602
+ Roll Formula
603
+ </label>
604
+ <input type="text" id="macro-formula-input" placeholder="e.g., 3d6" style="
605
+ width: 100%;
606
+ padding: 10px;
607
+ border: 2px solid var(--border-color);
608
+ border-radius: 6px;
609
+ font-size: 14px;
610
+ font-family: monospace;
611
+ background: var(--bg-primary);
612
+ color: var(--text-primary);
613
+ ">
614
+ <small style="color: var(--text-secondary); font-size: 0.85em;">
615
+ Examples: 1d20+5, 2d6+3, 8d6, 1d20+dexterity.modifier
616
+ </small>
617
+ </div>
618
+
619
+ <div style="margin-bottom: 20px;">
620
+ <label style="display: block; margin-bottom: 6px; font-weight: bold; color: var(--text-primary);">
621
+ Description (optional)
622
+ </label>
623
+ <input type="text" id="macro-description-input" placeholder="e.g., Extra damage on hit" style="
624
+ width: 100%;
625
+ padding: 10px;
626
+ border: 2px solid var(--border-color);
627
+ border-radius: 6px;
628
+ font-size: 14px;
629
+ background: var(--bg-primary);
630
+ color: var(--text-primary);
631
+ ">
632
+ </div>
633
+
634
+ <div style="display: flex; gap: 10px; justify-content: flex-end;">
635
+ <button id="macro-cancel-btn" style="
636
+ padding: 10px 20px;
637
+ background: var(--bg-tertiary);
638
+ color: var(--text-primary);
639
+ border: 2px solid var(--border-color);
640
+ border-radius: 6px;
641
+ cursor: pointer;
642
+ font-weight: bold;
643
+ ">
644
+ Cancel
645
+ </button>
646
+ <button id="macro-save-btn" style="
647
+ padding: 10px 20px;
648
+ background: var(--accent-primary);
649
+ color: white;
650
+ border: none;
651
+ border-radius: 6px;
652
+ cursor: pointer;
653
+ font-weight: bold;
654
+ ">
655
+ Save Macro
656
+ </button>
657
+ </div>
658
+ </div>
659
+ `;
660
+
661
+ document.body.appendChild(modal);
662
+
663
+ document.getElementById('macro-name-input').focus();
664
+
665
+ document.getElementById('macro-cancel-btn').addEventListener('click', () => {
666
+ document.body.removeChild(modal);
667
+ });
668
+
669
+ document.getElementById('macro-save-btn').addEventListener('click', () => {
670
+ const name = document.getElementById('macro-name-input').value.trim();
671
+ const formula = document.getElementById('macro-formula-input').value.trim();
672
+ const description = document.getElementById('macro-description-input').value.trim();
673
+
674
+ if (!name || !formula) {
675
+ alert('Please enter both a name and formula for the macro.');
676
+ return;
677
+ }
678
+
679
+ addCustomMacro(name, formula, description);
680
+ document.body.removeChild(modal);
681
+ updateMacrosDisplayInSettings();
682
+ });
683
+
684
+ modal.addEventListener('click', (e) => {
685
+ if (e.target === modal) {
686
+ document.body.removeChild(modal);
687
+ }
688
+ });
689
+ }
690
+
691
+ /**
692
+ * Update macros display in settings modal
693
+ */
694
+ function updateMacrosDisplayInSettings() {
695
+ const container = document.getElementById('custom-macros-container-settings');
696
+ if (!container) return;
697
+
698
+ if (customMacros.length === 0) {
699
+ container.innerHTML = '<p style="text-align: center; color: #888; padding: 15px;">No custom macros yet. Click "Add Macro" to create one.</p>';
700
+ return;
701
+ }
702
+
703
+ container.innerHTML = customMacros.map(macro => `
704
+ <div class="macro-item" style="
705
+ background: var(--bg-primary);
706
+ border: 2px solid var(--border-color);
707
+ border-radius: 8px;
708
+ padding: 12px;
709
+ margin-bottom: 10px;
710
+ display: flex;
711
+ justify-content: space-between;
712
+ align-items: center;
713
+ transition: all 0.2s;
714
+ ">
715
+ <div style="flex: 1;">
716
+ <div style="font-weight: bold; color: var(--text-primary); margin-bottom: 4px;">
717
+ ${macro.name}
718
+ </div>
719
+ <div style="font-family: monospace; color: var(--accent-info); font-size: 0.9em; margin-bottom: 4px;">
720
+ ${macro.formula}
721
+ </div>
722
+ ${macro.description ? `<div style="font-size: 0.85em; color: var(--text-secondary);">${macro.description}</div>` : ''}
723
+ </div>
724
+ <div style="display: flex; gap: 8px;">
725
+ <button class="macro-roll-btn" data-macro-id="${macro.id}" style="
726
+ background: var(--accent-primary);
727
+ color: white;
728
+ border: none;
729
+ padding: 8px 16px;
730
+ border-radius: 6px;
731
+ cursor: pointer;
732
+ font-weight: bold;
733
+ transition: all 0.2s;
734
+ ">
735
+ 🎲 Roll
736
+ </button>
737
+ <button class="macro-delete-btn" data-macro-id="${macro.id}" style="
738
+ background: var(--accent-danger);
739
+ color: white;
740
+ border: none;
741
+ padding: 8px 12px;
742
+ border-radius: 6px;
743
+ cursor: pointer;
744
+ transition: all 0.2s;
745
+ ">
746
+ 🗑️
747
+ </button>
748
+ </div>
749
+ </div>
750
+ `).join('');
751
+
752
+ container.querySelectorAll('.macro-roll-btn').forEach(btn => {
753
+ btn.addEventListener('click', () => {
754
+ const macroId = btn.dataset.macroId;
755
+ const macro = customMacros.find(m => m.id === macroId);
756
+ if (macro) {
757
+ executeMacro(macro);
758
+ // Close settings modal after rolling
759
+ const settingsModal = document.getElementById('settings-modal');
760
+ if (settingsModal) {
761
+ document.body.removeChild(settingsModal);
762
+ }
763
+ }
764
+ });
765
+ });
766
+
767
+ container.querySelectorAll('.macro-delete-btn').forEach(btn => {
768
+ btn.addEventListener('click', () => {
769
+ const macroId = btn.dataset.macroId;
770
+ const macro = customMacros.find(m => m.id === macroId);
771
+ if (macro && confirm(`Delete macro "${macro.name}"?`)) {
772
+ deleteCustomMacro(macroId);
773
+ updateMacrosDisplayInSettings();
774
+ }
775
+ });
776
+ });
777
+ }
778
+
779
+ /**
780
+ * Initialize custom macros system
781
+ */
782
+ function initCustomMacros() {
783
+ loadCustomMacros();
784
+
785
+ // Load custom macro button setting (default: disabled)
786
+ const savedSetting = localStorage.getItem('showCustomMacroButtons');
787
+ showCustomMacroButtons = savedSetting === 'true';
788
+ debug.log(`⚙️ Custom macro buttons setting: ${showCustomMacroButtons ? 'enabled' : 'disabled'}`);
789
+
790
+ debug.log('🎲 Custom macros system initialized');
791
+ }
792
+
793
+ /**
794
+ * Initialize settings button
795
+ */
796
+ function initSettingsButton() {
797
+ const settingsBtn = document.getElementById('settings-btn');
798
+ if (settingsBtn) {
799
+ settingsBtn.addEventListener('click', showSettingsModal);
800
+ debug.log('⚙️ Settings button initialized');
801
+ }
802
+ }
803
+
804
+ // ===== EXPORTS =====
805
+
806
+ // Export functions to globalThis
807
+ globalThis.loadCustomMacros = loadCustomMacros;
808
+ globalThis.saveAllCustomMacros = saveAllCustomMacros;
809
+ globalThis.addCustomMacro = addCustomMacro;
810
+ globalThis.deleteCustomMacro = deleteCustomMacro;
811
+ globalThis.executeMacro = executeMacro;
812
+ globalThis.updateMacrosDisplay = updateMacrosDisplay;
813
+ globalThis.showSettingsModal = showSettingsModal;
814
+ globalThis.showAddMacroModal = showAddMacroModal;
815
+ globalThis.updateMacrosDisplayInSettings = updateMacrosDisplayInSettings;
816
+ globalThis.initCustomMacros = initCustomMacros;
817
+ globalThis.initSettingsButton = initSettingsButton;
818
+
819
+ // Export state variables to globalThis
820
+ globalThis.customMacros = customMacros;
821
+ globalThis.showCustomMacroButtons = showCustomMacroButtons;
822
+
823
+ debug.log('✅ Macro System module loaded');
824
+
825
+ })();