New source found from dndbeyond.com

This commit is contained in:
2025-07-16 01:00:16 -07:00
parent 151590af7a
commit f9f8dd0e7a
25 changed files with 886 additions and 1299 deletions
@@ -0,0 +1,119 @@
import { FC, HTMLAttributes } from "react";
import { useDispatch } from "react-redux";
import {
characterActions,
ChoiceUtils,
} from "@dndbeyond/character-rules-engine";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
import { CharClass, Choice, ClassDefinitionContract, Feat } from "~/types";
export interface FeatureChoiceProps extends HTMLAttributes<HTMLDivElement> {
choice: Choice;
charClass?: CharClass;
featsData: Feat[];
subclassData?: ClassDefinitionContract[];
onChoiceChange: (
choiceId: string,
type: number,
value: any,
parentChoiceId: string | null
) => void;
collapseDescription?: boolean;
}
/**
* Component for rendering a features choice(s) - for species traits and class features - using DetailChoice for the choices and mapping over any feat sub choices if the choice from a species trait or class feature is a feat.
* It is used in both the builder and character sheet sidebar panes.
*/
export const FeatureChoice: FC<FeatureChoiceProps> = ({
charClass,
choice,
featsData,
subclassData,
onChoiceChange,
className,
collapseDescription,
...props
}) => {
const dispatch = useDispatch();
const {
classUtils,
featLookup,
preferences,
prerequisiteData,
choiceInfo,
ruleData,
entityRestrictionData,
} = useCharacterEngine();
const handleChoiceChange = (
id: string,
type: number,
value: any,
parentChoiceId: string | null
): void => {
onChoiceChange(id, type, value, parentChoiceId);
};
const handleFeatChoiceChange = (
featId: number,
id: string | null,
type: number,
value: any
): void => {
if (id !== null) {
dispatch(characterActions.featChoiceSetRequest(featId, type, id, value));
}
};
const { description, options, featSubChoices } =
ChoiceUtils.getSortedChoiceOptionsForFeaturesInfo(
choice,
featsData,
featLookup,
charClass || null,
subclassData || [],
preferences,
prerequisiteData,
ruleData,
entityRestrictionData
);
return (
<div className={className} {...props}>
<DetailChoice
{...choice}
choice={choice}
options={options}
onChange={handleChoiceChange}
description={description || ""}
choiceInfo={choiceInfo}
classId={charClass && classUtils.getId(charClass)}
showBackgroundProficiencyOptions={true}
collapseDescription={collapseDescription}
/>
{featSubChoices &&
featSubChoices.map((subChoice) => {
const id = ChoiceUtils.getId(subChoice.choice);
return (
<DetailChoice
{...subChoice.choice}
label={subChoice.choice.label}
choice={subChoice.choice}
key={id !== null ? id : ""}
options={subChoice.options}
onChange={(id, type, value) =>
handleFeatChoiceChange(subChoice.featId, id, type, value)
}
choiceInfo={choiceInfo}
showBackgroundProficiencyOptions={true}
description={subChoice.description}
/>
);
})}
</div>
);
};
@@ -0,0 +1,179 @@
import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
ApiAdapterUtils,
apiCreatorSelectors,
CharClass,
Choice,
ChoiceUtils,
ClassDefinitionContract,
ClassUtils,
Constants,
Feat,
FeatUtils,
} from "@dndbeyond/character-rules-engine";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { AppLoggerUtils } from "~/tools/js/Shared/utils";
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
import { DataLoadingStatusEnum } from "~/tools/js/smartComponents/componentConstants";
import { FeatureChoice } from "./FeatureChoice";
import styles from "./styles.module.css";
export interface FeatureChoicesProps extends HTMLAttributes<HTMLDivElement> {
choices: Array<Choice>;
showHeading?: boolean;
onChoiceChange: (
choiceId: string,
type: number,
value: any,
parentChoiceId: string | null
) => void;
collapseDescription?: boolean;
charClass?: CharClass;
shouldFetch?: boolean; // Optional prop to control fetching behavior
}
export const FeatureChoices: FC<FeatureChoicesProps> = ({
className,
choices,
showHeading,
onChoiceChange,
collapseDescription,
charClass,
shouldFetch = true, // Default to true if not provided
...props
}) => {
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const loadAvailableFeats = useSelector(
apiCreatorSelectors.makeLoadAvailableFeats
);
const loadAvailableSubclasses = useSelector(
apiCreatorSelectors.makeLoadAvailableSubclasses
);
const [subclassData, setSubclassData] = useState<
Array<ClassDefinitionContract>
>([]);
const [featsData, setFeatsData] = useState<Array<Feat>>([]);
const [featLoadingStatus, setFeatLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const [subclassLoadingStatus, setSubclassLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const hasFeatChoice = choices.some(
(choice) =>
ChoiceUtils.getType(choice) ===
Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION
);
const hasSubclassChoice = choices.some(
(choice) =>
ChoiceUtils.getType(choice) ===
Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION
);
useEffect(() => {
if (
hasFeatChoice &&
shouldFetch &&
featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
) {
setFeatLoadingStatus(DataLoadingStatusEnum.LOADING);
loadAvailableFeats()
.then((response) => {
let featsData: Array<Feat> = [];
const data = ApiAdapterUtils.getResponseData(response);
if (data !== null) {
featsData = data.map((definition) =>
FeatUtils.simulateFeat(definition)
);
}
setFeatsData(featsData);
setFeatLoadingStatus(DataLoadingStatusEnum.LOADED);
})
.catch(AppLoggerUtils.handleAdhocApiError);
}
if (
charClass &&
hasSubclassChoice &&
shouldFetch &&
subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
) {
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING);
loadAvailableSubclasses(ClassUtils.getId(charClass))
.then((response) => {
let subclassData: Array<ClassDefinitionContract> = [];
const data = ApiAdapterUtils.getResponseData(response);
if (data !== null) {
subclassData = data;
}
setSubclassData(subclassData);
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED);
})
.catch(AppLoggerUtils.handleAdhocApiError);
}
}, [
hasFeatChoice,
hasSubclassChoice,
subclassLoadingStatus,
featLoadingStatus,
charClass,
loadAvailableFeats,
loadAvailableSubclasses,
shouldFetch,
]);
const isDataLoaded = (): boolean => {
if (hasSubclassChoice) {
if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) {
return false;
}
}
if (hasFeatChoice) {
if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) {
return false;
}
}
return true;
};
return !isReadonly && choices.length > 0 ? (
<div className={clsx([styles.featureChoices, className])} {...props}>
<>
{showHeading && (
<Heading className={styles.heading}>{`Option${
choices.length > 1 ? "s" : ""
}`}</Heading>
)}
{isDataLoaded() ? (
choices.map((choice) => (
<FeatureChoice
charClass={charClass}
choice={choice}
featsData={featsData}
subclassData={subclassData}
className={styles.choice}
key={ChoiceUtils.getId(choice)}
onChoiceChange={onChoiceChange}
collapseDescription={collapseDescription}
/>
))
) : (
<LoadingPlaceholder />
)}
</>
</div>
) : null;
};