New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
@@ -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,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
@@ -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">
@@ -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>(
@@ -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>
);
};
@@ -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,
};
@@ -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;
@@ -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 [];
}
@@ -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;
@@ -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>
);
@@ -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);
@@ -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 {
@@ -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,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";
@@ -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
>
&lt; 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 &gt;
</Link>
@@ -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 dont 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 dont 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>
);
}
@@ -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>
);
}
@@ -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",
@@ -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}
/>
@@ -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>
);
}
@@ -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>
);
@@ -0,0 +1,4 @@
import OptionalFeatureManager from "./OptionalFeatureManager";
export default OptionalFeatureManager;
export { OptionalFeatureManager };
@@ -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 characters background describes where they came from, their
original occupation, and their place in the D&amp;D world.
</p>
<PageHeader>Background</PageHeader>
<p>
Your characters background describes where they came from, their
original occupation, and their place in the D&amp;D world.
</p>
<p>
Backgrounds from the Core D&amp;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&amp;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>
Youll need to decide your characters appearance and personality.
Choose your characters 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>
Youll need to decide your characters appearance and personality.
Choose your characters 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>
);
}
@@ -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>
);
}
@@ -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 characters
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 characters 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>
);
}
@@ -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 &amp;
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 youre 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 &amp;
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 youre 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>
);
};
@@ -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>
);
@@ -0,0 +1,4 @@
import OptionalOriginManager from "./OptionalOriginManager";
export default OptionalOriginManager;
export { OptionalOriginManager };
@@ -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! &#x2714;</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&amp;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 {
@@ -112,14 +112,14 @@ export default class ActionSnippet extends React.PureComponent<Props> {
if (item !== null) {
const infusion = ItemUtils.getInfusion(item);
name = (
<React.Fragment>
<>
<ItemName item={item} />
{infusion !== null && (
<span className="ct-feature-snippet__heading-extra">
(Infusion: {InfusionUtils.getName(infusion)})
</span>
)}
</React.Fragment>
</>
);
}
}
@@ -1,5 +1,7 @@
import clsx from "clsx";
import { orderBy } from "lodash";
import React, { HTMLAttributes, useMemo } from "react";
import { AttackTable } from "@dndbeyond/character-components/es";
import {
AbilityLookup,
@@ -21,15 +23,15 @@ import {
CharacterTheme,
Action,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import ActionSnippet from "../ActionSnippet";
import BasicActions from "../BasicActions";
import { FeatureSnippetSpells } from "../FeatureSnippet";
import clsx from "clsx";
import styles from "./styles.module.css";
import { ActionListSection } from "./ActionListSection";
import styles from "./styles.module.css";
interface ActionsListProps extends HTMLAttributes<HTMLDivElement> {
heading: React.ReactNode;
@@ -49,10 +51,10 @@ interface ActionsListProps extends HTMLAttributes<HTMLDivElement> {
abilityLookup: AbilityLookup;
dataOriginRefData: DataOriginRefData;
proficiencyBonus: number;
rollContext: IRollContext;
rollContext: RollContext;
inventoryLookup: InventoryLookup;
snippetData: SnippetData;
// ActionHandlers
onAttackClick: (attack: Attack) => void;
onActionClick: (action: Action) => void;
@@ -60,7 +62,7 @@ interface ActionsListProps extends HTMLAttributes<HTMLDivElement> {
onActionUseSet: (action: Action, used: number) => void;
onSpellClick: (spell: Spell) => void;
onSpellUseSet: (spell: Spell, used: number) => void;
};
}
export const ActionsList = ({
heading,
@@ -94,16 +96,17 @@ export const ActionsList = ({
className,
...props
}: ActionsListProps) => {
const { actionLookup } = useCharacterEngine();
const { actionLookup, itemPlans } = useCharacterEngine();
const isVisible = useMemo(
() => !!basicActions.length
|| !!actions.length
|| !!ritualSpells.length
|| !!attacks.length,
() =>
!!basicActions.length ||
!!actions.length ||
!!ritualSpells.length ||
!!attacks.length,
[basicActions, actions, ritualSpells, attacks]
);
const attackTableProps = {
attacks,
weaponSpellDamageGroups,
@@ -117,6 +120,7 @@ export const ActionsList = ({
dataOriginRefData,
proficiencyBonus,
rollContext,
itemPlans,
};
const spellSnippetProps = {
@@ -146,43 +150,42 @@ export const ActionsList = ({
}, [actions]);
const orderedRitualSpells = useMemo(
() => orderBy(
ritualSpells,
(spell) => SpellUtils.getName(spell)
),
() => orderBy(ritualSpells, (spell) => SpellUtils.getName(spell)),
[ritualSpells]
);
const features = useMemo(() => actions.filter(
a => a.type === Constants.ActivatableTypeEnum.ACTION)
.map(feature => {
const action = HelperUtils.lookupDataOrFallback(
actionLookup,
ActionUtils.getUniqueKey(feature.entity)
);
return (
<div
className={styles.activatable}
key={feature.key}
data-testid="actions-list-activatable-feature"
>
<ActionSnippet
theme={theme}
action={action ?? feature.entity}
onActionClick={onActionClick}
onActionUseSet={onActionUseSet}
abilityLookup={abilityLookup}
inventoryLookup={inventoryLookup}
ruleData={ruleData}
snippetData={snippetData}
isInteractive={isInteractive}
showActivationInfo={showActivationInfo}
activation={feature.activation}
proficiencyBonus={proficiencyBonus}
/>
</div>
);
}),
const features = useMemo(
() =>
actions
.filter((a) => a.type === Constants.ActivatableTypeEnum.ACTION)
.map((feature) => {
const action = HelperUtils.lookupDataOrFallback(
actionLookup,
ActionUtils.getUniqueKey(feature.entity)
);
return (
<div
className={styles.activatable}
key={feature.key}
data-testid="actions-list-activatable-feature"
>
<ActionSnippet
theme={theme}
action={action ?? feature.entity}
onActionClick={onActionClick}
onActionUseSet={onActionUseSet}
abilityLookup={abilityLookup}
inventoryLookup={inventoryLookup}
ruleData={ruleData}
snippetData={snippetData}
isInteractive={isInteractive}
showActivationInfo={showActivationInfo}
activation={feature.activation}
proficiencyBonus={proficiencyBonus}
/>
</div>
);
}),
[
actions,
actionLookup,
@@ -196,7 +199,8 @@ export const ActionsList = ({
isInteractive,
showActivationInfo,
proficiencyBonus,
]);
]
);
return isVisible ? (
<div className={clsx([styles.actionsList, className])} {...props}>
@@ -207,10 +211,16 @@ export const ActionsList = ({
{heading}
</div>
<div>
{!!attacks.length && <AttackTable className={styles.attackTable} {...attackTableProps} />}
{!!attacks.length && (
<AttackTable className={styles.attackTable} {...attackTableProps} />
)}
{!!basicActions.length && (
<ActionListSection headingText="Actions in Combat" testId="basic" theme={theme}>
<ActionListSection
headingText="Actions in Combat"
testId="basic"
theme={theme}
>
<BasicActions
onActionClick={onBasicActionClick}
basicActions={basicActions}
@@ -221,13 +231,21 @@ export const ActionsList = ({
{!!spells.length && (
<ActionListSection headingText="Spells" theme={theme}>
<FeatureSnippetSpells layoutType={"compact"} spells={spells} {...spellSnippetProps} />
<FeatureSnippetSpells
layoutType={"compact"}
spells={spells}
{...spellSnippetProps}
/>
</ActionListSection>
)}
{!!ritualSpells.length && (
<ActionListSection headingText="Ritual Spells" theme={theme}>
<FeatureSnippetSpells layoutType={"compact"} spells={orderedRitualSpells} {...spellSnippetProps} />
<FeatureSnippetSpells
layoutType={"compact"}
spells={orderedRitualSpells}
{...spellSnippetProps}
/>
</ActionListSection>
)}
@@ -1,110 +0,0 @@
import React, { useContext, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
FeatureManager,
FeaturesManager,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
import { Snippet } from "~/tools/js/smartComponents";
import { CharacterFeaturesManagerContext } from "../../../Shared/managers/CharacterFeaturesManagerContext";
interface Props {
// onActionUseSet: (action: Action, uses: number) => void;
// onActionClick: (action: Action) => void;
// onSpellClick: (spell: Spell) => void;
// onSpellUseSet: (spell: Spell, uses: number) => void;
// onFeatureClick: (feat: FeatManager) => void;
// snippetData: SnippetData;
// ruleData: RuleData;
// abilityLookup: AbilityLookup;
// dataOriginRefData: DataOriginRefData;
// isReadonly: boolean;
// proficiencyBonus: number;
// theme: CharacterTheme;
}
export const BlessingsDetail: React.FC<Props> = () => {
const { characterFeaturesManager } = useContext(
CharacterFeaturesManagerContext
);
const {
pane: { paneHistoryStart },
} = useSidebar();
const [blessings, setBlessings] = useState<Array<FeatureManager>>([]);
useEffect(() => {
async function onUpdate() {
const blessings = await characterFeaturesManager.getBlessings();
setBlessings(blessings);
}
return FeaturesManager.subscribeToUpdates({ onUpdate });
}, [setBlessings]);
const snippetData = useSelector(rulesEngineSelectors.getSnippetData);
// const ruleData = useSelector(rulesEngineSelectors.getRuleData);
// const abilityLookup = useSelector(rulesEngineSelectors.getAbilityLookup);
// const dataOriginRefData = useSelector(rulesEngineSelectors.getDataOriginRefData);
// const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const proficiencyBonus = useSelector(
rulesEngineSelectors.getProficiencyBonus
);
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
return (
<div className="ct-blessings-detail">
{blessings.length ? (
<React.Fragment>
{blessings.map((blessing) => (
<div
onClick={(evt: React.MouseEvent) => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(
PaneComponentEnum.BLESSING_DETAIL,
PaneIdentifierUtils.generateBlessing(blessing.getId())
);
}}
className={"ct-feature-snippet"}
key={blessing.getId()}
>
<div
className={`ct-feature-snippet__heading ${
theme?.isDarkMode
? "ct-feature-snippet__heading--dark-mode"
: ""
}`}
>
{blessing.getName()}
</div>
<div className="ct-feature-snippet__content">
<Snippet
snippetData={snippetData}
proficiencyBonus={proficiencyBonus}
theme={theme}
>
{blessing.getDescription()}
</Snippet>
</div>
</div>
))}
</React.Fragment>
) : (
<div
className={`ct-blessings-detail__default ${
theme.isDarkMode ? "ct-blessings-detail__default--dark-mode" : ""
}`}
>
<p>There is nothing here.</p>
</div>
)}
</div>
);
};
export default BlessingsDetail;
@@ -1,4 +0,0 @@
import BlessingsDetail from "./BlessingsDetail";
export default BlessingsDetail;
export { BlessingsDetail };
@@ -1,203 +0,0 @@
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardActionArea from "@mui/material/CardActionArea";
import CardActions from "@mui/material/CardActions";
import Typography from "@mui/material/Typography";
import { useState } from "react";
import { useSelector } from "react-redux";
import { MaxCharactersDialog } from "~/components/MaxCharactersDialog";
import { useAuth } from "~/contexts/Authentication";
import { useClaimCharacter } from "~/hooks/useClaimCharacter";
import backgroundImage from "~/images/claim.png";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import {
DESKTOP_COMPONENT_START_WIDTH,
TABLET_COMPONENT_START_WIDTH,
} from "../../config";
import ClaimConfirmationDialog from "../WatchTourDialog/ClaimConfirmationDialog";
const renderButtonContent = (signedIn: boolean) => (
<Box sx={{ display: "flex", justifyContent: "center", marginLeft: "10px" }}>
{!signedIn && (
<Typography
component="span"
sx={{
display: "flex",
alignItems: "center",
marginRight: "5px",
textTransform: "none",
fontWeight: 400,
fontSize: "14px",
lineHeight: "16px",
}}
>
Create Account to
</Typography>
)}
<Typography
component="span"
sx={{
display: "flex",
alignItems: "center",
textTransform: "none",
fontWeight: 700,
fontSize: "14px",
lineHeight: "16px",
}}
>
Claim Character
</Typography>
<ChevronRightIcon />
</Box>
);
// z-index of the sidebar controls
const zIndex = 60002;
const createAccountLink = `/create-account?returnUrl=${window.location.pathname}`;
const ClaimPremadeButton: React.FC = () => {
const params = new URLSearchParams(globalThis.location.search);
const campaignJoinCode = params.get("campaignJoinCode");
const isAssigned = params.get("isAssigned") === "false" ? false : true;
const [isMaxCharacterMessageOpen, setIsMaxCharacterMessageOpen] =
useState(false);
const envDimensions = useSelector(appEnvSelectors.getDimensions);
const { characterSlotLimit, activeCharacterCount } = useSelector(
appEnvSelectors.getCharacterSlots
);
// Character slot limit is null for admin accounts
const hasOpenSlot =
characterSlotLimit === null || activeCharacterCount < characterSlotLimit;
const [
claimCharacter,
isClaimingCharacter,
isFinishedClaimingCharacter,
newCharacterId,
campaignId,
] = useClaimCharacter({
campaignJoinCode,
isAssigned,
});
const user = useAuth();
const signedIn = !!user;
const handleClaimButtonClick = () => {
hasOpenSlot ? claimCharacter() : setIsMaxCharacterMessageOpen(true);
};
return (
<>
{envDimensions.window.width >= TABLET_COMPONENT_START_WIDTH ? (
<Card
className="claimPremade-button"
hidden={isFinishedClaimingCharacter && !!newCharacterId}
sx={{
zIndex,
position: "fixed",
right: 30,
bottom:
envDimensions.window.width >= DESKTOP_COMPONENT_START_WIDTH
? 30
: 70,
width: 300,
background: "#551710",
borderRadius: "8px",
overflow: "visible",
boxShadow:
"0px 57px 23px rgba(0, 0, 0, 0.01), 0px 32px 19px rgba(0, 0, 0, 0.05), 0px 14px 14px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.09), 0px 0px 0px rgba(0, 0, 0, 0.09)",
"&:hover": {
"& .claimPremade-buttonContent": {
background: "#F5F3EE",
boxShadow: "0px -4px 12px rgba(0, 0, 0, 0.25)",
},
},
}}
>
<CardActionArea
sx={{ textAlign: "center" }}
{...(!signedIn && { href: createAccountLink })}
onClick={signedIn ? handleClaimButtonClick : undefined}
disabled={isClaimingCharacter}
>
<Box
component="img"
src={backgroundImage}
sx={{ position: "relative", top: -20, width: 244, height: 144 }}
/>
<CardActions
className="claimPremade-buttonContent"
sx={{
justifyContent: "center",
height: 48,
color: "#000000",
background: "#FFFFFF",
borderRadius: "0 0 8px 8px",
}}
>
{renderButtonContent(signedIn)}
</CardActions>
</CardActionArea>
</Card>
) : (
<Button
variant="contained"
className="claimPremade-button"
{...(!signedIn && { href: createAccountLink })}
sx={{
display:
isFinishedClaimingCharacter && !!newCharacterId
? "none"
: "inline-flex",
zIndex,
position: "fixed",
left: "50%",
bottom: 70,
width: 300,
height: "50px",
marginLeft: "-150px",
color: "#000000",
backgroundColor: "#FFFFFF",
boxShadow:
"0px 57px 23px rgba(0, 0, 0, 0.01), 0px 32px 19px rgba(0, 0, 0, 0.05), 0px 14px 14px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.09), 0px 0px 0px rgba(0, 0, 0, 0.09)",
"&:hover": {
backgroundColor: "#F5F3EE",
boxShadow:
"0px 57px 23px rgba(0, 0, 0, 0.01), 0px 32px 19px rgba(0, 0, 0, 0.05), 0px 14px 14px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.09), 0px 0px 0px rgba(0, 0, 0, 0.09)",
},
"&.Mui-disabled": {
backgroundColor: "#FFFFFF",
},
}}
onClick={signedIn ? handleClaimButtonClick : undefined}
disabled={isClaimingCharacter}
>
{renderButtonContent(signedIn)}
</Button>
)}
<MaxCharactersDialog
open={isMaxCharacterMessageOpen}
onClose={() => setIsMaxCharacterMessageOpen(false)}
useMyCharactersLink
/>
{isFinishedClaimingCharacter && !!newCharacterId && (
<ClaimConfirmationDialog
characterId={newCharacterId}
campaignId={campaignId}
/>
)}
</>
);
};
export default ClaimPremadeButton;
@@ -1,4 +0,0 @@
import ClaimPremadeButton from "./ClaimPremadeButton";
export default ClaimPremadeButton;
export { ClaimPremadeButton as ClaimPremadeDialog };
@@ -16,6 +16,7 @@ interface CarouselWindowItem {
offset: number;
idx: number;
key: string;
itemKey: string;
}
interface Props {
@@ -233,7 +234,7 @@ export default class ComponentCarousel extends React.PureComponent<
return posIndex % total;
} else {
const indexMod = Math.abs(posIndex) % total;
return (indexMod === 0 ? 0 : 1) * (total - indexMod);
return indexMod === 0 ? 0 : total - indexMod;
}
};
@@ -306,6 +307,7 @@ export default class ComponentCarousel extends React.PureComponent<
swipedAmount,
childrenCount,
currentIdx,
childByIdxLookup,
} = state;
let currentPosIdx: number = ComponentCarousel.getSwipedItemPosIdx(
@@ -324,14 +326,21 @@ export default class ComponentCarousel extends React.PureComponent<
i,
childrenCount
);
const child = childByIdxLookup[itemIdx];
const itemKey = child?.props?.itemKey;
// Find existing window item by matching both itemKey AND offset
let existingItem = windowItems.find(
(windowItem) => windowItem.idx === itemIdx
(windowItem) =>
windowItem.itemKey === itemKey && windowItem.offset === offset
);
newWindowItems.push({
offset,
idx: itemIdx,
key: existingItem ? existingItem.key : uniqueId(),
itemKey: itemKey,
// Use itemKey + position index i to ensure uniqueness
key: existingItem ? existingItem.key : `${itemKey}-${i}`,
});
offset++;
@@ -505,7 +514,7 @@ export default class ComponentCarousel extends React.PureComponent<
<div className={classNames.join(" ")} ref={this.placeholderRef}>
<TransitionMotion styles={placeholderStyles}>
{(interpolatedStyles) => (
<React.Fragment>
<>
{interpolatedStyles.map((config) => {
const { PlaceholderComponent, placeholderProps, itemKey } =
config.data.child.props;
@@ -526,12 +535,11 @@ export default class ComponentCarousel extends React.PureComponent<
</div>
);
})}
</React.Fragment>
</>
)}
</TransitionMotion>
</div>
);
//`
};
renderActiveComponent = (): React.ReactNode => {
@@ -27,7 +27,7 @@ export default class ConditionsSummary extends React.PureComponent<Props> {
const { conditions, theme } = this.props;
return (
<React.Fragment>
<>
{conditions.map((condition) => (
<ConditionName
theme={theme}
@@ -35,7 +35,7 @@ export default class ConditionsSummary extends React.PureComponent<Props> {
condition={condition}
/>
))}
</React.Fragment>
</>
);
};
@@ -1,6 +1,6 @@
import React from "react";
import React, { PropsWithChildren } from "react";
interface Props {
interface Props extends PropsWithChildren {
header: React.ReactNode;
extra?: React.ReactNode;
}
@@ -57,14 +57,8 @@ const CoinContent = ({
}
const { pp, gp, ep, sp, cp } = coin;
const shouldReadonly =
isReadonly ||
(containerDefinitionKey ===
coinManager.getPartyEquipmentContainerDefinitionKey() &&
coinManager.isSharingTurnedDeleteOnly());
return (
<React.Fragment>
<>
<div className="ct-currency-pane__currencies">
<CurrencyPaneCurrencyRow
name="Platinum (pp)"
@@ -77,7 +71,7 @@ const CoinContent = ({
value={pp}
coinType={Constants.CoinTypeEnum.pp}
conversion="1 pp = 10 gp"
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Gold (gp)"
@@ -87,7 +81,7 @@ const CoinContent = ({
onError={(errorType) => handleCurrencyChangeError("Gold", errorType)}
value={gp}
coinType={Constants.CoinTypeEnum.gp}
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Electrum (ep)"
@@ -100,7 +94,7 @@ const CoinContent = ({
value={ep}
conversion="1 gp = 2 ep"
coinType={Constants.CoinTypeEnum.ep}
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Silver (sp)"
@@ -113,7 +107,7 @@ const CoinContent = ({
value={sp}
conversion="1 gp = 10 sp"
coinType={Constants.CoinTypeEnum.sp}
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Copper (cp)"
@@ -125,21 +119,16 @@ const CoinContent = ({
}
value={cp}
conversion="1 gp = 100 cp"
isReadonly={shouldReadonly}
isReadonly={isReadonly}
coinType={Constants.CoinTypeEnum.cp}
/>
</div>
{(coinManager.isSharingTurnedOn() ||
!coinManager.isSharedContainerDefinitionKey(
containerDefinitionKey
)) && (
<CurrencyPaneAdjuster
onAdjust={handleCurrencyAdjust}
isReadonly={isReadonly}
containerDefinitionKey={containerDefinitionKey}
/>
)}
</React.Fragment>
<CurrencyPaneAdjuster
onAdjust={handleCurrencyAdjust}
isReadonly={isReadonly}
containerDefinitionKey={containerDefinitionKey}
/>
</>
);
};
@@ -43,7 +43,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
const { theme } = this.props;
let orderedDefenses = orderBy(defenses, (group) => group.name);
return (
<React.Fragment>
<>
{orderedDefenses.map((defense: DefenseAdjustmentGroup, idx) => (
<span
className={`ct-defenses-summary__defense ${
@@ -61,7 +61,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
</Tooltip>
</span>
))}
</React.Fragment>
</>
);
//`
};
@@ -133,7 +133,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
vulnerabilities.length > 0 &&
immunities.length > 0;
return (
<React.Fragment>
<>
{this.renderDamageAdjustmentGroup(
DEFENSE_GROUP.RESISTANCE,
"Resistances",
@@ -155,7 +155,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
isSingleLine,
theme
)}
</React.Fragment>
</>
);
};
@@ -1,195 +0,0 @@
import React from "react";
import { Tooltip } from "@dndbeyond/character-common-components/es";
import {
CharacterCurrencyContract,
Constants,
FormatUtils,
RuleDataUtils,
} from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import CurrencyButton from "../CurrencyButton";
interface Props {
currencies: CharacterCurrencyContract | null;
weight: number;
weightSpeedType: Constants.WeightSpeedTypeEnum;
onCurrencyClick?: (evt: React.MouseEvent | React.KeyboardEvent) => void;
onWeightClick?: () => void;
onCampaignClick?: () => void;
onManageClick?: () => void;
enableManage: boolean;
isReadonly: boolean;
isDarkMode: boolean;
shouldShowPartyInventory: boolean;
campaignName: string | null;
}
export default class EquipmentOverview extends React.PureComponent<Props> {
static defaultProps = {
enableManage: true,
isReadonly: false,
};
handleCurrencyClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onCurrencyClick } = this.props;
if (onCurrencyClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onCurrencyClick(evt);
}
};
handleCampaignClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onCampaignClick } = this.props;
if (onCampaignClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onCampaignClick();
}
};
handleWeightClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onWeightClick } = this.props;
if (onWeightClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onWeightClick();
}
};
handleManageClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onManageClick } = this.props;
if (onManageClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onManageClick();
}
};
render() {
const {
weight,
weightSpeedType,
enableManage,
isReadonly,
isDarkMode,
shouldShowPartyInventory,
campaignName,
currencies,
} = this.props;
let weightSpeedLabel =
RuleDataUtils.getWeightSpeedTypeLabel(weightSpeedType);
return (
// TODO: add a min height?
<div className="ct-equipment-overview">
{!shouldShowPartyInventory ? (
<div
role="button"
className="ct-equipment-overview__weight"
onClick={this.handleWeightClick}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
this.handleWeightClick(evt);
}
}}
>
<div className="ct-equipment-overview__weight-carried">
<span
className={`ct-equipment-overview__weight-carried-label ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
Weight Carried:
</span>
<span
className={`ct-equipment-overview__weight-carried-amount ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
<NumberDisplay
data-testid="weight-carried-number"
type="weightInLb"
number={weight}
/>
</span>
</div>
<div
className={`ct-equipment-overview__weight-speed ct-equipment-overview__weight-speed--${FormatUtils.slugify(
weightSpeedLabel
)}`}
>
{weightSpeedLabel}
</div>
</div>
) : (
<div
role="button"
className="ct-equipment-overview__weight"
onClick={this.handleCampaignClick}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
this.handleCampaignClick(evt);
}
}}
>
<div className="ct-equipment-overview__weight-carried">
<span
className={`ct-equipment-overview__weight-carried-label ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
CAMPAIGN:
<span
className={`ct-equipment-overview__weight-carried-amount ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
{campaignName || "N/A"}
</span>
</span>
</div>
</div>
)}
<div className="ct-equipment-overview__sep" />
<CurrencyButton
handleCurrencyClick={this.handleCurrencyClick}
coin={currencies}
isDarkMode={isDarkMode}
shouldShowPartyInventory={shouldShowPartyInventory}
/>
{enableManage && !isReadonly && (
<div
role="button"
className="ct-equipment-overview__manage"
onClick={this.handleManageClick}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
this.handleManageClick(evt);
}
}}
>
<Tooltip title="Manage Equipment" isDarkMode={isDarkMode}>
<span className="ct-equipment-overview__manage-icon" />
</Tooltip>
</div>
)}
</div>
);
}
}
@@ -1,4 +0,0 @@
import EquipmentOverview from "./EquipmentOverview";
export default EquipmentOverview;
export { EquipmentOverview };
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilter extends React.PureComponent {
export default class ExtrasFilterAdvancedFilter extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter">{this.props.children}</div>
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilterLabel extends React.PureComponent {
export default class ExtrasFilterAdvancedFilterLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilterOption extends React.PureComponent {
export default class ExtrasFilterAdvancedFilterOption extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter-option">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilterOptions extends React.PureComponent {
export default class ExtrasFilterAdvancedFilterOptions extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter-options">
@@ -50,7 +50,7 @@ export const FeatsDetail: React.FC<Props> = ({
return (
<div className="ct-feats-detail">
{currentFeats.length ? (
<React.Fragment>
<>
{currentFeats.map((feat) => (
<FeatFeatureSnippet
key={feat.getId()}
@@ -69,7 +69,7 @@ export const FeatsDetail: React.FC<Props> = ({
theme={theme}
/>
))}
</React.Fragment>
</>
) : (
<div
className={`ct-feats-detail__default ${
@@ -73,6 +73,7 @@ export default class FeatureSnippetAction extends React.PureComponent<Props> {
case Constants.ActivationTypeEnum.ACTION:
case Constants.ActivationTypeEnum.REACTION:
case Constants.ActivationTypeEnum.BONUS_ACTION:
case Constants.ActivationTypeEnum.SPECIAL:
activationDisplay = ActivationUtils.renderActivation(
activation,
ruleData
@@ -5,6 +5,7 @@ import {
BaseFeat,
CharacterTheme,
Choice,
ChoiceUtils,
ClassDefinitionContract,
Constants,
DataOriginBaseAction,
@@ -112,7 +113,11 @@ export default class FeatureSnippetChoices extends React.PureComponent<Props> {
abilityLookup={abilityLookup}
dataOriginRefData={dataOriginRefData}
sourceDataLookup={sourceDataLookup}
showDescription={showDescription}
showDescription={
ChoiceUtils.getItemDefinitionKey(choice)
? false
: showDescription
}
isInteractive={isInteractive}
proficiencyBonus={proficiencyBonus}
theme={theme}
@@ -53,11 +53,7 @@ export default class FeatureSnippetInfusionChoices extends React.PureComponent<P
const simulatedInfusion =
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
if (simulatedInfusion !== null) {
nameNode = (
<React.Fragment>
{InfusionUtils.getName(simulatedInfusion)}
</React.Fragment>
);
nameNode = <>{InfusionUtils.getName(simulatedInfusion)}</>;
onClick = this.handleInfusionChoiceClick.bind(
this,
infusionChoice
@@ -161,7 +161,7 @@ export default class FeatureSnippetLimitedUse extends React.PureComponent<Props>
}
return (
<React.Fragment>
<>
<div
className={`ct-feature-snippet__limited-use ${
theme.isDarkMode ? "ct-feature-snippet__limited-use--dark-mode" : ""
@@ -180,7 +180,7 @@ export default class FeatureSnippetLimitedUse extends React.PureComponent<Props>
{extraNode}
</div>
)}
</React.Fragment>
</>
);
}
}
@@ -160,7 +160,7 @@ export class Infusions extends React.PureComponent<Props, State> {
const { infusionChoices, ruleData, theme } = this.props;
return (
<React.Fragment>
<>
{infusionChoices.map((infusionChoice) => {
const knownInfusion =
InfusionChoiceUtils.getKnownInfusion(infusionChoice);
@@ -279,7 +279,7 @@ export class Infusions extends React.PureComponent<Props, State> {
</div>
);
})}
</React.Fragment>
</>
);
};
@@ -1,4 +1,5 @@
import { FC } from "react";
import { useSearchParams } from "react-router-dom";
import { BuilderLinkButton } from "../../../Shared/components/common/LinkButton";
@@ -11,6 +12,10 @@ export const InvalidCharacter: FC<InvalidCharacterProps> = ({
builderUrl,
isReadonly,
}) => {
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const sendToNonVttBuilderUrl = builderUrl.replace("view=vtt", "");
return (
<div className="ct-invalid-character">
<h2>Character Not Ready!</h2>
@@ -30,9 +35,10 @@ export const InvalidCharacter: FC<InvalidCharacterProps> = ({
<p>
<BuilderLinkButton
url={builderUrl}
url={isVttView ? sendToNonVttBuilderUrl : builderUrl}
className="ct-invalid-character__button"
size="oversized"
target={isVttView ? "_blank" : undefined}
>
Character Builder
</BuilderLinkButton>
@@ -287,9 +287,11 @@ export default class InventoryFilter extends React.PureComponent<Props, State> {
handleFiltersClear = (): void => {
const inventory = this.getInventory(this.props);
const partyInventory = this.getPartyInventory(this.props);
this.setState(
{
filteredInventory: inventory,
filteredPartyInventory: partyInventory,
filterQuery: "",
filterTypes: [],
filterRarities: [],
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilter extends React.PureComponent {
export default class InventoryFilterAdvancedFilter extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilterLabel extends React.PureComponent {
export default class InventoryFilterAdvancedFilterLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilterOption extends React.PureComponent {
export default class InventoryFilterAdvancedFilterOption extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter-option">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilterOptions extends React.PureComponent {
export default class InventoryFilterAdvancedFilterOptions extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter-options">
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -8,6 +7,7 @@ import {
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import a11yStyles from "~/styles/accessibility.module.css";
interface Props {
proficiencyBonus: number;
@@ -31,7 +31,7 @@ export default class ProficiencyBonusBox extends React.PureComponent<Props> {
return (
<section className="ct-proficiency-bonus-box" onClick={this.handleClick}>
<BoxBackground StyleComponent={BeveledBoxSvg94x89} theme={theme} />
<h2 style={visuallyHidden}>Proficiency Bonus</h2>
<h2 className={a11yStyles.screenreaderOnly}>Proficiency Bonus</h2>
<div
className={`ct-proficiency-bonus-box__heading ${
theme.isDarkMode
@@ -31,7 +31,7 @@ export default class ProficiencyGroups extends React.PureComponent<Props> {
let itemsNode: React.ReactNode = "None";
if (group.modifierGroups.length) {
itemsNode = (
<React.Fragment>
<>
{group.modifierGroups.map((modifierGroup, idx) => {
let title: string = modifierGroup.sources.join(", ");
return (
@@ -59,7 +59,7 @@ export default class ProficiencyGroups extends React.PureComponent<Props> {
</Tooltip>
);
})}
</React.Fragment>
</>
);
}
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -11,6 +10,8 @@ import {
ProficiencyGroup,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
import ProficiencyGroups from "../ProficiencyGroups";
@@ -36,7 +37,9 @@ export default class ProficiencyGroupsBox extends React.PureComponent<Props> {
return (
<section className="ct-proficiency-groups-box">
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Proficiencies and Languages</h2>
<h2 className={a11yStyles.screenreaderOnly}>
Proficiencies and Languages
</h2>
<ProficiencyGroups
proficiencyGroups={proficiencyGroups}
onClick={onClick}
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import { orderBy } from "lodash";
import { Tooltip } from "@dndbeyond/character-common-components/es";
@@ -18,7 +17,9 @@ import {
RuleData,
SituationalSavingThrowInfoLookup,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import a11yStyles from "~/styles/accessibility.module.css";
import DiceAdjustmentSummary from "../../../Shared/components/DiceAdjustmentSummary";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
@@ -37,7 +38,7 @@ interface Props {
dimensions: AppEnvDimensionsState;
theme: CharacterTheme;
diceEnabled?: boolean;
rollContext: IRollContext;
rollContext: RollContext;
}
const sortDiceAdjustments = (
@@ -276,7 +277,7 @@ export default function SavingThrowsBox({
return (
<section className="ct-saving-throws-box" onClick={handleClick}>
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Saving Throws</h2>
<h2 className={a11yStyles.screenreaderOnly}>Saving Throws</h2>
<div className="ct-saving-throws-box__abilities">
<SavingThrowsSummary
abilities={abilities}
@@ -1,9 +1,11 @@
import clsx from "clsx";
import React from "react";
import {
BoxBackground,
ThemedSenseRowBoxSvg,
ThemedSenseRowSmallBoxSvg,
ThemedSenseRowMinimalSvg,
} from "@dndbeyond/character-components/es";
import {
CharacterTheme,
@@ -14,13 +16,14 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { TypeScriptUtils } from "../../../Shared/utils";
import styles from "./styles.module.css";
interface Props {
passivePerception: number;
passiveInvestigation: number;
passiveInsight: number;
senses: SenseInfo;
rowStyle: "small" | "normal";
rowStyle: "small" | "normal" | "minimal";
onClick?: () => void;
theme: CharacterTheme;
}
@@ -53,8 +56,6 @@ export default class Senses extends React.PureComponent<Props> {
};
renderSummaryInfo = (): React.ReactNode => {
const { theme } = this.props;
let senseKeys: Array<Constants.SenseTypeEnum> = [
Constants.SenseTypeEnum.BLINDSIGHT,
Constants.SenseTypeEnum.DARKVISION,
@@ -66,24 +67,17 @@ export default class Senses extends React.PureComponent<Props> {
.map((senseKey) => this.getSenseSummary(senseKey))
.filter(TypeScriptUtils.isNotNullOrUndefined);
let summaryClasses: Array<string> = ["ct-senses__summary"];
if (!senseSummaries.length) {
summaryClasses.push("ct-senses__summary--empty");
}
if (theme?.isDarkMode) {
summaryClasses.push("ct-senses__summary--dark-mode");
}
if (senseSummaries.length) {
return (
<div className={summaryClasses.join(" ")}>
{senseSummaries.join(", ")}
</div>
);
}
return (
<div className={summaryClasses.join(" ")}>Additional Sense Types</div>
<div
className={clsx([
styles.summary,
!senseSummaries.length && styles.summaryEmpty,
])}
>
{!senseSummaries.length
? "Additional Sense Types"
: senseSummaries.join(", ")}
</div>
);
};
@@ -96,84 +90,140 @@ export default class Senses extends React.PureComponent<Props> {
theme,
} = this.props;
let isMinimal = false;
let StyleComponent: React.ComponentType<any> = ThemedSenseRowBoxSvg;
if (rowStyle === "small") {
StyleComponent = ThemedSenseRowSmallBoxSvg;
} else if (rowStyle === "minimal") {
StyleComponent = ThemedSenseRowMinimalSvg;
isMinimal = true;
}
return (
<div className="ct-senses" onClick={this.handleClick}>
<div className="ct-senses__callouts">
<div className="ct-senses__callout">
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div
className={`ct-senses__callout-value ${
theme?.isDarkMode ? "ct-senses__callout-value--dark-mode" : ""
}`}
>
{passivePerception}
<div className={styles.senses} onClick={this.handleClick}>
{isMinimal ? (
<div className={styles.minimalRow}>
<div className={styles.minimalItem}>
<div className={styles.minimalCallout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passivePerception}</div>
</div>
<div
className={clsx([
styles.calloutLabel,
styles.minimalCalloutLabel,
])}
>
Perception
</div>
</div>
<div
className={`ct-senses__callout-label ${
theme?.isDarkMode ? "ct-senses__callout-label--dark-mode" : ""
}`}
>
Passive Perception
<div className={styles.minimalItem}>
<div className={styles.minimalCallout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>
{passiveInvestigation}
</div>
</div>
<div
className={clsx([
styles.calloutLabel,
styles.minimalCalloutLabel,
])}
>
Investigation
</div>
</div>
<div className={styles.minimalItem}>
<div className={styles.minimalCallout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passiveInsight}</div>
</div>
<div
className={clsx([
styles.calloutLabel,
styles.minimalCalloutLabel,
])}
>
Insight
</div>
</div>
</div>
<div className="ct-senses__callout">
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div
className={`ct-senses__callout-value ${
theme?.isDarkMode ? "ct-senses__callout-value--dark-mode" : ""
}`}
>
{passiveInvestigation}
) : (
<div>
<div className={styles.callout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passivePerception}</div>
<div
className={clsx([
styles.calloutLabel,
styles.regularCalloutLabel,
])}
>
Passive Perception
</div>
</div>
<div
className={`ct-senses__callout-label ${
theme?.isDarkMode ? "ct-senses__callout-label--dark-mode" : ""
}`}
>
Passive Investigation
<div className={styles.callout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passiveInvestigation}</div>
<div
className={clsx([
styles.calloutLabel,
styles.regularCalloutLabel,
])}
>
Passive Investigation
</div>
</div>
<div className={styles.callout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passiveInsight}</div>
<div
className={clsx([
styles.calloutLabel,
styles.regularCalloutLabel,
])}
>
Passive Insight
</div>
</div>
</div>
<div className="ct-senses__callout">
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div
className={`ct-senses__callout-value ${
theme?.isDarkMode ? "ct-senses__callout-value--dark-mode" : ""
}`}
>
{passiveInsight}
</div>
<div
className={`ct-senses__callout-label ${
theme?.isDarkMode ? "ct-senses__callout-label--dark-mode" : ""
}`}
>
Passive Insight
</div>
</div>
</div>
)}
{this.renderSummaryInfo()}
</div>
);
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -11,6 +10,8 @@ import {
SenseInfo,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
import Senses from "../Senses";
@@ -49,7 +50,7 @@ export default class SensesBox extends React.PureComponent<Props> {
return (
<section className="ct-senses-box">
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Senses</h2>
<h2 className={a11yStyles.screenreaderOnly}>Senses</h2>
<Senses
senses={senses}
passiveInsight={passiveInsight}
@@ -1,31 +0,0 @@
import SettingsIcon from "@mui/icons-material/Settings";
import Button from "@mui/material/Button";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
interface Props {
context: string;
isReadonly: boolean;
}
const SettingsButton: React.FC<Props> = ({ context, isReadonly }) => {
const {
pane: { paneHistoryPush },
} = useSidebar();
return !isReadonly ? (
<Button
onClick={(evt) => {
paneHistoryPush(PaneComponentEnum.SETTINGS, {
context,
});
}}
variant="text"
startIcon={<SettingsIcon />}
>
Settings
</Button>
) : null;
};
export default SettingsButton;
@@ -1,4 +0,0 @@
import SettingsButton from "./SettingsButton";
export default SettingsButton;
export { SettingsButton };
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import { orderBy } from "lodash";
import React from "react";
@@ -19,9 +18,12 @@ import {
ValueLookup,
ValueUtils,
} from "@dndbeyond/character-rules-engine/es";
import { RollType, DiceTools, IRollContext } from "@dndbeyond/dice";
import { GameLogContext } from "@dndbeyond/game-log-components";
import { RollTypes } from "@dndbeyond/pocket-dimension-dice/constants";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { NumberDisplay } from "~/components/NumberDisplay";
import a11yStyles from "~/styles/accessibility.module.css";
interface Props {
skills: Array<Skill>;
@@ -33,7 +35,7 @@ interface Props {
diceEnabled: boolean;
theme: CharacterTheme;
ruleData: RuleData;
rollContext: IRollContext;
rollContext: RollContext;
}
class Skills extends React.PureComponent<Props> {
static defaultProps = {
@@ -87,9 +89,6 @@ class Skills extends React.PureComponent<Props> {
const { customSkills, ruleData, diceEnabled, theme, rollContext } =
this.props;
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
this.context;
return customSkills.map((skill) => {
const modifier = SkillUtils.getModifier(skill);
const statName = RuleDataUtils.getAbilityShortName(
@@ -150,26 +149,17 @@ class Skills extends React.PureComponent<Props> {
"--"
) : (
<DigitalDiceWrapper
diceNotation={DiceTools.CustomD20(
SkillUtils.getModifier(skill)!
)}
rollType={RollType.Check}
rollAction={name ?? "UNKNOWN"}
diceEnabled={diceEnabled}
themeColor={theme.themeColor}
rollContext={rollContext}
rollTargetOptions={
messageTargetOptions
? Object.values(messageTargetOptions?.entities)
: undefined
}
rollTargetDefault={defaultMessageTargetOption}
userId={userId}
diceNotation={`1d20${
modifier > 0 ? `+${modifier}` : modifier !== 0 ? modifier : ""
}`}
rollType={RollTypes.Check}
action={name ?? "UNKNOWN"}
isDiceEnabled={diceEnabled}
entityId={String(rollContext.entityId)}
entityType={String(rollContext.entityType)}
name={String(rollContext.name)}
>
<NumberDisplay
number={modifier}
type="signed"
/>
<NumberDisplay number={modifier} type="signed" />
</DigitalDiceWrapper>
)}
</div>
@@ -189,9 +179,6 @@ class Skills extends React.PureComponent<Props> {
rollContext,
} = this.props;
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
this.context;
let orderedSkills = orderBy(skills, (skill) => SkillUtils.getName(skill));
return (
@@ -203,7 +190,7 @@ class Skills extends React.PureComponent<Props> {
>
<div className="ct-skills__header" role="row">
<div className="ct-skills__col--proficiency" role="columnheader">
<span style={visuallyHidden}>Proficiency</span>
<span className={a11yStyles.screenreaderOnly}>Proficiency</span>
<abbr
aria-hidden="true"
title="Proficiency"
@@ -215,7 +202,7 @@ class Skills extends React.PureComponent<Props> {
</abbr>
</div>
<div className="ct-skills__col--stat" role="columnheader">
<span style={visuallyHidden}>Modifier</span>
<span className={a11yStyles.screenreaderOnly}>Modifier</span>
<abbr
aria-hidden="true"
title="Modifier"
@@ -245,7 +232,11 @@ class Skills extends React.PureComponent<Props> {
</span>
</div>
</div>
<div className="ct-skills__list" role="rowgroup">
<div
className="ct-skills__list"
role="rowgroup"
data-scrollable-container="true"
>
{orderedSkills.map((skill) => {
let valueTypes: Array<Constants.AdjustmentTypeEnum> = [
Constants.AdjustmentTypeEnum.SKILL_STAT_OVERRIDE,
@@ -402,27 +393,25 @@ class Skills extends React.PureComponent<Props> {
"--"
) : (
<DigitalDiceWrapper
diceNotation={DiceTools.CustomD20(
SkillUtils.getModifier(skill)!
)}
rollType={RollType.Check}
rollAction={name ? name : "UNKNOWN"}
diceEnabled={diceEnabled}
themeColor={theme.themeColor}
rollContext={rollContext}
rollTargetOptions={
messageTargetOptions
? Object.values(messageTargetOptions?.entities)
: undefined
}
rollTargetDefault={defaultMessageTargetOption}
userId={userId}
diceNotation={`1d20${
modifier > 0
? `+${modifier}`
: modifier !== 0
? modifier
: ""
}`}
rollType={RollTypes.Check}
action={name ? name : "UNKNOWN"}
isDiceEnabled={diceEnabled}
entityId={String(rollContext.entityId)}
entityType={String(rollContext.entityType)}
name={String(rollContext.name)}
>
<NumberDisplay
number={modifier}
type="signed"
isModified={hasModifiedValue}
/>
/>
</DigitalDiceWrapper>
)}
</div>
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -13,7 +12,9 @@ import {
Skill,
ValueLookup,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import a11yStyles from "~/styles/accessibility.module.css";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
@@ -30,7 +31,7 @@ interface Props {
theme: CharacterTheme;
diceEnabled: boolean;
ruleData: RuleData;
rollContext: IRollContext;
rollContext: RollContext;
}
export default class SkillsBox extends React.PureComponent<Props> {
static defaultProps = {
@@ -63,7 +64,7 @@ export default class SkillsBox extends React.PureComponent<Props> {
return (
<section className="ct-skills-box">
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Skills</h2>
<h2 className={a11yStyles.screenreaderOnly}>Skills</h2>
<Skills
skills={skills}
customSkills={customSkills}
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -14,6 +13,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import a11yStyles from "~/styles/accessibility.module.css";
interface Props {
speeds: SpeedInfo;
@@ -47,7 +47,7 @@ export default class SpeedBox extends React.PureComponent<Props> {
return (
<section className="ct-speed-box" onClick={this.handleSpeedsClick}>
<h2 style={visuallyHidden}>Speed</h2>
<h2 className={a11yStyles.screenreaderOnly}>Speed</h2>
<BoxBackground StyleComponent={BeveledBoxSvg94x89} theme={theme} />
<div
className={`ct-speed-box__heading ${
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilter extends React.PureComponent {
export default class SpellsFilterAdvancedFilter extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter">{this.props.children}</div>
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilterLabel extends React.PureComponent {
export default class SpellsFilterAdvancedFilterLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilterOption extends React.PureComponent {
export default class SpellsFilterAdvancedFilterOption extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter-option">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilterOptions extends React.PureComponent {
export default class SpellsFilterAdvancedFilterOptions extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter-options">
@@ -1,12 +1,12 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
interface Props {
interface Props extends PropsWithChildren {
name?: string;
className: string;
}
export default class SubsectionMobile extends React.PureComponent<Props> {
export default class SubsectionMobile extends PureComponent<Props> {
static defaultProps = {
className: "",
};
@@ -1,12 +1,12 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
interface Props {
interface Props extends PropsWithChildren {
name?: string;
className: string;
}
export default class SubsectionTablet extends React.PureComponent<Props> {
export default class SubsectionTablet extends PureComponent<Props> {
static defaultProps = {
className: "",
};

Some files were not shown because too many files have changed in this diff Show More