@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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character Data Manager Module
|
|
3
|
+
*
|
|
4
|
+
* Handles loading, saving, and syncing character data.
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - saveCharacterData()
|
|
9
|
+
* - sendSyncMessage()
|
|
10
|
+
* - loadCharacterWithTabs()
|
|
11
|
+
* - loadAndBuildTabs()
|
|
12
|
+
* - getActiveCharacterId()
|
|
13
|
+
* - setActiveCharacter(characterId)
|
|
14
|
+
* - buildCharacterTabs(profiles, activeCharacterId)
|
|
15
|
+
* - validateCharacterData(data)
|
|
16
|
+
*
|
|
17
|
+
* State variables:
|
|
18
|
+
* - currentSlotId
|
|
19
|
+
* - syncDebounceTimer
|
|
20
|
+
* - characterCache
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
(function() {
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
// ===== STATE VARIABLES =====
|
|
27
|
+
|
|
28
|
+
let currentSlotId = null;
|
|
29
|
+
let syncDebounceTimer = null;
|
|
30
|
+
const characterCache = new Map();
|
|
31
|
+
|
|
32
|
+
// ===== CHARACTER DATA FUNCTIONS =====
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Save character data to browser storage and trigger sync
|
|
36
|
+
*/
|
|
37
|
+
function saveCharacterData() {
|
|
38
|
+
// Requires characterData to be available from global scope
|
|
39
|
+
if (typeof characterData === 'undefined' || !characterData) {
|
|
40
|
+
debug.warn('⚠️ No character data to save');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// CRITICAL: Save to browser storage to persist through refresh/close
|
|
45
|
+
if (typeof browserAPI !== 'undefined') {
|
|
46
|
+
debug.log('💾 saveCharacterData called with:', {
|
|
47
|
+
hasId: !!characterData.id,
|
|
48
|
+
id: characterData.id,
|
|
49
|
+
hasDicecloudCharacterId: !!characterData.dicecloud_character_id,
|
|
50
|
+
dicecloud_character_id: characterData.dicecloud_character_id,
|
|
51
|
+
name: characterData.name,
|
|
52
|
+
currentSlotId: currentSlotId
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
browserAPI.runtime.sendMessage({
|
|
56
|
+
action: 'storeCharacterData',
|
|
57
|
+
data: characterData,
|
|
58
|
+
slotId: currentSlotId // CRITICAL: Pass slotId for proper persistence
|
|
59
|
+
}).then(() => {
|
|
60
|
+
debug.log(`💾 Saved character data to browser storage (slotId: ${currentSlotId})`);
|
|
61
|
+
}).catch(err => {
|
|
62
|
+
debug.error('❌ Failed to save character data:', err);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Debounce sync messages to prevent flickering
|
|
67
|
+
// Clear any pending sync
|
|
68
|
+
if (syncDebounceTimer) {
|
|
69
|
+
clearTimeout(syncDebounceTimer);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Schedule sync after a short delay
|
|
73
|
+
syncDebounceTimer = setTimeout(() => {
|
|
74
|
+
sendSyncMessage();
|
|
75
|
+
syncDebounceTimer = null;
|
|
76
|
+
}, 300); // 300ms debounce delay
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Send sync message to DiceCloud
|
|
81
|
+
*/
|
|
82
|
+
function sendSyncMessage() {
|
|
83
|
+
// Requires characterData to be available from global scope
|
|
84
|
+
if (typeof characterData === 'undefined' || !characterData) {
|
|
85
|
+
debug.warn('⚠️ No character data to sync');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Send sync message to DiceCloud if experimental sync is available
|
|
90
|
+
debug.log('🔄 Sending character data update to DiceCloud sync...');
|
|
91
|
+
|
|
92
|
+
// Extract Channel Divinity from characterData.resources array (this has the current values after use)
|
|
93
|
+
let channelDivinityForSync = null;
|
|
94
|
+
const channelDivinityResource = characterData.resources?.find(r =>
|
|
95
|
+
r.name === 'Channel Divinity' ||
|
|
96
|
+
r.variableName === 'channelDivinityCleric' ||
|
|
97
|
+
r.variableName === 'channelDivinityPaladin' ||
|
|
98
|
+
r.variableName === 'channelDivinity' ||
|
|
99
|
+
r.varName === 'channelDivinity'
|
|
100
|
+
);
|
|
101
|
+
if (channelDivinityResource) {
|
|
102
|
+
channelDivinityForSync = {
|
|
103
|
+
current: channelDivinityResource.current || 0,
|
|
104
|
+
max: channelDivinityResource.max || 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use the existing resources array which contains current values
|
|
109
|
+
const resourcesForSync = characterData.resources || [];
|
|
110
|
+
|
|
111
|
+
// Debug logging to see what we're sending
|
|
112
|
+
console.log('[SYNC DEBUG] ========== SYNC MESSAGE DATA ==========');
|
|
113
|
+
console.log('[SYNC DEBUG] Character Name:', characterData.name);
|
|
114
|
+
console.log('[SYNC DEBUG] HP:', characterData.hitPoints?.current, '/', characterData.hitPoints?.max);
|
|
115
|
+
console.log('[SYNC DEBUG] Temp HP:', characterData.temporaryHP);
|
|
116
|
+
console.log('[SYNC DEBUG] Spell Slots:', characterData.spellSlots);
|
|
117
|
+
console.log('[SYNC DEBUG] Channel Divinity (extracted):', channelDivinityForSync);
|
|
118
|
+
console.log('[SYNC DEBUG] Channel Divinity (raw resource):', channelDivinityResource);
|
|
119
|
+
console.log('[SYNC DEBUG] Resources (count):', resourcesForSync?.length);
|
|
120
|
+
console.log('[SYNC DEBUG] Resources (full):', resourcesForSync);
|
|
121
|
+
console.log('[SYNC DEBUG] Actions (count):', characterData.actions?.length);
|
|
122
|
+
console.log('[SYNC DEBUG] Actions (full):', characterData.actions);
|
|
123
|
+
console.log('[SYNC DEBUG] Death Saves:', characterData.deathSaves);
|
|
124
|
+
console.log('[SYNC DEBUG] Inspiration:', characterData.inspiration);
|
|
125
|
+
console.log('[SYNC DEBUG] =========================================');
|
|
126
|
+
|
|
127
|
+
const syncMessage = {
|
|
128
|
+
type: 'characterDataUpdate',
|
|
129
|
+
characterData: {
|
|
130
|
+
name: characterData.name,
|
|
131
|
+
hp: characterData.hitPoints.current,
|
|
132
|
+
tempHp: characterData.temporaryHP || 0,
|
|
133
|
+
maxHp: characterData.hitPoints.max,
|
|
134
|
+
spellSlots: characterData.spellSlots || {},
|
|
135
|
+
channelDivinity: channelDivinityForSync,
|
|
136
|
+
resources: resourcesForSync,
|
|
137
|
+
actions: characterData.actions || [],
|
|
138
|
+
deathSaves: characterData.deathSaves,
|
|
139
|
+
inspiration: characterData.inspiration,
|
|
140
|
+
lastRoll: characterData.lastRoll
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Try to send to DiceCloud sync
|
|
145
|
+
window.postMessage(syncMessage, '*');
|
|
146
|
+
|
|
147
|
+
// Also send to Roll20 via relay
|
|
148
|
+
sendToRoll20(syncMessage);
|
|
149
|
+
sendToRoll20({
|
|
150
|
+
action: 'updateCharacterData',
|
|
151
|
+
data: characterData,
|
|
152
|
+
characterId: characterData.id || characterData.dicecloud_character_id || currentSlotId
|
|
153
|
+
});
|
|
154
|
+
debug.log('💾 Sent character data update to Roll20');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validate character data has required fields
|
|
159
|
+
*/
|
|
160
|
+
function validateCharacterData(data) {
|
|
161
|
+
if (!data) return { valid: false, missing: ['all data'] };
|
|
162
|
+
|
|
163
|
+
const hasSpells = Array.isArray(data.spells);
|
|
164
|
+
const hasActions = Array.isArray(data.actions);
|
|
165
|
+
|
|
166
|
+
if (!hasSpells || !hasActions) {
|
|
167
|
+
const missing = [];
|
|
168
|
+
if (!hasSpells) missing.push('spells');
|
|
169
|
+
if (!hasActions) missing.push('actions');
|
|
170
|
+
return { valid: false, missing };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { valid: true, missing: [] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the active character ID from storage
|
|
178
|
+
*/
|
|
179
|
+
async function getActiveCharacterId() {
|
|
180
|
+
if (typeof browserAPI === 'undefined') {
|
|
181
|
+
debug.warn('⚠️ browserAPI not available');
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Use Promise-based API (works in both Chrome and Firefox with our polyfill)
|
|
186
|
+
const result = await browserAPI.storage.local.get(['activeCharacterId']);
|
|
187
|
+
return result.activeCharacterId || null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set the active character ID in storage
|
|
192
|
+
*/
|
|
193
|
+
async function setActiveCharacter(characterId) {
|
|
194
|
+
if (typeof browserAPI === 'undefined') {
|
|
195
|
+
debug.warn('⚠️ browserAPI not available');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await browserAPI.storage.local.set({
|
|
201
|
+
activeCharacterId: characterId
|
|
202
|
+
});
|
|
203
|
+
console.log(`✅ Set active character: ${characterId}`);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('❌ Failed to set active character:', error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Load profiles and build tabs (without building sheet)
|
|
211
|
+
*/
|
|
212
|
+
async function loadAndBuildTabs() {
|
|
213
|
+
if (typeof browserAPI === 'undefined') {
|
|
214
|
+
debug.warn('⚠️ browserAPI not available');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
debug.log('📋 Loading character profiles for tabs...');
|
|
220
|
+
|
|
221
|
+
// Get all character profiles
|
|
222
|
+
const profilesResponse = await browserAPI.runtime.sendMessage({ action: 'getAllCharacterProfiles' });
|
|
223
|
+
const profiles = profilesResponse.success ? profilesResponse.profiles : {};
|
|
224
|
+
debug.log('📋 Profiles loaded:', Object.keys(profiles));
|
|
225
|
+
|
|
226
|
+
// Get active character ID (this is the slotId like "slot-1")
|
|
227
|
+
const activeCharacterId = await getActiveCharacterId();
|
|
228
|
+
debug.log('📋 Active character ID:', activeCharacterId);
|
|
229
|
+
|
|
230
|
+
// Build character tabs
|
|
231
|
+
buildCharacterTabs(profiles, activeCharacterId);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
debug.error('❌ Failed to load and build tabs:', error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Load character data and build tabs
|
|
239
|
+
*/
|
|
240
|
+
async function loadCharacterWithTabs() {
|
|
241
|
+
if (typeof browserAPI === 'undefined') {
|
|
242
|
+
debug.warn('⚠️ browserAPI not available');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if DOM is ready (domReady should be available from global scope)
|
|
247
|
+
if (typeof domReady !== 'undefined' && !domReady) {
|
|
248
|
+
debug.log('⏳ DOM not ready, queuing loadCharacterWithTabs...');
|
|
249
|
+
if (typeof pendingOperations !== 'undefined') {
|
|
250
|
+
pendingOperations.push(loadCharacterWithTabs);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
// Build tabs first
|
|
257
|
+
await loadAndBuildTabs();
|
|
258
|
+
|
|
259
|
+
// Get and store current slot ID for persistence
|
|
260
|
+
currentSlotId = await getActiveCharacterId();
|
|
261
|
+
debug.log('📋 Current slot ID set to:', currentSlotId);
|
|
262
|
+
|
|
263
|
+
// Get active character data
|
|
264
|
+
let activeCharacter = null;
|
|
265
|
+
|
|
266
|
+
// Check if this is a database character
|
|
267
|
+
if (currentSlotId && currentSlotId.startsWith('db-')) {
|
|
268
|
+
// Load from database
|
|
269
|
+
const characterId = currentSlotId.replace('db-', '');
|
|
270
|
+
try {
|
|
271
|
+
const dbResponse = await browserAPI.runtime.sendMessage({
|
|
272
|
+
action: 'getCharacterDataFromDatabase',
|
|
273
|
+
characterId: characterId
|
|
274
|
+
});
|
|
275
|
+
if (dbResponse.success) {
|
|
276
|
+
activeCharacter = dbResponse.data;
|
|
277
|
+
debug.log('✅ Loaded character from database:', activeCharacter.name);
|
|
278
|
+
}
|
|
279
|
+
} catch (dbError) {
|
|
280
|
+
debug.warn('⚠️ Failed to load database character:', dbError);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
// Load from local storage
|
|
284
|
+
const activeResponse = await browserAPI.runtime.sendMessage({ action: 'getCharacterData' });
|
|
285
|
+
activeCharacter = activeResponse.success ? activeResponse.data : null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Load active character
|
|
289
|
+
if (activeCharacter) {
|
|
290
|
+
// Validate character data
|
|
291
|
+
const validation = validateCharacterData(activeCharacter);
|
|
292
|
+
|
|
293
|
+
if (!validation.valid) {
|
|
294
|
+
debug.warn('⚠️ Character data is incomplete or outdated');
|
|
295
|
+
debug.warn(`Missing data: ${validation.missing.join(', ')}`);
|
|
296
|
+
|
|
297
|
+
// Show error message to user
|
|
298
|
+
const characterName = activeCharacter.name || activeCharacter.character_name || 'this character';
|
|
299
|
+
|
|
300
|
+
const errorContainer = document.getElementById('main-content');
|
|
301
|
+
if (errorContainer) {
|
|
302
|
+
errorContainer.innerHTML = `
|
|
303
|
+
<div style="padding: 40px; text-align: center; color: var(--text-primary);">
|
|
304
|
+
<h2 style="color: #e74c3c; margin-bottom: 20px;">⚠️ Incomplete Character Data</h2>
|
|
305
|
+
<p style="margin-bottom: 15px; font-size: 1.1em;">
|
|
306
|
+
The character data for <strong>${characterName}</strong> is missing ${validation.missing.join(' and ')}.
|
|
307
|
+
</p>
|
|
308
|
+
<p style="margin-bottom: 15px; color: var(--text-secondary);">
|
|
309
|
+
This usually happens when loading old cloud data that was saved before spells and actions were synced.
|
|
310
|
+
</p>
|
|
311
|
+
<div style="background: #2c3e50; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
312
|
+
<p style="margin-bottom: 10px; font-weight: bold;">To fix this:</p>
|
|
313
|
+
<ol style="text-align: left; max-width: 500px; margin: 0 auto; line-height: 1.8;">
|
|
314
|
+
<li>Go to your character on <a href="https://dicecloud.com" target="_blank" style="color: #3498db;">DiceCloud.com</a></li>
|
|
315
|
+
<li>Click the <strong>"Sync to Extension"</strong> button on the character page</li>
|
|
316
|
+
<li>Wait for the sync to complete</li>
|
|
317
|
+
<li>Reopen this character sheet</li>
|
|
318
|
+
</ol>
|
|
319
|
+
</div>
|
|
320
|
+
<p style="color: var(--text-secondary); font-size: 0.9em;">
|
|
321
|
+
Character ID: ${activeCharacter.id || activeCharacter.dicecloud_character_id || 'unknown'}
|
|
322
|
+
</p>
|
|
323
|
+
</div>
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Don't continue loading the incomplete character
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Set global characterData
|
|
332
|
+
if (typeof globalThis.characterData !== 'undefined') {
|
|
333
|
+
globalThis.characterData = activeCharacter;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Build sheet (buildSheet should be available from global scope)
|
|
337
|
+
if (typeof buildSheet !== 'undefined') {
|
|
338
|
+
buildSheet(activeCharacter);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Initialize racial traits, feat traits, class features
|
|
342
|
+
if (typeof initRacialTraits !== 'undefined') initRacialTraits();
|
|
343
|
+
if (typeof initFeatTraits !== 'undefined') initFeatTraits();
|
|
344
|
+
if (typeof initClassFeatures !== 'undefined') initClassFeatures();
|
|
345
|
+
} else {
|
|
346
|
+
debug.error('❌ No character data found');
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
debug.error('❌ Failed to load characters:', error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Build character tabs UI
|
|
355
|
+
*/
|
|
356
|
+
function buildCharacterTabs(profiles, activeCharacterId) {
|
|
357
|
+
const tabsContainer = document.getElementById('character-tabs');
|
|
358
|
+
if (!tabsContainer) {
|
|
359
|
+
debug.warn('⚠️ character-tabs container not found!');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
debug.log(`🏷️ Building character tabs. Active: ${activeCharacterId}`);
|
|
364
|
+
debug.log(`📋 Profiles:`, Object.keys(profiles));
|
|
365
|
+
|
|
366
|
+
tabsContainer.innerHTML = '';
|
|
367
|
+
const maxSlots = 10; // Support up to 10 character slots
|
|
368
|
+
|
|
369
|
+
// First, add database characters. Include both:
|
|
370
|
+
// 1. Keys starting with `db-` (direct database characters)
|
|
371
|
+
// 2. Characters with source='database' or hasCloudVersion=true (local profiles with cloud sync)
|
|
372
|
+
const databaseCharacters = Object.entries(profiles).filter(([slotId, profile]) =>
|
|
373
|
+
slotId.startsWith('db-') ||
|
|
374
|
+
profile.source === 'database' ||
|
|
375
|
+
profile.hasCloudVersion === true
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Add database character tabs
|
|
379
|
+
databaseCharacters.forEach(([slotId, charInSlot], index) => {
|
|
380
|
+
const isActive = slotId === activeCharacterId;
|
|
381
|
+
|
|
382
|
+
const displayName = charInSlot.name || charInSlot.character_name || (charInSlot._fullData && (charInSlot._fullData.character_name || charInSlot._fullData.name)) || 'Unknown';
|
|
383
|
+
debug.log(`🌐 DB Character: ${displayName} (active: ${isActive})`);
|
|
384
|
+
|
|
385
|
+
const tab = document.createElement('div');
|
|
386
|
+
tab.className = 'character-tab database-tab';
|
|
387
|
+
if (isActive) {
|
|
388
|
+
tab.classList.add('active');
|
|
389
|
+
}
|
|
390
|
+
tab.dataset.slotId = slotId;
|
|
391
|
+
|
|
392
|
+
// Create special styling for database characters
|
|
393
|
+
tab.innerHTML = `
|
|
394
|
+
<span class="slot-number">🌐</span>
|
|
395
|
+
<span class="char-name">${displayName}</span>
|
|
396
|
+
<span class="char-details">${charInSlot.level || 1} ${charInSlot.class || 'Unknown'}</span>
|
|
397
|
+
`;
|
|
398
|
+
|
|
399
|
+
// Add click handler
|
|
400
|
+
tab.addEventListener('click', (e) => {
|
|
401
|
+
debug.log(`🖱️ Database tab clicked for ${slotId}`, charInSlot.name);
|
|
402
|
+
if (typeof switchToCharacter === 'function') {
|
|
403
|
+
switchToCharacter(slotId);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
tabsContainer.appendChild(tab);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Add separator if we have both database and local characters
|
|
411
|
+
if (databaseCharacters.length > 0) {
|
|
412
|
+
const separator = document.createElement('div');
|
|
413
|
+
separator.className = 'tab-separator';
|
|
414
|
+
separator.innerHTML = '<span style="color: var(--text-secondary); font-size: 0.8em;">Local Characters</span>';
|
|
415
|
+
tabsContainer.appendChild(separator);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Create tabs for local slots (skip characters already shown in database section)
|
|
419
|
+
for (let slotNum = 1; slotNum <= maxSlots; slotNum++) {
|
|
420
|
+
const slotId = `slot-${slotNum}`;
|
|
421
|
+
// Find character in this slot using slotId as key
|
|
422
|
+
const charInSlot = profiles[slotId];
|
|
423
|
+
|
|
424
|
+
// Skip if this character was already shown in the database section
|
|
425
|
+
if (charInSlot && (charInSlot.source === 'database' || charInSlot.hasCloudVersion === true)) {
|
|
426
|
+
debug.log(` ⏭️ Slot ${slotNum}: ${charInSlot.name} (skipped - shown in cloud section)`);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (charInSlot) {
|
|
431
|
+
debug.log(` 📌 Slot ${slotNum}: ${charInSlot.name} (active: ${slotId === activeCharacterId})`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const tab = document.createElement('div');
|
|
435
|
+
tab.className = 'character-tab';
|
|
436
|
+
tab.dataset.slotId = slotId;
|
|
437
|
+
|
|
438
|
+
if (charInSlot) {
|
|
439
|
+
const isActive = slotId === activeCharacterId;
|
|
440
|
+
|
|
441
|
+
if (isActive) {
|
|
442
|
+
tab.classList.add('active');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
tab.innerHTML = `
|
|
446
|
+
<span class="slot-number">${slotNum}</span>
|
|
447
|
+
<span class="char-name">${charInSlot.name || 'Unknown'}</span>
|
|
448
|
+
<span class="close-tab" title="Clear slot">✕</span>
|
|
449
|
+
`;
|
|
450
|
+
|
|
451
|
+
// Click to switch character
|
|
452
|
+
tab.addEventListener('click', (e) => {
|
|
453
|
+
debug.log(`🖱️ Tab clicked for ${slotId}`, charInSlot.name);
|
|
454
|
+
if (!e.target.classList.contains('close-tab')) {
|
|
455
|
+
if (typeof switchToCharacter === 'function') {
|
|
456
|
+
switchToCharacter(slotId);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Close button - show options modal
|
|
462
|
+
const closeBtn = tab.querySelector('.close-tab');
|
|
463
|
+
closeBtn.addEventListener('click', (e) => {
|
|
464
|
+
e.stopPropagation();
|
|
465
|
+
if (typeof showClearCharacterOptions === 'function') {
|
|
466
|
+
showClearCharacterOptions(slotId, slotNum, charInSlot.name);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
// Empty slot
|
|
471
|
+
tab.classList.add('empty');
|
|
472
|
+
tab.innerHTML = `
|
|
473
|
+
<span class="slot-number">${slotNum}</span>
|
|
474
|
+
<span class="char-name">Empty Slot</span>
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
tabsContainer.appendChild(tab);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ===== EXPORTS =====
|
|
483
|
+
|
|
484
|
+
globalThis.saveCharacterData = saveCharacterData;
|
|
485
|
+
globalThis.sendSyncMessage = sendSyncMessage;
|
|
486
|
+
globalThis.loadCharacterWithTabs = loadCharacterWithTabs;
|
|
487
|
+
globalThis.loadAndBuildTabs = loadAndBuildTabs;
|
|
488
|
+
globalThis.getActiveCharacterId = getActiveCharacterId;
|
|
489
|
+
globalThis.setActiveCharacter = setActiveCharacter;
|
|
490
|
+
globalThis.buildCharacterTabs = buildCharacterTabs;
|
|
491
|
+
globalThis.validateCharacterData = validateCharacterData;
|
|
492
|
+
|
|
493
|
+
// Export state variables with getters and setters
|
|
494
|
+
Object.defineProperty(globalThis, 'currentSlotId', {
|
|
495
|
+
get: () => {
|
|
496
|
+
debug.log(`📍 currentSlotId getter called, returning: ${currentSlotId}`);
|
|
497
|
+
return currentSlotId;
|
|
498
|
+
},
|
|
499
|
+
set: (value) => {
|
|
500
|
+
debug.log(`📍 currentSlotId setter called with value: ${value}`);
|
|
501
|
+
currentSlotId = value;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
Object.defineProperty(globalThis, 'syncDebounceTimer', {
|
|
506
|
+
get: () => syncDebounceTimer,
|
|
507
|
+
set: (value) => { syncDebounceTimer = value; }
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
Object.defineProperty(globalThis, 'characterCache', {
|
|
511
|
+
get: () => characterCache
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
})();
|