@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,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HP Management Module
|
|
3
|
+
*
|
|
4
|
+
* Handles hit points, temporary HP, healing, damage, and resting.
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - showHPModal()
|
|
9
|
+
* - takeShortRest()
|
|
10
|
+
* - takeLongRest()
|
|
11
|
+
* - getHitDieType()
|
|
12
|
+
* - initializeHitDice()
|
|
13
|
+
* - spendHitDice()
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
(function() {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Show HP adjustment modal (heal, damage, temp HP)
|
|
21
|
+
*/
|
|
22
|
+
function showHPModal() {
|
|
23
|
+
// characterData should be available from global scope
|
|
24
|
+
if (typeof characterData === 'undefined' || !characterData) {
|
|
25
|
+
if (typeof showNotification !== 'undefined') {
|
|
26
|
+
showNotification('❌ Character data not available', 'error');
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create modal overlay
|
|
32
|
+
const modal = document.createElement('div');
|
|
33
|
+
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;';
|
|
34
|
+
|
|
35
|
+
// Create modal content
|
|
36
|
+
const modalContent = document.createElement('div');
|
|
37
|
+
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;';
|
|
38
|
+
|
|
39
|
+
const currentHP = characterData.hitPoints.current;
|
|
40
|
+
const maxHP = characterData.hitPoints.max;
|
|
41
|
+
const tempHP = characterData.temporaryHP || 0;
|
|
42
|
+
|
|
43
|
+
modalContent.innerHTML = `
|
|
44
|
+
<h3 style="margin: 0 0 20px 0; color: var(--text-primary); text-align: center;">Adjust Hit Points</h3>
|
|
45
|
+
<div style="text-align: center; font-size: 1.2em; margin-bottom: 20px; color: var(--text-secondary);">
|
|
46
|
+
Current: <strong>${currentHP}${tempHP > 0 ? `+${tempHP}` : ''} / ${maxHP}</strong>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div style="margin-bottom: 20px;">
|
|
50
|
+
<label style="display: block; margin-bottom: 10px; font-weight: bold; color: var(--text-primary);">Amount:</label>
|
|
51
|
+
<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);">
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div style="margin-bottom: 25px;">
|
|
55
|
+
<label style="display: block; margin-bottom: 10px; font-weight: bold; color: var(--text-primary);">Action:</label>
|
|
56
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;">
|
|
57
|
+
<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;">
|
|
58
|
+
💚 Heal
|
|
59
|
+
</button>
|
|
60
|
+
<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;">
|
|
61
|
+
💔 Damage
|
|
62
|
+
</button>
|
|
63
|
+
<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;">
|
|
64
|
+
🛡️ Temp HP
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div style="display: flex; gap: 10px;">
|
|
70
|
+
<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;">
|
|
71
|
+
Cancel
|
|
72
|
+
</button>
|
|
73
|
+
<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;">
|
|
74
|
+
Confirm
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
modal.appendChild(modalContent);
|
|
80
|
+
document.body.appendChild(modal);
|
|
81
|
+
|
|
82
|
+
// Toggle state: 'heal', 'damage', or 'temp'
|
|
83
|
+
let actionType = 'heal';
|
|
84
|
+
|
|
85
|
+
const healBtn = document.getElementById('hp-toggle-heal');
|
|
86
|
+
const damageBtn = document.getElementById('hp-toggle-damage');
|
|
87
|
+
const tempBtn = document.getElementById('hp-toggle-temp');
|
|
88
|
+
const amountInput = document.getElementById('hp-amount');
|
|
89
|
+
|
|
90
|
+
// Return early if modal elements don't exist
|
|
91
|
+
if (!healBtn || !damageBtn || !tempBtn || !amountInput) {
|
|
92
|
+
debug.warn('⚠️ HP modal elements not found');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Helper function to reset all buttons
|
|
97
|
+
const resetButtons = () => {
|
|
98
|
+
healBtn.style.background = 'var(--bg-tertiary)';
|
|
99
|
+
healBtn.style.color = '#7f8c8d';
|
|
100
|
+
healBtn.style.borderColor = '#bdc3c7';
|
|
101
|
+
damageBtn.style.background = 'var(--bg-tertiary)';
|
|
102
|
+
damageBtn.style.color = '#7f8c8d';
|
|
103
|
+
damageBtn.style.borderColor = '#bdc3c7';
|
|
104
|
+
tempBtn.style.background = 'var(--bg-tertiary)';
|
|
105
|
+
tempBtn.style.color = '#7f8c8d';
|
|
106
|
+
tempBtn.style.borderColor = '#bdc3c7';
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Toggle button handlers
|
|
110
|
+
healBtn.addEventListener('click', () => {
|
|
111
|
+
actionType = 'heal';
|
|
112
|
+
resetButtons();
|
|
113
|
+
healBtn.style.background = '#27ae60';
|
|
114
|
+
healBtn.style.color = 'white';
|
|
115
|
+
healBtn.style.borderColor = '#27ae60';
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
damageBtn.addEventListener('click', () => {
|
|
119
|
+
actionType = 'damage';
|
|
120
|
+
resetButtons();
|
|
121
|
+
damageBtn.style.background = '#e74c3c';
|
|
122
|
+
damageBtn.style.color = 'white';
|
|
123
|
+
damageBtn.style.borderColor = '#e74c3c';
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
tempBtn.addEventListener('click', () => {
|
|
127
|
+
actionType = 'temp';
|
|
128
|
+
resetButtons();
|
|
129
|
+
tempBtn.style.background = '#3498db';
|
|
130
|
+
tempBtn.style.color = 'white';
|
|
131
|
+
tempBtn.style.borderColor = '#3498db';
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Cancel button
|
|
135
|
+
document.getElementById('hp-cancel').addEventListener('click', () => {
|
|
136
|
+
modal.remove();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Confirm button
|
|
140
|
+
document.getElementById('hp-confirm').addEventListener('click', () => {
|
|
141
|
+
const amount = parseInt(amountInput.value);
|
|
142
|
+
|
|
143
|
+
if (isNaN(amount) || amount <= 0) {
|
|
144
|
+
if (typeof showNotification !== 'undefined') {
|
|
145
|
+
showNotification('❌ Please enter a valid amount', 'error');
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const oldHP = characterData.hitPoints.current;
|
|
151
|
+
const oldTempHP = characterData.temporaryHP || 0;
|
|
152
|
+
const colorBanner = typeof getColoredBanner !== 'undefined' ? getColoredBanner(characterData) : '';
|
|
153
|
+
let messageData;
|
|
154
|
+
|
|
155
|
+
if (actionType === 'heal') {
|
|
156
|
+
// Healing: increase current HP (up to max), doesn't affect temp HP (RAW)
|
|
157
|
+
characterData.hitPoints.current = Math.min(currentHP + amount, maxHP);
|
|
158
|
+
const actualHealing = characterData.hitPoints.current - oldHP;
|
|
159
|
+
|
|
160
|
+
// Reset death saves on healing
|
|
161
|
+
if (actualHealing > 0 && characterData.deathSaves && (characterData.deathSaves.successes > 0 || characterData.deathSaves.failures > 0)) {
|
|
162
|
+
characterData.deathSaves.successes = 0;
|
|
163
|
+
characterData.deathSaves.failures = 0;
|
|
164
|
+
debug.log('♻️ Death saves reset due to healing');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof showNotification !== 'undefined') {
|
|
168
|
+
showNotification(`💚 Healed ${actualHealing} HP! (${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP})`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
messageData = {
|
|
172
|
+
action: 'announceSpell',
|
|
173
|
+
message: `&{template:default} {{name=${colorBanner}${characterData.name} regains HP}} {{💚 Healing=${actualHealing} HP}} {{Current HP=${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP}}}`,
|
|
174
|
+
color: characterData.notificationColor
|
|
175
|
+
};
|
|
176
|
+
} else if (actionType === 'damage') {
|
|
177
|
+
// Damage: deplete temp HP first, then current HP (RAW)
|
|
178
|
+
let remainingDamage = amount;
|
|
179
|
+
let tempHPLost = 0;
|
|
180
|
+
let actualDamage = 0;
|
|
181
|
+
|
|
182
|
+
if (characterData.temporaryHP > 0) {
|
|
183
|
+
tempHPLost = Math.min(characterData.temporaryHP, remainingDamage);
|
|
184
|
+
characterData.temporaryHP -= tempHPLost;
|
|
185
|
+
remainingDamage -= tempHPLost;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (remainingDamage > 0) {
|
|
189
|
+
characterData.hitPoints.current = Math.max(currentHP - remainingDamage, 0);
|
|
190
|
+
actualDamage = oldHP - characterData.hitPoints.current;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const damageMsg = tempHPLost > 0
|
|
194
|
+
? `💔 Took ${amount} damage! (${tempHPLost} temp HP${actualDamage > 0 ? ` + ${actualDamage} HP` : ''})`
|
|
195
|
+
: `💔 Took ${actualDamage} damage!`;
|
|
196
|
+
|
|
197
|
+
if (typeof showNotification !== 'undefined') {
|
|
198
|
+
showNotification(`${damageMsg} (${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP})`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const damageDetails = tempHPLost > 0
|
|
202
|
+
? `{{Temp HP Lost=${tempHPLost}}}${actualDamage > 0 ? ` {{HP Lost=${actualDamage}}}` : ''}`
|
|
203
|
+
: `{{HP Lost=${actualDamage}}}`;
|
|
204
|
+
|
|
205
|
+
messageData = {
|
|
206
|
+
action: 'announceSpell',
|
|
207
|
+
message: `&{template:default} {{name=${colorBanner}${characterData.name} takes damage}} {{💔 Total Damage=${amount}}} ${damageDetails} {{Current HP=${characterData.hitPoints.current}${characterData.temporaryHP > 0 ? `+${characterData.temporaryHP}` : ''}/${maxHP}}}`,
|
|
208
|
+
color: characterData.notificationColor
|
|
209
|
+
};
|
|
210
|
+
} else if (actionType === 'temp') {
|
|
211
|
+
// Temp HP: RAW rules - new temp HP replaces old if higher, otherwise keep old
|
|
212
|
+
const newTempHP = amount;
|
|
213
|
+
if (newTempHP > oldTempHP) {
|
|
214
|
+
characterData.temporaryHP = newTempHP;
|
|
215
|
+
if (typeof showNotification !== 'undefined') {
|
|
216
|
+
showNotification(`🛡️ Gained ${newTempHP} temp HP! (${characterData.hitPoints.current}+${characterData.temporaryHP}/${maxHP})`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
messageData = {
|
|
220
|
+
action: 'announceSpell',
|
|
221
|
+
message: `&{template:default} {{name=${colorBanner}${characterData.name} gains temp HP}} {{🛡️ Temp HP=${newTempHP}}} {{Current HP=${characterData.hitPoints.current}+${characterData.temporaryHP}/${maxHP}}}`,
|
|
222
|
+
color: characterData.notificationColor
|
|
223
|
+
};
|
|
224
|
+
} else {
|
|
225
|
+
if (typeof showNotification !== 'undefined') {
|
|
226
|
+
showNotification(`⚠️ Kept ${oldTempHP} temp HP (higher than ${newTempHP})`);
|
|
227
|
+
}
|
|
228
|
+
modal.remove();
|
|
229
|
+
return; // Don't send message if temp HP wasn't gained
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Send message to Roll20
|
|
234
|
+
if (messageData) {
|
|
235
|
+
sendToRoll20(messageData);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Save and rebuild sheet
|
|
239
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
240
|
+
saveCharacterData();
|
|
241
|
+
}
|
|
242
|
+
if (typeof buildSheet !== 'undefined') {
|
|
243
|
+
buildSheet(characterData);
|
|
244
|
+
}
|
|
245
|
+
modal.remove();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Focus on input
|
|
249
|
+
amountInput.focus();
|
|
250
|
+
amountInput.select();
|
|
251
|
+
|
|
252
|
+
// Allow Enter key to confirm
|
|
253
|
+
amountInput.addEventListener('keypress', (e) => {
|
|
254
|
+
if (e.key === 'Enter') {
|
|
255
|
+
document.getElementById('hp-confirm').click();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Click outside to close
|
|
260
|
+
modal.addEventListener('click', (e) => {
|
|
261
|
+
if (e.target === modal) {
|
|
262
|
+
modal.remove();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get hit die type based on character class
|
|
269
|
+
*/
|
|
270
|
+
function getHitDieType() {
|
|
271
|
+
// characterData should be available from global scope
|
|
272
|
+
if (typeof characterData === 'undefined' || !characterData) return 'd8';
|
|
273
|
+
|
|
274
|
+
// Determine hit die based on class (D&D 5e)
|
|
275
|
+
const className = (characterData.class || '').toLowerCase();
|
|
276
|
+
|
|
277
|
+
const hitDiceMap = {
|
|
278
|
+
'barbarian': 'd12',
|
|
279
|
+
'fighter': 'd10',
|
|
280
|
+
'paladin': 'd10',
|
|
281
|
+
'ranger': 'd10',
|
|
282
|
+
'bard': 'd8',
|
|
283
|
+
'cleric': 'd8',
|
|
284
|
+
'druid': 'd8',
|
|
285
|
+
'monk': 'd8',
|
|
286
|
+
'rogue': 'd8',
|
|
287
|
+
'warlock': 'd8',
|
|
288
|
+
'sorcerer': 'd6',
|
|
289
|
+
'wizard': 'd6'
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
for (const [classKey, die] of Object.entries(hitDiceMap)) {
|
|
293
|
+
if (className.includes(classKey)) {
|
|
294
|
+
return die;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Default to d8 if class not found
|
|
299
|
+
return 'd8';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Initialize hit dice if not already set
|
|
304
|
+
*/
|
|
305
|
+
function initializeHitDice() {
|
|
306
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
307
|
+
|
|
308
|
+
// Initialize hit dice if not already set
|
|
309
|
+
if (characterData.hitDice === undefined) {
|
|
310
|
+
const level = characterData.level || 1;
|
|
311
|
+
characterData.hitDice = {
|
|
312
|
+
current: level,
|
|
313
|
+
max: level,
|
|
314
|
+
type: getHitDieType()
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Spend hit dice to restore HP (used during short rest)
|
|
321
|
+
*/
|
|
322
|
+
function spendHitDice() {
|
|
323
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
324
|
+
|
|
325
|
+
initializeHitDice();
|
|
326
|
+
|
|
327
|
+
const conMod = characterData.attributeMods?.constitution || 0;
|
|
328
|
+
const hitDie = characterData.hitDice.type;
|
|
329
|
+
const maxDice = parseInt(hitDie.substring(1)); // Extract number from "d8" -> 8
|
|
330
|
+
|
|
331
|
+
if (characterData.hitDice.current <= 0) {
|
|
332
|
+
alert('You have no Hit Dice remaining to spend!');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let totalHealed = 0;
|
|
337
|
+
let diceSpent = 0;
|
|
338
|
+
|
|
339
|
+
while (characterData.hitDice.current > 0 && characterData.hitPoints.current < characterData.hitPoints.max) {
|
|
340
|
+
const spend = confirm(
|
|
341
|
+
`Spend a Hit Die? (${characterData.hitDice.current}/${characterData.hitDice.max} remaining)\n\n` +
|
|
342
|
+
`Hit Die: ${hitDie}\n` +
|
|
343
|
+
`CON Modifier: ${conMod >= 0 ? '+' : ''}${conMod}\n` +
|
|
344
|
+
`Current HP: ${characterData.hitPoints.current}/${characterData.hitPoints.max}\n` +
|
|
345
|
+
`HP Healed so far: ${totalHealed}`
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (!spend) break;
|
|
349
|
+
|
|
350
|
+
// Roll the hit die
|
|
351
|
+
const roll = Math.floor(Math.random() * maxDice) + 1;
|
|
352
|
+
const healing = Math.max(1, roll + conMod); // Minimum 1 HP restored
|
|
353
|
+
|
|
354
|
+
characterData.hitDice.current--;
|
|
355
|
+
diceSpent++;
|
|
356
|
+
|
|
357
|
+
const oldHP = characterData.hitPoints.current;
|
|
358
|
+
characterData.hitPoints.current = Math.min(
|
|
359
|
+
characterData.hitPoints.current + healing,
|
|
360
|
+
characterData.hitPoints.max
|
|
361
|
+
);
|
|
362
|
+
const actualHealing = characterData.hitPoints.current - oldHP;
|
|
363
|
+
totalHealed += actualHealing;
|
|
364
|
+
|
|
365
|
+
debug.log(`🎲 Rolled ${hitDie}: ${roll} + ${conMod} = ${healing} HP (restored ${actualHealing})`);
|
|
366
|
+
|
|
367
|
+
// Announce the roll with fancy formatting
|
|
368
|
+
const colorBanner = typeof getColoredBanner !== 'undefined' ? getColoredBanner(characterData) : '';
|
|
369
|
+
sendToRoll20({
|
|
370
|
+
action: 'announceSpell',
|
|
371
|
+
message: `&{template:default} {{name=${colorBanner}${characterData.name} spends hit dice}} {{Roll=🎲 ${hitDie}: ${roll} + ${conMod} CON}} {{HP Restored=${healing}}} {{Current HP=${characterData.hitPoints.current}/${characterData.hitPoints.max}}}`,
|
|
372
|
+
color: characterData.notificationColor
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (diceSpent > 0) {
|
|
377
|
+
if (typeof showNotification !== 'undefined') {
|
|
378
|
+
showNotification(`🎲 Spent ${diceSpent} Hit Dice and restored ${totalHealed} HP!`);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
if (typeof showNotification !== 'undefined') {
|
|
382
|
+
showNotification('No Hit Dice spent.');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Announce short rest completion to Roll20
|
|
387
|
+
const colorBanner = typeof getColoredBanner !== 'undefined' ? getColoredBanner(characterData) : '';
|
|
388
|
+
const announcement = `&{template:default} {{name=${colorBanner}${characterData.name} takes a Short Rest!}} {{Type=Short Rest}} {{HP=${characterData.hitPoints.current}/${characterData.hitPoints.max}}}`;
|
|
389
|
+
const messageData = {
|
|
390
|
+
action: 'announceSpell',
|
|
391
|
+
message: announcement,
|
|
392
|
+
color: characterData.notificationColor
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
sendToRoll20(messageData);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Take a short rest - restores some resources and allows spending hit dice
|
|
400
|
+
*/
|
|
401
|
+
function takeShortRest() {
|
|
402
|
+
if (typeof characterData === 'undefined' || !characterData) {
|
|
403
|
+
if (typeof showNotification !== 'undefined') {
|
|
404
|
+
showNotification('❌ Character data not available', 'error');
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const confirmed = confirm('Take a Short Rest?\n\nThis will:\n- Allow you to spend Hit Dice to restore HP\n- Restore Warlock spell slots\n- Restore some class features');
|
|
410
|
+
|
|
411
|
+
if (!confirmed) return;
|
|
412
|
+
|
|
413
|
+
debug.log('☕ Taking short rest...');
|
|
414
|
+
|
|
415
|
+
// Clear temporary HP (RAW: temp HP doesn't persist through rest)
|
|
416
|
+
if (characterData.temporaryHP > 0) {
|
|
417
|
+
characterData.temporaryHP = 0;
|
|
418
|
+
debug.log('✅ Cleared temporary HP');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Note: Inspiration is NOT restored on short rest (DM grants it)
|
|
422
|
+
debug.log(`ℹ️ Inspiration status unchanged (${characterData.inspiration ? 'active' : 'none'})`);
|
|
423
|
+
|
|
424
|
+
// Restore Warlock Pact Magic slots (they recharge on short rest)
|
|
425
|
+
// Check both spellSlots and otherVariables for Pact Magic
|
|
426
|
+
if (characterData.spellSlots && characterData.spellSlots.pactMagicSlotsMax !== undefined) {
|
|
427
|
+
characterData.spellSlots.pactMagicSlots = characterData.spellSlots.pactMagicSlotsMax;
|
|
428
|
+
debug.log(`✅ Restored Pact Magic slots (spellSlots): ${characterData.spellSlots.pactMagicSlots}/${characterData.spellSlots.pactMagicSlotsMax}`);
|
|
429
|
+
}
|
|
430
|
+
if (characterData.otherVariables) {
|
|
431
|
+
if (characterData.otherVariables.pactMagicSlotsMax !== undefined) {
|
|
432
|
+
characterData.otherVariables.pactMagicSlots = characterData.otherVariables.pactMagicSlotsMax;
|
|
433
|
+
debug.log('✅ Restored Pact Magic slots (otherVariables)');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Restore Ki points for Monk (short rest feature)
|
|
437
|
+
if (characterData.otherVariables.kiMax !== undefined) {
|
|
438
|
+
characterData.otherVariables.ki = characterData.otherVariables.kiMax;
|
|
439
|
+
debug.log('✅ Restored Ki points');
|
|
440
|
+
} else if (characterData.otherVariables.kiPointsMax !== undefined) {
|
|
441
|
+
characterData.otherVariables.kiPoints = characterData.otherVariables.kiPointsMax;
|
|
442
|
+
debug.log('✅ Restored Ki points');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Restore Action Surge, Second Wind (short rest features)
|
|
446
|
+
if (characterData.otherVariables.actionSurgeMax !== undefined) {
|
|
447
|
+
characterData.otherVariables.actionSurge = characterData.otherVariables.actionSurgeMax;
|
|
448
|
+
}
|
|
449
|
+
if (characterData.otherVariables.secondWindMax !== undefined) {
|
|
450
|
+
characterData.otherVariables.secondWind = characterData.otherVariables.secondWindMax;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Handle Hit Dice spending for HP restoration
|
|
455
|
+
spendHitDice();
|
|
456
|
+
|
|
457
|
+
// Restore class resources that recharge on short rest
|
|
458
|
+
// Most resources restore on short rest (Ki, Channel Divinity, Action Surge, etc.)
|
|
459
|
+
// Notable exceptions: Sorcery Points and Rage restore on long rest only
|
|
460
|
+
if (characterData.resources && characterData.resources.length > 0) {
|
|
461
|
+
characterData.resources.forEach(resource => {
|
|
462
|
+
const lowerName = resource.name.toLowerCase();
|
|
463
|
+
|
|
464
|
+
// Long rest only resources
|
|
465
|
+
if (lowerName.includes('sorcery') || lowerName.includes('rage')) {
|
|
466
|
+
debug.log(`⏭️ Skipping ${resource.name} (long rest only)`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Restore all other resources
|
|
471
|
+
resource.current = resource.max;
|
|
472
|
+
|
|
473
|
+
// Also update otherVariables to keep data in sync
|
|
474
|
+
if (characterData.otherVariables && resource.varName) {
|
|
475
|
+
characterData.otherVariables[resource.varName] = resource.current;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
debug.log(`✅ Restored ${resource.name} (${resource.current}/${resource.max})`);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Reset limited uses for short rest abilities
|
|
483
|
+
if (characterData.actions) {
|
|
484
|
+
characterData.actions.forEach(action => {
|
|
485
|
+
if (action.uses) {
|
|
486
|
+
// Check if this ability resets on short rest
|
|
487
|
+
// DiceCloud uses 'reset' property with values: 'shortRest', 'longRest', etc.
|
|
488
|
+
const resetType = action.reset || action.uses?.reset;
|
|
489
|
+
const resetsOnShortRest =
|
|
490
|
+
resetType === 'shortRest' ||
|
|
491
|
+
resetType === 'short_rest' ||
|
|
492
|
+
resetType === 'short rest' ||
|
|
493
|
+
resetType === 'shortOrLongRest';
|
|
494
|
+
|
|
495
|
+
if (!resetsOnShortRest) {
|
|
496
|
+
debug.log(`⏭️ Skipping ${action.name} (does not reset on short rest, reset=${resetType})`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Handle usesUsed pattern (older/local data)
|
|
501
|
+
if (action.usesUsed !== undefined && action.usesUsed > 0) {
|
|
502
|
+
action.usesUsed = 0;
|
|
503
|
+
debug.log(`✅ Reset uses for ${action.name}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Handle usesLeft pattern (2024 D&D features, database data)
|
|
507
|
+
if (action.usesLeft !== undefined) {
|
|
508
|
+
const usesTotal = action.uses.total || action.uses.value || action.uses;
|
|
509
|
+
action.usesLeft = usesTotal;
|
|
510
|
+
debug.log(`✅ Restored ${action.name} (${action.usesLeft}/${usesTotal} uses)`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Save and rebuild sheet
|
|
517
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
518
|
+
saveCharacterData();
|
|
519
|
+
}
|
|
520
|
+
if (typeof buildSheet !== 'undefined') {
|
|
521
|
+
buildSheet(characterData);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (typeof showNotification !== 'undefined') {
|
|
525
|
+
showNotification('☕ Short Rest complete! Resources recharged.');
|
|
526
|
+
}
|
|
527
|
+
debug.log('✅ Short rest complete');
|
|
528
|
+
|
|
529
|
+
// Announce to Roll20 with fancy formatting
|
|
530
|
+
const colorBanner = typeof getColoredBanner !== 'undefined' ? getColoredBanner(characterData) : '';
|
|
531
|
+
const messageData = {
|
|
532
|
+
action: 'announceSpell',
|
|
533
|
+
message: `&{template:default} {{name=${colorBanner}${characterData.name} takes a short rest}} {{=☕ Short rest complete. Resources recharged!}}`,
|
|
534
|
+
color: characterData.notificationColor
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
sendToRoll20(messageData);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Take a long rest - fully restores HP, spell slots, and all resources
|
|
542
|
+
*/
|
|
543
|
+
function takeLongRest() {
|
|
544
|
+
if (typeof characterData === 'undefined' || !characterData) {
|
|
545
|
+
if (typeof showNotification !== 'undefined') {
|
|
546
|
+
showNotification('❌ Character data not available', 'error');
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const confirmed = confirm('Take a Long Rest?\n\nThis will:\n- Fully restore HP\n- Restore all spell slots\n- Restore all class features\n- Restore half your hit dice (minimum 1)');
|
|
552
|
+
|
|
553
|
+
if (!confirmed) return;
|
|
554
|
+
|
|
555
|
+
debug.log('🌙 Taking long rest...');
|
|
556
|
+
|
|
557
|
+
// Initialize hit dice if needed
|
|
558
|
+
initializeHitDice();
|
|
559
|
+
|
|
560
|
+
// Restore all HP
|
|
561
|
+
characterData.hitPoints.current = characterData.hitPoints.max;
|
|
562
|
+
debug.log('✅ Restored HP to max');
|
|
563
|
+
|
|
564
|
+
// Clear temporary HP (RAW: temp HP doesn't persist through rest)
|
|
565
|
+
if (characterData.temporaryHP > 0) {
|
|
566
|
+
characterData.temporaryHP = 0;
|
|
567
|
+
debug.log('✅ Cleared temporary HP');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Note: Inspiration is NOT automatically restored on long rest
|
|
571
|
+
// It must be granted by the DM, so we don't touch it here
|
|
572
|
+
debug.log(`ℹ️ Inspiration status unchanged (${characterData.inspiration ? 'active' : 'none'})`);
|
|
573
|
+
|
|
574
|
+
// Restore hit dice (half of max, minimum 1)
|
|
575
|
+
const hitDiceRestored = Math.max(1, Math.floor(characterData.hitDice.max / 2));
|
|
576
|
+
const oldHitDice = characterData.hitDice.current;
|
|
577
|
+
characterData.hitDice.current = Math.min(
|
|
578
|
+
characterData.hitDice.current + hitDiceRestored,
|
|
579
|
+
characterData.hitDice.max
|
|
580
|
+
);
|
|
581
|
+
debug.log(`✅ Restored ${characterData.hitDice.current - oldHitDice} hit dice (${characterData.hitDice.current}/${characterData.hitDice.max})`);
|
|
582
|
+
|
|
583
|
+
// Restore all spell slots
|
|
584
|
+
if (characterData.spellSlots) {
|
|
585
|
+
// Restore regular spell slots (levels 1-9)
|
|
586
|
+
// Supports flat keys (level1SpellSlotsMax) and nested format ({ level1: { current, max } })
|
|
587
|
+
for (let level = 1; level <= 9; level++) {
|
|
588
|
+
const slotVar = `level${level}SpellSlots`;
|
|
589
|
+
const slotMaxVar = `level${level}SpellSlotsMax`;
|
|
590
|
+
const nested = characterData.spellSlots[`level${level}`];
|
|
591
|
+
|
|
592
|
+
// Restore BOTH formats independently. After buildSheet normalizes nested
|
|
593
|
+
// -> flat, the data carries both at once; the display and casting read the
|
|
594
|
+
// flat key, so an `else if` here (restoring only nested) left the flat key
|
|
595
|
+
// stale and slots appeared un-restored after a long rest.
|
|
596
|
+
if (nested && nested.max !== undefined) {
|
|
597
|
+
nested.current = nested.max;
|
|
598
|
+
}
|
|
599
|
+
if (characterData.spellSlots[slotMaxVar] !== undefined) {
|
|
600
|
+
characterData.spellSlots[slotVar] = characterData.spellSlots[slotMaxVar];
|
|
601
|
+
debug.log(`✅ Restored level ${level} spell slots`);
|
|
602
|
+
} else if (nested && nested.max !== undefined) {
|
|
603
|
+
// No flat max key yet — derive flat from nested so the display updates.
|
|
604
|
+
characterData.spellSlots[slotVar] = nested.max;
|
|
605
|
+
characterData.spellSlots[slotMaxVar] = nested.max;
|
|
606
|
+
debug.log(`✅ Restored level ${level} spell slots (from nested)`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Also restore Pact Magic slots (Warlock)
|
|
611
|
+
if (characterData.spellSlots.pactMagicSlotsMax !== undefined) {
|
|
612
|
+
characterData.spellSlots.pactMagicSlots = characterData.spellSlots.pactMagicSlotsMax;
|
|
613
|
+
debug.log(`✅ Restored Pact Magic slots: ${characterData.spellSlots.pactMagicSlots}/${characterData.spellSlots.pactMagicSlotsMax}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Restore all class resources (Ki, Sorcery Points, Rage, etc.)
|
|
618
|
+
if (characterData.resources && characterData.resources.length > 0) {
|
|
619
|
+
characterData.resources.forEach(resource => {
|
|
620
|
+
resource.current = resource.max;
|
|
621
|
+
|
|
622
|
+
// Also update otherVariables to keep data in sync
|
|
623
|
+
if (characterData.otherVariables && resource.varName) {
|
|
624
|
+
characterData.otherVariables[resource.varName] = resource.current;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
debug.log(`✅ Restored ${resource.name} (${resource.current}/${resource.max})`);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Restore all class resources
|
|
632
|
+
if (characterData.otherVariables) {
|
|
633
|
+
Object.keys(characterData.otherVariables).forEach(key => {
|
|
634
|
+
// If there's a Max variant, restore to max
|
|
635
|
+
if (key.endsWith('Max')) {
|
|
636
|
+
const baseKey = key.replace('Max', '');
|
|
637
|
+
if (characterData.otherVariables[baseKey] !== undefined) {
|
|
638
|
+
characterData.otherVariables[baseKey] = characterData.otherVariables[key];
|
|
639
|
+
debug.log(`✅ Restored ${baseKey}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// Also restore specific resources that might not follow the Max pattern
|
|
645
|
+
if (characterData.otherVariables.kiMax !== undefined) {
|
|
646
|
+
characterData.otherVariables.ki = characterData.otherVariables.kiMax;
|
|
647
|
+
} else if (characterData.otherVariables.kiPointsMax !== undefined) {
|
|
648
|
+
characterData.otherVariables.kiPoints = characterData.otherVariables.kiPointsMax;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (characterData.otherVariables.sorceryPointsMax !== undefined) {
|
|
652
|
+
characterData.otherVariables.sorceryPoints = characterData.otherVariables.sorceryPointsMax;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (characterData.otherVariables.pactMagicSlotsMax !== undefined) {
|
|
656
|
+
characterData.otherVariables.pactMagicSlots = characterData.otherVariables.pactMagicSlotsMax;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Restore Channel Divinity (try all possible variable names)
|
|
660
|
+
if (characterData.otherVariables.channelDivinityClericMax !== undefined) {
|
|
661
|
+
characterData.otherVariables.channelDivinityCleric = characterData.otherVariables.channelDivinityClericMax;
|
|
662
|
+
} else if (characterData.otherVariables.channelDivinityPaladinMax !== undefined) {
|
|
663
|
+
characterData.otherVariables.channelDivinityPaladin = characterData.otherVariables.channelDivinityPaladinMax;
|
|
664
|
+
} else if (characterData.otherVariables.channelDivinityMax !== undefined) {
|
|
665
|
+
characterData.otherVariables.channelDivinity = characterData.otherVariables.channelDivinityMax;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Reset limited uses for long rest abilities
|
|
670
|
+
if (characterData.actions) {
|
|
671
|
+
characterData.actions.forEach(action => {
|
|
672
|
+
if (action.uses) {
|
|
673
|
+
// Check if this ability resets on long rest
|
|
674
|
+
// DiceCloud uses 'reset' property with values: 'shortRest', 'longRest', 'special', etc.
|
|
675
|
+
const resetType = action.reset || action.uses?.reset;
|
|
676
|
+
|
|
677
|
+
// Long rest resets both short rest and long rest abilities, but NOT special reset abilities
|
|
678
|
+
const resetsOnLongRest =
|
|
679
|
+
resetType === 'longRest' ||
|
|
680
|
+
resetType === 'long_rest' ||
|
|
681
|
+
resetType === 'long rest' ||
|
|
682
|
+
resetType === 'shortRest' ||
|
|
683
|
+
resetType === 'short_rest' ||
|
|
684
|
+
resetType === 'short rest' ||
|
|
685
|
+
resetType === 'shortOrLongRest';
|
|
686
|
+
|
|
687
|
+
// Check for special reset conditions (like Feline Agility which resets when not moving)
|
|
688
|
+
const isSpecialReset =
|
|
689
|
+
resetType === 'special' ||
|
|
690
|
+
resetType === 'custom' ||
|
|
691
|
+
(typeof resetType === 'string' && resetType.toLowerCase().includes('agility'));
|
|
692
|
+
|
|
693
|
+
if (isSpecialReset) {
|
|
694
|
+
debug.log(`⏭️ Skipping ${action.name} (special reset condition, reset=${resetType})`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!resetsOnLongRest) {
|
|
699
|
+
debug.log(`⏭️ Skipping ${action.name} (does not reset on long rest, reset=${resetType})`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Handle usesUsed pattern (older/local data)
|
|
704
|
+
if (action.usesUsed !== undefined && action.usesUsed > 0) {
|
|
705
|
+
action.usesUsed = 0;
|
|
706
|
+
debug.log(`✅ Reset uses for ${action.name}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Handle usesLeft pattern (2024 D&D features, database data)
|
|
710
|
+
if (action.usesLeft !== undefined) {
|
|
711
|
+
const usesTotal = action.uses.total || action.uses.value || action.uses;
|
|
712
|
+
action.usesLeft = usesTotal;
|
|
713
|
+
debug.log(`✅ Restored ${action.name} (${action.usesLeft}/${usesTotal} uses)`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Save and rebuild sheet
|
|
720
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
721
|
+
saveCharacterData();
|
|
722
|
+
}
|
|
723
|
+
if (typeof buildSheet !== 'undefined') {
|
|
724
|
+
buildSheet(characterData);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (typeof showNotification !== 'undefined') {
|
|
728
|
+
showNotification('🌙 Long Rest complete! All resources restored.');
|
|
729
|
+
}
|
|
730
|
+
debug.log('✅ Long rest complete');
|
|
731
|
+
|
|
732
|
+
// Announce to Roll20 with fancy formatting
|
|
733
|
+
const colorBanner = typeof getColoredBanner !== 'undefined' ? getColoredBanner(characterData) : '';
|
|
734
|
+
const messageData = {
|
|
735
|
+
action: 'announceSpell',
|
|
736
|
+
message: `&{template:default} {{name=${colorBanner}${characterData.name} takes a long rest}} {{=🌙 Long rest complete!}} {{HP=${characterData.hitPoints.current}/${characterData.hitPoints.max} (Fully Restored)}} {{=All spell slots and resources restored!}}`,
|
|
737
|
+
color: characterData.notificationColor
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
sendToRoll20(messageData);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ===== EXPORTS =====
|
|
744
|
+
|
|
745
|
+
globalThis.showHPModal = showHPModal;
|
|
746
|
+
globalThis.takeShortRest = takeShortRest;
|
|
747
|
+
globalThis.takeLongRest = takeLongRest;
|
|
748
|
+
globalThis.getHitDieType = getHitDieType;
|
|
749
|
+
globalThis.initializeHitDice = initializeHitDice;
|
|
750
|
+
globalThis.spendHitDice = spendHitDice;
|
|
751
|
+
|
|
752
|
+
})();
|