@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.
Files changed (118) hide show
  1. package/dist/cache/CacheManager.d.ts.map +1 -0
  2. package/dist/cache/CacheManager.js +131 -0
  3. package/dist/cache/CacheManager.js.map +1 -0
  4. package/dist/index.d.ts +18 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +22 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/ir/index.d.ts +11 -0
  9. package/dist/ir/index.d.ts.map +1 -0
  10. package/dist/ir/index.js +9 -0
  11. package/dist/ir/index.js.map +1 -0
  12. package/dist/ir/normalize.d.ts +10 -0
  13. package/dist/ir/normalize.d.ts.map +1 -0
  14. package/dist/ir/normalize.js +207 -0
  15. package/dist/ir/normalize.js.map +1 -0
  16. package/dist/ir/persistence.d.ts +26 -0
  17. package/dist/ir/persistence.d.ts.map +1 -0
  18. package/dist/ir/persistence.js +21 -0
  19. package/dist/ir/persistence.js.map +1 -0
  20. package/dist/ir/sync.d.ts +12 -0
  21. package/dist/ir/sync.d.ts.map +1 -0
  22. package/dist/ir/sync.js +36 -0
  23. package/dist/ir/sync.js.map +1 -0
  24. package/dist/ir/types.d.ts +143 -0
  25. package/dist/ir/types.d.ts.map +1 -0
  26. package/dist/ir/types.js +13 -0
  27. package/dist/ir/types.js.map +1 -0
  28. package/dist/ir/views/dnd5e.d.ts +40 -0
  29. package/dist/ir/views/dnd5e.d.ts.map +1 -0
  30. package/dist/ir/views/dnd5e.js +50 -0
  31. package/dist/ir/views/dnd5e.js.map +1 -0
  32. package/dist/render/character.d.ts +19 -0
  33. package/dist/render/character.d.ts.map +1 -0
  34. package/dist/render/character.js +156 -0
  35. package/dist/render/character.js.map +1 -0
  36. package/dist/render/h.d.ts +27 -0
  37. package/dist/render/h.d.ts.map +1 -0
  38. package/dist/render/h.js +64 -0
  39. package/dist/render/h.js.map +1 -0
  40. package/dist/render/index.d.ts +11 -0
  41. package/dist/render/index.d.ts.map +1 -0
  42. package/dist/render/index.js +8 -0
  43. package/dist/render/index.js.map +1 -0
  44. package/dist/render/mount.d.ts +31 -0
  45. package/dist/render/mount.d.ts.map +1 -0
  46. package/dist/render/mount.js +63 -0
  47. package/dist/render/mount.js.map +1 -0
  48. package/dist/supabase/fields.d.ts.map +1 -0
  49. package/dist/supabase/fields.js +120 -0
  50. package/dist/supabase/fields.js.map +1 -0
  51. package/dist/types/character.d.ts.map +1 -0
  52. package/dist/types/character.js +5 -0
  53. package/dist/types/character.js.map +1 -0
  54. package/package.json +73 -0
  55. package/src/browser.js +51 -0
  56. package/src/cache/CacheManager.ts +174 -0
  57. package/src/common/browser-polyfill.js +319 -0
  58. package/src/common/debug.js +123 -0
  59. package/src/common/html-utils.js +134 -0
  60. package/src/common/theme-manager.js +265 -0
  61. package/src/index.ts +25 -0
  62. package/src/ir/__fixtures__/dnd5e-character.json +75962 -0
  63. package/src/ir/__fixtures__/non-dnd-character.json +14218 -0
  64. package/src/ir/index.ts +10 -0
  65. package/src/ir/normalize.ts +245 -0
  66. package/src/ir/persistence.ts +37 -0
  67. package/src/ir/sync.ts +49 -0
  68. package/src/ir/types.ts +161 -0
  69. package/src/ir/views/dnd5e.ts +94 -0
  70. package/src/lib/indexeddb-cache.js +320 -0
  71. package/src/modules/action-announcements.js +102 -0
  72. package/src/modules/action-display.js +1557 -0
  73. package/src/modules/action-executor.js +860 -0
  74. package/src/modules/action-filters.js +167 -0
  75. package/src/modules/action-options.js +117 -0
  76. package/src/modules/card-creator.js +142 -0
  77. package/src/modules/character-portrait.js +169 -0
  78. package/src/modules/character-trait-popups.js +959 -0
  79. package/src/modules/character-traits.js +814 -0
  80. package/src/modules/class-feature-edge-cases.js +1320 -0
  81. package/src/modules/color-utils.js +69 -0
  82. package/src/modules/combat-maneuver-edge-cases.js +660 -0
  83. package/src/modules/companions-manager.js +178 -0
  84. package/src/modules/concentration-tracker.js +178 -0
  85. package/src/modules/data-manager.js +514 -0
  86. package/src/modules/dice-roller.js +719 -0
  87. package/src/modules/effects-manager.js +743 -0
  88. package/src/modules/feature-modals.js +1264 -0
  89. package/src/modules/formula-resolver.js +444 -0
  90. package/src/modules/gm-mode.js +184 -0
  91. package/src/modules/health-modals.js +399 -0
  92. package/src/modules/hp-management.js +752 -0
  93. package/src/modules/inventory-manager.js +242 -0
  94. package/src/modules/macro-system.js +825 -0
  95. package/src/modules/notification-system.js +92 -0
  96. package/src/modules/racial-feature-edge-cases.js +746 -0
  97. package/src/modules/resource-manager.js +775 -0
  98. package/src/modules/sheet-builder.js +654 -0
  99. package/src/modules/spell-action-modals.js +583 -0
  100. package/src/modules/spell-cards.js +602 -0
  101. package/src/modules/spell-casting.js +723 -0
  102. package/src/modules/spell-display.js +314 -0
  103. package/src/modules/spell-edge-cases.js +509 -0
  104. package/src/modules/spell-macros.js +201 -0
  105. package/src/modules/spell-modals.js +1221 -0
  106. package/src/modules/spell-slots.js +224 -0
  107. package/src/modules/status-bar-bridge.js +101 -0
  108. package/src/modules/ui-utilities.js +284 -0
  109. package/src/modules/warlock-invocations.js +219 -0
  110. package/src/modules/window-management.js +211 -0
  111. package/src/render/character.ts +234 -0
  112. package/src/render/h.ts +74 -0
  113. package/src/render/index.ts +10 -0
  114. package/src/render/mount.ts +94 -0
  115. package/src/supabase/client.js +1383 -0
  116. package/src/supabase/config.js +60 -0
  117. package/src/supabase/fields.ts +129 -0
  118. package/src/types/character.ts +85 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * IndexedDB Cache Wrapper for OwlCloud
3
+ * Service worker compatible caching layer for character data and token images
4
+ * Supports TTL-based expiration and version-based cache invalidation
5
+ */
6
+
7
+ class IndexedDBCache {
8
+ constructor(dbName = 'owlcloud_cache', version = 1) {
9
+ this.dbName = dbName
10
+ this.version = version
11
+ this.db = null
12
+ }
13
+
14
+ /**
15
+ * Open the IndexedDB database and create object stores
16
+ * @returns {Promise<IDBDatabase>}
17
+ */
18
+ async open() {
19
+ return new Promise((resolve, reject) => {
20
+ const request = indexedDB.open(this.dbName, this.version)
21
+
22
+ request.onerror = () => reject(request.error)
23
+ request.onsuccess = () => {
24
+ this.db = request.result
25
+ resolve(this.db)
26
+ }
27
+
28
+ request.onupgradeneeded = (event) => {
29
+ const db = event.target.result
30
+
31
+ // Characters store
32
+ if (!db.objectStoreNames.contains('characters')) {
33
+ const charStore = db.createObjectStore('characters', { keyPath: 'characterId' })
34
+ charStore.createIndex('expiresAt', 'expiresAt', { unique: false })
35
+ charStore.createIndex('timestamp', 'timestamp', { unique: false })
36
+ }
37
+
38
+ // Token images store
39
+ if (!db.objectStoreNames.contains('token_images')) {
40
+ const imgStore = db.createObjectStore('token_images', { keyPath: 'characterId' })
41
+ imgStore.createIndex('expiresAt', 'expiresAt', { unique: false })
42
+ }
43
+
44
+ // Metadata store for cache statistics
45
+ if (!db.objectStoreNames.contains('metadata')) {
46
+ db.createObjectStore('metadata', { keyPath: 'key' })
47
+ }
48
+ }
49
+ })
50
+ }
51
+
52
+ /**
53
+ * Get a character from cache
54
+ * @param {string} characterId - Character ID to retrieve
55
+ * @returns {Promise<Object|null>} Character data or null if not found/expired
56
+ */
57
+ async getCharacter(characterId) {
58
+ if (!this.db) throw new Error('Database not opened')
59
+
60
+ const tx = this.db.transaction(['characters'], 'readonly')
61
+ const store = tx.objectStore('characters')
62
+ const request = store.get(characterId)
63
+
64
+ return new Promise((resolve, reject) => {
65
+ request.onsuccess = () => {
66
+ const result = request.result
67
+ if (!result || (result.expiresAt && Date.now() > result.expiresAt)) {
68
+ if (result) this.deleteCharacter(characterId)
69
+ resolve(null)
70
+ return
71
+ }
72
+ resolve(result)
73
+ }
74
+ request.onerror = () => reject(request.error)
75
+ })
76
+ }
77
+
78
+ /**
79
+ * Store a character in cache
80
+ * @param {string} characterId - Character ID
81
+ * @param {Object} data - Character data to store
82
+ * @param {number} ttlSeconds - Time to live in seconds (default 300 = 5 minutes)
83
+ * @returns {Promise<boolean>}
84
+ */
85
+ async setCharacter(characterId, data, ttlSeconds = 300) {
86
+ if (!this.db) throw new Error('Database not opened')
87
+
88
+ const entry = {
89
+ characterId,
90
+ data,
91
+ timestamp: Date.now(),
92
+ version: data.updated_at || data.updatedAt || Date.now(),
93
+ expiresAt: Date.now() + (ttlSeconds * 1000)
94
+ }
95
+
96
+ const tx = this.db.transaction(['characters'], 'readwrite')
97
+ const store = tx.objectStore('characters')
98
+ const request = store.put(entry)
99
+
100
+ return new Promise((resolve, reject) => {
101
+ request.onsuccess = () => resolve(true)
102
+ request.onerror = () => reject(request.error)
103
+ })
104
+ }
105
+
106
+ /**
107
+ * Delete a character from cache
108
+ * @param {string} characterId - Character ID to delete
109
+ * @returns {Promise<boolean>}
110
+ */
111
+ async deleteCharacter(characterId) {
112
+ if (!this.db) throw new Error('Database not opened')
113
+
114
+ const tx = this.db.transaction(['characters'], 'readwrite')
115
+ const store = tx.objectStore('characters')
116
+ const request = store.delete(characterId)
117
+
118
+ return new Promise((resolve, reject) => {
119
+ request.onsuccess = () => resolve(true)
120
+ request.onerror = () => reject(request.error)
121
+ })
122
+ }
123
+
124
+ /**
125
+ * Get a token image from cache
126
+ * @param {string} characterId - Character ID
127
+ * @returns {Promise<Object|null>} Token image data or null if not found/expired
128
+ */
129
+ async getTokenImage(characterId) {
130
+ if (!this.db) throw new Error('Database not opened')
131
+
132
+ const tx = this.db.transaction(['token_images'], 'readonly')
133
+ const store = tx.objectStore('token_images')
134
+ const request = store.get(characterId)
135
+
136
+ return new Promise((resolve, reject) => {
137
+ request.onsuccess = () => {
138
+ const result = request.result
139
+ if (!result || (result.expiresAt && Date.now() > result.expiresAt)) {
140
+ if (result) this.deleteTokenImage(characterId)
141
+ resolve(null)
142
+ return
143
+ }
144
+ resolve(result)
145
+ }
146
+ request.onerror = () => reject(request.error)
147
+ })
148
+ }
149
+
150
+ /**
151
+ * Store a token image in cache
152
+ * @param {string} characterId - Character ID
153
+ * @param {string} imageUrl - URL of the image
154
+ * @param {Blob} blob - Image blob data
155
+ * @param {number} ttlSeconds - Time to live in seconds (default 3600 = 1 hour)
156
+ * @returns {Promise<boolean>}
157
+ */
158
+ async setTokenImage(characterId, imageUrl, blob, ttlSeconds = 3600) {
159
+ if (!this.db) throw new Error('Database not opened')
160
+
161
+ const entry = {
162
+ characterId,
163
+ imageUrl,
164
+ blob,
165
+ timestamp: Date.now(),
166
+ expiresAt: Date.now() + (ttlSeconds * 1000)
167
+ }
168
+
169
+ const tx = this.db.transaction(['token_images'], 'readwrite')
170
+ const store = tx.objectStore('token_images')
171
+ const request = store.put(entry)
172
+
173
+ return new Promise((resolve, reject) => {
174
+ request.onsuccess = () => resolve(true)
175
+ request.onerror = () => reject(request.error)
176
+ })
177
+ }
178
+
179
+ /**
180
+ * Delete a token image from cache
181
+ * @param {string} characterId - Character ID
182
+ * @returns {Promise<boolean>}
183
+ */
184
+ async deleteTokenImage(characterId) {
185
+ if (!this.db) throw new Error('Database not opened')
186
+
187
+ const tx = this.db.transaction(['token_images'], 'readwrite')
188
+ const store = tx.objectStore('token_images')
189
+ const request = store.delete(characterId)
190
+
191
+ return new Promise((resolve, reject) => {
192
+ request.onsuccess = () => resolve(true)
193
+ request.onerror = () => reject(request.error)
194
+ })
195
+ }
196
+
197
+ /**
198
+ * Clean up expired entries from all stores
199
+ * @returns {Promise<number>} Number of entries deleted
200
+ */
201
+ async cleanupExpired() {
202
+ if (!this.db) throw new Error('Database not opened')
203
+
204
+ const now = Date.now()
205
+ let deletedCount = 0
206
+
207
+ // Clean characters
208
+ try {
209
+ const charTx = this.db.transaction(['characters'], 'readwrite')
210
+ const charStore = charTx.objectStore('characters')
211
+ const charIndex = charStore.index('expiresAt')
212
+ const charRange = IDBKeyRange.upperBound(now)
213
+ const charRequest = charIndex.openCursor(charRange)
214
+
215
+ await new Promise((resolve, reject) => {
216
+ charRequest.onsuccess = (event) => {
217
+ const cursor = event.target.result
218
+ if (cursor) {
219
+ cursor.delete()
220
+ deletedCount++
221
+ cursor.continue()
222
+ } else {
223
+ resolve()
224
+ }
225
+ }
226
+ charRequest.onerror = () => reject(charRequest.error)
227
+ })
228
+ } catch (err) {
229
+ console.error('Error cleaning character cache:', err)
230
+ }
231
+
232
+ // Clean token images
233
+ try {
234
+ const imgTx = this.db.transaction(['token_images'], 'readwrite')
235
+ const imgStore = imgTx.objectStore('token_images')
236
+ const imgIndex = imgStore.index('expiresAt')
237
+ const imgRange = IDBKeyRange.upperBound(now)
238
+ const imgRequest = imgIndex.openCursor(imgRange)
239
+
240
+ await new Promise((resolve, reject) => {
241
+ imgRequest.onsuccess = (event) => {
242
+ const cursor = event.target.result
243
+ if (cursor) {
244
+ cursor.delete()
245
+ deletedCount++
246
+ cursor.continue()
247
+ } else {
248
+ resolve()
249
+ }
250
+ }
251
+ imgRequest.onerror = () => reject(imgRequest.error)
252
+ })
253
+ } catch (err) {
254
+ console.error('Error cleaning token image cache:', err)
255
+ }
256
+
257
+ return deletedCount
258
+ }
259
+
260
+ /**
261
+ * Get cache statistics
262
+ * @returns {Promise<Object>} Cache stats
263
+ */
264
+ async getStats() {
265
+ if (!this.db) throw new Error('Database not opened')
266
+
267
+ const stats = {
268
+ characters: 0,
269
+ tokenImages: 0,
270
+ totalSize: 0
271
+ }
272
+
273
+ // Count characters
274
+ const charTx = this.db.transaction(['characters'], 'readonly')
275
+ const charStore = charTx.objectStore('characters')
276
+ const charCountRequest = charStore.count()
277
+
278
+ stats.characters = await new Promise((resolve, reject) => {
279
+ charCountRequest.onsuccess = () => resolve(charCountRequest.result)
280
+ charCountRequest.onerror = () => reject(charCountRequest.error)
281
+ })
282
+
283
+ // Count token images
284
+ const imgTx = this.db.transaction(['token_images'], 'readonly')
285
+ const imgStore = imgTx.objectStore('token_images')
286
+ const imgCountRequest = imgStore.count()
287
+
288
+ stats.tokenImages = await new Promise((resolve, reject) => {
289
+ imgCountRequest.onsuccess = () => resolve(imgCountRequest.result)
290
+ imgCountRequest.onerror = () => reject(imgCountRequest.error)
291
+ })
292
+
293
+ return stats
294
+ }
295
+
296
+ /**
297
+ * Clear all cache data
298
+ * @returns {Promise<boolean>}
299
+ */
300
+ async clearAll() {
301
+ if (!this.db) throw new Error('Database not opened')
302
+
303
+ const tx = this.db.transaction(['characters', 'token_images'], 'readwrite')
304
+ const charStore = tx.objectStore('characters')
305
+ const imgStore = tx.objectStore('token_images')
306
+
307
+ charStore.clear()
308
+ imgStore.clear()
309
+
310
+ return new Promise((resolve, reject) => {
311
+ tx.oncomplete = () => resolve(true)
312
+ tx.onerror = () => reject(tx.error)
313
+ })
314
+ }
315
+ }
316
+
317
+ // Export for use in modules
318
+ if (typeof module !== 'undefined' && module.exports) {
319
+ module.exports = IndexedDBCache
320
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Action Announcements Module
3
+ *
4
+ * Handles announcing actions to chat.
5
+ * Loaded as a plain script (no ES6 modules) to export to globalThis.
6
+ *
7
+ * Functions exported to globalThis:
8
+ * - announceAction(action)
9
+ * - postActionToChat(actionLabel, state)
10
+ */
11
+
12
+ (function() {
13
+ 'use strict';
14
+
15
+ /**
16
+ * Announce the use of an action to chat
17
+ * @param {Object} action - Action object with name, actionType, description, etc.
18
+ */
19
+ function announceAction(action) {
20
+ // TODO: Add Owlbear Rodeo integration for action announcements
21
+ const colorBanner = getColoredBanner(characterData);
22
+
23
+ // Determine action type emoji
24
+ const actionTypeEmoji = {
25
+ 'bonus': '⚡',
26
+ 'reaction': '🛡️',
27
+ 'action': '⚔️',
28
+ 'free': '💨',
29
+ 'legendary': '👑',
30
+ 'lair': '🏰',
31
+ 'other': '✨'
32
+ };
33
+
34
+ const emoji = actionTypeEmoji[action.actionType?.toLowerCase()] || '✨';
35
+ const actionTypeText = action.actionType ? ` (${action.actionType})` : '';
36
+
37
+ let message = `&{template:default} {{name=${colorBanner}${characterData.name}}} {{${emoji} Action=${action.name}}} {{Type=${action.actionType || 'action'}}}`;
38
+
39
+ // Add summary if available
40
+ if (action.summary) {
41
+ const resolvedSummary = resolveVariablesInFormula(action.summary);
42
+ message += ` {{Summary=${resolvedSummary}}}`;
43
+ }
44
+
45
+ // Add description (resolve variables first)
46
+ if (action.description) {
47
+ const resolvedDescription = resolveVariablesInFormula(action.description);
48
+ message += ` {{Description=${resolvedDescription}}}`;
49
+ }
50
+
51
+ // Add uses if available
52
+ if (action.uses) {
53
+ const usesUsed = action.usesUsed || 0;
54
+ const usesTotal = action.uses.total || action.uses.value || action.uses;
55
+ // Prefer usesLeft from DiceCloud if available, otherwise calculate from usesUsed
56
+ const usesRemaining = action.usesLeft !== undefined ? action.usesLeft : (usesTotal - usesUsed);
57
+ const usesText = `${usesRemaining} / ${usesTotal}`;
58
+ message += ` {{Uses=${usesText}}}`;
59
+ }
60
+
61
+ // Get color emoji and character name for notification
62
+ const colorEmoji = typeof getColorEmoji === 'function' ? getColorEmoji(characterData.notificationColor) : '';
63
+ const notificationText = colorEmoji ? `${colorEmoji} ${characterData.name} used ${action.name}!` : `✨ ${characterData.name} used ${action.name}!`;
64
+
65
+ showNotification(notificationText);
66
+ debug.log('✅ Action announcement displayed');
67
+
68
+ // Send to Roll20 chat
69
+ const messageData = {
70
+ action: 'announceSpell',
71
+ message: message,
72
+ color: characterData.notificationColor
73
+ };
74
+
75
+ sendToRoll20(messageData);
76
+ debug.log('📨 Action announcement sent to Roll20');
77
+
78
+ // TODO: Add Owlbear Rodeo integration here to send action announcements to VTT
79
+ }
80
+
81
+ /**
82
+ * Post action economy update to chat
83
+ * @param {string} actionLabel - Label for the action (e.g., "Action", "Bonus Action")
84
+ * @param {string} state - State: "used" or "restored"
85
+ */
86
+ function postActionToChat(actionLabel, state) {
87
+ const emoji = state === 'used' ? '❌' : '✅';
88
+ const message = `${emoji} ${characterData.name} ${state === 'used' ? 'uses' : 'restores'} ${actionLabel}`;
89
+ postToChatIfOpener(message);
90
+
91
+ // Also post to Discord
92
+ postActionEconomyToDiscord();
93
+ }
94
+
95
+ // ===== EXPORTS =====
96
+
97
+ globalThis.announceAction = announceAction;
98
+ globalThis.postActionToChat = postActionToChat;
99
+
100
+ console.log('✅ Action Announcements module loaded');
101
+
102
+ })();