import React from "react"; import { Collapsible, DamageTypeIcon, MarketplaceCta, AoeTypeIcon, ComponentConstants, } from "@dndbeyond/character-components/es"; import { AbilityLookup, AccessUtils, Action, ActionUtils, ActivationUtils, BaseInventoryContract, CharacterTheme, Constants, DiceUtils, EntityUtils, EntityValueLookup, FormatUtils, HelperUtils, InfusionUtils, InventoryLookup, ItemUtils, LimitedUseUtils, ModelInfoContract, RuleData, RuleDataUtils, ValueUtils, } from "@dndbeyond/character-rules-engine/es"; import { HtmlContent } from "~/components/HtmlContent"; import { InfoItem } from "~/components/InfoItem"; import { NumberDisplay } from "~/components/NumberDisplay"; import EditorBox from "../EditorBox"; import SlotManager from "../SlotManager"; import SlotManagerLarge from "../SlotManagerLarge"; import ValueEditor from "../ValueEditor"; import { RemoveButton } from "../common/Button"; import styles from "./styles.module.css"; interface Props { action: Action; ruleData: RuleData; onCustomDataUpdate?: ( propertyKey: Constants.AdjustmentTypeEnum, value: any, source: any ) => void; showCustomize?: boolean; entityValueLookup: EntityValueLookup; abilityLookup: AbilityLookup; inventoryLookup: InventoryLookup; isReadonly: boolean; largePoolMinAmount?: number; onLimitedUseSet?: (usedAmount: number) => void; onCustomizationsRemove?: () => void; proficiencyBonus: number; theme?: CharacterTheme; } export const ActionDetail = ({ showCustomize = true, largePoolMinAmount = 11, onCustomizationsRemove, ruleData, abilityLookup, action, onLimitedUseSet, proficiencyBonus, isReadonly, entityValueLookup, onCustomDataUpdate, theme, inventoryLookup, }: Props) => { const infoItemProps = { role: "listItem", inline: true }; const handleRemoveCustomizations = () => { if (onCustomizationsRemove) { onCustomizationsRemove(); } }; const renderSmallAmountSlotPool = (): React.ReactNode => { let limitedUse = ActionUtils.getLimitedUse(action); if (!limitedUse) { return null; } const numberUsed = LimitedUseUtils.getNumberUsed(limitedUse); const maxUses = LimitedUseUtils.deriveMaxUses( limitedUse, abilityLookup, ruleData, proficiencyBonus ); return ( ); }; const renderLargeAmountSlotPool = (): React.ReactNode => { let limitedUse = ActionUtils.getLimitedUse(action); if (!limitedUse) { return null; } const numberUsed = LimitedUseUtils.getNumberUsed(limitedUse); const maxUses = LimitedUseUtils.deriveMaxUses( limitedUse, abilityLookup, ruleData, proficiencyBonus ); return ( ); }; const renderLimitedUses = (): React.ReactNode => { let limitedUse = ActionUtils.getLimitedUse(action); if (!limitedUse) { return null; } let maxUses = LimitedUseUtils.deriveMaxUses( limitedUse, abilityLookup, ruleData, proficiencyBonus ); if (!maxUses) { return null; } let numberUsed = LimitedUseUtils.getNumberUsed(limitedUse); let totalSlots: number = Math.max(maxUses, numberUsed); return (
Limited Use
{totalSlots >= largePoolMinAmount ? renderLargeAmountSlotPool() : renderSmallAmountSlotPool()}
); }; const renderCustomize = (): React.ReactNode => { const mappingId = ActionUtils.getMappingId(action); const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action); if ( !showCustomize || isReadonly || mappingId === null || mappingEntityTypeId === null ) { return null; } let valueEditorComponents: Array = [ Constants.AdjustmentTypeEnum.TO_HIT_OVERRIDE, Constants.AdjustmentTypeEnum.TO_HIT_BONUS, Constants.AdjustmentTypeEnum.FIXED_VALUE_BONUS, Constants.AdjustmentTypeEnum.DISPLAY_AS_ATTACK, Constants.AdjustmentTypeEnum.NAME_OVERRIDE, Constants.AdjustmentTypeEnum.NOTES, ]; const isCustomized = ActionUtils.isCustomized(action); return ( {isCustomized ? "Remove" : "No"} Customizations ); }; const getRangedInfoData = (): Array => { let rangeInfo = ActionUtils.getRange(action); let attackRangeId = ActionUtils.getAttackRangeId(action); let rangeAreas: Array = []; if ( rangeInfo !== null && attackRangeId === Constants.AttackTypeRangeEnum.RANGED ) { if ( rangeInfo.origin && rangeInfo.origin !== Constants.SpellRangeTypeEnum.RANGED ) { let spellRangeType = RuleDataUtils.getSpellRangeType( rangeInfo.origin, ruleData ); if (spellRangeType !== null) { rangeAreas.push(spellRangeType.name); } } if (rangeInfo.range) { rangeAreas.push( {rangeInfo.longRange && ({rangeInfo.longRange})} ); } if (rangeInfo.aoeSize) { let aoeType: ModelInfoContract | null = null; if (rangeInfo.aoeType !== null) { aoeType = RuleDataUtils.getAoeType(rangeInfo.aoeType, ruleData); } rangeAreas.push( {aoeType !== null && ( )} ); } } return rangeAreas; //` }; const renderDamageProperties = (): React.ReactNode => { let damage = ActionUtils.getDamage(action); let damageDisplay: React.ReactNode = null; if (damage.value !== null) { if (typeof damage.value === "number") { damageDisplay = damage.value; } else { damageDisplay = DiceUtils.renderDice(damage.value); } } return ( {damageDisplay !== null && ( {damageDisplay} )} {damage.type !== null && damage.type.name !== null && ( {damage.type.name} )} ); }; const renderWeaponProperties = (): React.ReactNode => { let isProficient = ActionUtils.isProficient(action); let toHit = ActionUtils.getToHit(action); let attackRangeId = ActionUtils.getAttackRangeId(action); let statId = ActionUtils.getStatId(action); let requiresAttackRoll = ActionUtils.requiresAttackRoll(action); let requiresSavingThrow = ActionUtils.requiresSavingThrow(action); let activation = ActionUtils.getActivation(action); let notes = ActionUtils.getNotes(action); let attackSubtypeId = ActionUtils.getAttackSubtypeId(action); let saveStateId = ActionUtils.getSaveStatId(action); let rangeAreas: Array = []; if (attackRangeId === Constants.AttackTypeRangeEnum.RANGED) { rangeAreas = getRangedInfoData(); } else { rangeAreas.push( {" "} Reach ); } return (
{activation !== null && activation.activationType && ( {ActivationUtils.renderActivation(activation, ruleData)} )} {attackSubtypeId && ( {ActionUtils.getAttackSubtypeName(action)} )} {requiresAttackRoll && ( )} {requiresSavingThrow && ( {saveStateId !== null ? RuleDataUtils.getStatNameById(saveStateId, ruleData) : ""}{" "} {ActionUtils.getAttackSaveValue(action)} )} {renderDamageProperties()} {requiresAttackRoll && ( {statId === null ? "--" : RuleDataUtils.getStatNameById(statId, ruleData)} )} {rangeAreas.length > 0 && ( {rangeAreas.map((node, idx) => ( {node} {idx + 1 < rangeAreas.length ? "/" : ""} ))} )} {isProficient && ( Yes )} {notes && ( {notes} )}
); }; const renderSpellProperties = (): React.ReactNode => { let isProficient = ActionUtils.isProficient(action); let toHit = ActionUtils.getToHit(action); let statId = ActionUtils.getStatId(action); let requiresAttackRoll = ActionUtils.requiresAttackRoll(action); let requiresSavingThrow = ActionUtils.requiresSavingThrow(action); let activation = ActionUtils.getActivation(action); let notes = ActionUtils.getNotes(action); let attackSubtypeId = ActionUtils.getAttackSubtypeId(action); let saveStateId = ActionUtils.getSaveStatId(action); let rangeAreas = getRangedInfoData(); return (
{activation !== null && activation.activationType && ( {ActivationUtils.renderActivation(activation, ruleData)} )} {attackSubtypeId && ( {ActionUtils.getAttackSubtypeName(action)} )} {requiresAttackRoll && ( )} {requiresSavingThrow && ( {saveStateId !== null ? RuleDataUtils.getStatNameById(saveStateId, ruleData) : ""}{" "} {ActionUtils.getAttackSaveValue(action)} )} {renderDamageProperties()} {requiresAttackRoll && ( {statId === null ? "--" : RuleDataUtils.getStatNameById(statId, ruleData)} )} {rangeAreas.length > 0 && ( {rangeAreas.map((node, idx) => ( {node} {idx + 1 < rangeAreas.length ? "/" : ""} ))} )} {isProficient && ( Yes )} {notes && ( {notes} )}
); }; const renderGeneralProperties = (): React.ReactNode => { let isProficient = ActionUtils.isProficient(action); let toHit = ActionUtils.getToHit(action); let statId = ActionUtils.getStatId(action); let requiresAttackRoll = ActionUtils.requiresAttackRoll(action); let requiresSavingThrow = ActionUtils.requiresSavingThrow(action); let activation = ActionUtils.getActivation(action); let notes = ActionUtils.getNotes(action); let attackRangeId = ActionUtils.getAttackRangeId(action); let saveStateId = ActionUtils.getSaveStatId(action); let rangeAreas: Array = []; if (attackRangeId === Constants.AttackTypeRangeEnum.RANGED) { rangeAreas = getRangedInfoData(); } else { rangeAreas.push( {" "} Reach ); } return (
{activation !== null && activation.activationType && ( {ActivationUtils.renderActivation(activation, ruleData)} )} {requiresAttackRoll && ( )} {requiresSavingThrow && ( {saveStateId !== null ? RuleDataUtils.getStatNameById(saveStateId, ruleData) : ""}{" "} {ActionUtils.getAttackSaveValue(action)} )} {renderDamageProperties()} {requiresAttackRoll && ( {statId === null ? "--" : RuleDataUtils.getStatNameById(statId, ruleData)} )} {rangeAreas.length > 0 && ( {rangeAreas.map((node, idx) => ( {node} {idx + 1 < rangeAreas.length ? "/" : ""} ))} )} {isProficient && ( Yes )} {notes && ( {notes} )}
); }; const renderProperties = (): React.ReactNode => { const actionTypeId = ActionUtils.getActionTypeId(action); switch (actionTypeId) { case Constants.ActionTypeEnum.SPELL: return renderSpellProperties(); case Constants.ActionTypeEnum.WEAPON: return renderWeaponProperties(); case Constants.ActionTypeEnum.GENERAL: return renderGeneralProperties(); default: // not implemented } return null; }; const renderDescription = (): React.ReactNode => { let description = ActionUtils.getDescription(action); let dataOrigin = ActionUtils.getDataOrigin(action); let dataOriginType = ActionUtils.getDataOriginType(action); if (!description) { description = EntityUtils.getPrimaryDescription(dataOrigin); } if (dataOriginType === Constants.DataOriginTypeEnum.ITEM) { const itemContract = dataOrigin.primary as BaseInventoryContract; const itemMappingId = ItemUtils.getMappingId(itemContract); const item = HelperUtils.lookupDataOrFallback( inventoryLookup, itemMappingId ); if (item === null) { return null; } let infusion = ItemUtils.getInfusion(item); if ( infusion && !AccessUtils.isAccessible(InfusionUtils.getAccessType(infusion)) ) { const sources = InfusionUtils.getSources(infusion); let sourceNames: Array = []; if (sources.length > 0) { sources.forEach((sourceMapping) => { let source = RuleDataUtils.getSourceDataInfo( sourceMapping.sourceId, ruleData ); if (source !== null && source.description !== null) { sourceNames.push(source.description); } }); } const sourceName: string | null = sourceNames.length > 0 ? FormatUtils.renderNonOxfordCommaList(sourceNames) : null; return (
); } } if (!description) { return null; } return ( ); }; return (
{renderLimitedUses()} {renderCustomize()} {renderProperties()} {renderDescription()}
); };