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:
+52
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
AbilityManager,
|
||||
HelperUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
|
||||
|
||||
|
||||
interface Props {
|
||||
abilities: Array<AbilityManager>;
|
||||
minScore?: number;
|
||||
maxScore?: number;
|
||||
}
|
||||
|
||||
export default function AbilityScoreManagerManual({
|
||||
abilities,
|
||||
minScore = 3,
|
||||
maxScore = 18,
|
||||
}: Props) {
|
||||
const handleTransformValueOnBlur = (value: string): number | null => {
|
||||
const parsedValue = HelperUtils.parseInputInt(value);
|
||||
let clampedValue: number | null = null;
|
||||
if (parsedValue !== null) {
|
||||
clampedValue = HelperUtils.clampInt(parsedValue, minScore, maxScore);
|
||||
}
|
||||
|
||||
return clampedValue;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" ability-score-manager ability-score-manager-manual">
|
||||
<div className="ability-score-manager-stats">
|
||||
{abilities.map((abilityScore) => (
|
||||
<div
|
||||
className="ability-score-manager-stat"
|
||||
key={abilityScore.getId()}
|
||||
data-stat-id={abilityScore.getId()}
|
||||
>
|
||||
<FormInputField
|
||||
transformValueOnBlur={handleTransformValueOnBlur}
|
||||
onBlur={(value) => abilityScore.handleScoreChange(Number(value))}
|
||||
label={abilityScore.getLabel() ?? ""}
|
||||
initialValue={abilityScore.getBaseScore()}
|
||||
/>
|
||||
<div className="ability-score-manager-stat-total">
|
||||
Total: {abilityScore.getTotalScore() ?? "--"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import AbilityScoreManagerManual from "./AbilityScoreManagerManual";
|
||||
|
||||
export default AbilityScoreManagerManual;
|
||||
export { AbilityScoreManagerManual };
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
HelperUtils,
|
||||
HtmlSelectOption,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
const totalPoints: number = 27;
|
||||
const scoreChoices: Array<number> = [8, 9, 10, 11, 12, 13, 14, 15];
|
||||
const scorePoints: Record<number, number> = {
|
||||
8: 0,
|
||||
9: 1,
|
||||
10: 2,
|
||||
11: 3,
|
||||
12: 4,
|
||||
13: 5,
|
||||
14: 7,
|
||||
15: 9,
|
||||
};
|
||||
|
||||
function renderScoreLabel(score: number, currentScore: number | null): string {
|
||||
if (currentScore !== null) {
|
||||
if (score > currentScore) {
|
||||
const pointDiff = scorePoints[score] - scorePoints[currentScore];
|
||||
return `${score} (-${pointDiff} Point${pointDiff === 1 ? "" : "s"})`;
|
||||
} else if (score < currentScore) {
|
||||
const pointDiff = scorePoints[currentScore] - scorePoints[score];
|
||||
return `${score} (+${pointDiff} Point${pointDiff === 1 ? "" : "s"})`;
|
||||
}
|
||||
}
|
||||
|
||||
return "" + score;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
abilities: Array<AbilityManager>;
|
||||
}
|
||||
export default function AbilityScoreManagerPointBuy({ abilities }: Props) {
|
||||
const currentPoints = abilities.reduce((acc: number, abilityScore) => {
|
||||
const baseScore = abilityScore.getBaseScore();
|
||||
if (baseScore !== null) {
|
||||
return acc + scorePoints[baseScore];
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
const remainingPoints: number = totalPoints - currentPoints;
|
||||
|
||||
return (
|
||||
<div className="ability-score-manager ability-score-manager-point">
|
||||
<div className="ability-score-manager-points">
|
||||
<div className="ability-score-manager-points-heading">
|
||||
Points Remaining
|
||||
</div>
|
||||
<div className="ability-score-manager-points-value">
|
||||
<span className="ability-score-manager-points-value-remaining">
|
||||
{remainingPoints}
|
||||
</span>
|
||||
<span className="ability-score-manager-points-value-sep">/</span>
|
||||
<span className="ability-score-manager-points-value-total">
|
||||
{totalPoints}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ability-score-manager-stats">
|
||||
{abilities.map((abilityScore) => {
|
||||
const uId = uniqueId("qry_");
|
||||
const baseScore = abilityScore.getBaseScore();
|
||||
const availableScores = scoreChoices.reduce(
|
||||
(acc: Array<number>, score) => {
|
||||
let pointDiff = scorePoints[score];
|
||||
if (baseScore !== null) {
|
||||
pointDiff -= scorePoints[baseScore];
|
||||
}
|
||||
|
||||
if (
|
||||
pointDiff < 0 ||
|
||||
(pointDiff > 0 && pointDiff <= remainingPoints) ||
|
||||
score === abilityScore.getBaseScore()
|
||||
) {
|
||||
acc.push(score);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
const availableScoreOptions: Array<HtmlSelectOption> =
|
||||
availableScores.map((score) => ({
|
||||
label: renderScoreLabel(score, abilityScore.getBaseScore()),
|
||||
value: score,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ability-score-manager-stat"
|
||||
key={abilityScore.getId()}
|
||||
>
|
||||
<div className="builder-field form-select-field">
|
||||
<span className="builder-field-label">
|
||||
<label
|
||||
className="builder-field-heading form-select-field-label"
|
||||
htmlFor={uId}
|
||||
>
|
||||
{abilityScore.getLabel()}
|
||||
</label>
|
||||
</span>
|
||||
<span className="builder-field-input">
|
||||
<Select
|
||||
id={uId}
|
||||
options={availableScoreOptions}
|
||||
value={abilityScore.getBaseScore()}
|
||||
onChange={(value) =>
|
||||
abilityScore.handleScoreChange(Number(value))
|
||||
}
|
||||
initialOptionRemoved={true}
|
||||
/>
|
||||
</span>
|
||||
<div className="ability-score-manager-stat-total">
|
||||
Total: {abilityScore.getTotalScore() ?? "--"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import AbilityScoreManagerPointBuy from "./AbilityScoreManagerPointBuy";
|
||||
|
||||
export default AbilityScoreManagerPointBuy;
|
||||
export { AbilityScoreManagerPointBuy };
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
HelperUtils,
|
||||
HtmlSelectOption,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { TypeScriptUtils } from "../../../Shared/utils";
|
||||
|
||||
const standardScores: Array<number> = [8, 10, 12, 13, 14, 15];
|
||||
|
||||
interface Props {
|
||||
abilities: Array<AbilityManager>;
|
||||
}
|
||||
export default function AbilityScoreManagerStandardArray({ abilities }: Props) {
|
||||
const usedScores = abilities
|
||||
.map((abilityScore) => abilityScore.getBaseScore())
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||
|
||||
return (
|
||||
<div className="ability-score-manager ability-score-manager-standard">
|
||||
<div className="ability-score-manager-stats">
|
||||
{abilities.map((abilityScore) => {
|
||||
const uId = uniqueId("qry_");
|
||||
const availableScores = standardScores.reduce(
|
||||
(acc: Array<number>, score) => {
|
||||
if (
|
||||
!usedScores.includes(score) ||
|
||||
score === abilityScore.getBaseScore()
|
||||
) {
|
||||
acc.push(score);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
const availableScoreOptions: Array<HtmlSelectOption> =
|
||||
availableScores.map((score) => ({
|
||||
label: "" + score,
|
||||
value: score,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ability-score-manager-stat"
|
||||
key={abilityScore.getId()}
|
||||
>
|
||||
<div className="builder-field form-select-field">
|
||||
<span className="builder-field-label">
|
||||
<label
|
||||
className="builder-field-heading form-select-field-label"
|
||||
htmlFor={uId}
|
||||
>
|
||||
{abilityScore.getLabel()}
|
||||
</label>
|
||||
</span>
|
||||
<span className="builder-field-input">
|
||||
<Select
|
||||
id={uId}
|
||||
options={availableScoreOptions}
|
||||
value={abilityScore.getBaseScore()}
|
||||
onChange={(value) => {
|
||||
const parsedValue = HelperUtils.parseInputInt(value);
|
||||
abilityScore.handleScoreChange(parsedValue);
|
||||
}}
|
||||
placeholder={"--"}
|
||||
/>
|
||||
</span>
|
||||
<div className="ability-score-manager-stat-total">
|
||||
Total: {abilityScore.getTotalScore() ?? "--"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import AbilityScoreManagerStandardArray from "./AbilityScoreManagerStandardArray";
|
||||
|
||||
export default AbilityScoreManagerStandardArray;
|
||||
export { AbilityScoreManagerStandardArray };
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button } from "@dndbeyond/character-components/es";
|
||||
|
||||
interface withBuilderProps {
|
||||
clsNames: Array<string>;
|
||||
}
|
||||
function withBuilderButton<P extends object, C>(
|
||||
WrappedComponent: React.JSXElementConstructor<P> & C
|
||||
) {
|
||||
type Props = JSX.LibraryManagedAttributes<C, P>;
|
||||
return class WithBuilderButton extends React.PureComponent<
|
||||
Props & withBuilderProps
|
||||
> {
|
||||
static defaultProps = {
|
||||
clsNames: [],
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, clsNames } = this.props;
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...(this.props as any)}
|
||||
clsNames={[...clsNames, "builder-button"]}
|
||||
>
|
||||
{children}
|
||||
</WrappedComponent>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default withBuilderButton(Button);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Button from "./Button";
|
||||
|
||||
export default Button;
|
||||
export { Button };
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import FormLabel from "@mui/material/FormLabel";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import MuiRadioGroup from "@mui/material/RadioGroup";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { ChangeEvent } from "react";
|
||||
|
||||
import { TitleH3 } from "../shared-styles";
|
||||
|
||||
type RadioGroupProps = {
|
||||
name?: string;
|
||||
defaultValue?: any;
|
||||
label: string;
|
||||
options?: any;
|
||||
subtitle?: string;
|
||||
onChange?: (event: ChangeEvent, value: any) => void;
|
||||
disabled?: boolean;
|
||||
value?: any;
|
||||
};
|
||||
|
||||
export const RadioGroup = ({
|
||||
name,
|
||||
defaultValue,
|
||||
label,
|
||||
options,
|
||||
subtitle,
|
||||
onChange,
|
||||
disabled = false,
|
||||
value,
|
||||
}: RadioGroupProps) => {
|
||||
return (
|
||||
<FormControl disabled={disabled}>
|
||||
<FormLabel
|
||||
sx={{ ...TitleH3, color: "#000" }}
|
||||
id={`radio-button-group-${label.split(" ").join("-")}`}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: "#12181ca3", fontSize: "14px" }}
|
||||
>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
<MuiRadioGroup
|
||||
aria-labelledby={`radio-button-group-${label.split(" ").join("-")}`}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
{...{
|
||||
...(defaultValue && { defaultValue }),
|
||||
...(value && { value }),
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
value={option.value}
|
||||
control={<Radio color="secondary" />}
|
||||
label={option.label}
|
||||
key={option.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MuiRadioGroup>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { RadioGroup } from "./RadioGroup";
|
||||
|
||||
export default RadioGroup;
|
||||
export { RadioGroup };
|
||||
@@ -0,0 +1,8 @@
|
||||
export const TitleH3 = {
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Roboto Condensed",
|
||||
letterSpacing: "normal",
|
||||
lineHeight: 1.5,
|
||||
marginTop: 4,
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
|
||||
import { ClassDefinitionContract } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import Button from "../Button";
|
||||
|
||||
interface Props {
|
||||
charClass: ClassDefinitionContract;
|
||||
onRequestChange: () => void;
|
||||
}
|
||||
export default class ClassDisplaySimple extends React.PureComponent<Props> {
|
||||
handleChangeClick = () => {
|
||||
const { onRequestChange } = this.props;
|
||||
|
||||
if (onRequestChange) {
|
||||
onRequestChange();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { charClass } = this.props;
|
||||
|
||||
let portraitAvatarUrl = charClass.portraitAvatarUrl;
|
||||
|
||||
return (
|
||||
<div className="builder-field builder-field-valid class-simple">
|
||||
<div className="builder-field-heading">Selected Class</div>
|
||||
<div className="class-simple-chosen">
|
||||
<div className="class-simple-chosen-preview">
|
||||
<img
|
||||
className="class-simple-chosen-preview-img"
|
||||
src={portraitAvatarUrl ? portraitAvatarUrl : ""}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="class-simple-chosen-heading">{charClass.name}</div>
|
||||
<div className="class-simple-chosen-action">
|
||||
<Button onClick={this.handleChangeClick} size="small">
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ClassDisplaySimple from "./ClassDisplaySimple";
|
||||
|
||||
export default ClassDisplaySimple;
|
||||
export { ClassDisplaySimple };
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
import { FeatList, FormatUtils } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { FeatDetail } from "~/subApps/sheet/components/Sidebar/panes/FeatsManagePane/FeatDetail";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
|
||||
type GrantedFeatProps = {
|
||||
featList: FeatList;
|
||||
requiredLevel?: number;
|
||||
};
|
||||
|
||||
export const GrantedFeat: React.FC<GrantedFeatProps> = ({
|
||||
featList,
|
||||
requiredLevel,
|
||||
}) => {
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
|
||||
// Only show the Feat List section if there is something to show.
|
||||
if (featList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let featContent: React.ReactNode | undefined;
|
||||
let selectBox: React.ReactNode | undefined;
|
||||
let infoMessage: React.ReactNode | undefined;
|
||||
let hasUnfinishedChoices = featList.hasChoiceToMake();
|
||||
let subtitleItems = ["Granted Feat"];
|
||||
|
||||
// Show a message when the character already has every feat in the feat list from somewhere else.
|
||||
if (featList.alreadyHasEveryFeat() && featList.chosenFeatId === null) {
|
||||
infoMessage = <p>This feat already exists on your character.</p>;
|
||||
} else {
|
||||
// Show the feat details if a feat has been chosen from the feat list.
|
||||
if (featList.chosenFeatId !== null) {
|
||||
const featId = featList.chosenFeatId;
|
||||
const featManager = characterFeaturesManager.getFeatById(featId);
|
||||
|
||||
if (featManager !== null) {
|
||||
const needToChooseFeatOptions =
|
||||
featManager.getUnfinishedChoices().length > 0;
|
||||
hasUnfinishedChoices = hasUnfinishedChoices || needToChooseFeatOptions;
|
||||
|
||||
featContent = <FeatDetail featManager={featManager} />;
|
||||
|
||||
if (featManager.isHiddenFeat()) {
|
||||
subtitleItems = []; // don't call it a feat if it isn't really a feat.
|
||||
}
|
||||
|
||||
const choiceCount = featManager.getChoices().length;
|
||||
if (choiceCount) {
|
||||
subtitleItems.push(
|
||||
`${choiceCount} Choice${choiceCount !== 1 ? "s" : ""}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When there is more than one feat in the list, show the select box to choose one.
|
||||
if (!featList.isSingleFeat()) {
|
||||
selectBox = (
|
||||
<Select
|
||||
onChangePromise={(...args) => featList.handleChoiceSelected(...args)}
|
||||
options={featList.availableChoices}
|
||||
value={featList.chosenFeatId !== null ? featList.chosenFeatId : -1}
|
||||
clsNames={["description-manage-background-granted-feat-chooser"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredLevel) {
|
||||
subtitleItems.push(`${FormatUtils.ordinalize(requiredLevel)} level`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
summary={featList.definition.name}
|
||||
summaryMetaItems={subtitleItems}
|
||||
showAlert={hasUnfinishedChoices}
|
||||
variant="paper"
|
||||
key={featList.definition.id}
|
||||
>
|
||||
{infoMessage}
|
||||
{selectBox}
|
||||
{featContent}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
import React from "react";
|
||||
|
||||
import { Checkbox, Select } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
Constants,
|
||||
FormatUtils,
|
||||
HtmlSelectOption,
|
||||
AccessUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { CollapsibleContent } from "~/components/CollapsibleContent";
|
||||
import { Link } from "~/components/Link";
|
||||
|
||||
export interface AffectedFeatureInfo {
|
||||
name: string;
|
||||
definitionKey: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
interface OptionalFeatureProps {
|
||||
name: string;
|
||||
requiredLevel?: number;
|
||||
description: string | null;
|
||||
featureType: Constants.FeatureTypeEnum | null;
|
||||
definitionKey: string;
|
||||
affectedFeatureDefinitionKey: string | null;
|
||||
isSelected: boolean;
|
||||
onSelection: (
|
||||
definitionKey: string,
|
||||
affectedFeatureDefinitionKey: string | null
|
||||
) => void;
|
||||
onRemoveSelectionPromise?: (
|
||||
definitionKey: string,
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
onChangeReplacementPromise?: (
|
||||
definitionKey: string,
|
||||
newAffectedDefinitionKey: string | null,
|
||||
oldAffectedDefinitionKey: string | null,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
affectedFeatures: Array<AffectedFeatureInfo>;
|
||||
accessType: Constants.AccessTypeEnum;
|
||||
}
|
||||
export class OptionalFeature extends React.PureComponent<
|
||||
OptionalFeatureProps,
|
||||
{}
|
||||
> {
|
||||
handleSelection = (isEnabled: boolean): void => {
|
||||
const { featureType, onSelection, definitionKey, affectedFeatures } =
|
||||
this.props;
|
||||
|
||||
switch (featureType) {
|
||||
case Constants.FeatureTypeEnum.REPLACEMENT: {
|
||||
if (isEnabled) {
|
||||
const affectedFeatureDefinitionKey: string | null =
|
||||
affectedFeatures.length === 1
|
||||
? affectedFeatures[0].definitionKey
|
||||
: null;
|
||||
|
||||
onSelection(definitionKey, affectedFeatureDefinitionKey);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Constants.FeatureTypeEnum.ADDITIONAL: {
|
||||
if (isEnabled) {
|
||||
onSelection(definitionKey, null);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
//not implemented
|
||||
}
|
||||
};
|
||||
|
||||
transformDefinitionKey = (definitionKey: string): string | null => {
|
||||
return definitionKey.length ? definitionKey : null;
|
||||
};
|
||||
|
||||
handleReplacementChangePromise = (
|
||||
newAffectedDefinitionKey: string,
|
||||
oldAffectedDefinitionKey: string,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => {
|
||||
const { onChangeReplacementPromise, definitionKey } = this.props;
|
||||
|
||||
if (onChangeReplacementPromise) {
|
||||
onChangeReplacementPromise(
|
||||
definitionKey,
|
||||
this.transformDefinitionKey(newAffectedDefinitionKey),
|
||||
this.transformDefinitionKey(oldAffectedDefinitionKey),
|
||||
accept,
|
||||
reject
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleUnselectPromise = (
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => {
|
||||
const { onRemoveSelectionPromise, definitionKey } = this.props;
|
||||
|
||||
if (onRemoveSelectionPromise) {
|
||||
onRemoveSelectionPromise(definitionKey, newIsEnabled, accept, reject);
|
||||
}
|
||||
};
|
||||
|
||||
renderReplacements = (): React.ReactNode => {
|
||||
const {
|
||||
affectedFeatures,
|
||||
isSelected,
|
||||
featureType,
|
||||
affectedFeatureDefinitionKey,
|
||||
} = this.props;
|
||||
|
||||
if (!affectedFeatures.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const introText: Array<string> = ["Replaces:"];
|
||||
const disabledText: string = "(already replaced)";
|
||||
if (affectedFeatures.length === 1) {
|
||||
introText.push(affectedFeatures[0].name);
|
||||
if (affectedFeatures[0].disabled) {
|
||||
introText.push(disabledText);
|
||||
}
|
||||
}
|
||||
|
||||
const options: Array<HtmlSelectOption> = [];
|
||||
if (affectedFeatures.length > 1) {
|
||||
affectedFeatures.forEach((feature) => {
|
||||
const label: Array<string> = [feature.name];
|
||||
if (feature.disabled) {
|
||||
label.push(disabledText);
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: label.join(" "),
|
||||
value: feature.definitionKey,
|
||||
disabled: feature.disabled,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const classNames: Array<string> = ["ct-optional-feature__select"];
|
||||
|
||||
if (
|
||||
featureType === Constants.FeatureTypeEnum.REPLACEMENT &&
|
||||
isSelected &&
|
||||
affectedFeatureDefinitionKey === null
|
||||
) {
|
||||
classNames.push("ct-optional-feature__select--todo");
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="ct-optional-feature__type">{introText.join(" ")}</div>
|
||||
{options.length > 1 && (
|
||||
<div className={classNames.join(" ")}>
|
||||
<Select
|
||||
options={options}
|
||||
value={affectedFeatureDefinitionKey}
|
||||
onChangePromise={this.handleReplacementChangePromise}
|
||||
disabled={!isSelected}
|
||||
preventClickPropagating={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
renderFeatureTypeContent = (): React.ReactNode => {
|
||||
const { featureType } = this.props;
|
||||
|
||||
switch (featureType) {
|
||||
case Constants.FeatureTypeEnum.ADDITIONAL:
|
||||
return (
|
||||
<div className="ct-optional-feature__type">Additional feature</div>
|
||||
);
|
||||
|
||||
case Constants.FeatureTypeEnum.REPLACEMENT:
|
||||
return this.renderReplacements();
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
renderDescription = (): React.ReactNode => {
|
||||
const { description, accessType } = this.props;
|
||||
|
||||
let contentNode: React.ReactNode = null;
|
||||
if (AccessUtils.isAccessible(accessType)) {
|
||||
contentNode = description ? (
|
||||
<CollapsibleContent className="ct-optional-feature__description">
|
||||
{description}
|
||||
</CollapsibleContent>
|
||||
) : null;
|
||||
} else {
|
||||
contentNode = (
|
||||
<div className="ct-character-tools__marketplace-callout">
|
||||
To fully unlock this feature, check out the{" "}
|
||||
<Link href="/marketplace">Marketplace</Link>.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return contentNode;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isSelected, name, affectedFeatures, requiredLevel } = this.props;
|
||||
|
||||
const isDisabled: boolean =
|
||||
affectedFeatures.length === 1 && affectedFeatures[0].disabled;
|
||||
|
||||
const classNames: Array<string> = ["ct-optional-feature"];
|
||||
if (isDisabled) {
|
||||
classNames.push("ct-optional-feature--is-disabled");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<div className="ct-optional-feature__selection">
|
||||
<div className="ct-optional-feature__checkmark">
|
||||
<Checkbox
|
||||
initiallyEnabled={isSelected}
|
||||
onChange={this.handleSelection}
|
||||
stopPropagation={true}
|
||||
onChangePromise={
|
||||
isSelected ? this.handleUnselectPromise : undefined
|
||||
}
|
||||
isInteractive={!isDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-optional-feature__primary">
|
||||
<div className="ct-optional-feature__name">{name}</div>
|
||||
{requiredLevel && (
|
||||
<div className="ct-optional-feature__level">
|
||||
{`${FormatUtils.ordinalize(requiredLevel)} level`}
|
||||
</div>
|
||||
)}
|
||||
{this.renderDescription()}
|
||||
<div className="ct-optional-feature__secondary">
|
||||
{this.renderFeatureTypeContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OptionalFeature;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
clsNames: Array<string>;
|
||||
}
|
||||
export default class Page extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
clsNames: [],
|
||||
};
|
||||
|
||||
render() {
|
||||
const { clsNames, children } = this.props;
|
||||
|
||||
const conClassNames = [...clsNames, "builder-page"];
|
||||
|
||||
return <div className={conClassNames.join(" ")}>{children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import Page from "./Page";
|
||||
|
||||
export default Page;
|
||||
export { Page };
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { BuilderMethod } from "~/subApps/builder/constants";
|
||||
|
||||
import { PortraitName } from "../../../../../subApps/builder/components/PortraitName";
|
||||
import { builderSelectors } from "../../selectors";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {}
|
||||
export const PageBody: FC<Props> = ({ children, ...props }) => {
|
||||
const builderMethod = useSelector(builderSelectors.getBuilderMethod);
|
||||
|
||||
return (
|
||||
<div className="builder-page-body" {...props}>
|
||||
{builderMethod === BuilderMethod.STEP_BY_STEP && (
|
||||
<div className="builder-page-body-character-name">
|
||||
<PortraitName useDefaultCharacterName={true} />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
export default class PageHeader extends React.PureComponent {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
return <div className="builder-page-header">{children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import PageHeader from "./PageHeader";
|
||||
|
||||
export default PageHeader;
|
||||
export { PageHeader };
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
export default class PageSubHeader extends React.PureComponent {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
return <div className="builder-page-subheader">{children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import PageSubHeader from "./PageSubHeader";
|
||||
|
||||
export default PageSubHeader;
|
||||
export { PageSubHeader };
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
RaceDefinitionContract,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import Button from "../Button";
|
||||
|
||||
interface Props {
|
||||
species: RaceDefinitionContract | null;
|
||||
onRequestAction: () => void;
|
||||
actionText: string;
|
||||
headingText: string;
|
||||
ruleData: RuleData;
|
||||
}
|
||||
export default class SpeciesDisplaySimple extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
actionText: "Change",
|
||||
headingText: "Selected ",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { species, actionText, headingText, ruleData, onRequestAction } =
|
||||
this.props;
|
||||
|
||||
if (species === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { portraitAvatarUrl, baseName, subRaceShortName } = species;
|
||||
const previewUrl: string | null = portraitAvatarUrl
|
||||
? portraitAvatarUrl
|
||||
: RuleDataUtils.getDefaultRaceImageUrl(ruleData);
|
||||
|
||||
return (
|
||||
<div className="builder-field builder-field-valid race-simple">
|
||||
<div className="builder-field-heading">{headingText}</div>
|
||||
<div className="race-simple-content">
|
||||
<div className="race-simple-preview">
|
||||
<img
|
||||
className="race-simple-preview-img"
|
||||
src={previewUrl ? previewUrl : ""}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="race-simple-info">
|
||||
{subRaceShortName ? (
|
||||
<div className="race-simple-subclass">{subRaceShortName}</div>
|
||||
) : null}
|
||||
<div className="race-simple-parent">{baseName}</div>
|
||||
</div>
|
||||
<div className="race-simple-action">
|
||||
<Button onClick={onRequestAction} size="small">
|
||||
{actionText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import SpeciesDisplaySimple from "./SpeciesDisplaySimple";
|
||||
|
||||
export default SpeciesDisplaySimple;
|
||||
export { SpeciesDisplaySimple };
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AnySimpleDataType,
|
||||
ApiAdapterPromise,
|
||||
ApiAdapterRequestConfig,
|
||||
ApiResponse,
|
||||
CharacterPreferences,
|
||||
ChoiceData,
|
||||
DefinitionPool,
|
||||
FeatDefinitionContract,
|
||||
FeatLookup,
|
||||
OptionalOriginLookup,
|
||||
PrerequisiteData,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { SpeciesManageSpeciesTrait } from "../../containers/pages/SpeciesManage";
|
||||
|
||||
interface Props {
|
||||
speciesTraits: RacialTrait[];
|
||||
featLookup: FeatLookup;
|
||||
loadAvailableFeats: (
|
||||
additionalConfig?: Partial<ApiAdapterRequestConfig>
|
||||
) => ApiAdapterPromise<ApiResponse<FeatDefinitionContract[]>>;
|
||||
handleSpeciesTraitChoiceChange: (
|
||||
speciesTraitId: number,
|
||||
choiceId: string,
|
||||
choiceType: number,
|
||||
optionValue: AnySimpleDataType
|
||||
) => void;
|
||||
choiceInfo: ChoiceData;
|
||||
prerequisiteData: PrerequisiteData;
|
||||
preferences: CharacterPreferences;
|
||||
optionalOriginLookup: OptionalOriginLookup;
|
||||
definitionPool: DefinitionPool;
|
||||
speciesName?: string | null;
|
||||
}
|
||||
|
||||
const SpeciesTraitList: React.FC<Props> = ({
|
||||
speciesTraits,
|
||||
featLookup,
|
||||
loadAvailableFeats,
|
||||
handleSpeciesTraitChoiceChange,
|
||||
choiceInfo,
|
||||
prerequisiteData,
|
||||
preferences,
|
||||
optionalOriginLookup,
|
||||
definitionPool,
|
||||
speciesName,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{speciesTraits.map((speciesTrait) => (
|
||||
<SpeciesManageSpeciesTrait
|
||||
key={RacialTraitUtils.getId(speciesTrait)}
|
||||
speciesTrait={speciesTrait}
|
||||
featLookup={featLookup}
|
||||
loadAvailableFeats={loadAvailableFeats}
|
||||
onChoiceChange={handleSpeciesTraitChoiceChange}
|
||||
choiceInfo={choiceInfo}
|
||||
prerequisiteData={prerequisiteData}
|
||||
preferences={preferences}
|
||||
optionalOriginLookup={optionalOriginLookup}
|
||||
definitionPool={definitionPool}
|
||||
speciesName={speciesName}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeciesTraitList;
|
||||
@@ -0,0 +1,4 @@
|
||||
import SpeciesTraitList from "./SpeciesTraitList";
|
||||
|
||||
export default SpeciesTraitList;
|
||||
export { SpeciesTraitList };
|
||||
Reference in New Issue
Block a user