2025-05-28 15:36:51 -07:00

1321 lines
50 KiB
JavaScript

import { TypeScriptUtils } from '../../utils';
import { AbilityAccessors, AbilityDerivers } from '../Ability';
import { CharacterDerivers } from '../Character';
import { ClassAccessors } from '../Class';
import { AdditionalTypeEnum, CoreSimulators, MulticlassSpellSlotRoundingEnum } from '../Core';
import { DataOriginGenerators, DataOriginTypeEnum } from '../DataOrigin';
import { DiceAccessors, DiceSimulators } from '../Dice';
import { HelperUtils } from '../Helper';
import { ItemAccessors } from '../Item';
import { LimitedUseAccessors } from '../LimitedUse';
import { ModifierAccessors, ModifierDerivers, ModifierValidators } from '../Modifier';
import { RuleDataAccessors } from '../RuleData';
import { AdjustmentTypeEnum, ValueHacks, ValueUtils, ValueValidators } from '../Value';
import { getCastAtLevel, getCharacterLevel, getDataOrigin, getDataOriginType, getDefinitionAtHigherLevels, getDefinitionModifiers, getDefinitionName, getExpandedDataOriginRef, getLevel, getLimitedUse, getMappingEntityTypeId, getMappingId, getModifiers, getOverrideSaveDc, getRange, getRequiresSavingThrow, getRitual, getRitualCastingType, getScaleType, getSpellCastingAbilityId, getSpellListId, getUsesSpellSlot, isActive, isAlwaysPrepared, isAttack, isCantrip, isRitual, } from './accessors';
import { DB_STRING_SPELL_ELDRITCH_BLAST, RitualCastingTypeEnum, SPELL_CUSTOMIZATION_ADJUSTMENT_TYPES, SpellPrepareTypeEnum, SpellScaleTypeNameEnum, } from './constants';
import { getSpellFinalScaledDie } from './utils';
/**
* Calculates the Spellcasting Ability Modifier for the specified Proficiency Bonus and Ability Score Modifier,
* @param profBonus The proficiency bonus
* @param statModifier The modifier from the Ability Score
*/
export function deriveSpellAttackModifier(profBonus, statModifier) {
return profBonus + statModifier;
}
/**
* Determines if the specified spell is currently prepared
* @param spell The spell in question
* @param includeAlwaysPrepared When set to true, "Always Prepared" spells will be return true even if they are not prepared.
*/
export function deriveIsPrepared(spell, includeAlwaysPrepared = true) {
return spell.prepared || (includeAlwaysPrepared && isAlwaysPrepared(spell));
}
/**
* Determines if the specified spell can be cast as a ritual
* @param spell The spell in question
*/
export function deriveCanCastAsRitual(spell) {
return getRitualCastingType(spell) === RitualCastingTypeEnum.CAN_CAST_AS_RITUAL;
}
/**
* Determines if the spell can only be cast as a ritual
* @param spell The spell in question
*/
export function deriveMustCastAsRitual(spell) {
return getRitualCastingType(spell) === RitualCastingTypeEnum.MUST_CAST_AS_RITUAL;
}
/**
*
* @param spell
*/
export function deriveIsRitual(spell) {
const dataOrigin = getDataOrigin(spell);
const dataOriginType = getDataOriginType(spell);
if (dataOrigin) {
switch (dataOriginType) {
case DataOriginTypeEnum.CLASS:
case DataOriginTypeEnum.CLASS_FEATURE:
return getRitual(spell) && deriveIsCastedByRitualSpellcaster(spell);
case DataOriginTypeEnum.RACE:
case DataOriginTypeEnum.FEAT:
case DataOriginTypeEnum.ITEM:
return deriveCanCastAsRitual(spell) || deriveMustCastAsRitual(spell);
case DataOriginTypeEnum.SIMULATED:
return getRitual(spell);
default:
// not implemented
}
}
return false;
}
/**
*
* @param spell
*/
export function deriveIsCastAsRitual(spell) {
if (!getDataOrigin(spell) || !isRitual(spell)) {
return false;
}
// if we have an override to only cast, then don't let them cast with spell slot at all
if (deriveMustCastAsRitual(spell)) {
return true;
}
// otherwise if the spell isn't active, but we got to this point, it could only be cast as ritual
return !isActive(spell);
}
/**
*
* @param spell
*/
export function deriveExpandedContextIds(spell) {
let contextId = null;
let contextTypeId = null;
const expandedDataOriginRef = getExpandedDataOriginRef(spell);
if (expandedDataOriginRef) {
const dataOrigin = getDataOrigin(spell);
const dataOriginType = getDataOriginType(spell);
switch (dataOriginType) {
case DataOriginTypeEnum.CLASS:
const charClass = dataOrigin.primary;
contextId = ClassAccessors.getMappingId(charClass);
contextTypeId = ClassAccessors.getMappingEntityTypeId(charClass);
break;
default:
// not implemented
}
}
return [contextId, contextTypeId];
}
/**
* TODO - move to utils
* @param level
* @param slotLevels
*/
export function getCastLevelAvailableCount(level, slotLevels) {
const slotLevel = slotLevels.find((slotLevel) => slotLevel.level === level);
if (!slotLevel) {
return null;
}
return slotLevel.available - slotLevel.used;
}
/**
* TODO - move to utils
* @param spell
* @param scaledAmount
*/
export function getConsumedLimitedUse(spell, scaledAmount) {
const limitedUse = getLimitedUse(spell);
if (limitedUse === null) {
return 0;
}
const minConsumed = LimitedUseAccessors.getMinNumberConsumed(limitedUse);
if (getDataOriginType(spell) === DataOriginTypeEnum.ITEM) {
return minConsumed + scaledAmount;
}
return minConsumed;
}
/**
*
* @param spell
*/
export function deriveStartingCastLevel(spell) {
if (isCantrip(spell)) {
return getLevel(spell);
}
const castAtLevel = getCastAtLevel(spell);
let startingCastLevel = getLevel(spell);
if (castAtLevel !== null) {
startingCastLevel = castAtLevel;
}
return startingCastLevel;
}
/**
* TODO - move to utils
* @param spell
* @param spellCasterInfo
* @param ruleData
* @param initialCastLevel
*/
export function getMinCastLevel(spell, spellCasterInfo, ruleData, initialCastLevel = null) {
const { startLevel, endLevel } = getCastLevelRange(spell, spellCasterInfo, ruleData);
let castLevel = deriveStartingCastLevel(spell);
if (initialCastLevel !== null) {
castLevel = initialCastLevel;
}
return Math.max(castLevel, startLevel);
}
/**
* TODO - move to utils
* @param spell
* @param spellCasterInfo
* @param ruleData
* @param initialCastLevel
*/
export function getCastableLevels(spell, spellCasterInfo, ruleData, initialCastLevel = null) {
const { startLevel, endLevel } = getCastLevelRange(spell, spellCasterInfo, ruleData);
const usesSpellSlot = getUsesSpellSlot(spell);
const castAtLevel = getCastAtLevel(spell);
const castableLevels = [];
for (let i = startLevel; i <= endLevel; i++) {
if (initialCastLevel !== null && initialCastLevel === i) {
castableLevels.push(i);
}
else if (castAtLevel !== null && castAtLevel === i) {
castableLevels.push(i);
}
else if (getDataOriginType(spell) === DataOriginTypeEnum.ITEM) {
castableLevels.push(i);
}
else if (usesSpellSlot) {
if (spellCasterInfo.availableSpellLevels.includes(i) ||
spellCasterInfo.availablePactMagicLevels.includes(i)) {
castableLevels.push(i);
}
}
}
return castableLevels;
}
/**
* TODO - move to utils
* @param spell
* @param spellCasterInfo
* @param ruleData
*/
export function getCastLevelRange(spell, spellCasterInfo, ruleData) {
let startLevel = getLevel(spell);
let endLevel = getLevel(spell);
const castAtLevel = getCastAtLevel(spell);
const limitedUse = getLimitedUse(spell);
if (isCantrip(spell)) {
return {
startLevel,
endLevel,
};
}
if (getUsesSpellSlot(spell)) {
const availableLevels = [
...spellCasterInfo.availableSpellLevels,
...spellCasterInfo.availablePactMagicLevels,
].filter((level) => level >= getLevel(spell));
startLevel = Math.max(getLevel(spell), Math.min(...availableLevels));
endLevel = Math.max(getLevel(spell), ...availableLevels);
if (limitedUse && getDataOriginType(spell) === DataOriginTypeEnum.CLASS_FEATURE) {
const dataOrigin = getDataOrigin(spell);
const charClass = dataOrigin.parent;
if (ClassAccessors.isPactMagicActive(charClass)) {
startLevel = Math.min(...spellCasterInfo.availablePactMagicLevels);
endLevel = Math.max(...spellCasterInfo.availablePactMagicLevels);
}
else {
startLevel = Math.max(getLevel(spell), Math.min(...spellCasterInfo.availableSpellLevels));
endLevel = Math.max(getLevel(spell), ...spellCasterInfo.availableSpellLevels);
}
}
}
if (getDataOriginType(spell) !== DataOriginTypeEnum.CLASS) {
if (castAtLevel === null) {
switch (getDataOriginType(spell)) {
case DataOriginTypeEnum.ITEM:
let itemSpellLevelDiff = 0;
if (limitedUse) {
const minNumberConsumed = LimitedUseAccessors.getMinNumberConsumed(limitedUse);
let maxNumberConsumed = LimitedUseAccessors.getMaxNumberConsumed(limitedUse);
if (maxNumberConsumed === null) {
maxNumberConsumed = minNumberConsumed;
}
itemSpellLevelDiff = maxNumberConsumed - minNumberConsumed;
}
endLevel = startLevel + itemSpellLevelDiff;
break;
default:
// not implemented
}
}
else {
startLevel = castAtLevel;
endLevel = castAtLevel;
}
}
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
return {
startLevel: HelperUtils.clampInt(startLevel, 1, maxSpellLevel),
endLevel: HelperUtils.clampInt(endLevel, 1, maxSpellLevel),
};
}
/**
* TODO - move to validators
* @param spell
* @param scaleType
* @param higherLevelsItems
* @param castLevel
*/
export function isSpellScaledByCastLevel(spell, scaleType, higherLevelsItems, castLevel) {
if (scaleType === null) {
return false;
}
if (scaleType === SpellScaleTypeNameEnum.CHARACTER_LEVEL) {
return false;
}
if (getCastAtLevel(spell) === castLevel) {
return false;
}
if (!higherLevelsItems.length) {
return false;
}
return !!spellLevelFilter(higherLevelsItems, castLevel);
}
/**
* TODO - move to utils
* @param spell
* @param scaleType
* @param higherLevelsItems
* @param characterLevel //deprecated and will be removed, DCP-1412
* @param castLevel
*/
export function getSpellScaledAtHigher(spell, scaleType, higherLevelsItems, characterLevel, castLevel) {
if (scaleType === null) {
return null;
}
const castAtLevel = getCastAtLevel(spell);
if (scaleType === SpellScaleTypeNameEnum.CHARACTER_LEVEL) {
return characterLevelFilter(higherLevelsItems, getCharacterLevel(spell));
}
else if (scaleType === SpellScaleTypeNameEnum.SPELL_SCALE || scaleType === SpellScaleTypeNameEnum.SPELL_LEVEL) {
return spellLevelFilter(higherLevelsItems, castAtLevel === null ? castLevel : castAtLevel);
}
return null;
}
/**
* TODO - move to utils
* @param spell
* @param characterLevel
*/
export function getScaledDamage(spell, characterLevel = null) {
const damageDice = [];
const castLevel = deriveStartingCastLevel(spell);
const damageModifiers = getModifiers(spell).filter((modifier) => ModifierValidators.isSpellDamageModifier(modifier));
damageModifiers.map((modifier) => {
const { atHigherLevels } = modifier;
if (atHigherLevels !== null && atHigherLevels.points !== null) {
const atHigherDamage = getSpellScaledAtHigher(spell, getScaleType(spell), atHigherLevels.points, characterLevel, castLevel);
const scaledDamageDie = getSpellFinalScaledDie(spell, modifier, atHigherDamage);
const type = ModifierAccessors.getFriendlySubtypeName(modifier);
if (scaledDamageDie !== null && type !== null) {
damageDice.push({
dice: scaledDamageDie,
type,
restriction: ModifierAccessors.getRestriction(modifier),
});
}
}
});
return damageDice;
}
/**
* TODO - move to validators
* @param spell
* @param level
* @param spellCasterInfo
*/
export function isSpellAvailableAtLevel(spell, level, spellCasterInfo) {
if (isCantrip(spell)) {
return true;
}
if (spellCasterInfo.availableSpellLevels.includes(level) ||
spellCasterInfo.availablePactMagicLevels.includes(level)) {
return true;
}
return false;
}
/**
* NOTE: leaving for now, this will go away eventually once we generate higher level locally
* @param higherLevelsItems
* @param characterLevel
*/
function characterLevelFilter(higherLevelsItems, characterLevel) {
if (characterLevel === null) {
return null;
}
if (!higherLevelsItems.length) {
return null;
}
return HelperUtils.getLast(higherLevelsItems.filter((item) => item.level !== null && item.level <= characterLevel), ['level']);
}
/**
* NOTE: leaving for now, this will go away eventually once we generate higher level locally
* @param higherLevelsItems
* @param castLevel
*/
function spellLevelFilter(higherLevelsItems, castLevel) {
if (castLevel === null) {
return null;
}
if (!higherLevelsItems.length) {
return null;
}
return HelperUtils.getLast(higherLevelsItems.filter((item) => item.level === null || item.level <= castLevel), ['level']);
}
/**
*
* @param prepareType
* @param level
* @param modifier
*/
export function deriveSpellPrepareMax(prepareType, level, modifier) {
switch (prepareType) {
case SpellPrepareTypeEnum.LEVEL:
return Math.max(modifier + level, 1);
case SpellPrepareTypeEnum.HALF_LEVEL:
return Math.max(modifier + Math.floor(level / 2), 1);
default:
// not implemented
}
return null;
}
/**
*
* @param charClass
* @param abilityId
* @param ruleData
* @param overrideIsRitualSpellCaster
*/
export function deriveClassSpellRules(charClass, abilityId, ruleData, overrideIsRitualSpellCaster = false) {
var _a;
const spellcastingAbilityStatId = abilityId;
let spellcastingAbilityStat = null;
if (spellcastingAbilityStatId) {
spellcastingAbilityStat = ruleData.statsLookup[spellcastingAbilityStatId].key;
}
const configSpellRules = ClassAccessors.getSpellConfiguration(charClass);
let filteredLevelCantripsKnownMaxes = [];
if (configSpellRules && configSpellRules.levelCantripsKnownMaxes !== null) {
filteredLevelCantripsKnownMaxes = configSpellRules.levelCantripsKnownMaxes.filter((max) => max !== 0);
}
const spellPrepareType = ClassAccessors.getSpellPrepareType(charClass);
const knowsAllSpells = ClassAccessors.getKnowsAllSpells(charClass);
const hasSpellPrepareMax = spellPrepareType !== null;
const isKnownSpellcaster = !hasSpellPrepareMax;
const isSpellbookSpellcaster = hasSpellPrepareMax && !knowsAllSpells;
const isPreparedSpellcaster = hasSpellPrepareMax && !isSpellbookSpellcaster;
let isRitualSpellCaster = false;
if (overrideIsRitualSpellCaster) {
isRitualSpellCaster = true;
}
else {
isRitualSpellCaster = configSpellRules === null ? false : configSpellRules.isRitualSpellCaster;
}
let levelCantripsKnownMaxes = null;
if (filteredLevelCantripsKnownMaxes.length) {
levelCantripsKnownMaxes = configSpellRules === null ? null : configSpellRules.levelCantripsKnownMaxes;
}
let levelSpellKnownMaxes = [];
if (configSpellRules !== null && configSpellRules.levelSpellKnownMaxes) {
levelSpellKnownMaxes = configSpellRules.levelSpellKnownMaxes.filter(TypeScriptUtils.isNotNullOrUndefined);
}
let levelPreparedSpellMaxes = [];
if (configSpellRules !== null && configSpellRules.levelPreparedSpellMaxes) {
levelPreparedSpellMaxes = configSpellRules.levelPreparedSpellMaxes.filter(TypeScriptUtils.isNotNullOrUndefined);
}
let levelSpellSlots = [];
if (configSpellRules !== null) {
levelSpellSlots = (_a = configSpellRules.levelSpellSlots) !== null && _a !== void 0 ? _a : [
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
];
}
return {
spellcastingAbilityStat,
spellcastingAbilityStatId,
knowsAllSpells,
levelCantripsKnownMaxes,
levelSpellKnownMaxes,
levelPreparedSpellMaxes,
levelSpellSlots,
isRitualSpellCaster,
isKnownSpellcaster,
isSpellbookSpellcaster,
isPreparedSpellcaster,
};
}
/**
*
* @param pactMagicClasses
* @param ruleData
*/
export function deriveClassPactMagicSlots(pactMagicClasses, ruleData) {
let pactMagicSlotsAvailable = [];
if (pactMagicClasses.length > 1) {
pactMagicSlotsAvailable = deriveMulticlassSlots(pactMagicClasses, ruleData, RuleDataAccessors.getPactMagicMultiClassSpellSlots(ruleData));
}
else if (pactMagicClasses.length === 1) {
pactMagicSlotsAvailable = deriveClassLevelSpellSlots(pactMagicClasses[0]);
}
return pactMagicSlotsAvailable;
}
/**
*
* @param spellcastingClasses
* @param ruleData
*/
export function deriveClassSpellSlots(spellcastingClasses, ruleData) {
let spellSlotsAvailable = [];
if (spellcastingClasses.length > 1) {
spellSlotsAvailable = deriveMulticlassSlots(spellcastingClasses, ruleData, RuleDataAccessors.getMultiClassSpellSlots(ruleData));
}
else if (spellcastingClasses.length === 1) {
spellSlotsAvailable = deriveClassLevelSpellSlots(spellcastingClasses[0]);
}
return spellSlotsAvailable;
}
/**
*
* @param charClass
*/
export function deriveClassLevelSpellSlots(charClass) {
const classLevel = ClassAccessors.getLevel(charClass);
const configSpellRules = ClassAccessors.getSpellConfiguration(charClass);
return configSpellRules && configSpellRules.levelSpellSlots ? configSpellRules.levelSpellSlots[classLevel] : [];
}
/**
*
* @param charClasses
* @param ruleData
* @param spellSlotsData
*/
export function deriveMulticlassSlots(charClasses, ruleData, spellSlotsData) {
if (spellSlotsData === null) {
return [];
}
const combinedLevel = charClasses.reduce((acc, charClass) => {
const configSpellRules = ClassAccessors.getSpellConfiguration(charClass);
if (configSpellRules !== null && configSpellRules.multiClassSpellSlotDivisor !== null) {
const levelContribution = ClassAccessors.getLevel(charClass) / configSpellRules.multiClassSpellSlotDivisor;
if (configSpellRules.multiClassSpellSlotRounding === MulticlassSpellSlotRoundingEnum.UP) {
acc += Math.ceil(levelContribution);
}
else {
acc += Math.floor(levelContribution);
}
}
return acc;
}, 0);
return spellSlotsData[combinedLevel];
}
/**
*
* @param knownCantripsMax
* @param activeCantripsCount
*/
export function deriveIsCantripsKnownMaxed(knownCantripsMax, activeCantripsCount) {
let cantripsKnownMaxed = false;
if (knownCantripsMax === null) {
cantripsKnownMaxed = true;
}
else {
cantripsKnownMaxed = activeCantripsCount >= knownCantripsMax;
}
return cantripsKnownMaxed;
}
/**
*
* @param knownSpellsMax
* @param knownSpellCount
* @param knowAllClassSpells
*/
export function deriveIsSpellsKnownMaxed(knownSpellsMax, knownSpellCount, knowAllClassSpells) {
let spellsKnownMaxed = false;
if (knowAllClassSpells) {
spellsKnownMaxed = true;
}
else if (knownSpellsMax === null) {
spellsKnownMaxed = false;
}
else {
spellsKnownMaxed = knownSpellCount >= knownSpellsMax;
}
return spellsKnownMaxed;
}
/**
*
* @param spellSlots
* @param pactMagicSlots
* @param ruleData
*/
export function deriveCombinedSpellSlotsInfo(spellSlots, pactMagicSlots, ruleData) {
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
const combinedSpellSlots = [];
for (let i = 1; i <= maxSpellLevel; i++) {
const pactMagicSlotLevel = pactMagicSlots.find((slotLevel) => slotLevel.level === i);
const spellSlotLevel = spellSlots.find((slotLevel) => slotLevel.level === i);
const pactMagicAvailable = pactMagicSlotLevel ? pactMagicSlotLevel.available : 0;
const pactMagicUsed = pactMagicSlotLevel ? pactMagicSlotLevel.used : 0;
const spellAvailable = spellSlotLevel ? spellSlotLevel.available : 0;
const spellUsed = spellSlotLevel ? spellSlotLevel.used : 0;
combinedSpellSlots.push({
level: i,
available: pactMagicAvailable + spellAvailable,
used: pactMagicUsed + spellUsed,
remaining: pactMagicAvailable + spellAvailable - (pactMagicUsed + spellUsed),
});
}
return combinedSpellSlots;
}
/**
*
* @param spellcastingClasses
* @param spellSlotsData
* @param ruleData
*/
export function deriveSpellSlotsInfo(spellcastingClasses, spellSlotsData, ruleData) {
const spellSlotsAvailable = deriveClassSpellSlots(spellcastingClasses, ruleData);
return spellSlotsAvailable
.map((availableCount, idx) => {
if (availableCount === 0) {
return null;
}
return {
available: availableCount,
used: spellSlotsData.length ? spellSlotsData[idx].used : 0,
level: idx + 1,
};
})
.filter(TypeScriptUtils.isNotNullOrUndefined);
}
/**
*
* @param pactMagicClasses
* @param pactMagicSlotsData
* @param ruleData
*/
export function derivePactMagicSlotsInfo(pactMagicClasses, pactMagicSlotsData, ruleData) {
const pactMagicSlotsAvailable = deriveClassPactMagicSlots(pactMagicClasses, ruleData);
return pactMagicSlotsAvailable
.map((availableCount, idx) => {
if (availableCount === 0) {
return null;
}
return {
available: availableCount,
used: pactMagicSlotsData.length ? pactMagicSlotsData[idx].used : 0,
level: idx + 1,
};
})
.filter(TypeScriptUtils.isNotNullOrUndefined)
.reduce((acc, slot) => {
if (slot.available !== 0) {
acc.push(slot);
}
return acc;
}, []);
}
/**
* TODO refactor this to supply base toHit and bonus toHit so that it doesn't have to know if bonuses are added in already
* @param spell
* @param modifiers
* @param abilityLookup
* @param profBonus
* @param fallbackToHit
* @param valueLookup
*/
export function deriveSpellToHit(spell, modifiers, abilityLookup, profBonus, fallbackToHit, valueLookup) {
let toHit = null;
if (isAttack(spell)) {
const toHitOverride = deriveCustomValue(spell, AdjustmentTypeEnum.TO_HIT_OVERRIDE, null, valueLookup);
let toHitBonus = deriveCustomValue(spell, AdjustmentTypeEnum.TO_HIT_BONUS, 0, valueLookup);
toHitBonus = toHitBonus === null ? 0 : toHitBonus;
if (toHitOverride !== null) {
return toHitOverride;
}
let baseToHit = 0;
const spellCastingAbilityId = getSpellCastingAbilityId(spell);
if (spellCastingAbilityId === null) {
baseToHit = fallbackToHit;
}
else {
baseToHit = deriveSpellAttackModifier(profBonus, AbilityDerivers.deriveStatModifier(spellCastingAbilityId, abilityLookup));
}
const dataOriginType = getDataOrigin(spell) ? getDataOriginType(spell) : null;
if (dataOriginType !== DataOriginTypeEnum.CLASS && dataOriginType !== DataOriginTypeEnum.CLASS_FEATURE) {
// find out about bonus toHit for current spell
const bonusToHitModifiers = modifiers.filter((modifier) => ModifierValidators.isValidBonusSpellAttackModifier(modifier, spell));
baseToHit += ModifierDerivers.sumModifiers(bonusToHitModifiers, abilityLookup);
}
toHit = baseToHit + toHitBonus;
}
return toHit;
}
/**
*
* @param spell
* @param abilityLookup
* @param fallbackModifier
*/
export function deriveSpellSpellcastingModifier(spell, abilityLookup, fallbackModifier) {
const spellCastingAbilityId = getSpellCastingAbilityId(spell);
if (spellCastingAbilityId !== null) {
const ability = abilityLookup[spellCastingAbilityId];
const abilityModifier = AbilityAccessors.getModifier(ability);
if (ability && abilityModifier !== null) {
return abilityModifier;
}
return fallbackModifier;
}
return fallbackModifier;
}
/**
*
* @param spell
*/
export function deriveIsCastedBySpellbookSpellcaster(spell) {
const dataOrigin = getDataOrigin(spell);
return !!(dataOrigin.primary !== null && ClassAccessors.isSpellbookSpellcaster(dataOrigin.primary));
}
/**
*
* @param spell
*/
export function deriveIsCastedByRitualSpellcaster(spell) {
const dataOrigin = getDataOrigin(spell);
const dataOriginType = getDataOriginType(spell);
switch (dataOriginType) {
case DataOriginTypeEnum.CLASS:
return !!(dataOrigin.primary !== null &&
ClassAccessors.isRitualSpellCaster(dataOrigin.primary));
case DataOriginTypeEnum.CLASS_FEATURE:
return !!(dataOrigin.parent !== null &&
ClassAccessors.isRitualSpellCaster(dataOrigin.parent));
}
return false;
}
/**
*
* @param spell
*/
export function deriveIsActive(spell) {
switch (getDataOriginType(spell)) {
case DataOriginTypeEnum.CLASS:
const dataOrigin = getDataOrigin(spell);
const spellIsPepared = deriveIsPrepared(spell);
const charClass = dataOrigin.primary;
return (isCantrip(spell) ||
(ClassAccessors.isPreparedSpellcaster(charClass) && spellIsPepared) ||
ClassAccessors.isKnownSpellcaster(charClass) ||
(ClassAccessors.isSpellbookSpellcaster(charClass) && spellIsPepared));
default:
return true;
}
}
/**
*
* @param spell
* @param modifiers
* @param abilityLookup
* @param fixedDamageBonus
* @param valueLookup
*/
export function deriveSpellBonusFixedDamage(spell, modifiers, abilityLookup, fixedDamageBonus, valueLookup) {
let adjustmentDamageBonus = deriveCustomValue(spell, AdjustmentTypeEnum.FIXED_VALUE_BONUS, 0, valueLookup);
adjustmentDamageBonus = adjustmentDamageBonus === null ? 0 : adjustmentDamageBonus;
const damageSpellAttackModifiers = modifiers.filter((modifier) => ModifierValidators.isValidDamageSpellAttackModifier(modifier, spell));
const damageSpellAttackModifierTotal = ModifierDerivers.sumModifiers(damageSpellAttackModifiers, abilityLookup);
let extraDamageBonus = 0;
if (getDefinitionName(spell) === DB_STRING_SPELL_ELDRITCH_BLAST) {
const eldritchBlastBonusDamageModifiers = modifiers.filter((modifier) => ModifierValidators.isEldritchBlastBonusDamageModifier(modifier));
const edlritchBlastBonusDamageTotal = ModifierDerivers.sumModifiers(eldritchBlastBonusDamageModifiers, abilityLookup);
extraDamageBonus += edlritchBlastBonusDamageTotal;
}
return damageSpellAttackModifierTotal + fixedDamageBonus + adjustmentDamageBonus + extraDamageBonus;
}
/**
*
* @param spell
* @param modifiers
* @param proficiencyBonus
* @param abilityLookup
* @param fallbackBaseSaveDc
* @param valueLookup
*/
export function deriveSpellAttackSaveValue(spell, modifiers, proficiencyBonus, abilityLookup, fallbackBaseSaveDc, valueLookup) {
if (!getRequiresSavingThrow(spell)) {
return null;
}
const saveDcOverride = deriveCustomValue(spell, AdjustmentTypeEnum.SAVE_DC_OVERRIDE, null, valueLookup);
let saveDcBonus = deriveCustomValue(spell, AdjustmentTypeEnum.SAVE_DC_BONUS, 0, valueLookup);
saveDcBonus = saveDcBonus === null ? 0 : saveDcBonus;
if (saveDcOverride !== null) {
return saveDcOverride;
}
const overrideSaveDc = getOverrideSaveDc(spell);
if (overrideSaveDc !== null) {
return overrideSaveDc;
}
let saveDc = 0;
const spellCastingAbilityId = getSpellCastingAbilityId(spell);
if (spellCastingAbilityId === null) {
saveDc = fallbackBaseSaveDc + saveDcBonus;
}
else {
const abilityModifier = AbilityDerivers.deriveStatModifier(spellCastingAbilityId, abilityLookup);
saveDc = CharacterDerivers.deriveAttackSaveValue(proficiencyBonus, abilityModifier) + saveDcBonus;
}
switch (getDataOriginType(spell)) {
case DataOriginTypeEnum.CLASS:
case DataOriginTypeEnum.CLASS_FEATURE:
case DataOriginTypeEnum.ITEM:
break;
default:
const bonusToHitModifiers = modifiers.filter((modifier) => ModifierValidators.isBonusSpellSaveDc(modifier));
saveDc += ModifierDerivers.sumModifiers(bonusToHitModifiers, abilityLookup);
}
return saveDc;
}
/**
*
* @param spell
* @param modifiers
* @param abilityLookup
*/
export function deriveSpellRange(spell, modifiers, abilityLookup) {
const range = getRange(spell);
let extraRangeBonus = 0;
if (getDefinitionName(spell) === DB_STRING_SPELL_ELDRITCH_BLAST) {
const eldritchBlastBonusRangeModifiers = modifiers.filter((modifier) => ModifierValidators.isEldritchBlastBonusRangeModifier(modifier));
const edlritchBlastBonusRangeTotal = ModifierDerivers.sumModifiers(eldritchBlastBonusRangeModifiers, abilityLookup);
extraRangeBonus += edlritchBlastBonusRangeTotal;
}
let spellAttackRangeMultiplierTotal = 1;
if (isAttack(spell)) {
const spellAttackRangeMultiplierModifiers = modifiers.filter((modifier) => ModifierValidators.isBonusSpellAttackRangeMultiplierModifier(modifier));
spellAttackRangeMultiplierTotal = Math.max(1, ModifierDerivers.sumModifiers(spellAttackRangeMultiplierModifiers, abilityLookup, 1));
}
return (((range !== null && range.rangeValue ? range.rangeValue : 0) + extraRangeBonus) *
spellAttackRangeMultiplierTotal);
}
/**
*
* @param spell
* @param valueLookup
*/
export function deriveIsCustomized(spell, valueLookup) {
const [contextId, contextTypeId] = deriveExpandedContextIds(spell);
return ValueValidators.validateHasCustomization(SPELL_CUSTOMIZATION_ADJUSTMENT_TYPES, valueLookup, ValueHacks.hack__toString(getMappingId(spell)), ValueHacks.hack__toString(getMappingEntityTypeId(spell)), ValueHacks.hack__toString(contextId), ValueHacks.hack__toString(contextTypeId));
}
/**
*
* @param spell
* @param spellListDataOriginLookup
*/
export function deriveExpandedDataOriginRef(spell, spellListDataOriginLookup) {
const spellListId = getSpellListId(spell);
if (spellListId !== null) {
const dataOriginRef = HelperUtils.lookupDataOrFallback(spellListDataOriginLookup, spellListId);
if (dataOriginRef === null) {
return DataOriginGenerators.generateDataOriginRef(DataOriginTypeEnum.UNKNOWN, '');
}
return dataOriginRef;
}
return null;
}
/**
*
* @param spell
* @param valueTypeId
* @param fallback
* @param valueLookup
*/
function deriveCustomValue(spell, valueTypeId, fallback, valueLookup) {
if (valueLookup) {
const [contextId, contextTypeId] = deriveExpandedContextIds(spell);
return ValueUtils.getKeyValue(valueLookup, valueTypeId, ValueHacks.hack__toString(getMappingId(spell)), ValueHacks.hack__toString(getMappingEntityTypeId(spell)), fallback, ValueHacks.hack__toString(contextId), ValueHacks.hack__toString(contextTypeId));
}
return fallback;
}
/**
*
* @param spell
* @param valueLookup
*/
export function deriveNotes(spell, valueLookup) {
return deriveCustomValue(spell, AdjustmentTypeEnum.NOTES, null, valueLookup);
}
/**
*
* @param spell
* @param valueLookup
*/
export function deriveName(spell, valueLookup) {
const definitionName = getDefinitionName(spell);
let fallbackName = '';
if (definitionName !== null) {
fallbackName = definitionName;
}
return deriveCustomValue(spell, AdjustmentTypeEnum.NAME_OVERRIDE, fallbackName, valueLookup);
}
/**
*
* @param spell
* @param modifiers
* @param abilityLookup
*/
export function deriveRange(spell, modifiers, abilityLookup) {
let range = null;
if (spell.range !== null) {
range = Object.assign(Object.assign({}, spell.range), { rangeValue: deriveSpellRange(spell, modifiers, abilityLookup) });
}
return range;
}
/**
*
* @param spell
* @param classInfo
*/
export function deriveCanRemove(spell, classInfo) {
let canRemove = false;
if (getLevel(spell) === 0) {
if (classInfo.knownCantripsMax !== null) {
canRemove = true;
}
}
else if (!classInfo.knowsAllSpells) {
canRemove = true;
}
return canRemove;
}
/**
*
* @param spell
* @param classInfo
*/
export function deriveCanPrepare(spell, classInfo) {
let canPrepare = !!classInfo.prepareMax;
if (getLevel(spell) === 0 || isAlwaysPrepared(spell)) {
canPrepare = false;
}
return canPrepare;
}
/**
*
* @param spell
* @param valueLookup
*/
export function deriveIsDisplayAsAttack(spell, valueLookup) {
return deriveCustomValue(spell, AdjustmentTypeEnum.DISPLAY_AS_ATTACK, null, valueLookup);
}
/**
*
* @param spell
* @param characterLevel
*/
export function deriveCharacterLevel(spell, characterLevel) {
var _a;
if (getScaleType(spell) === SpellScaleTypeNameEnum.CHARACTER_LEVEL) {
return (_a = getCastAtLevel(spell)) !== null && _a !== void 0 ? _a : characterLevel;
}
return characterLevel;
}
/**
*
* @param spell
*/
export function deriveUniqueKey(spell) {
let sourceId = '';
switch (getDataOriginType(spell)) {
case DataOriginTypeEnum.ITEM:
sourceId = ItemAccessors.getUniqueKey(getDataOrigin(spell).primary);
break;
case DataOriginTypeEnum.CLASS:
sourceId = ClassAccessors.getId(getDataOrigin(spell).primary);
break;
default:
// not implemented
}
return `${deriveKnownKey(spell)}-${sourceId}`;
}
/**
*
* @param spell
*/
export function deriveKnownKey(spell) {
return `${getMappingId(spell)}-${getMappingEntityTypeId(spell)}`;
}
/**
*
* @param spell
*/
export function deriveLeveledKnownKey(spell, castLevel) {
return `${getMappingId(spell)}-${getMappingEntityTypeId(spell)}-${castLevel}`;
}
/**
*
* @param scaleType
* @param higherLevelDefinition
* @param spell
* @param ruleData
*/
export function deriveAtHigherLevelAdditionalCountEntries(scaleType, higherLevelDefinition, spell, ruleData) {
var _a, _b, _c;
const entries = [];
if (scaleType === SpellScaleTypeNameEnum.SPELL_SCALE) {
const spellLevel = getLevel(spell);
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
const levelIncrement = (_a = higherLevelDefinition.level) !== null && _a !== void 0 ? _a : 1;
const start = spellLevel + levelIncrement;
let j = 1;
for (let i = start; i <= maxSpellLevel; i += levelIncrement, j++) {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: i,
description: higherLevelDefinition.details,
totalCount: j * ((_b = higherLevelDefinition.value) !== null && _b !== void 0 ? _b : 1),
}));
}
}
else {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: higherLevelDefinition.level,
description: higherLevelDefinition.details,
totalCount: (_c = higherLevelDefinition.value) !== null && _c !== void 0 ? _c : 1,
}));
}
return entries;
}
/**
*
* @param scaleType
* @param higherLevelDefinition
* @param spell
* @param ruleData
*/
export function deriveAtHigherLevelAdditionalTargetsEntries(scaleType, higherLevelDefinition, spell, ruleData) {
var _a, _b, _c;
const entries = [];
if (scaleType === SpellScaleTypeNameEnum.SPELL_SCALE) {
const spellLevel = getLevel(spell);
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
const levelIncrement = (_a = higherLevelDefinition.level) !== null && _a !== void 0 ? _a : 1;
const start = spellLevel + levelIncrement;
let j = 1;
for (let i = start; i <= maxSpellLevel; i += levelIncrement, j++) {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: i,
description: higherLevelDefinition.details,
targets: j * ((_b = higherLevelDefinition.value) !== null && _b !== void 0 ? _b : 1),
}));
}
}
else {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: higherLevelDefinition.level,
description: higherLevelDefinition.details,
targets: (_c = higherLevelDefinition.value) !== null && _c !== void 0 ? _c : 1,
}));
}
return entries;
}
/**
*
* @param scaleType
* @param higherLevelDefinition
* @param spell
* @param ruleData
*/
export function deriveAtHigherLevelExtendedAreaEntries(scaleType, higherLevelDefinition, spell, ruleData) {
var _a, _b, _c;
const entries = [];
if (scaleType === SpellScaleTypeNameEnum.SPELL_SCALE) {
const spellRange = getRange(spell);
const spellLevel = getLevel(spell);
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
const levelIncrement = (_a = higherLevelDefinition.level) !== null && _a !== void 0 ? _a : 1;
const start = spellLevel + levelIncrement;
let j = 1;
for (let i = start; i <= maxSpellLevel; i += levelIncrement, j++) {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: i,
description: higherLevelDefinition.details,
extendedAoe: j * ((_b = higherLevelDefinition.value) !== null && _b !== void 0 ? _b : 1) + ((_c = spellRange === null || spellRange === void 0 ? void 0 : spellRange.aoeValue) !== null && _c !== void 0 ? _c : 0),
}));
}
}
else {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: higherLevelDefinition.level,
description: higherLevelDefinition.details,
extendedAoe: higherLevelDefinition.value,
}));
}
return entries;
}
/**
*
* @param scaleType
* @param higherLevelDefinition
* @param spell
* @param ruleData
*/
export function deriveAtHigherLevelCommonDescriptionEntries(scaleType, higherLevelDefinition, spell, ruleData) {
const entries = [];
if (scaleType === SpellScaleTypeNameEnum.SPELL_SCALE) {
let levelIncrement = higherLevelDefinition.level;
if (levelIncrement === null) {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: higherLevelDefinition.level,
description: higherLevelDefinition.details,
}));
}
else {
const spellLevel = getLevel(spell);
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
const start = spellLevel + levelIncrement;
for (let i = start; i <= maxSpellLevel; i += levelIncrement) {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: i,
description: higherLevelDefinition.details,
}));
}
}
}
else {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: higherLevelDefinition.level,
description: higherLevelDefinition.details,
}));
}
return entries;
}
export function deriveAtHigherLevelExtendedRangeEntries(scaleType, higherLevelDefinition, spell, ruleData) {
var _a, _b, _c;
const entries = [];
if (scaleType === SpellScaleTypeNameEnum.SPELL_SCALE) {
const spellRange = getRange(spell);
const spellLevel = getLevel(spell);
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
const levelIncrement = (_a = higherLevelDefinition.level) !== null && _a !== void 0 ? _a : 1;
const start = spellLevel + levelIncrement;
let j = 1;
for (let i = start; i <= maxSpellLevel; i += levelIncrement, j++) {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: i,
description: higherLevelDefinition.details,
range: j * ((_b = higherLevelDefinition.value) !== null && _b !== void 0 ? _b : 1) + ((_c = spellRange === null || spellRange === void 0 ? void 0 : spellRange.rangeValue) !== null && _c !== void 0 ? _c : 0),
}));
}
}
else {
entries.push(CoreSimulators.simulateHigherLevelEntryContract({
level: higherLevelDefinition.level,
description: higherLevelDefinition.details,
range: higherLevelDefinition.value,
}));
}
return entries;
}
/**
*
* @param spell
* @param ruleData
*/
export function deriveAtHigherLevels(spell, ruleData) {
var _a;
const higherLevelData = getDefinitionAtHigherLevels(spell);
// TODO V5: do we need the check this julie/brian for context
const scaleType = higherLevelData ? getScaleType(spell) : null;
if (higherLevelData === null || scaleType === null) {
return null;
}
const additionalAttacks = [];
const additionalTargets = [];
const areaOfEffect = [];
const creatures = [];
const duration = [];
const special = [];
const range = [];
(_a = higherLevelData.higherLevelDefinitions) === null || _a === void 0 ? void 0 : _a.forEach((higherLevelDefinition) => {
switch (higherLevelDefinition.typeId) {
case AdditionalTypeEnum.ADDITIONAL_COUNT:
additionalAttacks.push(...deriveAtHigherLevelAdditionalCountEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.ADDITIONAL_TARGETS:
additionalTargets.push(...deriveAtHigherLevelAdditionalTargetsEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.EXTENDED_AREA:
areaOfEffect.push(...deriveAtHigherLevelExtendedAreaEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.EXTENDED_DURATION:
duration.push(...deriveAtHigherLevelCommonDescriptionEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.ADDITIONAL_CREATURES:
creatures.push(...deriveAtHigherLevelCommonDescriptionEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.EXTENDED_RANGE:
range.push(...deriveAtHigherLevelExtendedRangeEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.ADDITIONAL_POINTS:
case AdditionalTypeEnum.SPECIAL:
default:
special.push(...deriveAtHigherLevelCommonDescriptionEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
}
});
return {
additionalAttacks,
additionalTargets,
areaOfEffect,
creatures,
duration,
higherLevelDefinitions: higherLevelData.higherLevelDefinitions,
points: [],
special,
range,
};
}
/**
*
* @param modifier
* @param spell
* @param ruleData
*/
export function deriveModifier(modifier, spell, ruleData) {
var _a;
const higherLevelData = ModifierAccessors.getAtHigherLevels(modifier);
// TODO V5: do we need the check this julie/brian for context
const scaleType = higherLevelData ? getScaleType(spell) : null;
if (higherLevelData === null || scaleType === null) {
return modifier;
}
const additionalAttacks = [];
const additionalTargets = [];
const areaOfEffect = [];
const creatures = [];
const duration = [];
const points = [];
const special = [];
const range = [];
(_a = higherLevelData.higherLevelDefinitions) === null || _a === void 0 ? void 0 : _a.forEach((higherLevelDefinition) => {
var _a, _b, _c, _d;
switch (higherLevelDefinition.typeId) {
case AdditionalTypeEnum.ADDITIONAL_POINTS:
if (scaleType === SpellScaleTypeNameEnum.SPELL_SCALE) {
const spellLevel = getLevel(spell);
const maxSpellLevel = RuleDataAccessors.getMaxSpellLevel(ruleData);
const defDice = higherLevelDefinition.dice;
const defDiceValue = defDice === null ? null : DiceAccessors.getValue(defDice);
const defDiceCount = defDice === null ? null : DiceAccessors.getCount(defDice);
const modifierDice = ModifierAccessors.getDie(modifier);
const modifierDiceValue = modifierDice === null ? null : DiceAccessors.getValue(modifierDice);
const modifierDiceCount = modifierDice === null ? null : DiceAccessors.getCount(modifierDice);
const modifierDiceFixed = modifierDice === null ? null : DiceAccessors.getFixedValue(modifierDice);
let levelIncrement = (_a = higherLevelDefinition.level) !== null && _a !== void 0 ? _a : 1;
let start = spellLevel + levelIncrement;
let j = 1;
for (let i = start; i <= maxSpellLevel; i += levelIncrement, j++) {
// Dice count will be base spell modifier dice count plus the product of the iteration of the spell level bonus and the dice count of the bonus
let newDiceCount = (modifierDiceCount !== null && modifierDiceCount !== void 0 ? modifierDiceCount : 0) + j * (defDiceCount !== null && defDiceCount !== void 0 ? defDiceCount : 0);
// Add bonus fixed value to base fixed value
let newFixedValue = (modifierDiceFixed !== null && modifierDiceFixed !== void 0 ? modifierDiceFixed : 0) + j * ((_b = higherLevelDefinition.value) !== null && _b !== void 0 ? _b : 0);
points.push(CoreSimulators.simulateHigherLevelEntryContract({
level: i,
description: higherLevelDefinition.details,
die: DiceSimulators.simulateDiceContract({
diceCount: newDiceCount,
diceValue: defDiceValue !== null && defDiceValue !== void 0 ? defDiceValue : modifierDiceValue,
fixedValue: newFixedValue,
}),
}));
}
}
else {
const defDice = higherLevelDefinition.dice;
points.push(CoreSimulators.simulateHigherLevelEntryContract({
level: higherLevelDefinition.level,
description: higherLevelDefinition.details,
die: DiceSimulators.simulateDiceContract({
diceCount: defDice === null ? null : DiceAccessors.getCount(defDice),
diceValue: (_c = (defDice === null ? null : DiceAccessors.getValue(defDice))) !== null && _c !== void 0 ? _c : (_d = ModifierAccessors.getDie(modifier)) === null || _d === void 0 ? void 0 : _d.diceValue,
fixedValue: defDice === null ? null : DiceAccessors.getFixedValue(defDice),
}),
}));
}
break;
case AdditionalTypeEnum.ADDITIONAL_COUNT:
additionalAttacks.push(...deriveAtHigherLevelAdditionalCountEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.ADDITIONAL_TARGETS:
additionalTargets.push(...deriveAtHigherLevelAdditionalTargetsEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.EXTENDED_AREA:
areaOfEffect.push(...deriveAtHigherLevelExtendedAreaEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.EXTENDED_RANGE:
range.push(...deriveAtHigherLevelExtendedRangeEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.EXTENDED_DURATION:
duration.push(...deriveAtHigherLevelCommonDescriptionEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.ADDITIONAL_CREATURES:
creatures.push(...deriveAtHigherLevelCommonDescriptionEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
case AdditionalTypeEnum.SPECIAL:
default:
special.push(...deriveAtHigherLevelCommonDescriptionEntries(scaleType, higherLevelDefinition, spell, ruleData));
break;
}
});
return Object.assign(Object.assign({}, modifier), { atHigherLevels: {
additionalAttacks,
additionalTargets,
areaOfEffect,
creatures,
duration,
higherLevelDefinitions: higherLevelData.higherLevelDefinitions,
points,
special,
range,
} });
}
/**
*
* @param spell
* @param ruleData
*/
export function deriveModifiers(spell, ruleData) {
return getDefinitionModifiers(spell).map((modifier) => deriveModifier(modifier, spell, ruleData));
}