@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,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formula Resolver Module
|
|
3
|
+
*
|
|
4
|
+
* Handles variable substitution and formula resolution for DiceCloud formulas.
|
|
5
|
+
* This is the core formula parsing engine that resolves variables like:
|
|
6
|
+
* - Bare variables (e.g., "breathWeaponDamage")
|
|
7
|
+
* - DiceCloud references (e.g., "#spellList.abilityMod")
|
|
8
|
+
* - Attribute modifiers (e.g., "strength.modifier")
|
|
9
|
+
* - Math expressions (e.g., "ceil(level/2)")
|
|
10
|
+
* - Inline calculations (e.g., "{varName + 2}")
|
|
11
|
+
*
|
|
12
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
13
|
+
*
|
|
14
|
+
* Functions exported to globalThis:
|
|
15
|
+
* - resolveVariablesInFormula(formula)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
(function() {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves variables in a formula string
|
|
23
|
+
* @param {string} formula - Formula with variable references
|
|
24
|
+
* @returns {string} Formula with variables resolved to their values
|
|
25
|
+
*/
|
|
26
|
+
function resolveVariablesInFormula(formula) {
|
|
27
|
+
if (!formula || typeof formula !== 'string') {
|
|
28
|
+
return formula;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
debug.log(`🔧 resolveVariablesInFormula called with: "${formula}"`);
|
|
32
|
+
|
|
33
|
+
// Check if characterData is available
|
|
34
|
+
if (typeof characterData === 'undefined' || !characterData) {
|
|
35
|
+
debug.warn('⚠️ characterData not available for formula resolution');
|
|
36
|
+
return formula;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if characterData has otherVariables
|
|
40
|
+
if (!characterData.otherVariables || typeof characterData.otherVariables !== 'object') {
|
|
41
|
+
debug.log('⚠️ No otherVariables available for formula resolution');
|
|
42
|
+
return formula;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// NOTE: we intentionally do NOT bail when the text contains slotLevel.
|
|
46
|
+
// Bare slotLevel in a damage formula (e.g. "2d8 + slotLevel") is preserved
|
|
47
|
+
// because no pattern below resolves it — the cast modal substitutes the
|
|
48
|
+
// chosen level. But slotLevel inside a { ... } inline calc in description
|
|
49
|
+
// text is resolved to the base level (1) so summaries don't show raw
|
|
50
|
+
// template syntax.
|
|
51
|
+
let resolvedFormula = formula;
|
|
52
|
+
let variablesResolved = [];
|
|
53
|
+
|
|
54
|
+
// Pattern 0: Check if the entire formula is just a bare variable name (e.g., "breathWeaponDamage")
|
|
55
|
+
// This must be checked BEFORE other patterns to handle cases like action.damage = "breathWeaponDamage"
|
|
56
|
+
const bareVariablePattern = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
|
|
57
|
+
if (bareVariablePattern.test(formula.trim())) {
|
|
58
|
+
const varName = formula.trim();
|
|
59
|
+
if (characterData.otherVariables.hasOwnProperty(varName)) {
|
|
60
|
+
const variableValue = characterData.otherVariables[varName];
|
|
61
|
+
|
|
62
|
+
// Extract the value
|
|
63
|
+
let value = null;
|
|
64
|
+
if (typeof variableValue === 'number') {
|
|
65
|
+
value = variableValue;
|
|
66
|
+
} else if (typeof variableValue === 'string') {
|
|
67
|
+
value = variableValue;
|
|
68
|
+
} else if (typeof variableValue === 'object' && variableValue.value !== undefined) {
|
|
69
|
+
value = variableValue.value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (value !== null && value !== undefined) {
|
|
73
|
+
debug.log(`✅ Resolved bare variable: ${varName} = ${value}`);
|
|
74
|
+
return String(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
debug.log(`⚠️ Bare variable not found in otherVariables: ${varName}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Helper function to get variable value (handles dot notation like "bard.level")
|
|
81
|
+
const getVariableValue = (varPath) => {
|
|
82
|
+
// Strip # prefix if present (DiceCloud reference notation)
|
|
83
|
+
const cleanPath = varPath.startsWith('#') ? varPath.substring(1) : varPath;
|
|
84
|
+
|
|
85
|
+
// Handle attribute modifiers like "strength.modifier", "wisdom.modifier"
|
|
86
|
+
if (cleanPath.includes('.modifier')) {
|
|
87
|
+
const attrName = cleanPath.replace('.modifier', '');
|
|
88
|
+
if (characterData.attributeMods && characterData.attributeMods[attrName] !== undefined) {
|
|
89
|
+
const modifier = characterData.attributeMods[attrName];
|
|
90
|
+
debug.log(`✅ Resolved attribute modifier: ${cleanPath} = ${modifier}`);
|
|
91
|
+
return modifier;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle attribute scores like "strength", "wisdom"
|
|
96
|
+
if (characterData.attributes && characterData.attributes[cleanPath] !== undefined) {
|
|
97
|
+
const score = characterData.attributes[cleanPath];
|
|
98
|
+
debug.log(`✅ Resolved attribute score: ${cleanPath} = ${score}`);
|
|
99
|
+
return score;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle proficiency bonus
|
|
103
|
+
if (cleanPath === 'proficiencyBonus' && characterData.proficiencyBonus !== undefined) {
|
|
104
|
+
const profBonus = characterData.proficiencyBonus;
|
|
105
|
+
debug.log(`✅ Resolved proficiency bonus: ${cleanPath} = ${profBonus}`);
|
|
106
|
+
return profBonus;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle special DiceCloud spell references (e.g., "spellList.abilityMod")
|
|
110
|
+
// These reference the spellcasting ability modifier for the character's class
|
|
111
|
+
if (cleanPath === 'spellList.abilityMod' || cleanPath === 'spellList.ability') {
|
|
112
|
+
// Determine spellcasting ability based on character class
|
|
113
|
+
const charClass = (characterData.class || '').toLowerCase();
|
|
114
|
+
let spellcastingAbility = null;
|
|
115
|
+
|
|
116
|
+
// Map classes to their spellcasting abilities
|
|
117
|
+
if (charClass.includes('cleric') || charClass.includes('druid') || charClass.includes('ranger')) {
|
|
118
|
+
spellcastingAbility = 'wisdom';
|
|
119
|
+
} else if (charClass.includes('wizard') || charClass.includes('artificer')) {
|
|
120
|
+
spellcastingAbility = 'intelligence';
|
|
121
|
+
} else if (charClass.includes('bard') || charClass.includes('paladin') || charClass.includes('sorcerer') || charClass.includes('warlock')) {
|
|
122
|
+
spellcastingAbility = 'charisma';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (spellcastingAbility) {
|
|
126
|
+
// Prefer a precomputed modifier when present.
|
|
127
|
+
if (characterData.attributeMods && characterData.attributeMods[spellcastingAbility] !== undefined) {
|
|
128
|
+
const modifier = characterData.attributeMods[spellcastingAbility];
|
|
129
|
+
debug.log(`✅ Resolved ${cleanPath} to ${spellcastingAbility} modifier: ${modifier}`);
|
|
130
|
+
return modifier;
|
|
131
|
+
}
|
|
132
|
+
// Fall back to computing it from the ability score. attributeMods isn't
|
|
133
|
+
// always populated on the data feeding the sheet (e.g. cloud-loaded
|
|
134
|
+
// characters), but the raw score is — so #spellList.abilityMod / .dc
|
|
135
|
+
// still resolve instead of showing raw template text.
|
|
136
|
+
const score = characterData.attributes && characterData.attributes[spellcastingAbility];
|
|
137
|
+
if (typeof score === 'number') {
|
|
138
|
+
const modifier = Math.floor((score - 10) / 2);
|
|
139
|
+
debug.log(`✅ Computed ${cleanPath} from ${spellcastingAbility} score ${score}: ${modifier}`);
|
|
140
|
+
return modifier;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle spellList.dc (spell save DC)
|
|
146
|
+
if (cleanPath === 'spellList.dc') {
|
|
147
|
+
// Spell Save DC = 8 + proficiency bonus + spellcasting ability modifier
|
|
148
|
+
const profBonus = characterData.proficiencyBonus || 0;
|
|
149
|
+
const spellMod = getVariableValue('#spellList.abilityMod');
|
|
150
|
+
if (spellMod !== null) {
|
|
151
|
+
const spellDC = 8 + profBonus + spellMod;
|
|
152
|
+
debug.log(`✅ Calculated spell DC: 8 + ${profBonus} + ${spellMod} = ${spellDC}`);
|
|
153
|
+
return spellDC;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Handle spellList.attackBonus (spell attack bonus)
|
|
158
|
+
if (cleanPath === 'spellList.attackBonus') {
|
|
159
|
+
// Spell Attack Bonus = proficiency bonus + spellcasting ability modifier
|
|
160
|
+
const profBonus = characterData.proficiencyBonus || 0;
|
|
161
|
+
const spellMod = getVariableValue('#spellList.abilityMod');
|
|
162
|
+
if (spellMod !== null) {
|
|
163
|
+
const attackBonus = profBonus + spellMod;
|
|
164
|
+
debug.log(`✅ Calculated spell attack bonus: ${profBonus} + ${spellMod} = ${attackBonus}`);
|
|
165
|
+
return attackBonus;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try direct lookup first
|
|
170
|
+
if (characterData.otherVariables.hasOwnProperty(cleanPath)) {
|
|
171
|
+
const val = characterData.otherVariables[cleanPath];
|
|
172
|
+
if (typeof val === 'number') return val;
|
|
173
|
+
if (typeof val === 'boolean') return val;
|
|
174
|
+
if (typeof val === 'object' && val.value !== undefined) return val.value;
|
|
175
|
+
if (typeof val === 'string') return val;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try converting dot notation (e.g., "bard.level" -> "bardLevel")
|
|
179
|
+
const camelCase = cleanPath.replace(/\.([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
180
|
+
if (characterData.otherVariables.hasOwnProperty(camelCase)) {
|
|
181
|
+
const val = characterData.otherVariables[camelCase];
|
|
182
|
+
if (typeof val === 'number') return val;
|
|
183
|
+
if (typeof val === 'boolean') return val;
|
|
184
|
+
if (typeof val === 'object' && val.value !== undefined) return val.value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Try other common patterns
|
|
188
|
+
const alternatives = [
|
|
189
|
+
cleanPath.replace(/\./g, ''), // Remove dots
|
|
190
|
+
cleanPath.split('.').pop(), // Just the last part
|
|
191
|
+
cleanPath.replace(/\./g, '_') // Underscores instead
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const alt of alternatives) {
|
|
195
|
+
if (characterData.otherVariables.hasOwnProperty(alt)) {
|
|
196
|
+
const val = characterData.otherVariables[alt];
|
|
197
|
+
if (typeof val === 'number') return val;
|
|
198
|
+
if (typeof val === 'boolean') return val;
|
|
199
|
+
if (typeof val === 'object' && val.value !== undefined) return val.value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return null;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Resolve DiceCloud inline calculations wrapped in { ... } FIRST — before the
|
|
207
|
+
// bracket/parenthesis patterns below, which would otherwise mangle arrays
|
|
208
|
+
// inside them. DiceCloud uses { ... } for any calc embedded in description
|
|
209
|
+
// text, e.g. {#spellList.dc}, {#spellList.abilityMod}, {max(slotLevel, 1)},
|
|
210
|
+
// {[2,3,...][slotLevel]d8}. Identifiers resolve to their value (unknown -> 0);
|
|
211
|
+
// slotLevel -> base level (1); ceil/floor/round -> Math.*; DiceCloud arrays
|
|
212
|
+
// are 1-based.
|
|
213
|
+
const MATH_FUNC_MAP = { ceil: 'Math.ceil', floor: 'Math.floor', round: 'Math.round' };
|
|
214
|
+
resolvedFormula = resolvedFormula.replace(/\{([^}]+)\}/g, (fullMatch, expression) => {
|
|
215
|
+
try {
|
|
216
|
+
let expr = expression.replace(/#?[a-zA-Z_][a-zA-Z0-9_.]*/g, (token, offset, str) => {
|
|
217
|
+
const name = token.replace(/^#/, '');
|
|
218
|
+
const isCall = /^\s*\(/.test(str.slice(offset + token.length));
|
|
219
|
+
if (isCall) {
|
|
220
|
+
if (MATH_FUNC_MAP[name]) return MATH_FUNC_MAP[name];
|
|
221
|
+
if (name === 'max' || name === 'min' || name.startsWith('Math.')) return name;
|
|
222
|
+
return name;
|
|
223
|
+
}
|
|
224
|
+
if (name === 'slotLevel') return '1'; // base slot level for display
|
|
225
|
+
const value = getVariableValue(name);
|
|
226
|
+
return (value !== null && typeof value === 'number') ? String(value) : '0';
|
|
227
|
+
});
|
|
228
|
+
// DiceCloud arrays are 1-based: [a,b,c,...][n] -> the nth element.
|
|
229
|
+
expr = expr.replace(/\[\s*([^\[\]]+?)\s*\]\s*\[\s*(\d+)\s*\]/g, (m, list, idx) => {
|
|
230
|
+
const arr = list.split(',').map(s => s.trim());
|
|
231
|
+
const i = parseInt(idx, 10) - 1;
|
|
232
|
+
return (i >= 0 && i < arr.length) ? arr[i] : m;
|
|
233
|
+
});
|
|
234
|
+
const result = safeMathEval(expr);
|
|
235
|
+
if (typeof result === 'number' && isFinite(result)) {
|
|
236
|
+
debug.log(`✅ Evaluated inline calculation: {${expression}} = ${result}`);
|
|
237
|
+
return String(result);
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {
|
|
240
|
+
debug.log(`⚠️ Failed to evaluate inline calculation: {${expression}}`, e);
|
|
241
|
+
}
|
|
242
|
+
return fullMatch;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Pattern 1a: Find DiceCloud references in parentheses like (#spellList.abilityMod)
|
|
246
|
+
const diceCloudRefPattern = /\((#[a-zA-Z_][a-zA-Z0-9_.]*)\)/g;
|
|
247
|
+
let match;
|
|
248
|
+
|
|
249
|
+
while ((match = diceCloudRefPattern.exec(formula)) !== null) {
|
|
250
|
+
const varRef = match[1]; // e.g., "#spellList.abilityMod"
|
|
251
|
+
const fullMatch = match[0]; // e.g., "(#spellList.abilityMod)"
|
|
252
|
+
|
|
253
|
+
// Use getVariableValue which handles # prefix and dot notation
|
|
254
|
+
const value = getVariableValue(varRef);
|
|
255
|
+
|
|
256
|
+
if (value !== null && typeof value === 'number') {
|
|
257
|
+
resolvedFormula = resolvedFormula.replace(fullMatch, value);
|
|
258
|
+
variablesResolved.push(`${varRef}=${value}`);
|
|
259
|
+
debug.log(`✅ Resolved DiceCloud reference: ${varRef} = ${value}`);
|
|
260
|
+
} else {
|
|
261
|
+
debug.log(`⚠️ Could not resolve DiceCloud reference: ${varRef}, value: ${value}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Pattern 1a-bare: Find bare DiceCloud references like #spellList.abilityMod (not in parentheses)
|
|
266
|
+
// This handles cases like "2d8 + #spellList.abilityMod" in spell damage formulas
|
|
267
|
+
const bareDiceCloudRefPattern = /#([a-zA-Z_][a-zA-Z0-9_.]*)/g;
|
|
268
|
+
|
|
269
|
+
while ((match = bareDiceCloudRefPattern.exec(resolvedFormula)) !== null) {
|
|
270
|
+
const varRef = '#' + match[1]; // e.g., "#spellList.abilityMod"
|
|
271
|
+
const fullMatch = match[0]; // e.g., "#spellList.abilityMod"
|
|
272
|
+
|
|
273
|
+
// Use getVariableValue which handles # prefix and dot notation
|
|
274
|
+
const value = getVariableValue(varRef);
|
|
275
|
+
|
|
276
|
+
if (value !== null && typeof value === 'number') {
|
|
277
|
+
resolvedFormula = resolvedFormula.replace(fullMatch, value);
|
|
278
|
+
variablesResolved.push(`${varRef}=${value}`);
|
|
279
|
+
debug.log(`✅ Resolved bare DiceCloud reference: ${varRef} = ${value}`);
|
|
280
|
+
} else {
|
|
281
|
+
debug.log(`⚠️ Could not resolve bare DiceCloud reference: ${varRef}, value: ${value}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Pattern 1b: Find simple variables in parentheses like (variableName)
|
|
286
|
+
const parenthesesPattern = /\(([a-zA-Z_][a-zA-Z0-9_]*)\)/g;
|
|
287
|
+
|
|
288
|
+
while ((match = parenthesesPattern.exec(formula)) !== null) {
|
|
289
|
+
const variableName = match[1];
|
|
290
|
+
const fullMatch = match[0]; // e.g., "(sneakAttackDieAmount)"
|
|
291
|
+
|
|
292
|
+
// Look up the variable value
|
|
293
|
+
if (characterData.otherVariables.hasOwnProperty(variableName)) {
|
|
294
|
+
const variableValue = characterData.otherVariables[variableName];
|
|
295
|
+
|
|
296
|
+
// Extract numeric value
|
|
297
|
+
let numericValue = null;
|
|
298
|
+
if (typeof variableValue === 'number') {
|
|
299
|
+
numericValue = variableValue;
|
|
300
|
+
} else if (typeof variableValue === 'object' && variableValue.value !== undefined) {
|
|
301
|
+
numericValue = variableValue.value;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (numericValue !== null) {
|
|
305
|
+
resolvedFormula = resolvedFormula.replace(fullMatch, numericValue);
|
|
306
|
+
variablesResolved.push(`${variableName}=${numericValue}`);
|
|
307
|
+
debug.log(`✅ Resolved variable: ${variableName} = ${numericValue}`);
|
|
308
|
+
} else {
|
|
309
|
+
debug.log(`⚠️ Variable ${variableName} has non-numeric value:`, variableValue);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Pattern 2: DiceCloud expressions in square brackets like [ceil(level/2)]
|
|
315
|
+
// These support math functions like ceil, floor, round, abs
|
|
316
|
+
const bracketExprPattern = /\[([^\]]+)\]/g;
|
|
317
|
+
|
|
318
|
+
while ((match = bracketExprPattern.exec(formula)) !== null) {
|
|
319
|
+
const expression = match[1]; // e.g., "ceil(level/2)"
|
|
320
|
+
const fullMatch = match[0]; // e.g., "[ceil(level/2)]"
|
|
321
|
+
|
|
322
|
+
// Remove whitespace for easier parsing
|
|
323
|
+
const cleanExpr = expression.replace(/\s+/g, '');
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
// Check if it's a math function (ceil, floor, round, abs)
|
|
327
|
+
const mathFuncPattern = /^(ceil|floor|round|abs)\((.+)\)$/;
|
|
328
|
+
const funcMatch = mathFuncPattern.exec(cleanExpr);
|
|
329
|
+
|
|
330
|
+
if (funcMatch) {
|
|
331
|
+
const funcName = funcMatch[1];
|
|
332
|
+
const funcExpression = funcMatch[2];
|
|
333
|
+
|
|
334
|
+
// Replace variables in the expression
|
|
335
|
+
let evalExpression = funcExpression;
|
|
336
|
+
|
|
337
|
+
// Find all variable names and replace with values
|
|
338
|
+
const varPattern = /[a-zA-Z_][a-zA-Z0-9_.]*/g;
|
|
339
|
+
let varMatch;
|
|
340
|
+
const replacements = [];
|
|
341
|
+
|
|
342
|
+
while ((varMatch = varPattern.exec(funcExpression)) !== null) {
|
|
343
|
+
const varName = varMatch[0];
|
|
344
|
+
const value = getVariableValue(varName);
|
|
345
|
+
if (value !== null && typeof value === 'number') {
|
|
346
|
+
replacements.push({ name: varName, value: value });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Sort by length (longest first) to avoid partial replacements
|
|
351
|
+
replacements.sort((a, b) => b.name.length - a.name.length);
|
|
352
|
+
|
|
353
|
+
for (const {name, value} of replacements) {
|
|
354
|
+
evalExpression = evalExpression.replace(new RegExp(name.replace(/\./g, '\\.'), 'g'), value);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Evaluate the expression using safeMathEval
|
|
358
|
+
if (/^[\d\s+\-*/().]+$/.test(evalExpression)) {
|
|
359
|
+
const evalResult = safeMathEval(evalExpression);
|
|
360
|
+
let result;
|
|
361
|
+
|
|
362
|
+
switch (funcName) {
|
|
363
|
+
case 'ceil':
|
|
364
|
+
result = Math.ceil(evalResult);
|
|
365
|
+
break;
|
|
366
|
+
case 'floor':
|
|
367
|
+
result = Math.floor(evalResult);
|
|
368
|
+
break;
|
|
369
|
+
case 'round':
|
|
370
|
+
result = Math.round(evalResult);
|
|
371
|
+
break;
|
|
372
|
+
case 'abs':
|
|
373
|
+
result = Math.abs(evalResult);
|
|
374
|
+
break;
|
|
375
|
+
default:
|
|
376
|
+
result = evalResult;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
resolvedFormula = resolvedFormula.replace(fullMatch, result);
|
|
380
|
+
variablesResolved.push(`${funcName}(${expression})=${result}`);
|
|
381
|
+
debug.log(`✅ Resolved math function: ${funcName}(${expression}) = ${result}`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch (e) {
|
|
386
|
+
debug.log(`⚠️ Failed to resolve ${cleanExpr}`, e);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Try to evaluate as math expression
|
|
390
|
+
let evalExpression = cleanExpr;
|
|
391
|
+
|
|
392
|
+
// Replace all variable names with their values (sorted by length to avoid partial matches)
|
|
393
|
+
const varPattern = /[a-zA-Z_][a-zA-Z0-9_.]*/g;
|
|
394
|
+
let varMatch;
|
|
395
|
+
const replacements = [];
|
|
396
|
+
|
|
397
|
+
while ((varMatch = varPattern.exec(cleanExpr)) !== null) {
|
|
398
|
+
const varName = varMatch[0];
|
|
399
|
+
const value = getVariableValue(varName);
|
|
400
|
+
if (value !== null && typeof value === 'number') {
|
|
401
|
+
replacements.push({ name: varName, value: value });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Sort by length (longest first) to avoid partial replacements
|
|
406
|
+
replacements.sort((a, b) => b.name.length - a.name.length);
|
|
407
|
+
|
|
408
|
+
for (const {name, value} of replacements) {
|
|
409
|
+
evalExpression = evalExpression.replace(new RegExp(name.replace(/\./g, '\\.'), 'g'), value);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Try to evaluate the expression using safeMathEval
|
|
413
|
+
try {
|
|
414
|
+
if (/^[\d\s+\-*/().]+$/.test(evalExpression)) {
|
|
415
|
+
const result = safeMathEval(evalExpression);
|
|
416
|
+
resolvedFormula = resolvedFormula.replace(fullMatch, Math.floor(result));
|
|
417
|
+
variablesResolved.push(`${cleanExpr}=${Math.floor(result)}`);
|
|
418
|
+
debug.log(`✅ Resolved expression: ${cleanExpr} = ${Math.floor(result)}`);
|
|
419
|
+
} else {
|
|
420
|
+
debug.log(`⚠️ Could not resolve expression: ${cleanExpr} (eval: ${evalExpression})`);
|
|
421
|
+
}
|
|
422
|
+
} catch (e) {
|
|
423
|
+
debug.log(`⚠️ Failed to evaluate expression: ${cleanExpr}`, e);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (variablesResolved.length > 0) {
|
|
428
|
+
debug.log(`🔧 Formula resolution: "${formula}" -> "${resolvedFormula}" (${variablesResolved.join(', ')})`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Strip remaining markdown formatting
|
|
432
|
+
resolvedFormula = resolvedFormula.replace(/\*\*/g, ''); // Remove bold markers
|
|
433
|
+
|
|
434
|
+
return resolvedFormula;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ===== EXPORTS =====
|
|
438
|
+
|
|
439
|
+
// Export function to globalThis
|
|
440
|
+
globalThis.resolveVariablesInFormula = resolveVariablesInFormula;
|
|
441
|
+
|
|
442
|
+
debug.log('✅ Formula Resolver module loaded');
|
|
443
|
+
|
|
444
|
+
})();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GM Mode Module
|
|
3
|
+
*
|
|
4
|
+
* Handles GM (Game Master) mode features for Roll20 integration:
|
|
5
|
+
* - GM Mode toggle (enables/disables GM panel overlay on Roll20)
|
|
6
|
+
* - Character sharing (broadcasts full character sheet to GM via Roll20 chat)
|
|
7
|
+
* - Read-only mode (hides controls when sheet is opened from GM panel)
|
|
8
|
+
*
|
|
9
|
+
* GM Mode allows the DM to:
|
|
10
|
+
* - View a persistent overlay panel on Roll20 with player character stats
|
|
11
|
+
* - Receive character broadcasts via encoded chat messages
|
|
12
|
+
* - Open read-only character sheets from the GM panel
|
|
13
|
+
*
|
|
14
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
15
|
+
*
|
|
16
|
+
* Functions exported to globalThis:
|
|
17
|
+
* - hideGMControls()
|
|
18
|
+
* - initGMMode()
|
|
19
|
+
* - initShowToGM()
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
(function() {
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hide GM controls when opened from GM panel (read-only mode)
|
|
27
|
+
* This function is called when a character sheet is opened from the GM panel
|
|
28
|
+
* to prevent the GM from modifying player character data.
|
|
29
|
+
*/
|
|
30
|
+
function hideGMControls() {
|
|
31
|
+
// Hide GM mode toggle
|
|
32
|
+
const gmModeContainer = document.querySelector('.gm-mode-container');
|
|
33
|
+
if (gmModeContainer) {
|
|
34
|
+
gmModeContainer.style.display = 'none';
|
|
35
|
+
debug.log('👑 Hidden GM mode toggle');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Hide settings button
|
|
39
|
+
const settingsBtn = document.getElementById('settings-btn');
|
|
40
|
+
if (settingsBtn) {
|
|
41
|
+
settingsBtn.style.display = 'none';
|
|
42
|
+
debug.log('👑 Hidden settings button');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Hide color picker
|
|
46
|
+
const colorPickerContainer = document.querySelector('.color-picker-container');
|
|
47
|
+
if (colorPickerContainer) {
|
|
48
|
+
colorPickerContainer.style.display = 'none';
|
|
49
|
+
debug.log('👑 Hidden color picker');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Update title to indicate read-only mode
|
|
53
|
+
const titleElement = document.querySelector('.char-name-section');
|
|
54
|
+
if (titleElement) {
|
|
55
|
+
titleElement.innerHTML = titleElement.innerHTML.replace('🎲 Character Sheet', '🎲 Character Sheet (Read Only)');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize GM Mode toggle button
|
|
61
|
+
* Toggles the GM panel overlay on Roll20 tabs
|
|
62
|
+
*/
|
|
63
|
+
function initGMMode() {
|
|
64
|
+
const gmModeToggle = document.getElementById('gm-mode-toggle');
|
|
65
|
+
|
|
66
|
+
if (gmModeToggle) {
|
|
67
|
+
gmModeToggle.addEventListener('click', () => {
|
|
68
|
+
const isActive = gmModeToggle.classList.contains('active');
|
|
69
|
+
|
|
70
|
+
// Send message to Roll20 content script to toggle GM panel
|
|
71
|
+
sendToRoll20({
|
|
72
|
+
action: 'toggleGMMode',
|
|
73
|
+
enabled: !isActive
|
|
74
|
+
});
|
|
75
|
+
debug.log(`👑 GM Mode ${!isActive ? 'enabled' : 'disabled'}`);
|
|
76
|
+
|
|
77
|
+
// Toggle active state
|
|
78
|
+
gmModeToggle.classList.toggle('active');
|
|
79
|
+
showNotification(isActive ? '👑 GM Mode disabled' : '👑 GM Mode enabled!');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
debug.log('✅ GM Mode toggle initialized');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Initialize Show to GM button
|
|
88
|
+
* Broadcasts complete character data to GM via Roll20 chat
|
|
89
|
+
* The data is base64-encoded and wrapped in special markers for GM panel to detect
|
|
90
|
+
*/
|
|
91
|
+
function initShowToGM() {
|
|
92
|
+
const showToGMBtn = document.getElementById('show-to-gm-btn');
|
|
93
|
+
|
|
94
|
+
if (showToGMBtn) {
|
|
95
|
+
showToGMBtn.addEventListener('click', () => {
|
|
96
|
+
if (!characterData) {
|
|
97
|
+
showNotification('⚠️ No character data to share', 'warning');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
debug.log('👑 Preparing character data for GM share...', {
|
|
103
|
+
hasCharacterData: !!characterData,
|
|
104
|
+
characterName: characterData?.name,
|
|
105
|
+
characterKeys: characterData ? Object.keys(characterData) : []
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Create character broadcast message with ENTIRE sheet data
|
|
109
|
+
const broadcastData = {
|
|
110
|
+
type: 'OWLCLOUD_CHARACTER_BROADCAST',
|
|
111
|
+
character: characterData,
|
|
112
|
+
// Include ALL character data for complete sheet
|
|
113
|
+
fullSheet: {
|
|
114
|
+
...characterData,
|
|
115
|
+
// Ensure all sections are included
|
|
116
|
+
attributes: characterData.attributes || {},
|
|
117
|
+
skills: characterData.skills || [],
|
|
118
|
+
savingThrows: characterData.savingThrows || {},
|
|
119
|
+
actions: characterData.actions || [],
|
|
120
|
+
spells: characterData.spells || [],
|
|
121
|
+
features: characterData.features || [],
|
|
122
|
+
equipment: characterData.equipment || [],
|
|
123
|
+
inventory: characterData.inventory || {},
|
|
124
|
+
resources: characterData.resources || {},
|
|
125
|
+
spellSlots: characterData.spellSlots || {},
|
|
126
|
+
companions: characterData.companions || [],
|
|
127
|
+
conditions: characterData.conditions || [],
|
|
128
|
+
notes: characterData.notes || '',
|
|
129
|
+
background: characterData.background || '',
|
|
130
|
+
personality: characterData.personality || {},
|
|
131
|
+
proficiencies: characterData.proficiencies || [],
|
|
132
|
+
languages: characterData.languages || [],
|
|
133
|
+
// Add simplified properties for popout compatibility
|
|
134
|
+
hp: characterData.hitPoints?.current || characterData.hp || 0,
|
|
135
|
+
maxHp: characterData.hitPoints?.max || characterData.maxHp || 0,
|
|
136
|
+
ac: characterData.armorClass || characterData.ac || 10,
|
|
137
|
+
initiative: characterData.initiative || 0,
|
|
138
|
+
passivePerception: characterData.passivePerception || 10,
|
|
139
|
+
proficiency: characterData.proficiencyBonus || characterData.proficiency || 0,
|
|
140
|
+
speed: characterData.speed || '30 ft'
|
|
141
|
+
},
|
|
142
|
+
timestamp: new Date().toISOString()
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Encode the data for safe transmission (handle UTF-8 properly)
|
|
146
|
+
const jsonString = JSON.stringify(broadcastData);
|
|
147
|
+
const encodedData = btoa(unescape(encodeURIComponent(jsonString)));
|
|
148
|
+
const broadcastMessage = `👑[OWLCLOUD:CHARACTER:${encodedData}]👑`;
|
|
149
|
+
|
|
150
|
+
// Send to Roll20 chat
|
|
151
|
+
sendToRoll20({
|
|
152
|
+
action: 'postChatMessageFromPopup',
|
|
153
|
+
message: broadcastMessage
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
showNotification(`👑 ${characterData.name} shared with GM!`, 'success');
|
|
157
|
+
debug.log('👑 Character broadcast sent to GM:', characterData.name);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
debug.error('❌ Error creating character broadcast:', error);
|
|
160
|
+
debug.error('❌ Error details:', {
|
|
161
|
+
message: error.message,
|
|
162
|
+
stack: error.stack,
|
|
163
|
+
characterDataKeys: characterData ? Object.keys(characterData) : 'no data'
|
|
164
|
+
});
|
|
165
|
+
showNotification(`❌ Failed to prepare character data: ${error.message}`, 'error');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
debug.log('✅ Show to GM button initialized in settings');
|
|
170
|
+
} else {
|
|
171
|
+
debug.warn('⚠️ Show to GM button not found in settings');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ===== EXPORTS =====
|
|
176
|
+
|
|
177
|
+
// Export functions to globalThis
|
|
178
|
+
globalThis.hideGMControls = hideGMControls;
|
|
179
|
+
globalThis.initGMMode = initGMMode;
|
|
180
|
+
globalThis.initShowToGM = initShowToGM;
|
|
181
|
+
|
|
182
|
+
debug.log('✅ GM Mode module loaded');
|
|
183
|
+
|
|
184
|
+
})();
|