New source found from dndbeyond.com
This commit is contained in:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user