@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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warlock Invocations Module
|
|
3
|
+
*
|
|
4
|
+
* Handles detection and application of Warlock invocations that modify spells and abilities.
|
|
5
|
+
* Provides a centralized system for checking active invocations and applying their effects.
|
|
6
|
+
*
|
|
7
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
8
|
+
*
|
|
9
|
+
* Functions exported to globalThis:
|
|
10
|
+
* - getActiveInvocations(characterData)
|
|
11
|
+
* - applyInvocationToSpell(spell, invocations, characterData)
|
|
12
|
+
* - applyInvocationToDamage(spellName, damageFormula, invocations, characterData)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
(function() {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Warlock invocations that modify spell behavior
|
|
20
|
+
* Each entry defines the invocation name pattern and the effect it has
|
|
21
|
+
*/
|
|
22
|
+
const INVOCATION_DEFINITIONS = {
|
|
23
|
+
// Agonizing Blast: Add CHA modifier to Eldritch Blast damage
|
|
24
|
+
AGONIZING_BLAST: {
|
|
25
|
+
namePattern: /agonizing blast/i,
|
|
26
|
+
affectsSpells: ['eldritch blast'],
|
|
27
|
+
modifyDamage: (damage, characterData) => {
|
|
28
|
+
const charismaMod = characterData.abilityMods?.charismaMod || 0;
|
|
29
|
+
if (charismaMod === 0) return damage;
|
|
30
|
+
|
|
31
|
+
const modifier = charismaMod >= 0 ? `+${charismaMod}` : `${charismaMod}`;
|
|
32
|
+
return `${damage}${modifier}`;
|
|
33
|
+
},
|
|
34
|
+
description: 'Adds Charisma modifier to each Eldritch Blast beam'
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Repelling Blast: Push creatures 10 feet with Eldritch Blast
|
|
38
|
+
REPELLING_BLAST: {
|
|
39
|
+
namePattern: /repelling blast/i,
|
|
40
|
+
affectsSpells: ['eldritch blast'],
|
|
41
|
+
modifyDescription: (description) => {
|
|
42
|
+
return description + '\n\n**Repelling Blast:** On a hit, push the target up to 10 feet away from you in a straight line.';
|
|
43
|
+
},
|
|
44
|
+
description: 'Push Eldritch Blast targets 10 feet away'
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Grasp of Hadar: Pull creatures 10 feet with Eldritch Blast
|
|
48
|
+
GRASP_OF_HADAR: {
|
|
49
|
+
namePattern: /grasp of hadar/i,
|
|
50
|
+
affectsSpells: ['eldritch blast'],
|
|
51
|
+
modifyDescription: (description) => {
|
|
52
|
+
return description + '\n\n**Grasp of Hadar:** Once per turn when you hit with Eldritch Blast, pull the target up to 10 feet closer to you in a straight line.';
|
|
53
|
+
},
|
|
54
|
+
description: 'Pull Eldritch Blast target 10 feet closer (once per turn)'
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Lance of Lethargy: Reduce speed with Eldritch Blast
|
|
58
|
+
LANCE_OF_LETHARGY: {
|
|
59
|
+
namePattern: /lance of lethargy/i,
|
|
60
|
+
affectsSpells: ['eldritch blast'],
|
|
61
|
+
modifyDescription: (description) => {
|
|
62
|
+
return description + '\n\n**Lance of Lethargy:** Once per turn when you hit with Eldritch Blast, reduce the target\'s speed by 10 feet until the end of your next turn.';
|
|
63
|
+
},
|
|
64
|
+
description: 'Reduce Eldritch Blast target speed by 10 feet (once per turn)'
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Eldritch Spear: Increase Eldritch Blast range to 300 feet
|
|
68
|
+
ELDRITCH_SPEAR: {
|
|
69
|
+
namePattern: /eldritch spear/i,
|
|
70
|
+
affectsSpells: ['eldritch blast'],
|
|
71
|
+
modifyRange: () => '300 feet',
|
|
72
|
+
description: 'Increases Eldritch Blast range to 300 feet'
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all active invocations for a character
|
|
78
|
+
* @param {Object} characterData - The character data object
|
|
79
|
+
* @returns {Array} Array of active invocation objects with their definitions
|
|
80
|
+
*/
|
|
81
|
+
function getActiveInvocations(characterData) {
|
|
82
|
+
if (!characterData || !characterData.features) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const activeInvocations = [];
|
|
87
|
+
|
|
88
|
+
// Search through character features for invocations
|
|
89
|
+
characterData.features.forEach(feature => {
|
|
90
|
+
if (!feature.name) return;
|
|
91
|
+
|
|
92
|
+
// Check if this feature matches any known invocation
|
|
93
|
+
for (const [key, invocation] of Object.entries(INVOCATION_DEFINITIONS)) {
|
|
94
|
+
if (invocation.namePattern.test(feature.name)) {
|
|
95
|
+
activeInvocations.push({
|
|
96
|
+
name: feature.name,
|
|
97
|
+
key: key,
|
|
98
|
+
definition: invocation
|
|
99
|
+
});
|
|
100
|
+
debug.log(`🔮 Detected warlock invocation: ${feature.name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return activeInvocations;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a specific invocation is active
|
|
110
|
+
* @param {String} invocationKey - The invocation key (e.g., 'AGONIZING_BLAST')
|
|
111
|
+
* @param {Array} activeInvocations - Array of active invocations from getActiveInvocations()
|
|
112
|
+
* @returns {Boolean} True if the invocation is active
|
|
113
|
+
*/
|
|
114
|
+
function hasInvocation(invocationKey, activeInvocations) {
|
|
115
|
+
return activeInvocations.some(inv => inv.key === invocationKey);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply invocation modifications to a spell
|
|
120
|
+
* This modifies spell properties based on active invocations
|
|
121
|
+
* @param {Object} spell - The spell object to modify
|
|
122
|
+
* @param {Array} invocations - Array of active invocations
|
|
123
|
+
* @param {Object} characterData - The character data object
|
|
124
|
+
* @returns {Object} Modified spell object (creates a shallow copy)
|
|
125
|
+
*/
|
|
126
|
+
function applyInvocationToSpell(spell, invocations, characterData) {
|
|
127
|
+
if (!spell || !invocations || invocations.length === 0) {
|
|
128
|
+
return spell;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const spellNameLower = (spell.name || '').toLowerCase();
|
|
132
|
+
let modifiedSpell = { ...spell };
|
|
133
|
+
let hasModifications = false;
|
|
134
|
+
|
|
135
|
+
// Check each active invocation
|
|
136
|
+
invocations.forEach(invocation => {
|
|
137
|
+
const def = invocation.definition;
|
|
138
|
+
|
|
139
|
+
// Check if this invocation affects this spell
|
|
140
|
+
if (!def.affectsSpells || !def.affectsSpells.some(s => spellNameLower.includes(s))) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Apply description modifications
|
|
145
|
+
if (def.modifyDescription && modifiedSpell.description) {
|
|
146
|
+
modifiedSpell.description = def.modifyDescription(modifiedSpell.description);
|
|
147
|
+
hasModifications = true;
|
|
148
|
+
debug.log(`🔮 Applied ${invocation.name} description to ${spell.name}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Apply range modifications
|
|
152
|
+
if (def.modifyRange) {
|
|
153
|
+
modifiedSpell.range = def.modifyRange();
|
|
154
|
+
hasModifications = true;
|
|
155
|
+
debug.log(`🔮 Applied ${invocation.name} range to ${spell.name}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return hasModifications ? modifiedSpell : spell;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Apply invocation modifications to a damage formula
|
|
164
|
+
* This is specifically for damage rolls that need modification
|
|
165
|
+
* @param {String} spellName - Name of the spell being cast
|
|
166
|
+
* @param {String} damageFormula - The base damage formula
|
|
167
|
+
* @param {Array} invocations - Array of active invocations
|
|
168
|
+
* @param {Object} characterData - The character data object
|
|
169
|
+
* @returns {Object} { formula: modified formula, display: display formula }
|
|
170
|
+
*/
|
|
171
|
+
function applyInvocationToDamage(spellName, damageFormula, invocations, characterData) {
|
|
172
|
+
if (!damageFormula || !invocations || invocations.length === 0) {
|
|
173
|
+
return { formula: damageFormula, display: damageFormula };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const spellNameLower = (spellName || '').toLowerCase();
|
|
177
|
+
let modifiedFormula = damageFormula;
|
|
178
|
+
let modifiedDisplay = damageFormula;
|
|
179
|
+
let hasModifications = false;
|
|
180
|
+
|
|
181
|
+
// Check each active invocation
|
|
182
|
+
invocations.forEach(invocation => {
|
|
183
|
+
const def = invocation.definition;
|
|
184
|
+
|
|
185
|
+
// Check if this invocation affects this spell
|
|
186
|
+
if (!def.affectsSpells || !def.affectsSpells.some(s => spellNameLower.includes(s))) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Apply damage modifications
|
|
191
|
+
if (def.modifyDamage) {
|
|
192
|
+
const modified = def.modifyDamage(modifiedFormula, characterData);
|
|
193
|
+
if (modified !== modifiedFormula) {
|
|
194
|
+
modifiedFormula = modified;
|
|
195
|
+
modifiedDisplay = modified;
|
|
196
|
+
hasModifications = true;
|
|
197
|
+
debug.log(`🔮 Applied ${invocation.name} damage to ${spellName}: ${damageFormula} → ${modified}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
formula: modifiedFormula,
|
|
204
|
+
display: modifiedDisplay,
|
|
205
|
+
modified: hasModifications
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ===== EXPORTS =====
|
|
210
|
+
|
|
211
|
+
// Export functions to globalThis
|
|
212
|
+
globalThis.getActiveInvocations = getActiveInvocations;
|
|
213
|
+
globalThis.hasInvocation = hasInvocation;
|
|
214
|
+
globalThis.applyInvocationToSpell = applyInvocationToSpell;
|
|
215
|
+
globalThis.applyInvocationToDamage = applyInvocationToDamage;
|
|
216
|
+
|
|
217
|
+
debug.log('✅ Warlock Invocations module loaded');
|
|
218
|
+
|
|
219
|
+
})();
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window Management Module
|
|
3
|
+
*
|
|
4
|
+
* Handles popup window features:
|
|
5
|
+
* - Window size persistence (save/restore dimensions)
|
|
6
|
+
* - Status bar integration (toggle and data sync)
|
|
7
|
+
* - Window state management
|
|
8
|
+
*
|
|
9
|
+
* This module manages the popup window's appearance and communication
|
|
10
|
+
* with the Roll20 status bar overlay system.
|
|
11
|
+
*
|
|
12
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
13
|
+
*
|
|
14
|
+
* Functions exported to globalThis:
|
|
15
|
+
* - saveWindowSize()
|
|
16
|
+
* - loadWindowSize()
|
|
17
|
+
* - initWindowSizeTracking()
|
|
18
|
+
* - initStatusBarButton()
|
|
19
|
+
* - sendStatusUpdate(targetWindow)
|
|
20
|
+
*
|
|
21
|
+
* State exported to globalThis:
|
|
22
|
+
* - statusBarWindow (via getter/setter)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
(function() {
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
// ===== WINDOW SIZE PERSISTENCE =====
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save current window dimensions to storage
|
|
32
|
+
*/
|
|
33
|
+
function saveWindowSize() {
|
|
34
|
+
// Check if browserAPI is available
|
|
35
|
+
if (typeof browserAPI === 'undefined' || !browserAPI) {
|
|
36
|
+
debug.log('⚠️ browserAPI not available, skipping window size save');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const width = window.outerWidth;
|
|
41
|
+
const height = window.outerHeight;
|
|
42
|
+
|
|
43
|
+
browserAPI.storage.local.set({
|
|
44
|
+
popupWindowSize: { width, height }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
debug.log(`💾 Saved window size: ${width}x${height}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load and apply saved window dimensions
|
|
52
|
+
*/
|
|
53
|
+
async function loadWindowSize() {
|
|
54
|
+
// Check if browserAPI is available
|
|
55
|
+
if (typeof browserAPI === 'undefined' || !browserAPI) {
|
|
56
|
+
debug.log('⚠️ browserAPI not available, skipping window size restore');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await browserAPI.storage.local.get(['popupWindowSize']);
|
|
62
|
+
if (result.popupWindowSize) {
|
|
63
|
+
const { width, height } = result.popupWindowSize;
|
|
64
|
+
window.resizeTo(width, height);
|
|
65
|
+
debug.log(`📐 Restored window size: ${width}x${height}`);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
debug.warn('⚠️ Could not restore window size:', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize window size tracking
|
|
74
|
+
* Loads saved size on startup and tracks changes
|
|
75
|
+
*/
|
|
76
|
+
function initWindowSizeTracking() {
|
|
77
|
+
// Load saved size on startup
|
|
78
|
+
loadWindowSize();
|
|
79
|
+
|
|
80
|
+
// Save size when window is resized
|
|
81
|
+
let resizeTimeout;
|
|
82
|
+
window.addEventListener('resize', () => {
|
|
83
|
+
clearTimeout(resizeTimeout);
|
|
84
|
+
resizeTimeout = setTimeout(() => {
|
|
85
|
+
saveWindowSize();
|
|
86
|
+
}, 500); // Debounce to avoid excessive saves
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
debug.log('📐 Window size tracking initialized');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ===== STATUS BAR INTEGRATION =====
|
|
93
|
+
|
|
94
|
+
// Reference to status bar window (if using window-based status bar)
|
|
95
|
+
let statusBarWindow = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Initialize status bar button
|
|
99
|
+
* Sets up click handler to toggle Roll20 GM panel overlay (status bar)
|
|
100
|
+
*/
|
|
101
|
+
function initStatusBarButton() {
|
|
102
|
+
const statusBarBtn = document.getElementById('status-bar-btn');
|
|
103
|
+
if (statusBarBtn) {
|
|
104
|
+
statusBarBtn.addEventListener('click', async () => {
|
|
105
|
+
// Send message to Roll20 tabs to toggle the status bar overlay
|
|
106
|
+
try {
|
|
107
|
+
const tabs = await browserAPI.tabs.query({ url: '*://app.roll20.net/*' });
|
|
108
|
+
|
|
109
|
+
if (tabs.length === 0) {
|
|
110
|
+
showNotification('⚠️ No Roll20 tabs found. Open Roll20 to use the status bar.', 'warning');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Send toggle message to all Roll20 tabs
|
|
115
|
+
for (const tab of tabs) {
|
|
116
|
+
try {
|
|
117
|
+
await browserAPI.tabs.sendMessage(tab.id, {
|
|
118
|
+
action: 'toggleStatusBar',
|
|
119
|
+
enabled: undefined // Toggle current state
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
showNotification('📊 Status bar toggled', 'success');
|
|
123
|
+
debug.log('📊 Status bar overlay toggled');
|
|
124
|
+
} catch (error) {
|
|
125
|
+
debug.warn('⚠️ Could not toggle status bar on tab:', error.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
debug.error('❌ Failed to toggle status bar:', error);
|
|
130
|
+
showNotification('❌ Failed to toggle status bar', 'error');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
debug.log('✅ Status bar button initialized');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Send status update to status bar window
|
|
139
|
+
* @param {Window} targetWindow - Optional target window (defaults to statusBarWindow)
|
|
140
|
+
*/
|
|
141
|
+
function sendStatusUpdate(targetWindow = null) {
|
|
142
|
+
// Use provided target window or the stored statusBarWindow
|
|
143
|
+
const target = targetWindow || statusBarWindow;
|
|
144
|
+
|
|
145
|
+
if (!target || target.closed) {
|
|
146
|
+
debug.log('📊 No valid status bar window to send to');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!characterData) {
|
|
151
|
+
debug.log('📊 No character data available to send');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const statusData = {
|
|
156
|
+
action: 'updateStatusData',
|
|
157
|
+
data: {
|
|
158
|
+
name: characterData.name || characterData.character_name,
|
|
159
|
+
hitPoints: characterData.hitPoints || characterData.hit_points,
|
|
160
|
+
temporaryHP: characterData.temporaryHP || 0,
|
|
161
|
+
concentrating: !!concentratingSpell,
|
|
162
|
+
concentrationSpell: concentratingSpell || '',
|
|
163
|
+
activeBuffs: activeBuffs || [],
|
|
164
|
+
activeDebuffs: activeConditions || [],
|
|
165
|
+
spellSlots: characterData.spellSlots || {}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
target.postMessage(statusData, '*');
|
|
170
|
+
debug.log('📊 Sent status update to status bar', statusData.data);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ===== EXPORTS =====
|
|
174
|
+
|
|
175
|
+
// Export statusBarWindow state variable
|
|
176
|
+
Object.defineProperty(globalThis, 'statusBarWindow', {
|
|
177
|
+
get: () => statusBarWindow,
|
|
178
|
+
set: (value) => { statusBarWindow = value; },
|
|
179
|
+
configurable: true
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Export functions to globalThis
|
|
183
|
+
globalThis.saveWindowSize = saveWindowSize;
|
|
184
|
+
globalThis.loadWindowSize = loadWindowSize;
|
|
185
|
+
globalThis.initWindowSizeTracking = initWindowSizeTracking;
|
|
186
|
+
globalThis.initStatusBarButton = initStatusBarButton;
|
|
187
|
+
globalThis.sendStatusUpdate = sendStatusUpdate;
|
|
188
|
+
|
|
189
|
+
debug.log('✅ Window Management module loaded');
|
|
190
|
+
|
|
191
|
+
// ===== AUTO-INITIALIZATION =====
|
|
192
|
+
|
|
193
|
+
// Initialize window size tracking when module loads
|
|
194
|
+
// Check if we're in the main popup window context
|
|
195
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
196
|
+
// Initialize immediately if DOM is ready, otherwise wait
|
|
197
|
+
if (document.readyState === 'loading') {
|
|
198
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
199
|
+
initWindowSizeTracking();
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
// DOM already ready, but use pendingOperations if available
|
|
203
|
+
if (typeof pendingOperations !== 'undefined' && !domReady) {
|
|
204
|
+
pendingOperations.push(initWindowSizeTracking);
|
|
205
|
+
} else {
|
|
206
|
+
initWindowSizeTracking();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
})();
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-agnostic character renderer. Builds a sheet DOM from an IRCharacter
|
|
3
|
+
* using the `h()` builder (never innerHTML). Adapters mount the returned element
|
|
4
|
+
* and supply roll/use callbacks.
|
|
5
|
+
*
|
|
6
|
+
* D&D characters get the familiar ability grid (derived view); every character -
|
|
7
|
+
* D&D or not - also gets generic Resources / Attributes / Actions sections, which
|
|
8
|
+
* is where custom stats and charge-with-reset abilities finally show up.
|
|
9
|
+
*/
|
|
10
|
+
import type { IRAction, IRAttribute, IRCharacter } from '../ir/types';
|
|
11
|
+
import { deriveDnd, DND_ABILITIES } from '../ir/views/dnd5e';
|
|
12
|
+
import { h } from './h';
|
|
13
|
+
|
|
14
|
+
export interface RenderOpts {
|
|
15
|
+
/** Called when a rollable element (ability/save/skill) is clicked. */
|
|
16
|
+
onRoll?: (label: string, modifier: number) => void;
|
|
17
|
+
/** Called when an action/spell "use" is clicked. */
|
|
18
|
+
onUse?: (action: IRAction) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const RESET_LABEL: Record<string, string> = { shortRest: 'SR', longRest: 'LR' };
|
|
22
|
+
const signed = (n: number) => `${n >= 0 ? '+' : ''}${n}`;
|
|
23
|
+
|
|
24
|
+
function sectionHeader(title: string): HTMLElement {
|
|
25
|
+
return h('div', { class: 'section-header', text: title });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Small badge showing a reset period (SR / LR / raw). */
|
|
29
|
+
function resetBadge(reset: string | null | undefined): HTMLElement | null {
|
|
30
|
+
if (!reset) return null;
|
|
31
|
+
return h('span', { class: 'cc-reset-badge', text: RESET_LABEL[reset] ?? reset });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** current / max pill, e.g. "2 / 2". */
|
|
35
|
+
function poolPill(current: number, max: number): HTMLElement {
|
|
36
|
+
return h('span', { class: 'cc-pool' },
|
|
37
|
+
h('span', { class: 'cc-pool-current', text: String(current) }),
|
|
38
|
+
' / ',
|
|
39
|
+
h('span', { class: 'cc-pool-max', text: String(max) }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Top combat-stats strip (HP / AC / Speed / Init / Prof) - whichever exist. */
|
|
43
|
+
function combatStats(ir: IRCharacter): HTMLElement | null {
|
|
44
|
+
const { byVar } = ir;
|
|
45
|
+
// DiceCloud sheets name these variably; try common aliases.
|
|
46
|
+
const pick = (...names: string[]) => names.map((n) => byVar[n]).find(Boolean);
|
|
47
|
+
const items: [string, string][] = [];
|
|
48
|
+
|
|
49
|
+
const hp = pick('hitPoints', 'hp');
|
|
50
|
+
if (hp) items.push(['HP', `${hp.total - hp.damage}/${hp.total}`]);
|
|
51
|
+
const ac = pick('armorClass', 'armor', 'ac');
|
|
52
|
+
if (ac && ac.value) items.push(['AC', String(ac.value)]);
|
|
53
|
+
const speed = pick('speed', 'walkingSpeed');
|
|
54
|
+
if (speed && speed.value) items.push(['Speed', String(speed.value)]);
|
|
55
|
+
const init = pick('initiative', 'initiativeBonus', 'initiativeMod');
|
|
56
|
+
if (init) items.push(['Init', signed(init.value)]);
|
|
57
|
+
const prof = pick('proficiencyBonus', 'proficiency');
|
|
58
|
+
if (prof && prof.value) items.push(['Prof', signed(prof.value)]);
|
|
59
|
+
|
|
60
|
+
if (items.length === 0) return null;
|
|
61
|
+
return h('div', { class: 'cc-combat' },
|
|
62
|
+
...items.map(([label, val]) =>
|
|
63
|
+
h('div', { class: 'cc-stat' },
|
|
64
|
+
h('div', { class: 'cc-stat-label', text: label }),
|
|
65
|
+
h('div', { class: 'cc-stat-value', text: val }))));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Trained skills (skillType 'skill'), clickable to roll. */
|
|
69
|
+
function skillsSection(ir: IRCharacter, opts: RenderOpts): HTMLElement | null {
|
|
70
|
+
const skills = ir.skills.filter((s) => s.skillType === 'skill' && s.active && s.variableName);
|
|
71
|
+
if (skills.length === 0) return null;
|
|
72
|
+
|
|
73
|
+
const list = h('div', { class: 'cc-skill-list' });
|
|
74
|
+
for (const s of skills) {
|
|
75
|
+
list.appendChild(
|
|
76
|
+
h('div', {
|
|
77
|
+
class: 'cc-skill' + (s.proficiency > 0 ? ' cc-proficient' : ''),
|
|
78
|
+
title: `Roll ${s.name}`,
|
|
79
|
+
onClick: () => opts.onRoll?.(s.name, s.value),
|
|
80
|
+
},
|
|
81
|
+
h('span', { class: 'cc-skill-name', text: s.name }),
|
|
82
|
+
h('span', { class: 'cc-skill-bonus', text: signed(s.value) })),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return h('div', {}, sectionHeader('Skills'), list);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** D&D ability grid (only when the derived view has abilities). */
|
|
89
|
+
function abilityGrid(ir: IRCharacter, opts: RenderOpts): HTMLElement | null {
|
|
90
|
+
const dnd = deriveDnd(ir);
|
|
91
|
+
if (Object.keys(dnd.abilities).length === 0) return null;
|
|
92
|
+
|
|
93
|
+
const grid = h('div', { class: 'ability-grid' });
|
|
94
|
+
for (const ab of DND_ABILITIES) {
|
|
95
|
+
const a = dnd.abilities[ab];
|
|
96
|
+
if (!a) continue;
|
|
97
|
+
const label = ab.slice(0, 3).toUpperCase();
|
|
98
|
+
const save = dnd.saves[ab];
|
|
99
|
+
|
|
100
|
+
const rollCell = (kind: string, value: number) =>
|
|
101
|
+
h('div', {
|
|
102
|
+
class: 'cc-ability-roll',
|
|
103
|
+
title: `Roll ${label} ${kind === 'CHK' ? 'check' : 'save'}`,
|
|
104
|
+
onClick: () => opts.onRoll?.(`${label} ${kind === 'CHK' ? 'check' : 'save'}`, value),
|
|
105
|
+
},
|
|
106
|
+
h('div', { class: 'cc-roll-label', text: kind }),
|
|
107
|
+
h('div', { class: 'cc-roll-val', text: signed(value) }));
|
|
108
|
+
|
|
109
|
+
grid.appendChild(
|
|
110
|
+
h('div', { class: 'ability-box' },
|
|
111
|
+
h('div', { class: 'ability-name', text: label }),
|
|
112
|
+
h('div', { class: 'ability-score', text: String(a.score) }),
|
|
113
|
+
h('div', { class: 'cc-ability-rolls' },
|
|
114
|
+
rollCell('CHK', a.modifier),
|
|
115
|
+
save !== undefined ? rollCell('SAV', save) : null)),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return h('div', {}, sectionHeader('Abilities'), grid);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Resources: anything with a max + (usually) a reset - charges, slots, hit dice. */
|
|
122
|
+
function resourcesSection(ir: IRCharacter): HTMLElement | null {
|
|
123
|
+
const isResourceLike = (a: IRAttribute) =>
|
|
124
|
+
(a.type === 'resource' || a.type === 'spellSlot' || a.type === 'hitDice') && a.total > 0;
|
|
125
|
+
const resources = ir.attributes.filter(isResourceLike);
|
|
126
|
+
if (resources.length === 0) return null;
|
|
127
|
+
|
|
128
|
+
const list = h('div', { class: 'cc-resource-list' });
|
|
129
|
+
for (const r of resources) {
|
|
130
|
+
const current = r.total - r.damage;
|
|
131
|
+
const sizeNote = r.hitDiceSize ? ` ${r.hitDiceSize}` : '';
|
|
132
|
+
list.appendChild(
|
|
133
|
+
h('div', { class: 'cc-resource' + (r.active ? '' : ' cc-inactive') },
|
|
134
|
+
h('span', { class: 'cc-resource-name', text: r.name + sizeNote }),
|
|
135
|
+
poolPill(current, r.total),
|
|
136
|
+
resetBadge(r.reset)),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return h('div', {}, sectionHeader('Resources'), list);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generic attributes: custom stats with no D&D analog (sanity, glory, ...). We
|
|
144
|
+
* skip the ones already shown elsewhere (abilities, resources, HP, modifiers).
|
|
145
|
+
*/
|
|
146
|
+
function attributesSection(ir: IRCharacter): HTMLElement | null {
|
|
147
|
+
// Hide structural types (shown elsewhere) and 'utility' (internal computed
|
|
148
|
+
// values like Class DC / proficiency ranks). What's left is deliberate custom
|
|
149
|
+
// stats - sanity, glory, corruption, etc.
|
|
150
|
+
const hidden = new Set([
|
|
151
|
+
'ability', 'modifier', 'healthBar', 'resource', 'spellSlot', 'hitDice', 'utility',
|
|
152
|
+
]);
|
|
153
|
+
// Also drop zero-valued entries - on sparse sheets these are unset internals
|
|
154
|
+
// (Speed 0, Size 0, Level 0) rather than meaningful custom stats.
|
|
155
|
+
const custom = ir.attributes.filter((a) => !hidden.has(a.type) && a.variableName && a.value !== 0);
|
|
156
|
+
if (custom.length === 0) return null;
|
|
157
|
+
|
|
158
|
+
const list = h('div', { class: 'cc-attr-list' });
|
|
159
|
+
for (const a of custom) {
|
|
160
|
+
list.appendChild(
|
|
161
|
+
h('div', { class: 'cc-attr' + (a.active ? '' : ' cc-inactive') },
|
|
162
|
+
h('span', { class: 'cc-attr-name', text: a.name }),
|
|
163
|
+
h('span', { class: 'cc-attr-value', text: String(a.value) })),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return h('div', {}, sectionHeader('Attributes'), list);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Actions & spells, showing real uses (current/max + reset). */
|
|
170
|
+
function actionsSection(ir: IRCharacter, opts: RenderOpts): HTMLElement | null {
|
|
171
|
+
if (ir.actions.length === 0) return null;
|
|
172
|
+
|
|
173
|
+
const list = h('div', { class: 'cc-action-list' });
|
|
174
|
+
for (const action of ir.actions) {
|
|
175
|
+
const meta: HTMLElement[] = [];
|
|
176
|
+
if (action.kind === 'spell' && action.spell) {
|
|
177
|
+
meta.push(h('span', { class: 'cc-action-tag', text: `L${action.spell.level}` }));
|
|
178
|
+
}
|
|
179
|
+
if (action.attack) {
|
|
180
|
+
meta.push(h('span', { class: 'cc-action-attack', title: 'Attack bonus', text: signed(action.attack.bonus) }));
|
|
181
|
+
}
|
|
182
|
+
for (const d of action.damage) {
|
|
183
|
+
meta.push(h('span', { class: 'cc-action-damage', text: d.type ? `${d.formula} ${d.type}` : d.formula }));
|
|
184
|
+
}
|
|
185
|
+
const usesEl = action.uses
|
|
186
|
+
? h('span', { class: 'cc-action-uses' }, poolPill(action.uses.current, action.uses.max), resetBadge(action.uses.reset))
|
|
187
|
+
: null;
|
|
188
|
+
|
|
189
|
+
list.appendChild(
|
|
190
|
+
h('div', {
|
|
191
|
+
class: `cc-action cc-action-${action.kind}` + (action.active ? '' : ' cc-inactive'),
|
|
192
|
+
onClick: opts.onUse ? () => opts.onUse!(action) : undefined,
|
|
193
|
+
},
|
|
194
|
+
h('span', { class: 'cc-action-name', text: action.name }),
|
|
195
|
+
...meta,
|
|
196
|
+
usesEl),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return h('div', {}, sectionHeader('Actions & Spells'), list);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Inventory list with quantity + equipped marker. */
|
|
203
|
+
function inventorySection(ir: IRCharacter): HTMLElement | null {
|
|
204
|
+
if (ir.inventory.length === 0) return null;
|
|
205
|
+
const list = h('div', { class: 'cc-item-list' });
|
|
206
|
+
for (const item of ir.inventory) {
|
|
207
|
+
list.appendChild(
|
|
208
|
+
h('div', { class: 'cc-item' + (item.equipped ? ' cc-equipped' : '') },
|
|
209
|
+
item.equipped ? h('span', { class: 'cc-equipped-dot', title: 'Equipped' }) : null,
|
|
210
|
+
h('span', { class: 'cc-item-name', text: item.name }),
|
|
211
|
+
item.quantity !== 1 ? h('span', { class: 'cc-item-qty', text: `x${item.quantity}` }) : null),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return h('div', {}, sectionHeader('Inventory'), list);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Build the full character sheet element from an IR. */
|
|
218
|
+
export function renderCharacterSheet(ir: IRCharacter, opts: RenderOpts = {}): HTMLElement {
|
|
219
|
+
const header = h('div', { class: 'cc-header' },
|
|
220
|
+
ir.portrait ? h('img', { class: 'cc-portrait', src: ir.portrait, alt: ir.name }) : null,
|
|
221
|
+
h('div', { class: 'cc-title' },
|
|
222
|
+
h('div', { class: 'cc-name', text: ir.name || 'Unnamed' }),
|
|
223
|
+
h('span', { class: 'cc-system', text: ir.systemHint })));
|
|
224
|
+
|
|
225
|
+
return h('div', { class: 'cc-sheet', dataset: { system: ir.systemHint } },
|
|
226
|
+
header,
|
|
227
|
+
combatStats(ir),
|
|
228
|
+
abilityGrid(ir, opts),
|
|
229
|
+
skillsSection(ir, opts),
|
|
230
|
+
resourcesSection(ir),
|
|
231
|
+
actionsSection(ir, opts),
|
|
232
|
+
attributesSection(ir),
|
|
233
|
+
inventorySection(ir));
|
|
234
|
+
}
|