@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,399 @@
1
+ /**
2
+ * Health Modals Module
3
+ *
4
+ * HP adjustment and death saves tracking modals.
5
+ * Handles healing, damage, temporary HP, and death save mechanics.
6
+ *
7
+ * Loaded as a plain script (no ES6 modules) to export to globalThis.
8
+ *
9
+ * Functions exported to globalThis:
10
+ * - showHPModal()
11
+ * - showDeathSavesModal()
12
+ */
13
+
14
+ (function() {
15
+ 'use strict';
16
+
17
+ // ===== HP MODAL =====
18
+
19
+ function showHPModal() {
20
+ // Create modal overlay
21
+ const modal = document.createElement('div');
22
+ 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;';
23
+
24
+ // Create modal content
25
+ const modalContent = document.createElement('div');
26
+ modalContent.style.cssText = 'background: var(--bg-secondary); color: var(--text-primary); padding: 30px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); min-width: 300px;';
27
+
28
+ const currentHP = characterData.hitPoints.current;
29
+ const maxHP = characterData.hitPoints.max;
30
+ const tempHP = characterData.temporaryHP || 0;
31
+
32
+ modalContent.innerHTML = `
33
+ <h3 style="margin: 0 0 20px 0; color: var(--text-primary); text-align: center;">Adjust Hit Points</h3>
34
+ <div style="text-align: center; font-size: 1.2em; margin-bottom: 20px; color: var(--text-secondary);">
35
+ Current: <strong>${currentHP}${tempHP > 0 ? `+${tempHP}` : ''} / ${maxHP}</strong>
36
+ </div>
37
+
38
+ <div style="margin-bottom: 20px;">
39
+ <label style="display: block; margin-bottom: 10px; font-weight: bold; color: var(--text-primary);">Amount:</label>
40
+ <input type="number" id="hp-amount" min="1" value="1" style="width: 100%; padding: 10px; font-size: 1.1em; border: 2px solid var(--border-color); border-radius: 6px; box-sizing: border-box; background: var(--bg-tertiary); color: var(--text-primary);">
41
+ </div>
42
+
43
+ <div style="margin-bottom: 25px;">
44
+ <label style="display: block; margin-bottom: 10px; font-weight: bold; color: var(--text-primary);">Action:</label>
45
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;">
46
+ <button id="hp-toggle-heal" style="padding: 12px; font-size: 0.9em; font-weight: bold; border: 2px solid #27ae60; background: #27ae60; color: white; border-radius: 6px; cursor: pointer; transition: all 0.2s;">
47
+ 💚 Heal
48
+ </button>
49
+ <button id="hp-toggle-damage" style="padding: 12px; font-size: 0.9em; font-weight: bold; border: 2px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-secondary); border-radius: 6px; cursor: pointer; transition: all 0.2s;">
50
+ 💔 Damage
51
+ </button>
52
+ <button id="hp-toggle-temp" style="padding: 12px; font-size: 0.9em; font-weight: bold; border: 2px solid var(--border-color); background: var(--bg-tertiary); color: var(--text-secondary); border-radius: 6px; cursor: pointer; transition: all 0.2s;">
53
+ 🛡️ Temp HP
54
+ </button>
55
+ </div>
56
+ </div>
57
+
58
+ <div style="display: flex; gap: 10px;">
59
+ <button id="hp-cancel" style="flex: 1; padding: 12px; font-size: 1em; background: #95a5a6; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
60
+ Cancel
61
+ </button>
62
+ <button id="hp-confirm" style="flex: 1; padding: 12px; font-size: 1em; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
63
+ Confirm
64
+ </button>
65
+ </div>
66
+ `;
67
+
68
+ modal.appendChild(modalContent);
69
+ document.body.appendChild(modal);
70
+
71
+ // Toggle state: 'heal', 'damage', or 'temp'
72
+ let actionType = 'heal';
73
+
74
+ const healBtn = document.getElementById('hp-toggle-heal');
75
+ const damageBtn = document.getElementById('hp-toggle-damage');
76
+ const tempBtn = document.getElementById('hp-toggle-temp');
77
+ const amountInput = document.getElementById('hp-amount');
78
+
79
+ // Helper function to reset all buttons
80
+ const resetButtons = () => {
81
+ healBtn.style.background = 'var(--bg-tertiary)';
82
+ healBtn.style.color = '#7f8c8d';
83
+ healBtn.style.borderColor = '#bdc3c7';
84
+ damageBtn.style.background = 'var(--bg-tertiary)';
85
+ damageBtn.style.color = '#7f8c8d';
86
+ damageBtn.style.borderColor = '#bdc3c7';
87
+ tempBtn.style.background = 'var(--bg-tertiary)';
88
+ tempBtn.style.color = '#7f8c8d';
89
+ tempBtn.style.borderColor = '#bdc3c7';
90
+ };
91
+
92
+ // Toggle button handlers
93
+ healBtn.addEventListener('click', () => {
94
+ actionType = 'heal';
95
+ resetButtons();
96
+ healBtn.style.background = '#27ae60';
97
+ healBtn.style.color = 'white';
98
+ healBtn.style.borderColor = '#27ae60';
99
+ });
100
+
101
+ damageBtn.addEventListener('click', () => {
102
+ actionType = 'damage';
103
+ resetButtons();
104
+ damageBtn.style.background = '#e74c3c';
105
+ damageBtn.style.color = 'white';
106
+ damageBtn.style.borderColor = '#e74c3c';
107
+ });
108
+
109
+ tempBtn.addEventListener('click', () => {
110
+ actionType = 'temp';
111
+ resetButtons();
112
+ tempBtn.style.background = '#3498db';
113
+ tempBtn.style.color = 'white';
114
+ tempBtn.style.borderColor = '#3498db';
115
+ });
116
+
117
+ // Cancel button
118
+ document.getElementById('hp-cancel').addEventListener('click', () => {
119
+ modal.remove();
120
+ });
121
+
122
+ // Confirm button
123
+ document.getElementById('hp-confirm').addEventListener('click', () => {
124
+ const amount = parseInt(amountInput.value);
125
+
126
+ if (isNaN(amount) || amount <= 0) {
127
+ showNotification('❌ Please enter a valid amount', 'error');
128
+ return;
129
+ }
130
+
131
+ const oldHP = characterData.hitPoints.current;
132
+ const oldTempHP = characterData.temporaryHP || 0;
133
+ const colorBanner = getColoredBanner(characterData);
134
+ let messageData;
135
+
136
+ if (actionType === 'heal') {
137
+ // Healing: increase current HP (up to max), doesn't affect temp HP (RAW)
138
+ characterData.hitPoints.current = Math.min(currentHP + amount, maxHP);
139
+ const actualHealing = characterData.hitPoints.current - oldHP;
140
+
141
+ // Reset death saves on healing
142
+ if (actualHealing > 0 && characterData.deathSaves && (characterData.deathSaves.successes > 0 || characterData.deathSaves.failures > 0)) {
143
+ characterData.deathSaves.successes = 0;
144
+ characterData.deathSaves.failures = 0;
145
+ debug.log('♻️ Death saves reset due to healing');
146
+ }
147
+
148
+ showNotification(`💚 Healed ${actualHealing} HP! (${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP})`);
149
+
150
+ messageData = {
151
+ action: 'announceSpell',
152
+ message: `&{template:default} {{name=${colorBanner}${characterData.name} regains HP}} {{💚 Healing=${actualHealing} HP}} {{Current HP=${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP}}}`,
153
+ color: characterData.notificationColor
154
+ };
155
+ } else if (actionType === 'damage') {
156
+ // Damage: deplete temp HP first, then current HP (RAW)
157
+ let remainingDamage = amount;
158
+ let tempHPLost = 0;
159
+ let actualDamage = 0;
160
+
161
+ if (characterData.temporaryHP > 0) {
162
+ tempHPLost = Math.min(characterData.temporaryHP, remainingDamage);
163
+ characterData.temporaryHP -= tempHPLost;
164
+ remainingDamage -= tempHPLost;
165
+ }
166
+
167
+ if (remainingDamage > 0) {
168
+ characterData.hitPoints.current = Math.max(currentHP - remainingDamage, 0);
169
+ actualDamage = oldHP - characterData.hitPoints.current;
170
+ }
171
+
172
+ const damageMsg = tempHPLost > 0
173
+ ? `💔 Took ${amount} damage! (${tempHPLost} temp HP${actualDamage > 0 ? ` + ${actualDamage} HP` : ''})`
174
+ : `💔 Took ${actualDamage} damage!`;
175
+
176
+ showNotification(`${damageMsg} (${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP})`);
177
+
178
+ const damageDetails = tempHPLost > 0
179
+ ? `{{Temp HP Lost=${tempHPLost}}}${actualDamage > 0 ? ` {{HP Lost=${actualDamage}}}` : ''}`
180
+ : `{{HP Lost=${actualDamage}}}`;
181
+
182
+ messageData = {
183
+ action: 'announceSpell',
184
+ message: `&{template:default} {{name=${colorBanner}${characterData.name} takes damage}} {{💔 Total Damage=${amount}}} ${damageDetails} {{Current HP=${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP}}}`,
185
+ color: characterData.notificationColor
186
+ };
187
+ } else if (actionType === 'temp') {
188
+ // Temp HP: RAW rules - new temp HP replaces old if higher, otherwise keep old
189
+ const newTempHP = amount;
190
+ if (newTempHP > oldTempHP) {
191
+ characterData.temporaryHP = newTempHP;
192
+ showNotification(`🛡️ Gained ${newTempHP} temp HP! (${characterData.hitPoints.current}+${characterData.temporaryHP}/${maxHP})`);
193
+
194
+ messageData = {
195
+ action: 'announceSpell',
196
+ message: `&{template:default} {{name=${colorBanner}${characterData.name} gains temp HP}} {{🛡️ Temp HP=${newTempHP}}} {{Current HP=${characterData.hitPoints.current}+${characterData.temporaryHP}/${maxHP}}}`,
197
+ color: characterData.notificationColor
198
+ };
199
+ } else {
200
+ showNotification(`⚠️ Kept ${oldTempHP} temp HP (higher than ${newTempHP})`);
201
+ modal.remove();
202
+ return; // Don't send message if temp HP wasn't gained
203
+ }
204
+ }
205
+
206
+ // Send message to Roll20
207
+ if (messageData) {
208
+ sendToRoll20(messageData);
209
+ }
210
+
211
+ saveCharacterData();
212
+ buildSheet(characterData);
213
+ modal.remove();
214
+ });
215
+
216
+ // Focus on input
217
+ amountInput.focus();
218
+ amountInput.select();
219
+
220
+ // Allow Enter key to confirm
221
+ amountInput.addEventListener('keypress', (e) => {
222
+ if (e.key === 'Enter') {
223
+ document.getElementById('hp-confirm').click();
224
+ }
225
+ });
226
+
227
+ // Click outside to close
228
+ modal.addEventListener('click', (e) => {
229
+ if (e.target === modal) {
230
+ modal.remove();
231
+ }
232
+ });
233
+ }
234
+
235
+ function showDeathSavesModal() {
236
+ // Create modal overlay
237
+ const modal = document.createElement('div');
238
+ 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;';
239
+
240
+ // Create modal content
241
+ const modalContent = document.createElement('div');
242
+ modalContent.style.cssText = 'background: var(--bg-secondary); color: var(--text-primary); padding: 30px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); min-width: 300px;';
243
+
244
+ // Defensive initialization for death saves
245
+ const deathSaves = characterData.deathSaves || { successes: 0, failures: 0 };
246
+ const successes = deathSaves.successes || 0;
247
+ const failures = deathSaves.failures || 0;
248
+
249
+ modalContent.innerHTML = `
250
+ <h3 style="margin: 0 0 20px 0; color: var(--text-primary); text-align: center;">Death Saves</h3>
251
+ <div style="text-align: center; font-size: 1.2em; margin-bottom: 20px;">
252
+ <div style="margin-bottom: 10px;">
253
+ <span style="color: #27ae60; font-weight: bold;">Successes: ${successes}/3</span>
254
+ </div>
255
+ <div>
256
+ <span style="color: #e74c3c; font-weight: bold;">Failures: ${failures}/3</span>
257
+ </div>
258
+ </div>
259
+
260
+ <div style="margin-bottom: 20px;">
261
+ <button id="roll-death-save" style="width: 100%; padding: 15px; font-size: 1.1em; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; margin-bottom: 15px;">
262
+ 🎲 Roll Death Save
263
+ </button>
264
+ </div>
265
+
266
+ <div style="margin-bottom: 20px; border-top: 1px solid #ecf0f1; padding-top: 20px;">
267
+ <label style="display: block; margin-bottom: 10px; font-weight: bold; color: var(--text-primary);">Manual Adjustment:</label>
268
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;">
269
+ <button id="add-success" style="padding: 10px; background: #27ae60; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
270
+ + Success
271
+ </button>
272
+ <button id="add-failure" style="padding: 10px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
273
+ + Failure
274
+ </button>
275
+ </div>
276
+ <button id="reset-death-saves" style="width: 100%; padding: 10px; background: #95a5a6; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
277
+ Reset All
278
+ </button>
279
+ </div>
280
+
281
+ <button id="close-modal" style="width: 100%; padding: 12px; font-size: 1em; background: #7f8c8d; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
282
+ Close
283
+ </button>
284
+ `;
285
+
286
+ modal.appendChild(modalContent);
287
+ document.body.appendChild(modal);
288
+
289
+ // Roll death save button
290
+ document.getElementById('roll-death-save').addEventListener('click', () => {
291
+ // Roll 1d20 locally to determine outcome
292
+ const rollResult = Math.floor(Math.random() * 20) + 1;
293
+ debug.log(`🎲 Death Save rolled: ${rollResult}`);
294
+
295
+ // Determine outcome based on D&D 5e rules
296
+ let message = '';
297
+ let isSuccess = false;
298
+
299
+ if (rollResult === 20) {
300
+ // Natural 20: regain 1 HP (represented as 2 successes in death saves)
301
+ if (!characterData.deathSaves) characterData.deathSaves = { successes: 0, failures: 0 };
302
+ if (characterData.deathSaves.successes < 3) {
303
+ characterData.deathSaves.successes += 2;
304
+ if (characterData.deathSaves.successes > 3) characterData.deathSaves.successes = 3;
305
+ }
306
+ message = `💚 NAT 20! Death Save Success x2 (${characterData.deathSaves.successes}/3)`;
307
+ isSuccess = true;
308
+ } else if (rollResult === 1) {
309
+ // Natural 1: counts as 2 failures
310
+ if (!characterData.deathSaves) characterData.deathSaves = { successes: 0, failures: 0 };
311
+ if (characterData.deathSaves.failures < 3) {
312
+ characterData.deathSaves.failures += 2;
313
+ if (characterData.deathSaves.failures > 3) characterData.deathSaves.failures = 3;
314
+ }
315
+ message = `💀 NAT 1! Death Save Failure x2 (${characterData.deathSaves.failures}/3)`;
316
+ } else if (rollResult >= 10) {
317
+ // Success
318
+ if (!characterData.deathSaves) characterData.deathSaves = { successes: 0, failures: 0 };
319
+ if (characterData.deathSaves.successes < 3) {
320
+ characterData.deathSaves.successes++;
321
+ }
322
+ message = `✓ Death Save Success (${characterData.deathSaves.successes}/3)`;
323
+ isSuccess = true;
324
+ } else {
325
+ // Failure
326
+ if (!characterData.deathSaves) characterData.deathSaves = { successes: 0, failures: 0 };
327
+ if (characterData.deathSaves.failures < 3) {
328
+ characterData.deathSaves.failures++;
329
+ }
330
+ message = `✗ Death Save Failure (${characterData.deathSaves.failures}/3)`;
331
+ }
332
+
333
+ // Save updated death saves
334
+ saveCharacterData();
335
+ showNotification(message);
336
+
337
+ // Send roll result to Roll20 (show result in name since we rolled locally)
338
+ roll(`Death Save: ${rollResult}`, '1d20', rollResult);
339
+
340
+ // Rebuild sheet to show updated death saves
341
+ buildSheet(characterData);
342
+ modal.remove();
343
+ });
344
+
345
+ // Add success button
346
+ document.getElementById('add-success').addEventListener('click', () => {
347
+ if (!characterData.deathSaves) characterData.deathSaves = { successes: 0, failures: 0 };
348
+ if (characterData.deathSaves.successes < 3) {
349
+ characterData.deathSaves.successes++;
350
+ saveCharacterData();
351
+ showNotification(`✓ Death Save Success (${characterData.deathSaves.successes}/3)`);
352
+ buildSheet(characterData);
353
+ modal.remove();
354
+ }
355
+ });
356
+
357
+ // Add failure button
358
+ document.getElementById('add-failure').addEventListener('click', () => {
359
+ if (!characterData.deathSaves) characterData.deathSaves = { successes: 0, failures: 0 };
360
+ if (characterData.deathSaves.failures < 3) {
361
+ characterData.deathSaves.failures++;
362
+ saveCharacterData();
363
+ showNotification(`✗ Death Save Failure (${characterData.deathSaves.failures}/3)`);
364
+ buildSheet(characterData);
365
+ modal.remove();
366
+ }
367
+ });
368
+
369
+ // Reset button
370
+ document.getElementById('reset-death-saves').addEventListener('click', () => {
371
+ characterData.deathSaves = { successes: 0, failures: 0 };
372
+ saveCharacterData();
373
+ showNotification('♻️ Death saves reset');
374
+ buildSheet(characterData);
375
+ modal.remove();
376
+ });
377
+
378
+ // Close button
379
+ document.getElementById('close-modal').addEventListener('click', () => {
380
+ modal.remove();
381
+ });
382
+
383
+ // Click outside to close
384
+ modal.addEventListener('click', (e) => {
385
+ if (e.target === modal) {
386
+ modal.remove();
387
+ }
388
+ });
389
+ }
390
+
391
+ // ===== EXPORTS =====
392
+
393
+ // Export functions to globalThis
394
+ globalThis.showHPModal = showHPModal;
395
+ globalThis.showDeathSavesModal = showDeathSavesModal;
396
+
397
+ debug.log('✅ Health Modals module loaded');
398
+
399
+ })();