689 lines
20 KiB
TypeScript
689 lines
20 KiB
TypeScript
import clsx from "clsx";
|
||
import { throttle } from "lodash";
|
||
import { createRef, PureComponent } from "react";
|
||
import { connect, DispatchProp } from "react-redux";
|
||
import { useSearchParams } from "react-router-dom";
|
||
import { AnyAction } from "redux";
|
||
|
||
import {
|
||
characterEnvSelectors,
|
||
Constants,
|
||
FormatUtils,
|
||
rulesEngineSelectors,
|
||
CharacterTheme,
|
||
CharacterPreferences,
|
||
characterSelectors,
|
||
CharacterStatusSlug,
|
||
CampaignDataContract,
|
||
} from "@dndbeyond/character-rules-engine/es";
|
||
|
||
import { Link } from "~/components/Link";
|
||
import { NotificationSystem } from "~/components/NotificationSystem";
|
||
import { PremadeCharacterEditStatus } from "~/components/PremadeCharacterEditStatus";
|
||
import { usePositioning } from "~/hooks/usePositioning";
|
||
import { CharacterSheetGuidedTour } from "~/subApps/sheet/components/CharacterSheetGuidedTour";
|
||
import { ClaimPremadeButton } from "~/subApps/sheet/components/ClaimPremadeButton";
|
||
import { MobileNav } from "~/subApps/sheet/components/MobileNav";
|
||
|
||
import { useHeadContext } from "../../../../../contexts/Head";
|
||
import { ThemeManagerProvider } from "../../../../../contexts/ThemeManager";
|
||
import { getCharacterSlots } from "../../../../../helpers/characterServiceApi";
|
||
import SynchronousBlocker from "../../../CharacterBuilder/containers/SynchronousBlocker";
|
||
import { appEnvActions, toastMessageActions } from "../../../Shared/actions";
|
||
import LoadingBlocker from "../../../Shared/components/LoadingBlocker";
|
||
import AppErrorTypeEnum from "../../../Shared/constants/AppErrorTypeEnum";
|
||
import UserRoles from "../../../Shared/constants/UserRoles";
|
||
import { DiceContainer } from "../../../Shared/containers/DiceContainer";
|
||
import {
|
||
appEnvSelectors,
|
||
appInfoSelectors,
|
||
toastMessageSelectors,
|
||
} from "../../../Shared/selectors";
|
||
import {
|
||
AppEnvDimensionsState,
|
||
AppInfoErrorState,
|
||
DiceFeatureConfigurationState,
|
||
GameLogState,
|
||
ToastMessageState,
|
||
} from "../../../Shared/stores/typings";
|
||
import config from "../../../config";
|
||
import { sheetActions } from "../../actions";
|
||
import { InvalidCharacter } from "../../components/InvalidCharacter";
|
||
import {
|
||
DESKTOP_COMPONENT_START_WIDTH,
|
||
MQ_IS_MOBILE_SIZE,
|
||
TABLET_COMPONENT_START_WIDTH,
|
||
} from "../../config";
|
||
import { sheetAppSelectors, sheetSelectors } from "../../selectors";
|
||
import { SheetAppState, SheetPositioningInfo } from "../../typings";
|
||
import CharacterSheetDesktop from "../CharacterSheetDesktop";
|
||
import CharacterSheetMobile from "../CharacterSheetMobile";
|
||
import CharacterSheetTablet from "../CharacterSheetTablet";
|
||
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> {
|
||
sheetPosition: SheetPositioningInfo;
|
||
initFailed: boolean;
|
||
loadingStatus: Constants.CharacterLoadingStatusEnum;
|
||
toastMessages: ToastMessageState;
|
||
builderUrl: string;
|
||
isMobile: boolean;
|
||
envDimensions: AppEnvDimensionsState;
|
||
isReadonly: boolean;
|
||
canEdit: boolean;
|
||
nearTopLimit: number;
|
||
appError: AppInfoErrorState | null;
|
||
isCharacterSheetReady: boolean;
|
||
diceEnabled: boolean;
|
||
characterId: number | null;
|
||
authEndpoint: string;
|
||
diceFeatureConfiguration: DiceFeatureConfigurationState;
|
||
gameLog: GameLogState;
|
||
theme: CharacterTheme;
|
||
username: string;
|
||
characterName: string | null;
|
||
preferences: CharacterPreferences;
|
||
setManager: (manager: any) => void;
|
||
setTitle: (title: string) => void;
|
||
isVttView: boolean;
|
||
characterStatus: CharacterStatusSlug | null;
|
||
userRoles: UserRoles;
|
||
userId: number;
|
||
campaign: CampaignDataContract | null;
|
||
}
|
||
interface State {
|
||
curseHeaderHeight: number;
|
||
scrollTop: number;
|
||
isNotificationOpen: boolean;
|
||
isLoaded: boolean;
|
||
isSlotLimitsLoaded: boolean;
|
||
}
|
||
|
||
class CharacterSheet extends PureComponent<Props, State> {
|
||
static defaultProps = {
|
||
nearTopLimit: 40,
|
||
characterJson: null,
|
||
ruleDataJson: null,
|
||
diceEnabled: false,
|
||
};
|
||
|
||
// useImperativeHandle is hard to type when we us our own context this will just disapear
|
||
notificationSystem = createRef<any>();
|
||
sheetRef = createRef<HTMLDivElement>();
|
||
mobileMql: MediaQueryList | null = null;
|
||
|
||
constructor(props) {
|
||
super(props);
|
||
|
||
this.state = {
|
||
curseHeaderHeight: 0,
|
||
scrollTop: 0,
|
||
isNotificationOpen: false,
|
||
isLoaded: false,
|
||
isSlotLimitsLoaded: false,
|
||
};
|
||
}
|
||
|
||
componentDidMount() {
|
||
const { dispatch } = this.props;
|
||
const { isLoaded } = this.state;
|
||
if (!isLoaded) {
|
||
dispatch(sheetActions.characterInit());
|
||
this.setState({ isLoaded: true });
|
||
}
|
||
this.initScrollWatchers();
|
||
|
||
this.mobileMql = window.matchMedia(MQ_IS_MOBILE_SIZE);
|
||
dispatch(appEnvActions.mobileSet(this.mobileMql.matches));
|
||
this.mobileMql.onchange = (e) => {
|
||
dispatch(appEnvActions.mobileSet(e.matches));
|
||
};
|
||
|
||
this.initResizeWatchers();
|
||
}
|
||
|
||
componentDidUpdate(prevProps, prevState) {
|
||
const {
|
||
dispatch,
|
||
loadingStatus,
|
||
canEdit,
|
||
diceEnabled,
|
||
characterName,
|
||
setTitle,
|
||
characterStatus,
|
||
username,
|
||
userRoles,
|
||
userId,
|
||
campaign,
|
||
} = this.props;
|
||
|
||
if (loadingStatus === Constants.CharacterLoadingStatusEnum.LOADED) {
|
||
if (characterStatus === CharacterStatusSlug.PREMADE) {
|
||
dispatch(
|
||
appEnvActions.dataSet({
|
||
canOverrideReadOnly:
|
||
userRoles.includes(UserRoles.ADMIN) ||
|
||
userRoles.includes(UserRoles.DEV) ||
|
||
userRoles.includes(UserRoles.LOREKEEPER),
|
||
})
|
||
);
|
||
} else {
|
||
dispatch(
|
||
appEnvActions.dataSet({
|
||
isReadonly: !canEdit,
|
||
diceEnabled: canEdit ? diceEnabled : false,
|
||
isUserDM: Number(userId) === campaign?.dmUserId,
|
||
})
|
||
);
|
||
}
|
||
|
||
if (characterName) {
|
||
setTitle(`${characterName}'s Character Sheet`);
|
||
}
|
||
|
||
if (
|
||
username &&
|
||
characterStatus === CharacterStatusSlug.PREMADE &&
|
||
!this.state.isSlotLimitsLoaded
|
||
) {
|
||
this.setState({ isSlotLimitsLoaded: true });
|
||
getCharacterSlots().then((characterSlots) => {
|
||
dispatch(
|
||
appEnvActions.dataSet({
|
||
characterSlots,
|
||
})
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
this.checkForMessages();
|
||
}
|
||
|
||
updateSheetScrollState = (): void => {
|
||
const curScrollTop = window.scrollY;
|
||
const { scrollTop } = this.state;
|
||
const { nearTopLimit } = this.props;
|
||
|
||
if (curScrollTop === 0) {
|
||
document.body.classList.add("ct-character-sheet--top");
|
||
document.body.classList.remove("ct-character-sheet--not-top");
|
||
} else {
|
||
document.body.classList.add("ct-character-sheet--not-top");
|
||
document.body.classList.remove("ct-character-sheet--top");
|
||
}
|
||
|
||
if (curScrollTop < nearTopLimit) {
|
||
document.body.classList.add("ct-character-sheet--near-top");
|
||
document.body.classList.remove("ct-character-sheet--far-top");
|
||
} else {
|
||
document.body.classList.add("ct-character-sheet--far-top");
|
||
document.body.classList.remove("ct-character-sheet--near-top");
|
||
}
|
||
|
||
if (curScrollTop < scrollTop) {
|
||
// Going up
|
||
document.body.classList.add("ct-character-sheet--going-up");
|
||
document.body.classList.remove("ct-character-sheet--going-down");
|
||
} else {
|
||
// Going down
|
||
document.body.classList.add("ct-character-sheet--going-down");
|
||
document.body.classList.remove("ct-character-sheet--going-up");
|
||
}
|
||
|
||
this.setState({
|
||
scrollTop: curScrollTop,
|
||
});
|
||
};
|
||
|
||
// TODO: we should setup a remove listener for this
|
||
initScrollWatchers = (): void => {
|
||
// Assuming there should be only 1
|
||
const curseHeader = document.querySelector<HTMLElement>("header.main");
|
||
const curseHeaderHeight: number = curseHeader
|
||
? curseHeader.offsetHeight
|
||
: 0;
|
||
|
||
this.setState({ curseHeaderHeight: curseHeaderHeight });
|
||
this.updateSheetScrollState();
|
||
|
||
const SCROLL_THROTTLE_AMOUNT = 70;
|
||
document.addEventListener(
|
||
"scroll",
|
||
throttle(() => {
|
||
this.updateSheetScrollState();
|
||
}, SCROLL_THROTTLE_AMOUNT)
|
||
);
|
||
};
|
||
|
||
updateEnvDimensions = (): void => {
|
||
const { dispatch } = this.props;
|
||
|
||
if (this.sheetRef.current === null) {
|
||
return;
|
||
}
|
||
|
||
dispatch(
|
||
appEnvActions.dimensionsSet({
|
||
window: {
|
||
width: window.innerWidth,
|
||
height: window.innerHeight,
|
||
},
|
||
sheet: {
|
||
width: this.sheetRef.current.clientWidth,
|
||
height: this.sheetRef.current.clientHeight,
|
||
},
|
||
})
|
||
);
|
||
};
|
||
|
||
initResizeWatchers = (): void => {
|
||
const { envDimensions } = this.props;
|
||
|
||
if (this.sheetRef.current === null) {
|
||
return;
|
||
}
|
||
|
||
this.updateEnvDimensions();
|
||
|
||
const RESIZE_THROTTLE_AMOUNT: number = 300;
|
||
window.addEventListener(
|
||
"resize",
|
||
throttle(() => {
|
||
this.updateEnvDimensions();
|
||
}, RESIZE_THROTTLE_AMOUNT)
|
||
);
|
||
|
||
window.addEventListener(
|
||
"orientationchange",
|
||
throttle(() => {
|
||
let oldSize: number = envDimensions.window.width;
|
||
let count: number = 0;
|
||
let orientationIntervalId = window.setInterval(() => {
|
||
if (oldSize !== envDimensions.window.width || count > 15) {
|
||
window.clearInterval(orientationIntervalId);
|
||
return;
|
||
}
|
||
if (oldSize !== window.innerWidth) {
|
||
this.updateEnvDimensions();
|
||
}
|
||
count++;
|
||
}, 100);
|
||
}, RESIZE_THROTTLE_AMOUNT)
|
||
);
|
||
};
|
||
|
||
checkForMessages = (): void => {
|
||
if (this.notificationSystem.current === null) {
|
||
return;
|
||
}
|
||
|
||
const { toastMessages, dispatch } = this.props;
|
||
|
||
Object.keys(toastMessages).forEach((messageId) => {
|
||
const message = toastMessages[messageId];
|
||
const { componentProps, Component, ...metaProps } = message.meta;
|
||
|
||
let children: React.ReactNode;
|
||
if (Component) {
|
||
let childrenProps = componentProps || {};
|
||
children = <Component {...childrenProps} />;
|
||
}
|
||
|
||
if (this.notificationSystem.current !== null) {
|
||
this.notificationSystem.current.addNotification({
|
||
title: message.payload.title,
|
||
message: message.payload.message,
|
||
uid: messageId,
|
||
children,
|
||
onRemove: () => dispatch(toastMessageActions.removeToast(messageId)),
|
||
...metaProps,
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
isSheetErrored = (): boolean => {
|
||
const { initFailed, appError } = this.props;
|
||
|
||
return initFailed || appError !== null;
|
||
};
|
||
|
||
renderCharacterSheet = (): React.ReactNode => {
|
||
const { envDimensions, isCharacterSheetReady, builderUrl, isReadonly } =
|
||
this.props;
|
||
|
||
if (!isCharacterSheetReady) {
|
||
return (
|
||
<InvalidCharacter builderUrl={builderUrl} isReadonly={isReadonly} />
|
||
);
|
||
}
|
||
|
||
if (envDimensions.window.width >= DESKTOP_COMPONENT_START_WIDTH) {
|
||
return <CharacterSheetDesktop />;
|
||
} else if (envDimensions.window.width >= TABLET_COMPONENT_START_WIDTH) {
|
||
return <CharacterSheetTablet />;
|
||
}
|
||
|
||
return <CharacterSheetMobile />;
|
||
};
|
||
|
||
renderSyncBlocker = (): React.ReactNode => {
|
||
const { initFailed, appError } = this.props;
|
||
|
||
if (appError !== null) {
|
||
return null;
|
||
}
|
||
|
||
if (initFailed) {
|
||
return null;
|
||
}
|
||
|
||
return <SynchronousBlocker />;
|
||
};
|
||
|
||
renderInitFailed = (): React.ReactNode => {
|
||
return (
|
||
<div className="ct-character-sheet__failed">
|
||
<div className="ct-character-sheet__failed-content">
|
||
<p>
|
||
Whoops! We rolled a 1 on our Retrieve Character check. We're heading
|
||
into town to visit the blacksmith for repairs.
|
||
</p>
|
||
<p>Try again after a Short Rest.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
renderAppError = (): React.ReactNode => {
|
||
const { appError } = this.props;
|
||
|
||
if (appError === null) {
|
||
return null;
|
||
}
|
||
|
||
let contentNode: React.ReactNode;
|
||
switch (appError.type) {
|
||
case AppErrorTypeEnum.AUTH_MISSING:
|
||
let signInUrl = `/sign-in?returnUrl=${encodeURIComponent(
|
||
window.location.pathname
|
||
)}`;
|
||
contentNode = (
|
||
<>
|
||
<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>
|
||
</>
|
||
);
|
||
break;
|
||
|
||
case AppErrorTypeEnum.API_FAIL:
|
||
contentNode = (
|
||
<>
|
||
<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>
|
||
</>
|
||
);
|
||
break;
|
||
|
||
case AppErrorTypeEnum.API_DATA_FAIL:
|
||
contentNode = (
|
||
<>
|
||
<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>
|
||
</>
|
||
);
|
||
break;
|
||
|
||
case AppErrorTypeEnum.AUTH_FAIL:
|
||
contentNode = (
|
||
<>
|
||
<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>
|
||
</>
|
||
);
|
||
break;
|
||
|
||
case AppErrorTypeEnum.ACCESS_DENIED:
|
||
contentNode = (
|
||
<>
|
||
<p style={{ margin: 0 }}>You reach for the sheet,</p>
|
||
<p style={{ margin: 0 }}>But the sigil reads "Private"—</p>
|
||
<p style={{ margin: 0 }}>Permission denied.</p>
|
||
<p style={{ marginTop: 15 }}>
|
||
Ask the owner to set their Character Sheet to Campaign Only or
|
||
Public in their character's settings.
|
||
</p>
|
||
</>
|
||
);
|
||
break;
|
||
case AppErrorTypeEnum.NOT_FOUND:
|
||
appError.errorId = null;
|
||
contentNode = (
|
||
<>
|
||
<p>
|
||
We live in a world of uncertainty. But certainly, the page you
|
||
were looking for isn’t here. Perhaps this halfling has stolen it
|
||
and hidden it in another place. Try searching for what you were
|
||
looking for in another realm.
|
||
</p>
|
||
</>
|
||
);
|
||
break;
|
||
|
||
case AppErrorTypeEnum.GENERIC:
|
||
default:
|
||
contentNode = (
|
||
<>
|
||
<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>
|
||
</>
|
||
);
|
||
break;
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={[
|
||
"ct-character-sheet__failed",
|
||
`ct-character-sheet__failed--${FormatUtils.slugify(appError.type)}`,
|
||
].join(" ")}
|
||
>
|
||
<div className="ct-character-sheet__failed-content">
|
||
{contentNode}
|
||
{appError.errorId && (
|
||
<>
|
||
<div className="ct-character-sheet__failed-code">
|
||
<div className="ct-character-sheet__failed-code-label">
|
||
Error Code
|
||
</div>
|
||
<div className="ct-character-sheet__failed-code-value">
|
||
{appError.errorId}
|
||
</div>
|
||
</div>
|
||
<div className="ct-character-sheet__failed-code">
|
||
<div className="ct-character-sheet__failed-code-label">
|
||
Version
|
||
</div>
|
||
<div className="ct-character-sheet__failed-code-value">
|
||
{config.version}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
renderContent = (): React.ReactNode => {
|
||
const { loadingStatus, initFailed, appError } = this.props;
|
||
|
||
if (appError !== null) {
|
||
return this.renderAppError();
|
||
}
|
||
|
||
if (initFailed) {
|
||
return this.renderInitFailed();
|
||
}
|
||
|
||
const isLoaded =
|
||
loadingStatus === Constants.CharacterLoadingStatusEnum.LOADED;
|
||
|
||
return (
|
||
<>
|
||
{isLoaded && this.renderCharacterSheet()}
|
||
<LoadingBlocker isFinished={isLoaded || initFailed} />
|
||
</>
|
||
);
|
||
};
|
||
|
||
render() {
|
||
const {
|
||
loadingStatus,
|
||
sheetPosition,
|
||
envDimensions,
|
||
isCharacterSheetReady,
|
||
diceEnabled,
|
||
characterId,
|
||
authEndpoint,
|
||
diceFeatureConfiguration,
|
||
gameLog,
|
||
theme,
|
||
isReadonly,
|
||
characterStatus,
|
||
isVttView,
|
||
} = this.props;
|
||
|
||
let sheetStyles: React.CSSProperties = {};
|
||
if (envDimensions.window.width >= DESKTOP_COMPONENT_START_WIDTH) {
|
||
sheetStyles = {
|
||
transform: `translateX(${sheetPosition.left}px)`,
|
||
WebkitTransform: `translateX(${sheetPosition.left}px)`,
|
||
};
|
||
}
|
||
|
||
let classNames: Array<string> = ["ct-character-sheet"];
|
||
if (this.isSheetErrored()) {
|
||
classNames.push("ct-character-sheet--failed");
|
||
}
|
||
if (!isCharacterSheetReady) {
|
||
classNames.push("ct-character-sheet--invalid");
|
||
}
|
||
if (diceEnabled) {
|
||
classNames.push("ct-character-sheet--dice-enabled");
|
||
}
|
||
if (theme.isDarkMode) {
|
||
classNames.push("ct-character-sheet--dark-mode");
|
||
}
|
||
|
||
const everythingIsLoadedWithoutErrors: boolean =
|
||
loadingStatus === Constants.CharacterLoadingStatusEnum.LOADED &&
|
||
isCharacterSheetReady &&
|
||
!this.isSheetErrored();
|
||
|
||
return (
|
||
<CharacterSheetGuidedTour>
|
||
<div className={classNames.join(" ")}>
|
||
{this.renderSyncBlocker()}
|
||
<div
|
||
className={clsx([
|
||
"ct-character-sheet__inner",
|
||
!everythingIsLoadedWithoutErrors && styles.sheetNotReady,
|
||
])}
|
||
ref={this.sheetRef}
|
||
style={sheetStyles}
|
||
>
|
||
<ThemeManagerProvider
|
||
manager={{
|
||
lightOrDark: theme.isDarkMode ? "dark" : "light",
|
||
primary: theme.themeColor,
|
||
}}
|
||
>
|
||
{everythingIsLoadedWithoutErrors && (
|
||
<PremadeCharacterEditStatus
|
||
characterStatus={characterStatus}
|
||
isReadonly={isReadonly}
|
||
/>
|
||
)}
|
||
{this.renderContent()}
|
||
{everythingIsLoadedWithoutErrors && <MobileNav />}
|
||
</ThemeManagerProvider>
|
||
</div>
|
||
<NotificationSystem ref={this.notificationSystem} />
|
||
{diceEnabled && !isReadonly && (
|
||
<DiceContainer
|
||
canShow={everythingIsLoadedWithoutErrors}
|
||
isVttView={isVttView}
|
||
/>
|
||
)}
|
||
{everythingIsLoadedWithoutErrors &&
|
||
characterStatus === CharacterStatusSlug.PREMADE &&
|
||
isReadonly && <ClaimPremadeButton />}
|
||
</div>
|
||
</CharacterSheetGuidedTour>
|
||
);
|
||
}
|
||
}
|
||
|
||
const CharacterSheetWrapper = (props) => {
|
||
const { setTitle } = useHeadContext();
|
||
const { getSheetPositioning } = usePositioning();
|
||
const [searchParams] = useSearchParams();
|
||
const isVttView = searchParams.get("view") === "vtt";
|
||
return (
|
||
<CharacterSheet
|
||
{...props}
|
||
setTitle={setTitle}
|
||
sheetPosition={getSheetPositioning()}
|
||
isVttView={isVttView}
|
||
/>
|
||
);
|
||
};
|
||
|
||
function mapStateToProps(state: SheetAppState) {
|
||
return {
|
||
initFailed: sheetSelectors.getInitFailed(state),
|
||
loadingStatus: characterEnvSelectors.getLoadingStatus(state),
|
||
toastMessages: toastMessageSelectors.getToastMessages(state),
|
||
builderUrl: sheetAppSelectors.getBuilderUrl(state),
|
||
isMobile: appEnvSelectors.getIsMobile(state),
|
||
envDimensions: appEnvSelectors.getDimensions(state),
|
||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||
userRoles: appEnvSelectors.getUserRoles(state),
|
||
appError: appInfoSelectors.getError(state),
|
||
isCharacterSheetReady: rulesEngineSelectors.isCharacterSheetReady(state),
|
||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||
characterId: appEnvSelectors.getCharacterId(state),
|
||
diceFeatureConfiguration:
|
||
appEnvSelectors.getDiceFeatureConfiguration(state),
|
||
gameLog: appEnvSelectors.getGameLog(state),
|
||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||
canEdit: rulesEngineSelectors.getCanEdit(state),
|
||
username: appEnvSelectors.getUsername(state),
|
||
characterName: rulesEngineSelectors.getName(state),
|
||
preferences: rulesEngineSelectors.getPreferences(state),
|
||
characterStatus: characterSelectors.getStatusSlug(state),
|
||
userId: appEnvSelectors.getUserId(state),
|
||
campaign: characterSelectors.getCampaign(state),
|
||
};
|
||
}
|
||
export default connect(mapStateToProps)(CharacterSheetWrapper);
|