@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,743 @@
1
+ /**
2
+ * Effects Manager Module
3
+ *
4
+ * Handles buffs, debuffs, and conditions tracking.
5
+ * Loaded as a plain script (no ES6 modules) to export to window.
6
+ *
7
+ * Functions exported to globalThis:
8
+ * - initConditionsManager()
9
+ * - showEffectsModal()
10
+ * - addEffect(effectName, type)
11
+ * - removeEffect(effectName, type)
12
+ * - addCondition(conditionName) - legacy wrapper
13
+ * - removeCondition(conditionName) - legacy wrapper
14
+ * - updateEffectsDisplay()
15
+ * - updateConditionsDisplay() - legacy wrapper
16
+ * - calculateTotalAC()
17
+ *
18
+ * Constants exported to globalThis:
19
+ * - POSITIVE_EFFECTS
20
+ * - NEGATIVE_EFFECTS
21
+ *
22
+ * State variables exported to globalThis:
23
+ * - activeBuffs
24
+ * - activeConditions
25
+ */
26
+
27
+ (function() {
28
+ 'use strict';
29
+
30
+ // ===== EFFECTS CONSTANTS =====
31
+
32
+ const POSITIVE_EFFECTS = [
33
+ {
34
+ name: 'Bless',
35
+ icon: '✨',
36
+ color: '#f39c12',
37
+ description: '+1d4 to attack rolls and saving throws',
38
+ modifier: { attack: '1d4', save: '1d4' },
39
+ autoApply: true
40
+ },
41
+ {
42
+ name: 'Guidance',
43
+ icon: '🙏',
44
+ color: '#3498db',
45
+ description: '+1d4 to one ability check',
46
+ modifier: { skill: '1d4' },
47
+ autoApply: false // User choice required
48
+ },
49
+ {
50
+ name: 'Bardic Inspiration (d6)',
51
+ icon: '🎵',
52
+ color: '#9b59b6',
53
+ description: 'Bard levels 1-4: +d6 to ability check, attack, or save',
54
+ modifier: { attack: 'd6', skill: 'd6', save: 'd6' },
55
+ autoApply: false
56
+ },
57
+ {
58
+ name: 'Bardic Inspiration (d8)',
59
+ icon: '🎵',
60
+ color: '#9b59b6',
61
+ description: 'Bard levels 5-9: +d8 to ability check, attack, or save',
62
+ modifier: { attack: 'd8', skill: 'd8', save: 'd8' },
63
+ autoApply: false
64
+ },
65
+ {
66
+ name: 'Bardic Inspiration (d10)',
67
+ icon: '🎵',
68
+ color: '#9b59b6',
69
+ description: 'Bard levels 10-14: +d10 to ability check, attack, or save',
70
+ modifier: { attack: 'd10', skill: 'd10', save: 'd10' },
71
+ autoApply: false
72
+ },
73
+ {
74
+ name: 'Bardic Inspiration (d12)',
75
+ icon: '🎵',
76
+ color: '#9b59b6',
77
+ description: 'Bard levels 15-20: +d12 to ability check, attack, or save',
78
+ modifier: { attack: 'd12', skill: 'd12', save: 'd12' },
79
+ autoApply: false
80
+ },
81
+ {
82
+ name: 'Haste',
83
+ icon: '⚡',
84
+ color: '#3498db',
85
+ description: '+2 AC, advantage on DEX saves, extra action',
86
+ modifier: { ac: 2, dexSave: 'advantage' },
87
+ autoApply: true
88
+ },
89
+ {
90
+ name: 'Enlarge',
91
+ icon: '⬆️',
92
+ color: '#27ae60',
93
+ description: '+1d4 weapon damage, advantage on STR checks/saves',
94
+ modifier: { damage: '1d4', strCheck: 'advantage', strSave: 'advantage' },
95
+ autoApply: true
96
+ },
97
+ {
98
+ name: 'Invisibility',
99
+ icon: '👻',
100
+ color: '#ecf0f1',
101
+ description: 'Advantage on attack rolls, enemies have disadvantage',
102
+ modifier: { attack: 'advantage' },
103
+ autoApply: true
104
+ },
105
+ {
106
+ name: 'Shield of Faith',
107
+ icon: '🛡️',
108
+ color: '#f39c12',
109
+ description: '+2 AC',
110
+ modifier: { ac: 2 },
111
+ autoApply: true
112
+ },
113
+ {
114
+ name: 'Heroism',
115
+ icon: '🦸',
116
+ color: '#e67e22',
117
+ description: 'Immune to frightened, temp HP each turn',
118
+ modifier: { frightened: 'immune' },
119
+ autoApply: true
120
+ },
121
+ {
122
+ name: 'Enhance Ability',
123
+ icon: '💪',
124
+ color: '#27ae60',
125
+ description: 'Advantage on ability checks with chosen ability',
126
+ modifier: { skill: 'advantage' },
127
+ autoApply: false
128
+ },
129
+ {
130
+ name: 'Rage',
131
+ icon: '😡',
132
+ color: '#e74c3c',
133
+ description: '+2 damage on melee attacks, advantage on STR checks/saves, resistance to physical damage',
134
+ modifier: { damage: 2, strCheck: 'advantage', strSave: 'advantage', physicalResistance: true },
135
+ autoApply: true
136
+ },
137
+ {
138
+ name: 'Rage (+3)',
139
+ icon: '😤',
140
+ color: '#c0392b',
141
+ description: 'Level 9-15: +3 damage on melee attacks, advantage on STR checks/saves, resistance to physical damage',
142
+ modifier: { damage: 3, strCheck: 'advantage', strSave: 'advantage', physicalResistance: true },
143
+ autoApply: true
144
+ },
145
+ {
146
+ name: 'Rage (+4)',
147
+ icon: '🔥',
148
+ color: '#8b0000',
149
+ description: 'Level 16+: +4 damage on melee attacks, advantage on STR checks/saves, resistance to physical damage',
150
+ modifier: { damage: 4, strCheck: 'advantage', strSave: 'advantage', physicalResistance: true },
151
+ autoApply: true
152
+ },
153
+ {
154
+ name: 'Aid',
155
+ icon: '❤️',
156
+ color: '#e74c3c',
157
+ description: 'Max HP increased by 5',
158
+ modifier: { maxHp: 5 },
159
+ autoApply: true
160
+ },
161
+ {
162
+ name: 'True Strike',
163
+ icon: '🎯',
164
+ color: '#3498db',
165
+ description: 'Advantage on next attack roll',
166
+ modifier: { attack: 'advantage' },
167
+ autoApply: true
168
+ },
169
+ {
170
+ name: 'Faerie Fire',
171
+ icon: '✨',
172
+ color: '#9b59b6',
173
+ description: 'Attackers have advantage against target',
174
+ modifier: {},
175
+ autoApply: false
176
+ }
177
+ ];
178
+
179
+ // NEGATIVE EFFECTS (Debuffs/Conditions)
180
+ const NEGATIVE_EFFECTS = [
181
+ {
182
+ name: 'Bane',
183
+ icon: '💀',
184
+ color: '#e74c3c',
185
+ description: '-1d4 to attack rolls and saving throws',
186
+ modifier: { attack: '-1d4', save: '-1d4' },
187
+ autoApply: true
188
+ },
189
+ {
190
+ name: 'Poisoned',
191
+ icon: '☠️',
192
+ color: '#27ae60',
193
+ description: 'Disadvantage on attack rolls and ability checks',
194
+ modifier: { attack: 'disadvantage', skill: 'disadvantage' },
195
+ autoApply: true
196
+ },
197
+ {
198
+ name: 'Frightened',
199
+ icon: '😱',
200
+ color: '#e67e22',
201
+ description: 'Disadvantage on ability checks and attack rolls',
202
+ modifier: { attack: 'disadvantage', skill: 'disadvantage' },
203
+ autoApply: true
204
+ },
205
+ {
206
+ name: 'Stunned',
207
+ icon: '💫',
208
+ color: '#9b59b6',
209
+ description: 'Incapacitated, auto-fail STR/DEX saves, attackers have advantage',
210
+ modifier: { strSave: 'fail', dexSave: 'fail' },
211
+ autoApply: true
212
+ },
213
+ {
214
+ name: 'Paralyzed',
215
+ icon: '🧊',
216
+ color: '#34495e',
217
+ description: 'Incapacitated, auto-fail STR/DEX saves, attacks within 5ft are crits',
218
+ modifier: { strSave: 'fail', dexSave: 'fail' },
219
+ autoApply: true
220
+ },
221
+ {
222
+ name: 'Restrained',
223
+ icon: '⛓️',
224
+ color: '#7f8c8d',
225
+ description: 'Disadvantage on DEX saves and attack rolls',
226
+ modifier: { attack: 'disadvantage', dexSave: 'disadvantage' },
227
+ autoApply: true
228
+ },
229
+ {
230
+ name: 'Blinded',
231
+ icon: '🙈',
232
+ color: '#34495e',
233
+ description: 'Auto-fail sight checks, disadvantage on attacks',
234
+ modifier: { attack: 'disadvantage', perception: 'disadvantage' },
235
+ autoApply: true
236
+ },
237
+ {
238
+ name: 'Deafened',
239
+ icon: '🙉',
240
+ color: '#7f8c8d',
241
+ description: 'Auto-fail hearing checks',
242
+ modifier: { perception: 'disadvantage' },
243
+ autoApply: true
244
+ },
245
+ {
246
+ name: 'Charmed',
247
+ icon: '💖',
248
+ color: '#e91e63',
249
+ description: 'Cannot attack charmer, charmer has advantage on social checks',
250
+ modifier: {},
251
+ autoApply: false
252
+ },
253
+ {
254
+ name: 'Grappled',
255
+ icon: '🤼',
256
+ color: '#f39c12',
257
+ description: 'Speed becomes 0',
258
+ modifier: { speed: 0 },
259
+ autoApply: true
260
+ },
261
+ {
262
+ name: 'Prone',
263
+ icon: '⬇️',
264
+ color: '#95a5a6',
265
+ description: 'Disadvantage on attack rolls, melee attacks against you have advantage',
266
+ modifier: { attack: 'disadvantage' },
267
+ autoApply: true
268
+ },
269
+ {
270
+ name: 'Incapacitated',
271
+ icon: '😵',
272
+ color: '#c0392b',
273
+ description: 'Cannot take actions or reactions',
274
+ modifier: {},
275
+ autoApply: false
276
+ },
277
+ {
278
+ name: 'Unconscious',
279
+ icon: '😴',
280
+ color: '#34495e',
281
+ description: 'Incapacitated, drop everything, auto-fail STR/DEX saves',
282
+ modifier: { strSave: 'fail', dexSave: 'fail' },
283
+ autoApply: true
284
+ },
285
+ {
286
+ name: 'Petrified',
287
+ icon: '🗿',
288
+ color: '#95a5a6',
289
+ description: 'Incapacitated, auto-fail STR/DEX saves, resistance to all damage',
290
+ modifier: { strSave: 'fail', dexSave: 'fail' },
291
+ autoApply: true
292
+ },
293
+ {
294
+ name: 'Slowed',
295
+ icon: '🐌',
296
+ color: '#95a5a6',
297
+ description: 'Speed halved, -2 AC and DEX saves, no reactions',
298
+ modifier: { ac: -2, dexSave: '-2' },
299
+ autoApply: true
300
+ },
301
+ {
302
+ name: 'Hexed',
303
+ icon: '🔮',
304
+ color: '#9b59b6',
305
+ description: 'Disadvantage on ability checks with chosen ability, extra damage to caster',
306
+ modifier: { skill: 'disadvantage' },
307
+ autoApply: false
308
+ },
309
+ {
310
+ name: 'Cursed',
311
+ icon: '😈',
312
+ color: '#c0392b',
313
+ description: 'Disadvantage on attacks and saves against caster',
314
+ modifier: { attack: 'disadvantage', save: 'disadvantage' },
315
+ autoApply: true
316
+ }
317
+ ];
318
+
319
+ // ===== STATE VARIABLES =====
320
+
321
+ let activeConditions = [];
322
+ let activeBuffs = [];
323
+
324
+ // ===== EFFECTS MANAGEMENT FUNCTIONS =====
325
+
326
+ /**
327
+ * Initialize conditions manager UI
328
+ */
329
+ function initConditionsManager() {
330
+ const addConditionBtn = document.getElementById('add-condition-btn');
331
+
332
+ if (addConditionBtn) {
333
+ // Open modal when clicking conditions button
334
+ addConditionBtn.addEventListener('click', (e) => {
335
+ e.stopPropagation();
336
+ showEffectsModal();
337
+ });
338
+ }
339
+
340
+ debug.log('✅ Effects manager initialized (buffs + debuffs)');
341
+ }
342
+
343
+ /**
344
+ * Show effects modal for adding buffs and debuffs
345
+ */
346
+ function showEffectsModal() {
347
+ // Create modal overlay
348
+ const modal = document.createElement('div');
349
+ modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 10000;';
350
+
351
+ // Create modal content
352
+ const modalContent = document.createElement('div');
353
+ modalContent.style.cssText = 'background: var(--bg-secondary); color: var(--text-primary); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); width: 90%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden;';
354
+
355
+ // Modal header
356
+ const header = document.createElement('div');
357
+ header.style.cssText = 'padding: 20px; border-bottom: 2px solid #ecf0f1; background: #f8f9fa;';
358
+ header.innerHTML = `
359
+ <div style="display: flex; justify-content: space-between; align-items: center;">
360
+ <h3 style="margin: 0; color: var(--text-primary);">🎭 Effects & Conditions</h3>
361
+ <button id="effects-modal-close" style="background: #e74c3c; color: white; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-weight: bold;">✕</button>
362
+ </div>
363
+ `;
364
+
365
+ // Tab navigation
366
+ const tabNav = document.createElement('div');
367
+ tabNav.style.cssText = 'display: flex; background: #ecf0f1; border-bottom: 2px solid #bdc3c7;';
368
+ tabNav.innerHTML = `
369
+ <button class="effects-tab-btn" data-tab="buffs" style="flex: 1; padding: 15px; background: var(--bg-tertiary); border: none; border-bottom: 3px solid #27ae60; cursor: pointer; font-weight: bold; font-size: 1em; color: #27ae60; transition: all 0.2s;">✨ Buffs</button>
370
+ <button class="effects-tab-btn" data-tab="debuffs" style="flex: 1; padding: 15px; background: transparent; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-weight: bold; font-size: 1em; color: var(--text-secondary); transition: all 0.2s;">💀 Debuffs</button>
371
+ `;
372
+
373
+ // Tab content container
374
+ const tabContent = document.createElement('div');
375
+ tabContent.style.cssText = 'padding: 20px; overflow-y: auto; flex: 1;';
376
+
377
+ // Buffs tab
378
+ const buffsTab = document.createElement('div');
379
+ buffsTab.className = 'effects-tab-content';
380
+ buffsTab.dataset.tab = 'buffs';
381
+ buffsTab.style.display = 'block';
382
+ buffsTab.innerHTML = POSITIVE_EFFECTS.map(effect => `
383
+ <div class="effect-option" data-effect="${effect.name}" data-type="positive" style="padding: 12px; margin-bottom: 10px; border: 2px solid ${effect.color}40; border-radius: 8px; cursor: pointer; transition: all 0.2s; background: var(--bg-secondary);">
384
+ <div style="display: flex; align-items: center; gap: 12px;">
385
+ <span class="effect-icon" style="font-size: 1.5em;">${effect.icon}</span>
386
+ <div style="flex: 1;">
387
+ <div class="effect-name" style="font-weight: bold; color: var(--text-primary); margin-bottom: 4px;">${effect.name}</div>
388
+ <div class="effect-description" style="font-size: 0.85em; color: var(--text-secondary);">${effect.description}</div>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ `).join('');
393
+
394
+ // Debuffs tab
395
+ const debuffsTab = document.createElement('div');
396
+ debuffsTab.className = 'effects-tab-content';
397
+ debuffsTab.dataset.tab = 'debuffs';
398
+ debuffsTab.style.display = 'none';
399
+ debuffsTab.innerHTML = NEGATIVE_EFFECTS.map(effect => `
400
+ <div class="effect-option" data-effect="${effect.name}" data-type="negative" style="padding: 12px; margin-bottom: 10px; border: 2px solid ${effect.color}40; border-radius: 8px; cursor: pointer; transition: all 0.2s; background: var(--bg-secondary);">
401
+ <div style="display: flex; align-items: center; gap: 12px;">
402
+ <span class="effect-icon" style="font-size: 1.5em;">${effect.icon}</span>
403
+ <div style="flex: 1;">
404
+ <div class="effect-name" style="font-weight: bold; color: var(--text-primary); margin-bottom: 4px;">${effect.name}</div>
405
+ <div class="effect-description" style="font-size: 0.85em; color: var(--text-secondary);">${effect.description}</div>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ `).join('');
410
+
411
+ tabContent.appendChild(buffsTab);
412
+ tabContent.appendChild(debuffsTab);
413
+
414
+ // Assemble modal
415
+ modalContent.appendChild(header);
416
+ modalContent.appendChild(tabNav);
417
+ modalContent.appendChild(tabContent);
418
+ modal.appendChild(modalContent);
419
+ document.body.appendChild(modal);
420
+
421
+ // Tab switching
422
+ const tabButtons = tabNav.querySelectorAll('.effects-tab-btn');
423
+ const tabContents = modalContent.querySelectorAll('.effects-tab-content');
424
+
425
+ tabButtons.forEach(btn => {
426
+ btn.addEventListener('click', () => {
427
+ const targetTab = btn.dataset.tab;
428
+
429
+ // Update button styles
430
+ tabButtons.forEach(b => {
431
+ if (b.dataset.tab === targetTab) {
432
+ b.style.background = 'var(--bg-tertiary)';
433
+ b.style.color = targetTab === 'buffs' ? '#27ae60' : '#e74c3c';
434
+ b.style.borderBottom = `3px solid ${targetTab === 'buffs' ? '#27ae60' : '#e74c3c'}`;
435
+ } else {
436
+ b.style.background = 'transparent';
437
+ b.style.color = '#7f8c8d';
438
+ b.style.borderBottom = '3px solid transparent';
439
+ }
440
+ });
441
+
442
+ // Show target tab content
443
+ tabContents.forEach(content => {
444
+ content.style.display = content.dataset.tab === targetTab ? 'block' : 'none';
445
+ });
446
+ });
447
+ });
448
+
449
+ // Add hover effects
450
+ modalContent.querySelectorAll('.effect-option').forEach(option => {
451
+ option.addEventListener('mouseenter', () => {
452
+ option.style.transform = 'translateX(5px)';
453
+ option.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
454
+ });
455
+ option.addEventListener('mouseleave', () => {
456
+ option.style.transform = 'translateX(0)';
457
+ option.style.boxShadow = 'none';
458
+ });
459
+ });
460
+
461
+ // Add effect when clicking option
462
+ modalContent.querySelectorAll('.effect-option').forEach(option => {
463
+ option.addEventListener('click', () => {
464
+ const effectName = option.dataset.effect;
465
+ const type = option.dataset.type === 'positive' ? 'positive' : 'negative';
466
+ addEffect(effectName, type);
467
+ modal.remove();
468
+ });
469
+ });
470
+
471
+ // Close button
472
+ const closeBtn = modalContent.querySelector('#effects-modal-close');
473
+ closeBtn.addEventListener('click', () => modal.remove());
474
+
475
+ // Click outside to close
476
+ modal.addEventListener('click', (e) => {
477
+ if (e.target === modal) {
478
+ modal.remove();
479
+ }
480
+ });
481
+ }
482
+
483
+ /**
484
+ * Add an effect (buff or debuff)
485
+ */
486
+ function addEffect(effectName, type) {
487
+ // characterData should be available from global scope
488
+ if (typeof characterData === 'undefined') {
489
+ debug.warn('⚠️ characterData not available');
490
+ return;
491
+ }
492
+
493
+ const effectsList = type === 'positive' ? POSITIVE_EFFECTS : NEGATIVE_EFFECTS;
494
+ const activeList = type === 'positive' ? activeBuffs : activeConditions;
495
+
496
+ // Don't add if already active
497
+ if (activeList.includes(effectName)) {
498
+ if (typeof showNotification !== 'undefined') {
499
+ showNotification(`⚠️ ${effectName} already active`);
500
+ }
501
+ return;
502
+ }
503
+
504
+ const effect = effectsList.find(e => e.name === effectName);
505
+ activeList.push(effectName);
506
+
507
+ // Update the correct array reference
508
+ if (type === 'positive') {
509
+ activeBuffs = activeList;
510
+ } else {
511
+ activeConditions = activeList;
512
+ }
513
+
514
+ updateEffectsDisplay();
515
+ if (typeof showNotification !== 'undefined') {
516
+ showNotification(`${effect.icon} ${effectName} applied!`);
517
+ }
518
+ debug.log(`✅ Effect added: ${effectName} (${type})`);
519
+
520
+ // TODO: Add Owlbear Rodeo integration for effect announcements
521
+
522
+ // Save to character data
523
+ if (!characterData.activeEffects) {
524
+ characterData.activeEffects = { buffs: [], debuffs: [] };
525
+ }
526
+ if (type === 'positive') {
527
+ characterData.activeEffects.buffs = activeBuffs;
528
+ } else {
529
+ characterData.activeEffects.debuffs = activeConditions;
530
+ }
531
+ if (typeof saveCharacterData !== 'undefined') {
532
+ saveCharacterData();
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Remove an effect (buff or debuff)
538
+ */
539
+ function removeEffect(effectName, type) {
540
+ // characterData should be available from global scope
541
+ if (typeof characterData === 'undefined') {
542
+ debug.warn('⚠️ characterData not available');
543
+ return;
544
+ }
545
+
546
+ const effectsList = type === 'positive' ? POSITIVE_EFFECTS : NEGATIVE_EFFECTS;
547
+ const effect = effectsList.find(e => e.name === effectName);
548
+
549
+ if (type === 'positive') {
550
+ activeBuffs = activeBuffs.filter(e => e !== effectName);
551
+ } else {
552
+ activeConditions = activeConditions.filter(e => e !== effectName);
553
+ }
554
+
555
+ updateEffectsDisplay();
556
+ if (typeof showNotification !== 'undefined') {
557
+ showNotification(`✅ ${effectName} removed`);
558
+ }
559
+ debug.log(`🗑️ Effect removed: ${effectName} (${type})`);
560
+
561
+ // TODO: Add Owlbear Rodeo integration for effect removal announcements
562
+
563
+ // Save to character data
564
+ if (!characterData.activeEffects) {
565
+ characterData.activeEffects = { buffs: [], debuffs: [] };
566
+ }
567
+ if (type === 'positive') {
568
+ characterData.activeEffects.buffs = activeBuffs;
569
+ } else {
570
+ characterData.activeEffects.debuffs = activeConditions;
571
+ }
572
+ if (typeof saveCharacterData !== 'undefined') {
573
+ saveCharacterData();
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Legacy function for backwards compatibility
579
+ */
580
+ function addCondition(conditionName) {
581
+ addEffect(conditionName, 'negative');
582
+ }
583
+
584
+ /**
585
+ * Legacy function for backwards compatibility
586
+ */
587
+ function removeCondition(conditionName) {
588
+ removeEffect(conditionName, 'negative');
589
+ }
590
+
591
+ /**
592
+ * Update effects display UI
593
+ */
594
+ function updateEffectsDisplay() {
595
+ const container = document.getElementById('active-conditions');
596
+ if (!container) return;
597
+
598
+ let html = '';
599
+
600
+ // Show buffs section
601
+ if (activeBuffs.length > 0) {
602
+ html += '<div style="margin-bottom: 15px;">';
603
+ html += '<div style="font-size: 0.85em; font-weight: bold; color: #27ae60; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;"><span>✨</span> BUFFS</div>';
604
+ html += activeBuffs.map(effectName => {
605
+ const effect = POSITIVE_EFFECTS.find(e => e.name === effectName);
606
+ return `
607
+ <div class="effect-badge" data-effect="${effectName}" data-type="positive" title="${effect.description} - Click to remove" style="background: ${effect.color}20; border: 2px solid ${effect.color}; cursor: pointer; padding: 8px 12px; border-radius: 6px; margin-bottom: 8px; transition: all 0.2s;">
608
+ <div style="display: flex; align-items: center; gap: 8px;">
609
+ <span class="effect-badge-icon" style="font-size: 1.2em;">${effect.icon}</span>
610
+ <div style="flex: 1;">
611
+ <div style="font-weight: bold; color: var(--text-primary);">${effect.name}</div>
612
+ <div style="font-size: 0.75em; color: var(--text-secondary); margin-top: 2px;">${effect.description}</div>
613
+ </div>
614
+ <span class="effect-badge-remove" style="font-weight: bold; opacity: 0.7; color: #e74c3c;">✕</span>
615
+ </div>
616
+ </div>
617
+ `;
618
+ }).join('');
619
+ html += '</div>';
620
+ }
621
+
622
+ // Show debuffs section
623
+ if (activeConditions.length > 0) {
624
+ html += '<div style="margin-bottom: 15px;">';
625
+ html += '<div style="font-size: 0.85em; font-weight: bold; color: #e74c3c; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;"><span>💀</span> DEBUFFS</div>';
626
+ html += activeConditions.map(effectName => {
627
+ const effect = NEGATIVE_EFFECTS.find(e => e.name === effectName);
628
+ return `
629
+ <div class="effect-badge" data-effect="${effectName}" data-type="negative" title="${effect.description} - Click to remove" style="background: ${effect.color}20; border: 2px solid ${effect.color}; cursor: pointer; padding: 8px 12px; border-radius: 6px; margin-bottom: 8px; transition: all 0.2s;">
630
+ <div style="display: flex; align-items: center; gap: 8px;">
631
+ <span class="effect-badge-icon" style="font-size: 1.2em;">${effect.icon}</span>
632
+ <div style="flex: 1;">
633
+ <div style="font-weight: bold; color: var(--text-primary);">${effect.name}</div>
634
+ <div style="font-size: 0.75em; color: var(--text-secondary); margin-top: 2px;">${effect.description}</div>
635
+ </div>
636
+ <span class="effect-badge-remove" style="font-weight: bold; opacity: 0.7; color: #e74c3c;">✕</span>
637
+ </div>
638
+ </div>
639
+ `;
640
+ }).join('');
641
+ html += '</div>';
642
+ }
643
+
644
+ // Show empty state if no effects
645
+ if (activeBuffs.length === 0 && activeConditions.length === 0) {
646
+ html = '<div style="text-align: center; color: #888; padding: 15px; font-size: 0.9em;">No active effects</div>';
647
+ }
648
+
649
+ container.innerHTML = html;
650
+
651
+ // Update AC display to reflect any changes
652
+ const acElement = document.getElementById('char-ac');
653
+ if (acElement && typeof calculateTotalAC !== 'undefined') {
654
+ acElement.textContent = calculateTotalAC();
655
+ }
656
+
657
+ // Add click handlers to remove effects
658
+ container.querySelectorAll('.effect-badge').forEach(badge => {
659
+ const effectName = badge.dataset.effect;
660
+ const type = badge.dataset.type;
661
+
662
+ // Add hover effect
663
+ badge.addEventListener('mouseenter', () => {
664
+ badge.style.transform = 'translateX(3px)';
665
+ badge.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
666
+ });
667
+ badge.addEventListener('mouseleave', () => {
668
+ badge.style.transform = 'translateX(0)';
669
+ badge.style.boxShadow = 'none';
670
+ });
671
+
672
+ // Remove on click
673
+ badge.addEventListener('click', () => {
674
+ removeEffect(effectName, type);
675
+ });
676
+ });
677
+ }
678
+
679
+ /**
680
+ * Legacy function for backwards compatibility
681
+ */
682
+ function updateConditionsDisplay() {
683
+ updateEffectsDisplay();
684
+ }
685
+
686
+ /**
687
+ * Calculate total AC including active effects
688
+ * @param {Object} data - Character data object (optional, uses globalThis.characterData if not provided)
689
+ * @returns {number} Total AC with effect modifiers applied
690
+ */
691
+ function calculateTotalAC(data) {
692
+ // Use provided data or fall back to global character data
693
+ const charData = data || globalThis.characterData;
694
+ const baseAC = charData?.armorClass || 10;
695
+ let totalAC = baseAC;
696
+
697
+ // Combine all active effects
698
+ const allEffects = [
699
+ ...activeBuffs.map(name => ({ ...POSITIVE_EFFECTS.find(e => e.name === name), type: 'buff' })),
700
+ ...activeConditions.map(name => ({ ...NEGATIVE_EFFECTS.find(e => e.name === name), type: 'debuff' }))
701
+ ].filter(e => e && e.autoApply && e.modifier && e.modifier.ac);
702
+
703
+ // Apply AC modifiers from active effects
704
+ for (const effect of allEffects) {
705
+ const acMod = effect.modifier.ac;
706
+ if (typeof acMod === 'number') {
707
+ totalAC += acMod;
708
+ debug.log(`🛡️ Applied AC modifier: ${acMod} from ${effect.name} (${effect.type})`);
709
+ }
710
+ }
711
+
712
+ debug.log(`🛡️ Total AC calculation: ${baseAC} (base) + modifiers = ${totalAC}`);
713
+ return totalAC;
714
+ }
715
+
716
+ // ===== EXPORTS =====
717
+
718
+ window.initConditionsManager = initConditionsManager;
719
+ window.showEffectsModal = showEffectsModal;
720
+ window.addEffect = addEffect;
721
+ window.removeEffect = removeEffect;
722
+ window.addCondition = addCondition;
723
+ window.removeCondition = removeCondition;
724
+ window.updateEffectsDisplay = updateEffectsDisplay;
725
+ window.updateConditionsDisplay = updateConditionsDisplay;
726
+ window.calculateTotalAC = calculateTotalAC;
727
+
728
+ // Export constants
729
+ window.POSITIVE_EFFECTS = POSITIVE_EFFECTS;
730
+ window.NEGATIVE_EFFECTS = NEGATIVE_EFFECTS;
731
+
732
+ // Export state variables with getters and setters
733
+ Object.defineProperty(globalThis, 'activeBuffs', {
734
+ get: () => activeBuffs,
735
+ set: (value) => { activeBuffs = value; }
736
+ });
737
+
738
+ Object.defineProperty(globalThis, 'activeConditions', {
739
+ get: () => activeConditions,
740
+ set: (value) => { activeConditions = value; }
741
+ });
742
+
743
+ })();