@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,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spell Action Modals
|
|
3
|
+
*
|
|
4
|
+
* action-display.js routes ~95 spells/features to dedicated show*Modal()
|
|
5
|
+
* handlers, almost none of which were implemented. This module provides a
|
|
6
|
+
* single data-driven dispatcher: each spell has a compact definition in
|
|
7
|
+
* SPELL_DEFS and is rendered by a small set of templates (damage, damage-type
|
|
8
|
+
* choice, healing, concentration/buff, utility). Spells without a definition
|
|
9
|
+
* yet fall back to a plain chat announcement, so nothing ever crashes.
|
|
10
|
+
*
|
|
11
|
+
* Loaded as a plain script (exports to globalThis). Must load AFTER
|
|
12
|
+
* feature-modals.js (so createThemedModal etc. exist) and after its fallback
|
|
13
|
+
* loop, which this overwrites with the smarter dispatcher.
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const dbg = () => window.debug || console;
|
|
19
|
+
|
|
20
|
+
// โโ small helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
21
|
+
function abilityScore(ability) {
|
|
22
|
+
const a = (globalThis.characterData && characterData.attributes) || {};
|
|
23
|
+
const v = a[ability];
|
|
24
|
+
return typeof v === 'number' ? v : 10;
|
|
25
|
+
}
|
|
26
|
+
function abilityMod(ability) {
|
|
27
|
+
const mods = (globalThis.characterData && characterData.attributeMods) || {};
|
|
28
|
+
if (typeof mods[ability] === 'number') return mods[ability];
|
|
29
|
+
return Math.floor((abilityScore(ability) - 10) / 2);
|
|
30
|
+
}
|
|
31
|
+
function profBonus() {
|
|
32
|
+
const cd = globalThis.characterData || {};
|
|
33
|
+
return Number(cd.proficiencyBonus) || (Math.floor(((Number(cd.level) || 1) - 1) / 4) + 2);
|
|
34
|
+
}
|
|
35
|
+
function spellcastingAbility() {
|
|
36
|
+
const cls = ((globalThis.characterData && characterData.class) || '').toLowerCase();
|
|
37
|
+
if (/cleric|druid|ranger/.test(cls)) return 'wisdom';
|
|
38
|
+
if (/wizard|artificer/.test(cls)) return 'intelligence';
|
|
39
|
+
if (/bard|paladin|sorcerer|warlock/.test(cls)) return 'charisma';
|
|
40
|
+
return 'wisdom';
|
|
41
|
+
}
|
|
42
|
+
function spellMod() { return abilityMod(spellcastingAbility()); }
|
|
43
|
+
function spellSaveDC() { return 8 + profBonus() + spellMod(); }
|
|
44
|
+
function spellAttackBonus() { return profBonus() + spellMod(); }
|
|
45
|
+
const fmtMod = (m) => (m >= 0 ? `+${m}` : `${m}`);
|
|
46
|
+
// "DC 15 DEX save (half)" style text for save-based spells.
|
|
47
|
+
function saveText(def) {
|
|
48
|
+
if (!def.save) return '';
|
|
49
|
+
return ` โ DC ${spellSaveDC()} ${def.save} save${def.half ? ' (half)' : ''}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Available regular spell slots (flat keys), level >= minLevel, with max > 0.
|
|
53
|
+
function availableSlots(minLevel) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const slots = (globalThis.characterData && characterData.spellSlots) || {};
|
|
56
|
+
for (let l = Math.max(1, minLevel || 1); l <= 9; l++) {
|
|
57
|
+
const max = Number(slots[`level${l}SpellSlotsMax`]) || 0;
|
|
58
|
+
if (max > 0) out.push({ level: l, current: Number(slots[`level${l}SpellSlots`]) || 0, max });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function spendSlot(level) {
|
|
63
|
+
const slots = characterData.spellSlots || (characterData.spellSlots = {});
|
|
64
|
+
const key = `level${level}SpellSlots`;
|
|
65
|
+
const cur = Number(slots[key]) || 0;
|
|
66
|
+
if (cur > 0) slots[key] = cur - 1;
|
|
67
|
+
const nested = slots[`level${level}`];
|
|
68
|
+
if (nested && typeof nested === 'object') nested.current = slots[key];
|
|
69
|
+
}
|
|
70
|
+
function persist() {
|
|
71
|
+
if (typeof saveCharacterData === 'function') saveCharacterData();
|
|
72
|
+
if (typeof buildSheet === 'function') buildSheet(characterData);
|
|
73
|
+
}
|
|
74
|
+
function announce(name, description) {
|
|
75
|
+
if (typeof announceAction === 'function') announceAction({ name, description });
|
|
76
|
+
}
|
|
77
|
+
function notify(msg, type) {
|
|
78
|
+
if (typeof showNotification === 'function') showNotification(msg, type);
|
|
79
|
+
}
|
|
80
|
+
function doRoll(label, formula) {
|
|
81
|
+
if (typeof roll === 'function' && formula) roll(label, formula);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Add `perLevel` dice for each level the slot is above the spell's base level.
|
|
85
|
+
// e.g. base "3d8" + perLevel "1d8" cast at level 5 (base 3) -> "5d8".
|
|
86
|
+
function scaleDice(baseDice, perLevel, baseLevel, castLevel) {
|
|
87
|
+
if (!perLevel || castLevel <= baseLevel) return baseDice;
|
|
88
|
+
const steps = castLevel - baseLevel;
|
|
89
|
+
const b = /^(\d+)d(\d+)(.*)$/.exec(baseDice.replace(/\s+/g, ''));
|
|
90
|
+
const p = /^(\d+)d(\d+)$/.exec(perLevel.replace(/\s+/g, ''));
|
|
91
|
+
if (b && p && b[2] === p[2]) {
|
|
92
|
+
return `${parseInt(b[1], 10) + parseInt(p[1], 10) * steps}d${b[2]}${b[3] || ''}`;
|
|
93
|
+
}
|
|
94
|
+
return `${baseDice} + ${steps}ร(${perLevel})`;
|
|
95
|
+
}
|
|
96
|
+
// Replace MOD / SPELLMOD tokens in a dice string with the spellcasting modifier.
|
|
97
|
+
function resolveMod(dice) {
|
|
98
|
+
if (!dice) return dice;
|
|
99
|
+
return dice.replace(/SPELLMOD|MOD/g, () => String(spellMod()));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// โโ modal shell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
103
|
+
function openModal(icon, title, bodyHTML) {
|
|
104
|
+
const { modal, modalContent } = createThemedModal();
|
|
105
|
+
modalContent.innerHTML =
|
|
106
|
+
`<h2 style="margin:0 0 12px;font-size:1.4em;">${icon} ${title}</h2>${bodyHTML}`;
|
|
107
|
+
modal.appendChild(modalContent);
|
|
108
|
+
document.body.appendChild(modal);
|
|
109
|
+
const close = () => {
|
|
110
|
+
if (modal.parentNode) document.body.removeChild(modal);
|
|
111
|
+
document.removeEventListener('keydown', onEsc);
|
|
112
|
+
};
|
|
113
|
+
const onEsc = (e) => { if (e.key === 'Escape') close(); };
|
|
114
|
+
document.addEventListener('keydown', onEsc);
|
|
115
|
+
return { modal, content: modalContent, close };
|
|
116
|
+
}
|
|
117
|
+
const BTN = 'padding:10px 18px;font-size:0.95em;font-weight:bold;border:none;border-radius:6px;cursor:pointer;';
|
|
118
|
+
const cancelBtn = `<button data-cc-cancel style="${BTN}background:var(--accent-danger);color:#fff;">Cancel</button>`;
|
|
119
|
+
function metaLine(def) {
|
|
120
|
+
const bits = [];
|
|
121
|
+
if (def.level != null) bits.push(def.level === 0 ? 'Cantrip' : `Level ${def.level}`);
|
|
122
|
+
if (def.school) bits.push(def.school);
|
|
123
|
+
if (def.conc) bits.push('Concentration');
|
|
124
|
+
if (def.duration) bits.push(def.duration);
|
|
125
|
+
if (def.ritual) bits.push('Ritual');
|
|
126
|
+
return bits.length ? `<div style="font-size:0.8em;opacity:0.7;margin:-6px 0 12px;">${bits.join(' ยท ')}</div>` : '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// โโ templates โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
130
|
+
function tplDamage(def, name) {
|
|
131
|
+
const slots = def.level > 0 ? availableSlots(def.level) : [];
|
|
132
|
+
const slotSel = ((def.perLevel || def.perLevelRays) && slots.length)
|
|
133
|
+
? `<label style="display:block;font-size:0.85em;opacity:0.85;margin-bottom:6px;">Cast at level</label>
|
|
134
|
+
<select data-cc-slot style="width:100%;padding:8px;border:2px solid var(--accent-info);border-radius:6px;background:rgba(0,0,0,0.2);color:inherit;margin-bottom:14px;">
|
|
135
|
+
${slots.map(s => `<option value="${s.level}" ${s.current <= 0 ? 'disabled' : ''}>Level ${s.level} (${s.current}/${s.max})</option>`).join('')}
|
|
136
|
+
</select>` : '';
|
|
137
|
+
const atk = spellAttackBonus();
|
|
138
|
+
const atkLine = def.attack
|
|
139
|
+
? `<div style="font-size:1.05em;margin-bottom:8px;">Spell attack: <strong>${fmtMod(atk)}</strong> to hit${def.rays ? ' (per ray)' : ''}</div>`
|
|
140
|
+
: '';
|
|
141
|
+
const body = `${metaLine(def)}
|
|
142
|
+
<p style="font-size:0.9em;line-height:1.4;margin:0 0 14px;">${def.effect || ''}</p>
|
|
143
|
+
${slotSel}
|
|
144
|
+
${atkLine}
|
|
145
|
+
<div style="font-size:1.1em;margin-bottom:14px;">Damage: <strong data-cc-dmg></strong> ${def.type || ''}<span style="opacity:0.85;">${saveText(def)}</span></div>
|
|
146
|
+
<div style="display:flex;gap:10px;justify-content:center;">
|
|
147
|
+
<button data-cc-cast style="${BTN}background:var(--accent-warning);color:#fff;">๐ฒ ${def.attack ? 'Roll Attack + Damage' : 'Roll Damage'}</button>
|
|
148
|
+
${cancelBtn}
|
|
149
|
+
</div>`;
|
|
150
|
+
const m = openModal(def.icon || 'โจ', name, body);
|
|
151
|
+
const slotEl = m.content.querySelector('[data-cc-slot]');
|
|
152
|
+
const dmgEl = m.content.querySelector('[data-cc-dmg]');
|
|
153
|
+
const curLevel = () => slotEl ? parseInt(slotEl.value, 10) : def.level;
|
|
154
|
+
const curDice = () => {
|
|
155
|
+
let d = scaleDice(def.dice || '0', def.perLevel, def.level, curLevel());
|
|
156
|
+
d = resolveMod(d);
|
|
157
|
+
if (def.rays) {
|
|
158
|
+
const extra = slotEl ? Math.max(0, curLevel() - def.level) * (def.perLevelRays || 0) : 0;
|
|
159
|
+
return { dice: d, count: def.rays + extra };
|
|
160
|
+
}
|
|
161
|
+
return { dice: d, count: 1 };
|
|
162
|
+
};
|
|
163
|
+
const refresh = () => {
|
|
164
|
+
const { dice, count } = curDice();
|
|
165
|
+
dmgEl.textContent = count > 1 ? `${count} ร ${dice}` : dice;
|
|
166
|
+
};
|
|
167
|
+
refresh();
|
|
168
|
+
if (slotEl) slotEl.addEventListener('change', refresh);
|
|
169
|
+
m.content.querySelector('[data-cc-cast]').addEventListener('click', () => {
|
|
170
|
+
const lvl = curLevel();
|
|
171
|
+
if (def.level > 0 && slotEl) {
|
|
172
|
+
const s = availableSlots(def.level).find(x => x.level === lvl);
|
|
173
|
+
if (!s || s.current <= 0) { notify('โ No spell slot remaining at that level!', 'error'); return; }
|
|
174
|
+
spendSlot(lvl);
|
|
175
|
+
}
|
|
176
|
+
const { dice, count } = curDice();
|
|
177
|
+
const upcast = lvl > def.level ? ` (upcast L${lvl})` : '';
|
|
178
|
+
announce(name, `${def.attack ? `${fmtMod(atk)} to hit${count > 1 ? ` ร${count}` : ''} ยท ` : ''}${count > 1 ? count + ' ร ' : ''}${dice} ${def.type || ''} damage${saveText(def)}${upcast}`);
|
|
179
|
+
// Roll the spell attack(s) first, then the damage.
|
|
180
|
+
if (def.attack) {
|
|
181
|
+
for (let i = 0; i < count; i++) doRoll(`${name} attack${count > 1 ? ' ' + (i + 1) : ''}`, `1d20${fmtMod(atk)}`);
|
|
182
|
+
}
|
|
183
|
+
doRoll(`${name} damage`, count > 1 ? Array(count).fill(`(${dice})`).join('+') : dice);
|
|
184
|
+
if (def.conc && typeof setConcentration === 'function') setConcentration(name);
|
|
185
|
+
m.close();
|
|
186
|
+
persist();
|
|
187
|
+
});
|
|
188
|
+
m.content.querySelector('[data-cc-cancel]').addEventListener('click', m.close);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function tplChoice(def, name) {
|
|
192
|
+
const slots = def.level > 0 ? availableSlots(def.level) : [];
|
|
193
|
+
const slotSel = (def.perLevel && slots.length)
|
|
194
|
+
? `<label style="display:block;font-size:0.85em;opacity:0.85;margin-bottom:6px;">Cast at level</label>
|
|
195
|
+
<select data-cc-slot style="width:100%;padding:8px;border:2px solid var(--accent-info);border-radius:6px;background:rgba(0,0,0,0.2);color:inherit;margin-bottom:14px;">
|
|
196
|
+
${slots.map(s => `<option value="${s.level}" ${s.current <= 0 ? 'disabled' : ''}>Level ${s.level} (${s.current}/${s.max})</option>`).join('')}
|
|
197
|
+
</select>` : '';
|
|
198
|
+
const atk = spellAttackBonus();
|
|
199
|
+
const atkLine = def.attack ? `<div style="font-size:1.0em;margin-bottom:8px;">Spell attack: <strong>${fmtMod(atk)}</strong> to hit</div>` : '';
|
|
200
|
+
const body = `${metaLine(def)}
|
|
201
|
+
<p style="font-size:0.9em;line-height:1.4;margin:0 0 14px;">${def.effect || ''}</p>
|
|
202
|
+
${slotSel}
|
|
203
|
+
${atkLine}
|
|
204
|
+
<p style="font-size:0.85em;opacity:0.85;margin:0 0 8px;">${def.prompt || 'Choose:'}</p>
|
|
205
|
+
<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-bottom:14px;">
|
|
206
|
+
${def.choices.map((c, i) => `<button data-cc-choice="${i}" style="${BTN}background:${c.color || 'var(--accent-info)'};color:#fff;">${c.label}</button>`).join('')}
|
|
207
|
+
</div>
|
|
208
|
+
<div style="text-align:center;">${cancelBtn}</div>`;
|
|
209
|
+
const m = openModal(def.icon || 'โจ', name, body);
|
|
210
|
+
const slotEl = m.content.querySelector('[data-cc-slot]');
|
|
211
|
+
m.content.querySelectorAll('[data-cc-choice]').forEach((btn) => {
|
|
212
|
+
btn.addEventListener('click', () => {
|
|
213
|
+
const c = def.choices[parseInt(btn.dataset.ccChoice, 10)];
|
|
214
|
+
const lvl = slotEl ? parseInt(slotEl.value, 10) : def.level;
|
|
215
|
+
if (def.level > 0 && slotEl) {
|
|
216
|
+
const s = availableSlots(def.level).find(x => x.level === lvl);
|
|
217
|
+
if (!s || s.current <= 0) { notify('โ No spell slot remaining at that level!', 'error'); return; }
|
|
218
|
+
spendSlot(lvl);
|
|
219
|
+
}
|
|
220
|
+
if (def.dice) {
|
|
221
|
+
const dice = resolveMod(scaleDice(def.dice, def.perLevel, def.level, lvl));
|
|
222
|
+
announce(name, `${c.label}: ${def.attack ? `${fmtMod(atk)} to hit ยท ` : ''}${dice} ${c.type || def.type || ''} damage${saveText(def)}${lvl > def.level ? ` (upcast L${lvl})` : ''}`);
|
|
223
|
+
if (def.attack) doRoll(`${name} attack`, `1d20${fmtMod(atk)}`);
|
|
224
|
+
doRoll(`${name} (${c.label})`, dice);
|
|
225
|
+
} else {
|
|
226
|
+
announce(name, `${c.label}${c.effect ? ': ' + c.effect : ''}`);
|
|
227
|
+
}
|
|
228
|
+
if (def.conc && typeof setConcentration === 'function') setConcentration(name);
|
|
229
|
+
m.close();
|
|
230
|
+
persist();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
m.content.querySelector('[data-cc-cancel]').addEventListener('click', m.close);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function tplHealing(def, name) {
|
|
237
|
+
const slots = def.level > 0 ? availableSlots(def.level) : [];
|
|
238
|
+
const slotSel = (def.healPerLevel && slots.length)
|
|
239
|
+
? `<label style="display:block;font-size:0.85em;opacity:0.85;margin-bottom:6px;">Cast at level</label>
|
|
240
|
+
<select data-cc-slot style="width:100%;padding:8px;border:2px solid var(--accent-info);border-radius:6px;background:rgba(0,0,0,0.2);color:inherit;margin-bottom:14px;">
|
|
241
|
+
${slots.map(s => `<option value="${s.level}" ${s.current <= 0 ? 'disabled' : ''}>Level ${s.level} (${s.current}/${s.max})</option>`).join('')}
|
|
242
|
+
</select>` : '';
|
|
243
|
+
const body = `${metaLine(def)}
|
|
244
|
+
<p style="font-size:0.9em;line-height:1.4;margin:0 0 14px;">${def.effect || ''}</p>
|
|
245
|
+
${slotSel}
|
|
246
|
+
<div style="font-size:1.1em;margin-bottom:14px;">Healing: <strong data-cc-heal></strong></div>
|
|
247
|
+
<div style="display:flex;gap:10px;justify-content:center;">
|
|
248
|
+
<button data-cc-cast style="${BTN}background:var(--accent-success);color:#fff;">๐ Roll Healing</button>
|
|
249
|
+
${cancelBtn}
|
|
250
|
+
</div>`;
|
|
251
|
+
const m = openModal(def.icon || '๐', name, body);
|
|
252
|
+
const slotEl = m.content.querySelector('[data-cc-slot]');
|
|
253
|
+
const healEl = m.content.querySelector('[data-cc-heal]');
|
|
254
|
+
const curLevel = () => slotEl ? parseInt(slotEl.value, 10) : def.level;
|
|
255
|
+
const curHeal = () => resolveMod(scaleDice(def.heal, def.healPerLevel, def.level, curLevel()));
|
|
256
|
+
const refresh = () => { healEl.textContent = curHeal(); };
|
|
257
|
+
refresh();
|
|
258
|
+
if (slotEl) slotEl.addEventListener('change', refresh);
|
|
259
|
+
m.content.querySelector('[data-cc-cast]').addEventListener('click', () => {
|
|
260
|
+
const lvl = curLevel();
|
|
261
|
+
if (def.level > 0 && slotEl) {
|
|
262
|
+
const s = availableSlots(def.level).find(x => x.level === lvl);
|
|
263
|
+
if (!s || s.current <= 0) { notify('โ No spell slot remaining at that level!', 'error'); return; }
|
|
264
|
+
spendSlot(lvl);
|
|
265
|
+
}
|
|
266
|
+
announce(name, `Restores ${curHeal()} hit points${lvl > def.level ? ` (upcast L${lvl})` : ''}`);
|
|
267
|
+
doRoll(`${name} healing`, curHeal());
|
|
268
|
+
m.close();
|
|
269
|
+
persist();
|
|
270
|
+
});
|
|
271
|
+
m.content.querySelector('[data-cc-cancel]').addEventListener('click', m.close);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Buff / utility: a single Cast button that announces (and handles slot +
|
|
275
|
+
// concentration for buffs). Used for everything without dice to roll.
|
|
276
|
+
function tplCast(def, name, accent) {
|
|
277
|
+
const slots = (def.level > 0 && def.useSlot !== false) ? availableSlots(def.level) : [];
|
|
278
|
+
const slotSel = (def.useSlot !== false && def.level > 0 && slots.length)
|
|
279
|
+
? `<label style="display:block;font-size:0.85em;opacity:0.85;margin-bottom:6px;">Cast at level</label>
|
|
280
|
+
<select data-cc-slot style="width:100%;padding:8px;border:2px solid var(--accent-info);border-radius:6px;background:rgba(0,0,0,0.2);color:inherit;margin-bottom:14px;">
|
|
281
|
+
${slots.map(s => `<option value="${s.level}" ${s.current <= 0 ? 'disabled' : ''}>Level ${s.level} (${s.current}/${s.max})</option>`).join('')}
|
|
282
|
+
</select>` : '';
|
|
283
|
+
const body = `${metaLine(def)}
|
|
284
|
+
<p style="font-size:0.92em;line-height:1.45;margin:0 0 14px;">${def.effect || ''}</p>
|
|
285
|
+
${slotSel}
|
|
286
|
+
<div style="display:flex;gap:10px;justify-content:center;">
|
|
287
|
+
<button data-cc-cast style="${BTN}background:${accent || 'var(--accent-info)'};color:#fff;">${def.castLabel || 'โจ Cast'}</button>
|
|
288
|
+
${cancelBtn}
|
|
289
|
+
</div>`;
|
|
290
|
+
const m = openModal(def.icon || 'โจ', name, body);
|
|
291
|
+
const slotEl = m.content.querySelector('[data-cc-slot]');
|
|
292
|
+
m.content.querySelector('[data-cc-cast]').addEventListener('click', () => {
|
|
293
|
+
const lvl = slotEl ? parseInt(slotEl.value, 10) : def.level;
|
|
294
|
+
if (def.useSlot !== false && def.level > 0 && slotEl) {
|
|
295
|
+
const s = availableSlots(def.level).find(x => x.level === lvl);
|
|
296
|
+
if (!s || s.current <= 0) { notify('โ No spell slot remaining at that level!', 'error'); return; }
|
|
297
|
+
spendSlot(lvl);
|
|
298
|
+
}
|
|
299
|
+
announce(name, `${def.effect || ''}${lvl > def.level ? ` (upcast L${lvl})` : ''}`);
|
|
300
|
+
if (def.conc && typeof setConcentration === 'function') setConcentration(name);
|
|
301
|
+
notify(`${def.icon || 'โจ'} ${name}`);
|
|
302
|
+
m.close();
|
|
303
|
+
persist();
|
|
304
|
+
});
|
|
305
|
+
m.content.querySelector('[data-cc-cancel]').addEventListener('click', m.close);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// โโ dispatcher โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
309
|
+
function showSpellActionModal(action) {
|
|
310
|
+
if (typeof globalThis.characterData === 'undefined' || !characterData) return;
|
|
311
|
+
const name = (action && action.name) || '';
|
|
312
|
+
const def = SPELL_DEFS[name] || SPELL_DEFS[Object.keys(SPELL_DEFS).find(k => name.toLowerCase().includes(k.toLowerCase())) || ''];
|
|
313
|
+
if (!def) { announce(name || 'Action', (action && (action.summary || action.description)) || ''); return; }
|
|
314
|
+
try {
|
|
315
|
+
switch (def.kind) {
|
|
316
|
+
case 'damage': return tplDamage(def, def.title || name);
|
|
317
|
+
case 'choice': return tplChoice(def, def.title || name);
|
|
318
|
+
case 'healing': return tplHealing(def, def.title || name);
|
|
319
|
+
case 'buff': return tplCast(def, def.title || name, 'var(--accent-success)');
|
|
320
|
+
default: return tplCast(def, def.title || name, 'var(--accent-info)');
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
dbg().warn('Spell modal failed, announcing instead:', e);
|
|
324
|
+
announce(name, def.effect || '');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// โโ spell definitions (grow this list over time) โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
329
|
+
const SPELL_DEFS = {
|
|
330
|
+
'Absorb Elements': { kind: 'choice', icon: '๐ก๏ธ', level: 1, school: 'Abjuration', duration: '1 round',
|
|
331
|
+
effect: 'Reaction when you take acid/cold/fire/lightning/thunder damage: resistance to that type until your next turn, and your next melee hit deals +1d6 of it (+1d6 per slot level above 1).',
|
|
332
|
+
perLevel: '1d6', dice: '1d6', prompt: 'Triggering damage type:',
|
|
333
|
+
choices: [{ label: 'Acid', type: 'acid' }, { label: 'Cold', type: 'cold' }, { label: 'Fire', type: 'fire' }, { label: 'Lightning', type: 'lightning' }, { label: 'Thunder', type: 'thunder' }] },
|
|
334
|
+
'Aid': { kind: 'buff', icon: 'โ', level: 2, school: 'Abjuration', duration: '8 hours',
|
|
335
|
+
effect: 'Up to three creatures each gain +5 max and current HP (+5 per slot level above 2) for 8 hours.' },
|
|
336
|
+
'Animate Objects': { kind: 'buff', icon: '๐ช', level: 5, school: 'Transmutation', conc: true, duration: '1 minute',
|
|
337
|
+
effect: 'Animate up to 10 nonmagical objects to attack at your command (bonus action to direct).' },
|
|
338
|
+
'Armor of Agathys': { kind: 'buff', icon: '๐ง', level: 1, school: 'Abjuration', duration: '1 hour',
|
|
339
|
+
effect: 'Gain 5 temp HP and a cold aura (+5 temp HP / +5 cold per slot level above 1). A creature hitting you in melee takes 5 cold while you have the temp HP.' },
|
|
340
|
+
'Astral Projection': { icon: '๐', level: 9, school: 'Necromancy',
|
|
341
|
+
effect: 'Project the astral bodies of you and up to 8 willing creatures into the Astral Plane.' },
|
|
342
|
+
'Augury': { icon: '๐ฎ', level: 2, school: 'Divination', ritual: true,
|
|
343
|
+
effect: 'Learn whether a course of action over the next 30 minutes will bring weal, woe, both, or nothing.', castLabel: '๐ฎ Consult' },
|
|
344
|
+
'Bane': { kind: 'buff', icon: '๐', level: 1, school: 'Enchantment', conc: true, duration: '1 minute',
|
|
345
|
+
effect: 'Up to 3 creatures (Cha save) subtract 1d4 from attack rolls and saving throws (+1 target per slot level above 1).' },
|
|
346
|
+
"Bigby's Hand": { kind: 'buff', icon: 'โ', level: 5, school: 'Evocation', conc: true, duration: '1 minute', title: "Bigby's Hand",
|
|
347
|
+
effect: 'Create a Large force hand (AC 20, HP = your max). Bonus action: Clenched Fist (4d8 force), Forceful Hand (shove), Grasping Hand (grapple/crush 2d6+Str), or Interposing Hand.' },
|
|
348
|
+
'Bless': { kind: 'buff', icon: '๐', level: 1, school: 'Enchantment', conc: true, duration: '1 minute',
|
|
349
|
+
effect: 'Up to 3 creatures add 1d4 to attack rolls and saving throws (+1 target per slot level above 1).' },
|
|
350
|
+
'Booming Blade': { kind: 'damage', icon: 'โก', level: 0, school: 'Evocation', useSlot: false,
|
|
351
|
+
effect: 'Melee attack; on hit, target is sheathed in booming energy. If it moves before your next turn it takes thunder damage. Extra/move damage scales at levels 5/11/17.',
|
|
352
|
+
dice: '1d8', type: 'thunder (if it moves)', note: 'cantrip' },
|
|
353
|
+
'Chaos Bolt': { kind: 'damage', icon: '๐ฒ', level: 1, school: 'Evocation',
|
|
354
|
+
effect: 'Ranged spell attack; 2d8 + 1d6 damage. The d6 determines the type (1 acid,2 cold,3 fire,4 force,5 lightning,6 poison,7 psychic,8 thunder). On doubles it can leap to another target. +1d6 per slot level above 1.',
|
|
355
|
+
dice: '2d8+1d6', type: '(roll d8 for type)', perLevel: '1d6', attack: true },
|
|
356
|
+
'Chromatic Orb': { kind: 'choice', icon: '๐ด', level: 1, school: 'Evocation',
|
|
357
|
+
effect: 'Ranged spell attack, 3d8 of a chosen type (+1d8 per slot level above 1).',
|
|
358
|
+
dice: '3d8', perLevel: '1d8', attack: true, prompt: 'Damage type:',
|
|
359
|
+
choices: [{ label: 'Acid', type: 'acid' }, { label: 'Cold', type: 'cold' }, { label: 'Fire', type: 'fire' }, { label: 'Lightning', type: 'lightning' }, { label: 'Poison', type: 'poison' }, { label: 'Thunder', type: 'thunder' }] },
|
|
360
|
+
'Clone': { icon: '๐งฌ', level: 8, school: 'Necromancy',
|
|
361
|
+
effect: 'Grow an inert duplicate of a creature as a safeguard against death (120 days to mature).' },
|
|
362
|
+
'Cloud of Daggers': { kind: 'damage', icon: '๐ก๏ธ', level: 2, school: 'Conjuration', conc: true, duration: '1 minute',
|
|
363
|
+
effect: 'Fill a 5-ft cube with spinning daggers; 4d4 slashing on entry/start of turn (+2d4 per slot level above 2).',
|
|
364
|
+
dice: '4d4', type: 'slashing', perLevel: '2d4' },
|
|
365
|
+
'Commune': { icon: '๐๏ธ', level: 5, school: 'Divination', ritual: true,
|
|
366
|
+
effect: 'Ask your deity up to three yes/no questions.', castLabel: '๐๏ธ Commune' },
|
|
367
|
+
'Contact Other Plane': { icon: '๐๏ธ', level: 5, school: 'Divination', ritual: true,
|
|
368
|
+
effect: 'Contact an extraplanar intellect (Int DC 15 save or take 6d6 psychic and be insane for a time); ask up to five one-word-answer questions.' },
|
|
369
|
+
'Contingency': { icon: 'โณ', level: 6, school: 'Evocation',
|
|
370
|
+
effect: 'Store a spell (โค5th, casting time โค1 action) to trigger on a circumstance you describe.' },
|
|
371
|
+
'Counterspell': { kind: 'buff', icon: '๐ซ', level: 3, school: 'Abjuration', conc: false, useSlot: true,
|
|
372
|
+
effect: 'Reaction to interrupt a creature casting a spell. Automatically stops a spell of โค3rd level; otherwise make an ability check (DC 10 + spell level), or upcast to that level to auto-succeed.' },
|
|
373
|
+
'Delayed Blast Fireball': { kind: 'damage', icon: '๐ฅ', level: 7, school: 'Evocation', conc: true,
|
|
374
|
+
effect: 'A bead that grows 1d6 each of your turns (max 12d6), then detonates for 12d6 fire (Dex save half). Base 12d6 +1d6 per slot level above 7.',
|
|
375
|
+
dice: '12d6', type: 'fire', perLevel: '1d6', save: 'DEX', half: true },
|
|
376
|
+
'Detect Magic': { icon: 'โจ', level: 1, school: 'Divination', conc: true, ritual: true, duration: '10 minutes',
|
|
377
|
+
effect: 'Sense the presence of magic within 30 ft; an action to study an aura reveals its school.' },
|
|
378
|
+
'Dispel Evil and Good': { kind: 'buff', icon: 'โฏ๏ธ', level: 5, school: 'Abjuration', conc: true, duration: '1 minute',
|
|
379
|
+
effect: 'Celestials/elementals/fey/fiends/undead have disadvantage to hit you; you can use an action to Break Enchantment, Dismissal, or end a possession/charm.' },
|
|
380
|
+
'Dispel Magic': { kind: 'buff', icon: '๐', level: 3, school: 'Abjuration', useSlot: true,
|
|
381
|
+
effect: 'End one spell on a target. Spells of โค the slot level used end automatically; for higher, make an ability check (DC 10 + that spellโs level).' },
|
|
382
|
+
'Divination': { icon: '๐ฎ', level: 4, school: 'Divination', ritual: true,
|
|
383
|
+
effect: 'A short truthful reply about a goal/event within 7 days (a word, phrase, or omen).' },
|
|
384
|
+
"Dragon's Breath": { kind: 'choice', icon: '๐ฒ', level: 2, school: 'Transmutation', conc: true, duration: '1 minute', title: "Dragon's Breath",
|
|
385
|
+
effect: 'A willing creature can use an action to exhale a 15-ft cone: 3d6 of a chosen type (Dex save half), +1d6 per slot level above 2.',
|
|
386
|
+
dice: '3d6', perLevel: '1d6', save: 'DEX', prompt: 'Breath type:',
|
|
387
|
+
choices: [{ label: 'Acid', type: 'acid' }, { label: 'Cold', type: 'cold' }, { label: 'Fire', type: 'fire' }, { label: 'Lightning', type: 'lightning' }, { label: 'Poison', type: 'poison' }] },
|
|
388
|
+
'Dream': { icon: '๐ค', level: 5, school: 'Illusion',
|
|
389
|
+
effect: 'Shape the dreams of a creature you know; a messenger can deliver a message or haunt to deny rest (3d6 psychic).' },
|
|
390
|
+
'Elemental Weapon': { kind: 'choice', icon: '๐ก๏ธ', level: 3, school: 'Transmutation', conc: true, duration: '1 hour',
|
|
391
|
+
effect: 'A nonmagical weapon gains +1 (scales) and +1d4 of a chosen type. +2/+2d4 at 5thโ6th, +3/+3d4 at 7th+.',
|
|
392
|
+
dice: '1d4', type: '', prompt: 'Damage type:',
|
|
393
|
+
choices: [{ label: 'Acid', type: 'acid' }, { label: 'Cold', type: 'cold' }, { label: 'Fire', type: 'fire' }, { label: 'Lightning', type: 'lightning' }, { label: 'Thunder', type: 'thunder' }] },
|
|
394
|
+
'Etherealness': { icon: '๐ป', level: 7, school: 'Transmutation', duration: 'Up to 8 hours',
|
|
395
|
+
effect: 'Step into the Ethereal Plane (Border Ethereal). +2 creatures per slot level above 7.' },
|
|
396
|
+
'Feather Fall': { kind: 'buff', icon: '๐ชถ', level: 1, school: 'Transmutation', useSlot: true,
|
|
397
|
+
effect: 'Reaction: up to 5 falling creatures descend 60 ft/round and take no falling damage for 1 minute.' },
|
|
398
|
+
'Find the Path': { icon: '๐งญ', level: 6, school: 'Divination', conc: true, duration: 'Up to 1 day',
|
|
399
|
+
effect: 'Know the shortest, most direct route to a location familiar to you.' },
|
|
400
|
+
'Fire Shield': { kind: 'choice', icon: '๐ฅ', level: 4, school: 'Evocation', duration: '10 minutes',
|
|
401
|
+
effect: 'A warm shield (resist cold) or chill shield (resist fire). A creature hitting you in melee takes 2d8 of the opposite type.',
|
|
402
|
+
dice: '2d8', prompt: 'Shield:',
|
|
403
|
+
choices: [{ label: 'Warm (resist cold, deals fire)', type: 'fire' }, { label: 'Chill (resist fire, deals cold)', type: 'cold' }] },
|
|
404
|
+
'Flaming Sphere': { kind: 'damage', icon: '๐ฅ', level: 2, school: 'Conjuration', conc: true, duration: '1 minute',
|
|
405
|
+
effect: 'A 5-ft fiery sphere you move (bonus action). 2d6 fire (Dex save half) to creatures it ends adjacent to or rams. +1d6 per slot level above 2.',
|
|
406
|
+
dice: '2d6', type: 'fire', perLevel: '1d6', save: 'DEX', half: true },
|
|
407
|
+
'Forcecage': { icon: '๐ฒ', level: 7, school: 'Evocation', duration: '1 hour',
|
|
408
|
+
effect: 'A 20-ft cube cage or 10-ft box of force; escape only by teleport (Cha save) or planar travel.' },
|
|
409
|
+
'Freedom of Movement': { kind: 'buff', icon: '๐', level: 4, school: 'Abjuration', duration: '1 hour',
|
|
410
|
+
effect: 'A creature is unaffected by difficult terrain, and most paralysis/restraint; can spend 5 ft to escape nonmagical restraints/grapples.' },
|
|
411
|
+
'Gate': { icon: '๐', level: 9, school: 'Conjuration', conc: true,
|
|
412
|
+
effect: 'Open a portal to another plane and optionally pull a named creature through.' },
|
|
413
|
+
'Geas': { kind: 'buff', icon: 'โ๏ธ', level: 5, school: 'Enchantment', duration: '30 days',
|
|
414
|
+
effect: 'Command a creature (Wis save). While charmed it must obey; disobeying deals 5d10 psychic (once/day). Longer duration at higher slots.' },
|
|
415
|
+
'Glyph of Warding': { icon: '๐ฃ', level: 3, school: 'Abjuration',
|
|
416
|
+
effect: 'Inscribe a glyph that triggers an explosive rune (5d8, +1d8 per slot above 3) or a stored spell.' },
|
|
417
|
+
'Greater Restoration': { kind: 'buff', icon: 'โจ', level: 5, school: 'Abjuration',
|
|
418
|
+
effect: 'End one: a charm/petrification, a curse, reduced ability score, or reduced max HP.' },
|
|
419
|
+
'Green-Flame Blade': { kind: 'damage', icon: '๐ข', level: 0, school: 'Evocation', useSlot: false,
|
|
420
|
+
effect: 'Melee attack; on hit, green fire leaps to a second creature within 5 ft for fire = your spellcasting mod (scales at 5/11/17, which also adds fire to the primary target).',
|
|
421
|
+
dice: 'MOD', type: 'fire (to a second target)' },
|
|
422
|
+
'Guidance': { kind: 'buff', icon: '๐', level: 0, school: 'Divination', conc: true, useSlot: false, duration: '1 minute',
|
|
423
|
+
effect: 'Touch a willing creature; once before the spell ends it can add 1d10 to one ability check.' },
|
|
424
|
+
'Haste': { kind: 'buff', icon: 'โก', level: 3, school: 'Transmutation', conc: true, duration: '1 minute',
|
|
425
|
+
effect: 'A willing creature gains +2 AC, advantage on Dex saves, double speed, and one extra action (Attack/Dash/Disengage/Hide/Use). Ends with 1 lost turn of lethargy.' },
|
|
426
|
+
'Healing Spirit': { kind: 'healing', icon: '๐ง', level: 2, school: 'Conjuration', conc: true, duration: '1 minute',
|
|
427
|
+
effect: 'A spirit in a 5-ft cube heals 1d6 to a creature that enters/starts its turn there (+1d6 per slot level above 2). Limited uses = 1 + spellcasting mod.',
|
|
428
|
+
heal: '1d6', healPerLevel: '1d6' },
|
|
429
|
+
'Hellish Rebuke': { kind: 'damage', icon: '๐ฅ', level: 1, school: 'Evocation',
|
|
430
|
+
effect: 'Reaction when damaged by a creature you can see: 2d10 fire (Dex save half), +1d10 per slot level above 1.',
|
|
431
|
+
dice: '2d10', type: 'fire', perLevel: '1d10', save: 'DEX', half: true },
|
|
432
|
+
'Hex': { kind: 'choice', icon: '๐ฃ', level: 1, school: 'Enchantment', conc: true, duration: '1 hour',
|
|
433
|
+
effect: 'Curse a creature: your attacks deal +1d6 necrotic to it, and it has disadvantage on checks with a chosen ability. Moves on a kill (bonus action).',
|
|
434
|
+
dice: '1d6', type: 'necrotic', prompt: 'Ability with disadvantage:',
|
|
435
|
+
choices: ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA'].map(a => ({ label: a, effect: `disadvantage on ${a} checks`, type: 'necrotic' })) },
|
|
436
|
+
"Hunter's Mark": { kind: 'buff', icon: '๐ฏ', level: 1, school: 'Divination', conc: true, duration: '1 hour', title: "Hunter's Mark",
|
|
437
|
+
effect: 'Mark a creature: +1d6 damage from your weapon attacks against it, and advantage on Perception/Survival to find it. Moves on a kill (bonus action). Longer duration at 3rd/5th.' },
|
|
438
|
+
'Identify': { icon: '๐', level: 1, school: 'Divination', ritual: true,
|
|
439
|
+
effect: 'Learn a magic itemโs properties and attunement, or what spells affect a creature/object.' },
|
|
440
|
+
'Imprisonment': { icon: 'โ๏ธ', level: 9, school: 'Abjuration',
|
|
441
|
+
effect: 'Bind a creature (Wis save): Chain, Minimus Containment, Burial, Hedged Prison, or Slumber.' },
|
|
442
|
+
'Legend Lore': { icon: '๐', level: 5, school: 'Divination',
|
|
443
|
+
effect: 'Learn significant lore about a famous person, place, or object you name.' },
|
|
444
|
+
'Life Transference': { kind: 'damage', icon: 'โค๏ธ', level: 3, school: 'Necromancy',
|
|
445
|
+
effect: 'Sacrifice 4d8 of your own HP to heal another creature twice that much (+1d8 per slot level above 3).',
|
|
446
|
+
dice: '4d8', type: 'necrotic (to self)', perLevel: '1d8' },
|
|
447
|
+
'Magic Circle': { icon: 'โญ', level: 3, school: 'Abjuration', duration: '1 hour',
|
|
448
|
+
effect: 'A 10-ft cylinder that keeps a chosen creature type out (or trapped in). Hampers entry, blocks charm/fright/possession.' },
|
|
449
|
+
'Magic Jar': { icon: '๐บ', level: 6, school: 'Necromancy',
|
|
450
|
+
effect: 'Move your soul to a prepared vessel, then attempt to possess nearby creatures (Cha save).' },
|
|
451
|
+
'Magic Missile': { kind: 'damage', icon: '๐น', level: 1, school: 'Evocation',
|
|
452
|
+
effect: 'Three darts each automatically hit for 1d4+1 force (split as you like). +1 dart per slot level above 1.',
|
|
453
|
+
dice: '1d4+1', type: 'force', rays: 3, perLevelRays: 1 },
|
|
454
|
+
'Maze': { icon: '๐', level: 8, school: 'Conjuration', conc: true, duration: '10 minutes',
|
|
455
|
+
effect: 'Banish a creature to a labyrinthine demiplane (Int check DC to escape).' },
|
|
456
|
+
'Meld into Stone': { icon: '๐ชจ', level: 3, school: 'Transmutation', ritual: true, duration: '8 hours',
|
|
457
|
+
effect: 'Step into a stone object/surface large enough to fit you.' },
|
|
458
|
+
'Mirage Arcane': { icon: '๐๏ธ', level: 7, school: 'Illusion', duration: '10 days',
|
|
459
|
+
effect: 'Make terrain (up to 1 sq mile) look, sound, smell, and feel like a different sort of terrain.' },
|
|
460
|
+
'Moonbeam': { kind: 'damage', icon: '๐', level: 2, school: 'Evocation', conc: true, duration: '1 minute',
|
|
461
|
+
effect: 'A 5-ft beam (move with an action). 2d10 radiant (Con save half) on entry/start of turn; shapechangers have disadvantage. +1d10 per slot level above 2.',
|
|
462
|
+
dice: '2d10', type: 'radiant', perLevel: '1d10', save: 'CON', half: true },
|
|
463
|
+
'Nondetection': { kind: 'buff', icon: '๐ซ', level: 3, school: 'Abjuration', duration: '8 hours',
|
|
464
|
+
effect: 'Hide a target from divination magic and scrying for 8 hours.' },
|
|
465
|
+
'Polymorph': { kind: 'buff', icon: '๐ธ', level: 4, school: 'Transmutation', conc: true, duration: '1 hour',
|
|
466
|
+
effect: 'Transform a creature into a beast (CR โค its level, Wis save to resist). It gains the beastโs stats and temp HP; reverts at 0 HP.' },
|
|
467
|
+
'Programmed Illusion': { icon: '๐ญ', level: 6, school: 'Illusion',
|
|
468
|
+
effect: 'Create an illusion (โค30-ft cube) that activates on a trigger you specify.' },
|
|
469
|
+
'Protection from Energy': { kind: 'choice', icon: '๐ก๏ธ', level: 3, school: 'Abjuration', conc: true, duration: '1 hour',
|
|
470
|
+
effect: 'A willing creature gains resistance to one damage type.', prompt: 'Resist:',
|
|
471
|
+
choices: [{ label: 'Acid', effect: 'resistance to acid' }, { label: 'Cold', effect: 'resistance to cold' }, { label: 'Fire', effect: 'resistance to fire' }, { label: 'Lightning', effect: 'resistance to lightning' }, { label: 'Thunder', effect: 'resistance to thunder' }] },
|
|
472
|
+
'Protection from Evil and Good': { kind: 'buff', icon: 'โ๏ธ', level: 1, school: 'Abjuration', conc: true, duration: '10 minutes',
|
|
473
|
+
effect: 'A willing creature is protected from aberrations/celestials/elementals/fey/fiends/undead: they have disadvantage to hit it, and it canโt be charmed, frightened, or possessed by them.' },
|
|
474
|
+
'Raise Dead': { kind: 'buff', icon: 'โฐ๏ธ', level: 5, school: 'Necromancy',
|
|
475
|
+
effect: 'Return a creature dead โค10 days to life with 1 HP (โ4 penalty to all d20 rolls, recovering over days).' },
|
|
476
|
+
'Remove Curse': { kind: 'buff', icon: '๐งฟ', level: 3, school: 'Abjuration',
|
|
477
|
+
effect: 'End all curses on a creature, or break attunement to a cursed item.' },
|
|
478
|
+
'Resistance': { kind: 'buff', icon: '๐ก๏ธ', level: 0, school: 'Abjuration', conc: true, useSlot: false, duration: '1 minute',
|
|
479
|
+
effect: 'Touch a willing creature; once before the spell ends it can add 1d4 to one saving throw.' },
|
|
480
|
+
'Resurrection': { kind: 'buff', icon: 'โฐ๏ธ', level: 7, school: 'Necromancy',
|
|
481
|
+
effect: 'Return a creature dead โค100 years (not of old age) to life with full HP.' },
|
|
482
|
+
'Revivify': { kind: 'buff', icon: '๐', level: 3, school: 'Necromancy',
|
|
483
|
+
effect: 'Return a creature that died within the last minute to life with 1 HP (needs diamonds worth 300 gp).' },
|
|
484
|
+
'Sanctuary': { kind: 'buff', icon: '๐ก๏ธ', level: 1, school: 'Abjuration', useSlot: true, duration: '1 minute',
|
|
485
|
+
effect: 'Ward a creature: anyone targeting it with an attack/harmful spell must make a Wis save or choose a new target.' },
|
|
486
|
+
'Scorching Ray': { kind: 'damage', icon: 'โ๏ธ', level: 2, school: 'Evocation',
|
|
487
|
+
effect: 'Three rays, each a ranged spell attack for 2d6 fire. +1 ray per slot level above 2.',
|
|
488
|
+
dice: '2d6', type: 'fire', rays: 3, perLevelRays: 1, attack: true },
|
|
489
|
+
'Scrying': { icon: '๐ฎ', level: 5, school: 'Divination', conc: true, duration: '10 minutes',
|
|
490
|
+
effect: 'Spy on a creature (Wis save) via an invisible sensor; penalty depends on your knowledge/connection.' },
|
|
491
|
+
'Sending': { icon: '๐จ', level: 3, school: 'Evocation',
|
|
492
|
+
effect: 'Send a 25-word message to a creature youโre familiar with, who can reply in kind.' },
|
|
493
|
+
'Sequester': { icon: '๐ฆ', level: 7, school: 'Transmutation',
|
|
494
|
+
effect: 'Hide a creature/object from divination and render it invisible and in suspended animation until a trigger.' },
|
|
495
|
+
'Shield': { kind: 'buff', icon: '๐ก๏ธ', level: 1, school: 'Abjuration', useSlot: true, duration: '1 round',
|
|
496
|
+
effect: 'Reaction: +5 AC until your next turn (including against the triggering attack) and no damage from Magic Missile.' },
|
|
497
|
+
'Silence': { kind: 'buff', icon: '๐', level: 2, school: 'Illusion', conc: true, ritual: true, duration: '10 minutes',
|
|
498
|
+
effect: 'A 20-ft radius sphere where no sound can be created or pass; blocks verbal-component casting and deafens.' },
|
|
499
|
+
'Simulacrum': { icon: '๐ฅ', level: 7, school: 'Illusion',
|
|
500
|
+
effect: 'Create an obedient illusory duplicate of a beast/humanoid (half its HP, canโt recover slots).' },
|
|
501
|
+
'Speak with Animals': { icon: '๐พ', level: 1, school: 'Divination', ritual: true, duration: '10 minutes',
|
|
502
|
+
effect: 'Comprehend and verbally communicate with beasts.' },
|
|
503
|
+
'Speak with Dead': { icon: '๐', level: 3, school: 'Necromancy', duration: '10 minutes',
|
|
504
|
+
effect: 'Ask a corpse (with a mouth, dead โค10 days) up to five questions.' },
|
|
505
|
+
'Speak with Plants': { icon: '๐ฟ', level: 3, school: 'Transmutation', duration: '10 minutes',
|
|
506
|
+
effect: 'Question and lightly command plants within 30 ft.' },
|
|
507
|
+
'Spike Growth': { kind: 'buff', icon: '๐ต', level: 2, school: 'Transmutation', conc: true, duration: '10 minutes',
|
|
508
|
+
effect: 'A 20-ft radius becomes difficult terrain dealing 2d4 piercing per 5 ft moved through it; camouflaged (Perception/Survival to spot).' },
|
|
509
|
+
'Spirit Guardians': { kind: 'damage', icon: '๐ผ', level: 3, school: 'Conjuration', conc: true, duration: '10 minutes',
|
|
510
|
+
effect: 'Spirits fill a 15-ft radius around you (half speed for enemies). 3d8 radiant or necrotic (Wis save half) on entry/start of turn. +1d8 per slot level above 3.',
|
|
511
|
+
dice: '3d8', type: 'radiant/necrotic', perLevel: '1d8', save: 'WIS', half: true },
|
|
512
|
+
'Spiritual Weapon': { kind: 'damage', icon: '๐ก๏ธ', level: 2, school: 'Evocation', duration: '1 minute',
|
|
513
|
+
effect: 'A floating spectral weapon (bonus action to move + attack): melee spell attack for 1d8 + spellcasting mod force. +1d8 per two slot levels above 2.',
|
|
514
|
+
dice: '1d8+MOD', type: 'force', perLevel: '1d8', attack: true },
|
|
515
|
+
'Symbol': { icon: '๐ฃ', level: 7, school: 'Abjuration',
|
|
516
|
+
effect: 'Inscribe a harmful glyph (Death, Discord, Fear, Hopelessness, Insanity, Pain, Sleep, or Stunning) triggered by conditions you set.' },
|
|
517
|
+
'Teleport': { icon: 'โจ', level: 7, school: 'Conjuration',
|
|
518
|
+
effect: 'Instantly transport you and up to 8 willing creatures (or one object) to a destination you know; accuracy depends on familiarity.' },
|
|
519
|
+
'Time Stop': { icon: 'โฑ๏ธ', level: 9, school: 'Transmutation',
|
|
520
|
+
effect: 'Take 1d4+1 turns in a row; ends if you affect another creature or move >1,000 ft from where you cast it.' },
|
|
521
|
+
'True Resurrection': { kind: 'buff', icon: 'โฐ๏ธ', level: 9, school: 'Necromancy',
|
|
522
|
+
effect: 'Return a creature dead โค200 years to life with full HP, curing all conditions; can even recreate a destroyed body.' },
|
|
523
|
+
'Vampiric Touch': { kind: 'damage', icon: '๐ฉธ', level: 3, school: 'Necromancy', conc: true, duration: '1 minute',
|
|
524
|
+
effect: 'Melee spell attack: 3d6 necrotic and you regain half that. Recast as an action each turn. +1d6 per slot level above 3.',
|
|
525
|
+
dice: '3d6', type: 'necrotic', perLevel: '1d6', attack: true },
|
|
526
|
+
'Wall of Fire': { kind: 'damage', icon: '๐ฅ', level: 4, school: 'Evocation', conc: true, duration: '1 minute',
|
|
527
|
+
effect: 'A wall of fire (one side chosen). 5d8 fire (Dex save half) to creatures within 10 ft of the hot side and on entry/start of turn. +1d8 per slot level above 4.',
|
|
528
|
+
dice: '5d8', type: 'fire', perLevel: '1d8', save: 'DEX', half: true },
|
|
529
|
+
'Wish': { icon: '๐ ', level: 9, school: 'Conjuration',
|
|
530
|
+
effect: 'Duplicate any โค8th-level spell, or alter reality per the listed safe uses (other uses risk never casting it again + 1d4 exhaustion).' },
|
|
531
|
+
'Word of Recall': { icon: '๐ ', level: 6, school: 'Conjuration',
|
|
532
|
+
effect: 'Instantly teleport you and up to 5 willing creatures to a sanctuary you designated.' },
|
|
533
|
+
'Zone of Truth': { kind: 'buff', icon: 'โ๏ธ', level: 2, school: 'Enchantment', duration: '10 minutes',
|
|
534
|
+
effect: 'A 15-ft radius where creatures (Cha save) canโt speak deliberate lies; you know who saved.' },
|
|
535
|
+
'Conjure Animals': { kind: 'buff', icon: '๐บ', level: 3, school: 'Conjuration', conc: true, duration: '1 hour',
|
|
536
|
+
effect: 'Summon fey spirits as beasts: one CR 2, two CR 1, four CR 1/2, or eight CR 1/4 (double at 5th, triple at 7th, quadruple at 9th).' },
|
|
537
|
+
'Conjure Elemental': { kind: 'buff', icon: '๐', level: 5, school: 'Conjuration', conc: true, duration: '1 hour',
|
|
538
|
+
effect: 'Summon an elemental of CR โค the slot level used. It obeys you while you concentrate; otherwise it may turn hostile.' },
|
|
539
|
+
'Conjure Fey': { kind: 'buff', icon: '๐ง', level: 6, school: 'Conjuration', conc: true, duration: '1 hour',
|
|
540
|
+
effect: 'Summon a fey creature of CR โค the slot level used.' },
|
|
541
|
+
'Conjure Celestial': { kind: 'buff', icon: '๐', level: 7, school: 'Conjuration', conc: true, duration: '1 hour',
|
|
542
|
+
effect: 'Summon a celestial of CR 4 (CR 5 if cast with a 9th-level slot).' },
|
|
543
|
+
'Shapechange': { icon: '๐', level: 9, school: 'Transmutation', conc: true, duration: '1 hour',
|
|
544
|
+
effect: 'Transform into a creature with a CR โค your level (must have a CR). You keep your alignment, Int, Wis, Cha, and class features.' },
|
|
545
|
+
'True Polymorph': { kind: 'buff', icon: '๐ฒ', level: 9, school: 'Transmutation', conc: true, duration: '1 hour',
|
|
546
|
+
effect: 'Transform a creature into another creature, a creature into an object, or vice versa (Wis save). After concentrating the full hour it becomes permanent.' },
|
|
547
|
+
'Planar Binding': { kind: 'buff', icon: 'โ๏ธ', level: 5, school: 'Abjuration', duration: '24 hours',
|
|
548
|
+
effect: 'Bind a celestial, elemental, fey, or fiend to your service (Cha save). Longer duration with higher-level slots (up to a year at 8th).' },
|
|
549
|
+
'Divine Intervention': { icon: '๐', level: null, school: 'Cleric feature',
|
|
550
|
+
effect: 'Call on your deity to intervene. Roll d100; if you roll โค your cleric level, it works. On success, you canโt use it again for 7 days; otherwise, again after a long rest.', castLabel: '๐ Call on your deity' },
|
|
551
|
+
'Harness Divine Power': { icon: 'โจ', level: null, school: 'Channel Divinity',
|
|
552
|
+
effect: 'Expend a use of Channel Divinity to regain one expended spell slot (level up to half your proficiency bonus, rounded up).' },
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// โโ route every dangling spell modal name to the dispatcher โโโโโโโโโโโโโโ
|
|
556
|
+
[
|
|
557
|
+
'showAbsorbElementsModal','showAidModal','showAnimateObjectsModal','showArmorOfAgathysModal',
|
|
558
|
+
'showAstralProjectionModal','showAuguryModal','showBaneModal','showBigbysHandModal','showBlessModal',
|
|
559
|
+
'showBoomingBladeModal','showChaosBoltModal','showChromaticOrbModal','showCloneModal','showCloudOfDaggersModal',
|
|
560
|
+
'showCommuneModal','showConjureModal','showContactOtherPlaneModal','showContingencyModal','showCounterspellModal',
|
|
561
|
+
'showDelayedBlastFireballModal','showDetectMagicModal','showDispelEvilAndGoodModal','showDispelMagicModal',
|
|
562
|
+
'showDivinationModal','showDragonsBreathModal','showDreamModal','showElementalWeaponModal','showEtherealnessModal',
|
|
563
|
+
'showFeatherFallModal','showFindThePathModal','showFireShieldModal','showFlamingSphereModal','showForcecageModal',
|
|
564
|
+
'showFreedomOfMovementModal','showGateModal','showGeasModal','showGlyphOfWardingModal','showGreaterRestorationModal',
|
|
565
|
+
'showGreenFlameBladeModal','showGuidanceModal','showHasteModal','showHealingSpiritModal','showHellishRebukeModal',
|
|
566
|
+
'showHexModal','showHuntersMarkModal','showIdentifyModal','showImprisonmentModal','showLegendLoreModal',
|
|
567
|
+
'showLifeTransferenceModal','showMagicCircleModal','showMagicJarModal','showMagicMissileModal','showMazeModal',
|
|
568
|
+
'showMeldIntoStoneModal','showMirageArcaneModal','showMoonbeamModal','showNondetectionModal','showPolymorphModal',
|
|
569
|
+
'showProgrammedIllusionModal','showProtectionFromEnergyModal','showProtectionFromEvilAndGoodModal','showRaiseDeadModal',
|
|
570
|
+
'showRemoveCurseModal','showResistanceModal','showResurrectionModal','showRevivifyModal','showSanctuaryModal',
|
|
571
|
+
'showScorchingRayModal','showScryingModal','showSendingModal','showSequesterModal','showShieldModal','showSilenceModal',
|
|
572
|
+
'showSimulacrumModal','showSpeakWithAnimalsModal','showSpeakWithDeadModal','showSpeakWithPlantsModal','showSpikeGrowthModal',
|
|
573
|
+
'showSpiritGuardiansModal','showSpiritualWeaponModal','showSymbolModal','showTeleportModal','showTimeStopModal',
|
|
574
|
+
'showTrueResurrectionModal','showVampiricTouchModal','showWallOfFireModal','showWishModal','showWordOfRecallModal',
|
|
575
|
+
'showZoneOfTruthModal','showConjureModal','showShapechangeModal','showTruePolymorphModal','showPlanarBindingModal',
|
|
576
|
+
'showDivineInterventionModal','showHarnessDivinePowerModal',
|
|
577
|
+
].forEach((name) => { globalThis[name] = showSpellActionModal; });
|
|
578
|
+
|
|
579
|
+
globalThis.showSpellActionModal = showSpellActionModal;
|
|
580
|
+
globalThis.SPELL_ACTION_DEFS = SPELL_DEFS;
|
|
581
|
+
|
|
582
|
+
(window.debug || console).log(`โ
Spell Action Modals loaded (${Object.keys(SPELL_DEFS).length} spell definitions)`);
|
|
583
|
+
})();
|