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