@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,814 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character Traits Module
|
|
3
|
+
*
|
|
4
|
+
* Handles trait objects, initialization, checking, and resource management.
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to globalThis.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - initRacialTraits()
|
|
9
|
+
* - initFeatTraits()
|
|
10
|
+
* - initClassFeatures()
|
|
11
|
+
* - checkRacialTraits(rollResult, rollType, rollName)
|
|
12
|
+
* - checkFeatTraits(rollResult, rollType, rollName)
|
|
13
|
+
* - getBardicInspirationResource()
|
|
14
|
+
* - useBardicInspiration()
|
|
15
|
+
* - updateLuckyButtonText()
|
|
16
|
+
*
|
|
17
|
+
* State variables exported to globalThis:
|
|
18
|
+
* - activeRacialTraits
|
|
19
|
+
* - activeFeatTraits
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
(function() {
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
// ===== STATE =====
|
|
26
|
+
|
|
27
|
+
// Track active racial traits for the character
|
|
28
|
+
let activeRacialTraits = [];
|
|
29
|
+
|
|
30
|
+
// Track active feat and class feature traits
|
|
31
|
+
let activeFeatTraits = [];
|
|
32
|
+
|
|
33
|
+
// ===== TRAIT OBJECTS =====
|
|
34
|
+
|
|
35
|
+
// Halfling Luck Racial Trait
|
|
36
|
+
const HalflingLuck = {
|
|
37
|
+
name: 'Halfling Luck',
|
|
38
|
+
description: 'When you roll a 1 on an attack roll, ability check, or saving throw, you can reroll the die and must use the new roll.',
|
|
39
|
+
|
|
40
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
41
|
+
debug.log(`🧬 Halfling Luck onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
42
|
+
debug.log(`🧬 Halfling Luck DEBUG - rollType exists: ${!!rollType}, includes d20: ${rollType && rollType.includes('d20')}, rollResult === 1: ${parseInt(rollResult) === 1}`);
|
|
43
|
+
|
|
44
|
+
// Convert rollResult to number for comparison
|
|
45
|
+
const numericRollResult = parseInt(rollResult);
|
|
46
|
+
|
|
47
|
+
// Check if it's a d20 roll and the result is 1
|
|
48
|
+
if (rollType && rollType.includes('d20') && numericRollResult === 1) {
|
|
49
|
+
debug.log(`🧬 Halfling Luck: TRIGGERED! Roll was ${numericRollResult}`);
|
|
50
|
+
|
|
51
|
+
// Show the popup with error handling
|
|
52
|
+
try {
|
|
53
|
+
showHalflingLuckPopup({
|
|
54
|
+
rollResult: numericRollResult,
|
|
55
|
+
baseRoll: numericRollResult,
|
|
56
|
+
rollType: rollType,
|
|
57
|
+
rollName: rollName
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
debug.error('❌ Error showing Halfling Luck popup:', error);
|
|
61
|
+
// Fallback notification
|
|
62
|
+
showNotification('🍀 Halfling Luck triggered! Check console for details.', 'info');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true; // Trait triggered
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
debug.log(`🧬 Halfling Luck: No trigger - Roll: ${numericRollResult}, Type: ${rollType}`);
|
|
69
|
+
return false; // No trigger
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Lucky Feat Trait
|
|
74
|
+
const LuckyFeat = {
|
|
75
|
+
name: 'Lucky',
|
|
76
|
+
description: 'You have 3 luck points. When you make an attack roll, ability check, or saving throw, you can spend one luck point to roll an additional d20. You can then choose which of the d20 rolls to use.',
|
|
77
|
+
|
|
78
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
79
|
+
debug.log(`🎖️ Lucky feat onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
80
|
+
|
|
81
|
+
// Convert rollResult to number for comparison
|
|
82
|
+
const numericRollResult = parseInt(rollResult);
|
|
83
|
+
|
|
84
|
+
// Check if it's a d20 roll (attack, ability check, or saving throw)
|
|
85
|
+
if (rollType && rollType.includes('d20')) {
|
|
86
|
+
debug.log(`🎖️ Lucky: Checking if we should offer reroll for ${numericRollResult}`);
|
|
87
|
+
|
|
88
|
+
// Check if character has luck points available
|
|
89
|
+
const luckyResource = getLuckyResource();
|
|
90
|
+
if (!luckyResource || luckyResource.current <= 0) {
|
|
91
|
+
debug.log(`🎖️ Lucky: No luck points available (${luckyResource?.current || 0})`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
debug.log(`🎖️ Lucky: Has ${luckyResource.current} luck points available`);
|
|
96
|
+
|
|
97
|
+
// For Lucky feat, we offer reroll on any roll (not just 1s)
|
|
98
|
+
// But we should prioritize low rolls
|
|
99
|
+
if (numericRollResult <= 10) { // Offer reroll on rolls of 10 or less
|
|
100
|
+
debug.log(`🎖️ Lucky: TRIGGERED! Offering reroll for roll ${numericRollResult}`);
|
|
101
|
+
|
|
102
|
+
// Show the Lucky popup with error handling
|
|
103
|
+
try {
|
|
104
|
+
showLuckyPopup({
|
|
105
|
+
rollResult: numericRollResult,
|
|
106
|
+
baseRoll: numericRollResult,
|
|
107
|
+
rollType: rollType,
|
|
108
|
+
rollName: rollName,
|
|
109
|
+
luckPointsRemaining: luckyResource.current
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
debug.error('❌ Error showing Lucky popup:', error);
|
|
113
|
+
// Fallback notification
|
|
114
|
+
showNotification('🎖️ Lucky triggered! Check console for details.', 'info');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return true; // Trait triggered
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
debug.log(`🎖️ Lucky: No trigger - Roll: ${numericRollResult}, Type: ${rollType}`);
|
|
122
|
+
return false; // No trigger
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Elven Accuracy
|
|
127
|
+
const ElvenAccuracy = {
|
|
128
|
+
name: 'Elven Accuracy',
|
|
129
|
+
description: 'Whenever you have advantage on an attack roll using Dexterity, Intelligence, Wisdom, or Charisma, you can reroll one of the dice once.',
|
|
130
|
+
|
|
131
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
132
|
+
debug.log(`🧝 Elven Accuracy onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
133
|
+
|
|
134
|
+
// Check if it's an attack roll with advantage using DEX/INT/WIS/CHA
|
|
135
|
+
// The rollType should contain "advantage" and the roll should be an attack
|
|
136
|
+
if (rollType && rollType.includes('advantage') && rollType.includes('attack')) {
|
|
137
|
+
debug.log(`🧝 Elven Accuracy: TRIGGERED! Offering to reroll lower die`);
|
|
138
|
+
|
|
139
|
+
// Show popup asking if they want to reroll
|
|
140
|
+
showElvenAccuracyPopup({
|
|
141
|
+
rollName: rollName,
|
|
142
|
+
rollType: rollType,
|
|
143
|
+
rollResult: rollResult
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Dwarven Resilience
|
|
154
|
+
const DwarvenResilience = {
|
|
155
|
+
name: 'Dwarven Resilience',
|
|
156
|
+
description: 'You have advantage on saving throws against poison.',
|
|
157
|
+
|
|
158
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
159
|
+
debug.log(`⛏️ Dwarven Resilience onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
160
|
+
|
|
161
|
+
// Check if it's a poison save
|
|
162
|
+
const lowerRollName = rollName.toLowerCase();
|
|
163
|
+
if (rollType && rollType.includes('save') && lowerRollName.includes('poison')) {
|
|
164
|
+
debug.log(`⛏️ Dwarven Resilience: TRIGGERED! Auto-applying advantage`);
|
|
165
|
+
showNotification('⛏️ Dwarven Resilience: Advantage on poison saves!', 'success');
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Gnome Cunning
|
|
174
|
+
const GnomeCunning = {
|
|
175
|
+
name: 'Gnome Cunning',
|
|
176
|
+
description: 'You have advantage on all Intelligence, Wisdom, and Charisma saving throws against magic.',
|
|
177
|
+
|
|
178
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
179
|
+
debug.log(`🎩 Gnome Cunning onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
180
|
+
|
|
181
|
+
// Check if it's an INT/WIS/CHA save against magic
|
|
182
|
+
const lowerRollName = rollName.toLowerCase();
|
|
183
|
+
const isMentalSave = lowerRollName.includes('intelligence') ||
|
|
184
|
+
lowerRollName.includes('wisdom') ||
|
|
185
|
+
lowerRollName.includes('charisma') ||
|
|
186
|
+
lowerRollName.includes('int save') ||
|
|
187
|
+
lowerRollName.includes('wis save') ||
|
|
188
|
+
lowerRollName.includes('cha save');
|
|
189
|
+
|
|
190
|
+
const isMagic = lowerRollName.includes('spell') ||
|
|
191
|
+
lowerRollName.includes('magic') ||
|
|
192
|
+
lowerRollName.includes('charm') ||
|
|
193
|
+
lowerRollName.includes('illusion');
|
|
194
|
+
|
|
195
|
+
if (rollType && rollType.includes('save') && isMentalSave && isMagic) {
|
|
196
|
+
debug.log(`🎩 Gnome Cunning: TRIGGERED! Auto-applying advantage`);
|
|
197
|
+
showNotification('🎩 Gnome Cunning: Advantage on mental saves vs magic!', 'success');
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Reliable Talent (Rogue 11+)
|
|
206
|
+
const ReliableTalent = {
|
|
207
|
+
name: 'Reliable Talent',
|
|
208
|
+
description: 'Whenever you make an ability check that lets you add your proficiency bonus, you treat a d20 roll of 9 or lower as a 10.',
|
|
209
|
+
|
|
210
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
211
|
+
debug.log(`🎯 Reliable Talent onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
212
|
+
|
|
213
|
+
const numericRollResult = parseInt(rollResult);
|
|
214
|
+
|
|
215
|
+
// Check if it's a skill check (proficient skills would be marked somehow)
|
|
216
|
+
if (rollType && rollType.includes('skill') && numericRollResult < 10) {
|
|
217
|
+
debug.log(`🎯 Reliable Talent: TRIGGERED! Minimum roll is 10`);
|
|
218
|
+
showNotification(`🎯 Reliable Talent: ${numericRollResult} becomes 10!`, 'success');
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Jack of All Trades (Bard)
|
|
227
|
+
const JackOfAllTrades = {
|
|
228
|
+
name: 'Jack of All Trades',
|
|
229
|
+
description: 'You can add half your proficiency bonus (rounded down) to any ability check you make that doesn\'t already include your proficiency bonus.',
|
|
230
|
+
|
|
231
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
232
|
+
debug.log(`🎵 Jack of All Trades onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
233
|
+
|
|
234
|
+
// This would need to check if the skill is non-proficient
|
|
235
|
+
// For now, we'll show a reminder
|
|
236
|
+
if (rollType && rollType.includes('skill')) {
|
|
237
|
+
const profBonus = characterData.proficiencyBonus || 2;
|
|
238
|
+
const halfProf = Math.floor(profBonus / 2);
|
|
239
|
+
debug.log(`🎵 Jack of All Trades: Reminder to add +${halfProf} if non-proficient`);
|
|
240
|
+
showNotification(`🎵 Jack: Add +${halfProf} if non-proficient`, 'info');
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Rage Damage Bonus (Barbarian)
|
|
249
|
+
const RageDamageBonus = {
|
|
250
|
+
name: 'Rage',
|
|
251
|
+
description: 'While raging, you gain bonus damage on melee weapon attacks using Strength.',
|
|
252
|
+
|
|
253
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
254
|
+
debug.log(`😡 Rage Damage onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
255
|
+
|
|
256
|
+
// Check if character is raging (would need rage tracking)
|
|
257
|
+
const isRaging = characterData.conditions && characterData.conditions.some(c =>
|
|
258
|
+
c.toLowerCase().includes('rage') || c.toLowerCase().includes('raging')
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (isRaging && rollType && rollType.includes('attack')) {
|
|
262
|
+
const level = characterData.level || 1;
|
|
263
|
+
const rageDamage = level < 9 ? 2 : level < 16 ? 3 : 4;
|
|
264
|
+
debug.log(`😡 Rage Damage: TRIGGERED! Adding +${rageDamage} damage`);
|
|
265
|
+
showNotification(`😡 Rage: Add +${rageDamage} damage!`, 'success');
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Brutal Critical (Barbarian)
|
|
274
|
+
const BrutalCritical = {
|
|
275
|
+
name: 'Brutal Critical',
|
|
276
|
+
description: 'You can roll one additional weapon damage die when determining the extra damage for a critical hit with a melee attack.',
|
|
277
|
+
|
|
278
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
279
|
+
debug.log(`💥 Brutal Critical onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
280
|
+
|
|
281
|
+
const numericRollResult = parseInt(rollResult);
|
|
282
|
+
|
|
283
|
+
// Check for natural 20 on melee attack
|
|
284
|
+
if (rollType && rollType.includes('attack') && numericRollResult === 20) {
|
|
285
|
+
const level = characterData.level || 1;
|
|
286
|
+
const extraDice = level < 13 ? 1 : level < 17 ? 2 : 3;
|
|
287
|
+
debug.log(`💥 Brutal Critical: TRIGGERED! Roll ${extraDice} extra weapon die/dice`);
|
|
288
|
+
showNotification(`💥 Brutal Critical: Roll ${extraDice} extra weapon die!`, 'success');
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Portent Dice (Divination Wizard)
|
|
297
|
+
const PortentDice = {
|
|
298
|
+
name: 'Portent',
|
|
299
|
+
description: 'Roll two d20s and record the numbers. You can replace any attack roll, saving throw, or ability check made by you or a creature you can see with one of these rolls.',
|
|
300
|
+
|
|
301
|
+
portentRolls: [], // Store portent rolls for the day
|
|
302
|
+
|
|
303
|
+
rollPortentDice: function() {
|
|
304
|
+
const roll1 = Math.floor(Math.random() * 20) + 1;
|
|
305
|
+
const roll2 = Math.floor(Math.random() * 20) + 1;
|
|
306
|
+
this.portentRolls = [roll1, roll2];
|
|
307
|
+
debug.log(`🔮 Portent: Rolled ${roll1} and ${roll2}`);
|
|
308
|
+
showNotification(`🔮 Portent: You rolled ${roll1} and ${roll2}`, 'info');
|
|
309
|
+
return this.portentRolls;
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
usePortentRoll: function(index) {
|
|
313
|
+
if (index >= 0 && index < this.portentRolls.length) {
|
|
314
|
+
const roll = this.portentRolls.splice(index, 1)[0];
|
|
315
|
+
debug.log(`🔮 Portent: Used portent roll ${roll}`);
|
|
316
|
+
showNotification(`🔮 Portent: Applied roll of ${roll}`, 'success');
|
|
317
|
+
return roll;
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
323
|
+
// Portent is applied manually, not automatically triggered
|
|
324
|
+
if (this.portentRolls.length > 0) {
|
|
325
|
+
showNotification(`🔮 ${this.portentRolls.length} Portent dice available`, 'info');
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Wild Magic Surge Table (d100)
|
|
332
|
+
const WILD_MAGIC_EFFECTS = [
|
|
333
|
+
"Roll on this table at the start of each of your turns for the next minute, ignoring this result on subsequent rolls.",
|
|
334
|
+
"Roll on this table at the start of each of your turns for the next minute, ignoring this result on subsequent rolls.",
|
|
335
|
+
"For the next minute, you can see any invisible creature if you have line of sight to it.",
|
|
336
|
+
"For the next minute, you can see any invisible creature if you have line of sight to it.",
|
|
337
|
+
"A modron chosen and controlled by the DM appears in an unoccupied space within 5 feet of you, then disappears 1 minute later.",
|
|
338
|
+
"A modron chosen and controlled by the DM appears in an unoccupied space within 5 feet of you, then disappears 1 minute later.",
|
|
339
|
+
"You cast Fireball as a 3rd-level spell centered on yourself.",
|
|
340
|
+
"You cast Fireball as a 3rd-level spell centered on yourself.",
|
|
341
|
+
"You cast Magic Missile as a 5th-level spell.",
|
|
342
|
+
"You cast Magic Missile as a 5th-level spell.",
|
|
343
|
+
"Roll a d10. Your height changes by a number of inches equal to the roll. If the roll is odd, you shrink. If the roll is even, you grow.",
|
|
344
|
+
"Roll a d10. Your height changes by a number of inches equal to the roll. If the roll is odd, you shrink. If the roll is even, you grow.",
|
|
345
|
+
"You cast Confusion centered on yourself.",
|
|
346
|
+
"You cast Confusion centered on yourself.",
|
|
347
|
+
"For the next minute, you regain 5 hit points at the start of each of your turns.",
|
|
348
|
+
"For the next minute, you regain 5 hit points at the start of each of your turns.",
|
|
349
|
+
"You grow a long beard made of feathers that remains until you sneeze, at which point the feathers explode out from your face.",
|
|
350
|
+
"You grow a long beard made of feathers that remains until you sneeze, at which point the feathers explode out from your face.",
|
|
351
|
+
"You cast Grease centered on yourself.",
|
|
352
|
+
"You cast Grease centered on yourself.",
|
|
353
|
+
"Creatures have disadvantage on saving throws against the next spell you cast in the next minute that involves a saving throw.",
|
|
354
|
+
"Creatures have disadvantage on saving throws against the next spell you cast in the next minute that involves a saving throw.",
|
|
355
|
+
"Your skin turns a vibrant shade of blue. A Remove Curse spell can end this effect.",
|
|
356
|
+
"Your skin turns a vibrant shade of blue. A Remove Curse spell can end this effect.",
|
|
357
|
+
"An eye appears on your forehead for the next minute. During that time, you have advantage on Wisdom (Perception) checks that rely on sight.",
|
|
358
|
+
"An eye appears on your forehead for the next minute. During that time, you have advantage on Wisdom (Perception) checks that rely on sight.",
|
|
359
|
+
"For the next minute, all your spells with a casting time of 1 action have a casting time of 1 bonus action.",
|
|
360
|
+
"For the next minute, all your spells with a casting time of 1 action have a casting time of 1 bonus action.",
|
|
361
|
+
"You teleport up to 60 feet to an unoccupied space of your choice that you can see.",
|
|
362
|
+
"You teleport up to 60 feet to an unoccupied space of your choice that you can see.",
|
|
363
|
+
"You are transported to the Astral Plane until the end of your next turn, after which time you return to the space you previously occupied or the nearest unoccupied space if that space is occupied.",
|
|
364
|
+
"You are transported to the Astral Plane until the end of your next turn, after which time you return to the space you previously occupied or the nearest unoccupied space if that space is occupied.",
|
|
365
|
+
"Maximize the damage of the next damaging spell you cast within the next minute.",
|
|
366
|
+
"Maximize the damage of the next damaging spell you cast within the next minute.",
|
|
367
|
+
"Roll a d10. Your age changes by a number of years equal to the roll. If the roll is odd, you get younger (minimum 1 year old). If the roll is even, you get older.",
|
|
368
|
+
"Roll a d10. Your age changes by a number of years equal to the roll. If the roll is odd, you get younger (minimum 1 year old). If the roll is even, you get older.",
|
|
369
|
+
"1d6 flumphs controlled by the DM appear in unoccupied spaces within 60 feet of you and are frightened of you. They vanish after 1 minute.",
|
|
370
|
+
"1d6 flumphs controlled by the DM appear in unoccupied spaces within 60 feet of you and are frightened of you. They vanish after 1 minute.",
|
|
371
|
+
"You regain 2d10 hit points.",
|
|
372
|
+
"You regain 2d10 hit points.",
|
|
373
|
+
"You turn into a potted plant until the start of your next turn. While a plant, you are incapacitated and have vulnerability to all damage. If you drop to 0 hit points, your pot breaks, and your form reverts.",
|
|
374
|
+
"You turn into a potted plant until the start of your next turn. While a plant, you are incapacitated and have vulnerability to all damage. If you drop to 0 hit points, your pot breaks, and your form reverts.",
|
|
375
|
+
"For the next minute, you can teleport up to 20 feet as a bonus action on each of your turns.",
|
|
376
|
+
"For the next minute, you can teleport up to 20 feet as a bonus action on each of your turns.",
|
|
377
|
+
"You cast Levitate on yourself.",
|
|
378
|
+
"You cast Levitate on yourself.",
|
|
379
|
+
"A unicorn controlled by the DM appears in a space within 5 feet of you, then disappears 1 minute later.",
|
|
380
|
+
"A unicorn controlled by the DM appears in a space within 5 feet of you, then disappears 1 minute later.",
|
|
381
|
+
"You can't speak for the next minute. Whenever you try, pink bubbles float out of your mouth.",
|
|
382
|
+
"You can't speak for the next minute. Whenever you try, pink bubbles float out of your mouth.",
|
|
383
|
+
"A spectral shield hovers near you for the next minute, granting you a +2 bonus to AC and immunity to Magic Missile.",
|
|
384
|
+
"A spectral shield hovers near you for the next minute, granting you a +2 bonus to AC and immunity to Magic Missile.",
|
|
385
|
+
"You are immune to being intoxicated by alcohol for the next 5d6 days.",
|
|
386
|
+
"You are immune to being intoxicated by alcohol for the next 5d6 days.",
|
|
387
|
+
"Your hair falls out but grows back within 24 hours.",
|
|
388
|
+
"Your hair falls out but grows back within 24 hours.",
|
|
389
|
+
"For the next minute, any flammable object you touch that isn't being worn or carried by another creature bursts into flame.",
|
|
390
|
+
"For the next minute, any flammable object you touch that isn't being worn or carried by another creature bursts into flame.",
|
|
391
|
+
"You regain your lowest-level expended spell slot.",
|
|
392
|
+
"You regain your lowest-level expended spell slot.",
|
|
393
|
+
"For the next minute, you must shout when you speak.",
|
|
394
|
+
"For the next minute, you must shout when you speak.",
|
|
395
|
+
"You cast Fog Cloud centered on yourself.",
|
|
396
|
+
"You cast Fog Cloud centered on yourself.",
|
|
397
|
+
"Up to three creatures you choose within 30 feet of you take 4d10 lightning damage.",
|
|
398
|
+
"Up to three creatures you choose within 30 feet of you take 4d10 lightning damage.",
|
|
399
|
+
"You are frightened by the nearest creature until the end of your next turn.",
|
|
400
|
+
"You are frightened by the nearest creature until the end of your next turn.",
|
|
401
|
+
"Each creature within 30 feet of you becomes invisible for the next minute. The invisibility ends on a creature when it attacks or casts a spell.",
|
|
402
|
+
"Each creature within 30 feet of you becomes invisible for the next minute. The invisibility ends on a creature when it attacks or casts a spell.",
|
|
403
|
+
"You gain resistance to all damage for the next minute.",
|
|
404
|
+
"You gain resistance to all damage for the next minute.",
|
|
405
|
+
"A random creature within 60 feet of you becomes poisoned for 1d4 hours.",
|
|
406
|
+
"A random creature within 60 feet of you becomes poisoned for 1d4 hours.",
|
|
407
|
+
"You glow with bright light in a 30-foot radius for the next minute. Any creature that ends its turn within 5 feet of you is blinded until the end of its next turn.",
|
|
408
|
+
"You glow with bright light in a 30-foot radius for the next minute. Any creature that ends its turn within 5 feet of you is blinded until the end of its next turn.",
|
|
409
|
+
"You cast Polymorph on yourself. If you fail the saving throw, you turn into a sheep for the spell's duration.",
|
|
410
|
+
"You cast Polymorph on yourself. If you fail the saving throw, you turn into a sheep for the spell's duration.",
|
|
411
|
+
"Illusory butterflies and flower petals flutter in the air within 10 feet of you for the next minute.",
|
|
412
|
+
"Illusory butterflies and flower petals flutter in the air within 10 feet of you for the next minute.",
|
|
413
|
+
"You can take one additional action immediately.",
|
|
414
|
+
"You can take one additional action immediately.",
|
|
415
|
+
"Each creature within 30 feet of you takes 1d10 necrotic damage. You regain hit points equal to the sum of the necrotic damage dealt.",
|
|
416
|
+
"Each creature within 30 feet of you takes 1d10 necrotic damage. You regain hit points equal to the sum of the necrotic damage dealt.",
|
|
417
|
+
"You cast Mirror Image.",
|
|
418
|
+
"You cast Mirror Image.",
|
|
419
|
+
"You cast Fly on a random creature within 60 feet of you.",
|
|
420
|
+
"You cast Fly on a random creature within 60 feet of you.",
|
|
421
|
+
"You become invisible for the next minute. During that time, other creatures can't hear you. The invisibility ends if you attack or cast a spell.",
|
|
422
|
+
"You become invisible for the next minute. During that time, other creatures can't hear you. The invisibility ends if you attack or cast a spell.",
|
|
423
|
+
"If you die within the next minute, you immediately come back to life as if by the Reincarnate spell.",
|
|
424
|
+
"If you die within the next minute, you immediately come back to life as if by the Reincarnate spell.",
|
|
425
|
+
"Your size increases by one size category for the next minute.",
|
|
426
|
+
"Your size increases by one size category for the next minute.",
|
|
427
|
+
"You and all creatures within 30 feet of you gain vulnerability to piercing damage for the next minute.",
|
|
428
|
+
"You and all creatures within 30 feet of you gain vulnerability to piercing damage for the next minute.",
|
|
429
|
+
"You are surrounded by faint, ethereal music for the next minute.",
|
|
430
|
+
"You are surrounded by faint, ethereal music for the next minute.",
|
|
431
|
+
"You regain all expended sorcery points.",
|
|
432
|
+
"You regain all expended sorcery points."
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
// Wild Magic Surge (Wild Magic Sorcerer)
|
|
436
|
+
const WildMagicSurge = {
|
|
437
|
+
name: 'Wild Magic Surge',
|
|
438
|
+
description: 'Immediately after you cast a sorcerer spell of 1st level or higher, the DM can have you roll a d20. If you roll a 1, roll on the Wild Magic Surge table.',
|
|
439
|
+
|
|
440
|
+
onSpellCast: function(spellLevel) {
|
|
441
|
+
if (spellLevel >= 1) {
|
|
442
|
+
const surgeRoll = Math.floor(Math.random() * 20) + 1;
|
|
443
|
+
debug.log(`🌀 Wild Magic: Rolled ${surgeRoll} for surge check`);
|
|
444
|
+
|
|
445
|
+
if (surgeRoll === 1) {
|
|
446
|
+
const surgeTableRoll = Math.floor(Math.random() * 100) + 1;
|
|
447
|
+
const effect = WILD_MAGIC_EFFECTS[surgeTableRoll - 1];
|
|
448
|
+
debug.log(`🌀 Wild Magic: SURGE! d100 = ${surgeTableRoll}: ${effect}`);
|
|
449
|
+
showWildMagicSurgePopup(surgeTableRoll, effect);
|
|
450
|
+
return true;
|
|
451
|
+
} else {
|
|
452
|
+
showNotification(`🌀 Wild Magic check: ${surgeRoll} (no surge)`, 'info');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
459
|
+
// Wild Magic is triggered on spell cast, not regular rolls
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Bardic Inspiration (Bard)
|
|
465
|
+
const BardicInspiration = {
|
|
466
|
+
name: 'Bardic Inspiration',
|
|
467
|
+
description: 'You can inspire others through stirring words or music. As a bonus action, grant an ally a Bardic Inspiration die they can add to an ability check, attack roll, or saving throw.',
|
|
468
|
+
|
|
469
|
+
onRoll: function(rollResult, rollType, rollName) {
|
|
470
|
+
debug.log(`🎵 Bardic Inspiration onRoll called with: ${rollResult}, ${rollType}, ${rollName}`);
|
|
471
|
+
|
|
472
|
+
// Check if it's a d20 roll (ability check, attack, or save)
|
|
473
|
+
if (rollType && rollType.includes('d20')) {
|
|
474
|
+
debug.log(`🎵 Bardic Inspiration: Checking if we should offer inspiration for ${rollName}`);
|
|
475
|
+
|
|
476
|
+
// Check if character has Bardic Inspiration uses available
|
|
477
|
+
const inspirationResource = getBardicInspirationResource();
|
|
478
|
+
if (!inspirationResource || inspirationResource.current <= 0) {
|
|
479
|
+
debug.log(`🎵 Bardic Inspiration: No uses available (${inspirationResource?.current || 0})`);
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
debug.log(`🎵 Bardic Inspiration: Has ${inspirationResource.current} uses available`);
|
|
484
|
+
|
|
485
|
+
// Get the inspiration die size based on bard level
|
|
486
|
+
const level = characterData.level || 1;
|
|
487
|
+
const inspirationDie = level < 5 ? 'd6' : level < 10 ? 'd8' : level < 15 ? 'd10' : 'd12';
|
|
488
|
+
|
|
489
|
+
// Offer Bardic Inspiration on any d20 roll
|
|
490
|
+
debug.log(`🎵 Bardic Inspiration: TRIGGERED! Offering ${inspirationDie}`);
|
|
491
|
+
|
|
492
|
+
// Show the Bardic Inspiration popup with error handling
|
|
493
|
+
try {
|
|
494
|
+
showBardicInspirationPopup({
|
|
495
|
+
rollResult: parseInt(rollResult),
|
|
496
|
+
baseRoll: parseInt(rollResult),
|
|
497
|
+
rollType: rollType,
|
|
498
|
+
rollName: rollName,
|
|
499
|
+
inspirationDie: inspirationDie,
|
|
500
|
+
usesRemaining: inspirationResource.current
|
|
501
|
+
});
|
|
502
|
+
} catch (error) {
|
|
503
|
+
debug.error('❌ Error showing Bardic Inspiration popup:', error);
|
|
504
|
+
// Fallback notification
|
|
505
|
+
showNotification(`🎵 Bardic Inspiration available! (${inspirationDie})`, 'info');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return true; // Trait triggered
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
debug.log(`🎵 Bardic Inspiration: No trigger - Type: ${rollType}`);
|
|
512
|
+
return false; // No trigger
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// ===== INITIALIZATION FUNCTIONS =====
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Initialize racial traits based on character race
|
|
520
|
+
*/
|
|
521
|
+
function initRacialTraits() {
|
|
522
|
+
debug.log('🧬 Initializing racial traits...');
|
|
523
|
+
debug.log('🧬 Character data:', characterData);
|
|
524
|
+
debug.log('🧬 Character race:', characterData?.race);
|
|
525
|
+
|
|
526
|
+
// Reset racial traits
|
|
527
|
+
activeRacialTraits = [];
|
|
528
|
+
|
|
529
|
+
if (!characterData || !characterData.race) {
|
|
530
|
+
debug.log('🧬 No race data available');
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const race = characterData.race.toLowerCase();
|
|
535
|
+
|
|
536
|
+
// Halfling Luck
|
|
537
|
+
if (race.includes('halfling')) {
|
|
538
|
+
debug.log('🧬 Halfling detected, adding Halfling Luck trait');
|
|
539
|
+
activeRacialTraits.push(HalflingLuck);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Elven Accuracy (check for feat in features)
|
|
543
|
+
if (characterData.features && characterData.features.some(f =>
|
|
544
|
+
f.name && f.name.toLowerCase().includes('elven accuracy')
|
|
545
|
+
)) {
|
|
546
|
+
debug.log('🧝 Elven Accuracy feat detected');
|
|
547
|
+
activeRacialTraits.push(ElvenAccuracy);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Dwarven Resilience
|
|
551
|
+
if (race.includes('dwarf')) {
|
|
552
|
+
debug.log('⛏️ Dwarf detected, adding Dwarven Resilience trait');
|
|
553
|
+
activeRacialTraits.push(DwarvenResilience);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Gnome Cunning
|
|
557
|
+
if (race.includes('gnome')) {
|
|
558
|
+
debug.log('🎩 Gnome detected, adding Gnome Cunning trait');
|
|
559
|
+
activeRacialTraits.push(GnomeCunning);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
debug.log(`🧬 Initialized ${activeRacialTraits.length} racial traits`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Initialize feat traits based on character features
|
|
567
|
+
*/
|
|
568
|
+
function initFeatTraits() {
|
|
569
|
+
debug.log('🎖️ Initializing feat traits...');
|
|
570
|
+
debug.log('🎖️ Character features:', characterData?.features);
|
|
571
|
+
|
|
572
|
+
// Reset feat traits
|
|
573
|
+
activeFeatTraits = [];
|
|
574
|
+
|
|
575
|
+
if (!characterData || !characterData.features) {
|
|
576
|
+
debug.log('🎖️ No features data available');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Lucky feat is now handled as an action, not a trait
|
|
581
|
+
debug.log('🎖️ Lucky feat will be available as an action button');
|
|
582
|
+
|
|
583
|
+
debug.log(`🎖️ Initialized ${activeFeatTraits.length} feat traits`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Initialize class features based on character class and level
|
|
588
|
+
*/
|
|
589
|
+
function initClassFeatures() {
|
|
590
|
+
debug.log('⚔️ Initializing class features...');
|
|
591
|
+
debug.log('⚔️ Character class:', characterData?.class);
|
|
592
|
+
debug.log('⚔️ Character level:', characterData?.level);
|
|
593
|
+
|
|
594
|
+
if (!characterData) {
|
|
595
|
+
debug.log('⚔️ No character data available');
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const characterClass = (characterData.class || '').toLowerCase();
|
|
600
|
+
const level = characterData.level || 1;
|
|
601
|
+
|
|
602
|
+
// Reliable Talent (Rogue 11+)
|
|
603
|
+
if (characterClass.includes('rogue') && level >= 11) {
|
|
604
|
+
debug.log('🎯 Rogue 11+ detected, adding Reliable Talent');
|
|
605
|
+
activeFeatTraits.push(ReliableTalent);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Bardic Inspiration (Bard)
|
|
609
|
+
if (characterClass.includes('bard') && level >= 1) {
|
|
610
|
+
debug.log('🎵 Bard detected, adding Bardic Inspiration');
|
|
611
|
+
activeFeatTraits.push(BardicInspiration);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Jack of All Trades (Bard)
|
|
615
|
+
if (characterClass.includes('bard') && level >= 2) {
|
|
616
|
+
debug.log('🎵 Bard detected, adding Jack of All Trades');
|
|
617
|
+
activeFeatTraits.push(JackOfAllTrades);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Rage Damage Bonus (Barbarian)
|
|
621
|
+
if (characterClass.includes('barbarian')) {
|
|
622
|
+
debug.log('😡 Barbarian detected, adding Rage Damage Bonus');
|
|
623
|
+
activeFeatTraits.push(RageDamageBonus);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Brutal Critical (Barbarian 9+)
|
|
627
|
+
if (characterClass.includes('barbarian') && level >= 9) {
|
|
628
|
+
debug.log('💥 Barbarian 9+ detected, adding Brutal Critical');
|
|
629
|
+
activeFeatTraits.push(BrutalCritical);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Portent (Divination Wizard 2+)
|
|
633
|
+
if (characterClass.includes('wizard') && level >= 2) {
|
|
634
|
+
// Check for Divination subclass in features
|
|
635
|
+
const isDivination = characterData.features && characterData.features.some(f =>
|
|
636
|
+
f.name && (f.name.toLowerCase().includes('divination') || f.name.toLowerCase().includes('portent'))
|
|
637
|
+
);
|
|
638
|
+
if (isDivination) {
|
|
639
|
+
debug.log('🔮 Divination Wizard detected, adding Portent');
|
|
640
|
+
activeFeatTraits.push(PortentDice);
|
|
641
|
+
// Auto-roll portent dice
|
|
642
|
+
PortentDice.rollPortentDice();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Wild Magic Surge (Wild Magic Sorcerer)
|
|
647
|
+
if (characterClass.includes('sorcerer')) {
|
|
648
|
+
// Check for Wild Magic subclass in features
|
|
649
|
+
const isWildMagic = characterData.features && characterData.features.some(f =>
|
|
650
|
+
f.name && f.name.toLowerCase().includes('wild magic')
|
|
651
|
+
);
|
|
652
|
+
if (isWildMagic) {
|
|
653
|
+
debug.log('🌀 Wild Magic Sorcerer detected, adding Wild Magic Surge');
|
|
654
|
+
activeFeatTraits.push(WildMagicSurge);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
debug.log(`⚔️ Initialized ${activeFeatTraits.length} class feature traits`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ===== CHECKING FUNCTIONS =====
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Check racial traits for roll triggers
|
|
665
|
+
* @param {number} rollResult - Roll result
|
|
666
|
+
* @param {string} rollType - Type of roll
|
|
667
|
+
* @param {string} rollName - Name of roll
|
|
668
|
+
* @returns {boolean} Whether any trait was triggered
|
|
669
|
+
*/
|
|
670
|
+
function checkRacialTraits(rollResult, rollType, rollName) {
|
|
671
|
+
debug.log(`🧬 Checking racial traits for roll: ${rollResult} (${rollType}) - ${rollName}`);
|
|
672
|
+
debug.log(`🧬 Active racial traits count: ${activeRacialTraits.length}`);
|
|
673
|
+
|
|
674
|
+
let traitTriggered = false;
|
|
675
|
+
|
|
676
|
+
for (const trait of activeRacialTraits) {
|
|
677
|
+
if (trait.onRoll && typeof trait.onRoll === 'function') {
|
|
678
|
+
const result = trait.onRoll(rollResult, rollType, rollName);
|
|
679
|
+
if (result) {
|
|
680
|
+
traitTriggered = true;
|
|
681
|
+
debug.log(`🧬 ${trait.name} triggered!`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return traitTriggered;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Check feat traits for roll triggers
|
|
691
|
+
* @param {number} rollResult - Roll result
|
|
692
|
+
* @param {string} rollType - Type of roll
|
|
693
|
+
* @param {string} rollName - Name of roll
|
|
694
|
+
* @returns {boolean} Whether any trait was triggered
|
|
695
|
+
*/
|
|
696
|
+
function checkFeatTraits(rollResult, rollType, rollName) {
|
|
697
|
+
debug.log(`🎖️ Checking feat traits for roll: ${rollResult} (${rollType}) - ${rollName}`);
|
|
698
|
+
debug.log(`🎖️ Active feat traits count: ${activeFeatTraits.length}`);
|
|
699
|
+
|
|
700
|
+
let traitTriggered = false;
|
|
701
|
+
|
|
702
|
+
for (const trait of activeFeatTraits) {
|
|
703
|
+
if (trait.onRoll && typeof trait.onRoll === 'function') {
|
|
704
|
+
const result = trait.onRoll(rollResult, rollType, rollName);
|
|
705
|
+
if (result) {
|
|
706
|
+
traitTriggered = true;
|
|
707
|
+
debug.log(`🎖️ ${trait.name} triggered!`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return traitTriggered;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ===== RESOURCE MANAGEMENT =====
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Get Bardic Inspiration resource from character data
|
|
719
|
+
* @returns {Object|null} Bardic Inspiration resource object
|
|
720
|
+
*/
|
|
721
|
+
function getBardicInspirationResource() {
|
|
722
|
+
if (!characterData || !characterData.resources) {
|
|
723
|
+
debug.log('🎵 No characterData or resources for Bardic Inspiration detection');
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Find Bardic Inspiration in resources (flexible matching)
|
|
728
|
+
const inspirationResource = characterData.resources.find(r => {
|
|
729
|
+
const lowerName = r.name.toLowerCase().trim();
|
|
730
|
+
return (
|
|
731
|
+
lowerName.includes('bardic inspiration') ||
|
|
732
|
+
lowerName === 'bardic inspiration' ||
|
|
733
|
+
lowerName === 'inspiration' ||
|
|
734
|
+
lowerName.includes('inspiration die') ||
|
|
735
|
+
lowerName.includes('inspiration dice')
|
|
736
|
+
);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
if (inspirationResource) {
|
|
740
|
+
debug.log(`🎵 Found Bardic Inspiration resource: ${inspirationResource.name} (${inspirationResource.current}/${inspirationResource.max})`);
|
|
741
|
+
} else {
|
|
742
|
+
debug.log('🎵 No Bardic Inspiration resource found in character data');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return inspirationResource;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Use one Bardic Inspiration charge
|
|
750
|
+
* @returns {boolean} Whether the use was successful
|
|
751
|
+
*/
|
|
752
|
+
function useBardicInspiration() {
|
|
753
|
+
debug.log('🎵 useBardicInspiration called');
|
|
754
|
+
const inspirationResource = getBardicInspirationResource();
|
|
755
|
+
debug.log('🎵 Bardic Inspiration resource found:', inspirationResource);
|
|
756
|
+
|
|
757
|
+
if (!inspirationResource) {
|
|
758
|
+
debug.error('❌ No Bardic Inspiration resource found');
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (inspirationResource.current <= 0) {
|
|
763
|
+
debug.error(`❌ No Bardic Inspiration uses available (current: ${inspirationResource.current})`);
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Decrement Bardic Inspiration uses
|
|
768
|
+
const oldCurrent = inspirationResource.current;
|
|
769
|
+
inspirationResource.current--;
|
|
770
|
+
|
|
771
|
+
debug.log(`✅ Used Bardic Inspiration (${oldCurrent} → ${inspirationResource.current})`);
|
|
772
|
+
|
|
773
|
+
// Save to storage
|
|
774
|
+
browserAPI.storage.local.set({ characterData: characterData });
|
|
775
|
+
|
|
776
|
+
// Refresh resources display
|
|
777
|
+
buildResourcesDisplay();
|
|
778
|
+
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Update Lucky button text with remaining points
|
|
784
|
+
*/
|
|
785
|
+
function updateLuckyButtonText() {
|
|
786
|
+
const luckyButton = document.querySelector('#lucky-action-button');
|
|
787
|
+
if (luckyButton) {
|
|
788
|
+
const luckyResource = getLuckyResource();
|
|
789
|
+
if (luckyResource) {
|
|
790
|
+
const pointsText = luckyResource.current > 0 ? ` (${luckyResource.current}/3)` : ' (0/3)';
|
|
791
|
+
luckyButton.textContent = `🎖️ Lucky${pointsText}`;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ===== EXPORTS =====
|
|
797
|
+
|
|
798
|
+
// Export functions to globalThis
|
|
799
|
+
globalThis.initRacialTraits = initRacialTraits;
|
|
800
|
+
globalThis.initFeatTraits = initFeatTraits;
|
|
801
|
+
globalThis.initClassFeatures = initClassFeatures;
|
|
802
|
+
globalThis.checkRacialTraits = checkRacialTraits;
|
|
803
|
+
globalThis.checkFeatTraits = checkFeatTraits;
|
|
804
|
+
globalThis.getBardicInspirationResource = getBardicInspirationResource;
|
|
805
|
+
globalThis.useBardicInspiration = useBardicInspiration;
|
|
806
|
+
globalThis.updateLuckyButtonText = updateLuckyButtonText;
|
|
807
|
+
|
|
808
|
+
// Export state variables to globalThis
|
|
809
|
+
globalThis.activeRacialTraits = activeRacialTraits;
|
|
810
|
+
globalThis.activeFeatTraits = activeFeatTraits;
|
|
811
|
+
|
|
812
|
+
debug.log('✅ Character Traits module loaded');
|
|
813
|
+
|
|
814
|
+
})();
|