New source found from dndbeyond.com
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
export const QUICK_BUILD_REQUEST = "builder.QUICK_BUILD_REQUEST";
|
||||
export const RANDOM_BUILD_REQUEST = "builder.RANDOM_BUILD_REQUEST";
|
||||
export const STEP_BUILD_REQUEST = "builder.STEP_BUILD_REQUEST";
|
||||
export const CHARACTER_LOAD_REQUEST = "builder.CHARACTER_LOAD_REQUEST";
|
||||
export const BUILDER_METHOD_SET = "builder.BUILDER_METHOD_SET";
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
CharacterLoadRequestAction,
|
||||
ConfirmClassSetAction,
|
||||
ConfirmSpeciesSetAction,
|
||||
QuickBuildRequestAction,
|
||||
RandomBuildRequestAction,
|
||||
ShowHelpTextSetAction,
|
||||
StepBuildRequestAction,
|
||||
SuggestedNamesRequestAction,
|
||||
@@ -28,46 +26,6 @@ export function characterLoadRequest(): CharacterLoadRequestAction {
|
||||
};
|
||||
}
|
||||
|
||||
export function quickBuildRequest(
|
||||
entitySpeciesId: number | null,
|
||||
entitySpeciesTypeId: number | null,
|
||||
classId: number | null,
|
||||
name: string | null
|
||||
): QuickBuildRequestAction {
|
||||
return {
|
||||
type: builderActionTypes.QUICK_BUILD_REQUEST,
|
||||
payload: {
|
||||
entityRaceId: entitySpeciesId,
|
||||
entityRaceTypeId: entitySpeciesTypeId,
|
||||
classId,
|
||||
name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function randomBuildRequest(
|
||||
level: number | null,
|
||||
entitySpeciesId: number | null,
|
||||
entitySpeciesTypeId: number | null,
|
||||
classId: number | null,
|
||||
allowMulticlass: boolean,
|
||||
allowFeats: boolean,
|
||||
name: string | null
|
||||
): RandomBuildRequestAction {
|
||||
return {
|
||||
type: builderActionTypes.RANDOM_BUILD_REQUEST,
|
||||
payload: {
|
||||
level,
|
||||
entityRaceId: entitySpeciesId,
|
||||
entityRaceTypeId: entitySpeciesTypeId,
|
||||
classId,
|
||||
allowMulticlass,
|
||||
allowFeats,
|
||||
name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function stepBuildRequest(
|
||||
showHelpText: boolean
|
||||
): StepBuildRequestAction {
|
||||
@@ -110,7 +68,9 @@ export function showHelpTextSet(showHelpText: boolean): ShowHelpTextSetAction {
|
||||
};
|
||||
}
|
||||
|
||||
export function builderMethodSet(method: string): BuilderMethodSetAction {
|
||||
export function builderMethodSet(
|
||||
method: string | null
|
||||
): BuilderMethodSetAction {
|
||||
return {
|
||||
type: builderActionTypes.BUILDER_METHOD_SET,
|
||||
payload: {
|
||||
|
||||
+5
-2
@@ -5,7 +5,10 @@ import {
|
||||
HelperUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { InputField } from "~/subApps/builder/components/InputField";
|
||||
import {
|
||||
InputField,
|
||||
InputElements,
|
||||
} from "~/subApps/builder/components/InputField";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
@@ -21,7 +24,7 @@ export default function AbilityScoreManagerManual({
|
||||
maxScore = 18,
|
||||
}: Props) {
|
||||
const handleBlur = (
|
||||
e: FocusEvent<HTMLInputElement>,
|
||||
e: FocusEvent<InputElements>,
|
||||
abilityScore: AbilityManager
|
||||
) => {
|
||||
// Get the int value from the input
|
||||
|
||||
+5
-3
@@ -1,12 +1,12 @@
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
HelperUtils,
|
||||
HtmlSelectOption,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Select } from "~/components/Select";
|
||||
|
||||
const totalPoints: number = 27;
|
||||
const scoreChoices: Array<number> = [8, 9, 10, 11, 12, 13, 14, 15];
|
||||
const scorePoints: Record<number, number> = {
|
||||
@@ -114,7 +114,9 @@ export default function AbilityScoreManagerPointBuy({ abilities }: Props) {
|
||||
onChange={(value) =>
|
||||
abilityScore.handleScoreChange(Number(value))
|
||||
}
|
||||
initialOptionRemoved={true}
|
||||
optionsWidth="large"
|
||||
name={uId}
|
||||
hidePlaceholderOption
|
||||
/>
|
||||
</span>
|
||||
<div className="ability-score-manager-stat-total">
|
||||
|
||||
+5
-3
@@ -1,12 +1,13 @@
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
HelperUtils,
|
||||
HtmlSelectOption,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Select } from "~/components/Select";
|
||||
|
||||
import { TypeScriptUtils } from "../../../Shared/utils";
|
||||
|
||||
const standardScores: Array<number> = [8, 10, 12, 13, 14, 15];
|
||||
@@ -61,12 +62,13 @@ export default function AbilityScoreManagerStandardArray({ abilities }: Props) {
|
||||
<Select
|
||||
id={uId}
|
||||
options={availableScoreOptions}
|
||||
value={abilityScore.getBaseScore()}
|
||||
onChange={(value) => {
|
||||
value={abilityScore.ability.baseScore}
|
||||
onChange={(value: string) => {
|
||||
const parsedValue = HelperUtils.parseInputInt(value);
|
||||
abilityScore.handleScoreChange(parsedValue);
|
||||
}}
|
||||
placeholder={"--"}
|
||||
name={uId}
|
||||
/>
|
||||
</span>
|
||||
<div className="ability-score-manager-stat-total">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import React, { JSX, PropsWithChildren } from "react";
|
||||
|
||||
import { Button } from "@dndbeyond/character-components/es";
|
||||
|
||||
interface withBuilderProps {
|
||||
interface withBuilderProps extends PropsWithChildren {
|
||||
clsNames: Array<string>;
|
||||
}
|
||||
function withBuilderButton<P extends object, C>(
|
||||
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
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
@@ -1,4 +0,0 @@
|
||||
import { RadioGroup } from "./RadioGroup";
|
||||
|
||||
export default RadioGroup;
|
||||
export { RadioGroup };
|
||||
@@ -1,8 +0,0 @@
|
||||
export const TitleH3 = {
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Roboto Condensed",
|
||||
letterSpacing: "normal",
|
||||
lineHeight: 1.5,
|
||||
marginTop: 4,
|
||||
};
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import ClassDisplaySimple from "./ClassDisplaySimple";
|
||||
|
||||
export default ClassDisplaySimple;
|
||||
export { ClassDisplaySimple };
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { Select } from "~/components/Select";
|
||||
import { FeatDetail } from "~/subApps/sheet/components/Sidebar/panes/FeatsManagePane/FeatDetail";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
|
||||
@@ -64,10 +64,11 @@ export const GrantedFeat: React.FC<GrantedFeatProps> = ({
|
||||
if (!featList.isSingleFeat()) {
|
||||
selectBox = (
|
||||
<Select
|
||||
onChangePromise={(...args) => featList.handleChoiceSelected(...args)}
|
||||
className="description-manage-background-granted-feat-chooser"
|
||||
onChangeConfirm={(...args) => featList.handleChoiceSelected(...args)}
|
||||
options={featList.availableChoices}
|
||||
value={featList.chosenFeatId !== null ? featList.chosenFeatId : -1}
|
||||
clsNames={["description-manage-background-granted-feat-chooser"]}
|
||||
name={`granted-feat-select-${featList.definition.id}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { Checkbox, Select } from "@dndbeyond/character-components/es";
|
||||
import { Checkbox } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
Constants,
|
||||
FormatUtils,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
import { CollapsibleContent } from "~/components/CollapsibleContent";
|
||||
import { Link } from "~/components/Link";
|
||||
import { Select } from "~/components/Select";
|
||||
|
||||
export interface AffectedFeatureInfo {
|
||||
name: string;
|
||||
@@ -161,20 +162,20 @@ export class OptionalFeature extends React.PureComponent<
|
||||
}
|
||||
|
||||
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}
|
||||
onChangeConfirm={this.handleReplacementChangePromise}
|
||||
disabled={!isSelected}
|
||||
preventClickPropagating={true}
|
||||
name={"ct-optional-feature__select-input"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { PureComponent, PropsWithChildren } from "react";
|
||||
|
||||
interface Props {
|
||||
interface Props extends PropsWithChildren {
|
||||
clsNames: Array<string>;
|
||||
}
|
||||
export default class Page extends React.PureComponent<Props> {
|
||||
export default class Page extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
clsNames: [],
|
||||
};
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { PureComponent, PropsWithChildren } from "react";
|
||||
|
||||
export default class PageHeader extends React.PureComponent {
|
||||
export default class PageHeader extends PureComponent<PropsWithChildren> {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { PureComponent, PropsWithChildren } from "react";
|
||||
|
||||
export default class PageSubHeader extends React.PureComponent {
|
||||
export default class PageSubHeader extends PureComponent<PropsWithChildren> {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import SpeciesDisplaySimple from "./SpeciesDisplaySimple";
|
||||
|
||||
export default SpeciesDisplaySimple;
|
||||
export { SpeciesDisplaySimple };
|
||||
@@ -1,10 +1,7 @@
|
||||
import React from "react";
|
||||
import { matchPath } from "react-router-dom";
|
||||
|
||||
import {
|
||||
FeatureFlagInfoState,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
// export interface RouteDefinition {
|
||||
// key: string,
|
||||
@@ -21,10 +18,10 @@ import {
|
||||
SectionKey,
|
||||
} from "~/subApps/builder/constants";
|
||||
import { ClassChoose } from "~/subApps/builder/routes/ClassChoose";
|
||||
import { QuickBuild } from "~/subApps/builder/routes/QuickBuild";
|
||||
import { RandomBuild } from "~/subApps/builder/routes/RandomBuild";
|
||||
import { Home } from "~/subApps/builder/routes/Home";
|
||||
import { SpeciesChoose } from "~/subApps/builder/routes/SpeciesChoose";
|
||||
|
||||
import { WhatsNext } from "../../../../subApps/builder/routes/WhatsNext";
|
||||
import AbilityScoresHelp from "../containers/pages/AbilityScoresHelp";
|
||||
import AbilityScoresManage from "../containers/pages/AbilityScoresManage";
|
||||
import ClassHelp from "../containers/pages/ClassHelp";
|
||||
@@ -33,41 +30,23 @@ import DescriptionHelp from "../containers/pages/DescriptionHelp";
|
||||
import DescriptionManage from "../containers/pages/DescriptionManage";
|
||||
import EquipmentHelp from "../containers/pages/EquipmentHelp";
|
||||
import EquipmentManage from "../containers/pages/EquipmentManage";
|
||||
import HomeBasicInfo from "../containers/pages/HomeBasicInfo";
|
||||
import HomeHelp from "../containers/pages/HomeHelp";
|
||||
import SpeciesHelp from "../containers/pages/SpeciesHelp";
|
||||
import SpeciesManage from "../containers/pages/SpeciesManage";
|
||||
import WhatsNext from "../containers/pages/WhatsNext";
|
||||
import { NavigationUtils } from "../utils";
|
||||
|
||||
// TODO: remove the use of either `Component` or `getComponent` on the ROUTE_DEFINITIONS and we can get rid of the no-dd-sa rule..
|
||||
|
||||
const BASE_PATHNAME = config.basePathname;
|
||||
|
||||
export const ROUTE_DEFINITIONS = {
|
||||
[RouteKey.QUICK_BUILD]: {
|
||||
key: RouteKey.QUICK_BUILD,
|
||||
type: NavigationType.Page,
|
||||
path: `${BASE_PATHNAME}/builder/quick-build`,
|
||||
Component: QuickBuild,
|
||||
getComponent: () => <QuickBuild />,
|
||||
checkCanAccess: (routeDef, state) => {
|
||||
return NavigationUtils.checkStdBuilderPageRequirements(routeDef, state);
|
||||
},
|
||||
},
|
||||
[RouteKey.RANDOMIZE_BUILD]: {
|
||||
key: RouteKey.RANDOMIZE_BUILD,
|
||||
type: NavigationType.Page,
|
||||
path: `${BASE_PATHNAME}/builder/randomize`,
|
||||
Component: RandomBuild,
|
||||
getComponent: () => <RandomBuild />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
},
|
||||
[RouteKey.HOME_BASIC_INFO]: {
|
||||
key: RouteKey.HOME_BASIC_INFO,
|
||||
type: NavigationType.Page,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/home/basic`,
|
||||
Component: HomeBasicInfo,
|
||||
getComponent: () => <HomeBasicInfo />,
|
||||
Component: Home,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <Home />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
},
|
||||
@@ -77,7 +56,8 @@ export const ROUTE_DEFINITIONS = {
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/species/choose`,
|
||||
Component: SpeciesChoose,
|
||||
getComponent: () => <SpeciesChoose />,
|
||||
checkCanAccess: (routeDef, state, dispatch) => {
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
checkCanAccess: (routeDef, state) => {
|
||||
if (!NavigationUtils.checkStdBuilderPageRequirements(routeDef, state)) {
|
||||
return false;
|
||||
}
|
||||
@@ -95,7 +75,12 @@ export const ROUTE_DEFINITIONS = {
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/species/manage`,
|
||||
Component: SpeciesManage,
|
||||
getComponent: () => <SpeciesManage />,
|
||||
checkCanAccess: (routeDef, state, dispatch) => {
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
checkCanAccess: (routeDef, state) => {
|
||||
if (!NavigationUtils.checkStdBuilderPageRequirements(routeDef, state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rulesEngineSelectors.getRace(state) === null) {
|
||||
return false;
|
||||
}
|
||||
@@ -109,7 +94,8 @@ export const ROUTE_DEFINITIONS = {
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/class/choose`,
|
||||
Component: ClassChoose,
|
||||
getComponent: () => <ClassChoose />,
|
||||
checkCanAccess: (routeDef, state, dispatch) => {
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
checkCanAccess: (routeDef, state) => {
|
||||
if (!NavigationUtils.checkStdBuilderPageRequirements(routeDef, state)) {
|
||||
return false;
|
||||
}
|
||||
@@ -127,7 +113,8 @@ export const ROUTE_DEFINITIONS = {
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/class/manage`,
|
||||
Component: ClassesManage,
|
||||
getComponent: () => <ClassesManage />,
|
||||
checkCanAccess: (routeDef, state, dispatch) => {
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
checkCanAccess: (routeDef, state) => {
|
||||
if (!NavigationUtils.checkStdBuilderPageRequirements(routeDef, state)) {
|
||||
return false;
|
||||
}
|
||||
@@ -144,6 +131,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.Page,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/ability-scores/manage`,
|
||||
Component: AbilityScoresManage,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <AbilityScoresManage />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -153,6 +141,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.Page,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/description/manage`,
|
||||
Component: DescriptionManage,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <DescriptionManage />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -162,6 +151,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.Page,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/equipment/manage`,
|
||||
Component: EquipmentManage,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <EquipmentManage />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -172,6 +162,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.Page,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/whats-next`,
|
||||
Component: WhatsNext,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <WhatsNext />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -182,6 +173,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.HelpPage,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/home/help`,
|
||||
Component: HomeHelp,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <HomeHelp />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -191,6 +183,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.HelpPage,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/species/help`,
|
||||
Component: SpeciesHelp,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <SpeciesHelp />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -200,6 +193,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.HelpPage,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/class/help`,
|
||||
Component: ClassHelp,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <ClassHelp />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -209,6 +203,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.HelpPage,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/ability-scores/help`,
|
||||
Component: AbilityScoresHelp,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <AbilityScoresHelp />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -218,6 +213,7 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.HelpPage,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/description/help`,
|
||||
Component: DescriptionHelp,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <DescriptionHelp />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
@@ -227,49 +223,51 @@ export const ROUTE_DEFINITIONS = {
|
||||
type: NavigationType.HelpPage,
|
||||
path: `${BASE_PATHNAME}/:characterId/builder/equipment/help`,
|
||||
Component: EquipmentHelp,
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => <EquipmentHelp />,
|
||||
checkCanAccess: (routeDef, state) =>
|
||||
NavigationUtils.checkStdBuilderPageRequirements(routeDef, state),
|
||||
},
|
||||
};
|
||||
|
||||
export interface RouteDef {
|
||||
key: string; // get the thing as an enum
|
||||
type: typeof NavigationType;
|
||||
type: string;
|
||||
path: string;
|
||||
Component: React.ComponentType<any>;
|
||||
getComponent: () => React.ComponentType<any>;
|
||||
// no-dd-sa:typescript-best-practices/no-dupe-keys
|
||||
getComponent: () => React.ReactElement;
|
||||
checkCanAccess?: (routeDef: RouteDef, state: any) => boolean;
|
||||
}
|
||||
|
||||
export const getAllRouterRoutes = () => {
|
||||
return Object.values(ROUTE_DEFINITIONS).map((routeDef: RouteDef) => ({
|
||||
return (Object.values(ROUTE_DEFINITIONS) as RouteDef[]).map((routeDef) => ({
|
||||
element: <routeDef.Component />,
|
||||
path: routeDef.path,
|
||||
routeDef,
|
||||
}));
|
||||
};
|
||||
|
||||
const addSearchParamsToRouteDef = (routeDef: RouteDef): RouteDef => {
|
||||
let params = "";
|
||||
if (typeof window !== "undefined") {
|
||||
// If we ever need to know specific search params, we can use URLSearchParams here instead.
|
||||
params = window.location.search;
|
||||
}
|
||||
return {
|
||||
...routeDef,
|
||||
path: `${routeDef.path}${params}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const getRouteDef = (routeKey) => ROUTE_DEFINITIONS[routeKey];
|
||||
export const getRouteDefPath = (routeKey) => getRouteDef(routeKey).path;
|
||||
export const getRouteDefPath = (routeKey) => {
|
||||
const basePath = getRouteDef(routeKey);
|
||||
return addSearchParamsToRouteDef(basePath).path;
|
||||
};
|
||||
export const getRouteDefType = (routeKey) => getRouteDef(routeKey).type;
|
||||
|
||||
export const NAVIGATION_DEFINITIONS = {
|
||||
[BuilderMethod.QUICK]: [
|
||||
{
|
||||
type: NavigationType.Section,
|
||||
key: SectionKey.QUICK_BUILD,
|
||||
getLabel: () => "Quick Build",
|
||||
routes: [getRouteDef(RouteKey.QUICK_BUILD)],
|
||||
},
|
||||
],
|
||||
[BuilderMethod.RANDOMIZE]: [
|
||||
{
|
||||
type: NavigationType.Section,
|
||||
key: SectionKey.RANDOMIZE,
|
||||
getLabel: () => "Randomize Build",
|
||||
routes: [getRouteDef(RouteKey.RANDOMIZE_BUILD)],
|
||||
},
|
||||
],
|
||||
[BuilderMethod.STEP_BY_STEP]: [
|
||||
{
|
||||
type: NavigationType.Section,
|
||||
@@ -346,12 +344,8 @@ export function checkIsRouteInSection(testRouteDef, sectionRoutes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getNavigationSections(
|
||||
builderMethod,
|
||||
currentRouteDef,
|
||||
featureFlags: FeatureFlagInfoState
|
||||
) {
|
||||
const navConfig = getNavBuilderConfig(builderMethod, featureFlags);
|
||||
export function getNavigationSections(builderMethod, currentRouteDef) {
|
||||
const navConfig = getNavBuilderConfig(builderMethod);
|
||||
|
||||
return navConfig.map((section) => ({
|
||||
...section,
|
||||
@@ -359,12 +353,17 @@ export function getNavigationSections(
|
||||
}));
|
||||
}
|
||||
|
||||
export function getNavBuilderConfig(
|
||||
builderMethod,
|
||||
featureFlags: FeatureFlagInfoState
|
||||
) {
|
||||
export function getNavBuilderConfig(builderMethod) {
|
||||
if (builderMethod) {
|
||||
return NAVIGATION_DEFINITIONS[builderMethod];
|
||||
const config = NAVIGATION_DEFINITIONS[builderMethod];
|
||||
|
||||
// Add search params to all route paths in the config
|
||||
return config.map((section) => ({
|
||||
...section,
|
||||
routes: section.routes.map((routeDef) =>
|
||||
addSearchParamsToRouteDef(routeDef)
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
+67
-17
@@ -1,5 +1,12 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { connect, DispatchProp, useSelector } from "react-redux";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import {
|
||||
DiceRollGroupManager,
|
||||
@@ -18,13 +25,14 @@ import {
|
||||
Constants,
|
||||
RollGroupContract,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import {
|
||||
DiceNotation,
|
||||
DiceOperation,
|
||||
DieTerm,
|
||||
RollRequest,
|
||||
RollRequestRoll,
|
||||
} from "@dndbeyond/dice";
|
||||
import { getCharacterName } from "@dndbeyond/character-rules-engine/es/engine/Campaign/utils";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
import { DieTypes, RollTos } from "@dndbeyond/pocket-dimension-dice/constants";
|
||||
import { DiceOperations } from "@dndbeyond/pocket-dimension-dice/enums";
|
||||
import { useDiceRollPrivacyStore } from "@dndbeyond/pocket-dimension-dice/hooks/useDiceRollPrivacyStore";
|
||||
import { RollDicePayload } from "@dndbeyond/pocket-dimension-dice/types";
|
||||
|
||||
import useUser from "~/hooks/useUser";
|
||||
|
||||
import { rollResultActions } from "../../../Shared/actions/rollResult";
|
||||
import * as toastActions from "../../../Shared/actions/toastMessage/actions";
|
||||
@@ -41,7 +49,10 @@ interface AbilityScore {
|
||||
statId: number | null;
|
||||
value: number | null;
|
||||
}
|
||||
interface AbilityScoreDiceManagerProps extends DispatchProp {
|
||||
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface AbilityScoreDiceManagerProps extends DispatchProp<AnyAction> {
|
||||
abilities: Array<Ability>;
|
||||
configuration: CharacterConfiguration;
|
||||
rollResultGroupsLookup: RollResultGroupsLookup;
|
||||
@@ -58,8 +69,22 @@ const AbilityScoreDiceManager: React.FunctionComponent<
|
||||
rollStatusLookup,
|
||||
dispatch,
|
||||
}) => {
|
||||
const [{ entityId, activeCampaign, userId }] = useContext(GameLogContext);
|
||||
const characterName = useSelector(rulesEngineSelectors.getName) ?? undefined;
|
||||
const SIMULATED_GROUP_COUNT: number = 1;
|
||||
const SIMULATED_ROLL_RESULT_COUNT: number = 6;
|
||||
const rollTo = useDiceRollPrivacyStore(
|
||||
(state) => state.getDiceRollPrivacy(Number(userId))?.rollTo
|
||||
);
|
||||
const messageScope = rollTo === RollTos.Self ? "userId" : "gameId";
|
||||
const messageTarget = String(
|
||||
rollTo === RollTos.Self
|
||||
? userId === activeCampaign?.dmId
|
||||
? userId
|
||||
: activeCampaign?.dmId
|
||||
: activeCampaign?.id
|
||||
);
|
||||
const avatarUrl = useUser()?.avatarUrl;
|
||||
|
||||
const [loadingStatus, setLoadingStatus] = useState(
|
||||
DataLoadingStatusEnum.NOT_INITIALIZED
|
||||
@@ -122,14 +147,39 @@ const AbilityScoreDiceManager: React.FunctionComponent<
|
||||
setLoadingStatus,
|
||||
]);
|
||||
|
||||
const diceRollRequest = useMemo<RollRequest>(() => {
|
||||
const dieTerm: DieTerm = new DieTerm(4, "d6", DiceOperation.Max, 3);
|
||||
const rollRequestRoll = new RollRequestRoll(new DiceNotation([dieTerm]));
|
||||
return {
|
||||
const diceRollRequest = useMemo<RollDicePayload["data"]>(
|
||||
() => ({
|
||||
action: "Ability Score",
|
||||
rolls: [rollRequestRoll],
|
||||
};
|
||||
}, []);
|
||||
rolls: [
|
||||
{
|
||||
diceNotation: {
|
||||
constant: 0,
|
||||
set: [
|
||||
{
|
||||
count: 4,
|
||||
dice: [...Array(4)].map(() => ({
|
||||
dieType: DieTypes.d6,
|
||||
})),
|
||||
dieType: DieTypes.d6,
|
||||
operand: 3,
|
||||
operation: DiceOperations.Max,
|
||||
},
|
||||
],
|
||||
},
|
||||
rollType: "roll",
|
||||
},
|
||||
],
|
||||
context: {
|
||||
avatarUrl,
|
||||
messageTarget,
|
||||
entityId: String(entityId),
|
||||
entityType: "character",
|
||||
name: characterName,
|
||||
messageScope,
|
||||
},
|
||||
}),
|
||||
[avatarUrl, characterName, entityId, messageScope, messageTarget]
|
||||
);
|
||||
|
||||
const abilityStatOptions = useMemo<Array<HtmlSelectOption>>(() => {
|
||||
return abilities.map((ability) => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import AbilityScoreDiceManager from "./AbilityScoreDiceManager";
|
||||
|
||||
export { AbilityScoreDiceManager };
|
||||
|
||||
export default AbilityScoreDiceManager;
|
||||
+13
-5
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
characterActions,
|
||||
CharacterConfiguration,
|
||||
@@ -11,9 +11,14 @@ import {
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { BuilderAppState } from "../../typings";
|
||||
import { Select } from "~/components/Select";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
import { BuilderAppState } from "../../typings";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
configuration: CharacterConfiguration;
|
||||
initialOptionRemoved: boolean;
|
||||
onScoreMethodChange?: (value: number | null) => void;
|
||||
@@ -32,6 +37,8 @@ class AbilityScoreTypeChooser extends React.PureComponent<Props> {
|
||||
|
||||
dispatch(characterActions.abilityScoreTypeSetRequest(parsedValue));
|
||||
|
||||
// We need to reset the ability scores value based on which option is selected
|
||||
|
||||
if (onScoreMethodChange) {
|
||||
onScoreMethodChange(parsedValue);
|
||||
}
|
||||
@@ -61,13 +68,14 @@ class AbilityScoreTypeChooser extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ability-score-type-chooser">
|
||||
<div className={styles.typeChooser}>
|
||||
<Select
|
||||
options={scoreMethodOptions}
|
||||
onChange={this.handleScoreMethodChange}
|
||||
initialOptionRemoved={initialOptionRemoved}
|
||||
value={configuration.abilityScoreType}
|
||||
placeholder="-- Choose a Generation Method --"
|
||||
name="ability-score-type"
|
||||
hidePlaceholderOption={initialOptionRemoved}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+90
-61
@@ -1,9 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import { connect, DispatchProp, useDispatch } from "react-redux";
|
||||
import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
import {
|
||||
matchPath,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from "react-router-dom";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import { CharacterAvatarPortrait } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CampaignDataContract,
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
@@ -18,13 +27,15 @@ import {
|
||||
import { Link } from "~/components/Link";
|
||||
import { NotificationSystem } from "~/components/NotificationSystem";
|
||||
import config from "~/config";
|
||||
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||
import { useHeadContext } from "~/contexts/Head";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { PortraitName } from "~/subApps/builder/components/PortraitName/PortraitName";
|
||||
import { BuilderMethod } from "~/subApps/builder/constants";
|
||||
import { ClassProvider } from "~/subApps/builder/contexts/Class";
|
||||
import { ModalManagerProvider } from "~/subApps/builder/contexts/ModalManager";
|
||||
import { SpeciesProvider } from "~/subApps/builder/contexts/Species";
|
||||
import { BuilderTypeChoicePage } from "~/subApps/builder/routes/BuilderTypeChoicePage";
|
||||
import pageStyles from "~/subApps/builder/styles/page.module.css";
|
||||
|
||||
import { appEnvActions, toastMessageActions } from "../../../Shared/actions";
|
||||
import LoadingBlocker from "../../../Shared/components/LoadingBlocker";
|
||||
@@ -40,7 +51,11 @@ import {
|
||||
import { AppInfoErrorState } from "../../../Shared/stores/typings";
|
||||
import { builderActions } from "../../actions/builder";
|
||||
import { BUILDER_MQ_IS_MOBILE_SIZE } from "../../config";
|
||||
import { ROUTE_DEFINITIONS } from "../../config/navigation";
|
||||
import {
|
||||
getCurrentRouteDef,
|
||||
RouteDef,
|
||||
ROUTE_DEFINITIONS,
|
||||
} from "../../config/navigation";
|
||||
import { builderSelectors } from "../../selectors";
|
||||
import { BuilderAppState } from "../../typings";
|
||||
import { getUrlWithParams } from "../../utils/navigationUtils";
|
||||
@@ -51,7 +66,9 @@ import styles from "./styles.module.css";
|
||||
|
||||
const BASE_PATHNAME = config.basePathname;
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
appError: AppInfoErrorState | null;
|
||||
toastMessages: any;
|
||||
builderMethod: string | null;
|
||||
@@ -68,6 +85,9 @@ interface Props extends DispatchProp {
|
||||
isReadonly: boolean;
|
||||
userRoles: string[] | null | undefined;
|
||||
characterStatus: string | null;
|
||||
userId: number;
|
||||
campaign: CampaignDataContract | null;
|
||||
isVttView: boolean;
|
||||
}
|
||||
|
||||
const RenderAuthError: React.FC<{ appError: Props["appError"] }> | null = ({
|
||||
@@ -84,55 +104,55 @@ const RenderAuthError: React.FC<{ appError: Props["appError"] }> | null = ({
|
||||
window.location.pathname
|
||||
)}`;
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<p>
|
||||
You are no longer signed into D&D Beyond. Go to the{" "}
|
||||
<Link href={signInUrl}>Sign In page</Link> to continue the
|
||||
adventure!
|
||||
</p>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
|
||||
case AppErrorTypeEnum.API_FAIL:
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<p>
|
||||
Whoops! We rolled a 1 on our API check. We're heading into town to
|
||||
visit the blacksmith for repairs.
|
||||
</p>
|
||||
<p>Try again after a Short Rest.</p>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
|
||||
case AppErrorTypeEnum.API_DATA_FAIL:
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<p>
|
||||
Whoops! We rolled a 1 on our API Data check. We're heading into town
|
||||
to visit the blacksmith for repairs.
|
||||
</p>
|
||||
<p>Try again after a Short Rest.</p>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
|
||||
case AppErrorTypeEnum.AUTH_FAIL:
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<p>
|
||||
Whoops! We rolled a 1 on our Authentication check. We're heading
|
||||
into town to visit the blacksmith for repairs.
|
||||
</p>
|
||||
<p>Try again after a Short Rest.</p>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
|
||||
case AppErrorTypeEnum.ACCESS_DENIED:
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<p>You don't have access to this page.</p>
|
||||
<p>
|
||||
If you are trying to access a character sheet, make sure its privacy
|
||||
@@ -143,7 +163,7 @@ const RenderAuthError: React.FC<{ appError: Props["appError"] }> | null = ({
|
||||
the endless dungeon and the ancient dragon Rylzrayrth is rising up
|
||||
to block your path...
|
||||
</p>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case AppErrorTypeEnum.NOT_FOUND:
|
||||
@@ -163,13 +183,13 @@ const RenderAuthError: React.FC<{ appError: Props["appError"] }> | null = ({
|
||||
case AppErrorTypeEnum.GENERIC:
|
||||
default:
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<p>
|
||||
Whoops! We rolled a 1 on our check. We're heading into town to visit
|
||||
the blacksmith for repairs.
|
||||
</p>
|
||||
<p>Try again after a Short Rest.</p>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -185,7 +205,7 @@ const RenderAuthError: React.FC<{ appError: Props["appError"] }> | null = ({
|
||||
<div className="ct-character-sheet__failed-content">
|
||||
{contentNode}
|
||||
{appError.errorId && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<div className="ct-character-sheet__failed-code">
|
||||
<div className="ct-character-sheet__failed-code-label">
|
||||
Error Code
|
||||
@@ -202,7 +222,7 @@ const RenderAuthError: React.FC<{ appError: Props["appError"] }> | null = ({
|
||||
{config.version}
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,6 +277,9 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
isReadonly,
|
||||
userRoles,
|
||||
characterStatus,
|
||||
userId,
|
||||
campaign,
|
||||
isVttView,
|
||||
}) => {
|
||||
let mobileMql: MediaQueryList | null = null;
|
||||
// useImperativeHandle is hard to type when we us our own context this will just disapear
|
||||
@@ -266,8 +289,7 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
const { pathname } = useLocation();
|
||||
const { setTitle } = useHeadContext();
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
const { featureFlags } = useFeatureFlags();
|
||||
const prevPathnameRef = React.useRef<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (characterId !== null && !isLoaded) {
|
||||
@@ -297,12 +319,15 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
React.useEffect(() => {
|
||||
//check for isCharacterLoaded and canEdit to set isReadonly and redirect, or is Random build to set isReadonly
|
||||
if (isCharacterLoaded) {
|
||||
dispatch(appEnvActions.dataSet({ isReadonly: !canEdit }));
|
||||
dispatch(
|
||||
appEnvActions.dataSet({
|
||||
isReadonly: !canEdit,
|
||||
isUserDM: Number(userId) === campaign?.dmUserId,
|
||||
})
|
||||
);
|
||||
if (!canEdit) {
|
||||
navigate(getUrlWithParams(`${BASE_PATHNAME}/${characterId}`));
|
||||
}
|
||||
} else if (builderMethod && builderMethod === BuilderMethod.RANDOMIZE) {
|
||||
dispatch(appEnvActions.dataSet({ isReadonly: !canEdit }));
|
||||
}
|
||||
}, [
|
||||
isCharacterLoaded,
|
||||
@@ -311,25 +336,13 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
navigate,
|
||||
dispatch,
|
||||
builderMethod,
|
||||
userId,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
checkForMessages(toastMessages, dispatch, notificationSystem);
|
||||
});
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const toolsTarget = document.getElementById("character-tools-target");
|
||||
if (toolsTarget) {
|
||||
toolsTarget.style.zIndex = "0";
|
||||
}
|
||||
return () => {
|
||||
const toolsTarget = document.getElementById("character-tools-target");
|
||||
if (toolsTarget) {
|
||||
toolsTarget.style.zIndex = "auto";
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setTitle("Character Builder");
|
||||
}, [setTitle]);
|
||||
@@ -377,11 +390,15 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<div className="character-builder">
|
||||
<SynchronousBlocker />
|
||||
<div className={styles.characterBuilder}>
|
||||
<div className="character-builder-inner">
|
||||
{anonNode}
|
||||
{<BuilderTypeChoicePage isEnabled={!!username} />}
|
||||
{
|
||||
<BuilderTypeChoicePage
|
||||
className="builder-methods"
|
||||
isEnabled={!!username}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -414,7 +431,7 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
<SynchronousBlocker />
|
||||
<div className="character-builder-inner">
|
||||
{builderMethod === BuilderMethod.STEP_BY_STEP && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
{!isMobile && (
|
||||
<div className="character-builder-page-header">
|
||||
<h1 className="character-builder-page-header-heading">
|
||||
@@ -447,26 +464,27 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
pathname={pathname}
|
||||
characterId={characterId}
|
||||
/>
|
||||
</React.Fragment>
|
||||
<PortraitName
|
||||
useDefaultCharacterName={true}
|
||||
className={styles.portraitName}
|
||||
/>
|
||||
<hr className={pageStyles.divider} />
|
||||
</>
|
||||
)}
|
||||
<Routes>
|
||||
{Object.values(ROUTE_DEFINITIONS).map(
|
||||
({ key, path, getComponent }) => {
|
||||
const Component = getComponent(featureFlags);
|
||||
// console.log('COMPONENT', Component);
|
||||
return (
|
||||
<Route
|
||||
key={key}
|
||||
path={path
|
||||
.replace(`${BASE_PATHNAME}/builder/`, "")
|
||||
.replace(
|
||||
`${BASE_PATHNAME}/:characterId/builder/`,
|
||||
""
|
||||
)}
|
||||
element={Component}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{(Object.values(ROUTE_DEFINITIONS) as RouteDef[]).map(
|
||||
({ key, path, getComponent }) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={path
|
||||
.replace(`${BASE_PATHNAME}/builder/`, "")
|
||||
.replace(
|
||||
`${BASE_PATHNAME}/:characterId/builder/`,
|
||||
""
|
||||
)}
|
||||
element={getComponent()}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Route
|
||||
path="/"
|
||||
@@ -480,7 +498,10 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
)}
|
||||
<NotificationSystem ref={notificationSystem} />
|
||||
{diceEnabled && !isReadonly ? (
|
||||
<DiceContainer canShow={isCharacterLoaded} />
|
||||
<DiceContainer
|
||||
canShow={isCharacterLoaded}
|
||||
isVttView={isVttView}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
@@ -489,6 +510,12 @@ const CharacterBuilder: React.FC<Props> | null = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CharacterBuilderWrapper = (props) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const isVttView = searchParams.get("view") === "vtt";
|
||||
return <CharacterBuilder {...props} isVttView={isVttView} />;
|
||||
};
|
||||
|
||||
function mapStateToProps(state: BuilderAppState) {
|
||||
return {
|
||||
toastMessages: toastMessageSelectors.getToastMessages(state),
|
||||
@@ -507,7 +534,9 @@ function mapStateToProps(state: BuilderAppState) {
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
userRoles: appEnvSelectors.getUserRoles(state),
|
||||
characterStatus: characterSelectors.getStatusSlug(state),
|
||||
userId: appEnvSelectors.getUserId(state),
|
||||
campaign: characterSelectors.getCampaign(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CharacterBuilder);
|
||||
export default connect(mapStateToProps)(CharacterBuilderWrapper);
|
||||
|
||||
+30
-3
@@ -1,11 +1,37 @@
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useEffect, useLayoutEffect, useState } from "react";
|
||||
|
||||
import { getCharacterSlots } from "~/helpers/characterServiceApi";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { BuilderProvider } from "~/subApps/builder/contexts/Builder";
|
||||
import { PremadeListing } from "~/subApps/builder/routes/PremadeListing";
|
||||
import { appEnvActions } from "~/tools/js/Shared/actions/appEnv";
|
||||
|
||||
import ErrorBoundary from "../../../Shared/components/ErrorBoundary";
|
||||
import CharacterBuilder from "../CharacterBuilder";
|
||||
|
||||
export function CharacterBuilderContainer(props) {
|
||||
interface CharacterBuilderContainerProps {
|
||||
isPremadeListing?: boolean;
|
||||
}
|
||||
|
||||
export function CharacterBuilderContainer(
|
||||
props: CharacterBuilderContainerProps
|
||||
) {
|
||||
const dispatch = useDispatch();
|
||||
const [isSlotLimitsLoaded, setIsSlotLimitsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSlotLimitsLoaded) {
|
||||
getCharacterSlots().then((characterSlots) => {
|
||||
dispatch(
|
||||
appEnvActions.dataSet({
|
||||
characterSlots,
|
||||
})
|
||||
);
|
||||
setIsSlotLimitsLoaded(true);
|
||||
});
|
||||
}
|
||||
}, [isSlotLimitsLoaded]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document
|
||||
.getElementsByTagName("body")[0]
|
||||
@@ -26,10 +52,11 @@ export function CharacterBuilderContainer(props) {
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BuilderProvider>
|
||||
<ErrorBoundary>
|
||||
<CharacterBuilder />
|
||||
{props.isPremadeListing ? <PremadeListing /> : <CharacterBuilder />}
|
||||
</ErrorBoundary>
|
||||
</BuilderProvider>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import {
|
||||
CharacterConfiguration,
|
||||
@@ -9,7 +10,9 @@ import {
|
||||
import { builderActions } from "../../actions";
|
||||
import { BuilderAppState } from "../../typings";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
configuration: CharacterConfiguration;
|
||||
}
|
||||
interface State {
|
||||
|
||||
+127
-124
@@ -1,17 +1,21 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import React, { FC } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import SwipeableViews from "react-swipeable-views";
|
||||
|
||||
import { FeatureFlagContext } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
featureFlagInfoSelectors,
|
||||
CharacterTheme,
|
||||
FormatUtils,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import CharacterSheetSvg from "@dndbeyond/fontawesome-cache/svgs/light/address-card.svg";
|
||||
import { GameLogNotificationWrapper } from "@dndbeyond/game-log-components";
|
||||
|
||||
import { Link } from "~/components/Link";
|
||||
import { PremadeCharacterEditStatus } from "~/components/PremadeCharacterEditStatus";
|
||||
import { Tooltip } from "~/components/Tooltip";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { MobileMessengerUtils } from "../../../Shared/utils";
|
||||
@@ -19,6 +23,7 @@ import { navigationConfig } from "../../config";
|
||||
import { builderEnvSelectors, builderSelectors } from "../../selectors";
|
||||
import { BuilderAppState } from "../../typings";
|
||||
import HelpTextManager from "../HelpTextManager";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props {
|
||||
characterId: number | null;
|
||||
@@ -31,114 +36,118 @@ interface Props {
|
||||
isMobile: boolean;
|
||||
characterStatus: CharacterStatusSlug | null;
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class NavigationSections extends React.PureComponent<Props> {
|
||||
handleSheetShowClick = () => {
|
||||
const { characterId } = this.props;
|
||||
|
||||
const NavigationSections: FC<Props> = ({
|
||||
characterId,
|
||||
isCharacterSheetReady,
|
||||
characterSheetUrl,
|
||||
theme,
|
||||
sections,
|
||||
firstAvailableSectionRoutes,
|
||||
activeSectionIdx,
|
||||
builderMethod,
|
||||
isMobile,
|
||||
characterStatus,
|
||||
isReadonly,
|
||||
}) => {
|
||||
const handleSheetShowClick = () => {
|
||||
if (characterId !== null) {
|
||||
MobileMessengerUtils.sendMessage(
|
||||
MobileMessengerUtils.createShowCharacterSheetMessage(characterId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderNonMobileUi = (): React.ReactNode => {
|
||||
const {
|
||||
characterId,
|
||||
sections,
|
||||
firstAvailableSectionRoutes,
|
||||
isCharacterSheetReady,
|
||||
characterSheetUrl,
|
||||
} = this.props;
|
||||
|
||||
let classNames: Array<string> = ["builder-sections-sheet"];
|
||||
if (isCharacterSheetReady) {
|
||||
classNames.push("builder-sections-sheet-ready");
|
||||
} else {
|
||||
classNames.push("builder-sections-sheet-disabled");
|
||||
}
|
||||
const renderCharacterSheetLink = () => {
|
||||
//trading out a Router Link for a div if not ready for accessibility
|
||||
const Component = isCharacterSheetReady ? Link : "div";
|
||||
const CharacterSheetLink = () => (
|
||||
<>
|
||||
<Component
|
||||
className={clsx([
|
||||
styles.characterSheetIcon,
|
||||
!isCharacterSheetReady && styles.disabled,
|
||||
])}
|
||||
href={isCharacterSheetReady ? characterSheetUrl : undefined}
|
||||
onClick={handleSheetShowClick}
|
||||
userouter={isCharacterSheetReady ? true : undefined}
|
||||
data-tooltip-place="bottom"
|
||||
data-tooltip-id={"character-sheet-tooltip"}
|
||||
data-tooltip-content={
|
||||
isCharacterSheetReady
|
||||
? "Go to Character Sheet"
|
||||
: "Character is incomplete"
|
||||
}
|
||||
>
|
||||
<CharacterSheetSvg
|
||||
className={clsx([
|
||||
styles.characterSheetIcon,
|
||||
!isCharacterSheetReady && styles.disabled,
|
||||
])}
|
||||
/>
|
||||
</Component>
|
||||
<Tooltip id="character-sheet-tooltip" />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="builder-sections builder-sections-large">
|
||||
<HelpTextManager />
|
||||
<div className={classNames.join(" ")}>
|
||||
{isCharacterSheetReady ? (
|
||||
<Link
|
||||
className="builder-sections-sheet-icon"
|
||||
href={characterSheetUrl}
|
||||
onClick={this.handleSheetShowClick}
|
||||
useRouter
|
||||
/>
|
||||
) : (
|
||||
<div className="builder-sections-sheet-icon" />
|
||||
)}
|
||||
</div>
|
||||
<div className="builder-sections-large-routes">
|
||||
{sections.map((section) => {
|
||||
const firstRoute = firstAvailableSectionRoutes[section.key];
|
||||
let clsNames: Array<string> = [
|
||||
"builder-sections-link",
|
||||
`builder-sections-${FormatUtils.slugify(section.key)}`,
|
||||
];
|
||||
if (section.active) {
|
||||
clsNames.push("builder-sections-link-active");
|
||||
}
|
||||
return (
|
||||
<div className="builder-sections-view" key={section.key}>
|
||||
<Link
|
||||
href={firstRoute.path.replace(":characterId", characterId)}
|
||||
className={clsNames.join(" ")}
|
||||
useRouter
|
||||
>
|
||||
<FeatureFlagContext.Consumer>
|
||||
{(featureFlags) => section.getLabel(featureFlags)}
|
||||
</FeatureFlagContext.Consumer>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={clsx([
|
||||
"builder-sections-sheet",
|
||||
!isCharacterSheetReady && "builder-sections-sheet-disabled",
|
||||
])}
|
||||
>
|
||||
<GameLogNotificationWrapper
|
||||
themeColor={theme.themeColor}
|
||||
gameLogIsOpen={false}
|
||||
notificationOnClick={() => {}}
|
||||
isCharacterBuilder
|
||||
>
|
||||
<CharacterSheetLink />
|
||||
</GameLogNotificationWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderMobileUi = (): React.ReactNode => {
|
||||
const {
|
||||
characterId,
|
||||
sections,
|
||||
firstAvailableSectionRoutes,
|
||||
activeSectionIdx,
|
||||
isCharacterSheetReady,
|
||||
characterSheetUrl,
|
||||
} = this.props;
|
||||
const renderNonMobileUi = () => (
|
||||
<div className="builder-sections builder-sections-large">
|
||||
<HelpTextManager />
|
||||
{renderCharacterSheetLink()}
|
||||
<div className="builder-sections-large-routes">
|
||||
{sections.map((section) => {
|
||||
const firstRoute = firstAvailableSectionRoutes[section.key];
|
||||
let clsNames: Array<string> = [
|
||||
"builder-sections-link",
|
||||
`builder-sections-${FormatUtils.slugify(section.key)}`,
|
||||
];
|
||||
if (section.active) {
|
||||
clsNames.push("builder-sections-link-active");
|
||||
}
|
||||
return (
|
||||
<div className="builder-sections-view" key={section.key}>
|
||||
<Link
|
||||
href={firstRoute.path.replace(":characterId", characterId)}
|
||||
className={clsNames.join(" ")}
|
||||
userouter
|
||||
>
|
||||
{section.getLabel()}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderMobileUi = () => {
|
||||
const scrollbarStyles: React.CSSProperties = {
|
||||
padding: "0 37.5%",
|
||||
padding: "0 35%",
|
||||
};
|
||||
|
||||
let classNames = ["builder-sections-sheet"];
|
||||
if (isCharacterSheetReady) {
|
||||
classNames.push("builder-sections-sheet-ready");
|
||||
} else {
|
||||
classNames.push("builder-sections-sheet-disabled");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="builder-sections builder-sections-small">
|
||||
<HelpTextManager />
|
||||
<div className={classNames.join(" ")}>
|
||||
{isCharacterSheetReady ? (
|
||||
<Link
|
||||
className="builder-sections-sheet-icon"
|
||||
href={characterSheetUrl}
|
||||
onClick={this.handleSheetShowClick}
|
||||
useRouter
|
||||
/>
|
||||
) : (
|
||||
<div className="builder-sections-sheet-icon" />
|
||||
)}
|
||||
</div>
|
||||
{renderCharacterSheetLink()}
|
||||
<SwipeableViews
|
||||
index={activeSectionIdx}
|
||||
style={scrollbarStyles}
|
||||
@@ -159,11 +168,9 @@ class NavigationSections extends React.PureComponent<Props> {
|
||||
<Link
|
||||
href={firstRoute.path.replace(":characterId", characterId)}
|
||||
className={clsNames.join(" ")}
|
||||
useRouter
|
||||
userouter
|
||||
>
|
||||
<FeatureFlagContext.Consumer>
|
||||
{(featureFlags) => section.getLabel(featureFlags)}
|
||||
</FeatureFlagContext.Consumer>
|
||||
{section.getLabel()}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
@@ -173,41 +180,36 @@ class NavigationSections extends React.PureComponent<Props> {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { builderMethod, isMobile, characterStatus, isReadonly } = this.props;
|
||||
|
||||
if (builderMethod === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isMobile ? (
|
||||
<>
|
||||
{this.renderMobileUi()}
|
||||
<PremadeCharacterEditStatus
|
||||
characterStatus={characterStatus}
|
||||
isReadonly={isReadonly}
|
||||
isBuilderView
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{this.renderNonMobileUi()}
|
||||
<PremadeCharacterEditStatus
|
||||
characterStatus={characterStatus}
|
||||
isReadonly={isReadonly}
|
||||
isBuilderView
|
||||
/>
|
||||
</>
|
||||
);
|
||||
if (builderMethod === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return isMobile ? (
|
||||
<>
|
||||
{renderMobileUi()}
|
||||
<PremadeCharacterEditStatus
|
||||
characterStatus={characterStatus}
|
||||
isReadonly={isReadonly}
|
||||
isBuilderView
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{renderNonMobileUi()}
|
||||
<PremadeCharacterEditStatus
|
||||
characterStatus={characterStatus}
|
||||
isReadonly={isReadonly}
|
||||
isBuilderView
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: BuilderAppState, ownProps) {
|
||||
const currentPath = ownProps.pathname;
|
||||
const sections = navigationConfig.getNavigationSections(
|
||||
builderSelectors.getBuilderMethod(state),
|
||||
navigationConfig.getCurrentRouteDef(currentPath),
|
||||
featureFlagInfoSelectors.getFeatureFlagInfo(state)
|
||||
navigationConfig.getCurrentRouteDef(currentPath)
|
||||
);
|
||||
|
||||
let activeSectionIdx: number = 0;
|
||||
@@ -228,6 +230,7 @@ function mapStateToProps(state: BuilderAppState, ownProps) {
|
||||
isMobile: appEnvSelectors.getIsMobile(state),
|
||||
characterStatus: characterSelectors.getStatusSlug(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,5 @@
|
||||
import clsx from "clsx";
|
||||
import { FC, HTMLAttributes, useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import {
|
||||
characterActions,
|
||||
@@ -12,6 +11,7 @@ import { Button } from "~/components/Button";
|
||||
import { ConfirmModal } from "~/components/ConfirmModal";
|
||||
import { XpManager } from "~/components/XpManager";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { useDispatch } from "~/hooks/useDispatch";
|
||||
import { toastMessageActions } from "~/tools/js/Shared/actions";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { syncTransactionSelectors } from "@dndbeyond/character-rules-engine/es";
|
||||
@@ -9,7 +9,7 @@ import { SyncBlockerLoadingPlaceholder } from "./SynchronousBlockerLoadingPlaceh
|
||||
let showBlockerTimerId: number;
|
||||
let hideBlockerTimerId: number;
|
||||
|
||||
interface Props {
|
||||
interface Props extends PropsWithChildren {
|
||||
waitDelayToShow: number;
|
||||
waitDelayToHide: number;
|
||||
className: string;
|
||||
|
||||
@@ -100,7 +100,7 @@ class TodoNavigation extends React.PureComponent<Props> {
|
||||
className="builder-navigation-large-link"
|
||||
href={prevRouteDef.path.replace(":characterId", characterId)}
|
||||
onClick={this.handleNavClick}
|
||||
useRouter
|
||||
userouter
|
||||
>
|
||||
<TodoNavigationPrev />
|
||||
</Link>
|
||||
@@ -114,7 +114,7 @@ class TodoNavigation extends React.PureComponent<Props> {
|
||||
className="builder-navigation-large-link"
|
||||
href={nextRouteDef.path.replace(":characterId", characterId)}
|
||||
onClick={this.handleNavClick}
|
||||
useRouter
|
||||
userouter
|
||||
>
|
||||
<TodoNavigationNext />
|
||||
</Link>
|
||||
@@ -137,7 +137,7 @@ class TodoNavigation extends React.PureComponent<Props> {
|
||||
className="builder-navigation-link"
|
||||
href={prevRouteDef.path.replace(":characterId", characterId)}
|
||||
onClick={this.handleNavClick}
|
||||
useRouter
|
||||
userouter
|
||||
>
|
||||
< Prev
|
||||
</Link>
|
||||
@@ -151,7 +151,7 @@ class TodoNavigation extends React.PureComponent<Props> {
|
||||
className="builder-navigation-link"
|
||||
href={nextRouteDef.path.replace(":characterId", characterId)}
|
||||
onClick={this.handleNavClick}
|
||||
useRouter
|
||||
userouter
|
||||
>
|
||||
Next >
|
||||
</Link>
|
||||
|
||||
+90
-94
@@ -3,7 +3,6 @@ import React from "react";
|
||||
import { RouteKey } from "~/subApps/builder/constants";
|
||||
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import PageSubHeader from "../../../components/PageSubHeader";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
@@ -12,104 +11,101 @@ class AbilityScoresHelp extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<PageHeader>Determine Ability Scores</PageHeader>
|
||||
<p>
|
||||
Much of what your character does in the game depends on their six
|
||||
abilities: <strong>Strength</strong>, <strong>Dexterity</strong>,{" "}
|
||||
<strong>Constitution</strong>, <strong>Intelligence</strong>,{" "}
|
||||
<strong>Wisdom</strong>, and <strong>Charisma</strong>. Each ability
|
||||
has a score, which is a number you record for use during play.
|
||||
</p>
|
||||
<PageHeader>Determine Ability Scores</PageHeader>
|
||||
<p>
|
||||
Much of what your character does in the game depends on their six
|
||||
abilities: <strong>Strength</strong>, <strong>Dexterity</strong>,{" "}
|
||||
<strong>Constitution</strong>, <strong>Intelligence</strong>,{" "}
|
||||
<strong>Wisdom</strong>, and <strong>Charisma</strong>. Each ability
|
||||
has a score, which is a number you record for use during play.
|
||||
</p>
|
||||
|
||||
<PageHeader>Generation Method</PageHeader>
|
||||
<p>
|
||||
You can determine ability scores in one of three ways, chosen in the
|
||||
next step:
|
||||
</p>
|
||||
<PageHeader>Generation Method</PageHeader>
|
||||
<p>
|
||||
You can determine ability scores in one of three ways, chosen in the
|
||||
next step:
|
||||
</p>
|
||||
|
||||
<PageSubHeader>Standard Array</PageSubHeader>
|
||||
<p>
|
||||
If you want to save time or don’t like the idea of randomly
|
||||
determining ability scores, you can choose from a fixed list (15,
|
||||
14, 13, 12, 10, 8).
|
||||
</p>
|
||||
<PageSubHeader>Standard Array</PageSubHeader>
|
||||
<p>
|
||||
If you want to save time or don’t like the idea of randomly
|
||||
determining ability scores, you can choose from a fixed list (15, 14,
|
||||
13, 12, 10, 8).
|
||||
</p>
|
||||
|
||||
<PageSubHeader>Manual or Random Generation</PageSubHeader>
|
||||
<p>
|
||||
Manually enter your ability scores. If you roll to randomly
|
||||
determine scores, choose this option to record your results. The
|
||||
standard method is to roll 4d6 and drop the lowest dice roll for
|
||||
each score. This system is used when you select Roll in the Manual
|
||||
category.
|
||||
</p>
|
||||
<PageSubHeader>Manual or Random Generation</PageSubHeader>
|
||||
<p>
|
||||
Manually enter your ability scores. If you roll to randomly determine
|
||||
scores, choose this option to record your results. The standard method
|
||||
is to roll 4d6 and drop the lowest dice roll for each score. This
|
||||
system is used when you select Roll in the Manual category.
|
||||
</p>
|
||||
|
||||
<PageSubHeader>Point Cost</PageSubHeader>
|
||||
<p>
|
||||
Customize your ability scores by spending points. If you are playing
|
||||
an Adventurers League character, choose this option. You have 27
|
||||
points to spend on your ability scores. The cost of each score is
|
||||
shown on the Ability Score Point Costs table. For example, a score
|
||||
of 14 costs 7 of your 27 points.
|
||||
</p>
|
||||
<PageSubHeader>Point Cost</PageSubHeader>
|
||||
<p>
|
||||
Customize your ability scores by spending points. If you are playing
|
||||
an Adventurers League character, choose this option. You have 27
|
||||
points to spend on your ability scores. The cost of each score is
|
||||
shown on the Ability Score Point Costs table. For example, a score of
|
||||
14 costs 7 of your 27 points.
|
||||
</p>
|
||||
|
||||
<h5>Ability Score Point Costs</h5>
|
||||
<table
|
||||
className="table-compendium"
|
||||
style={
|
||||
{
|
||||
maxWidth: "420px",
|
||||
"--sb-table-row-bg-hover": "#b3d3df",
|
||||
"--sb-table-row-bg-dark": "#d7e8ee",
|
||||
"--sb-table-row-bg-light": "#f1f7f9",
|
||||
"--dark-sb-table-row-bg-light": "#5e7982",
|
||||
"--dark-sb-table-row-bg-dark": "#5c7f8c",
|
||||
"--dark-sb-table-row-bg-hover": "#78a2b0",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Score</th>
|
||||
<th>Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>11</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>13</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>14</td>
|
||||
<td>7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>15</td>
|
||||
<td>9</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</PageBody>
|
||||
<h5>Ability Score Point Costs</h5>
|
||||
<table
|
||||
className="table-compendium"
|
||||
style={
|
||||
{
|
||||
maxWidth: "420px",
|
||||
"--sb-table-row-bg-hover": "#b3d3df",
|
||||
"--sb-table-row-bg-dark": "#d7e8ee",
|
||||
"--sb-table-row-bg-light": "#f1f7f9",
|
||||
"--dark-sb-table-row-bg-light": "#5e7982",
|
||||
"--dark-sb-table-row-bg-dark": "#5c7f8c",
|
||||
"--dark-sb-table-row-bg-hover": "#78a2b0",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Score</th>
|
||||
<th>Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>11</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>13</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>14</td>
|
||||
<td>7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>15</td>
|
||||
<td>9</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
+27
-29
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
rulesEngineSelectors,
|
||||
serviceDataSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { Dice } from "@dndbeyond/dice";
|
||||
|
||||
import { AbilityScoreManager } from "~/components/AbilityScoreManager";
|
||||
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
|
||||
@@ -50,14 +50,15 @@ import AbilityScoreManagerPointBuy from "../../../components/AbilityScoreManager
|
||||
import AbilityScoreManagerStandardArray from "../../../components/AbilityScoreManagerStandardArray";
|
||||
import { Button } from "../../../components/Button";
|
||||
import { Page } from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import { PageHeader } from "../../../components/PageHeader";
|
||||
import SpeciesTraitList from "../../../components/SpeciesTraitList";
|
||||
import { AbilityScoreDiceManager } from "../../AbilityScoreDiceManager";
|
||||
import AbilityScoreTypeChooser from "../../AbilityScoreTypeChooser";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
abilities: AbilityManager[];
|
||||
configuration: CharacterConfiguration;
|
||||
diceEnabled: boolean;
|
||||
@@ -90,7 +91,6 @@ class AbilityScoresManage extends React.PureComponent<Props> {
|
||||
|
||||
try {
|
||||
localStorage.setItem("dice-enabled", newDiceEnabledSetting.toString());
|
||||
Dice.setEnabled(newDiceEnabledSetting);
|
||||
} catch (e) {}
|
||||
|
||||
dispatch(
|
||||
@@ -258,31 +258,29 @@ class AbilityScoresManage extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<PageHeader>Ability Scores</PageHeader>
|
||||
<AbilityScoreTypeChooser initialOptionRemoved={false} />
|
||||
{this.renderInformationCollapsible()}
|
||||
{this.renderAbilityScoreManager()}
|
||||
{configuration.abilityScoreType ===
|
||||
Constants.AbilityScoreTypeEnum.MANUAL && this.renderAbilityDice()}
|
||||
{this.renderSpeciesTraits()}
|
||||
<PageHeader>Score Calculations</PageHeader>
|
||||
<p>
|
||||
Calculations, including the base scores you set above and any
|
||||
modifiers, are found below. You can also override any automatic
|
||||
calculations or modify them under each ability summary.
|
||||
</p>
|
||||
<div className="ability-score-calcs">
|
||||
{abilities.map((ability) => (
|
||||
<AbilityScoreManager
|
||||
ability={ability}
|
||||
showHeader={true}
|
||||
isReadonly={false}
|
||||
isBuilder={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</PageBody>
|
||||
<PageHeader>Ability Scores</PageHeader>
|
||||
<AbilityScoreTypeChooser initialOptionRemoved={false} />
|
||||
{this.renderInformationCollapsible()}
|
||||
{this.renderAbilityScoreManager()}
|
||||
{configuration.abilityScoreType ===
|
||||
Constants.AbilityScoreTypeEnum.MANUAL && this.renderAbilityDice()}
|
||||
{this.renderSpeciesTraits()}
|
||||
<PageHeader>Score Calculations</PageHeader>
|
||||
<p>
|
||||
Calculations, including the base scores you set above and any
|
||||
modifiers, are found below. You can also override any automatic
|
||||
calculations or modify them under each ability summary.
|
||||
</p>
|
||||
<div className="ability-score-calcs">
|
||||
{abilities.map((ability) => (
|
||||
<AbilityScoreManager
|
||||
ability={ability}
|
||||
showHeader={true}
|
||||
isReadonly={false}
|
||||
isBuilder={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { RouteKey } from "~/subApps/builder/constants";
|
||||
import { CollapsibleContent } from "../../../../../../components/CollapsibleContent";
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import { BuilderAppState } from "../../../typings";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
@@ -19,46 +18,44 @@ class ClassHelp extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<PageHeader>Choose your Class</PageHeader>
|
||||
<PageHeader>Choose your Class</PageHeader>
|
||||
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<p>
|
||||
In the first step, choose a class for your character. Every
|
||||
adventurer is a member of a class. Class broadly describes a
|
||||
character's vocation, what special talents they possess, and the
|
||||
tactics they are most likely to employ when exploring a dungeon,
|
||||
fighting monsters, or engaging in a tense negotiation.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<p>
|
||||
Your character receives a number of benefits from your chosen
|
||||
class. These features are capabilities (such as Spellcasting) that
|
||||
set your character apart from members of other classes. You also
|
||||
gain a number of proficiencies: weapons, skills, saving throws,
|
||||
and sometimes tools. Your proficiencies define many of the things
|
||||
your character can do particularly well, from using certain
|
||||
weapons to telling a convincing lie.
|
||||
In the first step, choose a class for your character. Every
|
||||
adventurer is a member of a class. Class broadly describes a
|
||||
character's vocation, what special talents they possess, and the
|
||||
tactics they are most likely to employ when exploring a dungeon,
|
||||
fighting monsters, or engaging in a tense negotiation.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
|
||||
<PageHeader>Multiclassing</PageHeader>
|
||||
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Adventurers sometimes advance in more than one class. A Rogue might
|
||||
switch direction in life and swear the oath of a Paladin, A
|
||||
Barbarian might discover latent magical ability and dabble in the
|
||||
Sorcerer class while continuing to advance as a Barbarian.
|
||||
Your character receives a number of benefits from your chosen class.
|
||||
These features are capabilities (such as Spellcasting) that set your
|
||||
character apart from members of other classes. You also gain a
|
||||
number of proficiencies: weapons, skills, saving throws, and
|
||||
sometimes tools. Your proficiencies define many of the things your
|
||||
character can do particularly well, from using certain weapons to
|
||||
telling a convincing lie.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
|
||||
<p>
|
||||
The optional rules for combining classes in this way is called
|
||||
Multiclassing.
|
||||
</p>
|
||||
</PageBody>
|
||||
<PageHeader>Multiclassing</PageHeader>
|
||||
|
||||
<p>
|
||||
Adventurers sometimes advance in more than one class. A Rogue might
|
||||
switch direction in life and swear the oath of a Paladin, A Barbarian
|
||||
might discover latent magical ability and dabble in the Sorcerer class
|
||||
while continuing to advance as a Barbarian.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The optional rules for combining classes in this way is called
|
||||
Multiclassing.
|
||||
</p>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-5
@@ -48,6 +48,7 @@ import {
|
||||
} from "../../../../../Shared/selectors/composite/apiCreator";
|
||||
import ClassManagerFeature from "../ClassManagerFeature";
|
||||
import { OptionalFeatureManager } from "../OptionalFeatureManager";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props {
|
||||
charClass: CharClass;
|
||||
@@ -69,7 +70,8 @@ interface Props {
|
||||
choiceType: any,
|
||||
choiceId: string,
|
||||
optionValue: number | null,
|
||||
parentChoiceId: string | null
|
||||
parentChoiceId: string | null,
|
||||
hasItemMappings: boolean
|
||||
) => void;
|
||||
onSpellPrepare: (spell: Spell, classMappingId: number) => void;
|
||||
onSpellUnprepare: (spell: Spell, classMappingId: number) => void;
|
||||
@@ -185,7 +187,8 @@ export default class ClassManager extends React.PureComponent<Props, State> {
|
||||
choiceId: string,
|
||||
choiceType: any,
|
||||
optionValue: number | null,
|
||||
parentChoiceId: string | null
|
||||
parentChoiceId: string | null,
|
||||
hasItemMappings: boolean
|
||||
): void => {
|
||||
const { onClassFeatureChoiceChange, charClass } = this.props;
|
||||
|
||||
@@ -196,7 +199,8 @@ export default class ClassManager extends React.PureComponent<Props, State> {
|
||||
choiceType,
|
||||
choiceId,
|
||||
optionValue,
|
||||
parentChoiceId
|
||||
parentChoiceId,
|
||||
hasItemMappings
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -420,8 +424,10 @@ export default class ClassManager extends React.PureComponent<Props, State> {
|
||||
levelsRemaining={levelsRemaining}
|
||||
/>
|
||||
<TabList
|
||||
variant="toggle"
|
||||
defaultActiveId={isMulticlass ? "none" : "features"}
|
||||
className={styles.tabList}
|
||||
variant="collapse"
|
||||
defaultActiveId="features"
|
||||
forceShow={!isMulticlass}
|
||||
tabs={[
|
||||
{
|
||||
label: "Class Features",
|
||||
|
||||
+11
-7
@@ -60,7 +60,8 @@ interface Props {
|
||||
choiceId: string,
|
||||
type: any,
|
||||
value: number | null,
|
||||
parentChoiceId: string | null
|
||||
parentChoiceId: string | null,
|
||||
hasItemMappings: boolean
|
||||
) => void;
|
||||
onInfusionChoiceItemChangePromise?: (
|
||||
infusionChoiceKey: string,
|
||||
@@ -163,7 +164,8 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
id,
|
||||
type,
|
||||
HelperUtils.parseInputInt(value),
|
||||
parentChoiceId
|
||||
parentChoiceId,
|
||||
ClassFeatureUtils.getHasItemMappings(feature)
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -232,14 +234,15 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
};
|
||||
|
||||
getChoiceCount = (): number => {
|
||||
const { feature } = this.props;
|
||||
return ClassFeatureUtils.getChoices(feature).length;
|
||||
const { feature, isActive } = this.props;
|
||||
return isActive ? ClassFeatureUtils.getChoices(feature).length : 0;
|
||||
};
|
||||
|
||||
getTodoChoiceCount = (): number => {
|
||||
const { feature } = this.props;
|
||||
return ClassFeatureUtils.getChoices(feature).filter(ChoiceUtils.isTodo)
|
||||
.length;
|
||||
const { feature, isActive } = this.props;
|
||||
return ClassFeatureUtils.getChoices(feature).filter(
|
||||
(feature) => isActive && ChoiceUtils.isTodo(feature)
|
||||
).length;
|
||||
};
|
||||
|
||||
getTotalChoiceCount = (): number => {
|
||||
@@ -390,6 +393,7 @@ export default class ClassManagerFeature extends React.PureComponent<
|
||||
<FeatureChoices
|
||||
choices={ClassFeatureUtils.getChoices(feature)}
|
||||
charClass={charClass}
|
||||
classFeature={feature}
|
||||
onChoiceChange={this.handleChoiceChange}
|
||||
shouldFetch={collapsibleOpened}
|
||||
/>
|
||||
|
||||
+92
-88
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { useContext } from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
import { NavigateFunction, useNavigate } from "react-router-dom";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import {
|
||||
ApiAdapterPromise,
|
||||
@@ -68,7 +69,6 @@ import {
|
||||
} from "../../../../Shared/selectors/composite/apiCreator";
|
||||
import { AppNotificationUtils } from "../../../../Shared/utils";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import { navigationConfig } from "../../../config";
|
||||
import { ProgressionManager } from "../../ProgressionManager";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
@@ -84,7 +84,9 @@ function getClassSpellList(
|
||||
return foundClassSpellList ? foundClassSpellList : null;
|
||||
}
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
ruleData: RuleData;
|
||||
overallSpellInfo: OverallSpellInfo;
|
||||
prerequisiteData: PrerequisiteData;
|
||||
@@ -859,9 +861,10 @@ class ClassesManage extends React.PureComponent<Props> {
|
||||
choiceType: any,
|
||||
choiceId: string,
|
||||
optionValue: number | null,
|
||||
parentChoiceId: string | null
|
||||
parentChoiceId: string | null,
|
||||
hasItemMappings: boolean
|
||||
): void => {
|
||||
const { dispatch } = this.props;
|
||||
const { dispatch, inventoryManager } = this.props;
|
||||
dispatch(
|
||||
characterActions.classFeatureChoiceSetRequest(
|
||||
classId,
|
||||
@@ -869,7 +872,10 @@ class ClassesManage extends React.PureComponent<Props> {
|
||||
choiceType,
|
||||
choiceId,
|
||||
optionValue,
|
||||
parentChoiceId
|
||||
parentChoiceId,
|
||||
hasItemMappings
|
||||
? inventoryManager.handleAcceptOnSuccess(true)
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -900,7 +906,7 @@ class ClassesManage extends React.PureComponent<Props> {
|
||||
navigate(
|
||||
navigationConfig
|
||||
.getRouteDefPath(RouteKey.CLASS_CHOOSE)
|
||||
.replace(":characterId", characterId)
|
||||
.replace(":characterId", characterId.toString())
|
||||
);
|
||||
};
|
||||
|
||||
@@ -943,90 +949,88 @@ class ClassesManage extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<Page clsNames={["classes-manage"]}>
|
||||
<PageBody>
|
||||
<div className="classes-manage-primary">
|
||||
<div className="classes-manage-primary-section classes-manage-primary-section-progression">
|
||||
<ProgressionManager />
|
||||
</div>
|
||||
<HpSummary />
|
||||
<div className="classes-manage-primary">
|
||||
<div className="classes-manage-primary-section classes-manage-primary-section-progression">
|
||||
<ProgressionManager />
|
||||
</div>
|
||||
{classes.map((charClass) => (
|
||||
<ClassManager
|
||||
theme={theme}
|
||||
charClass={charClass}
|
||||
loadAvailableFeats={loadAvailableFeats}
|
||||
loadAvailableSubclasses={loadAvailableSubclasses}
|
||||
loadRemainingSpellList={makeLoadClassRemainingSpells(charClass)}
|
||||
loadAlwaysKnownSpells={makeLoadClassAlwaysKnownSpells(charClass)}
|
||||
loadAvailableOptionalClassFeatures={loadAvailableOptionalClassFeatures(
|
||||
charClass
|
||||
)}
|
||||
loadAvailableInfusions={loadAvailableInfusions}
|
||||
levelsRemaining={levelsRemaining}
|
||||
onClassFeatureChoiceChange={this.handleClassFeatureChoiceChange}
|
||||
onSpellPrepare={this.handlePrepare}
|
||||
onSpellUnprepare={this.handleUnprepare}
|
||||
onSpellRemove={this.handleRemove}
|
||||
onSpellAdd={this.handleSpellAdd.bind(this, charClass)}
|
||||
onAlwaysKnownLoad={this.handleAlwaysKnownLoad}
|
||||
onInfusionChoiceItemChangePromise={
|
||||
this.handleInfusionChoiceItemChangePromise
|
||||
}
|
||||
onInfusionChoiceItemDestroyPromise={
|
||||
this.handleInfusionChoiceItemDestroyPromise
|
||||
}
|
||||
onInfusionChoiceChangePromise={
|
||||
this.handleInfusionChoiceChangePromise
|
||||
}
|
||||
onInfusionChoiceDestroyPromise={
|
||||
this.handleInfusionChoiceDestroyPromise
|
||||
}
|
||||
onInfusionChoiceCreatePromise={
|
||||
this.handleInfusionChoiceCreatePromise
|
||||
}
|
||||
onOptionalFeatureSelection={this.handleOptionalFeatureSelection}
|
||||
onRemoveSelectionPromise={
|
||||
this.handleRemoveOptionalFeatureSelectionPromise
|
||||
}
|
||||
onChangeReplacementPromise={
|
||||
this.handleOnChangeOptionalClassFeatureReplacementPromise
|
||||
}
|
||||
onDefinitionsLoaded={this.handleDefinitionsLoaded}
|
||||
onFeatureDefinitionsLoaded={this.handleFeatureDefinitionsLoaded}
|
||||
key={charClass.id}
|
||||
isMulticlass={classes.length > 1}
|
||||
choiceInfo={choiceInfo}
|
||||
preferences={preferences}
|
||||
classSpellList={getClassSpellList(charClass, classSpellLists)}
|
||||
spellCasterInfo={spellCasterInfo}
|
||||
ruleData={ruleData}
|
||||
overallSpellInfo={overallSpellInfo}
|
||||
prerequisiteData={prerequisiteData}
|
||||
typeValueLookup={typeValueLookup}
|
||||
globalModifiers={globalModifiers}
|
||||
definitionPool={definitionPool}
|
||||
featLookup={featLookup}
|
||||
knownInfusionLookup={knownInfusionLookup}
|
||||
knownReplicatedItems={knownReplicatedItems}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
optionalClassFeatureLookup={optionalClassFeatureLookup}
|
||||
classSpellListSpellsLookup={classSpellListSpellsLookup}
|
||||
activeSourceCategories={activeSourceCategories}
|
||||
inventoryManager={inventoryManager}
|
||||
entityRestrictionData={entityRestrictionData}
|
||||
/>
|
||||
))}
|
||||
{levelsRemaining !== 0 && (
|
||||
<div className="classes-manage-actions">
|
||||
<div
|
||||
className="classes-manage-actions-action"
|
||||
onClick={this.handleAddAnotherClass}
|
||||
>
|
||||
+ Add Another Class
|
||||
</div>
|
||||
<HpSummary />
|
||||
</div>
|
||||
{classes.map((charClass) => (
|
||||
<ClassManager
|
||||
theme={theme}
|
||||
charClass={charClass}
|
||||
loadAvailableFeats={loadAvailableFeats}
|
||||
loadAvailableSubclasses={loadAvailableSubclasses}
|
||||
loadRemainingSpellList={makeLoadClassRemainingSpells(charClass)}
|
||||
loadAlwaysKnownSpells={makeLoadClassAlwaysKnownSpells(charClass)}
|
||||
loadAvailableOptionalClassFeatures={loadAvailableOptionalClassFeatures(
|
||||
charClass
|
||||
)}
|
||||
loadAvailableInfusions={loadAvailableInfusions}
|
||||
levelsRemaining={levelsRemaining}
|
||||
onClassFeatureChoiceChange={this.handleClassFeatureChoiceChange}
|
||||
onSpellPrepare={this.handlePrepare}
|
||||
onSpellUnprepare={this.handleUnprepare}
|
||||
onSpellRemove={this.handleRemove}
|
||||
onSpellAdd={this.handleSpellAdd.bind(this, charClass)}
|
||||
onAlwaysKnownLoad={this.handleAlwaysKnownLoad}
|
||||
onInfusionChoiceItemChangePromise={
|
||||
this.handleInfusionChoiceItemChangePromise
|
||||
}
|
||||
onInfusionChoiceItemDestroyPromise={
|
||||
this.handleInfusionChoiceItemDestroyPromise
|
||||
}
|
||||
onInfusionChoiceChangePromise={
|
||||
this.handleInfusionChoiceChangePromise
|
||||
}
|
||||
onInfusionChoiceDestroyPromise={
|
||||
this.handleInfusionChoiceDestroyPromise
|
||||
}
|
||||
onInfusionChoiceCreatePromise={
|
||||
this.handleInfusionChoiceCreatePromise
|
||||
}
|
||||
onOptionalFeatureSelection={this.handleOptionalFeatureSelection}
|
||||
onRemoveSelectionPromise={
|
||||
this.handleRemoveOptionalFeatureSelectionPromise
|
||||
}
|
||||
onChangeReplacementPromise={
|
||||
this.handleOnChangeOptionalClassFeatureReplacementPromise
|
||||
}
|
||||
onDefinitionsLoaded={this.handleDefinitionsLoaded}
|
||||
onFeatureDefinitionsLoaded={this.handleFeatureDefinitionsLoaded}
|
||||
key={charClass.id}
|
||||
isMulticlass={classes.length > 1}
|
||||
choiceInfo={choiceInfo}
|
||||
preferences={preferences}
|
||||
classSpellList={getClassSpellList(charClass, classSpellLists)}
|
||||
spellCasterInfo={spellCasterInfo}
|
||||
ruleData={ruleData}
|
||||
overallSpellInfo={overallSpellInfo}
|
||||
prerequisiteData={prerequisiteData}
|
||||
typeValueLookup={typeValueLookup}
|
||||
globalModifiers={globalModifiers}
|
||||
definitionPool={definitionPool}
|
||||
featLookup={featLookup}
|
||||
knownInfusionLookup={knownInfusionLookup}
|
||||
knownReplicatedItems={knownReplicatedItems}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
optionalClassFeatureLookup={optionalClassFeatureLookup}
|
||||
classSpellListSpellsLookup={classSpellListSpellsLookup}
|
||||
activeSourceCategories={activeSourceCategories}
|
||||
inventoryManager={inventoryManager}
|
||||
entityRestrictionData={entityRestrictionData}
|
||||
/>
|
||||
))}
|
||||
{levelsRemaining !== 0 && (
|
||||
<div className="classes-manage-actions">
|
||||
<div
|
||||
className="classes-manage-actions-action"
|
||||
onClick={this.handleAddAnotherClass}
|
||||
>
|
||||
+ Add Another Class
|
||||
</div>
|
||||
)}
|
||||
</PageBody>
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-4
@@ -291,7 +291,7 @@ export class OptionalFeatureManager extends React.PureComponent<
|
||||
contentNode = (
|
||||
<div className="ct-optional-feature-manager__content">
|
||||
{replacementFeatures.length > 0 && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<PageSubHeader>Replacement Features</PageSubHeader>
|
||||
{ClassUtils.deriveOrderedClassFeatures(replacementFeatures).map(
|
||||
(feature) => {
|
||||
@@ -332,10 +332,10 @@ export class OptionalFeatureManager extends React.PureComponent<
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
{additionalFeatures.length > 0 && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<PageSubHeader>Additional Features</PageSubHeader>
|
||||
{ClassUtils.deriveOrderedClassFeatures(additionalFeatures).map(
|
||||
(feature) => {
|
||||
@@ -376,7 +376,7 @@ export class OptionalFeatureManager extends React.PureComponent<
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import OptionalFeatureManager from "./OptionalFeatureManager";
|
||||
|
||||
export default OptionalFeatureManager;
|
||||
export { OptionalFeatureManager };
|
||||
+28
-31
@@ -3,7 +3,6 @@ import React from "react";
|
||||
import { RouteKey } from "~/subApps/builder/constants";
|
||||
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
|
||||
@@ -12,40 +11,38 @@ class DescriptionHelp extends React.PureComponent<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<PageHeader>Background</PageHeader>
|
||||
<p>
|
||||
Your character’s background describes where they came from, their
|
||||
original occupation, and their place in the D&D world.
|
||||
</p>
|
||||
<PageHeader>Background</PageHeader>
|
||||
<p>
|
||||
Your character’s background describes where they came from, their
|
||||
original occupation, and their place in the D&D world.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Backgrounds from the Core D&D source category give your
|
||||
character ability score increases, an Origin feat, and proficiencies
|
||||
in specific skills and tools.
|
||||
</p>
|
||||
<p>
|
||||
Backgrounds from the Core D&D source category give your character
|
||||
ability score increases, an Origin feat, and proficiencies in specific
|
||||
skills and tools.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Backgrounds from other sources give your character a background
|
||||
feature (a general benefit) and proficiency in two skills, and it
|
||||
might also give you additional languages or proficiency with certain
|
||||
kinds of tools.
|
||||
</p>
|
||||
<p>
|
||||
Backgrounds from other sources give your character a background
|
||||
feature (a general benefit) and proficiency in two skills, and it
|
||||
might also give you additional languages or proficiency with certain
|
||||
kinds of tools.
|
||||
</p>
|
||||
|
||||
<PageHeader>Describe Your Character</PageHeader>
|
||||
<p>
|
||||
In this step, you will flesh your character out as a person. Your
|
||||
character needs a name.
|
||||
</p>
|
||||
<PageHeader>Describe Your Character</PageHeader>
|
||||
<p>
|
||||
In this step, you will flesh your character out as a person. Your
|
||||
character needs a name.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You’ll need to decide your character’s appearance and personality.
|
||||
Choose your character’s alignment (the moral compass that guides his
|
||||
or her decisions) and ideals. Identify the things your character
|
||||
holds most dear, called bonds, and the flaws that could one day
|
||||
undermine them.
|
||||
</p>
|
||||
</PageBody>
|
||||
<p>
|
||||
You’ll need to decide your character’s appearance and personality.
|
||||
Choose your character’s alignment (the moral compass that guides his
|
||||
or her decisions) and ideals. Identify the things your character holds
|
||||
most dear, called bonds, and the flaws that could one day undermine
|
||||
them.
|
||||
</p>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
+375
-406
@@ -2,8 +2,9 @@ import axios, { Canceler } from "axios";
|
||||
import { orderBy } from "lodash";
|
||||
import React, { FocusEvent, HTMLAttributes, useContext } from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es";
|
||||
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AlignmentContract,
|
||||
ApiAdapterPromise,
|
||||
@@ -44,12 +45,16 @@ import {
|
||||
rulesEngineSelectors,
|
||||
serviceDataSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import MinusIcon from "@dndbeyond/fontawesome-cache/svgs/solid/minus.svg";
|
||||
import PlusIcon from "@dndbeyond/fontawesome-cache/svgs/solid/plus.svg";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { Button } from "~/components/Button";
|
||||
import { CollapsibleContent } from "~/components/CollapsibleContent";
|
||||
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { Link } from "~/components/Link";
|
||||
import { Select } from "~/components/Select";
|
||||
import { useSource } from "~/hooks/useSource";
|
||||
import { EditorWithDialog } from "~/subApps/builder/components/EditorWithDialog";
|
||||
import { InputField } from "~/subApps/builder/components/InputField";
|
||||
@@ -70,10 +75,8 @@ import {
|
||||
appEnvSelectors,
|
||||
} from "../../../../Shared/selectors";
|
||||
import { AppLoggerUtils } from "../../../../Shared/utils";
|
||||
import Button from "../../../components/Button";
|
||||
import { GrantedFeat } from "../../../components/GrantedFeat/GrantedFeat";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageSubHeader from "../../../components/PageSubHeader";
|
||||
import { BuilderAppState } from "../../../typings";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
@@ -188,89 +191,67 @@ class CustomBackgroundManager extends React.PureComponent<
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="custom-background">
|
||||
<div className="custom-background-editor">
|
||||
<div className="custom-background-properties">
|
||||
<div className="custom-background-property custom-background-property-full">
|
||||
<div className="custom-background-property-value">
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={name || ""}
|
||||
onBlur={this.handleTextInputBlur.bind(this, "name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="custom-background-property-label">Name</div>
|
||||
</div>
|
||||
<div className="custom-background-property custom-background-property-textarea">
|
||||
<div className="custom-background-property-value">
|
||||
<textarea
|
||||
defaultValue={description || ""}
|
||||
onBlur={this.handleTextInputBlur.bind(this, "description")}
|
||||
/>
|
||||
</div>
|
||||
<div className="custom-background-property-label">
|
||||
Description
|
||||
</div>
|
||||
</div>
|
||||
<div className="custom-background-property custom-background-property-choice">
|
||||
<div className="custom-background-property-value">
|
||||
<Select
|
||||
onChange={this.handleNumberSelectChange.bind(
|
||||
this,
|
||||
"modifierType"
|
||||
)}
|
||||
options={modifierTypeOptions}
|
||||
value={modifierType}
|
||||
clsNames={[""]}
|
||||
/>
|
||||
</div>
|
||||
<div className="custom-background-property-label">
|
||||
Proficiency/Language Choices
|
||||
</div>
|
||||
</div>
|
||||
<div className="custom-background-property custom-background-property-choice">
|
||||
<div className="custom-background-property-value">
|
||||
<Select
|
||||
onChange={this.handleNumberSelectChange.bind(
|
||||
this,
|
||||
"featureId"
|
||||
)}
|
||||
options={featureOptions}
|
||||
value={featureId}
|
||||
clsNames={[""]}
|
||||
/>
|
||||
</div>
|
||||
<div className="custom-background-property-label">
|
||||
Background Feature
|
||||
</div>
|
||||
</div>
|
||||
<div className="custom-background-property custom-background-property-choice">
|
||||
<div className="custom-background-property-value">
|
||||
<Select
|
||||
onChange={this.handleNumberSelectChange.bind(
|
||||
this,
|
||||
"characteristicsId"
|
||||
)}
|
||||
options={characteristicsOptions}
|
||||
value={characteristicsId}
|
||||
clsNames={[""]}
|
||||
/>
|
||||
</div>
|
||||
<div className="custom-background-property-label">
|
||||
Background Characteristics
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="custom-background-actions">
|
||||
<Button onClick={this.handleSave}>Save</Button>
|
||||
</div>
|
||||
<div className={styles.customBackground}>
|
||||
<InputField
|
||||
className={styles.customBackgroundField}
|
||||
type="text"
|
||||
label="Name"
|
||||
defaultValue={name || ""}
|
||||
onBlur={this.handleTextInputBlur.bind(this, "name")}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.customBackgroundField}
|
||||
type="textarea"
|
||||
label="Description"
|
||||
defaultValue={description || ""}
|
||||
onBlur={this.handleTextInputBlur.bind(this, "description")}
|
||||
/>
|
||||
<label className={styles.customBackgroundLabel}>
|
||||
Proficiency/Language Choices
|
||||
</label>
|
||||
<Select
|
||||
className={styles.customBackgroundField}
|
||||
onChange={this.handleNumberSelectChange.bind(this, "modifierType")}
|
||||
options={modifierTypeOptions}
|
||||
value={modifierType}
|
||||
name="custom-background-description"
|
||||
/>
|
||||
<label className={styles.customBackgroundLabel}>
|
||||
Background Feature
|
||||
</label>
|
||||
<Select
|
||||
className={styles.customBackgroundField}
|
||||
onChange={this.handleNumberSelectChange.bind(this, "featureId")}
|
||||
options={featureOptions}
|
||||
value={featureId}
|
||||
name="custom-background-feature"
|
||||
/>
|
||||
<label className={styles.customBackgroundLabel}>
|
||||
Background Characteristics
|
||||
</label>
|
||||
<Select
|
||||
className={styles.customBackgroundField}
|
||||
onChange={this.handleNumberSelectChange.bind(
|
||||
this,
|
||||
"characteristicsId"
|
||||
)}
|
||||
options={characteristicsOptions}
|
||||
value={characteristicsId}
|
||||
name="custom-background-characteristics"
|
||||
/>
|
||||
<div className={styles.customBackgroundActions}>
|
||||
<Button variant="builder" size="x-small" onClick={this.handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
ruleData: RuleData;
|
||||
background: Background | null;
|
||||
currentBackground: BackgroundContract | null;
|
||||
@@ -719,6 +700,7 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
options={availableOptions}
|
||||
onChange={this.handleChoiceChange}
|
||||
choiceInfo={choiceInfo}
|
||||
className={styles.backgroundChoiceSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -1105,23 +1087,24 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.accordionGroup}>
|
||||
{this.renderBackgroundChoices(background)}
|
||||
{featuresBackground &&
|
||||
this.renderBackgroundFeature(featuresBackground, [
|
||||
featuresBackground.name ? featuresBackground.name : "",
|
||||
])}
|
||||
|
||||
{this.renderBackgroundChoices(background)}
|
||||
{featuresBackground &&
|
||||
this.renderBackgroundFeature(featuresBackground, [
|
||||
featuresBackground.name ? featuresBackground.name : "",
|
||||
])}
|
||||
|
||||
{characteristicsBackground &&
|
||||
this.hasCharacteristics(characteristicsBackground) && (
|
||||
<Accordion
|
||||
summary="Suggested Characteristics"
|
||||
summaryMetaItems={metaItems}
|
||||
variant="paper"
|
||||
>
|
||||
{this.renderBackgroundSuggestionsUi(characteristicsBackground)}
|
||||
</Accordion>
|
||||
)}
|
||||
{characteristicsBackground &&
|
||||
this.hasCharacteristics(characteristicsBackground) && (
|
||||
<Accordion
|
||||
summary="Suggested Characteristics"
|
||||
summaryMetaItems={metaItems}
|
||||
variant="paper"
|
||||
>
|
||||
{this.renderBackgroundSuggestionsUi(characteristicsBackground)}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1130,15 +1113,15 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
const { customizeCollapsed } = this.state;
|
||||
const { background, getGroupedOptionsBySourceCategory } = this.props;
|
||||
|
||||
const backgroundOptions: Array<HtmlSelectOption | HtmlSelectOptionGroup> = [
|
||||
const backgroundOptions = [
|
||||
{
|
||||
label: "Custom Background",
|
||||
value: -1,
|
||||
},
|
||||
{
|
||||
label: "---------",
|
||||
disabled: true,
|
||||
value: "-----",
|
||||
optGroupLabel: "---------",
|
||||
options: [
|
||||
{
|
||||
label: "Custom Background",
|
||||
value: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
...getGroupedOptionsBySourceCategory(this.getBackgroundData()),
|
||||
];
|
||||
@@ -1152,15 +1135,8 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
let clsNames: string[] = ["custom-background-customize-header"];
|
||||
if (customizeCollapsed) {
|
||||
clsNames.push("custom-background-customize-header-closed");
|
||||
} else {
|
||||
clsNames.push("custom-background-customize-header-opened");
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<div className="ct-character-tools__marketplace-callout">
|
||||
Check your source settings on the <strong>Home</strong> tab if you
|
||||
can't find Backgrounds you've purchased.
|
||||
@@ -1169,27 +1145,24 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
<Link href="/marketplace">Marketplace</Link> for more Background
|
||||
options.
|
||||
</div>
|
||||
<div className="description-manage-background-chooser-con">
|
||||
<div className="description-manage-background-chooser-field">
|
||||
<Select
|
||||
onChangePromise={this.handleBackgroundChangePromise}
|
||||
options={backgroundOptions}
|
||||
value={backgroundId}
|
||||
clsNames={["description-manage-background-chooser"]}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.backgroundSelectContainer}>
|
||||
<Select
|
||||
className={styles.backgroundSelect}
|
||||
options={backgroundOptions}
|
||||
onChangeConfirm={this.handleBackgroundChangePromise}
|
||||
value={backgroundId}
|
||||
name="background-chooser"
|
||||
/>
|
||||
{background && background.hasCustomBackground && (
|
||||
<div className="description-manage-background-chooser-extra">
|
||||
<div
|
||||
className={clsNames.join(" ")}
|
||||
onClick={this.handleConfigureClick}
|
||||
>
|
||||
<div className="custom-background-customize-heading">
|
||||
Configure
|
||||
</div>
|
||||
<div className="custom-background-customize-trigger" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.customBackgroundToggle}
|
||||
variant="builder-text"
|
||||
size="x-small"
|
||||
onClick={this.handleConfigureClick}
|
||||
>
|
||||
Configure
|
||||
{customizeCollapsed ? <PlusIcon /> : <MinusIcon />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{background &&
|
||||
@@ -1198,7 +1171,7 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
{background &&
|
||||
background.hasCustomBackground &&
|
||||
this.renderCustomBackgroundUI()}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1293,7 +1266,7 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
lifestyle.cost === "-" ? "" : `(${lifestyle.cost})`
|
||||
}`,
|
||||
value: lifestyle.id,
|
||||
}));
|
||||
})) as HtmlSelectOption[];
|
||||
|
||||
const numberInputAttributes = {
|
||||
min: CHARACTER_DESCRIPTION_NUMBER_VALUE.MIN,
|
||||
@@ -1302,283 +1275,279 @@ class DescriptionManage extends React.PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<Page clsNames={["description-manage"]}>
|
||||
<PageBody>
|
||||
<div className="description-manage-information">
|
||||
{this.renderInformationCollapsible()}
|
||||
<div className="description-manage-information">
|
||||
{this.renderInformationCollapsible()}
|
||||
</div>
|
||||
{this.renderBackgroundUi()}
|
||||
<Accordion
|
||||
summary="Character Details"
|
||||
summaryMetaItems={["Alignment", "Faith", "Lifestyle"]}
|
||||
variant="paper"
|
||||
>
|
||||
<div className="description-manage-alignment">
|
||||
<label className="description-manage-alignment-label">
|
||||
Alignment
|
||||
</label>
|
||||
<Select
|
||||
options={alignmentOptions}
|
||||
value={alignment === null ? null : alignment.id}
|
||||
onChange={this.handleAlignmentChange}
|
||||
name="alignment-select"
|
||||
/>
|
||||
{alignmentDescriptionNode}
|
||||
</div>
|
||||
{this.renderBackgroundUi()}
|
||||
<Accordion
|
||||
summary="Character Details"
|
||||
summaryMetaItems={["Alignment", "Faith", "Lifestyle"]}
|
||||
variant="paper"
|
||||
>
|
||||
<div className="description-manage-alignment">
|
||||
<label className="description-manage-alignment-label">
|
||||
Alignment
|
||||
</label>
|
||||
<Select
|
||||
options={alignmentOptions}
|
||||
value={alignment === null ? null : alignment.id}
|
||||
onChange={this.handleAlignmentChange}
|
||||
/>
|
||||
{alignmentDescriptionNode}
|
||||
</div>
|
||||
<div className="description-manage-faith">
|
||||
<InputField
|
||||
label="Faith"
|
||||
initialValue={faith}
|
||||
onBlur={this.handleFaithChange}
|
||||
maxLength={512}
|
||||
inputProps={
|
||||
{
|
||||
spellCheck: false,
|
||||
autoComplete: "off",
|
||||
} as HTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="description-manage-lifestyle">
|
||||
<label className="description-manage-lifestyle-label">
|
||||
Lifestyle
|
||||
</label>
|
||||
<Select
|
||||
options={lifestyleOptions}
|
||||
value={lifestyle === null ? null : lifestyle.id}
|
||||
onChange={this.handleLifestyleChange}
|
||||
/>
|
||||
{lifestyle !== null && (
|
||||
<CollapsibleContent className="description-manage-lifestyle-desc">
|
||||
{lifestyle.description ? lifestyle.description : ""}
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
summary="Physical Characteristics"
|
||||
summaryMetaItems={[
|
||||
"Hair",
|
||||
"Skin",
|
||||
"Eyes",
|
||||
"Height",
|
||||
"Weight",
|
||||
"Age",
|
||||
"Gender",
|
||||
]}
|
||||
variant="paper"
|
||||
>
|
||||
<div className="description-manage-faith">
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Hair"
|
||||
initialValue={hair}
|
||||
onBlur={this.handleHairChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Skin"
|
||||
initialValue={skin}
|
||||
onBlur={this.handleSkinChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Eyes"
|
||||
initialValue={eyes}
|
||||
onBlur={this.handleEyesChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Height"
|
||||
initialValue={height}
|
||||
onBlur={this.handleHeightChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Weight (lbs)"
|
||||
type="number"
|
||||
initialValue={weight}
|
||||
onBlur={this.handleWeightChange}
|
||||
inputProps={numberInputAttributes}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Age (Years)"
|
||||
type="number"
|
||||
initialValue={age}
|
||||
onBlur={this.handleAgeChange}
|
||||
inputProps={numberInputAttributes}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Gender"
|
||||
initialValue={gender}
|
||||
onBlur={this.handleGenderChange}
|
||||
maxLength={128}
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
summary="Personal Characteristics"
|
||||
summaryMetaItems={["Personality", "Ideals", "Bonds", "Flaws"]}
|
||||
variant="paper"
|
||||
>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Personality Traits</h3>}
|
||||
editButtonLabel="Edit Traits"
|
||||
addButtonLabel="Add Traits"
|
||||
placeholder="Add traits here..."
|
||||
content={this.getTraitContent(
|
||||
Constants.TraitTypeEnum.PERSONALITY_TRAITS
|
||||
)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.PERSONALITY_TRAITS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList(
|
||||
"Personality Traits",
|
||||
"personalityTraits",
|
||||
"Choose Two"
|
||||
)}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Ideals</h3>}
|
||||
editButtonLabel="Edit Ideals"
|
||||
addButtonLabel="Add Ideals"
|
||||
placeholder="Add ideals here..."
|
||||
content={this.getTraitContent(Constants.TraitTypeEnum.IDEALS)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.IDEALS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList("Ideals", "ideals")}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Bonds</h3>}
|
||||
editButtonLabel="Edit Bonds"
|
||||
addButtonLabel="Add Bonds"
|
||||
placeholder="Add bonds here..."
|
||||
content={this.getTraitContent(Constants.TraitTypeEnum.BONDS)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.BONDS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList("Bonds", "bonds")}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Flaws</h3>}
|
||||
editButtonLabel="Edit Flaws"
|
||||
addButtonLabel="Add Flaws"
|
||||
placeholder="Add flaws here..."
|
||||
content={this.getTraitContent(Constants.TraitTypeEnum.FLAWS)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.FLAWS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList("Flaws", "flaws")}
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
summary="Notes"
|
||||
summaryMetaItems={[
|
||||
"Organizations",
|
||||
"Allies",
|
||||
"Enemies",
|
||||
"Backstory",
|
||||
"Other",
|
||||
]}
|
||||
variant="paper"
|
||||
>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Organizations</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add organization notes here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.ORGANIZATIONS)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.ORGANIZATIONS
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Are there any important organizations your character belongs
|
||||
to? Any societies, orders, cults, or agencies?
|
||||
</p>
|
||||
label="Faith"
|
||||
initialValue={faith}
|
||||
onBlur={this.handleFaithChange}
|
||||
maxLength={512}
|
||||
inputProps={
|
||||
{
|
||||
spellCheck: false,
|
||||
autoComplete: "off",
|
||||
} as HTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Allies</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add allies here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.ALLIES)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.ALLIES
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
With whom does your character associate? When in need of aid,
|
||||
do they seek out their kinfolk, followers of their deity, or
|
||||
other members of their order? What groups or individuals are
|
||||
they aligned with?
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div className="description-manage-lifestyle">
|
||||
<label className="description-manage-lifestyle-label">
|
||||
Lifestyle
|
||||
</label>
|
||||
<Select
|
||||
options={lifestyleOptions}
|
||||
value={lifestyle === null ? null : lifestyle.id}
|
||||
onChange={this.handleLifestyleChange}
|
||||
name="lifestyle-select"
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Enemies</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add enemies here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.ENEMIES)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.ENEMIES
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Who does your character fear or fight? Have they sworn a vow
|
||||
to rid the world of undead? Is there an order or organization
|
||||
they are opposed to? Are there any specific foes from their
|
||||
past?
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Backstory</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add a backstory..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.BACKSTORY)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.BACKSTORY
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Talk about your character's origins. Where are they from? How
|
||||
did they end up adventuring? How did they choose their class?
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Other</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add additional notes here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.OTHER)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.OTHER
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Anything at all you'd like to mention about your character.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Accordion>
|
||||
</PageBody>
|
||||
{lifestyle !== null && (
|
||||
<CollapsibleContent className="description-manage-lifestyle-desc">
|
||||
{lifestyle.description ? lifestyle.description : ""}
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
summary="Physical Characteristics"
|
||||
summaryMetaItems={[
|
||||
"Hair",
|
||||
"Skin",
|
||||
"Eyes",
|
||||
"Height",
|
||||
"Weight",
|
||||
"Age",
|
||||
"Gender",
|
||||
]}
|
||||
variant="paper"
|
||||
>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Hair"
|
||||
initialValue={hair}
|
||||
onBlur={this.handleHairChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Skin"
|
||||
initialValue={skin}
|
||||
onBlur={this.handleSkinChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Eyes"
|
||||
initialValue={eyes}
|
||||
onBlur={this.handleEyesChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Height"
|
||||
initialValue={height}
|
||||
onBlur={this.handleHeightChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Weight (lbs)"
|
||||
type="number"
|
||||
initialValue={weight}
|
||||
onBlur={this.handleWeightChange}
|
||||
inputProps={numberInputAttributes}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Age (Years)"
|
||||
type="number"
|
||||
initialValue={age}
|
||||
onBlur={this.handleAgeChange}
|
||||
inputProps={numberInputAttributes}
|
||||
/>
|
||||
<InputField
|
||||
className={styles.inputField}
|
||||
label="Gender"
|
||||
initialValue={gender}
|
||||
onBlur={this.handleGenderChange}
|
||||
maxLength={128}
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
summary="Personal Characteristics"
|
||||
summaryMetaItems={["Personality", "Ideals", "Bonds", "Flaws"]}
|
||||
variant="paper"
|
||||
>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Personality Traits</h3>}
|
||||
editButtonLabel="Edit Traits"
|
||||
addButtonLabel="Add Traits"
|
||||
placeholder="Add traits here..."
|
||||
content={this.getTraitContent(
|
||||
Constants.TraitTypeEnum.PERSONALITY_TRAITS
|
||||
)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.PERSONALITY_TRAITS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList(
|
||||
"Personality Traits",
|
||||
"personalityTraits",
|
||||
"Choose Two"
|
||||
)}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Ideals</h3>}
|
||||
editButtonLabel="Edit Ideals"
|
||||
addButtonLabel="Add Ideals"
|
||||
placeholder="Add ideals here..."
|
||||
content={this.getTraitContent(Constants.TraitTypeEnum.IDEALS)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.IDEALS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList("Ideals", "ideals")}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Bonds</h3>}
|
||||
editButtonLabel="Edit Bonds"
|
||||
addButtonLabel="Add Bonds"
|
||||
placeholder="Add bonds here..."
|
||||
content={this.getTraitContent(Constants.TraitTypeEnum.BONDS)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.BONDS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList("Bonds", "bonds")}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Flaws</h3>}
|
||||
editButtonLabel="Edit Flaws"
|
||||
addButtonLabel="Add Flaws"
|
||||
placeholder="Add flaws here..."
|
||||
content={this.getTraitContent(Constants.TraitTypeEnum.FLAWS)}
|
||||
onSave={this.handleSaveTrait.bind(
|
||||
this,
|
||||
Constants.TraitTypeEnum.FLAWS
|
||||
)}
|
||||
extraNode={this.renderTextareaSuggestionList("Flaws", "flaws")}
|
||||
/>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
summary="Notes"
|
||||
summaryMetaItems={[
|
||||
"Organizations",
|
||||
"Allies",
|
||||
"Enemies",
|
||||
"Backstory",
|
||||
"Other",
|
||||
]}
|
||||
variant="paper"
|
||||
>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Organizations</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add organization notes here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.ORGANIZATIONS)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.ORGANIZATIONS
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Are there any important organizations your character belongs to?
|
||||
Any societies, orders, cults, or agencies?
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Allies</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add allies here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.ALLIES)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.ALLIES
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
With whom does your character associate? When in need of aid, do
|
||||
they seek out their kinfolk, followers of their deity, or other
|
||||
members of their order? What groups or individuals are they
|
||||
aligned with?
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Enemies</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add enemies here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.ENEMIES)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.ENEMIES
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Who does your character fear or fight? Have they sworn a vow to
|
||||
rid the world of undead? Is there an order or organization they
|
||||
are opposed to? Are there any specific foes from their past?
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Backstory</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add a backstory..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.BACKSTORY)}
|
||||
onSave={this.handleSaveNote.bind(
|
||||
this,
|
||||
Constants.NoteKeyEnum.BACKSTORY
|
||||
)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Talk about your character's origins. Where are they from? How
|
||||
did they end up adventuring? How did they choose their class?
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<EditorWithDialog
|
||||
heading={<h3>Other</h3>}
|
||||
editButtonLabel="Edit Notes"
|
||||
addButtonLabel="Add Notes"
|
||||
placeholder="Add additional notes here..."
|
||||
content={this.getNoteContent(Constants.NoteKeyEnum.OTHER)}
|
||||
onSave={this.handleSaveNote.bind(this, Constants.NoteKeyEnum.OTHER)}
|
||||
extraNode={
|
||||
<p className="description-suggestion-info">
|
||||
Anything at all you'd like to mention about your character.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Accordion>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
+19
-22
@@ -3,7 +3,6 @@ import React from "react";
|
||||
import { RouteKey } from "~/subApps/builder/constants";
|
||||
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import PageSubHeader from "../../../components/PageSubHeader";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
@@ -12,29 +11,27 @@ class EquipmentHelp extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<PageHeader>Choose Equipment</PageHeader>
|
||||
<p>
|
||||
In this step, you will select weapons, armor, and other adventuring
|
||||
gear for your character.
|
||||
</p>
|
||||
<PageHeader>Choose Equipment</PageHeader>
|
||||
<p>
|
||||
In this step, you will select weapons, armor, and other adventuring
|
||||
gear for your character.
|
||||
</p>
|
||||
|
||||
<PageSubHeader>Starting Equipment</PageSubHeader>
|
||||
<p>
|
||||
Your starting equipment options will be displayed on the next
|
||||
screen. Your class and background determine your character’s
|
||||
starting equipment. Review the options and add the appropriate items
|
||||
using Manage Equipment.
|
||||
</p>
|
||||
<PageSubHeader>Starting Equipment</PageSubHeader>
|
||||
<p>
|
||||
Your starting equipment options will be displayed on the next screen.
|
||||
Your class and background determine your character’s starting
|
||||
equipment. Review the options and add the appropriate items using
|
||||
Manage Equipment.
|
||||
</p>
|
||||
|
||||
<PageSubHeader>Starting Gold</PageSubHeader>
|
||||
<p>
|
||||
Instead of taking the gear given to you by your class and
|
||||
background, you can purchase starting equipment. You have a number
|
||||
of gold pieces (GP) to spend based on your class, as shown on the
|
||||
next screen if you choose this option.
|
||||
</p>
|
||||
</PageBody>
|
||||
<PageSubHeader>Starting Gold</PageSubHeader>
|
||||
<p>
|
||||
Instead of taking the gear given to you by your class and background,
|
||||
you can purchase starting equipment. You have a number of gold pieces
|
||||
(GP) to spend based on your class, as shown on the next screen if you
|
||||
choose this option.
|
||||
</p>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
+14
-18
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { useContext } from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
import { AnyAction } from "redux";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import {
|
||||
@@ -48,12 +49,13 @@ import { CoinManagerContext } from "../../../../Shared/managers/CoinManagerConte
|
||||
import { InventoryManagerContext } from "../../../../Shared/managers/InventoryManagerContext";
|
||||
import { AppNotificationUtils } from "../../../../Shared/utils";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import { PageHeader } from "../../../components/PageHeader";
|
||||
import { BuilderAppState } from "../../../typings";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
configuration: CharacterConfiguration;
|
||||
inventory: Array<Item>;
|
||||
containerLookup: ContainerLookup;
|
||||
@@ -65,7 +67,6 @@ interface Props extends DispatchProp {
|
||||
theme: CharacterTheme;
|
||||
globalModifiers: Array<Modifier>;
|
||||
valueLookupByType: TypeValueLookup;
|
||||
proficiencyBonus: number;
|
||||
inventoryManager: InventoryManager;
|
||||
coinManager: CoinManager;
|
||||
activeSourceCategories: Array<number>;
|
||||
@@ -260,7 +261,7 @@ class EquipmentManage extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
{ItemUtils.sortInventoryItems(inventory).map((item) =>
|
||||
this.renderItem(
|
||||
item,
|
||||
@@ -270,7 +271,7 @@ class EquipmentManage extends React.PureComponent<Props, State> {
|
||||
gearLabels
|
||||
)
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -349,7 +350,6 @@ class EquipmentManage extends React.PureComponent<Props, State> {
|
||||
theme,
|
||||
globalModifiers,
|
||||
valueLookupByType,
|
||||
proficiencyBonus,
|
||||
activeSourceCategories,
|
||||
inventoryManager,
|
||||
} = this.props;
|
||||
@@ -369,7 +369,6 @@ class EquipmentManage extends React.PureComponent<Props, State> {
|
||||
valueLookupByType={valueLookupByType}
|
||||
onItemAdd={this.handleItemAdd}
|
||||
ruleData={ruleData}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
containerDefinitionKey={characterContainerKey}
|
||||
activeSourceCategories={activeSourceCategories}
|
||||
inventoryManager={inventoryManager}
|
||||
@@ -426,16 +425,14 @@ class EquipmentManage extends React.PureComponent<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<PageHeader>Choose Equipment</PageHeader>
|
||||
<div className="equipment-manage">
|
||||
{this.renderStartingEquipment()}
|
||||
{this.renderInventory()}
|
||||
{this.renderOtherPossessions()}
|
||||
{this.renderAddItems()}
|
||||
{this.renderCurrency()}
|
||||
</div>
|
||||
</PageBody>
|
||||
<PageHeader>Choose Equipment</PageHeader>
|
||||
<div className="equipment-manage">
|
||||
{this.renderStartingEquipment()}
|
||||
{this.renderInventory()}
|
||||
{this.renderOtherPossessions()}
|
||||
{this.renderAddItems()}
|
||||
{this.renderCurrency()}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -469,7 +466,6 @@ export default ConnectedBuilderPage(
|
||||
currencies: rulesEngineSelectors.getCurrencies(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
hasMaxAttunedItems: rulesEngineSelectors.hasMaxAttunedItems(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
characterId: rulesEngineSelectors.getId(state),
|
||||
globalModifiers: rulesEngineSelectors.getValidGlobalModifiers(state),
|
||||
valueLookupByType:
|
||||
|
||||
@@ -1,852 +0,0 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
HTMLAttributes,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
characterActions,
|
||||
CharacterPreferences,
|
||||
Constants,
|
||||
HelperUtils,
|
||||
HtmlSelectOption,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
rulesEngineSelectors,
|
||||
CharacterUtils,
|
||||
ClassSpellListSpellsLookup,
|
||||
ClassUtils,
|
||||
CharClass,
|
||||
Race,
|
||||
RaceUtils,
|
||||
characterSelectors,
|
||||
PremadeInfo,
|
||||
PremadeInfoStatus,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { Dice } from "@dndbeyond/dice";
|
||||
|
||||
import { SourceCategoryDescription } from "~/constants";
|
||||
import { InputField } from "~/subApps/builder/components/InputField";
|
||||
import { RouteKey } from "~/subApps/builder/constants";
|
||||
import {
|
||||
ModalData,
|
||||
useModalManager,
|
||||
} from "~/subApps/builder/contexts/ModalManager";
|
||||
import UserRoles from "~/tools/js/Shared/constants/UserRoles";
|
||||
import PrivacyTypeRadio from "~/tools/js/smartComponents/PrivacyTypeRadio";
|
||||
|
||||
import { appEnvActions } from "../../../../Shared/actions";
|
||||
import { SimpleClassSpellList } from "../../../../Shared/components/SimpleClassSpellList";
|
||||
import {
|
||||
FormCheckBoxesField,
|
||||
CheckboxInfo,
|
||||
} from "../../../../Shared/components/common/FormCheckBoxesField";
|
||||
import FormSelectField from "../../../../Shared/components/common/FormSelectField";
|
||||
import FormToggleField from "../../../../Shared/components/common/FormToggleField";
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import config from "../../../../config";
|
||||
import RadioGroup from "../../../components/CharacterSheetOptions/RadioGroup";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import { BuilderAppState } from "../../../typings";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
preferences: CharacterPreferences;
|
||||
ruleData: RuleData;
|
||||
activeSourceCategories: Array<number>;
|
||||
diceEnabled: boolean;
|
||||
userRoles: string[] | null | undefined;
|
||||
classes: Array<CharClass>;
|
||||
species: Race | null;
|
||||
classSpellListSpellsLookup: ClassSpellListSpellsLookup;
|
||||
premadeInfo: PremadeInfo | null;
|
||||
characterId: number;
|
||||
createModal: (modalData: ModalData) => void;
|
||||
}
|
||||
|
||||
class HomeBasicInfo extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
handlePreferenceChange = (prefKey: string, value: boolean): void => {
|
||||
const { dispatch } = this.props;
|
||||
const typedPrefKey = CharacterUtils.getPreferenceKey(prefKey);
|
||||
if (typedPrefKey !== null) {
|
||||
dispatch(characterActions.preferenceChoose(typedPrefKey, value));
|
||||
}
|
||||
};
|
||||
|
||||
handleIntPreferenceChange = (prefKey: string, value: string): void => {
|
||||
const { dispatch } = this.props;
|
||||
const typedPrefKey = CharacterUtils.getPreferenceKey(prefKey);
|
||||
if (typedPrefKey !== null) {
|
||||
dispatch(
|
||||
characterActions.preferenceChoose(
|
||||
typedPrefKey,
|
||||
HelperUtils.parseInputInt(value)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleSourceCategoryChange = (sourceId: number, isActive: boolean): void => {
|
||||
const { dispatch, activeSourceCategories } = this.props;
|
||||
|
||||
let newSourceCats: Array<any> = [];
|
||||
if (isActive) {
|
||||
newSourceCats = [...activeSourceCategories, sourceId];
|
||||
} else {
|
||||
newSourceCats = activeSourceCategories.filter((id) => id !== sourceId);
|
||||
}
|
||||
|
||||
dispatch(characterActions.activeSourceCategoriesSet(newSourceCats));
|
||||
};
|
||||
|
||||
handlePartneredSourceChangeAll = (
|
||||
sourceIds: number[],
|
||||
isActive: boolean
|
||||
): void => {
|
||||
const { dispatch, activeSourceCategories } = this.props;
|
||||
|
||||
let newSourceCats: Array<any> = [];
|
||||
if (isActive) {
|
||||
newSourceCats = [...activeSourceCategories, ...sourceIds];
|
||||
} else {
|
||||
newSourceCats = activeSourceCategories.filter(
|
||||
(id) => !sourceIds.includes(id)
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(characterActions.activeSourceCategoriesSet(newSourceCats));
|
||||
};
|
||||
|
||||
handleOptionalClassFeaturesPreferenceChangePromise = (
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
): void => {
|
||||
const { classes, classSpellListSpellsLookup, createModal } = this.props;
|
||||
const spellListIds: Array<number> =
|
||||
ClassUtils.getUpdateEnableOptionalClassFeaturesSpellListIdsToRemove(
|
||||
classes,
|
||||
newIsEnabled
|
||||
);
|
||||
|
||||
const hasSpellsToRemove = spellListIds.some((id) =>
|
||||
classSpellListSpellsLookup.hasOwnProperty(id)
|
||||
);
|
||||
|
||||
if (!hasSpellsToRemove) {
|
||||
this.handlePreferenceChange("enableOptionalClassFeatures", newIsEnabled);
|
||||
accept();
|
||||
} else {
|
||||
createModal({
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to disable{" "}
|
||||
<strong>Optional Class Features</strong> for this character?
|
||||
</p>
|
||||
<p>
|
||||
After doing so, the following spells provided by these features
|
||||
will be removed from your character:
|
||||
</p>
|
||||
<SimpleClassSpellList
|
||||
spellListIds={spellListIds}
|
||||
classSpellListSpellsLookup={classSpellListSpellsLookup}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
props: {
|
||||
heading: "Optional Class Features",
|
||||
size: "fit-content",
|
||||
variant: "remove",
|
||||
onConfirm: () => {
|
||||
this.handlePreferenceChange(
|
||||
"enableOptionalClassFeatures",
|
||||
newIsEnabled
|
||||
);
|
||||
accept();
|
||||
},
|
||||
onClose: () => {
|
||||
reject();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleOptionalOriginsPreferenceChangePromise = (
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
): void => {
|
||||
const { createModal, species, classSpellListSpellsLookup } = this.props;
|
||||
|
||||
if (!species) {
|
||||
this.handlePreferenceChange("enableOptionalOrigins", newIsEnabled);
|
||||
accept();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const spellListIds: Array<number> =
|
||||
RaceUtils.getUpdateEnableOptionalOriginsSpellListIdsToRemove(
|
||||
species,
|
||||
newIsEnabled
|
||||
);
|
||||
|
||||
const hasSpellsToRemove = spellListIds.some((id) =>
|
||||
classSpellListSpellsLookup.hasOwnProperty(id)
|
||||
);
|
||||
|
||||
if (!hasSpellsToRemove) {
|
||||
this.handlePreferenceChange("enableOptionalOrigins", newIsEnabled);
|
||||
accept();
|
||||
} else {
|
||||
createModal({
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to disable{" "}
|
||||
<strong>Customized Origins</strong> for this character?
|
||||
</p>
|
||||
<p>
|
||||
After doing so, the following spells provided by these features
|
||||
will be removed from your character:
|
||||
</p>
|
||||
<SimpleClassSpellList
|
||||
spellListIds={spellListIds}
|
||||
classSpellListSpellsLookup={classSpellListSpellsLookup}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
props: {
|
||||
heading: "Customized Origin Features",
|
||||
size: "fit-content",
|
||||
variant: "remove",
|
||||
onConfirm: () => {
|
||||
this.handlePreferenceChange("enableOptionalOrigins", newIsEnabled);
|
||||
accept();
|
||||
},
|
||||
onClose: () => {
|
||||
reject();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleProgressionPreferenceChangePromise = (
|
||||
newValue: string,
|
||||
oldValue: string,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
): void => {
|
||||
const { dispatch, createModal } = this.props;
|
||||
|
||||
const prefKey = CharacterUtils.getPreferenceKey("progressionType");
|
||||
const newIdValue = HelperUtils.parseInputInt(newValue);
|
||||
|
||||
let content: ReactNode;
|
||||
let heading: string | null = null;
|
||||
if (newIdValue === Constants.PreferenceProgressionTypeEnum.XP) {
|
||||
heading = "XP Advancement";
|
||||
content = (
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to change your advancement method to XP
|
||||
progression?
|
||||
</p>
|
||||
<p>You will begin with the base XP value for your current level.</p>
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
newIdValue === Constants.PreferenceProgressionTypeEnum.MILESTONE
|
||||
) {
|
||||
heading = "Milestone Advancement";
|
||||
content = (
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to change your advancement method to Milestone
|
||||
progression?
|
||||
</p>
|
||||
<p>Your current XP values will be lost.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (prefKey !== null && heading) {
|
||||
createModal({
|
||||
content,
|
||||
props: {
|
||||
heading,
|
||||
size: "fit-content",
|
||||
onConfirm: () => {
|
||||
dispatch(characterActions.preferenceChoose(prefKey, newIdValue));
|
||||
accept();
|
||||
},
|
||||
onClose: () => {
|
||||
reject();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleHitPointPreferenceChangePromise = (
|
||||
newValue: string,
|
||||
oldValue: string,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
): void => {
|
||||
const { dispatch, createModal } = this.props;
|
||||
|
||||
const prefKey = CharacterUtils.getPreferenceKey("hitPointType");
|
||||
const newIdValue = HelperUtils.parseInputInt(newValue);
|
||||
|
||||
let content: ReactNode;
|
||||
let heading: string | null = null;
|
||||
if (newIdValue === Constants.PreferenceHitPointTypeEnum.FIXED) {
|
||||
heading = "Fixed Hit Points";
|
||||
content = (
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to change your hit points to the fixed value?
|
||||
</p>
|
||||
<p>Any rolled hit point totals will be lost.</p>
|
||||
</div>
|
||||
);
|
||||
} else if (newIdValue === Constants.PreferenceHitPointTypeEnum.MANUAL) {
|
||||
heading = "Manual Hit Points";
|
||||
content = (
|
||||
<div>
|
||||
<p>Are you sure you want to change your hit points manual entry?</p>
|
||||
<p>
|
||||
After doing so, use Manage HP in the Class section to enter your
|
||||
rolled values.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (prefKey !== null && heading) {
|
||||
createModal({
|
||||
content,
|
||||
props: {
|
||||
heading,
|
||||
size: "fit-content",
|
||||
onConfirm: () => {
|
||||
dispatch(characterActions.preferenceChoose(prefKey, newIdValue));
|
||||
accept();
|
||||
},
|
||||
onClose: () => {
|
||||
reject();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleDiceToggle = (): void => {
|
||||
const { dispatch, diceEnabled } = this.props;
|
||||
|
||||
const newDiceEnabledSetting: boolean = !diceEnabled;
|
||||
|
||||
try {
|
||||
localStorage.setItem("dice-enabled", newDiceEnabledSetting.toString());
|
||||
Dice.setEnabled(newDiceEnabledSetting);
|
||||
} catch (e) {}
|
||||
|
||||
dispatch(
|
||||
appEnvActions.dataSet({
|
||||
diceEnabled: newDiceEnabledSetting,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
handleChangePrivacy = (value: number | null): void => {
|
||||
const typedPrefKey = CharacterUtils.getPreferenceKey("privacyType");
|
||||
if (typedPrefKey !== null) {
|
||||
this.props.dispatch(
|
||||
characterActions.preferenceChoose(typedPrefKey, value)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderPreferences = (): React.ReactNode => {
|
||||
const { preferences, ruleData, activeSourceCategories, diceEnabled } =
|
||||
this.props;
|
||||
|
||||
const {
|
||||
useHomebrewContent,
|
||||
encumbranceType,
|
||||
hitPointType,
|
||||
progressionType,
|
||||
abilityScoreDisplayType,
|
||||
privacyType,
|
||||
ignoreCoinWeight,
|
||||
enforceFeatRules,
|
||||
enforceMulticlassRules,
|
||||
showScaledSpells,
|
||||
enableOptionalOrigins,
|
||||
enableOptionalClassFeatures,
|
||||
} = preferences;
|
||||
|
||||
const encumbranceOptions: Array<HtmlSelectOption> = [
|
||||
{
|
||||
label: "Use Encumbrance",
|
||||
value: Constants.PreferenceEncumbranceTypeEnum.ENCUMBRANCE,
|
||||
},
|
||||
{
|
||||
label: "No Encumbrance",
|
||||
value: Constants.PreferenceEncumbranceTypeEnum.NONE,
|
||||
},
|
||||
{
|
||||
label: "Variant Encumbrance",
|
||||
value: Constants.PreferenceEncumbranceTypeEnum.VARIANT,
|
||||
},
|
||||
];
|
||||
|
||||
const hpOptions: Array<HtmlSelectOption> = [
|
||||
{ label: "Fixed", value: Constants.PreferenceHitPointTypeEnum.FIXED },
|
||||
{ label: "Manual", value: Constants.PreferenceHitPointTypeEnum.MANUAL },
|
||||
];
|
||||
|
||||
const advancementOptions: Array<HtmlSelectOption> = [
|
||||
{
|
||||
label: "Milestone",
|
||||
value: Constants.PreferenceProgressionTypeEnum.MILESTONE,
|
||||
},
|
||||
{ label: "XP", value: Constants.PreferenceProgressionTypeEnum.XP },
|
||||
];
|
||||
|
||||
const abilityDisplayOptions: Array<HtmlSelectOption> = [
|
||||
{
|
||||
label: "Modifiers Top",
|
||||
value: Constants.PreferenceAbilityScoreDisplayTypeEnum.MODIFIERS_TOP,
|
||||
},
|
||||
{
|
||||
label: "Scores Top",
|
||||
value: Constants.PreferenceAbilityScoreDisplayTypeEnum.SCORES_TOP,
|
||||
},
|
||||
];
|
||||
|
||||
let sourceToggles: Array<CheckboxInfo> = [];
|
||||
let partneredSourceCheckboxes: Array<CheckboxInfo> = [];
|
||||
let allPartneredSources: Array<number> = [];
|
||||
|
||||
RuleDataUtils.getSourceCategories(ruleData).forEach((sourceCategory) => {
|
||||
if (!sourceCategory.isToggleable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkbox: CheckboxInfo = {
|
||||
label: `${sourceCategory.name}`,
|
||||
initiallyEnabled: activeSourceCategories.includes(sourceCategory.id),
|
||||
onChange: this.handleSourceCategoryChange.bind(this, sourceCategory.id),
|
||||
sortOrder: sourceCategory.sortOrder,
|
||||
description: sourceCategory.description ?? "",
|
||||
};
|
||||
|
||||
if (sourceCategory.isPartneredContent) {
|
||||
delete checkbox.description; // remove description from partnered content
|
||||
partneredSourceCheckboxes.push(checkbox);
|
||||
allPartneredSources.push(sourceCategory.id);
|
||||
} else {
|
||||
sourceToggles.push(checkbox);
|
||||
}
|
||||
});
|
||||
|
||||
const checkboxAllPartneredContent: CheckboxInfo = {
|
||||
label: "All Partnered Content",
|
||||
initiallyEnabled: allPartneredSources.every((id) =>
|
||||
activeSourceCategories.includes(id)
|
||||
),
|
||||
onChange: this.handlePartneredSourceChangeAll.bind(
|
||||
this,
|
||||
allPartneredSources
|
||||
),
|
||||
};
|
||||
return (
|
||||
<div className="home-manage-preferences">
|
||||
<FormCheckBoxesField
|
||||
heading="Sources"
|
||||
description={SourceCategoryDescription.official}
|
||||
checkboxes={[
|
||||
...sourceToggles,
|
||||
{
|
||||
label: "Homebrew",
|
||||
description: SourceCategoryDescription.homebrew,
|
||||
initiallyEnabled: useHomebrewContent,
|
||||
onChange: this.handlePreferenceChange.bind(
|
||||
this,
|
||||
"useHomebrewContent"
|
||||
),
|
||||
sortOrder: 0,
|
||||
},
|
||||
]}
|
||||
showAccordion={false}
|
||||
variant="builder"
|
||||
/>
|
||||
|
||||
<FormCheckBoxesField
|
||||
heading="Partnered Content"
|
||||
description={SourceCategoryDescription.partnered}
|
||||
checkboxes={partneredSourceCheckboxes}
|
||||
checkUncheckAllEnabled={true}
|
||||
onCheckUncheckAll={checkboxAllPartneredContent}
|
||||
showAccordion={true}
|
||||
accordionHeading="Choose Partners"
|
||||
variant="builder"
|
||||
allText="All"
|
||||
/>
|
||||
|
||||
<FormCheckBoxesField
|
||||
heading="Dice Rolling"
|
||||
description="Enables digital dice rolling for this character"
|
||||
checkboxes={[
|
||||
{
|
||||
initiallyEnabled: diceEnabled,
|
||||
label: "Enable Dice Rolling",
|
||||
onChange: this.handleDiceToggle,
|
||||
},
|
||||
]}
|
||||
variant="builder"
|
||||
/>
|
||||
|
||||
<FormCheckBoxesField
|
||||
heading="Optional Features"
|
||||
description="Allow or restrict optional features for this character."
|
||||
checkboxes={[
|
||||
{
|
||||
label: "Optional Class Features",
|
||||
initiallyEnabled: enableOptionalClassFeatures,
|
||||
onChangePromise:
|
||||
this.handleOptionalClassFeaturesPreferenceChangePromise,
|
||||
},
|
||||
{
|
||||
label: "Customize Your Origin",
|
||||
initiallyEnabled: enableOptionalOrigins,
|
||||
onChangePromise:
|
||||
this.handleOptionalOriginsPreferenceChangePromise,
|
||||
},
|
||||
]}
|
||||
variant="builder"
|
||||
/>
|
||||
|
||||
<FormSelectField
|
||||
heading="Advancement Type"
|
||||
description="Story-based character progression / XP-based character progression"
|
||||
onChangePromise={this.handleProgressionPreferenceChangePromise}
|
||||
initialOptionRemoved={true}
|
||||
options={advancementOptions}
|
||||
initialValue={"" + progressionType}
|
||||
block={true}
|
||||
/>
|
||||
<FormSelectField
|
||||
heading="Hit Point Type"
|
||||
description="When leveling up, increase hit points by the fixed value for your chosen class or manually enter a rolled value"
|
||||
onChangePromise={this.handleHitPointPreferenceChangePromise}
|
||||
initialOptionRemoved={true}
|
||||
options={hpOptions}
|
||||
initialValue={"" + hitPointType}
|
||||
block={true}
|
||||
/>
|
||||
|
||||
<FormCheckBoxesField
|
||||
heading="Use Prerequisites"
|
||||
description="Allow or restrict choices based on rule prerequisites for the following for this character"
|
||||
checkboxes={[
|
||||
{
|
||||
label: "Feats",
|
||||
initiallyEnabled: enforceFeatRules,
|
||||
onChange: this.handlePreferenceChange.bind(
|
||||
this,
|
||||
"enforceFeatRules"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Multiclass Requirements",
|
||||
initiallyEnabled: enforceMulticlassRules,
|
||||
onChange: this.handlePreferenceChange.bind(
|
||||
this,
|
||||
"enforceMulticlassRules"
|
||||
),
|
||||
},
|
||||
]}
|
||||
variant="builder"
|
||||
/>
|
||||
|
||||
<FormCheckBoxesField
|
||||
heading="Show Level-Scaled Spells"
|
||||
description="Display and highlight available spells to cast with higher level spell slots"
|
||||
checkboxes={[
|
||||
{
|
||||
label: "Show Level-Scaled Spells",
|
||||
initiallyEnabled: showScaledSpells,
|
||||
onChange: this.handlePreferenceChange.bind(
|
||||
this,
|
||||
"showScaledSpells"
|
||||
),
|
||||
},
|
||||
]}
|
||||
variant="builder"
|
||||
/>
|
||||
|
||||
<FormSelectField
|
||||
heading="Encumbrance Type"
|
||||
description="Use the standard encumbrance rules / Disable the encumbrance display / Use the more detailed rules for encumbrance"
|
||||
onChange={this.handleIntPreferenceChange.bind(
|
||||
this,
|
||||
"encumbranceType"
|
||||
)}
|
||||
initialOptionRemoved={true}
|
||||
options={encumbranceOptions}
|
||||
initialValue={"" + encumbranceType}
|
||||
block={true}
|
||||
/>
|
||||
|
||||
<FormCheckBoxesField
|
||||
heading="Ignore Coin Weight"
|
||||
description="Coins do not count against your total weight carried (50 coins weigh 1 lb.)"
|
||||
checkboxes={[
|
||||
{
|
||||
label: "Ignore Coin Weight",
|
||||
initiallyEnabled: ignoreCoinWeight,
|
||||
onChange: this.handlePreferenceChange.bind(
|
||||
this,
|
||||
"ignoreCoinWeight"
|
||||
),
|
||||
},
|
||||
]}
|
||||
variant="builder"
|
||||
/>
|
||||
|
||||
<FormSelectField
|
||||
heading="Ability Score/Modifier Display"
|
||||
description="Reverse the arrangement of ability modifiers and scores"
|
||||
onChange={this.handleIntPreferenceChange.bind(
|
||||
this,
|
||||
"abilityScoreDisplayType"
|
||||
)}
|
||||
initialOptionRemoved={true}
|
||||
options={abilityDisplayOptions}
|
||||
initialValue={"" + abilityScoreDisplayType}
|
||||
block={true}
|
||||
/>
|
||||
|
||||
<PrivacyTypeRadio
|
||||
initialValue={privacyType}
|
||||
handleChange={(e) =>
|
||||
this.handleChangePrivacy(parseInt(e.target.value))
|
||||
}
|
||||
variant="builder"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
handleAddPremadeInfo = (): void => {
|
||||
const { dispatch, characterId } = this.props;
|
||||
|
||||
const data = {
|
||||
characterId: characterId,
|
||||
publishStatus: PremadeInfoStatus.DRAFT,
|
||||
definition: {
|
||||
longDescription: null,
|
||||
shortDescription: null,
|
||||
imageUrl: null,
|
||||
imageAltText: null,
|
||||
mobileImageUrl: null,
|
||||
mobileImageAccessibility: null,
|
||||
themeColor: null,
|
||||
},
|
||||
};
|
||||
|
||||
dispatch(characterActions.premadeInfoAdd(data));
|
||||
};
|
||||
|
||||
handleDeletePremadeInfo = (): void => {
|
||||
const { dispatch, characterId } = this.props;
|
||||
dispatch(characterActions.premadeInfoDelete(characterId));
|
||||
};
|
||||
|
||||
handlePremadeInfoChanged = (premadeInfo: PremadeInfo): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.premadeInfoUpdate(premadeInfo));
|
||||
};
|
||||
|
||||
renderPremadePreferences = (): React.ReactNode => {
|
||||
const { premadeInfo } = this.props;
|
||||
|
||||
const statusOptions: Array<HtmlSelectOption> = [
|
||||
{ label: "Draft", value: PremadeInfoStatus.DRAFT },
|
||||
{ label: "Published", value: PremadeInfoStatus.PUBLISHED },
|
||||
{ label: "Archived", value: PremadeInfoStatus.ARCHIVED },
|
||||
];
|
||||
|
||||
const inputAttributes = {
|
||||
disabled: !premadeInfo,
|
||||
} as HTMLAttributes<HTMLInputElement>;
|
||||
|
||||
return (
|
||||
<div className="home-manage-preferences">
|
||||
<FormToggleField
|
||||
heading="Premade Character"
|
||||
toggleLabel="Enabled official character"
|
||||
description="Toggle on to make this a premade character. You cannot disable this if the premade status is Published."
|
||||
initiallyEnabled={!!premadeInfo}
|
||||
onChange={(toggledOn) => {
|
||||
toggledOn
|
||||
? this.handleAddPremadeInfo()
|
||||
: this.handleDeletePremadeInfo();
|
||||
}}
|
||||
isReadOnly={
|
||||
premadeInfo?.publishStatus === PremadeInfoStatus.PUBLISHED
|
||||
}
|
||||
/>
|
||||
{premadeInfo && (
|
||||
<>
|
||||
<RadioGroup
|
||||
name="premadeStatus"
|
||||
label="Publish Status"
|
||||
subtitle="Mark this character as published when you are ready for it to be publically visible."
|
||||
value={premadeInfo.publishStatus}
|
||||
options={statusOptions}
|
||||
disabled={!premadeInfo}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
premadeInfo.publishStatus = event.currentTarget
|
||||
.value as PremadeInfoStatus;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
this.forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Long Description"
|
||||
initialValue={premadeInfo.definition.longDescription}
|
||||
inputProps={inputAttributes}
|
||||
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
||||
premadeInfo.definition.longDescription = e.target.value;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Short Description"
|
||||
initialValue={premadeInfo.definition.shortDescription}
|
||||
inputProps={inputAttributes}
|
||||
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
||||
premadeInfo.definition.shortDescription = e.target.value;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Image Url"
|
||||
initialValue={premadeInfo.definition.imageUrl}
|
||||
inputProps={inputAttributes}
|
||||
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
||||
premadeInfo.definition.imageUrl = e.target.value;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Image Alt Text"
|
||||
initialValue={premadeInfo.definition.imageAltText}
|
||||
inputProps={inputAttributes}
|
||||
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
||||
premadeInfo.definition.imageAltText = e.target.value;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Mobile Image Url"
|
||||
initialValue={premadeInfo.definition.mobileImageUrl}
|
||||
inputProps={inputAttributes}
|
||||
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
||||
premadeInfo.definition.mobileImageUrl = e.target.value;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Mobile Image Accessibility"
|
||||
initialValue={premadeInfo.definition.mobileImageAccessibility}
|
||||
inputProps={inputAttributes}
|
||||
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
||||
premadeInfo.definition.mobileImageAccessibility =
|
||||
e.target.value;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Theme Color Hex Code"
|
||||
initialValue={premadeInfo.definition.themeColor}
|
||||
inputProps={inputAttributes}
|
||||
onBlur={(e: FocusEvent<HTMLInputElement>) => {
|
||||
premadeInfo.definition.themeColor = e.target.value;
|
||||
this.handlePremadeInfoChanged(premadeInfo);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { userRoles } = this.props;
|
||||
const isLorekeeper =
|
||||
userRoles?.includes(UserRoles.LOREKEEPER) ||
|
||||
userRoles?.includes(UserRoles.ADMIN);
|
||||
|
||||
return (
|
||||
<Page clsNames={["home-manage"]}>
|
||||
<PageBody>
|
||||
{isLorekeeper && (
|
||||
<div style={{ border: "5px solid hotpink" }}>
|
||||
<PageHeader>
|
||||
Premade Character Preferences - Lorekeepers Only
|
||||
</PageHeader>
|
||||
{this.renderPremadePreferences()}
|
||||
</div>
|
||||
)}
|
||||
<Typography fontSize={24}>Character Preferences</Typography>
|
||||
{this.renderPreferences()}
|
||||
<div className="home-manage__version">
|
||||
<div className="home-manage__version-label">Version:</div>
|
||||
<div className="home-manage__version-value">{config.version}</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HomeBasicInfoContainer = (props: Props) => {
|
||||
const { createModal } = useModalManager();
|
||||
return <HomeBasicInfo {...props} createModal={createModal} />;
|
||||
};
|
||||
|
||||
export default ConnectedBuilderPage(
|
||||
HomeBasicInfoContainer,
|
||||
RouteKey.HOME_BASIC_INFO,
|
||||
(state: BuilderAppState) => ({
|
||||
preferences: rulesEngineSelectors.getCharacterPreferences(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
activeSourceCategories:
|
||||
rulesEngineSelectors.getActiveSourceCategories(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
classes: rulesEngineSelectors.getClasses(state),
|
||||
species: rulesEngineSelectors.getRace(state),
|
||||
classSpellListSpellsLookup:
|
||||
rulesEngineSelectors.getClassSpellListSpellsLookup(state),
|
||||
userRoles: appEnvSelectors.getUserRoles(state),
|
||||
premadeInfo: characterSelectors.getPremadeInfo(state),
|
||||
characterId: rulesEngineSelectors.getId(state),
|
||||
})
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
import HomeBasicInfo from "./HomeBasicInfo";
|
||||
|
||||
export default HomeBasicInfo;
|
||||
export { HomeBasicInfo };
|
||||
@@ -5,7 +5,6 @@ import { RouteKey } from "~/subApps/builder/constants";
|
||||
import { CollapsibleContent } from "../../../../../../components/CollapsibleContent";
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import { BuilderAppState } from "../../../typings";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
@@ -20,73 +19,71 @@ class HomeHelp extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<>
|
||||
<PageHeader>Creating a Character</PageHeader>
|
||||
<p>
|
||||
Your first step in playing an adventure in the Dungeons &
|
||||
Dragons game is to imagine and create a character of your own.
|
||||
Your character is a combination of game statistics,
|
||||
roleplaying hooks, and your imagination.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
You choose a class (such as Fighter or Wizard), a background (such
|
||||
as Artisan or Soldier), and a species (such as Human or Halfling).
|
||||
You also invent the personality, appearance, and backstory of your
|
||||
character. Once completed, your character serves as your
|
||||
representative in the game, your avatar in the Dungeons &
|
||||
Dragons multiverse.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Before you dive into the character builder, think about the kind
|
||||
of adventurer you want to play. You might be a courageous Fighter,
|
||||
a skulking Rogue, a fervent Cleric, or a flamboyant Wizard. Or you
|
||||
might be more interested in an unconventional character, such as a
|
||||
brawny Rogue who likes hand-to-hand combat, or a sharpshooter who
|
||||
picks off enemies from afar. Do you like fantasy fiction featuring
|
||||
Dwarves or Elves? Try building a character of one of those
|
||||
species. Do you want your character to be the toughest adventurer
|
||||
at the table? Consider a class like Barbarian or Paladin. Once you
|
||||
have a character in mind, follow the steps in this builder in
|
||||
order, making decisions that reflect the character you want. Your
|
||||
conception of your character might evolve with each choice you
|
||||
make. What's important is that you come to the table with a
|
||||
character you're excited to play.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<>
|
||||
<PageHeader>Character Level</PageHeader>
|
||||
<p>
|
||||
Typically, your character begins play at 1st level. In the
|
||||
next step, adjust your level if you’re playing in a
|
||||
higher-powered campaign.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
As your character goes on adventures and overcomes challenges,
|
||||
they gain experience, represented by Experience Points (XP). Once
|
||||
you reach a specified XP total, you gain a new level. Adjusting
|
||||
your character level will change your XP total and vice versa.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
<PageHeader>Preferences</PageHeader>
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<>
|
||||
<PageHeader>Creating a Character</PageHeader>
|
||||
<p>
|
||||
Your first step in playing an adventure in the Dungeons &
|
||||
Dragons game is to imagine and create a character of your own.
|
||||
Your character is a combination of game statistics, roleplaying
|
||||
hooks, and your imagination.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
The next step also includes various preferences for your character.
|
||||
You can proceed with the default options or make changes if desired.
|
||||
You choose a class (such as Fighter or Wizard), a background (such
|
||||
as Artisan or Soldier), and a species (such as Human or Halfling).
|
||||
You also invent the personality, appearance, and backstory of your
|
||||
character. Once completed, your character serves as your
|
||||
representative in the game, your avatar in the Dungeons &
|
||||
Dragons multiverse.
|
||||
</p>
|
||||
</PageBody>
|
||||
|
||||
<p>
|
||||
Before you dive into the character builder, think about the kind of
|
||||
adventurer you want to play. You might be a courageous Fighter, a
|
||||
skulking Rogue, a fervent Cleric, or a flamboyant Wizard. Or you
|
||||
might be more interested in an unconventional character, such as a
|
||||
brawny Rogue who likes hand-to-hand combat, or a sharpshooter who
|
||||
picks off enemies from afar. Do you like fantasy fiction featuring
|
||||
Dwarves or Elves? Try building a character of one of those species.
|
||||
Do you want your character to be the toughest adventurer at the
|
||||
table? Consider a class like Barbarian or Paladin. Once you have a
|
||||
character in mind, follow the steps in this builder in order, making
|
||||
decisions that reflect the character you want. Your conception of
|
||||
your character might evolve with each choice you make. What's
|
||||
important is that you come to the table with a character you're
|
||||
excited to play.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<>
|
||||
<PageHeader>Character Level</PageHeader>
|
||||
<p>
|
||||
Typically, your character begins play at 1st level. In the next
|
||||
step, adjust your level if you’re playing in a higher-powered
|
||||
campaign.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
As your character goes on adventures and overcomes challenges, they
|
||||
gain experience, represented by Experience Points (XP). Once you
|
||||
reach a specified XP total, you gain a new level. Adjusting your
|
||||
character level will change your XP total and vice versa.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
<PageHeader>Preferences</PageHeader>
|
||||
<p>
|
||||
The next step also includes various preferences for your character.
|
||||
You can proceed with the default options or make changes if desired.
|
||||
</p>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { RouteKey } from "~/subApps/builder/constants";
|
||||
import { CollapsibleContent } from "../../../../../../components/CollapsibleContent";
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import { BuilderAppState } from "../../../typings";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
@@ -17,96 +16,93 @@ interface Props {
|
||||
const SpeciesHelp: React.FC<Props> = ({ isMobile }) => {
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<PageHeader>Choose your Species</PageHeader>
|
||||
<PageHeader>Choose your Species</PageHeader>
|
||||
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<p>
|
||||
Choose a species in the next step. Your choice of species affects
|
||||
many different aspects of your character. It establishes
|
||||
fundamental qualities that exist throughout your character's
|
||||
adventuring career. When making this decision, keep in mind the
|
||||
kind of character you want to play.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<p>
|
||||
For example, a halfling could be a good choice for a sneaky rogue, a
|
||||
dwarf makes a tough warrior, and an elf can be a master of arcane
|
||||
magic.
|
||||
Choose a species in the next step. Your choice of species affects
|
||||
many different aspects of your character. It establishes fundamental
|
||||
qualities that exist throughout your character's adventuring career.
|
||||
When making this decision, keep in mind the kind of character you
|
||||
want to play.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
For example, a halfling could be a good choice for a sneaky rogue, a
|
||||
dwarf makes a tough warrior, and an elf can be a master of arcane
|
||||
magic.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Your character species not only affects your ability scores and traits
|
||||
but also provides the cues for building your character's story. Each
|
||||
species' description includes information to help you roleplay a
|
||||
character of that species, including personality, physical appearance,
|
||||
features of society, and alignment tendencies.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
These details are suggestions to help you think about your character;
|
||||
adventurers can deviate widely from the norm for their species. It's
|
||||
worthwhile to consider why your character is different, as a helpful
|
||||
way to think about your character's background and personality.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
|
||||
<PageHeader>Traits</PageHeader>
|
||||
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<p>
|
||||
Your character species not only affects your ability scores and
|
||||
traits but also provides the cues for building your character's
|
||||
story. Each species' description includes information to help you
|
||||
roleplay a character of that species, including personality,
|
||||
physical appearance, features of society, and alignment tendencies.
|
||||
The description of each species includes traits that are common to
|
||||
members of that species. The following entries appear among the
|
||||
traits of most species.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<strong>AGE</strong>
|
||||
<br />
|
||||
The age entry notes the age when a member of the species is considered
|
||||
an adult, as well as the species' expected lifespan. This information
|
||||
can help you decide how old your character is at the start of the
|
||||
game. You can choose any age for your character, which could provide
|
||||
an explanation for some of your ability scores.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
These details are suggestions to help you think about your
|
||||
character; adventurers can deviate widely from the norm for their
|
||||
species. It's worthwhile to consider why your character is
|
||||
different, as a helpful way to think about your character's
|
||||
background and personality.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
<p>
|
||||
For example, if you play a young or very old character, your age could
|
||||
explain a particularly low Strength or Constitution score, while
|
||||
advanced age could account for a high Intelligence or Wisdom.
|
||||
</p>
|
||||
|
||||
<PageHeader>Traits</PageHeader>
|
||||
<p>
|
||||
<strong>SIZE</strong>
|
||||
<br />
|
||||
Characters of most species are Medium, a size category including
|
||||
creatures that are roughly 4 to 8 feet tall. Members of a few species
|
||||
are Small (between 2 and 4 feet tall), which means that certain rules
|
||||
of the game affect them differently.
|
||||
</p>
|
||||
|
||||
<CollapsibleContent
|
||||
forceShow={!isMobile}
|
||||
heading={
|
||||
<p>
|
||||
The description of each species includes traits that are common to
|
||||
members of that species. The following entries appear among the
|
||||
traits of most species.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<strong>AGE</strong>
|
||||
<br />
|
||||
The age entry notes the age when a member of the species is
|
||||
considered an adult, as well as the species' expected lifespan. This
|
||||
information can help you decide how old your character is at the
|
||||
start of the game. You can choose any age for your character, which
|
||||
could provide an explanation for some of your ability scores.
|
||||
</p>
|
||||
<p>
|
||||
<strong>SPEED</strong>
|
||||
<br />
|
||||
Your speed determines how far you can move when traveling and
|
||||
fighting.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For example, if you play a young or very old character, your age
|
||||
could explain a particularly low Strength or Constitution score,
|
||||
while advanced age could account for a high Intelligence or Wisdom.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>SIZE</strong>
|
||||
<br />
|
||||
Characters of most species are Medium, a size category including
|
||||
creatures that are roughly 4 to 8 feet tall. Members of a few
|
||||
species are Small (between 2 and 4 feet tall), which means that
|
||||
certain rules of the game affect them differently.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>SPEED</strong>
|
||||
<br />
|
||||
Your speed determines how far you can move when traveling and
|
||||
fighting.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>LANGUAGES</strong>
|
||||
<br />
|
||||
By virtue of your species, your character can speak, read, and write
|
||||
certain languages.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
</PageBody>
|
||||
<p>
|
||||
<strong>LANGUAGES</strong>
|
||||
<br />
|
||||
By virtue of your species, your character can speak, read, and write
|
||||
certain languages.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-6
@@ -258,7 +258,7 @@ export class OptionalOriginManager extends React.PureComponent<
|
||||
contentNode = (
|
||||
<div className="ct-optional-origin-manager__content">
|
||||
{replacementOrigins.length > 0 && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<PageSubHeader>Replacement Traits</PageSubHeader>
|
||||
{RaceUtils.deriveOrderedRacialTraits(replacementOrigins).map(
|
||||
(origin: RacialTrait) => {
|
||||
@@ -285,7 +285,7 @@ export class OptionalOriginManager extends React.PureComponent<
|
||||
affectedFeatureDefinitionKey={
|
||||
affectedFeatureDefinitionKey
|
||||
}
|
||||
isSelected={!!optionalOriginMapping ?? false}
|
||||
isSelected={!!optionalOriginMapping}
|
||||
onSelection={onSelection}
|
||||
onChangeReplacementPromise={onChangeReplacementPromise}
|
||||
onRemoveSelectionPromise={onRemoveSelectionPromise}
|
||||
@@ -295,10 +295,10 @@ export class OptionalOriginManager extends React.PureComponent<
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
{additionalOrigins.length > 0 && (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<PageSubHeader>Additional Traits</PageSubHeader>
|
||||
{RaceUtils.deriveOrderedRacialTraits(additionalOrigins).map(
|
||||
(origin: RacialTrait) => {
|
||||
@@ -325,7 +325,7 @@ export class OptionalOriginManager extends React.PureComponent<
|
||||
affectedFeatureDefinitionKey={
|
||||
affectedFeatureDefinitionKey
|
||||
}
|
||||
isSelected={!!optionalOriginMapping ?? false}
|
||||
isSelected={!!optionalOriginMapping}
|
||||
onSelection={onSelection}
|
||||
onChangeReplacementPromise={onChangeReplacementPromise}
|
||||
onRemoveSelectionPromise={onRemoveSelectionPromise}
|
||||
@@ -335,7 +335,7 @@ export class OptionalOriginManager extends React.PureComponent<
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import OptionalOriginManager from "./OptionalOriginManager";
|
||||
|
||||
export default OptionalOriginManager;
|
||||
export { OptionalOriginManager };
|
||||
+40
-43
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
import { NavigateFunction, useNavigate } from "react-router-dom";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import {
|
||||
ApiAdapterPromise,
|
||||
@@ -44,7 +45,6 @@ import {
|
||||
import { SimpleClassSpellList } from "../../../../Shared/components/SimpleClassSpellList";
|
||||
import { apiCreatorSelectors } from "../../../../Shared/selectors";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import SpeciesTraitList from "../../../components/SpeciesTraitList";
|
||||
import { navigationConfig } from "../../../config";
|
||||
@@ -52,7 +52,9 @@ import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
import { OptionalOriginManager } from "./OptionalOriginManager";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
||||
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
||||
interface Props extends DispatchProp<AnyAction> {
|
||||
ruleData: RuleData;
|
||||
preferences: CharacterPreferences;
|
||||
prerequisiteData: PrerequisiteData;
|
||||
@@ -81,7 +83,7 @@ class SpeciesManage extends React.PureComponent<Props> {
|
||||
navigate(
|
||||
navigationConfig
|
||||
.getRouteDefPath(RouteKey.RACE_CHOOSE)
|
||||
.replace(":characterId", characterId)
|
||||
.replace(":characterId", characterId.toString())
|
||||
);
|
||||
};
|
||||
|
||||
@@ -460,50 +462,45 @@ class SpeciesManage extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<div className="manage-race race-detail">
|
||||
<div className="race-detail-primary">
|
||||
<div className="race-detail-aside">
|
||||
<div className="race-detail-preview">
|
||||
<img
|
||||
className="race-detail-preview-img"
|
||||
src={previewUrl ? previewUrl : ""}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="manage-race-chosen-action"
|
||||
onClick={this.handleChangeSpecies}
|
||||
>
|
||||
Change Species
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader>{fullName}</PageHeader>
|
||||
<HtmlContent
|
||||
className={descriptionClassNames.join(" ")}
|
||||
html={description ? description : ""}
|
||||
withoutTooltips
|
||||
/>
|
||||
{calledOutSpeciesTraits.length > 0 && (
|
||||
<SummaryList
|
||||
list={calledOutSpeciesTraits}
|
||||
title="Species Traits"
|
||||
className={styles.summaryList}
|
||||
<div className="manage-race race-detail">
|
||||
<div className="race-detail-primary">
|
||||
<div className="race-detail-aside">
|
||||
<div className="race-detail-preview">
|
||||
<img
|
||||
className="race-detail-preview-img"
|
||||
src={previewUrl || ""}
|
||||
alt={fullName || "Species preview portrait"}
|
||||
/>
|
||||
)}
|
||||
<div className="race-detail-more">
|
||||
<a
|
||||
className="race-detail-more-link"
|
||||
href={moreDetailsUrl ? moreDetailsUrl : ""}
|
||||
>
|
||||
{fullName} Details Page
|
||||
</a>
|
||||
</div>
|
||||
{this.renderInformationCollapsible()}
|
||||
<div
|
||||
className="manage-race-chosen-action"
|
||||
onClick={this.handleChangeSpecies}
|
||||
>
|
||||
Change Species
|
||||
</div>
|
||||
</div>
|
||||
{this.renderSpeciesTraitsContent()}
|
||||
<PageHeader>{fullName}</PageHeader>
|
||||
<HtmlContent
|
||||
className={descriptionClassNames.join(" ")}
|
||||
html={description || ""}
|
||||
withoutTooltips
|
||||
/>
|
||||
{calledOutSpeciesTraits.length > 0 && (
|
||||
<SummaryList
|
||||
list={calledOutSpeciesTraits}
|
||||
title="Species Traits"
|
||||
className={styles.summaryList}
|
||||
/>
|
||||
)}
|
||||
<div className="race-detail-more">
|
||||
<a className="race-detail-more-link" href={moreDetailsUrl || ""}>
|
||||
{fullName} Details Page
|
||||
</a>
|
||||
</div>
|
||||
{this.renderInformationCollapsible()}
|
||||
</div>
|
||||
</PageBody>
|
||||
{this.renderSpeciesTraitsContent()}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
import React from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
LightExportSvg,
|
||||
DisabledExportSvg,
|
||||
DarkExportSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
|
||||
import { Link } from "~/components/Link";
|
||||
import { ExportPdfData, usePdfExport } from "~/hooks/usePdfExport";
|
||||
import { RouteKey } from "~/subApps/builder/constants";
|
||||
|
||||
import { BuilderLinkButton } from "../../../../Shared/components/common/LinkButton";
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import { ClipboardUtils, MobileMessengerUtils } from "../../../../Shared/utils";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import { builderEnvSelectors, builderSelectors } from "../../../selectors";
|
||||
import { BuilderAppState } from "../../../typings";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
characterId: number | null;
|
||||
characterListingUrl: string;
|
||||
characterSheetUrl: string;
|
||||
isCharacterSheetReady: boolean;
|
||||
exportPdfData: ExportPdfData;
|
||||
}
|
||||
interface State {
|
||||
hasCopied: boolean;
|
||||
}
|
||||
class WhatsNext extends React.PureComponent<Props, State> {
|
||||
urlInput = React.createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasCopied: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
const {
|
||||
exportPdfData: { pdfUrl },
|
||||
} = this.props;
|
||||
|
||||
if (pdfUrl !== null) {
|
||||
this.selectUrlInput();
|
||||
}
|
||||
}
|
||||
|
||||
handleExportPdf = (evt: React.MouseEvent): void => {
|
||||
const {
|
||||
isCharacterSheetReady,
|
||||
exportPdfData: { exportPdf, isLoading, isFinished },
|
||||
} = this.props;
|
||||
|
||||
if (isCharacterSheetReady && !isLoading && !isFinished) {
|
||||
exportPdf();
|
||||
}
|
||||
};
|
||||
|
||||
selectUrlInput = (): void => {
|
||||
if (this.urlInput.current) {
|
||||
this.urlInput.current.focus();
|
||||
this.urlInput.current.setSelectionRange(
|
||||
0,
|
||||
this.urlInput.current.value.length
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const {
|
||||
exportPdfData: { pdfUrl },
|
||||
} = this.props;
|
||||
|
||||
ClipboardUtils.copyTextToClipboard(pdfUrl === null ? "" : pdfUrl);
|
||||
this.setState({
|
||||
hasCopied: true,
|
||||
});
|
||||
};
|
||||
|
||||
handleSheetShowClick = (isDisabled: boolean) => {
|
||||
const { characterId } = this.props;
|
||||
|
||||
if (characterId !== null && !isDisabled) {
|
||||
MobileMessengerUtils.sendMessage(
|
||||
MobileMessengerUtils.createShowCharacterSheetMessage(characterId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderPdfButton = (): React.ReactNode => {
|
||||
const {
|
||||
isCharacterSheetReady,
|
||||
exportPdfData: { isLoading, isFinished },
|
||||
} = this.props;
|
||||
// const { pdfLoadingStatus } = this.state;
|
||||
|
||||
let pdfButtonClasses: Array<string> = [
|
||||
"ct-button",
|
||||
"builder-button",
|
||||
"whats-next-action-pdf",
|
||||
"character-button-oversized",
|
||||
"character-tools-export-pdf-button",
|
||||
];
|
||||
let iconNode: React.ReactNode;
|
||||
if (isCharacterSheetReady) {
|
||||
iconNode = <LightExportSvg />;
|
||||
} else {
|
||||
pdfButtonClasses.push("character-button-disabled");
|
||||
pdfButtonClasses.push("character-button-oversized-disabled");
|
||||
|
||||
iconNode = <DisabledExportSvg />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className={pdfButtonClasses.join(" ")}
|
||||
onClick={this.handleExportPdf}
|
||||
>
|
||||
<span className="whats-next-action-icon">{iconNode}</span>
|
||||
<span className="whats-next-action-text">Exporting PDF...</span>
|
||||
</div>
|
||||
);
|
||||
} else if (isFinished) {
|
||||
pdfButtonClasses.push("whats-next-action-confirmed");
|
||||
return (
|
||||
<div
|
||||
className={pdfButtonClasses.join(" ")}
|
||||
onClick={this.handleExportPdf}
|
||||
>
|
||||
<span className="whats-next-action-icon">✓</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
pdfButtonClasses.push("whats-next-action-clickable");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={pdfButtonClasses.join(" ")}
|
||||
onClick={this.handleExportPdf}
|
||||
>
|
||||
<span className="whats-next-action-icon">{iconNode}</span>
|
||||
<span className="whats-next-action-text">Export to PDF</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderPdfData = (): React.ReactNode => {
|
||||
const {
|
||||
exportPdfData: { pdfUrl, isFinished },
|
||||
} = this.props;
|
||||
const { hasCopied } = this.state;
|
||||
|
||||
if (!isFinished || pdfUrl === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="whats-next-pdf-data">
|
||||
<div className="whats-next-pdf-data-splash-icon">
|
||||
<DarkExportSvg />
|
||||
</div>
|
||||
<div className="whats-next-pdf-data-header">PDF Generated</div>
|
||||
<div className="whats-next-pdf-data-url">
|
||||
<input
|
||||
type="text"
|
||||
value={pdfUrl ? pdfUrl.replace(/https*:\/\//, "") : ""}
|
||||
className="whats-next-pdf-data-input"
|
||||
ref={this.urlInput}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="whats-next-pdf-data-clipboard"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<React.Fragment>Copied! ✔</React.Fragment>
|
||||
) : (
|
||||
"Click to Copy"
|
||||
)}
|
||||
</div>
|
||||
{pdfUrl !== null && (
|
||||
<div className="whats-next-pdf-data-download">
|
||||
<BuilderLinkButton
|
||||
url={pdfUrl}
|
||||
size={"large"}
|
||||
download={true}
|
||||
className="whats-next-pdf-data-download-link"
|
||||
>
|
||||
Click to Download
|
||||
</BuilderLinkButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { characterListingUrl, characterSheetUrl, isCharacterSheetReady } =
|
||||
this.props;
|
||||
|
||||
return (
|
||||
<Page clsNames={["whats-next"]}>
|
||||
<PageBody>
|
||||
<p>
|
||||
Once you have completed creating your character, you can view your
|
||||
statistics on the digital character sheet or export it for printing.
|
||||
</p>
|
||||
|
||||
<div className="whats-next-actions">
|
||||
<div className="whats-next-action">
|
||||
<BuilderLinkButton
|
||||
url={characterSheetUrl}
|
||||
className="whats-next-action-sheet-link"
|
||||
size="oversized"
|
||||
disabled={!isCharacterSheetReady}
|
||||
onClick={this.handleSheetShowClick}
|
||||
>
|
||||
<div className="whats-next-action-text">
|
||||
<div className="whats-next-action-text">
|
||||
View Character Sheet
|
||||
{!isCharacterSheetReady && (
|
||||
<span className="whats-next-action-subtext">
|
||||
Unavailable - Character Incomplete
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BuilderLinkButton>
|
||||
</div>
|
||||
<div className="whats-next-action">{this.renderPdfButton()}</div>
|
||||
</div>
|
||||
{this.renderPdfData()}
|
||||
{!isCharacterSheetReady && (
|
||||
<p className={styles.returnToHomeText}>
|
||||
If you are unable to create a character due to missing options,
|
||||
return to the <strong>Home</strong> tab and change your source
|
||||
settings.
|
||||
</p>
|
||||
)}
|
||||
<div className="whats-next-characters">
|
||||
<Link href={characterListingUrl}>View all my characters</Link>
|
||||
</div>
|
||||
|
||||
<PageHeader>Come Together</PageHeader>
|
||||
|
||||
<p>
|
||||
Most D&D characters don't work alone. Each character plays a
|
||||
role within a party, a group of adventurers working together for a
|
||||
common purpose. Talk to your fellow players and your DM to decide
|
||||
whether your characters know one another, how they met, and what
|
||||
sorts of quests the group might undertake.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A campaign is a series of adventures undertaken by your group's
|
||||
characters. If your DM has a campaign, you can join it by visiting
|
||||
the invite link for the campaign. You can also start your own.
|
||||
</p>
|
||||
|
||||
<div className="whats-next-campaign">
|
||||
<Link href="/campaigns/create" className="whats-next-campaign-link">
|
||||
Start a new campaign
|
||||
</Link>
|
||||
</div>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function WhatsNextContainer(props) {
|
||||
const exportPdfData = usePdfExport();
|
||||
return <WhatsNext {...props} exportPdfData={exportPdfData} />;
|
||||
}
|
||||
|
||||
export default ConnectedBuilderPage(
|
||||
WhatsNextContainer,
|
||||
RouteKey.WHATS_NEXT,
|
||||
(state: BuilderAppState) => {
|
||||
return {
|
||||
characterId: appEnvSelectors.getCharacterId(state),
|
||||
characterListingUrl: builderEnvSelectors.getProfileCharacterListingUrl(),
|
||||
characterSheetUrl: builderEnvSelectors.getCharacterSheetUrl(state),
|
||||
isCharacterSheetReady: builderSelectors.checkIsCharacterSheetReady(state),
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
import WhatsNext from "./WhatsNext";
|
||||
|
||||
export default WhatsNext;
|
||||
export { WhatsNext };
|
||||
@@ -27,8 +27,6 @@ import {
|
||||
builderActionTypes,
|
||||
BuilderAction,
|
||||
BuilderMethodSetAction,
|
||||
QuickBuildRequestAction,
|
||||
RandomBuildRequestAction,
|
||||
StepBuildRequestAction,
|
||||
SuggestedNamesRequestAction,
|
||||
} from "../actions";
|
||||
@@ -39,8 +37,6 @@ import { NavigationUtils } from "../utils";
|
||||
|
||||
const syncActions = [
|
||||
builderActionTypes.CHARACTER_LOAD_REQUEST,
|
||||
builderActionTypes.QUICK_BUILD_REQUEST,
|
||||
builderActionTypes.RANDOM_BUILD_REQUEST,
|
||||
builderActionTypes.STEP_BUILD_REQUEST,
|
||||
builderActionTypes.BUILDER_METHOD_SET,
|
||||
builderActionTypes.SUGGESTED_NAMES_REQUEST,
|
||||
@@ -212,14 +208,6 @@ function* syncFilter(action: BuilderAction) {
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case builderActionTypes.QUICK_BUILD_REQUEST:
|
||||
yield call(handleQuickBuildRequest, action);
|
||||
break;
|
||||
|
||||
case builderActionTypes.RANDOM_BUILD_REQUEST:
|
||||
yield call(handleRandomBuildRequest, action);
|
||||
break;
|
||||
|
||||
case builderActionTypes.STEP_BUILD_REQUEST:
|
||||
yield call(handleStepBuildRequest, action);
|
||||
break;
|
||||
@@ -237,18 +225,11 @@ function* syncFilter(action: BuilderAction) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case builderActionTypes.QUICK_BUILD_REQUEST:
|
||||
case builderActionTypes.RANDOM_BUILD_REQUEST:
|
||||
break;
|
||||
default:
|
||||
let syncTransactionInitiator = yield select(
|
||||
syncTransactionSelectors.getInitiator
|
||||
);
|
||||
if (syncTransactionInitiator === transactionInitiatorId) {
|
||||
yield put(syncTransactionActions.deactivate());
|
||||
}
|
||||
break;
|
||||
let syncTransactionInitiator = yield select(
|
||||
syncTransactionSelectors.getInitiator
|
||||
);
|
||||
if (syncTransactionInitiator === transactionInitiatorId) {
|
||||
yield put(syncTransactionActions.deactivate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,43 +247,6 @@ function* handleBuilderMethodSet(action: BuilderMethodSetAction) {
|
||||
yield put(builderActions.builderMethodSetCommit(action.payload));
|
||||
}
|
||||
|
||||
function* handleQuickBuildRequest(action: QuickBuildRequestAction) {
|
||||
const characterId: UnpackMakeApiResponseData<
|
||||
typeof ApiRequests.postCharacterBuilderQuickBuild
|
||||
> = yield call(
|
||||
SagaHelpers.getApiRequestData,
|
||||
ApiRequests.postCharacterBuilderQuickBuild,
|
||||
action.payload
|
||||
);
|
||||
|
||||
// if (data.success) {
|
||||
const characterUrl = NavigationUtils.getCharacterBuilderUrl(characterId);
|
||||
const initialPath = navConfig.getRouteDefPath(RouteKey.WHATS_NEXT);
|
||||
// todo just a soft navigation
|
||||
window.location = `${characterUrl}${initialPath}` as any;
|
||||
// } else {
|
||||
//TODO do something with error
|
||||
// }
|
||||
}
|
||||
|
||||
function* handleRandomBuildRequest(action: RandomBuildRequestAction) {
|
||||
const characterId: UnpackMakeApiResponseData<
|
||||
typeof ApiRequests.postCharacterBuilderRandomBuild
|
||||
> = yield call(
|
||||
SagaHelpers.getApiRequestData,
|
||||
ApiRequests.postCharacterBuilderRandomBuild,
|
||||
action.payload
|
||||
);
|
||||
|
||||
// if (data.success) {
|
||||
const characterUrl = NavigationUtils.getCharacterBuilderUrl(characterId);
|
||||
const initialPath = navConfig.getRouteDefPath(RouteKey.WHATS_NEXT);
|
||||
window.location = `${characterUrl}#${initialPath}` as any;
|
||||
// } else {
|
||||
//TODO do something with error
|
||||
// }
|
||||
}
|
||||
|
||||
function* handleStepBuildRequest(action: StepBuildRequestAction) {
|
||||
yield put(builderActions.characterLoadingSetCommit(true));
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
import {
|
||||
featureFlagInfoSelectors,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { NavigationType } from "~/subApps/builder/constants";
|
||||
|
||||
import { getNavBuilderConfig } from "../config/navigation";
|
||||
import { BuilderAppState } from "../typings";
|
||||
|
||||
export const getCurrentPath = (state) => window.location.pathname;
|
||||
export const getCurrentPath = () => window.location.pathname;
|
||||
export const getCurrentSearchParams = () => window.location.search;
|
||||
|
||||
export const getBuilderMethod = (state: BuilderAppState) =>
|
||||
state.builder.method;
|
||||
@@ -32,10 +30,7 @@ export const getRoutePathing = createSelector(
|
||||
rulesEngineSelectors.getCharacterConfiguration,
|
||||
],
|
||||
(state, builderMethod, configuration) => {
|
||||
const navConfig = getNavBuilderConfig(
|
||||
builderMethod,
|
||||
featureFlagInfoSelectors.getFeatureFlagInfo(state)
|
||||
);
|
||||
const navConfig = getNavBuilderConfig(builderMethod);
|
||||
|
||||
let exclusionNavTypes: Array<any> = [];
|
||||
if (!configuration.showHelpText) {
|
||||
@@ -71,10 +66,7 @@ export const getAvailableRoutePathing = createSelector(
|
||||
rulesEngineSelectors.getCharacterConfiguration,
|
||||
],
|
||||
(state, builderMethod, configuration) => {
|
||||
const navConfig = getNavBuilderConfig(
|
||||
builderMethod,
|
||||
featureFlagInfoSelectors.getFeatureFlagInfo(state)
|
||||
);
|
||||
const navConfig = getNavBuilderConfig(builderMethod);
|
||||
|
||||
let exclusionNavTypes: Array<any> = [];
|
||||
if (!configuration.showHelpText) {
|
||||
@@ -108,10 +100,7 @@ export const getFirstAvailableSectionRoutes = createSelector(
|
||||
rulesEngineSelectors.getCharacterConfiguration,
|
||||
],
|
||||
(state, builderMethod, configuration) => {
|
||||
const navConfig = getNavBuilderConfig(
|
||||
builderMethod,
|
||||
featureFlagInfoSelectors.getFeatureFlagInfo(state)
|
||||
);
|
||||
const navConfig = getNavBuilderConfig(builderMethod);
|
||||
|
||||
let exclusionNavTypes: Array<any> = [];
|
||||
if (!configuration.showHelpText) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { flatten } from "lodash";
|
||||
|
||||
import {
|
||||
featureFlagInfoSelectors,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import config from "~/config";
|
||||
import { NavigationType } from "~/subApps/builder/constants";
|
||||
@@ -26,10 +23,7 @@ export function checkStdBuilderPageRequirements(routeDef, state) {
|
||||
builderSelectors.getBuilderMethod(state) !== null &&
|
||||
checkIsRouteInNavConfig(
|
||||
routeDef,
|
||||
navConfig.getNavBuilderConfig(
|
||||
builderSelectors.getBuilderMethod(state),
|
||||
featureFlagInfoSelectors.getFeatureFlagInfo(state)
|
||||
)
|
||||
navConfig.getNavBuilderConfig(builderSelectors.getBuilderMethod(state))
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -73,11 +67,10 @@ export function getRoutePathIdxByPath(path, routePathDef, characterId = 0) {
|
||||
}
|
||||
|
||||
export function getCurrentSectionHelpRoutePath(state) {
|
||||
const currentPath = builderSelectors.getCurrentPath(state);
|
||||
const currentPath = builderSelectors.getCurrentPath();
|
||||
const sections = navConfig.getNavigationSections(
|
||||
builderSelectors.getBuilderMethod(state),
|
||||
navConfig.getCurrentRouteDef(currentPath),
|
||||
featureFlagInfoSelectors.getFeatureFlagInfo(state)
|
||||
navConfig.getCurrentRouteDef(currentPath)
|
||||
);
|
||||
const currentSection = sections.find((section) => section.active);
|
||||
const currentSectionHelpRoute = currentSection.routes.find(
|
||||
@@ -92,16 +85,18 @@ export function getCurrentSectionHelpRoutePath(state) {
|
||||
}
|
||||
|
||||
export function getAvailablePathRedirect(state) {
|
||||
const currentPath = builderSelectors.getCurrentPath(state);
|
||||
const currentPath = builderSelectors.getCurrentPath();
|
||||
const currentSearch = builderSelectors.getCurrentSearchParams();
|
||||
const routePathing = builderSelectors.getRoutePathing(state);
|
||||
const characterId = rulesEngineSelectors.getId(state);
|
||||
const currentRoutePathIdx = getRoutePathIdxByPath(
|
||||
currentPath,
|
||||
`${currentPath}${currentSearch}`,
|
||||
routePathing,
|
||||
characterId
|
||||
);
|
||||
|
||||
const currentRoutePathingNode = routePathing[currentRoutePathIdx];
|
||||
|
||||
if (!currentRoutePathingNode.available) {
|
||||
for (let i = currentRoutePathIdx; i < routePathing.length; i++) {
|
||||
if (routePathing[i].available) {
|
||||
@@ -144,15 +139,15 @@ export const getAvailableNextRoute = (routeDef, state) => {
|
||||
};
|
||||
|
||||
export function getCharacterBuilderUrl(characterId): string {
|
||||
return `${BASE_PATHNAME}/${characterId}/builder`;
|
||||
return getUrlWithParams(`${BASE_PATHNAME}/${characterId}/builder`);
|
||||
}
|
||||
|
||||
export function getCharacterSheetUrl(characterId): string {
|
||||
return `${BASE_PATHNAME}/${characterId}`;
|
||||
return getUrlWithParams(`${BASE_PATHNAME}/${characterId}`);
|
||||
}
|
||||
|
||||
export function getCharacterListingUrl(): string {
|
||||
return `${BASE_PATHNAME}`;
|
||||
return getUrlWithParams(`${BASE_PATHNAME}`);
|
||||
}
|
||||
|
||||
export function getUrlWithParams(url?: string): string {
|
||||
|
||||
Reference in New Issue
Block a user