New source found from dndbeyond.com

This commit is contained in:
2025-07-16 01:00:16 -07:00
parent 151590af7a
commit f9f8dd0e7a
25 changed files with 886 additions and 1299 deletions
@@ -1,11 +1,8 @@
import axios, { Canceler } from "axios";
import React from "react";
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
ApiAdapterUtils,
ApiResponse,
CharacterPreferences,
CharClass,
@@ -18,10 +15,8 @@ import {
Constants,
DefinitionPool,
EntitledEntity,
Feat,
FeatDefinitionContract,
FeatLookup,
FeatUtils,
FormatUtils,
HelperUtils,
InfusionChoice,
@@ -35,20 +30,16 @@ import {
OptionalClassFeatureUtils,
PrerequisiteData,
RuleData,
RuleDataUtils,
SourceData,
TypeValueLookup,
} from "@dndbeyond/character-rules-engine/es";
import { Accordion } from "~/components/Accordion";
import { CollapsibleContent } from "~/components/CollapsibleContent";
import { FeatureChoice } from "~/components/FeatureChoice";
import { FeatureChoices } from "~/components/FeatureChoices";
import { Link } from "~/components/Link";
import { GrantedFeat } from "~/tools/js/CharacterBuilder/components/GrantedFeat/GrantedFeat";
import InfusionChoiceManager from "../../../../../Shared/components/InfusionChoiceManager";
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
import { AppLoggerUtils, TypeScriptUtils } from "../../../../../Shared/utils";
interface Props {
isActive: boolean;
@@ -122,13 +113,7 @@ interface Props {
inventoryManager: InventoryManager;
}
interface State {
featData: Array<Feat>;
subclassData: Array<ClassDefinitionContract>;
hasFeatChoice: boolean;
hasSubclassChoice: boolean;
collapsibleOpened: boolean;
featLoadingStatus: DataLoadingStatusEnum;
subclassLoadingStatus: DataLoadingStatusEnum;
}
export default class ClassManagerFeature extends React.PureComponent<
Props,
@@ -139,157 +124,16 @@ export default class ClassManagerFeature extends React.PureComponent<
featLookup: {},
};
loadFeatsCanceler: null | Canceler = null;
loadSubclassesCanceler: null | Canceler = null;
constructor(props: Props) {
super(props);
this.state = {
featData: [],
subclassData: [],
hasFeatChoice: false,
hasSubclassChoice: false,
collapsibleOpened: false,
featLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
subclassLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
};
}
componentDidMount(): void {
this.setState(
{
hasFeatChoice: this.hasFeatChoice(this.props),
hasSubclassChoice: this.hasSubclassChoice(this.props),
},
() => {
this.conditionallyLoadFeatData();
}
);
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<State>,
snapshot?: any
): void {
hasSubclassChoice = (): boolean => {
const { feature } = this.props;
const { collapsibleOpened } = this.state;
if (feature !== prevProps.feature) {
this.setState(
{
hasFeatChoice: this.hasFeatChoice(this.props),
hasSubclassChoice: this.hasSubclassChoice(this.props),
},
() => {
this.conditionallyLoadFeatData();
}
);
}
if (collapsibleOpened && !prevState.collapsibleOpened) {
this.conditionallyLoadFeatData();
}
}
conditionallyLoadFeatData = (): void => {
const { loadAvailableFeats, loadAvailableSubclasses, charClass } =
this.props;
const {
hasFeatChoice,
hasSubclassChoice,
featLoadingStatus,
subclassLoadingStatus,
collapsibleOpened,
} = this.state;
if (
hasFeatChoice &&
collapsibleOpened &&
featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
) {
this.setState({
featLoadingStatus: DataLoadingStatusEnum.LOADING,
});
loadAvailableFeats({
cancelToken: new axios.CancelToken((c) => {
this.loadFeatsCanceler = c;
}),
})
.then((response) => {
let featData: Array<Feat> = [];
const data = ApiAdapterUtils.getResponseData(response);
if (data !== null) {
featData = data.map((definition) =>
FeatUtils.simulateFeat(definition)
);
}
this.setState({
featData,
featLoadingStatus: DataLoadingStatusEnum.LOADED,
});
this.loadFeatsCanceler = null;
})
.catch(AppLoggerUtils.handleAdhocApiError);
}
if (
hasSubclassChoice &&
collapsibleOpened &&
subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
) {
this.setState({
subclassLoadingStatus: DataLoadingStatusEnum.LOADING,
});
loadAvailableSubclasses(ClassUtils.getId(charClass), {
cancelToken: new axios.CancelToken((c) => {
this.loadSubclassesCanceler = c;
}),
})
.then((response) => {
let subclassData: Array<ClassDefinitionContract> = [];
const data = ApiAdapterUtils.getResponseData(response);
if (data !== null) {
subclassData = data;
}
this.setState({
subclassData,
subclassLoadingStatus: DataLoadingStatusEnum.LOADED,
});
this.loadSubclassesCanceler = null;
})
.catch(AppLoggerUtils.handleAdhocApiError);
}
};
componentWillUnmount(): void {
if (this.loadFeatsCanceler !== null) {
this.loadFeatsCanceler();
}
if (this.loadSubclassesCanceler !== null) {
this.loadSubclassesCanceler();
}
}
hasFeatChoice = (props: Props): boolean => {
const { feature } = props;
const choices = ClassFeatureUtils.getChoices(feature);
return choices.some(
(choice) =>
ChoiceUtils.getType(choice) ===
Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION
);
};
hasSubclassChoice = (props: Props): boolean => {
const { feature } = props;
const choices = ClassFeatureUtils.getChoices(feature);
return choices.some(
@@ -299,28 +143,6 @@ export default class ClassManagerFeature extends React.PureComponent<
);
};
isDataLoaded = (): boolean => {
const {
hasSubclassChoice,
hasFeatChoice,
subclassLoadingStatus,
featLoadingStatus,
} = this.state;
if (hasSubclassChoice) {
if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) {
return false;
}
}
if (hasFeatChoice) {
if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) {
return false;
}
}
return true;
};
handleCollapsibleOpened = (id: string, state: boolean): void => {
this.setState({
collapsibleOpened: state,
@@ -436,46 +258,6 @@ export default class ClassManagerFeature extends React.PureComponent<
);
};
getSubclassSources = (
subclass: ClassDefinitionContract
): Array<SourceData> => {
const { ruleData } = this.props;
if (subclass.sources === null) {
return [];
}
return subclass.sources
.map((sourceMapping) =>
HelperUtils.lookupDataOrFallback(
RuleDataUtils.getSourceDataLookup(ruleData),
sourceMapping.sourceId
)
)
.filter(TypeScriptUtils.isNotNullOrUndefined);
};
getSubclassData = (): Array<ClassDefinitionContract> => {
const { subclassData } = this.state;
const { charClass } = this.props;
let data: Array<ClassDefinitionContract> = [...subclassData];
let existingSubclass = ClassUtils.getSubclass(charClass);
if (
existingSubclass !== null &&
!data.some(
(classDefinition) =>
existingSubclass !== null &&
classDefinition.id === existingSubclass.id
)
) {
data.push(existingSubclass);
}
return data;
};
getClassFeatureDescription = (): string => {
const { feature, isStartingClass } = this.props;
@@ -493,33 +275,6 @@ export default class ClassManagerFeature extends React.PureComponent<
return description === null ? "" : description;
};
renderChoices = (): React.ReactNode => {
const { isActive, feature, charClass } = this.props;
const { featData } = this.state;
if (!isActive) {
return null;
}
const choices = ClassFeatureUtils.getChoices(feature);
if (choices === null) {
return null;
}
return choices.map((choice) => (
<FeatureChoice
choice={choice}
charClass={charClass}
feature={feature}
featsData={featData}
subclassData={this.getSubclassData()}
onChoiceChange={this.handleChoiceChange}
key={ChoiceUtils.getId(choice)}
/>
));
};
getMetaItems = () => {
const { feature, optionalClassFeatureLookup, definitionPool } = this.props;
const choiceCount = this.getTotalChoiceCount();
@@ -590,7 +345,7 @@ export default class ClassManagerFeature extends React.PureComponent<
feature,
inventoryManager,
} = this.props;
const { hasSubclassChoice, hasFeatChoice } = this.state;
const { collapsibleOpened } = this.state;
// If the class feature grants feats, render those instead of the class feature.
if (feature.featLists.length > 0) {
@@ -604,22 +359,11 @@ export default class ClassManagerFeature extends React.PureComponent<
}
const conClsNames: Array<string> = ["class-manager-feature-name"];
let contentNode: React.ReactNode;
if (isActive) {
if (this.getTotalTodoCount() > 0) {
conClsNames.push("collapsible-todo");
}
if (hasFeatChoice || hasSubclassChoice) {
if (this.isDataLoaded()) {
contentNode = this.renderChoices();
} else {
contentNode = <LoadingPlaceholder />;
}
} else {
contentNode = this.renderChoices();
}
}
const hasChoices = this.getTotalTodoCount() > 0;
const accordionId = ClassFeatureUtils.getId(feature).toString();
@@ -636,13 +380,20 @@ export default class ClassManagerFeature extends React.PureComponent<
<CollapsibleContent className="class-manager-feature-description">
{this.getClassFeatureDescription()}
</CollapsibleContent>
{isActive && hasSubclassChoice && (
{isActive && this.hasSubclassChoice() && (
<div className="ct-character-tools__marketplace-callout">
Looking for something not in the list below? Unlock all official
options in the <Link href="/marketplace">Marketplace</Link>.
</div>
)}
{contentNode}
{isActive && (
<FeatureChoices
choices={ClassFeatureUtils.getChoices(feature)}
charClass={charClass}
onChoiceChange={this.handleChoiceChange}
shouldFetch={collapsibleOpened}
/>
)}
{isActive && (
<InfusionChoiceManager
infusionChoices={this.getAvailableInfusionChoices()}
@@ -585,7 +585,6 @@ class DescriptionManage extends React.PureComponent<Props, State> {
handleChoiceChange = (
choiceId: string,
choiceType: number,
choiceSubType: number | null,
optionValue: any
): void => {
const { dispatch } = this.props;
@@ -1,11 +1,8 @@
import axios, { Canceler } from "axios";
import React from "react";
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
ApiAdapterUtils,
ApiResponse,
CharacterPreferences,
ChoiceData,
@@ -13,10 +10,8 @@ import {
Constants,
CoreUtils,
DefinitionPool,
Feat,
FeatDefinitionContract,
FeatLookup,
FeatUtils,
HelperUtils,
OptionalOriginLookup,
OptionalOriginUtils,
@@ -26,12 +21,9 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { Accordion } from "~/components/Accordion";
import { FeatureChoice } from "~/components/FeatureChoice";
import { FeatureChoices } from "~/components/FeatureChoices";
import { HtmlContent } from "~/components/HtmlContent";
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
import { AppLoggerUtils } from "../../../../../Shared/utils";
interface Props {
speciesTrait: RacialTrait;
choiceInfo: ChoiceData;
@@ -52,10 +44,7 @@ interface Props {
speciesName?: string | null;
}
interface State {
featData: Array<Feat>;
hasFeatChoice: boolean;
collapsibleOpened: boolean;
loadingStatus: DataLoadingStatusEnum;
}
export default class SpeciesManageSpeciesTrait extends React.PureComponent<
Props,
@@ -65,115 +54,10 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
super(props);
this.state = {
featData: [],
hasFeatChoice: false,
collapsibleOpened: false,
loadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
};
}
loadFeatsCanceler: null | Canceler = null;
componentDidMount(): void {
this.setState(
{
hasFeatChoice: this.hasFeatChoice(this.props),
},
() => {
this.conditionallyLoadFeatData();
}
);
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<State>,
snapshot?: any
): void {
const { speciesTrait } = this.props;
const { collapsibleOpened } = this.state;
if (speciesTrait !== prevProps.speciesTrait) {
this.setState(
{
hasFeatChoice: this.hasFeatChoice(this.props),
},
() => {
this.conditionallyLoadFeatData();
}
);
}
if (collapsibleOpened && !prevState.collapsibleOpened) {
this.conditionallyLoadFeatData();
}
}
componentWillUnmount(): void {
if (this.loadFeatsCanceler !== null) {
this.loadFeatsCanceler();
}
}
conditionallyLoadFeatData = (): void => {
const { loadAvailableFeats } = this.props;
const { hasFeatChoice, loadingStatus, collapsibleOpened } = this.state;
if (
hasFeatChoice &&
collapsibleOpened &&
loadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED
) {
this.setState({
loadingStatus: DataLoadingStatusEnum.LOADING,
});
loadAvailableFeats({
cancelToken: new axios.CancelToken((c) => {
this.loadFeatsCanceler = c;
}),
})
.then((response) => {
let featData: Array<Feat> = [];
const data = ApiAdapterUtils.getResponseData(response);
if (data !== null) {
featData = data.map((definition) =>
FeatUtils.simulateFeat(definition)
);
}
this.setState({
featData,
loadingStatus: DataLoadingStatusEnum.LOADED,
});
this.loadFeatsCanceler = null;
})
.catch(AppLoggerUtils.handleAdhocApiError);
}
};
getFeatData = (existingFeatId: number | null): Array<Feat> => {
const { featData } = this.state;
const { featLookup } = this.props;
let data: Array<Feat> = [...featData];
if (existingFeatId !== null) {
let existingFeat = HelperUtils.lookupDataOrFallback(
featLookup,
existingFeatId
);
if (
existingFeat !== null &&
!data.some((feat) => FeatUtils.getId(feat) === existingFeatId)
) {
data.push(existingFeat);
}
}
return data;
};
getSummary = () => {
const { speciesTrait, speciesName } = this.props;
// Get base name for the species trait
@@ -277,29 +161,9 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
}
};
renderChoices = (): React.ReactNode => {
const { speciesTrait } = this.props;
const { featData } = this.state;
const choices = RacialTraitUtils.getChoices(speciesTrait);
if (choices === null) {
return null;
}
return choices.map((choice) => (
<FeatureChoice
choice={choice}
featsData={featData}
onChoiceChange={this.handleChoiceChange}
key={ChoiceUtils.getId(choice)}
/>
));
};
render() {
const { speciesTrait } = this.props;
const { hasFeatChoice, loadingStatus } = this.state;
const { collapsibleOpened } = this.state;
const description = RacialTraitUtils.getDescription(speciesTrait);
const choices = RacialTraitUtils.getChoices(speciesTrait);
@@ -314,17 +178,6 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
conClsNames.push("collapsible-todo");
}
let contentNode: React.ReactNode;
if (hasFeatChoice) {
if (loadingStatus === DataLoadingStatusEnum.LOADED) {
contentNode = this.renderChoices();
} else {
contentNode = <LoadingPlaceholder />;
}
} else {
contentNode = this.renderChoices();
}
return (
<Accordion
id={accordionId}
@@ -339,7 +192,11 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent<
html={description ? description : ""}
withoutTooltips
/>
{contentNode}
<FeatureChoices
choices={RacialTraitUtils.getChoices(speciesTrait)}
onChoiceChange={this.handleChoiceChange}
shouldFetch={collapsibleOpened}
/>
</Accordion>
);
}