@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,1320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class Features Edge Cases Configuration
|
|
3
|
+
*
|
|
4
|
+
* This file contains all the "weird" class features, racial features, and combat actions
|
|
5
|
+
* that need special handling beyond standard attack/damage/healing buttons.
|
|
6
|
+
*
|
|
7
|
+
* Each feature can have:
|
|
8
|
+
* - type: Category of edge case
|
|
9
|
+
* - Custom properties for that category
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const CLASS_FEATURE_EDGE_CASES = {
|
|
13
|
+
// ===== BARBARIAN FEATURES =====
|
|
14
|
+
'rage': {
|
|
15
|
+
type: 'resource_tracking',
|
|
16
|
+
resource: 'rage_points',
|
|
17
|
+
maxResource: 'barbarian_level',
|
|
18
|
+
duration: '1_minute',
|
|
19
|
+
endCondition: 'no_attack_or_damage',
|
|
20
|
+
description: 'Ends if no attack or damage since last turn'
|
|
21
|
+
},
|
|
22
|
+
'reckless attack': {
|
|
23
|
+
type: 'advantage_disadvantage',
|
|
24
|
+
selfAdvantage: 'attack',
|
|
25
|
+
selfDisadvantage: 'defense',
|
|
26
|
+
description: 'Advantage on attacks, disadvantage on attacks against you'
|
|
27
|
+
},
|
|
28
|
+
'danger sense': {
|
|
29
|
+
type: 'conditional_advantage',
|
|
30
|
+
condition: 'can_see_dex_save_source',
|
|
31
|
+
appliesTo: 'dexterity_save',
|
|
32
|
+
description: 'Advantage on Dex saves if you can see the source'
|
|
33
|
+
},
|
|
34
|
+
'relentless rage': {
|
|
35
|
+
type: 'death_save',
|
|
36
|
+
trigger: 'drop_to_0_hp_while_raging',
|
|
37
|
+
saveType: 'constitution',
|
|
38
|
+
baseDC: 10,
|
|
39
|
+
dcIncrement: 5,
|
|
40
|
+
resetCondition: 'short_rest',
|
|
41
|
+
description: 'Con save to drop to 1 HP instead of 0'
|
|
42
|
+
},
|
|
43
|
+
'persistent rage': {
|
|
44
|
+
type: 'override_condition',
|
|
45
|
+
overrides: 'rage_end_condition',
|
|
46
|
+
description: 'Rage doesn\'t end from not attacking/taking damage'
|
|
47
|
+
},
|
|
48
|
+
'retaliation': {
|
|
49
|
+
type: 'reaction',
|
|
50
|
+
timing: 'when_damaged_within_5ft',
|
|
51
|
+
action: 'melee_attack',
|
|
52
|
+
description: 'Reaction melee attack when damaged within 5ft'
|
|
53
|
+
},
|
|
54
|
+
'brutal critical': {
|
|
55
|
+
type: 'damage_bonus',
|
|
56
|
+
timing: 'critical_hit',
|
|
57
|
+
effect: 'extra_damage_dice',
|
|
58
|
+
formula: 'barbarian_level // 2',
|
|
59
|
+
description: 'Extra damage dice on critical hits (scales with level)'
|
|
60
|
+
},
|
|
61
|
+
'feral instinct': {
|
|
62
|
+
type: 'initiative_bonus',
|
|
63
|
+
effect: 'advantage_on_initiative',
|
|
64
|
+
condition: 'while_raging',
|
|
65
|
+
additionalEffect: 'cannot_be_surprised_while_raging',
|
|
66
|
+
description: 'Advantage on initiative, cannot be surprised while raging'
|
|
67
|
+
},
|
|
68
|
+
'primal champion': {
|
|
69
|
+
type: 'ability_score_increase',
|
|
70
|
+
abilities: ['strength', 'constitution'],
|
|
71
|
+
amount: 4,
|
|
72
|
+
maxScore: 24,
|
|
73
|
+
description: 'STR and CON increase by 4 (max 24)'
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// ===== FIGHTER FEATURES =====
|
|
77
|
+
'action surge': {
|
|
78
|
+
type: 'bonus_action',
|
|
79
|
+
effect: 'additional_action',
|
|
80
|
+
limitation: 'one_additional_action_even_with_multiple_uses',
|
|
81
|
+
description: 'Take one additional action on your turn'
|
|
82
|
+
},
|
|
83
|
+
'second wind': {
|
|
84
|
+
type: 'healing',
|
|
85
|
+
actionType: 'bonus_action',
|
|
86
|
+
healingFormula: '1d10 + fighter_level',
|
|
87
|
+
resource: 'once_per_rest',
|
|
88
|
+
description: 'Bonus action to regain HP'
|
|
89
|
+
},
|
|
90
|
+
'indomitable': {
|
|
91
|
+
type: 'save_reroll',
|
|
92
|
+
trigger: 'failed_save',
|
|
93
|
+
resource: 'uses_per_long_rest',
|
|
94
|
+
description: 'Reroll a failed save'
|
|
95
|
+
},
|
|
96
|
+
'riposte': {
|
|
97
|
+
type: 'reaction',
|
|
98
|
+
timing: 'when_melee_attack_misses_you',
|
|
99
|
+
action: 'melee_attack',
|
|
100
|
+
description: 'Reaction attack when melee attack misses you'
|
|
101
|
+
},
|
|
102
|
+
'parry': {
|
|
103
|
+
type: 'reaction',
|
|
104
|
+
timing: 'when_hit',
|
|
105
|
+
effect: 'damage_reduction',
|
|
106
|
+
formula: 'superiority_die + dex_mod',
|
|
107
|
+
description: 'Reduce damage by superiority die + Dex mod'
|
|
108
|
+
},
|
|
109
|
+
'precision attack': {
|
|
110
|
+
type: 'attack_bonus',
|
|
111
|
+
timing: 'when_making_attack_roll',
|
|
112
|
+
effect: 'add_superiority_die',
|
|
113
|
+
description: 'Add superiority die to attack roll'
|
|
114
|
+
},
|
|
115
|
+
'eldritch strike': {
|
|
116
|
+
type: 'debuff',
|
|
117
|
+
trigger: 'weapon_attack_hit',
|
|
118
|
+
effect: 'disadvantage_on_next_spell_save',
|
|
119
|
+
duration: 'until_end_of_next_turn',
|
|
120
|
+
description: 'Target has disadvantage on next save vs your spell'
|
|
121
|
+
},
|
|
122
|
+
'war magic': {
|
|
123
|
+
type: 'bonus_action_combo',
|
|
124
|
+
trigger: 'cast_cantrip',
|
|
125
|
+
action: 'weapon_attack',
|
|
126
|
+
description: 'Cast cantrip + bonus action weapon attack'
|
|
127
|
+
},
|
|
128
|
+
'arcane charge': {
|
|
129
|
+
type: 'teleport',
|
|
130
|
+
trigger: 'action_surge',
|
|
131
|
+
distance: '30_feet',
|
|
132
|
+
description: 'Teleport 30ft when using Action Surge'
|
|
133
|
+
},
|
|
134
|
+
'defensive duelist': {
|
|
135
|
+
type: 'reaction',
|
|
136
|
+
timing: 'when_attacked_with_finesse_weapon',
|
|
137
|
+
effect: 'add_proficiency_to_ac',
|
|
138
|
+
condition: 'wielding_finesse_weapon',
|
|
139
|
+
description: 'Reaction to add proficiency to AC when wielding finesse weapon'
|
|
140
|
+
},
|
|
141
|
+
'great weapon master': {
|
|
142
|
+
type: 'attack_option',
|
|
143
|
+
choice: 'bonus_attack_on_crit_kill_or_minus_5_plus_10',
|
|
144
|
+
penalty: 'minus_5_to_hit',
|
|
145
|
+
bonus: 'plus_10_damage',
|
|
146
|
+
condition: 'wielding_heavy_weapon',
|
|
147
|
+
description: 'Bonus attack on crit/kill OR -5 to hit for +10 damage'
|
|
148
|
+
},
|
|
149
|
+
'improved critical': {
|
|
150
|
+
type: 'trigger',
|
|
151
|
+
triggerType: 'expanded_crit_range',
|
|
152
|
+
critRange: '19-20',
|
|
153
|
+
description: 'Critical hits occur on 19-20 (Champion Fighter feature)'
|
|
154
|
+
},
|
|
155
|
+
'superior critical': {
|
|
156
|
+
type: 'trigger',
|
|
157
|
+
triggerType: 'expanded_crit_range',
|
|
158
|
+
critRange: '18-20',
|
|
159
|
+
description: 'Critical hits occur on 18-20 (Champion Fighter level 15 feature)'
|
|
160
|
+
},
|
|
161
|
+
'sharpshooter': {
|
|
162
|
+
type: 'attack_option',
|
|
163
|
+
choice: 'ignore_cover_range_or_minus_5_plus_10',
|
|
164
|
+
penalty: 'minus_5_to_hit',
|
|
165
|
+
bonus: 'plus_10_damage',
|
|
166
|
+
condition: 'using_ranged_weapon',
|
|
167
|
+
effects: ['ignore_cover', 'ignore_range_penalty'],
|
|
168
|
+
description: 'Ignore cover/range OR -5 to hit for +10 damage'
|
|
169
|
+
},
|
|
170
|
+
'lucky feat': {
|
|
171
|
+
type: 'resource_tracking',
|
|
172
|
+
resource: 'luck_points',
|
|
173
|
+
maxResource: 3,
|
|
174
|
+
resetCondition: 'long_rest',
|
|
175
|
+
effect: 'reroll_or_force_enemy_reroll',
|
|
176
|
+
description: '3 luck points to reroll or force enemy reroll'
|
|
177
|
+
},
|
|
178
|
+
'sentinel': {
|
|
179
|
+
type: 'reaction_opportunities',
|
|
180
|
+
effects: ['multiple_reaction_attacks', 'stop_creature_movement'],
|
|
181
|
+
condition: 'opportunity_attack',
|
|
182
|
+
description: 'Multiple reaction-based attack opportunities'
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// ===== ROGUE FEATURES =====
|
|
186
|
+
'sneak attack': {
|
|
187
|
+
type: 'conditional_damage',
|
|
188
|
+
condition: 'advantage_or_ally_within_5ft',
|
|
189
|
+
damageFormula: 'sneak_attack_dice',
|
|
190
|
+
limitation: 'once_per_turn',
|
|
191
|
+
description: 'Extra damage with advantage or ally adjacent'
|
|
192
|
+
},
|
|
193
|
+
'cunning action': {
|
|
194
|
+
type: 'bonus_action',
|
|
195
|
+
options: ['dash', 'disengage', 'hide'],
|
|
196
|
+
description: 'Dash/Disengage/Hide as bonus action'
|
|
197
|
+
},
|
|
198
|
+
'uncanny dodge': {
|
|
199
|
+
type: 'reaction',
|
|
200
|
+
timing: 'when_hit_by_attack_you_can_see',
|
|
201
|
+
effect: 'half_damage',
|
|
202
|
+
description: 'Reaction to halve damage when hit'
|
|
203
|
+
},
|
|
204
|
+
'evasion': {
|
|
205
|
+
type: 'save_modifier',
|
|
206
|
+
saveType: 'dexterity',
|
|
207
|
+
effect: 'no_damage_on_success_half_on_failure',
|
|
208
|
+
description: 'Dex save: no damage on success, half on failure'
|
|
209
|
+
},
|
|
210
|
+
'reliable talent': {
|
|
211
|
+
type: 'minimum_roll',
|
|
212
|
+
appliesTo: 'proficient_ability_checks',
|
|
213
|
+
minimum: 10,
|
|
214
|
+
description: 'Treat rolls below 10 as 10 on proficient checks'
|
|
215
|
+
},
|
|
216
|
+
'blindsense': {
|
|
217
|
+
type: 'senses',
|
|
218
|
+
range: '10_feet',
|
|
219
|
+
detects: 'hidden_invisible_creatures',
|
|
220
|
+
description: 'Know location of hidden/invisible creatures within 10ft'
|
|
221
|
+
},
|
|
222
|
+
'slippery mind': {
|
|
223
|
+
type: 'save_reroll',
|
|
224
|
+
trigger: 'failed_wisdom_save',
|
|
225
|
+
timing: 'reaction',
|
|
226
|
+
resource: 'once_per_long_rest',
|
|
227
|
+
description: 'Reaction to succeed on failed Wis save'
|
|
228
|
+
},
|
|
229
|
+
'elusive': {
|
|
230
|
+
type: 'immunity',
|
|
231
|
+
effect: 'no_advantage_against_you_when_visible',
|
|
232
|
+
description: 'Attackers don\'t have advantage when they can see you'
|
|
233
|
+
},
|
|
234
|
+
'stroke of luck': {
|
|
235
|
+
type: 'success_conversion',
|
|
236
|
+
trigger: 'miss_attack_or_fail_ability_check',
|
|
237
|
+
effect: 'turn_into_hit_or_success',
|
|
238
|
+
resource: 'once_per_long_rest',
|
|
239
|
+
description: 'Turn miss into hit or failure into success'
|
|
240
|
+
},
|
|
241
|
+
'steady aim': {
|
|
242
|
+
type: 'bonus_action',
|
|
243
|
+
effect: 'advantage_on_next_attack',
|
|
244
|
+
penalty: 'lose_movement',
|
|
245
|
+
condition: 'ranged_attack',
|
|
246
|
+
description: 'Bonus action to get advantage but lose movement'
|
|
247
|
+
},
|
|
248
|
+
'soul of deceit': {
|
|
249
|
+
type: 'immunity',
|
|
250
|
+
effects: ['immune_to_telepathy', 'immune_to_mind_reading'],
|
|
251
|
+
description: 'Immune to telepathy/mind reading'
|
|
252
|
+
},
|
|
253
|
+
'death strike': {
|
|
254
|
+
type: 'conditional_damage',
|
|
255
|
+
condition: 'surprised_target',
|
|
256
|
+
effect: 'double_damage',
|
|
257
|
+
trigger: 'failed_save',
|
|
258
|
+
saveType: 'constitution',
|
|
259
|
+
description: 'Failed save = double damage on surprise'
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// ===== MONK FEATURES =====
|
|
263
|
+
'flurry of blows': {
|
|
264
|
+
type: 'bonus_action_attacks',
|
|
265
|
+
trigger: 'take_attack_action',
|
|
266
|
+
cost: '1_ki_point',
|
|
267
|
+
attacks: 'two_unarmed_strikes',
|
|
268
|
+
description: 'Bonus action: 2 unarmed strikes for 1 ki'
|
|
269
|
+
},
|
|
270
|
+
'patient defense': {
|
|
271
|
+
type: 'bonus_action',
|
|
272
|
+
cost: '1_ki_point',
|
|
273
|
+
effect: 'dodge_action',
|
|
274
|
+
description: 'Bonus action: Dodge for 1 ki'
|
|
275
|
+
},
|
|
276
|
+
'step of the wind': {
|
|
277
|
+
type: 'bonus_action',
|
|
278
|
+
cost: '1_ki_point',
|
|
279
|
+
effects: ['disengage_or_dash', 'jump_distance_doubles'],
|
|
280
|
+
description: 'Bonus action: Disengage/Dash + double jump distance'
|
|
281
|
+
},
|
|
282
|
+
'deflect missiles': {
|
|
283
|
+
type: 'reaction',
|
|
284
|
+
timing: 'when_hit_by_ranged_weapon',
|
|
285
|
+
effect: 'damage_reduction',
|
|
286
|
+
formula: '1d10 + dex_mod + monk_level',
|
|
287
|
+
special: 'can_catch_and_throw_missile_for_1_ki',
|
|
288
|
+
description: 'Reduce ranged damage, can throw back for 1 ki'
|
|
289
|
+
},
|
|
290
|
+
'deflect attacks': {
|
|
291
|
+
type: 'reaction',
|
|
292
|
+
timing: 'when_hit_by_attack',
|
|
293
|
+
effect: 'damage_reduction',
|
|
294
|
+
formula: '1d10 + dex_mod + monk_level',
|
|
295
|
+
special: 'can_redirect_attack_for_1_ki',
|
|
296
|
+
description: '2024 Monk: Reduce melee/ranged damage, can redirect for 1 ki'
|
|
297
|
+
},
|
|
298
|
+
'slow fall': {
|
|
299
|
+
type: 'damage_reduction',
|
|
300
|
+
trigger: 'falling',
|
|
301
|
+
formula: '5 × monk_level',
|
|
302
|
+
description: 'Reduce falling damage by 5 × monk level'
|
|
303
|
+
},
|
|
304
|
+
'stunning strike': {
|
|
305
|
+
type: 'save_effect',
|
|
306
|
+
trigger: 'melee_weapon_attack_hit',
|
|
307
|
+
cost: '1_ki_point',
|
|
308
|
+
saveType: 'constitution',
|
|
309
|
+
effect: 'stunned',
|
|
310
|
+
description: 'Force Con save or be stunned (1 ki)'
|
|
311
|
+
},
|
|
312
|
+
'stillness of mind': {
|
|
313
|
+
type: 'condition_end',
|
|
314
|
+
timing: 'action',
|
|
315
|
+
conditions: ['charmed', 'frightened'],
|
|
316
|
+
description: 'Action to end charmed or frightened'
|
|
317
|
+
},
|
|
318
|
+
'purity of body': {
|
|
319
|
+
type: 'immunity',
|
|
320
|
+
effects: ['poison_immunity', 'disease_immunity'],
|
|
321
|
+
description: 'Immune to poison and disease'
|
|
322
|
+
},
|
|
323
|
+
'diamond soul': {
|
|
324
|
+
type: 'save_reroll',
|
|
325
|
+
trigger: 'failed_save',
|
|
326
|
+
cost: '1_ki_point',
|
|
327
|
+
description: 'Reroll failed save for 1 ki'
|
|
328
|
+
},
|
|
329
|
+
'empty body': {
|
|
330
|
+
type: 'defensive_buff',
|
|
331
|
+
cost: '4_ki_points',
|
|
332
|
+
effects: ['invisible', 'resistance_to_all_damage_except_force'],
|
|
333
|
+
description: 'Invisible + resistance (except force) for 4 ki'
|
|
334
|
+
},
|
|
335
|
+
'wholeness of body': {
|
|
336
|
+
type: 'healing',
|
|
337
|
+
actionType: 'action',
|
|
338
|
+
healingFormula: 'monk_level * 3',
|
|
339
|
+
cost: 'action',
|
|
340
|
+
description: 'Action to heal monk level × 3 HP'
|
|
341
|
+
},
|
|
342
|
+
'tongue of the sun and moon': {
|
|
343
|
+
type: 'language_understanding',
|
|
344
|
+
effect: 'understand_all_spoken_languages',
|
|
345
|
+
description: 'Understand all spoken languages'
|
|
346
|
+
},
|
|
347
|
+
'timeless body': {
|
|
348
|
+
type: 'immunity',
|
|
349
|
+
effects: ['no_aging', 'no_food_water_requirements'],
|
|
350
|
+
description: "Don't age, no food/water requirements"
|
|
351
|
+
},
|
|
352
|
+
'perfect self': {
|
|
353
|
+
type: 'resource_recovery',
|
|
354
|
+
trigger: 'start_turn_with_0_ki',
|
|
355
|
+
effect: 'regain_4_ki_points',
|
|
356
|
+
condition: 'start_of_turn',
|
|
357
|
+
description: 'Regain 4 ki if you start turn with 0'
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// ===== PALADIN FEATURES =====
|
|
361
|
+
'divine smite': {
|
|
362
|
+
type: 'divine_smite_modal',
|
|
363
|
+
trigger: 'melee_weapon_attack_hit',
|
|
364
|
+
resource: 'spell_slot',
|
|
365
|
+
damageFormula: '2d8 + 1d8_per_spell_level_above_1st',
|
|
366
|
+
damageType: 'radiant',
|
|
367
|
+
description: 'Expend spell slot for extra radiant damage'
|
|
368
|
+
},
|
|
369
|
+
'lay on hands': {
|
|
370
|
+
type: 'healing_pool',
|
|
371
|
+
resource: 'lay_on_hands_pool',
|
|
372
|
+
poolSize: '5 × paladin_level',
|
|
373
|
+
action: 'touch',
|
|
374
|
+
description: 'Heal from pool (5 × level total)'
|
|
375
|
+
},
|
|
376
|
+
'divine sense': {
|
|
377
|
+
type: 'senses',
|
|
378
|
+
actionType: 'action',
|
|
379
|
+
range: '60_feet',
|
|
380
|
+
detects: ['celestials', 'fiends', 'undead'],
|
|
381
|
+
description: 'Detect celestials/fiends/undead within 60ft'
|
|
382
|
+
},
|
|
383
|
+
'aura of protection': {
|
|
384
|
+
type: 'save_bonus',
|
|
385
|
+
range: '10_feet',
|
|
386
|
+
bonus: 'charisma_modifier',
|
|
387
|
+
appliesTo: 'saves',
|
|
388
|
+
targets: 'self_and_allies',
|
|
389
|
+
description: 'Allies within 10ft add Cha mod to saves'
|
|
390
|
+
},
|
|
391
|
+
'aura of courage': {
|
|
392
|
+
type: 'immunity',
|
|
393
|
+
range: '10_feet',
|
|
394
|
+
effect: 'frightened_immunity',
|
|
395
|
+
targets: 'self_and_allies',
|
|
396
|
+
description: 'Allies within 10ft immune to frightened'
|
|
397
|
+
},
|
|
398
|
+
'cleansing touch': {
|
|
399
|
+
type: 'spell_end',
|
|
400
|
+
actionType: 'action',
|
|
401
|
+
cost: 'uses_equal_to_charisma_modifier',
|
|
402
|
+
effect: 'end_one_spell',
|
|
403
|
+
description: 'End one spell on touched creature'
|
|
404
|
+
},
|
|
405
|
+
'avenging angel': {
|
|
406
|
+
type: 'attack_penalty',
|
|
407
|
+
range: '10_feet',
|
|
408
|
+
condition: 'attack_target_other_than_you',
|
|
409
|
+
effect: 'disadvantage',
|
|
410
|
+
description: 'Disadvantage on attacks against others within 10ft'
|
|
411
|
+
},
|
|
412
|
+
'improved divine smite': {
|
|
413
|
+
type: 'passive_damage',
|
|
414
|
+
trigger: 'melee_weapon_hit',
|
|
415
|
+
damageFormula: '1d8',
|
|
416
|
+
damageType: 'radiant',
|
|
417
|
+
description: 'Always deal +1d8 radiant on melee weapon hits'
|
|
418
|
+
},
|
|
419
|
+
'divine health': {
|
|
420
|
+
type: 'immunity',
|
|
421
|
+
effect: 'disease_immunity',
|
|
422
|
+
description: 'Immunity to disease'
|
|
423
|
+
},
|
|
424
|
+
'sacred weapon': {
|
|
425
|
+
type: 'channel_divinity',
|
|
426
|
+
actionType: 'bonus_action',
|
|
427
|
+
effect: 'magical_weapon_plus_cha_to_attacks',
|
|
428
|
+
duration: '1_minute',
|
|
429
|
+
description: 'Channel Divinity for magical weapon + add CHA to attacks'
|
|
430
|
+
},
|
|
431
|
+
'turn the unholy': {
|
|
432
|
+
type: 'channel_divinity',
|
|
433
|
+
actionType: 'action',
|
|
434
|
+
effect: 'turn_fiends_undead',
|
|
435
|
+
saveType: 'charisma',
|
|
436
|
+
saveDC: '8 + proficiency + cha_mod',
|
|
437
|
+
description: 'Channel Divinity to turn fiends/undead'
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
// ===== RANGER FEATURES =====
|
|
441
|
+
'favored enemy': {
|
|
442
|
+
type: 'advantage',
|
|
443
|
+
appliesTo: ['survival_checks_to_track', 'intelligence_checks_to_recall_info'],
|
|
444
|
+
condition: 'against_favored_enemy',
|
|
445
|
+
description: 'Advantage on tracking/recall checks vs favored enemy'
|
|
446
|
+
},
|
|
447
|
+
'natural explorer': {
|
|
448
|
+
type: 'terrain_benefits',
|
|
449
|
+
condition: 'in_favored_terrain',
|
|
450
|
+
effects: [
|
|
451
|
+
'difficult_terrain_no_slow',
|
|
452
|
+
'cannot_get_lost_except_by_magic',
|
|
453
|
+
'advantage_on_initiative_checks',
|
|
454
|
+
'advantage_on_attacks_against_creatures_that_havent_acted_yet'
|
|
455
|
+
],
|
|
456
|
+
description: 'Various benefits in favored terrain'
|
|
457
|
+
},
|
|
458
|
+
'hunter\'s mark': {
|
|
459
|
+
type: 'conditional_damage',
|
|
460
|
+
trigger: 'hit_marked_creature',
|
|
461
|
+
damageFormula: '1d6',
|
|
462
|
+
moveable: true,
|
|
463
|
+
description: 'Extra 1d6 damage vs marked creature'
|
|
464
|
+
},
|
|
465
|
+
'colossus slayer': {
|
|
466
|
+
type: 'conditional_damage',
|
|
467
|
+
trigger: 'hit_creature_below_max_hp',
|
|
468
|
+
damageFormula: '1d8',
|
|
469
|
+
limitation: 'once_per_turn',
|
|
470
|
+
description: 'Extra 1d8 damage vs creatures below max HP'
|
|
471
|
+
},
|
|
472
|
+
'horde breaker': {
|
|
473
|
+
type: 'bonus_attack',
|
|
474
|
+
trigger: 'attack_creature_with_enemy_within_5ft',
|
|
475
|
+
condition: 'second_enemy_within_reach',
|
|
476
|
+
description: 'Bonus attack against second enemy within 5ft'
|
|
477
|
+
},
|
|
478
|
+
'escape the horde': {
|
|
479
|
+
type: 'defense_bonus',
|
|
480
|
+
trigger: 'opportunity_attack_against_you',
|
|
481
|
+
effect: 'disadvantage_on_attack',
|
|
482
|
+
description: 'Opportunity attacks against you have disadvantage'
|
|
483
|
+
},
|
|
484
|
+
'multiattack defense': {
|
|
485
|
+
type: 'defense_bonus',
|
|
486
|
+
trigger: 'hit_by_attack',
|
|
487
|
+
effect: '+4_ac_against_same_attacker_this_turn',
|
|
488
|
+
description: '+4 AC against subsequent attacks from same attacker'
|
|
489
|
+
},
|
|
490
|
+
'vanish': {
|
|
491
|
+
type: 'stealth',
|
|
492
|
+
actionType: 'bonus_action',
|
|
493
|
+
effect: 'hide_plus_cannot_be_tracked_nonmagically',
|
|
494
|
+
description: 'Hide + cannot be tracked nonmagically'
|
|
495
|
+
},
|
|
496
|
+
'primeval awareness': {
|
|
497
|
+
type: 'senses',
|
|
498
|
+
effect: 'detect_creature_types',
|
|
499
|
+
range: '1_to_6_miles',
|
|
500
|
+
description: 'Detect creature types within 1-6 miles'
|
|
501
|
+
},
|
|
502
|
+
'feral senses': {
|
|
503
|
+
type: 'advantage_override',
|
|
504
|
+
effect: 'no_disadvantage_on_attacks_vs_unseen_creatures',
|
|
505
|
+
condition: 'creatures_you_cannot_see',
|
|
506
|
+
description: "Can't have disadvantage on attacks vs creatures you can't see"
|
|
507
|
+
},
|
|
508
|
+
'foe slayer': {
|
|
509
|
+
type: 'conditional_bonus',
|
|
510
|
+
condition: 'once_per_turn',
|
|
511
|
+
effect: 'add_wis_mod_to_attack_or_damage',
|
|
512
|
+
appliesTo: ['attack_roll', 'damage_roll'],
|
|
513
|
+
description: 'Add WIS mod to attack or damage once per turn'
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
// ===== ARTIFICER FEATURES =====
|
|
517
|
+
'arcane firearm': {
|
|
518
|
+
type: 'trigger',
|
|
519
|
+
triggerType: 'spell_damage_bonus',
|
|
520
|
+
spellType: 'artificer_spells',
|
|
521
|
+
bonusDamage: '1d8',
|
|
522
|
+
description: 'Add 1d8 to damage roll of artificer spell (Artillerist feature)'
|
|
523
|
+
},
|
|
524
|
+
'infuse item': {
|
|
525
|
+
type: 'item_modification',
|
|
526
|
+
effect: 'add_magical_properties',
|
|
527
|
+
resource: 'infusions_known',
|
|
528
|
+
description: 'Imbue mundane items with magical infusions'
|
|
529
|
+
},
|
|
530
|
+
'flash of genius': {
|
|
531
|
+
type: 'reaction',
|
|
532
|
+
timing: 'ally_or_self_fails_ability_check_or_save',
|
|
533
|
+
effect: 'add_int_mod_to_roll',
|
|
534
|
+
resource: 'int_mod_uses_per_long_rest',
|
|
535
|
+
description: 'Reaction to add INT mod to check/save'
|
|
536
|
+
},
|
|
537
|
+
'magic item adept': {
|
|
538
|
+
type: 'attunement_bonus',
|
|
539
|
+
effect: 'extra_attunement_slot',
|
|
540
|
+
bonus: '+1_attunement_slot',
|
|
541
|
+
description: 'Can attune to one extra magic item'
|
|
542
|
+
},
|
|
543
|
+
'spell storing item': {
|
|
544
|
+
type: 'action_granting',
|
|
545
|
+
effect: 'cast_1st_or_2nd_level_spell_from_item',
|
|
546
|
+
uses: '2_times_int_mod',
|
|
547
|
+
reset: 'long_rest',
|
|
548
|
+
description: 'Store spell in item for allies to cast'
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
// ===== CLERIC FEATURES =====
|
|
552
|
+
'channel divinity': {
|
|
553
|
+
type: 'resource_feature',
|
|
554
|
+
resource: 'channel_divinity_uses',
|
|
555
|
+
reset: 'short_rest',
|
|
556
|
+
options: 'domain_specific',
|
|
557
|
+
description: 'Domain-specific abilities (uses reset on short rest)'
|
|
558
|
+
},
|
|
559
|
+
'turn undead': {
|
|
560
|
+
type: 'save_effect',
|
|
561
|
+
actionType: 'action',
|
|
562
|
+
saveType: 'wisdom',
|
|
563
|
+
effect: 'turned_and_flee',
|
|
564
|
+
targets: 'undead',
|
|
565
|
+
description: 'Undead fail save = turned and must flee'
|
|
566
|
+
},
|
|
567
|
+
'destroy undead': {
|
|
568
|
+
type: 'instant_kill',
|
|
569
|
+
trigger: 'undead_fails_turn_undead_save',
|
|
570
|
+
condition: 'cr_equal_or_below_threshold',
|
|
571
|
+
description: 'Destroy weak undead that fail Turn Undead'
|
|
572
|
+
},
|
|
573
|
+
'divine intervention': {
|
|
574
|
+
type: 'utility_dm_discretion',
|
|
575
|
+
trigger: 'roll_d100_equal_or_below_cleric_level',
|
|
576
|
+
cooldown: '7_days',
|
|
577
|
+
description: 'Deity intervention on successful d100 roll'
|
|
578
|
+
},
|
|
579
|
+
'preserve life': {
|
|
580
|
+
type: 'aoe_healing',
|
|
581
|
+
resource: 'channel_divinity',
|
|
582
|
+
healingFormula: '5 × cleric_level',
|
|
583
|
+
distribution: 'among_creatures',
|
|
584
|
+
description: 'Heal 5 × level distributed among creatures'
|
|
585
|
+
},
|
|
586
|
+
'wrath of the storm': {
|
|
587
|
+
type: 'reaction_damage',
|
|
588
|
+
timing: 'when_creature_within_5ft_hits_you',
|
|
589
|
+
damageTypes: ['lightning', 'thunder'],
|
|
590
|
+
description: 'Reaction lightning/thunder damage when hit within 5ft'
|
|
591
|
+
},
|
|
592
|
+
'blessed healer': {
|
|
593
|
+
type: 'self_healing',
|
|
594
|
+
trigger: 'cast_healing_spell_on_other',
|
|
595
|
+
effect: 'regain_hp_equal_to_2_plus_spell_level',
|
|
596
|
+
description: 'Heal yourself when healing others'
|
|
597
|
+
},
|
|
598
|
+
'disciple of life': {
|
|
599
|
+
type: 'healing_bonus',
|
|
600
|
+
effect: 'extra_healing',
|
|
601
|
+
formula: '2_plus_spell_level',
|
|
602
|
+
appliesTo: 'all_healing_spells',
|
|
603
|
+
description: 'Extra 2+spell level healing'
|
|
604
|
+
},
|
|
605
|
+
'potent spellcasting': {
|
|
606
|
+
type: 'damage_bonus',
|
|
607
|
+
appliesTo: 'cantrip_damage',
|
|
608
|
+
effect: 'add_wis_mod_to_damage',
|
|
609
|
+
description: 'Add WIS mod to cantrip damage'
|
|
610
|
+
},
|
|
611
|
+
'divine strike': {
|
|
612
|
+
type: 'conditional_damage',
|
|
613
|
+
condition: 'weapon_attack',
|
|
614
|
+
effect: 'extra_damage',
|
|
615
|
+
formula: '1d8_domain_dependent_2d8_at_14th_level',
|
|
616
|
+
description: 'Extra 1d8/2d8 damage (weapon dependent on domain)'
|
|
617
|
+
},
|
|
618
|
+
'corona of light': {
|
|
619
|
+
type: 'save_penalty',
|
|
620
|
+
appliesTo: 'enemy_saves',
|
|
621
|
+
effects: ['disadvantage_vs_fire', 'disadvantage_vs_radiant'],
|
|
622
|
+
condition: 'within_10_feet',
|
|
623
|
+
description: 'Enemies have disadvantage on saves vs fire/radiant'
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
// ===== WIZARD FEATURES =====
|
|
627
|
+
'arcane recovery': {
|
|
628
|
+
type: 'resource_recovery',
|
|
629
|
+
timing: 'short_rest',
|
|
630
|
+
resource: 'spell_slots',
|
|
631
|
+
formula: 'wizard_level ÷ 2_rounded_up',
|
|
632
|
+
maxLevel: 'half_wizard_level',
|
|
633
|
+
description: 'Recover spell slots during short rest'
|
|
634
|
+
},
|
|
635
|
+
'spell mastery': {
|
|
636
|
+
type: 'at_will_casting',
|
|
637
|
+
condition: 'chosen_1st_and_2nd_level_spells',
|
|
638
|
+
effect: 'cast_without_slot',
|
|
639
|
+
description: 'Cast chosen 1st/2nd level spells at will'
|
|
640
|
+
},
|
|
641
|
+
'signature spells': {
|
|
642
|
+
type: 'free_casting',
|
|
643
|
+
condition: 'two_chosen_3rd_level_spells',
|
|
644
|
+
trigger: 'no_3rd_level_slots_remaining',
|
|
645
|
+
limitation: 'once_each_per_long_rest',
|
|
646
|
+
description: 'Free casting of chosen 3rd level spells when out of slots'
|
|
647
|
+
},
|
|
648
|
+
'sculpt spells': {
|
|
649
|
+
type: 'save_modification',
|
|
650
|
+
condition: 'evocation_spell',
|
|
651
|
+
effect: 'choose_creatures_to_auto_succeed_and_take_no_damage',
|
|
652
|
+
description: 'Protect allies from evocation spells'
|
|
653
|
+
},
|
|
654
|
+
'potent cantrip': {
|
|
655
|
+
type: 'damage_modification',
|
|
656
|
+
condition: 'cantrip_save_for_half_damage',
|
|
657
|
+
effect: 'still_take_half_damage_on_save',
|
|
658
|
+
description: 'Cantrips still deal half damage on successful save'
|
|
659
|
+
},
|
|
660
|
+
'empowered evocation': {
|
|
661
|
+
type: 'damage_bonus',
|
|
662
|
+
condition: 'evocation_spell',
|
|
663
|
+
effect: 'add_int_modifier_to_one_damage_roll',
|
|
664
|
+
description: 'Add Int mod to one evocation damage roll'
|
|
665
|
+
},
|
|
666
|
+
'overchannel': {
|
|
667
|
+
type: 'max_damage',
|
|
668
|
+
condition: '1st_to_5th_level_spell',
|
|
669
|
+
effect: 'damage_rolls_are_maximum',
|
|
670
|
+
drawback: 'take_necrotic_damage',
|
|
671
|
+
description: 'Max damage but you take necrotic damage'
|
|
672
|
+
},
|
|
673
|
+
'portent': {
|
|
674
|
+
type: 'roll_replacement',
|
|
675
|
+
resource: 'portent_dice',
|
|
676
|
+
trigger: 'attack_roll_save_or_ability_check',
|
|
677
|
+
condition: 'creature_you_can_see',
|
|
678
|
+
description: 'Replace roll with portent die'
|
|
679
|
+
},
|
|
680
|
+
'expert divination': {
|
|
681
|
+
type: 'slot_recovery',
|
|
682
|
+
trigger: 'cast_divination_spell_2nd_level_or_higher',
|
|
683
|
+
effect: 'recover_spell_slot',
|
|
684
|
+
maxLevel: 'half_spell_level_rounded_down',
|
|
685
|
+
description: 'Recover spell slot after casting divination spells'
|
|
686
|
+
},
|
|
687
|
+
'benign transposition': {
|
|
688
|
+
type: 'teleport',
|
|
689
|
+
trigger: 'action_or_conjuration_spell_1st_level_or_higher',
|
|
690
|
+
distance: '30_feet',
|
|
691
|
+
options: ['teleport_self', 'swap_places_with_willing_creature'],
|
|
692
|
+
description: 'Teleport or swap places when casting conjuration'
|
|
693
|
+
},
|
|
694
|
+
'instinctive charm': {
|
|
695
|
+
type: 'reaction_redirect',
|
|
696
|
+
timing: 'when_creature_attacks_you',
|
|
697
|
+
effect: 'wisdom_save_to_redirect_attack',
|
|
698
|
+
description: 'Reaction to redirect attack to another target'
|
|
699
|
+
},
|
|
700
|
+
'illusory reality': {
|
|
701
|
+
type: 'illusion_enhancement',
|
|
702
|
+
trigger: 'cast_illusion_spell',
|
|
703
|
+
effect: 'make_illusion_object_real',
|
|
704
|
+
duration: '1_minute',
|
|
705
|
+
description: 'Make illusion object real for 1 minute'
|
|
706
|
+
},
|
|
707
|
+
'improved minor illusion': {
|
|
708
|
+
type: 'spell_enhancement',
|
|
709
|
+
appliesTo: 'minor_illusion',
|
|
710
|
+
effect: 'create_sound_and_image_simultaneously',
|
|
711
|
+
description: 'Can create sound AND image simultaneously'
|
|
712
|
+
},
|
|
713
|
+
'spell resistance': {
|
|
714
|
+
type: 'defense_bonus',
|
|
715
|
+
effects: ['advantage_on_saves_vs_spells', 'resistance_to_spell_damage'],
|
|
716
|
+
description: 'Advantage on saves vs spells, resistance to spell damage'
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
// ===== SORCERER FEATURES =====
|
|
720
|
+
'flexible casting': {
|
|
721
|
+
type: 'resource_conversion',
|
|
722
|
+
resource: 'sorcery_points',
|
|
723
|
+
conversion: 'spell_slots_to_sorcery_points_and_vice_versa',
|
|
724
|
+
description: 'Convert spell slots ↔ sorcery points'
|
|
725
|
+
},
|
|
726
|
+
'font of magic': {
|
|
727
|
+
type: 'resource_recovery',
|
|
728
|
+
timing: 'short_rest',
|
|
729
|
+
resource: 'sorcery_points',
|
|
730
|
+
amount: 'half_sorcery_points',
|
|
731
|
+
limitation: 'once_per_long_rest',
|
|
732
|
+
description: 'Regain half sorcery points on short rest'
|
|
733
|
+
},
|
|
734
|
+
'tides of chaos': {
|
|
735
|
+
type: 'advantage_with_wild_magic_trigger',
|
|
736
|
+
trigger: 'attack_roll_ability_check_or_save',
|
|
737
|
+
effect: 'advantage',
|
|
738
|
+
drawback: 'dm_may_trigger_wild_magic_surge',
|
|
739
|
+
description: 'Advantage but may trigger Wild Magic surge'
|
|
740
|
+
},
|
|
741
|
+
'metamagic variants': {
|
|
742
|
+
type: 'spell_modification_options',
|
|
743
|
+
options: [
|
|
744
|
+
{
|
|
745
|
+
name: 'quicken spell',
|
|
746
|
+
effect: 'cast_as_bonus_action',
|
|
747
|
+
cost: '2_sorcery_points'
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: 'twinned spell',
|
|
751
|
+
effect: 'target_second_creature',
|
|
752
|
+
cost: '1_sorcery_point'
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
name: 'heightened spell',
|
|
756
|
+
effect: 'disadvantage_on_save',
|
|
757
|
+
cost: '3_sorcery_points'
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: 'empowered spell',
|
|
761
|
+
effect: 'reroll_damage_dice',
|
|
762
|
+
cost: '1_sorcery_point'
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'subtle spell',
|
|
766
|
+
effect: 'no_verbal_somatic_components',
|
|
767
|
+
cost: '1_sorcery_point'
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: 'distant spell',
|
|
771
|
+
effect: 'double_range',
|
|
772
|
+
cost: '1_sorcery_point'
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: 'extended spell',
|
|
776
|
+
effect: 'double_duration',
|
|
777
|
+
cost: '1_sorcery_point'
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
description: 'Various spell modification options'
|
|
781
|
+
},
|
|
782
|
+
'elemental affinity': {
|
|
783
|
+
type: 'damage_bonus',
|
|
784
|
+
condition: 'draconic_origin_damage_type',
|
|
785
|
+
effect: 'add_cha_mod_to_one_damage_roll',
|
|
786
|
+
description: 'Add CHA mod to one damage roll of draconic type'
|
|
787
|
+
},
|
|
788
|
+
'bend luck': {
|
|
789
|
+
type: 'reaction_roll_modification',
|
|
790
|
+
timing: 'when_attack_save_or_check_made',
|
|
791
|
+
effect: 'add_or_subtract_1d4',
|
|
792
|
+
description: 'Reaction to add/subtract 1d4 from attack/save/check'
|
|
793
|
+
},
|
|
794
|
+
|
|
795
|
+
// ===== WARLOCK FEATURES =====
|
|
796
|
+
'pact magic': {
|
|
797
|
+
type: 'resource_recovery',
|
|
798
|
+
timing: 'short_rest',
|
|
799
|
+
resource: 'all_spell_slots',
|
|
800
|
+
description: 'Regain all spell slots on short rest'
|
|
801
|
+
},
|
|
802
|
+
'mystic arcanum': {
|
|
803
|
+
type: 'limited_casting',
|
|
804
|
+
resource: 'once_per_long_rest_per_spell',
|
|
805
|
+
spellLevels: [6, 7, 8, 9],
|
|
806
|
+
description: 'Cast 6th-9th level spells once per long rest each'
|
|
807
|
+
},
|
|
808
|
+
'dark one\'s blessing': {
|
|
809
|
+
type: 'healing_trigger',
|
|
810
|
+
trigger: 'reduce_creature_to_0_hp',
|
|
811
|
+
effect: 'gain_temp_hp',
|
|
812
|
+
tempHpFormula: 'warlock_level',
|
|
813
|
+
description: 'Temp HP when you reduce creature to 0'
|
|
814
|
+
},
|
|
815
|
+
'entropic ward': {
|
|
816
|
+
type: 'reaction',
|
|
817
|
+
timing: 'when_creature_succeeds_on_save_against_your_spell',
|
|
818
|
+
effect: 'impose_disadvantage_then_advantage_on_next_attack',
|
|
819
|
+
description: 'Reaction to impose disadvantage, then advantage on your next attack'
|
|
820
|
+
},
|
|
821
|
+
'thought shield': {
|
|
822
|
+
type: 'defense_bonus',
|
|
823
|
+
effects: ['resistance_to_psychic', 'reflect_psychic_damage'],
|
|
824
|
+
description: 'Resistance to psychic + reflect damage'
|
|
825
|
+
},
|
|
826
|
+
'eldritch invocations': {
|
|
827
|
+
type: 'feature_enhancement',
|
|
828
|
+
notableOptions: [
|
|
829
|
+
{
|
|
830
|
+
name: 'agonizing blast',
|
|
831
|
+
effect: 'add_cha_to_eldritch_blast_damage'
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
name: 'repelling blast',
|
|
835
|
+
effect: 'push_10ft_on_eldritch_blast_hit'
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
name: 'devil\'s sight',
|
|
839
|
+
effect: 'see_in_magical_darkness_120ft'
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: 'mask of many faces',
|
|
843
|
+
effect: 'at_will_disguise_self'
|
|
844
|
+
}
|
|
845
|
+
],
|
|
846
|
+
description: 'Various eldritch invocations (too many to list)'
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
// ===== BARD FEATURES =====
|
|
850
|
+
'bardic inspiration': {
|
|
851
|
+
type: 'resource_die',
|
|
852
|
+
resource: 'bardic_inspiration_die',
|
|
853
|
+
duration: '10_minutes',
|
|
854
|
+
trigger: 'attack_roll_ability_check_or_save',
|
|
855
|
+
effect: 'add_die_to_roll',
|
|
856
|
+
description: 'Give d6/d8/d10/d12 to add to rolls'
|
|
857
|
+
},
|
|
858
|
+
'jack of all trades': {
|
|
859
|
+
type: 'skill_bonus',
|
|
860
|
+
condition: 'non_proficient_ability_checks',
|
|
861
|
+
bonus: 'half_proficiency_bonus',
|
|
862
|
+
description: 'Add half prof bonus to non-proficient checks'
|
|
863
|
+
},
|
|
864
|
+
'song of rest': {
|
|
865
|
+
type: 'healing_bonus',
|
|
866
|
+
timing: 'short_rest',
|
|
867
|
+
condition: 'allies_regain_hp',
|
|
868
|
+
effect: 'extra_hp',
|
|
869
|
+
formula: 'bardic_inspiration_die',
|
|
870
|
+
description: 'Allies regain extra HP during short rest'
|
|
871
|
+
},
|
|
872
|
+
'countercharm': {
|
|
873
|
+
type: 'save_bonus',
|
|
874
|
+
range: '30_feet',
|
|
875
|
+
condition: 'saves_against_frightened_or_charmed',
|
|
876
|
+
effect: 'advantage',
|
|
877
|
+
targets: 'self_and_allies',
|
|
878
|
+
description: 'Advantage on saves vs frightened/charmed'
|
|
879
|
+
},
|
|
880
|
+
'cutting words': {
|
|
881
|
+
type: 'roll_subtraction',
|
|
882
|
+
trigger: 'attack_roll_ability_check_or_damage_roll',
|
|
883
|
+
condition: 'creature_you_can_see',
|
|
884
|
+
effect: 'subtract_bardic_inspiration_die',
|
|
885
|
+
description: 'Subtract Bardic Inspiration die from enemy rolls'
|
|
886
|
+
},
|
|
887
|
+
'peerless skill': {
|
|
888
|
+
type: 'roll_bonus',
|
|
889
|
+
trigger: 'ability_check',
|
|
890
|
+
effect: 'add_bardic_inspiration_die',
|
|
891
|
+
description: 'Add Bardic Inspiration die to ability check'
|
|
892
|
+
},
|
|
893
|
+
'mantle of inspiration': {
|
|
894
|
+
type: 'defensive_buff',
|
|
895
|
+
actionType: 'bonus_action',
|
|
896
|
+
cost: 'bardic_inspiration_die',
|
|
897
|
+
effects: ['temp_hp_to_allies', 'reaction_movement'],
|
|
898
|
+
description: 'Allies gain temp HP + reaction movement'
|
|
899
|
+
},
|
|
900
|
+
'magical secrets': {
|
|
901
|
+
type: 'spell_learning',
|
|
902
|
+
effect: 'learn_spells_from_other_classes',
|
|
903
|
+
description: 'Learn spells from other classes'
|
|
904
|
+
},
|
|
905
|
+
'superior inspiration': {
|
|
906
|
+
type: 'resource_recovery',
|
|
907
|
+
trigger: 'roll_initiative_with_no_inspiration_left',
|
|
908
|
+
effect: 'regain_one_use',
|
|
909
|
+
description: 'Regain one use when you roll initiative with none left'
|
|
910
|
+
},
|
|
911
|
+
'incomparable performance': {
|
|
912
|
+
type: 'social_aoe',
|
|
913
|
+
actionType: 'action',
|
|
914
|
+
range: '60_feet',
|
|
915
|
+
effects: ['charm_creatures', 'frighten_creatures'],
|
|
916
|
+
duration: '1_minute',
|
|
917
|
+
description: 'Use action to charm/frighten creatures within 60ft'
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
// ===== DRUID FEATURES =====
|
|
921
|
+
'wild shape': {
|
|
922
|
+
type: 'transformation',
|
|
923
|
+
resource: 'uses',
|
|
924
|
+
reset: 'short_rest',
|
|
925
|
+
revertCondition: 'drop_to_0_hp',
|
|
926
|
+
effect: 'revert_with_previous_hp',
|
|
927
|
+
description: 'Transform into beast, revert with previous HP at 0 HP'
|
|
928
|
+
},
|
|
929
|
+
'combat wild shape': {
|
|
930
|
+
type: 'wild_shape_enhancement',
|
|
931
|
+
actionType: 'bonus_action',
|
|
932
|
+
options: ['bonus_action_transform', 'expend_spell_slots_to_heal_in_beast_form'],
|
|
933
|
+
description: 'Bonus action Wild Shape + heal in beast form'
|
|
934
|
+
},
|
|
935
|
+
'primal strike': {
|
|
936
|
+
type: 'damage_enhancement',
|
|
937
|
+
condition: 'in_beast_form',
|
|
938
|
+
effect: 'attacks_count_as_magical',
|
|
939
|
+
description: 'Beast form attacks count as magical'
|
|
940
|
+
},
|
|
941
|
+
'elemental wild shape': {
|
|
942
|
+
type: 'wild_shape_enhancement',
|
|
943
|
+
condition: 'wild_shape_use',
|
|
944
|
+
options: ['transform_into_elemental'],
|
|
945
|
+
description: 'Wild Shape into elementals instead of beasts'
|
|
946
|
+
},
|
|
947
|
+
'thousand forms': {
|
|
948
|
+
type: 'at_will_spell',
|
|
949
|
+
spell: 'alter_self',
|
|
950
|
+
description: 'Cast Alter Self at will'
|
|
951
|
+
},
|
|
952
|
+
'circle forms': {
|
|
953
|
+
type: 'wild_shape_enhancement',
|
|
954
|
+
effect: 'higher_cr_beast_forms_based_on_level',
|
|
955
|
+
description: 'Transform into higher CR beasts based on level'
|
|
956
|
+
},
|
|
957
|
+
'symbiotic entity': {
|
|
958
|
+
type: 'alternative_wild_shape',
|
|
959
|
+
condition: 'use_wild_shape_charges',
|
|
960
|
+
effects: ['temp_hp', 'melee_damage_boost'],
|
|
961
|
+
description: 'Temp HP + damage boost instead of transforming'
|
|
962
|
+
},
|
|
963
|
+
'archdruid': {
|
|
964
|
+
type: 'wild_shape_enhancement',
|
|
965
|
+
effect: 'unlimited_wild_shapes',
|
|
966
|
+
additionalEffects: ['ignore_verbal_somatic_components'],
|
|
967
|
+
description: 'Unlimited wild shapes, ignore verbal/somatic components'
|
|
968
|
+
},
|
|
969
|
+
|
|
970
|
+
// ===== 2024 PHB CHANGES =====
|
|
971
|
+
'barbarian rage (2024)': {
|
|
972
|
+
type: 'resource_tracking',
|
|
973
|
+
resource: 'rage_points',
|
|
974
|
+
maxResource: 'barbarian_level',
|
|
975
|
+
duration: '1_minute',
|
|
976
|
+
endCondition: 'no_attack_or_damage',
|
|
977
|
+
damageBonus: 'scales_with_level',
|
|
978
|
+
ruleset: '2024',
|
|
979
|
+
description: 'Now adds more damage based on level (not just +2/+3/+4)'
|
|
980
|
+
},
|
|
981
|
+
'fighter second wind (2024)': {
|
|
982
|
+
type: 'healing',
|
|
983
|
+
actionType: 'bonus_action',
|
|
984
|
+
healingFormula: '1d10 + 2 × fighter_level',
|
|
985
|
+
resource: 'once_per_rest',
|
|
986
|
+
ruleset: '2024',
|
|
987
|
+
description: 'Now 1d10 + 2×fighter level (was 1d10 + fighter level)'
|
|
988
|
+
},
|
|
989
|
+
'monk ki (2024)': {
|
|
990
|
+
type: 'resource_tracking',
|
|
991
|
+
resource: 'discipline_points_or_focus_points',
|
|
992
|
+
ruleset: '2024',
|
|
993
|
+
description: 'Now called Discipline Points or Focus Points in some versions'
|
|
994
|
+
},
|
|
995
|
+
'paladin divine smite (2024)': {
|
|
996
|
+
type: 'resource_damage',
|
|
997
|
+
trigger: 'melee_weapon_attack_hit',
|
|
998
|
+
resource: 'spell_slot',
|
|
999
|
+
condition: 'part_of_attack_action',
|
|
1000
|
+
ruleset: '2024',
|
|
1001
|
+
description: 'Now requires using a spell slot as part of the attack action (can\'t stockpile)'
|
|
1002
|
+
},
|
|
1003
|
+
'ranger favored enemy (2024)': {
|
|
1004
|
+
type: 'advantage',
|
|
1005
|
+
appliesTo: ['survival_checks_to_track', 'intelligence_checks_to_recall_info'],
|
|
1006
|
+
condition: 'against_favored_enemy',
|
|
1007
|
+
ruleset: '2024',
|
|
1008
|
+
description: 'Completely redesigned'
|
|
1009
|
+
},
|
|
1010
|
+
'sorcerer metamagic (2024)': {
|
|
1011
|
+
type: 'spell_modification_options',
|
|
1012
|
+
ruleset: '2024',
|
|
1013
|
+
description: 'Some options changed/rebalanced'
|
|
1014
|
+
},
|
|
1015
|
+
'warlock pact boon (2024)': {
|
|
1016
|
+
type: 'feature_enhancement',
|
|
1017
|
+
ruleset: '2024',
|
|
1018
|
+
description: 'Features associated with pacts changed significantly'
|
|
1019
|
+
},
|
|
1020
|
+
'bardic inspiration (2024)': {
|
|
1021
|
+
type: 'resource_die',
|
|
1022
|
+
resource: 'bardic_inspiration_die',
|
|
1023
|
+
duration: '10_minutes',
|
|
1024
|
+
trigger: 'attack_roll_ability_check_or_save',
|
|
1025
|
+
effect: 'add_die_to_roll',
|
|
1026
|
+
recharge: 'short_rest_automatic',
|
|
1027
|
+
ruleset: '2024',
|
|
1028
|
+
description: 'Now recharges on short rest automatically'
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Check if a class feature is an edge case
|
|
1034
|
+
*/
|
|
1035
|
+
function isClassFeatureEdgeCase(featureName) {
|
|
1036
|
+
if (!featureName) return false;
|
|
1037
|
+
const normalizedLowerName = featureName.toLowerCase()
|
|
1038
|
+
.replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
|
|
1039
|
+
.replace(/\s+/g, ' ') // Normalize spaces
|
|
1040
|
+
.trim();
|
|
1041
|
+
|
|
1042
|
+
// Exact match first
|
|
1043
|
+
if (CLASS_FEATURE_EDGE_CASES.hasOwnProperty(normalizedLowerName)) {
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Special handling for Lay on Hands: Heal ONLY (not Restore or other variants)
|
|
1048
|
+
if (normalizedLowerName === 'lay on hands: heal') {
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Get class feature edge case configuration
|
|
1057
|
+
*/
|
|
1058
|
+
function getClassFeatureEdgeCase(featureName) {
|
|
1059
|
+
if (!featureName) return null;
|
|
1060
|
+
const normalizedLowerName = featureName.toLowerCase()
|
|
1061
|
+
.replace(/[^a-z0-9\s:]/g, '') // Remove special chars except colon and space
|
|
1062
|
+
.replace(/\s+/g, ' ') // Normalize spaces
|
|
1063
|
+
.trim();
|
|
1064
|
+
|
|
1065
|
+
// Exact match first
|
|
1066
|
+
if (CLASS_FEATURE_EDGE_CASES.hasOwnProperty(normalizedLowerName)) {
|
|
1067
|
+
return CLASS_FEATURE_EDGE_CASES[normalizedLowerName];
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Special handling for Lay on Hands: Heal ONLY - return the base "lay on hands" config
|
|
1071
|
+
if (normalizedLowerName === 'lay on hands: heal') {
|
|
1072
|
+
return CLASS_FEATURE_EDGE_CASES['lay on hands'] || null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Get all class features of a specific edge case type
|
|
1080
|
+
*/
|
|
1081
|
+
function getClassFeaturesByType(type) {
|
|
1082
|
+
return Object.entries(CLASS_FEATURE_EDGE_CASES)
|
|
1083
|
+
.filter(([name, config]) => config.type === type)
|
|
1084
|
+
.map(([name, config]) => ({ name, ...config }));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Get all class feature edge case types
|
|
1089
|
+
*/
|
|
1090
|
+
function getAllClassFeatureEdgeCaseTypes() {
|
|
1091
|
+
const types = new Set();
|
|
1092
|
+
Object.values(CLASS_FEATURE_EDGE_CASES).forEach(config => {
|
|
1093
|
+
types.add(config.type);
|
|
1094
|
+
});
|
|
1095
|
+
return Array.from(types);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Apply class feature edge case modifications to action options
|
|
1100
|
+
*/
|
|
1101
|
+
function applyClassFeatureEdgeCaseModifications(feature, options) {
|
|
1102
|
+
const edgeCase = getClassFeatureEdgeCase(feature.name);
|
|
1103
|
+
if (!edgeCase) {
|
|
1104
|
+
return { options, skipNormalButtons: false };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const modifiedOptions = [...options];
|
|
1108
|
+
let skipNormalButtons = false;
|
|
1109
|
+
|
|
1110
|
+
switch (edgeCase.type) {
|
|
1111
|
+
case 'utility_dm_discretion':
|
|
1112
|
+
// Skip all normal buttons, just show action button for DM-dependent features
|
|
1113
|
+
skipNormalButtons = true;
|
|
1114
|
+
break;
|
|
1115
|
+
|
|
1116
|
+
case 'resource_tracking':
|
|
1117
|
+
// Add resource tracking info
|
|
1118
|
+
modifiedOptions.forEach(opt => {
|
|
1119
|
+
opt.edgeCaseNote = `Resource: ${edgeCase.resource} (${edgeCase.maxResource})`;
|
|
1120
|
+
});
|
|
1121
|
+
break;
|
|
1122
|
+
|
|
1123
|
+
case 'advantage_disadvantage':
|
|
1124
|
+
// Add advantage/disadvantage info
|
|
1125
|
+
modifiedOptions.forEach(opt => {
|
|
1126
|
+
opt.edgeCaseNote = `⚖️ ${edgeCase.description}`;
|
|
1127
|
+
});
|
|
1128
|
+
break;
|
|
1129
|
+
|
|
1130
|
+
case 'conditional_advantage':
|
|
1131
|
+
// Add condition for advantage
|
|
1132
|
+
modifiedOptions.forEach(opt => {
|
|
1133
|
+
opt.edgeCaseNote = `✅ ${edgeCase.condition}`;
|
|
1134
|
+
});
|
|
1135
|
+
break;
|
|
1136
|
+
|
|
1137
|
+
case 'reaction':
|
|
1138
|
+
// Add reaction timing info
|
|
1139
|
+
modifiedOptions.forEach(opt => {
|
|
1140
|
+
opt.edgeCaseNote = `⚡ ${edgeCase.timing}`;
|
|
1141
|
+
});
|
|
1142
|
+
break;
|
|
1143
|
+
|
|
1144
|
+
case 'save_reroll':
|
|
1145
|
+
// Add reroll info
|
|
1146
|
+
modifiedOptions.forEach(opt => {
|
|
1147
|
+
opt.edgeCaseNote = `🔄 ${edgeCase.trigger}`;
|
|
1148
|
+
});
|
|
1149
|
+
break;
|
|
1150
|
+
|
|
1151
|
+
case 'bonus_action':
|
|
1152
|
+
// Add bonus action indicator
|
|
1153
|
+
modifiedOptions.forEach(opt => {
|
|
1154
|
+
opt.edgeCaseNote = `⚡ Bonus Action`;
|
|
1155
|
+
});
|
|
1156
|
+
break;
|
|
1157
|
+
|
|
1158
|
+
case 'divine_smite_modal':
|
|
1159
|
+
// Skip normal buttons and show custom modal
|
|
1160
|
+
skipNormalButtons = true;
|
|
1161
|
+
break;
|
|
1162
|
+
|
|
1163
|
+
case 'resource_damage':
|
|
1164
|
+
// Add resource cost info
|
|
1165
|
+
modifiedOptions.forEach(opt => {
|
|
1166
|
+
opt.edgeCaseNote = `💰 Cost: ${edgeCase.resource}`;
|
|
1167
|
+
});
|
|
1168
|
+
break;
|
|
1169
|
+
|
|
1170
|
+
case 'healing_pool':
|
|
1171
|
+
// Skip normal buttons and show custom modal for healing pool actions
|
|
1172
|
+
skipNormalButtons = true;
|
|
1173
|
+
break;
|
|
1174
|
+
|
|
1175
|
+
default:
|
|
1176
|
+
// Add description note for other types
|
|
1177
|
+
if (edgeCase.description) {
|
|
1178
|
+
modifiedOptions.forEach(opt => {
|
|
1179
|
+
opt.edgeCaseNote = edgeCase.description;
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return { options: modifiedOptions, skipNormalButtons };
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Process DiceCloud triggers and apply edge case logic
|
|
1190
|
+
* @param {Array} triggers - Array of trigger objects from DiceCloud
|
|
1191
|
+
* @param {Object} characterData - Full character data for context
|
|
1192
|
+
* @returns {Object} Processed trigger effects (expanded crit range, damage bonuses, etc.)
|
|
1193
|
+
*/
|
|
1194
|
+
function processTriggers(triggers, characterData) {
|
|
1195
|
+
if (!triggers || !Array.isArray(triggers) || triggers.length === 0) {
|
|
1196
|
+
return {
|
|
1197
|
+
expandedCritRange: null,
|
|
1198
|
+
spellDamageBonuses: [],
|
|
1199
|
+
attackModifiers: [],
|
|
1200
|
+
otherEffects: []
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const result = {
|
|
1205
|
+
expandedCritRange: null, // Will be set to 19, 18, etc. if found
|
|
1206
|
+
spellDamageBonuses: [], // Array of {type, formula, condition}
|
|
1207
|
+
attackModifiers: [], // Array of {type, formula, condition}
|
|
1208
|
+
otherEffects: [] // Other trigger effects
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
console.log(`⚡ Processing ${triggers.length} triggers...`);
|
|
1212
|
+
|
|
1213
|
+
triggers.forEach(trigger => {
|
|
1214
|
+
const triggerName = (trigger.name || '').toLowerCase();
|
|
1215
|
+
console.log(`⚡ Processing trigger: "${trigger.name}"`);
|
|
1216
|
+
|
|
1217
|
+
// Check if this trigger is an edge case
|
|
1218
|
+
const edgeCase = getClassFeatureEdgeCase(trigger.name);
|
|
1219
|
+
|
|
1220
|
+
if (edgeCase) {
|
|
1221
|
+
console.log(`⚡ Found edge case for trigger "${trigger.name}":`, edgeCase);
|
|
1222
|
+
|
|
1223
|
+
if (edgeCase.type === 'trigger') {
|
|
1224
|
+
// Handle trigger-type edge cases
|
|
1225
|
+
if (edgeCase.triggerType === 'expanded_crit_range') {
|
|
1226
|
+
// Parse crit range (e.g., "19-20" -> 19)
|
|
1227
|
+
const critRangeMatch = edgeCase.critRange.match(/(\d+)-20/);
|
|
1228
|
+
if (critRangeMatch) {
|
|
1229
|
+
const minCrit = parseInt(critRangeMatch[1]);
|
|
1230
|
+
// Use the lowest crit threshold (improved critical 19 vs superior critical 18)
|
|
1231
|
+
if (!result.expandedCritRange || minCrit < result.expandedCritRange) {
|
|
1232
|
+
result.expandedCritRange = minCrit;
|
|
1233
|
+
console.log(`⚡ Set expanded crit range to ${minCrit}-20`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} else if (edgeCase.triggerType === 'spell_damage_bonus') {
|
|
1237
|
+
// Add spell damage bonus
|
|
1238
|
+
result.spellDamageBonuses.push({
|
|
1239
|
+
name: trigger.name,
|
|
1240
|
+
formula: edgeCase.bonusDamage,
|
|
1241
|
+
spellType: edgeCase.spellType,
|
|
1242
|
+
description: edgeCase.description
|
|
1243
|
+
});
|
|
1244
|
+
console.log(`⚡ Added spell damage bonus: ${edgeCase.bonusDamage}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
// Not an edge case - parse as best we can
|
|
1249
|
+
console.log(`⚡ Trigger "${trigger.name}" is not an edge case, parsing generically...`);
|
|
1250
|
+
|
|
1251
|
+
// Generic crit range detection
|
|
1252
|
+
if (triggerName.includes('critical') || triggerName.includes('crit')) {
|
|
1253
|
+
const desc = (trigger.description || '').toLowerCase();
|
|
1254
|
+
const summary = (trigger.summary || '').toLowerCase();
|
|
1255
|
+
const fullText = `${triggerName} ${desc} ${summary}`;
|
|
1256
|
+
|
|
1257
|
+
// Look for patterns like "19 or 20", "18-20", etc.
|
|
1258
|
+
const critPatterns = [
|
|
1259
|
+
/\b(1[89])[- ]?(?:or[- ])?20\b/,
|
|
1260
|
+
/\b(1[89])[- ]?to[- ]?20\b/,
|
|
1261
|
+
/critical.*?on.*?(?:a|an)\s+(1[89]|20)/
|
|
1262
|
+
];
|
|
1263
|
+
|
|
1264
|
+
for (const pattern of critPatterns) {
|
|
1265
|
+
const match = fullText.match(pattern);
|
|
1266
|
+
if (match) {
|
|
1267
|
+
const minCrit = parseInt(match[1] || 19);
|
|
1268
|
+
if (!result.expandedCritRange || minCrit < result.expandedCritRange) {
|
|
1269
|
+
result.expandedCritRange = minCrit;
|
|
1270
|
+
console.log(`⚡ Detected expanded crit range from description: ${minCrit}-20`);
|
|
1271
|
+
}
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Generic damage bonus detection
|
|
1278
|
+
if (triggerName.includes('damage') || triggerName.includes('bonus')) {
|
|
1279
|
+
const desc = (trigger.description || '').toLowerCase();
|
|
1280
|
+
const summary = (trigger.summary || '').toLowerCase();
|
|
1281
|
+
const fullText = `${desc} ${summary}`;
|
|
1282
|
+
|
|
1283
|
+
// Look for dice patterns like "1d8", "2d6", etc.
|
|
1284
|
+
const dicePattern = /(\d+d\d+)/;
|
|
1285
|
+
const match = fullText.match(dicePattern);
|
|
1286
|
+
if (match) {
|
|
1287
|
+
result.spellDamageBonuses.push({
|
|
1288
|
+
name: trigger.name,
|
|
1289
|
+
formula: match[1],
|
|
1290
|
+
description: trigger.description,
|
|
1291
|
+
generic: true
|
|
1292
|
+
});
|
|
1293
|
+
console.log(`⚡ Detected generic damage bonus: ${match[1]}`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Store other effects for potential future handling
|
|
1298
|
+
result.otherEffects.push({
|
|
1299
|
+
name: trigger.name,
|
|
1300
|
+
description: trigger.description,
|
|
1301
|
+
condition: trigger.condition,
|
|
1302
|
+
raw: trigger.raw
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
console.log('⚡ Trigger processing complete:', result);
|
|
1308
|
+
return result;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Expose to globalThis for importScripts usage
|
|
1312
|
+
if (typeof globalThis !== 'undefined') {
|
|
1313
|
+
globalThis.CLASS_FEATURE_EDGE_CASES = CLASS_FEATURE_EDGE_CASES;
|
|
1314
|
+
globalThis.isClassFeatureEdgeCase = isClassFeatureEdgeCase;
|
|
1315
|
+
globalThis.getClassFeatureEdgeCase = getClassFeatureEdgeCase;
|
|
1316
|
+
globalThis.applyClassFeatureEdgeCaseModifications = applyClassFeatureEdgeCaseModifications;
|
|
1317
|
+
globalThis.getClassFeaturesByType = getClassFeaturesByType;
|
|
1318
|
+
globalThis.getAllClassFeatureEdgeCaseTypes = getAllClassFeatureEdgeCaseTypes;
|
|
1319
|
+
globalThis.processTriggers = processTriggers;
|
|
1320
|
+
}
|