@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,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sheet Builder Module
|
|
3
|
+
*
|
|
4
|
+
* Handles the main character sheet UI construction.
|
|
5
|
+
* Builds all UI sections including:
|
|
6
|
+
* - Character header (name, class, level, race)
|
|
7
|
+
* - Combat stats (AC, HP, initiative, death saves, inspiration)
|
|
8
|
+
* - Abilities, saves, and skills
|
|
9
|
+
* - Actions & attacks
|
|
10
|
+
* - Spells (organized by source and level)
|
|
11
|
+
* - Inventory & equipment
|
|
12
|
+
* - Companions (familiars, summons, animal companions)
|
|
13
|
+
* - Resources and spell slots
|
|
14
|
+
*
|
|
15
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
16
|
+
*
|
|
17
|
+
* Functions exported to globalThis:
|
|
18
|
+
* - buildSheet(data)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
(function() {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the entire character sheet UI from character data
|
|
26
|
+
* @param {Object} data - Character data object
|
|
27
|
+
*/
|
|
28
|
+
function buildSheet(data) {
|
|
29
|
+
debug.log('Building character sheet...');
|
|
30
|
+
debug.log('📊 Character data received:', data);
|
|
31
|
+
debug.log('✨ Spell slots data:', data.spellSlots);
|
|
32
|
+
|
|
33
|
+
// Normalize snake_case fields to camelCase (database uses snake_case, UI expects camelCase)
|
|
34
|
+
debug.log('🔄 HP normalization check:', {
|
|
35
|
+
has_hit_points: !!data.hit_points,
|
|
36
|
+
has_hitPoints: !!data.hitPoints,
|
|
37
|
+
hit_points_value: data.hit_points,
|
|
38
|
+
hitPoints_value: data.hitPoints,
|
|
39
|
+
hitPoints_type: typeof data.hitPoints,
|
|
40
|
+
full_data_keys: Object.keys(data)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (data.hit_points && !data.hitPoints) {
|
|
44
|
+
data.hitPoints = data.hit_points;
|
|
45
|
+
debug.log('✅ Normalized hit_points to hitPoints:', data.hitPoints);
|
|
46
|
+
} else if (!data.hit_points && !data.hitPoints) {
|
|
47
|
+
debug.warn('⚠️ No HP data found in character data! Keys available:', Object.keys(data));
|
|
48
|
+
}
|
|
49
|
+
if (data.character_name && !data.name) {
|
|
50
|
+
data.name = data.character_name;
|
|
51
|
+
}
|
|
52
|
+
if (data.armor_class !== undefined && data.armorClass === undefined) {
|
|
53
|
+
data.armorClass = data.armor_class;
|
|
54
|
+
}
|
|
55
|
+
if (data.hit_dice && !data.hitDice) {
|
|
56
|
+
data.hitDice = data.hit_dice;
|
|
57
|
+
}
|
|
58
|
+
if (data.temporary_hp !== undefined && data.temporaryHP === undefined) {
|
|
59
|
+
data.temporaryHP = data.temporary_hp;
|
|
60
|
+
}
|
|
61
|
+
if (data.death_saves && !data.deathSaves) {
|
|
62
|
+
data.deathSaves = data.death_saves;
|
|
63
|
+
}
|
|
64
|
+
if (data.proficiency_bonus !== undefined && data.proficiencyBonus === undefined) {
|
|
65
|
+
data.proficiencyBonus = data.proficiency_bonus;
|
|
66
|
+
}
|
|
67
|
+
if (data.spell_slots && !data.spellSlots) {
|
|
68
|
+
data.spellSlots = data.spell_slots;
|
|
69
|
+
}
|
|
70
|
+
// Normalize nested spell-slot format ({ level1: { current, max } }) into the
|
|
71
|
+
// flat keys (level1SpellSlots / level1SpellSlotsMax) that the cast, upcast,
|
|
72
|
+
// and Divine Smite modals read. Some parsers emit nested, others flat; the
|
|
73
|
+
// slot *display* tolerates both, but those modals read flat only — so with
|
|
74
|
+
// nested data the slots render yet never decrement on cast and Divine Smite
|
|
75
|
+
// lists no levels. Flat is authoritative (casting decrements the flat key),
|
|
76
|
+
// so we only fill flat keys that are missing and never clobber them.
|
|
77
|
+
if (data.spellSlots) {
|
|
78
|
+
for (let level = 1; level <= 9; level++) {
|
|
79
|
+
const nested = data.spellSlots[`level${level}`];
|
|
80
|
+
if (nested && typeof nested === 'object') {
|
|
81
|
+
const curKey = `level${level}SpellSlots`;
|
|
82
|
+
const maxKey = `level${level}SpellSlotsMax`;
|
|
83
|
+
if (data.spellSlots[maxKey] === undefined && nested.max !== undefined) {
|
|
84
|
+
data.spellSlots[maxKey] = nested.max;
|
|
85
|
+
}
|
|
86
|
+
if (data.spellSlots[curKey] === undefined && nested.current !== undefined) {
|
|
87
|
+
data.spellSlots[curKey] = nested.current;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (data.attribute_mods && !data.attributeMods) {
|
|
93
|
+
data.attributeMods = data.attribute_mods;
|
|
94
|
+
}
|
|
95
|
+
if (data.notification_color && !data.notificationColor) {
|
|
96
|
+
data.notificationColor = data.notification_color;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// DEBUG: Log actions and spells arrays
|
|
100
|
+
debug.log('🔍 Actions array check:', {
|
|
101
|
+
has_actions: !!data.actions,
|
|
102
|
+
is_array: Array.isArray(data.actions),
|
|
103
|
+
length: data.actions?.length,
|
|
104
|
+
first_action: data.actions?.[0]?.name
|
|
105
|
+
});
|
|
106
|
+
debug.log('🔍 Spells array check:', {
|
|
107
|
+
has_spells: !!data.spells,
|
|
108
|
+
is_array: Array.isArray(data.spells),
|
|
109
|
+
length: data.spells?.length,
|
|
110
|
+
first_spell: data.spells?.[0]?.name
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Safety check: Ensure critical DOM elements exist before building
|
|
114
|
+
const charNameEl = document.getElementById('char-name');
|
|
115
|
+
if (!charNameEl) {
|
|
116
|
+
debug.error('❌ Critical DOM elements not found! DOM may not be ready yet.');
|
|
117
|
+
debug.log('⏳ Queuing buildSheet for when DOM is ready...');
|
|
118
|
+
if (typeof domReady !== 'undefined' && !domReady) {
|
|
119
|
+
if (typeof pendingOperations !== 'undefined') {
|
|
120
|
+
pendingOperations.push(() => buildSheet(data));
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// DOM claims to be ready but elements aren't there - retry after a short delay
|
|
124
|
+
debug.log('⏱️ DOM ready but elements missing - retrying in 100ms...');
|
|
125
|
+
setTimeout(() => buildSheet(data), 100);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Store character data globally for other modules to access
|
|
131
|
+
globalThis.characterData = data;
|
|
132
|
+
|
|
133
|
+
// Helper function to safely set element properties
|
|
134
|
+
const safeSet = (id, prop, value) => {
|
|
135
|
+
const el = document.getElementById(id);
|
|
136
|
+
if (el) el[prop] = value;
|
|
137
|
+
return el;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Initialize concentration from saved data
|
|
141
|
+
if (data.concentration) {
|
|
142
|
+
if (typeof window.concentratingSpell !== 'undefined') {
|
|
143
|
+
window.concentratingSpell = data.concentration;
|
|
144
|
+
}
|
|
145
|
+
if (typeof updateConcentrationDisplay === 'function') {
|
|
146
|
+
updateConcentrationDisplay();
|
|
147
|
+
}
|
|
148
|
+
debug.log(`🧠 Restored concentration: ${data.concentration}`);
|
|
149
|
+
} else {
|
|
150
|
+
if (typeof window.concentratingSpell !== 'undefined') {
|
|
151
|
+
window.concentratingSpell = null;
|
|
152
|
+
}
|
|
153
|
+
if (typeof updateConcentrationDisplay === 'function') {
|
|
154
|
+
updateConcentrationDisplay();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Character name (without source badge)
|
|
159
|
+
const characterName = data.name || 'Character';
|
|
160
|
+
charNameEl.textContent = characterName;
|
|
161
|
+
|
|
162
|
+
// Update color picker emoji in systems bar
|
|
163
|
+
const currentColorEmoji = getColorEmoji(data.notificationColor || '#3498db');
|
|
164
|
+
const colorEmojiEl = document.getElementById('color-emoji');
|
|
165
|
+
if (colorEmojiEl) {
|
|
166
|
+
colorEmojiEl.textContent = currentColorEmoji;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Populate color palette in systems bar (but keep it hidden initially)
|
|
170
|
+
const colorPaletteEl = document.getElementById('color-palette');
|
|
171
|
+
if (colorPaletteEl) {
|
|
172
|
+
colorPaletteEl.innerHTML = createColorPalette(data.notificationColor || '#3498db');
|
|
173
|
+
colorPaletteEl.style.display = 'none'; // Start hidden - user must click to show
|
|
174
|
+
colorPaletteEl.style.gridTemplateColumns = 'repeat(4, 1fr)';
|
|
175
|
+
colorPaletteEl.style.gap = '10px';
|
|
176
|
+
colorPaletteEl.style.width = '180px';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Initialize hit dice if needed
|
|
180
|
+
initializeHitDice();
|
|
181
|
+
|
|
182
|
+
// Initialize temporary HP if needed
|
|
183
|
+
if (data.temporaryHP === undefined) {
|
|
184
|
+
data.temporaryHP = 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Initialize inspiration if needed
|
|
188
|
+
if (data.inspiration === undefined) {
|
|
189
|
+
data.inspiration = false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Initialize last roll tracking for heroic inspiration
|
|
193
|
+
if (data.lastRoll === undefined) {
|
|
194
|
+
data.lastRoll = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Capitalize race name - handle both string and object formats
|
|
198
|
+
let raceName = 'Unknown';
|
|
199
|
+
if (data.race) {
|
|
200
|
+
if (typeof data.race === 'string') {
|
|
201
|
+
raceName = data.race.charAt(0).toUpperCase() + data.race.slice(1);
|
|
202
|
+
} else if (typeof data.race === 'object') {
|
|
203
|
+
// If race is an object, try to extract the value from various possible properties
|
|
204
|
+
let raceValue = data.race.value || data.race.name || data.race.text ||
|
|
205
|
+
data.race.variableName || data.race.displayName;
|
|
206
|
+
|
|
207
|
+
// If still no value, try to get something useful from the object
|
|
208
|
+
if (!raceValue) {
|
|
209
|
+
// Check if it has a tags property that might indicate race type
|
|
210
|
+
if (data.race.tags && Array.isArray(data.race.tags)) {
|
|
211
|
+
const raceTags = data.race.tags.filter(tag =>
|
|
212
|
+
!tag.toLowerCase().includes('class') &&
|
|
213
|
+
!tag.toLowerCase().includes('level')
|
|
214
|
+
);
|
|
215
|
+
if (raceTags.length > 0) {
|
|
216
|
+
raceValue = raceTags[0];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Last resort: look for any string property that seems like a race name
|
|
221
|
+
if (!raceValue) {
|
|
222
|
+
const keys = Object.keys(data.race);
|
|
223
|
+
for (const key of keys) {
|
|
224
|
+
if (typeof data.race[key] === 'string' && data.race[key].length > 0 && data.race[key].length < 50) {
|
|
225
|
+
raceValue = data.race[key];
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// If we found something, capitalize it; otherwise use "Unknown"
|
|
233
|
+
if (raceValue && typeof raceValue === 'string') {
|
|
234
|
+
raceName = raceValue.charAt(0).toUpperCase() + raceValue.slice(1);
|
|
235
|
+
} else {
|
|
236
|
+
debug.warn('Could not extract race name from object:', data.race);
|
|
237
|
+
raceName = 'Unknown Race';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Layer 1: Class, Level, Race, Hit Dice
|
|
243
|
+
safeSet('char-class', 'textContent', data.class || 'Unknown');
|
|
244
|
+
safeSet('char-level', 'textContent', data.level || 1);
|
|
245
|
+
safeSet('char-race', 'textContent', raceName);
|
|
246
|
+
// Defensive initialization for hitDice
|
|
247
|
+
if (!data.hitDice) {
|
|
248
|
+
data.hitDice = { current: 0, max: 0, type: 'd6' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
safeSet('char-hit-dice', 'textContent', `${data.hitDice.current || 0}/${data.hitDice.max || 0} ${data.hitDice.type || 'd6'}`);
|
|
252
|
+
|
|
253
|
+
// Layer 2: AC, Speed, Proficiency, Death Saves, Inspiration
|
|
254
|
+
safeSet('char-ac', 'textContent', calculateTotalAC(data));
|
|
255
|
+
safeSet('char-speed', 'textContent', `${data.speed || 30} ft`);
|
|
256
|
+
safeSet('char-proficiency', 'textContent', `+${data.proficiencyBonus || 0}`);
|
|
257
|
+
|
|
258
|
+
// Death Saves
|
|
259
|
+
const deathSavesDisplay = document.getElementById('death-saves-display');
|
|
260
|
+
const deathSavesValue = document.getElementById('death-saves-value');
|
|
261
|
+
|
|
262
|
+
// Defensive initialization for deathSaves
|
|
263
|
+
if (!data.deathSaves) {
|
|
264
|
+
data.deathSaves = { successes: 0, failures: 0 };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (deathSavesValue) {
|
|
268
|
+
deathSavesValue.innerHTML = `
|
|
269
|
+
<span style="color: var(--accent-success);">✓${data.deathSaves.successes || 0}</span> /
|
|
270
|
+
<span style="color: var(--accent-danger);">✗${data.deathSaves.failures || 0}</span>
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
if (deathSavesDisplay) {
|
|
274
|
+
if (data.deathSaves.successes > 0 || data.deathSaves.failures > 0) {
|
|
275
|
+
deathSavesDisplay.style.background = 'var(--bg-action)';
|
|
276
|
+
} else {
|
|
277
|
+
deathSavesDisplay.style.background = 'var(--bg-tertiary)';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Inspiration
|
|
282
|
+
const inspirationDisplay = document.getElementById('inspiration-display');
|
|
283
|
+
const inspirationValue = document.getElementById('inspiration-value');
|
|
284
|
+
if (inspirationValue) {
|
|
285
|
+
if (data.inspiration) {
|
|
286
|
+
inspirationValue.textContent = '⭐ Active';
|
|
287
|
+
inspirationValue.style.color = '#f57f17';
|
|
288
|
+
if (inspirationDisplay) {
|
|
289
|
+
inspirationDisplay.style.background = '#fff9c4';
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
inspirationValue.textContent = '☆ None';
|
|
293
|
+
inspirationValue.style.color = 'var(--text-muted)';
|
|
294
|
+
if (inspirationDisplay) {
|
|
295
|
+
inspirationDisplay.style.background = 'var(--bg-tertiary)';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Layer 3: Hit Points
|
|
301
|
+
const hpValue = document.getElementById('hp-value');
|
|
302
|
+
|
|
303
|
+
// Defensive initialization for hitPoints - ensure proper structure
|
|
304
|
+
debug.log('🔍 HP before defensive init:', { hitPoints: data.hitPoints, type: typeof data.hitPoints });
|
|
305
|
+
if (!data.hitPoints || typeof data.hitPoints !== 'object') {
|
|
306
|
+
debug.warn('⚠️ DEFENSIVE INIT TRIGGERED! Setting HP to 0/0. Original value:', data.hitPoints);
|
|
307
|
+
data.hitPoints = { current: 0, max: 0 };
|
|
308
|
+
}
|
|
309
|
+
// Ensure current and max exist (hitPoints might be an object but missing these)
|
|
310
|
+
if (data.hitPoints.current === undefined) {
|
|
311
|
+
debug.warn('⚠️ HP current is undefined, setting to 0');
|
|
312
|
+
data.hitPoints.current = 0;
|
|
313
|
+
}
|
|
314
|
+
if (data.hitPoints.max === undefined) {
|
|
315
|
+
debug.warn('⚠️ HP max is undefined, setting to 0');
|
|
316
|
+
data.hitPoints.max = 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
debug.log('💚 HP display values:', { current: data.hitPoints.current, max: data.hitPoints.max, tempHP: data.temporaryHP });
|
|
320
|
+
|
|
321
|
+
if (hpValue) {
|
|
322
|
+
hpValue.textContent = `${data.hitPoints.current}${data.temporaryHP > 0 ? `+${data.temporaryHP}` : ''} / ${data.hitPoints.max}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Initiative
|
|
326
|
+
const initiativeValue = document.getElementById('initiative-value');
|
|
327
|
+
if (initiativeValue) {
|
|
328
|
+
initiativeValue.textContent = `+${data.initiative || 0}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Remove old event listeners by cloning and replacing elements
|
|
332
|
+
// This prevents duplicate listeners when buildSheet() is called multiple times
|
|
333
|
+
const hpDisplayOld = document.getElementById('hp-display');
|
|
334
|
+
let hpDisplayNew = null;
|
|
335
|
+
if (hpDisplayOld) {
|
|
336
|
+
hpDisplayNew = hpDisplayOld.cloneNode(true);
|
|
337
|
+
hpDisplayOld.parentNode.replaceChild(hpDisplayNew, hpDisplayOld);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const initiativeOld = document.getElementById('initiative-button');
|
|
341
|
+
let initiativeNew = null;
|
|
342
|
+
if (initiativeOld) {
|
|
343
|
+
initiativeNew = initiativeOld.cloneNode(true);
|
|
344
|
+
initiativeOld.parentNode.replaceChild(initiativeNew, initiativeOld);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const deathSavesOld = document.getElementById('death-saves-display');
|
|
348
|
+
let deathSavesNew = null;
|
|
349
|
+
if (deathSavesOld) {
|
|
350
|
+
deathSavesNew = deathSavesOld.cloneNode(true);
|
|
351
|
+
deathSavesOld.parentNode.replaceChild(deathSavesNew, deathSavesOld);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const inspirationOld = document.getElementById('inspiration-display');
|
|
355
|
+
let inspirationNew = null;
|
|
356
|
+
if (inspirationOld) {
|
|
357
|
+
inspirationNew = inspirationOld.cloneNode(true);
|
|
358
|
+
inspirationOld.parentNode.replaceChild(inspirationNew, inspirationOld);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Add click handler for HP display
|
|
362
|
+
if (hpDisplayNew) {
|
|
363
|
+
hpDisplayNew.addEventListener('click', showHPModal);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Add click handler for initiative button
|
|
367
|
+
if (initiativeNew) {
|
|
368
|
+
initiativeNew.addEventListener('click', () => {
|
|
369
|
+
const initiativeBonus = data.initiative || 0;
|
|
370
|
+
|
|
371
|
+
// Announce initiative roll
|
|
372
|
+
const announcement = `&{template:default} {{name=${getColoredBanner(data)}${data.name} rolls for initiative!}} {{Type=Initiative}} {{Bonus=+${initiativeBonus}}}`;
|
|
373
|
+
const messageData = {
|
|
374
|
+
action: 'announceSpell',
|
|
375
|
+
message: announcement,
|
|
376
|
+
color: data.notificationColor
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
sendToRoll20(messageData);
|
|
380
|
+
|
|
381
|
+
roll('Initiative', `1d20+${initiativeBonus}`);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add click handler for death saves display
|
|
386
|
+
if (deathSavesNew) {
|
|
387
|
+
deathSavesNew.addEventListener('click', showDeathSavesModal);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Add click handler for inspiration display
|
|
391
|
+
if (inspirationNew) {
|
|
392
|
+
inspirationNew.addEventListener('click', toggleInspiration);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Update HP display color based on percentage
|
|
396
|
+
if (hpDisplayNew) {
|
|
397
|
+
const hpPercent = data.hitPoints && data.hitPoints.max > 0 ? (data.hitPoints.current / data.hitPoints.max) * 100 : 0;
|
|
398
|
+
// Use the new hpDisplayNew element we just created above
|
|
399
|
+
if (hpPercent > 50) {
|
|
400
|
+
hpDisplayNew.style.background = 'var(--accent-success)';
|
|
401
|
+
} else if (hpPercent > 25) {
|
|
402
|
+
hpDisplayNew.style.background = 'var(--accent-warning)';
|
|
403
|
+
} else {
|
|
404
|
+
hpDisplayNew.style.background = 'var(--accent-danger)';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Resources
|
|
409
|
+
buildResourcesDisplay();
|
|
410
|
+
|
|
411
|
+
// Spell Slots
|
|
412
|
+
buildSpellSlotsDisplay();
|
|
413
|
+
|
|
414
|
+
// Ability Scores & Saving Throws (Bundled format like OwlCloud)
|
|
415
|
+
const abilitiesSavesGrid = document.getElementById('abilities-saves-grid');
|
|
416
|
+
if (abilitiesSavesGrid) {
|
|
417
|
+
abilitiesSavesGrid.innerHTML = ''; // Clear existing
|
|
418
|
+
const abilities = ['strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma'];
|
|
419
|
+
|
|
420
|
+
abilities.forEach(ability => {
|
|
421
|
+
const score = data.attributes?.[ability] || 10;
|
|
422
|
+
const checkMod = data.attributeMods?.[ability] || 0;
|
|
423
|
+
const saveMod = data.saves?.[ability] || data.savingThrows?.[ability] || checkMod; // Fallback to check if no save proficiency
|
|
424
|
+
|
|
425
|
+
// Create bundled card
|
|
426
|
+
const card = document.createElement('div');
|
|
427
|
+
card.className = 'stat-card';
|
|
428
|
+
card.style.cssText = `
|
|
429
|
+
padding: 12px;
|
|
430
|
+
background: var(--bg-secondary, #2a2a2a);
|
|
431
|
+
border: 2px solid var(--border-color, #444);
|
|
432
|
+
border-radius: 8px;
|
|
433
|
+
text-align: center;
|
|
434
|
+
cursor: pointer;
|
|
435
|
+
transition: all 0.2s;
|
|
436
|
+
`;
|
|
437
|
+
|
|
438
|
+
const abilityShort = ability.substring(0, 3).toUpperCase();
|
|
439
|
+
|
|
440
|
+
card.innerHTML = `
|
|
441
|
+
<div style="font-weight: bold; font-size: 14px; margin-bottom: 8px; color: var(--text-primary, #e0e0e0);">${abilityShort}</div>
|
|
442
|
+
<div style="font-size: 24px; font-weight: bold; margin-bottom: 8px; color: var(--accent-primary, #9b59b6);">${score}</div>
|
|
443
|
+
<div style="display: flex; justify-content: space-around; gap: 8px;">
|
|
444
|
+
<button class="check-btn" style="flex: 1; padding: 6px; background: var(--bg-tertiary, #333); border: 1px solid var(--border-color, #555); border-radius: 4px; color: var(--text-secondary, #b0b0b0); font-size: 11px; cursor: pointer;">
|
|
445
|
+
<div style="font-size: 10px;">Check</div>
|
|
446
|
+
<div style="font-weight: bold;">${checkMod >= 0 ? '+' : ''}${checkMod}</div>
|
|
447
|
+
</button>
|
|
448
|
+
<button class="save-btn" style="flex: 1; padding: 6px; background: var(--bg-tertiary, #333); border: 1px solid var(--border-color, #555); border-radius: 4px; color: var(--text-secondary, #b0b0b0); font-size: 11px; cursor: pointer;">
|
|
449
|
+
<div style="font-size: 10px;">Save</div>
|
|
450
|
+
<div style="font-weight: bold;">${saveMod >= 0 ? '+' : ''}${saveMod}</div>
|
|
451
|
+
</button>
|
|
452
|
+
</div>
|
|
453
|
+
`;
|
|
454
|
+
|
|
455
|
+
// Check button click handler
|
|
456
|
+
const checkBtn = card.querySelector('.check-btn');
|
|
457
|
+
checkBtn.addEventListener('click', (e) => {
|
|
458
|
+
e.stopPropagation();
|
|
459
|
+
const announcement = `&{template:default} {{name=${getColoredBanner(data)}${data.name} makes a ${ability.charAt(0).toUpperCase() + ability.slice(1)} check!}} {{Type=Ability Check}} {{Bonus=${checkMod >= 0 ? '+' : ''}${checkMod}}}`;
|
|
460
|
+
const messageData = {
|
|
461
|
+
action: 'announceSpell',
|
|
462
|
+
message: announcement,
|
|
463
|
+
color: data.notificationColor
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
sendToRoll20(messageData);
|
|
467
|
+
|
|
468
|
+
roll(`${ability.charAt(0).toUpperCase() + ability.slice(1)} Check`, `1d20${checkMod >= 0 ? '+' : ''}${checkMod}`);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Save button click handler
|
|
472
|
+
const saveBtn = card.querySelector('.save-btn');
|
|
473
|
+
saveBtn.addEventListener('click', (e) => {
|
|
474
|
+
e.stopPropagation();
|
|
475
|
+
const announcement = `&{template:default} {{name=${getColoredBanner(data)}${data.name} makes a ${abilityShort} save!}} {{Type=Saving Throw}} {{Bonus=${saveMod >= 0 ? '+' : ''}${saveMod}}}`;
|
|
476
|
+
const messageData = {
|
|
477
|
+
action: 'announceSpell',
|
|
478
|
+
message: announcement,
|
|
479
|
+
color: data.notificationColor
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
sendToRoll20(messageData);
|
|
483
|
+
|
|
484
|
+
roll(`${abilityShort} Save`, `1d20${saveMod >= 0 ? '+' : ''}${saveMod}`);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
abilitiesSavesGrid.appendChild(card);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Skills - deduplicate and show unique skills only
|
|
492
|
+
const skillsGrid = document.getElementById('skills-grid');
|
|
493
|
+
if (skillsGrid) {
|
|
494
|
+
skillsGrid.innerHTML = ''; // Clear existing
|
|
495
|
+
|
|
496
|
+
// Create a map to deduplicate skills (in case data has duplicates)
|
|
497
|
+
const uniqueSkills = new Map();
|
|
498
|
+
Object.entries(data.skills || {}).forEach(([skill, bonus]) => {
|
|
499
|
+
const normalizedSkill = skill.toLowerCase().trim();
|
|
500
|
+
// Only keep the skill if we haven't seen it, or if this bonus is higher
|
|
501
|
+
if (!uniqueSkills.has(normalizedSkill) || bonus > uniqueSkills.get(normalizedSkill).bonus) {
|
|
502
|
+
uniqueSkills.set(normalizedSkill, { skill, bonus });
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Sort skills alphabetically and display
|
|
507
|
+
const sortedSkills = Array.from(uniqueSkills.values()).sort((a, b) =>
|
|
508
|
+
a.skill.localeCompare(b.skill)
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
sortedSkills.forEach(({ skill, bonus }) => {
|
|
512
|
+
const displayName = skill.charAt(0).toUpperCase() + skill.slice(1).replace(/-/g, ' ');
|
|
513
|
+
const card = createCard(displayName, `${bonus >= 0 ? '+' : ''}${bonus}`, '', () => {
|
|
514
|
+
// Announce skill check
|
|
515
|
+
const announcement = `&{template:default} {{name=${getColoredBanner(data)}${data.name} makes a ${displayName} check!}} {{Type=Skill Check}} {{Bonus=${bonus >= 0 ? '+' : ''}${bonus}}}`;
|
|
516
|
+
const messageData = {
|
|
517
|
+
action: 'announceSpell',
|
|
518
|
+
message: announcement,
|
|
519
|
+
color: data.notificationColor
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
sendToRoll20(messageData);
|
|
523
|
+
|
|
524
|
+
roll(displayName, `1d20${bonus >= 0 ? '+' : ''}${bonus}`);
|
|
525
|
+
});
|
|
526
|
+
skillsGrid.appendChild(card);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Actions & Attacks
|
|
531
|
+
const actionsContainer = document.getElementById('actions-container');
|
|
532
|
+
if (actionsContainer) {
|
|
533
|
+
debug.log('🎬 Actions display check:', {
|
|
534
|
+
has_actions: !!data.actions,
|
|
535
|
+
is_array: Array.isArray(data.actions),
|
|
536
|
+
length: data.actions?.length,
|
|
537
|
+
sample_names: data.actions?.slice(0, 5).map(a => a.name)
|
|
538
|
+
});
|
|
539
|
+
if (data.actions && Array.isArray(data.actions) && data.actions.length > 0) {
|
|
540
|
+
buildActionsDisplay(actionsContainer, data.actions);
|
|
541
|
+
} else {
|
|
542
|
+
actionsContainer.innerHTML = '<p style="text-align: center; color: #666;">No actions available</p>';
|
|
543
|
+
debug.warn('⚠️ No actions to display - showing placeholder');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Companions (Animal Companions, Familiars, Summons, etc.)
|
|
548
|
+
if (data.companions && Array.isArray(data.companions) && data.companions.length > 0) {
|
|
549
|
+
buildCompanionsDisplay(data.companions);
|
|
550
|
+
} else {
|
|
551
|
+
// Hide companions section if character has no companions
|
|
552
|
+
const companionsSection = document.getElementById('companions-container');
|
|
553
|
+
if (companionsSection) {
|
|
554
|
+
companionsSection.style.display = 'none';
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Inventory & Equipment
|
|
559
|
+
const inventoryContainer = document.getElementById('inventory-container');
|
|
560
|
+
if (inventoryContainer) {
|
|
561
|
+
if (data.inventory && Array.isArray(data.inventory) && data.inventory.length > 0) {
|
|
562
|
+
buildInventoryDisplay(inventoryContainer, data.inventory);
|
|
563
|
+
} else {
|
|
564
|
+
inventoryContainer.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">No items in inventory</p>';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Spells - organized by source then level
|
|
569
|
+
const spellsContainer = document.getElementById('spells-container');
|
|
570
|
+
if (spellsContainer) {
|
|
571
|
+
debug.log('✨ Spells display check:', {
|
|
572
|
+
has_spells: !!data.spells,
|
|
573
|
+
is_array: Array.isArray(data.spells),
|
|
574
|
+
length: data.spells?.length,
|
|
575
|
+
sample_names: data.spells?.slice(0, 5).map(s => s.name)
|
|
576
|
+
});
|
|
577
|
+
if (data.spells && Array.isArray(data.spells) && data.spells.length > 0) {
|
|
578
|
+
buildSpellsBySource(spellsContainer, data.spells);
|
|
579
|
+
expandSectionByContainerId('spells-container');
|
|
580
|
+
} else {
|
|
581
|
+
spellsContainer.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">No spells prepared</p>';
|
|
582
|
+
debug.warn('⚠️ No spells to display - showing placeholder');
|
|
583
|
+
// Collapse the section when empty
|
|
584
|
+
collapseSectionByContainerId('spells-container');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Restore active effects from character data
|
|
589
|
+
if (typeof window.activeBuffs !== 'undefined' && typeof window.activeConditions !== 'undefined') {
|
|
590
|
+
if (data.activeEffects) {
|
|
591
|
+
window.activeBuffs = data.activeEffects.buffs || [];
|
|
592
|
+
window.activeConditions = data.activeEffects.debuffs || [];
|
|
593
|
+
debug.log('✅ Restored active effects:', { buffs: window.activeBuffs, debuffs: window.activeConditions });
|
|
594
|
+
} else {
|
|
595
|
+
window.activeBuffs = [];
|
|
596
|
+
window.activeConditions = [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Sync conditions from Dicecloud (if any were detected as active)
|
|
600
|
+
if (data.conditions && Array.isArray(data.conditions) && data.conditions.length > 0 &&
|
|
601
|
+
typeof window.POSITIVE_EFFECTS !== 'undefined' && typeof window.NEGATIVE_EFFECTS !== 'undefined') {
|
|
602
|
+
debug.log('✨ Syncing conditions from Dicecloud:', data.conditions);
|
|
603
|
+
data.conditions.forEach(condition => {
|
|
604
|
+
// Map Dicecloud condition names to our effect names
|
|
605
|
+
const conditionName = condition.name;
|
|
606
|
+
const isPositive = window.POSITIVE_EFFECTS.some(e => e.name === conditionName);
|
|
607
|
+
const isNegative = window.NEGATIVE_EFFECTS.some(e => e.name === conditionName);
|
|
608
|
+
|
|
609
|
+
if (isPositive && !window.activeBuffs.includes(conditionName)) {
|
|
610
|
+
window.activeBuffs.push(conditionName);
|
|
611
|
+
debug.log(` ✅ Added buff from Dicecloud: ${conditionName}`);
|
|
612
|
+
} else if (isNegative && !window.activeConditions.includes(conditionName)) {
|
|
613
|
+
window.activeConditions.push(conditionName);
|
|
614
|
+
debug.log(` ✅ Added debuff from Dicecloud: ${conditionName}`);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (typeof updateEffectsDisplay === 'function') {
|
|
621
|
+
updateEffectsDisplay();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Initialize color palette after sheet is built
|
|
625
|
+
if (typeof initColorPalette === 'function') {
|
|
626
|
+
initColorPalette();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Initialize filter event listeners
|
|
630
|
+
if (typeof initializeFilters === 'function') {
|
|
631
|
+
initializeFilters();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Hide loading overlay and show the sheet with fade-in effect
|
|
635
|
+
const loadingOverlay = document.getElementById('loading-overlay');
|
|
636
|
+
const container = document.querySelector('.container');
|
|
637
|
+
if (loadingOverlay) {
|
|
638
|
+
loadingOverlay.style.display = 'none';
|
|
639
|
+
}
|
|
640
|
+
if (container) {
|
|
641
|
+
container.classList.add('loaded');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
debug.log('✅ Sheet built successfully');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ===== EXPORTS =====
|
|
648
|
+
|
|
649
|
+
// Export function to window (for access from content script context)
|
|
650
|
+
window.buildSheet = buildSheet;
|
|
651
|
+
|
|
652
|
+
debug.log('✅ Sheet Builder module loaded');
|
|
653
|
+
|
|
654
|
+
})();
|