@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,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
|
+
})();
|