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;
|
||||
};
|
||||
@@ -1,11 +1,15 @@
|
||||
import { orderBy } from 'lodash';
|
||||
import { TypeScriptUtils } from "../../utils";
|
||||
import { ChoiceAccessors } from '.';
|
||||
import { ClassAccessors } from '../Class';
|
||||
import { AbilityStatEnum, BuilderChoiceSubtypeEnum } from '../Core';
|
||||
import { AbilityStatEnum, BuilderChoiceSubtypeEnum, BuilderChoiceTypeEnum, } from '../Core';
|
||||
import { DataOriginTypeEnum } from '../DataOrigin';
|
||||
import { EntityUtils } from '../Entity';
|
||||
import { FeatAccessors, FeatUtils } from '../Feat';
|
||||
import { HelperUtils } from '../Helper';
|
||||
import { ModifierAccessors } from '../Modifier';
|
||||
import { PrerequisiteValidators } from '../Prerequisite';
|
||||
import { RuleDataAccessors } from '../RuleData';
|
||||
import { SourceUtils } from '../Source';
|
||||
import { SpellUtils } from '../Spell';
|
||||
import { getDefaultSubtypes, getOptions, getOptionValue, isOptional } from './accessors';
|
||||
@@ -33,8 +37,9 @@ export function isTodo(choice) {
|
||||
return !isOptional(choice) && getOptionValue(choice) === null;
|
||||
}
|
||||
// Given a Choice, return options for a select element that are potentially grouped by sourceCategory
|
||||
//used in DetailChoiceFeat.tsx for feat choices
|
||||
export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionData) {
|
||||
//used in FeatPane and FeatDetail for feat choices
|
||||
// and in getSortedChoiceOptionsForFeaturesInfo for feature choices that are feats and have subchoices
|
||||
export function getSortedChoiceOptionsForFeatsInfo(featId, choice, ruleData, entityRestrictionData) {
|
||||
var _a;
|
||||
const options = getOptions(choice);
|
||||
let description;
|
||||
@@ -56,10 +61,126 @@ export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionDa
|
||||
featChoiceOptions = orderBy(featChoiceOptions, 'label');
|
||||
}
|
||||
return {
|
||||
featId,
|
||||
choice,
|
||||
options: featChoiceOptions,
|
||||
description,
|
||||
};
|
||||
}
|
||||
// Given a Choice, return options for a select element that are potentially grouped by sourceCategory
|
||||
//used in FeatureChoice for all choices
|
||||
// extracted from FeatureChoice.tsx
|
||||
// TODO needs to be broken down into smaller utils and simplified
|
||||
export function getSortedChoiceOptionsForFeaturesInfo(choice, fetchedFeats, featLookup, charClass, fetchedSubclasses, characterPreferences, prerequisiteData, ruleData, entityRestrictionData) {
|
||||
var _a, _b;
|
||||
let description;
|
||||
let availableOptions;
|
||||
let featSubChoices;
|
||||
const optionValue = ChoiceAccessors.getOptionValue(choice);
|
||||
const options = ChoiceAccessors.getOptions(choice);
|
||||
const type = ChoiceAccessors.getType(choice);
|
||||
const tagConstraints = ChoiceAccessors.getTagConstraints(choice);
|
||||
switch (type) {
|
||||
case BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION:
|
||||
const availableFeats = getAvailableFeatChoices(optionValue, fetchedFeats, featLookup);
|
||||
const repeatableFeatTracker = new Set();
|
||||
// Add selected feat to repeatable tracker if repeatable
|
||||
const selectedFeat = optionValue ? featLookup[optionValue] : null;
|
||||
if (selectedFeat && FeatAccessors.isRepeatable(selectedFeat)) {
|
||||
const parentId = FeatUtils.getRepeatableGroupId(selectedFeat);
|
||||
if (parentId) {
|
||||
repeatableFeatTracker.add(parentId);
|
||||
}
|
||||
}
|
||||
const filteredFeats = availableFeats.filter((feat) => {
|
||||
const featId = FeatAccessors.getId(feat);
|
||||
const isRepeatable = FeatAccessors.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 = FeatAccessors.getCategories(feat);
|
||||
if (tagConstraints && !FeatUtils.doesSatisfyTagConstraints(tagCategories, tagConstraints)) {
|
||||
return false;
|
||||
}
|
||||
// Handle prerequisites when enforcing feat rules
|
||||
if (characterPreferences.enforceFeatRules &&
|
||||
!PrerequisiteValidators.validatePrerequisiteGrouping(FeatAccessors.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
|
||||
availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(filteredFeats
|
||||
.map((feat) => FeatAccessors.getDefinition(feat))
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined), ruleData);
|
||||
if (selectedFeat && optionValue !== null) {
|
||||
description = (_a = FeatAccessors.getDescription(selectedFeat)) !== null && _a !== void 0 ? _a : '';
|
||||
const featChoices = FeatAccessors.getChoices(selectedFeat);
|
||||
if (featChoices.length > 0) {
|
||||
featSubChoices = featChoices.map((featChoice) => {
|
||||
return getSortedChoiceOptionsForFeatsInfo(optionValue, featChoice, ruleData, entityRestrictionData);
|
||||
});
|
||||
}
|
||||
// subchoicesNode = <DetailChoiceFeat featId={optionValue} />;
|
||||
}
|
||||
break;
|
||||
case BuilderChoiceTypeEnum.SUB_CLASS_OPTION:
|
||||
const subclassData = charClass ? getAvailableSubclassChoices(charClass, fetchedSubclasses) : [];
|
||||
//Group available subclasses by source category
|
||||
availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(subclassData, ruleData);
|
||||
const chosenSubclass = subclassData.find((subclass) => subclass.id === optionValue);
|
||||
if (chosenSubclass) {
|
||||
description = '';
|
||||
const sources = chosenSubclass.sources
|
||||
? chosenSubclass.sources
|
||||
.map((sourceMapping) => HelperUtils.lookupDataOrFallback(RuleDataAccessors.getSourceDataLookup(ruleData), sourceMapping.sourceId))
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined)
|
||||
: [];
|
||||
sources.forEach((source) => {
|
||||
var _a, _b;
|
||||
if ((_a = source.sourceCategory) === null || _a === void 0 ? void 0 : _a.isToggleable) {
|
||||
description += source.sourceCategory.description ? source.sourceCategory.description : '';
|
||||
}
|
||||
description += (_b = chosenSubclass.description) !== null && _b !== void 0 ? _b : '';
|
||||
});
|
||||
}
|
||||
break;
|
||||
case BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION:
|
||||
// Map over the options to mock parts of a SpellDefinitionContract.
|
||||
const spellOptions = SourceUtils.getSimpleSourcedDefinitionContracts(options);
|
||||
availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(spellOptions, ruleData, optionValue, entityRestrictionData);
|
||||
// If there is a chosen spell, set detailChoiceDesc to its description.
|
||||
const chosenSpell = spellOptions.find((spell) => spell.id === optionValue);
|
||||
if (chosenSpell) {
|
||||
description = (_b = chosenSpell.description) !== null && _b !== void 0 ? _b : '';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
availableOptions = options.map((option) => (Object.assign(Object.assign({}, option), { value: option.id })));
|
||||
}
|
||||
return {
|
||||
choice,
|
||||
options: availableOptions,
|
||||
featSubChoices,
|
||||
description,
|
||||
};
|
||||
}
|
||||
// filter Choice options based on modifiers and default subtypes that identify when a user has made a choice from another source. We filter those options out or we append the source data origin name next to the option label
|
||||
export function getRemainingOptions(options, modifiers, defaultSubtypes, showBackgroundProficiencyOptions, optionValue) {
|
||||
let remainingSubtypes = [];
|
||||
@@ -209,3 +330,21 @@ export function getSortedRenderOptions(options, classId, subType, entityRestrict
|
||||
? orderBy(renderOptions, 'sortOrder')
|
||||
: orderBy(renderOptions, 'label');
|
||||
}
|
||||
//Taken from FeatureChoice.tsx
|
||||
function getAvailableSubclassChoices(charClass, fetchedSubclasses) {
|
||||
const existingSubclass = ClassAccessors.getSubclass(charClass);
|
||||
//if there is an existing subclass that isn't already in the fetched subclasses, append it to the list
|
||||
if (existingSubclass && !fetchedSubclasses.some((subclass) => subclass.id === existingSubclass.id)) {
|
||||
return [...fetchedSubclasses, existingSubclass];
|
||||
}
|
||||
return fetchedSubclasses;
|
||||
}
|
||||
//Taken from FeatureChoice.tsx
|
||||
function getAvailableFeatChoices(existingFeatId, fetchedFeats, featLookup) {
|
||||
const existingFeat = existingFeatId ? HelperUtils.lookupDataOrFallback(featLookup, existingFeatId) : null;
|
||||
// If there is an existing feat that isn't already in the fetched feats, append it to the list
|
||||
if (existingFeat !== null && !fetchedFeats.some((feat) => FeatAccessors.getId(feat) === existingFeatId)) {
|
||||
return [...fetchedFeats, existingFeat];
|
||||
}
|
||||
return fetchedFeats;
|
||||
}
|
||||
|
||||
@@ -171,8 +171,9 @@ export function deriveReplacementWeaponAbilityStats(classModifiers) {
|
||||
classModifiers.forEach((modifier) => {
|
||||
if (ModifierValidators.isEnablesAbilityStatModifier(modifier)) {
|
||||
const statId = ModifierAccessors.getEntityId(modifier) || 0;
|
||||
if (statId && !replacementWeaponAbilityStats.includes(statId)) {
|
||||
replacementWeaponAbilityStats.push(statId);
|
||||
const dataOrigin = ModifierAccessors.getDataOrigin(modifier);
|
||||
if (statId) {
|
||||
replacementWeaponAbilityStats.push({ statId, dataOrigin });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -343,6 +343,11 @@ export function* handleAdhocFeatCreate(action) {
|
||||
* @param action
|
||||
*/
|
||||
export function* handleAdhocFeatRemove(action) {
|
||||
const featLookup = yield select(rulesEngineSelectors.getFeatLookup);
|
||||
const currentFeat = HelperUtils.lookupDataOrFallback(featLookup, action.payload.id, null);
|
||||
if (currentFeat !== null) {
|
||||
yield call(apiRemoveSpellsBySpellListIds, FeatAccessors.getSpellListIds(currentFeat));
|
||||
}
|
||||
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.deleteCharacterFeatAdHoc, action.payload);
|
||||
yield call(handleDataUpdates, data);
|
||||
}
|
||||
@@ -457,7 +462,13 @@ export function* handleRaceChoose(action) {
|
||||
const { race } = action.payload;
|
||||
const existingRace = yield select(rulesEngineSelectors.getRace);
|
||||
if (existingRace !== null) {
|
||||
yield call(apiRemoveSpellsBySpellListIds, RaceAccessors.getSpellListIds(existingRace));
|
||||
const spellListIdsToRemove = RaceAccessors.getSpellListIds(existingRace);
|
||||
const racialTraitsToRemove = RaceAccessors.getRacialTraits(existingRace);
|
||||
const featsFromRacialTraits = racialTraitsToRemove.map((trait) => RacialTraitAccessors.getFeats(trait)).flat();
|
||||
featsFromRacialTraits.forEach((feat) => {
|
||||
spellListIdsToRemove.push(...FeatAccessors.getSpellListIds(feat));
|
||||
});
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove);
|
||||
const existingOptionalOrigins = yield select(rulesEngineSelectors.getOptionalOrigins);
|
||||
const optionalOriginsIdsToRemove = existingOptionalOrigins.map(OptionalOriginAccessors.getRacialTraitId);
|
||||
yield call(apiRemoveOptionalOriginsCollection, {
|
||||
@@ -1309,17 +1320,23 @@ export function* handleCurrencyTransactionSet(action) {
|
||||
*/
|
||||
export function* handleRacialTraitChoiceSetRequest(action) {
|
||||
const { racialTraitId, choiceId } = action.payload;
|
||||
yield call(apiRacialTraitChoiceSet, action);
|
||||
const race = yield select(rulesEngineSelectors.getRace);
|
||||
if (race) {
|
||||
const racialTrait = RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId);
|
||||
const racialTrait = race
|
||||
? RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId)
|
||||
: null;
|
||||
// Remove any spells that are associated with the feat from the racial trait choices
|
||||
const feats = racialTrait ? RacialTraitAccessors.getFeats(racialTrait) : [];
|
||||
const featSpellListIds = feats
|
||||
.map(FeatAccessors.getSpellListIds)
|
||||
.reduce((acc, ids) => acc.concat(ids), []);
|
||||
yield call(apiRemoveSpellsBySpellListIds, featSpellListIds);
|
||||
yield call(apiRacialTraitChoiceSet, action);
|
||||
if (racialTrait) {
|
||||
const racialTraitChoice = RacialTraitAccessors.getChoices(racialTrait).find((choice) => ChoiceAccessors.getId(choice) === choiceId);
|
||||
if (racialTraitChoice) {
|
||||
yield call(autoUpdateChoices, racialTraitChoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -1343,6 +1360,7 @@ export function* handleClassFeatureChoiceSetRequest(action) {
|
||||
const { classId, classFeatureId, choiceId, choiceType, optionValue } = action.payload;
|
||||
let oldClasses = yield select(rulesEngineSelectors.getClasses);
|
||||
let oldCharClass = oldClasses.find((charClass) => ClassAccessors.getActiveId(charClass) === classId);
|
||||
const spellListIdsToRemove = [];
|
||||
if (choiceType === BuilderChoiceTypeEnum.SUB_CLASS_OPTION && oldCharClass) {
|
||||
const subclass = ClassAccessors.getSubclass(oldCharClass);
|
||||
if (subclass !== null) {
|
||||
@@ -1350,9 +1368,19 @@ export function* handleClassFeatureChoiceSetRequest(action) {
|
||||
.filter((classFeature) => ClassFeatureAccessors.getClassId(classFeature) === subclass.id)
|
||||
.map(ClassFeatureAccessors.getSpellListIds)
|
||||
.reduce((acc, ids) => acc.concat(ids), []);
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIds);
|
||||
spellListIdsToRemove.push(...spellListIds);
|
||||
}
|
||||
}
|
||||
if (choiceType === BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION && oldCharClass) {
|
||||
const classFeature = ClassAccessors.getClassFeatures(oldCharClass).find((classFeature) => ClassFeatureAccessors.getId(classFeature) === classFeatureId);
|
||||
const featsFromClassFeature = classFeature ? ClassFeatureAccessors.getFeats(classFeature) : [];
|
||||
featsFromClassFeature.forEach((feat) => {
|
||||
const spellListIds = FeatAccessors.getSpellListIds(feat);
|
||||
spellListIdsToRemove.push(...spellListIds);
|
||||
});
|
||||
}
|
||||
//remove any spells that are associated with the spell lists from the subclass or feat choices
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove);
|
||||
yield call(apiClassFeatureChoiceSet, action);
|
||||
const classes = yield select(rulesEngineSelectors.getClasses);
|
||||
const charClass = classes.find((charClass) => !!oldCharClass && ClassAccessors.getId(charClass) === ClassAccessors.getId(oldCharClass));
|
||||
@@ -1453,12 +1481,21 @@ export function* handleClassLevelSetRequest(action) {
|
||||
const oldClassId = ClassAccessors.getId(oldCharClass);
|
||||
const removedClassFeatures = ClassAccessors.getActiveClassFeatures(oldCharClass).filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level);
|
||||
const isRemovingSubclass = removedClassFeatures.some((feature) => ClassFeatureAccessors.getChoices(feature).some((choice) => ChoiceAccessors.getType(choice) === BuilderChoiceTypeEnum.SUB_CLASS_OPTION));
|
||||
const spellListIdsFromFeatsToRemove = [];
|
||||
removedClassFeatures.forEach((classFeature) => {
|
||||
const feats = ClassFeatureAccessors.getFeats(classFeature);
|
||||
feats.forEach((feat) => {
|
||||
const spellListIds = FeatAccessors.getSpellListIds(feat);
|
||||
spellListIdsFromFeatsToRemove.push(...spellListIds);
|
||||
});
|
||||
});
|
||||
const removedSpellListIds = ClassAccessors.getActiveClassFeatures(oldCharClass)
|
||||
.filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level ||
|
||||
(isRemovingSubclass && ClassFeatureAccessors.getClassId(classFeature) !== oldClassId))
|
||||
.map(ClassFeatureAccessors.getSpellListIds)
|
||||
.reduce((acc, spellListIds) => acc.concat(spellListIds), []);
|
||||
yield call(apiRemoveSpellsBySpellListIds, removedSpellListIds);
|
||||
// Remove spells associated with the spell lists from the removed class features
|
||||
yield call(apiRemoveSpellsBySpellListIds, [...removedSpellListIds, ...spellListIdsFromFeatsToRemove]);
|
||||
}
|
||||
}
|
||||
yield call(apiClassLevelSet, action);
|
||||
@@ -1582,7 +1619,21 @@ function* updateBackgroundResult(action, data) {
|
||||
export function* handleBackgroundSetRequest(action) {
|
||||
const existingBackground = yield select(rulesEngineSelectors.getBackgroundInfo);
|
||||
if (existingBackground !== null) {
|
||||
yield call(apiRemoveSpellsBySpellListIds, BackgroundAccessors.getSpellListIds(existingBackground));
|
||||
//start an array of spellListIds to remove with any spellListIds from the existing background
|
||||
const spellListIdsToRemove = BackgroundAccessors.getSpellListIds(existingBackground);
|
||||
//check for any granted or chosen feats on the background and get their spellListIds
|
||||
const featLookup = yield select(rulesEngineSelectors.getFeatLookup);
|
||||
const chosenFeatIds = BackgroundAccessors.getFeatLists(existingBackground)
|
||||
.map((featList) => featList.chosenFeatId)
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||
chosenFeatIds.forEach((featId) => {
|
||||
const feat = HelperUtils.lookupDataOrFallback(featLookup, featId);
|
||||
if (feat) {
|
||||
spellListIdsToRemove.push(...FeatAccessors.getSpellListIds(feat));
|
||||
}
|
||||
});
|
||||
//remove any spellListIds that were collected
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove);
|
||||
}
|
||||
yield put(callCommitAction(characterActions.backgroundHasCustomSet, false));
|
||||
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.putCharacterBackground, action.payload);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CreaturePane } from "~/subApps/sheet/components/Sidebar/panes/CreaturePane";
|
||||
import { GameLogPane } from "~/subApps/sheet/components/Sidebar/panes/GameLogPane";
|
||||
import { SpeciesTraitPane } from "~/subApps/sheet/components/Sidebar/panes/SpeciesTraitPane";
|
||||
import BlessingPane from "~/tools/js/Shared/containers/panes/BlessingPane";
|
||||
import CharacterSpellPane from "~/tools/js/Shared/containers/panes/CharacterSpellPane";
|
||||
import ClassSpellPane from "~/tools/js/Shared/containers/panes/ClassSpellPane";
|
||||
@@ -34,7 +35,6 @@ import { ShareUrlPane } from "~/tools/js/Shared/containers/panes/ShareUrlPane";
|
||||
import ShortRestPane from "~/tools/js/Shared/containers/panes/ShortRestPane";
|
||||
import SkillPane from "~/tools/js/Shared/containers/panes/SkillPane";
|
||||
import SkillsPane from "~/tools/js/Shared/containers/panes/SkillsPane";
|
||||
import SpeciesTraitPane from "~/tools/js/Shared/containers/panes/SpeciesTraitPane";
|
||||
import SpeedManagePane from "~/tools/js/Shared/containers/panes/SpeedManagePane";
|
||||
import SpellManagePane from "~/tools/js/Shared/containers/panes/SpellManagePane";
|
||||
import StartingEquipmentPane from "~/tools/js/Shared/containers/panes/StartingEquipmentPane";
|
||||
|
||||
+9
-170
@@ -1,45 +1,27 @@
|
||||
import {
|
||||
FC,
|
||||
HTMLAttributes,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
ApiAdapterUtils,
|
||||
FeatManager,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
import { FeatManager } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { FeatureChoice } from "~/components/FeatureChoice";
|
||||
import { BuilderChoiceTypeEnum } from "~/constants";
|
||||
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
import DataLoadingStatusEnum from "~/tools/js/Shared/constants/DataLoadingStatusEnum";
|
||||
import { ClassFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
import { apiCreatorSelectors } from "~/tools/js/Shared/selectors";
|
||||
import { AppLoggerUtils, PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
import {
|
||||
Action,
|
||||
CharClass,
|
||||
ClassDefinitionContract,
|
||||
ClassFeature,
|
||||
Feat,
|
||||
InfusionChoice,
|
||||
Spell,
|
||||
} from "~/types";
|
||||
|
||||
import { ClassFeatureSnippet } from "../../../../../../tools/js/CharacterSheet/components/FeatureSnippet";
|
||||
import { appEnvSelectors } from "../../../../../../tools/js/Shared/selectors";
|
||||
import { Heading } from "../../components/Heading";
|
||||
import { PaneInitFailureContent } from "../../components/PaneInitFailureContent";
|
||||
import { PaneComponentEnum, PaneIdentifiersClassFeature } from "../../types";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
identifiers: PaneIdentifiersClassFeature | null;
|
||||
@@ -61,8 +43,6 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
proficiencyBonus,
|
||||
originRef: dataOriginRefData,
|
||||
characterTheme: theme,
|
||||
choiceUtils,
|
||||
featUtils,
|
||||
helperUtils,
|
||||
} = useCharacterEngine();
|
||||
|
||||
@@ -76,23 +56,6 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...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 currentCharClass = useRef<CharClass | null>(null);
|
||||
|
||||
const charClass = identifiers?.classMappingId
|
||||
? classes.find(
|
||||
(charClass) =>
|
||||
@@ -105,98 +68,6 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
(feature) => identifiers?.id === classFeatureUtils.getId(feature)
|
||||
) ?? null;
|
||||
|
||||
const featureChoices = classFeature
|
||||
? classFeatureUtils.getChoices(classFeature)
|
||||
: [];
|
||||
|
||||
const hasFeatChoice = featureChoices.some(
|
||||
(choice) =>
|
||||
choiceUtils.getType(choice) === BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION
|
||||
);
|
||||
|
||||
const hasSubclassChoice = featureChoices.some(
|
||||
(choice) =>
|
||||
choiceUtils.getType(choice) === BuilderChoiceTypeEnum.SUB_CLASS_OPTION
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const isSameClass = (
|
||||
c1: CharClass | null,
|
||||
c2: CharClass | null
|
||||
): boolean => {
|
||||
if (!c1 || !c2) return false;
|
||||
return classUtils.getId(c1) === classUtils.getId(c2);
|
||||
};
|
||||
|
||||
if (
|
||||
hasFeatChoice &&
|
||||
featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
|
||||
) {
|
||||
setFeatLoadingStatus(DataLoadingStatusEnum.LOADING);
|
||||
|
||||
loadAvailableFeats({
|
||||
// cancelToken: new axios.CancelToken((c) => {
|
||||
// this.loadFeatsCanceler = c;
|
||||
// }),
|
||||
})
|
||||
.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);
|
||||
// this.loadFeatsCanceler = null;
|
||||
})
|
||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
||||
}
|
||||
|
||||
if (
|
||||
charClass &&
|
||||
!isSameClass(charClass, currentCharClass.current) && // Only pull data if the class has changed from previous fetch
|
||||
hasSubclassChoice &&
|
||||
subclassLoadingStatus !== DataLoadingStatusEnum.LOADING // Check if its not already fetching data
|
||||
) {
|
||||
currentCharClass.current = charClass; //Update ref of fetched class
|
||||
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING);
|
||||
|
||||
loadAvailableSubclasses(classUtils.getId(charClass), {
|
||||
// cancelToken: new axios.CancelToken((c) => {
|
||||
// this.loadSubclassesCanceler = c;
|
||||
// }),
|
||||
})
|
||||
.then((response) => {
|
||||
let subclassData: Array<ClassDefinitionContract> = [];
|
||||
|
||||
const data = ApiAdapterUtils.getResponseData(response);
|
||||
if (data !== null) {
|
||||
subclassData = data;
|
||||
}
|
||||
|
||||
setSubclassData(subclassData);
|
||||
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED);
|
||||
// this.loadSubclassesCanceler = null;
|
||||
})
|
||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
||||
}
|
||||
}, [
|
||||
hasFeatChoice,
|
||||
hasSubclassChoice,
|
||||
subclassLoadingStatus,
|
||||
featLoadingStatus,
|
||||
charClass,
|
||||
classUtils,
|
||||
featUtils,
|
||||
loadAvailableFeats,
|
||||
loadAvailableSubclasses,
|
||||
currentCharClass,
|
||||
]);
|
||||
|
||||
const handleInfusionChoiceShow = (infusionChoice: InfusionChoice): void => {
|
||||
const choiceKey = infusionChoiceUtils.getKey(infusionChoice);
|
||||
if (choiceKey !== null) {
|
||||
@@ -302,21 +173,6 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isDataLoaded = (): boolean => {
|
||||
if (hasSubclassChoice) {
|
||||
if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (hasFeatChoice) {
|
||||
if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!charClass || !classFeature) {
|
||||
return <PaneInitFailureContent />;
|
||||
}
|
||||
@@ -356,30 +212,13 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
onFeatClick={handleFeatShow}
|
||||
featuresManager={characterFeaturesManager}
|
||||
/>
|
||||
{!isReadonly && featureChoices.length > 0 && (
|
||||
<>
|
||||
<Heading className={styles.heading}>{`Option${
|
||||
featureChoices.length > 1 ? "s" : ""
|
||||
}`}</Heading>
|
||||
{isDataLoaded() ? (
|
||||
featureChoices.map((choice) => (
|
||||
<FeatureChoice
|
||||
<FeatureChoices
|
||||
choices={classFeatureUtils.getChoices(classFeature)}
|
||||
charClass={charClass}
|
||||
choice={choice}
|
||||
feature={classFeature}
|
||||
featsData={featsData}
|
||||
subclassData={subclassData}
|
||||
className={styles.choice}
|
||||
key={choiceUtils.getId(choice)}
|
||||
onChoiceChange={handleChoiceChange}
|
||||
collapseDescription={true}
|
||||
showHeading={true}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<LoadingPlaceholder />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { ChoiceUtils } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { FeatFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet";
|
||||
import { DetailChoiceFeat } from "~/tools/js/Shared/containers/DetailChoice";
|
||||
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
import { DdbBadgeSvg } from "~/tools/js/smartComponents/Svg";
|
||||
import { Action, Spell } from "~/types";
|
||||
|
||||
import { Header } from "../../components/Header";
|
||||
@@ -38,6 +37,8 @@ export const FeatPane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
originRef: dataOriginRefData,
|
||||
characterTheme: theme,
|
||||
entityUtils,
|
||||
entityRestrictionData,
|
||||
choiceInfo,
|
||||
} = useCharacterEngine();
|
||||
|
||||
const { characterFeaturesManager } = useContext(
|
||||
@@ -115,6 +116,18 @@ export const FeatPane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleChoiceChange = (
|
||||
id: string | null,
|
||||
type: number,
|
||||
value: any
|
||||
): void => {
|
||||
if (feat && id !== null) {
|
||||
dispatch(
|
||||
characterActions.featChoiceSetRequest(feat.getId(), type, id, value)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (feat === null) {
|
||||
return <PaneInitFailureContent />;
|
||||
}
|
||||
@@ -163,7 +176,31 @@ export const FeatPane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
/>
|
||||
{!isReadonly && feat.getChoices().length > 0 && (
|
||||
<div className={styles.choices}>
|
||||
<DetailChoiceFeat featId={feat.getId()} />
|
||||
{feat.getChoices().map((choice) => {
|
||||
const id = ChoiceUtils.getId(choice);
|
||||
|
||||
const { description, options: featChoiceOptions } =
|
||||
ChoiceUtils.getSortedChoiceOptionsForFeatsInfo(
|
||||
feat.getId(),
|
||||
choice,
|
||||
ruleData,
|
||||
entityRestrictionData
|
||||
);
|
||||
|
||||
return (
|
||||
<DetailChoice
|
||||
{...choice}
|
||||
label={choice.label}
|
||||
choice={choice}
|
||||
key={id !== null ? id : ""}
|
||||
options={featChoiceOptions}
|
||||
onChange={handleChoiceChange}
|
||||
choiceInfo={choiceInfo}
|
||||
showBackgroundProficiencyOptions={true}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!feat.isHiddenFeat() &&
|
||||
|
||||
+52
-3
@@ -1,9 +1,15 @@
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import { FeatManager } from "@dndbeyond/character-rules-engine/es";
|
||||
import {
|
||||
characterActions,
|
||||
ChoiceUtils,
|
||||
FeatManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { DetailChoiceFeat } from "~/tools/js/Shared/containers/DetailChoice";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
@@ -12,9 +18,28 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
export const FeatDetail: FC<Props> = ({ featManager, className, ...props }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { ruleData, entityRestrictionData, choiceInfo } = useCharacterEngine();
|
||||
|
||||
const prerequisiteDescription = featManager.getPrerequisiteDescription();
|
||||
const featDescription = featManager.getDescription();
|
||||
|
||||
const handleChoiceChange = (
|
||||
id: string | null,
|
||||
type: number,
|
||||
value: any
|
||||
): void => {
|
||||
if (featManager && id !== null) {
|
||||
dispatch(
|
||||
characterActions.featChoiceSetRequest(
|
||||
featManager.getId(),
|
||||
type,
|
||||
id,
|
||||
value
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{prerequisiteDescription && (
|
||||
@@ -27,7 +52,31 @@ export const FeatDetail: FC<Props> = ({ featManager, className, ...props }) => {
|
||||
)}
|
||||
{featManager.getChoices().length > 0 && (
|
||||
<div className={styles.choices}>
|
||||
<DetailChoiceFeat featId={featManager.getId()} />
|
||||
{featManager.getChoices().map((choice) => {
|
||||
const id = ChoiceUtils.getId(choice);
|
||||
|
||||
const { description, options: featChoiceOptions } =
|
||||
ChoiceUtils.getSortedChoiceOptionsForFeatsInfo(
|
||||
featManager.getId(),
|
||||
choice,
|
||||
ruleData,
|
||||
entityRestrictionData
|
||||
);
|
||||
|
||||
return (
|
||||
<DetailChoice
|
||||
{...choice}
|
||||
label={choice.label}
|
||||
choice={choice}
|
||||
key={id !== null ? id : ""}
|
||||
options={featChoiceOptions}
|
||||
onChange={handleChoiceChange}
|
||||
choiceInfo={choiceInfo}
|
||||
showBackgroundProficiencyOptions={true}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
import { FC, HTMLAttributes, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
Action,
|
||||
ActionUtils,
|
||||
characterActions,
|
||||
FeatManager,
|
||||
HelperUtils,
|
||||
RaceUtils,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
Spell,
|
||||
SpellUtils,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
import {
|
||||
PaneComponentEnum,
|
||||
PaneIdentifiersRacialTrait,
|
||||
} from "~/subApps/sheet/components/Sidebar/types";
|
||||
import { SpeciesTraitFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||
|
||||
import { PaneInitFailureContent } from "../../components/PaneInitFailureContent";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
identifiers: PaneIdentifiersRacialTrait | null;
|
||||
}
|
||||
|
||||
export const SpeciesTraitPane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
race: species,
|
||||
feats,
|
||||
snippetData,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
originRef: dataOriginRefData,
|
||||
proficiencyBonus,
|
||||
characterTheme: theme,
|
||||
} = useCharacterEngine();
|
||||
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
|
||||
const {
|
||||
pane: { paneHistoryPush, paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
const speciesTrait = species
|
||||
? RaceUtils.getVisibleRacialTraits(species).find(
|
||||
(trait) => identifiers?.id === RacialTraitUtils.getId(trait)
|
||||
)
|
||||
: null;
|
||||
|
||||
const handleActionUseSet = (action: Action, uses: number): void => {
|
||||
const id = ActionUtils.getId(action);
|
||||
const entityTypeId = ActionUtils.getEntityTypeId(action);
|
||||
if (id !== null && entityTypeId !== null) {
|
||||
dispatch(
|
||||
characterActions.actionUseSet(
|
||||
id,
|
||||
entityTypeId,
|
||||
uses,
|
||||
ActionUtils.getDataOriginType(action)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpellUseSet = (spell: Spell, uses: number): void => {
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
const mappingEntityTypeId = SpellUtils.getMappingEntityTypeId(spell);
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
dispatch(
|
||||
characterActions.spellUseSet(
|
||||
mappingId,
|
||||
mappingEntityTypeId,
|
||||
uses,
|
||||
SpellUtils.getDataOriginType(spell)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpellDetailShow = (spell: Spell): void => {
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
if (mappingId !== null) {
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.CHARACTER_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpeciesTraitShow = (speciesTrait: RacialTrait): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.SPECIES_TRAIT_DETAIL,
|
||||
PaneIdentifierUtils.generateRacialTrait(
|
||||
RacialTraitUtils.getId(speciesTrait)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleActionShow = (action: Action): void => {
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.ACTION,
|
||||
PaneIdentifierUtils.generateAction(mappingId, mappingEntityTypeId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFeatShow = (feat: FeatManager): void => {
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.FEAT_DETAIL,
|
||||
PaneIdentifierUtils.generateFeat(feat.getId())
|
||||
);
|
||||
};
|
||||
|
||||
const handleChoiceChange = (
|
||||
choiceId: string,
|
||||
type: number,
|
||||
value: any
|
||||
): void => {
|
||||
if (!species || !speciesTrait) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
characterActions.racialTraitChoiceSetRequest(
|
||||
RacialTraitUtils.getId(speciesTrait),
|
||||
type,
|
||||
choiceId,
|
||||
HelperUtils.parseInputInt(value)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
if (species === null || !speciesTrait) {
|
||||
return <PaneInitFailureContent />;
|
||||
}
|
||||
|
||||
const portrait = RaceUtils.getPortraitAvatarUrl(species);
|
||||
|
||||
return (
|
||||
<div key={RacialTraitUtils.getId(speciesTrait)} {...props}>
|
||||
<Header
|
||||
parent={RaceUtils.getFullName(species)}
|
||||
preview={portrait && <Preview imageUrl={portrait} />}
|
||||
>
|
||||
{RacialTraitUtils.getName(speciesTrait)}
|
||||
</Header>
|
||||
<SpeciesTraitFeatureSnippet
|
||||
speciesTrait={speciesTrait}
|
||||
onActionUseSet={handleActionUseSet}
|
||||
onActionClick={handleActionShow}
|
||||
onSpellUseSet={handleSpellUseSet}
|
||||
onSpellClick={handleSpellDetailShow}
|
||||
onFeatureClick={handleSpeciesTraitShow}
|
||||
showHeader={false}
|
||||
showDescription={true}
|
||||
feats={feats}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
isReadonly={isReadonly}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
onFeatClick={handleFeatShow}
|
||||
featuresManager={characterFeaturesManager}
|
||||
/>
|
||||
<FeatureChoices
|
||||
choices={RacialTraitUtils.getChoices(speciesTrait)}
|
||||
onChoiceChange={handleChoiceChange}
|
||||
collapseDescription={true}
|
||||
showHeading={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+12
-261
@@ -1,11 +1,8 @@
|
||||
import axios, { Canceler } from "axios";
|
||||
import React from "react";
|
||||
|
||||
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
ApiAdapterPromise,
|
||||
ApiAdapterRequestConfig,
|
||||
ApiAdapterUtils,
|
||||
ApiResponse,
|
||||
CharacterPreferences,
|
||||
CharClass,
|
||||
@@ -18,10 +15,8 @@ import {
|
||||
Constants,
|
||||
DefinitionPool,
|
||||
EntitledEntity,
|
||||
Feat,
|
||||
FeatDefinitionContract,
|
||||
FeatLookup,
|
||||
FeatUtils,
|
||||
FormatUtils,
|
||||
HelperUtils,
|
||||
InfusionChoice,
|
||||
@@ -35,20 +30,16 @@ import {
|
||||
OptionalClassFeatureUtils,
|
||||
PrerequisiteData,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
SourceData,
|
||||
TypeValueLookup,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { CollapsibleContent } from "~/components/CollapsibleContent";
|
||||
import { FeatureChoice } from "~/components/FeatureChoice";
|
||||
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||
import { Link } from "~/components/Link";
|
||||
import { GrantedFeat } from "~/tools/js/CharacterBuilder/components/GrantedFeat/GrantedFeat";
|
||||
|
||||
import InfusionChoiceManager from "../../../../../Shared/components/InfusionChoiceManager";
|
||||
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
|
||||
import { AppLoggerUtils, TypeScriptUtils } from "../../../../../Shared/utils";
|
||||
|
||||
interface Props {
|
||||
isActive: boolean;
|
||||
@@ -122,13 +113,7 @@ interface Props {
|
||||
inventoryManager: InventoryManager;
|
||||
}
|
||||
interface State {
|
||||
featData: Array<Feat>;
|
||||
subclassData: Array<ClassDefinitionContract>;
|
||||
hasFeatChoice: boolean;
|
||||
hasSubclassChoice: boolean;
|
||||
collapsibleOpened: boolean;
|
||||
featLoadingStatus: DataLoadingStatusEnum;
|
||||
subclassLoadingStatus: DataLoadingStatusEnum;
|
||||
}
|
||||
export default class ClassManagerFeature extends React.PureComponent<
|
||||
Props,
|
||||
@@ -139,157 +124,16 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
featLookup: {},
|
||||
};
|
||||
|
||||
loadFeatsCanceler: null | Canceler = null;
|
||||
loadSubclassesCanceler: null | Canceler = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
featData: [],
|
||||
subclassData: [],
|
||||
hasFeatChoice: false,
|
||||
hasSubclassChoice: false,
|
||||
collapsibleOpened: false,
|
||||
featLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
|
||||
subclassLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.setState(
|
||||
{
|
||||
hasFeatChoice: this.hasFeatChoice(this.props),
|
||||
hasSubclassChoice: this.hasSubclassChoice(this.props),
|
||||
},
|
||||
() => {
|
||||
this.conditionallyLoadFeatData();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
hasSubclassChoice = (): boolean => {
|
||||
const { feature } = this.props;
|
||||
const { collapsibleOpened } = this.state;
|
||||
|
||||
if (feature !== prevProps.feature) {
|
||||
this.setState(
|
||||
{
|
||||
hasFeatChoice: this.hasFeatChoice(this.props),
|
||||
hasSubclassChoice: this.hasSubclassChoice(this.props),
|
||||
},
|
||||
() => {
|
||||
this.conditionallyLoadFeatData();
|
||||
}
|
||||
);
|
||||
}
|
||||
if (collapsibleOpened && !prevState.collapsibleOpened) {
|
||||
this.conditionallyLoadFeatData();
|
||||
}
|
||||
}
|
||||
|
||||
conditionallyLoadFeatData = (): void => {
|
||||
const { loadAvailableFeats, loadAvailableSubclasses, charClass } =
|
||||
this.props;
|
||||
const {
|
||||
hasFeatChoice,
|
||||
hasSubclassChoice,
|
||||
featLoadingStatus,
|
||||
subclassLoadingStatus,
|
||||
collapsibleOpened,
|
||||
} = this.state;
|
||||
|
||||
if (
|
||||
hasFeatChoice &&
|
||||
collapsibleOpened &&
|
||||
featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
|
||||
) {
|
||||
this.setState({
|
||||
featLoadingStatus: DataLoadingStatusEnum.LOADING,
|
||||
});
|
||||
|
||||
loadAvailableFeats({
|
||||
cancelToken: new axios.CancelToken((c) => {
|
||||
this.loadFeatsCanceler = c;
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
let featData: Array<Feat> = [];
|
||||
|
||||
const data = ApiAdapterUtils.getResponseData(response);
|
||||
if (data !== null) {
|
||||
featData = data.map((definition) =>
|
||||
FeatUtils.simulateFeat(definition)
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
featData,
|
||||
featLoadingStatus: DataLoadingStatusEnum.LOADED,
|
||||
});
|
||||
this.loadFeatsCanceler = null;
|
||||
})
|
||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
||||
}
|
||||
|
||||
if (
|
||||
hasSubclassChoice &&
|
||||
collapsibleOpened &&
|
||||
subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
|
||||
) {
|
||||
this.setState({
|
||||
subclassLoadingStatus: DataLoadingStatusEnum.LOADING,
|
||||
});
|
||||
|
||||
loadAvailableSubclasses(ClassUtils.getId(charClass), {
|
||||
cancelToken: new axios.CancelToken((c) => {
|
||||
this.loadSubclassesCanceler = c;
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
let subclassData: Array<ClassDefinitionContract> = [];
|
||||
|
||||
const data = ApiAdapterUtils.getResponseData(response);
|
||||
if (data !== null) {
|
||||
subclassData = data;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
subclassData,
|
||||
subclassLoadingStatus: DataLoadingStatusEnum.LOADED,
|
||||
});
|
||||
this.loadSubclassesCanceler = null;
|
||||
})
|
||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.loadFeatsCanceler !== null) {
|
||||
this.loadFeatsCanceler();
|
||||
}
|
||||
if (this.loadSubclassesCanceler !== null) {
|
||||
this.loadSubclassesCanceler();
|
||||
}
|
||||
}
|
||||
|
||||
hasFeatChoice = (props: Props): boolean => {
|
||||
const { feature } = props;
|
||||
|
||||
const choices = ClassFeatureUtils.getChoices(feature);
|
||||
return choices.some(
|
||||
(choice) =>
|
||||
ChoiceUtils.getType(choice) ===
|
||||
Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION
|
||||
);
|
||||
};
|
||||
|
||||
hasSubclassChoice = (props: Props): boolean => {
|
||||
const { feature } = props;
|
||||
|
||||
const choices = ClassFeatureUtils.getChoices(feature);
|
||||
return choices.some(
|
||||
@@ -299,28 +143,6 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
);
|
||||
};
|
||||
|
||||
isDataLoaded = (): boolean => {
|
||||
const {
|
||||
hasSubclassChoice,
|
||||
hasFeatChoice,
|
||||
subclassLoadingStatus,
|
||||
featLoadingStatus,
|
||||
} = this.state;
|
||||
|
||||
if (hasSubclassChoice) {
|
||||
if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (hasFeatChoice) {
|
||||
if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
handleCollapsibleOpened = (id: string, state: boolean): void => {
|
||||
this.setState({
|
||||
collapsibleOpened: state,
|
||||
@@ -436,46 +258,6 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
);
|
||||
};
|
||||
|
||||
getSubclassSources = (
|
||||
subclass: ClassDefinitionContract
|
||||
): Array<SourceData> => {
|
||||
const { ruleData } = this.props;
|
||||
|
||||
if (subclass.sources === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return subclass.sources
|
||||
.map((sourceMapping) =>
|
||||
HelperUtils.lookupDataOrFallback(
|
||||
RuleDataUtils.getSourceDataLookup(ruleData),
|
||||
sourceMapping.sourceId
|
||||
)
|
||||
)
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||
};
|
||||
|
||||
getSubclassData = (): Array<ClassDefinitionContract> => {
|
||||
const { subclassData } = this.state;
|
||||
const { charClass } = this.props;
|
||||
|
||||
let data: Array<ClassDefinitionContract> = [...subclassData];
|
||||
|
||||
let existingSubclass = ClassUtils.getSubclass(charClass);
|
||||
if (
|
||||
existingSubclass !== null &&
|
||||
!data.some(
|
||||
(classDefinition) =>
|
||||
existingSubclass !== null &&
|
||||
classDefinition.id === existingSubclass.id
|
||||
)
|
||||
) {
|
||||
data.push(existingSubclass);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
getClassFeatureDescription = (): string => {
|
||||
const { feature, isStartingClass } = this.props;
|
||||
|
||||
@@ -493,33 +275,6 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
return description === null ? "" : description;
|
||||
};
|
||||
|
||||
renderChoices = (): React.ReactNode => {
|
||||
const { isActive, feature, charClass } = this.props;
|
||||
const { featData } = this.state;
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const choices = ClassFeatureUtils.getChoices(feature);
|
||||
|
||||
if (choices === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return choices.map((choice) => (
|
||||
<FeatureChoice
|
||||
choice={choice}
|
||||
charClass={charClass}
|
||||
feature={feature}
|
||||
featsData={featData}
|
||||
subclassData={this.getSubclassData()}
|
||||
onChoiceChange={this.handleChoiceChange}
|
||||
key={ChoiceUtils.getId(choice)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
getMetaItems = () => {
|
||||
const { feature, optionalClassFeatureLookup, definitionPool } = this.props;
|
||||
const choiceCount = this.getTotalChoiceCount();
|
||||
@@ -590,7 +345,7 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
feature,
|
||||
inventoryManager,
|
||||
} = this.props;
|
||||
const { hasSubclassChoice, hasFeatChoice } = this.state;
|
||||
const { collapsibleOpened } = this.state;
|
||||
|
||||
// If the class feature grants feats, render those instead of the class feature.
|
||||
if (feature.featLists.length > 0) {
|
||||
@@ -604,22 +359,11 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
}
|
||||
|
||||
const conClsNames: Array<string> = ["class-manager-feature-name"];
|
||||
let contentNode: React.ReactNode;
|
||||
|
||||
if (isActive) {
|
||||
if (this.getTotalTodoCount() > 0) {
|
||||
conClsNames.push("collapsible-todo");
|
||||
}
|
||||
|
||||
if (hasFeatChoice || hasSubclassChoice) {
|
||||
if (this.isDataLoaded()) {
|
||||
contentNode = this.renderChoices();
|
||||
} else {
|
||||
contentNode = <LoadingPlaceholder />;
|
||||
}
|
||||
} else {
|
||||
contentNode = this.renderChoices();
|
||||
}
|
||||
}
|
||||
const hasChoices = this.getTotalTodoCount() > 0;
|
||||
const accordionId = ClassFeatureUtils.getId(feature).toString();
|
||||
@@ -636,13 +380,20 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
<CollapsibleContent className="class-manager-feature-description">
|
||||
{this.getClassFeatureDescription()}
|
||||
</CollapsibleContent>
|
||||
{isActive && hasSubclassChoice && (
|
||||
{isActive && this.hasSubclassChoice() && (
|
||||
<div className="ct-character-tools__marketplace-callout">
|
||||
Looking for something not in the list below? Unlock all official
|
||||
options in the <Link href="/marketplace">Marketplace</Link>.
|
||||
</div>
|
||||
)}
|
||||
{contentNode}
|
||||
{isActive && (
|
||||
<FeatureChoices
|
||||
choices={ClassFeatureUtils.getChoices(feature)}
|
||||
charClass={charClass}
|
||||
onChoiceChange={this.handleChoiceChange}
|
||||
shouldFetch={collapsibleOpened}
|
||||
/>
|
||||
)}
|
||||
{isActive && (
|
||||
<InfusionChoiceManager
|
||||
infusionChoices={this.getAvailableInfusionChoices()}
|
||||
|
||||
-1
@@ -585,7 +585,6 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
handleChoiceChange = (
|
||||
choiceId: string,
|
||||
choiceType: number,
|
||||
choiceSubType: number | null,
|
||||
optionValue: any
|
||||
): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
+7
-150
@@ -1,11 +1,8 @@
|
||||
import axios, { Canceler } from "axios";
|
||||
import React from "react";
|
||||
|
||||
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
ApiAdapterPromise,
|
||||
ApiAdapterRequestConfig,
|
||||
ApiAdapterUtils,
|
||||
ApiResponse,
|
||||
CharacterPreferences,
|
||||
ChoiceData,
|
||||
@@ -13,10 +10,8 @@ import {
|
||||
Constants,
|
||||
CoreUtils,
|
||||
DefinitionPool,
|
||||
Feat,
|
||||
FeatDefinitionContract,
|
||||
FeatLookup,
|
||||
FeatUtils,
|
||||
HelperUtils,
|
||||
OptionalOriginLookup,
|
||||
OptionalOriginUtils,
|
||||
@@ -26,12 +21,9 @@ import {
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { FeatureChoice } from "~/components/FeatureChoice";
|
||||
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
|
||||
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
|
||||
import { AppLoggerUtils } from "../../../../../Shared/utils";
|
||||
|
||||
interface Props {
|
||||
speciesTrait: RacialTrait;
|
||||
choiceInfo: ChoiceData;
|
||||
@@ -52,10 +44,7 @@ interface Props {
|
||||
speciesName?: string | null;
|
||||
}
|
||||
interface State {
|
||||
featData: Array<Feat>;
|
||||
hasFeatChoice: boolean;
|
||||
collapsibleOpened: boolean;
|
||||
loadingStatus: DataLoadingStatusEnum;
|
||||
}
|
||||
export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
||||
Props,
|
||||
@@ -65,115 +54,10 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
featData: [],
|
||||
hasFeatChoice: false,
|
||||
collapsibleOpened: false,
|
||||
loadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
|
||||
};
|
||||
}
|
||||
|
||||
loadFeatsCanceler: null | Canceler = null;
|
||||
|
||||
componentDidMount(): void {
|
||||
this.setState(
|
||||
{
|
||||
hasFeatChoice: this.hasFeatChoice(this.props),
|
||||
},
|
||||
() => {
|
||||
this.conditionallyLoadFeatData();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
const { speciesTrait } = this.props;
|
||||
const { collapsibleOpened } = this.state;
|
||||
|
||||
if (speciesTrait !== prevProps.speciesTrait) {
|
||||
this.setState(
|
||||
{
|
||||
hasFeatChoice: this.hasFeatChoice(this.props),
|
||||
},
|
||||
() => {
|
||||
this.conditionallyLoadFeatData();
|
||||
}
|
||||
);
|
||||
}
|
||||
if (collapsibleOpened && !prevState.collapsibleOpened) {
|
||||
this.conditionallyLoadFeatData();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.loadFeatsCanceler !== null) {
|
||||
this.loadFeatsCanceler();
|
||||
}
|
||||
}
|
||||
|
||||
conditionallyLoadFeatData = (): void => {
|
||||
const { loadAvailableFeats } = this.props;
|
||||
const { hasFeatChoice, loadingStatus, collapsibleOpened } = this.state;
|
||||
|
||||
if (
|
||||
hasFeatChoice &&
|
||||
collapsibleOpened &&
|
||||
loadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
|
||||
) {
|
||||
this.setState({
|
||||
loadingStatus: DataLoadingStatusEnum.LOADING,
|
||||
});
|
||||
|
||||
loadAvailableFeats({
|
||||
cancelToken: new axios.CancelToken((c) => {
|
||||
this.loadFeatsCanceler = c;
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
let featData: Array<Feat> = [];
|
||||
|
||||
const data = ApiAdapterUtils.getResponseData(response);
|
||||
if (data !== null) {
|
||||
featData = data.map((definition) =>
|
||||
FeatUtils.simulateFeat(definition)
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
featData,
|
||||
loadingStatus: DataLoadingStatusEnum.LOADED,
|
||||
});
|
||||
this.loadFeatsCanceler = null;
|
||||
})
|
||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
||||
}
|
||||
};
|
||||
|
||||
getFeatData = (existingFeatId: number | null): Array<Feat> => {
|
||||
const { featData } = this.state;
|
||||
const { featLookup } = this.props;
|
||||
|
||||
let data: Array<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;
|
||||
};
|
||||
|
||||
getSummary = () => {
|
||||
const { speciesTrait, speciesName } = this.props;
|
||||
// Get base name for the species trait
|
||||
@@ -277,29 +161,9 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
||||
}
|
||||
};
|
||||
|
||||
renderChoices = (): React.ReactNode => {
|
||||
const { speciesTrait } = this.props;
|
||||
const { featData } = this.state;
|
||||
|
||||
const choices = RacialTraitUtils.getChoices(speciesTrait);
|
||||
|
||||
if (choices === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return choices.map((choice) => (
|
||||
<FeatureChoice
|
||||
choice={choice}
|
||||
featsData={featData}
|
||||
onChoiceChange={this.handleChoiceChange}
|
||||
key={ChoiceUtils.getId(choice)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { speciesTrait } = this.props;
|
||||
const { hasFeatChoice, loadingStatus } = this.state;
|
||||
const { collapsibleOpened } = this.state;
|
||||
|
||||
const description = RacialTraitUtils.getDescription(speciesTrait);
|
||||
const choices = RacialTraitUtils.getChoices(speciesTrait);
|
||||
@@ -314,17 +178,6 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
||||
conClsNames.push("collapsible-todo");
|
||||
}
|
||||
|
||||
let contentNode: React.ReactNode;
|
||||
if (hasFeatChoice) {
|
||||
if (loadingStatus === DataLoadingStatusEnum.LOADED) {
|
||||
contentNode = this.renderChoices();
|
||||
} else {
|
||||
contentNode = <LoadingPlaceholder />;
|
||||
}
|
||||
} else {
|
||||
contentNode = this.renderChoices();
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
id={accordionId}
|
||||
@@ -339,7 +192,11 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
||||
html={description ? description : ""}
|
||||
withoutTooltips
|
||||
/>
|
||||
{contentNode}
|
||||
<FeatureChoices
|
||||
choices={RacialTraitUtils.getChoices(speciesTrait)}
|
||||
onChoiceChange={this.handleChoiceChange}
|
||||
shouldFetch={collapsibleOpened}
|
||||
/>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
+9
-2
@@ -64,12 +64,19 @@ export default class ClassFeatureSnippet extends React.PureComponent<Props> {
|
||||
};
|
||||
|
||||
renderDescription = (): React.ReactNode => {
|
||||
const { feature, showDescription } = this.props;
|
||||
const { feature, showDescription, charClass } = this.props;
|
||||
|
||||
const isStartingClass = ClassUtils.isStartingClass(charClass);
|
||||
const multiClassDescription =
|
||||
ClassFeatureUtils.getMultiClassDescription(feature);
|
||||
|
||||
// If the class feature is accessible, show the description or multi-class description, otherwise show a "Check out the Marketplace" message.
|
||||
const description: string | null = AccessUtils.isAccessible(
|
||||
ClassFeatureUtils.getAccessType(feature)
|
||||
)
|
||||
? ClassFeatureUtils.getDescription(feature)
|
||||
? !isStartingClass && multiClassDescription
|
||||
? multiClassDescription
|
||||
: ClassFeatureUtils.getDescription(feature)
|
||||
: "Check out the Marketplace to unlock this Class Feature.";
|
||||
|
||||
return showDescription
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { RacialTraitAccessors } from "@dndbeyond/character-rules-engine/es/engine/RacialTrait";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
BaseFeat,
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
CharacterFeaturesManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
|
||||
import { DataOriginTypeEnum } from "~/constants";
|
||||
import { FeatureSnippet } from "~/subApps/sheet/components/FeatureSnippet";
|
||||
|
||||
@@ -83,7 +82,7 @@ export default class SpeciesTraitFeatureSnippet extends React.PureComponent<Prop
|
||||
|
||||
const feats = featuresManager.getDataOriginOnlyFeatsByPrimary(
|
||||
DataOriginTypeEnum.RACE,
|
||||
`${RacialTraitAccessors.getId(speciesTrait)}`
|
||||
`${RacialTraitUtils.getId(speciesTrait)}`
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -383,18 +383,38 @@ export class ItemDetail extends React.PureComponent<Props> {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{replacementWeaponStats.map((statId) => (
|
||||
<div className="ct-item-detail__class-customize-item" key={statId}>
|
||||
{replacementWeaponStats.map((replacementInfo) => (
|
||||
<div
|
||||
className="ct-item-detail__class-customize-item"
|
||||
key={`${replacementInfo.statId}-${replacementInfo.dataOrigin.primary.definition.id}`}
|
||||
>
|
||||
<Checkbox
|
||||
enabled={appliedWeaponReplacementStats.includes(statId)}
|
||||
enabled={appliedWeaponReplacementStats.includes(
|
||||
replacementInfo.statId
|
||||
)}
|
||||
onChange={(enabled) =>
|
||||
this.handleReplaceAbilityStatChange(enabled, statId)
|
||||
this.handleReplaceAbilityStatChange(
|
||||
enabled,
|
||||
replacementInfo.statId
|
||||
)
|
||||
}
|
||||
label={`Use ${RuleDataUtils.getStatNameById(
|
||||
statId,
|
||||
label={
|
||||
<>
|
||||
Use{" "}
|
||||
{RuleDataUtils.getStatNameById(
|
||||
replacementInfo.statId,
|
||||
ruleData,
|
||||
true
|
||||
)}`}
|
||||
)}{" "}
|
||||
<span className={styles.origin}>
|
||||
(<span className={styles.originLabel}>From</span>
|
||||
<span className={styles.originName}>
|
||||
{replacementInfo.dataOrigin.primary.definition.name}
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ import Collapsible from "~/tools/js/smartComponents/Collapsible";
|
||||
import { Select } from "~/tools/js/smartComponents/legacy";
|
||||
|
||||
// TODO this needs to not extend builder contract and options typing needs to be fixed
|
||||
// TODO scss to styles.module.css
|
||||
interface Props extends Omit<BuilderChoiceContract, "options"> {
|
||||
choice: Choice;
|
||||
description?: string;
|
||||
@@ -31,7 +33,6 @@ interface Props extends Omit<BuilderChoiceContract, "options"> {
|
||||
onChange?: (
|
||||
id: string,
|
||||
type: number,
|
||||
subType: number | null,
|
||||
value: any,
|
||||
parentChoiceId: string | null
|
||||
) => void;
|
||||
@@ -61,12 +62,17 @@ export const DetailChoice: FC<Props> = ({
|
||||
const { entityRestrictionData, ruleData } = useCharacterEngine();
|
||||
const { languages } = useRuleData();
|
||||
|
||||
const hasTodo =
|
||||
(ChoiceUtils.isInfinite(choice) && !ChoiceUtils.isOptionSelected(choice)) ||
|
||||
ChoiceUtils.isTodo(choice);
|
||||
|
||||
const handleChoiceChange = (value: string): void => {
|
||||
if (onChange && id !== null) {
|
||||
onChange(id, type, subType, value, parentChoiceId);
|
||||
onChange(id, type, value, parentChoiceId);
|
||||
}
|
||||
};
|
||||
|
||||
//Don't render the choice if hideWhenOnlyDefaultSelected is true and the choice is only default selected.
|
||||
if (
|
||||
hideWhenOnlyDefaultSelected &&
|
||||
ChoiceUtils.isOnlyDefaultSelected(choice)
|
||||
@@ -74,14 +80,6 @@ export const DetailChoice: FC<Props> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
let classNames: Array<string> = ["detail-choice", className];
|
||||
if (
|
||||
(ChoiceUtils.isInfinite(choice) && !ChoiceUtils.isOptionSelected(choice)) ||
|
||||
ChoiceUtils.isTodo(choice)
|
||||
) {
|
||||
classNames.push("detail-choice--todo");
|
||||
}
|
||||
|
||||
// Use the description prop or, if a chosenOption is found and a description wasn't passed in, use the chosenOption description instead.
|
||||
let choiceDescription = description;
|
||||
|
||||
@@ -94,10 +92,6 @@ export const DetailChoice: FC<Props> = ({
|
||||
choiceDescription = description || chosenOption["description"];
|
||||
}
|
||||
|
||||
if (parentChoiceId !== null) {
|
||||
classNames.push("detail-choice--child");
|
||||
}
|
||||
|
||||
let choiceNode: ReactNode;
|
||||
if (choiceDescription) {
|
||||
switch (type) {
|
||||
@@ -142,7 +136,14 @@ export const DetailChoice: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<div
|
||||
className={clsx([
|
||||
"detail-choice",
|
||||
hasTodo && "detail-choice--todo",
|
||||
parentChoiceId !== null && "detail-choice--child",
|
||||
className,
|
||||
])}
|
||||
>
|
||||
<Select
|
||||
className="detail-choice-input"
|
||||
options={ChoiceUtils.getSortedRenderOptions(
|
||||
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import {
|
||||
characterActions,
|
||||
ChoiceUtils,
|
||||
FeatUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
|
||||
import { DetailChoice } from "../DetailChoice";
|
||||
|
||||
//This component is a wrapper for DetailChoice that is specifically for Feats
|
||||
//given a featId, it will render the choices for that feat
|
||||
//used in FeatDetail, FeatPane and FeatureChoice (for feats that are given by Class Features, Species Traits and Backgrounds)
|
||||
|
||||
export interface Props {
|
||||
featId: number;
|
||||
}
|
||||
|
||||
export const DetailChoiceFeat: FC<Props> = ({ featId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { entityRestrictionData, choiceInfo, feats, ruleData } =
|
||||
useCharacterEngine();
|
||||
|
||||
const handleChoiceChange = (
|
||||
id: string | null,
|
||||
type: number,
|
||||
subType: number | null,
|
||||
value: any
|
||||
): void => {
|
||||
if (id !== null) {
|
||||
dispatch(characterActions.featChoiceSetRequest(featId, type, id, value));
|
||||
}
|
||||
};
|
||||
|
||||
const feat = feats.find((feat) => FeatUtils.getId(feat) === featId);
|
||||
|
||||
if (!feat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const choices = FeatUtils.getChoices(feat);
|
||||
if (choices.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ct-detail-choice-feat">
|
||||
{choices.map((choice) => {
|
||||
const id = ChoiceUtils.getId(choice);
|
||||
|
||||
const { description, options: featChoiceOptions } =
|
||||
ChoiceUtils.getSortedChoiceOptionsInfo(
|
||||
choice,
|
||||
ruleData,
|
||||
entityRestrictionData
|
||||
);
|
||||
|
||||
return (
|
||||
<DetailChoice
|
||||
{...choice}
|
||||
label={choice.label}
|
||||
choice={choice}
|
||||
key={id !== null ? id : ""}
|
||||
options={featChoiceOptions}
|
||||
onChange={handleChoiceChange}
|
||||
choiceInfo={choiceInfo}
|
||||
showBackgroundProficiencyOptions={true}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,267 +0,0 @@
|
||||
import React, { useContext } from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
Action,
|
||||
ActionUtils,
|
||||
BaseFeat,
|
||||
characterActions,
|
||||
CharacterFeaturesManager,
|
||||
CharacterTheme,
|
||||
DataOriginRefData,
|
||||
FeatManager,
|
||||
Race,
|
||||
RaceUtils,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
RuleData,
|
||||
rulesEngineSelectors,
|
||||
SnippetData,
|
||||
Spell,
|
||||
SpellUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
import {
|
||||
PaneComponentEnum,
|
||||
PaneIdentifiersRacialTrait,
|
||||
} from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { PaneInitFailureContent } from "../../../../../../subApps/sheet/components/Sidebar/components/PaneInitFailureContent";
|
||||
import { SpeciesTraitFeatureSnippet } from "../../../../CharacterSheet/components/FeatureSnippet";
|
||||
import { CharacterFeaturesManagerContext } from "../../../managers/CharacterFeaturesManagerContext";
|
||||
import { appEnvSelectors } from "../../../selectors";
|
||||
import { SharedAppState } from "../../../stores/typings";
|
||||
import { PaneIdentifierUtils } from "../../../utils";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
species: Race | null;
|
||||
identifiers: PaneIdentifiersRacialTrait | null;
|
||||
feats: Array<BaseFeat>;
|
||||
snippetData: SnippetData;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
isReadonly: boolean;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
characterFeaturesManager: CharacterFeaturesManager;
|
||||
paneContext: PaneInfo;
|
||||
}
|
||||
interface State {
|
||||
speciesTrait: RacialTrait | null;
|
||||
}
|
||||
class SpeciesTraitPane extends React.PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.generateStateData(props);
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>
|
||||
): void {
|
||||
const { species, identifiers } = this.props;
|
||||
|
||||
if (
|
||||
species !== prevProps.species ||
|
||||
identifiers !== prevProps.identifiers
|
||||
) {
|
||||
this.setState(this.generateStateData(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
generateStateData = (props: Props): State => {
|
||||
const { species, identifiers } = props;
|
||||
|
||||
let foundSpeciesTrait: RacialTrait | null | undefined = null;
|
||||
if (species !== null && identifiers !== null) {
|
||||
foundSpeciesTrait = RaceUtils.getVisibleRacialTraits(species).find(
|
||||
(speciesTrait) =>
|
||||
identifiers.id === RacialTraitUtils.getId(speciesTrait)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
speciesTrait: foundSpeciesTrait ? foundSpeciesTrait : null,
|
||||
};
|
||||
};
|
||||
|
||||
handleActionUseSet = (action: Action, uses: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const id = ActionUtils.getId(action);
|
||||
const entityTypeId = ActionUtils.getEntityTypeId(action);
|
||||
if (id !== null && entityTypeId !== null) {
|
||||
dispatch(
|
||||
characterActions.actionUseSet(
|
||||
id,
|
||||
entityTypeId,
|
||||
uses,
|
||||
ActionUtils.getDataOriginType(action)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellUseSet = (spell: Spell, uses: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
const mappingEntityTypeId = SpellUtils.getMappingEntityTypeId(spell);
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
dispatch(
|
||||
characterActions.spellUseSet(
|
||||
mappingId,
|
||||
mappingEntityTypeId,
|
||||
uses,
|
||||
SpellUtils.getDataOriginType(spell)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellDetailShow = (spell: Spell): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryPush },
|
||||
} = this.props;
|
||||
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
if (mappingId !== null) {
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.CHARACTER_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpeciesTraitShow = (speciesTrait: RacialTrait): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryStart },
|
||||
} = this.props;
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.SPECIES_TRAIT_DETAIL,
|
||||
PaneIdentifierUtils.generateRacialTrait(
|
||||
RacialTraitUtils.getId(speciesTrait)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
handleActionShow = (action: Action): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryPush },
|
||||
} = this.props;
|
||||
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.ACTION,
|
||||
PaneIdentifierUtils.generateAction(mappingId, mappingEntityTypeId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleFeatShow = (feat: FeatManager): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryPush },
|
||||
} = this.props;
|
||||
paneHistoryPush(
|
||||
PaneComponentEnum.FEAT_DETAIL,
|
||||
PaneIdentifierUtils.generateFeat(feat.getId())
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { speciesTrait } = this.state;
|
||||
const {
|
||||
species,
|
||||
feats,
|
||||
abilityLookup,
|
||||
snippetData,
|
||||
ruleData,
|
||||
dataOriginRefData,
|
||||
isReadonly,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
characterFeaturesManager,
|
||||
} = this.props;
|
||||
|
||||
if (species === null || speciesTrait === null) {
|
||||
return <PaneInitFailureContent />;
|
||||
}
|
||||
|
||||
let fullName = RaceUtils.getFullName(species);
|
||||
let portrait = RaceUtils.getPortraitAvatarUrl(species);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ct-racial-trait-pane"
|
||||
key={RacialTraitUtils.getId(speciesTrait)}
|
||||
>
|
||||
<Header
|
||||
parent={fullName}
|
||||
preview={portrait && <Preview imageUrl={portrait} />}
|
||||
>
|
||||
{RacialTraitUtils.getName(speciesTrait)}
|
||||
</Header>
|
||||
<SpeciesTraitFeatureSnippet
|
||||
speciesTrait={speciesTrait}
|
||||
onActionUseSet={this.handleActionUseSet}
|
||||
onActionClick={this.handleActionShow}
|
||||
onSpellUseSet={this.handleSpellUseSet}
|
||||
onSpellClick={this.handleSpellDetailShow}
|
||||
onFeatureClick={this.handleSpeciesTraitShow}
|
||||
showHeader={false}
|
||||
showDescription={true}
|
||||
feats={feats}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
isReadonly={isReadonly}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
onFeatClick={this.handleFeatShow}
|
||||
featuresManager={characterFeaturesManager}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SharedAppState) {
|
||||
return {
|
||||
species: rulesEngineSelectors.getRace(state),
|
||||
feats: rulesEngineSelectors.getBaseFeats(state),
|
||||
snippetData: rulesEngineSelectors.getSnippetData(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
abilityLookup: rulesEngineSelectors.getAbilityLookup(state),
|
||||
dataOriginRefData: rulesEngineSelectors.getDataOriginRefData(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
};
|
||||
}
|
||||
|
||||
const SpeciesTraitPaneWrapper = (props) => {
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
|
||||
const { pane } = useSidebar();
|
||||
|
||||
return (
|
||||
<SpeciesTraitPane
|
||||
characterFeaturesManager={characterFeaturesManager}
|
||||
paneContext={pane}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SpeciesTraitPaneWrapper);
|
||||
@@ -1,4 +0,0 @@
|
||||
import SpeciesTraitPane from "./SpeciesTraitPane";
|
||||
|
||||
export default SpeciesTraitPane;
|
||||
export { SpeciesTraitPane };
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
label?: string;
|
||||
label?: React.ReactNode;
|
||||
preventDefault: boolean;
|
||||
stopPropagation: boolean;
|
||||
isInteractive?: boolean;
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// extracted by mini-css-extract-plugin
|
||||
export default {"choice":"styles_choice__qE4aQ","heading":"styles_heading__qlmfe"};
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// extracted by mini-css-extract-plugin
|
||||
export default {"choice":"styles_choice__eGzfJ","heading":"styles_heading__IAIav"};
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// extracted by mini-css-extract-plugin
|
||||
export default {"action":"styles_action__Q1OAQ","label":"styles_label__-pkXo"};
|
||||
export default {"action":"styles_action__Q1OAQ","label":"styles_label__-pkXo","origin":"styles_origin__9Ke5E","originLabel":"styles_originLabel__Zwjnz","originName":"styles_originName__F2eRI"};
|
||||
Reference in New Issue
Block a user