import React from "react"; import { Tooltip } from "@dndbeyond/character-common-components/es"; import { Collapsible, AoeTypeIcon, ComponentConstants, } from "@dndbeyond/character-components/es"; import { ActivationUtils, AnySimpleDataType, CharacterTheme, CharacterUtils, CharacterValuesContract, Constants, DurationUtils, EntityValueLookup, FormatUtils, RuleData, RuleDataUtils, SourceMappingContract, Spell, SpellCasterInfo, SpellUtils, ValueUtils, } from "@dndbeyond/character-rules-engine/es"; import { HtmlContent } from "~/components/HtmlContent"; import { InfoItem } from "~/components/InfoItem"; import { NumberDisplay } from "~/components/NumberDisplay"; import { Reference } from "~/components/Reference"; import { TagGroup } from "~/components/TagGroup"; import SpellDetailCaster from "../../containers/SpellDetailCaster"; import EditorBox from "../EditorBox"; import ValueEditor from "../ValueEditor"; import { RemoveButton, ThemeButton } from "../common/Button"; interface Props { spell: Spell; enableCaster: boolean; castAsRitual: boolean; initialCastLevel: number | null; isPreparedMaxed: boolean; onRemove?: () => void; onPrepareToggle?: () => void; onCustomDataUpdate?: ( key: number, value: AnySimpleDataType, source: string | null ) => void; onCustomizationsRemove?: () => void; showCustomize: boolean; showActions: boolean; entityValueLookup?: EntityValueLookup; spellCasterInfo: SpellCasterInfo; ruleData: RuleData; isReadonly: boolean; proficiencyBonus: number; theme: CharacterTheme; isCustomizeClosed?: boolean; onCustomizeClick?: (isCollapsed: boolean) => void; } export default class SpellDetail extends React.PureComponent { static defaultProps = { initialCastLevel: null, isPreparedMaxed: true, enableCaster: true, showActions: true, showCustomize: true, castAsRitual: false, spellCasterInfo: {}, isReadonly: false, }; handleRemoveCustomizations = () => { const { onCustomizationsRemove } = this.props; if (onCustomizationsRemove) { onCustomizationsRemove(); } }; renderCustomize = (): React.ReactNode => { const { spell, showCustomize, onCustomDataUpdate, entityValueLookup, ruleData, isReadonly, isCustomizeClosed, onCustomizeClick, } = this.props; const dataOrigin = SpellUtils.getDataOrigin(spell); const dataOriginType = SpellUtils.getDataOriginType(spell); if ( !entityValueLookup || !showCustomize || !dataOrigin || (dataOrigin && dataOriginType === Constants.DataOriginTypeEnum.ITEM) || isReadonly ) { return null; } const displayAsAttack = SpellUtils.isDisplayAsAttack(spell); const initialDisplayAsAttack: boolean = displayAsAttack || (SpellUtils.isAttack(spell) && (displayAsAttack || displayAsAttack === null)); let customizationValues: Array = [ Constants.AdjustmentTypeEnum.TO_HIT_OVERRIDE, Constants.AdjustmentTypeEnum.TO_HIT_BONUS, Constants.AdjustmentTypeEnum.FIXED_VALUE_BONUS, Constants.AdjustmentTypeEnum.SAVE_DC_OVERRIDE, Constants.AdjustmentTypeEnum.SAVE_DC_BONUS, ]; if (!SpellUtils.asPartOfWeaponAttack(spell)) { customizationValues.push(Constants.AdjustmentTypeEnum.DISPLAY_AS_ATTACK); } customizationValues.push(Constants.AdjustmentTypeEnum.NAME_OVERRIDE); customizationValues.push(Constants.AdjustmentTypeEnum.NOTES); let customizationLabelOverrides: Partial< Record > = { [Constants.AdjustmentTypeEnum.NAME_OVERRIDE]: "Name", [Constants.AdjustmentTypeEnum.FIXED_VALUE_BONUS]: "Damage Bonus", [Constants.AdjustmentTypeEnum.SAVE_DC_OVERRIDE]: "DC Override", [Constants.AdjustmentTypeEnum.SAVE_DC_BONUS]: "DC Bonus", }; let customizationDefaults: Partial< Record > = { [Constants.AdjustmentTypeEnum.DISPLAY_AS_ATTACK]: initialDisplayAsAttack, }; let [contextId, contextTypeId] = SpellUtils.deriveExpandedContextIds(spell); const mappingId = SpellUtils.getMappingId(spell); const mappingEntityTypeId = SpellUtils.getMappingEntityTypeId(spell); let dataLookup: Record = {}; if (mappingId !== null && mappingEntityTypeId !== null) { dataLookup = ValueUtils.getEntityData( entityValueLookup, ValueUtils.hack__toString(mappingId), ValueUtils.hack__toString(mappingEntityTypeId), ValueUtils.hack__toString(contextId), ValueUtils.hack__toString(contextTypeId) ); } const isCustomized = SpellUtils.isCustomized(spell); return (
{isCustomized ? "Remove" : "No"} Customizations
); //` }; renderActions = (): React.ReactNode => { const { spell, isPreparedMaxed, onRemove, onPrepareToggle, showActions } = this.props; const dataOrigin = SpellUtils.getDataOrigin(spell); const dataOriginType = SpellUtils.getDataOriginType(spell); if ( !showActions || !dataOrigin || (dataOrigin && dataOriginType !== Constants.DataOriginTypeEnum.CLASS) ) { return null; } const isPrepared = SpellUtils.isPrepared(spell); const showRemove = SpellUtils.canRemove(spell); const showPrepare = SpellUtils.canPrepare(spell) && !SpellUtils.isAlwaysPrepared(spell); if (!showRemove && !showPrepare) { return null; } return (
{showPrepare && (
{isPrepared ? "Prepared" : "Prepare"}
)} {showRemove && (
)}
); }; renderCaster = (): React.ReactNode => { const { spell, initialCastLevel, enableCaster, castAsRitual, spellCasterInfo, proficiencyBonus, } = this.props; if (!enableCaster || castAsRitual || !SpellUtils.isActive(spell)) { return null; } return ( ); }; render() { const { spell, ruleData, theme } = this.props; let { castAsRitual } = this.props; castAsRitual = castAsRitual || SpellUtils.isCastAsRitual(spell); const attackSaveValue = SpellUtils.getAttackSaveValue(spell); const notes = SpellUtils.getNotes(spell); const range = SpellUtils.getRange(spell); const additionalDescription = SpellUtils.getAdditionalDescription(spell); const level = SpellUtils.getLevel(spell); const components = SpellUtils.getComponents(spell); const componentsDescription = SpellUtils.getComponentsDescription(spell); const duration = SpellUtils.getDuration(spell); const school = SpellUtils.getSchool(spell); const description = SpellUtils.getDescription(spell); const version = SpellUtils.getVersion(spell); const requiresSavingThrow = SpellUtils.getRequiresSavingThrow(spell); const tags = SpellUtils.getTags(spell); const activation = SpellUtils.getActivation(spell); const castingTimeDescription = SpellUtils.getCastingTimeDescription(spell); let sourceId: number | null = null; let sourcePage: number | null = null; let filteredSources = SpellUtils.getSources(spell).filter( CharacterUtils.isPrimarySource ); if (filteredSources.length) { let primarySource: SourceMappingContract = filteredSources[0]; sourceId = primarySource.sourceId; sourcePage = primarySource.pageNumber; } let rangeAreas: Array = []; if (range) { if ( range.origin && range.origin !== Constants.SpellRangeTypeNameEnum.RANGED ) { rangeAreas.push(range.origin); } if (range.rangeValue) { rangeAreas.push( ); } if (range.aoeValue) { rangeAreas.push( ); } } let componentsNode: React.ReactNode; if (components) { componentsNode = ( {components.map((componentId, idx) => { let componentInfo = RuleDataUtils.getSpellComponentInfo( componentId, ruleData ); if (componentInfo === null) { return null; } return ( {componentInfo.shortName} {idx + 1 < components.length ? ", " : ""} ); })} ); } let castingTimeDisplay: React.ReactNode; if (activation) { castingTimeDisplay = ActivationUtils.renderCastingTime( activation, castAsRitual ? 10 : 0, ruleData ); } const infoItemProps = { role: "listitem", inline: true }; const getSourceInfo = (sourceId) => sourceId ? RuleDataUtils.getSourceDataInfo(sourceId, ruleData) : null; return (
{SpellUtils.isLegacy(spell) && ( Legacy •{" "} )} {SpellUtils.isCantrip(spell) ? school : FormatUtils.renderSpellLevelName(level)} {SpellUtils.isCantrip(spell) ? FormatUtils.renderSpellLevelName(level) : school}
{this.renderCaster()} {this.renderCustomize()}
{castingTimeDisplay} {castingTimeDescription ? `, ${castingTimeDescription}` : ""} {rangeAreas.map((node, idx) => ( {node} {idx + 1 < rangeAreas.length ? "/" : ""} ))} {componentsNode} {componentsDescription && ( ({componentsDescription}) )} {duration !== null && ( {DurationUtils.renderDuration( duration, SpellUtils.getConcentration(spell) )} )} {requiresSavingThrow && ( {SpellUtils.getSaveDcAbilityShortName(spell, ruleData)}{" "} {attackSaveValue} )} {version !== null && ( {version} )} {notes && ( {notes} )} {sourceId !== null && ( )}
{description !== null && ( )} {additionalDescription && ( )} {this.renderActions()} {tags.length > 0 && ( )}
); } }