@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,1264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Modals Module
|
|
3
|
+
*
|
|
4
|
+
* Handles special feature modals (Inspiration, Divine Smite, Lay on Hands, Lucky).
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - toggleInspiration()
|
|
9
|
+
* - showGainInspirationModal()
|
|
10
|
+
* - showUseInspirationModal()
|
|
11
|
+
* - showDivineSmiteModal(spell)
|
|
12
|
+
* - showLayOnHandsModal(layOnHandsPool)
|
|
13
|
+
* - showLuckyModal()
|
|
14
|
+
* - rollLuckyDie(type)
|
|
15
|
+
* - getLuckyResource()
|
|
16
|
+
* - useLuckyPoint()
|
|
17
|
+
* - getLayOnHandsResource()
|
|
18
|
+
* - createThemedModal()
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
(function() {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// ===== HELPER FUNCTIONS =====
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a theme-aware modal
|
|
28
|
+
*/
|
|
29
|
+
function createThemedModal() {
|
|
30
|
+
const modal = document.createElement('div');
|
|
31
|
+
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;';
|
|
32
|
+
|
|
33
|
+
const modalContent = document.createElement('div');
|
|
34
|
+
modalContent.className = 'owlcloud-modal-content';
|
|
35
|
+
|
|
36
|
+
// Check for system theme preference
|
|
37
|
+
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
38
|
+
const isDarkTheme = document.body.classList.contains('dark-theme') ||
|
|
39
|
+
document.body.classList.contains('theme-dark') ||
|
|
40
|
+
prefersDark;
|
|
41
|
+
|
|
42
|
+
// Apply theme class
|
|
43
|
+
if (isDarkTheme) {
|
|
44
|
+
modalContent.classList.add('theme-dark');
|
|
45
|
+
} else {
|
|
46
|
+
modalContent.classList.add('theme-light');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Base styling (theme-specific colors will be in CSS)
|
|
50
|
+
modalContent.style.cssText = 'padding: 30px; border-radius: 12px; max-width: 500px; width: 90%; text-align: center; box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 1px solid #e1e8ed;';
|
|
51
|
+
|
|
52
|
+
return { modal, modalContent, isDarkTheme };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get Lay on Hands resource from character data
|
|
57
|
+
*/
|
|
58
|
+
function getLayOnHandsResource() {
|
|
59
|
+
// Requires characterData to be available from global scope
|
|
60
|
+
if (typeof characterData === 'undefined' || !characterData || !characterData.resources) return null;
|
|
61
|
+
|
|
62
|
+
// Find Lay on Hands pool in resources
|
|
63
|
+
const layOnHandsResource = characterData.resources.find(r => {
|
|
64
|
+
const lowerName = r.name.toLowerCase();
|
|
65
|
+
return lowerName.includes('lay on hands') || lowerName === 'lay on hands pool';
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return layOnHandsResource || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get Lucky resource from character data
|
|
73
|
+
*/
|
|
74
|
+
function getLuckyResource() {
|
|
75
|
+
// Requires characterData to be available from global scope
|
|
76
|
+
if (typeof characterData === 'undefined' || !characterData || !characterData.resources) {
|
|
77
|
+
debug.log('🎖️ No characterData or resources for Lucky detection');
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Find Lucky points in resources (flexible matching)
|
|
82
|
+
const luckyResource = characterData.resources.find(r => {
|
|
83
|
+
const lowerName = r.name.toLowerCase().trim();
|
|
84
|
+
return (
|
|
85
|
+
lowerName.includes('lucky point') ||
|
|
86
|
+
lowerName.includes('luck point') ||
|
|
87
|
+
lowerName === 'lucky points' ||
|
|
88
|
+
lowerName === 'lucky'
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (luckyResource) {
|
|
93
|
+
debug.log(`🎖️ Found Lucky resource: ${luckyResource.name} (${luckyResource.current}/${luckyResource.max})`);
|
|
94
|
+
} else {
|
|
95
|
+
debug.log('🎖️ No Lucky resource found in character data');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return luckyResource;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Use a Lucky point
|
|
103
|
+
*/
|
|
104
|
+
function useLuckyPoint() {
|
|
105
|
+
debug.log('🎖️ useLuckyPoint called');
|
|
106
|
+
const luckyResource = getLuckyResource();
|
|
107
|
+
debug.log('🎖️ Lucky resource found:', luckyResource);
|
|
108
|
+
|
|
109
|
+
if (!luckyResource) {
|
|
110
|
+
debug.error('❌ No Lucky resource found');
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (luckyResource.current <= 0) {
|
|
115
|
+
debug.error(`❌ No Lucky points available (current: ${luckyResource.current})`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Decrement Lucky points
|
|
120
|
+
const oldCurrent = luckyResource.current;
|
|
121
|
+
luckyResource.current--;
|
|
122
|
+
debug.log(`🎖️ Used Lucky point. ${oldCurrent} → ${luckyResource.current}/${luckyResource.max}`);
|
|
123
|
+
|
|
124
|
+
// Save character data to preserve state when switching characters
|
|
125
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
126
|
+
saveCharacterData();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Update display (buildResourcesDisplay and updateLuckyButtonText should be available from global scope)
|
|
130
|
+
if (typeof buildResourcesDisplay !== 'undefined') {
|
|
131
|
+
buildResourcesDisplay();
|
|
132
|
+
}
|
|
133
|
+
if (typeof updateLuckyButtonText !== 'undefined') {
|
|
134
|
+
updateLuckyButtonText();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
debug.log('🎖️ Lucky button updated and character data saved');
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ===== INSPIRATION MODALS =====
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Toggle inspiration (gain or use based on current state)
|
|
146
|
+
*/
|
|
147
|
+
function toggleInspiration() {
|
|
148
|
+
// Requires characterData to be available from global scope
|
|
149
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
150
|
+
|
|
151
|
+
if (!characterData.inspiration) {
|
|
152
|
+
// Show modal to gain inspiration
|
|
153
|
+
showGainInspirationModal();
|
|
154
|
+
} else {
|
|
155
|
+
// Show modal to choose how to use inspiration (2014 vs 2024)
|
|
156
|
+
showUseInspirationModal();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Show modal for gaining inspiration
|
|
162
|
+
*/
|
|
163
|
+
function showGainInspirationModal() {
|
|
164
|
+
// Requires characterData to be available from global scope
|
|
165
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
166
|
+
|
|
167
|
+
// Create modal overlay
|
|
168
|
+
const modal = document.createElement('div');
|
|
169
|
+
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;';
|
|
170
|
+
|
|
171
|
+
// Create modal content
|
|
172
|
+
const modalContent = document.createElement('div');
|
|
173
|
+
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: 350px; max-width: 450px;';
|
|
174
|
+
|
|
175
|
+
modalContent.innerHTML = `
|
|
176
|
+
<h3 style="margin: 0 0 20px 0; color: var(--text-primary); text-align: center;">⭐ Gain Inspiration</h3>
|
|
177
|
+
<p style="text-align: center; margin-bottom: 25px; color: var(--text-secondary);">
|
|
178
|
+
You're about to gain Inspiration! This can be used for:
|
|
179
|
+
</p>
|
|
180
|
+
<div style="margin-bottom: 25px; padding: 15px; background: var(--bg-tertiary); border-radius: 8px;">
|
|
181
|
+
<div style="margin-bottom: 12px;">
|
|
182
|
+
<strong style="color: #3498db;">📖 D&D 2014:</strong> Gain advantage on an attack roll, saving throw, or ability check
|
|
183
|
+
</div>
|
|
184
|
+
<div>
|
|
185
|
+
<strong style="color: #e74c3c;">📖 D&D 2024:</strong> Reroll any die immediately after rolling it
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
189
|
+
<button id="gain-inspiration" style="padding: 15px; background: #27ae60; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
|
|
190
|
+
⭐ Gain It
|
|
191
|
+
</button>
|
|
192
|
+
<button id="cancel-modal" style="padding: 15px; background: #95a5a6; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
|
|
193
|
+
Cancel
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
modal.appendChild(modalContent);
|
|
199
|
+
document.body.appendChild(modal);
|
|
200
|
+
|
|
201
|
+
// Gain inspiration button
|
|
202
|
+
document.getElementById('gain-inspiration').addEventListener('click', () => {
|
|
203
|
+
characterData.inspiration = true;
|
|
204
|
+
const emoji = '⭐';
|
|
205
|
+
|
|
206
|
+
debug.log(`${emoji} Inspiration gained`);
|
|
207
|
+
if (typeof showNotification !== 'undefined') {
|
|
208
|
+
showNotification(`${emoji} Inspiration gained!`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// TODO: Add Owlbear Rodeo integration for inspiration announcements
|
|
212
|
+
|
|
213
|
+
if (typeof saveCharacterData !== 'undefined') saveCharacterData();
|
|
214
|
+
if (typeof buildSheet !== 'undefined') buildSheet(characterData);
|
|
215
|
+
modal.remove();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Cancel button
|
|
219
|
+
document.getElementById('cancel-modal').addEventListener('click', () => {
|
|
220
|
+
modal.remove();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Click outside to close
|
|
224
|
+
modal.addEventListener('click', (e) => {
|
|
225
|
+
if (e.target === modal) {
|
|
226
|
+
modal.remove();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Show modal for using inspiration (2014 vs 2024 rules)
|
|
233
|
+
*/
|
|
234
|
+
function showUseInspirationModal() {
|
|
235
|
+
// Requires characterData to be available from global scope
|
|
236
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
237
|
+
|
|
238
|
+
// Create modal overlay
|
|
239
|
+
const modal = document.createElement('div');
|
|
240
|
+
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;';
|
|
241
|
+
|
|
242
|
+
// Create modal content
|
|
243
|
+
const modalContent = document.createElement('div');
|
|
244
|
+
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: 400px; max-width: 500px;';
|
|
245
|
+
|
|
246
|
+
const lastRollInfo = characterData.lastRoll
|
|
247
|
+
? `<div style="margin-bottom: 20px; padding: 12px; background: var(--bg-tertiary); border-left: 4px solid #27ae60; border-radius: 4px;">
|
|
248
|
+
<strong style="color: var(--text-primary);">Last Roll:</strong> <span style="color: var(--text-secondary);">${characterData.lastRoll.name}</span>
|
|
249
|
+
</div>`
|
|
250
|
+
: `<div style="margin-bottom: 20px; padding: 12px; background: var(--bg-tertiary); border-left: 4px solid #e74c3c; border-radius: 4px;">
|
|
251
|
+
<strong style="color: var(--text-primary);">⚠️ No previous roll to reroll</strong>
|
|
252
|
+
</div>`;
|
|
253
|
+
|
|
254
|
+
modalContent.innerHTML = `
|
|
255
|
+
<h3 style="margin: 0 0 20px 0; color: var(--text-primary); text-align: center;">✨ Use Inspiration</h3>
|
|
256
|
+
<p style="text-align: center; margin-bottom: 20px; color: var(--text-secondary);">
|
|
257
|
+
How do you want to use your Inspiration?
|
|
258
|
+
</p>
|
|
259
|
+
${lastRollInfo}
|
|
260
|
+
<div style="display: grid; gap: 12px; margin-bottom: 20px;">
|
|
261
|
+
<button id="use-2014" style="padding: 18px; background: #3498db; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; text-align: left;">
|
|
262
|
+
<div style="font-size: 1.1em; margin-bottom: 5px;">📖 D&D 2014 - Advantage</div>
|
|
263
|
+
<div style="font-size: 0.85em; opacity: 0.9;">Gain advantage on your next attack roll, saving throw, or ability check</div>
|
|
264
|
+
</button>
|
|
265
|
+
<button id="use-2024" ${!characterData.lastRoll ? 'disabled' : ''} style="padding: 18px; background: ${!characterData.lastRoll ? '#95a5a6' : '#e74c3c'}; color: white; border: none; border-radius: 8px; cursor: ${!characterData.lastRoll ? 'not-allowed' : 'pointer'}; font-weight: bold; text-align: left;">
|
|
266
|
+
<div style="font-size: 1.1em; margin-bottom: 5px;">📖 D&D 2024 - Reroll</div>
|
|
267
|
+
<div style="font-size: 0.85em; opacity: 0.9;">Reroll your last roll and use the new result</div>
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
<button id="cancel-use-modal" style="width: 100%; padding: 12px; background: #7f8c8d; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
|
|
271
|
+
Cancel
|
|
272
|
+
</button>
|
|
273
|
+
`;
|
|
274
|
+
|
|
275
|
+
modal.appendChild(modalContent);
|
|
276
|
+
document.body.appendChild(modal);
|
|
277
|
+
|
|
278
|
+
// 2014 Advantage button
|
|
279
|
+
document.getElementById('use-2014').addEventListener('click', () => {
|
|
280
|
+
characterData.inspiration = false;
|
|
281
|
+
const emoji = '✨';
|
|
282
|
+
|
|
283
|
+
debug.log(`${emoji} Inspiration spent (2014 - Advantage)`);
|
|
284
|
+
if (typeof showNotification !== 'undefined') {
|
|
285
|
+
showNotification(`${emoji} Inspiration used! Gain advantage on your next roll.`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// TODO: Add Owlbear Rodeo integration for inspiration usage announcements
|
|
289
|
+
|
|
290
|
+
if (typeof saveCharacterData !== 'undefined') saveCharacterData();
|
|
291
|
+
if (typeof buildSheet !== 'undefined') buildSheet(characterData);
|
|
292
|
+
modal.remove();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 2024 Reroll button
|
|
296
|
+
if (characterData.lastRoll) {
|
|
297
|
+
document.getElementById('use-2024').addEventListener('click', () => {
|
|
298
|
+
characterData.inspiration = false;
|
|
299
|
+
const emoji = '✨';
|
|
300
|
+
|
|
301
|
+
debug.log(`${emoji} Inspiration spent (2024 - Reroll): ${characterData.lastRoll.name}`);
|
|
302
|
+
if (typeof showNotification !== 'undefined') {
|
|
303
|
+
showNotification(`${emoji} Inspiration used! Rerolling ${characterData.lastRoll.name}...`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// TODO: Add Owlbear Rodeo integration for inspiration reroll announcements
|
|
307
|
+
|
|
308
|
+
// Trigger reroll
|
|
309
|
+
if (typeof roll !== 'undefined' && characterData.lastRoll) {
|
|
310
|
+
roll(characterData.lastRoll.name, characterData.lastRoll.formula);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (typeof saveCharacterData !== 'undefined') saveCharacterData();
|
|
314
|
+
if (typeof buildSheet !== 'undefined') buildSheet(characterData);
|
|
315
|
+
modal.remove();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Cancel button
|
|
320
|
+
document.getElementById('cancel-use-modal').addEventListener('click', () => {
|
|
321
|
+
modal.remove();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Click outside to close
|
|
325
|
+
modal.addEventListener('click', (e) => {
|
|
326
|
+
if (e.target === modal) {
|
|
327
|
+
modal.remove();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ===== DIVINE SMITE MODAL =====
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Show Divine Smite modal for selecting spell slot and modifiers
|
|
336
|
+
*/
|
|
337
|
+
function showDivineSmiteModal(spell) {
|
|
338
|
+
// Requires characterData to be available from global scope
|
|
339
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
340
|
+
|
|
341
|
+
// Get all available spell slots (like upcast modal)
|
|
342
|
+
const availableSlots = [];
|
|
343
|
+
|
|
344
|
+
// Helper to extract numeric value from DiceCloud objects
|
|
345
|
+
const extractNum = (val) => {
|
|
346
|
+
if (val === null || val === undefined) return 0;
|
|
347
|
+
if (typeof val === 'number') return val;
|
|
348
|
+
if (typeof val === 'object') {
|
|
349
|
+
return val.value ?? val.total ?? val.currentValue ?? 0;
|
|
350
|
+
}
|
|
351
|
+
return parseInt(val) || 0;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Check for Pact Magic slots (Warlock) - these are SEPARATE from regular spell slots
|
|
355
|
+
const rawPactLevel = characterData.spellSlots?.pactMagicSlotLevel ||
|
|
356
|
+
characterData.otherVariables?.pactMagicSlotLevel ||
|
|
357
|
+
characterData.otherVariables?.pactSlotLevelVisible ||
|
|
358
|
+
characterData.otherVariables?.pactSlotLevel;
|
|
359
|
+
const rawPactSlots = characterData.spellSlots?.pactMagicSlots ??
|
|
360
|
+
characterData.otherVariables?.pactMagicSlots ??
|
|
361
|
+
characterData.otherVariables?.pactSlot;
|
|
362
|
+
const rawPactSlotsMax = characterData.spellSlots?.pactMagicSlotsMax ??
|
|
363
|
+
characterData.otherVariables?.pactMagicSlotsMax;
|
|
364
|
+
|
|
365
|
+
// Extract numeric values (DiceCloud stores these as objects like {value: 2})
|
|
366
|
+
const pactMagicSlots = extractNum(rawPactSlots);
|
|
367
|
+
const pactMagicSlotsMax = extractNum(rawPactSlotsMax);
|
|
368
|
+
const effectivePactLevel = extractNum(rawPactLevel) || (pactMagicSlotsMax > 0 ? 5 : 0);
|
|
369
|
+
|
|
370
|
+
debug.log('🔮 Pact Magic detection for Divine Smite:', { rawPactLevel, rawPactSlots, rawPactSlotsMax, pactMagicSlots, pactMagicSlotsMax, effectivePactLevel });
|
|
371
|
+
|
|
372
|
+
// Add Pact Magic slots first if available
|
|
373
|
+
if (pactMagicSlotsMax > 0) {
|
|
374
|
+
availableSlots.push({
|
|
375
|
+
level: effectivePactLevel,
|
|
376
|
+
current: pactMagicSlots,
|
|
377
|
+
max: pactMagicSlotsMax,
|
|
378
|
+
slotVar: 'pactMagicSlots',
|
|
379
|
+
slotMaxVar: 'pactMagicSlotsMax',
|
|
380
|
+
isPactMagic: true,
|
|
381
|
+
label: `Level ${effectivePactLevel} - Pact Magic (${pactMagicSlots}/${pactMagicSlotsMax})`
|
|
382
|
+
});
|
|
383
|
+
debug.log(`🔮 Added Pact Magic to Divine Smite options: Level ${effectivePactLevel} (${pactMagicSlots}/${pactMagicSlotsMax})`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Then check regular spell slots - Divine Smite only works up to 5th level
|
|
387
|
+
for (let level = 1; level <= 5; level++) {
|
|
388
|
+
const slotVar = `level${level}SpellSlots`;
|
|
389
|
+
const slotMaxVar = `level${level}SpellSlotsMax`;
|
|
390
|
+
let current = characterData.spellSlots?.[slotVar] || 0;
|
|
391
|
+
let max = characterData.spellSlots?.[slotMaxVar] || 0;
|
|
392
|
+
|
|
393
|
+
// Skip if this level's slots are actually Pact Magic slots (avoid duplicates)
|
|
394
|
+
if (pactMagicSlotsMax > 0 && level === effectivePactLevel) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Show slot level if character has access to it (max > 0), even if depleted
|
|
399
|
+
if (max > 0) {
|
|
400
|
+
availableSlots.push({
|
|
401
|
+
level,
|
|
402
|
+
current,
|
|
403
|
+
max,
|
|
404
|
+
slotVar,
|
|
405
|
+
slotMaxVar,
|
|
406
|
+
isPactMagic: false,
|
|
407
|
+
label: `Level ${level} (${current}/${max})`
|
|
408
|
+
});
|
|
409
|
+
debug.log(`🔮 Added Level ${level} to Divine Smite options: ${current}/${max}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
debug.log('🔮 Available slots for Divine Smite:', availableSlots);
|
|
414
|
+
|
|
415
|
+
// Sort by level (lowest first)
|
|
416
|
+
availableSlots.sort((a, b) => a.level - b.level);
|
|
417
|
+
|
|
418
|
+
// Create theme-aware modal
|
|
419
|
+
const { modal, modalContent, isDarkTheme } = createThemedModal();
|
|
420
|
+
|
|
421
|
+
// Generate slot options
|
|
422
|
+
const slotOptions = availableSlots.map((slot, index) =>
|
|
423
|
+
`<option value="${index}" ${slot.current <= 0 ? 'disabled' : ''}>
|
|
424
|
+
${slot.label} ${slot.current <= 0 ? '(No slots remaining)' : ''}
|
|
425
|
+
</option>`
|
|
426
|
+
).join('');
|
|
427
|
+
|
|
428
|
+
modalContent.innerHTML = `
|
|
429
|
+
<h2 style="margin: 0 0 20px 0; font-size: 1.5em;">⚡ Divine Smite</h2>
|
|
430
|
+
<p style="margin: 0 0 20px 0; font-size: 0.95em;">
|
|
431
|
+
Expend a spell slot to deal extra radiant damage on a melee weapon hit
|
|
432
|
+
</p>
|
|
433
|
+
|
|
434
|
+
<div style="margin: 20px 0;">
|
|
435
|
+
<label style="display: block; margin-bottom: 8px; font-size: 0.95em;">Choose Spell Slot:</label>
|
|
436
|
+
<select id="spellSlotSelect" style="width: 100%; padding: 8px; font-size: 1em; border: 2px solid var(--accent-info); border-radius: 6px;">
|
|
437
|
+
${slotOptions}
|
|
438
|
+
</select>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div style="margin: 20px 0; text-align: left;">
|
|
442
|
+
<h3 style="margin: 0 0 15px 0; font-size: 1.1em;">Damage Options:</h3>
|
|
443
|
+
|
|
444
|
+
<label style="display: flex; align-items: center; margin: 10px 0; cursor: pointer;">
|
|
445
|
+
<input type="checkbox" id="critCheckbox" style="margin-right: 10px; width: 18px; height: 18px;">
|
|
446
|
+
<span>Critical Hit (double damage dice)</span>
|
|
447
|
+
</label>
|
|
448
|
+
|
|
449
|
+
<label style="display: flex; align-items: center; margin: 10px 0; cursor: pointer;">
|
|
450
|
+
<input type="checkbox" id="fiendCheckbox" style="margin-right: 10px; width: 18px; height: 18px;">
|
|
451
|
+
<span>Against Fiend or Undead (+1d8)</span>
|
|
452
|
+
</label>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div id="damagePreview" style="margin: 15px 0; padding: 10px; border-radius: 6px; font-weight: bold; display: none;">
|
|
456
|
+
<!-- Hidden - damage shown only on button -->
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div style="margin-top: 25px; display: flex; gap: 10px; justify-content: center;">
|
|
460
|
+
<button id="confirmDivineSmite" style="padding: 12px 24px; font-size: 1em; font-weight: bold; background: var(--accent-warning); color: white; border: none; border-radius: 6px; cursor: pointer;" disabled>
|
|
461
|
+
Select Slot
|
|
462
|
+
</button>
|
|
463
|
+
<button id="cancelDivineSmite" style="padding: 12px 24px; font-size: 1em; font-weight: bold; background: var(--accent-danger); color: white; border: none; border-radius: 6px; cursor: pointer;">
|
|
464
|
+
Cancel
|
|
465
|
+
</button>
|
|
466
|
+
</div>
|
|
467
|
+
`;
|
|
468
|
+
|
|
469
|
+
modal.appendChild(modalContent);
|
|
470
|
+
document.body.appendChild(modal);
|
|
471
|
+
|
|
472
|
+
// Get elements
|
|
473
|
+
const critCheckbox = document.getElementById('critCheckbox');
|
|
474
|
+
const fiendCheckbox = document.getElementById('fiendCheckbox');
|
|
475
|
+
const slotSelect = document.getElementById('spellSlotSelect');
|
|
476
|
+
const confirmBtn = document.getElementById('confirmDivineSmite');
|
|
477
|
+
const cancelBtn = document.getElementById('cancelDivineSmite');
|
|
478
|
+
|
|
479
|
+
// Update button text when options change
|
|
480
|
+
function updateDamagePreview() {
|
|
481
|
+
const selectedIndex = parseInt(slotSelect.value);
|
|
482
|
+
if (isNaN(selectedIndex) || !availableSlots[selectedIndex]) {
|
|
483
|
+
confirmBtn.disabled = true;
|
|
484
|
+
confirmBtn.textContent = 'Select Slot';
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const slot = availableSlots[selectedIndex];
|
|
489
|
+
if (slot.current <= 0) {
|
|
490
|
+
confirmBtn.disabled = true;
|
|
491
|
+
confirmBtn.textContent = 'No Slots';
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const level = slot.level;
|
|
496
|
+
const baseDice = 1 + level; // 2d8 at level 1, +1d8 per level above
|
|
497
|
+
let damageFormula = `${baseDice}d8`;
|
|
498
|
+
|
|
499
|
+
// Add +1d8 for fiends/undead
|
|
500
|
+
if (fiendCheckbox.checked) {
|
|
501
|
+
damageFormula += ` + 1d8`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Apply critical hit doubling
|
|
505
|
+
if (critCheckbox.checked) {
|
|
506
|
+
damageFormula = `(${damageFormula}) * 2`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Update confirm button
|
|
510
|
+
let buttonText = '⚡ ';
|
|
511
|
+
let modifiers = [];
|
|
512
|
+
|
|
513
|
+
if (damageFormula.includes('* 2')) {
|
|
514
|
+
buttonText += `${baseDice}d8`;
|
|
515
|
+
modifiers.push('(CRIT)');
|
|
516
|
+
} else {
|
|
517
|
+
buttonText += `${baseDice}d8`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (damageFormula.includes('+ 1d8')) {
|
|
521
|
+
modifiers.unshift('+1d8');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (modifiers.length > 0) {
|
|
525
|
+
buttonText += ' ' + modifiers.join(' ');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
buttonText += ` Damage (Lvl ${slot.level})`;
|
|
529
|
+
|
|
530
|
+
confirmBtn.innerHTML = buttonText;
|
|
531
|
+
confirmBtn.disabled = false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
critCheckbox.addEventListener('change', updateDamagePreview);
|
|
535
|
+
fiendCheckbox.addEventListener('change', updateDamagePreview);
|
|
536
|
+
slotSelect.addEventListener('change', updateDamagePreview);
|
|
537
|
+
|
|
538
|
+
// Handle confirm
|
|
539
|
+
confirmBtn.addEventListener('click', () => {
|
|
540
|
+
const selectedIndex = parseInt(slotSelect.value);
|
|
541
|
+
const slot = availableSlots[selectedIndex];
|
|
542
|
+
|
|
543
|
+
if (slot.current <= 0) {
|
|
544
|
+
if (typeof showNotification !== 'undefined') {
|
|
545
|
+
showNotification(`❌ No Level ${slot.level} spell slot available!`, 'error');
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Calculate damage
|
|
551
|
+
const level = slot.level;
|
|
552
|
+
const baseDice = 1 + level;
|
|
553
|
+
let damageFormula = `${baseDice}d8`;
|
|
554
|
+
|
|
555
|
+
// Add +1d8 for fiends/undead
|
|
556
|
+
if (fiendCheckbox.checked) {
|
|
557
|
+
damageFormula += ` + 1d8`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Apply critical hit doubling
|
|
561
|
+
if (critCheckbox.checked) {
|
|
562
|
+
damageFormula = `(${damageFormula}) * 2`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Consume the spell slot
|
|
566
|
+
if (slot.isPactMagic) {
|
|
567
|
+
characterData.spellSlots[slot.slotVar] = Math.max(0, characterData.spellSlots[slot.slotVar] - 1);
|
|
568
|
+
} else {
|
|
569
|
+
characterData.spellSlots[slot.slotVar] = Math.max(0, characterData.spellSlots[slot.slotVar] - 1);
|
|
570
|
+
}
|
|
571
|
+
if (typeof saveCharacterData !== 'undefined') saveCharacterData();
|
|
572
|
+
|
|
573
|
+
// Build description
|
|
574
|
+
let description = `Divine Smite (Level ${level}`;
|
|
575
|
+
if (critCheckbox.checked) description += ', Critical';
|
|
576
|
+
if (fiendCheckbox.checked) description += ', vs Fiend/Undead';
|
|
577
|
+
description += ')';
|
|
578
|
+
|
|
579
|
+
// Announce and roll the damage
|
|
580
|
+
if (typeof announceAction !== 'undefined') {
|
|
581
|
+
announceAction({
|
|
582
|
+
name: 'Divine Smite',
|
|
583
|
+
description: description
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (typeof roll !== 'undefined') {
|
|
588
|
+
roll('Divine Smite', damageFormula);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Show notification
|
|
592
|
+
const remaining = slot.isPactMagic ?
|
|
593
|
+
characterData.spellSlots[slot.slotVar] :
|
|
594
|
+
characterData.spellSlots[slot.slotVar];
|
|
595
|
+
if (typeof showNotification !== 'undefined') {
|
|
596
|
+
showNotification(`⚡ Divine Smite! Used Level ${slot.level} slot (${remaining}/${slot.max} left)`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Remove modal and refresh display
|
|
600
|
+
document.body.removeChild(modal);
|
|
601
|
+
if (typeof buildSheet !== 'undefined') buildSheet(characterData);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Handle cancel
|
|
605
|
+
cancelBtn.addEventListener('click', () => {
|
|
606
|
+
document.body.removeChild(modal);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Handle escape key
|
|
610
|
+
const handleEscape = (e) => {
|
|
611
|
+
if (e.key === 'Escape') {
|
|
612
|
+
document.body.removeChild(modal);
|
|
613
|
+
document.removeEventListener('keydown', handleEscape);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
document.addEventListener('keydown', handleEscape);
|
|
617
|
+
|
|
618
|
+
// Initialize the damage preview
|
|
619
|
+
updateDamagePreview();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ===== LAY ON HANDS MODAL =====
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Show Lay on Hands modal for spending healing points
|
|
626
|
+
*/
|
|
627
|
+
function showLayOnHandsModal(layOnHandsPool) {
|
|
628
|
+
// Requires characterData to be available from global scope
|
|
629
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
630
|
+
|
|
631
|
+
// Create theme-aware modal
|
|
632
|
+
const { modal, modalContent, isDarkTheme } = createThemedModal();
|
|
633
|
+
|
|
634
|
+
modalContent.innerHTML = `
|
|
635
|
+
<h2 style="margin: 0 0 20px 0; font-size: 1.5em;">💚 Lay on Hands</h2>
|
|
636
|
+
<p style="margin: 0 0 15px 0; font-size: 1.1em;">
|
|
637
|
+
Available Points: <strong>${layOnHandsPool.current}/${layOnHandsPool.max}</strong>
|
|
638
|
+
</p>
|
|
639
|
+
<p style="margin: 0 0 20px 0; font-size: 0.95em;">
|
|
640
|
+
How many points do you want to spend?
|
|
641
|
+
</p>
|
|
642
|
+
<div style="margin: 20px 0;">
|
|
643
|
+
<input type="number" id="layOnHandsAmount" min="1" max="${layOnHandsPool.current}" value="1"
|
|
644
|
+
style="width: 80px; padding: 8px; font-size: 1.1em; text-align: center; border: 2px solid var(--accent-info); border-radius: 6px;">
|
|
645
|
+
<span style="margin-left: 10px; font-weight: bold;" id="healingDisplay">1 HP healed</span>
|
|
646
|
+
</div>
|
|
647
|
+
<div style="margin-top: 25px; display: flex; gap: 10px; justify-content: center;">
|
|
648
|
+
<button id="confirmLayOnHands" style="padding: 12px 24px; font-size: 1em; font-weight: bold; background: var(--accent-success); color: white; border: none; border-radius: 6px; cursor: pointer;">
|
|
649
|
+
Heal
|
|
650
|
+
</button>
|
|
651
|
+
<button id="cancelLayOnHands" style="padding: 12px 24px; font-size: 1em; font-weight: bold; background: var(--accent-danger); color: white; border: none; border-radius: 6px; cursor: pointer;">
|
|
652
|
+
Cancel
|
|
653
|
+
</button>
|
|
654
|
+
</div>
|
|
655
|
+
`;
|
|
656
|
+
|
|
657
|
+
modal.appendChild(modalContent);
|
|
658
|
+
document.body.appendChild(modal);
|
|
659
|
+
|
|
660
|
+
// Get elements
|
|
661
|
+
const amountInput = document.getElementById('layOnHandsAmount');
|
|
662
|
+
const healingDisplay = document.getElementById('healingDisplay');
|
|
663
|
+
const confirmBtn = document.getElementById('confirmLayOnHands');
|
|
664
|
+
const cancelBtn = document.getElementById('cancelLayOnHands');
|
|
665
|
+
|
|
666
|
+
// Update healing display when amount changes
|
|
667
|
+
function updateHealingDisplay() {
|
|
668
|
+
const amount = parseInt(amountInput.value) || 0;
|
|
669
|
+
healingDisplay.textContent = `${amount} HP healed`;
|
|
670
|
+
healingDisplay.style.color = '#3498db';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
amountInput.addEventListener('input', updateHealingDisplay);
|
|
674
|
+
|
|
675
|
+
// Handle confirm
|
|
676
|
+
confirmBtn.addEventListener('click', () => {
|
|
677
|
+
const amount = parseInt(amountInput.value);
|
|
678
|
+
|
|
679
|
+
if (isNaN(amount) || amount < 1 || amount > layOnHandsPool.current) {
|
|
680
|
+
if (typeof showNotification !== 'undefined') {
|
|
681
|
+
showNotification(`❌ Please enter a number between 1 and ${layOnHandsPool.current}`, 'error');
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Deduct points
|
|
687
|
+
layOnHandsPool.current -= amount;
|
|
688
|
+
if (typeof saveCharacterData !== 'undefined') saveCharacterData();
|
|
689
|
+
|
|
690
|
+
// Announce the healing
|
|
691
|
+
debug.log(`💚 Used ${amount} Lay on Hands points. Remaining: ${layOnHandsPool.current}/${layOnHandsPool.max}`);
|
|
692
|
+
|
|
693
|
+
if (amount === 5) {
|
|
694
|
+
if (typeof announceAction !== 'undefined') {
|
|
695
|
+
announceAction({
|
|
696
|
+
name: 'Lay on Hands',
|
|
697
|
+
description: `Cured disease/poison`
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
if (typeof showNotification !== 'undefined') {
|
|
701
|
+
showNotification(`💚 Lay on Hands: Cured disease/poison (${layOnHandsPool.current}/${layOnHandsPool.max} points left)`);
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
if (typeof announceAction !== 'undefined') {
|
|
705
|
+
announceAction({
|
|
706
|
+
name: 'Lay on Hands',
|
|
707
|
+
description: `Restored ${amount} HP`
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (typeof showNotification !== 'undefined') {
|
|
711
|
+
showNotification(`💚 Lay on Hands: Restored ${amount} HP (${layOnHandsPool.current}/${layOnHandsPool.max} points left)`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Remove modal and refresh display
|
|
716
|
+
document.body.removeChild(modal);
|
|
717
|
+
if (typeof buildSheet !== 'undefined') buildSheet(characterData);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Handle cancel
|
|
721
|
+
cancelBtn.addEventListener('click', () => {
|
|
722
|
+
document.body.removeChild(modal);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Handle escape key
|
|
726
|
+
const handleEscape = (e) => {
|
|
727
|
+
if (e.key === 'Escape') {
|
|
728
|
+
document.body.removeChild(modal);
|
|
729
|
+
document.removeEventListener('keydown', handleEscape);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
document.addEventListener('keydown', handleEscape);
|
|
733
|
+
|
|
734
|
+
// Focus input
|
|
735
|
+
amountInput.focus();
|
|
736
|
+
amountInput.select();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ===== STARRY FORM (Stars Druid) =====
|
|
740
|
+
|
|
741
|
+
function getWisModForStarryForm() {
|
|
742
|
+
if (characterData.attributeMods && typeof characterData.attributeMods.wisdom === 'number') {
|
|
743
|
+
return characterData.attributeMods.wisdom;
|
|
744
|
+
}
|
|
745
|
+
const score = characterData.attributes && characterData.attributes.wisdom;
|
|
746
|
+
if (typeof score === 'number') return Math.floor((score - 10) / 2);
|
|
747
|
+
return 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Find a tracked "Wild Shape" resource (used to power Starry Form), if present.
|
|
751
|
+
function getWildShapeResource() {
|
|
752
|
+
if (!characterData || !Array.isArray(characterData.resources)) return null;
|
|
753
|
+
return characterData.resources.find(r => r && r.name && /wild\s*shape/i.test(r.name)) || null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Starry Form (Circle of Stars Druid): choose a constellation. Spends one Wild
|
|
758
|
+
* Shape use, announces the constellation and its effect, and records the active
|
|
759
|
+
* constellation. The per-constellation mechanics are announced as their rules
|
|
760
|
+
* text because the dice roll on Roll20, not in the extension.
|
|
761
|
+
*/
|
|
762
|
+
function showStarryFormModal(action) {
|
|
763
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
764
|
+
|
|
765
|
+
const wis = getWisModForStarryForm();
|
|
766
|
+
const fmtMod = (m) => (m >= 0 ? `+${m}` : `${m}`);
|
|
767
|
+
const wsRes = getWildShapeResource();
|
|
768
|
+
|
|
769
|
+
const constellations = [
|
|
770
|
+
{ key: 'Archer', icon: '🏹', color: 'var(--accent-info)',
|
|
771
|
+
effect: `As a bonus action now and on later turns, make a ranged spell attack (120 ft) for 1d8 ${fmtMod(wis)} radiant. (Use the Archer Attack action.)` },
|
|
772
|
+
{ key: 'Chalice', icon: '🍷', color: 'var(--accent-success)',
|
|
773
|
+
effect: `Whenever you cast a spell using a spell slot to restore hit points, you or a creature within 30 ft regains 1d8 ${fmtMod(wis)} hit points.` },
|
|
774
|
+
{ key: 'Dragon', icon: '🐉', color: 'var(--accent-warning)',
|
|
775
|
+
effect: `When you make an Intelligence or Wisdom check, or a Constitution save to maintain concentration, treat a d20 roll of 9 or lower as 10.` },
|
|
776
|
+
];
|
|
777
|
+
|
|
778
|
+
const { modal, modalContent } = createThemedModal();
|
|
779
|
+
modalContent.innerHTML = `
|
|
780
|
+
<h2 style="margin:0 0 8px;font-size:1.5em;">✨ Starry Form</h2>
|
|
781
|
+
<p style="margin:0 0 4px;font-size:0.95em;">Assume a Starry Form (expends one Wild Shape). Choose a constellation:</p>
|
|
782
|
+
<p style="margin:0 0 16px;font-size:0.85em;opacity:0.8;">${wsRes ? `Wild Shape: ${wsRes.current}/${wsRes.max}` : 'Lasts 10 minutes'}</p>
|
|
783
|
+
<div style="display:flex;flex-direction:column;gap:10px;text-align:left;">
|
|
784
|
+
${constellations.map((c, i) => `
|
|
785
|
+
<button class="starry-choice" data-index="${i}" style="display:block;width:100%;padding:12px 14px;border:none;border-radius:8px;background:${c.color};color:#fff;cursor:pointer;text-align:left;">
|
|
786
|
+
<div style="font-weight:bold;font-size:1.05em;">${c.icon} ${c.key}</div>
|
|
787
|
+
<div style="font-size:0.82em;opacity:0.95;margin-top:4px;line-height:1.35;">${c.effect}</div>
|
|
788
|
+
</button>`).join('')}
|
|
789
|
+
</div>
|
|
790
|
+
<div style="margin-top:18px;">
|
|
791
|
+
<button id="cancelStarryForm" style="padding:10px 22px;font-size:1em;font-weight:bold;background:var(--accent-danger);color:#fff;border:none;border-radius:6px;cursor:pointer;">Cancel</button>
|
|
792
|
+
</div>
|
|
793
|
+
`;
|
|
794
|
+
modal.appendChild(modalContent);
|
|
795
|
+
document.body.appendChild(modal);
|
|
796
|
+
|
|
797
|
+
const close = () => {
|
|
798
|
+
if (modal.parentNode) document.body.removeChild(modal);
|
|
799
|
+
document.removeEventListener('keydown', onEsc);
|
|
800
|
+
};
|
|
801
|
+
const onEsc = (e) => { if (e.key === 'Escape') close(); };
|
|
802
|
+
document.addEventListener('keydown', onEsc);
|
|
803
|
+
document.getElementById('cancelStarryForm').addEventListener('click', close);
|
|
804
|
+
|
|
805
|
+
modalContent.querySelectorAll('.starry-choice').forEach(btn => {
|
|
806
|
+
btn.addEventListener('click', () => {
|
|
807
|
+
const c = constellations[parseInt(btn.dataset.index, 10)];
|
|
808
|
+
|
|
809
|
+
// Spend one Wild Shape use, if tracked.
|
|
810
|
+
if (wsRes) {
|
|
811
|
+
if (wsRes.current <= 0) {
|
|
812
|
+
if (typeof showNotification !== 'undefined') showNotification('❌ No Wild Shape uses remaining!', 'error');
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
wsRes.current -= 1;
|
|
816
|
+
if (characterData.otherVariables && wsRes.varName) {
|
|
817
|
+
characterData.otherVariables[wsRes.varName] = wsRes.current;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (typeof saveCharacterData !== 'undefined') saveCharacterData();
|
|
821
|
+
|
|
822
|
+
// Record the active constellation (used for display / reminders).
|
|
823
|
+
characterData.starryFormConstellation = c.key;
|
|
824
|
+
|
|
825
|
+
// Announce to chat + notify.
|
|
826
|
+
if (typeof announceAction !== 'undefined') {
|
|
827
|
+
announceAction({ name: `Starry Form: ${c.key}`, description: c.effect });
|
|
828
|
+
}
|
|
829
|
+
if (typeof showNotification !== 'undefined') {
|
|
830
|
+
showNotification(`✨ Starry Form: ${c.key}${wsRes ? ` (Wild Shape ${wsRes.current}/${wsRes.max})` : ''}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
close();
|
|
834
|
+
if (typeof buildSheet !== 'undefined') buildSheet(characterData);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ===== WILD SHAPE (Druid) =====
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Wild Shape: spend a use to transform into a beast. Beast stat blocks are
|
|
843
|
+
* open-ended, so this records the chosen beast by name, announces the
|
|
844
|
+
* transformation + duration, and decrements the Wild Shape use.
|
|
845
|
+
*/
|
|
846
|
+
function showWildShapeModal(action) {
|
|
847
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
848
|
+
|
|
849
|
+
const wsRes = getWildShapeResource();
|
|
850
|
+
const level = Number(characterData.level) || 1;
|
|
851
|
+
const durationHours = Math.max(1, Math.floor(level / 2)); // half druid level, min 1
|
|
852
|
+
|
|
853
|
+
const { modal, modalContent } = createThemedModal();
|
|
854
|
+
modalContent.innerHTML = `
|
|
855
|
+
<h2 style="margin:0 0 8px;font-size:1.5em;">🐾 Wild Shape</h2>
|
|
856
|
+
<p style="margin:0 0 12px;font-size:0.95em;">
|
|
857
|
+
Transform into a beast for up to ${durationHours} hour${durationHours === 1 ? '' : 's'}${wsRes ? ` (Wild Shape: ${wsRes.current}/${wsRes.max})` : ''}.
|
|
858
|
+
</p>
|
|
859
|
+
<div style="margin:0 0 16px;text-align:left;">
|
|
860
|
+
<label style="display:block;font-size:0.85em;margin-bottom:6px;opacity:0.85;">Beast form (name)</label>
|
|
861
|
+
<input type="text" id="wildShapeBeast" placeholder="e.g. Dire Wolf, Giant Eagle…"
|
|
862
|
+
style="width:100%;padding:10px;font-size:1em;border:2px solid var(--accent-info);border-radius:6px;background:rgba(0,0,0,0.2);color:inherit;">
|
|
863
|
+
</div>
|
|
864
|
+
<div style="display:flex;gap:10px;justify-content:center;">
|
|
865
|
+
<button id="confirmWildShape" style="padding:12px 24px;font-size:1em;font-weight:bold;background:var(--accent-success);color:#fff;border:none;border-radius:6px;cursor:pointer;">Transform</button>
|
|
866
|
+
<button id="cancelWildShape" style="padding:12px 24px;font-size:1em;font-weight:bold;background:var(--accent-danger);color:#fff;border:none;border-radius:6px;cursor:pointer;">Cancel</button>
|
|
867
|
+
</div>
|
|
868
|
+
`;
|
|
869
|
+
modal.appendChild(modalContent);
|
|
870
|
+
document.body.appendChild(modal);
|
|
871
|
+
|
|
872
|
+
const input = document.getElementById('wildShapeBeast');
|
|
873
|
+
const close = () => {
|
|
874
|
+
if (modal.parentNode) document.body.removeChild(modal);
|
|
875
|
+
document.removeEventListener('keydown', onEsc);
|
|
876
|
+
};
|
|
877
|
+
const onEsc = (e) => { if (e.key === 'Escape') close(); };
|
|
878
|
+
document.addEventListener('keydown', onEsc);
|
|
879
|
+
document.getElementById('cancelWildShape').addEventListener('click', close);
|
|
880
|
+
|
|
881
|
+
document.getElementById('confirmWildShape').addEventListener('click', () => {
|
|
882
|
+
const beast = (input.value || '').trim() || 'a beast';
|
|
883
|
+
|
|
884
|
+
if (wsRes) {
|
|
885
|
+
if (wsRes.current <= 0) {
|
|
886
|
+
if (typeof showNotification !== 'undefined') showNotification('❌ No Wild Shape uses remaining!', 'error');
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
wsRes.current -= 1;
|
|
890
|
+
if (characterData.otherVariables && wsRes.varName) {
|
|
891
|
+
characterData.otherVariables[wsRes.varName] = wsRes.current;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (typeof saveCharacterData !== 'undefined') saveCharacterData();
|
|
895
|
+
|
|
896
|
+
if (typeof announceAction !== 'undefined') {
|
|
897
|
+
announceAction({ name: 'Wild Shape', description: `Transforms into ${beast} for up to ${durationHours} hour${durationHours === 1 ? '' : 's'}.` });
|
|
898
|
+
}
|
|
899
|
+
if (typeof showNotification !== 'undefined') {
|
|
900
|
+
showNotification(`🐾 Wild Shape: ${beast}${wsRes ? ` (${wsRes.current}/${wsRes.max})` : ''}`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
close();
|
|
904
|
+
if (typeof buildSheet !== 'undefined') buildSheet(characterData);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
input.focus();
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ===== LUCKY FEAT MODAL =====
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Show Lucky feat modal for using luck points
|
|
914
|
+
*/
|
|
915
|
+
function showLuckyModal() {
|
|
916
|
+
debug.log('🎖️ Lucky modal called');
|
|
917
|
+
|
|
918
|
+
const luckyResource = getLuckyResource();
|
|
919
|
+
if (!luckyResource || luckyResource.current <= 0) {
|
|
920
|
+
if (typeof showNotification !== 'undefined') {
|
|
921
|
+
showNotification('❌ No luck points available!', 'error');
|
|
922
|
+
}
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Create modal overlay
|
|
927
|
+
const modal = document.createElement('div');
|
|
928
|
+
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;';
|
|
929
|
+
|
|
930
|
+
// Create modal content
|
|
931
|
+
const modalContent = document.createElement('div');
|
|
932
|
+
modalContent.style.cssText = 'background: var(--bg-secondary); color: var(--text-primary); border-radius: 8px; padding: 20px; max-width: 400px; width: 90%; box-shadow: 0 4px 20px rgba(0,0,0,0.3);';
|
|
933
|
+
|
|
934
|
+
modalContent.innerHTML = `
|
|
935
|
+
<h3 style="margin: 0 0 15px 0; color: #f39c12;">🎖️ Use Lucky Point</h3>
|
|
936
|
+
<p style="margin: 0 0 15px 0; color: #666;">Choose what to use Lucky for:</p>
|
|
937
|
+
<div style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 4px;">
|
|
938
|
+
<strong>Luck Points:</strong> ${luckyResource.current}/${luckyResource.max}
|
|
939
|
+
</div>
|
|
940
|
+
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
941
|
+
<button id="luckyOffensive" style="padding: 10px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">⚔️ Attack/Check/Saving Throw</button>
|
|
942
|
+
<button id="luckyDefensive" style="padding: 10px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">🛡️ Against Attack on You</button>
|
|
943
|
+
<button id="luckyCancel" style="padding: 10px; background: #95a5a6; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
|
|
944
|
+
</div>
|
|
945
|
+
`;
|
|
946
|
+
|
|
947
|
+
modal.appendChild(modalContent);
|
|
948
|
+
document.body.appendChild(modal);
|
|
949
|
+
|
|
950
|
+
// Add event listeners
|
|
951
|
+
document.getElementById('luckyOffensive').addEventListener('click', () => {
|
|
952
|
+
if (useLuckyPoint()) {
|
|
953
|
+
modal.remove();
|
|
954
|
+
// Roll a d20 for Lucky
|
|
955
|
+
rollLuckyDie('offensive');
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
document.getElementById('luckyDefensive').addEventListener('click', () => {
|
|
960
|
+
if (useLuckyPoint()) {
|
|
961
|
+
modal.remove();
|
|
962
|
+
// Roll a d20 for Lucky defense
|
|
963
|
+
rollLuckyDie('defensive');
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
document.getElementById('luckyCancel').addEventListener('click', () => {
|
|
968
|
+
modal.remove();
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// Close on overlay click
|
|
972
|
+
modal.addEventListener('click', (e) => {
|
|
973
|
+
if (e.target === modal) modal.remove();
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
debug.log('🎖️ Lucky modal displayed');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Roll Lucky d20 die
|
|
981
|
+
*/
|
|
982
|
+
function rollLuckyDie(type) {
|
|
983
|
+
// Requires characterData to be available from global scope
|
|
984
|
+
if (typeof characterData === 'undefined' || !characterData) return;
|
|
985
|
+
|
|
986
|
+
debug.log(`🎖️ Rolling Lucky d20 for ${type}`);
|
|
987
|
+
|
|
988
|
+
// Roll a d20
|
|
989
|
+
const luckyRoll = Math.floor(Math.random() * 20) + 1;
|
|
990
|
+
|
|
991
|
+
// TODO: Add Owlbear Rodeo integration for Lucky rolls
|
|
992
|
+
|
|
993
|
+
if (type === 'offensive') {
|
|
994
|
+
if (typeof showNotification !== 'undefined') {
|
|
995
|
+
showNotification(`🎖️ Lucky roll: ${luckyRoll}! Use this instead of your next d20 roll.`, 'success');
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
if (typeof showNotification !== 'undefined') {
|
|
999
|
+
showNotification(`🎖️ Lucky defense roll: ${luckyRoll}! Compare against attacker's roll.`, 'success');
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
debug.log(`🎖️ Lucky d20 result: ${luckyRoll} - sent to chat`);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ===== DIVINE SPARK (CLERIC CHANNEL DIVINITY) =====
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Show Divine Spark modal (Cleric Channel Divinity feature)
|
|
1010
|
+
* @param {object} action - Action object
|
|
1011
|
+
* @param {object} channelDivinityResource - Channel Divinity resource
|
|
1012
|
+
*/
|
|
1013
|
+
function showDivineSparkModal(action, channelDivinityResource) {
|
|
1014
|
+
// Create modal overlay
|
|
1015
|
+
const modal = document.createElement('div');
|
|
1016
|
+
modal.className = 'modal-overlay';
|
|
1017
|
+
modal.style.cssText = `
|
|
1018
|
+
position: fixed;
|
|
1019
|
+
top: 0;
|
|
1020
|
+
left: 0;
|
|
1021
|
+
right: 0;
|
|
1022
|
+
bottom: 0;
|
|
1023
|
+
background: rgba(0, 0, 0, 0.7);
|
|
1024
|
+
display: flex;
|
|
1025
|
+
align-items: center;
|
|
1026
|
+
justify-content: center;
|
|
1027
|
+
z-index: 10000;
|
|
1028
|
+
`;
|
|
1029
|
+
|
|
1030
|
+
// Get cleric level for damage calculation
|
|
1031
|
+
const clericLevel = characterData.otherVariables?.clericLevel || characterData.otherVariables?.cleric?.level || 1;
|
|
1032
|
+
const wisdomMod = characterData.abilityScores?.wisdom?.modifier || 0;
|
|
1033
|
+
|
|
1034
|
+
// Calculate number of d8 dice based on cleric level
|
|
1035
|
+
const diceArray = [1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,4,4,4];
|
|
1036
|
+
const numDice = diceArray[Math.min(clericLevel, 20) - 1] || 1;
|
|
1037
|
+
|
|
1038
|
+
// Create modal content
|
|
1039
|
+
const modalContent = document.createElement('div');
|
|
1040
|
+
modalContent.style.cssText = `
|
|
1041
|
+
background: #2a2a2a;
|
|
1042
|
+
border-radius: 8px;
|
|
1043
|
+
padding: 24px;
|
|
1044
|
+
max-width: 400px;
|
|
1045
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
|
1046
|
+
color: #fff;
|
|
1047
|
+
`;
|
|
1048
|
+
|
|
1049
|
+
modalContent.innerHTML = `
|
|
1050
|
+
<h3 style="margin-top: 0; margin-bottom: 16px; color: #ffd700; text-align: center;">
|
|
1051
|
+
✨ Divine Spark
|
|
1052
|
+
</h3>
|
|
1053
|
+
<p style="margin-bottom: 8px; text-align: center; color: #ccc;">
|
|
1054
|
+
Roll: ${numDice}d8 + ${wisdomMod}
|
|
1055
|
+
</p>
|
|
1056
|
+
<p style="margin-bottom: 20px; text-align: center; font-size: 14px; color: #aaa;">
|
|
1057
|
+
Channel Divinity: ${channelDivinityResource.current}/${channelDivinityResource.max}
|
|
1058
|
+
</p>
|
|
1059
|
+
<p style="margin-bottom: 20px; text-align: center; color: #fff;">
|
|
1060
|
+
Choose the effect:
|
|
1061
|
+
</p>
|
|
1062
|
+
<div style="display: flex; flex-direction: column; gap: 12px;">
|
|
1063
|
+
<button id="divine-spark-heal" style="
|
|
1064
|
+
padding: 12px 20px;
|
|
1065
|
+
font-size: 16px;
|
|
1066
|
+
border: 2px solid #4ade80;
|
|
1067
|
+
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
1068
|
+
color: white;
|
|
1069
|
+
border-radius: 6px;
|
|
1070
|
+
cursor: pointer;
|
|
1071
|
+
font-weight: bold;
|
|
1072
|
+
transition: all 0.2s;
|
|
1073
|
+
">💚 Heal Target</button>
|
|
1074
|
+
<button id="divine-spark-necrotic" style="
|
|
1075
|
+
padding: 12px 20px;
|
|
1076
|
+
font-size: 16px;
|
|
1077
|
+
border: 2px solid #a78bfa;
|
|
1078
|
+
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
|
1079
|
+
color: white;
|
|
1080
|
+
border-radius: 6px;
|
|
1081
|
+
cursor: pointer;
|
|
1082
|
+
font-weight: bold;
|
|
1083
|
+
transition: all 0.2s;
|
|
1084
|
+
">🖤 Necrotic Damage</button>
|
|
1085
|
+
<button id="divine-spark-radiant" style="
|
|
1086
|
+
padding: 12px 20px;
|
|
1087
|
+
font-size: 16px;
|
|
1088
|
+
border: 2px solid #fbbf24;
|
|
1089
|
+
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
1090
|
+
color: white;
|
|
1091
|
+
border-radius: 6px;
|
|
1092
|
+
cursor: pointer;
|
|
1093
|
+
font-weight: bold;
|
|
1094
|
+
transition: all 0.2s;
|
|
1095
|
+
">✨ Radiant Damage</button>
|
|
1096
|
+
<button id="divine-spark-cancel" style="
|
|
1097
|
+
padding: 10px 20px;
|
|
1098
|
+
font-size: 14px;
|
|
1099
|
+
background: #444;
|
|
1100
|
+
color: white;
|
|
1101
|
+
border: 1px solid #666;
|
|
1102
|
+
border-radius: 6px;
|
|
1103
|
+
cursor: pointer;
|
|
1104
|
+
margin-top: 8px;
|
|
1105
|
+
">Cancel</button>
|
|
1106
|
+
</div>
|
|
1107
|
+
`;
|
|
1108
|
+
|
|
1109
|
+
modal.appendChild(modalContent);
|
|
1110
|
+
|
|
1111
|
+
// Add hover effects
|
|
1112
|
+
const buttons = modalContent.querySelectorAll('button:not(#divine-spark-cancel)');
|
|
1113
|
+
buttons.forEach(btn => {
|
|
1114
|
+
btn.addEventListener('mouseenter', () => {
|
|
1115
|
+
btn.style.transform = 'scale(1.05)';
|
|
1116
|
+
btn.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
|
|
1117
|
+
});
|
|
1118
|
+
btn.addEventListener('mouseleave', () => {
|
|
1119
|
+
btn.style.transform = 'scale(1)';
|
|
1120
|
+
btn.style.boxShadow = 'none';
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// Helper function to execute Divine Spark
|
|
1125
|
+
const executeDivineSpark = (type, color, damageType = null) => {
|
|
1126
|
+
// Consume Channel Divinity use
|
|
1127
|
+
channelDivinityResource.current -= 1;
|
|
1128
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
1129
|
+
saveCharacterData();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Set the Divine Spark Choice variable in DiceCloud
|
|
1133
|
+
const choiceValue = type === 'heal' ? 1 : (type === 'necrotic' ? 2 : 3);
|
|
1134
|
+
if (characterData.otherVariables) {
|
|
1135
|
+
characterData.otherVariables.divineSparkChoice = choiceValue;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Build the roll formula
|
|
1139
|
+
const rollFormula = `${numDice}d8 + ${wisdomMod}`;
|
|
1140
|
+
|
|
1141
|
+
// Create roll description
|
|
1142
|
+
const effectText = type === 'heal' ? 'Healing' : `${damageType} Damage`;
|
|
1143
|
+
|
|
1144
|
+
// TODO: Add Owlbear Rodeo integration for Divine Spark rolls
|
|
1145
|
+
|
|
1146
|
+
// Show notification
|
|
1147
|
+
if (typeof showNotification !== 'undefined') {
|
|
1148
|
+
showNotification(`✨ Divine Spark (${effectText})! Channel Divinity: ${channelDivinityResource.current}/${channelDivinityResource.max}`, 'success');
|
|
1149
|
+
}
|
|
1150
|
+
debug.log(`✨ Divine Spark used: ${effectText}`);
|
|
1151
|
+
|
|
1152
|
+
// Rebuild sheet to show updated Channel Divinity count
|
|
1153
|
+
if (typeof buildSheet !== 'undefined') {
|
|
1154
|
+
buildSheet(characterData);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Remove modal
|
|
1158
|
+
modal.remove();
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
// Add button click handlers
|
|
1162
|
+
document.getElementById('divine-spark-heal')?.addEventListener('click', () => {
|
|
1163
|
+
executeDivineSpark('heal', '#22c55e');
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
document.getElementById('divine-spark-necrotic')?.addEventListener('click', () => {
|
|
1167
|
+
executeDivineSpark('necrotic', '#8b5cf6', 'Necrotic');
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
document.getElementById('divine-spark-radiant')?.addEventListener('click', () => {
|
|
1171
|
+
executeDivineSpark('radiant', '#f59e0b', 'Radiant');
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
document.getElementById('divine-spark-cancel')?.addEventListener('click', () => {
|
|
1175
|
+
modal.remove();
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Close on overlay click
|
|
1179
|
+
modal.addEventListener('click', (e) => {
|
|
1180
|
+
if (e.target === modal) {
|
|
1181
|
+
modal.remove();
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// Add to document
|
|
1186
|
+
document.body.appendChild(modal);
|
|
1187
|
+
|
|
1188
|
+
// Wait for modal to be in DOM before adding event listeners
|
|
1189
|
+
requestAnimationFrame(() => {
|
|
1190
|
+
const healBtn = document.getElementById('divine-spark-heal');
|
|
1191
|
+
const necroticBtn = document.getElementById('divine-spark-necrotic');
|
|
1192
|
+
const radiantBtn = document.getElementById('divine-spark-radiant');
|
|
1193
|
+
const cancelBtn = document.getElementById('divine-spark-cancel');
|
|
1194
|
+
|
|
1195
|
+
healBtn?.addEventListener('click', () => executeDivineSpark('heal', '#22c55e'));
|
|
1196
|
+
necroticBtn?.addEventListener('click', () => executeDivineSpark('necrotic', '#8b5cf6', 'Necrotic'));
|
|
1197
|
+
radiantBtn?.addEventListener('click', () => executeDivineSpark('radiant', '#f59e0b', 'Radiant'));
|
|
1198
|
+
cancelBtn?.addEventListener('click', () => modal.remove());
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// ===== EXPORTS =====
|
|
1203
|
+
|
|
1204
|
+
globalThis.toggleInspiration = toggleInspiration;
|
|
1205
|
+
globalThis.showGainInspirationModal = showGainInspirationModal;
|
|
1206
|
+
globalThis.showUseInspirationModal = showUseInspirationModal;
|
|
1207
|
+
globalThis.showDivineSmiteModal = showDivineSmiteModal;
|
|
1208
|
+
globalThis.showLayOnHandsModal = showLayOnHandsModal;
|
|
1209
|
+
globalThis.showStarryFormModal = showStarryFormModal;
|
|
1210
|
+
globalThis.showDivineSparkModal = showDivineSparkModal;
|
|
1211
|
+
globalThis.showLuckyModal = showLuckyModal;
|
|
1212
|
+
globalThis.rollLuckyDie = rollLuckyDie;
|
|
1213
|
+
globalThis.getLuckyResource = getLuckyResource;
|
|
1214
|
+
globalThis.useLuckyPoint = useLuckyPoint;
|
|
1215
|
+
globalThis.getLayOnHandsResource = getLayOnHandsResource;
|
|
1216
|
+
globalThis.showWildShapeModal = showWildShapeModal;
|
|
1217
|
+
globalThis.createThemedModal = createThemedModal;
|
|
1218
|
+
|
|
1219
|
+
// ===== Safe fallbacks for not-yet-implemented feature/spell modals =====
|
|
1220
|
+
// action-display.js routes ~95 spells/features to dedicated show*Modal()
|
|
1221
|
+
// handlers, but only a handful are implemented. Calling an undefined one threw
|
|
1222
|
+
// a ReferenceError, so clicking those actions did nothing. Define a generic
|
|
1223
|
+
// fallback (announce the action) for every modal name that isn't already a
|
|
1224
|
+
// real implementation, so nothing crashes and the action still posts to chat.
|
|
1225
|
+
function genericFeatureModalFallback(action) {
|
|
1226
|
+
try {
|
|
1227
|
+
if (typeof announceAction === 'function') {
|
|
1228
|
+
announceAction(action || {});
|
|
1229
|
+
} else if (typeof showNotification === 'function') {
|
|
1230
|
+
showNotification(`${(action && action.name) || 'Action'} used`);
|
|
1231
|
+
}
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
(window.debug || console).warn('Feature modal fallback failed:', e);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
[
|
|
1238
|
+
'showAbsorbElementsModal','showAidModal','showAnimateObjectsModal','showArmorOfAgathysModal',
|
|
1239
|
+
'showAstralProjectionModal','showAuguryModal','showBaneModal','showBigbysHandModal','showBlessModal',
|
|
1240
|
+
'showBoomingBladeModal','showChaosBoltModal','showChromaticOrbModal','showCloneModal','showCloudOfDaggersModal',
|
|
1241
|
+
'showCommuneModal','showConjureModal','showContactOtherPlaneModal','showContingencyModal','showCounterspellModal',
|
|
1242
|
+
'showDelayedBlastFireballModal','showDetectMagicModal','showDispelEvilAndGoodModal','showDispelMagicModal',
|
|
1243
|
+
'showDivinationModal','showDivineInterventionModal','showDragonsBreathModal','showDreamModal',
|
|
1244
|
+
'showElementalWeaponModal','showEtherealnessModal','showFeatherFallModal','showFindThePathModal',
|
|
1245
|
+
'showFireShieldModal','showFlamingSphereModal','showForcecageModal','showFreedomOfMovementModal','showGateModal',
|
|
1246
|
+
'showGeasModal','showGlyphOfWardingModal','showGreaterRestorationModal','showGreenFlameBladeModal','showGuidanceModal',
|
|
1247
|
+
'showHarnessDivinePowerModal','showHasteModal','showHealingSpiritModal','showHellishRebukeModal','showHexModal',
|
|
1248
|
+
'showHuntersMarkModal','showIdentifyModal','showImprisonmentModal','showLegendLoreModal','showLifeTransferenceModal',
|
|
1249
|
+
'showMagicCircleModal','showMagicJarModal','showMagicMissileModal','showMazeModal','showMeldIntoStoneModal',
|
|
1250
|
+
'showMirageArcaneModal','showMoonbeamModal','showNondetectionModal','showPlanarBindingModal','showPolymorphModal',
|
|
1251
|
+
'showProgrammedIllusionModal','showProtectionFromEnergyModal','showProtectionFromEvilAndGoodModal','showRaiseDeadModal',
|
|
1252
|
+
'showRemoveCurseModal','showResistanceModal','showResurrectionModal','showRevivifyModal','showSanctuaryModal',
|
|
1253
|
+
'showScorchingRayModal','showScryingModal','showSendingModal','showSequesterModal','showShapechangeModal',
|
|
1254
|
+
'showShieldModal','showSilenceModal','showSimulacrumModal','showSpeakWithAnimalsModal','showSpeakWithDeadModal',
|
|
1255
|
+
'showSpeakWithPlantsModal','showSpikeGrowthModal','showSpiritGuardiansModal','showSpiritualWeaponModal','showSymbolModal',
|
|
1256
|
+
'showTeleportModal','showTimeStopModal','showTruePolymorphModal','showTrueResurrectionModal','showVampiricTouchModal',
|
|
1257
|
+
'showWallOfFireModal','showWishModal','showWordOfRecallModal','showZoneOfTruthModal',
|
|
1258
|
+
].forEach((name) => {
|
|
1259
|
+
if (typeof globalThis[name] !== 'function') {
|
|
1260
|
+
globalThis[name] = genericFeatureModalFallback;
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
})();
|