Grabbed dndbeyond's source code

```
~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js
```
This commit is contained in:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
@@ -0,0 +1,484 @@
import React from "react";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
ApiResponse,
BaseSpellContract,
CharacterPreferences,
CharacterTheme,
CharClass,
ChoiceData,
ClassDefinitionContract,
ClassFeatureDefinitionContract,
ClassFeatureUtils,
ClassSpellInfo,
ClassSpellListSpellsLookup,
ClassUtils,
Constants,
DataOriginRefData,
DefinitionPool,
DefinitionUtils,
EntitledEntity,
FeatDefinitionContract,
FeatLookup,
InfusionChoice,
InfusionDefinitionContract,
InventoryManager,
Modifier,
OptionalClassFeatureLookup,
OverallSpellInfo,
PrerequisiteData,
RuleData,
RuleDataUtils,
Spell,
SpellCasterInfo,
TypeValueLookup,
} from "@dndbeyond/character-rules-engine/es";
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
import { TabList } from "~/components/TabList";
import { ClassHeader } from "~/subApps/builder/routes/Class/ClassHeader";
import ClassSpellListManager from "../../../../../Shared/components/legacy/ClassSpellListManager";
import {
ApiClassFeaturesRequest,
ApiSpellsRequest,
} from "../../../../../Shared/selectors/composite/apiCreator";
import ClassManagerFeature from "../ClassManagerFeature";
import { OptionalFeatureManager } from "../OptionalFeatureManager";
interface Props {
charClass: CharClass;
spellCasterInfo: SpellCasterInfo;
overallSpellInfo: OverallSpellInfo;
ruleData: RuleData;
prerequisiteData: PrerequisiteData;
choiceInfo: ChoiceData;
preferences: CharacterPreferences;
globalModifiers: Array<Modifier>;
typeValueLookup: TypeValueLookup;
definitionPool: DefinitionPool;
knownInfusionLookup: Record<string, InfusionChoice>;
knownReplicatedItems: Array<string>;
activeSourceCategories: Array<number>;
onClassFeatureChoiceChange: (
classId: number,
classFeatureId: number,
choiceType: any,
choiceId: string,
optionValue: number | null,
parentChoiceId: string | null
) => void;
onSpellPrepare: (spell: Spell, classMappingId: number) => void;
onSpellUnprepare: (spell: Spell, classMappingId: number) => void;
onSpellRemove: (spell: Spell, classMappingId: number) => void;
onSpellAdd: (spell: Spell, classMappingId: number) => void;
onAlwaysKnownLoad: (
spells: Array<BaseSpellContract>,
classId: number
) => void;
onInfusionChoiceItemChangePromise?: (
infusionChoiceKey: string,
infusionId: string,
itemDefinitionKey: string,
itemName: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceItemDestroyPromise?: (
infusionChoiceKey: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceChangePromise?: (
infusionChoiceKey: string,
infusionId: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceDestroyPromise?: (
infusionChoiceKey: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceCreatePromise?: (
infusionChoiceKey: string,
infusionId: string,
accept: () => void,
reject: () => void
) => void;
onOptionalFeatureSelection: (
definitionKey: string,
affectedClassFeatureDefinitionKey: string | null
) => void;
onChangeReplacementPromise?: (
definitionKey: string,
newAffectedDefinitionKey: string | null,
oldAffectedDefinitionKey: string | null,
accept: () => void,
reject: () => void
) => void;
onRemoveSelectionPromise?: (
definitionKey: string,
newIsEnabled: boolean,
accept: () => void,
reject: () => void
) => void;
classSpellList: ClassSpellInfo | null;
levelsRemaining: number;
isMulticlass: boolean;
loadAvailableSubclasses: (
baseClassId: number,
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<ApiResponse<Array<ClassDefinitionContract>>>;
loadAvailableFeats: (
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<ApiResponse<Array<FeatDefinitionContract>>>;
loadRemainingSpellList: ApiSpellsRequest | null;
loadAlwaysKnownSpells?: ApiSpellsRequest | null;
loadAvailableOptionalClassFeatures: ApiClassFeaturesRequest | null;
loadAvailableInfusions: (
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<
ApiResponse<EntitledEntity<InfusionDefinitionContract>>
>;
onDefinitionsLoaded?: (
definitions: Array<InfusionDefinitionContract>,
accessTypes: Record<string, number>
) => void;
onFeatureDefinitionsLoaded?: (
definitionData: EntitledEntity<ClassFeatureDefinitionContract>
) => void;
featLookup: FeatLookup;
dataOriginRefData: DataOriginRefData;
classSpellListSpellsLookup: ClassSpellListSpellsLookup;
optionalClassFeatureLookup: OptionalClassFeatureLookup;
theme: CharacterTheme;
inventoryManager: InventoryManager;
}
interface State {
showClassFeatures: boolean;
showAtHigherLevelsClassFeatures: boolean;
}
export default class ClassManager extends React.PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
showClassFeatures: true,
showAtHigherLevelsClassFeatures: false,
};
}
handleAtHigherLevelsTriggerClick = (evt: React.MouseEvent): void => {
this.setState((prevState) => ({
showAtHigherLevelsClassFeatures:
!prevState.showAtHigherLevelsClassFeatures,
}));
};
handleFeatureChoiceChange = (
classFeatureId: number,
choiceId: string,
choiceType: any,
optionValue: number | null,
parentChoiceId: string | null
): void => {
const { onClassFeatureChoiceChange, charClass } = this.props;
if (onClassFeatureChoiceChange) {
onClassFeatureChoiceChange(
ClassUtils.getActiveId(charClass),
classFeatureId,
choiceType,
choiceId,
optionValue,
parentChoiceId
);
}
};
renderInformationCollapsible = (): React.ReactNode => {
const { ruleData, charClass } = this.props;
if (charClass === null) {
return null;
}
const definitionKey = DefinitionUtils.hack__generateDefinitionKey(
ClassUtils.getMappingEntityTypeId(charClass),
ClassUtils.getId(charClass)
);
const builderText = RuleDataUtils.getBuilderHelperTextByDefinitionKeys(
[definitionKey],
ruleData,
Constants.DisplayConfigurationTypeEnum.CLASS_FEATURE
);
return <HelperTextAccordion builderHelperText={builderText} />;
};
renderClassFeatureGroups = (): React.ReactNode => {
const { showClassFeatures, showAtHigherLevelsClassFeatures } = this.state;
const {
charClass,
choiceInfo,
prerequisiteData,
preferences,
loadAvailableSubclasses,
loadAvailableFeats,
typeValueLookup,
globalModifiers,
ruleData,
definitionPool,
loadAvailableInfusions,
onDefinitionsLoaded,
onInfusionChoiceItemChangePromise,
onInfusionChoiceItemDestroyPromise,
onInfusionChoiceChangePromise,
onInfusionChoiceDestroyPromise,
onInfusionChoiceCreatePromise,
featLookup,
knownInfusionLookup,
knownReplicatedItems,
optionalClassFeatureLookup,
inventoryManager,
} = this.props;
if (!showClassFeatures) {
return null;
}
const level = ClassUtils.getLevel(charClass);
const isStartingClass = ClassUtils.isStartingClass(charClass);
const visibleClassFeatures = ClassUtils.getVisibileClassFeatures(charClass);
const orderedVisibleFeatures = ClassUtils.deriveOrderedClassFeatures(
visibleClassFeatures
).filter((feature) => ClassFeatureUtils.getRequiredLevel(feature) <= level);
const classFeatures = ClassUtils.getClassFeatures(charClass);
const orderedClassFeatures =
ClassUtils.deriveOrderedClassFeatures(classFeatures);
const atHigherLevelsClassFeatures = orderedClassFeatures.filter(
(feature) => ClassFeatureUtils.getRequiredLevel(feature) > level
);
return (
<div className="class-manager-features-groups">
{this.renderInformationCollapsible()}
{orderedVisibleFeatures.length > 0 && (
<div className="class-manager-features-group">
<div className="class-manager-features-group-items">
{orderedVisibleFeatures.map((feature) => (
<ClassManagerFeature
key={ClassFeatureUtils.getId(feature)}
charClass={charClass}
feature={feature}
isStartingClass={isStartingClass}
onChoiceChange={this.handleFeatureChoiceChange}
onInfusionChoiceItemChangePromise={
onInfusionChoiceItemChangePromise
}
onInfusionChoiceItemDestroyPromise={
onInfusionChoiceItemDestroyPromise
}
onInfusionChoiceChangePromise={onInfusionChoiceChangePromise}
onInfusionChoiceDestroyPromise={
onInfusionChoiceDestroyPromise
}
onInfusionChoiceCreatePromise={onInfusionChoiceCreatePromise}
onDefinitionsLoaded={onDefinitionsLoaded}
choiceInfo={choiceInfo}
prerequisiteData={prerequisiteData}
preferences={preferences}
loadAvailableInfusions={loadAvailableInfusions}
loadAvailableSubclasses={loadAvailableSubclasses}
loadAvailableFeats={loadAvailableFeats}
typeValueLookup={typeValueLookup}
globalModifiers={globalModifiers}
ruleData={ruleData}
definitionPool={definitionPool}
featLookup={featLookup}
knownReplicatedItems={knownReplicatedItems}
knownInfusionLookup={knownInfusionLookup}
optionalClassFeatureLookup={optionalClassFeatureLookup}
inventoryManager={inventoryManager}
/>
))}
</div>
</div>
)}
{atHigherLevelsClassFeatures.length > 0 && (
<div
className={`class-manager-features-group ${
showAtHigherLevelsClassFeatures
? "class-manager-features-group-opened"
: "class-manager-features-group-collapsed"
}`}
>
<div className="class-manager-features-group-header">
<div
className="class-manager-features-group-heading"
onClick={this.handleAtHigherLevelsTriggerClick}
>
Available at Higher Levels ({atHigherLevelsClassFeatures.length}
)
</div>
<div
className="class-manager-features-group-trigger"
onClick={this.handleAtHigherLevelsTriggerClick}
/>
</div>
{showAtHigherLevelsClassFeatures && (
<div className="class-manager-features-group-items">
{atHigherLevelsClassFeatures.map((feature) => (
<ClassManagerFeature
key={ClassFeatureUtils.getId(feature)}
charClass={charClass}
choiceInfo={choiceInfo}
feature={feature}
isStartingClass={isStartingClass}
isActive={false}
prerequisiteData={prerequisiteData}
preferences={preferences}
loadAvailableInfusions={loadAvailableInfusions}
loadAvailableSubclasses={loadAvailableSubclasses}
loadAvailableFeats={loadAvailableFeats}
typeValueLookup={typeValueLookup}
globalModifiers={globalModifiers}
ruleData={ruleData}
definitionPool={definitionPool}
knownReplicatedItems={knownReplicatedItems}
knownInfusionLookup={knownInfusionLookup}
optionalClassFeatureLookup={optionalClassFeatureLookup}
inventoryManager={inventoryManager}
/>
))}
</div>
)}
</div>
)}
</div>
);
};
renderClassFeatures = (): React.ReactNode => {
const { showClassFeatures } = this.state;
return (
<div
className={`class-manager-features ${
showClassFeatures
? "class-manager-features-opened"
: "class-manager-features-collapsed"
}`}
>
{/*<div className="class-manager-features-header">*/}
{/*<div className="class-manager-features-heading" onClick={this.handleFeaturesTriggerClick}>Class Features</div>*/}
{/*<div className="class-manager-features-trigger" onClick={this.handleFeaturesTriggerClick} />*/}
{/*</div>*/}
{this.renderClassFeatureGroups()}
</div>
);
};
render() {
const {
charClass,
definitionPool,
levelsRemaining,
isMulticlass,
loadRemainingSpellList,
loadAlwaysKnownSpells,
loadAvailableOptionalClassFeatures,
preferences,
classSpellList,
spellCasterInfo,
ruleData,
overallSpellInfo,
onSpellAdd,
onSpellRemove,
onSpellPrepare,
onSpellUnprepare,
onAlwaysKnownLoad,
dataOriginRefData,
onOptionalFeatureSelection,
onRemoveSelectionPromise,
onChangeReplacementPromise,
onFeatureDefinitionsLoaded,
optionalClassFeatureLookup,
theme,
activeSourceCategories,
} = this.props;
return (
<div className="class-manager">
<ClassHeader
charClass={charClass}
isMulticlass={isMulticlass}
levelsRemaining={levelsRemaining}
/>
<TabList
variant="toggle"
defaultActiveId={isMulticlass ? "none" : "features"}
tabs={[
{
label: "Class Features",
content: this.renderClassFeatures(),
id: "features",
},
preferences.enableOptionalClassFeatures
? {
label: "Optional Feature Manager",
content: (
<OptionalFeatureManager
charClass={charClass}
definitionPool={definitionPool}
optionalClassFeatureLookup={optionalClassFeatureLookup}
onDefinitionsLoaded={onFeatureDefinitionsLoaded}
loadAvailableOptionalClassFeatures={
loadAvailableOptionalClassFeatures
}
onSelection={onOptionalFeatureSelection}
onChangeReplacementPromise={onChangeReplacementPromise}
onRemoveSelectionPromise={onRemoveSelectionPromise}
ruleData={ruleData}
/>
),
id: "optional-features",
}
: null,
classSpellList
? {
label: "Spells",
content: (
<ClassSpellListManager
{...(classSpellList as any)}
loadRemainingSpellList={loadRemainingSpellList}
loadAlwaysKnownSpells={loadAlwaysKnownSpells}
onSpellPrepare={onSpellPrepare}
onSpellUnprepare={onSpellUnprepare}
onSpellRemove={onSpellRemove}
onSpellAdd={onSpellAdd}
onAlwaysKnownLoad={onAlwaysKnownLoad}
showHeader={false}
spellCasterInfo={spellCasterInfo}
enableSpellcasting={false}
enableCustomization={false}
ruleData={ruleData}
overallSpellInfo={overallSpellInfo}
dataOriginRefData={dataOriginRefData}
theme={theme}
activeSourceCategories={activeSourceCategories}
/>
),
id: "spells",
}
: null,
]}
/>
</div>
);
}
}
@@ -0,0 +1,4 @@
import ClassManager from "./ClassManager";
export default ClassManager;
export { ClassManager };
@@ -0,0 +1,673 @@
import axios, { Canceler } from "axios";
import React from "react";
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
ApiAdapterUtils,
ApiResponse,
CharacterPreferences,
CharClass,
ChoiceData,
ChoiceUtils,
ClassDefinitionContract,
ClassFeature,
ClassFeatureUtils,
ClassUtils,
Constants,
DefinitionPool,
EntitledEntity,
Feat,
FeatDefinitionContract,
FeatLookup,
FeatUtils,
FormatUtils,
HelperUtils,
InfusionChoice,
InfusionChoiceUtils,
InfusionDefinitionContract,
InfusionUtils,
InventoryManager,
KnownInfusionUtils,
Modifier,
OptionalClassFeatureLookup,
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 { 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;
charClass: CharClass;
choiceInfo: ChoiceData;
feature: ClassFeature;
prerequisiteData: PrerequisiteData;
isStartingClass: boolean;
preferences: CharacterPreferences;
globalModifiers: Array<Modifier>;
typeValueLookup: TypeValueLookup;
ruleData: RuleData;
definitionPool: DefinitionPool;
knownInfusionLookup: Record<string, InfusionChoice>;
knownReplicatedItems: Array<string>;
onChoiceChange?: (
classFeatureId: number,
choiceId: string,
type: any,
value: number | null,
parentChoiceId: string | null
) => void;
onInfusionChoiceItemChangePromise?: (
infusionChoiceKey: string,
infusionId: string,
itemDefinitionKey: string,
itemName: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceItemDestroyPromise?: (
infusionChoiceKey: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceChangePromise?: (
infusionChoiceKey: string,
infusionId: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceDestroyPromise?: (
infusionChoiceKey: string,
accept: () => void,
reject: () => void
) => void;
onInfusionChoiceCreatePromise?: (
infusionChoiceKey: string,
infusionId: string,
accept: () => void,
reject: () => void
) => void;
loadAvailableSubclasses: (
baseClassId: number,
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<ApiResponse<Array<ClassDefinitionContract>>>;
loadAvailableFeats: (
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<ApiResponse<Array<FeatDefinitionContract>>>;
loadAvailableInfusions: (
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<
ApiResponse<EntitledEntity<InfusionDefinitionContract>>
>;
onDefinitionsLoaded?: (
definitions: Array<InfusionDefinitionContract>,
accessTypes: Record<string, number>
) => void;
featLookup: FeatLookup;
optionalClassFeatureLookup: OptionalClassFeatureLookup;
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,
State
> {
static defaultProps = {
isActive: true,
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 {
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(
(choice) =>
ChoiceUtils.getType(choice) ===
Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION
);
};
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,
});
};
handleChoiceChange = (
id: string,
type: number,
value: any,
parentChoiceId: string | null
): void => {
const { onChoiceChange, feature } = this.props;
if (onChoiceChange) {
onChoiceChange(
ClassFeatureUtils.getId(feature),
id,
type,
HelperUtils.parseInputInt(value),
parentChoiceId
);
}
};
getAvailableInfusionChoices = (): Array<InfusionChoice> => {
const { feature } = this.props;
return ClassFeatureUtils.getInfusionChoices(feature).filter(
InfusionChoiceUtils.validateIsAvailable
);
};
getInfusionChoiceCount = (): number => {
return this.getAvailableInfusionChoices().length;
};
getTodoInfusionChoiceCount = (): number => {
return this.getAvailableInfusionChoices().filter(
(infusionChoice) =>
InfusionChoiceUtils.getKnownInfusion(infusionChoice) === null
).length;
};
getInfusionItemChoiceCount = (): number => {
return this.getAvailableInfusionChoices().filter((infusionChoice) => {
const knownInfusion =
InfusionChoiceUtils.getKnownInfusion(infusionChoice);
if (knownInfusion === null) {
return false;
}
const simulatedInfusion =
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
if (simulatedInfusion === null) {
return false;
}
return (
InfusionUtils.getType(simulatedInfusion) ===
Constants.InfusionTypeEnum.REPLICATE
);
}).length;
};
getTodoInfusionItemChoiceCount = (): number => {
return this.getAvailableInfusionChoices().filter((infusionChoice) => {
const knownInfusion =
InfusionChoiceUtils.getKnownInfusion(infusionChoice);
if (knownInfusion === null) {
return false;
}
const simulatedInfusion =
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
if (simulatedInfusion === null) {
return false;
}
if (
InfusionUtils.getType(simulatedInfusion) !==
Constants.InfusionTypeEnum.REPLICATE
) {
return false;
}
if (KnownInfusionUtils.getItemId(knownInfusion) !== null) {
return false;
}
return true;
}).length;
};
getChoiceCount = (): number => {
const { feature } = this.props;
return ClassFeatureUtils.getChoices(feature).length;
};
getTodoChoiceCount = (): number => {
const { feature } = this.props;
return ClassFeatureUtils.getChoices(feature).filter(ChoiceUtils.isTodo)
.length;
};
getTotalChoiceCount = (): number => {
return (
this.getChoiceCount() +
this.getInfusionChoiceCount() +
this.getInfusionItemChoiceCount()
);
};
getTotalTodoCount = (): number => {
return (
this.getTodoChoiceCount() +
this.getTodoInfusionChoiceCount() +
this.getTodoInfusionItemChoiceCount()
);
};
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;
const multiClassDescription =
ClassFeatureUtils.getMultiClassDescription(feature);
if (
!isStartingClass &&
multiClassDescription &&
multiClassDescription.length > 0
) {
return multiClassDescription;
}
const description = ClassFeatureUtils.getDescription(feature);
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();
const featureType = ClassFeatureUtils.getFeatureType(feature);
let metaItems: Array<string> = [];
// Display the number of choices if there are any
if (choiceCount) {
metaItems.push(`${choiceCount} Choice${choiceCount !== 1 ? "s" : ""}`);
}
// Display the required level
metaItems.push(
`${FormatUtils.ordinalize(
ClassFeatureUtils.getRequiredLevel(feature)
)} level`
);
// Display that this is an optional class feature if it is
if (featureType !== Constants.FeatureTypeEnum.GRANTED) {
metaItems.push("Optional Class Feature");
}
// Display the feature that this feature replaces if it is a replacement
if (featureType === Constants.FeatureTypeEnum.REPLACEMENT) {
const optionalFeature = HelperUtils.lookupDataOrFallback(
optionalClassFeatureLookup,
ClassFeatureUtils.getDefinitionKey(feature)
);
if (optionalFeature) {
const affectedDefinitionKey =
OptionalClassFeatureUtils.getAffectedClassFeatureDefinitionKey(
optionalFeature
);
if (affectedDefinitionKey) {
const replacedFeature = ClassFeatureUtils.simulateClassFeature(
affectedDefinitionKey,
definitionPool
);
if (replacedFeature) {
metaItems.push(
`Replaces ${ClassFeatureUtils.getName(replacedFeature)}`
);
}
}
}
}
return metaItems;
};
render() {
const {
isActive,
charClass,
definitionPool,
ruleData,
globalModifiers,
typeValueLookup,
loadAvailableInfusions,
onInfusionChoiceItemChangePromise,
onInfusionChoiceItemDestroyPromise,
onInfusionChoiceCreatePromise,
onInfusionChoiceDestroyPromise,
onInfusionChoiceChangePromise,
onDefinitionsLoaded,
knownReplicatedItems,
knownInfusionLookup,
feature,
inventoryManager,
} = this.props;
const { hasSubclassChoice, hasFeatChoice } = this.state;
// If the class feature grants feats, render those instead of the class feature.
if (feature.featLists.length > 0) {
return feature.featLists.map((fl) => (
<GrantedFeat
featList={fl}
key={fl.definition.id}
requiredLevel={ClassFeatureUtils.getRequiredLevel(feature)}
/>
));
}
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();
return (
<Accordion
id={accordionId}
summary={ClassFeatureUtils.getName(feature)}
summaryMetaItems={this.getMetaItems()}
variant="paper"
showAlert={hasChoices}
handleIsOpen={this.handleCollapsibleOpened}
>
<CollapsibleContent className="class-manager-feature-description">
{this.getClassFeatureDescription()}
</CollapsibleContent>
{isActive && 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 && (
<InfusionChoiceManager
infusionChoices={this.getAvailableInfusionChoices()}
contextLevel={ClassUtils.getLevel(charClass)}
definitionPool={definitionPool}
ruleData={ruleData}
globalModifiers={globalModifiers}
typeValueLookup={typeValueLookup}
knownInfusionLookup={knownInfusionLookup}
knownReplicatedItems={knownReplicatedItems}
loadAvailableInfusions={loadAvailableInfusions}
onInfusionChoiceItemChangePromise={
onInfusionChoiceItemChangePromise
}
onInfusionChoiceItemDestroyPromise={
onInfusionChoiceItemDestroyPromise
}
onInfusionChoiceChangePromise={onInfusionChoiceChangePromise}
onInfusionChoiceDestroyPromise={onInfusionChoiceDestroyPromise}
onInfusionChoiceCreatePromise={onInfusionChoiceCreatePromise}
onDefinitionsLoaded={onDefinitionsLoaded}
inventoryManager={inventoryManager}
/>
)}
</Accordion>
);
}
}
@@ -0,0 +1,4 @@
import ClassManagerFeature from "./ClassManagerFeature";
export default ClassManagerFeature;
export { ClassManagerFeature };
@@ -0,0 +1,374 @@
import axios, { Canceler } from "axios";
import React from "react";
import {
MarketplaceCta,
LoadingPlaceholder,
} from "@dndbeyond/character-components/es";
import {
ApiAdapterUtils,
CharClass,
ClassFeature,
ClassFeatureUtils,
Constants,
DefinitionPool,
DefinitionPoolUtils,
DefinitionUtils,
OptionalClassFeature,
OptionalClassFeatureLookup,
OptionalClassFeatureUtils,
ClassUtils,
HelperUtils,
RuleData,
} from "@dndbeyond/character-rules-engine/es";
import { Link } from "~/components/Link";
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
import {
ApiClassFeatureResponseData,
ApiClassFeaturesRequest,
} from "../../../../../Shared/selectors/composite/apiCreator";
import { AppLoggerUtils, TypeScriptUtils } from "../../../../../Shared/utils";
import { OptionalFeature } from "../../../../components/OptionalFeature";
import { AffectedFeatureInfo } from "../../../../components/OptionalFeature/OptionalFeature";
import { PageSubHeader } from "../../../../components/PageSubHeader";
interface OptionalFeatureManagerProps {
charClass: CharClass;
definitionPool: DefinitionPool;
optionalClassFeatureLookup: OptionalClassFeatureLookup;
loadAvailableOptionalClassFeatures?: ApiClassFeaturesRequest | null;
onDefinitionsLoaded?: (definitionData: ApiClassFeatureResponseData) => void;
onSelection: (
definitionKey: string,
affectedClassFeatureDefinitionKey: string | null
) => void;
onChangeReplacementPromise?: (
definitionKey: string,
newAffectedDefinitionKey: string | null,
oldAffectedDefinitionKey: string | null,
accept: () => void,
reject: () => void
) => void;
onRemoveSelectionPromise?: (
definitionKey: string,
newIsEnabled: boolean,
accept: () => void,
reject: () => void
) => void;
ruleData: RuleData;
}
interface OptionalFeatureManagerState {
loadingStatus: DataLoadingStatusEnum;
}
export class OptionalFeatureManager extends React.PureComponent<
OptionalFeatureManagerProps,
OptionalFeatureManagerState
> {
loadOptionalOriginsCanceler: null | Canceler = null;
constructor(props: OptionalFeatureManagerProps) {
super(props);
this.state = {
loadingStatus: DataLoadingStatusEnum.NOT_LOADED,
};
}
componentDidMount() {
const { loadAvailableOptionalClassFeatures, onDefinitionsLoaded } =
this.props;
const { loadingStatus } = this.state;
if (
loadAvailableOptionalClassFeatures &&
loadingStatus === DataLoadingStatusEnum.NOT_LOADED
) {
this.setState({
loadingStatus: DataLoadingStatusEnum.LOADING,
});
loadAvailableOptionalClassFeatures({
cancelToken: new axios.CancelToken((c) => {
this.loadOptionalOriginsCanceler = c;
}),
})
.then((response) => {
let data = ApiAdapterUtils.getResponseData(response);
if (data?.definitionData.length && onDefinitionsLoaded) {
onDefinitionsLoaded(data);
}
this.setState({
loadingStatus: DataLoadingStatusEnum.LOADED,
});
this.loadOptionalOriginsCanceler = null;
})
.catch(AppLoggerUtils.handleAdhocApiError);
} else {
this.setState({
loadingStatus: DataLoadingStatusEnum.LOADED,
});
}
}
componentWillUnmount(): void {
if (this.loadOptionalOriginsCanceler !== null) {
this.loadOptionalOriginsCanceler();
}
}
getAffectedFeatureInfos = (
optionalClassFeature: ClassFeature
): Array<AffectedFeatureInfo> => {
const { definitionPool, charClass } = this.props;
return ClassFeatureUtils.getAffectedFeatureDefinitionKeys(
optionalClassFeature
)
.map((definitionKey: string) =>
ClassFeatureUtils.simulateClassFeature(definitionKey, definitionPool)
)
.filter(TypeScriptUtils.isNotNullOrUndefined)
.map((affectedClassFeature: ClassFeature) => {
const definitionKey =
ClassFeatureUtils.getDefinitionKey(affectedClassFeature);
let disabled = ClassUtils.getOptionalClassFeatures(charClass).some(
(optionalFeatureMapping: OptionalClassFeature) =>
OptionalClassFeatureUtils.getDefinitionKey(
optionalFeatureMapping
) !== ClassFeatureUtils.getDefinitionKey(optionalClassFeature) &&
OptionalClassFeatureUtils.getAffectedClassFeatureDefinitionKey(
optionalFeatureMapping
) === definitionKey
);
return {
name: ClassFeatureUtils.getName(affectedClassFeature),
definitionKey,
disabled,
};
});
};
renderOptionalFeatureCta = (): React.ReactNode => {
return (
<div className="ct-optional-feature-manager__content">
<div className="ct-optional-feature-manager__content--empty">
<span>
You currently have no available optional class features for this
class
</span>
<div className="ct-character-tools__marketplace-callout">
Unlock all official options in the{" "}
<Link href="/marketplace">Marketplace</Link> or create them for{" "}
<Link href="/my-creations">Homebrew</Link> Subclasses.
</div>
</div>
</div>
);
};
render() {
const { loadingStatus } = this.state;
const {
definitionPool,
charClass,
optionalClassFeatureLookup,
onChangeReplacementPromise,
onRemoveSelectionPromise,
onSelection,
} = this.props;
let contentNode: React.ReactNode;
if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
contentNode = <LoadingPlaceholder />;
} else {
let availableOptionalFeatures: Array<ClassFeature> =
DefinitionPoolUtils.getTypedDefinitionList(
Constants.DefinitionTypeEnum.CLASS_FEATURE,
definitionPool
)
.map((featureDefinition) =>
ClassFeatureUtils.simulateClassFeature(
DefinitionUtils.getDefinitionKey(featureDefinition),
definitionPool
)
)
.filter(TypeScriptUtils.isNotNullOrUndefined)
.filter((classFeature) =>
ClassFeatureUtils.isValidClassClassFeature(charClass, classFeature)
)
.filter(
(classFeature) =>
ClassFeatureUtils.getFeatureType(classFeature) !==
Constants.FeatureTypeEnum.GRANTED
);
let availableOptionalClassFeatureLookup =
HelperUtils.generateNonNullLookup(
availableOptionalFeatures,
ClassFeatureUtils.getDefinitionKey
);
const unEntitledOptionalClassFeatures: Array<ClassFeature> = [];
Object.keys(optionalClassFeatureLookup).forEach((definitionKey) => {
if (
!availableOptionalClassFeatureLookup.hasOwnProperty(definitionKey)
) {
const classFeature = ClassFeatureUtils.simulateClassFeature(
definitionKey,
definitionPool
);
if (
classFeature &&
ClassFeatureUtils.isValidClassClassFeature(charClass, classFeature)
) {
unEntitledOptionalClassFeatures.push(classFeature);
}
}
});
const consolidatedOptionalOrigins = [
...availableOptionalFeatures,
...unEntitledOptionalClassFeatures,
].filter(
(feature) =>
!ClassFeatureUtils.getHideInContext(
feature,
Constants.AppContextTypeEnum.BUILDER
)
);
if (!consolidatedOptionalOrigins.length) {
contentNode = this.renderOptionalFeatureCta();
} else {
const replacementFeatures: Array<ClassFeature> = [];
const additionalFeatures: Array<ClassFeature> = [];
consolidatedOptionalOrigins.forEach((feature) => {
switch (ClassFeatureUtils.getFeatureType(feature)) {
case Constants.FeatureTypeEnum.ADDITIONAL:
additionalFeatures.push(feature);
break;
case Constants.FeatureTypeEnum.REPLACEMENT:
replacementFeatures.push(feature);
break;
default:
//not implemented
}
});
contentNode = (
<div className="ct-optional-feature-manager__content">
{replacementFeatures.length > 0 && (
<React.Fragment>
<PageSubHeader>Replacement Features</PageSubHeader>
{ClassUtils.deriveOrderedClassFeatures(replacementFeatures).map(
(feature) => {
const optionalFeatureMapping =
HelperUtils.lookupDataOrFallback(
optionalClassFeatureLookup,
ClassFeatureUtils.getDefinitionKey(feature)
);
const affectedFeatureDefinitionKey: string | null =
optionalFeatureMapping
? OptionalClassFeatureUtils.getAffectedClassFeatureDefinitionKey(
optionalFeatureMapping
)
: null;
return (
<OptionalFeature
key={ClassFeatureUtils.getDefinitionKey(feature)}
name={ClassFeatureUtils.getName(feature)}
description={ClassFeatureUtils.getDescription(feature)}
requiredLevel={ClassFeatureUtils.getRequiredLevel(
feature
)}
featureType={ClassFeatureUtils.getFeatureType(feature)}
definitionKey={ClassFeatureUtils.getDefinitionKey(
feature
)}
affectedFeatureDefinitionKey={
affectedFeatureDefinitionKey
}
isSelected={!!optionalFeatureMapping ?? false}
onSelection={onSelection}
onChangeReplacementPromise={onChangeReplacementPromise}
onRemoveSelectionPromise={onRemoveSelectionPromise}
affectedFeatures={this.getAffectedFeatureInfos(feature)}
accessType={ClassFeatureUtils.getAccessType(feature)}
/>
);
}
)}
</React.Fragment>
)}
{additionalFeatures.length > 0 && (
<React.Fragment>
<PageSubHeader>Additional Features</PageSubHeader>
{ClassUtils.deriveOrderedClassFeatures(additionalFeatures).map(
(feature) => {
const optionalFeatureMapping =
HelperUtils.lookupDataOrFallback(
optionalClassFeatureLookup,
ClassFeatureUtils.getDefinitionKey(feature)
);
const affectedFeatureDefinitionKey: string | null =
optionalFeatureMapping
? OptionalClassFeatureUtils.getAffectedClassFeatureDefinitionKey(
optionalFeatureMapping
)
: null;
return (
<OptionalFeature
key={ClassFeatureUtils.getDefinitionKey(feature)}
name={ClassFeatureUtils.getName(feature)}
description={ClassFeatureUtils.getDescription(feature)}
requiredLevel={ClassFeatureUtils.getRequiredLevel(
feature
)}
featureType={ClassFeatureUtils.getFeatureType(feature)}
definitionKey={ClassFeatureUtils.getDefinitionKey(
feature
)}
affectedFeatureDefinitionKey={
affectedFeatureDefinitionKey
}
isSelected={!!optionalFeatureMapping ?? false}
onSelection={onSelection}
onChangeReplacementPromise={onChangeReplacementPromise}
onRemoveSelectionPromise={onRemoveSelectionPromise}
affectedFeatures={this.getAffectedFeatureInfos(feature)}
accessType={ClassFeatureUtils.getAccessType(feature)}
/>
);
}
)}
</React.Fragment>
)}
</div>
);
}
}
return (
<div className="ct-optional-feature-manager">
<div className="ct-optional-feature-manager__intro">
Unlike the features in the Players Handbook, you dont gain the
features here automatically. Consulting with your DM, you decide
whether to gain a feature in this section if you meet the level
requirement noted in the features description. These features can be
selected separately from one another; you can use some, all, or none
of them.
</div>
{contentNode}
</div>
);
}
}
export default OptionalFeatureManager;
@@ -0,0 +1,5 @@
import ClassManagerFeature from "./ClassManagerFeature";
import ClassesManage from "./ClassesManage";
export default ClassesManage;
export { ClassesManage, ClassManagerFeature };