@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.
- package/dist/cache/CacheManager.d.ts.map +1 -0
- package/dist/cache/CacheManager.js +131 -0
- package/dist/cache/CacheManager.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/index.d.ts +11 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +9 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/normalize.d.ts +10 -0
- package/dist/ir/normalize.d.ts.map +1 -0
- package/dist/ir/normalize.js +207 -0
- package/dist/ir/normalize.js.map +1 -0
- package/dist/ir/persistence.d.ts +26 -0
- package/dist/ir/persistence.d.ts.map +1 -0
- package/dist/ir/persistence.js +21 -0
- package/dist/ir/persistence.js.map +1 -0
- package/dist/ir/sync.d.ts +12 -0
- package/dist/ir/sync.d.ts.map +1 -0
- package/dist/ir/sync.js +36 -0
- package/dist/ir/sync.js.map +1 -0
- package/dist/ir/types.d.ts +143 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/ir/types.js +13 -0
- package/dist/ir/types.js.map +1 -0
- package/dist/ir/views/dnd5e.d.ts +40 -0
- package/dist/ir/views/dnd5e.d.ts.map +1 -0
- package/dist/ir/views/dnd5e.js +50 -0
- package/dist/ir/views/dnd5e.js.map +1 -0
- package/dist/render/character.d.ts +19 -0
- package/dist/render/character.d.ts.map +1 -0
- package/dist/render/character.js +156 -0
- package/dist/render/character.js.map +1 -0
- package/dist/render/h.d.ts +27 -0
- package/dist/render/h.d.ts.map +1 -0
- package/dist/render/h.js +64 -0
- package/dist/render/h.js.map +1 -0
- package/dist/render/index.d.ts +11 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +8 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/mount.d.ts +31 -0
- package/dist/render/mount.d.ts.map +1 -0
- package/dist/render/mount.js +63 -0
- package/dist/render/mount.js.map +1 -0
- package/dist/supabase/fields.d.ts.map +1 -0
- package/dist/supabase/fields.js +120 -0
- package/dist/supabase/fields.js.map +1 -0
- package/dist/types/character.d.ts.map +1 -0
- package/dist/types/character.js +5 -0
- package/dist/types/character.js.map +1 -0
- package/package.json +73 -0
- package/src/browser.js +51 -0
- package/src/cache/CacheManager.ts +174 -0
- package/src/common/browser-polyfill.js +319 -0
- package/src/common/debug.js +123 -0
- package/src/common/html-utils.js +134 -0
- package/src/common/theme-manager.js +265 -0
- package/src/index.ts +25 -0
- package/src/ir/__fixtures__/dnd5e-character.json +75962 -0
- package/src/ir/__fixtures__/non-dnd-character.json +14218 -0
- package/src/ir/index.ts +10 -0
- package/src/ir/normalize.ts +245 -0
- package/src/ir/persistence.ts +37 -0
- package/src/ir/sync.ts +49 -0
- package/src/ir/types.ts +161 -0
- package/src/ir/views/dnd5e.ts +94 -0
- package/src/lib/indexeddb-cache.js +320 -0
- package/src/modules/action-announcements.js +102 -0
- package/src/modules/action-display.js +1557 -0
- package/src/modules/action-executor.js +860 -0
- package/src/modules/action-filters.js +167 -0
- package/src/modules/action-options.js +117 -0
- package/src/modules/card-creator.js +142 -0
- package/src/modules/character-portrait.js +169 -0
- package/src/modules/character-trait-popups.js +959 -0
- package/src/modules/character-traits.js +814 -0
- package/src/modules/class-feature-edge-cases.js +1320 -0
- package/src/modules/color-utils.js +69 -0
- package/src/modules/combat-maneuver-edge-cases.js +660 -0
- package/src/modules/companions-manager.js +178 -0
- package/src/modules/concentration-tracker.js +178 -0
- package/src/modules/data-manager.js +514 -0
- package/src/modules/dice-roller.js +719 -0
- package/src/modules/effects-manager.js +743 -0
- package/src/modules/feature-modals.js +1264 -0
- package/src/modules/formula-resolver.js +444 -0
- package/src/modules/gm-mode.js +184 -0
- package/src/modules/health-modals.js +399 -0
- package/src/modules/hp-management.js +752 -0
- package/src/modules/inventory-manager.js +242 -0
- package/src/modules/macro-system.js +825 -0
- package/src/modules/notification-system.js +92 -0
- package/src/modules/racial-feature-edge-cases.js +746 -0
- package/src/modules/resource-manager.js +775 -0
- package/src/modules/sheet-builder.js +654 -0
- package/src/modules/spell-action-modals.js +583 -0
- package/src/modules/spell-cards.js +602 -0
- package/src/modules/spell-casting.js +723 -0
- package/src/modules/spell-display.js +314 -0
- package/src/modules/spell-edge-cases.js +509 -0
- package/src/modules/spell-macros.js +201 -0
- package/src/modules/spell-modals.js +1221 -0
- package/src/modules/spell-slots.js +224 -0
- package/src/modules/status-bar-bridge.js +101 -0
- package/src/modules/ui-utilities.js +284 -0
- package/src/modules/warlock-invocations.js +219 -0
- package/src/modules/window-management.js +211 -0
- package/src/render/character.ts +234 -0
- package/src/render/h.ts +74 -0
- package/src/render/index.ts +10 -0
- package/src/render/mount.ts +94 -0
- package/src/supabase/client.js +1383 -0
- package/src/supabase/config.js +60 -0
- package/src/supabase/fields.ts +129 -0
- 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
|
+
})();
|