@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,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Manager Module
|
|
3
|
+
*
|
|
4
|
+
* Handles class resource tracking, consumption, and conversion.
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to window.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - buildResourcesDisplay()
|
|
9
|
+
* - adjustResource(resource)
|
|
10
|
+
* - getSorceryPointsResource()
|
|
11
|
+
* - getKiPointsResource()
|
|
12
|
+
* - findResourceByVariableName(variableName)
|
|
13
|
+
* - getResourceCostsFromAction(action)
|
|
14
|
+
* - getKiCostFromAction(action)
|
|
15
|
+
* - getSorceryPointCostFromAction(action)
|
|
16
|
+
* - decrementActionResources(action)
|
|
17
|
+
* - showConvertSlotToPointsModal()
|
|
18
|
+
* - showFontOfMagicModal()
|
|
19
|
+
* - showSpellSlotRestorationModal(channelDivinityResource, maxSlotLevel)
|
|
20
|
+
* - restoreSpellSlot(level, channelDivinityResource)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
(function() {
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
// ===== RESOURCE DISPLAY FUNCTIONS =====
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build resources display grid
|
|
30
|
+
*/
|
|
31
|
+
function buildResourcesDisplay() {
|
|
32
|
+
const container = document.getElementById('resources-container');
|
|
33
|
+
|
|
34
|
+
if (!container) {
|
|
35
|
+
debug.warn('⚠️ Resources container not found in DOM');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!characterData || !characterData.resources || characterData.resources.length === 0) {
|
|
40
|
+
container.innerHTML = '<p style="text-align: center; color: #666;">No class resources available</p>';
|
|
41
|
+
debug.log('⚠️ No resources in character data');
|
|
42
|
+
// Collapse the section when empty
|
|
43
|
+
if (typeof collapseSectionByContainerId !== 'undefined') {
|
|
44
|
+
collapseSectionByContainerId('resources-container');
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Expand the section when it has content
|
|
50
|
+
if (typeof expandSectionByContainerId !== 'undefined') {
|
|
51
|
+
expandSectionByContainerId('resources-container');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
debug.log(`📊 Building resources display with ${characterData.resources.length} resources:`,
|
|
55
|
+
characterData.resources.map(r => `${r.name} (${r.current}/${r.max})`));
|
|
56
|
+
|
|
57
|
+
const resourcesGrid = document.createElement('div');
|
|
58
|
+
resourcesGrid.className = 'spell-slots-grid'; // Reuse spell slot styling
|
|
59
|
+
|
|
60
|
+
characterData.resources.forEach(resource => {
|
|
61
|
+
// Skip resources with MAX = 0 (useless paladin amount resources)
|
|
62
|
+
if (resource.max === 0) {
|
|
63
|
+
debug.log(`⏭️ Skipping resource with MAX = 0: ${resource.name}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Skip Lucky resources since they have their own action button
|
|
68
|
+
const lowerName = resource.name.toLowerCase().trim();
|
|
69
|
+
if (lowerName.includes('lucky point') || lowerName.includes('luck point') || lowerName === 'lucky points' || lowerName === 'lucky') {
|
|
70
|
+
debug.log(`⏭️ Skipping Lucky resource from display: ${resource.name}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Skip HP resources since HP has its own dedicated UI section
|
|
75
|
+
if (lowerName.includes('hit point') || lowerName === 'hp' || lowerName === 'health' || lowerName.includes('hitpoint')) {
|
|
76
|
+
debug.log(`⏭️ Skipping HP resource from display: ${resource.name}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Skip "Spell Level" resource - this is metadata, not an actual resource
|
|
81
|
+
if (lowerName === 'spell level' || lowerName === 'spelllevel') {
|
|
82
|
+
debug.log(`⏭️ Skipping Spell Level resource from display: ${resource.name}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
debug.log(`📊 Displaying resource: ${resource.name} (${resource.current}/${resource.max})`);
|
|
87
|
+
|
|
88
|
+
const resourceCard = document.createElement('div');
|
|
89
|
+
resourceCard.className = resource.current > 0 ? 'spell-slot-card' : 'spell-slot-card empty';
|
|
90
|
+
resourceCard.innerHTML = `
|
|
91
|
+
<div class="spell-slot-level">${resource.name}</div>
|
|
92
|
+
<div class="spell-slot-count">${resource.current}/${resource.max}</div>
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
// Add click to manually adjust resource
|
|
96
|
+
resourceCard.addEventListener('click', () => {
|
|
97
|
+
adjustResource(resource);
|
|
98
|
+
});
|
|
99
|
+
resourceCard.style.cursor = 'pointer';
|
|
100
|
+
|
|
101
|
+
resourcesGrid.appendChild(resourceCard);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
container.innerHTML = '';
|
|
105
|
+
container.appendChild(resourcesGrid);
|
|
106
|
+
|
|
107
|
+
const note = document.createElement('p');
|
|
108
|
+
note.style.cssText = 'text-align: center; color: #95a5a6; font-size: 0.85em; margin-top: 10px;';
|
|
109
|
+
note.textContent = 'Click a resource to manually adjust';
|
|
110
|
+
container.appendChild(note);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Manual resource adjustment modal
|
|
115
|
+
*/
|
|
116
|
+
function adjustResource(resource) {
|
|
117
|
+
const newValue = prompt(`Adjust ${resource.name}\n\nCurrent: ${resource.current}/${resource.max}\n\nEnter new current value (0-${resource.max}):`);
|
|
118
|
+
|
|
119
|
+
if (newValue === null) return; // Cancelled
|
|
120
|
+
|
|
121
|
+
const parsed = parseInt(newValue);
|
|
122
|
+
if (isNaN(parsed) || parsed < 0 || parsed > resource.max) {
|
|
123
|
+
alert(`Please enter a number between 0 and ${resource.max}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
resource.current = parsed;
|
|
128
|
+
|
|
129
|
+
// Also update otherVariables to keep data in sync
|
|
130
|
+
if (characterData.otherVariables && resource.varName) {
|
|
131
|
+
characterData.otherVariables[resource.varName] = resource.current;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
135
|
+
saveCharacterData();
|
|
136
|
+
}
|
|
137
|
+
if (typeof buildSheet !== 'undefined') {
|
|
138
|
+
buildSheet(characterData);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof showNotification !== 'undefined') {
|
|
142
|
+
showNotification(`✅ ${resource.name} updated to ${resource.current}/${resource.max}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ===== RESOURCE FINDER FUNCTIONS =====
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get Sorcery Points resource (uses action-executor)
|
|
150
|
+
*/
|
|
151
|
+
function getSorceryPointsResource() {
|
|
152
|
+
if (typeof executorGetSorceryPointsResource !== 'undefined') {
|
|
153
|
+
return executorGetSorceryPointsResource(characterData);
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get Ki Points resource
|
|
160
|
+
*/
|
|
161
|
+
function getKiPointsResource() {
|
|
162
|
+
if (!characterData || !characterData.resources) return null;
|
|
163
|
+
|
|
164
|
+
// Find ki points in resources
|
|
165
|
+
const kiResource = characterData.resources.find(r => {
|
|
166
|
+
const lowerName = r.name.toLowerCase();
|
|
167
|
+
return lowerName.includes('ki point') || lowerName === 'ki points' || lowerName === 'ki';
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return kiResource || null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Find resource by variable name (with flexible Channel Divinity matching)
|
|
175
|
+
*/
|
|
176
|
+
function findResourceByVariableName(variableName) {
|
|
177
|
+
// Check for exact match first
|
|
178
|
+
let resource = characterData.resources?.find(r => r.variableName === variableName);
|
|
179
|
+
|
|
180
|
+
if (resource) {
|
|
181
|
+
return resource;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Special handling for Channel Divinity - try all possible variable names
|
|
185
|
+
if (variableName === 'channelDivinity' ||
|
|
186
|
+
variableName === 'channelDivinityCleric' ||
|
|
187
|
+
variableName === 'channelDivinityPaladin') {
|
|
188
|
+
resource = characterData.resources?.find(r =>
|
|
189
|
+
r.name === 'Channel Divinity' ||
|
|
190
|
+
r.variableName === 'channelDivinityCleric' ||
|
|
191
|
+
r.variableName === 'channelDivinityPaladin' ||
|
|
192
|
+
r.variableName === 'channelDivinity'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return resource;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ===== RESOURCE COST EXTRACTION FUNCTIONS =====
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get resource costs from action (uses DiceCloud structured data)
|
|
203
|
+
*/
|
|
204
|
+
function getResourceCostsFromAction(action) {
|
|
205
|
+
// Use DiceCloud's structured resource consumption data instead of regex parsing
|
|
206
|
+
if (!action || !action.resources || !action.resources.attributesConsumed) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const costs = action.resources.attributesConsumed.map(consumed => {
|
|
211
|
+
const quantity = consumed.quantity?.value || 0;
|
|
212
|
+
return {
|
|
213
|
+
name: consumed.statName || '',
|
|
214
|
+
variableName: consumed.variableName || '',
|
|
215
|
+
quantity: quantity
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (costs.length > 0) {
|
|
220
|
+
debug.log(`💰 Resource costs for ${action.name}:`, costs);
|
|
221
|
+
// Debug: Log each cost with its variableName
|
|
222
|
+
costs.forEach(cost => {
|
|
223
|
+
debug.log(` 📋 Cost: ${cost.name || 'unnamed'}, variableName: "${cost.variableName}", quantity: ${cost.quantity}`);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return costs;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get Ki cost from action (legacy compatibility)
|
|
232
|
+
*/
|
|
233
|
+
function getKiCostFromAction(action) {
|
|
234
|
+
const costs = getResourceCostsFromAction(action);
|
|
235
|
+
const kiCost = costs.find(c =>
|
|
236
|
+
c.variableName === 'kiPoints' ||
|
|
237
|
+
c.name.toLowerCase().includes('ki point')
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (kiCost) {
|
|
241
|
+
debug.log(`💨 Ki cost for ${action.name}: ${kiCost.quantity} ki points`);
|
|
242
|
+
return kiCost.quantity;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get Sorcery Point cost from action (legacy compatibility)
|
|
250
|
+
*/
|
|
251
|
+
function getSorceryPointCostFromAction(action) {
|
|
252
|
+
const costs = getResourceCostsFromAction(action);
|
|
253
|
+
const sorceryCost = costs.find(c =>
|
|
254
|
+
c.variableName === 'sorceryPoints' ||
|
|
255
|
+
c.name.toLowerCase().includes('sorcery point')
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (sorceryCost) {
|
|
259
|
+
debug.log(`✨ Sorcery Point cost for ${action.name}: ${sorceryCost.quantity} SP`);
|
|
260
|
+
return sorceryCost.quantity;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ===== RESOURCE CONSUMPTION FUNCTIONS =====
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Decrement action resources (Wild Shape uses, Breath Weapon uses, etc.)
|
|
270
|
+
*/
|
|
271
|
+
function decrementActionResources(action) {
|
|
272
|
+
// Decrement all resource costs for an action
|
|
273
|
+
const costs = getResourceCostsFromAction(action);
|
|
274
|
+
|
|
275
|
+
if (!costs || costs.length === 0) {
|
|
276
|
+
return true; // No resources to decrement
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check all resources have sufficient quantities before decrementing any
|
|
280
|
+
for (const cost of costs) {
|
|
281
|
+
// Skip Ki and Sorcery Points as they're handled separately
|
|
282
|
+
if (cost.variableName === 'kiPoints' || cost.variableName === 'sorceryPoints') {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!cost.variableName) {
|
|
287
|
+
debug.log(`⚠️ Resource cost missing variableName for ${action.name}:`, cost);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Find the resource in character data (with flexible Channel Divinity matching)
|
|
292
|
+
const resource = findResourceByVariableName(cost.variableName);
|
|
293
|
+
|
|
294
|
+
if (!resource) {
|
|
295
|
+
debug.log(`⚠️ Resource not found: ${cost.variableName} for ${action.name}`);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (resource.current < cost.quantity) {
|
|
300
|
+
if (typeof showNotification !== 'undefined') {
|
|
301
|
+
showNotification(`❌ Not enough ${cost.name || cost.variableName}! Need ${cost.quantity}, have ${resource.current}`, 'error');
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// All checks passed, now decrement the resources
|
|
308
|
+
for (const cost of costs) {
|
|
309
|
+
// Skip Ki and Sorcery Points as they're handled separately
|
|
310
|
+
if (cost.variableName === 'kiPoints' || cost.variableName === 'sorceryPoints') {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!cost.variableName) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Find the resource (with flexible Channel Divinity matching)
|
|
319
|
+
const resource = findResourceByVariableName(cost.variableName);
|
|
320
|
+
|
|
321
|
+
if (resource) {
|
|
322
|
+
resource.current -= cost.quantity;
|
|
323
|
+
|
|
324
|
+
// Also update otherVariables to keep data in sync
|
|
325
|
+
if (characterData.otherVariables && resource.varName) {
|
|
326
|
+
characterData.otherVariables[resource.varName] = resource.current;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
debug.log(`✅ Used ${cost.quantity} ${cost.name || cost.variableName} for ${action.name}. Remaining: ${resource.current}/${resource.max}`);
|
|
330
|
+
if (typeof showNotification !== 'undefined') {
|
|
331
|
+
showNotification(`✅ Used ${action.name}! (${resource.current}/${resource.max} ${cost.name || cost.variableName} left)`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
337
|
+
saveCharacterData();
|
|
338
|
+
}
|
|
339
|
+
if (typeof buildSheet !== 'undefined') {
|
|
340
|
+
buildSheet(characterData); // Refresh display
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ===== SORCERY POINT CONVERSION MODALS =====
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Convert spell slot to sorcery points (Font of Magic)
|
|
349
|
+
*/
|
|
350
|
+
function showConvertSlotToPointsModal() {
|
|
351
|
+
const sorceryPoints = getSorceryPointsResource();
|
|
352
|
+
|
|
353
|
+
if (!sorceryPoints) {
|
|
354
|
+
if (typeof showNotification !== 'undefined') {
|
|
355
|
+
showNotification('❌ No Sorcery Points resource found', 'error');
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Get available spell slots
|
|
361
|
+
const availableSlots = [];
|
|
362
|
+
for (let level = 1; level <= 9; level++) {
|
|
363
|
+
const slotVar = `level${level}SpellSlots`;
|
|
364
|
+
const maxSlotVar = `level${level}SpellSlotsMax`;
|
|
365
|
+
const current = characterData.spellSlots?.[slotVar] || 0;
|
|
366
|
+
const max = characterData.spellSlots?.[maxSlotVar] || 0;
|
|
367
|
+
|
|
368
|
+
if (current > 0) {
|
|
369
|
+
availableSlots.push({ level, current, max, slotVar, maxSlotVar });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (availableSlots.length === 0) {
|
|
374
|
+
if (typeof showNotification !== 'undefined') {
|
|
375
|
+
showNotification('❌ No spell slots available to convert!', 'error');
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Create modal
|
|
381
|
+
const modal = document.createElement('div');
|
|
382
|
+
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;';
|
|
383
|
+
|
|
384
|
+
const modalContent = document.createElement('div');
|
|
385
|
+
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); max-width: 400px; width: 90%;';
|
|
386
|
+
|
|
387
|
+
let optionsHTML = `
|
|
388
|
+
<h3 style="margin: 0 0 15px 0; color: var(--text-primary); text-align: center;">Convert Spell Slot to Sorcery Points</h3>
|
|
389
|
+
<p style="text-align: center; color: #e74c3c; margin-bottom: 20px; font-weight: bold;">Current: ${sorceryPoints.current}/${sorceryPoints.max} SP</p>
|
|
390
|
+
|
|
391
|
+
<div style="margin-bottom: 25px;">
|
|
392
|
+
<label style="display: block; margin-bottom: 10px; font-weight: bold; color: var(--text-primary);">Expend Spell Slot:</label>
|
|
393
|
+
<select id="slot-to-points-level" style="width: 100%; padding: 12px; 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);">
|
|
394
|
+
`;
|
|
395
|
+
|
|
396
|
+
availableSlots.forEach(slot => {
|
|
397
|
+
optionsHTML += `<option value="${slot.level}">Level ${slot.level} - Gain ${slot.level} SP (${slot.current}/${slot.max} slots)</option>`;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
optionsHTML += `
|
|
401
|
+
</select>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<div style="display: flex; gap: 10px;">
|
|
405
|
+
<button id="slot-cancel" style="flex: 1; padding: 12px; font-size: 1em; background: #95a5a6; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;">
|
|
406
|
+
Cancel
|
|
407
|
+
</button>
|
|
408
|
+
<button id="slot-confirm" style="flex: 1; padding: 12px; font-size: 1em; background: #9b59b6; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;">
|
|
409
|
+
Convert
|
|
410
|
+
</button>
|
|
411
|
+
</div>
|
|
412
|
+
`;
|
|
413
|
+
|
|
414
|
+
modalContent.innerHTML = optionsHTML;
|
|
415
|
+
modal.appendChild(modalContent);
|
|
416
|
+
document.body.appendChild(modal);
|
|
417
|
+
|
|
418
|
+
const selectElement = document.getElementById('slot-to-points-level');
|
|
419
|
+
const confirmBtn = document.getElementById('slot-confirm');
|
|
420
|
+
const cancelBtn = document.getElementById('slot-cancel');
|
|
421
|
+
|
|
422
|
+
cancelBtn.addEventListener('click', () => {
|
|
423
|
+
document.body.removeChild(modal);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
confirmBtn.addEventListener('click', () => {
|
|
427
|
+
const selectedLevel = parseInt(selectElement.value);
|
|
428
|
+
const slotVar = `level${selectedLevel}SpellSlots`;
|
|
429
|
+
const currentSlots = characterData.spellSlots?.[slotVar] || 0;
|
|
430
|
+
|
|
431
|
+
if (currentSlots <= 0) {
|
|
432
|
+
if (typeof showNotification !== 'undefined') {
|
|
433
|
+
showNotification(`❌ No Level ${selectedLevel} spell slots available!`, 'error');
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Remove spell slot
|
|
439
|
+
characterData.spellSlots[slotVar] -= 1;
|
|
440
|
+
|
|
441
|
+
// Gain sorcery points equal to slot level
|
|
442
|
+
const pointsGained = selectedLevel;
|
|
443
|
+
sorceryPoints.current = Math.min(sorceryPoints.current + pointsGained, sorceryPoints.max);
|
|
444
|
+
|
|
445
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
446
|
+
saveCharacterData();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const maxSlotVar = `level${selectedLevel}SpellSlotsMax`;
|
|
450
|
+
const newSlotCount = characterData.spellSlots[slotVar];
|
|
451
|
+
const maxSlots = characterData.spellSlots[maxSlotVar];
|
|
452
|
+
if (typeof showNotification !== 'undefined') {
|
|
453
|
+
showNotification(`✨ Gained ${pointsGained} Sorcery Points! (${sorceryPoints.current}/${sorceryPoints.max} SP, ${newSlotCount}/${maxSlots} slots)`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Announce to Roll20
|
|
457
|
+
if (typeof getColoredBanner !== 'undefined') {
|
|
458
|
+
const colorBanner = getColoredBanner(characterData);
|
|
459
|
+
const message = `&{template:default} {{name=${colorBanner}${characterData.name} uses Font of Magic⚡}} {{Action=Convert Spell Slot to Sorcery Points}} {{Result=Expended Level ${selectedLevel} spell slot for ${pointsGained} SP}} {{Sorcery Points=${sorceryPoints.current}/${sorceryPoints.max}}}`;
|
|
460
|
+
|
|
461
|
+
sendToRoll20({
|
|
462
|
+
action: 'roll',
|
|
463
|
+
characterName: characterData.name,
|
|
464
|
+
message: message,
|
|
465
|
+
color: characterData.notificationColor
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
document.body.removeChild(modal);
|
|
470
|
+
if (typeof buildSheet !== 'undefined') {
|
|
471
|
+
buildSheet(characterData); // Refresh display
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Convert sorcery points to spell slot (Font of Magic)
|
|
478
|
+
*/
|
|
479
|
+
function showFontOfMagicModal() {
|
|
480
|
+
const sorceryPoints = getSorceryPointsResource();
|
|
481
|
+
|
|
482
|
+
if (!sorceryPoints) {
|
|
483
|
+
if (typeof showNotification !== 'undefined') {
|
|
484
|
+
showNotification('❌ No Sorcery Points resource found', 'error');
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Font of Magic spell slot creation costs (D&D 5e rules)
|
|
490
|
+
const slotCosts = {
|
|
491
|
+
1: 2,
|
|
492
|
+
2: 3,
|
|
493
|
+
3: 5,
|
|
494
|
+
4: 6,
|
|
495
|
+
5: 7
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// Create modal
|
|
499
|
+
const modal = document.createElement('div');
|
|
500
|
+
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;';
|
|
501
|
+
|
|
502
|
+
const modalContent = document.createElement('div');
|
|
503
|
+
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); max-width: 400px; width: 90%;';
|
|
504
|
+
|
|
505
|
+
let optionsHTML = `
|
|
506
|
+
<h3 style="margin: 0 0 15px 0; color: var(--text-primary); text-align: center;">Convert Sorcery Points to Spell Slot</h3>
|
|
507
|
+
<p style="text-align: center; color: #e74c3c; margin-bottom: 20px; font-weight: bold;">Current: ${sorceryPoints.current}/${sorceryPoints.max} SP</p>
|
|
508
|
+
|
|
509
|
+
<div style="margin-bottom: 25px;">
|
|
510
|
+
<label style="display: block; margin-bottom: 10px; font-weight: bold; color: var(--text-primary);">Create Spell Slot Level:</label>
|
|
511
|
+
<select id="font-of-magic-slot" style="width: 100%; padding: 12px; 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);">
|
|
512
|
+
`;
|
|
513
|
+
|
|
514
|
+
// Add options for each spell slot level
|
|
515
|
+
for (let level = 1; level <= 5; level++) {
|
|
516
|
+
const cost = slotCosts[level];
|
|
517
|
+
const canAfford = sorceryPoints.current >= cost;
|
|
518
|
+
const slotVar = `level${level}SpellSlots`;
|
|
519
|
+
const maxSlotVar = `level${level}SpellSlotsMax`;
|
|
520
|
+
const currentSlots = characterData.spellSlots?.[slotVar] || 0;
|
|
521
|
+
const maxSlots = characterData.spellSlots?.[maxSlotVar] || 0;
|
|
522
|
+
|
|
523
|
+
const disabledAttr = canAfford ? '' : 'disabled';
|
|
524
|
+
const affordText = canAfford ? '' : ' (not enough SP)';
|
|
525
|
+
|
|
526
|
+
optionsHTML += `<option value="${level}" ${disabledAttr}>Level ${level} - ${cost} SP${affordText} (${currentSlots}/${maxSlots} slots)</option>`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
optionsHTML += `
|
|
530
|
+
</select>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<div style="display: flex; gap: 10px;">
|
|
534
|
+
<button id="font-cancel" style="flex: 1; padding: 12px; font-size: 1em; background: #95a5a6; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;">
|
|
535
|
+
Cancel
|
|
536
|
+
</button>
|
|
537
|
+
<button id="font-confirm" style="flex: 1; padding: 12px; font-size: 1em; background: #e74c3c; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;">
|
|
538
|
+
Convert
|
|
539
|
+
</button>
|
|
540
|
+
</div>
|
|
541
|
+
`;
|
|
542
|
+
|
|
543
|
+
modalContent.innerHTML = optionsHTML;
|
|
544
|
+
modal.appendChild(modalContent);
|
|
545
|
+
document.body.appendChild(modal);
|
|
546
|
+
|
|
547
|
+
const selectElement = document.getElementById('font-of-magic-slot');
|
|
548
|
+
const confirmBtn = document.getElementById('font-confirm');
|
|
549
|
+
const cancelBtn = document.getElementById('font-cancel');
|
|
550
|
+
|
|
551
|
+
cancelBtn.addEventListener('click', () => {
|
|
552
|
+
document.body.removeChild(modal);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
confirmBtn.addEventListener('click', () => {
|
|
556
|
+
const selectedLevel = parseInt(selectElement.value);
|
|
557
|
+
const cost = slotCosts[selectedLevel];
|
|
558
|
+
|
|
559
|
+
if (sorceryPoints.current < cost) {
|
|
560
|
+
if (typeof showNotification !== 'undefined') {
|
|
561
|
+
showNotification(`❌ Not enough Sorcery Points! Need ${cost}, have ${sorceryPoints.current}`, 'error');
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Deduct sorcery points
|
|
567
|
+
sorceryPoints.current -= cost;
|
|
568
|
+
|
|
569
|
+
// Add spell slot
|
|
570
|
+
const slotVar = `level${selectedLevel}SpellSlots`;
|
|
571
|
+
const maxSlotVar = `level${selectedLevel}SpellSlotsMax`;
|
|
572
|
+
const maxSlots = characterData.spellSlots?.[maxSlotVar] || 0;
|
|
573
|
+
|
|
574
|
+
characterData.spellSlots[slotVar] = Math.min((characterData.spellSlots[slotVar] || 0) + 1, maxSlots);
|
|
575
|
+
|
|
576
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
577
|
+
saveCharacterData();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const currentSlots = characterData.spellSlots[slotVar];
|
|
581
|
+
if (typeof showNotification !== 'undefined') {
|
|
582
|
+
showNotification(`✨ Created Level ${selectedLevel} spell slot! (${sorceryPoints.current}/${sorceryPoints.max} SP left, ${currentSlots}/${maxSlots} slots)`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Announce to Roll20
|
|
586
|
+
if (typeof getColoredBanner !== 'undefined') {
|
|
587
|
+
const colorBanner = getColoredBanner(characterData);
|
|
588
|
+
const message = `&{template:default} {{name=${colorBanner}${characterData.name} uses Font of Magic⚡}} {{Action=Convert Sorcery Points to Spell Slot}} {{Result=Created Level ${selectedLevel} spell slot for ${cost} SP}} {{Sorcery Points=${sorceryPoints.current}/${sorceryPoints.max}}}`;
|
|
589
|
+
|
|
590
|
+
sendToRoll20({
|
|
591
|
+
action: 'roll',
|
|
592
|
+
characterName: characterData.name,
|
|
593
|
+
message: message,
|
|
594
|
+
color: characterData.notificationColor
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
document.body.removeChild(modal);
|
|
599
|
+
if (typeof buildSheet !== 'undefined') {
|
|
600
|
+
buildSheet(characterData); // Refresh display
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ===== CHANNEL DIVINITY SPELL SLOT RESTORATION =====
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Show spell slot restoration modal (Harness Divine Power)
|
|
609
|
+
*/
|
|
610
|
+
function showSpellSlotRestorationModal(channelDivinityResource, maxSlotLevel) {
|
|
611
|
+
// Create modal overlay
|
|
612
|
+
const modal = document.createElement('div');
|
|
613
|
+
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;';
|
|
614
|
+
|
|
615
|
+
// Create modal content
|
|
616
|
+
const modalContent = document.createElement('div');
|
|
617
|
+
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;';
|
|
618
|
+
|
|
619
|
+
// Build spell slot buttons
|
|
620
|
+
let slotButtonsHTML = '';
|
|
621
|
+
const spellSlots = characterData.spellSlots || {};
|
|
622
|
+
|
|
623
|
+
for (let level = 1; level <= maxSlotLevel; level++) {
|
|
624
|
+
const slotVar = `level${level}SpellSlots`;
|
|
625
|
+
const slotMaxVar = `level${level}SpellSlotsMax`;
|
|
626
|
+
const current = spellSlots[slotVar] || 0;
|
|
627
|
+
const max = spellSlots[slotMaxVar] || 0;
|
|
628
|
+
|
|
629
|
+
const isAvailable = max > 0 && current < max;
|
|
630
|
+
const disabled = !isAvailable ? 'disabled' : '';
|
|
631
|
+
const bgColor = isAvailable ? '#9b59b6' : '#bdc3c7';
|
|
632
|
+
const cursor = isAvailable ? 'pointer' : 'not-allowed';
|
|
633
|
+
const opacity = isAvailable ? '1' : '0.6';
|
|
634
|
+
|
|
635
|
+
slotButtonsHTML += `
|
|
636
|
+
<button
|
|
637
|
+
class="spell-slot-restore-btn"
|
|
638
|
+
data-level="${level}"
|
|
639
|
+
${disabled}
|
|
640
|
+
style="width: 100%; padding: 15px; background: ${bgColor}; color: white; border: none; border-radius: 8px; cursor: ${cursor}; font-weight: bold; margin-bottom: 10px; opacity: ${opacity};">
|
|
641
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
642
|
+
<span>Level ${level} Spell Slot</span>
|
|
643
|
+
<span style="font-size: 0.9em;">${current}/${max}</span>
|
|
644
|
+
</div>
|
|
645
|
+
</button>
|
|
646
|
+
`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
modalContent.innerHTML = `
|
|
650
|
+
<h3 style="margin: 0 0 15px 0; color: var(--text-primary); text-align: center;">🔮 Harness Divine Power</h3>
|
|
651
|
+
<p style="text-align: center; margin-bottom: 20px; color: #555; font-size: 0.95em;">
|
|
652
|
+
Choose which spell slot to restore (max level ${maxSlotLevel})
|
|
653
|
+
</p>
|
|
654
|
+
<div style="margin-bottom: 20px;">
|
|
655
|
+
${slotButtonsHTML}
|
|
656
|
+
</div>
|
|
657
|
+
<button id="cancel-restore-modal" style="width: 100%; padding: 12px; background: #7f8c8d; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;">
|
|
658
|
+
Cancel
|
|
659
|
+
</button>
|
|
660
|
+
`;
|
|
661
|
+
|
|
662
|
+
modal.appendChild(modalContent);
|
|
663
|
+
document.body.appendChild(modal);
|
|
664
|
+
|
|
665
|
+
// Add click handlers to spell slot buttons
|
|
666
|
+
const slotButtons = modal.querySelectorAll('.spell-slot-restore-btn:not([disabled])');
|
|
667
|
+
slotButtons.forEach(button => {
|
|
668
|
+
button.addEventListener('click', () => {
|
|
669
|
+
const level = parseInt(button.getAttribute('data-level'));
|
|
670
|
+
restoreSpellSlot(level, channelDivinityResource);
|
|
671
|
+
modal.remove();
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Cancel button
|
|
676
|
+
document.getElementById('cancel-restore-modal').addEventListener('click', () => {
|
|
677
|
+
modal.remove();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Click outside to close
|
|
681
|
+
modal.addEventListener('click', (e) => {
|
|
682
|
+
if (e.target === modal) {
|
|
683
|
+
modal.remove();
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Restore a spell slot using Channel Divinity (Harness Divine Power)
|
|
690
|
+
*/
|
|
691
|
+
function restoreSpellSlot(level, channelDivinityResource) {
|
|
692
|
+
const slotVar = `level${level}SpellSlots`;
|
|
693
|
+
const slotMaxVar = `level${level}SpellSlotsMax`;
|
|
694
|
+
|
|
695
|
+
if (!characterData.spellSlots) {
|
|
696
|
+
if (typeof showNotification !== 'undefined') {
|
|
697
|
+
showNotification('❌ No spell slots available!', 'error');
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const current = characterData.spellSlots[slotVar] || 0;
|
|
703
|
+
const max = characterData.spellSlots[slotMaxVar] || 0;
|
|
704
|
+
|
|
705
|
+
if (max === 0) {
|
|
706
|
+
if (typeof showNotification !== 'undefined') {
|
|
707
|
+
showNotification('❌ No spell slots at that level!', 'error');
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (current >= max) {
|
|
713
|
+
if (typeof showNotification !== 'undefined') {
|
|
714
|
+
showNotification(`❌ Level ${level} spell slots already full!`, 'error');
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Restore the spell slot
|
|
720
|
+
characterData.spellSlots[slotVar] = Math.min(current + 1, max);
|
|
721
|
+
|
|
722
|
+
// Expend Channel Divinity use
|
|
723
|
+
channelDivinityResource.current = Math.max(0, channelDivinityResource.current - 1);
|
|
724
|
+
|
|
725
|
+
// Update character data - sync with otherVariables using the correct variable name
|
|
726
|
+
if (characterData.otherVariables && channelDivinityResource.variableName) {
|
|
727
|
+
characterData.otherVariables[channelDivinityResource.variableName] = channelDivinityResource.current;
|
|
728
|
+
} else if (characterData.otherVariables && channelDivinityResource.varName) {
|
|
729
|
+
characterData.otherVariables[channelDivinityResource.varName] = channelDivinityResource.current;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
733
|
+
saveCharacterData();
|
|
734
|
+
}
|
|
735
|
+
if (typeof buildSheet !== 'undefined') {
|
|
736
|
+
buildSheet(characterData);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Announce to Roll20
|
|
740
|
+
if (typeof getColoredBanner !== 'undefined') {
|
|
741
|
+
const colorBanner = getColoredBanner(characterData);
|
|
742
|
+
const newCurrent = characterData.spellSlots[slotVar];
|
|
743
|
+
const messageData = {
|
|
744
|
+
action: 'announceSpell',
|
|
745
|
+
message: `&{template:default} {{name=${colorBanner}${characterData.name} uses Harness Divine Power}} {{🔮=Restored a Level ${level} spell slot! (${newCurrent}/${max})}}`,
|
|
746
|
+
color: characterData.notificationColor
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// Send to Roll20
|
|
750
|
+
sendToRoll20(messageData);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (typeof showNotification !== 'undefined') {
|
|
754
|
+
showNotification(`🔮 Harness Divine Power! Restored Level ${level} spell slot. Channel Divinity: ${channelDivinityResource.current}/${channelDivinityResource.max}`);
|
|
755
|
+
}
|
|
756
|
+
debug.log(`✨ Harness Divine Power used to restore Level ${level} spell slot`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ===== EXPORTS =====
|
|
760
|
+
|
|
761
|
+
window.buildResourcesDisplay = buildResourcesDisplay;
|
|
762
|
+
window.adjustResource = adjustResource;
|
|
763
|
+
window.getSorceryPointsResource = getSorceryPointsResource;
|
|
764
|
+
window.getKiPointsResource = getKiPointsResource;
|
|
765
|
+
window.findResourceByVariableName = findResourceByVariableName;
|
|
766
|
+
window.getResourceCostsFromAction = getResourceCostsFromAction;
|
|
767
|
+
window.getKiCostFromAction = getKiCostFromAction;
|
|
768
|
+
window.getSorceryPointCostFromAction = getSorceryPointCostFromAction;
|
|
769
|
+
window.decrementActionResources = decrementActionResources;
|
|
770
|
+
window.showConvertSlotToPointsModal = showConvertSlotToPointsModal;
|
|
771
|
+
window.showFontOfMagicModal = showFontOfMagicModal;
|
|
772
|
+
window.showSpellSlotRestorationModal = showSpellSlotRestorationModal;
|
|
773
|
+
window.restoreSpellSlot = restoreSpellSlot;
|
|
774
|
+
|
|
775
|
+
})();
|