``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
626 lines
18 KiB
TypeScript
626 lines
18 KiB
TypeScript
import axios, { Canceler } from "axios";
|
|
import { orderBy } from "lodash";
|
|
import * as React from "react";
|
|
|
|
import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es";
|
|
import {
|
|
ApiAdapterPromise,
|
|
ApiAdapterRequestConfig,
|
|
ApiAdapterUtils,
|
|
ApiResponse,
|
|
Constants,
|
|
ContainerManager,
|
|
DefinitionPool,
|
|
DefinitionPoolUtils,
|
|
DefinitionUtils,
|
|
EntitledEntity,
|
|
HelperUtils,
|
|
HtmlSelectOption,
|
|
HtmlSelectOptionGroup,
|
|
InfusionChoice,
|
|
InfusionChoiceUtils,
|
|
InfusionDefinitionContract,
|
|
InfusionUtils,
|
|
InventoryManager,
|
|
Item,
|
|
ItemUtils,
|
|
KnownInfusionUtils,
|
|
Modifier,
|
|
RuleData,
|
|
RuleDataUtils,
|
|
TypeValueLookup,
|
|
} from "@dndbeyond/character-rules-engine/es";
|
|
|
|
import { HtmlContent } from "~/components/HtmlContent";
|
|
|
|
import DataLoadingStatusEnum from "../../constants/DataLoadingStatusEnum";
|
|
import { AppLoggerUtils, TypeScriptUtils } from "../../utils";
|
|
|
|
type OnInfusionChangeFunc = (
|
|
choiceKey: string,
|
|
infusionId: string | null,
|
|
oldInfusionId: string | null,
|
|
accept: () => void,
|
|
reject: () => void
|
|
) => void;
|
|
|
|
interface Props {
|
|
infusionChoices: Array<InfusionChoice>;
|
|
contextLevel: number | null;
|
|
globalModifiers: Array<Modifier>;
|
|
typeValueLookup: TypeValueLookup;
|
|
ruleData: RuleData;
|
|
definitionPool: DefinitionPool;
|
|
knownInfusionLookup: Record<string, InfusionChoice>;
|
|
knownReplicatedItems: Array<string>;
|
|
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;
|
|
loadAvailableInfusions: (
|
|
additionalConfig?: Partial<ApiAdapterRequestConfig>
|
|
) => ApiAdapterPromise<
|
|
ApiResponse<EntitledEntity<InfusionDefinitionContract>>
|
|
>;
|
|
onDefinitionsLoaded?: (
|
|
definitions: Array<InfusionDefinitionContract>,
|
|
accessTypes: Record<string, number>
|
|
) => void;
|
|
inventoryManager: InventoryManager;
|
|
}
|
|
interface State {
|
|
equipment: Array<Item>;
|
|
loadingStatus: DataLoadingStatusEnum;
|
|
}
|
|
class InfusionChoiceManager extends React.PureComponent<Props, State> {
|
|
static defaultProps = {
|
|
loadAvailableInfusions: null,
|
|
};
|
|
|
|
loadItemsCanceler: null | Canceler = null;
|
|
loadInfusionsCanceler: null | Canceler = null;
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
equipment: [],
|
|
loadingStatus: DataLoadingStatusEnum.NOT_LOADED,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
const { loadingStatus } = this.state;
|
|
const {
|
|
infusionChoices,
|
|
inventoryManager,
|
|
loadAvailableInfusions,
|
|
globalModifiers,
|
|
typeValueLookup,
|
|
ruleData,
|
|
onDefinitionsLoaded,
|
|
} = this.props;
|
|
|
|
if (
|
|
infusionChoices.length &&
|
|
loadingStatus === DataLoadingStatusEnum.NOT_LOADED
|
|
) {
|
|
this.setState({
|
|
loadingStatus: DataLoadingStatusEnum.LOADING,
|
|
});
|
|
|
|
// TODO fix typings "any" once axios fixes how all can have multiple return types
|
|
axios
|
|
.all<any>([
|
|
inventoryManager.getInventoryShoppe({
|
|
onSuccess: (shoppeContainer: ContainerManager) => {
|
|
this.setState({
|
|
equipment: shoppeContainer
|
|
.getInventoryItems()
|
|
.items.map((item) => item.getItem()),
|
|
loadingStatus: DataLoadingStatusEnum.LOADED,
|
|
});
|
|
},
|
|
additionalApiConfig: {
|
|
cancelToken: new axios.CancelToken((c) => {
|
|
this.loadItemsCanceler = c;
|
|
}),
|
|
},
|
|
}),
|
|
loadAvailableInfusions({
|
|
cancelToken: new axios.CancelToken((c) => {
|
|
this.loadInfusionsCanceler = c;
|
|
}),
|
|
}),
|
|
])
|
|
.then(([equipmentResponse, infusionResponse]) => {
|
|
//do nothing with equipmentResponse since we moved converted to the inventoryManager which sets state on onsuccess
|
|
|
|
let infusionData =
|
|
ApiAdapterUtils.getResponseData<
|
|
EntitledEntity<InfusionDefinitionContract>
|
|
>(infusionResponse);
|
|
if (
|
|
infusionData &&
|
|
infusionData.definitionData.length > 0 &&
|
|
onDefinitionsLoaded
|
|
) {
|
|
onDefinitionsLoaded(
|
|
infusionData.definitionData,
|
|
infusionData.accessTypes
|
|
);
|
|
}
|
|
|
|
this.loadItemsCanceler = null;
|
|
this.loadInfusionsCanceler = null;
|
|
})
|
|
.catch(AppLoggerUtils.handleAdhocApiError);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount(): void {
|
|
if (this.loadItemsCanceler !== null) {
|
|
this.loadItemsCanceler();
|
|
}
|
|
if (this.loadInfusionsCanceler !== null) {
|
|
this.loadInfusionsCanceler();
|
|
}
|
|
}
|
|
|
|
getItemKeyParts = (itemKey: string): Array<string> => {
|
|
return itemKey.split("|");
|
|
};
|
|
|
|
getItemKey = (id: number, name: string): string => {
|
|
return [
|
|
DefinitionUtils.hack__generateDefinitionKey(
|
|
Constants.FUTURE_ITEM_DEFINITION_TYPE,
|
|
id
|
|
),
|
|
name,
|
|
].join("|");
|
|
};
|
|
|
|
handleChoiceItemChangePromise = (
|
|
infusionChoice: InfusionChoice,
|
|
itemKey: string | null,
|
|
oldItemKey: string | null,
|
|
accept: () => void,
|
|
reject: () => void
|
|
): void => {
|
|
const {
|
|
onInfusionChoiceItemChangePromise,
|
|
onInfusionChoiceItemDestroyPromise,
|
|
} = this.props;
|
|
|
|
const choiceKey = InfusionChoiceUtils.getKey(infusionChoice);
|
|
|
|
if (choiceKey === null) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
if (itemKey) {
|
|
if (onInfusionChoiceItemChangePromise) {
|
|
const knownInfusion =
|
|
InfusionChoiceUtils.getKnownInfusion(infusionChoice);
|
|
if (knownInfusion === null) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
const simulatedInfusion =
|
|
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
|
|
if (simulatedInfusion === null) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
const infusionId = InfusionUtils.getId(simulatedInfusion);
|
|
if (infusionId === null) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
const itemKeyParts = this.getItemKeyParts(itemKey);
|
|
|
|
onInfusionChoiceItemChangePromise(
|
|
choiceKey,
|
|
infusionId,
|
|
itemKeyParts[0],
|
|
itemKeyParts[1],
|
|
accept,
|
|
reject
|
|
);
|
|
}
|
|
} else {
|
|
if (onInfusionChoiceItemDestroyPromise) {
|
|
onInfusionChoiceItemDestroyPromise(choiceKey, accept, reject);
|
|
}
|
|
}
|
|
};
|
|
|
|
handleChoiceChangePromise = (
|
|
choiceKey: string,
|
|
infusionId: string | null,
|
|
oldInfusionId: string | null,
|
|
accept: () => void,
|
|
reject: () => void
|
|
): void => {
|
|
const { onInfusionChoiceChangePromise, onInfusionChoiceDestroyPromise } =
|
|
this.props;
|
|
|
|
if (infusionId) {
|
|
if (onInfusionChoiceChangePromise) {
|
|
onInfusionChoiceChangePromise(choiceKey, infusionId, accept, reject);
|
|
}
|
|
} else {
|
|
if (onInfusionChoiceDestroyPromise) {
|
|
onInfusionChoiceDestroyPromise(choiceKey, accept, reject);
|
|
}
|
|
}
|
|
};
|
|
|
|
handleChoiceCreatePromise = (
|
|
choiceKey: string,
|
|
infusionId: string,
|
|
oldInfusionId: string | null,
|
|
accept: () => void,
|
|
reject: () => void
|
|
) => {
|
|
const { onInfusionChoiceCreatePromise } = this.props;
|
|
|
|
if (onInfusionChoiceCreatePromise) {
|
|
onInfusionChoiceCreatePromise(choiceKey, infusionId, accept, reject);
|
|
}
|
|
};
|
|
|
|
renderInfusionReplicateItemChoice = (
|
|
infusionChoice: InfusionChoice
|
|
): React.ReactNode => {
|
|
const { equipment, loadingStatus } = this.state;
|
|
const { contextLevel, knownReplicatedItems } = this.props;
|
|
|
|
let knownInfusion = InfusionChoiceUtils.getKnownInfusion(infusionChoice);
|
|
if (knownInfusion === null) {
|
|
return null;
|
|
}
|
|
|
|
let simulatedInfusion =
|
|
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
|
|
if (
|
|
simulatedInfusion === null ||
|
|
InfusionUtils.getType(simulatedInfusion) !==
|
|
Constants.InfusionTypeEnum.REPLICATE
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
let contentNode: React.ReactNode;
|
|
const classNames: Array<string> = [
|
|
"ct-infusion-choice-manager__choice",
|
|
"ct-infusion-choice-manager__choice--child",
|
|
];
|
|
|
|
if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
|
|
contentNode = <LoadingPlaceholder />;
|
|
} else {
|
|
let groups: Array<HtmlSelectOptionGroup> = [];
|
|
let infusableItems: Array<Item> = equipment.filter((item) => {
|
|
const levelInfusionGranted = ItemUtils.getLevelInfusionGranted(item);
|
|
const itemDefinitionKey = ItemUtils.getDefinitionKey(item);
|
|
let chosenItemDefinitionKey: string | null = null;
|
|
if (knownInfusion !== null) {
|
|
chosenItemDefinitionKey =
|
|
KnownInfusionUtils.getItemDefinitionKey(knownInfusion);
|
|
}
|
|
const hasKnownReplicatedItem: boolean =
|
|
knownReplicatedItems.includes(itemDefinitionKey);
|
|
if (
|
|
hasKnownReplicatedItem &&
|
|
chosenItemDefinitionKey === itemDefinitionKey
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
!hasKnownReplicatedItem &&
|
|
levelInfusionGranted !== null &&
|
|
contextLevel !== null &&
|
|
contextLevel >= levelInfusionGranted
|
|
);
|
|
});
|
|
|
|
let orderedInfusableItems = orderBy(infusableItems, (item) =>
|
|
ItemUtils.getName(item)
|
|
);
|
|
|
|
orderedInfusableItems.forEach((item) => {
|
|
const levelInfusionGranted = ItemUtils.getLevelInfusionGranted(item);
|
|
|
|
if (levelInfusionGranted === null) {
|
|
return;
|
|
}
|
|
|
|
if (!groups.hasOwnProperty(levelInfusionGranted)) {
|
|
groups[levelInfusionGranted] = {
|
|
optGroupLabel: `Level ${levelInfusionGranted}`,
|
|
options: [],
|
|
};
|
|
}
|
|
|
|
const definitionName = ItemUtils.getDefinitionName(item);
|
|
groups[levelInfusionGranted].options.push({
|
|
label: definitionName,
|
|
value: this.getItemKey(
|
|
ItemUtils.getId(item),
|
|
definitionName === null ? "" : definitionName
|
|
),
|
|
});
|
|
});
|
|
|
|
groups.forEach((group) => {
|
|
group.options = orderBy(
|
|
group.options,
|
|
(option: HtmlSelectOption) => option.label
|
|
);
|
|
});
|
|
|
|
let selectedValue: string | null = null;
|
|
let knownItemId = KnownInfusionUtils.getItemId(knownInfusion);
|
|
let knownItemName = KnownInfusionUtils.getItemName(knownInfusion);
|
|
if (knownItemId && knownItemName) {
|
|
selectedValue = this.getItemKey(knownItemId, knownItemName);
|
|
}
|
|
|
|
if (selectedValue === null) {
|
|
classNames.push("ct-infusion-choice-manager__choice--todo");
|
|
}
|
|
|
|
contentNode = (
|
|
<Select
|
|
options={groups}
|
|
onChangePromise={this.handleChoiceItemChangePromise.bind(
|
|
this,
|
|
infusionChoice
|
|
)}
|
|
value={selectedValue}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return <div className={classNames.join(" ")}>{contentNode}</div>;
|
|
};
|
|
|
|
renderInfusionChoice = (infusionChoice: InfusionChoice): React.ReactNode => {
|
|
const { ruleData, knownInfusionLookup, definitionPool, contextLevel } =
|
|
this.props;
|
|
|
|
// TODO check typing once definition pool is updated
|
|
let infusionDefinitions = DefinitionPoolUtils.getTypedDefinitionList(
|
|
Constants.DefinitionTypeEnum.INFUSION,
|
|
definitionPool,
|
|
true
|
|
);
|
|
let infusionOptions: Array<HtmlSelectOption> = infusionDefinitions
|
|
.map((infusionDefinition) =>
|
|
InfusionUtils.simulateInfusion(
|
|
DefinitionUtils.getDefinitionKey(infusionDefinition),
|
|
definitionPool
|
|
)
|
|
)
|
|
.filter(TypeScriptUtils.isNotNullOrUndefined)
|
|
.filter((infusion) => {
|
|
// if the infusion allows duplicates just let it through
|
|
if (InfusionUtils.getAllowDuplicates(infusion)) {
|
|
return true;
|
|
}
|
|
|
|
const infusionDefinitionKey = InfusionUtils.getDefinitionKey(infusion);
|
|
let knownInfusion =
|
|
InfusionChoiceUtils.getKnownInfusion(infusionChoice);
|
|
// if choice has the infusion, show it in the list
|
|
if (
|
|
knownInfusion !== null &&
|
|
KnownInfusionUtils.getDefinitionKey(knownInfusion) ===
|
|
infusionDefinitionKey
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// if its already selected somewhere else and is here, it shouldnt be shown
|
|
if (
|
|
infusionDefinitionKey !== null &&
|
|
HelperUtils.lookupDataOrFallback(
|
|
knownInfusionLookup,
|
|
infusionDefinitionKey
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
!InfusionUtils.validateIsAvailableByContextLevel(
|
|
infusion,
|
|
contextLevel
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.map((infusion) => {
|
|
let label: string | null = InfusionUtils.getName(infusion);
|
|
|
|
const sources = InfusionUtils.getSources(infusion);
|
|
|
|
const hasAllToggleableSources: boolean = sources.every(
|
|
(sourceMapping) => {
|
|
const sourceDataInfo = RuleDataUtils.getSourceDataInfo(
|
|
sourceMapping.sourceId,
|
|
ruleData
|
|
);
|
|
return sourceDataInfo?.sourceCategory?.isToggleable;
|
|
}
|
|
);
|
|
|
|
if (sources.length && hasAllToggleableSources) {
|
|
const calloutSources: Array<string> = sources
|
|
.map((sourceMapping) => {
|
|
const sourceDataInfo = RuleDataUtils.getSourceDataInfo(
|
|
sourceMapping.sourceId,
|
|
ruleData
|
|
);
|
|
return sourceDataInfo?.name ?? null;
|
|
})
|
|
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
|
|
|
label = `${calloutSources.join(", ")} • ${label}`;
|
|
}
|
|
|
|
const infusionId = InfusionUtils.getId(infusion);
|
|
return {
|
|
label,
|
|
value: infusionId ?? "",
|
|
};
|
|
});
|
|
|
|
let knownInfusion = InfusionChoiceUtils.getKnownInfusion(infusionChoice);
|
|
|
|
//TODO this typing is weird because of the binding that will happen further down... may need to split this into more components to remove the bind and make more sense
|
|
let onChangePromise: OnInfusionChangeFunc = this.handleChoiceCreatePromise;
|
|
if (knownInfusion !== null) {
|
|
onChangePromise = this.handleChoiceChangePromise;
|
|
}
|
|
|
|
let warningNode: React.ReactNode;
|
|
let descriptionNode: React.ReactNode;
|
|
const classNames: Array<string> = ["ct-infusion-choice-manager__choice"];
|
|
if (knownInfusion === null) {
|
|
classNames.push("ct-infusion-choice-manager__choice--todo");
|
|
} else {
|
|
const simulatedInfusion =
|
|
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
|
|
if (simulatedInfusion !== null) {
|
|
const description = InfusionUtils.getDescription(simulatedInfusion);
|
|
if (description !== null) {
|
|
descriptionNode = (
|
|
<HtmlContent
|
|
className="ct-infusion-choice-manager__choice-description"
|
|
html={description}
|
|
withoutTooltips
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (
|
|
!InfusionUtils.validateIsAvailableByContextLevel(
|
|
simulatedInfusion,
|
|
contextLevel
|
|
)
|
|
) {
|
|
classNames.push("ct-infusion-choice-manager__choice--warning");
|
|
warningNode = (
|
|
<div className="ct-infusion-choice-manager__choice-warning">
|
|
<strong>{InfusionUtils.getName(simulatedInfusion)}</strong> is not
|
|
a valid choice because it requires level{" "}
|
|
<strong>{InfusionUtils.getLevel(simulatedInfusion)}</strong>.
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let knownInfusionDefinitionKey: string | null = null;
|
|
if (knownInfusion !== null) {
|
|
knownInfusionDefinitionKey =
|
|
KnownInfusionUtils.getDefinitionKey(knownInfusion);
|
|
}
|
|
|
|
const choiceKey = InfusionChoiceUtils.getKey(infusionChoice);
|
|
return (
|
|
<div
|
|
className={classNames.join(" ")}
|
|
key={choiceKey === null ? "" : choiceKey}
|
|
>
|
|
<Select
|
|
options={infusionOptions}
|
|
placeholder={`- Choose a Level ${InfusionChoiceUtils.getLevel(
|
|
infusionChoice
|
|
)} Option -`}
|
|
onChangePromise={onChangePromise.bind(this, choiceKey)}
|
|
value={
|
|
knownInfusionDefinitionKey === null
|
|
? null
|
|
: DefinitionUtils.getDefinitionKeyId(knownInfusionDefinitionKey)
|
|
}
|
|
/>
|
|
{this.renderInfusionReplicateItemChoice(infusionChoice)}
|
|
{warningNode}
|
|
{descriptionNode}
|
|
</div>
|
|
);
|
|
//`
|
|
};
|
|
|
|
render() {
|
|
const { loadingStatus } = this.state;
|
|
const { infusionChoices } = this.props;
|
|
|
|
if (!infusionChoices.length) {
|
|
return null;
|
|
}
|
|
|
|
let contentNode: React.ReactNode;
|
|
if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
|
|
contentNode = <LoadingPlaceholder />;
|
|
} else {
|
|
contentNode = (
|
|
<React.Fragment>
|
|
{infusionChoices.map((infusionChoice) =>
|
|
this.renderInfusionChoice(infusionChoice)
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="ct-infusion-choice-manager">
|
|
<div className="ct-infusion-choice-manager__header">
|
|
Infusion Choices
|
|
</div>
|
|
{contentNode}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default InfusionChoiceManager;
|