Grabbed dndbeyond's source code
``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
This commit is contained in:
@@ -0,0 +1,733 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React, { useContext } from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
Action,
|
||||
ActionUtils,
|
||||
Activatable,
|
||||
Attack,
|
||||
AttacksPerActionInfo,
|
||||
BaseInventoryContract,
|
||||
BasicActionContract,
|
||||
characterActions,
|
||||
CharacterTheme,
|
||||
CharClass,
|
||||
ClassFeature,
|
||||
ClassUtils,
|
||||
Constants,
|
||||
DataOriginRefData,
|
||||
Feat,
|
||||
FeatureUtils,
|
||||
FeatUtils,
|
||||
Hack__BaseCharClass,
|
||||
InventoryLookup,
|
||||
InventoryManager,
|
||||
Item,
|
||||
ItemManager,
|
||||
ItemUtils,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
rulesEngineSelectors,
|
||||
SnippetData,
|
||||
Spell,
|
||||
SpellCasterInfo,
|
||||
SpellUtils,
|
||||
WeaponSpellDamageGroup,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { IRollContext } from "@dndbeyond/dice";
|
||||
|
||||
import { Link } from "~/components/Link";
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import {
|
||||
PaneComponentEnum,
|
||||
PaneIdentifiers,
|
||||
} from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { InventoryManagerContext } from "../../../Shared/managers/InventoryManagerContext";
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../Shared/selectors";
|
||||
import { SharedAppState } from "../../../Shared/stores/typings";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import { ActionsList } from "../../components/ActionsList";
|
||||
|
||||
type AttackGroups = Record<string, Array<Attack>>;
|
||||
|
||||
interface ActionListConfig {
|
||||
onBasicActionClick: (basicAction: BasicActionContract) => void;
|
||||
onActionClick: (action: Action) => void;
|
||||
onActionUseSet: (action: Action, used: number) => void;
|
||||
onSpellClick: (spell: Spell) => void;
|
||||
onSpellUseSet: (spell: Spell, used: number) => void;
|
||||
onAttackClick: (attack: Attack) => void;
|
||||
weaponSpellDamageGroups: Array<WeaponSpellDamageGroup>;
|
||||
snippetData: SnippetData;
|
||||
spellCasterInfo: SpellCasterInfo;
|
||||
showNotes: boolean;
|
||||
abilityLookup: AbilityLookup;
|
||||
ruleData: RuleData;
|
||||
inventoryLookup: InventoryLookup;
|
||||
isInteractive: boolean;
|
||||
diceEnabled: boolean;
|
||||
theme: CharacterTheme;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
proficiencyBonus: number;
|
||||
rollContext: IRollContext;
|
||||
}
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
showNotes: boolean;
|
||||
|
||||
activatables: Array<Activatable>;
|
||||
attacks: Array<Attack>;
|
||||
weaponSpellDamageGroups: Array<WeaponSpellDamageGroup>;
|
||||
attacksPerActionInfo: AttacksPerActionInfo;
|
||||
ritualSpells: Array<Spell>;
|
||||
abilityLookup: AbilityLookup;
|
||||
inventoryLookup: InventoryLookup;
|
||||
ruleData: RuleData;
|
||||
snippetData: SnippetData;
|
||||
spellCasterInfo: SpellCasterInfo;
|
||||
isReadonly: boolean;
|
||||
diceEnabled: boolean;
|
||||
theme: CharacterTheme;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
proficiencyBonus: number;
|
||||
characterRollContext: IRollContext;
|
||||
inventoryManager: InventoryManager;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
|
||||
interface State {
|
||||
attackGroups: AttackGroups;
|
||||
}
|
||||
|
||||
class Actions extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
showNotes: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.generateStateData(props);
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>
|
||||
): void {
|
||||
const { attacks } = this.props;
|
||||
|
||||
if (attacks !== prevProps.attacks) {
|
||||
this.setState(this.generateStateData(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
generateStateData = (props: Props): State => {
|
||||
const { attacks } = props;
|
||||
|
||||
let attackGroups: AttackGroups = {};
|
||||
attackGroups["ACTIONS"] = attacks.filter(
|
||||
(attack) => attack.activation === Constants.ActivationTypeEnum.ACTION
|
||||
);
|
||||
attackGroups["BONUS_ACTIONS"] = attacks.filter(
|
||||
(attack) =>
|
||||
attack.activation === Constants.ActivationTypeEnum.BONUS_ACTION
|
||||
);
|
||||
attackGroups["REACTIONS"] = attacks.filter(
|
||||
(attack) => attack.activation === Constants.ActivationTypeEnum.REACTION
|
||||
);
|
||||
attackGroups["OTHER"] = attacks.filter(
|
||||
(attack) =>
|
||||
attack.activation !== Constants.ActivationTypeEnum.ACTION &&
|
||||
attack.activation !== Constants.ActivationTypeEnum.BONUS_ACTION &&
|
||||
attack.activation !== Constants.ActivationTypeEnum.REACTION
|
||||
);
|
||||
|
||||
return {
|
||||
attackGroups,
|
||||
};
|
||||
};
|
||||
|
||||
getActionListConfig = (): ActionListConfig => {
|
||||
const {
|
||||
weaponSpellDamageGroups,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
inventoryLookup,
|
||||
spellCasterInfo,
|
||||
snippetData,
|
||||
showNotes,
|
||||
isReadonly,
|
||||
diceEnabled,
|
||||
theme,
|
||||
dataOriginRefData,
|
||||
proficiencyBonus,
|
||||
characterRollContext,
|
||||
} = this.props;
|
||||
|
||||
return {
|
||||
onBasicActionClick: this.handleBasicActionShow,
|
||||
onActionClick: this.handleActionShow,
|
||||
onActionUseSet: this.handleActionUseSet,
|
||||
onSpellClick: this.handleSpellDetailShow,
|
||||
onSpellUseSet: this.handleSpellUseSet,
|
||||
onAttackClick: this.handleAttackClick,
|
||||
weaponSpellDamageGroups,
|
||||
abilityLookup,
|
||||
inventoryLookup,
|
||||
ruleData,
|
||||
snippetData,
|
||||
spellCasterInfo,
|
||||
showNotes,
|
||||
isInteractive: !isReadonly,
|
||||
diceEnabled,
|
||||
theme,
|
||||
dataOriginRefData,
|
||||
proficiencyBonus,
|
||||
rollContext: characterRollContext,
|
||||
};
|
||||
};
|
||||
|
||||
handleActionUseSet = (action: Action, uses: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const dataOrigin = ActionUtils.getDataOrigin(action);
|
||||
const dataOriginType = ActionUtils.getDataOriginType(action);
|
||||
|
||||
if (dataOriginType === Constants.DataOriginTypeEnum.ITEM) {
|
||||
const itemData = dataOrigin.primary as BaseInventoryContract;
|
||||
const item = ItemManager.getItem(ItemUtils.getMappingId(itemData));
|
||||
item.handleItemLimitedUseSet(uses);
|
||||
} else {
|
||||
const id = ActionUtils.getId(action);
|
||||
const entityTypeId = ActionUtils.getEntityTypeId(action);
|
||||
if (id !== null && entityTypeId !== null) {
|
||||
dispatch(
|
||||
characterActions.actionUseSet(id, entityTypeId, uses, dataOriginType)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellUseSet = (spell: Spell, uses: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
const mappingEntityTypeId = SpellUtils.getMappingEntityTypeId(spell);
|
||||
const dataOriginType = SpellUtils.getDataOriginType(spell);
|
||||
|
||||
if (mappingId && mappingEntityTypeId) {
|
||||
dispatch(
|
||||
characterActions.spellUseSet(
|
||||
mappingId,
|
||||
mappingEntityTypeId,
|
||||
uses,
|
||||
dataOriginType
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellDetailShow = (spell: Spell): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
let spellMappingId = SpellUtils.getMappingId(spell);
|
||||
const dataOriginType = SpellUtils.getDataOriginType(spell);
|
||||
const dataOrigin = SpellUtils.getDataOrigin(spell);
|
||||
|
||||
if (spellMappingId !== null) {
|
||||
let paneComponent: PaneComponentEnum;
|
||||
let identifiers: PaneIdentifiers;
|
||||
if (dataOriginType === Constants.DataOriginTypeEnum.CLASS) {
|
||||
paneComponent = PaneComponentEnum.CLASS_SPELL_DETAIL;
|
||||
identifiers = PaneIdentifierUtils.generateClassSpell(
|
||||
ClassUtils.getMappingId(dataOrigin.primary as Hack__BaseCharClass),
|
||||
spellMappingId
|
||||
);
|
||||
} else {
|
||||
paneComponent = PaneComponentEnum.CHARACTER_SPELL_DETAIL;
|
||||
identifiers =
|
||||
PaneIdentifierUtils.generateCharacterSpell(spellMappingId);
|
||||
}
|
||||
paneHistoryStart(paneComponent, identifiers);
|
||||
}
|
||||
};
|
||||
|
||||
// this is not being used downstream is marked for deletion, was used in ActionsList
|
||||
handleClassFeatureShow = (
|
||||
feature: ClassFeature,
|
||||
charClass: CharClass
|
||||
): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CLASS_FEATURE_DETAIL,
|
||||
PaneIdentifierUtils.generateClassFeature(
|
||||
FeatureUtils.getId(feature),
|
||||
ClassUtils.getMappingId(charClass)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// this is not being used downstream is marked for deletion, was used in ActionsList
|
||||
handleRacialTraitShow = (racialTrait: RacialTrait): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.SPECIES_TRAIT_DETAIL,
|
||||
PaneIdentifierUtils.generateRacialTrait(
|
||||
RacialTraitUtils.getId(racialTrait)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// this is not being used downstream is marked for deletion, was used in ActionsList
|
||||
handleFeatShow = (feat: Feat): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.FEAT_DETAIL,
|
||||
PaneIdentifierUtils.generateFeat(FeatUtils.getId(feat))
|
||||
);
|
||||
};
|
||||
|
||||
handleActionShow = (action: Action): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
const id = ActionUtils.getMappingId(action);
|
||||
const entityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
if (id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dataOriginType = ActionUtils.getDataOriginType(action);
|
||||
|
||||
let paneComponentType: PaneComponentEnum = PaneComponentEnum.ACTION;
|
||||
let paneComponentIdentifiers: PaneIdentifiers =
|
||||
PaneIdentifierUtils.generateAction(id, entityTypeId);
|
||||
|
||||
if (dataOriginType === Constants.DataOriginTypeEnum.CUSTOM) {
|
||||
paneComponentType = PaneComponentEnum.CUSTOM_ACTION;
|
||||
paneComponentIdentifiers = PaneIdentifierUtils.generateCustomAction(id);
|
||||
}
|
||||
|
||||
paneHistoryStart(paneComponentType, paneComponentIdentifiers);
|
||||
};
|
||||
|
||||
handleManageCustomActionsShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.CUSTOM_ACTIONS);
|
||||
};
|
||||
|
||||
handleBasicActionShow = (basicAction: BasicActionContract): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.BASIC_ACTION,
|
||||
PaneIdentifierUtils.generateBasicAction(basicAction.id)
|
||||
);
|
||||
};
|
||||
|
||||
handleAttackClick = (attack: Attack): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
let paneComponentType: PaneComponentEnum | null = null;
|
||||
let paneComponentIdentifiers: PaneIdentifiers | null = null;
|
||||
|
||||
switch (attack.type) {
|
||||
case Constants.AttackSourceTypeEnum.ACTION: {
|
||||
const action = attack.data as Action;
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneComponentType = PaneComponentEnum.ACTION;
|
||||
paneComponentIdentifiers = PaneIdentifierUtils.generateAction(
|
||||
mappingId,
|
||||
mappingEntityTypeId
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Constants.AttackSourceTypeEnum.CUSTOM: {
|
||||
const action = attack.data as Action;
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneComponentType = PaneComponentEnum.CUSTOM_ACTION;
|
||||
paneComponentIdentifiers = PaneIdentifierUtils.generateAction(
|
||||
mappingId,
|
||||
mappingEntityTypeId
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Constants.AttackSourceTypeEnum.ITEM: {
|
||||
paneComponentType = PaneComponentEnum.ITEM_DETAIL;
|
||||
paneComponentIdentifiers = PaneIdentifierUtils.generateItem(
|
||||
ItemUtils.getMappingId(attack.data as Item)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Constants.AttackSourceTypeEnum.SPELL: {
|
||||
const spell: Spell = attack.data as Spell;
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
const dataOriginType = SpellUtils.getDataOriginType(spell);
|
||||
const dataOrigin = SpellUtils.getDataOrigin(spell);
|
||||
|
||||
if (mappingId !== null) {
|
||||
if (dataOriginType === Constants.DataOriginTypeEnum.CLASS) {
|
||||
paneComponentType = PaneComponentEnum.CLASS_SPELL_DETAIL;
|
||||
paneComponentIdentifiers = PaneIdentifierUtils.generateClassSpell(
|
||||
ClassUtils.getMappingId(
|
||||
dataOrigin.primary as Hack__BaseCharClass
|
||||
),
|
||||
mappingId
|
||||
);
|
||||
} else {
|
||||
paneComponentType = PaneComponentEnum.CHARACTER_SPELL_DETAIL;
|
||||
paneComponentIdentifiers =
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// not implemented
|
||||
}
|
||||
|
||||
if (paneComponentType !== null) {
|
||||
paneHistoryStart(paneComponentType, paneComponentIdentifiers);
|
||||
}
|
||||
};
|
||||
|
||||
renderManageCustomLink = (): React.ReactNode => {
|
||||
const { isReadonly } = this.props;
|
||||
|
||||
if (isReadonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
useTheme={true}
|
||||
className="ct-actions__manage-custom-link"
|
||||
onClick={this.handleManageCustomActionsShow}
|
||||
>
|
||||
Manage Custom
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
renderActionsHeader = (): React.ReactNode => {
|
||||
const { attacksPerActionInfo, theme } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="ct-actions__attacks-heading">
|
||||
Actions •{" "}
|
||||
<span
|
||||
className={`ct-actions__attacks-per-action ${
|
||||
theme?.isDarkMode
|
||||
? "ct-actions__attacks-per-action--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Attacks per Action: {attacksPerActionInfo.value}
|
||||
</span>
|
||||
</div>
|
||||
{attacksPerActionInfo.restriction && (
|
||||
<div className="ct-actions__attacks-restriction">
|
||||
{attacksPerActionInfo.restriction}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { attackGroups } = this.state;
|
||||
const { activatables, ritualSpells, ruleData } = this.props;
|
||||
|
||||
let actionGroups: Record<string, Array<Activatable>> = {};
|
||||
actionGroups["ACTIONS"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType === Constants.ActivationTypeEnum.ACTION &&
|
||||
a.type !== Constants.ActivatableTypeEnum.CLASS_SPELL &&
|
||||
a.type !== Constants.ActivatableTypeEnum.CHARACTER_SPELL
|
||||
);
|
||||
actionGroups["BONUS_ACTIONS"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType ===
|
||||
Constants.ActivationTypeEnum.BONUS_ACTION
|
||||
);
|
||||
actionGroups["REACTIONS"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType === Constants.ActivationTypeEnum.REACTION
|
||||
);
|
||||
actionGroups["OTHER"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType !== Constants.ActivationTypeEnum.ACTION &&
|
||||
a.activation.activationType !==
|
||||
Constants.ActivationTypeEnum.BONUS_ACTION &&
|
||||
a.activation.activationType !== Constants.ActivationTypeEnum.REACTION
|
||||
);
|
||||
|
||||
let otherBasicActions: Array<BasicActionContract> = [
|
||||
...RuleDataUtils.getActivationTypeBasicActions(
|
||||
Constants.ActivationTypeEnum.NO_ACTION,
|
||||
ruleData
|
||||
),
|
||||
...RuleDataUtils.getActivationTypeBasicActions(
|
||||
Constants.ActivationTypeEnum.MINUTE,
|
||||
ruleData
|
||||
),
|
||||
...RuleDataUtils.getActivationTypeBasicActions(
|
||||
Constants.ActivationTypeEnum.HOUR,
|
||||
ruleData
|
||||
),
|
||||
...RuleDataUtils.getActivationTypeBasicActions(
|
||||
Constants.ActivationTypeEnum.SPECIAL,
|
||||
ruleData
|
||||
),
|
||||
];
|
||||
|
||||
let limitedUseActionGroups: Record<string, Array<Activatable>> = {};
|
||||
limitedUseActionGroups["ACTIONS"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType === Constants.ActivationTypeEnum.ACTION &&
|
||||
a.limitedUse
|
||||
);
|
||||
limitedUseActionGroups["BONUS_ACTIONS"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType ===
|
||||
Constants.ActivationTypeEnum.BONUS_ACTION && a.limitedUse
|
||||
);
|
||||
limitedUseActionGroups["REACTIONS"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType === Constants.ActivationTypeEnum.REACTION &&
|
||||
a.limitedUse
|
||||
);
|
||||
limitedUseActionGroups["OTHER"] = activatables.filter(
|
||||
(a) =>
|
||||
a.activation.activationType !== Constants.ActivationTypeEnum.ACTION &&
|
||||
a.activation.activationType !==
|
||||
Constants.ActivationTypeEnum.BONUS_ACTION &&
|
||||
a.activation.activationType !== Constants.ActivationTypeEnum.REACTION &&
|
||||
a.limitedUse
|
||||
);
|
||||
|
||||
let limitedUseRitualSpells = ritualSpells.filter((spell) =>
|
||||
SpellUtils.getLimitedUse(spell)
|
||||
);
|
||||
|
||||
const actionListData: ActionListConfig = {
|
||||
...this.getActionListConfig(),
|
||||
};
|
||||
|
||||
const hasLimitedUse =
|
||||
!!limitedUseActionGroups["ACTIONS"].length ||
|
||||
!!limitedUseActionGroups["BONUS_ACTIONS"].length ||
|
||||
!!limitedUseActionGroups["REACTIONS"].length ||
|
||||
!!limitedUseActionGroups["OTHER"].length ||
|
||||
!!limitedUseRitualSpells.length;
|
||||
|
||||
const hasAttackGroups =
|
||||
!!attackGroups["ACTIONS"].length ||
|
||||
!!attackGroups["BONUS_ACTIONS"].length ||
|
||||
!!attackGroups["REACTIONS"].length ||
|
||||
!!attackGroups["OTHER"].length;
|
||||
|
||||
return (
|
||||
<section className="ct-actions">
|
||||
<h2 style={visuallyHidden}>Actions</h2>
|
||||
<TabFilter
|
||||
filters={[
|
||||
...(!hasAttackGroups
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: "Attack",
|
||||
content: (
|
||||
<>
|
||||
{this.renderManageCustomLink()}
|
||||
<ActionsList
|
||||
heading={this.renderActionsHeader()}
|
||||
attacks={attackGroups["ACTIONS"]}
|
||||
{...actionListData}
|
||||
/>
|
||||
<ActionsList
|
||||
heading="Bonus Actions"
|
||||
attacks={attackGroups["BONUS_ACTIONS"]}
|
||||
{...actionListData}
|
||||
/>
|
||||
<ActionsList
|
||||
heading="Reactions"
|
||||
attacks={attackGroups["REACTIONS"]}
|
||||
{...actionListData}
|
||||
/>
|
||||
<ActionsList
|
||||
heading="Other"
|
||||
attacks={attackGroups["OTHER"]}
|
||||
{...actionListData}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]),
|
||||
{
|
||||
label: "Action",
|
||||
content: (
|
||||
<>
|
||||
{this.renderManageCustomLink()}
|
||||
<ActionsList
|
||||
heading={this.renderActionsHeader()}
|
||||
actions={actionGroups["ACTIONS"]}
|
||||
attacks={attackGroups["ACTIONS"]}
|
||||
basicActions={RuleDataUtils.getActivationTypeBasicActions(
|
||||
Constants.ActivationTypeEnum.ACTION,
|
||||
ruleData
|
||||
)}
|
||||
{...actionListData}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Bonus Action",
|
||||
content: (
|
||||
<>
|
||||
{this.renderManageCustomLink()}
|
||||
<ActionsList
|
||||
heading="Bonus Actions"
|
||||
actions={actionGroups["BONUS_ACTIONS"]}
|
||||
attacks={attackGroups["BONUS_ACTIONS"]}
|
||||
basicActions={RuleDataUtils.getActivationTypeBasicActions(
|
||||
Constants.ActivationTypeEnum.BONUS_ACTION,
|
||||
ruleData
|
||||
)}
|
||||
{...actionListData}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Reaction",
|
||||
content: (
|
||||
<>
|
||||
{this.renderManageCustomLink()}
|
||||
<ActionsList
|
||||
heading="Reactions"
|
||||
actions={actionGroups["REACTIONS"]}
|
||||
attacks={attackGroups["REACTIONS"]}
|
||||
basicActions={RuleDataUtils.getActivationTypeBasicActions(
|
||||
Constants.ActivationTypeEnum.REACTION,
|
||||
ruleData
|
||||
)}
|
||||
{...actionListData}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
content: (
|
||||
<>
|
||||
{this.renderManageCustomLink()}
|
||||
<ActionsList
|
||||
heading="Other"
|
||||
actions={actionGroups["OTHER"]}
|
||||
attacks={attackGroups["OTHER"]}
|
||||
basicActions={otherBasicActions}
|
||||
ritualSpells={ritualSpells}
|
||||
showActivationInfo
|
||||
{...actionListData}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
...(!hasLimitedUse
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: "Limited Use",
|
||||
content: (
|
||||
<>
|
||||
{this.renderManageCustomLink()}
|
||||
<ActionsList
|
||||
heading="Actions"
|
||||
actions={limitedUseActionGroups["ACTIONS"]}
|
||||
{...actionListData}
|
||||
/>
|
||||
<ActionsList
|
||||
heading="Bonus Actions"
|
||||
actions={limitedUseActionGroups["BONUS_ACTIONS"]}
|
||||
{...actionListData}
|
||||
/>
|
||||
<ActionsList
|
||||
heading="Reactions"
|
||||
actions={limitedUseActionGroups["REACTIONS"]}
|
||||
{...actionListData}
|
||||
/>
|
||||
<ActionsList
|
||||
heading="Other"
|
||||
actions={limitedUseActionGroups["OTHER"]}
|
||||
ritualSpells={limitedUseRitualSpells}
|
||||
showActivationInfo
|
||||
{...actionListData}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]),
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SharedAppState) {
|
||||
return {
|
||||
activatables: rulesEngineSelectors.getActivatables(state),
|
||||
attacks: rulesEngineSelectors.getAttacks(state),
|
||||
weaponSpellDamageGroups:
|
||||
rulesEngineSelectors.getWeaponSpellDamageGroups(state),
|
||||
attacksPerActionInfo: rulesEngineSelectors.getAttacksPerActionInfo(state),
|
||||
ritualSpells: rulesEngineSelectors.getRitualSpells(state),
|
||||
abilityLookup: rulesEngineSelectors.getAbilityLookup(state),
|
||||
inventoryLookup: rulesEngineSelectors.getInventoryLookup(state),
|
||||
spellCasterInfo: rulesEngineSelectors.getSpellCasterInfo(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
snippetData: rulesEngineSelectors.getSnippetData(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
dataOriginRefData: rulesEngineSelectors.getDataOriginRefData(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
characterRollContext:
|
||||
characterRollContextSelectors.getCharacterRollContext(state),
|
||||
};
|
||||
}
|
||||
|
||||
const ActionsContainer = (props) => {
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<Actions
|
||||
inventoryManager={inventoryManager}
|
||||
paneHistoryStart={paneHistoryStart}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ActionsContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Actions from "./Actions";
|
||||
|
||||
export default Actions;
|
||||
export { Actions };
|
||||
@@ -0,0 +1,181 @@
|
||||
import { groupBy, orderBy } from "lodash";
|
||||
import React, { useContext } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { AttunementSlot } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
Item,
|
||||
ItemUtils,
|
||||
CampaignUtils,
|
||||
CharacterTheme,
|
||||
InventoryManager,
|
||||
serviceDataSelectors,
|
||||
PartyInfo,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { InventoryManagerContext } from "../../../Shared/managers/InventoryManagerContext";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import AttunementItem from "./AttunementItem";
|
||||
|
||||
interface Props {
|
||||
items: Array<Item>;
|
||||
attunedSlots: Array<Item | null>;
|
||||
isFiltered: boolean;
|
||||
isMobile: boolean;
|
||||
theme: CharacterTheme;
|
||||
inventoryManager: InventoryManager;
|
||||
partyInfo: PartyInfo | null;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class Attunement extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
isFiltered: false,
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
handleItemShow = (item: Item): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ITEM_DETAIL,
|
||||
PaneIdentifierUtils.generateItem(ItemUtils.getMappingId(item))
|
||||
);
|
||||
};
|
||||
|
||||
renderItems = (): React.ReactNode => {
|
||||
const { items, theme, inventoryManager, partyInfo } = this.props;
|
||||
|
||||
const attunableItems = items.filter((item) => ItemUtils.canAttune(item));
|
||||
const sortedItems = groupBy(
|
||||
orderBy(
|
||||
attunableItems,
|
||||
[
|
||||
(item) => ItemUtils.getRarityLevel(item),
|
||||
(item) => ItemUtils.getName(item),
|
||||
(item) => ItemUtils.getMappingId(item),
|
||||
],
|
||||
["desc", "asc"]
|
||||
),
|
||||
(item) => {
|
||||
if (inventoryManager.isShared(item)) {
|
||||
return "party";
|
||||
} else {
|
||||
return "character";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ct-attunement__group ct-attunement__group--items">
|
||||
<div className="ct-attunement__group-header">
|
||||
Items Requiring Attunement
|
||||
</div>
|
||||
<div className="ct-attunement__group-items">
|
||||
{sortedItems.character?.length > 0 ? (
|
||||
sortedItems.character?.map((item) => (
|
||||
<AttunementItem
|
||||
key={ItemUtils.getUniqueKey(item)}
|
||||
item={item}
|
||||
onItemShow={this.handleItemShow}
|
||||
theme={theme}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="ct-attunement__group-empty">
|
||||
Items that you can attune to will display here as you make them
|
||||
active.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{partyInfo &&
|
||||
CampaignUtils.isSharingStateActive(
|
||||
CampaignUtils.getSharingState(partyInfo)
|
||||
) && (
|
||||
<>
|
||||
<div className="ct-attunement__group-header">
|
||||
Party Items Requiring Attunement
|
||||
</div>
|
||||
<div className="ct-attunement__group-items">
|
||||
{sortedItems.party?.length > 0 ? (
|
||||
sortedItems.party?.map((item) => (
|
||||
<AttunementItem
|
||||
key={ItemUtils.getUniqueKey(item)}
|
||||
item={item}
|
||||
onItemShow={this.handleItemShow}
|
||||
theme={theme}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="ct-attunement__group-empty">
|
||||
Party items that you can attune to will display here as you
|
||||
make them active.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderAttuned = (): React.ReactNode => {
|
||||
const { attunedSlots, isMobile, theme } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-attunement__group ct-attunement__group--attuned">
|
||||
<div className="ct-attunement__group-header">Attuned Items</div>
|
||||
<div className="ct-attunement__group-items">
|
||||
{attunedSlots.map((slot, idx) => (
|
||||
<AttunementSlot
|
||||
theme={theme}
|
||||
key={`slot-${idx}-${slot ? ItemUtils.getUniqueKey(slot) : ""}`}
|
||||
slot={slot}
|
||||
onClick={this.handleItemShow}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-attunement">
|
||||
{this.renderAttuned()}
|
||||
{this.renderItems()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
attunedSlots: rulesEngineSelectors.getAttunedSlots(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
items: rulesEngineSelectors.getEquippedItems(state),
|
||||
partyInfo: serviceDataSelectors.getPartyInfo(state),
|
||||
};
|
||||
}
|
||||
|
||||
const AttunementContainer = (props) => {
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<Attunement
|
||||
inventoryManager={inventoryManager}
|
||||
paneHistoryStart={paneHistoryStart}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(AttunementContainer);
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
InventoryManager,
|
||||
Item,
|
||||
ItemUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
|
||||
import { ItemSlotManager } from "../../../../Shared/components/ItemSlotManager";
|
||||
import { InventoryManagerContext } from "../../../../Shared/managers/InventoryManagerContext";
|
||||
|
||||
interface Props {
|
||||
item: Item;
|
||||
onItemShow?: (item: Item) => void;
|
||||
className: string;
|
||||
theme: CharacterTheme;
|
||||
inventoryManager: InventoryManager;
|
||||
}
|
||||
export class AttunementItem extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
handleSetAttune = (uses: number): void => {
|
||||
const { inventoryManager, item } = this.props;
|
||||
|
||||
//TODO use different component than SlotManager for attune/unattune
|
||||
if (uses === 1) {
|
||||
inventoryManager.handleAttune({ item });
|
||||
}
|
||||
|
||||
if (uses === 0) {
|
||||
inventoryManager.handleUnattune({ item });
|
||||
}
|
||||
};
|
||||
|
||||
handleItemClick = (evt: React.MouseEvent): void => {
|
||||
const { onItemShow, item } = this.props;
|
||||
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (onItemShow) {
|
||||
onItemShow(item);
|
||||
}
|
||||
};
|
||||
|
||||
renderMetaItems = (): React.ReactNode => {
|
||||
const { item, inventoryManager } = this.props;
|
||||
|
||||
let metaItems: Array<string> = [];
|
||||
|
||||
if (ItemUtils.isLegacy(item)) {
|
||||
metaItems.push("Legacy");
|
||||
}
|
||||
|
||||
let type = ItemUtils.getType(item);
|
||||
if (type !== null) {
|
||||
metaItems.push(type);
|
||||
}
|
||||
|
||||
if (ItemUtils.isWeaponContract(item)) {
|
||||
if (ItemUtils.isOffhand(item)) {
|
||||
metaItems.push("Dual Wield");
|
||||
}
|
||||
} else if (ItemUtils.isArmorContract(item)) {
|
||||
let baseArmorName = ItemUtils.getBaseArmorName(item);
|
||||
if (baseArmorName) {
|
||||
metaItems.push(baseArmorName);
|
||||
}
|
||||
} else if (ItemUtils.isGearContract(item)) {
|
||||
let subType = ItemUtils.getSubType(item);
|
||||
if (subType) {
|
||||
metaItems.push(subType);
|
||||
}
|
||||
if (ItemUtils.isOffhand(item)) {
|
||||
metaItems.push("Dual Wield");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
ItemUtils.isEquipped(item) &&
|
||||
!inventoryManager.isEquippedToCurrentCharacter(item)
|
||||
) {
|
||||
metaItems.push(
|
||||
`${
|
||||
ItemUtils.isAttuned(item) ? "Attuned" : "Equipped"
|
||||
} by ${inventoryManager.getEquippedCharacterName(item)}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ct-attunement__meta">
|
||||
{metaItems.map((metaItem, idx) => (
|
||||
<span className="ct-attunement__meta-item" key={idx}>
|
||||
{metaItem}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
//`
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item, className, theme, inventoryManager } = this.props;
|
||||
|
||||
const canAttune = inventoryManager.canAttuneUnattuneItem(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ct-attunement__item ${className}`}
|
||||
onClick={this.handleItemClick}
|
||||
>
|
||||
<div className="ct-attunement__item-action">
|
||||
<ItemSlotManager
|
||||
isUsed={!!ItemUtils.isAttuned(item)}
|
||||
canUse={canAttune}
|
||||
theme={theme}
|
||||
onSet={this.handleSetAttune}
|
||||
useTooltip={
|
||||
!!ItemUtils.isEquipped(item) &&
|
||||
!inventoryManager.isEquippedToCurrentCharacter(item)
|
||||
}
|
||||
tooltipTitle={`${
|
||||
ItemUtils.isAttuned(item) ? "Attuned" : "Equipped"
|
||||
} by ${inventoryManager.getEquippedCharacterName(item)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-attunement__item-name">
|
||||
<div className="ct-attunement__item-heading">
|
||||
<ItemName item={item} />
|
||||
</div>
|
||||
{this.renderMetaItems()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
//`
|
||||
}
|
||||
}
|
||||
|
||||
const AttunementItemContainer = (props) => {
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
return <AttunementItem inventoryManager={inventoryManager} {...props} />;
|
||||
};
|
||||
|
||||
export default AttunementItemContainer;
|
||||
@@ -0,0 +1,4 @@
|
||||
import AttunementItem from "./AttunementItem";
|
||||
|
||||
export default AttunementItem;
|
||||
export { AttunementItem };
|
||||
@@ -0,0 +1,5 @@
|
||||
import Attunement from "./Attunement";
|
||||
import AttunementItem from "./AttunementItem";
|
||||
|
||||
export default Attunement;
|
||||
export { Attunement, AttunementItem };
|
||||
@@ -0,0 +1,88 @@
|
||||
import jss, { StyleSheet } from "jss";
|
||||
import preset from "jss-preset-default";
|
||||
import React from "react";
|
||||
|
||||
import { BackdropInfo } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DDB_MEDIA_URL } from "../../../../../constants";
|
||||
|
||||
interface Props {
|
||||
backdrop: BackdropInfo;
|
||||
}
|
||||
export default class BackdropStyles extends React.PureComponent<Props> {
|
||||
sheet: StyleSheet | null = null;
|
||||
|
||||
componentDidMount() {
|
||||
jss.setup(preset());
|
||||
this.renderStyleSheet(this.props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.removeStyleSheet();
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<{}>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
this.removeStyleSheet();
|
||||
this.renderStyleSheet(this.props);
|
||||
}
|
||||
|
||||
renderStyleSheet = (props: Props): void => {
|
||||
const { backdrop } = props;
|
||||
|
||||
if (backdrop.backdropAvatarUrl !== null) {
|
||||
this.sheet = jss.createStyleSheet({});
|
||||
|
||||
if (this.sheet !== null) {
|
||||
let breakpointRules: Record<string, string> = [
|
||||
{ width: 768, height: 152, image: backdrop.backdropAvatarUrl },
|
||||
{ width: 1024, height: 218, image: backdrop.backdropAvatarUrl },
|
||||
{ width: 1200, height: 230, image: backdrop.backdropAvatarUrl },
|
||||
{
|
||||
width: 1921,
|
||||
height: 230,
|
||||
image:
|
||||
backdrop.smallBackdropAvatarUrl === null
|
||||
? ""
|
||||
: backdrop.smallBackdropAvatarUrl,
|
||||
},
|
||||
{
|
||||
width: 2561,
|
||||
height: 230,
|
||||
image:
|
||||
backdrop.largeBackdropAvatarUrl === null
|
||||
? ""
|
||||
: backdrop.largeBackdropAvatarUrl,
|
||||
},
|
||||
].reduce((acc, breakpoint) => {
|
||||
acc[`@media (min-width: ${breakpoint.width}px)`] = {
|
||||
background: `url(${breakpoint.image}) no-repeat center ${breakpoint.height}px, url(${DDB_MEDIA_URL}/attachments/0/84/background_texture.png) #f9f9f9 !important`,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.sheet.addRules({
|
||||
"@global": {
|
||||
"html body.body-rpgcharacter-sheet": breakpointRules,
|
||||
},
|
||||
});
|
||||
|
||||
this.sheet.attach();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
removeStyleSheet = (): void => {
|
||||
if (this.sheet) {
|
||||
jss.removeStyleSheet(this.sheet);
|
||||
this.sheet = null;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import BackdropStyles from "./BackdropStyles";
|
||||
|
||||
export default BackdropStyles;
|
||||
export { BackdropStyles };
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
CampaignSummary,
|
||||
LightBuilderSvg,
|
||||
ThemedLongRestSvg,
|
||||
ThemedShareSvg,
|
||||
ThemedShortRestSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CampaignDataContract,
|
||||
CharacterPreferences,
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
Item,
|
||||
ItemUtils,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { GameLogState } from "../../../Shared/stores/typings";
|
||||
import WatchTourDialog from "../../components/WatchTourDialog";
|
||||
import { sheetAppSelectors } from "../../selectors";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import CharacterHeaderInfo from "../CharacterHeaderInfo";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
campaign: CampaignDataContract | null;
|
||||
builderUrl: string;
|
||||
items: Array<Item>;
|
||||
preferences: CharacterPreferences;
|
||||
theme: CharacterTheme;
|
||||
isReadonly: boolean;
|
||||
gameLog: GameLogState;
|
||||
status: CharacterStatusSlug | null;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
|
||||
class CharacterHeaderDesktop extends React.PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleCampaignShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
|
||||
};
|
||||
|
||||
handleShortResetClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.SHORT_REST);
|
||||
};
|
||||
|
||||
handleLongResetClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.LONG_REST);
|
||||
};
|
||||
|
||||
handleShareClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.SHARE_URL);
|
||||
};
|
||||
|
||||
hasMagicItem = (): boolean => {
|
||||
const { items } = this.props;
|
||||
|
||||
return !!items.find((item) => ItemUtils.isMagic(item));
|
||||
};
|
||||
|
||||
renderSideContent = (): React.ReactNode => {
|
||||
const {
|
||||
builderUrl,
|
||||
campaign,
|
||||
preferences,
|
||||
isReadonly,
|
||||
theme,
|
||||
gameLog,
|
||||
status,
|
||||
} = this.props;
|
||||
|
||||
if (isReadonly) {
|
||||
if (status === CharacterStatusSlug.PREMADE) {
|
||||
return <WatchTourDialog />;
|
||||
}
|
||||
|
||||
if (!campaign) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CampaignSummary
|
||||
campaign={campaign}
|
||||
onCampaignShow={this.handleCampaignShow}
|
||||
gameLog={gameLog}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{preferences !== null &&
|
||||
preferences.privacyType ===
|
||||
Constants.PreferencePrivacyTypeEnum.PUBLIC && (
|
||||
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--share">
|
||||
<div
|
||||
className="ct-character-header-desktop__button"
|
||||
onClick={this.handleShareClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="ct-character-header-desktop__button-icon">
|
||||
<ThemedShareSvg theme={theme} />
|
||||
</div>
|
||||
<span className="ct-character-header-desktop__button-label">
|
||||
Share
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--short-rest">
|
||||
<div
|
||||
className="ct-character-header-desktop__button"
|
||||
onClick={this.handleShortResetClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="ct-character-header-desktop__button-icon">
|
||||
<ThemedShortRestSvg theme={theme} />
|
||||
</div>
|
||||
<span className="ct-character-header-desktop__button-label">
|
||||
Short Rest
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--long-rest">
|
||||
<div
|
||||
className="ct-character-header-desktop__button"
|
||||
onClick={this.handleLongResetClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="ct-character-header-desktop__button-icon">
|
||||
<ThemedLongRestSvg theme={theme} />
|
||||
</div>
|
||||
<span className="ct-character-header-desktop__button-label">
|
||||
Long Rest
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/*{this.hasMagicItem() &&*/}
|
||||
{/*<div className="ct-character-header-desktop__group">*/}
|
||||
{/*<div className="ct-character-header-desktop__button" onClick={this.handleDawnClick}>*/}
|
||||
{/*<i className="i-menu-dawn" />*/}
|
||||
{/*<span className="ct-character-header-desktop__button-label">Dawn</span>*/}
|
||||
{/*</div>*/}
|
||||
{/*</div>*/}
|
||||
{/*}*/}
|
||||
{campaign !== null && (
|
||||
<CampaignSummary
|
||||
campaign={campaign}
|
||||
onCampaignShow={this.handleCampaignShow}
|
||||
gameLog={gameLog}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--builder">
|
||||
<Tooltip
|
||||
isDarkMode={theme.isDarkMode}
|
||||
title="Go to builder"
|
||||
className="ct-character-header-desktop__builder"
|
||||
>
|
||||
<RouterLink
|
||||
to={builderUrl}
|
||||
className="ct-character-header-desktop__builder-link"
|
||||
aria-label="Go to builder"
|
||||
>
|
||||
<LightBuilderSvg />
|
||||
</RouterLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-character-header-desktop">
|
||||
<div className="ct-character-header-desktop__group ct-character-header-desktop__group-tidbits">
|
||||
<CharacterHeaderInfo />
|
||||
</div>
|
||||
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--gap" />
|
||||
{this.renderSideContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
builderUrl: sheetAppSelectors.getBuilderUrl(state),
|
||||
campaign: rulesEngineSelectors.getCampaign(state),
|
||||
items: rulesEngineSelectors.getInventory(state),
|
||||
preferences: rulesEngineSelectors.getCharacterPreferences(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
gameLog: appEnvSelectors.getGameLog(state),
|
||||
status: characterSelectors.getStatusSlug(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CharacterHeaderDesktopContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<CharacterHeaderDesktop paneHistoryStart={paneHistoryStart} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CharacterHeaderDesktopContainer);
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { CharacterTidbits } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
CharacterPreferences,
|
||||
CharClass,
|
||||
Constants,
|
||||
ExperienceInfo,
|
||||
Race,
|
||||
RuleData,
|
||||
DecorationInfo,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { ThemeButton } from "../../../Shared/components/common/Button";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { SheetAppState } from "../../typings";
|
||||
|
||||
interface Props {
|
||||
classes: Array<CharClass>;
|
||||
name: string;
|
||||
gender: string | null;
|
||||
species: Race | null;
|
||||
deathCause: Constants.DeathCauseEnum;
|
||||
ruleData: RuleData;
|
||||
xpInfo: ExperienceInfo;
|
||||
preferences: CharacterPreferences;
|
||||
decorationInfo: DecorationInfo;
|
||||
isReadonly: boolean;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class CharacterHeaderInfo extends React.PureComponent<Props, {}> {
|
||||
handleInfoClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.CHARACTER_MANAGE);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
deathCause,
|
||||
name,
|
||||
classes,
|
||||
gender,
|
||||
xpInfo,
|
||||
species,
|
||||
preferences,
|
||||
ruleData,
|
||||
decorationInfo,
|
||||
isReadonly,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-character-header-info">
|
||||
<div
|
||||
className="ct-character-header-info__content"
|
||||
onClick={this.handleInfoClick}
|
||||
>
|
||||
<CharacterTidbits
|
||||
classes={classes}
|
||||
decorationInfo={decorationInfo}
|
||||
name={name}
|
||||
gender={gender}
|
||||
species={species}
|
||||
deathCause={deathCause}
|
||||
preferences={preferences}
|
||||
ruleData={ruleData}
|
||||
xpInfo={xpInfo}
|
||||
isInteractive={!isReadonly}
|
||||
calloutNode={
|
||||
<ThemeButton size="small" style="outline">
|
||||
Manage
|
||||
</ThemeButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
name: rulesEngineSelectors.getName(state),
|
||||
gender: rulesEngineSelectors.getGender(state),
|
||||
species: rulesEngineSelectors.getRace(state),
|
||||
classes: rulesEngineSelectors.getClasses(state),
|
||||
xpInfo: rulesEngineSelectors.getExperienceInfo(state),
|
||||
deathCause: rulesEngineSelectors.getDeathCause(state),
|
||||
preferences: rulesEngineSelectors.getCharacterPreferences(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
decorationInfo: rulesEngineSelectors.getDecorationInfo(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CharacterHeaderInfoContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <CharacterHeaderInfo paneHistoryStart={paneHistoryStart} {...props} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CharacterHeaderInfoContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterHeaderInfo from "./CharacterHeaderInfo";
|
||||
|
||||
export default CharacterHeaderInfo;
|
||||
export { CharacterHeaderInfo };
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
characterActions,
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
Constants,
|
||||
HitPointInfo,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import StatusSummaryMobile from "../../components/StatusSummaryMobile";
|
||||
import WatchTourDialog from "../../components/WatchTourDialog";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import CharacterHeaderInfo from "../CharacterHeaderInfo";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
hitPointInfo: HitPointInfo;
|
||||
fails: number;
|
||||
successes: number;
|
||||
deathCause: Constants.DeathCauseEnum;
|
||||
inspiration: boolean;
|
||||
ruleData: RuleData;
|
||||
isReadonly: boolean;
|
||||
status: CharacterStatusSlug | null;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class CharacterHeaderMobile extends React.PureComponent<Props> {
|
||||
handleHealthSummaryClick = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.HEALTH_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
handleInspirationClick = (): void => {
|
||||
const { inspiration, dispatch } = this.props;
|
||||
|
||||
dispatch(characterActions.inspirationSet(!inspiration));
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
hitPointInfo,
|
||||
fails,
|
||||
successes,
|
||||
deathCause,
|
||||
inspiration,
|
||||
ruleData,
|
||||
isReadonly,
|
||||
status,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-character-header-mobile">
|
||||
<div className="ct-character-header-mobile__group ct-character-header-mobile__group-tidbits">
|
||||
<CharacterHeaderInfo />
|
||||
</div>
|
||||
<div className="ct-character-header-mobile__group ct-character-header-mobile__group--gap" />
|
||||
{isReadonly && status === CharacterStatusSlug.PREMADE ? (
|
||||
<div>
|
||||
<WatchTourDialog />
|
||||
</div>
|
||||
) : (
|
||||
<div className="ct-character-header-mobile__group ct-character-header-mobile__group--summary">
|
||||
<StatusSummaryMobile
|
||||
hitPointInfo={hitPointInfo}
|
||||
fails={fails}
|
||||
successes={successes}
|
||||
deathCause={deathCause}
|
||||
inspiration={inspiration}
|
||||
ruleData={ruleData}
|
||||
onHealthClick={this.handleHealthSummaryClick}
|
||||
onInspirationClick={this.handleInspirationClick}
|
||||
isInteractive={!isReadonly}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
hitPointInfo: rulesEngineSelectors.getHitPointInfo(state),
|
||||
fails: rulesEngineSelectors.getDeathSavesFailCount(state),
|
||||
successes: rulesEngineSelectors.getDeathSavesSuccessCount(state),
|
||||
inspiration: rulesEngineSelectors.getInspiration(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
deathCause: rulesEngineSelectors.getDeathCause(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
status: characterSelectors.getStatusSlug(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CharacterHeaderMobileContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<CharacterHeaderMobile paneHistoryStart={paneHistoryStart} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CharacterHeaderMobileContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterHeaderMobile from "./CharacterHeaderMobile";
|
||||
|
||||
export default CharacterHeaderMobile;
|
||||
export { CharacterHeaderMobile };
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
LightBuilderSvg,
|
||||
ThemedShareSvg,
|
||||
ThemedLongRestSvg,
|
||||
ThemedShortRestSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
characterActions,
|
||||
CampaignDataContract,
|
||||
CharacterPreferences,
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
HitPointInfo,
|
||||
Item,
|
||||
ItemUtils,
|
||||
RuleData,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Link } from "~/components/Link";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import StatusSummaryMobile from "../../components/StatusSummaryMobile";
|
||||
import WatchTourDialog from "../../components/WatchTourDialog";
|
||||
import { sheetAppSelectors } from "../../selectors";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import { CharacterHeaderInfo } from "../CharacterHeaderInfo";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
hitPointInfo: HitPointInfo;
|
||||
fails: number;
|
||||
successes: number;
|
||||
deathCause: Constants.DeathCauseEnum;
|
||||
inspiration: boolean;
|
||||
campaign: CampaignDataContract | null;
|
||||
builderUrl: string;
|
||||
items: Array<Item>;
|
||||
preferences: CharacterPreferences;
|
||||
ruleData: RuleData;
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
status: CharacterStatusSlug | null;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class CharacterHeaderTablet extends React.PureComponent<Props> {
|
||||
handleCampaignShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
|
||||
};
|
||||
|
||||
handleShortResetClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.SHORT_REST);
|
||||
};
|
||||
|
||||
handleGameLogClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.GAME_LOG);
|
||||
};
|
||||
|
||||
handleLongResetClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.LONG_REST);
|
||||
};
|
||||
|
||||
handleShareClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.SHARE_URL);
|
||||
};
|
||||
|
||||
handleHealthSummaryClick = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.HEALTH_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
handleInspirationClick = (): void => {
|
||||
const { inspiration, dispatch } = this.props;
|
||||
|
||||
dispatch(characterActions.inspirationSet(!inspiration));
|
||||
};
|
||||
|
||||
hasMagicItem = (): boolean => {
|
||||
const { items } = this.props;
|
||||
|
||||
return !!items.find((item) => ItemUtils.isMagic(item));
|
||||
};
|
||||
|
||||
renderSideContent = (): React.ReactNode => {
|
||||
const {
|
||||
hitPointInfo,
|
||||
fails,
|
||||
successes,
|
||||
deathCause,
|
||||
inspiration,
|
||||
builderUrl,
|
||||
preferences,
|
||||
isReadonly,
|
||||
ruleData,
|
||||
theme,
|
||||
status,
|
||||
} = this.props;
|
||||
|
||||
if (isReadonly) {
|
||||
if (status === CharacterStatusSlug.PREMADE) {
|
||||
return (
|
||||
<div>
|
||||
<WatchTourDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--summary">
|
||||
<StatusSummaryMobile
|
||||
hitPointInfo={hitPointInfo}
|
||||
fails={fails}
|
||||
successes={successes}
|
||||
inspiration={inspiration}
|
||||
ruleData={ruleData}
|
||||
deathCause={deathCause}
|
||||
onHealthClick={this.handleHealthSummaryClick}
|
||||
onInspirationClick={this.handleInspirationClick}
|
||||
isInteractive={!isReadonly}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{preferences !== null &&
|
||||
preferences.privacyType ===
|
||||
Constants.PreferencePrivacyTypeEnum.PUBLIC && (
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--share">
|
||||
<div
|
||||
className="ct-character-header-tablet__button"
|
||||
onClick={this.handleShareClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="ct-character-header-tablet__button-icon">
|
||||
<ThemedShareSvg theme={theme} />
|
||||
</div>
|
||||
<span className="ct-character-header-tablet__button-label">
|
||||
Share
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--short-rest">
|
||||
<div
|
||||
className="ct-character-header-tablet__button"
|
||||
onClick={this.handleShortResetClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="ct-character-header-tablet__button-icon">
|
||||
<ThemedShortRestSvg theme={theme} />
|
||||
</div>
|
||||
<span className="ct-character-header-tablet__button-label">
|
||||
Short Rest
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--long-rest">
|
||||
<div
|
||||
className="ct-character-header-tablet__button"
|
||||
onClick={this.handleLongResetClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="ct-character-header-tablet__button-icon">
|
||||
<ThemedLongRestSvg theme={theme} />
|
||||
</div>
|
||||
<span className="ct-character-header-tablet__button-label">
|
||||
Long Rest
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--builder">
|
||||
<Tooltip
|
||||
isDarkMode={theme.isDarkMode}
|
||||
title="Go to builder"
|
||||
className="ct-character-header-tablet__builder"
|
||||
>
|
||||
<Link
|
||||
href={builderUrl}
|
||||
className="ct-character-header-tablet__builder-link"
|
||||
useRouter
|
||||
>
|
||||
<LightBuilderSvg />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--summary">
|
||||
<StatusSummaryMobile
|
||||
hitPointInfo={hitPointInfo}
|
||||
fails={fails}
|
||||
successes={successes}
|
||||
deathCause={deathCause}
|
||||
inspiration={inspiration}
|
||||
ruleData={ruleData}
|
||||
onHealthClick={this.handleHealthSummaryClick}
|
||||
onInspirationClick={this.handleInspirationClick}
|
||||
isInteractive={!isReadonly}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-character-header-tablet">
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group-tidbits">
|
||||
<CharacterHeaderInfo />
|
||||
</div>
|
||||
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--gap" />
|
||||
{this.renderSideContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
hitPointInfo: rulesEngineSelectors.getHitPointInfo(state),
|
||||
fails: rulesEngineSelectors.getDeathSavesFailCount(state),
|
||||
successes: rulesEngineSelectors.getDeathSavesSuccessCount(state),
|
||||
inspiration: rulesEngineSelectors.getInspiration(state),
|
||||
builderUrl: sheetAppSelectors.getBuilderUrl(state),
|
||||
campaign: rulesEngineSelectors.getCampaign(state),
|
||||
items: rulesEngineSelectors.getInventory(state),
|
||||
preferences: rulesEngineSelectors.getCharacterPreferences(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
deathCause: rulesEngineSelectors.getDeathCause(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
status: characterSelectors.getStatusSlug(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CharacterHeaderTabletContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<CharacterHeaderTablet paneHistoryStart={paneHistoryStart} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CharacterHeaderTabletContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterHeaderTablet from "./CharacterHeaderTablet";
|
||||
|
||||
export default CharacterHeaderTablet;
|
||||
export { CharacterHeaderTablet };
|
||||
@@ -0,0 +1,676 @@
|
||||
import { throttle } from "lodash";
|
||||
import { createRef, PureComponent } from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
characterEnvSelectors,
|
||||
Constants,
|
||||
FormatUtils,
|
||||
rulesEngineSelectors,
|
||||
CharacterTheme,
|
||||
CharacterPreferences,
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { GameLogContextProvider } from "@dndbeyond/game-log-components";
|
||||
|
||||
import { Link } from "~/components/Link";
|
||||
import { NotificationSystem } from "~/components/NotificationSystem";
|
||||
import { PremadeCharacterEditStatus } from "~/components/PremadeCharacterEditStatus";
|
||||
import { usePositioning } from "~/hooks/usePositioning";
|
||||
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 ClaimPremadeButton from "../../components/ClaimPremadeButton";
|
||||
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 CharacterSheetGuidedTour from "../CharacterSheetGuidedTour";
|
||||
import CharacterSheetMobile from "../CharacterSheetMobile";
|
||||
import CharacterSheetTablet from "../CharacterSheetTablet";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
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;
|
||||
characterStatus: CharacterStatusSlug | null;
|
||||
userRoles: UserRoles;
|
||||
}
|
||||
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,
|
||||
} = 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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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 ? 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>You don't have access to this page.</p>
|
||||
<p>
|
||||
If you are trying to access a character sheet, make sure its
|
||||
privacy setting is configured correctly for you to access it.
|
||||
</p>
|
||||
<p>
|
||||
It is also possible that you are trying to enter the 403rd level
|
||||
of the endless dungeon and the ancient dragon Rylzrayrth is rising
|
||||
up to block your path...
|
||||
</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,
|
||||
} = 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>
|
||||
<GameLogContextProvider
|
||||
baseUrl={gameLog.apiEndpoint}
|
||||
ddbApiUrl={gameLog.ddbApiEndpoint}
|
||||
diceServiceUrl={diceFeatureConfiguration.apiEndpoint}
|
||||
authUrl={authEndpoint}
|
||||
diceThumbnailsUrl={`${diceFeatureConfiguration.assetBaseLocation}/images/thumbnails`}
|
||||
entityId={characterId ? characterId.toString() : ""}
|
||||
>
|
||||
<div className={classNames.join(" ")}>
|
||||
{this.renderSyncBlocker()}
|
||||
<div
|
||||
className="ct-character-sheet__inner"
|
||||
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} />
|
||||
)}
|
||||
{everythingIsLoadedWithoutErrors &&
|
||||
characterStatus === CharacterStatusSlug.PREMADE &&
|
||||
isReadonly && <ClaimPremadeButton />}
|
||||
</div>
|
||||
</GameLogContextProvider>
|
||||
</CharacterSheetGuidedTour>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CharacterSheetWrapper = (props) => {
|
||||
const { setTitle } = useHeadContext();
|
||||
const { getSheetPositioning } = usePositioning();
|
||||
return (
|
||||
<CharacterSheet
|
||||
{...props}
|
||||
setTitle={setTitle}
|
||||
sheetPosition={getSheetPositioning()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps)(CharacterSheetWrapper);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSheet from "./CharacterSheet";
|
||||
|
||||
export default CharacterSheet;
|
||||
export { CharacterSheet };
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
import { SheetProvider } from "~/subApps/sheet/contexts/Sheet";
|
||||
|
||||
import ErrorBoundary from "../../../Shared/components/ErrorBoundary";
|
||||
import CharacterSheet from "../CharacterSheet";
|
||||
|
||||
export function CharacterSheetContainer({
|
||||
authEndpoint,
|
||||
}: {
|
||||
authEndpoint: string;
|
||||
}) {
|
||||
useLayoutEffect(() => {
|
||||
document
|
||||
.getElementsByTagName("body")[0]
|
||||
.classList.add(
|
||||
"site",
|
||||
"body-rpgcharacter-sheet",
|
||||
"site-dndbeyond",
|
||||
"body-rpgcharacter",
|
||||
"body-rpgcharacter-details"
|
||||
);
|
||||
return () => {
|
||||
document
|
||||
.getElementsByTagName("body")[0]
|
||||
.classList.remove(
|
||||
"site",
|
||||
"body-rpgcharacter-sheet",
|
||||
"site-dndbeyond",
|
||||
"body-rpgcharacter",
|
||||
"body-rpgcharacter-details"
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<SheetProvider>
|
||||
<CharacterSheet authEndpoint={authEndpoint} />
|
||||
</SheetProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default CharacterSheetContainer;
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSheetContainer from "./CharacterSheetContainer";
|
||||
|
||||
export default CharacterSheetContainer;
|
||||
export { CharacterSheetContainer };
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { spring, TransitionMotion } from "@serprex/react-motion";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import { ThemeStyles } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
DecorationInfo,
|
||||
DecorationUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { SidebarInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { usePositioning } from "~/hooks/usePositioning";
|
||||
import { Sidebar } from "~/subApps/sheet/components/Sidebar";
|
||||
import { SidebarPositionInfo } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import Subsections from "../../components/Subsections";
|
||||
import { DESKTOP_LARGE_COMPONENT_START_WIDTH } from "../../config";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import BackdropStyles from "../BackdropStyles";
|
||||
import { CharacterHeaderDesktop } from "../CharacterHeaderDesktop";
|
||||
import Combat from "../Combat";
|
||||
import PrimaryBox from "../PrimaryBox";
|
||||
import ProficiencyGroupsDesktop from "../ProficiencyGroupsDesktop";
|
||||
import QuickInfo from "../QuickInfo";
|
||||
import SavingThrowsDesktop from "../SavingThrowsDesktop";
|
||||
import SensesDesktop from "../SensesDesktop";
|
||||
import SkillsDesktop from "../SkillsDesktop";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
sidebarInfo: SidebarInfo;
|
||||
sidebarPosition: SidebarPositionInfo;
|
||||
decorationInfo: DecorationInfo;
|
||||
envDimensions: AppEnvDimensionsState;
|
||||
}
|
||||
interface State {
|
||||
swipedAmount: number;
|
||||
}
|
||||
|
||||
class CharacterSheetDesktop extends React.PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
swipedAmount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
getPositionX = (swipedAmount: number): number => {
|
||||
const { sidebarInfo } = this.props;
|
||||
|
||||
let position: number =
|
||||
(sidebarInfo.isVisible ? 0 : sidebarInfo.width) + swipedAmount;
|
||||
|
||||
if (swipedAmount > 0) {
|
||||
return Math.min(sidebarInfo.width, position);
|
||||
} else {
|
||||
return Math.max(0, position);
|
||||
}
|
||||
};
|
||||
|
||||
renderContent = (): React.ReactNode => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CharacterHeaderDesktop />
|
||||
<QuickInfo />
|
||||
<Subsections>
|
||||
<SavingThrowsDesktop />
|
||||
<SensesDesktop />
|
||||
<ProficiencyGroupsDesktop />
|
||||
<SkillsDesktop />
|
||||
<Combat />
|
||||
<PrimaryBox />
|
||||
</Subsections>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
renderSmall = (): React.ReactNode => {
|
||||
const { swipedAmount } = this.state;
|
||||
const { sidebarPosition } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-character-sheet-desktop">
|
||||
{this.renderContent()}
|
||||
<TransitionMotion
|
||||
styles={[
|
||||
{
|
||||
key: "1",
|
||||
style: {
|
||||
transform: spring(this.getPositionX(swipedAmount), {
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}),
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(interpolatedStyles) => (
|
||||
<React.Fragment>
|
||||
{interpolatedStyles.map((config) => (
|
||||
<>
|
||||
<Sidebar
|
||||
key={config.key}
|
||||
style={{
|
||||
...sidebarPosition,
|
||||
transform: `translateX(${config.style.transform}px)`,
|
||||
}}
|
||||
setSwipedAmount={(swipedAmount) =>
|
||||
this.setState({ swipedAmount })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderLarge = (): React.ReactNode => {
|
||||
const { sidebarPosition } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-character-sheet-desktop">
|
||||
{this.renderContent()}
|
||||
<Sidebar style={sidebarPosition} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderSheet = (): React.ReactNode => {
|
||||
const { envDimensions } = this.props;
|
||||
|
||||
if (envDimensions.window.width < DESKTOP_LARGE_COMPONENT_START_WIDTH) {
|
||||
return this.renderSmall();
|
||||
}
|
||||
|
||||
return this.renderLarge();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { decorationInfo } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderSheet()}
|
||||
<BackdropStyles
|
||||
backdrop={DecorationUtils.getBackdropInfo(decorationInfo)}
|
||||
/>
|
||||
<ThemeStyles decorationInfo={decorationInfo} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
decorationInfo: rulesEngineSelectors.getDecorationInfo(state),
|
||||
envDimensions: appEnvSelectors.getDimensions(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CharacterSheetContainer = (props) => {
|
||||
const { sidebar } = useSidebar();
|
||||
const { getSidebarPositioning } = usePositioning();
|
||||
|
||||
return (
|
||||
<CharacterSheetDesktop
|
||||
sidebarInfo={sidebar}
|
||||
sidebarPosition={getSidebarPositioning()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CharacterSheetContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSheetDesktop from "./CharacterSheetDesktop";
|
||||
|
||||
export default CharacterSheetDesktop;
|
||||
export { CharacterSheetDesktop };
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { connect, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
Constants,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||
|
||||
import { GuidedTour } from "../../../Shared/containers/GuidedTour";
|
||||
import { getCharacterSheetSteps } from "./getCharacterSheetSteps";
|
||||
|
||||
const CharacterSheetGuidedTour = ({ children }) => {
|
||||
const [step, setStep] = useState(0);
|
||||
const { characterSheetTourFlag } = useFeatureFlags();
|
||||
const isTablet = useMediaQuery("(min-width: 768px)");
|
||||
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||
const ready = useSelector(rulesEngineSelectors.isCharacterSheetReady);
|
||||
// const hasSpells = useSelector(rulesEngineSelectors.hasSpells);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const setCurrentStep = (currentStep) => {
|
||||
switch (currentStep) {
|
||||
case 11: {
|
||||
if (!isTablet && !isDesktop) {
|
||||
const toggle = document.querySelector(
|
||||
"[class^='styles_navToggle']"
|
||||
) as HTMLButtonElement;
|
||||
toggle?.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 13: {
|
||||
if (isTablet && !isDesktop) {
|
||||
const toggle = document.querySelector(
|
||||
"[class^='styles_navToggle']"
|
||||
) as HTMLButtonElement;
|
||||
toggle?.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setStep(currentStep);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// If character sheet has loaded, enable tour
|
||||
if (ready) setLoading(false);
|
||||
}, [ready]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{characterSheetTourFlag && !loading ? (
|
||||
<GuidedTour
|
||||
steps={getCharacterSheetSteps(false, isTablet, isDesktop)}
|
||||
step={step}
|
||||
setStep={setCurrentStep}
|
||||
showOnFirstLoad={true}
|
||||
cookieName="characterSheetGuidedTour"
|
||||
>
|
||||
{children}
|
||||
</GuidedTour>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterSheetGuidedTour;
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
import { StepType } from "@reactour/tour";
|
||||
|
||||
import { GuidedTourStep } from "~/tools/js/Shared/containers/GuidedTour";
|
||||
|
||||
/**
|
||||
* CHARACTER SHEET TOUR STEPS
|
||||
**/
|
||||
export const getCharacterSheetSteps = (
|
||||
hasSpells: boolean,
|
||||
isTablet: boolean,
|
||||
isDesktop: boolean
|
||||
): StepType[] => {
|
||||
const getSelector = (
|
||||
mobileSelector: string,
|
||||
tabletSelector?: string,
|
||||
desktopSelector?: string
|
||||
) =>
|
||||
isDesktop && desktopSelector
|
||||
? desktopSelector
|
||||
: isTablet && tabletSelector
|
||||
? tabletSelector
|
||||
: mobileSelector;
|
||||
|
||||
const stepList = [
|
||||
{
|
||||
selector: getSelector("html"),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Welcome!"
|
||||
content={
|
||||
<>
|
||||
Welcome to your character sheet! Here, you can find information
|
||||
about your character, roll dice, and find more information about
|
||||
other characters in your game. If you ever have any questions
|
||||
about rules or what something is for, you can click on almost
|
||||
anything to learn more!
|
||||
</>
|
||||
}
|
||||
showClose
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(".dice-toolbar"),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Digital Dice"
|
||||
content={
|
||||
<>
|
||||
Click here if you need to make a custom dice roll. Your dice
|
||||
collection can be found{" "}
|
||||
<a
|
||||
href="https://www.dndbeyond.com/my-dice"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-character-header-mobile__group-tidbits",
|
||||
".ct-character-header-tablet__group-tidbits",
|
||||
".ct-character-header-desktop__group-tidbits"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Character Overview"
|
||||
content={
|
||||
<>
|
||||
Basic information about your character such as Name, Species,
|
||||
Class, and Level can be found here. Click this area to change your
|
||||
character's name, sheet styles, and modify other settings for your
|
||||
character.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-character-header-desktop__group--builder",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Character Builder"
|
||||
content={
|
||||
<>
|
||||
Click here to visit the Character Builder. Each time you level up,
|
||||
check here to see how to develop your character.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-main-mobile__abilities",
|
||||
".ct-main-tablet__abilities",
|
||||
".ct-quick-info__abilities"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Ability Scores"
|
||||
content={
|
||||
<>
|
||||
Much of what your character does in the game depends on his or her
|
||||
six abilities: Strength, Dexterity, Constitution, Intelligence,
|
||||
Wisdom, and Charisma. Learn more about an ability by clicking on
|
||||
it.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--proficiency",
|
||||
".ct-combat-tablet__extra--proficiency",
|
||||
".ct-quick-info__box--proficiency"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Proficiency Bonus"
|
||||
content={
|
||||
<>
|
||||
Proficiency is added to rolls made to accomplish tasks with which
|
||||
your character is proficient. The bonus is automatically added to
|
||||
rolls when it is needed. Click here to learn more about how
|
||||
Proficiency Bonuses are used.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--speed",
|
||||
".ct-combat-tablet__extra--speed",
|
||||
".ct-quick-info__box--speed"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Speed"
|
||||
content={
|
||||
<>
|
||||
In each round of combat, your character can move up to the total
|
||||
distance indicated by your speed. Click here to learn more or
|
||||
customize your speed.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-status-summary-mobile__health",
|
||||
".ct-status-summary-mobile__health",
|
||||
".ct-health-summary"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Hit Points"
|
||||
content={
|
||||
<>
|
||||
In this area, you can manage your character's hit points. When
|
||||
your character reaches zero hit points, they are on the brink of
|
||||
death and begin making death saving throws.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--initiative",
|
||||
".ct-combat-tablet__extra--initiative",
|
||||
".ct-combat__summary-group--initiative"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Initiative"
|
||||
content={
|
||||
<>
|
||||
Initiative rolls determine the order in which you go in combat.
|
||||
The higher the number, the sooner you get to fight. Click here to
|
||||
roll initiative!
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-combat-mobile__extra--ac",
|
||||
".ct-combat-tablet__extra--ac",
|
||||
".ct-combat__summary-group--ac"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Armor Class"
|
||||
content={
|
||||
<>
|
||||
Your Armor Class (AC) represents how well your character avoids
|
||||
being wounded in battle.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: getSelector(
|
||||
".ct-main-mobile__saving-throws",
|
||||
".ct-saving-throws-box",
|
||||
".ct-saving-throws-box"
|
||||
),
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Saving Throws"
|
||||
content={
|
||||
<>
|
||||
Your DM may ask you to roll a saving throw (or make a save) to
|
||||
resist an incoming effect. Click on a save to automatically roll.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(isDesktop || isTablet
|
||||
? [
|
||||
{
|
||||
selector: ".ct-proficiency-groups-box",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Proficiencies"
|
||||
content={
|
||||
<>
|
||||
Proficiencies tell you what tools, equipment, and languages
|
||||
your character is skilled at using.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-skills-box",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Skills"
|
||||
content={
|
||||
<>
|
||||
If you wish to perform an action, your DM will determine
|
||||
which of these skills you will use. Click on a skill to roll
|
||||
or learn more.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--skills",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Skills"
|
||||
content={
|
||||
<>
|
||||
If you wish to perform an action, your DM will determine
|
||||
which of these skills you will use. Click on a skill to roll
|
||||
or learn more.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]),
|
||||
...(!isDesktop
|
||||
? [
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--actions",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Actions"
|
||||
content={
|
||||
<>
|
||||
When you take your action on your turn, you can take one of
|
||||
the actions presented here. You can track actions or make
|
||||
rolls to perform them by clicking in this panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--equipment",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Inventory"
|
||||
content={
|
||||
<>
|
||||
View and manage your character's items and coin from this
|
||||
panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--spells",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Spells"
|
||||
content={
|
||||
<>
|
||||
If your character has the ability to cast spells, look here
|
||||
for a list of spells and to track your spellcasting.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--features",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Features & Traits"
|
||||
content={
|
||||
<>
|
||||
This section describes the source of your character's
|
||||
abilities. How this manifests in your character's
|
||||
personality is up to you.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(!isTablet
|
||||
? [
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--proficiencies",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Proficiencies"
|
||||
content={
|
||||
<>
|
||||
Proficiencies tell you what tools, equipment, and
|
||||
languages your character is skilled at using.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
selector: ".ct-quick-nav__menu-item--description",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Description"
|
||||
content={
|
||||
<>
|
||||
Use the description panel to tell your character's story.
|
||||
Your character's background features are also found here.
|
||||
</>
|
||||
}
|
||||
showClose
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
selector: ".ct-primary-box__tab--actions",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Actions"
|
||||
content={
|
||||
<>
|
||||
When you take your action on your turn, you can take one of
|
||||
the actions presented here. You can track actions or make
|
||||
rolls to perform them by clicking in this panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(hasSpells
|
||||
? [
|
||||
{
|
||||
selector: ".ct-primary-box__tab--spells",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Spells"
|
||||
content={
|
||||
<>
|
||||
If your character has the ability to cast spells, look
|
||||
here for a list of spells and to track your
|
||||
spellcasting.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
selector: ".ct-primary-box__tab--equipment",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Inventory"
|
||||
content={
|
||||
<>
|
||||
View and manage your character's items and coin from this
|
||||
panel.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-primary-box__tab--features",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Features & Traits"
|
||||
content={
|
||||
<>
|
||||
This section describes the source of your character's
|
||||
abilities. How this manifests in your character's
|
||||
personality is up to you.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
selector: ".ct-primary-box__tab--description",
|
||||
content: (
|
||||
<GuidedTourStep
|
||||
title="Description"
|
||||
content={
|
||||
<>
|
||||
Use the description panel to tell your character's story.
|
||||
Your character's background features are also found here.
|
||||
</>
|
||||
}
|
||||
showClose
|
||||
/>
|
||||
),
|
||||
},
|
||||
]),
|
||||
];
|
||||
return stepList.filter((s) => s);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSheetGuidedTour from "./CharacterSheetGuidedTour";
|
||||
|
||||
export default CharacterSheetGuidedTour;
|
||||
export { CharacterSheetGuidedTour };
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
import { spring, TransitionMotion } from "@serprex/react-motion";
|
||||
import React, { useContext } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
ThemeStyles,
|
||||
DarkAbilitiesSvg,
|
||||
DarkActionsSvg,
|
||||
DarkSkillsSvg,
|
||||
DarkEquipmentSvg,
|
||||
DarkSpellsSvg,
|
||||
DarkProficienciesSvg,
|
||||
DarkDescriptionSvg,
|
||||
DarkNotesSvg,
|
||||
DarkFeaturesSvg,
|
||||
DarkExtrasSvg,
|
||||
LightAbilitiesSvg,
|
||||
LightActionsSvg,
|
||||
LightSkillsSvg,
|
||||
LightEquipmentSvg,
|
||||
LightSpellsSvg,
|
||||
LightProficienciesSvg,
|
||||
LightDescriptionSvg,
|
||||
LightNotesSvg,
|
||||
LightFeaturesSvg,
|
||||
LightExtrasSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
DecorationInfo,
|
||||
DecorationUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { SidebarInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { usePositioning } from "~/hooks/usePositioning";
|
||||
import { Sidebar } from "~/subApps/sheet/components/Sidebar";
|
||||
import { SidebarPositionInfo } from "~/subApps/sheet/components/Sidebar/types";
|
||||
import { SheetContext } from "~/subApps/sheet/contexts/Sheet";
|
||||
import { MobileSections } from "~/subApps/sheet/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import {
|
||||
ComponentCarousel,
|
||||
ComponentCarouselItem,
|
||||
} from "../../components/ComponentCarousel";
|
||||
import SectionPlaceholder from "../../components/SectionPlaceholder";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import CharacterHeaderMobile from "../CharacterHeaderMobile";
|
||||
import ActionsMobile from "../mobile/ActionsMobile";
|
||||
import CombatMobile from "../mobile/CombatMobile";
|
||||
import { DescriptionMobile } from "../mobile/DescriptionMobile";
|
||||
import EquipmentMobile from "../mobile/EquipmentMobile";
|
||||
import ExtrasMobile from "../mobile/ExtrasMobile";
|
||||
import FeaturesMobile from "../mobile/FeaturesMobile";
|
||||
import MainMobile from "../mobile/MainMobile";
|
||||
import NotesMobile from "../mobile/NotesMobile";
|
||||
import { ProficiencyGroupsMobile } from "../mobile/ProficiencyGroupsMobile";
|
||||
import SkillsMobile from "../mobile/SkillsMobile";
|
||||
import SpellsMobile from "../mobile/SpellsMobile";
|
||||
|
||||
interface Props {
|
||||
sidebarInfo: SidebarInfo;
|
||||
sidebarPosition: SidebarPositionInfo;
|
||||
envDimensions: AppEnvDimensionsState;
|
||||
decorationInfo: DecorationInfo;
|
||||
hasSpells: boolean;
|
||||
isReadonly: boolean;
|
||||
mobileActiveSectionId: MobileSections;
|
||||
setMobileActiveSectionId: (id: MobileSections) => void;
|
||||
}
|
||||
interface State {
|
||||
swipedAmount: number;
|
||||
}
|
||||
class CharacterSheetMobile extends React.PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
swipedAmount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
handleComponentChange = (id: MobileSections): void => {
|
||||
const { setMobileActiveSectionId } = this.props;
|
||||
setMobileActiveSectionId(id);
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
handleItemChange = (newKey: MobileSections, oldKey: MobileSections): void => {
|
||||
if (newKey !== oldKey) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
const { setMobileActiveSectionId } = this.props;
|
||||
setMobileActiveSectionId(newKey);
|
||||
};
|
||||
|
||||
getPositionX = (swipedAmount: number): number => {
|
||||
const { sidebarInfo } = this.props;
|
||||
|
||||
let position: number =
|
||||
(sidebarInfo.isVisible ? 0 : sidebarInfo.width) + swipedAmount;
|
||||
|
||||
if (swipedAmount > 0) {
|
||||
return Math.min(sidebarInfo.width, position);
|
||||
} else {
|
||||
return Math.max(0, position);
|
||||
}
|
||||
};
|
||||
|
||||
renderSections = (activeSectionId: MobileSections): React.ReactNode => {
|
||||
const { envDimensions, hasSpells, isReadonly, decorationInfo } = this.props;
|
||||
|
||||
const isDarkMode = DecorationUtils.isDarkMode(decorationInfo);
|
||||
|
||||
return (
|
||||
<ComponentCarousel
|
||||
activeItemKey={activeSectionId}
|
||||
onItemChange={this.handleItemChange}
|
||||
envDimensions={envDimensions}
|
||||
>
|
||||
<ComponentCarouselItem
|
||||
itemKey="main"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Abilities, Saves, Senses",
|
||||
IconComponent: isDarkMode ? LightAbilitiesSvg : DarkAbilitiesSvg,
|
||||
}}
|
||||
ContentComponent={MainMobile}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="skills"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Skills",
|
||||
IconComponent: isDarkMode ? LightSkillsSvg : DarkSkillsSvg,
|
||||
}}
|
||||
ContentComponent={SkillsMobile}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="actions"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Actions",
|
||||
IconComponent: isDarkMode ? LightActionsSvg : DarkActionsSvg,
|
||||
}}
|
||||
ContentComponent={ActionsMobile}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="equipment"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Inventory",
|
||||
IconComponent: isDarkMode ? LightEquipmentSvg : DarkEquipmentSvg,
|
||||
}}
|
||||
ContentComponent={EquipmentMobile}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="spells"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Spells",
|
||||
IconComponent: isDarkMode ? LightSpellsSvg : DarkSpellsSvg,
|
||||
}}
|
||||
ContentComponent={SpellsMobile}
|
||||
isEnabled={hasSpells}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="features_traits"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Features & Traits",
|
||||
IconComponent: isDarkMode ? LightFeaturesSvg : DarkFeaturesSvg,
|
||||
}}
|
||||
ContentComponent={FeaturesMobile}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="proficiencies"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Proficiencies & Training",
|
||||
IconComponent: isDarkMode
|
||||
? LightProficienciesSvg
|
||||
: DarkProficienciesSvg,
|
||||
}}
|
||||
ContentComponent={ProficiencyGroupsMobile}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="description"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Background",
|
||||
IconComponent: isDarkMode
|
||||
? LightDescriptionSvg
|
||||
: DarkDescriptionSvg,
|
||||
}}
|
||||
ContentComponent={DescriptionMobile}
|
||||
isEnabled={!isReadonly}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="notes"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Notes",
|
||||
IconComponent: isDarkMode ? LightNotesSvg : DarkNotesSvg,
|
||||
}}
|
||||
ContentComponent={NotesMobile}
|
||||
isEnabled={!isReadonly}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="extras"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Extras",
|
||||
IconComponent: isDarkMode ? LightExtrasSvg : DarkExtrasSvg,
|
||||
}}
|
||||
ContentComponent={ExtrasMobile}
|
||||
isEnabled={!isReadonly}
|
||||
/>
|
||||
</ComponentCarousel>
|
||||
);
|
||||
};
|
||||
|
||||
renderSidebar = (): React.ReactNode => {
|
||||
const { swipedAmount } = this.state;
|
||||
const { sidebarPosition } = this.props;
|
||||
return (
|
||||
<TransitionMotion
|
||||
styles={[
|
||||
{
|
||||
key: "1",
|
||||
style: {
|
||||
transform: spring(this.getPositionX(swipedAmount), {
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}),
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(interpolatedStyles) => (
|
||||
<React.Fragment>
|
||||
{interpolatedStyles.map((config) => (
|
||||
<Sidebar
|
||||
key={config.key}
|
||||
style={{
|
||||
...sidebarPosition,
|
||||
transform: `translateX(${config.style.transform}px)`,
|
||||
}}
|
||||
setSwipedAmount={(swipedAmount) =>
|
||||
this.setState({ swipedAmount })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { decorationInfo } = this.props;
|
||||
|
||||
return (
|
||||
<SheetContext.Consumer>
|
||||
{({ mobileActiveSectionId }) => (
|
||||
<div
|
||||
className={`ct-character-sheet-mobile ${
|
||||
DecorationUtils.isDarkMode(decorationInfo)
|
||||
? "ct-character-sheet-mobile--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="ct-character-sheet-mobile__header">
|
||||
<CharacterHeaderMobile />
|
||||
<CombatMobile />
|
||||
</div>
|
||||
{this.renderSections(mobileActiveSectionId)}
|
||||
{this.renderSidebar()}
|
||||
<ThemeStyles decorationInfo={decorationInfo} />
|
||||
</div>
|
||||
)}
|
||||
</SheetContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
decorationInfo: rulesEngineSelectors.getDecorationInfo(state),
|
||||
hasSpells: rulesEngineSelectors.hasSpells(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
envDimensions: appEnvSelectors.getDimensions(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CharacterSheetMobileContainer = (props) => {
|
||||
const { mobileActiveSectionId, setMobileActiveSectionId } =
|
||||
useContext(SheetContext);
|
||||
const { sidebar } = useSidebar();
|
||||
const { getSidebarPositioning } = usePositioning();
|
||||
|
||||
return (
|
||||
<CharacterSheetMobile
|
||||
mobileActiveSectionId={mobileActiveSectionId}
|
||||
setMobileActiveSectionId={setMobileActiveSectionId}
|
||||
sidebarInfo={sidebar}
|
||||
sidebarPosition={getSidebarPositioning()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CharacterSheetMobileContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSheetMobile from "./CharacterSheetMobile";
|
||||
|
||||
export default CharacterSheetMobile;
|
||||
export { CharacterSheetMobile };
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import { spring, TransitionMotion } from "@serprex/react-motion";
|
||||
import React, { useContext } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
ThemeStyles,
|
||||
DarkAbilitiesSvg,
|
||||
DarkActionsSvg,
|
||||
DarkEquipmentSvg,
|
||||
DarkSpellsSvg,
|
||||
DarkDescriptionSvg,
|
||||
DarkNotesSvg,
|
||||
DarkFeaturesSvg,
|
||||
DarkExtrasSvg,
|
||||
LightAbilitiesSvg,
|
||||
LightActionsSvg,
|
||||
LightEquipmentSvg,
|
||||
LightSpellsSvg,
|
||||
LightDescriptionSvg,
|
||||
LightNotesSvg,
|
||||
LightFeaturesSvg,
|
||||
LightExtrasSvg,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
DecorationUtils,
|
||||
DecorationInfo,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { SidebarInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { usePositioning } from "~/hooks/usePositioning";
|
||||
import { Sidebar } from "~/subApps/sheet/components/Sidebar";
|
||||
import { SidebarPositionInfo } from "~/subApps/sheet/components/Sidebar/types";
|
||||
import { SheetContext } from "~/subApps/sheet/contexts/Sheet";
|
||||
import { MobileSections } from "~/subApps/sheet/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import {
|
||||
ComponentCarousel,
|
||||
ComponentCarouselItem,
|
||||
} from "../../components/ComponentCarousel";
|
||||
import SectionPlaceholder from "../../components/SectionPlaceholder";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import BackdropStyles from "../BackdropStyles";
|
||||
import CharacterHeaderTablet from "../CharacterHeaderTablet";
|
||||
import ActionsTablet from "../tablet/ActionsTablet";
|
||||
import CombatTablet from "../tablet/CombatTablet";
|
||||
import { DescriptionTablet } from "../tablet/DescriptionTablet";
|
||||
import EquipmentTablet from "../tablet/EquipmentTablet";
|
||||
import ExtrasTablet from "../tablet/ExtrasTablet";
|
||||
import FeaturesTablet from "../tablet/FeaturesTablet";
|
||||
import MainTablet from "../tablet/MainTablet";
|
||||
import NotesTablet from "../tablet/NotesTablet";
|
||||
import SpellsTablet from "../tablet/SpellsTablet";
|
||||
|
||||
interface Props {
|
||||
sidebarPosition: SidebarPositionInfo;
|
||||
sidebarInfo: SidebarInfo;
|
||||
envDimensions: AppEnvDimensionsState;
|
||||
decorationInfo: DecorationInfo;
|
||||
hasSpells: boolean;
|
||||
isReadonly: boolean;
|
||||
mobileActiveSectionId: MobileSections;
|
||||
setMobileActiveSectionId: (id: MobileSections) => void;
|
||||
}
|
||||
interface State {
|
||||
swipedAmount: number;
|
||||
}
|
||||
class CharacterSheetTablet extends React.PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
swipedAmount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
handleComponentChange = (id: MobileSections): void => {
|
||||
const { setMobileActiveSectionId } = this.props;
|
||||
setMobileActiveSectionId(id);
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
handleItemChange = (newKey: MobileSections, oldKey: MobileSections): void => {
|
||||
if (newKey !== oldKey) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
const { setMobileActiveSectionId } = this.props;
|
||||
setMobileActiveSectionId(newKey);
|
||||
};
|
||||
|
||||
getPositionX = (swipedAmount: number): number => {
|
||||
const { sidebarInfo } = this.props;
|
||||
|
||||
let position: number =
|
||||
(sidebarInfo.isVisible ? 0 : sidebarInfo.width) + swipedAmount;
|
||||
|
||||
if (swipedAmount > 0) {
|
||||
return Math.min(sidebarInfo.width, position);
|
||||
} else {
|
||||
return Math.max(0, position);
|
||||
}
|
||||
};
|
||||
|
||||
renderSections = (activeSectionId: MobileSections): React.ReactNode => {
|
||||
const { envDimensions, hasSpells, isReadonly, decorationInfo } = this.props;
|
||||
|
||||
const isDarkMode = DecorationUtils.isDarkMode(decorationInfo);
|
||||
|
||||
return (
|
||||
<ComponentCarousel
|
||||
activeItemKey={activeSectionId}
|
||||
onItemChange={this.handleItemChange}
|
||||
envDimensions={envDimensions}
|
||||
changingWaitTime={750}
|
||||
>
|
||||
<ComponentCarouselItem
|
||||
itemKey="main"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Abilities, Saves, Senses",
|
||||
IconComponent: isDarkMode ? LightAbilitiesSvg : DarkAbilitiesSvg,
|
||||
}}
|
||||
ContentComponent={MainTablet}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="actions"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Actions",
|
||||
IconComponent: isDarkMode ? LightActionsSvg : DarkActionsSvg,
|
||||
}}
|
||||
ContentComponent={ActionsTablet}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="equipment"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Equipment",
|
||||
IconComponent: isDarkMode ? LightEquipmentSvg : DarkEquipmentSvg,
|
||||
}}
|
||||
ContentComponent={EquipmentTablet}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="spells"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Spells",
|
||||
IconComponent: isDarkMode ? LightSpellsSvg : DarkSpellsSvg,
|
||||
}}
|
||||
ContentComponent={SpellsTablet}
|
||||
isEnabled={hasSpells}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="features_traits"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Features & Traits",
|
||||
IconComponent: isDarkMode ? LightFeaturesSvg : DarkFeaturesSvg,
|
||||
}}
|
||||
ContentComponent={FeaturesTablet}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="description"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Description",
|
||||
IconComponent: isDarkMode
|
||||
? LightDescriptionSvg
|
||||
: DarkDescriptionSvg,
|
||||
}}
|
||||
ContentComponent={DescriptionTablet}
|
||||
isEnabled={!isReadonly}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="notes"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Notes",
|
||||
IconComponent: isDarkMode ? LightNotesSvg : DarkNotesSvg,
|
||||
}}
|
||||
ContentComponent={NotesTablet}
|
||||
isEnabled={!isReadonly}
|
||||
/>
|
||||
<ComponentCarouselItem
|
||||
itemKey="extras"
|
||||
PlaceholderComponent={SectionPlaceholder}
|
||||
placeholderProps={{
|
||||
name: "Extras",
|
||||
IconComponent: isDarkMode ? LightExtrasSvg : DarkExtrasSvg,
|
||||
}}
|
||||
ContentComponent={ExtrasTablet}
|
||||
isEnabled={!isReadonly}
|
||||
/>
|
||||
</ComponentCarousel>
|
||||
);
|
||||
};
|
||||
|
||||
renderSidebar = (): React.ReactNode => {
|
||||
const { swipedAmount } = this.state;
|
||||
const { sidebarPosition } = this.props;
|
||||
|
||||
return (
|
||||
<TransitionMotion
|
||||
styles={[
|
||||
{
|
||||
key: "1",
|
||||
style: {
|
||||
transform: spring(this.getPositionX(swipedAmount), {
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}),
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(interpolatedStyles) => (
|
||||
<React.Fragment>
|
||||
{interpolatedStyles.map((config) => (
|
||||
<Sidebar
|
||||
key={config.key}
|
||||
style={{
|
||||
...sidebarPosition,
|
||||
transform: `translateX(${config.style.transform}px)`,
|
||||
}}
|
||||
setSwipedAmount={(swipedAmount) =>
|
||||
this.setState({ swipedAmount })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { decorationInfo } = this.props;
|
||||
|
||||
return (
|
||||
<SheetContext.Consumer>
|
||||
{({ mobileActiveSectionId }) => (
|
||||
<div className="ct-character-sheet-tablet">
|
||||
<CharacterHeaderTablet />
|
||||
<CombatTablet />
|
||||
{this.renderSections(mobileActiveSectionId)}
|
||||
{this.renderSidebar()}
|
||||
<ThemeStyles decorationInfo={decorationInfo} />
|
||||
<BackdropStyles
|
||||
backdrop={DecorationUtils.getBackdropInfo(decorationInfo)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SheetContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
decorationInfo: rulesEngineSelectors.getDecorationInfo(state),
|
||||
hasSpells: rulesEngineSelectors.hasSpells(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
envDimensions: appEnvSelectors.getDimensions(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CharacterSheetTabletContainer = (props) => {
|
||||
const { mobileActiveSectionId, setMobileActiveSectionId } =
|
||||
useContext(SheetContext);
|
||||
const { sidebar } = useSidebar();
|
||||
const { getSidebarPositioning } = usePositioning();
|
||||
|
||||
return (
|
||||
<CharacterSheetTablet
|
||||
mobileActiveSectionId={mobileActiveSectionId}
|
||||
setMobileActiveSectionId={setMobileActiveSectionId}
|
||||
sidebarInfo={sidebar}
|
||||
sidebarPosition={getSidebarPositioning()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CharacterSheetTabletContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSheetTablet from "./CharacterSheetTablet";
|
||||
|
||||
export default CharacterSheetTablet;
|
||||
export { CharacterSheetTablet };
|
||||
@@ -0,0 +1,204 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
ArmorClassBox,
|
||||
BoxBackground,
|
||||
SquaredBoxSvg344x95,
|
||||
SquaredBoxSvg408x95,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
AbilityLookup,
|
||||
Condition,
|
||||
DefenseAdjustmentGroup,
|
||||
SpellCasterInfo,
|
||||
RuleData,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { InitiativeBox } from "~/subApps/sheet/components/InitiativeBox";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import Subsection from "../../../Shared/components/Subsection";
|
||||
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import ConditionsSummary from "../../components/ConditionsSummary";
|
||||
import DefensesSummary from "../../components/DefensesSummary";
|
||||
import { SheetAppState } from "../../typings";
|
||||
|
||||
interface Props {
|
||||
armorClass: number;
|
||||
resistances: Array<DefenseAdjustmentGroup>;
|
||||
immunities: Array<DefenseAdjustmentGroup>;
|
||||
vulnerabilities: Array<DefenseAdjustmentGroup>;
|
||||
conditions: Array<Condition>;
|
||||
abilityLookup: AbilityLookup;
|
||||
ruleData: RuleData;
|
||||
spellCasterInfo: SpellCasterInfo;
|
||||
dimensions: AppEnvDimensionsState;
|
||||
theme: CharacterTheme;
|
||||
diceEnabled: boolean;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class Combat extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
handleArmorClassClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.ARMOR_MANAGE);
|
||||
};
|
||||
|
||||
handleDefensesSummaryClick = (evt: React.MouseEvent): void => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.handleDefensesSummaryShow();
|
||||
};
|
||||
|
||||
handleDefensesSummaryShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.DEFENSE_MANAGE);
|
||||
};
|
||||
|
||||
handleConditionSummaryClick = (evt: React.MouseEvent): void => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.handleConditionSummaryShow();
|
||||
};
|
||||
|
||||
handleConditionSummaryShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.CONDITION_MANAGE);
|
||||
};
|
||||
|
||||
renderDefault = (): React.ReactNode => {
|
||||
return (
|
||||
<div className="ct-combat__attacks-default">
|
||||
Equip weapons or add spells to see your attacks here.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
armorClass,
|
||||
resistances,
|
||||
vulnerabilities,
|
||||
immunities,
|
||||
conditions,
|
||||
theme,
|
||||
dimensions,
|
||||
} = this.props;
|
||||
|
||||
let BoxBackgroundComponent: React.ComponentType = SquaredBoxSvg344x95;
|
||||
if (dimensions.styleSizeType > StyleSizeTypeEnum.DESKTOP) {
|
||||
BoxBackgroundComponent = SquaredBoxSvg408x95;
|
||||
}
|
||||
|
||||
return (
|
||||
<Subsection name="Combat">
|
||||
<div className="ct-combat">
|
||||
<div className="ct-combat__summary">
|
||||
<div className="ct-combat__summary-group ct-combat__summary-group--initiative">
|
||||
<InitiativeBox />
|
||||
</div>
|
||||
<div className="ct-combat__summary-group ct-combat__summary-group--ac">
|
||||
<ArmorClassBox
|
||||
theme={theme}
|
||||
armorClass={armorClass}
|
||||
onClick={this.handleArmorClassClick}
|
||||
/>
|
||||
</div>
|
||||
<section className="ct-combat__summary-group ct-combat__summary-group--statuses">
|
||||
<div className="ct-combat__statuses">
|
||||
<BoxBackground
|
||||
StyleComponent={BoxBackgroundComponent}
|
||||
theme={theme}
|
||||
/>
|
||||
<h2 style={visuallyHidden}>Defenses and Conditions</h2>
|
||||
<div
|
||||
className="ct-combat__statuses-group ct-combat__statuses-group--defenses"
|
||||
onClick={this.handleDefensesSummaryClick}
|
||||
>
|
||||
<div
|
||||
className={`ct-combat__summary-label ${
|
||||
theme.isDarkMode
|
||||
? "ct-combat__summary-label--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Defenses
|
||||
</div>
|
||||
<DefensesSummary
|
||||
theme={theme}
|
||||
resistances={resistances}
|
||||
vulnerabilities={vulnerabilities}
|
||||
immunities={immunities}
|
||||
onClick={this.handleDefensesSummaryShow}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ct-combat__statuses-group ct-combat__statuses-group--conditions"
|
||||
style={
|
||||
theme?.isDarkMode
|
||||
? {
|
||||
borderColor: `${theme.themeColor}66`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClick={this.handleConditionSummaryClick}
|
||||
>
|
||||
<div
|
||||
className={`ct-combat__summary-label ${
|
||||
theme.isDarkMode
|
||||
? "ct-combat__summary-label--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Conditions
|
||||
</div>
|
||||
<ConditionsSummary
|
||||
theme={theme}
|
||||
conditions={conditions}
|
||||
onClick={this.handleConditionSummaryShow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</Subsection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
spellCasterInfo: rulesEngineSelectors.getSpellCasterInfo(state),
|
||||
armorClass: rulesEngineSelectors.getAcTotal(state),
|
||||
resistances: rulesEngineSelectors.getActiveGroupedResistances(state),
|
||||
immunities: rulesEngineSelectors.getActiveGroupedImmunities(state),
|
||||
vulnerabilities:
|
||||
rulesEngineSelectors.getActiveGroupedVulnerabilities(state),
|
||||
conditions: rulesEngineSelectors.getActiveConditions(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
abilityLookup: rulesEngineSelectors.getAbilityLookup(state),
|
||||
dimensions: appEnvSelectors.getDimensions(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CombatContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <Combat paneHistoryStart={paneHistoryStart} {...props} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CombatContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Combat from "./Combat";
|
||||
|
||||
export default Combat;
|
||||
export { Combat };
|
||||
@@ -0,0 +1,381 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import clsx from "clsx";
|
||||
import React, { useContext } from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
AlignmentContract,
|
||||
Background,
|
||||
CharacterTraits,
|
||||
Constants,
|
||||
SizeContract,
|
||||
rulesEngineSelectors,
|
||||
CharacterTheme,
|
||||
CharacterFeaturesManager,
|
||||
SnippetData,
|
||||
RuleData,
|
||||
AbilityLookup,
|
||||
DataOriginRefData,
|
||||
FeatManager,
|
||||
Action,
|
||||
ActionUtils,
|
||||
Spell,
|
||||
SpellUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { InfoItem } from "~/components/InfoItem";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
|
||||
|
||||
import {
|
||||
handleActionUseSet,
|
||||
handleSpellUseSet,
|
||||
} from "../../../../../handlers/commonHandlers";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import BackgroundDetail from "../../components/BackgroundDetail";
|
||||
import ContentGroup from "../../components/ContentGroup";
|
||||
import TraitContent from "../../components/TraitContent";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const DEFAULT_VALUE = "--";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
isVertical: boolean;
|
||||
background: Background | null;
|
||||
alignment: AlignmentContract | null;
|
||||
height: string | null;
|
||||
weight: number | null;
|
||||
size: SizeContract | null;
|
||||
faith: string | null;
|
||||
skin: string | null;
|
||||
eyes: string | null;
|
||||
hair: string | null;
|
||||
age: number | null;
|
||||
gender: string | null;
|
||||
traits: CharacterTraits;
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
characterFeaturesManager: CharacterFeaturesManager;
|
||||
snippetData: SnippetData;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
proficiencyBonus: number;
|
||||
paneContext: PaneInfo;
|
||||
}
|
||||
class Description extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
isVertical: false,
|
||||
};
|
||||
|
||||
handlePhysicalCharacteristicsClick = (): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryStart },
|
||||
isReadonly,
|
||||
} = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.DESCRIPTION);
|
||||
}
|
||||
};
|
||||
|
||||
handleBackgroundClick = (): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryStart },
|
||||
} = this.props;
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.BACKGROUND);
|
||||
};
|
||||
|
||||
handleFeatClick = (feat: FeatManager): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryStart },
|
||||
} = this.props;
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.FEAT_DETAIL,
|
||||
PaneIdentifierUtils.generateFeat(feat.getId())
|
||||
);
|
||||
};
|
||||
|
||||
handleTraitShow = (key: Constants.TraitTypeEnum): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryStart },
|
||||
isReadonly,
|
||||
} = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.TRAIT,
|
||||
PaneIdentifierUtils.generateTrait(key)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleActionClick = (action: Action): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryStart },
|
||||
} = this.props;
|
||||
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ACTION,
|
||||
PaneIdentifierUtils.generateAction(mappingId, mappingEntityTypeId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellDetailClick = (spell: Spell): void => {
|
||||
const {
|
||||
paneContext: { paneHistoryStart },
|
||||
} = this.props;
|
||||
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
if (mappingId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CHARACTER_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = (e: React.MouseEvent, onClick: Function): void => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
onClick();
|
||||
};
|
||||
|
||||
renderDescriptionItem = (
|
||||
value: number | string | null,
|
||||
fallback: string = DEFAULT_VALUE
|
||||
): React.ReactNode => {
|
||||
return value === null ? fallback : value;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatch,
|
||||
background,
|
||||
characterFeaturesManager,
|
||||
snippetData,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
dataOriginRefData,
|
||||
isReadonly,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
isVertical,
|
||||
size,
|
||||
height,
|
||||
weight,
|
||||
alignment,
|
||||
gender,
|
||||
eyes,
|
||||
hair,
|
||||
skin,
|
||||
age,
|
||||
faith,
|
||||
traits,
|
||||
} = this.props;
|
||||
|
||||
const infoItemProps = {
|
||||
role: "listitem",
|
||||
inline: isVertical,
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="ct-description">
|
||||
<h2 style={visuallyHidden}>Description</h2>
|
||||
<TabFilter
|
||||
filters={[
|
||||
{
|
||||
label: "Background",
|
||||
content: (
|
||||
<ContentGroup header="Background">
|
||||
<BackgroundDetail
|
||||
background={background}
|
||||
onClick={this.handleBackgroundClick}
|
||||
featuresManager={characterFeaturesManager}
|
||||
onActionUseSet={(action, uses) =>
|
||||
handleActionUseSet(action, uses, dispatch)
|
||||
}
|
||||
onSpellUseSet={(spell, uses) =>
|
||||
handleSpellUseSet(spell, uses, dispatch)
|
||||
}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
isReadonly={isReadonly}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
/>
|
||||
</ContentGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Characteristics",
|
||||
content: (
|
||||
<>
|
||||
<ContentGroup header="Characteristics">
|
||||
<div
|
||||
className={clsx([
|
||||
"ct-description__physical",
|
||||
styles.physical,
|
||||
])}
|
||||
role="list"
|
||||
onClick={(e) =>
|
||||
this.handleClick(
|
||||
e,
|
||||
this.handlePhysicalCharacteristicsClick
|
||||
)
|
||||
}
|
||||
>
|
||||
<InfoItem label="Alignment" {...infoItemProps}>
|
||||
{alignment === null ? DEFAULT_VALUE : alignment.name}
|
||||
</InfoItem>
|
||||
<InfoItem label="Gender" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(gender)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Eyes" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(eyes)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Size" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(size ? size.name : null)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Height" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(height)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Faith" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(faith)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Hair" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(hair)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Skin" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(skin)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Age" {...infoItemProps}>
|
||||
{this.renderDescriptionItem(age)}
|
||||
</InfoItem>
|
||||
<InfoItem label="Weight" {...infoItemProps}>
|
||||
{weight === null ? (
|
||||
DEFAULT_VALUE
|
||||
) : (
|
||||
<NumberDisplay type="weightInLb" number={weight} />
|
||||
)}
|
||||
</InfoItem>
|
||||
</div>
|
||||
</ContentGroup>
|
||||
<div
|
||||
className="ct-description__traits"
|
||||
style={
|
||||
theme.isDarkMode
|
||||
? { borderColor: `${theme.themeColor}66` }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<TraitContent
|
||||
traits={traits}
|
||||
traitKey={Constants.TraitTypeEnum.PERSONALITY_TRAITS}
|
||||
label="Personality Traits"
|
||||
fallback="+ Add Personality Traits"
|
||||
onClick={this.handleTraitShow}
|
||||
/>
|
||||
<TraitContent
|
||||
traits={traits}
|
||||
traitKey={Constants.TraitTypeEnum.IDEALS}
|
||||
label="Ideals"
|
||||
fallback="+ Add Ideals"
|
||||
onClick={this.handleTraitShow}
|
||||
/>
|
||||
<TraitContent
|
||||
traits={traits}
|
||||
traitKey={Constants.TraitTypeEnum.BONDS}
|
||||
label="Bonds"
|
||||
fallback="+ Add Bonds"
|
||||
onClick={this.handleTraitShow}
|
||||
/>
|
||||
<TraitContent
|
||||
traits={traits}
|
||||
traitKey={Constants.TraitTypeEnum.FLAWS}
|
||||
label="Flaws"
|
||||
fallback="+ Add Flaws"
|
||||
onClick={this.handleTraitShow}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
...(traits !== null
|
||||
? [
|
||||
{
|
||||
label: "Appearance",
|
||||
content: (
|
||||
<ContentGroup header="Appearance">
|
||||
<TraitContent
|
||||
traits={traits}
|
||||
traitKey={Constants.TraitTypeEnum.APPEARANCE}
|
||||
fallback="+ Add Appearance information"
|
||||
onClick={this.handleTraitShow}
|
||||
/>
|
||||
</ContentGroup>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
background: rulesEngineSelectors.getBackgroundInfo(state),
|
||||
alignment: rulesEngineSelectors.getAlignment(state),
|
||||
height: rulesEngineSelectors.getHeight(state),
|
||||
weight: rulesEngineSelectors.getWeight(state),
|
||||
size: rulesEngineSelectors.getSize(state),
|
||||
faith: rulesEngineSelectors.getFaith(state),
|
||||
skin: rulesEngineSelectors.getSkin(state),
|
||||
eyes: rulesEngineSelectors.getEyes(state),
|
||||
hair: rulesEngineSelectors.getHair(state),
|
||||
age: rulesEngineSelectors.getAge(state),
|
||||
gender: rulesEngineSelectors.getGender(state),
|
||||
traits: rulesEngineSelectors.getCharacterTraits(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
snippetData: rulesEngineSelectors.getSnippetData(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
abilityLookup: rulesEngineSelectors.getAbilityLookup(state),
|
||||
dataOriginRefData: rulesEngineSelectors.getDataOriginRefData(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
};
|
||||
}
|
||||
|
||||
const DescriptionWrapper = (props) => {
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
const { pane } = useSidebar();
|
||||
|
||||
return (
|
||||
<Description
|
||||
paneContext={pane}
|
||||
characterFeaturesManager={characterFeaturesManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(DescriptionWrapper);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Description from "./Description";
|
||||
|
||||
export default Description;
|
||||
export { Description };
|
||||
@@ -0,0 +1,883 @@
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { useContext, ReactNode, MouseEvent, PureComponent } from "react";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
DarkBuilderSvg,
|
||||
FeatureFlagContext,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CharacterCurrencyContract,
|
||||
CharacterNotes,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
ContainerManager,
|
||||
Creature,
|
||||
CreatureUtils,
|
||||
InfusionChoice,
|
||||
InfusionChoiceUtils,
|
||||
Item,
|
||||
ItemUtils,
|
||||
SnippetData,
|
||||
RuleData,
|
||||
rulesEngineSelectors,
|
||||
characterSelectors,
|
||||
BaseCharClass,
|
||||
ContainerUtils,
|
||||
InventoryManager,
|
||||
FormatUtils,
|
||||
CampaignUtils,
|
||||
serviceDataSelectors,
|
||||
PartyInfo,
|
||||
CoinManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
import { Link } from "~/components/Link";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import CurrencyButton from "../../../CharacterSheet/components/CurrencyButton";
|
||||
import { ItemSlotManager } from "../../../Shared/components/ItemSlotManager";
|
||||
import { ThemeButton } from "../../../Shared/components/common/Button";
|
||||
import { CoinManagerContext } from "../../../Shared/managers/CoinManagerContext";
|
||||
import { InventoryManagerContext } from "../../../Shared/managers/InventoryManagerContext";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import ContentGroup from "../../components/ContentGroup";
|
||||
import EquipmentOverview from "../../components/EquipmentOverview";
|
||||
import Infusions from "../../components/Infusions";
|
||||
import InventoryFilter from "../../components/InventoryFilter";
|
||||
import InventoryTableHeader from "../../components/InventoryTableHeader";
|
||||
import OtherPossessions from "../../components/OtherPossessions";
|
||||
import { sheetAppSelectors } from "../../selectors";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import Attunement from "../Attunement";
|
||||
import Inventory from "../Inventory";
|
||||
|
||||
interface Props {
|
||||
showNotes: boolean;
|
||||
|
||||
builderUrl: string;
|
||||
inventory: Array<Item>;
|
||||
partyInventory: Array<Item>;
|
||||
creatures: Array<Creature>;
|
||||
currencies: CharacterCurrencyContract | null;
|
||||
notes: CharacterNotes;
|
||||
infusionChoices: Array<InfusionChoice>;
|
||||
weight: number;
|
||||
weightSpeedType: Constants.WeightSpeedTypeEnum;
|
||||
ruleData: RuleData;
|
||||
snippetData: SnippetData;
|
||||
isReadonly: boolean;
|
||||
isMobile: boolean;
|
||||
theme: CharacterTheme;
|
||||
proficiencyBonus: number;
|
||||
classes: Array<BaseCharClass>;
|
||||
inventoryManager: InventoryManager;
|
||||
campaignInfo: PartyInfo | null;
|
||||
coinManager: CoinManager;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
interface StateFilterData {
|
||||
filteredInventory: Array<Item>;
|
||||
filteredPartyInventory: Array<Item>;
|
||||
showAdvancedFilters: boolean;
|
||||
isFiltering: boolean;
|
||||
}
|
||||
interface State {
|
||||
infusableChoices: Array<InfusionChoice>;
|
||||
filterData: StateFilterData;
|
||||
filteredEquippedInventory: Array<Item>;
|
||||
shouldShowPartyInventory: number;
|
||||
shouldShowDeleteOnlyInfo: boolean;
|
||||
}
|
||||
|
||||
class Equipment extends PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
showNotes: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
infusableChoices: props.infusionChoices.filter((infusionChoice) =>
|
||||
InfusionChoiceUtils.canInfuse(infusionChoice)
|
||||
),
|
||||
filterData: {
|
||||
filteredInventory: [],
|
||||
filteredPartyInventory: [],
|
||||
showAdvancedFilters: false,
|
||||
isFiltering: false,
|
||||
},
|
||||
filteredEquippedInventory: [],
|
||||
shouldShowPartyInventory: 0, // mui used 0,1,2 for tabs so 0 is off 1 is on...
|
||||
shouldShowDeleteOnlyInfo: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (prevProps.infusionChoices !== this.props.infusionChoices) {
|
||||
this.setState({
|
||||
infusableChoices: this.props.infusionChoices.filter((infusionChoice) =>
|
||||
InfusionChoiceUtils.canInfuse(infusionChoice)
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleCurrencyClick = (
|
||||
evt: React.MouseEvent | React.KeyboardEvent,
|
||||
containerDefinitionKey: string
|
||||
): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CURRENCY,
|
||||
PaneIdentifierUtils.generateCurrencyContext(containerDefinitionKey)
|
||||
);
|
||||
};
|
||||
|
||||
handleWeightClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.ENCUMBRANCE);
|
||||
};
|
||||
|
||||
handleCampaignClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
|
||||
};
|
||||
|
||||
handlePossessionsManage = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.NOTE_MANAGE,
|
||||
PaneIdentifierUtils.generateNote(
|
||||
Constants.NoteKeyEnum.PERSONAL_POSSESSIONS
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleManageClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE);
|
||||
};
|
||||
|
||||
handleFilterUpdate = (filterData: StateFilterData): void => {
|
||||
this.setState({
|
||||
filterData,
|
||||
filteredEquippedInventory: [
|
||||
...filterData.filteredInventory.filter(ItemUtils.isEquipped),
|
||||
...filterData.filteredPartyInventory.filter(ItemUtils.isEquipped),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
handleInfusionChoiceShow = (infusionChoice: InfusionChoice): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
const choiceKey = InfusionChoiceUtils.getKey(infusionChoice);
|
||||
if (choiceKey !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.INFUSION_CHOICE,
|
||||
PaneIdentifierUtils.generateInfusionChoice(choiceKey)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleItemShow = (item: Item): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ITEM_DETAIL,
|
||||
PaneIdentifierUtils.generateItem(ItemUtils.getMappingId(item))
|
||||
);
|
||||
};
|
||||
|
||||
handleContainerShow = (container: ContainerManager): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CONTAINER,
|
||||
PaneIdentifierUtils.generateContainer(container.getDefinitionKey())
|
||||
);
|
||||
};
|
||||
|
||||
handleCreatureShow = (creature: Creature): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CREATURE,
|
||||
PaneIdentifierUtils.generateCreature(CreatureUtils.getMappingId(creature))
|
||||
);
|
||||
};
|
||||
|
||||
shouldRenderInfusions = (): boolean => {
|
||||
const { infusionChoices } = this.props;
|
||||
|
||||
return infusionChoices.length > 0;
|
||||
};
|
||||
|
||||
renderContainer = (
|
||||
container: ContainerManager,
|
||||
inventory: Array<Item>
|
||||
): ReactNode => {
|
||||
const { shouldShowPartyInventory } = this.state;
|
||||
const { isReadonly, showNotes, theme, coinManager } = this.props;
|
||||
const containerMappingId = container.getMappingId();
|
||||
|
||||
const containerName = container.getName();
|
||||
const weightInfo = container.getWeightInfo();
|
||||
|
||||
const containerInventory = container.getInventoryItems({
|
||||
filteredInventory: inventory,
|
||||
}).items;
|
||||
const containerItem = container.getContainerItem();
|
||||
|
||||
const quantityNode = (
|
||||
<span className="ct-equipment__container-quantity">{`(${containerInventory.length})`}</span>
|
||||
);
|
||||
const nameNode: ReactNode = containerItem ? (
|
||||
<>
|
||||
<ItemName item={containerItem.item} /> {quantityNode}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{containerName} {quantityNode}
|
||||
</>
|
||||
);
|
||||
|
||||
const headerNode: ReactNode = (
|
||||
<div
|
||||
className="ct-equipment__container ct-inventory-item"
|
||||
onClick={(evt) => {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.handleContainerShow(container);
|
||||
}}
|
||||
role="button"
|
||||
data-testid={`${containerItem?.getName() || containerName}-section`
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, "-")}
|
||||
>
|
||||
<div className="ct-equipment__container-action ct-inventory-item__action">
|
||||
{containerItem && (
|
||||
<ItemSlotManager
|
||||
isUsed={!!containerItem.isEquipped()}
|
||||
isReadonly={isReadonly}
|
||||
canUse={
|
||||
!(
|
||||
containerItem.isEquipped() &&
|
||||
!containerItem.isEquippedToCurrentCharacter()
|
||||
)
|
||||
}
|
||||
onSet={(uses) => {
|
||||
if (uses === 0) {
|
||||
containerItem.handleUnequip();
|
||||
}
|
||||
|
||||
if (uses === 1) {
|
||||
containerItem.handleEquip();
|
||||
}
|
||||
}}
|
||||
theme={theme}
|
||||
useTooltip={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="ct-equipment__container-name ct-inventory-item__name">
|
||||
{nameNode}
|
||||
</div>
|
||||
<div className="ct-equipment__container-weight ct-inventory-item__weight">
|
||||
<NumberDisplay type="weightInLb" number={weightInfo.applied} />
|
||||
{weightInfo.capacity > 0 && (
|
||||
<span className="ct-equipment__container-weight-capacity">
|
||||
({Math.ceil(weightInfo.total)}/
|
||||
{FormatUtils.renderWeight(weightInfo.capacity)})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{coinManager.canUseCointainers() ? (
|
||||
<div className="ct-equipment__container-coin">
|
||||
<CurrencyButton
|
||||
coin={ContainerUtils.getCoin(container.container)}
|
||||
isDarkMode={theme.isDarkMode}
|
||||
shouldShowPartyInventory={shouldShowPartyInventory === 1}
|
||||
handleCurrencyClick={(evt) =>
|
||||
this.handleCurrencyClick(
|
||||
evt,
|
||||
ContainerUtils.getDefinitionKey(container.container)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
containerItem &&
|
||||
container.isEquipped() &&
|
||||
!containerItem.isEquippedToCurrentCharacter() && (
|
||||
<div className="ct-equipment__container-equipped">
|
||||
Equipped by {containerItem.getEquippedCharacterName()}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ContentGroup key={containerMappingId} header={headerNode}>
|
||||
<Inventory
|
||||
container={container}
|
||||
inventory={containerInventory}
|
||||
showNotes={showNotes}
|
||||
showTableHeader={false}
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
/>
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
renderCharacterContainers = (): ReactNode => {
|
||||
const { filterData } = this.state;
|
||||
const { inventoryManager } = this.props;
|
||||
|
||||
return inventoryManager
|
||||
.getCharacterContainers()
|
||||
.map((container) =>
|
||||
this.renderContainer(container, filterData.filteredInventory)
|
||||
);
|
||||
};
|
||||
|
||||
renderPartyContainers = (): ReactNode => {
|
||||
const { filterData } = this.state;
|
||||
const { inventoryManager } = this.props;
|
||||
|
||||
return inventoryManager
|
||||
.getPartyContainers()
|
||||
.map((container) =>
|
||||
this.renderContainer(container, filterData.filteredPartyInventory)
|
||||
);
|
||||
};
|
||||
|
||||
renderAttunement = (): ReactNode => {
|
||||
const { isMobile, theme } = this.props;
|
||||
|
||||
return (
|
||||
<ContentGroup header="Attunement">
|
||||
<Attunement isMobile={isMobile} />
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
renderInfusions = (): ReactNode => {
|
||||
const { infusableChoices } = this.state;
|
||||
const {
|
||||
builderUrl,
|
||||
infusionChoices,
|
||||
isReadonly,
|
||||
snippetData,
|
||||
inventory,
|
||||
partyInventory,
|
||||
creatures,
|
||||
ruleData,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const headerNode: ReactNode = (
|
||||
<>
|
||||
Infusions{" "}
|
||||
{infusableChoices.length !== infusionChoices.length && (
|
||||
<>
|
||||
({infusableChoices.length}/{infusionChoices.length} Known Infusions)
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
let extraNode: ReactNode = null;
|
||||
if (!isReadonly) {
|
||||
extraNode = (
|
||||
<Link
|
||||
href={builderUrl}
|
||||
className="ct-equipment__builder-link"
|
||||
useRouter
|
||||
>
|
||||
<DarkBuilderSvg />
|
||||
<span className="ct-equipment__builder-link-text">
|
||||
Manage Infusions
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentGroup header={headerNode} extra={extraNode}>
|
||||
<Infusions
|
||||
theme={theme}
|
||||
infusionChoices={infusableChoices}
|
||||
inventory={[...inventory, ...partyInventory]}
|
||||
creatures={creatures}
|
||||
snippetData={snippetData}
|
||||
onCreatureShow={this.handleCreatureShow}
|
||||
onItemShow={this.handleItemShow}
|
||||
onInfusionChoiceShow={this.handleInfusionChoiceShow}
|
||||
ruleData={ruleData}
|
||||
isReadonly={isReadonly}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
/>
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
renderOtherPossessions = (): ReactNode => {
|
||||
const { notes, isReadonly } = this.props;
|
||||
|
||||
if (isReadonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentGroup header="Other Possessions">
|
||||
<OtherPossessions
|
||||
notes={notes}
|
||||
onClick={this.handlePossessionsManage}
|
||||
/>
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
renderContent = (): ReactNode => {
|
||||
const {
|
||||
isReadonly,
|
||||
inventoryManager,
|
||||
inventory,
|
||||
partyInventory,
|
||||
showNotes,
|
||||
} = this.props;
|
||||
const { shouldShowPartyInventory } = this.state;
|
||||
|
||||
const headerNode: ReactNode = (
|
||||
<div
|
||||
className="ct-equipment__container"
|
||||
onClick={(evt) => {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
this.handleManageClick();
|
||||
}}
|
||||
>
|
||||
Party Inventory
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ct-equipment__content">
|
||||
<TabFilter
|
||||
sharedChildren={<InventoryTableHeader showNotes={showNotes} />}
|
||||
filters={[
|
||||
{
|
||||
label: "All",
|
||||
content: (
|
||||
<>
|
||||
{shouldShowPartyInventory === 0 ? (
|
||||
<>
|
||||
{this.renderCharacterContainers()}
|
||||
{this.renderAttunement()}
|
||||
{this.shouldRenderInfusions() && this.renderInfusions()}
|
||||
{this.renderOtherPossessions()}
|
||||
</>
|
||||
) : null}
|
||||
<FeatureFlagContext.Consumer>
|
||||
{({ imsFlag }) => {
|
||||
return imsFlag &&
|
||||
shouldShowPartyInventory === 1 &&
|
||||
inventoryManager.getPartyContainers().length > 0 ? (
|
||||
<ContentGroup header={headerNode}>
|
||||
{this.renderPartyContainers()}
|
||||
</ContentGroup>
|
||||
) : null;
|
||||
}}
|
||||
</FeatureFlagContext.Consumer>
|
||||
</>
|
||||
),
|
||||
},
|
||||
...(shouldShowPartyInventory === 0
|
||||
? inventoryManager
|
||||
.getCharacterContainers()
|
||||
.map((characterContainer) => ({
|
||||
label: characterContainer.getName(),
|
||||
content: this.renderContainer(
|
||||
characterContainer,
|
||||
inventory
|
||||
),
|
||||
}))
|
||||
: []),
|
||||
...(shouldShowPartyInventory === 1
|
||||
? inventoryManager.getPartyContainers().map((partyContainer) => ({
|
||||
label: partyContainer.getName(),
|
||||
content: this.renderContainer(partyContainer, partyInventory),
|
||||
}))
|
||||
: []),
|
||||
...(shouldShowPartyInventory === 0
|
||||
? [
|
||||
{
|
||||
label: "Attunement",
|
||||
content: this.renderAttunement(),
|
||||
},
|
||||
...(this.shouldRenderInfusions()
|
||||
? [
|
||||
{
|
||||
label: "Infusions",
|
||||
content: this.renderInfusions(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isReadonly
|
||||
? [
|
||||
{
|
||||
label: "Other Possessions",
|
||||
content: this.renderOtherPossessions(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
showAllTab={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderManageButton = (): ReactNode => {
|
||||
const { isReadonly } = this.props;
|
||||
|
||||
if (isReadonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeButton
|
||||
style="outline"
|
||||
size="medium"
|
||||
onClick={this.handleManageClick}
|
||||
data-testid="open-manage-equipment"
|
||||
>
|
||||
Manage Inventory
|
||||
</ThemeButton>
|
||||
);
|
||||
};
|
||||
|
||||
toggleShouldShowPartyInventory = (event, newValue): void => {
|
||||
this.setState({
|
||||
shouldShowPartyInventory: newValue,
|
||||
});
|
||||
};
|
||||
handleCloseDeleteOnlyInfo = (event: MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.setState({
|
||||
shouldShowDeleteOnlyInfo: false,
|
||||
});
|
||||
};
|
||||
handleOpenDeleteOnlyInfo = (event: MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
this.setState({
|
||||
shouldShowDeleteOnlyInfo: true,
|
||||
});
|
||||
};
|
||||
|
||||
renderDeleteOnlyAlertAction = (): ReactNode => {
|
||||
const { shouldShowDeleteOnlyInfo } = this.state;
|
||||
const { campaignInfo } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ThemeButton
|
||||
style="outline"
|
||||
size="medium"
|
||||
onClick={this.handleOpenDeleteOnlyInfo}
|
||||
>
|
||||
More Info
|
||||
</ThemeButton>
|
||||
<Dialog
|
||||
open={shouldShowDeleteOnlyInfo}
|
||||
onClose={this.handleCloseDeleteOnlyInfo}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Delete Only</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Party Inventory sharing isn’t currently enabled for this campaign.
|
||||
Your old items are still here, but you won’t be able to add new
|
||||
items until it’s turned on again.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{campaignInfo ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
component="a"
|
||||
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
|
||||
size="small"
|
||||
>
|
||||
go to campaign
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={this.handleCloseDeleteOnlyInfo}
|
||||
autoFocus
|
||||
size="small"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderOverviewFiltersAndContent = (): ReactNode => {
|
||||
const { filterData, shouldShowPartyInventory } = this.state;
|
||||
const {
|
||||
currencies,
|
||||
weight,
|
||||
weightSpeedType,
|
||||
inventory,
|
||||
ruleData,
|
||||
theme,
|
||||
partyInventory,
|
||||
campaignInfo,
|
||||
inventoryManager,
|
||||
coinManager,
|
||||
} = this.props;
|
||||
|
||||
const cointainerFlagEnabled: boolean = coinManager.canUseCointainers();
|
||||
let coin: CharacterCurrencyContract | null = currencies;
|
||||
if (shouldShowPartyInventory === 1 && campaignInfo) {
|
||||
// We need all the party coin if flag is on, or just Party Equipment coin if not
|
||||
if (cointainerFlagEnabled) {
|
||||
coin = coinManager.getAllPartyCoin();
|
||||
} else {
|
||||
coin = CampaignUtils.getCoin(campaignInfo);
|
||||
}
|
||||
} else if (cointainerFlagEnabled) {
|
||||
// We need to change it to all container currencies
|
||||
coin = coinManager.getAllCharacterCoin();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ct-equipment__overview">
|
||||
<EquipmentOverview
|
||||
shouldShowPartyInventory={shouldShowPartyInventory === 1}
|
||||
currencies={coin}
|
||||
weight={weight}
|
||||
weightSpeedType={weightSpeedType}
|
||||
onCurrencyClick={(evt) =>
|
||||
this.handleCurrencyClick(
|
||||
evt,
|
||||
shouldShowPartyInventory === 1
|
||||
? inventoryManager.getPartyEquipmentContainerDefinitionKey() ??
|
||||
inventoryManager.getCharacterContainerDefinitionKey()
|
||||
: inventoryManager.getCharacterContainerDefinitionKey()
|
||||
)
|
||||
}
|
||||
onWeightClick={this.handleWeightClick}
|
||||
onCampaignClick={this.handleCampaignClick}
|
||||
enableManage={false}
|
||||
isDarkMode={theme.isDarkMode}
|
||||
campaignName={
|
||||
campaignInfo !== null ? CampaignUtils.getName(campaignInfo) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-equipment__filter">
|
||||
<InventoryFilter
|
||||
partyInventory={partyInventory}
|
||||
inventory={inventory}
|
||||
ruleData={ruleData}
|
||||
onDataUpdate={this.handleFilterUpdate}
|
||||
callout={this.renderManageButton()}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
{!filterData.showAdvancedFilters && this.renderContent()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderOptinToInventorySharing = (): ReactNode => {
|
||||
const { campaignInfo } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack justifyContent="center">
|
||||
<Stack marginBottom="10px" marginTop="40px">
|
||||
<Typography color="primary" align="center" variant="body2">
|
||||
Want a hand with that loot?
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack marginBottom="30px" alignItems={"center"}>
|
||||
<Typography
|
||||
align="center"
|
||||
variant="body1"
|
||||
sx={{ maxWidth: "320px" }}
|
||||
>
|
||||
Party Inventory makes it easy for your group to keep track of all
|
||||
their shared loot and coin. Add it to your campaign today with a
|
||||
subscription to D&D Beyond!
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
spacing={2}
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
justifyContent="center"
|
||||
>
|
||||
{campaignInfo ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component="a"
|
||||
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
|
||||
size="large"
|
||||
sx={{
|
||||
marginLeft: { xs: 0 },
|
||||
marginRight: { xs: 0 },
|
||||
}}
|
||||
>
|
||||
go to campaign
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button variant="contained" color="primary" size="large">
|
||||
view subscription plans
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={{ display: "flex", justifyContent: "center", marginTop: 51 }}
|
||||
>
|
||||
<div className="ct-item-detail__full-image">
|
||||
<img
|
||||
className="ct-item-detail__full-image-img"
|
||||
src="https://www.dndbeyond.com/avatars/thumbnails/7/120/315/315/636284708068284913.jpeg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { shouldShowPartyInventory } = this.state;
|
||||
const { campaignInfo, inventoryManager } = this.props;
|
||||
|
||||
return (
|
||||
<section className="ct-equipment">
|
||||
<h2 style={visuallyHidden}>Inventory</h2>
|
||||
<FeatureFlagContext.Consumer>
|
||||
{({ imsFlag }) => {
|
||||
return imsFlag && campaignInfo !== null ? (
|
||||
<>
|
||||
<Tabs
|
||||
value={shouldShowPartyInventory}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={this.toggleShouldShowPartyInventory}
|
||||
aria-label="disabled tabs example"
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab label="My Inventory" data-testid="my-inventory" />
|
||||
<Tab label="Party Inventory" data-testid="party-inventory" />
|
||||
</Tabs>
|
||||
{/* TODO: make this go away when last itme is removed... */}
|
||||
{shouldShowPartyInventory &&
|
||||
inventoryManager.isSharingTurnedDeleteOnly() ? (
|
||||
<div role="button" onMouseUp={this.handleOpenDeleteOnlyInfo}>
|
||||
<Alert
|
||||
severity="info"
|
||||
action={this.renderDeleteOnlyAlertAction()}
|
||||
>
|
||||
Party Inventory is currently turned off for this campaign.
|
||||
</Alert>
|
||||
</div>
|
||||
) : null}
|
||||
{shouldShowPartyInventory &&
|
||||
inventoryManager.isSharingTurnedOff()
|
||||
? this.renderOptinToInventorySharing()
|
||||
: this.renderOverviewFiltersAndContent()}
|
||||
</>
|
||||
) : (
|
||||
this.renderOverviewFiltersAndContent()
|
||||
);
|
||||
}}
|
||||
</FeatureFlagContext.Consumer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
builderUrl: sheetAppSelectors.getBuilderUrl(state),
|
||||
inventory: rulesEngineSelectors.getInventory(state),
|
||||
partyInventory: rulesEngineSelectors.getPartyInventory(state),
|
||||
creatures: rulesEngineSelectors.getCreatures(state),
|
||||
weight: rulesEngineSelectors.getTotalCarriedWeight(state),
|
||||
weightSpeedType: rulesEngineSelectors.getCurrentCarriedWeightType(state),
|
||||
notes: rulesEngineSelectors.getCharacterNotes(state),
|
||||
infusionChoices: rulesEngineSelectors.getAvailableInfusionChoices(state),
|
||||
currencies: rulesEngineSelectors.getCurrencies(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
snippetData: rulesEngineSelectors.getSnippetData(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
isMobile: appEnvSelectors.getIsMobile(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
classes: characterSelectors.getClasses(state),
|
||||
campaignInfo: serviceDataSelectors.getPartyInfo(state),
|
||||
};
|
||||
}
|
||||
|
||||
const EquipmentContainer = (props) => {
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
const { coinManager } = useContext(CoinManagerContext);
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<Equipment
|
||||
inventoryManager={inventoryManager}
|
||||
coinManager={coinManager}
|
||||
paneHistoryStart={paneHistoryStart}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(EquipmentContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Equipment from "./Equipment";
|
||||
|
||||
export default Equipment;
|
||||
export { Equipment };
|
||||
@@ -0,0 +1,270 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React, { useContext } from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
CharacterTheme,
|
||||
ExtrasFilterData,
|
||||
ExtraGroupInfo,
|
||||
Constants,
|
||||
ExtrasManager,
|
||||
ExtraManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { useExtras } from "~/hooks/useExtras";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import ExtraList from "../../../Shared/components/ExtraList";
|
||||
import { ThemeButton } from "../../../Shared/components/common/Button";
|
||||
import { ExtrasManagerContext } from "../../../Shared/managers/ExtrasManagerContext";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import ContentGroup from "../../components/ContentGroup";
|
||||
import ExtrasFilter from "../../components/ExtrasFilter";
|
||||
import { SheetAppState } from "../../typings";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
extras: Array<ExtraManager>;
|
||||
extrasManager: ExtrasManager;
|
||||
isReadonly: boolean;
|
||||
showNotes: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
interface State {
|
||||
filterData: ExtrasFilterData;
|
||||
}
|
||||
class Extras extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
isReadonly: false,
|
||||
showNotes: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filterData: {
|
||||
filteredExtras: [],
|
||||
showAdvancedFilters: false,
|
||||
isFiltering: false,
|
||||
filterCount: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleFilterUpdate = (filterData: ExtrasFilterData): void => {
|
||||
this.setState({
|
||||
filterData,
|
||||
});
|
||||
};
|
||||
|
||||
handleManageClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.EXTRA_MANAGE);
|
||||
};
|
||||
|
||||
handleExtraShow = (extra: ExtraManager): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
if (extra.isCreature()) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CREATURE,
|
||||
PaneIdentifierUtils.generateCreature(extra.getMappingId())
|
||||
);
|
||||
} else if (extra.isVehicle()) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.VEHICLE,
|
||||
PaneIdentifierUtils.generateVehicle(extra.getMappingId())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleExtraStatusChange = (extra: ExtraManager, isActive: boolean): void => {
|
||||
extra.handleSetActive({ isActive });
|
||||
};
|
||||
|
||||
renderManageButton = (): React.ReactNode => {
|
||||
const { isReadonly } = this.props;
|
||||
|
||||
if (isReadonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeButton
|
||||
style="outline"
|
||||
size="medium"
|
||||
onClick={this.handleManageClick}
|
||||
>
|
||||
Manage Extras
|
||||
</ThemeButton>
|
||||
);
|
||||
};
|
||||
|
||||
renderDefault = (): React.ReactNode => {
|
||||
return (
|
||||
<div className="ct-extras__empty">
|
||||
Extras that you add will display here.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderNoResults = (): React.ReactNode => {
|
||||
return (
|
||||
<div className="ct-extras__empty">No Extras Match the Current Filter</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderContent = (): React.ReactNode => {
|
||||
const { filterData } = this.state;
|
||||
const { extras, extrasManager, showNotes, isReadonly, theme } = this.props;
|
||||
|
||||
if (!extras.length) {
|
||||
return this.renderDefault();
|
||||
}
|
||||
|
||||
if (filterData.showAdvancedFilters) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!filterData.filteredExtras.length) {
|
||||
return this.renderNoResults();
|
||||
}
|
||||
|
||||
let groups = extrasManager.getExtrasGroups(filterData.filteredExtras);
|
||||
const orderedGroupInfos = extrasManager.getGroupInfosForExtras(
|
||||
filterData.filteredExtras
|
||||
);
|
||||
|
||||
let primaryGroups: Array<ExtraGroupInfo> = [];
|
||||
let otherGroups: Array<ExtraGroupInfo> = [];
|
||||
let vehicleGroups: Array<ExtraGroupInfo> = [];
|
||||
orderedGroupInfos.forEach((groupInfo) => {
|
||||
if (groupInfo) {
|
||||
if (groupInfo.isPrimary) {
|
||||
primaryGroups.push(groupInfo);
|
||||
} else if (groupInfo.id === Constants.ExtraGroupTypeEnum.VEHICLE) {
|
||||
vehicleGroups.push(groupInfo);
|
||||
} else {
|
||||
otherGroups.push(groupInfo);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="ct-extras__content">
|
||||
<TabFilter
|
||||
filters={[
|
||||
...primaryGroups.map((groupInfo) => ({
|
||||
label: groupInfo.name || "",
|
||||
content: (
|
||||
<ContentGroup header={groupInfo.name}>
|
||||
<ExtraList
|
||||
theme={theme}
|
||||
extras={groups[groupInfo.id]}
|
||||
showNotes={showNotes}
|
||||
onShow={this.handleExtraShow}
|
||||
onStatusChange={this.handleExtraStatusChange}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
</ContentGroup>
|
||||
),
|
||||
})),
|
||||
...(otherGroups.length > 0
|
||||
? [
|
||||
{
|
||||
label: "Other",
|
||||
content: otherGroups.map((groupInfo) => (
|
||||
<ContentGroup key={groupInfo.id} header={groupInfo.name}>
|
||||
<ExtraList
|
||||
theme={theme}
|
||||
extras={groups[groupInfo.id]}
|
||||
showNotes={showNotes}
|
||||
onShow={this.handleExtraShow}
|
||||
onStatusChange={this.handleExtraStatusChange}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
</ContentGroup>
|
||||
)),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(vehicleGroups.length > 0
|
||||
? [
|
||||
{
|
||||
label: "Vehicle",
|
||||
content: vehicleGroups.map((groupInfo) => (
|
||||
<ContentGroup key={groupInfo.id} header={groupInfo.name}>
|
||||
<ExtraList
|
||||
theme={theme}
|
||||
extras={groups[groupInfo.id]}
|
||||
showNotes={showNotes}
|
||||
onShow={this.handleExtraShow}
|
||||
onStatusChange={this.handleExtraStatusChange}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
</ContentGroup>
|
||||
)),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { extras, theme } = this.props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`ct-extras ${
|
||||
theme.isDarkMode ? "ct-extras--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
<h2 style={visuallyHidden}>Extras</h2>
|
||||
<div className="ct-extras__filter">
|
||||
<ExtrasFilter
|
||||
extras={extras}
|
||||
onDataUpdate={this.handleFilterUpdate}
|
||||
callout={this.renderManageButton()}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
{this.renderContent()}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
function ExtrasContainer(props) {
|
||||
const { extrasManager } = useContext(ExtrasManagerContext);
|
||||
const extras = useExtras();
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<Extras
|
||||
extrasManager={extrasManager}
|
||||
paneHistoryStart={paneHistoryStart}
|
||||
extras={extras}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ExtrasContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Extras from "./Extras";
|
||||
|
||||
export default Extras;
|
||||
export { Extras };
|
||||
@@ -0,0 +1,366 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
ClassUtils,
|
||||
FeatureUtils,
|
||||
InfusionChoiceUtils,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
FeaturesManager,
|
||||
FeatManager,
|
||||
Action,
|
||||
ActionUtils,
|
||||
Spell,
|
||||
SpellUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
import { SpeciesDetail } from "~/subApps/sheet/components/SpeciesDetail";
|
||||
import { ThemeButton } from "~/tools/js/Shared/components/common/Button";
|
||||
|
||||
import {
|
||||
handleActionUseSet,
|
||||
handleSpellUseSet,
|
||||
} from "../../../../../handlers/commonHandlers";
|
||||
import { CharacterFeaturesManagerContext } from "../../../Shared/managers/CharacterFeaturesManagerContext";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import BlessingsDetail from "../../components/BlessingsDetail";
|
||||
import ClassesDetail from "../../components/ClassesDetail";
|
||||
import ContentGroup from "../../components/ContentGroup";
|
||||
import FeatsDetail from "../../components/FeatsDetail";
|
||||
|
||||
//TODO we should break this file out into seperate components rather than have them all in one file
|
||||
|
||||
const SpeciesTraitsGroup: React.FC<{}> = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
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
|
||||
) as number;
|
||||
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
|
||||
const speciesTraits = useSelector(
|
||||
rulesEngineSelectors.getCurrentLevelRacialTraits
|
||||
) as RacialTrait[];
|
||||
|
||||
const handleFeatClick = (feat: FeatManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.FEAT_DETAIL,
|
||||
PaneIdentifierUtils.generateFeat(feat.getId())
|
||||
);
|
||||
};
|
||||
|
||||
const handleActionClick = (action: Action): void => {
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ACTION,
|
||||
PaneIdentifierUtils.generateAction(mappingId, mappingEntityTypeId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpellDetailClick = (spell: Spell): void => {
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
if (mappingId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CHARACTER_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ContentGroup header="Species Traits">
|
||||
<SpeciesDetail
|
||||
onActionUseSet={(action, uses) =>
|
||||
handleActionUseSet(action, uses, dispatch)
|
||||
}
|
||||
onActionClick={(action) => handleActionClick(action)}
|
||||
onSpellClick={(spell) => handleSpellDetailClick(spell)}
|
||||
onSpellUseSet={(spell, uses) =>
|
||||
handleSpellUseSet(spell, uses, dispatch)
|
||||
}
|
||||
onFeatureClick={(feature) => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.SPECIES_TRAIT_DETAIL,
|
||||
PaneIdentifierUtils.generateRacialTrait(
|
||||
RacialTraitUtils.getId(feature)
|
||||
)
|
||||
);
|
||||
}}
|
||||
feats={characterFeaturesManager
|
||||
.getFeats()
|
||||
.map((manager) => manager.feat)}
|
||||
isReadonly={isReadonly}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
speciesTraits={speciesTraits}
|
||||
onFeatClick={(feat) => handleFeatClick(feat)}
|
||||
featuresManager={characterFeaturesManager}
|
||||
/>
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const ClassFeaturesGroup: React.FC<{}> = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
const classes = useSelector(rulesEngineSelectors.getClasses);
|
||||
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);
|
||||
|
||||
const handleFeatClick = (feat: FeatManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.FEAT_DETAIL,
|
||||
PaneIdentifierUtils.generateFeat(feat.getId())
|
||||
);
|
||||
};
|
||||
|
||||
const handleActionClick = (action: Action): void => {
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ACTION,
|
||||
PaneIdentifierUtils.generateAction(mappingId, mappingEntityTypeId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpellDetailClick = (spell: Spell): void => {
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
if (mappingId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CHARACTER_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ContentGroup header="Class Features">
|
||||
<ClassesDetail
|
||||
classes={classes}
|
||||
onActionUseSet={(action, uses) =>
|
||||
handleActionUseSet(action, uses, dispatch)
|
||||
}
|
||||
onActionClick={(action) => handleActionClick(action)}
|
||||
onSpellClick={(spell) => handleSpellDetailClick(spell)}
|
||||
onSpellUseSet={(spell, uses) =>
|
||||
handleSpellUseSet(spell, uses, dispatch)
|
||||
}
|
||||
onFeatureClick={(feature, charClass) => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CLASS_FEATURE_DETAIL,
|
||||
PaneIdentifierUtils.generateClassFeature(
|
||||
FeatureUtils.getId(feature),
|
||||
ClassUtils.getMappingId(charClass)
|
||||
)
|
||||
);
|
||||
}}
|
||||
onInfusionChoiceClick={(infusionChoice) => {
|
||||
const choiceKey = InfusionChoiceUtils.getKey(infusionChoice);
|
||||
if (choiceKey !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.INFUSION_CHOICE,
|
||||
PaneIdentifierUtils.generateInfusionChoice(choiceKey)
|
||||
);
|
||||
}
|
||||
}}
|
||||
feats={characterFeaturesManager
|
||||
.getFeats()
|
||||
.map((manager) => manager.feat)}
|
||||
isReadonly={isReadonly}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
onFeatClick={(feat) => handleFeatClick(feat)}
|
||||
featuresManager={characterFeaturesManager}
|
||||
/>
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const FeatsGroup: React.FC<{}> = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { gfsBlessingsUiFlag } = useFeatureFlags();
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
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);
|
||||
|
||||
const handleFeatClick = (feat: FeatManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.FEAT_DETAIL,
|
||||
PaneIdentifierUtils.generateFeat(feat.getId())
|
||||
);
|
||||
};
|
||||
|
||||
const handleActionClick = (action: Action): void => {
|
||||
const mappingId = ActionUtils.getMappingId(action);
|
||||
const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action);
|
||||
|
||||
if (mappingId !== null && mappingEntityTypeId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ACTION,
|
||||
PaneIdentifierUtils.generateAction(mappingId, mappingEntityTypeId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpellDetailClick = (spell: Spell): void => {
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
if (mappingId !== null) {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CHARACTER_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ContentGroup header="Feats">
|
||||
{!isReadonly && (
|
||||
<div className="ct-features__management-link">
|
||||
<ThemeButton
|
||||
onClick={() => paneHistoryStart(PaneComponentEnum.FEATS_MANAGE)}
|
||||
style="outline"
|
||||
size="medium"
|
||||
>
|
||||
Manage {gfsBlessingsUiFlag ? "Features" : "Feats"}
|
||||
</ThemeButton>
|
||||
</div>
|
||||
)}
|
||||
<FeatsDetail
|
||||
onActionUseSet={(action, uses) =>
|
||||
handleActionUseSet(action, uses, dispatch)
|
||||
}
|
||||
onActionClick={(action) => handleActionClick(action)}
|
||||
onSpellClick={(spell) => handleSpellDetailClick(spell)}
|
||||
onSpellUseSet={(spell, uses) =>
|
||||
handleSpellUseSet(spell, uses, dispatch)
|
||||
}
|
||||
onFeatureClick={(feat) => handleFeatClick(feat)}
|
||||
isReadonly={isReadonly}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
/>
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const BlessingsGroup: React.FC<{}> = () => {
|
||||
return (
|
||||
<ContentGroup header="Blessings">
|
||||
<BlessingsDetail />
|
||||
</ContentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const Features: React.FC<{}> = () => {
|
||||
const { gfsBlessingsUiFlag } = useFeatureFlags();
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
|
||||
const [hasBlessings, setHasBlessings] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function onUpdate() {
|
||||
const hasBlessings = gfsBlessingsUiFlag
|
||||
? await characterFeaturesManager.hasBlessings()
|
||||
: false;
|
||||
setHasBlessings(hasBlessings);
|
||||
}
|
||||
return FeaturesManager.subscribeToUpdates({ onUpdate });
|
||||
}, [setHasBlessings]);
|
||||
|
||||
return (
|
||||
<section className="ct-features">
|
||||
<h2 style={visuallyHidden}>Features and Traits</h2>
|
||||
<TabFilter
|
||||
filters={[
|
||||
{
|
||||
label: "Class Features",
|
||||
content: <ClassFeaturesGroup />,
|
||||
},
|
||||
{
|
||||
label: "Species Traits",
|
||||
content: <SpeciesTraitsGroup />,
|
||||
},
|
||||
{
|
||||
label: "Feats",
|
||||
content: <FeatsGroup />,
|
||||
},
|
||||
...(gfsBlessingsUiFlag && hasBlessings
|
||||
? [{ label: "Blessings", content: <BlessingsGroup /> }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Features;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Features from "./Features";
|
||||
|
||||
export default Features;
|
||||
export { Features };
|
||||
@@ -0,0 +1,242 @@
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
ContainerManager,
|
||||
ItemManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { ThemeButton } from "../../../Shared/components/common/Button";
|
||||
import { InventoryManagerContext } from "../../../Shared/managers/InventoryManagerContext";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import InventoryItem from "../../components/InventoryItem";
|
||||
import { InventoryTableHeader } from "../../components/InventoryTableHeader";
|
||||
|
||||
interface Props {
|
||||
container: ContainerManager;
|
||||
inventory: Array<ItemManager>;
|
||||
showNotes?: boolean;
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
showTableHeader?: boolean;
|
||||
}
|
||||
|
||||
interface InventoryActionsProps
|
||||
extends Pick<Props, "isReadonly" | "container" | "inventory"> {
|
||||
onContainerClick: (
|
||||
evt: React.MouseEvent | React.KeyboardEvent,
|
||||
container: ContainerManager,
|
||||
showAddItems: boolean
|
||||
) => void;
|
||||
showContents: boolean;
|
||||
onShowContentsClick: (showContents: boolean) => void;
|
||||
}
|
||||
const InventoryActions: React.FC<InventoryActionsProps> = ({
|
||||
isReadonly,
|
||||
container,
|
||||
inventory,
|
||||
onContainerClick,
|
||||
onShowContentsClick,
|
||||
showContents,
|
||||
}) => {
|
||||
const { inventoryManager } = useContext(InventoryManagerContext);
|
||||
|
||||
if (isReadonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerName = container.getName();
|
||||
|
||||
const classNames: Array<string> = ["ct-inventory__actions"];
|
||||
if (inventory.length > 0 && showContents) {
|
||||
classNames.push("ct-inventory__actions--collapsed");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
{inventoryManager.canAddToContainer(container.container) ? (
|
||||
<span
|
||||
role="button"
|
||||
className="ct-inventory__action"
|
||||
onClick={(evt) => onContainerClick(evt, container, true)}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
onContainerClick(evt, container, true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{container.isCharacterContainer()
|
||||
? `+ Add ${containerName}`
|
||||
: `+ Add items to your ${containerName}`}
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{inventory.length > 0 && (
|
||||
<span
|
||||
role="button"
|
||||
className="ct-inventory__action"
|
||||
onClick={() => onShowContentsClick(!showContents)}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
onShowContentsClick(!showContents);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showContents ? "Hide Contents" : "Show Contents"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyInventory: React.FC<InventoryActionsProps> = ({
|
||||
isReadonly,
|
||||
container,
|
||||
inventory,
|
||||
...restProps
|
||||
}) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
if (isReadonly) {
|
||||
return (
|
||||
<div className="ct-inventory__empty">
|
||||
No equipment has been added for this character.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (container.isCharacterContainer()) {
|
||||
return (
|
||||
<div className="ct-inventory__choose">
|
||||
<div className="ct-inventory__choose-actions">
|
||||
<div className="ct-inventory__choose-action">
|
||||
<ThemeButton
|
||||
block={true}
|
||||
onClick={() =>
|
||||
paneHistoryStart(PaneComponentEnum.STARTING_EQUIPMENT)
|
||||
}
|
||||
stopPropagation={true}
|
||||
>
|
||||
Starting Equipment or Gold
|
||||
</ThemeButton>
|
||||
</div>
|
||||
<div className="ct-inventory__choose-action-sep">or</div>
|
||||
<div className="ct-inventory__choose-action">
|
||||
<ThemeButton
|
||||
block={true}
|
||||
onClick={() =>
|
||||
paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE)
|
||||
}
|
||||
stopPropagation={true}
|
||||
>
|
||||
Add Items
|
||||
</ThemeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InventoryActions
|
||||
inventory={inventory}
|
||||
container={container}
|
||||
isReadonly={isReadonly}
|
||||
onContainerClick={restProps.onContainerClick}
|
||||
showContents={restProps.showContents}
|
||||
onShowContentsClick={restProps.onShowContentsClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Inventory: React.FC<Props> = ({
|
||||
container,
|
||||
inventory,
|
||||
showNotes = true,
|
||||
isReadonly,
|
||||
theme,
|
||||
showTableHeader = true,
|
||||
}) => {
|
||||
const [showContents, setShowContents] = useState(true);
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
const handleContainerShow = useCallback(
|
||||
(
|
||||
evt: React.MouseEvent | React.KeyboardEvent,
|
||||
container: ContainerManager,
|
||||
showAddItems: boolean
|
||||
): void => {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
evt.stopPropagation();
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CONTAINER,
|
||||
PaneIdentifierUtils.generateContainer(
|
||||
container.getDefinitionKey(),
|
||||
showAddItems
|
||||
)
|
||||
);
|
||||
},
|
||||
[container]
|
||||
);
|
||||
|
||||
const isEmpty: boolean = inventory.length === 0;
|
||||
|
||||
return (
|
||||
<div className="ct-inventory">
|
||||
{/*<h2 style={visuallyHidden}>Inventory</h2>*/}
|
||||
{showTableHeader && <InventoryTableHeader showNotes={showNotes} />}
|
||||
<div className="ct-inventory__items">
|
||||
{isEmpty ? (
|
||||
<EmptyInventory
|
||||
inventory={inventory}
|
||||
container={container}
|
||||
isReadonly={isReadonly}
|
||||
onShowContentsClick={setShowContents}
|
||||
showContents={showContents}
|
||||
onContainerClick={handleContainerShow}
|
||||
/>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{showContents &&
|
||||
inventory.map((item) => (
|
||||
<InventoryItem
|
||||
key={item.getUniqueKey()}
|
||||
item={item}
|
||||
onEquip={item.handleEquip}
|
||||
onUnequip={item.handleUnequip}
|
||||
onItemShow={(mappingId) =>
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ITEM_DETAIL,
|
||||
PaneIdentifierUtils.generateItem(mappingId)
|
||||
)
|
||||
}
|
||||
showNotes={showNotes}
|
||||
theme={theme}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
))}
|
||||
<InventoryActions
|
||||
inventory={inventory}
|
||||
container={container}
|
||||
isReadonly={isReadonly}
|
||||
onContainerClick={handleContainerShow}
|
||||
onShowContentsClick={setShowContents}
|
||||
showContents={showContents}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Inventory;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Inventory from "./Inventory";
|
||||
|
||||
export default Inventory;
|
||||
export { Inventory };
|
||||
@@ -0,0 +1,138 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterNotes,
|
||||
Constants,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import ContentGroup from "../../components/ContentGroup";
|
||||
import { SheetAppState } from "../../typings";
|
||||
|
||||
interface NotesProps {
|
||||
notes: CharacterNotes;
|
||||
isReadonly: boolean;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class Notes extends React.PureComponent<NotesProps> {
|
||||
handleNotesManage = (
|
||||
noteType: Constants.NoteKeyEnum,
|
||||
evt: React.MouseEvent
|
||||
): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.NOTE_MANAGE,
|
||||
PaneIdentifierUtils.generateNote(noteType)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderNoteGroup(
|
||||
label: string,
|
||||
key: Constants.NoteKeyEnum,
|
||||
fallback: string = ""
|
||||
) {
|
||||
const { notes } = this.props;
|
||||
|
||||
let noteData = notes[key];
|
||||
let hasContent: boolean = !!noteData;
|
||||
let content: string = !!noteData ? noteData : fallback;
|
||||
|
||||
let classNames: Array<string> = ["ct-notes__note"];
|
||||
if (!hasContent) {
|
||||
classNames.push("ct-notes__note--no-content");
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentGroup header={label}>
|
||||
<div
|
||||
className={classNames.join(" ")}
|
||||
onClick={this.handleNotesManage.bind(this, key)}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</ContentGroup>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="ct-notes">
|
||||
<h2 style={visuallyHidden}>Notes</h2>
|
||||
<TabFilter
|
||||
filters={[
|
||||
{
|
||||
label: "Orgs",
|
||||
content: this.renderNoteGroup(
|
||||
"Organizations",
|
||||
Constants.NoteKeyEnum.ORGANIZATIONS,
|
||||
"+ Add Organizations"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Allies",
|
||||
content: this.renderNoteGroup(
|
||||
"Allies",
|
||||
Constants.NoteKeyEnum.ALLIES,
|
||||
"+ Add Allies"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Enemies",
|
||||
content: this.renderNoteGroup(
|
||||
"Enemies",
|
||||
Constants.NoteKeyEnum.ENEMIES,
|
||||
"+ Add Enemies"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Backstory",
|
||||
content: this.renderNoteGroup(
|
||||
"Backstory",
|
||||
Constants.NoteKeyEnum.BACKSTORY,
|
||||
"+ Add Backstory"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
content: this.renderNoteGroup(
|
||||
"Other",
|
||||
Constants.NoteKeyEnum.OTHER,
|
||||
"+ Add Other"
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
notes: rulesEngineSelectors.getCharacterNotes(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
};
|
||||
}
|
||||
|
||||
const NotesContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <Notes {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NotesContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Notes from "./Notes";
|
||||
|
||||
export default Notes;
|
||||
export { Notes };
|
||||
@@ -0,0 +1,142 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
BeveledBoxSvg517x660,
|
||||
BeveledBoxSvg623x660,
|
||||
BoxBackground,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { TabList } from "~/components/TabList";
|
||||
|
||||
import Subsection from "../../../Shared/components/Subsection";
|
||||
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import Actions from "../Actions";
|
||||
import Description from "../Description";
|
||||
import Equipment from "../Equipment";
|
||||
import Extras from "../Extras";
|
||||
import Features from "../Features";
|
||||
import Notes from "../Notes";
|
||||
import Spells from "../Spells";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const TAB_KEY = {
|
||||
ACTIONS: "ACTIONS",
|
||||
SPELLS: "SPELLS",
|
||||
EQUIPMENT: "EQUIPMENT",
|
||||
DESCRIPTION: "DESCRIPTION",
|
||||
NOTES: "NOTES",
|
||||
FEATURES_TRAITS: "FEATURES_TRAITS",
|
||||
EXTRAS: "EXTRAS",
|
||||
};
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
hasSpells: boolean;
|
||||
dimensions: AppEnvDimensionsState;
|
||||
theme: CharacterTheme;
|
||||
isReadonly: boolean;
|
||||
characterStatus: string | null;
|
||||
}
|
||||
|
||||
class PrimaryBox extends React.PureComponent<Props, {}> {
|
||||
render() {
|
||||
const { hasSpells, dimensions, theme, isReadonly, characterStatus } =
|
||||
this.props;
|
||||
|
||||
let isPremade = CharacterStatusSlug.PREMADE === characterStatus;
|
||||
|
||||
let BoxBackgroundComponent: React.ComponentType = BeveledBoxSvg517x660;
|
||||
if (dimensions.styleSizeType > StyleSizeTypeEnum.DESKTOP) {
|
||||
BoxBackgroundComponent = BeveledBoxSvg623x660;
|
||||
}
|
||||
|
||||
return (
|
||||
<Subsection name="Primary Box">
|
||||
<div
|
||||
className={`ct-primary-box ${
|
||||
theme.isDarkMode ? "ct-primary-box--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
<BoxBackground
|
||||
StyleComponent={BoxBackgroundComponent}
|
||||
theme={theme}
|
||||
/>
|
||||
<TabList
|
||||
className={styles.tabList}
|
||||
tabs={[
|
||||
{
|
||||
label: "Actions",
|
||||
content: <Actions />,
|
||||
className: "ct-primary-box__tab--actions",
|
||||
id: TAB_KEY.ACTIONS,
|
||||
},
|
||||
hasSpells
|
||||
? {
|
||||
label: "Spells",
|
||||
content: <Spells />,
|
||||
className: "ct-primary-box__tab--spells",
|
||||
id: TAB_KEY.SPELLS,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
label: "Inventory",
|
||||
content: <Equipment />,
|
||||
className: "ct-primary-box__tab--equipment",
|
||||
id: TAB_KEY.EQUIPMENT,
|
||||
},
|
||||
{
|
||||
label: "Features & Traits",
|
||||
content: <Features />,
|
||||
className: "ct-primary-box__tab--features",
|
||||
id: TAB_KEY.FEATURES_TRAITS,
|
||||
},
|
||||
!isReadonly || isPremade
|
||||
? {
|
||||
label: "Background",
|
||||
content: <Description theme={theme} />,
|
||||
className: "ct-primary-box__tab--description",
|
||||
id: TAB_KEY.DESCRIPTION,
|
||||
}
|
||||
: null,
|
||||
!isReadonly || isPremade
|
||||
? {
|
||||
label: "Notes",
|
||||
content: <Notes />,
|
||||
className: "ct-primary-box__tab--notes",
|
||||
id: TAB_KEY.NOTES,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
label: "Extras",
|
||||
content: <Extras />,
|
||||
className: "ct-primary-box__tab--extras",
|
||||
id: TAB_KEY.EXTRAS,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Subsection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
hasSpells: rulesEngineSelectors.hasSpells(state),
|
||||
dimensions: appEnvSelectors.getDimensions(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
characterStatus: characterSelectors.getStatusSlug(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PrimaryBox);
|
||||
@@ -0,0 +1,4 @@
|
||||
import PrimaryBox from "./PrimaryBox";
|
||||
|
||||
export default PrimaryBox;
|
||||
export { PrimaryBox };
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { ManageIcon } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
ProficiencyGroup,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import {
|
||||
Subsection,
|
||||
SubsectionFooter,
|
||||
} from "../../../Shared/components/Subsection";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import ProficiencyGroupsBox from "../../components/ProficiencyGroupsBox";
|
||||
import { SheetAppState } from "../../typings";
|
||||
|
||||
interface Props {
|
||||
dimensions: AppEnvDimensionsState;
|
||||
theme: CharacterTheme;
|
||||
proficiencyGroups: Array<ProficiencyGroup>;
|
||||
isReadonly: boolean;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class ProficiencyGroupsDesktop extends React.PureComponent<Props> {
|
||||
handleManageShow = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.PROFICIENCIES);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dimensions, theme, proficiencyGroups, isReadonly } = this.props;
|
||||
|
||||
return (
|
||||
<Subsection name="Proficiency Groups">
|
||||
<ProficiencyGroupsBox
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
proficiencyGroups={proficiencyGroups}
|
||||
onClick={this.handleManageShow}
|
||||
/>
|
||||
<SubsectionFooter>
|
||||
<ManageIcon
|
||||
onClick={this.handleManageShow}
|
||||
showIcon={!isReadonly}
|
||||
enableTooltip={!isReadonly}
|
||||
theme={theme}
|
||||
>
|
||||
Proficiencies & Training
|
||||
</ManageIcon>
|
||||
</SubsectionFooter>
|
||||
</Subsection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
dimensions: appEnvSelectors.getDimensions(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
proficiencyGroups: rulesEngineSelectors.getProficiencyGroups(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
};
|
||||
}
|
||||
|
||||
const ProficiencyGroupsDesktopContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return (
|
||||
<ProficiencyGroupsDesktop {...props} paneHistoryStart={paneHistoryStart} />
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ProficiencyGroupsDesktopContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import ProficiencyGroupsDesktop from "./ProficiencyGroupsDesktop";
|
||||
|
||||
export default ProficiencyGroupsDesktop;
|
||||
export { ProficiencyGroupsDesktop };
|
||||
@@ -0,0 +1,123 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { AbilitySummary } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
characterActions,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useAbilities } from "~/hooks/useAbilities";
|
||||
import { HitPointsBox } from "~/subApps/sheet/components/HitPointsBox/HitPointsBox";
|
||||
import { Inspiration } from "~/subApps/sheet/components/Inspiration";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import ProficiencyBonusBox from "../../components/ProficiencyBonusBox";
|
||||
import SpeedBox from "../../components/SpeedBox";
|
||||
|
||||
export default function QuickInfo() {
|
||||
const abilities = useAbilities();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
const proficiencyBonus = useSelector(
|
||||
rulesEngineSelectors.getProficiencyBonus
|
||||
);
|
||||
const speeds = useSelector(rulesEngineSelectors.getCurrentCarriedWeightSpeed);
|
||||
const preferences = useSelector(rulesEngineSelectors.getCharacterPreferences);
|
||||
const inspiration = useSelector(rulesEngineSelectors.getInspiration);
|
||||
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
const ruleData = useSelector(rulesEngineSelectors.getRuleData);
|
||||
const diceEnabled = useSelector(appEnvSelectors.getDiceEnabled);
|
||||
const characterRollContext = useSelector(
|
||||
characterRollContextSelectors.getCharacterRollContext
|
||||
);
|
||||
|
||||
const handleProficiencyBonusClick = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.PROFICIENCY_BONUS);
|
||||
};
|
||||
|
||||
const handleInspirationLabelClick = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.INSPIRATION);
|
||||
};
|
||||
|
||||
const handleSpeedsClick = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.SPEED_MANAGE);
|
||||
};
|
||||
|
||||
const handleToggleInspiration = (): void => {
|
||||
dispatch(characterActions.inspirationSet(!inspiration));
|
||||
};
|
||||
|
||||
const handleAbilitySummaryClick = (ability: AbilityManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ABILITY,
|
||||
PaneIdentifierUtils.generateAbility(ability)
|
||||
);
|
||||
};
|
||||
|
||||
let inspirationClasses = ["ct-quick-info__inspiration-status"];
|
||||
if (inspiration) {
|
||||
inspirationClasses.push("ct-quick-info__inspiration-status--active");
|
||||
} else {
|
||||
inspirationClasses.push("ct-quick-info__inspiration-status--inactive");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ct-quick-info">
|
||||
<section className="ct-quick-info__abilities">
|
||||
<h2 style={visuallyHidden}>Ability Scores</h2>
|
||||
{abilities.map((ability) => (
|
||||
<div className="ct-quick-info__ability" key={ability.getStatKey()}>
|
||||
<AbilitySummary
|
||||
theme={theme}
|
||||
ability={ability}
|
||||
preferences={preferences}
|
||||
onClick={handleAbilitySummaryClick}
|
||||
diceEnabled={diceEnabled}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<div className="ct-quick-info__box ct-quick-info__box--proficiency">
|
||||
<ProficiencyBonusBox
|
||||
theme={theme}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
onClick={handleProficiencyBonusClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-quick-info__box ct-quick-info__box--speed">
|
||||
<SpeedBox
|
||||
speeds={speeds}
|
||||
preferences={preferences}
|
||||
theme={theme}
|
||||
ruleData={ruleData}
|
||||
onClick={handleSpeedsClick}
|
||||
/>
|
||||
</div>
|
||||
<section className="ct-quick-info__inspiration">
|
||||
<h2 style={visuallyHidden}>Inspiration</h2>
|
||||
<Inspiration
|
||||
inspiration={!!inspiration}
|
||||
onToggle={handleToggleInspiration}
|
||||
onClick={handleInspirationLabelClick}
|
||||
isInteractive={!isReadonly}
|
||||
/>
|
||||
</section>
|
||||
<section className="ct-quick-info__health">
|
||||
<HitPointsBox />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import QuickInfo from "./QuickInfo";
|
||||
|
||||
export default QuickInfo;
|
||||
export { QuickInfo };
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { ManageIcon } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useAbilities } from "~/hooks/useAbilities";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import {
|
||||
Subsection,
|
||||
SubsectionFooter,
|
||||
} from "../../../Shared/components/Subsection";
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import SavingThrowsBox from "../../components/SavingThrowsBox";
|
||||
|
||||
export default function SavingThrowsDesktop() {
|
||||
const abilities = useAbilities();
|
||||
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
const ruleData = useSelector(rulesEngineSelectors.getRuleData);
|
||||
const savingThrowDiceAdjustments = useSelector(
|
||||
rulesEngineSelectors.getSavingThrowDiceAdjustments
|
||||
); // TODO: GFS move to mangers
|
||||
const situationalBonusSavingThrowsLookup = useSelector(
|
||||
rulesEngineSelectors.getSituationalBonusSavingThrowsLookup
|
||||
); // TODO: GFS move to mangers
|
||||
const deathSaveInfo = useSelector(rulesEngineSelectors.getDeathSaveInfo);
|
||||
const dimensions = useSelector(appEnvSelectors.getDimensions);
|
||||
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
const diceEnabled = useSelector(appEnvSelectors.getDiceEnabled);
|
||||
const characterRollContext = useSelector(
|
||||
characterRollContextSelectors.getCharacterRollContext
|
||||
);
|
||||
const handleManageShow = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.SAVING_THROWS);
|
||||
};
|
||||
|
||||
const handleAbilityClick = (ability: AbilityManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ABILITY_SAVING_THROW,
|
||||
PaneIdentifierUtils.generateAbilitySavingThrows(ability.getId())
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Subsection name="Abilities">
|
||||
<SavingThrowsBox
|
||||
ruleData={ruleData}
|
||||
abilities={abilities}
|
||||
savingThrowDiceAdjustments={savingThrowDiceAdjustments}
|
||||
situationalBonusSavingThrowsLookup={situationalBonusSavingThrowsLookup}
|
||||
deathSaveInfo={deathSaveInfo}
|
||||
onAbilityClick={handleAbilityClick}
|
||||
onInfoClick={handleManageShow}
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
diceEnabled={diceEnabled}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
<SubsectionFooter>
|
||||
<ManageIcon
|
||||
onClick={handleManageShow}
|
||||
tooltip={isReadonly ? "View" : "Manage"}
|
||||
theme={theme}
|
||||
>
|
||||
Saving Throws
|
||||
</ManageIcon>
|
||||
</SubsectionFooter>
|
||||
</Subsection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import SavingThrowsDesktop from "./SavingThrowsDesktop";
|
||||
|
||||
export default SavingThrowsDesktop;
|
||||
export { SavingThrowsDesktop };
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { ManageIcon } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
SenseInfo,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import {
|
||||
Subsection,
|
||||
SubsectionFooter,
|
||||
} from "../../../Shared/components/Subsection";
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import SensesBox from "../../components/SensesBox";
|
||||
import { SheetAppState } from "../../typings";
|
||||
|
||||
interface Props {
|
||||
passivePerception: number;
|
||||
passiveInvestigation: number;
|
||||
passiveInsight: number;
|
||||
senses: SenseInfo;
|
||||
dimensions: AppEnvDimensionsState;
|
||||
theme: CharacterTheme;
|
||||
isReadonly: boolean;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class SensesDesktop extends React.PureComponent<Props> {
|
||||
handleManageShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.SENSE_MANAGE);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
passiveInsight,
|
||||
passiveInvestigation,
|
||||
passivePerception,
|
||||
senses,
|
||||
isReadonly,
|
||||
dimensions,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Subsection name="Senses" className="ct-senses-desktop">
|
||||
<SensesBox
|
||||
senses={senses}
|
||||
passiveInsight={passiveInsight}
|
||||
passiveInvestigation={passiveInvestigation}
|
||||
passivePerception={passivePerception}
|
||||
onClick={this.handleManageShow}
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
/>
|
||||
<SubsectionFooter>
|
||||
<ManageIcon
|
||||
onClick={this.handleManageShow}
|
||||
tooltip={isReadonly ? "View" : "Manage"}
|
||||
theme={theme}
|
||||
>
|
||||
Senses
|
||||
</ManageIcon>
|
||||
</SubsectionFooter>
|
||||
</Subsection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
passivePerception: rulesEngineSelectors.getPassivePerception(state),
|
||||
passiveInvestigation: rulesEngineSelectors.getPassiveInvestigation(state),
|
||||
passiveInsight: rulesEngineSelectors.getPassiveInsight(state),
|
||||
senses: rulesEngineSelectors.getSenseInfo(state),
|
||||
dimensions: appEnvSelectors.getDimensions(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
};
|
||||
}
|
||||
|
||||
const SensesDesktopContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <SensesDesktop {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SensesDesktopContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import SensesDesktop from "./SensesDesktop";
|
||||
|
||||
export default SensesDesktop;
|
||||
export { SensesDesktop };
|
||||
@@ -0,0 +1,139 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { ManageIcon } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
Skill,
|
||||
SkillUtils,
|
||||
ValueLookup,
|
||||
CharacterTheme,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { IRollContext } from "@dndbeyond/dice";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import Subsection, {
|
||||
SubsectionFooter,
|
||||
} from "../../../Shared/components/Subsection";
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../Shared/selectors";
|
||||
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import SkillsBox from "../../components/SkillsBox";
|
||||
import { SheetAppState } from "../../typings";
|
||||
|
||||
interface Props {
|
||||
dimensions: AppEnvDimensionsState;
|
||||
theme: CharacterTheme;
|
||||
skills: Array<Skill>;
|
||||
customSkills: Array<Skill>;
|
||||
valueLookup: ValueLookup;
|
||||
isReadonly: boolean;
|
||||
diceEnabled: boolean;
|
||||
ruleData: RuleData;
|
||||
characterRollContext: IRollContext;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class SkillsDesktop extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
handleManageShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.SKILLS);
|
||||
};
|
||||
|
||||
handleCustomSkillClick = (skill: Skill): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CUSTOM_SKILL,
|
||||
PaneIdentifierUtils.generateCustomSkill(SkillUtils.getId(skill))
|
||||
);
|
||||
};
|
||||
|
||||
handleEmptyClick = (): void => {
|
||||
this.handleManageShow();
|
||||
};
|
||||
|
||||
handleSkillClick = (skill: Skill): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.SKILL,
|
||||
PaneIdentifierUtils.generateSkill(SkillUtils.getId(skill))
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dimensions,
|
||||
theme,
|
||||
skills,
|
||||
valueLookup,
|
||||
customSkills,
|
||||
isReadonly,
|
||||
diceEnabled,
|
||||
ruleData,
|
||||
characterRollContext,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Subsection name="Skills">
|
||||
<SkillsBox
|
||||
skills={skills}
|
||||
customSkills={customSkills}
|
||||
valueLookup={valueLookup}
|
||||
onCustomSkillClick={this.handleCustomSkillClick}
|
||||
onSkillClick={this.handleSkillClick}
|
||||
onEmptyClick={this.handleEmptyClick}
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
diceEnabled={diceEnabled}
|
||||
ruleData={ruleData}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
<SubsectionFooter>
|
||||
<ManageIcon
|
||||
onClick={this.handleManageShow}
|
||||
tooltip={isReadonly ? "View" : "Manage"}
|
||||
theme={theme}
|
||||
>
|
||||
Skills
|
||||
</ManageIcon>
|
||||
</SubsectionFooter>
|
||||
</Subsection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
skills: rulesEngineSelectors.getSkills(state),
|
||||
valueLookup: rulesEngineSelectors.getCharacterValueLookup(state),
|
||||
customSkills: rulesEngineSelectors.getCustomSkills(state),
|
||||
dimensions: appEnvSelectors.getDimensions(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
characterRollContext:
|
||||
characterRollContextSelectors.getCharacterRollContext(state),
|
||||
};
|
||||
}
|
||||
|
||||
const SkillsDesktopContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <SkillsDesktop {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SkillsDesktopContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import SkillsDesktop from "./SkillsDesktop";
|
||||
|
||||
export default SkillsDesktop;
|
||||
export { SkillsDesktop };
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
ApiRequestHelpers,
|
||||
rulesEngineSelectors,
|
||||
characterActions,
|
||||
SpellSlotContract,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { appEnvSelectors } from "../../../Shared/selectors";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import SpellSlotManagerGroup from "./SpellSlotManagerGroup";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
spellSlots: Array<SpellSlotContract>;
|
||||
pactSlots: Array<SpellSlotContract>;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
class SpellSlotManager extends React.PureComponent<Props> {
|
||||
handleSlotSet = (level: number, used: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
const spellLevelSpellSlotRequestsDataKey =
|
||||
ApiRequestHelpers.getSpellLevelSpellSlotRequestsDataKey(level);
|
||||
|
||||
if (spellLevelSpellSlotRequestsDataKey !== null) {
|
||||
dispatch(
|
||||
characterActions.spellLevelSpellSlotsSet({
|
||||
[spellLevelSpellSlotRequestsDataKey]: used,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handlePactSet = (level: number, used: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const spellLevelPactMagicRequestsDataKey =
|
||||
ApiRequestHelpers.getSpellLevelPactMagicRequestsDataKey(level);
|
||||
|
||||
if (spellLevelPactMagicRequestsDataKey !== null) {
|
||||
dispatch(
|
||||
characterActions.spellLevelPactMagicSlotsSet({
|
||||
[spellLevelPactMagicRequestsDataKey]: used,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { spellSlots, pactSlots, isReadonly } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-spell-slot-manager">
|
||||
<SpellSlotManagerGroup
|
||||
heading="Spell Slots"
|
||||
slots={spellSlots}
|
||||
onSlotSet={this.handleSlotSet}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
<SpellSlotManagerGroup
|
||||
heading="Pact Magic"
|
||||
slots={pactSlots}
|
||||
onSlotSet={this.handlePactSet}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
spellSlots: rulesEngineSelectors.getSpellSlots(state),
|
||||
pactSlots: rulesEngineSelectors.getPactMagicSlots(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(SpellSlotManager);
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleHeaderContent,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
FormatUtils,
|
||||
SpellSlotContract,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import SpellSlotManagerLevel from "../SpellSlotManagerLevel";
|
||||
|
||||
interface Props {
|
||||
heading: string;
|
||||
slots: Array<SpellSlotContract>;
|
||||
onSlotSet?: (level: number, used: number) => void;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
export default class SpellSlotManagerGroup extends React.PureComponent<Props> {
|
||||
renderCallout = (): React.ReactNode => {
|
||||
const { slots } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-spell-slot-manager__group-summary">
|
||||
{slots.map((levelSlots) => (
|
||||
<div
|
||||
className="ct-spell-slot-manager__group-level"
|
||||
key={levelSlots.level}
|
||||
>
|
||||
<div className="ct-spell-slot-manager__group-level-name">
|
||||
{FormatUtils.ordinalize(levelSlots.level)}
|
||||
</div>
|
||||
<div className="ct-spell-slot-manager__group-level-available">
|
||||
{levelSlots.available === 0 ? "-" : levelSlots.available}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { slots, onSlotSet, heading, isReadonly } = this.props;
|
||||
|
||||
let headerNode: React.ReactNode = (
|
||||
<CollapsibleHeaderContent
|
||||
heading={heading}
|
||||
callout={this.renderCallout()}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!slots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible header={headerNode} className="ct-spell-slot-manager__group">
|
||||
<div className="ct-spell-slot-manager__levels">
|
||||
{slots.map((levelSlots) => {
|
||||
if (levelSlots.available === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SpellSlotManagerLevel
|
||||
key={levelSlots.level}
|
||||
{...levelSlots}
|
||||
onSlotSet={onSlotSet}
|
||||
isInteractive={!isReadonly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import SpellSlotManagerGroup from "./SpellSlotManagerGroup";
|
||||
|
||||
export default SpellSlotManagerGroup;
|
||||
export { SpellSlotManagerGroup };
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import SlotManager from "../../../../Shared/components/SlotManager";
|
||||
|
||||
interface Props {
|
||||
level: number;
|
||||
available: number;
|
||||
used: number;
|
||||
onSlotSet?: (level: number, used: number) => void;
|
||||
isInteractive: boolean;
|
||||
}
|
||||
export default class SpellSlotManagerLevel extends React.PureComponent<Props> {
|
||||
handleSlotSet = (used: number): void => {
|
||||
const { onSlotSet, level } = this.props;
|
||||
|
||||
if (onSlotSet) {
|
||||
onSlotSet(level, used);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let { level, available, used, isInteractive } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-spell-slot-manager__level">
|
||||
<div className="ct-spell-slot-manager__level-name">
|
||||
{FormatUtils.ordinalize(level)} Level
|
||||
</div>
|
||||
<SlotManager
|
||||
available={available}
|
||||
used={used}
|
||||
onSet={this.handleSlotSet}
|
||||
size="small"
|
||||
isInteractive={isInteractive}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import SpellSlotManagerLevel from "./SpellSlotManagerLevel";
|
||||
|
||||
export default SpellSlotManagerLevel;
|
||||
export { SpellSlotManagerLevel };
|
||||
@@ -0,0 +1,6 @@
|
||||
import SpellSlotManager from "./SpellSlotManager";
|
||||
import SpellSlotManagerGroup from "./SpellSlotManagerGroup";
|
||||
import SpellSlotManagerLevel from "./SpellSlotManagerLevel";
|
||||
|
||||
export default SpellSlotManager;
|
||||
export { SpellSlotManager, SpellSlotManagerGroup, SpellSlotManagerLevel };
|
||||
@@ -0,0 +1,562 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
ConcentrationIcon,
|
||||
RitualIcon,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityLookup,
|
||||
ApiRequestHelpers,
|
||||
characterActions,
|
||||
CharacterTheme,
|
||||
ClassUtils,
|
||||
Constants,
|
||||
DataOriginRefData,
|
||||
ExperienceInfo,
|
||||
FormatUtils,
|
||||
Hack__BaseCharClass,
|
||||
HelperUtils,
|
||||
InventoryLookup,
|
||||
InventoryManager,
|
||||
ItemManager,
|
||||
RuleData,
|
||||
rulesEngineSelectors,
|
||||
ScaledSpell,
|
||||
SpellCasterInfo,
|
||||
SpellSlotContract,
|
||||
SpellUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { IRollContext } from "@dndbeyond/dice";
|
||||
|
||||
import { TabFilter } from "~/components/TabFilter";
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import SpellsLevel from "../../../Shared/components/SpellsLevel";
|
||||
import SpellsLevelCasting from "../../../Shared/components/SpellsLevelCasting";
|
||||
import { ThemeButton } from "../../../Shared/components/common/Button";
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../Shared/utils";
|
||||
import ContentGroup from "../../components/ContentGroup";
|
||||
import SpellsFilter from "../../components/SpellsFilter";
|
||||
import { SheetAppState } from "../../typings";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const ALL_LEVELS: number = -1;
|
||||
|
||||
function filterLevelSpells(
|
||||
levelSpells: Array<Array<ScaledSpell>>,
|
||||
testFunc: (spell: ScaledSpell) => boolean
|
||||
): Array<Array<ScaledSpell>> {
|
||||
return levelSpells.map((spells) => spells.filter((spell) => testFunc(spell)));
|
||||
}
|
||||
|
||||
function hasFilteredLevelSpells(
|
||||
levelSpells: Array<Array<ScaledSpell>>,
|
||||
testFunc: (spell: ScaledSpell) => boolean
|
||||
): boolean {
|
||||
return levelSpells.reduce((acc, spells) => {
|
||||
return (
|
||||
acc ||
|
||||
spells.reduce((spellAcc, spell) => {
|
||||
return spellAcc || testFunc(spell);
|
||||
}, false)
|
||||
);
|
||||
}, false);
|
||||
}
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
levelSpells: Array<Array<ScaledSpell>>;
|
||||
spellCasterInfo: SpellCasterInfo;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
xpInfo: ExperienceInfo;
|
||||
isReadonly: boolean;
|
||||
spellSlots: Array<SpellSlotContract>;
|
||||
pactMagicSlots: Array<SpellSlotContract>;
|
||||
showNotes: boolean;
|
||||
theme: CharacterTheme;
|
||||
diceEnabled: boolean;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
proficiencyBonus: number;
|
||||
characterRollContext: IRollContext;
|
||||
inventoryManager: InventoryManager;
|
||||
inventoryLookup: InventoryLookup;
|
||||
partyInventoryLookup: InventoryLookup;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
interface StateFilterData {
|
||||
filteredLevelSpells: Array<Array<ScaledSpell>>;
|
||||
showAdvancedFilters: boolean;
|
||||
isFiltering: boolean;
|
||||
}
|
||||
interface State {
|
||||
filterData: StateFilterData;
|
||||
ritualLevelSpells: Array<Array<ScaledSpell>>;
|
||||
concentrationLevelSpells: Array<Array<ScaledSpell>>;
|
||||
hasRitualSpells: boolean;
|
||||
hasConcentrationSpells: boolean;
|
||||
}
|
||||
class Spells extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
showNotes: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filterData: {
|
||||
filteredLevelSpells: [],
|
||||
showAdvancedFilters: false,
|
||||
isFiltering: false,
|
||||
},
|
||||
ritualLevelSpells: filterLevelSpells(
|
||||
props.levelSpells,
|
||||
SpellUtils.isRitual
|
||||
),
|
||||
concentrationLevelSpells: filterLevelSpells(
|
||||
props.levelSpells,
|
||||
SpellUtils.getConcentration
|
||||
),
|
||||
hasRitualSpells: hasFilteredLevelSpells(
|
||||
props.levelSpells,
|
||||
SpellUtils.isRitual
|
||||
),
|
||||
hasConcentrationSpells: hasFilteredLevelSpells(
|
||||
props.levelSpells,
|
||||
SpellUtils.getConcentration
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State): void {
|
||||
if (this.props.levelSpells !== prevProps.levelSpells) {
|
||||
this.setState({
|
||||
ritualLevelSpells: filterLevelSpells(
|
||||
this.props.levelSpells,
|
||||
SpellUtils.isRitual
|
||||
),
|
||||
concentrationLevelSpells: filterLevelSpells(
|
||||
this.props.levelSpells,
|
||||
SpellUtils.getConcentration
|
||||
),
|
||||
hasRitualSpells: hasFilteredLevelSpells(
|
||||
this.props.levelSpells,
|
||||
SpellUtils.isRitual
|
||||
),
|
||||
hasConcentrationSpells: hasFilteredLevelSpells(
|
||||
this.props.levelSpells,
|
||||
SpellUtils.getConcentration
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSpellSlotSet = (level: number, used: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const spellLevelSpellSlotRequestsDataKey =
|
||||
ApiRequestHelpers.getSpellLevelSpellSlotRequestsDataKey(level);
|
||||
if (spellLevelSpellSlotRequestsDataKey !== null) {
|
||||
dispatch(
|
||||
characterActions.spellLevelSpellSlotsSet({
|
||||
[spellLevelSpellSlotRequestsDataKey]: used,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellSlotChange = (level: number, changeAmount: number): void => {
|
||||
const { spellSlots } = this.props;
|
||||
|
||||
const usedAmount = this.getUsedSpellSlotLevelAmount(
|
||||
level,
|
||||
changeAmount,
|
||||
spellSlots
|
||||
);
|
||||
|
||||
if (usedAmount !== null) {
|
||||
this.handleSpellSlotSet(level, usedAmount);
|
||||
}
|
||||
};
|
||||
|
||||
handlePactSlotSet = (level: number, used: number): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const spellLevelPactMagicRequestsDataKey =
|
||||
ApiRequestHelpers.getSpellLevelPactMagicRequestsDataKey(level);
|
||||
|
||||
if (spellLevelPactMagicRequestsDataKey !== null) {
|
||||
dispatch(
|
||||
characterActions.spellLevelPactMagicSlotsSet({
|
||||
[spellLevelPactMagicRequestsDataKey]: used,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handlePactSlotChange = (level: number, changeAmount: number): void => {
|
||||
const { pactMagicSlots } = this.props;
|
||||
|
||||
const usedAmount = this.getUsedSpellSlotLevelAmount(
|
||||
level,
|
||||
changeAmount,
|
||||
pactMagicSlots
|
||||
);
|
||||
|
||||
if (usedAmount !== null) {
|
||||
this.handlePactSlotSet(level, usedAmount);
|
||||
}
|
||||
};
|
||||
|
||||
getUsedSpellSlotLevelAmount = (
|
||||
level: number,
|
||||
changeAmount: number,
|
||||
spellSlots: Array<SpellSlotContract>
|
||||
): number | null => {
|
||||
const foundSlotLevel = spellSlots.find(
|
||||
(spellSlot) => spellSlot.level === level
|
||||
);
|
||||
|
||||
if (!foundSlotLevel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usedAmount: number = foundSlotLevel.used + changeAmount;
|
||||
const maxAmount: number = foundSlotLevel.available;
|
||||
|
||||
return HelperUtils.clampInt(usedAmount, 0, maxAmount);
|
||||
};
|
||||
|
||||
handleSpellLimitedUseSet = (
|
||||
mappingId: number,
|
||||
mappingTypeId: number,
|
||||
uses: number,
|
||||
dataOrigin: Constants.DataOriginTypeEnum
|
||||
): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(
|
||||
characterActions.spellUseSet(mappingId, mappingTypeId, uses, dataOrigin)
|
||||
);
|
||||
};
|
||||
|
||||
handleItemLimitedUseSet = (
|
||||
mappingId: number,
|
||||
mappingTypeId: number,
|
||||
uses: number
|
||||
): void => {
|
||||
let item: ItemManager | null = null;
|
||||
item = ItemManager.getItem(mappingId);
|
||||
if (item) {
|
||||
item.handleItemLimitedUseSet(uses);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellDetailShow = (spell: ScaledSpell, castLevel: number): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
const mappingId = SpellUtils.getMappingId(spell);
|
||||
if (mappingId !== null) {
|
||||
const dataOrigin = SpellUtils.getDataOrigin(spell);
|
||||
const dataOriginType = SpellUtils.getDataOriginType(spell);
|
||||
|
||||
switch (dataOriginType) {
|
||||
case Constants.DataOriginTypeEnum.CLASS:
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CLASS_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateClassSpell(
|
||||
ClassUtils.getMappingId(
|
||||
dataOrigin.primary as Hack__BaseCharClass
|
||||
),
|
||||
mappingId,
|
||||
castLevel
|
||||
)
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CHARACTER_SPELL_DETAIL,
|
||||
PaneIdentifierUtils.generateCharacterSpell(mappingId, castLevel)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleManageSpellsOpen = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.SPELL_MANAGE);
|
||||
};
|
||||
|
||||
handleFilterUpdate = (filterData: StateFilterData): void => {
|
||||
this.setState({
|
||||
filterData,
|
||||
});
|
||||
};
|
||||
|
||||
renderEmptySpellLevel = (level: number): React.ReactNode => {
|
||||
let spellAbbr = FormatUtils.renderSpellLevelAbbreviation(level);
|
||||
return (
|
||||
<div className="ct-spells__empty-spell-level">
|
||||
You do not have any {spellAbbr}-level spells or spells that scale to{" "}
|
||||
{spellAbbr} level available, but you can cast lower level spells using
|
||||
your {spellAbbr}-level spell slots.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderManageButton = (): React.ReactNode => {
|
||||
const { isReadonly } = this.props;
|
||||
|
||||
if (isReadonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeButton
|
||||
style="outline"
|
||||
size="medium"
|
||||
onClick={this.handleManageSpellsOpen}
|
||||
>
|
||||
Manage Spells
|
||||
</ThemeButton>
|
||||
);
|
||||
};
|
||||
|
||||
renderSpellLevels = (
|
||||
levelSpells: Array<Array<ScaledSpell>>,
|
||||
showLevels: Array<number>,
|
||||
filterRitual: boolean = false,
|
||||
filterConcentration: boolean = false
|
||||
): React.ReactNode => {
|
||||
const { filterData, ritualLevelSpells, concentrationLevelSpells } =
|
||||
this.state;
|
||||
const {
|
||||
spellCasterInfo,
|
||||
ruleData,
|
||||
xpInfo,
|
||||
abilityLookup,
|
||||
isReadonly,
|
||||
showNotes,
|
||||
diceEnabled,
|
||||
theme,
|
||||
dataOriginRefData,
|
||||
proficiencyBonus,
|
||||
characterRollContext,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{levelSpells.map((spells, level) => {
|
||||
if (!showLevels.includes(ALL_LEVELS) && !showLevels.includes(level)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let levelHasSlots =
|
||||
spellCasterInfo.availablePactMagicLevels.includes(level) ||
|
||||
spellCasterInfo.availableSpellLevels.includes(level);
|
||||
|
||||
if (filterRitual) {
|
||||
spells = ritualLevelSpells[level];
|
||||
}
|
||||
if (filterConcentration) {
|
||||
spells = concentrationLevelSpells[level];
|
||||
}
|
||||
|
||||
if ((filterRitual || filterConcentration) && !spells.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!spells.length && !levelHasSlots) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let filteredSpells = filterData.filteredLevelSpells[level];
|
||||
|
||||
if (filteredSpells && filterRitual) {
|
||||
filteredSpells = filteredSpells.filter((spell) =>
|
||||
SpellUtils.isRitual(spell)
|
||||
);
|
||||
}
|
||||
if (filteredSpells && filterConcentration) {
|
||||
filteredSpells = filteredSpells.filter((spell) =>
|
||||
SpellUtils.getConcentration(spell)
|
||||
);
|
||||
}
|
||||
|
||||
if (!filteredSpells) {
|
||||
return null;
|
||||
}
|
||||
let extraNode: React.ReactNode = (
|
||||
<SpellsLevelCasting
|
||||
level={level}
|
||||
spellCasterInfo={spellCasterInfo}
|
||||
onSpellSlotSet={this.handleSpellSlotSet}
|
||||
onPactSlotSet={this.handlePactSlotSet}
|
||||
showCastingInfo={false}
|
||||
isInteractive={!isReadonly}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ContentGroup
|
||||
header={FormatUtils.renderSpellLevelName(level)}
|
||||
extra={extraNode}
|
||||
key={level}
|
||||
>
|
||||
{spells.length ? (
|
||||
<SpellsLevel
|
||||
level={level}
|
||||
spells={filteredSpells}
|
||||
spellCasterInfo={spellCasterInfo}
|
||||
ruleData={ruleData}
|
||||
xpInfo={xpInfo}
|
||||
abilityLookup={abilityLookup}
|
||||
onSpellClick={this.handleSpellDetailShow}
|
||||
onSpellSlotChange={this.handleSpellSlotChange}
|
||||
onPactSlotChange={this.handlePactSlotChange}
|
||||
onSpellLimitedUseSet={this.handleSpellLimitedUseSet}
|
||||
onItemLimitedUseSet={this.handleItemLimitedUseSet}
|
||||
isInteractive={!isReadonly}
|
||||
showNotes={showNotes}
|
||||
diceEnabled={diceEnabled}
|
||||
theme={theme}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
) : (
|
||||
this.renderEmptySpellLevel(level)
|
||||
)}
|
||||
</ContentGroup>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { filterData, hasConcentrationSpells, hasRitualSpells } = this.state;
|
||||
const { ruleData, abilityLookup, levelSpells, spellCasterInfo, theme } =
|
||||
this.props;
|
||||
|
||||
return (
|
||||
<section className="ct-spells">
|
||||
<h2 style={visuallyHidden}>Spells</h2>
|
||||
<div className="ct-spells__casting">
|
||||
<SpellsLevelCasting
|
||||
spellCasterInfo={spellCasterInfo}
|
||||
showSlots={false}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
<SpellsFilter
|
||||
abilityLookup={abilityLookup}
|
||||
levelSpells={levelSpells}
|
||||
spellCasterInfo={spellCasterInfo}
|
||||
ruleData={ruleData}
|
||||
onDataUpdate={this.handleFilterUpdate}
|
||||
callout={this.renderManageButton()}
|
||||
theme={theme}
|
||||
/>
|
||||
{!filterData.showAdvancedFilters && (
|
||||
<div className="ct-spells__content">
|
||||
<TabFilter
|
||||
className={styles.tabFilter}
|
||||
filters={[
|
||||
...levelSpells?.map((spells, level) => {
|
||||
let levelHasSlots =
|
||||
spellCasterInfo.availablePactMagicLevels.includes(level) ||
|
||||
spellCasterInfo.availableSpellLevels.includes(level);
|
||||
|
||||
if (!spells.length && !levelHasSlots)
|
||||
return { label: "", content: null };
|
||||
|
||||
let filterBadgeText: number | null = null;
|
||||
const filteredSpells = filterData.filteredLevelSpells[level];
|
||||
if (filterData.isFiltering && filteredSpells.length) {
|
||||
filterBadgeText = filteredSpells.length;
|
||||
}
|
||||
|
||||
return {
|
||||
label: `${FormatUtils.renderSpellLevelAbbreviation(level)}`,
|
||||
badge: filterBadgeText ?? undefined,
|
||||
content: this.renderSpellLevels(levelSpells, [level]),
|
||||
};
|
||||
}),
|
||||
...(hasConcentrationSpells
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<ConcentrationIcon
|
||||
themeMode={theme?.isDarkMode ? "gray" : "dark"}
|
||||
/>
|
||||
),
|
||||
content: this.renderSpellLevels(
|
||||
levelSpells,
|
||||
[ALL_LEVELS],
|
||||
false,
|
||||
true
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(hasRitualSpells
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<RitualIcon
|
||||
themeMode={theme?.isDarkMode ? "gray" : "dark"}
|
||||
/>
|
||||
),
|
||||
content: this.renderSpellLevels(
|
||||
levelSpells,
|
||||
[ALL_LEVELS],
|
||||
true
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
levelSpells: rulesEngineSelectors.getLevelSpells(state),
|
||||
spellCasterInfo: rulesEngineSelectors.getSpellCasterInfo(state),
|
||||
abilityLookup: rulesEngineSelectors.getAbilityLookup(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
xpInfo: rulesEngineSelectors.getExperienceInfo(state),
|
||||
spellSlots: rulesEngineSelectors.getSpellSlots(state),
|
||||
pactMagicSlots: rulesEngineSelectors.getPactMagicSlots(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
dataOriginRefData: rulesEngineSelectors.getDataOriginRefData(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
characterRollContext:
|
||||
characterRollContextSelectors.getCharacterRollContext(state),
|
||||
};
|
||||
}
|
||||
|
||||
const SpellsContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <Spells {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SpellsContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import Spells from "./Spells";
|
||||
|
||||
export default Spells;
|
||||
export { Spells };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Actions from "../../Actions";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class ActionsMobile extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionMobile>
|
||||
<section className="ct-actions-mobile">
|
||||
<h2 style={visuallyHidden}>Actions</h2>
|
||||
<MobileDivider label="Actions" theme={theme} />
|
||||
<Actions showNotes={false} />
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</section>
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ActionsMobile);
|
||||
@@ -0,0 +1,4 @@
|
||||
import ActionsMobile from "./ActionsMobile";
|
||||
|
||||
export default ActionsMobile;
|
||||
export { ActionsMobile };
|
||||
@@ -0,0 +1,244 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterPreferences,
|
||||
SpeedInfo,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
rulesEngineSelectors,
|
||||
CharacterTheme,
|
||||
characterSelectors,
|
||||
CharacterStatusSlug,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { PremadeCharacterEditStatus } from "~/components/PremadeCharacterEditStatus";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { InitiativeBox } from "~/subApps/sheet/components/InitiativeBox";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
armorClass: number;
|
||||
speeds: SpeedInfo;
|
||||
proficiencyBonus: number;
|
||||
preferences: CharacterPreferences;
|
||||
ruleData: RuleData;
|
||||
diceEnabled: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
isReadonly: boolean;
|
||||
characterStatus: CharacterStatusSlug | null;
|
||||
}
|
||||
class CombatMobile extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
handleProficiencyBonusClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.PROFICIENCY_BONUS);
|
||||
};
|
||||
|
||||
handleSpeedsClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.SPEED_MANAGE);
|
||||
};
|
||||
|
||||
handleDefensesSummaryClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.DEFENSE_MANAGE);
|
||||
};
|
||||
|
||||
handleConditionSummaryClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.CONDITION_MANAGE);
|
||||
};
|
||||
|
||||
handleArmorClassClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.ARMOR_MANAGE);
|
||||
};
|
||||
|
||||
handleInitiativeClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.INITIATIVE);
|
||||
};
|
||||
|
||||
renderProficiency = (): React.ReactNode => {
|
||||
const { proficiencyBonus } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ct-combat-mobile__extra ct-combat-mobile__extra--proficiency"
|
||||
onClick={this.handleProficiencyBonusClick}
|
||||
>
|
||||
<div className="ct-combat-mobile__extra-heading">
|
||||
<div className="ct-combat-mobile__extra-label">Proficiency</div>
|
||||
</div>
|
||||
<div className="ct-combat-mobile__extra-value">
|
||||
<NumberDisplay
|
||||
type="signed"
|
||||
number={proficiencyBonus}
|
||||
size={"large"}
|
||||
className={clsx([
|
||||
!this.props.theme.isDarkMode && styles.numberColorOverride,
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-combat-mobile__extra-footer">
|
||||
<div className="ct-combat-mobile__extra-label">Bonus</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderSpeed = (): React.ReactNode => {
|
||||
const { preferences, speeds, ruleData, theme } = this.props;
|
||||
|
||||
const displaySpeedValue: number =
|
||||
speeds[
|
||||
RuleDataUtils.getSpeedMovementKeyById(preferences.primaryMovement)
|
||||
];
|
||||
const displaySpeedLabel = RuleDataUtils.getMovementDescription(
|
||||
preferences.primaryMovement,
|
||||
ruleData
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ct-combat-mobile__extra ct-combat-mobile__extra--speed"
|
||||
onClick={this.handleSpeedsClick}
|
||||
>
|
||||
<div className="ct-combat-mobile__extra-heading">
|
||||
<div className="ct-combat-mobile__extra-subheading">
|
||||
{displaySpeedLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-combat-mobile__extra-value">
|
||||
<NumberDisplay
|
||||
type="distanceInFt"
|
||||
number={displaySpeedValue}
|
||||
size={"large"}
|
||||
className={clsx([
|
||||
!this.props.theme.isDarkMode && styles.numberColorOverride,
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-combat-mobile__extra-footer">
|
||||
<div className="ct-combat-mobile__extra-label">Speed</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderArmorClass = (): React.ReactNode => {
|
||||
const { armorClass } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ct-combat-mobile__extra ct-combat-mobile__extra--ac"
|
||||
onClick={this.handleArmorClassClick}
|
||||
>
|
||||
<div className="ct-combat-mobile__extra-heading">
|
||||
<div className="ct-combat-mobile__extra-label">Armor</div>
|
||||
</div>
|
||||
<div
|
||||
className="ct-combat-mobile__extra-value"
|
||||
data-testid="armor-class-value"
|
||||
>
|
||||
{armorClass}
|
||||
</div>
|
||||
<div className="ct-combat-mobile__extra-footer">
|
||||
<div className="ct-combat-mobile__extra-label">Class</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme, isReadonly, characterStatus } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={`ct-combat-mobile ${
|
||||
theme.isDarkMode ? "ct-combat-mobile--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="ct-combat-mobile__extras">
|
||||
{this.renderProficiency()}
|
||||
{this.renderSpeed()}
|
||||
<InitiativeBox isMobile={true} />
|
||||
{this.renderArmorClass()}
|
||||
<div className="ct-combat-mobile__extra ct-combat-mobile__extra--statuses">
|
||||
<div className="ct-combat-mobile__ctas">
|
||||
<div className="ct-combat-mobile__cta">
|
||||
<div
|
||||
className="ct-combat-mobile__cta-button"
|
||||
onClick={this.handleDefensesSummaryClick}
|
||||
>
|
||||
Defenses
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-combat-mobile__cta">
|
||||
<div
|
||||
className="ct-combat-mobile__cta-button"
|
||||
onClick={this.handleConditionSummaryClick}
|
||||
>
|
||||
Conditions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PremadeCharacterEditStatus
|
||||
characterStatus={characterStatus}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
armorClass: rulesEngineSelectors.getAcTotal(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
speeds: rulesEngineSelectors.getCurrentCarriedWeightSpeed(state),
|
||||
preferences: rulesEngineSelectors.getCharacterPreferences(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
characterStatus: characterSelectors.getStatusSlug(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
};
|
||||
}
|
||||
|
||||
const CombatMobileContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <CombatMobile {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
CombatMobileContainer.contextType = GameLogContext;
|
||||
|
||||
export default connect(mapStateToProps)(CombatMobileContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CombatMobile from "./CombatMobile";
|
||||
|
||||
export default CombatMobile;
|
||||
export { CombatMobile };
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { FC } from "react";
|
||||
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import Description from "../../Description";
|
||||
|
||||
export const DescriptionMobile: FC = () => {
|
||||
const { characterTheme } = useCharacterEngine();
|
||||
return (
|
||||
<SubsectionMobile className="ct-description-mobile">
|
||||
<MobileDivider label={"Background"} theme={characterTheme} />
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Background</h2>
|
||||
<Description theme={characterTheme} isVertical={true} />
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={characterTheme} />
|
||||
</SubsectionMobile>
|
||||
);
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import { Equipment } from "../../Equipment";
|
||||
|
||||
interface Props {
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class EquipmentMobile extends React.PureComponent<Props> {
|
||||
handleManageClick = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isReadonly, theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionMobile>
|
||||
<MobileDivider
|
||||
label="Inventory"
|
||||
onClick={this.handleManageClick}
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
/>
|
||||
<section className="ct-equipment-mobile">
|
||||
<h2 style={visuallyHidden}>Inventory</h2>
|
||||
<Equipment showNotes={false} />
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
const EquipmentMobileContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <EquipmentMobile paneHistoryStart={paneHistoryStart} {...props} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(EquipmentMobileContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import EquipmentMobile from "./EquipmentMobile";
|
||||
|
||||
export default EquipmentMobile;
|
||||
export { EquipmentMobile };
|
||||
@@ -0,0 +1,68 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Extras from "../../Extras";
|
||||
|
||||
interface Props {
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class ExtrasMobile extends React.PureComponent<Props> {
|
||||
handleManageOpen = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.EXTRA_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isReadonly, theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionMobile>
|
||||
<MobileDivider
|
||||
label="Extras"
|
||||
onClick={this.handleManageOpen}
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
/>
|
||||
<section className="ct-extras-mobile">
|
||||
<h2 style={visuallyHidden}>Extras</h2>
|
||||
<Extras showNotes={false} />
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
const ExtrasMobileContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <ExtrasMobile paneHistoryStart={paneHistoryStart} {...props} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ExtrasMobileContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import ExtrasMobile from "./ExtrasMobile";
|
||||
|
||||
export default ExtrasMobile;
|
||||
export { ExtrasMobile };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Features from "../../Features";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class FeaturesMobile extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionMobile>
|
||||
<MobileDivider label="Features & Traits" theme={theme} />
|
||||
<section className="ct-features-mobile">
|
||||
<h2 style={visuallyHidden}>Features and Traits</h2>
|
||||
<Features />
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(FeaturesMobile);
|
||||
@@ -0,0 +1,4 @@
|
||||
import FeaturesMobile from "./FeaturesMobile";
|
||||
|
||||
export default FeaturesMobile;
|
||||
export { FeaturesMobile };
|
||||
@@ -0,0 +1,176 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
AbilitySummary,
|
||||
CampaignSummary,
|
||||
SavingThrowsSummary,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
DataOrigin,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useAbilities } from "~/hooks/useAbilities";
|
||||
|
||||
import { getDataOriginComponentInfo } from "../../../../../../subApps/sheet/components/Sidebar/helpers/paneUtils";
|
||||
import { PaneComponentEnum } from "../../../../../../subApps/sheet/components/Sidebar/types";
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../../Shared/utils";
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SavingThrowsDetails from "../../../components/SavingThrowsDetails";
|
||||
import Senses from "../../../components/Senses";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
|
||||
export default function MainMobile() {
|
||||
const abilities = useAbilities();
|
||||
const {
|
||||
pane: { paneHistoryStart, paneHistoryPush },
|
||||
} = useSidebar();
|
||||
const preferences = useSelector(rulesEngineSelectors.getCharacterPreferences);
|
||||
const savingThrowDiceAdjustments = useSelector(
|
||||
rulesEngineSelectors.getSavingThrowDiceAdjustments
|
||||
); // TODO: GFS move to mangers
|
||||
const situationalBonusSavingThrowsLookup = useSelector(
|
||||
rulesEngineSelectors.getSituationalBonusSavingThrowsLookup
|
||||
); // TODO: GFS move to mangers
|
||||
const passivePerception = useSelector(
|
||||
rulesEngineSelectors.getPassivePerception
|
||||
); // TODO: GFS move to mangers
|
||||
const passiveInvestigation = useSelector(
|
||||
rulesEngineSelectors.getPassiveInvestigation
|
||||
); // TODO: GFS move to mangers
|
||||
const passiveInsight = useSelector(rulesEngineSelectors.getPassiveInsight); // TODO: GFS move to mangers
|
||||
const senses = useSelector(rulesEngineSelectors.getSenseInfo); // TODO: GFS move to mangers
|
||||
const ruleData = useSelector(rulesEngineSelectors.getRuleData);
|
||||
const campaign = useSelector(rulesEngineSelectors.getCampaign);
|
||||
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
|
||||
const deathSaveInfo = useSelector(rulesEngineSelectors.getDeathSaveInfo);
|
||||
const diceEnabled = useSelector(appEnvSelectors.getDiceEnabled);
|
||||
const characterRollContext = useSelector(
|
||||
characterRollContextSelectors.getCharacterRollContext
|
||||
);
|
||||
const gameLog = useSelector(appEnvSelectors.getGameLog);
|
||||
|
||||
const handleSensesManageShow = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.SENSE_MANAGE);
|
||||
};
|
||||
|
||||
const handleSavingThrowsClick = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.SAVING_THROWS);
|
||||
};
|
||||
|
||||
const handleAbilitySummaryClick = (ability: AbilityManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ABILITY,
|
||||
PaneIdentifierUtils.generateAbility(ability)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAbilityClick = (ability: AbilityManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ABILITY_SAVING_THROW,
|
||||
PaneIdentifierUtils.generateAbilitySavingThrows(ability.getId())
|
||||
);
|
||||
};
|
||||
|
||||
const handleDataOriginClick = (dataOrigin: DataOrigin): void => {
|
||||
let component = getDataOriginComponentInfo(dataOrigin);
|
||||
if (component.type !== PaneComponentEnum.ERROR_404) {
|
||||
paneHistoryPush(component.type, component.identifiers);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCampaignShow = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
|
||||
};
|
||||
|
||||
return (
|
||||
<SubsectionMobile name="Main">
|
||||
<div
|
||||
className={`ct-main-mobile ${
|
||||
theme.isDarkMode ? "ct-main-mobile--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{campaign && (
|
||||
<CampaignSummary
|
||||
campaign={campaign}
|
||||
onCampaignShow={handleCampaignShow}
|
||||
className={
|
||||
theme.isDarkMode
|
||||
? "ct-main-mobile__campaign-button--dark-mode"
|
||||
: ""
|
||||
}
|
||||
theme={theme}
|
||||
gameLog={gameLog}
|
||||
/>
|
||||
)}
|
||||
<section className="ct-main-mobile__abilities">
|
||||
<h2 style={visuallyHidden}>Ability Scores</h2>
|
||||
{abilities.map((ability) => (
|
||||
<div className="ct-main-mobile__ability" key={ability.getStatKey()}>
|
||||
<AbilitySummary
|
||||
ability={ability}
|
||||
preferences={preferences}
|
||||
theme={theme}
|
||||
onClick={handleAbilitySummaryClick}
|
||||
diceEnabled={diceEnabled}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<MobileDivider
|
||||
label="Saving Throws"
|
||||
onClick={handleSavingThrowsClick}
|
||||
theme={theme}
|
||||
/>
|
||||
<section
|
||||
className={`ct-main-mobile__saving-throws ${
|
||||
theme.isDarkMode ? "ct-main-mobile__saving-throws--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
<h2 style={visuallyHidden}>Saving Throws</h2>
|
||||
<SavingThrowsSummary
|
||||
abilities={abilities}
|
||||
situationalBonusSavingThrowsLookup={
|
||||
situationalBonusSavingThrowsLookup
|
||||
}
|
||||
onClick={handleAbilityClick}
|
||||
diceEnabled={diceEnabled}
|
||||
theme={theme}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
<SavingThrowsDetails
|
||||
theme={theme}
|
||||
ruleData={ruleData}
|
||||
savingThrowDiceAdjustments={savingThrowDiceAdjustments}
|
||||
onDataOriginClick={handleDataOriginClick}
|
||||
deathSaveInfo={deathSaveInfo}
|
||||
/>
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
<MobileDivider
|
||||
label="Senses"
|
||||
onClick={handleSensesManageShow}
|
||||
theme={theme}
|
||||
/>
|
||||
<h2 style={visuallyHidden}>Senses</h2>
|
||||
<Senses
|
||||
senses={senses}
|
||||
theme={theme}
|
||||
passiveInsight={Number(passiveInsight)}
|
||||
passiveInvestigation={Number(passiveInvestigation)}
|
||||
passivePerception={Number(passivePerception)}
|
||||
onClick={handleSensesManageShow}
|
||||
/>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</div>
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import MainMobile from "./MainMobile";
|
||||
|
||||
export default MainMobile;
|
||||
export { MainMobile };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Notes from "../../Notes";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class NotesMobile extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionMobile className="ct-notes-mobile">
|
||||
<MobileDivider label="Notes" theme={theme} />
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Notes</h2>
|
||||
<Notes />
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NotesMobile);
|
||||
@@ -0,0 +1,4 @@
|
||||
import NotesMobile from "./NotesMobile";
|
||||
|
||||
export default NotesMobile;
|
||||
export { NotesMobile };
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { FC } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import ProficiencyGroups from "../../../components/ProficiencyGroups";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
|
||||
interface Props {}
|
||||
export const ProficiencyGroupsMobile: FC<Props> = () => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
const { proficiencyGroups, characterTheme } = useCharacterEngine();
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
|
||||
const handleManageShow = (): void => {
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.PROFICIENCIES);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubsectionMobile name="Proficiency Groups">
|
||||
<MobileDivider
|
||||
label={"Proficiencies & Training"}
|
||||
onClick={handleManageShow}
|
||||
isReadonly={isReadonly}
|
||||
theme={characterTheme}
|
||||
/>
|
||||
<section className="ct-proficiency-groups-mobile">
|
||||
<h2 style={visuallyHidden}>Proficiencies and Training</h2>
|
||||
<ProficiencyGroups
|
||||
proficiencyGroups={proficiencyGroups}
|
||||
onClick={handleManageShow}
|
||||
/>
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={characterTheme} />
|
||||
</SubsectionMobile>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
rulesEngineSelectors,
|
||||
Skill,
|
||||
SkillUtils,
|
||||
ValueLookup,
|
||||
CharacterTheme,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { IRollContext } from "@dndbeyond/dice";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../../Shared/utils";
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import Skills from "../../../components/Skills";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
|
||||
interface Props {
|
||||
skills: Array<Skill>;
|
||||
customSkills: Array<Skill>;
|
||||
valueLookup: ValueLookup;
|
||||
theme: CharacterTheme;
|
||||
diceEnabled: boolean;
|
||||
ruleData: RuleData;
|
||||
characterRollContext: IRollContext;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class SkillsBox extends React.PureComponent<Props> {
|
||||
handleManageShow = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(PaneComponentEnum.SKILLS);
|
||||
};
|
||||
|
||||
handleCustomSkillClick = (skill: Skill): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CUSTOM_SKILL,
|
||||
PaneIdentifierUtils.generateCustomSkill(SkillUtils.getId(skill))
|
||||
);
|
||||
};
|
||||
|
||||
handleEmptyClick = (): void => {
|
||||
this.handleManageShow();
|
||||
};
|
||||
|
||||
handleSkillClick = (skill: Skill): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.SKILL,
|
||||
PaneIdentifierUtils.generateSkill(SkillUtils.getId(skill))
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
skills,
|
||||
valueLookup,
|
||||
customSkills,
|
||||
theme,
|
||||
diceEnabled,
|
||||
ruleData,
|
||||
characterRollContext,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionMobile name="Skills">
|
||||
<section
|
||||
className={`ct-skills-mobile ${
|
||||
theme.isDarkMode ? "ct-skills-mobile--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
<MobileDivider
|
||||
label="Skills"
|
||||
onClick={this.handleManageShow}
|
||||
theme={theme}
|
||||
/>
|
||||
<h2 style={visuallyHidden}>Skills</h2>
|
||||
<Skills
|
||||
skills={skills}
|
||||
customSkills={customSkills}
|
||||
valueLookup={valueLookup}
|
||||
onCustomSkillClick={this.handleCustomSkillClick}
|
||||
onSkillClick={this.handleSkillClick}
|
||||
onEmptyClick={this.handleEmptyClick}
|
||||
diceEnabled={diceEnabled}
|
||||
ruleData={ruleData}
|
||||
theme={theme}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</section>
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
skills: rulesEngineSelectors.getSkills(state),
|
||||
valueLookup: rulesEngineSelectors.getCharacterValueLookup(state),
|
||||
customSkills: rulesEngineSelectors.getCustomSkills(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
characterRollContext:
|
||||
characterRollContextSelectors.getCharacterRollContext(state),
|
||||
};
|
||||
}
|
||||
|
||||
const SkillsBoxContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <SkillsBox {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SkillsBoxContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import SkillsMobile from "./SkillsMobile";
|
||||
|
||||
export default SkillsMobile;
|
||||
export { SkillsMobile };
|
||||
@@ -0,0 +1,68 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import MobileDivider from "../../../components/MobileDivider";
|
||||
import SubsectionMobile from "../../../components/SubsectionMobile";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Spells from "../../Spells";
|
||||
|
||||
interface Props {
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class SpellsMobile extends React.PureComponent<Props> {
|
||||
handleManageSpellsOpen = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.SPELL_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isReadonly, theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionMobile>
|
||||
<MobileDivider
|
||||
label="Spells"
|
||||
onClick={this.handleManageSpellsOpen}
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
/>
|
||||
<section className="ct-spells-mobile">
|
||||
<h2 style={visuallyHidden}>Spells</h2>
|
||||
<Spells showNotes={false} />
|
||||
</section>
|
||||
<MobileDivider isEnd={true} theme={theme} />
|
||||
</SubsectionMobile>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
const SpellsMobileContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <SpellsMobile {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SpellsMobileContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import SpellsMobile from "./SpellsMobile";
|
||||
|
||||
export default SpellsMobile;
|
||||
export { SpellsMobile };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
import TabletBox from "../../../components/TabletBox";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Actions from "../../Actions";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class ActionsTablet extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionTablet>
|
||||
<TabletBox theme={theme} header="Actions" className="ct-actions-tablet">
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Actions</h2>
|
||||
<Actions />
|
||||
</section>
|
||||
</TabletBox>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ActionsTablet);
|
||||
@@ -0,0 +1,4 @@
|
||||
import ActionsTablet from "./ActionsTablet";
|
||||
|
||||
export default ActionsTablet;
|
||||
export { ActionsTablet };
|
||||
@@ -0,0 +1,160 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { ArmorClassBox } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CharacterPreferences,
|
||||
SpeedInfo,
|
||||
RuleData,
|
||||
rulesEngineSelectors,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { InitiativeBox } from "~/subApps/sheet/components/InitiativeBox";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import ProficiencyBonusBox from "../../../components/ProficiencyBonusBox";
|
||||
import SpeedBox from "../../../components/SpeedBox";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
|
||||
interface Props {
|
||||
armorClass: number;
|
||||
speeds: SpeedInfo;
|
||||
proficiencyBonus: number;
|
||||
preferences: CharacterPreferences;
|
||||
theme: CharacterTheme;
|
||||
ruleData: RuleData;
|
||||
isReadonly: boolean;
|
||||
diceEnabled: boolean;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class CombatTablet extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
handleProficiencyBonusClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.PROFICIENCY_BONUS);
|
||||
};
|
||||
|
||||
handleSpeedsClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.SPEED_MANAGE);
|
||||
};
|
||||
|
||||
handleDefensesSummaryClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.DEFENSE_MANAGE);
|
||||
};
|
||||
|
||||
handleConditionSummaryClick = (evt: React.MouseEvent): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
paneHistoryStart(PaneComponentEnum.CONDITION_MANAGE);
|
||||
};
|
||||
|
||||
handleArmorClassClick = (): void => {
|
||||
const { paneHistoryStart } = this.props;
|
||||
paneHistoryStart(PaneComponentEnum.ARMOR_MANAGE);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
preferences,
|
||||
proficiencyBonus,
|
||||
speeds,
|
||||
armorClass,
|
||||
theme,
|
||||
ruleData,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="ct-combat-tablet">
|
||||
<div className="ct-combat-tablet__extras">
|
||||
<div className="ct-combat-tablet__extra ct-combat-tablet__extra--proficiency">
|
||||
<ProficiencyBonusBox
|
||||
theme={theme}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
onClick={this.handleProficiencyBonusClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-combat-tablet__extra ct-combat-tablet__extra--speed">
|
||||
<SpeedBox
|
||||
speeds={speeds}
|
||||
preferences={preferences}
|
||||
theme={theme}
|
||||
ruleData={ruleData}
|
||||
onClick={this.handleSpeedsClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-combat-tablet__extra ct-combat-tablet__extra--initiative">
|
||||
<InitiativeBox isTablet />
|
||||
</div>
|
||||
<div className="ct-combat-tablet__extra ct-combat-tablet__extra--ac">
|
||||
<ArmorClassBox
|
||||
theme={theme}
|
||||
armorClass={armorClass}
|
||||
onClick={this.handleArmorClassClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-combat-tablet__extra ct-combat-tablet__extra--statuses">
|
||||
<div className="ct-combat-tablet__ctas">
|
||||
<div className="ct-combat-tablet__cta">
|
||||
<div
|
||||
role="button"
|
||||
className={`ct-combat-tablet__cta-button ${
|
||||
theme.isDarkMode
|
||||
? "ct-combat-tablet__cta-button--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
onClick={this.handleDefensesSummaryClick}
|
||||
>
|
||||
Defenses
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-combat-tablet__cta">
|
||||
<div
|
||||
role="button"
|
||||
className={`ct-combat-tablet__cta-button ${
|
||||
theme.isDarkMode
|
||||
? "ct-combat-tablet__cta-button--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
onClick={this.handleConditionSummaryClick}
|
||||
>
|
||||
Conditions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
armorClass: rulesEngineSelectors.getAcTotal(state),
|
||||
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
|
||||
speeds: rulesEngineSelectors.getCurrentCarriedWeightSpeed(state),
|
||||
preferences: rulesEngineSelectors.getCharacterPreferences(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
diceEnabled: appEnvSelectors.getDiceEnabled(state),
|
||||
};
|
||||
}
|
||||
const CombatTabletContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <CombatTablet {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
export default connect(mapStateToProps)(CombatTabletContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import CombatTablet from "./CombatTablet";
|
||||
|
||||
export default CombatTablet;
|
||||
export { CombatTablet };
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { FC } from "react";
|
||||
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
import TabletBox from "../../../components/TabletBox";
|
||||
import Description from "../../Description";
|
||||
|
||||
export const DescriptionTablet: FC = () => {
|
||||
const { characterTheme } = useCharacterEngine();
|
||||
|
||||
return (
|
||||
<SubsectionTablet>
|
||||
<TabletBox
|
||||
header={"Background"}
|
||||
className="ct-description-tablet"
|
||||
theme={characterTheme}
|
||||
>
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Background</h2>
|
||||
<Description theme={characterTheme} />
|
||||
</section>
|
||||
</TabletBox>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
import TabletBox from "../../../components/TabletBox";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Equipment from "../../Equipment";
|
||||
|
||||
interface Props {
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class EquipmentTablet extends React.PureComponent<Props> {
|
||||
handleManageClick = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isReadonly, theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionTablet>
|
||||
<TabletBox
|
||||
header="Inventory"
|
||||
className="ct-equipment-tablet"
|
||||
onHeaderClick={this.handleManageClick}
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
>
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Inventory</h2>
|
||||
<Equipment />
|
||||
</section>
|
||||
</TabletBox>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
const EquipmentTabletContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
return <EquipmentTablet {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(EquipmentTabletContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import EquipmentTablet from "./EquipmentTablet";
|
||||
|
||||
export default EquipmentTablet;
|
||||
export { EquipmentTablet };
|
||||
@@ -0,0 +1,69 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
import TabletBox from "../../../components/TabletBox";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Extras from "../../Extras";
|
||||
|
||||
interface Props {
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class ExtrasTablet extends React.PureComponent<Props> {
|
||||
handleManageOpen = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.EXTRA_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isReadonly, theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionTablet>
|
||||
<TabletBox
|
||||
header="Extras"
|
||||
onHeaderClick={this.handleManageOpen}
|
||||
className="ct-extras-tablet"
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
>
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Extras</h2>
|
||||
<Extras />
|
||||
</section>
|
||||
</TabletBox>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
const ExtrasTabletContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <ExtrasTablet {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ExtrasTabletContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import ExtrasTablet from "./ExtrasTablet";
|
||||
|
||||
export default ExtrasTablet;
|
||||
export { ExtrasTablet };
|
||||
@@ -0,0 +1,45 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
import TabletBox from "../../../components/TabletBox";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Features from "../../Features";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class FeaturesTablet extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionTablet className="ct-features-tablet">
|
||||
<TabletBox
|
||||
header="Features & Traits"
|
||||
className="ct-actions-tablet"
|
||||
theme={theme}
|
||||
>
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Features and Traits</h2>
|
||||
<Features />
|
||||
</section>
|
||||
</TabletBox>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(FeaturesTablet);
|
||||
@@ -0,0 +1,4 @@
|
||||
import FeaturesTablet from "./FeaturesTablet";
|
||||
|
||||
export default FeaturesTablet;
|
||||
export { FeaturesTablet };
|
||||
@@ -0,0 +1,248 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
AbilitySummary,
|
||||
CampaignSummary,
|
||||
ManageIcon,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityManager,
|
||||
rulesEngineSelectors,
|
||||
Skill,
|
||||
SkillUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useAbilities } from "~/hooks/useAbilities";
|
||||
|
||||
import { PaneComponentEnum } from "../../../../../../subApps/sheet/components/Sidebar/types";
|
||||
import {
|
||||
appEnvSelectors,
|
||||
characterRollContextSelectors,
|
||||
} from "../../../../Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../../Shared/utils";
|
||||
import ProficiencyGroupsBox from "../../../components/ProficiencyGroupsBox";
|
||||
import SavingThrowsBox from "../../../components/SavingThrowsBox";
|
||||
import SensesBox from "../../../components/SensesBox";
|
||||
import SkillsBox from "../../../components/SkillsBox";
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
|
||||
export default function MainTablet() {
|
||||
const abilities = useAbilities();
|
||||
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
const preferences = useSelector(rulesEngineSelectors.getCharacterPreferences);
|
||||
const savingThrowDiceAdjustments = useSelector(
|
||||
rulesEngineSelectors.getSavingThrowDiceAdjustments
|
||||
);
|
||||
const situationalBonusSavingThrowsLookup = useSelector(
|
||||
rulesEngineSelectors.getSituationalBonusSavingThrowsLookup
|
||||
);
|
||||
const deathSaveInfo = useSelector(rulesEngineSelectors.getDeathSaveInfo);
|
||||
const passivePerception = useSelector(
|
||||
rulesEngineSelectors.getPassivePerception
|
||||
);
|
||||
const passiveInvestigation = useSelector(
|
||||
rulesEngineSelectors.getPassiveInvestigation
|
||||
);
|
||||
const passiveInsight = useSelector(rulesEngineSelectors.getPassiveInsight);
|
||||
const senses = useSelector(rulesEngineSelectors.getSenseInfo);
|
||||
const campaign = useSelector(rulesEngineSelectors.getCampaign);
|
||||
const skills = useSelector(rulesEngineSelectors.getSkills);
|
||||
const valueLookup = useSelector(rulesEngineSelectors.getCharacterValueLookup);
|
||||
const customSkills = useSelector(rulesEngineSelectors.getCustomSkills);
|
||||
const proficiencyGroups = useSelector(
|
||||
rulesEngineSelectors.getProficiencyGroups
|
||||
);
|
||||
const dimensions = useSelector(appEnvSelectors.getDimensions);
|
||||
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
const diceEnabled = useSelector(appEnvSelectors.getDiceEnabled);
|
||||
const ruleData = useSelector(rulesEngineSelectors.getRuleData);
|
||||
const characterRollContext = useSelector(
|
||||
characterRollContextSelectors.getCharacterRollContext
|
||||
);
|
||||
const gameLog = useSelector(appEnvSelectors.getGameLog);
|
||||
const handleSensesManageShow = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.SENSE_MANAGE);
|
||||
};
|
||||
|
||||
const handleSavingThrowsClick = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.SAVING_THROWS);
|
||||
};
|
||||
|
||||
const handleAbilitySummaryClick = (ability: AbilityManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ABILITY,
|
||||
PaneIdentifierUtils.generateAbility(ability)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAbilityClick = (ability: AbilityManager): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.ABILITY_SAVING_THROW,
|
||||
PaneIdentifierUtils.generateAbilitySavingThrows(ability.getId())
|
||||
);
|
||||
};
|
||||
|
||||
const handleProficienciesManageShow = (): void => {
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.PROFICIENCIES);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCampaignShow = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
|
||||
};
|
||||
|
||||
const handleCustomSkillClick = (skill: Skill): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.CUSTOM_SKILL,
|
||||
PaneIdentifierUtils.generateCustomSkill(SkillUtils.getId(skill))
|
||||
);
|
||||
};
|
||||
|
||||
const handleSkillsShow = (): void => {
|
||||
paneHistoryStart(PaneComponentEnum.SKILLS);
|
||||
};
|
||||
|
||||
const handleSkillClick = (skill: Skill): void => {
|
||||
paneHistoryStart(
|
||||
PaneComponentEnum.SKILL,
|
||||
PaneIdentifierUtils.generateSkill(SkillUtils.getId(skill))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SubsectionTablet name="Main">
|
||||
<div className="ct-main-tablet">
|
||||
{campaign && (
|
||||
<CampaignSummary
|
||||
campaign={campaign}
|
||||
onCampaignShow={handleCampaignShow}
|
||||
className={
|
||||
theme.isDarkMode
|
||||
? "ct-main-tablet__campaign-button--dark-mode"
|
||||
: ""
|
||||
}
|
||||
theme={theme}
|
||||
gameLog={gameLog}
|
||||
/>
|
||||
)}
|
||||
<section className="ct-main-tablet__abilities">
|
||||
<h2 style={visuallyHidden}>Abilities</h2>
|
||||
{abilities.map((ability) => (
|
||||
<div className="ct-main-tablet__ability" key={ability.getStatKey()}>
|
||||
<AbilitySummary
|
||||
theme={theme}
|
||||
ability={ability}
|
||||
preferences={preferences}
|
||||
onClick={handleAbilitySummaryClick}
|
||||
diceEnabled={diceEnabled}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<div className="ct-main-tablet__large-boxes">
|
||||
<div className="ct-main-tablet__large-boxes-bucket">
|
||||
<div className="ct-main-tablet__large-box">
|
||||
<SavingThrowsBox
|
||||
abilities={abilities}
|
||||
ruleData={ruleData}
|
||||
savingThrowDiceAdjustments={savingThrowDiceAdjustments}
|
||||
situationalBonusSavingThrowsLookup={
|
||||
situationalBonusSavingThrowsLookup
|
||||
}
|
||||
deathSaveInfo={deathSaveInfo}
|
||||
onAbilityClick={handleAbilityClick}
|
||||
onClick={handleSavingThrowsClick}
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
diceEnabled={diceEnabled}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
<div className="ct-main-tablet__large-box-footer">
|
||||
<ManageIcon
|
||||
onClick={handleSavingThrowsClick}
|
||||
enableTooltip={false}
|
||||
theme={theme}
|
||||
>
|
||||
Saving Throws
|
||||
</ManageIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-main-tablet__large-box">
|
||||
<SensesBox
|
||||
senses={senses}
|
||||
passiveInsight={Number(passiveInsight)}
|
||||
passiveInvestigation={Number(passiveInvestigation)}
|
||||
passivePerception={Number(passivePerception)}
|
||||
onClick={handleSensesManageShow}
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
/>
|
||||
<div className="ct-main-tablet__large-box-footer">
|
||||
<ManageIcon
|
||||
onClick={handleSensesManageShow}
|
||||
enableTooltip={false}
|
||||
theme={theme}
|
||||
>
|
||||
Senses
|
||||
</ManageIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-main-tablet__large-box">
|
||||
<ProficiencyGroupsBox
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
proficiencyGroups={proficiencyGroups}
|
||||
onClick={handleProficienciesManageShow}
|
||||
/>
|
||||
<div className="ct-main-tablet__large-box-footer">
|
||||
<ManageIcon
|
||||
onClick={handleProficienciesManageShow}
|
||||
enableTooltip={false}
|
||||
showIcon={!isReadonly}
|
||||
theme={theme}
|
||||
>
|
||||
Proficiencies & Training
|
||||
</ManageIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ct-main-tablet__large-boxes-bucket">
|
||||
<div className="ct-main-tablet__large-box">
|
||||
<SkillsBox
|
||||
skills={skills}
|
||||
customSkills={customSkills}
|
||||
valueLookup={valueLookup}
|
||||
onCustomSkillClick={handleCustomSkillClick}
|
||||
onSkillClick={handleSkillClick}
|
||||
onEmptyClick={handleSkillsShow}
|
||||
dimensions={dimensions}
|
||||
theme={theme}
|
||||
diceEnabled={diceEnabled}
|
||||
ruleData={ruleData}
|
||||
rollContext={characterRollContext}
|
||||
/>
|
||||
<div className="ct-main-tablet__large-box-footer">
|
||||
<ManageIcon
|
||||
onClick={handleSkillsShow}
|
||||
enableTooltip={false}
|
||||
theme={theme}
|
||||
>
|
||||
Skills
|
||||
</ManageIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import MainTablet from "./MainTablet";
|
||||
|
||||
export default MainTablet;
|
||||
export { MainTablet };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
import TabletBox from "../../../components/TabletBox";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Notes from "../../Notes";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class NotesTablet extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionTablet>
|
||||
<TabletBox header="Notes" className="ct-notes-tablet" theme={theme}>
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Notes</h2>
|
||||
<Notes />
|
||||
</section>
|
||||
</TabletBox>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NotesTablet);
|
||||
@@ -0,0 +1,4 @@
|
||||
import NotesTablet from "./NotesTablet";
|
||||
|
||||
export default NotesTablet;
|
||||
export { NotesTablet };
|
||||
@@ -0,0 +1,69 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
rulesEngineSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
|
||||
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import { appEnvSelectors } from "../../../../Shared/selectors";
|
||||
import SubsectionTablet from "../../../components/SubsectionTablet";
|
||||
import TabletBox from "../../../components/TabletBox";
|
||||
import { SheetAppState } from "../../../typings";
|
||||
import Spells from "../../Spells";
|
||||
|
||||
interface Props {
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
||||
}
|
||||
class SpellsTablet extends React.PureComponent<Props> {
|
||||
handleManageSpellsOpen = (): void => {
|
||||
const { paneHistoryStart, isReadonly } = this.props;
|
||||
|
||||
if (!isReadonly) {
|
||||
paneHistoryStart(PaneComponentEnum.SPELL_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isReadonly, theme } = this.props;
|
||||
|
||||
return (
|
||||
<SubsectionTablet>
|
||||
<TabletBox
|
||||
header="Spells"
|
||||
onHeaderClick={this.handleManageSpellsOpen}
|
||||
className="ct-spells-tablet"
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
>
|
||||
<section>
|
||||
<h2 style={visuallyHidden}>Spells</h2>
|
||||
<Spells />
|
||||
</section>
|
||||
</TabletBox>
|
||||
</SubsectionTablet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SheetAppState) {
|
||||
return {
|
||||
isReadonly: appEnvSelectors.getIsReadonly(state),
|
||||
theme: rulesEngineSelectors.getCharacterTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
const SpellsTabletContainer = (props) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
return <SpellsTablet {...props} paneHistoryStart={paneHistoryStart} />;
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SpellsTabletContainer);
|
||||
@@ -0,0 +1,4 @@
|
||||
import SpellsTablet from "./SpellsTablet";
|
||||
|
||||
export default SpellsTablet;
|
||||
export { SpellsTablet };
|
||||
Reference in New Issue
Block a user