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
@@ -1,306 +0,0 @@
import { FC, HTMLAttributes } from "react";
import { BuilderChoiceTypeEnum } from "~/constants";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useRuleData } from "~/hooks/useRuleData";
import { useSource } from "~/hooks/useSource";
import {
DetailChoice,
DetailChoiceFeat,
} from "~/tools/js/Shared/containers/DetailChoice";
import { TypeScriptUtils } from "~/tools/js/Shared/utils";
import {
CharClass,
Choice,
ClassDefinitionContract,
ClassFeature,
Feat,
FeatLookup,
FeatureChoiceOption,
HtmlSelectOptionGroup,
SourceData,
} from "~/types";
export interface FeatureChoiceProps extends HTMLAttributes<HTMLDivElement> {
choice: Choice;
charClass?: CharClass;
feature?: ClassFeature;
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 DetailChoiceFeat for a selected feat's choices and DetailChoice for all others.
It is used in both the builder and character sheet sidebar panes.
*/
export const FeatureChoice: FC<FeatureChoiceProps> = ({
charClass,
choice,
feature,
featsData,
subclassData,
onChoiceChange,
className,
collapseDescription,
...props
}) => {
const {
choiceUtils,
featUtils,
helperUtils,
prerequisiteUtils,
classUtils,
featLookup,
preferences,
prerequisiteData,
choiceInfo,
ruleData,
entityRestrictionData,
} = useCharacterEngine();
const { ruleDataUtils } = useRuleData();
const {
getGroupedOptionsBySourceCategory,
getSimpleSourcedDefinitionContracts,
} = useSource();
const optionValue = choiceUtils.getOptionValue(choice);
const options = choiceUtils.getOptions(choice);
const type = choiceUtils.getType(choice);
const tagConstraints = choiceUtils.getTagConstraints(choice);
let availableOptions: Array<FeatureChoiceOption> = [];
let availableGroupedOptions: HtmlSelectOptionGroup[] = [];
let detailChoiceDesc: string | null = null;
let subchoicesNode: React.ReactNode;
const handleChoiceChange = (
id: string,
type: number,
subType: number | null,
value: any,
parentChoiceId: string | null
): void => {
onChoiceChange(id, type, value, parentChoiceId);
};
const getSubclassData = (): ClassDefinitionContract[] => {
if (!subclassData || !charClass) {
return [];
}
let data: ClassDefinitionContract[] = [...subclassData];
let existingSubclass = classUtils.getSubclass(charClass);
if (
existingSubclass !== null &&
!data.some(
(classDefinition) =>
existingSubclass !== null &&
classDefinition.id === existingSubclass.id
)
) {
data.push(existingSubclass);
}
return data;
};
const getAvailableFeatChoices = (
existingFeatId: number | null,
featData: Feat[],
featLookup: FeatLookup
): Feat[] => {
let data: Feat[] = [...featData];
if (existingFeatId !== null) {
let existingFeat = helperUtils.lookupDataOrFallback(
featLookup,
existingFeatId
);
if (
existingFeat !== null &&
!data.some((feat) => featUtils.getId(feat) === existingFeatId)
) {
data.push(existingFeat);
}
}
return data;
};
const getSubclassSources = (
subclass: ClassDefinitionContract
): SourceData[] => {
if (subclass.sources === null) {
return [];
}
return subclass.sources
.map((sourceMapping) =>
helperUtils.lookupDataOrFallback(
ruleDataUtils.getSourceDataLookup(ruleData),
sourceMapping.sourceId
)
)
.filter(TypeScriptUtils.isNotNullOrUndefined);
};
switch (type) {
case BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION:
const availableFeats = getAvailableFeatChoices(
optionValue,
featsData,
featLookup
);
const repeatableFeatTracker = new Set();
// Add selected feat to repeatable tracker if repeatable
const selectedFeat = optionValue ? featLookup[optionValue] : null;
if (selectedFeat && featUtils.isRepeatable(selectedFeat)) {
const parentId = featUtils.getRepeatableGroupId(selectedFeat);
if (parentId) {
repeatableFeatTracker.add(parentId);
}
}
const filteredFeats = availableFeats.filter((feat) => {
const featId = featUtils.getId(feat);
const isRepeatable = featUtils.isRepeatable(feat);
// If the feat is the currently selected, always include it
if (featId === optionValue) {
return true;
}
// Always exclude all previous selected feats
if (featLookup[featId]) {
return false;
}
// If the Feat does not meet the tag constraints, should they exist, exclude it
const tagCategories = featUtils.getCategories(feat);
if (
tagConstraints &&
!featUtils.doesSatisfyTagConstraints(tagCategories, tagConstraints)
) {
return false;
}
// Handle prerequisites when enforcing feat rules
if (
preferences.enforceFeatRules &&
!prerequisiteUtils.validatePrerequisiteGrouping(
featUtils.getPrerequisites(feat),
prerequisiteData
)
) {
return false;
}
// Special handling for repeatable feats, there can be only one
if (isRepeatable) {
const parentId = featUtils.getRepeatableGroupId(feat);
// If a feat from this repeatable group exist exclude all others
if (repeatableFeatTracker.has(parentId)) {
return false;
}
repeatableFeatTracker.add(parentId);
}
// If none of the exclusions above are met, include the feat
return true;
});
//Group available feats by source category
availableGroupedOptions = getGroupedOptionsBySourceCategory(
filteredFeats
.map((feat) => featUtils.getDefinition(feat))
.filter(TypeScriptUtils.isNotNullOrUndefined)
);
if (selectedFeat && optionValue !== null) {
detailChoiceDesc = featUtils.getDescription(selectedFeat);
subchoicesNode = <DetailChoiceFeat featId={optionValue} />;
}
break;
case BuilderChoiceTypeEnum.SUB_CLASS_OPTION:
const subclassData = getSubclassData();
//Group available subclasses by source category
availableGroupedOptions = getGroupedOptionsBySourceCategory(subclassData);
const chosenSubclass = subclassData.find(
(subclass) => subclass.id === optionValue
);
if (chosenSubclass) {
detailChoiceDesc = "";
let sources = getSubclassSources(chosenSubclass);
sources.forEach((source) => {
if (source.sourceCategory && source.sourceCategory.isToggleable) {
detailChoiceDesc += source.sourceCategory.description
? source.sourceCategory.description
: "";
}
});
}
break;
case BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION:
// Map over the options to mock parts of a SpellDefinitionContract.
const spellOptions = getSimpleSourcedDefinitionContracts(options);
availableGroupedOptions = getGroupedOptionsBySourceCategory(
spellOptions,
optionValue,
entityRestrictionData
);
// If there is a chosen spell, set detailChoiceDesc to its description.
const chosenSpell = spellOptions.find(
(spell) => spell.id === optionValue
);
if (chosenSpell) {
detailChoiceDesc = chosenSpell.description ?? "";
}
break;
default:
availableOptions = options.map((option) => ({
...option,
value: option.id,
}));
}
return (
<div className={className} {...props}>
<DetailChoice
{...choice}
choice={choice}
options={
availableGroupedOptions.length > 0
? availableGroupedOptions
: availableOptions
}
onChange={handleChoiceChange}
description={detailChoiceDesc || ""}
choiceInfo={choiceInfo}
classId={charClass && classUtils.getId(charClass)}
showBackgroundProficiencyOptions={true}
collapseDescription={collapseDescription}
/>
{subchoicesNode}
</div>
);
};
@@ -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;
};