import { TypeScriptUtils } from '../../utils'; import { AbilityAccessors } from '../Ability'; import { CharacterDerivers } from '../Character'; import { ClassAccessors } from '../Class'; import { AbilityStatEnum, AttackTypeRangeEnum } from '../Core'; import { DataOriginTypeEnum } from '../DataOrigin'; import { DiceAccessors, DiceDerivers, DiceUtils } from '../Dice'; import { HelperUtils } from '../Helper'; import { ModifierAccessors, ModifierDerivers, ModifierValidators } from '../Modifier'; import { RuleDataAccessors, RuleDataUtils } from '../RuleData'; import { AdjustmentTypeEnum, ValueUtils, ValueValidators } from '../Value'; import { getAbilityModifierStatId, getActionTypeId, getAttackRangeId, getAttackSubtypeId, getDamageTypeId, getDataOrigin, getDataOriginType, getDefinitionName, getDefinitionRange, getDice, getDisplayAsAttack, getEntityTypeId, getFixedSaveDc, getFixedToHit, getId, getIsMartialArts, getIsProficienct, getMappingEntityTypeId, getMappingId, getSaveStatId, getSpellRangeType, requiresAttackRoll, } from './accessors'; import { ACTION_CUSTOMIZATION_ADJUSTMENT_TYPES, ActionTypeEnum, AttackSubtypeEnum } from './constants'; /** * * @param action */ export function deriveUniqueKey(action) { return `${getId(action)}-${getEntityTypeId(action)}`; } /** * * @param action */ export function deriveRequiresAttackRoll(action) { return getAttackRangeId(action) !== null; } /** * * @param action */ export function deriveRequiresSavingThrow(action) { return getSaveStatId(action) !== null; } /** * * @param action */ export function deriveIsDefaultDisplayAsAttack(action) { const displayAsAttack = getDisplayAsAttack(action); if (displayAsAttack !== null) { return displayAsAttack; } return deriveRequiresAttackRoll(action); } /** * * @param action */ export function deriveAttackSubtypeName(action) { switch (getAttackSubtypeId(action)) { case AttackSubtypeEnum.UNARMED: return 'Unarmed Strike'; case AttackSubtypeEnum.NATURAL: return 'Natural Attack'; } return null; } /** * * @param action * @param martialArtsLevelScale */ export function deriveIsMartialArtsAvailable(action, martialArtsLevelScale) { const isMartialArtsEnabled = getIsMartialArts(action); const attackSubtype = getAttackSubtypeId(action); if (martialArtsLevelScale !== null && (attackSubtype === AttackSubtypeEnum.UNARMED || isMartialArtsEnabled)) { return true; } return false; } /** * * @param action * @param ruleData */ export function deriveFallbackAbilityIds(action, ruleData) { const actionType = getActionTypeId(action); const attackTypeRange = getAttackRangeId(action); const availableAbilities = []; if (actionType === ActionTypeEnum.WEAPON) { if (attackTypeRange === AttackTypeRangeEnum.RANGED) { availableAbilities.push(AbilityStatEnum.DEXTERITY); } else { availableAbilities.push(AbilityStatEnum.STRENGTH); } } else { const dataOrigin = getDataOrigin(action); const dataOriginType = getDataOriginType(action); switch (dataOriginType) { case DataOriginTypeEnum.CLASS_FEATURE: const primaryAbilities = ClassAccessors.getPrimaryAbilities(dataOrigin.parent); if (primaryAbilities.length) { availableAbilities.push(...primaryAbilities); } break; } } return availableAbilities; } /** * * @param action * @param abilityLookup * @param ruleData */ export function deriveBestFallbackAbility(action, abilityLookup, ruleData) { const fallbackAbilityIds = deriveFallbackAbilityIds(action, ruleData); const fallbackAbilities = fallbackAbilityIds.map((id) => abilityLookup[id]); return HelperUtils.getLast(fallbackAbilities, 'modifier'); } /** * * @param action * @param martialArtsLevelScale * @param ruleData * @param includeFallbacks */ export function deriveAvailableAbilities(action, martialArtsLevelScale, ruleData, includeFallbacks = true) { let attackStat = null; if (requiresAttackRoll(action)) { attackStat = getAbilityModifierStatId(action); } const availableAbilities = []; if (attackStat) { availableAbilities.push(attackStat); } else if (includeFallbacks) { availableAbilities.push(...deriveFallbackAbilityIds(action, ruleData)); } if (deriveIsMartialArtsAvailable(action, martialArtsLevelScale) && !availableAbilities.includes(AbilityStatEnum.DEXTERITY)) { availableAbilities.push(AbilityStatEnum.DEXTERITY); } return availableAbilities; } /** * * @param action * @param abilityLookup * @param proficiencyBonus * @param valueLookup * @param ruleData */ export function deriveAttackSaveValue(action, abilityLookup, proficiencyBonus, valueLookup, ruleData) { const id = getMappingId(action); const entityId = getMappingEntityTypeId(action); if (getSaveStatId(action) === null) { return null; } const fixedSaveDc = getFixedSaveDc(action); if (fixedSaveDc) { return fixedSaveDc; } const saveDcOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.SAVE_DC_OVERRIDE, id, entityId); if (saveDcOverride !== null) { return saveDcOverride; } let attackSaveAbility = null; const abilityModifierStatId = getAbilityModifierStatId(action); if (abilityModifierStatId) { attackSaveAbility = abilityLookup[abilityModifierStatId]; } else { attackSaveAbility = deriveBestFallbackAbility(action, abilityLookup, ruleData); } if (!attackSaveAbility) { return null; } const abilityModifier = AbilityAccessors.getModifier(attackSaveAbility); const attackSaveAbilityModifier = abilityModifier ? abilityModifier : 0; let saveDc = CharacterDerivers.deriveAttackSaveValue(proficiencyBonus, attackSaveAbilityModifier); saveDc += ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.SAVE_DC_BONUS, id, entityId, 0); return saveDc; } /** * * @param bestAbilityInfo * @param action * @param modifiers * @param abilityLookup * @param valueLookup */ export function deriveToHit(bestAbilityInfo, action, modifiers, abilityLookup, valueLookup) { const actionType = getActionTypeId(action); let toHit = null; if (bestAbilityInfo && getAttackRangeId(action) !== null) { const id = getMappingId(action); const entityId = getMappingEntityTypeId(action); const toHitOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.TO_HIT_OVERRIDE, id, entityId); if (toHitOverride !== null) { return toHitOverride; } let bonusToHitModifiers = null; let bonusToHitModifierTotal = 0; if (actionType === ActionTypeEnum.SPELL) { bonusToHitModifiers = modifiers.filter((modifier) => ModifierValidators.isValidBonusActionSpellToHitModifier(modifier, action)); } else if (actionType === ActionTypeEnum.WEAPON) { bonusToHitModifiers = modifiers.filter((modifier) => ModifierValidators.isValidBonusActionWeaponToHitModifier(modifier, action)); } if (bonusToHitModifiers !== null) { bonusToHitModifierTotal = ModifierDerivers.sumModifiers(bonusToHitModifiers, abilityLookup); } const toHitCustomBonus = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.TO_HIT_BONUS, id, entityId, 0); toHit = bestAbilityInfo.toHit + bonusToHitModifierTotal + toHitCustomBonus; } const fixedToHit = getFixedToHit(action); if (fixedToHit !== null) { toHit = fixedToHit; } return toHit; } /** * * @param action * @param baseDamage * @param fixedDamageBonuses * @param valueLookup * @param ruleData */ export function deriveDamage(action, baseDamage, fixedDamageBonuses, valueLookup, ruleData) { const id = getMappingId(action); const entityId = getMappingEntityTypeId(action); const damageTypeId = getDamageTypeId(action); let type = null; let value = null; let dataOrigin = null; let isMartialArts = false; const damageCustomBonus = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.FIXED_VALUE_BONUS, id, entityId, 0); const fixedDamage = fixedDamageBonuses + damageCustomBonus; if (baseDamage !== null) { if (typeof baseDamage === 'number') { value = Math.max(0, baseDamage + fixedDamage); } else if (baseDamage.isMartialArts !== undefined) { if (baseDamage.dataOrigin) { dataOrigin = baseDamage.dataOrigin; } else if (baseDamage.isMartialArts) { isMartialArts = true; } const fixedValue = DiceAccessors.getFixedValue(baseDamage); value = Object.assign(Object.assign({}, baseDamage), { fixedValue: (fixedValue ? fixedValue : 0) + fixedDamage }); } } if (damageTypeId) { type = RuleDataUtils.getDamageType(damageTypeId, ruleData); } return { type, value, dataOrigin, isMartialArts, }; } /** * * @param action * @param valueLookup */ export function deriveName(action, valueLookup) { const id = getMappingId(action); const entityId = getMappingEntityTypeId(action); let derivedName = getDefinitionName(action); const nameOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.NAME_OVERRIDE, id, entityId); if (nameOverride) { derivedName = nameOverride; } if (derivedName === null) { derivedName = ''; } return derivedName; } /** * * @param action * @param modifiers */ export function deriveLabel(action, modifiers) { const masteryModifiers = modifiers.filter((modifier) => ModifierValidators.isValidWeaponMasteryModifier(modifier)); const masteryModifier = masteryModifiers.find((modifier) => modifier.componentId === action.componentId); if (masteryModifier) { return ModifierAccessors.getFriendlySubtypeName(masteryModifier); } return null; } /** * * @param action * @param proficiencyBonus */ export function deriveProficiencyBonus(action, proficiencyBonus) { let bonus = 0; if (getIsProficienct(action)) { bonus = proficiencyBonus; } return bonus; } /** * * @param action */ export function deriveProficiency(action) { return getIsProficienct(action); } /** * * @param action * @param valueLookup */ export function deriveDisplayAsAttack(action, valueLookup) { const id = getMappingId(action); const entityId = getMappingEntityTypeId(action); const displayAsAttackOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.DISPLAY_AS_ATTACK, id, entityId); if (displayAsAttackOverride !== null) { return displayAsAttackOverride; } return deriveIsDefaultDisplayAsAttack(action); } /** * * @param action * @param modifiers * @param modifierData * @param ruleData */ export function deriveReach(action, modifiers, modifierData, ruleData) { let baseReach = null; if (getActionTypeId(action) === ActionTypeEnum.SPELL) { return null; } // Reach is null if it isn't a melee attack if (getAttackRangeId(action) !== AttackTypeRangeEnum.MELEE) { return null; } // Figure out base reach based on initial data const range = getDefinitionRange(action); if (range) { baseReach = range.range; } if (baseReach === null) { baseReach = RuleDataAccessors.getBaseWeaponReach(ruleData); } const bonusReachModifiers = modifiers.filter((modifier) => ModifierValidators.isBonusMeleeReachModifier(modifier)); const bonusReachModifierTotal = ModifierDerivers.deriveTotalValue(bonusReachModifiers, modifierData); return baseReach + bonusReachModifierTotal; } /** * * @param action * @param martialArtsLevelScale * @param modifiers */ export function deriveHighestDamageDie(action, martialArtsLevelScale, modifiers) { const actionType = getActionTypeId(action); const baseDice = getDice(action); const attackSubtype = getAttackSubtypeId(action); const isMartialArtsAvailable = deriveIsMartialArtsAvailable(action, martialArtsLevelScale); const allDamageDice = []; if (baseDice) { allDamageDice.push(Object.assign(Object.assign({}, baseDice), { isMartialArts: false, dataOrigin: null })); } if (actionType === ActionTypeEnum.WEAPON && attackSubtype === AttackSubtypeEnum.UNARMED) { const setDamageDieModifiers = modifiers.filter((modifier) => ModifierValidators.isSetUnarmedDamageDieModifier(modifier)); const setDamageDice = setDamageDieModifiers .map((modifier) => { const modifierDice = ModifierAccessors.getDice(modifier); if (modifierDice === null) { return null; } return Object.assign(Object.assign({}, modifierDice), { isMartialArts: false, dataOrigin: ModifierAccessors.getDataOrigin(modifier) }); }) .filter(TypeScriptUtils.isNotNullOrUndefined); allDamageDice.push(...setDamageDice); } if (martialArtsLevelScale !== null && isMartialArtsAvailable) { const martialArtsDie = DiceDerivers.deriveMartialArtsDamageDie(martialArtsLevelScale); allDamageDice.push(Object.assign(Object.assign({}, martialArtsDie), { isMartialArts: true, dataOrigin: null })); } return DiceUtils.getHighestDie(allDamageDice); } /** * * @param bestAbilityInfo * @param action * @param modifiers * @param abilityLookup */ export function deriveFixedDamageBonuses(bestAbilityInfo, action, modifiers, abilityLookup) { const actionType = getActionTypeId(action); let damageModifiers = []; let damageModifierTotal = 0; if (actionType === ActionTypeEnum.SPELL) { damageModifiers = modifiers.filter((modifier) => ModifierValidators.isValidDamageActionSpellModifier(modifier, action)); } else if (actionType === ActionTypeEnum.WEAPON) { damageModifiers = modifiers.filter((modifier) => ModifierValidators.isValidDamageActionWeaponModifier(modifier, action)); } if (damageModifiers.length) { damageModifierTotal = ModifierDerivers.sumModifiers(damageModifiers, abilityLookup); } return damageModifierTotal + (bestAbilityInfo ? bestAbilityInfo.damageBonus : 0); } /** * * @param action * @param modifiers * @param abilityLookup */ export function deriveRange(action, modifiers, abilityLookup) { const actionType = getActionTypeId(action); const definitionRange = getDefinitionRange(action); if (definitionRange === null) { return definitionRange; } let range = Object.assign(Object.assign({}, definitionRange), { origin: null }); if (actionType === ActionTypeEnum.SPELL) { const attackTypeRange = getAttackRangeId(action); let newRange = definitionRange.range; if (attackTypeRange !== null && newRange) { const spellAttackRangeMultiplierModifiers = modifiers.filter((modifier) => ModifierValidators.isBonusSpellAttackRangeMultiplierModifier(modifier)); const spellAttackRangeMultiplierTotal = Math.max(1, ModifierDerivers.sumModifiers(spellAttackRangeMultiplierModifiers, abilityLookup, 1)); newRange *= spellAttackRangeMultiplierTotal; } range = Object.assign(Object.assign({}, definitionRange), { range: newRange, origin: getSpellRangeType(action) }); } return range; } /** * * @param action * @param valueLookup */ export function deriveIsOffhand(action, valueLookup) { let isOffhand = false; if (getActionTypeId(action) === ActionTypeEnum.WEAPON) { const id = getMappingId(action); const entityId = getMappingEntityTypeId(action); isOffhand = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.IS_OFFHAND, id, entityId, false); } return isOffhand; } /** * * @param action * @param valueLookup */ export function deriveIsSilvered(action, valueLookup) { let isSilvered = false; if (getActionTypeId(action) === ActionTypeEnum.WEAPON) { const id = getMappingId(action); const entityId = getMappingEntityTypeId(action); isSilvered = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.IS_SILVER, id, entityId, false); } return isSilvered; } /** * * @param action * @param valueLookup */ export function deriveIsCustomized(action, valueLookup) { return ValueValidators.validateHasCustomization(ACTION_CUSTOMIZATION_ADJUSTMENT_TYPES, valueLookup, getMappingId(action), getMappingEntityTypeId(action)); }