@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,1557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Display Module
|
|
3
|
+
*
|
|
4
|
+
* Handles rendering of actions and action cards.
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to window.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - buildActionsDisplay(container, actions)
|
|
9
|
+
* - decrementActionUses(action)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
(function() {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// ===== MAIN DISPLAY FUNCTION =====
|
|
16
|
+
|
|
17
|
+
function buildActionsDisplay(container, actions) {
|
|
18
|
+
// Clear container
|
|
19
|
+
container.innerHTML = '';
|
|
20
|
+
|
|
21
|
+
// Class feature toggle states
|
|
22
|
+
let sneakAttackEnabled = false;
|
|
23
|
+
let sneakAttackDamage = null;
|
|
24
|
+
let elementalWeaponEnabled = false;
|
|
25
|
+
let elementalWeaponDamage = null;
|
|
26
|
+
|
|
27
|
+
// DEBUG: Log all actions to see what we have
|
|
28
|
+
debug.log('🔍 buildActionsDisplay called with actions:', actions.map(a => ({ name: a.name, damage: a.damage, actionType: a.actionType })));
|
|
29
|
+
debug.log('🔍 Total actions received:', actions.length);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize action name by removing common suffixes that indicate variants
|
|
33
|
+
* @param {string} name - The action name
|
|
34
|
+
* @returns {string} Normalized name for deduplication
|
|
35
|
+
*/
|
|
36
|
+
function normalizeActionName(name) {
|
|
37
|
+
if (!name) return '';
|
|
38
|
+
|
|
39
|
+
// Remove common suffixes that indicate the same ability but free/different action type
|
|
40
|
+
const suffixPatterns = [
|
|
41
|
+
/\s*\(free\)$/i,
|
|
42
|
+
/\s*\(free action\)$/i,
|
|
43
|
+
/\s*\(bonus action\)$/i,
|
|
44
|
+
/\s*\(bonus\)$/i,
|
|
45
|
+
/\s*\(reaction\)$/i,
|
|
46
|
+
/\s*\(action\)$/i,
|
|
47
|
+
/\s*\(no spell slot\)$/i,
|
|
48
|
+
/\s*\(at will\)$/i
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
let normalized = name.trim();
|
|
52
|
+
for (const pattern of suffixPatterns) {
|
|
53
|
+
normalized = normalized.replace(pattern, '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return normalized.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Deduplicate actions by normalized name and combine sources (similar to spells)
|
|
60
|
+
const deduplicatedActions = [];
|
|
61
|
+
const actionsByNormalizedName = {};
|
|
62
|
+
|
|
63
|
+
// Sort actions by name for consistent processing
|
|
64
|
+
// Prefer base names without suffixes (shorter names first within same normalized group)
|
|
65
|
+
const sortedActions = [...actions].sort((a, b) => {
|
|
66
|
+
const normA = normalizeActionName(a.name || '');
|
|
67
|
+
const normB = normalizeActionName(b.name || '');
|
|
68
|
+
|
|
69
|
+
// First sort by normalized name
|
|
70
|
+
if (normA !== normB) {
|
|
71
|
+
return normA.localeCompare(normB);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Within same normalized name, prefer shorter names (base versions)
|
|
75
|
+
return (a.name || '').length - (b.name || '').length;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
sortedActions.forEach(action => {
|
|
79
|
+
const actionName = (action.name || '').trim();
|
|
80
|
+
const normalizedName = normalizeActionName(actionName);
|
|
81
|
+
|
|
82
|
+
if (!normalizedName) {
|
|
83
|
+
debug.log('⚠️ Skipping action with no name');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!actionsByNormalizedName[normalizedName]) {
|
|
88
|
+
// First occurrence of this action (by normalized name)
|
|
89
|
+
actionsByNormalizedName[normalizedName] = action;
|
|
90
|
+
deduplicatedActions.push(action);
|
|
91
|
+
debug.log(`📝 First occurrence of action: "${actionName}" (normalized: "${normalizedName}")`);
|
|
92
|
+
} else {
|
|
93
|
+
// Duplicate action - combine sources and other properties
|
|
94
|
+
const existingAction = actionsByNormalizedName[normalizedName];
|
|
95
|
+
|
|
96
|
+
// Combine sources if they exist
|
|
97
|
+
if (action.source && !existingAction.source.includes(action.source)) {
|
|
98
|
+
existingAction.source = existingAction.source
|
|
99
|
+
? existingAction.source + '; ' + action.source
|
|
100
|
+
: action.source;
|
|
101
|
+
debug.log(`📝 Combined duplicate action "${actionName}": ${existingAction.source}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Combine descriptions if they exist and are different
|
|
105
|
+
if (action.description && action.description !== existingAction.description) {
|
|
106
|
+
existingAction.description = existingAction.description
|
|
107
|
+
? existingAction.description + '\n\n' + action.description
|
|
108
|
+
: action.description;
|
|
109
|
+
debug.log(`📝 Combined descriptions for "${actionName}"`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Merge other useful properties
|
|
113
|
+
if (action.uses && !existingAction.uses) {
|
|
114
|
+
existingAction.uses = action.uses;
|
|
115
|
+
debug.log(`📝 Added uses to "${actionName}"`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (action.damage && !existingAction.damage) {
|
|
119
|
+
existingAction.damage = action.damage;
|
|
120
|
+
debug.log(`📝 Added damage to "${actionName}"`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (action.attackRoll && !existingAction.attackRoll) {
|
|
124
|
+
existingAction.attackRoll = action.attackRoll;
|
|
125
|
+
debug.log(`📝 Added attackRoll to "${actionName}"`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
debug.log(`🔄 Merged duplicate action: "${actionName}"`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
debug.log(`📊 Deduplicated ${actions.length} actions to ${deduplicatedActions.length} unique actions`);
|
|
133
|
+
|
|
134
|
+
// Apply filters
|
|
135
|
+
let filteredActions = deduplicatedActions.filter(action => {
|
|
136
|
+
const actionName = (action.name || '').toLowerCase();
|
|
137
|
+
|
|
138
|
+
// Filter out duplicate Divine Smite entries - keep only the main one
|
|
139
|
+
if (actionName.includes('divine smite')) {
|
|
140
|
+
// Skip variants like "Divine Smite Level 1", "Divine Smite (Against Fiends, Critical) Level 1", etc.
|
|
141
|
+
// Keep only the base "Divine Smite" entry
|
|
142
|
+
if (actionName !== 'divine smite' && !actionName.match(/^divine smite$/)) {
|
|
143
|
+
debug.log(`⏭️ Filtering out duplicate Divine Smite entry: ${action.name}`);
|
|
144
|
+
return false;
|
|
145
|
+
} else {
|
|
146
|
+
debug.log(`✅ Keeping main Divine Smite entry: ${action.name}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Debug: Log all Lay on Hands related actions
|
|
151
|
+
if (actionName.includes('lay on hands')) {
|
|
152
|
+
const normalizedActionName = action.name.toLowerCase()
|
|
153
|
+
.replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
|
|
154
|
+
.replace(/\s+/g, ' ') // Normalize spaces
|
|
155
|
+
.trim();
|
|
156
|
+
const normalizedSearch = 'lay on hands: heal';
|
|
157
|
+
|
|
158
|
+
debug.log(`🔍 Found Lay on Hands action: "${action.name}"`);
|
|
159
|
+
debug.log(`🔍 Normalized action name: "${normalizedActionName}"`);
|
|
160
|
+
debug.log(`🔍 Normalized search term: "${normalizedSearch}"`);
|
|
161
|
+
debug.log(`🔍 Do they match? ${normalizedActionName === normalizedSearch}`);
|
|
162
|
+
debug.log(`🔍 Action object:`, action);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Filter by action type
|
|
166
|
+
if (actionFilters.actionType !== 'all') {
|
|
167
|
+
const actionType = (action.actionType || '').toLowerCase();
|
|
168
|
+
if (actionType !== actionFilters.actionType) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Filter by category
|
|
174
|
+
if (actionFilters.category !== 'all') {
|
|
175
|
+
const category = categorizeAction(action);
|
|
176
|
+
if (category !== actionFilters.category) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Filter by search term
|
|
182
|
+
if (actionFilters.search) {
|
|
183
|
+
const searchLower = actionFilters.search;
|
|
184
|
+
const name = (action.name || '').toLowerCase();
|
|
185
|
+
const desc = (action.description || '').toLowerCase();
|
|
186
|
+
if (!name.includes(searchLower) && !desc.includes(searchLower)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
debug.log(`🔍 Filtered ${deduplicatedActions.length} actions to ${filteredActions.length} actions`);
|
|
195
|
+
|
|
196
|
+
// Check if character has Sneak Attack available (from DiceCloud)
|
|
197
|
+
// We only check if it EXISTS, not whether it's enabled on DiceCloud
|
|
198
|
+
// The toggle state on our sheet is independent and user-controlled
|
|
199
|
+
// Use flexible matching in case the name has slight variations
|
|
200
|
+
const sneakAttackAction = deduplicatedActions.find(a =>
|
|
201
|
+
a.name === 'Sneak Attack' ||
|
|
202
|
+
a.name.toLowerCase().includes('sneak attack')
|
|
203
|
+
);
|
|
204
|
+
debug.log('🎯 Sneak Attack search result:', sneakAttackAction);
|
|
205
|
+
if (sneakAttackAction && sneakAttackAction.damage) {
|
|
206
|
+
sneakAttackDamage = sneakAttackAction.damage;
|
|
207
|
+
|
|
208
|
+
// Resolve variables in the damage formula for display
|
|
209
|
+
const resolvedDamage = resolveVariablesInFormula(sneakAttackDamage);
|
|
210
|
+
debug.log(`🎯 Sneak Attack damage: "${sneakAttackDamage}" resolved to "${resolvedDamage}"`);
|
|
211
|
+
|
|
212
|
+
// Add toggle section at the top of actions
|
|
213
|
+
const toggleSection = document.createElement('div');
|
|
214
|
+
toggleSection.style.cssText = 'background: #2c3e50; color: white; padding: 10px; border-radius: 5px; margin-bottom: 10px; display: flex; align-items: center; gap: 10px;';
|
|
215
|
+
|
|
216
|
+
const toggleLabel = document.createElement('label');
|
|
217
|
+
toggleLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: bold;';
|
|
218
|
+
|
|
219
|
+
const checkbox = document.createElement('input');
|
|
220
|
+
checkbox.type = 'checkbox';
|
|
221
|
+
checkbox.id = 'sneak-attack-toggle';
|
|
222
|
+
checkbox.checked = sneakAttackEnabled; // Always starts false - IGNORES DiceCloud toggle state
|
|
223
|
+
checkbox.style.cssText = 'width: 18px; height: 18px; cursor: pointer;';
|
|
224
|
+
checkbox.addEventListener('change', (e) => {
|
|
225
|
+
sneakAttackEnabled = e.target.checked;
|
|
226
|
+
debug.log(`🎯 Sneak Attack toggle on our sheet: ${sneakAttackEnabled ? 'ON' : 'OFF'} (independent of DiceCloud)`);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const labelText = document.createElement('span');
|
|
230
|
+
labelText.textContent = `Add Sneak Attack (${resolvedDamage}) to weapon damage`;
|
|
231
|
+
|
|
232
|
+
toggleLabel.appendChild(checkbox);
|
|
233
|
+
toggleLabel.appendChild(labelText);
|
|
234
|
+
toggleSection.appendChild(toggleLabel);
|
|
235
|
+
container.appendChild(toggleSection);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check if character has Elemental Weapon spell prepared (check spells list)
|
|
239
|
+
// We only check if it EXISTS, the toggle is user-controlled
|
|
240
|
+
const hasElementalWeapon = characterData.spells && characterData.spells.some(s =>
|
|
241
|
+
s.name === 'Elemental Weapon' || (s.spell && s.spell.name === 'Elemental Weapon')
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (hasElementalWeapon) {
|
|
245
|
+
debug.log(`⚔️ Elemental Weapon spell found, adding toggle`);
|
|
246
|
+
// Set default elemental weapon damage (typically 1d4, but can vary by spell slot)
|
|
247
|
+
elementalWeaponDamage = '1d4';
|
|
248
|
+
|
|
249
|
+
// Add toggle section for Elemental Weapon
|
|
250
|
+
const elementalToggleSection = document.createElement('div');
|
|
251
|
+
elementalToggleSection.style.cssText = 'background: #8b4513; color: white; padding: 10px; border-radius: 5px; margin-bottom: 10px; display: flex; align-items: center; gap: 10px;';
|
|
252
|
+
|
|
253
|
+
const elementalToggleLabel = document.createElement('label');
|
|
254
|
+
elementalToggleLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: bold;';
|
|
255
|
+
|
|
256
|
+
const elementalCheckbox = document.createElement('input');
|
|
257
|
+
elementalCheckbox.type = 'checkbox';
|
|
258
|
+
elementalCheckbox.id = 'elemental-weapon-toggle';
|
|
259
|
+
elementalCheckbox.checked = elementalWeaponEnabled; // Always starts false
|
|
260
|
+
elementalCheckbox.style.cssText = 'width: 18px; height: 18px; cursor: pointer;';
|
|
261
|
+
elementalCheckbox.addEventListener('change', (e) => {
|
|
262
|
+
elementalWeaponEnabled = e.target.checked;
|
|
263
|
+
debug.log(`⚔️ Elemental Weapon toggle: ${elementalWeaponEnabled ? 'ON' : 'OFF'}`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const elementalLabelText = document.createElement('span');
|
|
267
|
+
elementalLabelText.textContent = `Add Elemental Weapon (${elementalWeaponDamage}) to weapon damage`;
|
|
268
|
+
|
|
269
|
+
elementalToggleLabel.appendChild(elementalCheckbox);
|
|
270
|
+
elementalToggleLabel.appendChild(elementalLabelText);
|
|
271
|
+
elementalToggleSection.appendChild(elementalToggleLabel);
|
|
272
|
+
container.appendChild(elementalToggleSection);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if character has Lucky feat
|
|
276
|
+
const hasLuckyFeat = characterData.features && characterData.features.some(f =>
|
|
277
|
+
f.name && f.name.toLowerCase().includes('lucky')
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (hasLuckyFeat) {
|
|
281
|
+
debug.log(`🎖️ Lucky feat found, adding action button`);
|
|
282
|
+
|
|
283
|
+
// Add action button for Lucky feat
|
|
284
|
+
const luckyActionSection = document.createElement('div');
|
|
285
|
+
luckyActionSection.style.cssText = 'background: #f39c12; color: white; padding: 12px; border-radius: 5px; margin-bottom: 10px;';
|
|
286
|
+
|
|
287
|
+
const luckyButton = document.createElement('button');
|
|
288
|
+
luckyButton.id = 'lucky-action-button';
|
|
289
|
+
luckyButton.style.cssText = `
|
|
290
|
+
background: #e67e22;
|
|
291
|
+
color: white;
|
|
292
|
+
border: none;
|
|
293
|
+
padding: 10px 16px;
|
|
294
|
+
border-radius: 5px;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
font-size: 14px;
|
|
297
|
+
font-weight: bold;
|
|
298
|
+
width: 100%;
|
|
299
|
+
transition: background 0.2s;
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
justify-content: center;
|
|
303
|
+
gap: 8px;
|
|
304
|
+
`;
|
|
305
|
+
luckyButton.onmouseover = () => luckyButton.style.background = '#d35400';
|
|
306
|
+
luckyButton.onmouseout = () => luckyButton.style.background = '#e67e22';
|
|
307
|
+
|
|
308
|
+
// Update button text based on available luck points
|
|
309
|
+
const luckyResource = getLuckyResource();
|
|
310
|
+
const luckPointsAvailable = luckyResource ? luckyResource.current : 0;
|
|
311
|
+
luckyButton.innerHTML = `
|
|
312
|
+
<span style="font-size: 16px;">🎖️</span>
|
|
313
|
+
<span>Use Lucky Point (${luckPointsAvailable}/3)</span>
|
|
314
|
+
`;
|
|
315
|
+
|
|
316
|
+
luckyButton.addEventListener('click', () => {
|
|
317
|
+
const currentLuckyResource = getLuckyResource();
|
|
318
|
+
if (!currentLuckyResource || currentLuckyResource.current <= 0) {
|
|
319
|
+
showNotification('❌ No luck points available!', 'error');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Show simple Lucky modal like metamagic
|
|
324
|
+
showLuckyModal();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
luckyActionSection.appendChild(luckyButton);
|
|
328
|
+
container.appendChild(luckyActionSection);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
filteredActions.forEach((action, index) => {
|
|
332
|
+
// Skip rendering standalone Sneak Attack button if it exists
|
|
333
|
+
if ((action.name === 'Sneak Attack' || action.name.toLowerCase().includes('sneak attack')) && action.actionType === 'feature') {
|
|
334
|
+
debug.log('⏭️ Skipping standalone Sneak Attack button (using toggle instead)');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Clean up weapon damage to remove sneak attack if it was auto-added
|
|
339
|
+
// Pattern: remove any multi-dice formulas like "+3d6", "+4d6", etc. that come after the base damage
|
|
340
|
+
if (action.damage && action.attackRoll && sneakAttackDamage) {
|
|
341
|
+
// Remove the sneak attack damage pattern from weapon damage
|
|
342
|
+
const sneakPattern = new RegExp(`\\+?${sneakAttackDamage.replace(/[+\-]/g, '')}`, 'g');
|
|
343
|
+
const cleanedDamage = action.damage.replace(sneakPattern, '');
|
|
344
|
+
if (cleanedDamage !== action.damage) {
|
|
345
|
+
debug.log(`🧹 Cleaned weapon damage: "${action.damage}" -> "${cleanedDamage}"`);
|
|
346
|
+
action.damage = cleanedDamage;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const actionCard = document.createElement('div');
|
|
351
|
+
actionCard.className = 'action-card';
|
|
352
|
+
|
|
353
|
+
const actionHeader = document.createElement('div');
|
|
354
|
+
actionHeader.className = 'action-header';
|
|
355
|
+
|
|
356
|
+
const nameDiv = document.createElement('div');
|
|
357
|
+
nameDiv.className = 'action-name';
|
|
358
|
+
|
|
359
|
+
// Show uses if available
|
|
360
|
+
let nameText = action.name;
|
|
361
|
+
|
|
362
|
+
// Rename "Recover Spell Slot" to "Harness Divine Power" (Cleric feature)
|
|
363
|
+
if (nameText === 'Recover Spell Slot') {
|
|
364
|
+
nameText = 'Harness Divine Power';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (action.uses) {
|
|
368
|
+
const usesTotal = action.uses.total || action.uses.value || action.uses;
|
|
369
|
+
// Prefer usesLeft from DiceCloud if available, otherwise calculate from usesUsed
|
|
370
|
+
const usesRemaining = action.usesLeft !== undefined ? action.usesLeft : (usesTotal - (action.usesUsed || 0));
|
|
371
|
+
nameText += ` <span class="uses-badge">${usesRemaining}/${usesTotal} uses</span>`;
|
|
372
|
+
}
|
|
373
|
+
nameDiv.innerHTML = nameText;
|
|
374
|
+
|
|
375
|
+
const buttonsDiv = document.createElement('div');
|
|
376
|
+
buttonsDiv.className = 'action-buttons';
|
|
377
|
+
|
|
378
|
+
// Get action options with edge case modifications
|
|
379
|
+
const actionOptionsResult = getActionOptions(action);
|
|
380
|
+
const actionOptions = actionOptionsResult.options;
|
|
381
|
+
|
|
382
|
+
// Check if this is a "utility only" action that should just announce
|
|
383
|
+
if (actionOptionsResult.skipNormalButtons) {
|
|
384
|
+
// Create simple action button for utility-only actions
|
|
385
|
+
const actionBtn = document.createElement('button');
|
|
386
|
+
actionBtn.className = 'action-btn';
|
|
387
|
+
actionBtn.textContent = '✨ Use';
|
|
388
|
+
actionBtn.style.cssText = `
|
|
389
|
+
background: #9b59b6;
|
|
390
|
+
color: white;
|
|
391
|
+
border: none;
|
|
392
|
+
padding: 8px 12px;
|
|
393
|
+
border-radius: 4px;
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
font-size: 12px;
|
|
396
|
+
font-weight: bold;
|
|
397
|
+
`;
|
|
398
|
+
actionBtn.addEventListener('click', () => {
|
|
399
|
+
// Check for Divine Smite special handling
|
|
400
|
+
if (action.name.toLowerCase().includes('divine smite')) {
|
|
401
|
+
showDivineSmiteModal(action);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check for Lay on Hands: Heal special handling
|
|
406
|
+
const normalizedActionName = action.name.toLowerCase()
|
|
407
|
+
.replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
|
|
408
|
+
.replace(/\s+/g, ' ') // Normalize spaces
|
|
409
|
+
.trim();
|
|
410
|
+
const normalizedSearch = 'lay on hands: heal';
|
|
411
|
+
|
|
412
|
+
if (normalizedActionName === normalizedSearch) {
|
|
413
|
+
debug.log(`💚 Lay on Hands: Heal action clicked: ${action.name}, showing custom modal`);
|
|
414
|
+
debug.log(`💚 Normalized match: "${normalizedActionName}" === "${normalizedSearch}"`);
|
|
415
|
+
const layOnHandsPool = getLayOnHandsResource();
|
|
416
|
+
if (layOnHandsPool) {
|
|
417
|
+
showLayOnHandsModal(layOnHandsPool);
|
|
418
|
+
} else {
|
|
419
|
+
showNotification('❌ No Lay on Hands pool resource found', 'error');
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Fallback: Catch ANY Lay on Hands action for debugging
|
|
425
|
+
if (action.name.toLowerCase().includes('lay on hands')) {
|
|
426
|
+
debug.log(`🚨 FALLBACK: Caught Lay on Hands action: "${action.name}"`);
|
|
427
|
+
debug.log(`🚨 This action didn't match 'lay on hands: heal' but contains 'lay on hands'`);
|
|
428
|
+
debug.log(`🚨 Showing modal anyway for debugging`);
|
|
429
|
+
const layOnHandsPool = getLayOnHandsResource();
|
|
430
|
+
if (layOnHandsPool) {
|
|
431
|
+
showLayOnHandsModal(layOnHandsPool);
|
|
432
|
+
} else {
|
|
433
|
+
showNotification('❌ No Lay on Hands pool resource found', 'error');
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check and decrement uses BEFORE announcing (so announcement shows correct count)
|
|
439
|
+
if (action.uses && !decrementActionUses(action)) {
|
|
440
|
+
return; // No uses remaining
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check and decrement other resources
|
|
444
|
+
if (!decrementActionResources(action)) {
|
|
445
|
+
return; // Not enough resources
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Announce the action with description AFTER decrements
|
|
449
|
+
announceAction(action);
|
|
450
|
+
});
|
|
451
|
+
buttonsDiv.appendChild(actionBtn);
|
|
452
|
+
} else {
|
|
453
|
+
// Create buttons for each action option
|
|
454
|
+
let actionAnnounced = false; // Track if action has been announced
|
|
455
|
+
actionOptions.forEach((option, optionIndex) => {
|
|
456
|
+
const actionBtn = document.createElement('button');
|
|
457
|
+
actionBtn.className = `${option.type}-btn`;
|
|
458
|
+
|
|
459
|
+
// Add edge case note if present
|
|
460
|
+
const edgeCaseNote = option.edgeCaseNote ? `<div style="font-size: 0.7em; color: #666; margin-top: 1px;">${option.edgeCaseNote}</div>` : '';
|
|
461
|
+
actionBtn.innerHTML = `${option.label}${edgeCaseNote}`;
|
|
462
|
+
|
|
463
|
+
actionBtn.style.cssText = `
|
|
464
|
+
background: ${option.color};
|
|
465
|
+
color: white;
|
|
466
|
+
border: none;
|
|
467
|
+
padding: 8px 12px;
|
|
468
|
+
border-radius: 4px;
|
|
469
|
+
cursor: pointer;
|
|
470
|
+
font-size: 12px;
|
|
471
|
+
font-weight: bold;
|
|
472
|
+
margin-right: 4px;
|
|
473
|
+
margin-bottom: 4px;
|
|
474
|
+
`;
|
|
475
|
+
|
|
476
|
+
actionBtn.addEventListener('click', () => {
|
|
477
|
+
// Check for Divine Smite special handling
|
|
478
|
+
if (action.name.toLowerCase().includes('divine smite')) {
|
|
479
|
+
showDivineSmiteModal(action);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Check for Lay on Hands: Heal special handling
|
|
484
|
+
const normalizedActionName = action.name.toLowerCase()
|
|
485
|
+
.replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
|
|
486
|
+
.replace(/\s+/g, ' ') // Normalize spaces
|
|
487
|
+
.trim();
|
|
488
|
+
const normalizedSearch = 'lay on hands: heal';
|
|
489
|
+
|
|
490
|
+
if (normalizedActionName === normalizedSearch) {
|
|
491
|
+
debug.log(`💚 Lay on Hands: Heal action clicked: ${action.name}, showing custom modal`);
|
|
492
|
+
debug.log(`💚 Normalized match: "${normalizedActionName}" === "${normalizedSearch}"`);
|
|
493
|
+
const layOnHandsPool = getLayOnHandsResource();
|
|
494
|
+
if (layOnHandsPool) {
|
|
495
|
+
showLayOnHandsModal(layOnHandsPool);
|
|
496
|
+
} else {
|
|
497
|
+
showNotification('❌ No Lay on Hands pool resource found', 'error');
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Fallback: Catch ANY Lay on Hands action for debugging
|
|
503
|
+
if (action.name.toLowerCase().includes('lay on hands')) {
|
|
504
|
+
debug.log(`🚨 FALLBACK: Caught Lay on Hands action: "${action.name}"`);
|
|
505
|
+
debug.log(`🚨 This action didn't match 'lay on hands: heal' but contains 'lay on hands'`);
|
|
506
|
+
debug.log(`🚨 Showing modal anyway for debugging`);
|
|
507
|
+
const layOnHandsPool = getLayOnHandsResource();
|
|
508
|
+
if (layOnHandsPool) {
|
|
509
|
+
showLayOnHandsModal(layOnHandsPool);
|
|
510
|
+
} else {
|
|
511
|
+
showNotification('❌ No Lay on Hands pool resource found', 'error');
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Announce the action on the FIRST button click only
|
|
517
|
+
if (!actionAnnounced) {
|
|
518
|
+
announceAction(action);
|
|
519
|
+
actionAnnounced = true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Handle different option types
|
|
523
|
+
if (option.type === 'attack') {
|
|
524
|
+
// Mark action as used for attacks
|
|
525
|
+
const actionType = action.actionType || 'action';
|
|
526
|
+
if (typeof window.markActionEconomyUsed === 'function') {
|
|
527
|
+
window.markActionEconomyUsed(actionType);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Attack roll is just the d20 + modifiers, no damage dice
|
|
531
|
+
debug.log(`🎯 Attack button clicked for "${action.name}", formula: "${option.formula}"`);
|
|
532
|
+
console.log(`🎯 ATTACK DEBUG: Rolling attack for ${action.name} with formula ${option.formula}`);
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
roll(`${action.name} Attack`, option.formula);
|
|
536
|
+
debug.log(`✅ Attack roll called successfully for "${action.name}"`);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
debug.error(`❌ Error rolling attack for "${action.name}":`, error);
|
|
539
|
+
console.error('❌ ATTACK ERROR:', error);
|
|
540
|
+
showNotification(`❌ Error rolling attack: ${error.message}`, 'error');
|
|
541
|
+
}
|
|
542
|
+
} else if (option.type === 'healing' || option.type === 'temphp' || option.type === 'damage') {
|
|
543
|
+
// Check and decrement uses before rolling
|
|
544
|
+
if (action.uses && !decrementActionUses(action)) {
|
|
545
|
+
return; // No uses remaining
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Check and decrement Ki points if action costs Ki
|
|
549
|
+
const kiCost = getKiCostFromAction(action);
|
|
550
|
+
if (kiCost > 0) {
|
|
551
|
+
const kiResource = getKiPointsResource();
|
|
552
|
+
if (!kiResource) {
|
|
553
|
+
showNotification(`❌ No Ki Points resource found`, 'error');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (kiResource.current < kiCost) {
|
|
557
|
+
showNotification(`❌ Not enough Ki Points! Need ${kiCost}, have ${kiResource.current}`, 'error');
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
kiResource.current -= kiCost;
|
|
561
|
+
saveCharacterData();
|
|
562
|
+
debug.log(`✨ Used ${kiCost} Ki points for ${action.name}. Remaining: ${kiResource.current}/${kiResource.max}`);
|
|
563
|
+
showNotification(`✨ ${action.name}! (${kiResource.current}/${kiResource.max} Ki left)`);
|
|
564
|
+
buildSheet(characterData); // Refresh display
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Check and decrement Sorcery Points if action costs them
|
|
568
|
+
const sorceryCost = getSorceryPointCostFromAction(action);
|
|
569
|
+
if (sorceryCost > 0) {
|
|
570
|
+
const sorceryResource = getSorceryPointsResource();
|
|
571
|
+
if (!sorceryResource) {
|
|
572
|
+
showNotification(`❌ No Sorcery Points resource found`, 'error');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (sorceryResource.current < sorceryCost) {
|
|
576
|
+
showNotification(`❌ Not enough Sorcery Points! Need ${sorceryCost}, have ${sorceryResource.current}`, 'error');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
sorceryResource.current -= sorceryCost;
|
|
580
|
+
saveCharacterData();
|
|
581
|
+
debug.log(`✨ Used ${sorceryCost} Sorcery Points for ${action.name}. Remaining: ${sorceryResource.current}/${sorceryResource.max}`);
|
|
582
|
+
showNotification(`✨ ${action.name}! (${sorceryResource.current}/${sorceryResource.max} SP left)`);
|
|
583
|
+
buildSheet(characterData); // Refresh display
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Check and decrement other resources (Wild Shape, Breath Weapon, etc.)
|
|
587
|
+
if (!decrementActionResources(action)) {
|
|
588
|
+
return; // Not enough resources
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Roll the damage/healing
|
|
592
|
+
const rollType = option.type === 'healing' ? 'Healing' : (option.type === 'temphp' ? 'Temp HP' : 'Damage');
|
|
593
|
+
let damageFormula = option.formula;
|
|
594
|
+
|
|
595
|
+
// Add Sneak Attack if toggle is enabled and this is a damage roll (not healing/temphp)
|
|
596
|
+
if (option.type === 'damage' && sneakAttackEnabled && sneakAttackDamage && action.attackRoll) {
|
|
597
|
+
damageFormula += `+${sneakAttackDamage}`;
|
|
598
|
+
debug.log(`🎯 Adding Sneak Attack to ${action.name} damage: ${damageFormula}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Add Elemental Weapon if toggle is enabled and this is a damage roll
|
|
602
|
+
if (option.type === 'damage' && elementalWeaponEnabled && elementalWeaponDamage && action.attackRoll) {
|
|
603
|
+
damageFormula += `+${elementalWeaponDamage}`;
|
|
604
|
+
debug.log(`⚔️ Adding Elemental Weapon to ${action.name} damage: ${damageFormula}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
roll(`${action.name} ${rollType}`, damageFormula);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
buttonsDiv.appendChild(actionBtn);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Add "Use" button for actions with no attack/damage options
|
|
616
|
+
// Show for any action that should be usable (has description OR is a valid action type)
|
|
617
|
+
if (actionOptions.length === 0 && !actionOptionsResult.skipNormalButtons) {
|
|
618
|
+
const useBtn = document.createElement('button');
|
|
619
|
+
useBtn.className = 'use-btn';
|
|
620
|
+
useBtn.textContent = '✨ Use';
|
|
621
|
+
useBtn.style.cssText = `
|
|
622
|
+
background: #9b59b6;
|
|
623
|
+
color: white;
|
|
624
|
+
border: none;
|
|
625
|
+
padding: 8px 12px;
|
|
626
|
+
border-radius: 4px;
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
font-size: 12px;
|
|
629
|
+
font-weight: bold;
|
|
630
|
+
`;
|
|
631
|
+
useBtn.addEventListener('click', () => {
|
|
632
|
+
// Special handling for Divine Spark
|
|
633
|
+
if (action.name === 'Divine Spark') {
|
|
634
|
+
// Find Channel Divinity resource from the resources array
|
|
635
|
+
const channelDivinityResource = characterData.resources?.find(r =>
|
|
636
|
+
r.name === 'Channel Divinity' ||
|
|
637
|
+
r.variableName === 'channelDivinityCleric' ||
|
|
638
|
+
r.variableName === 'channelDivinityPaladin' ||
|
|
639
|
+
r.variableName === 'channelDivinity'
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (!channelDivinityResource) {
|
|
643
|
+
showNotification('❌ No Channel Divinity resource found', 'error');
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (channelDivinityResource.current <= 0) {
|
|
648
|
+
showNotification('❌ No Channel Divinity uses remaining!', 'error');
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Show the Divine Spark choice modal
|
|
653
|
+
showDivineSparkModal(action, channelDivinityResource);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Special handling for Harness Divine Power
|
|
658
|
+
if (action.name === 'Harness Divine Power' || action.name === 'Recover Spell Slot') {
|
|
659
|
+
// Find Channel Divinity resource from the resources array
|
|
660
|
+
const channelDivinityResource = characterData.resources?.find(r =>
|
|
661
|
+
r.name === 'Channel Divinity' ||
|
|
662
|
+
r.variableName === 'channelDivinityCleric' ||
|
|
663
|
+
r.variableName === 'channelDivinityPaladin' ||
|
|
664
|
+
r.variableName === 'channelDivinity'
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
if (!channelDivinityResource) {
|
|
668
|
+
showNotification('❌ No Channel Divinity resource found', 'error');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (channelDivinityResource.current <= 0) {
|
|
673
|
+
showNotification('❌ No Channel Divinity uses remaining!', 'error');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Show the Harness Divine Power choice modal
|
|
678
|
+
showHarnessDivinePowerModal(action, channelDivinityResource);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Special handling for Elemental Weapon
|
|
683
|
+
if (action.name === 'Elemental Weapon') {
|
|
684
|
+
// Show the Elemental Weapon choice modal
|
|
685
|
+
showElementalWeaponModal(action);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Special handling for Divine Intervention
|
|
690
|
+
if (action.name === 'Divine Intervention') {
|
|
691
|
+
// Show the Divine Intervention modal
|
|
692
|
+
showDivineInterventionModal(action);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Special handling for Starry Form (Stars Druid)
|
|
697
|
+
if (action.name && /starry form/i.test(action.name)) {
|
|
698
|
+
if (typeof showStarryFormModal === 'function') {
|
|
699
|
+
showStarryFormModal(action);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Special handling for Wild Shape
|
|
705
|
+
if (action.name === 'Wild Shape' || action.name === 'Combat Wild Shape') {
|
|
706
|
+
// Show the Wild Shape choice modal
|
|
707
|
+
showWildShapeModal(action);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Special handling for Shapechange
|
|
712
|
+
if (action.name === 'Shapechange') {
|
|
713
|
+
// Show the Shapechange choice modal
|
|
714
|
+
showShapechangeModal(action);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Special handling for True Polymorph
|
|
719
|
+
if (action.name === 'True Polymorph') {
|
|
720
|
+
// Show the True Polymorph choice modal
|
|
721
|
+
showTruePolymorphModal(action);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Special handling for Conjure Animals/Elementals/Fey/Celestial
|
|
726
|
+
if (action.name && (
|
|
727
|
+
action.name.includes('Conjure Animals') ||
|
|
728
|
+
action.name.includes('Conjure Elemental') ||
|
|
729
|
+
action.name.includes('Conjure Fey') ||
|
|
730
|
+
action.name.includes('Conjure Celestial')
|
|
731
|
+
)) {
|
|
732
|
+
// Show the Conjure choice modal
|
|
733
|
+
showConjureModal(action);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Special handling for Planar Binding
|
|
738
|
+
if (action.name === 'Planar Binding') {
|
|
739
|
+
// Show the Planar Binding choice modal
|
|
740
|
+
showPlanarBindingModal(action);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Special handling for Teleport
|
|
745
|
+
if (action.name === 'Teleport') {
|
|
746
|
+
// Show the Teleport choice modal
|
|
747
|
+
showTeleportModal(action);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Special handling for Word of Recall
|
|
752
|
+
if (action.name === 'Word of Recall') {
|
|
753
|
+
// Show the Word of Recall choice modal
|
|
754
|
+
showWordOfRecallModal(action);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Special handling for Contingency
|
|
759
|
+
if (action.name === 'Contingency') {
|
|
760
|
+
// Show the Contingency choice modal
|
|
761
|
+
showContingencyModal(action);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Special handling for Glyph of Warding
|
|
766
|
+
if (action.name === 'Glyph of Warding') {
|
|
767
|
+
// Show the Glyph of Warding choice modal
|
|
768
|
+
showGlyphOfWardingModal(action);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Special handling for Symbol
|
|
773
|
+
if (action.name === 'Symbol') {
|
|
774
|
+
// Show the Symbol choice modal
|
|
775
|
+
showSymbolModal(action);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Special handling for Programmed Illusion
|
|
780
|
+
if (action.name === 'Programmed Illusion') {
|
|
781
|
+
// Show the Programmed Illusion choice modal
|
|
782
|
+
showProgrammedIllusionModal(action);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Special handling for Sequester
|
|
787
|
+
if (action.name === 'Sequester') {
|
|
788
|
+
// Show the Sequester choice modal
|
|
789
|
+
showSequesterModal(action);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Special handling for Clone
|
|
794
|
+
if (action.name === 'Clone') {
|
|
795
|
+
// Show the Clone choice modal
|
|
796
|
+
showCloneModal(action);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Special handling for Astral Projection
|
|
801
|
+
if (action.name === 'Astral Projection') {
|
|
802
|
+
// Show the Astral Projection choice modal
|
|
803
|
+
showAstralProjectionModal(action);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Special handling for Etherealness
|
|
808
|
+
if (action.name === 'Etherealness') {
|
|
809
|
+
// Show the Etherealness choice modal
|
|
810
|
+
showEtherealnessModal(action);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Special handling for Magic Jar
|
|
815
|
+
if (action.name === 'Magic Jar') {
|
|
816
|
+
// Show the Magic Jar choice modal
|
|
817
|
+
showMagicJarModal(action);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Special handling for Imprisonment
|
|
822
|
+
if (action.name === 'Imprisonment') {
|
|
823
|
+
// Show the Imprisonment choice modal
|
|
824
|
+
showImprisonmentModal(action);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Special handling for Time Stop
|
|
829
|
+
if (action.name === 'Time Stop') {
|
|
830
|
+
// Show the Time Stop choice modal
|
|
831
|
+
showTimeStopModal(action);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Special handling for Mirage Arcane
|
|
836
|
+
if (action.name === 'Mirage Arcane') {
|
|
837
|
+
// Show the Mirage Arcane choice modal
|
|
838
|
+
showMirageArcaneModal(action);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Special handling for Forcecage
|
|
843
|
+
if (action.name === 'Forcecage') {
|
|
844
|
+
// Show the Forcecage choice modal
|
|
845
|
+
showForcecageModal(action);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Special handling for Maze
|
|
850
|
+
if (action.name === 'Maze') {
|
|
851
|
+
// Show the Maze choice modal
|
|
852
|
+
showMazeModal(action);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Special handling for Wish
|
|
857
|
+
if (action.name === 'Wish') {
|
|
858
|
+
// Show the Wish choice modal
|
|
859
|
+
showWishModal(action);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Special handling for Simulacrum
|
|
864
|
+
if (action.name === 'Simulacrum') {
|
|
865
|
+
// Show the Simulacrum choice modal
|
|
866
|
+
showSimulacrumModal(action);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Special handling for Gate
|
|
871
|
+
if (action.name === 'Gate') {
|
|
872
|
+
// Show the Gate choice modal
|
|
873
|
+
showGateModal(action);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Special handling for Legend Lore
|
|
878
|
+
if (action.name === 'Legend Lore') {
|
|
879
|
+
// Show the Legend Lore choice modal
|
|
880
|
+
showLegendLoreModal(action);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Special handling for Commune
|
|
885
|
+
if (action.name === 'Commune') {
|
|
886
|
+
// Show the Commune choice modal
|
|
887
|
+
showCommuneModal(action);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Special handling for Augury
|
|
892
|
+
if (action.name === 'Augury') {
|
|
893
|
+
// Show the Augury choice modal
|
|
894
|
+
showAuguryModal(action);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Special handling for Divination
|
|
899
|
+
if (action.name === 'Divination') {
|
|
900
|
+
// Show the Divination choice modal
|
|
901
|
+
showDivinationModal(action);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Special handling for Contact Other Plane
|
|
906
|
+
if (action.name === 'Contact Other Plane') {
|
|
907
|
+
// Show the Contact Other Plane choice modal
|
|
908
|
+
showContactOtherPlaneModal(action);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Special handling for Find the Path
|
|
913
|
+
if (action.name === 'Find the Path') {
|
|
914
|
+
// Show the Find the Path choice modal
|
|
915
|
+
showFindThePathModal(action);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Special handling for Speak with Dead
|
|
920
|
+
if (action.name === 'Speak with Dead') {
|
|
921
|
+
// Show the Speak with Dead choice modal
|
|
922
|
+
showSpeakWithDeadModal(action);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Special handling for Speak with Animals
|
|
927
|
+
if (action.name === 'Speak with Animals') {
|
|
928
|
+
// Show the Speak with Animals choice modal
|
|
929
|
+
showSpeakWithAnimalsModal(action);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Special handling for Speak with Plants
|
|
934
|
+
if (action.name === 'Speak with Plants') {
|
|
935
|
+
// Show the Speak with Plants choice modal
|
|
936
|
+
showSpeakWithPlantsModal(action);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Special handling for Zone of Truth
|
|
941
|
+
if (action.name === 'Zone of Truth') {
|
|
942
|
+
// Show the Zone of Truth choice modal
|
|
943
|
+
showZoneOfTruthModal(action);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Special handling for Sending
|
|
948
|
+
if (action.name === 'Sending') {
|
|
949
|
+
// Show the Sending choice modal
|
|
950
|
+
showSendingModal(action);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Special handling for Dream
|
|
955
|
+
if (action.name === 'Dream') {
|
|
956
|
+
// Show the Dream choice modal
|
|
957
|
+
showDreamModal(action);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Special handling for Scrying
|
|
962
|
+
if (action.name === 'Scrying') {
|
|
963
|
+
// Show the Scrying choice modal
|
|
964
|
+
showScryingModal(action);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Special handling for Dispel Evil and Good
|
|
969
|
+
if (action.name === 'Dispel Evil and Good') {
|
|
970
|
+
// Show the Dispel Evil and Good choice modal
|
|
971
|
+
showDispelEvilAndGoodModal(action);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Special handling for Freedom of Movement
|
|
976
|
+
if (action.name === 'Freedom of Movement') {
|
|
977
|
+
// Show the Freedom of Movement choice modal
|
|
978
|
+
showFreedomOfMovementModal(action);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Special handling for Nondetection
|
|
983
|
+
if (action.name === 'Nondetection') {
|
|
984
|
+
// Show the Nondetection choice modal
|
|
985
|
+
showNondetectionModal(action);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Special handling for Protection from Energy
|
|
990
|
+
if (action.name === 'Protection from Energy') {
|
|
991
|
+
// Show the Protection from Energy choice modal
|
|
992
|
+
showProtectionFromEnergyModal(action);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Special handling for Protection from Evil and Good
|
|
997
|
+
if (action.name === 'Protection from Evil and Good') {
|
|
998
|
+
// Show the Protection from Evil and Good choice modal
|
|
999
|
+
showProtectionFromEvilAndGoodModal(action);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Special handling for Sanctuary
|
|
1004
|
+
if (action.name === 'Sanctuary') {
|
|
1005
|
+
// Show the Sanctuary choice modal
|
|
1006
|
+
showSanctuaryModal(action);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Special handling for Silence
|
|
1011
|
+
if (action.name === 'Silence') {
|
|
1012
|
+
// Show the Silence choice modal
|
|
1013
|
+
showSilenceModal(action);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Special handling for Magic Circle
|
|
1018
|
+
if (action.name === 'Magic Circle') {
|
|
1019
|
+
// Show the Magic Circle choice modal
|
|
1020
|
+
showMagicCircleModal(action);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Special handling for Greater Restoration
|
|
1025
|
+
if (action.name === 'Greater Restoration') {
|
|
1026
|
+
// Show the Greater Restoration choice modal
|
|
1027
|
+
showGreaterRestorationModal(action);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Special handling for Remove Curse
|
|
1032
|
+
if (action.name === 'Remove Curse') {
|
|
1033
|
+
// Show the Remove Curse choice modal
|
|
1034
|
+
showRemoveCurseModal(action);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Special handling for Revivify
|
|
1039
|
+
if (action.name === 'Revivify') {
|
|
1040
|
+
// Show the Revivify choice modal
|
|
1041
|
+
showRevivifyModal(action);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Special handling for Raise Dead
|
|
1046
|
+
if (action.name === 'Raise Dead') {
|
|
1047
|
+
// Show the Raise Dead choice modal
|
|
1048
|
+
showRaiseDeadModal(action);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Special handling for Resurrection
|
|
1053
|
+
if (action.name === 'Resurrection') {
|
|
1054
|
+
// Show the Resurrection choice modal
|
|
1055
|
+
showResurrectionModal(action);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Special handling for True Resurrection
|
|
1060
|
+
if (action.name === 'True Resurrection') {
|
|
1061
|
+
// Show the True Resurrection choice modal
|
|
1062
|
+
showTrueResurrectionModal(action);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Special handling for Detect Magic
|
|
1067
|
+
if (action.name === 'Detect Magic') {
|
|
1068
|
+
// Show the Detect Magic choice modal
|
|
1069
|
+
showDetectMagicModal(action);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Special handling for Identify
|
|
1074
|
+
if (action.name === 'Identify') {
|
|
1075
|
+
// Show the Identify choice modal
|
|
1076
|
+
showIdentifyModal(action);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Special handling for Dispel Magic
|
|
1081
|
+
if (action.name === 'Dispel Magic') {
|
|
1082
|
+
// Show the Dispel Magic choice modal
|
|
1083
|
+
showDispelMagicModal(action);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Special handling for Feather Fall
|
|
1088
|
+
if (action.name === 'Feather Fall') {
|
|
1089
|
+
// Show the Feather Fall choice modal
|
|
1090
|
+
showFeatherFallModal(action);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Special handling for Hellish Rebuke
|
|
1095
|
+
if (action.name === 'Hellish Rebuke') {
|
|
1096
|
+
// Show the Hellish Rebuke choice modal
|
|
1097
|
+
showHellishRebukeModal(action);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Special handling for Shield
|
|
1102
|
+
if (action.name === 'Shield') {
|
|
1103
|
+
// Show the Shield choice modal
|
|
1104
|
+
showShieldModal(action);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Special handling for Absorb Elements
|
|
1109
|
+
if (action.name === 'Absorb Elements') {
|
|
1110
|
+
// Show the Absorb Elements choice modal
|
|
1111
|
+
showAbsorbElementsModal(action);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Special handling for Counterspell
|
|
1116
|
+
if (action.name === 'Counterspell') {
|
|
1117
|
+
// Show the Counterspell choice modal
|
|
1118
|
+
showCounterspellModal(action);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Special handling for Fire Shield
|
|
1123
|
+
if (action.name === 'Fire Shield') {
|
|
1124
|
+
// Show the Fire Shield choice modal
|
|
1125
|
+
showFireShieldModal(action);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Special handling for Armor of Agathys
|
|
1130
|
+
if (action.name === 'Armor of Agathys') {
|
|
1131
|
+
// Show the Armor of Agathys choice modal
|
|
1132
|
+
showArmorOfAgathysModal(action);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Special handling for Meld into Stone
|
|
1137
|
+
if (action.name === 'Meld into Stone') {
|
|
1138
|
+
// Show the Meld into Stone choice modal
|
|
1139
|
+
showMeldIntoStoneModal(action);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Special handling for Vampiric Touch
|
|
1144
|
+
if (action.name === 'Vampiric Touch') {
|
|
1145
|
+
// Show the Vampiric Touch choice modal
|
|
1146
|
+
showVampiricTouchModal(action);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Special handling for Life Transference
|
|
1151
|
+
if (action.name === 'Life Transference') {
|
|
1152
|
+
// Show the Life Transference choice modal
|
|
1153
|
+
showLifeTransferenceModal(action);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Special handling for Geas
|
|
1158
|
+
if (action.name === 'Geas') {
|
|
1159
|
+
// Show the Geas choice modal
|
|
1160
|
+
showGeasModal(action);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Special handling for Symbol
|
|
1165
|
+
if (action.name === 'Symbol') {
|
|
1166
|
+
// Show the Symbol choice modal
|
|
1167
|
+
showSymbolModal(action);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Special handling for Spiritual Weapon
|
|
1172
|
+
if (action.name === 'Spiritual Weapon') {
|
|
1173
|
+
// Show the Spiritual Weapon choice modal
|
|
1174
|
+
showSpiritualWeaponModal(action);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Special handling for Flaming Sphere
|
|
1179
|
+
if (action.name === 'Flaming Sphere') {
|
|
1180
|
+
// Show the Flaming Sphere choice modal
|
|
1181
|
+
showFlamingSphereModal(action);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Special handling for Bigby's Hand
|
|
1186
|
+
if (action.name === 'Bigby\'s Hand') {
|
|
1187
|
+
// Show the Bigby's Hand choice modal
|
|
1188
|
+
showBigbysHandModal(action);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Special handling for Animate Objects
|
|
1193
|
+
if (action.name === 'Animate Objects') {
|
|
1194
|
+
// Show the Animate Objects choice modal
|
|
1195
|
+
showAnimateObjectsModal(action);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Special handling for Moonbeam
|
|
1200
|
+
if (action.name === 'Moonbeam') {
|
|
1201
|
+
// Show the Moonbeam choice modal
|
|
1202
|
+
showMoonbeamModal(action);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Special handling for Healing Spirit
|
|
1207
|
+
if (action.name === 'Healing Spirit') {
|
|
1208
|
+
// Show the Healing Spirit choice modal
|
|
1209
|
+
showHealingSpiritModal(action);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Special handling for Bless
|
|
1214
|
+
if (action.name === 'Bless') {
|
|
1215
|
+
// Show the Bless choice modal
|
|
1216
|
+
showBlessModal(action);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Special handling for Bane
|
|
1221
|
+
if (action.name === 'Bane') {
|
|
1222
|
+
// Show the Bane choice modal
|
|
1223
|
+
showBaneModal(action);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Special handling for Guidance
|
|
1228
|
+
if (action.name === 'Guidance') {
|
|
1229
|
+
// Show the Guidance choice modal
|
|
1230
|
+
showGuidanceModal(action);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Special handling for Resistance
|
|
1235
|
+
if (action.name === 'Resistance') {
|
|
1236
|
+
// Show the Resistance choice modal
|
|
1237
|
+
showResistanceModal(action);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Special handling for Hex
|
|
1242
|
+
if (action.name === 'Hex') {
|
|
1243
|
+
// Show the Hex choice modal
|
|
1244
|
+
showHexModal(action);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Special handling for Hunter's Mark
|
|
1249
|
+
if (action.name === 'Hunter\'s Mark') {
|
|
1250
|
+
// Show the Hunter's Mark choice modal
|
|
1251
|
+
showHuntersMarkModal(action);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Special handling for Magic Missile
|
|
1256
|
+
if (action.name === 'Magic Missile') {
|
|
1257
|
+
// Show the Magic Missile choice modal
|
|
1258
|
+
showMagicMissileModal(action);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Special handling for Scorching Ray
|
|
1263
|
+
if (action.name === 'Scorching Ray') {
|
|
1264
|
+
// Show the Scorching Ray choice modal
|
|
1265
|
+
showScorchingRayModal(action);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Special handling for Aid
|
|
1270
|
+
if (action.name === 'Aid') {
|
|
1271
|
+
// Show the Aid choice modal
|
|
1272
|
+
showAidModal(action);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Note: Eldritch Blast uses standard attack/damage buttons, no special modal needed
|
|
1277
|
+
|
|
1278
|
+
// Special handling for Spirit Guardians
|
|
1279
|
+
if (action.name === 'Spirit Guardians') {
|
|
1280
|
+
// Show the Spirit Guardians choice modal
|
|
1281
|
+
showSpiritGuardiansModal(action);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Special handling for Cloud of Daggers
|
|
1286
|
+
if (action.name === 'Cloud of Daggers') {
|
|
1287
|
+
// Show the Cloud of Daggers choice modal
|
|
1288
|
+
showCloudOfDaggersModal(action);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Special handling for Spike Growth
|
|
1293
|
+
if (action.name === 'Spike Growth') {
|
|
1294
|
+
// Show the Spike Growth choice modal
|
|
1295
|
+
showSpikeGrowthModal(action);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Special handling for Wall of Fire
|
|
1300
|
+
if (action.name === 'Wall of Fire') {
|
|
1301
|
+
// Show the Wall of Fire choice modal
|
|
1302
|
+
showWallOfFireModal(action);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Special handling for Haste
|
|
1307
|
+
if (action.name === 'Haste') {
|
|
1308
|
+
// Show the Haste choice modal
|
|
1309
|
+
showHasteModal(action);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Special handling for Booming Blade
|
|
1314
|
+
if (action.name === 'Booming Blade') {
|
|
1315
|
+
// Show the Booming Blade choice modal
|
|
1316
|
+
showBoomingBladeModal(action);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Special handling for Green-Flame Blade
|
|
1321
|
+
if (action.name === 'Green-Flame Blade') {
|
|
1322
|
+
// Show the Green-Flame Blade choice modal
|
|
1323
|
+
showGreenFlameBladeModal(action);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Special handling for Chromatic Orb
|
|
1328
|
+
if (action.name === 'Chromatic Orb') {
|
|
1329
|
+
// Show the Chromatic Orb choice modal
|
|
1330
|
+
showChromaticOrbModal(action);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Special handling for Dragon's Breath
|
|
1335
|
+
if (action.name === 'Dragon\'s Breath') {
|
|
1336
|
+
// Show the Dragons Breath choice modal
|
|
1337
|
+
showDragonsBreathModal(action);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Special handling for Chaos Bolt
|
|
1342
|
+
if (action.name === 'Chaos Bolt') {
|
|
1343
|
+
// Show the Chaos Bolt choice modal
|
|
1344
|
+
showChaosBoltModal(action);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Special handling for Delayed Blast Fireball
|
|
1349
|
+
if (action.name === 'Delayed Blast Fireball') {
|
|
1350
|
+
// Show the Delayed Blast Fireball choice modal
|
|
1351
|
+
showDelayedBlastFireballModal(action);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Special handling for Polymorph
|
|
1356
|
+
if (action.name === 'Polymorph') {
|
|
1357
|
+
// Show the Polymorph choice modal
|
|
1358
|
+
showPolymorphModal(action);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Special handling for True Polymorph
|
|
1363
|
+
if (action.name === 'True Polymorph') {
|
|
1364
|
+
// Show the True Polymorph choice modal
|
|
1365
|
+
showTruePolymorphModal(action);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Default handling for other actions
|
|
1370
|
+
|
|
1371
|
+
// Check and decrement uses BEFORE announcing (so announcement shows correct count)
|
|
1372
|
+
if (action.uses && !decrementActionUses(action)) {
|
|
1373
|
+
return; // No uses remaining
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Check and decrement Ki points if action costs Ki
|
|
1377
|
+
const kiCost = getKiCostFromAction(action);
|
|
1378
|
+
if (kiCost > 0) {
|
|
1379
|
+
const kiResource = getKiPointsResource();
|
|
1380
|
+
if (!kiResource) {
|
|
1381
|
+
showNotification(`❌ No Ki Points resource found`, 'error');
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (kiResource.current < kiCost) {
|
|
1385
|
+
showNotification(`❌ Not enough Ki Points! Need ${kiCost}, have ${kiResource.current}`, 'error');
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
kiResource.current -= kiCost;
|
|
1389
|
+
saveCharacterData();
|
|
1390
|
+
debug.log(`✨ Used ${kiCost} Ki points for ${action.name}. Remaining: ${kiResource.current}/${kiResource.max}`);
|
|
1391
|
+
showNotification(`✨ ${action.name}! (${kiResource.current}/${kiResource.max} Ki left)`);
|
|
1392
|
+
buildSheet(characterData); // Refresh display
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Check and decrement Sorcery Points if action costs them
|
|
1396
|
+
const sorceryCost = getSorceryPointCostFromAction(action);
|
|
1397
|
+
if (sorceryCost > 0) {
|
|
1398
|
+
const sorceryResource = getSorceryPointsResource();
|
|
1399
|
+
if (!sorceryResource) {
|
|
1400
|
+
showNotification(`❌ No Sorcery Points resource found`, 'error');
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (sorceryResource.current < sorceryCost) {
|
|
1404
|
+
showNotification(`❌ Not enough Sorcery Points! Need ${sorceryCost}, have ${sorceryResource.current}`, 'error');
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
sorceryResource.current -= sorceryCost;
|
|
1408
|
+
saveCharacterData();
|
|
1409
|
+
debug.log(`✨ Used ${sorceryCost} Sorcery Points for ${action.name}. Remaining: ${sorceryResource.current}/${sorceryResource.max}`);
|
|
1410
|
+
showNotification(`✨ ${action.name}! (${sorceryResource.current}/${sorceryResource.max} SP left)`);
|
|
1411
|
+
buildSheet(characterData); // Refresh display
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Check and decrement other resources (Wild Shape, Breath Weapon, etc.)
|
|
1415
|
+
if (!decrementActionResources(action)) {
|
|
1416
|
+
return; // Not enough resources
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Announce the action AFTER all decrements (so announcement shows correct counts)
|
|
1420
|
+
announceAction(action);
|
|
1421
|
+
|
|
1422
|
+
// Mark action as used based on action type (auto-tracking during combat)
|
|
1423
|
+
const actionType = action.actionType || 'action';
|
|
1424
|
+
debug.log(`🎯 Action type for "${action.name}": "${actionType}"`);
|
|
1425
|
+
|
|
1426
|
+
// Call global markActionEconomyUsed if available (from popup-sheet.js)
|
|
1427
|
+
if (typeof window.markActionEconomyUsed === 'function') {
|
|
1428
|
+
window.markActionEconomyUsed(actionType);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
buttonsDiv.appendChild(useBtn);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Add Details button if there's any useful information to show
|
|
1435
|
+
const hasDetails = action.description || action.summary || action.damageType || action.attackRoll || action.damage || action.source || action.range;
|
|
1436
|
+
if (hasDetails) {
|
|
1437
|
+
const detailsBtn = document.createElement('button');
|
|
1438
|
+
detailsBtn.className = 'details-btn';
|
|
1439
|
+
detailsBtn.innerHTML = '▼ Details';
|
|
1440
|
+
detailsBtn.style.cssText = `
|
|
1441
|
+
background: #34495e;
|
|
1442
|
+
color: white;
|
|
1443
|
+
border: none;
|
|
1444
|
+
padding: 8px 12px;
|
|
1445
|
+
border-radius: 4px;
|
|
1446
|
+
cursor: pointer;
|
|
1447
|
+
font-size: 12px;
|
|
1448
|
+
font-weight: bold;
|
|
1449
|
+
margin-right: 4px;
|
|
1450
|
+
margin-bottom: 4px;
|
|
1451
|
+
`;
|
|
1452
|
+
detailsBtn.addEventListener('click', () => {
|
|
1453
|
+
const descDiv = actionCard.querySelector('.action-details');
|
|
1454
|
+
if (descDiv) {
|
|
1455
|
+
descDiv.style.display = descDiv.style.display === 'none' ? 'block' : 'none';
|
|
1456
|
+
detailsBtn.innerHTML = descDiv.style.display === 'none' ? '▼ Details' : '▲ Hide';
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
buttonsDiv.appendChild(detailsBtn);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Append nameDiv and buttonsDiv to actionHeader
|
|
1463
|
+
actionHeader.appendChild(nameDiv);
|
|
1464
|
+
actionHeader.appendChild(buttonsDiv);
|
|
1465
|
+
|
|
1466
|
+
// Append actionHeader to actionCard
|
|
1467
|
+
actionCard.appendChild(actionHeader);
|
|
1468
|
+
|
|
1469
|
+
// Add details section if any useful information exists (hidden by default, toggled by Details button)
|
|
1470
|
+
if (hasDetails) {
|
|
1471
|
+
const detailsDiv = document.createElement('div');
|
|
1472
|
+
detailsDiv.className = 'action-details';
|
|
1473
|
+
detailsDiv.style.display = 'none'; // Hidden by default
|
|
1474
|
+
|
|
1475
|
+
let detailsHTML = '<div style="margin-top: 10px; padding: 10px; background: var(--bg-secondary, #f5f5f5); border-radius: 4px; font-size: 0.9em;">';
|
|
1476
|
+
|
|
1477
|
+
// Add summary if available
|
|
1478
|
+
if (action.summary) {
|
|
1479
|
+
const resolvedSummary = resolveVariablesInFormula(action.summary);
|
|
1480
|
+
detailsHTML += `<div style="margin-bottom: 8px;"><strong>Summary:</strong> ${resolvedSummary}</div>`;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Add description if available
|
|
1484
|
+
if (action.description) {
|
|
1485
|
+
const resolvedDescription = resolveVariablesInFormula(action.description);
|
|
1486
|
+
detailsHTML += `<div style="margin-bottom: 8px;">${resolvedDescription}</div>`;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Add action details
|
|
1490
|
+
const details = [];
|
|
1491
|
+
if (action.attackRoll) details.push(`<strong>Attack:</strong> ${action.attackRoll}`);
|
|
1492
|
+
if (action.damage) details.push(`<strong>Damage:</strong> ${action.damage}`);
|
|
1493
|
+
if (action.damageType) details.push(`<strong>Type:</strong> ${action.damageType}`);
|
|
1494
|
+
if (action.range) details.push(`<strong>Range:</strong> ${action.range}`);
|
|
1495
|
+
if (action.source) details.push(`<strong>Source:</strong> ${action.source}`);
|
|
1496
|
+
|
|
1497
|
+
if (details.length > 0) {
|
|
1498
|
+
detailsHTML += `<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 0.85em;">${details.join(' • ')}</div>`;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
detailsHTML += '</div>';
|
|
1502
|
+
detailsDiv.innerHTML = detailsHTML;
|
|
1503
|
+
|
|
1504
|
+
actionCard.appendChild(detailsDiv);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
container.appendChild(actionCard);
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Note: Inventory functions (rebuildInventory, buildInventoryDisplay, createInventoryCard)
|
|
1512
|
+
// are now provided by inventory-manager.js
|
|
1513
|
+
|
|
1514
|
+
// buildCompanionsDisplay is now in modules/companions-manager.js
|
|
1515
|
+
|
|
1516
|
+
function decrementActionUses(action) {
|
|
1517
|
+
if (!action.uses) {
|
|
1518
|
+
return true; // No uses to track, allow action
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const usesTotal = action.uses.total || action.uses.value || action.uses;
|
|
1522
|
+
const usesUsed = action.usesUsed || 0;
|
|
1523
|
+
const usesRemaining = action.usesLeft !== undefined ? action.usesLeft : (usesTotal - usesUsed);
|
|
1524
|
+
|
|
1525
|
+
if (usesRemaining <= 0) {
|
|
1526
|
+
showNotification(`❌ No uses remaining for ${action.name}`, 'error');
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Increment usesUsed and decrement usesLeft
|
|
1531
|
+
action.usesUsed = usesUsed + 1;
|
|
1532
|
+
if (action.usesLeft !== undefined) {
|
|
1533
|
+
action.usesLeft = usesRemaining - 1;
|
|
1534
|
+
}
|
|
1535
|
+
const newRemaining = action.usesLeft !== undefined ? action.usesLeft : (usesTotal - action.usesUsed);
|
|
1536
|
+
|
|
1537
|
+
// Update character data and save
|
|
1538
|
+
saveCharacterData();
|
|
1539
|
+
|
|
1540
|
+
// Show notification
|
|
1541
|
+
showNotification(`✅ Used ${action.name} (${newRemaining}/${usesTotal} remaining)`);
|
|
1542
|
+
|
|
1543
|
+
// Rebuild the actions display to show updated count
|
|
1544
|
+
const actionsContainer = document.getElementById('actions-container');
|
|
1545
|
+
buildActionsDisplay(actionsContainer, characterData.actions);
|
|
1546
|
+
|
|
1547
|
+
return true;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// ===== EXPORTS =====
|
|
1551
|
+
|
|
1552
|
+
window.buildActionsDisplay = buildActionsDisplay;
|
|
1553
|
+
window.decrementActionUses = decrementActionUses;
|
|
1554
|
+
|
|
1555
|
+
console.log('✅ Action Display module loaded');
|
|
1556
|
+
|
|
1557
|
+
})();
|