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 { orderBy } from 'lodash';
|
||||||
import { TypeScriptUtils } from "../../utils";
|
import { TypeScriptUtils } from "../../utils";
|
||||||
|
import { ChoiceAccessors } from '.';
|
||||||
import { ClassAccessors } from '../Class';
|
import { ClassAccessors } from '../Class';
|
||||||
import { AbilityStatEnum, BuilderChoiceSubtypeEnum } from '../Core';
|
import { AbilityStatEnum, BuilderChoiceSubtypeEnum, BuilderChoiceTypeEnum, } from '../Core';
|
||||||
import { DataOriginTypeEnum } from '../DataOrigin';
|
import { DataOriginTypeEnum } from '../DataOrigin';
|
||||||
import { EntityUtils } from '../Entity';
|
import { EntityUtils } from '../Entity';
|
||||||
|
import { FeatAccessors, FeatUtils } from '../Feat';
|
||||||
import { HelperUtils } from '../Helper';
|
import { HelperUtils } from '../Helper';
|
||||||
import { ModifierAccessors } from '../Modifier';
|
import { ModifierAccessors } from '../Modifier';
|
||||||
|
import { PrerequisiteValidators } from '../Prerequisite';
|
||||||
|
import { RuleDataAccessors } from '../RuleData';
|
||||||
import { SourceUtils } from '../Source';
|
import { SourceUtils } from '../Source';
|
||||||
import { SpellUtils } from '../Spell';
|
import { SpellUtils } from '../Spell';
|
||||||
import { getDefaultSubtypes, getOptions, getOptionValue, isOptional } from './accessors';
|
import { getDefaultSubtypes, getOptions, getOptionValue, isOptional } from './accessors';
|
||||||
@@ -33,8 +37,9 @@ export function isTodo(choice) {
|
|||||||
return !isOptional(choice) && getOptionValue(choice) === null;
|
return !isOptional(choice) && getOptionValue(choice) === null;
|
||||||
}
|
}
|
||||||
// Given a Choice, return options for a select element that are potentially grouped by sourceCategory
|
// Given a Choice, return options for a select element that are potentially grouped by sourceCategory
|
||||||
//used in DetailChoiceFeat.tsx for feat choices
|
//used in FeatPane and FeatDetail for feat choices
|
||||||
export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionData) {
|
// and in getSortedChoiceOptionsForFeaturesInfo for feature choices that are feats and have subchoices
|
||||||
|
export function getSortedChoiceOptionsForFeatsInfo(featId, choice, ruleData, entityRestrictionData) {
|
||||||
var _a;
|
var _a;
|
||||||
const options = getOptions(choice);
|
const options = getOptions(choice);
|
||||||
let description;
|
let description;
|
||||||
@@ -56,10 +61,126 @@ export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionDa
|
|||||||
featChoiceOptions = orderBy(featChoiceOptions, 'label');
|
featChoiceOptions = orderBy(featChoiceOptions, 'label');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
featId,
|
||||||
|
choice,
|
||||||
options: featChoiceOptions,
|
options: featChoiceOptions,
|
||||||
description,
|
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
|
// 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) {
|
export function getRemainingOptions(options, modifiers, defaultSubtypes, showBackgroundProficiencyOptions, optionValue) {
|
||||||
let remainingSubtypes = [];
|
let remainingSubtypes = [];
|
||||||
@@ -209,3 +330,21 @@ export function getSortedRenderOptions(options, classId, subType, entityRestrict
|
|||||||
? orderBy(renderOptions, 'sortOrder')
|
? orderBy(renderOptions, 'sortOrder')
|
||||||
: orderBy(renderOptions, 'label');
|
: 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) => {
|
classModifiers.forEach((modifier) => {
|
||||||
if (ModifierValidators.isEnablesAbilityStatModifier(modifier)) {
|
if (ModifierValidators.isEnablesAbilityStatModifier(modifier)) {
|
||||||
const statId = ModifierAccessors.getEntityId(modifier) || 0;
|
const statId = ModifierAccessors.getEntityId(modifier) || 0;
|
||||||
if (statId && !replacementWeaponAbilityStats.includes(statId)) {
|
const dataOrigin = ModifierAccessors.getDataOrigin(modifier);
|
||||||
replacementWeaponAbilityStats.push(statId);
|
if (statId) {
|
||||||
|
replacementWeaponAbilityStats.push({ statId, dataOrigin });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -343,6 +343,11 @@ export function* handleAdhocFeatCreate(action) {
|
|||||||
* @param action
|
* @param action
|
||||||
*/
|
*/
|
||||||
export function* handleAdhocFeatRemove(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);
|
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.deleteCharacterFeatAdHoc, action.payload);
|
||||||
yield call(handleDataUpdates, data);
|
yield call(handleDataUpdates, data);
|
||||||
}
|
}
|
||||||
@@ -457,7 +462,13 @@ export function* handleRaceChoose(action) {
|
|||||||
const { race } = action.payload;
|
const { race } = action.payload;
|
||||||
const existingRace = yield select(rulesEngineSelectors.getRace);
|
const existingRace = yield select(rulesEngineSelectors.getRace);
|
||||||
if (existingRace !== null) {
|
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 existingOptionalOrigins = yield select(rulesEngineSelectors.getOptionalOrigins);
|
||||||
const optionalOriginsIdsToRemove = existingOptionalOrigins.map(OptionalOriginAccessors.getRacialTraitId);
|
const optionalOriginsIdsToRemove = existingOptionalOrigins.map(OptionalOriginAccessors.getRacialTraitId);
|
||||||
yield call(apiRemoveOptionalOriginsCollection, {
|
yield call(apiRemoveOptionalOriginsCollection, {
|
||||||
@@ -1309,15 +1320,21 @@ export function* handleCurrencyTransactionSet(action) {
|
|||||||
*/
|
*/
|
||||||
export function* handleRacialTraitChoiceSetRequest(action) {
|
export function* handleRacialTraitChoiceSetRequest(action) {
|
||||||
const { racialTraitId, choiceId } = action.payload;
|
const { racialTraitId, choiceId } = action.payload;
|
||||||
yield call(apiRacialTraitChoiceSet, action);
|
|
||||||
const race = yield select(rulesEngineSelectors.getRace);
|
const race = yield select(rulesEngineSelectors.getRace);
|
||||||
if (race) {
|
const racialTrait = race
|
||||||
const racialTrait = RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId);
|
? RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId)
|
||||||
if (racialTrait) {
|
: null;
|
||||||
const racialTraitChoice = RacialTraitAccessors.getChoices(racialTrait).find((choice) => ChoiceAccessors.getId(choice) === choiceId);
|
// Remove any spells that are associated with the feat from the racial trait choices
|
||||||
if (racialTraitChoice) {
|
const feats = racialTrait ? RacialTraitAccessors.getFeats(racialTrait) : [];
|
||||||
yield call(autoUpdateChoices, racialTraitChoice);
|
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;
|
const { classId, classFeatureId, choiceId, choiceType, optionValue } = action.payload;
|
||||||
let oldClasses = yield select(rulesEngineSelectors.getClasses);
|
let oldClasses = yield select(rulesEngineSelectors.getClasses);
|
||||||
let oldCharClass = oldClasses.find((charClass) => ClassAccessors.getActiveId(charClass) === classId);
|
let oldCharClass = oldClasses.find((charClass) => ClassAccessors.getActiveId(charClass) === classId);
|
||||||
|
const spellListIdsToRemove = [];
|
||||||
if (choiceType === BuilderChoiceTypeEnum.SUB_CLASS_OPTION && oldCharClass) {
|
if (choiceType === BuilderChoiceTypeEnum.SUB_CLASS_OPTION && oldCharClass) {
|
||||||
const subclass = ClassAccessors.getSubclass(oldCharClass);
|
const subclass = ClassAccessors.getSubclass(oldCharClass);
|
||||||
if (subclass !== null) {
|
if (subclass !== null) {
|
||||||
@@ -1350,9 +1368,19 @@ export function* handleClassFeatureChoiceSetRequest(action) {
|
|||||||
.filter((classFeature) => ClassFeatureAccessors.getClassId(classFeature) === subclass.id)
|
.filter((classFeature) => ClassFeatureAccessors.getClassId(classFeature) === subclass.id)
|
||||||
.map(ClassFeatureAccessors.getSpellListIds)
|
.map(ClassFeatureAccessors.getSpellListIds)
|
||||||
.reduce((acc, ids) => acc.concat(ids), []);
|
.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);
|
yield call(apiClassFeatureChoiceSet, action);
|
||||||
const classes = yield select(rulesEngineSelectors.getClasses);
|
const classes = yield select(rulesEngineSelectors.getClasses);
|
||||||
const charClass = classes.find((charClass) => !!oldCharClass && ClassAccessors.getId(charClass) === ClassAccessors.getId(oldCharClass));
|
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 oldClassId = ClassAccessors.getId(oldCharClass);
|
||||||
const removedClassFeatures = ClassAccessors.getActiveClassFeatures(oldCharClass).filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level);
|
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 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)
|
const removedSpellListIds = ClassAccessors.getActiveClassFeatures(oldCharClass)
|
||||||
.filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level ||
|
.filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level ||
|
||||||
(isRemovingSubclass && ClassFeatureAccessors.getClassId(classFeature) !== oldClassId))
|
(isRemovingSubclass && ClassFeatureAccessors.getClassId(classFeature) !== oldClassId))
|
||||||
.map(ClassFeatureAccessors.getSpellListIds)
|
.map(ClassFeatureAccessors.getSpellListIds)
|
||||||
.reduce((acc, spellListIds) => acc.concat(spellListIds), []);
|
.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);
|
yield call(apiClassLevelSet, action);
|
||||||
@@ -1582,7 +1619,21 @@ function* updateBackgroundResult(action, data) {
|
|||||||
export function* handleBackgroundSetRequest(action) {
|
export function* handleBackgroundSetRequest(action) {
|
||||||
const existingBackground = yield select(rulesEngineSelectors.getBackgroundInfo);
|
const existingBackground = yield select(rulesEngineSelectors.getBackgroundInfo);
|
||||||
if (existingBackground !== null) {
|
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));
|
yield put(callCommitAction(characterActions.backgroundHasCustomSet, false));
|
||||||
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.putCharacterBackground, action.payload);
|
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.putCharacterBackground, action.payload);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CreaturePane } from "~/subApps/sheet/components/Sidebar/panes/CreaturePane";
|
import { CreaturePane } from "~/subApps/sheet/components/Sidebar/panes/CreaturePane";
|
||||||
import { GameLogPane } from "~/subApps/sheet/components/Sidebar/panes/GameLogPane";
|
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 BlessingPane from "~/tools/js/Shared/containers/panes/BlessingPane";
|
||||||
import CharacterSpellPane from "~/tools/js/Shared/containers/panes/CharacterSpellPane";
|
import CharacterSpellPane from "~/tools/js/Shared/containers/panes/CharacterSpellPane";
|
||||||
import ClassSpellPane from "~/tools/js/Shared/containers/panes/ClassSpellPane";
|
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 ShortRestPane from "~/tools/js/Shared/containers/panes/ShortRestPane";
|
||||||
import SkillPane from "~/tools/js/Shared/containers/panes/SkillPane";
|
import SkillPane from "~/tools/js/Shared/containers/panes/SkillPane";
|
||||||
import SkillsPane from "~/tools/js/Shared/containers/panes/SkillsPane";
|
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 SpeedManagePane from "~/tools/js/Shared/containers/panes/SpeedManagePane";
|
||||||
import SpellManagePane from "~/tools/js/Shared/containers/panes/SpellManagePane";
|
import SpellManagePane from "~/tools/js/Shared/containers/panes/SpellManagePane";
|
||||||
import StartingEquipmentPane from "~/tools/js/Shared/containers/panes/StartingEquipmentPane";
|
import StartingEquipmentPane from "~/tools/js/Shared/containers/panes/StartingEquipmentPane";
|
||||||
|
|||||||
+13
-174
@@ -1,45 +1,27 @@
|
|||||||
import {
|
import { FC, HTMLAttributes, useContext } from "react";
|
||||||
FC,
|
|
||||||
HTMLAttributes,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import {
|
import { FeatManager } from "@dndbeyond/character-rules-engine";
|
||||||
ApiAdapterUtils,
|
|
||||||
FeatManager,
|
|
||||||
} from "@dndbeyond/character-rules-engine";
|
|
||||||
|
|
||||||
import { FeatureChoice } from "~/components/FeatureChoice";
|
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||||
import { BuilderChoiceTypeEnum } from "~/constants";
|
|
||||||
import { useSidebar } from "~/contexts/Sidebar";
|
import { useSidebar } from "~/contexts/Sidebar";
|
||||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
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 { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||||
import { apiCreatorSelectors } from "~/tools/js/Shared/selectors";
|
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||||
import { AppLoggerUtils, PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||||
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
|
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
CharClass,
|
CharClass,
|
||||||
ClassDefinitionContract,
|
|
||||||
ClassFeature,
|
ClassFeature,
|
||||||
Feat,
|
|
||||||
InfusionChoice,
|
InfusionChoice,
|
||||||
Spell,
|
Spell,
|
||||||
} from "~/types";
|
} 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 { PaneInitFailureContent } from "../../components/PaneInitFailureContent";
|
||||||
import { PaneComponentEnum, PaneIdentifiersClassFeature } from "../../types";
|
import { PaneComponentEnum, PaneIdentifiersClassFeature } from "../../types";
|
||||||
import styles from "./styles.module.css";
|
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
identifiers: PaneIdentifiersClassFeature | null;
|
identifiers: PaneIdentifiersClassFeature | null;
|
||||||
@@ -61,8 +43,6 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
|||||||
proficiencyBonus,
|
proficiencyBonus,
|
||||||
originRef: dataOriginRefData,
|
originRef: dataOriginRefData,
|
||||||
characterTheme: theme,
|
characterTheme: theme,
|
||||||
choiceUtils,
|
|
||||||
featUtils,
|
|
||||||
helperUtils,
|
helperUtils,
|
||||||
} = useCharacterEngine();
|
} = useCharacterEngine();
|
||||||
|
|
||||||
@@ -76,23 +56,6 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
|||||||
|
|
||||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
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
|
const charClass = identifiers?.classMappingId
|
||||||
? classes.find(
|
? classes.find(
|
||||||
(charClass) =>
|
(charClass) =>
|
||||||
@@ -105,98 +68,6 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
|||||||
(feature) => identifiers?.id === classFeatureUtils.getId(feature)
|
(feature) => identifiers?.id === classFeatureUtils.getId(feature)
|
||||||
) ?? null;
|
) ?? 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 handleInfusionChoiceShow = (infusionChoice: InfusionChoice): void => {
|
||||||
const choiceKey = infusionChoiceUtils.getKey(infusionChoice);
|
const choiceKey = infusionChoiceUtils.getKey(infusionChoice);
|
||||||
if (choiceKey !== null) {
|
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) {
|
if (!charClass || !classFeature) {
|
||||||
return <PaneInitFailureContent />;
|
return <PaneInitFailureContent />;
|
||||||
}
|
}
|
||||||
@@ -356,30 +212,13 @@ export const ClassFeaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
|||||||
onFeatClick={handleFeatShow}
|
onFeatClick={handleFeatShow}
|
||||||
featuresManager={characterFeaturesManager}
|
featuresManager={characterFeaturesManager}
|
||||||
/>
|
/>
|
||||||
{!isReadonly && featureChoices.length > 0 && (
|
<FeatureChoices
|
||||||
<>
|
choices={classFeatureUtils.getChoices(classFeature)}
|
||||||
<Heading className={styles.heading}>{`Option${
|
charClass={charClass}
|
||||||
featureChoices.length > 1 ? "s" : ""
|
onChoiceChange={handleChoiceChange}
|
||||||
}`}</Heading>
|
collapseDescription={true}
|
||||||
{isDataLoaded() ? (
|
showHeading={true}
|
||||||
featureChoices.map((choice) => (
|
/>
|
||||||
<FeatureChoice
|
|
||||||
charClass={charClass}
|
|
||||||
choice={choice}
|
|
||||||
feature={classFeature}
|
|
||||||
featsData={featsData}
|
|
||||||
subclassData={subclassData}
|
|
||||||
className={styles.choice}
|
|
||||||
key={choiceUtils.getId(choice)}
|
|
||||||
onChoiceChange={handleChoiceChange}
|
|
||||||
collapseDescription={true}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<LoadingPlaceholder />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import { FC, HTMLAttributes, useContext } from "react";
|
import { FC, HTMLAttributes, useContext } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import { Accordion } from "~/components/Accordion";
|
import { ChoiceUtils } from "@dndbeyond/character-rules-engine";
|
||||||
|
|
||||||
import { Button } from "~/components/Button";
|
import { Button } from "~/components/Button";
|
||||||
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
|
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
|
||||||
import { HtmlContent } from "~/components/HtmlContent";
|
|
||||||
import { useSidebar } from "~/contexts/Sidebar";
|
import { useSidebar } from "~/contexts/Sidebar";
|
||||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||||
import { FeatFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet";
|
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 { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||||
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
|
||||||
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
|
||||||
import { DdbBadgeSvg } from "~/tools/js/smartComponents/Svg";
|
|
||||||
import { Action, Spell } from "~/types";
|
import { Action, Spell } from "~/types";
|
||||||
|
|
||||||
import { Header } from "../../components/Header";
|
import { Header } from "../../components/Header";
|
||||||
@@ -38,6 +37,8 @@ export const FeatPane: FC<Props> = ({ identifiers, ...props }) => {
|
|||||||
originRef: dataOriginRefData,
|
originRef: dataOriginRefData,
|
||||||
characterTheme: theme,
|
characterTheme: theme,
|
||||||
entityUtils,
|
entityUtils,
|
||||||
|
entityRestrictionData,
|
||||||
|
choiceInfo,
|
||||||
} = useCharacterEngine();
|
} = useCharacterEngine();
|
||||||
|
|
||||||
const { characterFeaturesManager } = useContext(
|
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) {
|
if (feat === null) {
|
||||||
return <PaneInitFailureContent />;
|
return <PaneInitFailureContent />;
|
||||||
}
|
}
|
||||||
@@ -163,7 +176,31 @@ export const FeatPane: FC<Props> = ({ identifiers, ...props }) => {
|
|||||||
/>
|
/>
|
||||||
{!isReadonly && feat.getChoices().length > 0 && (
|
{!isReadonly && feat.getChoices().length > 0 && (
|
||||||
<div className={styles.choices}>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!feat.isHiddenFeat() &&
|
{!feat.isHiddenFeat() &&
|
||||||
|
|||||||
+52
-3
@@ -1,9 +1,15 @@
|
|||||||
import { FC, HTMLAttributes } from "react";
|
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 { 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";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
@@ -12,9 +18,28 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FeatDetail: FC<Props> = ({ featManager, className, ...props }) => {
|
export const FeatDetail: FC<Props> = ({ featManager, className, ...props }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { ruleData, entityRestrictionData, choiceInfo } = useCharacterEngine();
|
||||||
|
|
||||||
const prerequisiteDescription = featManager.getPrerequisiteDescription();
|
const prerequisiteDescription = featManager.getPrerequisiteDescription();
|
||||||
const featDescription = featManager.getDescription();
|
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 (
|
return (
|
||||||
<div className={className} {...props}>
|
<div className={className} {...props}>
|
||||||
{prerequisiteDescription && (
|
{prerequisiteDescription && (
|
||||||
@@ -27,7 +52,31 @@ export const FeatDetail: FC<Props> = ({ featManager, className, ...props }) => {
|
|||||||
)}
|
)}
|
||||||
{featManager.getChoices().length > 0 && (
|
{featManager.getChoices().length > 0 && (
|
||||||
<div className={styles.choices}>
|
<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>
|
||||||
)}
|
)}
|
||||||
</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 React from "react";
|
||||||
|
|
||||||
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
|
|
||||||
import {
|
import {
|
||||||
ApiAdapterPromise,
|
ApiAdapterPromise,
|
||||||
ApiAdapterRequestConfig,
|
ApiAdapterRequestConfig,
|
||||||
ApiAdapterUtils,
|
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
CharacterPreferences,
|
CharacterPreferences,
|
||||||
CharClass,
|
CharClass,
|
||||||
@@ -18,10 +15,8 @@ import {
|
|||||||
Constants,
|
Constants,
|
||||||
DefinitionPool,
|
DefinitionPool,
|
||||||
EntitledEntity,
|
EntitledEntity,
|
||||||
Feat,
|
|
||||||
FeatDefinitionContract,
|
FeatDefinitionContract,
|
||||||
FeatLookup,
|
FeatLookup,
|
||||||
FeatUtils,
|
|
||||||
FormatUtils,
|
FormatUtils,
|
||||||
HelperUtils,
|
HelperUtils,
|
||||||
InfusionChoice,
|
InfusionChoice,
|
||||||
@@ -35,20 +30,16 @@ import {
|
|||||||
OptionalClassFeatureUtils,
|
OptionalClassFeatureUtils,
|
||||||
PrerequisiteData,
|
PrerequisiteData,
|
||||||
RuleData,
|
RuleData,
|
||||||
RuleDataUtils,
|
|
||||||
SourceData,
|
|
||||||
TypeValueLookup,
|
TypeValueLookup,
|
||||||
} from "@dndbeyond/character-rules-engine/es";
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
import { Accordion } from "~/components/Accordion";
|
import { Accordion } from "~/components/Accordion";
|
||||||
import { CollapsibleContent } from "~/components/CollapsibleContent";
|
import { CollapsibleContent } from "~/components/CollapsibleContent";
|
||||||
import { FeatureChoice } from "~/components/FeatureChoice";
|
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||||
import { Link } from "~/components/Link";
|
import { Link } from "~/components/Link";
|
||||||
import { GrantedFeat } from "~/tools/js/CharacterBuilder/components/GrantedFeat/GrantedFeat";
|
import { GrantedFeat } from "~/tools/js/CharacterBuilder/components/GrantedFeat/GrantedFeat";
|
||||||
|
|
||||||
import InfusionChoiceManager from "../../../../../Shared/components/InfusionChoiceManager";
|
import InfusionChoiceManager from "../../../../../Shared/components/InfusionChoiceManager";
|
||||||
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
|
|
||||||
import { AppLoggerUtils, TypeScriptUtils } from "../../../../../Shared/utils";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -122,13 +113,7 @@ interface Props {
|
|||||||
inventoryManager: InventoryManager;
|
inventoryManager: InventoryManager;
|
||||||
}
|
}
|
||||||
interface State {
|
interface State {
|
||||||
featData: Array<Feat>;
|
|
||||||
subclassData: Array<ClassDefinitionContract>;
|
|
||||||
hasFeatChoice: boolean;
|
|
||||||
hasSubclassChoice: boolean;
|
|
||||||
collapsibleOpened: boolean;
|
collapsibleOpened: boolean;
|
||||||
featLoadingStatus: DataLoadingStatusEnum;
|
|
||||||
subclassLoadingStatus: DataLoadingStatusEnum;
|
|
||||||
}
|
}
|
||||||
export default class ClassManagerFeature extends React.PureComponent<
|
export default class ClassManagerFeature extends React.PureComponent<
|
||||||
Props,
|
Props,
|
||||||
@@ -139,157 +124,16 @@ export default class ClassManagerFeature extends React.PureComponent<
|
|||||||
featLookup: {},
|
featLookup: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
loadFeatsCanceler: null | Canceler = null;
|
|
||||||
loadSubclassesCanceler: null | Canceler = null;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
featData: [],
|
|
||||||
subclassData: [],
|
|
||||||
hasFeatChoice: false,
|
|
||||||
hasSubclassChoice: false,
|
|
||||||
collapsibleOpened: false,
|
collapsibleOpened: false,
|
||||||
featLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
|
|
||||||
subclassLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
hasSubclassChoice = (): boolean => {
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
hasFeatChoice: this.hasFeatChoice(this.props),
|
|
||||||
hasSubclassChoice: this.hasSubclassChoice(this.props),
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.conditionallyLoadFeatData();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(
|
|
||||||
prevProps: Readonly<Props>,
|
|
||||||
prevState: Readonly<State>,
|
|
||||||
snapshot?: any
|
|
||||||
): void {
|
|
||||||
const { feature } = this.props;
|
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);
|
const choices = ClassFeatureUtils.getChoices(feature);
|
||||||
return choices.some(
|
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 => {
|
handleCollapsibleOpened = (id: string, state: boolean): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
collapsibleOpened: state,
|
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 => {
|
getClassFeatureDescription = (): string => {
|
||||||
const { feature, isStartingClass } = this.props;
|
const { feature, isStartingClass } = this.props;
|
||||||
|
|
||||||
@@ -493,33 +275,6 @@ export default class ClassManagerFeature extends React.PureComponent<
|
|||||||
return description === null ? "" : description;
|
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 = () => {
|
getMetaItems = () => {
|
||||||
const { feature, optionalClassFeatureLookup, definitionPool } = this.props;
|
const { feature, optionalClassFeatureLookup, definitionPool } = this.props;
|
||||||
const choiceCount = this.getTotalChoiceCount();
|
const choiceCount = this.getTotalChoiceCount();
|
||||||
@@ -590,7 +345,7 @@ export default class ClassManagerFeature extends React.PureComponent<
|
|||||||
feature,
|
feature,
|
||||||
inventoryManager,
|
inventoryManager,
|
||||||
} = this.props;
|
} = 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 the class feature grants feats, render those instead of the class feature.
|
||||||
if (feature.featLists.length > 0) {
|
if (feature.featLists.length > 0) {
|
||||||
@@ -604,22 +359,11 @@ export default class ClassManagerFeature extends React.PureComponent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const conClsNames: Array<string> = ["class-manager-feature-name"];
|
const conClsNames: Array<string> = ["class-manager-feature-name"];
|
||||||
let contentNode: React.ReactNode;
|
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
if (this.getTotalTodoCount() > 0) {
|
if (this.getTotalTodoCount() > 0) {
|
||||||
conClsNames.push("collapsible-todo");
|
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 hasChoices = this.getTotalTodoCount() > 0;
|
||||||
const accordionId = ClassFeatureUtils.getId(feature).toString();
|
const accordionId = ClassFeatureUtils.getId(feature).toString();
|
||||||
@@ -636,13 +380,20 @@ export default class ClassManagerFeature extends React.PureComponent<
|
|||||||
<CollapsibleContent className="class-manager-feature-description">
|
<CollapsibleContent className="class-manager-feature-description">
|
||||||
{this.getClassFeatureDescription()}
|
{this.getClassFeatureDescription()}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
{isActive && hasSubclassChoice && (
|
{isActive && this.hasSubclassChoice() && (
|
||||||
<div className="ct-character-tools__marketplace-callout">
|
<div className="ct-character-tools__marketplace-callout">
|
||||||
Looking for something not in the list below? Unlock all official
|
Looking for something not in the list below? Unlock all official
|
||||||
options in the <Link href="/marketplace">Marketplace</Link>.
|
options in the <Link href="/marketplace">Marketplace</Link>.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{contentNode}
|
{isActive && (
|
||||||
|
<FeatureChoices
|
||||||
|
choices={ClassFeatureUtils.getChoices(feature)}
|
||||||
|
charClass={charClass}
|
||||||
|
onChoiceChange={this.handleChoiceChange}
|
||||||
|
shouldFetch={collapsibleOpened}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<InfusionChoiceManager
|
<InfusionChoiceManager
|
||||||
infusionChoices={this.getAvailableInfusionChoices()}
|
infusionChoices={this.getAvailableInfusionChoices()}
|
||||||
|
|||||||
-1
@@ -585,7 +585,6 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
|||||||
handleChoiceChange = (
|
handleChoiceChange = (
|
||||||
choiceId: string,
|
choiceId: string,
|
||||||
choiceType: number,
|
choiceType: number,
|
||||||
choiceSubType: number | null,
|
|
||||||
optionValue: any
|
optionValue: any
|
||||||
): void => {
|
): void => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|||||||
+7
-150
@@ -1,11 +1,8 @@
|
|||||||
import axios, { Canceler } from "axios";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
|
|
||||||
import {
|
import {
|
||||||
ApiAdapterPromise,
|
ApiAdapterPromise,
|
||||||
ApiAdapterRequestConfig,
|
ApiAdapterRequestConfig,
|
||||||
ApiAdapterUtils,
|
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
CharacterPreferences,
|
CharacterPreferences,
|
||||||
ChoiceData,
|
ChoiceData,
|
||||||
@@ -13,10 +10,8 @@ import {
|
|||||||
Constants,
|
Constants,
|
||||||
CoreUtils,
|
CoreUtils,
|
||||||
DefinitionPool,
|
DefinitionPool,
|
||||||
Feat,
|
|
||||||
FeatDefinitionContract,
|
FeatDefinitionContract,
|
||||||
FeatLookup,
|
FeatLookup,
|
||||||
FeatUtils,
|
|
||||||
HelperUtils,
|
HelperUtils,
|
||||||
OptionalOriginLookup,
|
OptionalOriginLookup,
|
||||||
OptionalOriginUtils,
|
OptionalOriginUtils,
|
||||||
@@ -26,12 +21,9 @@ import {
|
|||||||
} from "@dndbeyond/character-rules-engine/es";
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
import { Accordion } from "~/components/Accordion";
|
import { Accordion } from "~/components/Accordion";
|
||||||
import { FeatureChoice } from "~/components/FeatureChoice";
|
import { FeatureChoices } from "~/components/FeatureChoices";
|
||||||
import { HtmlContent } from "~/components/HtmlContent";
|
import { HtmlContent } from "~/components/HtmlContent";
|
||||||
|
|
||||||
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
|
|
||||||
import { AppLoggerUtils } from "../../../../../Shared/utils";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
speciesTrait: RacialTrait;
|
speciesTrait: RacialTrait;
|
||||||
choiceInfo: ChoiceData;
|
choiceInfo: ChoiceData;
|
||||||
@@ -52,10 +44,7 @@ interface Props {
|
|||||||
speciesName?: string | null;
|
speciesName?: string | null;
|
||||||
}
|
}
|
||||||
interface State {
|
interface State {
|
||||||
featData: Array<Feat>;
|
|
||||||
hasFeatChoice: boolean;
|
|
||||||
collapsibleOpened: boolean;
|
collapsibleOpened: boolean;
|
||||||
loadingStatus: DataLoadingStatusEnum;
|
|
||||||
}
|
}
|
||||||
export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
||||||
Props,
|
Props,
|
||||||
@@ -65,115 +54,10 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
featData: [],
|
|
||||||
hasFeatChoice: false,
|
|
||||||
collapsibleOpened: 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 = () => {
|
getSummary = () => {
|
||||||
const { speciesTrait, speciesName } = this.props;
|
const { speciesTrait, speciesName } = this.props;
|
||||||
// Get base name for the species trait
|
// 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() {
|
render() {
|
||||||
const { speciesTrait } = this.props;
|
const { speciesTrait } = this.props;
|
||||||
const { hasFeatChoice, loadingStatus } = this.state;
|
const { collapsibleOpened } = this.state;
|
||||||
|
|
||||||
const description = RacialTraitUtils.getDescription(speciesTrait);
|
const description = RacialTraitUtils.getDescription(speciesTrait);
|
||||||
const choices = RacialTraitUtils.getChoices(speciesTrait);
|
const choices = RacialTraitUtils.getChoices(speciesTrait);
|
||||||
@@ -314,17 +178,6 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
|||||||
conClsNames.push("collapsible-todo");
|
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 (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
id={accordionId}
|
id={accordionId}
|
||||||
@@ -339,7 +192,11 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
|||||||
html={description ? description : ""}
|
html={description ? description : ""}
|
||||||
withoutTooltips
|
withoutTooltips
|
||||||
/>
|
/>
|
||||||
{contentNode}
|
<FeatureChoices
|
||||||
|
choices={RacialTraitUtils.getChoices(speciesTrait)}
|
||||||
|
onChoiceChange={this.handleChoiceChange}
|
||||||
|
shouldFetch={collapsibleOpened}
|
||||||
|
/>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -64,12 +64,19 @@ export default class ClassFeatureSnippet extends React.PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderDescription = (): React.ReactNode => {
|
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(
|
const description: string | null = AccessUtils.isAccessible(
|
||||||
ClassFeatureUtils.getAccessType(feature)
|
ClassFeatureUtils.getAccessType(feature)
|
||||||
)
|
)
|
||||||
? ClassFeatureUtils.getDescription(feature)
|
? !isStartingClass && multiClassDescription
|
||||||
|
? multiClassDescription
|
||||||
|
: ClassFeatureUtils.getDescription(feature)
|
||||||
: "Check out the Marketplace to unlock this Class Feature.";
|
: "Check out the Marketplace to unlock this Class Feature.";
|
||||||
|
|
||||||
return showDescription
|
return showDescription
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { RacialTraitAccessors } from "@dndbeyond/character-rules-engine/es/engine/RacialTrait";
|
|
||||||
import {
|
import {
|
||||||
AbilityLookup,
|
AbilityLookup,
|
||||||
BaseFeat,
|
BaseFeat,
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
CharacterFeaturesManager,
|
CharacterFeaturesManager,
|
||||||
} from "@dndbeyond/character-rules-engine/es";
|
} from "@dndbeyond/character-rules-engine/es";
|
||||||
|
|
||||||
|
|
||||||
import { DataOriginTypeEnum } from "~/constants";
|
import { DataOriginTypeEnum } from "~/constants";
|
||||||
import { FeatureSnippet } from "~/subApps/sheet/components/FeatureSnippet";
|
import { FeatureSnippet } from "~/subApps/sheet/components/FeatureSnippet";
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ export default class SpeciesTraitFeatureSnippet extends React.PureComponent<Prop
|
|||||||
|
|
||||||
const feats = featuresManager.getDataOriginOnlyFeatsByPrimary(
|
const feats = featuresManager.getDataOriginOnlyFeatsByPrimary(
|
||||||
DataOriginTypeEnum.RACE,
|
DataOriginTypeEnum.RACE,
|
||||||
`${RacialTraitAccessors.getId(speciesTrait)}`
|
`${RacialTraitUtils.getId(speciesTrait)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -383,18 +383,38 @@ export class ItemDetail extends React.PureComponent<Props> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{replacementWeaponStats.map((statId) => (
|
{replacementWeaponStats.map((replacementInfo) => (
|
||||||
<div className="ct-item-detail__class-customize-item" key={statId}>
|
<div
|
||||||
|
className="ct-item-detail__class-customize-item"
|
||||||
|
key={`${replacementInfo.statId}-${replacementInfo.dataOrigin.primary.definition.id}`}
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
enabled={appliedWeaponReplacementStats.includes(statId)}
|
enabled={appliedWeaponReplacementStats.includes(
|
||||||
|
replacementInfo.statId
|
||||||
|
)}
|
||||||
onChange={(enabled) =>
|
onChange={(enabled) =>
|
||||||
this.handleReplaceAbilityStatChange(enabled, statId)
|
this.handleReplaceAbilityStatChange(
|
||||||
|
enabled,
|
||||||
|
replacementInfo.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>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
label={`Use ${RuleDataUtils.getStatNameById(
|
|
||||||
statId,
|
|
||||||
ruleData,
|
|
||||||
true
|
|
||||||
)}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ import Collapsible from "~/tools/js/smartComponents/Collapsible";
|
|||||||
import { Select } from "~/tools/js/smartComponents/legacy";
|
import { Select } from "~/tools/js/smartComponents/legacy";
|
||||||
|
|
||||||
// TODO this needs to not extend builder contract and options typing needs to be fixed
|
// 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"> {
|
interface Props extends Omit<BuilderChoiceContract, "options"> {
|
||||||
choice: Choice;
|
choice: Choice;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -31,7 +33,6 @@ interface Props extends Omit<BuilderChoiceContract, "options"> {
|
|||||||
onChange?: (
|
onChange?: (
|
||||||
id: string,
|
id: string,
|
||||||
type: number,
|
type: number,
|
||||||
subType: number | null,
|
|
||||||
value: any,
|
value: any,
|
||||||
parentChoiceId: string | null
|
parentChoiceId: string | null
|
||||||
) => void;
|
) => void;
|
||||||
@@ -61,12 +62,17 @@ export const DetailChoice: FC<Props> = ({
|
|||||||
const { entityRestrictionData, ruleData } = useCharacterEngine();
|
const { entityRestrictionData, ruleData } = useCharacterEngine();
|
||||||
const { languages } = useRuleData();
|
const { languages } = useRuleData();
|
||||||
|
|
||||||
|
const hasTodo =
|
||||||
|
(ChoiceUtils.isInfinite(choice) && !ChoiceUtils.isOptionSelected(choice)) ||
|
||||||
|
ChoiceUtils.isTodo(choice);
|
||||||
|
|
||||||
const handleChoiceChange = (value: string): void => {
|
const handleChoiceChange = (value: string): void => {
|
||||||
if (onChange && id !== null) {
|
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 (
|
if (
|
||||||
hideWhenOnlyDefaultSelected &&
|
hideWhenOnlyDefaultSelected &&
|
||||||
ChoiceUtils.isOnlyDefaultSelected(choice)
|
ChoiceUtils.isOnlyDefaultSelected(choice)
|
||||||
@@ -74,14 +80,6 @@ export const DetailChoice: FC<Props> = ({
|
|||||||
return null;
|
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.
|
// 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;
|
let choiceDescription = description;
|
||||||
|
|
||||||
@@ -94,10 +92,6 @@ export const DetailChoice: FC<Props> = ({
|
|||||||
choiceDescription = description || chosenOption["description"];
|
choiceDescription = description || chosenOption["description"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentChoiceId !== null) {
|
|
||||||
classNames.push("detail-choice--child");
|
|
||||||
}
|
|
||||||
|
|
||||||
let choiceNode: ReactNode;
|
let choiceNode: ReactNode;
|
||||||
if (choiceDescription) {
|
if (choiceDescription) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -142,7 +136,14 @@ export const DetailChoice: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames.join(" ")}>
|
<div
|
||||||
|
className={clsx([
|
||||||
|
"detail-choice",
|
||||||
|
hasTodo && "detail-choice--todo",
|
||||||
|
parentChoiceId !== null && "detail-choice--child",
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
className="detail-choice-input"
|
className="detail-choice-input"
|
||||||
options={ChoiceUtils.getSortedRenderOptions(
|
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,
|
accept: () => void,
|
||||||
reject: () => void
|
reject: () => void
|
||||||
) => void;
|
) => void;
|
||||||
label?: string;
|
label?: React.ReactNode;
|
||||||
preventDefault: boolean;
|
preventDefault: boolean;
|
||||||
stopPropagation: boolean;
|
stopPropagation: boolean;
|
||||||
isInteractive?: 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
|
// 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