@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,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dice Roller Module
|
|
3
|
+
*
|
|
4
|
+
* Core dice rolling system with math evaluation, effect modifiers, and VTT integration.
|
|
5
|
+
* Handles all dice roll execution, advantage/disadvantage, and optional effects.
|
|
6
|
+
*
|
|
7
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
8
|
+
*
|
|
9
|
+
* Functions exported to globalThis:
|
|
10
|
+
* - safeMathEval(expr)
|
|
11
|
+
* - evaluateMathInFormula(formula)
|
|
12
|
+
* - applyEffectModifiers(rollName, formula)
|
|
13
|
+
* - checkOptionalEffects(rollName, formula, onApply)
|
|
14
|
+
* - showOptionalEffectPopup(effects, rollName, formula, onApply)
|
|
15
|
+
* - roll(name, formula, prerolledResult)
|
|
16
|
+
* - applyAdvantageToFormula(formula, effectNotes)
|
|
17
|
+
* - executeRoll(name, formula, effectNotes, prerolledResult)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
(function() {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// ===== CORE FUNCTIONS =====
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Safe math expression evaluator that doesn't use eval() or Function()
|
|
27
|
+
* CSP-compliant parser for basic arithmetic and Math functions
|
|
28
|
+
*/
|
|
29
|
+
function safeMathEval(expr) {
|
|
30
|
+
// Tokenize the expression
|
|
31
|
+
const tokens = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
expr = expr.replace(/\s+/g, ''); // Remove whitespace
|
|
34
|
+
|
|
35
|
+
while (i < expr.length) {
|
|
36
|
+
// Check for Math functions and max/min
|
|
37
|
+
if (expr.substr(i, 10) === 'Math.floor') {
|
|
38
|
+
tokens.push({ type: 'function', value: 'floor' });
|
|
39
|
+
i += 10;
|
|
40
|
+
} else if (expr.substr(i, 9) === 'Math.ceil') {
|
|
41
|
+
tokens.push({ type: 'function', value: 'ceil' });
|
|
42
|
+
i += 9;
|
|
43
|
+
} else if (expr.substr(i, 10) === 'Math.round') {
|
|
44
|
+
tokens.push({ type: 'function', value: 'round' });
|
|
45
|
+
i += 10;
|
|
46
|
+
} else if (expr.substr(i, 3) === 'max') {
|
|
47
|
+
tokens.push({ type: 'function', value: 'max' });
|
|
48
|
+
i += 3;
|
|
49
|
+
} else if (expr.substr(i, 3) === 'min') {
|
|
50
|
+
tokens.push({ type: 'function', value: 'min' });
|
|
51
|
+
i += 3;
|
|
52
|
+
} else if (expr[i] >= '0' && expr[i] <= '9' || expr[i] === '.') {
|
|
53
|
+
// Parse number
|
|
54
|
+
let num = '';
|
|
55
|
+
while (i < expr.length && (expr[i] >= '0' && expr[i] <= '9' || expr[i] === '.')) {
|
|
56
|
+
num += expr[i];
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
tokens.push({ type: 'number', value: parseFloat(num) });
|
|
60
|
+
} else if ('+-*/(),'.includes(expr[i])) {
|
|
61
|
+
tokens.push({ type: 'operator', value: expr[i] });
|
|
62
|
+
i++;
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`Unexpected character: ${expr[i]}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Parse and evaluate using recursive descent
|
|
69
|
+
let pos = 0;
|
|
70
|
+
|
|
71
|
+
function parseExpression() {
|
|
72
|
+
let left = parseTerm();
|
|
73
|
+
|
|
74
|
+
while (pos < tokens.length && tokens[pos].type === 'operator' && (tokens[pos].value === '+' || tokens[pos].value === '-')) {
|
|
75
|
+
const op = tokens[pos].value;
|
|
76
|
+
pos++;
|
|
77
|
+
const right = parseTerm();
|
|
78
|
+
left = op === '+' ? left + right : left - right;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return left;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseTerm() {
|
|
85
|
+
let left = parseFactor();
|
|
86
|
+
|
|
87
|
+
while (pos < tokens.length && tokens[pos].type === 'operator' && (tokens[pos].value === '*' || tokens[pos].value === '/')) {
|
|
88
|
+
const op = tokens[pos].value;
|
|
89
|
+
pos++;
|
|
90
|
+
const right = parseFactor();
|
|
91
|
+
left = op === '*' ? left * right : left / right;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return left;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseFactor() {
|
|
98
|
+
const token = tokens[pos];
|
|
99
|
+
|
|
100
|
+
// Handle numbers
|
|
101
|
+
if (token.type === 'number') {
|
|
102
|
+
pos++;
|
|
103
|
+
return token.value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle Math functions
|
|
107
|
+
if (token.type === 'function') {
|
|
108
|
+
const funcName = token.value;
|
|
109
|
+
pos++;
|
|
110
|
+
if (pos >= tokens.length || tokens[pos].value !== '(') {
|
|
111
|
+
throw new Error('Expected ( after function name');
|
|
112
|
+
}
|
|
113
|
+
pos++; // Skip (
|
|
114
|
+
|
|
115
|
+
// Handle multiple arguments for max/min functions
|
|
116
|
+
const args = [];
|
|
117
|
+
if (funcName === 'max' || funcName === 'min') {
|
|
118
|
+
// Parse comma-separated arguments
|
|
119
|
+
args.push(parseExpression());
|
|
120
|
+
while (pos < tokens.length && tokens[pos].value === ',') {
|
|
121
|
+
pos++; // Skip comma
|
|
122
|
+
args.push(parseExpression());
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Single argument for other functions
|
|
126
|
+
args.push(parseExpression());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (pos >= tokens.length || tokens[pos].value !== ')') {
|
|
130
|
+
throw new Error('Expected ) after function argument');
|
|
131
|
+
}
|
|
132
|
+
pos++; // Skip )
|
|
133
|
+
|
|
134
|
+
if (funcName === 'floor') return Math.floor(args[0]);
|
|
135
|
+
if (funcName === 'ceil') return Math.ceil(args[0]);
|
|
136
|
+
if (funcName === 'round') return Math.round(args[0]);
|
|
137
|
+
if (funcName === 'max') return Math.max(...args);
|
|
138
|
+
if (funcName === 'min') return Math.min(...args);
|
|
139
|
+
throw new Error(`Unknown function: ${funcName}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle parentheses
|
|
143
|
+
if (token.type === 'operator' && token.value === '(') {
|
|
144
|
+
pos++;
|
|
145
|
+
const result = parseExpression();
|
|
146
|
+
if (pos >= tokens.length || tokens[pos].value !== ')') {
|
|
147
|
+
throw new Error('Mismatched parentheses');
|
|
148
|
+
}
|
|
149
|
+
pos++;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle unary minus
|
|
154
|
+
if (token.type === 'operator' && token.value === '-') {
|
|
155
|
+
pos++;
|
|
156
|
+
return -parseFactor();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new Error(`Unexpected token: ${JSON.stringify(token)}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return parseExpression();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Evaluate simple mathematical expressions in formulas
|
|
167
|
+
* Converts things like "5*5" to "25" before rolling dice
|
|
168
|
+
* CSP-compliant - does not use eval() or Function() constructor
|
|
169
|
+
*/
|
|
170
|
+
function evaluateMathInFormula(formula) {
|
|
171
|
+
if (!formula || typeof formula !== 'string') {
|
|
172
|
+
return formula;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let currentFormula = formula;
|
|
176
|
+
let previousFormula = null;
|
|
177
|
+
let iterations = 0;
|
|
178
|
+
const maxIterations = 10; // Prevent infinite loops
|
|
179
|
+
|
|
180
|
+
// Keep simplifying until formula doesn't change or max iterations reached
|
|
181
|
+
while (currentFormula !== previousFormula && iterations < maxIterations) {
|
|
182
|
+
previousFormula = currentFormula;
|
|
183
|
+
iterations++;
|
|
184
|
+
|
|
185
|
+
// Replace floor() with Math.floor() for parsing
|
|
186
|
+
let processedFormula = currentFormula.replace(/floor\(/g, 'Math.floor(');
|
|
187
|
+
processedFormula = processedFormula.replace(/ceil\(/g, 'Math.ceil(');
|
|
188
|
+
processedFormula = processedFormula.replace(/round\(/g, 'Math.round(');
|
|
189
|
+
|
|
190
|
+
// Check if the formula is just a simple math expression (no dice)
|
|
191
|
+
// Pattern: numbers and operators only (e.g., "5*5", "10+5", "20/4")
|
|
192
|
+
const simpleMathPattern = /^[\d\s+\-*/().]+$/;
|
|
193
|
+
|
|
194
|
+
if (simpleMathPattern.test(processedFormula)) {
|
|
195
|
+
try {
|
|
196
|
+
const result = safeMathEval(processedFormula);
|
|
197
|
+
if (typeof result === 'number' && !isNaN(result)) {
|
|
198
|
+
debug.log(`✅ Evaluated simple math: ${currentFormula} = ${result} (iteration ${iterations})`);
|
|
199
|
+
currentFormula = String(result);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
} catch (e) {
|
|
203
|
+
debug.log(`⚠️ Could not evaluate math expression: ${currentFormula}`, e);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Handle formulas with dice notation like "(floor((9 + 1) / 6) + 1)d8" or "(3 * 1)d6"
|
|
208
|
+
// Extract math before the dice, evaluate it, then reconstruct
|
|
209
|
+
const dicePattern = /^(.+?)(d\d+.*)$/i;
|
|
210
|
+
const match = processedFormula.match(dicePattern);
|
|
211
|
+
|
|
212
|
+
if (match) {
|
|
213
|
+
const mathPart = match[1]; // e.g., "(Math.floor((9 + 1) / 6) + 1)" or "(3 * 1)"
|
|
214
|
+
const dicePart = match[2]; // e.g., "d8" or "d6"
|
|
215
|
+
|
|
216
|
+
// Check if the math part is evaluable (allows numbers, operators, parens, and Math functions)
|
|
217
|
+
const mathOnlyPattern = /^[\d\s+\-*/().\w]+$/;
|
|
218
|
+
if (mathOnlyPattern.test(mathPart)) {
|
|
219
|
+
try {
|
|
220
|
+
const result = safeMathEval(mathPart);
|
|
221
|
+
if (typeof result === 'number' && !isNaN(result)) {
|
|
222
|
+
debug.log(`✅ Evaluated dice formula math: ${mathPart} = ${result} (iteration ${iterations})`);
|
|
223
|
+
currentFormula = String(result) + dicePart;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
} catch (e) {
|
|
227
|
+
debug.log(`⚠️ Could not evaluate dice formula math: ${mathPart}`, e);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (iterations > 1) {
|
|
234
|
+
debug.log(`🔄 Formula simplified in ${iterations} iterations: "${formula}" -> "${currentFormula}"`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return currentFormula;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Apply active effect modifiers to a roll
|
|
242
|
+
* @param {string} rollName - Name of the roll (e.g., "Attack", "Perception", "Strength Save")
|
|
243
|
+
* @param {string} formula - Original formula
|
|
244
|
+
* @returns {object} - { modifiedFormula, effectNotes }
|
|
245
|
+
*/
|
|
246
|
+
function applyEffectModifiers(rollName, formula) {
|
|
247
|
+
const rollLower = rollName.toLowerCase();
|
|
248
|
+
let modifiedFormula = formula;
|
|
249
|
+
const effectNotes = [];
|
|
250
|
+
|
|
251
|
+
// Combine all active effects
|
|
252
|
+
const allEffects = [
|
|
253
|
+
...activeBuffs.map(name => ({ ...POSITIVE_EFFECTS.find(e => e.name === name), type: 'buff' })),
|
|
254
|
+
...activeConditions.map(name => ({ ...NEGATIVE_EFFECTS.find(e => e.name === name), type: 'debuff' }))
|
|
255
|
+
].filter(e => e && e.autoApply);
|
|
256
|
+
|
|
257
|
+
debug.log(`🎲 Checking effects for roll: ${rollName}`, allEffects);
|
|
258
|
+
|
|
259
|
+
for (const effect of allEffects) {
|
|
260
|
+
if (!effect.modifier) continue;
|
|
261
|
+
|
|
262
|
+
let applied = false;
|
|
263
|
+
|
|
264
|
+
// Check for attack roll modifiers
|
|
265
|
+
if (rollLower.includes('attack') && effect.modifier.attack) {
|
|
266
|
+
const mod = effect.modifier.attack;
|
|
267
|
+
if (mod === 'advantage') {
|
|
268
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: Advantage]`);
|
|
269
|
+
applied = true;
|
|
270
|
+
} else if (mod === 'disadvantage') {
|
|
271
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: Disadvantage]`);
|
|
272
|
+
applied = true;
|
|
273
|
+
} else {
|
|
274
|
+
modifiedFormula += ` + ${mod}`;
|
|
275
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: ${mod}]`);
|
|
276
|
+
applied = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for saving throw modifiers
|
|
281
|
+
if (rollLower.includes('save') && (effect.modifier.save || effect.modifier.strSave || effect.modifier.dexSave)) {
|
|
282
|
+
const mod = effect.modifier.save ||
|
|
283
|
+
(rollLower.includes('strength') && effect.modifier.strSave) ||
|
|
284
|
+
(rollLower.includes('dexterity') && effect.modifier.dexSave);
|
|
285
|
+
|
|
286
|
+
if (mod === 'advantage') {
|
|
287
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: Advantage]`);
|
|
288
|
+
applied = true;
|
|
289
|
+
} else if (mod === 'disadvantage') {
|
|
290
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: Disadvantage]`);
|
|
291
|
+
applied = true;
|
|
292
|
+
} else if (mod === 'fail') {
|
|
293
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: Auto-fail]`);
|
|
294
|
+
applied = true;
|
|
295
|
+
} else if (mod) {
|
|
296
|
+
modifiedFormula += ` + ${mod}`;
|
|
297
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: ${mod}]`);
|
|
298
|
+
applied = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check for skill check modifiers
|
|
303
|
+
if ((rollLower.includes('check') || rollLower.includes('perception') ||
|
|
304
|
+
rollLower.includes('stealth') || rollLower.includes('investigation') ||
|
|
305
|
+
rollLower.includes('insight') || rollLower.includes('persuasion') ||
|
|
306
|
+
rollLower.includes('deception') || rollLower.includes('intimidation') ||
|
|
307
|
+
rollLower.includes('athletics') || rollLower.includes('acrobatics')) &&
|
|
308
|
+
effect.modifier.skill) {
|
|
309
|
+
const mod = effect.modifier.skill;
|
|
310
|
+
if (mod === 'advantage') {
|
|
311
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: Advantage]`);
|
|
312
|
+
applied = true;
|
|
313
|
+
} else if (mod === 'disadvantage') {
|
|
314
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: Disadvantage]`);
|
|
315
|
+
applied = true;
|
|
316
|
+
} else {
|
|
317
|
+
modifiedFormula += ` + ${mod}`;
|
|
318
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: ${mod}]`);
|
|
319
|
+
applied = true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check for damage modifiers
|
|
324
|
+
if (rollLower.includes('damage') && effect.modifier.damage) {
|
|
325
|
+
modifiedFormula += ` + ${effect.modifier.damage}`;
|
|
326
|
+
effectNotes.push(`[${effect.icon} ${effect.name}: +${effect.modifier.damage}]`);
|
|
327
|
+
applied = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (applied) {
|
|
331
|
+
debug.log(`✅ Applied ${effect.name} (${effect.type}) to ${rollName}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { modifiedFormula, effectNotes };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check for optional effects that could apply to a roll and show popup if found
|
|
340
|
+
* @param {string} rollName - Name of the roll
|
|
341
|
+
* @param {string} formula - Original formula
|
|
342
|
+
* @param {function} onApply - Callback function to apply the effect
|
|
343
|
+
*/
|
|
344
|
+
function checkOptionalEffects(rollName, formula, onApply) {
|
|
345
|
+
const rollLower = rollName.toLowerCase();
|
|
346
|
+
|
|
347
|
+
// Combine all active effects that are NOT autoApply
|
|
348
|
+
const optionalEffects = [
|
|
349
|
+
...activeBuffs.map(name => ({ ...POSITIVE_EFFECTS.find(e => e.name === name), type: 'buff' })),
|
|
350
|
+
...activeConditions.map(name => ({ ...NEGATIVE_EFFECTS.find(e => e.name === name), type: 'debuff' }))
|
|
351
|
+
].filter(e => e && !e.autoApply && e.modifier);
|
|
352
|
+
|
|
353
|
+
if (optionalEffects.length === 0) return;
|
|
354
|
+
|
|
355
|
+
debug.log(`🎲 Checking optional effects for roll: ${rollName}`, optionalEffects);
|
|
356
|
+
|
|
357
|
+
const applicableEffects = [];
|
|
358
|
+
|
|
359
|
+
for (const effect of optionalEffects) {
|
|
360
|
+
let applicable = false;
|
|
361
|
+
|
|
362
|
+
// Check for skill check modifiers (for Guidance) - ALL skills
|
|
363
|
+
const isSkillCheck = rollLower.includes('check') ||
|
|
364
|
+
rollLower.includes('acrobatics') || rollLower.includes('animal') ||
|
|
365
|
+
rollLower.includes('arcana') || rollLower.includes('athletics') ||
|
|
366
|
+
rollLower.includes('deception') || rollLower.includes('history') ||
|
|
367
|
+
rollLower.includes('insight') || rollLower.includes('intimidation') ||
|
|
368
|
+
rollLower.includes('investigation') || rollLower.includes('medicine') ||
|
|
369
|
+
rollLower.includes('nature') || rollLower.includes('perception') ||
|
|
370
|
+
rollLower.includes('performance') || rollLower.includes('persuasion') ||
|
|
371
|
+
rollLower.includes('religion') || rollLower.includes('sleight') ||
|
|
372
|
+
rollLower.includes('stealth') || rollLower.includes('survival');
|
|
373
|
+
|
|
374
|
+
if (isSkillCheck && effect.modifier.skill) {
|
|
375
|
+
applicable = true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check for attack roll modifiers
|
|
379
|
+
if (rollLower.includes('attack') && effect.modifier.attack) {
|
|
380
|
+
applicable = true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Check for saving throw modifiers
|
|
384
|
+
if (rollLower.includes('save') && effect.modifier.save) {
|
|
385
|
+
applicable = true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Special handling for Bardic Inspiration - applies to checks, attacks, and saves
|
|
389
|
+
if (effect.name.startsWith('Bardic Inspiration')) {
|
|
390
|
+
if (rollLower.includes('check') || rollLower.includes('perception') ||
|
|
391
|
+
rollLower.includes('stealth') || rollLower.includes('investigation') ||
|
|
392
|
+
rollLower.includes('insight') || rollLower.includes('persuasion') ||
|
|
393
|
+
rollLower.includes('deception') || rollLower.includes('intimidation') ||
|
|
394
|
+
rollLower.includes('athletics') || rollLower.includes('acrobatics') ||
|
|
395
|
+
rollLower.includes('attack') || rollLower.includes('save')) {
|
|
396
|
+
applicable = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (applicable) {
|
|
401
|
+
applicableEffects.push(effect);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (applicableEffects.length > 0) {
|
|
406
|
+
showOptionalEffectPopup(applicableEffects, rollName, formula, onApply);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Show popup for optional effects
|
|
412
|
+
*/
|
|
413
|
+
function showOptionalEffectPopup(effects, rollName, formula, onApply) {
|
|
414
|
+
debug.log('🎯 Showing optional effect popup for:', effects);
|
|
415
|
+
|
|
416
|
+
if (!document.body) {
|
|
417
|
+
debug.error('❌ document.body not available for optional effect popup');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Get theme-aware colors
|
|
422
|
+
const colors = getPopupThemeColors();
|
|
423
|
+
|
|
424
|
+
// Create modal overlay
|
|
425
|
+
const popupOverlay = document.createElement('div');
|
|
426
|
+
popupOverlay.style.cssText = `
|
|
427
|
+
position: fixed;
|
|
428
|
+
top: 0;
|
|
429
|
+
left: 0;
|
|
430
|
+
width: 100%;
|
|
431
|
+
height: 100%;
|
|
432
|
+
background: rgba(0, 0, 0, 0.6);
|
|
433
|
+
backdrop-filter: blur(2px);
|
|
434
|
+
z-index: 10000;
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: center;
|
|
438
|
+
`;
|
|
439
|
+
|
|
440
|
+
// Create popup content
|
|
441
|
+
const popupContent = document.createElement('div');
|
|
442
|
+
popupContent.style.cssText = `
|
|
443
|
+
background: ${colors.background};
|
|
444
|
+
border-radius: 12px;
|
|
445
|
+
padding: 24px;
|
|
446
|
+
max-width: 400px;
|
|
447
|
+
width: 90%;
|
|
448
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
449
|
+
text-align: center;
|
|
450
|
+
border: 2px solid var(--accent-primary);
|
|
451
|
+
`;
|
|
452
|
+
|
|
453
|
+
// Build effects list
|
|
454
|
+
const effectsList = effects.map(effect => `
|
|
455
|
+
<div style="margin: 12px 0; padding: 12px; background: ${effect.color}20; border: 2px solid ${effect.color}; border-radius: 8px; cursor: pointer; transition: all 0.2s;"
|
|
456
|
+
onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,0,0,0.15)'"
|
|
457
|
+
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'"
|
|
458
|
+
data-effect="${effect.name}" data-type="${effect.type}">
|
|
459
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
460
|
+
<span style="font-size: 1.2em;">${effect.icon}</span>
|
|
461
|
+
<div style="flex: 1; text-align: left;">
|
|
462
|
+
<div style="font-weight: bold; color: var(--text-primary);">${effect.name}</div>
|
|
463
|
+
<div style="font-size: 0.85em; color: var(--text-secondary); margin-top: 2px;">${effect.description}</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
`).join('');
|
|
468
|
+
|
|
469
|
+
popupContent.innerHTML = `
|
|
470
|
+
<div style="font-size: 24px; margin-bottom: 16px;">🎯</div>
|
|
471
|
+
<h2 style="margin: 0 0 8px 0; color: ${colors.heading};">Optional Effect Available!</h2>
|
|
472
|
+
<p style="margin: 0 0 16px 0; color: ${colors.text};">
|
|
473
|
+
You can apply an optional effect to your <strong>${rollName}</strong> roll:
|
|
474
|
+
</p>
|
|
475
|
+
${effectsList}
|
|
476
|
+
<div style="margin-top: 20px; display: flex; gap: 10px; justify-content: center;">
|
|
477
|
+
<button id="skip-effect" style="background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: 600;">
|
|
478
|
+
Skip
|
|
479
|
+
</button>
|
|
480
|
+
</div>
|
|
481
|
+
`;
|
|
482
|
+
|
|
483
|
+
popupOverlay.appendChild(popupContent);
|
|
484
|
+
document.body.appendChild(popupOverlay);
|
|
485
|
+
|
|
486
|
+
// Add click handlers for effect options
|
|
487
|
+
popupContent.querySelectorAll('[data-effect]').forEach(effectDiv => {
|
|
488
|
+
effectDiv.addEventListener('click', () => {
|
|
489
|
+
const effectName = effectDiv.dataset.effect;
|
|
490
|
+
const effectType = effectDiv.dataset.type;
|
|
491
|
+
const effect = effects.find(e => e.name === effectName);
|
|
492
|
+
|
|
493
|
+
if (effect && onApply) {
|
|
494
|
+
onApply(effect);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
document.body.removeChild(popupOverlay);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Skip button
|
|
502
|
+
document.getElementById('skip-effect').addEventListener('click', () => {
|
|
503
|
+
document.body.removeChild(popupOverlay);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Close on overlay click
|
|
507
|
+
popupOverlay.addEventListener('click', (e) => {
|
|
508
|
+
if (e.target === popupOverlay) {
|
|
509
|
+
document.body.removeChild(popupOverlay);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
debug.log('🎯 Optional effect popup displayed');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function roll(name, formula, prerolledResult = null) {
|
|
517
|
+
debug.log('🎲 Rolling:', name, formula, prerolledResult ? `(prerolled: ${prerolledResult})` : '');
|
|
518
|
+
|
|
519
|
+
// Resolve any variables in the formula
|
|
520
|
+
let resolvedFormula = resolveVariablesInFormula(formula);
|
|
521
|
+
|
|
522
|
+
// Check if there are any optional effects that could apply to this roll
|
|
523
|
+
const rollLower = name.toLowerCase();
|
|
524
|
+
const optionalEffects = [
|
|
525
|
+
...activeBuffs.map(name => ({ ...POSITIVE_EFFECTS.find(e => e.name === name), type: 'buff' })),
|
|
526
|
+
...activeConditions.map(name => ({ ...NEGATIVE_EFFECTS.find(e => e.name === name), type: 'debuff' }))
|
|
527
|
+
].filter(e => e && !e.autoApply && e.modifier);
|
|
528
|
+
|
|
529
|
+
const hasApplicableOptionalEffects = optionalEffects.some(effect => {
|
|
530
|
+
// Check if this is a skill check (any skill name or the word "check")
|
|
531
|
+
const isSkillCheck = rollLower.includes('check') ||
|
|
532
|
+
rollLower.includes('acrobatics') || rollLower.includes('animal') ||
|
|
533
|
+
rollLower.includes('arcana') || rollLower.includes('athletics') ||
|
|
534
|
+
rollLower.includes('deception') || rollLower.includes('history') ||
|
|
535
|
+
rollLower.includes('insight') || rollLower.includes('intimidation') ||
|
|
536
|
+
rollLower.includes('investigation') || rollLower.includes('medicine') ||
|
|
537
|
+
rollLower.includes('nature') || rollLower.includes('perception') ||
|
|
538
|
+
rollLower.includes('performance') || rollLower.includes('persuasion') ||
|
|
539
|
+
rollLower.includes('religion') || rollLower.includes('sleight') ||
|
|
540
|
+
rollLower.includes('stealth') || rollLower.includes('survival');
|
|
541
|
+
|
|
542
|
+
return (isSkillCheck && effect.modifier.skill) ||
|
|
543
|
+
(rollLower.includes('attack') && effect.modifier.attack);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// If there are applicable optional effects, show popup and wait for user choice
|
|
547
|
+
if (hasApplicableOptionalEffects) {
|
|
548
|
+
debug.log('🎯 Found applicable optional effects, showing popup...');
|
|
549
|
+
checkOptionalEffects(name, resolvedFormula, (chosenEffect) => {
|
|
550
|
+
// Apply the chosen effect and then roll
|
|
551
|
+
const { modifiedFormula, effectNotes } = applyEffectModifiers(name, resolvedFormula);
|
|
552
|
+
let finalFormula = modifiedFormula;
|
|
553
|
+
|
|
554
|
+
// Check if this is a skill/ability check (same logic as popup detection)
|
|
555
|
+
const isSkillOrAbilityCheck = rollLower.includes('check') ||
|
|
556
|
+
rollLower.includes('acrobatics') || rollLower.includes('animal') ||
|
|
557
|
+
rollLower.includes('arcana') || rollLower.includes('athletics') ||
|
|
558
|
+
rollLower.includes('deception') || rollLower.includes('history') ||
|
|
559
|
+
rollLower.includes('insight') || rollLower.includes('intimidation') ||
|
|
560
|
+
rollLower.includes('investigation') || rollLower.includes('medicine') ||
|
|
561
|
+
rollLower.includes('nature') || rollLower.includes('perception') ||
|
|
562
|
+
rollLower.includes('performance') || rollLower.includes('persuasion') ||
|
|
563
|
+
rollLower.includes('religion') || rollLower.includes('sleight') ||
|
|
564
|
+
rollLower.includes('stealth') || rollLower.includes('survival') ||
|
|
565
|
+
rollLower.includes('strength') || rollLower.includes('dexterity') ||
|
|
566
|
+
rollLower.includes('constitution') || rollLower.includes('intelligence') ||
|
|
567
|
+
rollLower.includes('wisdom') || rollLower.includes('charisma');
|
|
568
|
+
|
|
569
|
+
debug.log(`🎯 Applying chosen effect: ${chosenEffect.name}`, {
|
|
570
|
+
modifier: chosenEffect.modifier,
|
|
571
|
+
rollLower: rollLower,
|
|
572
|
+
hasSkillMod: !!chosenEffect.modifier?.skill,
|
|
573
|
+
isSkillOrAbilityCheck: isSkillOrAbilityCheck,
|
|
574
|
+
formulaBefore: finalFormula
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Add the chosen effect's modifier
|
|
578
|
+
if (chosenEffect.modifier?.skill && isSkillOrAbilityCheck) {
|
|
579
|
+
finalFormula += ` + ${chosenEffect.modifier.skill}`;
|
|
580
|
+
effectNotes.push(`[${chosenEffect.icon} ${chosenEffect.name}: ${chosenEffect.modifier.skill}]`);
|
|
581
|
+
debug.log(`✅ Added skill modifier: ${chosenEffect.modifier.skill}, formula now: ${finalFormula}`);
|
|
582
|
+
} else if (chosenEffect.modifier?.attack && rollLower.includes('attack')) {
|
|
583
|
+
finalFormula += ` + ${chosenEffect.modifier.attack}`;
|
|
584
|
+
effectNotes.push(`[${chosenEffect.icon} ${chosenEffect.name}: ${chosenEffect.modifier.attack}]`);
|
|
585
|
+
debug.log(`✅ Added attack modifier: ${chosenEffect.modifier.attack}, formula now: ${finalFormula}`);
|
|
586
|
+
} else {
|
|
587
|
+
debug.log(`⚠️ No modifier applied - skill: ${chosenEffect.modifier?.skill}, check: ${rollLower.includes('check')}, attack: ${chosenEffect.modifier?.attack}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Remove the chosen effect from active effects since it's been used
|
|
591
|
+
if (chosenEffect.type === 'buff') {
|
|
592
|
+
activeBuffs = activeBuffs.filter(e => e !== chosenEffect.name);
|
|
593
|
+
debug.log(`🗑️ Removed buff: ${chosenEffect.name}`);
|
|
594
|
+
} else if (chosenEffect.type === 'debuff') {
|
|
595
|
+
activeConditions = activeConditions.filter(e => e !== chosenEffect.name);
|
|
596
|
+
debug.log(`🗑️ Removed debuff: ${chosenEffect.name}`);
|
|
597
|
+
}
|
|
598
|
+
updateEffectsDisplay();
|
|
599
|
+
|
|
600
|
+
// Apply advantage/disadvantage state
|
|
601
|
+
const formulaWithAdvantage = applyAdvantageToFormula(finalFormula, effectNotes);
|
|
602
|
+
|
|
603
|
+
// Proceed with the roll
|
|
604
|
+
executeRoll(name, formulaWithAdvantage, effectNotes, prerolledResult);
|
|
605
|
+
});
|
|
606
|
+
// Return early - don't execute the roll yet, wait for popup response
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// No optional effects, proceed with normal roll
|
|
611
|
+
const { modifiedFormula, effectNotes } = applyEffectModifiers(name, resolvedFormula);
|
|
612
|
+
|
|
613
|
+
// Apply advantage/disadvantage state
|
|
614
|
+
const formulaWithAdvantage = applyAdvantageToFormula(modifiedFormula, effectNotes);
|
|
615
|
+
|
|
616
|
+
executeRoll(name, formulaWithAdvantage, effectNotes, prerolledResult);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Apply advantage/disadvantage state to dice formula
|
|
621
|
+
*/
|
|
622
|
+
function applyAdvantageToFormula(formula, effectNotes) {
|
|
623
|
+
if (advantageState === 'normal') {
|
|
624
|
+
return formula;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check if this is a d20 roll
|
|
628
|
+
if (!formula.includes('1d20') && !formula.includes('d20')) {
|
|
629
|
+
return formula; // Not a d20 roll, don't modify
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
let modifiedFormula = formula;
|
|
633
|
+
|
|
634
|
+
if (advantageState === 'advantage') {
|
|
635
|
+
// Replace 1d20 with 2d20kh1 (keep highest)
|
|
636
|
+
modifiedFormula = modifiedFormula.replace(/1d20/g, '2d20kh1');
|
|
637
|
+
modifiedFormula = modifiedFormula.replace(/(?<!\d)d20/g, '2d20kh1');
|
|
638
|
+
effectNotes.push('[⚡ Advantage]');
|
|
639
|
+
debug.log('⚡ Applied advantage to roll');
|
|
640
|
+
} else if (advantageState === 'disadvantage') {
|
|
641
|
+
// Replace 1d20 with 2d20kl1 (keep lowest)
|
|
642
|
+
modifiedFormula = modifiedFormula.replace(/1d20/g, '2d20kl1');
|
|
643
|
+
modifiedFormula = modifiedFormula.replace(/(?<!\d)d20/g, '2d20kl1');
|
|
644
|
+
effectNotes.push('[⚠️ Disadvantage]');
|
|
645
|
+
debug.log('⚠️ Applied disadvantage to roll');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Reset advantage state after use
|
|
649
|
+
setTimeout(() => setAdvantageState('normal'), 100);
|
|
650
|
+
|
|
651
|
+
return modifiedFormula;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Execute the roll after optional effects have been handled
|
|
656
|
+
*/
|
|
657
|
+
function executeRoll(name, formula, effectNotes, prerolledResult = null) {
|
|
658
|
+
// Format: "CharacterName rolls Initiative" (Roll20 adds color indicator)
|
|
659
|
+
let rollName = `${characterData.name} rolls ${name}`;
|
|
660
|
+
|
|
661
|
+
// Add effect notes to roll name if any
|
|
662
|
+
if (effectNotes.length > 0) {
|
|
663
|
+
rollName += ` ${effectNotes.join(' ')}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Save this as the character's last roll (for heroic inspiration reroll)
|
|
667
|
+
if (characterData) {
|
|
668
|
+
characterData.lastRoll = {
|
|
669
|
+
name: name,
|
|
670
|
+
formula: formula,
|
|
671
|
+
effectNotes: effectNotes
|
|
672
|
+
};
|
|
673
|
+
saveCharacterData();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// If we have a prerolled result (e.g., from death saves), include it
|
|
677
|
+
const messageData = {
|
|
678
|
+
action: 'rollFromPopout',
|
|
679
|
+
name: rollName,
|
|
680
|
+
formula: formula,
|
|
681
|
+
color: characterData.notificationColor,
|
|
682
|
+
characterName: characterData.name
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
if (prerolledResult !== null) {
|
|
686
|
+
messageData.prerolledResult = prerolledResult;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Send roll to Roll20 content script
|
|
690
|
+
if (typeof sendToRoll20 === 'function') {
|
|
691
|
+
sendToRoll20(messageData);
|
|
692
|
+
debug.log('🎲 Roll sent to Roll20:', messageData);
|
|
693
|
+
} else {
|
|
694
|
+
debug.warn('⚠️ sendToRoll20 not available, trying window.opener directly');
|
|
695
|
+
if (window.opener && !window.opener.closed) {
|
|
696
|
+
window.opener.postMessage(messageData, '*');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// TODO: Add Owlbear Rodeo integration for dice rolls
|
|
701
|
+
showNotification(`🎲 Rolling ${name}...`);
|
|
702
|
+
debug.log('✅ Roll executed');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ===== EXPORTS =====
|
|
706
|
+
|
|
707
|
+
// Export functions to globalThis
|
|
708
|
+
globalThis.safeMathEval = safeMathEval;
|
|
709
|
+
globalThis.evaluateMathInFormula = evaluateMathInFormula;
|
|
710
|
+
globalThis.applyEffectModifiers = applyEffectModifiers;
|
|
711
|
+
globalThis.checkOptionalEffects = checkOptionalEffects;
|
|
712
|
+
globalThis.showOptionalEffectPopup = showOptionalEffectPopup;
|
|
713
|
+
globalThis.roll = roll;
|
|
714
|
+
globalThis.applyAdvantageToFormula = applyAdvantageToFormula;
|
|
715
|
+
globalThis.executeRoll = executeRoll;
|
|
716
|
+
|
|
717
|
+
debug.log('✅ Dice Roller module loaded');
|
|
718
|
+
|
|
719
|
+
})();
|