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,908 @@
import axios, { Canceler } from "axios";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import {
Button,
LoadingPlaceholder,
Select,
} from "@dndbeyond/character-components/es";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
ApiAdapterUtils,
ApiResponse,
Background,
BackgroundUtils,
BaseItemDefinitionContract,
characterActions,
CharClass,
ClassUtils,
Constants,
ContainerUtils,
DiceContract,
DiceUtils,
HelperUtils,
Modifier,
RuleData,
rulesEngineSelectors,
StartingEquipmentContract,
StartingEquipmentSlotContract,
TypeValueLookup,
DefinitionUtils,
} from "@dndbeyond/character-rules-engine/es";
import { Link } from "~/components/Link";
// TODO need to remove this builder reference and replace with better heading
import PageSubHeader from "../../../CharacterBuilder/components/PageSubHeader";
import { ThemeButton } from "../../components/common/Button";
import DataLoadingStatusEnum from "../../constants/DataLoadingStatusEnum";
import * as apiCreatorSelectors from "../../selectors/composite/apiCreator";
import { SharedAppState } from "../../stores/typings";
import { AppLoggerUtils, AppNotificationUtils } from "../../utils";
import StartingEquipmentSlots from "./StartingEquipmentSlots";
import { StartingEquipmentRuleSlotSelection } from "./typings";
//duplicated from characterActions typingParts to avoid import issues
interface StartingEquipmentAddRequestPayloadItem {
containerEntityId: number | null;
containerEntityTypeId: number | null;
entityId: number;
entityTypeId: number;
quantity: number;
}
//duplicated from characterActions typingParts to avoid import issues
interface StartingEquipmentAddRequestPayload {
items: Array<StartingEquipmentAddRequestPayloadItem>;
gold: number;
custom: Array<string>;
}
type StartingEquipmentChoice = "equipment" | "gold";
type RuleSlotChoiceKey = "startingClass" | "background";
interface RuleChoiceGroupSelectionPayload {
groupKey: RuleSlotChoiceKey;
slotIdx: number;
ruleSlotIdx: number;
ruleIdx: number;
dataId: number | null;
activeRuleSlotIdx: number | null;
}
interface RuleChoiceGroupClearPayload {
groupKey: RuleSlotChoiceKey;
}
interface Props extends DispatchProp {
isInitialView: boolean;
onStartingEquipmentChoose?: () => void;
startingClass: CharClass | null;
background: Background | null;
loadClassStartingEquipment: (
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<ApiResponse<StartingEquipmentContract>>;
loadBackgroundStartingEquipment: (
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<ApiResponse<StartingEquipmentContract>>;
globalModifiers: Array<Modifier>;
valueLookupByType: TypeValueLookup;
ruleData: RuleData;
characterId: number;
}
interface SlotChoiceInfoState {
activeRuleSlots: Array<number | null>;
dataValues: Record<string, number | null>;
}
type RuleSlotChoicesState = Record<RuleSlotChoiceKey, SlotChoiceInfoState>;
interface State {
activeChoice: StartingEquipmentChoice | null;
startingRules: Record<RuleSlotChoiceKey, StartingEquipmentContract>;
loadingStatus: DataLoadingStatusEnum;
loadingRuleTypes: Array<RuleSlotChoiceKey>;
ruleSlotChoices: RuleSlotChoicesState;
rolledGoldTotal: number | null;
}
class StartingEquipment extends React.PureComponent<Props, State> {
static defaultProps = {
isInitialView: false,
};
loadRuleCancelers: Array<Canceler> = [];
defaultStartingRules: Record<RuleSlotChoiceKey, StartingEquipmentContract> = {
startingClass: {
slots: [],
},
background: {
slots: [],
},
};
constructor(props) {
super(props);
this.state = {
activeChoice: null,
startingRules: this.defaultStartingRules,
loadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED,
loadingRuleTypes: [],
ruleSlotChoices: {
startingClass: {
activeRuleSlots: [],
dataValues: {},
},
background: {
activeRuleSlots: [],
dataValues: {},
},
},
rolledGoldTotal: null,
};
}
componentDidMount() {
const {
background,
startingClass,
loadClassStartingEquipment,
loadBackgroundStartingEquipment,
} = this.props;
let requests: Array<
ApiAdapterPromise<ApiResponse<StartingEquipmentContract>>
> = [];
let loadingRuleTypes: Array<RuleSlotChoiceKey> = [];
if (
background !== null &&
!BackgroundUtils.getHasCustomBackground(background)
) {
requests.push(
loadBackgroundStartingEquipment({
cancelToken: new axios.CancelToken((c) => {
this.loadRuleCancelers.push(c);
}),
})
);
loadingRuleTypes.push("background");
}
if (startingClass !== null) {
requests.push(
loadClassStartingEquipment({
cancelToken: new axios.CancelToken((c) => {
this.loadRuleCancelers.push(c);
}),
})
);
loadingRuleTypes.push("startingClass");
}
this.setState({
loadingStatus: DataLoadingStatusEnum.LOADING,
loadingRuleTypes,
});
axios
.all(requests)
.then((responses) => {
this.setState((prevState: State) => {
let startingRules: Record<
RuleSlotChoiceKey,
StartingEquipmentContract
> = prevState.startingRules;
for (var i = 0; i < responses.length; i++) {
let response = responses[i];
let loadingRuleType = prevState.loadingRuleTypes[i];
let rules = ApiAdapterUtils.getResponseData(response);
startingRules = {
...startingRules,
[loadingRuleType]: rules,
};
}
return {
startingRules,
loadingStatus: DataLoadingStatusEnum.LOADED,
};
});
this.loadRuleCancelers = [];
})
.catch(AppLoggerUtils.handleAdhocApiError);
}
componentWillUnmount(): void {
if (this.loadRuleCancelers.length > 0) {
this.loadRuleCancelers.forEach((cancel) => {
cancel();
});
}
}
handleChoiceClick = (choiceKey: StartingEquipmentChoice): void => {
this.setState((prevState: State) => ({
activeChoice: choiceKey,
ruleSlotChoices:
prevState.activeChoice !== choiceKey
? this.getRuleSlotChoicesDefaultState(prevState.startingRules)
: prevState.ruleSlotChoices,
}));
};
handleRuleSelection = (
groupKey: RuleSlotChoiceKey,
slotIdx: number,
ruleSlotIdx: number,
ruleIdx: number,
dataId: number | null
): void => {
this.setState((prevState: State) => {
return this.reduceSelectionForRuleChoiceGroup(prevState, {
groupKey,
slotIdx,
ruleSlotIdx,
ruleIdx,
dataId,
activeRuleSlotIdx:
prevState.ruleSlotChoices[groupKey].activeRuleSlots[slotIdx],
});
});
};
handleRuleSlotSelection = (
groupKey: RuleSlotChoiceKey,
slotIdx: number,
selectedRuleData: Array<StartingEquipmentRuleSlotSelection>
): void => {
if (!selectedRuleData.length) {
return;
}
this.setState((prevState: State) => {
let modifiedState = prevState;
selectedRuleData.forEach((ruleData) => {
const { ruleSlotIdx, ruleIdx, dataId } = ruleData;
let activeRuleSlotIdx =
prevState.ruleSlotChoices[groupKey].activeRuleSlots[slotIdx] ===
ruleSlotIdx
? null
: ruleSlotIdx;
modifiedState = this.reduceSelectionForRuleChoiceGroup(modifiedState, {
groupKey,
slotIdx,
ruleSlotIdx,
ruleIdx,
dataId,
activeRuleSlotIdx,
});
});
return modifiedState;
});
};
handleStartingEquipmentAdd = (): void => {
const { dispatch, onStartingEquipmentChoose } = this.props;
const startingEquipment = this.getCurrentStartingEquipmentSelected();
dispatch(
characterActions.startingEquipmentAddRequest(
startingEquipment,
AppNotificationUtils.handleStartingEquipmentAccepted,
AppNotificationUtils.handleStartingEquipmentRejected
)
);
if (onStartingEquipmentChoose) {
onStartingEquipmentChoose();
}
};
handleStartingGoldAdd = (): void => {
const { dispatch, onStartingEquipmentChoose } = this.props;
dispatch(
characterActions.startingGoldAddRequest(
this.getStartingGoldTotal(),
AppNotificationUtils.handleStartingGoldAccepted
)
);
if (onStartingEquipmentChoose) {
onStartingEquipmentChoose();
}
};
handleClearStartingEquipment = (): void => {
this.setState((prevState: State) => {
let modifiedState = prevState;
Object.keys(prevState.ruleSlotChoices).forEach(
(groupKey: RuleSlotChoiceKey) => {
modifiedState = this.reduceClearForRuleChoiceGroup(modifiedState, {
groupKey,
});
}
);
return modifiedState;
});
};
handleRandomizeGold = (): void => {
const { startingClass } = this.props;
if (startingClass) {
const wealthDice = this.getStartingClassWealthDice();
if (wealthDice) {
this.setState({
rolledGoldTotal: DiceUtils.getDiceRandomValue(wealthDice),
});
}
}
};
handleGoldRolledNumberChange = (value: string): void => {
this.setState({
rolledGoldTotal: HelperUtils.parseInputInt(value),
});
};
getRuleSlotChoicesDefaultState = (
startingRules: Record<RuleSlotChoiceKey, StartingEquipmentContract>
): RuleSlotChoicesState => {
let background = this.getSlotDefaultState(
startingRules.background && startingRules.background.slots
? startingRules.background.slots
: []
);
let startingClass = this.getSlotDefaultState(
startingRules.startingClass && startingRules.startingClass.slots
? startingRules.startingClass.slots
: []
);
return {
background,
startingClass,
};
};
getDataKey(slotIdx: number, ruleSlotIdx: number, ruleIdx: number): string {
return `${slotIdx}-${ruleSlotIdx}-${ruleIdx}`;
}
getSlotDefaultState(
slots: Array<StartingEquipmentSlotContract>
): SlotChoiceInfoState {
return slots.reduce(
(slotAcc, slot, slotIdx): SlotChoiceInfoState => {
let slotDataValues: Record<string, null> = {};
if (slot.ruleSlots !== null) {
slot.ruleSlots.reduce(
(acc, ruleSlot, ruleSlotIdx): Record<string, null> => {
if (ruleSlot.rules !== null) {
ruleSlot.rules.forEach((rule, ruleIdx) => {
if (rule.definitions !== null) {
rule.definitions.forEach((dataItem, dataId) => {
acc = {
...acc,
[this.getDataKey(slotIdx, ruleSlotIdx, ruleIdx)]: null,
};
});
}
});
}
return acc;
},
{}
);
}
return {
activeRuleSlots: [...slotAcc.activeRuleSlots, null],
dataValues: {
...slotAcc.dataValues,
...slotDataValues,
},
};
},
{ activeRuleSlots: [], dataValues: {} }
);
}
reduceSelectionForRuleChoiceGroup = (
state: State,
payload: RuleChoiceGroupSelectionPayload
): State => {
const {
groupKey,
slotIdx,
ruleSlotIdx,
ruleIdx,
dataId,
activeRuleSlotIdx,
} = payload;
return {
...state,
ruleSlotChoices: {
...state.ruleSlotChoices,
[groupKey]: {
activeRuleSlots: [
...state.ruleSlotChoices[groupKey].activeRuleSlots.slice(
0,
slotIdx
),
activeRuleSlotIdx,
...state.ruleSlotChoices[groupKey].activeRuleSlots.slice(
slotIdx + 1
),
],
dataValues: {
...state.ruleSlotChoices[groupKey].dataValues,
[this.getDataKey(slotIdx, ruleSlotIdx, ruleIdx)]: dataId,
},
},
},
};
};
processSlotGroups = (
groupData: StartingEquipmentContract | null,
groupChoices: SlotChoiceInfoState
): StartingEquipmentAddRequestPayload => {
const { characterId } = this.props;
let items: Array<StartingEquipmentAddRequestPayloadItem> = [];
let gold: number = 0;
let custom: Array<string> = [];
let slotGroups: StartingEquipmentAddRequestPayload = {
items,
gold,
custom,
};
if (!groupData || groupData.slots === null) {
return slotGroups;
}
groupData.slots.forEach((slot, slotIdx) => {
let activeRuleSlotIdx = groupChoices.activeRuleSlots[slotIdx];
if (activeRuleSlotIdx === null) {
return;
}
if (slot.ruleSlots === null) {
return;
}
slot.ruleSlots.forEach((ruleSlot, ruleSlotIdx) => {
if (activeRuleSlotIdx !== ruleSlotIdx) {
return;
}
if (ruleSlot.rules === null) {
return;
}
ruleSlot.rules.forEach((rule, ruleIdx) => {
let dataKey = this.getDataKey(slotIdx, ruleSlotIdx, ruleIdx);
if (groupChoices.activeRuleSlots[slotIdx] === null) {
return;
}
const characterContainerDefinitionKey =
ContainerUtils.getCharacterContainerDefinitionKey(characterId);
switch (rule.ruleType) {
case Constants.StartingEquipmentRuleTypeEnum.ARMOR:
case Constants.StartingEquipmentRuleTypeEnum.ARMOR_TYPE:
case Constants.StartingEquipmentRuleTypeEnum.GEAR:
case Constants.StartingEquipmentRuleTypeEnum.GEAR_TYPE:
case Constants.StartingEquipmentRuleTypeEnum.WEAPON:
case Constants.StartingEquipmentRuleTypeEnum.WEAPON_TYPE:
let activeDataId = groupChoices.dataValues[dataKey];
if (activeDataId === null) {
return;
}
let item: BaseItemDefinitionContract | null | undefined =
rule.definitions &&
rule.definitions.find(
(dataItem) => dataItem.id === activeDataId
);
if (item) {
items.push({
containerEntityId: DefinitionUtils.hack__getDefinitionKeyId(
characterContainerDefinitionKey
),
containerEntityTypeId:
DefinitionUtils.hack__getDefinitionKeyType(
characterContainerDefinitionKey
),
entityId: item.id,
entityTypeId: item.entityTypeId,
quantity: rule.quantity ? rule.quantity : 1,
});
}
break;
case Constants.StartingEquipmentRuleTypeEnum.GOLD_VALUE:
let ruleGold = rule.gold;
if (ruleGold !== null) {
gold += ruleGold;
}
break;
case Constants.StartingEquipmentRuleTypeEnum.CUSTOM:
if (rule.custom !== null) {
custom.push(rule.custom);
}
break;
default:
// not implemented
}
});
});
});
return {
...slotGroups,
gold,
};
};
getCurrentStartingEquipmentSelected =
(): StartingEquipmentAddRequestPayload => {
const { startingRules, ruleSlotChoices } = this.state;
const {
startingClass: classStartingEquipment,
background: backgroundStartingEquipment,
} = startingRules;
let startingClassAdds = this.processSlotGroups(
classStartingEquipment,
ruleSlotChoices.startingClass
);
let backgroundAdds = this.processSlotGroups(
backgroundStartingEquipment,
ruleSlotChoices.background
);
let items: Array<StartingEquipmentAddRequestPayloadItem> = [
...startingClassAdds.items,
...backgroundAdds.items,
];
let gold: number = startingClassAdds.gold + backgroundAdds.gold;
let custom: Array<string> = [
...startingClassAdds.custom,
...backgroundAdds.custom,
];
return {
items,
gold,
custom,
};
};
isStartingEquipmentEmpty = (): boolean => {
const startingEquipment = this.getCurrentStartingEquipmentSelected();
return (
!startingEquipment.items.length &&
!startingEquipment.gold &&
!startingEquipment.custom.length
);
};
reduceClearForRuleChoiceGroup = (
state: State,
payload: RuleChoiceGroupClearPayload
): State => {
const { groupKey } = payload;
return {
...state,
ruleSlotChoices: {
...state.ruleSlotChoices,
[groupKey]: {
...state.ruleSlotChoices[groupKey],
activeRuleSlots: state.ruleSlotChoices[groupKey].activeRuleSlots.map(
(ruleSlot) => null
),
},
},
};
};
getStartingGoldTotal = (): number => {
const { rolledGoldTotal } = this.state;
if (!rolledGoldTotal) {
return 0;
}
const wealthDice = this.getStartingClassWealthDice();
let diceMultiplier: number = 1;
if (wealthDice && wealthDice.diceMultiplier) {
diceMultiplier = wealthDice.diceMultiplier;
}
return rolledGoldTotal * diceMultiplier;
};
getStartingClassName = (): string | null => {
const { startingClass } = this.props;
if (!startingClass) {
return null;
}
return ClassUtils.getName(startingClass);
};
getStartingClassWealthDice = (): DiceContract | null => {
const { startingClass } = this.props;
if (!startingClass) {
return null;
}
return ClassUtils.getWealthDice(startingClass);
};
renderGoldChoiceUi = (): React.ReactNode => {
const { rolledGoldTotal } = this.state;
const { startingClass } = this.props;
if (startingClass) {
const wealthDice = this.getStartingClassWealthDice();
let diceCount: number | null = null;
let diceMultiplier: number | null = null;
let diceValue: number | null = null;
let diceValues: Array<number> = [];
if (wealthDice !== null) {
diceCount = DiceUtils.getCount(wealthDice);
diceMultiplier = DiceUtils.getMultiplier(wealthDice);
diceValue = DiceUtils.getValue(wealthDice);
diceValues = DiceUtils.getDiceValuesRange(wealthDice);
}
const diceOptions = diceValues.map((value) => ({
value,
label: value,
}));
let totalGold = this.getStartingGoldTotal();
return (
<div className="starting-equipment-gold">
<div className="starting-equipment-gold-explanation">
{this.getStartingClassName()} Starting Gold:
</div>
<div className="starting-equipment-gold-entry">
<div className="starting-equipment-gold-entry-actions">
<Link onClick={this.handleRandomizeGold}>Randomize</Link>
</div>
<div className="starting-equipment-gold-entry-input">
<Select
placeholder={`${diceCount}d${diceValue}`}
options={diceOptions}
onChange={this.handleGoldRolledNumberChange}
value={rolledGoldTotal}
/>
</div>
<div className="starting-equipment-gold-entry-multiplier">
{diceMultiplier ? ` x ${diceMultiplier} ` : ""} gp
</div>
{totalGold !== null && (
<div className="starting-equipment-gold-entry-total">
= {totalGold} gp
</div>
)}
</div>
<div className="starting-equipment-gold-actions">
<Button
onClick={this.handleStartingGoldAdd}
disabled={rolledGoldTotal === null}
>
Add Starting Gold
</Button>
</div>
</div>
);
}
return (
<div className="starting-equipment-requirements-missing">
Choose a class to view starting gold.
</div>
);
};
renderEquipmentChoiceUi = (): React.ReactNode => {
const {
background,
startingClass,
globalModifiers,
ruleData,
valueLookupByType,
} = this.props;
const { startingRules, ruleSlotChoices } = this.state;
const classStartingEquipment = HelperUtils.lookupDataOrFallback(
startingRules,
"startingClass"
);
const backgroundStartingEquipment = HelperUtils.lookupDataOrFallback(
startingRules,
"background"
);
let hasCustomBackground: boolean =
background !== null && BackgroundUtils.getHasCustomBackground(background);
let hasValidStartingClass: boolean =
startingClass !== null && classStartingEquipment !== null;
let hasValidBackground: boolean =
background !== null &&
backgroundStartingEquipment !== null &&
!hasCustomBackground;
if (!hasValidStartingClass && !hasValidBackground) {
return (
<div className="starting-equipment-requirements-missing">
Choose a class
{hasCustomBackground ? " " : " or background "}
to view starting equipment options.
</div>
);
}
return (
<div className="starting-equipment-gear">
{hasValidStartingClass &&
classStartingEquipment !== null &&
classStartingEquipment.slots !== null && (
<div>
<PageSubHeader>
{this.getStartingClassName()} Starting Equipment
</PageSubHeader>
<StartingEquipmentSlots
slots={classStartingEquipment.slots}
onRuleSelection={this.handleRuleSelection.bind(
this,
"startingClass"
)}
onRuleSlotSelection={this.handleRuleSlotSelection.bind(
this,
"startingClass"
)}
activeRuleSlots={ruleSlotChoices.startingClass.activeRuleSlots}
globalModifiers={globalModifiers}
valueLookupByType={valueLookupByType}
ruleData={ruleData}
/>
</div>
)}
{hasValidBackground &&
backgroundStartingEquipment !== null &&
backgroundStartingEquipment.slots !== null && (
<div>
<PageSubHeader>
{background ? BackgroundUtils.getName(background) : ""} Starting
Equipment
</PageSubHeader>
<StartingEquipmentSlots
slots={backgroundStartingEquipment.slots}
onRuleSelection={this.handleRuleSelection.bind(
this,
"background"
)}
onRuleSlotSelection={this.handleRuleSlotSelection.bind(
this,
"background"
)}
activeRuleSlots={ruleSlotChoices.background.activeRuleSlots}
globalModifiers={globalModifiers}
valueLookupByType={valueLookupByType}
ruleData={ruleData}
/>
</div>
)}
<div className="starting-equipment-gear-actions">
<ThemeButton onClick={this.handleStartingEquipmentAdd}>
{this.isStartingEquipmentEmpty() ? "Skip " : "Add "} Starting
Equipment
</ThemeButton>
<Link onClick={this.handleClearStartingEquipment}>Clear All</Link>
</div>
</div>
);
};
renderUi = (): React.ReactNode => {
const { isInitialView, startingClass } = this.props;
const { loadingStatus, activeChoice } = this.state;
if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
return null;
}
const wealthDice = this.getStartingClassWealthDice();
if (startingClass && !wealthDice) {
return this.renderEquipmentChoiceUi();
}
return (
<div className="starting-equipment-ui">
{isInitialView && (
<div className="starting-equipment-choices-label">Choose</div>
)}
<div className="starting-equipment-choices">
<div
className={`starting-equipment-choice ${
activeChoice === "equipment"
? "starting-equipment-info-choice-active"
: ""
}`}
onClick={this.handleChoiceClick.bind(this, "equipment")}
>
Equipment
</div>
<div className="starting-equipment-choice-sep">or</div>
<div
className={`starting-equipment-choice ${
activeChoice === "gold"
? "starting-equipment-info-choice-active"
: ""
}`}
onClick={this.handleChoiceClick.bind(this, "gold")}
>
Gold
</div>
</div>
{activeChoice === "equipment" && this.renderEquipmentChoiceUi()}
{activeChoice === "gold" && this.renderGoldChoiceUi()}
</div>
);
};
renderLoading = (): React.ReactNode => {
return <LoadingPlaceholder />;
};
render() {
const { loadingStatus } = this.state;
let contentNode: React.ReactNode;
if (loadingStatus === DataLoadingStatusEnum.LOADED) {
contentNode = this.renderUi();
} else {
contentNode = this.renderLoading();
}
return <div className="starting-equipment">{contentNode}</div>;
}
}
function mapStateToProps(state: SharedAppState) {
return {
startingClass: rulesEngineSelectors.getStartingClass(state),
background: rulesEngineSelectors.getBackgroundInfo(state),
loadBackgroundStartingEquipment:
apiCreatorSelectors.makeLoadBackgroundStartingEquipment(state),
loadClassStartingEquipment:
apiCreatorSelectors.makeLoadClassStartingEquipment(state),
globalModifiers: rulesEngineSelectors.getValidGlobalModifiers(state),
ruleData: rulesEngineSelectors.getRuleData(state),
valueLookupByType:
rulesEngineSelectors.getCharacterValueLookupByType(state),
characterId: rulesEngineSelectors.getId(state),
};
}
export default connect(mapStateToProps)(StartingEquipment);
@@ -0,0 +1,300 @@
import React from "react";
import { Checkbox, Select } from "@dndbeyond/character-components/es";
import {
BaseItemDefinitionContract,
Constants,
HelperUtils,
ItemUtils,
RuleData,
SourceUtils,
StartingEquipmentRuleContract,
StartingEquipmentUtils,
} from "@dndbeyond/character-rules-engine/es";
import { HtmlContent } from "~/components/HtmlContent";
import { TypeScriptUtils } from "../../../utils";
import { StartingEquipmentRuleSlotSelection } from "../typings";
interface Props {
isMultiSlot: boolean;
rules: Array<StartingEquipmentRuleContract>;
name: string | null;
slotIdx: number;
onRuleSlotSelection: (
selections: Array<StartingEquipmentRuleSlotSelection>
) => void;
onRuleSelection: (
slotIdx: number,
ruleIdx: number,
dataId: number | null
) => void;
isActive: boolean;
isSelected: boolean;
isDisabled: boolean;
ruleData: RuleData;
}
export default class StartingEquipmentRuleSlot extends React.PureComponent<Props> {
static defaultProps = {
isMultiSlot: false,
rules: [],
name: "",
slotIdx: 0,
isActive: true,
isSelected: false,
isDisabled: false,
};
componentDidMount() {
const { isMultiSlot, rules } = this.props;
if (
!isMultiSlot &&
rules.length &&
rules[0].ruleType !== Constants.StartingEquipmentRuleTypeEnum.INSTRUCTION
) {
this.handleSelection();
}
}
handleSelection = (): void => {
const { rules, onRuleSlotSelection, slotIdx, isDisabled } = this.props;
if (isDisabled) {
return;
}
if (onRuleSlotSelection) {
onRuleSlotSelection(
rules
.filter(
(rule) =>
rule.ruleType !==
Constants.StartingEquipmentRuleTypeEnum.INSTRUCTION
)
.map((rule, ruleIdx): StartingEquipmentRuleSlotSelection => {
let defaultDataId: number = 0;
if (
StartingEquipmentUtils.deriveIsEquipmentStartingEquipmentRuleType(
rule.ruleType
) &&
rule.definitions &&
rule.definitions.length
) {
let firstItem = rule.definitions[0];
defaultDataId = firstItem.id;
}
let dataId: number | null;
if (rule.definitions && rule.definitions.length === 1) {
dataId = defaultDataId;
} else {
dataId = null;
}
return {
ruleSlotIdx: slotIdx,
ruleIdx,
dataId,
};
})
);
}
};
handleRuleSelection = (ruleIdx: number, id: string): void => {
const { onRuleSelection, slotIdx, isDisabled } = this.props;
if (isDisabled) {
return;
}
if (onRuleSelection) {
onRuleSelection(slotIdx, ruleIdx, HelperUtils.parseInputInt(id));
}
};
render() {
const {
rules,
name,
isMultiSlot,
isActive,
isSelected,
isDisabled,
ruleData,
} = this.props;
let classNames: Array<string> = ["starting-equipment-rule-slot"];
if ((!isMultiSlot || (isMultiSlot && isActive)) && !isDisabled) {
classNames.push("starting-equipment-rule-slot-active");
} else {
classNames.push("starting-equipment-rule-slot-inactive");
}
if (isSelected) {
classNames.push("starting-equipment-rule-slot-selected");
}
if (isDisabled) {
classNames.push("starting-equipment-rule-slot-disabled");
}
let isInstruction: boolean = !!rules.find(
(rule) =>
rule.ruleType === Constants.StartingEquipmentRuleTypeEnum.INSTRUCTION
);
let isOnlyInstruction: boolean = false;
if (
isInstruction &&
!rules.find(
(rule) =>
rule.ruleType !== Constants.StartingEquipmentRuleTypeEnum.INSTRUCTION
)
) {
isOnlyInstruction = true;
classNames.push("starting-equipment-rule-slot-instruction");
}
let hasCustom: boolean = !!rules.find(
(rule) => rule.ruleType === Constants.StartingEquipmentRuleTypeEnum.CUSTOM
);
let hasMixedCustom: boolean = false;
if (
hasCustom &&
rules.find(
(rule) =>
rule.ruleType !== Constants.StartingEquipmentRuleTypeEnum.CUSTOM
)
) {
hasMixedCustom = true;
}
let itemPacks: Array<BaseItemDefinitionContract> = rules
.filter((rule) => {
let isPack: boolean = false;
if (rule.definitions && rule.definitions.length) {
let firstItem = rule.definitions[0];
isPack = ItemUtils.isDefinitionPack(firstItem);
}
return (
isPack &&
rule.ruleType === Constants.StartingEquipmentRuleTypeEnum.GEAR
);
})
.map((rule) => rule.definitions && rule.definitions[0])
.filter(TypeScriptUtils.isNotNullOrUndefined);
return (
<div
className={classNames.join(" ")}
onClick={isOnlyInstruction ? undefined : this.handleSelection}
>
<div className="starting-equipment-rule-slot-selection">
{!isOnlyInstruction && (
<div className="starting-equipment-rule-slot-checkmark">
<Checkbox
initiallyEnabled={isSelected}
onChange={this.handleSelection}
stopPropagation={true}
/>
</div>
)}
</div>
<div className="starting-equipment-rule-content">
<div className="starting-equipment-rule-slot-name">{name}</div>
{isDisabled && (
<div className="starting-equipment-disabled-message">
Missing required proficiency
</div>
)}
{isSelected && (
<div className="starting-equipment-rules">
{rules.map((rule, ruleIdx) => {
const { definitions, ruleType } = rule;
let contentNode: React.ReactNode;
if (
ruleType === Constants.StartingEquipmentRuleTypeEnum.ARMOR ||
ruleType === Constants.StartingEquipmentRuleTypeEnum.GEAR ||
ruleType === Constants.StartingEquipmentRuleTypeEnum.WEAPON ||
ruleType ===
Constants.StartingEquipmentRuleTypeEnum.ARMOR_TYPE ||
ruleType ===
Constants.StartingEquipmentRuleTypeEnum.GEAR_TYPE ||
ruleType ===
Constants.StartingEquipmentRuleTypeEnum.WEAPON_TYPE
) {
if (definitions && definitions.length > 1) {
const groupedOptions =
SourceUtils.getGroupedOptionsBySourceCategory(
definitions,
ruleData
);
contentNode = (
<div className="starting-equipment-rule">
<Select
options={groupedOptions}
value={null}
preventClickPropagating={true}
onChange={this.handleRuleSelection.bind(
this,
ruleIdx
)}
/>
</div>
);
}
}
if (!contentNode) {
return null;
}
return (
<div className="starting-equipment-rule" key={ruleIdx}>
{contentNode}
</div>
);
})}
{itemPacks.length > 0 && (
<div className="starting-equipment-packs">
{(itemPacks as any).map(
(itemDefinition: BaseItemDefinitionContract) => (
<div
className="starting-equipment-pack"
key={itemDefinition.id}
>
{itemPacks.length > 1 && (
<div className="starting-equipment-pack-name">
{itemDefinition.name}
</div>
)}
{itemDefinition.description && (
<HtmlContent
className="starting-equipment-pack-desc"
html={itemDefinition.description}
withoutTooltips
/>
)}
</div>
)
)}
</div>
)}
{hasCustom && (
<div className="starting-equipment-custom-add">
<div className="starting-equipment-custom-add-label">
{hasMixedCustom ? "Some item(s) " : "Item(s) "}
will be added to <strong>Other Possessions</strong>
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}
}
@@ -0,0 +1,4 @@
import StartingEquipmentRuleSlot from "./StartingEquipmentRuleSlot";
export default StartingEquipmentRuleSlot;
export { StartingEquipmentRuleSlot };
@@ -0,0 +1,102 @@
import React from "react";
import {
BaseItemDefinitionContract,
Modifier,
RuleData,
StartingEquipmentRuleSlotContract,
TypeValueLookup,
} from "@dndbeyond/character-rules-engine/es";
import { Constants, ItemUtils } from "@dndbeyond/character-rules-engine/es";
import StartingEquipmentRuleSlot from "../StartingEquipmentRuleSlot";
import { StartingEquipmentRuleSlotSelection } from "../typings";
interface Props {
ruleSlots: Array<StartingEquipmentRuleSlotContract>;
onRuleSlotSelection: (
selections: Array<StartingEquipmentRuleSlotSelection>
) => void;
onRuleSelection: (
slotIdx: number,
ruleIdx: number,
dataId: number | null
) => void;
activeIdx: number | null;
globalModifiers: Array<Modifier>;
valueLookupByType: TypeValueLookup;
ruleData: RuleData;
}
export default class StartingEquipmentRuleSlots extends React.PureComponent<Props> {
static defaultProps = {
activeIdx: null,
};
isRuleSlotDisabled = (
ruleSlot: StartingEquipmentRuleSlotContract
): boolean => {
const { globalModifiers, valueLookupByType, ruleData } = this.props;
let isDisabled: boolean = false;
let proficiencyRuleTypes: Array<Constants.StartingEquipmentRuleTypeEnum> = [
Constants.StartingEquipmentRuleTypeEnum.WEAPON,
Constants.StartingEquipmentRuleTypeEnum.ARMOR,
];
if (ruleSlot.rules) {
ruleSlot.rules
.filter(
(rule) =>
rule.proficiencyRequired &&
proficiencyRuleTypes.includes(rule.ruleType)
)
.forEach((rule) => {
if (rule.definitions) {
rule.definitions.forEach((item: BaseItemDefinitionContract) => {
let simulatedItem = ItemUtils.simulateItem(
item,
globalModifiers,
valueLookupByType,
ruleData
);
if (!ItemUtils.hasProficiency(simulatedItem)) {
isDisabled = true;
}
});
}
});
}
return isDisabled;
};
render() {
const {
ruleSlots,
activeIdx,
onRuleSelection,
onRuleSlotSelection,
ruleData,
} = this.props;
return (
<div className="starting-equipment-rule-slots">
{ruleSlots.map((ruleSlot, ruleSlotIdx) => (
<StartingEquipmentRuleSlot
key={ruleSlotIdx}
isMultiSlot={ruleSlots.length > 1}
slotIdx={ruleSlotIdx}
onRuleSelection={onRuleSelection}
onRuleSlotSelection={onRuleSlotSelection}
isActive={activeIdx === null || activeIdx === ruleSlotIdx}
isSelected={activeIdx === ruleSlotIdx}
isDisabled={this.isRuleSlotDisabled(ruleSlot)}
{...ruleSlot}
rules={ruleSlot.rules ? ruleSlot.rules : []}
ruleData={ruleData}
/>
))}
</div>
);
}
}
@@ -0,0 +1,4 @@
import StartingEquipmentRuleSlots from "./StartingEquipmentRuleSlots";
export default StartingEquipmentRuleSlots;
export { StartingEquipmentRuleSlots };
@@ -0,0 +1,58 @@
import React from "react";
import {
Modifier,
RuleData,
StartingEquipmentSlotContract,
TypeValueLookup,
} from "@dndbeyond/character-rules-engine/es";
import StartingEquipmentRuleSlots from "../StartingEquipmentRuleSlots";
import { StartingEquipmentRuleSlotSelection } from "../typings";
interface Props {
slots: Array<StartingEquipmentSlotContract>;
onRuleSlotSelection: (
selections: Array<StartingEquipmentRuleSlotSelection>
) => void;
onRuleSelection: (
slotIdx: number,
ruleIdx: number,
dataId: number | null
) => void;
activeRuleSlots: Array<number | null>;
globalModifiers: Array<Modifier>;
valueLookupByType: TypeValueLookup;
ruleData: RuleData;
}
export default class StartingEquipmentSlots extends React.PureComponent<Props> {
render() {
const {
slots,
activeRuleSlots,
globalModifiers,
valueLookupByType,
ruleData,
onRuleSelection,
onRuleSlotSelection,
} = this.props;
return (
<div className="starting-equipment-slots">
{slots.map((slot, slotIdx) => (
<div className="starting-equipment-slot" key={slotIdx}>
<StartingEquipmentRuleSlots
ruleSlots={slot.ruleSlots ? slot.ruleSlots : []}
onRuleSelection={onRuleSelection.bind(this, slotIdx)}
onRuleSlotSelection={onRuleSlotSelection.bind(this, slotIdx)}
activeIdx={activeRuleSlots[slotIdx]}
globalModifiers={globalModifiers}
valueLookupByType={valueLookupByType}
ruleData={ruleData}
/>
</div>
))}
</div>
);
}
}
@@ -0,0 +1,4 @@
import StartingEquipmentSlots from "./StartingEquipmentSlots";
export default StartingEquipmentSlots;
export { StartingEquipmentSlots };
@@ -0,0 +1,12 @@
import StartingEquipment from "./StartingEquipment";
import StartingEquipmentRuleSlot from "./StartingEquipmentRuleSlot";
import StartingEquipmentRuleSlots from "./StartingEquipmentRuleSlots";
import StartingEquipmentSlots from "./StartingEquipmentSlots";
export default StartingEquipment;
export {
StartingEquipment,
StartingEquipmentRuleSlot,
StartingEquipmentRuleSlots,
StartingEquipmentSlots,
};