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:
+484
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import ClassManager from "./ClassManager";
|
||||
|
||||
export default ClassManager;
|
||||
export { ClassManager };
|
||||
+673
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import ClassManagerFeature from "./ClassManagerFeature";
|
||||
|
||||
export default ClassManagerFeature;
|
||||
export { ClassManagerFeature };
|
||||
File diff suppressed because it is too large
Load Diff
+374
@@ -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 Player’s Handbook, you don’t 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 feature’s 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 };
|
||||
Reference in New Issue
Block a user