@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,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effects Manager Module
|
|
3
|
+
*
|
|
4
|
+
* Handles buffs, debuffs, and conditions tracking.
|
|
5
|
+
* Loaded as a plain script (no ES6 modules) to export to window.
|
|
6
|
+
*
|
|
7
|
+
* Functions exported to globalThis:
|
|
8
|
+
* - initConditionsManager()
|
|
9
|
+
* - showEffectsModal()
|
|
10
|
+
* - addEffect(effectName, type)
|
|
11
|
+
* - removeEffect(effectName, type)
|
|
12
|
+
* - addCondition(conditionName) - legacy wrapper
|
|
13
|
+
* - removeCondition(conditionName) - legacy wrapper
|
|
14
|
+
* - updateEffectsDisplay()
|
|
15
|
+
* - updateConditionsDisplay() - legacy wrapper
|
|
16
|
+
* - calculateTotalAC()
|
|
17
|
+
*
|
|
18
|
+
* Constants exported to globalThis:
|
|
19
|
+
* - POSITIVE_EFFECTS
|
|
20
|
+
* - NEGATIVE_EFFECTS
|
|
21
|
+
*
|
|
22
|
+
* State variables exported to globalThis:
|
|
23
|
+
* - activeBuffs
|
|
24
|
+
* - activeConditions
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
(function() {
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
// ===== EFFECTS CONSTANTS =====
|
|
31
|
+
|
|
32
|
+
const POSITIVE_EFFECTS = [
|
|
33
|
+
{
|
|
34
|
+
name: 'Bless',
|
|
35
|
+
icon: '✨',
|
|
36
|
+
color: '#f39c12',
|
|
37
|
+
description: '+1d4 to attack rolls and saving throws',
|
|
38
|
+
modifier: { attack: '1d4', save: '1d4' },
|
|
39
|
+
autoApply: true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Guidance',
|
|
43
|
+
icon: '🙏',
|
|
44
|
+
color: '#3498db',
|
|
45
|
+
description: '+1d4 to one ability check',
|
|
46
|
+
modifier: { skill: '1d4' },
|
|
47
|
+
autoApply: false // User choice required
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Bardic Inspiration (d6)',
|
|
51
|
+
icon: '🎵',
|
|
52
|
+
color: '#9b59b6',
|
|
53
|
+
description: 'Bard levels 1-4: +d6 to ability check, attack, or save',
|
|
54
|
+
modifier: { attack: 'd6', skill: 'd6', save: 'd6' },
|
|
55
|
+
autoApply: false
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Bardic Inspiration (d8)',
|
|
59
|
+
icon: '🎵',
|
|
60
|
+
color: '#9b59b6',
|
|
61
|
+
description: 'Bard levels 5-9: +d8 to ability check, attack, or save',
|
|
62
|
+
modifier: { attack: 'd8', skill: 'd8', save: 'd8' },
|
|
63
|
+
autoApply: false
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Bardic Inspiration (d10)',
|
|
67
|
+
icon: '🎵',
|
|
68
|
+
color: '#9b59b6',
|
|
69
|
+
description: 'Bard levels 10-14: +d10 to ability check, attack, or save',
|
|
70
|
+
modifier: { attack: 'd10', skill: 'd10', save: 'd10' },
|
|
71
|
+
autoApply: false
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Bardic Inspiration (d12)',
|
|
75
|
+
icon: '🎵',
|
|
76
|
+
color: '#9b59b6',
|
|
77
|
+
description: 'Bard levels 15-20: +d12 to ability check, attack, or save',
|
|
78
|
+
modifier: { attack: 'd12', skill: 'd12', save: 'd12' },
|
|
79
|
+
autoApply: false
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Haste',
|
|
83
|
+
icon: '⚡',
|
|
84
|
+
color: '#3498db',
|
|
85
|
+
description: '+2 AC, advantage on DEX saves, extra action',
|
|
86
|
+
modifier: { ac: 2, dexSave: 'advantage' },
|
|
87
|
+
autoApply: true
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Enlarge',
|
|
91
|
+
icon: '⬆️',
|
|
92
|
+
color: '#27ae60',
|
|
93
|
+
description: '+1d4 weapon damage, advantage on STR checks/saves',
|
|
94
|
+
modifier: { damage: '1d4', strCheck: 'advantage', strSave: 'advantage' },
|
|
95
|
+
autoApply: true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'Invisibility',
|
|
99
|
+
icon: '👻',
|
|
100
|
+
color: '#ecf0f1',
|
|
101
|
+
description: 'Advantage on attack rolls, enemies have disadvantage',
|
|
102
|
+
modifier: { attack: 'advantage' },
|
|
103
|
+
autoApply: true
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Shield of Faith',
|
|
107
|
+
icon: '🛡️',
|
|
108
|
+
color: '#f39c12',
|
|
109
|
+
description: '+2 AC',
|
|
110
|
+
modifier: { ac: 2 },
|
|
111
|
+
autoApply: true
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'Heroism',
|
|
115
|
+
icon: '🦸',
|
|
116
|
+
color: '#e67e22',
|
|
117
|
+
description: 'Immune to frightened, temp HP each turn',
|
|
118
|
+
modifier: { frightened: 'immune' },
|
|
119
|
+
autoApply: true
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Enhance Ability',
|
|
123
|
+
icon: '💪',
|
|
124
|
+
color: '#27ae60',
|
|
125
|
+
description: 'Advantage on ability checks with chosen ability',
|
|
126
|
+
modifier: { skill: 'advantage' },
|
|
127
|
+
autoApply: false
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'Rage',
|
|
131
|
+
icon: '😡',
|
|
132
|
+
color: '#e74c3c',
|
|
133
|
+
description: '+2 damage on melee attacks, advantage on STR checks/saves, resistance to physical damage',
|
|
134
|
+
modifier: { damage: 2, strCheck: 'advantage', strSave: 'advantage', physicalResistance: true },
|
|
135
|
+
autoApply: true
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Rage (+3)',
|
|
139
|
+
icon: '😤',
|
|
140
|
+
color: '#c0392b',
|
|
141
|
+
description: 'Level 9-15: +3 damage on melee attacks, advantage on STR checks/saves, resistance to physical damage',
|
|
142
|
+
modifier: { damage: 3, strCheck: 'advantage', strSave: 'advantage', physicalResistance: true },
|
|
143
|
+
autoApply: true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'Rage (+4)',
|
|
147
|
+
icon: '🔥',
|
|
148
|
+
color: '#8b0000',
|
|
149
|
+
description: 'Level 16+: +4 damage on melee attacks, advantage on STR checks/saves, resistance to physical damage',
|
|
150
|
+
modifier: { damage: 4, strCheck: 'advantage', strSave: 'advantage', physicalResistance: true },
|
|
151
|
+
autoApply: true
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'Aid',
|
|
155
|
+
icon: '❤️',
|
|
156
|
+
color: '#e74c3c',
|
|
157
|
+
description: 'Max HP increased by 5',
|
|
158
|
+
modifier: { maxHp: 5 },
|
|
159
|
+
autoApply: true
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'True Strike',
|
|
163
|
+
icon: '🎯',
|
|
164
|
+
color: '#3498db',
|
|
165
|
+
description: 'Advantage on next attack roll',
|
|
166
|
+
modifier: { attack: 'advantage' },
|
|
167
|
+
autoApply: true
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'Faerie Fire',
|
|
171
|
+
icon: '✨',
|
|
172
|
+
color: '#9b59b6',
|
|
173
|
+
description: 'Attackers have advantage against target',
|
|
174
|
+
modifier: {},
|
|
175
|
+
autoApply: false
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// NEGATIVE EFFECTS (Debuffs/Conditions)
|
|
180
|
+
const NEGATIVE_EFFECTS = [
|
|
181
|
+
{
|
|
182
|
+
name: 'Bane',
|
|
183
|
+
icon: '💀',
|
|
184
|
+
color: '#e74c3c',
|
|
185
|
+
description: '-1d4 to attack rolls and saving throws',
|
|
186
|
+
modifier: { attack: '-1d4', save: '-1d4' },
|
|
187
|
+
autoApply: true
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'Poisoned',
|
|
191
|
+
icon: '☠️',
|
|
192
|
+
color: '#27ae60',
|
|
193
|
+
description: 'Disadvantage on attack rolls and ability checks',
|
|
194
|
+
modifier: { attack: 'disadvantage', skill: 'disadvantage' },
|
|
195
|
+
autoApply: true
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'Frightened',
|
|
199
|
+
icon: '😱',
|
|
200
|
+
color: '#e67e22',
|
|
201
|
+
description: 'Disadvantage on ability checks and attack rolls',
|
|
202
|
+
modifier: { attack: 'disadvantage', skill: 'disadvantage' },
|
|
203
|
+
autoApply: true
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'Stunned',
|
|
207
|
+
icon: '💫',
|
|
208
|
+
color: '#9b59b6',
|
|
209
|
+
description: 'Incapacitated, auto-fail STR/DEX saves, attackers have advantage',
|
|
210
|
+
modifier: { strSave: 'fail', dexSave: 'fail' },
|
|
211
|
+
autoApply: true
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'Paralyzed',
|
|
215
|
+
icon: '🧊',
|
|
216
|
+
color: '#34495e',
|
|
217
|
+
description: 'Incapacitated, auto-fail STR/DEX saves, attacks within 5ft are crits',
|
|
218
|
+
modifier: { strSave: 'fail', dexSave: 'fail' },
|
|
219
|
+
autoApply: true
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Restrained',
|
|
223
|
+
icon: '⛓️',
|
|
224
|
+
color: '#7f8c8d',
|
|
225
|
+
description: 'Disadvantage on DEX saves and attack rolls',
|
|
226
|
+
modifier: { attack: 'disadvantage', dexSave: 'disadvantage' },
|
|
227
|
+
autoApply: true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'Blinded',
|
|
231
|
+
icon: '🙈',
|
|
232
|
+
color: '#34495e',
|
|
233
|
+
description: 'Auto-fail sight checks, disadvantage on attacks',
|
|
234
|
+
modifier: { attack: 'disadvantage', perception: 'disadvantage' },
|
|
235
|
+
autoApply: true
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'Deafened',
|
|
239
|
+
icon: '🙉',
|
|
240
|
+
color: '#7f8c8d',
|
|
241
|
+
description: 'Auto-fail hearing checks',
|
|
242
|
+
modifier: { perception: 'disadvantage' },
|
|
243
|
+
autoApply: true
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'Charmed',
|
|
247
|
+
icon: '💖',
|
|
248
|
+
color: '#e91e63',
|
|
249
|
+
description: 'Cannot attack charmer, charmer has advantage on social checks',
|
|
250
|
+
modifier: {},
|
|
251
|
+
autoApply: false
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'Grappled',
|
|
255
|
+
icon: '🤼',
|
|
256
|
+
color: '#f39c12',
|
|
257
|
+
description: 'Speed becomes 0',
|
|
258
|
+
modifier: { speed: 0 },
|
|
259
|
+
autoApply: true
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'Prone',
|
|
263
|
+
icon: '⬇️',
|
|
264
|
+
color: '#95a5a6',
|
|
265
|
+
description: 'Disadvantage on attack rolls, melee attacks against you have advantage',
|
|
266
|
+
modifier: { attack: 'disadvantage' },
|
|
267
|
+
autoApply: true
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'Incapacitated',
|
|
271
|
+
icon: '😵',
|
|
272
|
+
color: '#c0392b',
|
|
273
|
+
description: 'Cannot take actions or reactions',
|
|
274
|
+
modifier: {},
|
|
275
|
+
autoApply: false
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'Unconscious',
|
|
279
|
+
icon: '😴',
|
|
280
|
+
color: '#34495e',
|
|
281
|
+
description: 'Incapacitated, drop everything, auto-fail STR/DEX saves',
|
|
282
|
+
modifier: { strSave: 'fail', dexSave: 'fail' },
|
|
283
|
+
autoApply: true
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'Petrified',
|
|
287
|
+
icon: '🗿',
|
|
288
|
+
color: '#95a5a6',
|
|
289
|
+
description: 'Incapacitated, auto-fail STR/DEX saves, resistance to all damage',
|
|
290
|
+
modifier: { strSave: 'fail', dexSave: 'fail' },
|
|
291
|
+
autoApply: true
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'Slowed',
|
|
295
|
+
icon: '🐌',
|
|
296
|
+
color: '#95a5a6',
|
|
297
|
+
description: 'Speed halved, -2 AC and DEX saves, no reactions',
|
|
298
|
+
modifier: { ac: -2, dexSave: '-2' },
|
|
299
|
+
autoApply: true
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'Hexed',
|
|
303
|
+
icon: '🔮',
|
|
304
|
+
color: '#9b59b6',
|
|
305
|
+
description: 'Disadvantage on ability checks with chosen ability, extra damage to caster',
|
|
306
|
+
modifier: { skill: 'disadvantage' },
|
|
307
|
+
autoApply: false
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'Cursed',
|
|
311
|
+
icon: '😈',
|
|
312
|
+
color: '#c0392b',
|
|
313
|
+
description: 'Disadvantage on attacks and saves against caster',
|
|
314
|
+
modifier: { attack: 'disadvantage', save: 'disadvantage' },
|
|
315
|
+
autoApply: true
|
|
316
|
+
}
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
// ===== STATE VARIABLES =====
|
|
320
|
+
|
|
321
|
+
let activeConditions = [];
|
|
322
|
+
let activeBuffs = [];
|
|
323
|
+
|
|
324
|
+
// ===== EFFECTS MANAGEMENT FUNCTIONS =====
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Initialize conditions manager UI
|
|
328
|
+
*/
|
|
329
|
+
function initConditionsManager() {
|
|
330
|
+
const addConditionBtn = document.getElementById('add-condition-btn');
|
|
331
|
+
|
|
332
|
+
if (addConditionBtn) {
|
|
333
|
+
// Open modal when clicking conditions button
|
|
334
|
+
addConditionBtn.addEventListener('click', (e) => {
|
|
335
|
+
e.stopPropagation();
|
|
336
|
+
showEffectsModal();
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
debug.log('✅ Effects manager initialized (buffs + debuffs)');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Show effects modal for adding buffs and debuffs
|
|
345
|
+
*/
|
|
346
|
+
function showEffectsModal() {
|
|
347
|
+
// Create modal overlay
|
|
348
|
+
const modal = document.createElement('div');
|
|
349
|
+
modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 10000;';
|
|
350
|
+
|
|
351
|
+
// Create modal content
|
|
352
|
+
const modalContent = document.createElement('div');
|
|
353
|
+
modalContent.style.cssText = 'background: var(--bg-secondary); color: var(--text-primary); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); width: 90%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden;';
|
|
354
|
+
|
|
355
|
+
// Modal header
|
|
356
|
+
const header = document.createElement('div');
|
|
357
|
+
header.style.cssText = 'padding: 20px; border-bottom: 2px solid #ecf0f1; background: #f8f9fa;';
|
|
358
|
+
header.innerHTML = `
|
|
359
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
360
|
+
<h3 style="margin: 0; color: var(--text-primary);">🎭 Effects & Conditions</h3>
|
|
361
|
+
<button id="effects-modal-close" style="background: #e74c3c; color: white; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-weight: bold;">✕</button>
|
|
362
|
+
</div>
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
// Tab navigation
|
|
366
|
+
const tabNav = document.createElement('div');
|
|
367
|
+
tabNav.style.cssText = 'display: flex; background: #ecf0f1; border-bottom: 2px solid #bdc3c7;';
|
|
368
|
+
tabNav.innerHTML = `
|
|
369
|
+
<button class="effects-tab-btn" data-tab="buffs" style="flex: 1; padding: 15px; background: var(--bg-tertiary); border: none; border-bottom: 3px solid #27ae60; cursor: pointer; font-weight: bold; font-size: 1em; color: #27ae60; transition: all 0.2s;">✨ Buffs</button>
|
|
370
|
+
<button class="effects-tab-btn" data-tab="debuffs" style="flex: 1; padding: 15px; background: transparent; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-weight: bold; font-size: 1em; color: var(--text-secondary); transition: all 0.2s;">💀 Debuffs</button>
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
// Tab content container
|
|
374
|
+
const tabContent = document.createElement('div');
|
|
375
|
+
tabContent.style.cssText = 'padding: 20px; overflow-y: auto; flex: 1;';
|
|
376
|
+
|
|
377
|
+
// Buffs tab
|
|
378
|
+
const buffsTab = document.createElement('div');
|
|
379
|
+
buffsTab.className = 'effects-tab-content';
|
|
380
|
+
buffsTab.dataset.tab = 'buffs';
|
|
381
|
+
buffsTab.style.display = 'block';
|
|
382
|
+
buffsTab.innerHTML = POSITIVE_EFFECTS.map(effect => `
|
|
383
|
+
<div class="effect-option" data-effect="${effect.name}" data-type="positive" style="padding: 12px; margin-bottom: 10px; border: 2px solid ${effect.color}40; border-radius: 8px; cursor: pointer; transition: all 0.2s; background: var(--bg-secondary);">
|
|
384
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
385
|
+
<span class="effect-icon" style="font-size: 1.5em;">${effect.icon}</span>
|
|
386
|
+
<div style="flex: 1;">
|
|
387
|
+
<div class="effect-name" style="font-weight: bold; color: var(--text-primary); margin-bottom: 4px;">${effect.name}</div>
|
|
388
|
+
<div class="effect-description" style="font-size: 0.85em; color: var(--text-secondary);">${effect.description}</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
`).join('');
|
|
393
|
+
|
|
394
|
+
// Debuffs tab
|
|
395
|
+
const debuffsTab = document.createElement('div');
|
|
396
|
+
debuffsTab.className = 'effects-tab-content';
|
|
397
|
+
debuffsTab.dataset.tab = 'debuffs';
|
|
398
|
+
debuffsTab.style.display = 'none';
|
|
399
|
+
debuffsTab.innerHTML = NEGATIVE_EFFECTS.map(effect => `
|
|
400
|
+
<div class="effect-option" data-effect="${effect.name}" data-type="negative" style="padding: 12px; margin-bottom: 10px; border: 2px solid ${effect.color}40; border-radius: 8px; cursor: pointer; transition: all 0.2s; background: var(--bg-secondary);">
|
|
401
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
402
|
+
<span class="effect-icon" style="font-size: 1.5em;">${effect.icon}</span>
|
|
403
|
+
<div style="flex: 1;">
|
|
404
|
+
<div class="effect-name" style="font-weight: bold; color: var(--text-primary); margin-bottom: 4px;">${effect.name}</div>
|
|
405
|
+
<div class="effect-description" style="font-size: 0.85em; color: var(--text-secondary);">${effect.description}</div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
`).join('');
|
|
410
|
+
|
|
411
|
+
tabContent.appendChild(buffsTab);
|
|
412
|
+
tabContent.appendChild(debuffsTab);
|
|
413
|
+
|
|
414
|
+
// Assemble modal
|
|
415
|
+
modalContent.appendChild(header);
|
|
416
|
+
modalContent.appendChild(tabNav);
|
|
417
|
+
modalContent.appendChild(tabContent);
|
|
418
|
+
modal.appendChild(modalContent);
|
|
419
|
+
document.body.appendChild(modal);
|
|
420
|
+
|
|
421
|
+
// Tab switching
|
|
422
|
+
const tabButtons = tabNav.querySelectorAll('.effects-tab-btn');
|
|
423
|
+
const tabContents = modalContent.querySelectorAll('.effects-tab-content');
|
|
424
|
+
|
|
425
|
+
tabButtons.forEach(btn => {
|
|
426
|
+
btn.addEventListener('click', () => {
|
|
427
|
+
const targetTab = btn.dataset.tab;
|
|
428
|
+
|
|
429
|
+
// Update button styles
|
|
430
|
+
tabButtons.forEach(b => {
|
|
431
|
+
if (b.dataset.tab === targetTab) {
|
|
432
|
+
b.style.background = 'var(--bg-tertiary)';
|
|
433
|
+
b.style.color = targetTab === 'buffs' ? '#27ae60' : '#e74c3c';
|
|
434
|
+
b.style.borderBottom = `3px solid ${targetTab === 'buffs' ? '#27ae60' : '#e74c3c'}`;
|
|
435
|
+
} else {
|
|
436
|
+
b.style.background = 'transparent';
|
|
437
|
+
b.style.color = '#7f8c8d';
|
|
438
|
+
b.style.borderBottom = '3px solid transparent';
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Show target tab content
|
|
443
|
+
tabContents.forEach(content => {
|
|
444
|
+
content.style.display = content.dataset.tab === targetTab ? 'block' : 'none';
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Add hover effects
|
|
450
|
+
modalContent.querySelectorAll('.effect-option').forEach(option => {
|
|
451
|
+
option.addEventListener('mouseenter', () => {
|
|
452
|
+
option.style.transform = 'translateX(5px)';
|
|
453
|
+
option.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
|
454
|
+
});
|
|
455
|
+
option.addEventListener('mouseleave', () => {
|
|
456
|
+
option.style.transform = 'translateX(0)';
|
|
457
|
+
option.style.boxShadow = 'none';
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Add effect when clicking option
|
|
462
|
+
modalContent.querySelectorAll('.effect-option').forEach(option => {
|
|
463
|
+
option.addEventListener('click', () => {
|
|
464
|
+
const effectName = option.dataset.effect;
|
|
465
|
+
const type = option.dataset.type === 'positive' ? 'positive' : 'negative';
|
|
466
|
+
addEffect(effectName, type);
|
|
467
|
+
modal.remove();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Close button
|
|
472
|
+
const closeBtn = modalContent.querySelector('#effects-modal-close');
|
|
473
|
+
closeBtn.addEventListener('click', () => modal.remove());
|
|
474
|
+
|
|
475
|
+
// Click outside to close
|
|
476
|
+
modal.addEventListener('click', (e) => {
|
|
477
|
+
if (e.target === modal) {
|
|
478
|
+
modal.remove();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Add an effect (buff or debuff)
|
|
485
|
+
*/
|
|
486
|
+
function addEffect(effectName, type) {
|
|
487
|
+
// characterData should be available from global scope
|
|
488
|
+
if (typeof characterData === 'undefined') {
|
|
489
|
+
debug.warn('⚠️ characterData not available');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const effectsList = type === 'positive' ? POSITIVE_EFFECTS : NEGATIVE_EFFECTS;
|
|
494
|
+
const activeList = type === 'positive' ? activeBuffs : activeConditions;
|
|
495
|
+
|
|
496
|
+
// Don't add if already active
|
|
497
|
+
if (activeList.includes(effectName)) {
|
|
498
|
+
if (typeof showNotification !== 'undefined') {
|
|
499
|
+
showNotification(`⚠️ ${effectName} already active`);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const effect = effectsList.find(e => e.name === effectName);
|
|
505
|
+
activeList.push(effectName);
|
|
506
|
+
|
|
507
|
+
// Update the correct array reference
|
|
508
|
+
if (type === 'positive') {
|
|
509
|
+
activeBuffs = activeList;
|
|
510
|
+
} else {
|
|
511
|
+
activeConditions = activeList;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
updateEffectsDisplay();
|
|
515
|
+
if (typeof showNotification !== 'undefined') {
|
|
516
|
+
showNotification(`${effect.icon} ${effectName} applied!`);
|
|
517
|
+
}
|
|
518
|
+
debug.log(`✅ Effect added: ${effectName} (${type})`);
|
|
519
|
+
|
|
520
|
+
// TODO: Add Owlbear Rodeo integration for effect announcements
|
|
521
|
+
|
|
522
|
+
// Save to character data
|
|
523
|
+
if (!characterData.activeEffects) {
|
|
524
|
+
characterData.activeEffects = { buffs: [], debuffs: [] };
|
|
525
|
+
}
|
|
526
|
+
if (type === 'positive') {
|
|
527
|
+
characterData.activeEffects.buffs = activeBuffs;
|
|
528
|
+
} else {
|
|
529
|
+
characterData.activeEffects.debuffs = activeConditions;
|
|
530
|
+
}
|
|
531
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
532
|
+
saveCharacterData();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Remove an effect (buff or debuff)
|
|
538
|
+
*/
|
|
539
|
+
function removeEffect(effectName, type) {
|
|
540
|
+
// characterData should be available from global scope
|
|
541
|
+
if (typeof characterData === 'undefined') {
|
|
542
|
+
debug.warn('⚠️ characterData not available');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const effectsList = type === 'positive' ? POSITIVE_EFFECTS : NEGATIVE_EFFECTS;
|
|
547
|
+
const effect = effectsList.find(e => e.name === effectName);
|
|
548
|
+
|
|
549
|
+
if (type === 'positive') {
|
|
550
|
+
activeBuffs = activeBuffs.filter(e => e !== effectName);
|
|
551
|
+
} else {
|
|
552
|
+
activeConditions = activeConditions.filter(e => e !== effectName);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
updateEffectsDisplay();
|
|
556
|
+
if (typeof showNotification !== 'undefined') {
|
|
557
|
+
showNotification(`✅ ${effectName} removed`);
|
|
558
|
+
}
|
|
559
|
+
debug.log(`🗑️ Effect removed: ${effectName} (${type})`);
|
|
560
|
+
|
|
561
|
+
// TODO: Add Owlbear Rodeo integration for effect removal announcements
|
|
562
|
+
|
|
563
|
+
// Save to character data
|
|
564
|
+
if (!characterData.activeEffects) {
|
|
565
|
+
characterData.activeEffects = { buffs: [], debuffs: [] };
|
|
566
|
+
}
|
|
567
|
+
if (type === 'positive') {
|
|
568
|
+
characterData.activeEffects.buffs = activeBuffs;
|
|
569
|
+
} else {
|
|
570
|
+
characterData.activeEffects.debuffs = activeConditions;
|
|
571
|
+
}
|
|
572
|
+
if (typeof saveCharacterData !== 'undefined') {
|
|
573
|
+
saveCharacterData();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Legacy function for backwards compatibility
|
|
579
|
+
*/
|
|
580
|
+
function addCondition(conditionName) {
|
|
581
|
+
addEffect(conditionName, 'negative');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Legacy function for backwards compatibility
|
|
586
|
+
*/
|
|
587
|
+
function removeCondition(conditionName) {
|
|
588
|
+
removeEffect(conditionName, 'negative');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Update effects display UI
|
|
593
|
+
*/
|
|
594
|
+
function updateEffectsDisplay() {
|
|
595
|
+
const container = document.getElementById('active-conditions');
|
|
596
|
+
if (!container) return;
|
|
597
|
+
|
|
598
|
+
let html = '';
|
|
599
|
+
|
|
600
|
+
// Show buffs section
|
|
601
|
+
if (activeBuffs.length > 0) {
|
|
602
|
+
html += '<div style="margin-bottom: 15px;">';
|
|
603
|
+
html += '<div style="font-size: 0.85em; font-weight: bold; color: #27ae60; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;"><span>✨</span> BUFFS</div>';
|
|
604
|
+
html += activeBuffs.map(effectName => {
|
|
605
|
+
const effect = POSITIVE_EFFECTS.find(e => e.name === effectName);
|
|
606
|
+
return `
|
|
607
|
+
<div class="effect-badge" data-effect="${effectName}" data-type="positive" title="${effect.description} - Click to remove" style="background: ${effect.color}20; border: 2px solid ${effect.color}; cursor: pointer; padding: 8px 12px; border-radius: 6px; margin-bottom: 8px; transition: all 0.2s;">
|
|
608
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
609
|
+
<span class="effect-badge-icon" style="font-size: 1.2em;">${effect.icon}</span>
|
|
610
|
+
<div style="flex: 1;">
|
|
611
|
+
<div style="font-weight: bold; color: var(--text-primary);">${effect.name}</div>
|
|
612
|
+
<div style="font-size: 0.75em; color: var(--text-secondary); margin-top: 2px;">${effect.description}</div>
|
|
613
|
+
</div>
|
|
614
|
+
<span class="effect-badge-remove" style="font-weight: bold; opacity: 0.7; color: #e74c3c;">✕</span>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
`;
|
|
618
|
+
}).join('');
|
|
619
|
+
html += '</div>';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Show debuffs section
|
|
623
|
+
if (activeConditions.length > 0) {
|
|
624
|
+
html += '<div style="margin-bottom: 15px;">';
|
|
625
|
+
html += '<div style="font-size: 0.85em; font-weight: bold; color: #e74c3c; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;"><span>💀</span> DEBUFFS</div>';
|
|
626
|
+
html += activeConditions.map(effectName => {
|
|
627
|
+
const effect = NEGATIVE_EFFECTS.find(e => e.name === effectName);
|
|
628
|
+
return `
|
|
629
|
+
<div class="effect-badge" data-effect="${effectName}" data-type="negative" title="${effect.description} - Click to remove" style="background: ${effect.color}20; border: 2px solid ${effect.color}; cursor: pointer; padding: 8px 12px; border-radius: 6px; margin-bottom: 8px; transition: all 0.2s;">
|
|
630
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
631
|
+
<span class="effect-badge-icon" style="font-size: 1.2em;">${effect.icon}</span>
|
|
632
|
+
<div style="flex: 1;">
|
|
633
|
+
<div style="font-weight: bold; color: var(--text-primary);">${effect.name}</div>
|
|
634
|
+
<div style="font-size: 0.75em; color: var(--text-secondary); margin-top: 2px;">${effect.description}</div>
|
|
635
|
+
</div>
|
|
636
|
+
<span class="effect-badge-remove" style="font-weight: bold; opacity: 0.7; color: #e74c3c;">✕</span>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
`;
|
|
640
|
+
}).join('');
|
|
641
|
+
html += '</div>';
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Show empty state if no effects
|
|
645
|
+
if (activeBuffs.length === 0 && activeConditions.length === 0) {
|
|
646
|
+
html = '<div style="text-align: center; color: #888; padding: 15px; font-size: 0.9em;">No active effects</div>';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
container.innerHTML = html;
|
|
650
|
+
|
|
651
|
+
// Update AC display to reflect any changes
|
|
652
|
+
const acElement = document.getElementById('char-ac');
|
|
653
|
+
if (acElement && typeof calculateTotalAC !== 'undefined') {
|
|
654
|
+
acElement.textContent = calculateTotalAC();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Add click handlers to remove effects
|
|
658
|
+
container.querySelectorAll('.effect-badge').forEach(badge => {
|
|
659
|
+
const effectName = badge.dataset.effect;
|
|
660
|
+
const type = badge.dataset.type;
|
|
661
|
+
|
|
662
|
+
// Add hover effect
|
|
663
|
+
badge.addEventListener('mouseenter', () => {
|
|
664
|
+
badge.style.transform = 'translateX(3px)';
|
|
665
|
+
badge.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
|
|
666
|
+
});
|
|
667
|
+
badge.addEventListener('mouseleave', () => {
|
|
668
|
+
badge.style.transform = 'translateX(0)';
|
|
669
|
+
badge.style.boxShadow = 'none';
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Remove on click
|
|
673
|
+
badge.addEventListener('click', () => {
|
|
674
|
+
removeEffect(effectName, type);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Legacy function for backwards compatibility
|
|
681
|
+
*/
|
|
682
|
+
function updateConditionsDisplay() {
|
|
683
|
+
updateEffectsDisplay();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Calculate total AC including active effects
|
|
688
|
+
* @param {Object} data - Character data object (optional, uses globalThis.characterData if not provided)
|
|
689
|
+
* @returns {number} Total AC with effect modifiers applied
|
|
690
|
+
*/
|
|
691
|
+
function calculateTotalAC(data) {
|
|
692
|
+
// Use provided data or fall back to global character data
|
|
693
|
+
const charData = data || globalThis.characterData;
|
|
694
|
+
const baseAC = charData?.armorClass || 10;
|
|
695
|
+
let totalAC = baseAC;
|
|
696
|
+
|
|
697
|
+
// Combine all active effects
|
|
698
|
+
const allEffects = [
|
|
699
|
+
...activeBuffs.map(name => ({ ...POSITIVE_EFFECTS.find(e => e.name === name), type: 'buff' })),
|
|
700
|
+
...activeConditions.map(name => ({ ...NEGATIVE_EFFECTS.find(e => e.name === name), type: 'debuff' }))
|
|
701
|
+
].filter(e => e && e.autoApply && e.modifier && e.modifier.ac);
|
|
702
|
+
|
|
703
|
+
// Apply AC modifiers from active effects
|
|
704
|
+
for (const effect of allEffects) {
|
|
705
|
+
const acMod = effect.modifier.ac;
|
|
706
|
+
if (typeof acMod === 'number') {
|
|
707
|
+
totalAC += acMod;
|
|
708
|
+
debug.log(`🛡️ Applied AC modifier: ${acMod} from ${effect.name} (${effect.type})`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
debug.log(`🛡️ Total AC calculation: ${baseAC} (base) + modifiers = ${totalAC}`);
|
|
713
|
+
return totalAC;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ===== EXPORTS =====
|
|
717
|
+
|
|
718
|
+
window.initConditionsManager = initConditionsManager;
|
|
719
|
+
window.showEffectsModal = showEffectsModal;
|
|
720
|
+
window.addEffect = addEffect;
|
|
721
|
+
window.removeEffect = removeEffect;
|
|
722
|
+
window.addCondition = addCondition;
|
|
723
|
+
window.removeCondition = removeCondition;
|
|
724
|
+
window.updateEffectsDisplay = updateEffectsDisplay;
|
|
725
|
+
window.updateConditionsDisplay = updateConditionsDisplay;
|
|
726
|
+
window.calculateTotalAC = calculateTotalAC;
|
|
727
|
+
|
|
728
|
+
// Export constants
|
|
729
|
+
window.POSITIVE_EFFECTS = POSITIVE_EFFECTS;
|
|
730
|
+
window.NEGATIVE_EFFECTS = NEGATIVE_EFFECTS;
|
|
731
|
+
|
|
732
|
+
// Export state variables with getters and setters
|
|
733
|
+
Object.defineProperty(globalThis, 'activeBuffs', {
|
|
734
|
+
get: () => activeBuffs,
|
|
735
|
+
set: (value) => { activeBuffs = value; }
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
Object.defineProperty(globalThis, 'activeConditions', {
|
|
739
|
+
get: () => activeConditions,
|
|
740
|
+
set: (value) => { activeConditions = value; }
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
})();
|