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()}
);
};