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,142 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import {
|
||||
AbilityManager,
|
||||
CharacterPreferences,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { DiceTools, IRollContext, RollType } from "@dndbeyond/dice";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
|
||||
import BoxBackground from "../BoxBackground";
|
||||
import { DigitalDiceWrapper } from "../Dice";
|
||||
import AbilityScoreBoxSvg from "../Svg/boxes/AbilityScoreBoxSvg";
|
||||
import { isNotNullOrUndefined } from "../utils/TypeScriptUtils";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
ability: AbilityManager | { name: string; score: number; modifier: number };
|
||||
preferences: CharacterPreferences;
|
||||
theme: CharacterTheme;
|
||||
onClick?: (ability: AbilityManager) => void;
|
||||
diceEnabled?: boolean;
|
||||
rollContext: IRollContext;
|
||||
}
|
||||
export default function AbilitySummary({
|
||||
className = "",
|
||||
ability,
|
||||
preferences,
|
||||
theme,
|
||||
onClick,
|
||||
diceEnabled = false,
|
||||
rollContext,
|
||||
}: Props) {
|
||||
const isManager = "getStatKey" in ability;
|
||||
|
||||
const handleClick = (evt: React.MouseEvent): void => {
|
||||
if (onClick && isManager) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onClick(ability);
|
||||
}
|
||||
};
|
||||
|
||||
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
|
||||
useContext(GameLogContext);
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-ability-summary"];
|
||||
|
||||
let primaryNode: React.ReactNode;
|
||||
let secondaryNode: React.ReactNode;
|
||||
if (!ability) return null;
|
||||
const abilityStatKey = isManager ? ability.getStatKey() : 0;
|
||||
const abilityLabel = isManager ? ability.getLabel() : ability.name;
|
||||
const abilityName = isManager ? ability.getName() : ability.name;
|
||||
const abilityModifier = isManager ? ability.getModifier() : ability.modifier;
|
||||
const abilityTotalScore = isManager ? ability.getTotalScore() : ability.score;
|
||||
|
||||
const diceWrapperProps = {
|
||||
rollType: RollType.Check,
|
||||
rollAction: abilityName,
|
||||
diceEnabled: diceEnabled,
|
||||
diceNotation: DiceTools.CustomD20(abilityModifier ?? 0),
|
||||
themeColor: theme.themeColor,
|
||||
rollContext: rollContext,
|
||||
rollTargetOptions: !!messageTargetOptions?.entities
|
||||
? Object.values(messageTargetOptions.entities).filter(
|
||||
isNotNullOrUndefined
|
||||
)
|
||||
: undefined,
|
||||
rollTargetDefault :defaultMessageTargetOption,
|
||||
userId: Number(userId),
|
||||
};
|
||||
|
||||
if (
|
||||
preferences.abilityScoreDisplayType ===
|
||||
Constants.PreferenceAbilityScoreDisplayTypeEnum.SCORES_TOP
|
||||
) {
|
||||
primaryNode = (
|
||||
<DigitalDiceWrapper
|
||||
{...diceWrapperProps}
|
||||
>
|
||||
{abilityTotalScore}
|
||||
</DigitalDiceWrapper>
|
||||
);
|
||||
|
||||
if (abilityModifier === null) {
|
||||
secondaryNode = "--";
|
||||
} else {
|
||||
secondaryNode = <NumberDisplay type="signed" number={abilityModifier} />;
|
||||
}
|
||||
} else {
|
||||
if (abilityModifier === null) {
|
||||
primaryNode = "--";
|
||||
} else {
|
||||
primaryNode = (
|
||||
<DigitalDiceWrapper
|
||||
{...diceWrapperProps}
|
||||
>
|
||||
<NumberDisplay
|
||||
number={abilityModifier}
|
||||
type="signed"
|
||||
size="large"
|
||||
/>
|
||||
</DigitalDiceWrapper>
|
||||
);
|
||||
}
|
||||
secondaryNode = abilityTotalScore;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")} onClick={handleClick}>
|
||||
<BoxBackground StyleComponent={AbilityScoreBoxSvg} theme={theme} />
|
||||
<div className="ddbc-ability-summary__heading">
|
||||
<span
|
||||
className={`ddbc-ability-summary__label ${
|
||||
theme.isDarkMode ? "ddbc-ability-summary__label--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{abilityLabel}
|
||||
</span>
|
||||
<span className="ddbc-ability-summary__abbr">{abilityStatKey}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`ddbc-ability-summary__primary ${
|
||||
theme.isDarkMode ? "ddbc-ability-summary__primary--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{primaryNode}
|
||||
</div>
|
||||
<div
|
||||
className={`ddbc-ability-summary__secondary ${
|
||||
theme.isDarkMode ? "ddbc-ability-summary__secondary--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{secondaryNode}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import AbilitySummary from "./AbilitySummary";
|
||||
|
||||
export default AbilitySummary;
|
||||
export { AbilitySummary };
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
Action,
|
||||
ActionUtils,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
action: Action;
|
||||
onClick?: (action: Action) => void;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
export default class ActionName extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, action } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
onClick(action);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { action, className, theme } = this.props;
|
||||
|
||||
let isCustomized = ActionUtils.isCustomized(action);
|
||||
let classNames: Array<string> = [className, "ddbc-action-name"];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")} onClick={this.handleClick}>
|
||||
{ActionUtils.getName(action)}
|
||||
{isCustomized && (
|
||||
<Tooltip
|
||||
title="Action is Customized"
|
||||
className="ddbc-action-name__customized"
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
*
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ActionName from "./ActionName";
|
||||
|
||||
export default ActionName;
|
||||
export { ActionName };
|
||||
@@ -0,0 +1,65 @@
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import * as React from "react";
|
||||
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import BoxBackground from "../BoxBackground";
|
||||
import ArmorClassBoxSvg from "../Svg/boxes/ArmorClassBoxSvg";
|
||||
|
||||
interface Props {
|
||||
armorClass: number;
|
||||
theme: CharacterTheme;
|
||||
onClick?: () => void;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export default class ArmorClassBox extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { armorClass, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-armor-class-box"];
|
||||
|
||||
return (
|
||||
<section className={classNames.join(" ")} onClick={this.handleClick}>
|
||||
<BoxBackground StyleComponent={ArmorClassBoxSvg} theme={theme} />
|
||||
<h2 style={visuallyHidden}>Armor Class</h2>
|
||||
<div
|
||||
className={`ddbc-armor-class-box__label ${
|
||||
theme.isDarkMode ? "ddbc-armor-class-box__label--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
Armor
|
||||
</div>
|
||||
<div
|
||||
className={`ddbc-armor-class-box__value ${
|
||||
theme.isDarkMode ? "ddbc-armor-class-box__value--dark-mode" : ""
|
||||
}`}
|
||||
data-testid="armor-class-value"
|
||||
>
|
||||
{armorClass}
|
||||
</div>
|
||||
<div
|
||||
className={`ddbc-armor-class-box__label ${
|
||||
theme.isDarkMode ? "ddbc-armor-class-box__label--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
Class
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
AbilityLookup,
|
||||
Action,
|
||||
Attack,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
DataOriginRefData,
|
||||
Item,
|
||||
RuleData,
|
||||
Spell,
|
||||
SpellCasterInfo,
|
||||
WeaponSpellDamageGroup,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { IRollContext } from "@dndbeyond/dice";
|
||||
import CombatActionAttack from "../CombatAttack/CombatActionAttack";
|
||||
import CombatItemAttack from "../CombatAttack/CombatItemAttack";
|
||||
import CombatSpellAttack from "../CombatAttack/CombatSpellAttack";
|
||||
import { useMemo } from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface AttackTableProps extends React.HTMLAttributes<HTMLElement> {
|
||||
theme: CharacterTheme;
|
||||
showNotes?: boolean;
|
||||
diceEnabled?: boolean;
|
||||
attacks: Array<Attack>;
|
||||
weaponSpellDamageGroups: Array<WeaponSpellDamageGroup>;
|
||||
abilityLookup: AbilityLookup;
|
||||
ruleData: RuleData;
|
||||
spellCasterInfo: SpellCasterInfo;
|
||||
onAttackClick?: (attack: Attack) => void;
|
||||
rollContext: IRollContext;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
proficiencyBonus: number;
|
||||
};
|
||||
|
||||
export const AttackTable = ({
|
||||
theme,
|
||||
attacks,
|
||||
weaponSpellDamageGroups,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
spellCasterInfo,
|
||||
onAttackClick,
|
||||
dataOriginRefData,
|
||||
proficiencyBonus,
|
||||
rollContext,
|
||||
|
||||
showNotes = true,
|
||||
diceEnabled = false,
|
||||
|
||||
...props
|
||||
}: AttackTableProps) => {
|
||||
const columnClassNameBase = useMemo(() => [
|
||||
styles.col,
|
||||
theme?.isDarkMode && styles.darkMode,
|
||||
], [theme]);
|
||||
|
||||
const getColumnClass = (addtionalClass: string) => clsx([
|
||||
...columnClassNameBase,
|
||||
addtionalClass,
|
||||
]);
|
||||
|
||||
const renderedAttacks = useMemo(
|
||||
(): React.ReactNode => !!attacks.length
|
||||
? attacks.map((attack): React.ReactNode => {
|
||||
const commonCombatAttackProps = {
|
||||
key: attack.key,
|
||||
attack,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
onClick: onAttackClick,
|
||||
showNotes,
|
||||
diceEnabled,
|
||||
theme,
|
||||
rollContext,
|
||||
proficiencyBonus,
|
||||
className: styles.attack,
|
||||
};
|
||||
|
||||
switch (attack.type) {
|
||||
case Constants.AttackSourceTypeEnum.ACTION:
|
||||
case Constants.AttackSourceTypeEnum.CUSTOM:
|
||||
return (
|
||||
<CombatActionAttack
|
||||
action={attack.data as Action}
|
||||
{...commonCombatAttackProps}
|
||||
/>
|
||||
);
|
||||
|
||||
case Constants.AttackSourceTypeEnum.ITEM:
|
||||
return (
|
||||
<CombatItemAttack
|
||||
item={attack.data as Item}
|
||||
weaponSpellDamageGroups={weaponSpellDamageGroups}
|
||||
{...commonCombatAttackProps}
|
||||
/>
|
||||
);
|
||||
|
||||
case Constants.AttackSourceTypeEnum.SPELL:
|
||||
return (
|
||||
<CombatSpellAttack
|
||||
spell={attack.data as Spell}
|
||||
spellCasterInfo={spellCasterInfo}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
{...commonCombatAttackProps}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
: (
|
||||
<div className={clsx([
|
||||
styles.default,
|
||||
theme?.isDarkMode && styles.darkMode,
|
||||
])}>
|
||||
Equip weapons or add spells to see your attacks here.
|
||||
</div>
|
||||
),
|
||||
[
|
||||
attacks,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
onAttackClick,
|
||||
showNotes,
|
||||
diceEnabled,
|
||||
theme,
|
||||
rollContext,
|
||||
proficiencyBonus,
|
||||
weaponSpellDamageGroups,
|
||||
spellCasterInfo,
|
||||
dataOriginRefData
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<div className={styles.tableHeader}>
|
||||
<div className={getColumnClass(styles.icon)} />
|
||||
<div className={getColumnClass(styles.name)}>
|
||||
Attack
|
||||
</div>
|
||||
<div className={getColumnClass(styles.range)}>
|
||||
Range
|
||||
</div>
|
||||
<div className={getColumnClass(styles.tohit)}>
|
||||
Hit / DC
|
||||
</div>
|
||||
<div className={getColumnClass(styles.damage)}>
|
||||
Damage
|
||||
</div>
|
||||
{showNotes && (
|
||||
<div className={getColumnClass(styles.notes)}>
|
||||
Notes
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div data-testid="attack-table-content">
|
||||
{renderedAttacks}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Item,
|
||||
ItemUtils,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
|
||||
import BoxBackground from "../BoxBackground";
|
||||
import {
|
||||
EmptyAttunementSlotBoxSvg,
|
||||
ThemedAttunementSlotBoxSvg,
|
||||
ThemedWithOpacityAttunementSlotBoxSvg,
|
||||
} from "../Svg/boxes/AttunementSlotBoxSvg";
|
||||
|
||||
interface Props {
|
||||
slot: Item | null;
|
||||
onClick?: (slot: Item) => void;
|
||||
isMobile: boolean;
|
||||
className: string;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class AttunementSlot extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, slot } = this.props;
|
||||
|
||||
if (onClick && slot !== null) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
onClick(slot);
|
||||
}
|
||||
};
|
||||
|
||||
renderMetaItems = (): React.ReactNode => {
|
||||
const { slot } = this.props;
|
||||
|
||||
if (slot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let metaItems: Array<string> = [];
|
||||
|
||||
if (ItemUtils.isLegacy(slot)) {
|
||||
metaItems.push("Legacy");
|
||||
}
|
||||
|
||||
let type = ItemUtils.getType(slot);
|
||||
if (type !== null) {
|
||||
metaItems.push(type);
|
||||
}
|
||||
|
||||
if (ItemUtils.isWeaponContract(slot)) {
|
||||
if (ItemUtils.isOffhand(slot)) {
|
||||
metaItems.push("Dual Wield");
|
||||
}
|
||||
} else if (ItemUtils.isArmorContract(slot)) {
|
||||
let baseArmorName = ItemUtils.getBaseArmorName(slot);
|
||||
if (baseArmorName) {
|
||||
metaItems.push(baseArmorName);
|
||||
}
|
||||
} else if (ItemUtils.isGearContract(slot)) {
|
||||
let subType = ItemUtils.getSubType(slot);
|
||||
if (subType) {
|
||||
metaItems.push(subType);
|
||||
}
|
||||
if (ItemUtils.isOffhand(slot)) {
|
||||
metaItems.push("Dual Wield");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-attunement-slot__meta">
|
||||
{metaItems.map(
|
||||
(metaItem, idx): React.ReactNode => (
|
||||
<span className="ddbc-attunement-slot__meta-item" key={idx}>
|
||||
{metaItem}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = (): React.ReactNode => {
|
||||
const { slot } = this.props;
|
||||
|
||||
if (slot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let itemImage = ItemUtils.getAvatarUrl(slot);
|
||||
let previewStyles: React.CSSProperties = {
|
||||
backgroundImage: `url(${itemImage})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="ddbc-attunement-slot__preview" style={previewStyles} />
|
||||
<div className="ddbc-attunement-slot__content">
|
||||
<div className="ddbc-attunement-slot__name">
|
||||
<ItemName item={slot} showAttunement={false} />
|
||||
</div>
|
||||
{this.renderMetaItems()}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
renderEmpty = (): React.ReactNode => {
|
||||
const { isMobile } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="ddbc-attunement-slot__preview" />
|
||||
<div className="ddbc-attunement-slot__content">
|
||||
Choose an Item from {isMobile ? "Below" : " the Right"}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { slot, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-attunement-slot"];
|
||||
let StyleComponent: React.ComponentType<any> = theme?.isDarkMode
|
||||
? ThemedWithOpacityAttunementSlotBoxSvg
|
||||
: EmptyAttunementSlotBoxSvg;
|
||||
if (slot) {
|
||||
classNames.push("ddbc-attunement-slot--filled");
|
||||
StyleComponent = ThemedAttunementSlotBoxSvg;
|
||||
} else {
|
||||
classNames.push("ddbc-attunement-slot--empty");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")} onClick={this.handleClick}>
|
||||
<BoxBackground StyleComponent={StyleComponent} theme={theme} />
|
||||
{slot ? this.renderItem() : this.renderEmpty()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
StyleComponent: React.ComponentType<any>;
|
||||
theme: CharacterTheme;
|
||||
className: string;
|
||||
}
|
||||
export default class BoxBackground extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { StyleComponent, theme, className } = this.props;
|
||||
|
||||
let classNames: Array<string> = ["ddbc-box-background", className];
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<StyleComponent theme={theme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import BoxBackground from "./BoxBackground";
|
||||
|
||||
export default BoxBackground;
|
||||
export { BoxBackground };
|
||||
@@ -0,0 +1,110 @@
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { ThemedPlayButtonSvg } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
CampaignDataContract,
|
||||
CampaignUtils,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import D6 from "@dndbeyond/fontawesome-cache/svgs/regular/dice-d6.svg";
|
||||
import { GameLogNotificationWrapper } from "@dndbeyond/game-log-components";
|
||||
|
||||
import { Link } from "~/components/Link";
|
||||
|
||||
import { useSidebar } from "../../../../contexts/Sidebar";
|
||||
import { PaneComponentEnum } from "../../../../subApps/sheet/components/Sidebar/types";
|
||||
import { GameLogState } from "../../Shared/stores/typings";
|
||||
import { NavigationUtils } from "../../Shared/utils";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props {
|
||||
campaign: CampaignDataContract;
|
||||
onCampaignShow?: () => void;
|
||||
className?: string;
|
||||
gameLog?: GameLogState;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
|
||||
const CampaignSummary: React.FC<Props> = ({
|
||||
campaign,
|
||||
onCampaignShow,
|
||||
className = "",
|
||||
gameLog,
|
||||
theme,
|
||||
}) => {
|
||||
const {
|
||||
pane: { paneHistoryStart },
|
||||
} = useSidebar();
|
||||
|
||||
const handleClick = (evt: React.MouseEvent): void => {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (onCampaignShow) {
|
||||
onCampaignShow();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGameLogClick = (evt: React.MouseEvent): void => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
// Assuming dispatch is available in props or context
|
||||
paneHistoryStart(PaneComponentEnum.GAME_LOG);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.container)}>
|
||||
<div
|
||||
className={clsx(styles.campaignSummary)}
|
||||
onClick={handleClick}
|
||||
data-testid="campaign-summary"
|
||||
>
|
||||
<span className={clsx(styles.summaryLabel)}>Campaign:</span>
|
||||
<span className={clsx(styles.summaryName)}>
|
||||
{CampaignUtils.getName(campaign)}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip title="Launch Game" isDarkMode={theme.isDarkMode}>
|
||||
<Link
|
||||
href={NavigationUtils.getLaunchGameUrl(campaign)}
|
||||
target="maps"
|
||||
aria-label="Launch Game"
|
||||
>
|
||||
<div className={clsx(styles.campaignButtonGroup)}>
|
||||
<div className={clsx(styles.campaignButton)}>
|
||||
<ThemedPlayButtonSvg
|
||||
className={clsx([
|
||||
styles.campaignButtonIcon,
|
||||
styles.campaignPlayButtonIcon,
|
||||
])}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<GameLogNotificationWrapper
|
||||
themeColor={theme.themeColor}
|
||||
gameLogIsOpen={gameLog?.isOpen ?? false}
|
||||
notificationOnClick={handleGameLogClick}
|
||||
>
|
||||
<Tooltip title="Game Log" isDarkMode={theme.isDarkMode}>
|
||||
<div
|
||||
role="button"
|
||||
aria-roledescription="Game Log"
|
||||
className={clsx(styles.campaignButtonGroup)}
|
||||
onClick={handleGameLogClick}
|
||||
>
|
||||
<div className={clsx(styles.campaignButton)}>
|
||||
<D6 className={clsx(styles.campaignButtonIcon)} />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</GameLogNotificationWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CampaignSummary;
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { AvatarInfo } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import CharacterAvatarPortrait from "./CharacterAvatarPortrait";
|
||||
|
||||
interface Props {
|
||||
avatarInfo: AvatarInfo;
|
||||
className: string;
|
||||
}
|
||||
export default class CharacterAvatar extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { avatarInfo, className } = this.props;
|
||||
|
||||
const classNames: Array<string> = ["ddbc-character-avatar", className];
|
||||
if (avatarInfo.frameId) {
|
||||
classNames.push(`ddbc-character-avatar--${avatarInfo.frameId}`);
|
||||
}
|
||||
let animatedStyles: React.CSSProperties = {};
|
||||
if (avatarInfo.frameAvatarDecorationKey) {
|
||||
const [name, style, animationUrl] =
|
||||
avatarInfo.frameAvatarDecorationKey.split("|");
|
||||
name && classNames.push(`ddbc-character-avatar--${name}`);
|
||||
style && classNames.push(`ddbc-character-avatar--${style}`);
|
||||
if (animationUrl) {
|
||||
classNames.push(`ddbc-character-avatar--animated`);
|
||||
// NOTE: using the backround key for all of these did not work well so they are separated
|
||||
animatedStyles = {
|
||||
backgroundImage: `url(https://www.dndbeyond.com/attachments/${animationUrl}.png)`,
|
||||
backgroundPositionX: "0px",
|
||||
backgroundPositionY: "0px",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: `1240px 1240px`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let frameStyles: React.CSSProperties = {};
|
||||
if (avatarInfo.frameUrl) {
|
||||
frameStyles = {
|
||||
backgroundImage: `url(${avatarInfo.frameUrl})`,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<CharacterAvatarPortrait avatarUrl={avatarInfo.avatarUrl} />
|
||||
{avatarInfo.frameId && (
|
||||
<div className={"ddbc-character-avatar__frame"} style={frameStyles} />
|
||||
)}
|
||||
{avatarInfo.frameId && (
|
||||
<div className={"ddbc-character-avatar__frame-extra1"}>
|
||||
<div
|
||||
className="ddbc-character-avatar__frame-extra-content ddbc-character-avatar__frame-extra1-content"
|
||||
style={animatedStyles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{avatarInfo.frameId && (
|
||||
<div className={"ddbc-character-avatar__frame-extra2"}>
|
||||
<div className="ddbc-character-avatar__frame-extra-content ddbc-character-avatar__frame-extra2-content" />
|
||||
</div>
|
||||
)}
|
||||
{avatarInfo.frameId && (
|
||||
<div className={"ddbc-character-avatar__frame-extra3"}>
|
||||
<div className="ddbc-character-avatar__frame-extra-content ddbc-character-avatar__frame-extra3-content" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import React, { HTMLAttributes } from "react";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement | HTMLImageElement> {
|
||||
className?: string;
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
|
||||
const CharacterAvatarPortrait: React.FC<Props> = ({
|
||||
className = "",
|
||||
avatarUrl,
|
||||
...props
|
||||
}) => {
|
||||
let portraitClasses: Array<string> = [
|
||||
"ddbc-character-avatar__portrait",
|
||||
className,
|
||||
];
|
||||
let portraitStyles: React.CSSProperties = {};
|
||||
if (avatarUrl) {
|
||||
portraitStyles = {
|
||||
backgroundImage: `url(${avatarUrl})`,
|
||||
};
|
||||
} else {
|
||||
portraitClasses.push("ddbc-character-avatar__portrait--none");
|
||||
}
|
||||
|
||||
return avatarUrl ? (
|
||||
<img
|
||||
className={portraitClasses.join(" ")}
|
||||
src={avatarUrl}
|
||||
alt="Character portrait"
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={portraitClasses.join(" ")}
|
||||
style={portraitStyles}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterAvatarPortrait;
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterAvatarPortrait from "./CharacterAvatarPortrait";
|
||||
|
||||
export default CharacterAvatarPortrait;
|
||||
export { CharacterAvatarPortrait };
|
||||
@@ -0,0 +1,5 @@
|
||||
import CharacterAvatar from "./CharacterAvatar";
|
||||
import CharacterAvatarPortrait from "./CharacterAvatarPortrait";
|
||||
|
||||
export default CharacterAvatar;
|
||||
export { CharacterAvatar, CharacterAvatarPortrait };
|
||||
@@ -0,0 +1,40 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { DefaultCharacterName as DefaultName } from "~/constants";
|
||||
|
||||
interface Props {
|
||||
defaultCharacterName?: string;
|
||||
isDead?: boolean;
|
||||
isFaceMenu?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function CharacterName({
|
||||
defaultCharacterName = DefaultName,
|
||||
isDead = false,
|
||||
isFaceMenu = false,
|
||||
name,
|
||||
}: Props) {
|
||||
let displayName: string = name ? name : defaultCharacterName;
|
||||
if (isDead) {
|
||||
displayName = `(Dead) ${displayName}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
style={{ fontFamily: "Roboto Condensed" }}
|
||||
sx={{
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
fontSize: isFaceMenu ? "24px" : { xs: "18px", sm: "24px" },
|
||||
lineHeight: 1.4,
|
||||
letterSpacing: "normal",
|
||||
}}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterName from "./CharacterName";
|
||||
|
||||
export default CharacterName;
|
||||
export { CharacterName };
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Constants,
|
||||
ExperienceInfo,
|
||||
FormatUtils,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import XpBar from "../XpBar";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
ruleData: RuleData;
|
||||
xpInfo: ExperienceInfo;
|
||||
progressionType: Constants.PreferenceProgressionTypeEnum;
|
||||
}
|
||||
export default class CharacterProgressionSummary extends React.PureComponent<
|
||||
Props,
|
||||
{}
|
||||
> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, ruleData, xpInfo, progressionType } = this.props;
|
||||
|
||||
const classNames: Array<string> = [
|
||||
"ddbc-character-progression-summary",
|
||||
className,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
{progressionType ===
|
||||
Constants.PreferenceProgressionTypeEnum.MILESTONE && (
|
||||
<div className="ddbc-character-progression-summary__level">
|
||||
Level {xpInfo.currentLevel}
|
||||
</div>
|
||||
)}
|
||||
{progressionType === Constants.PreferenceProgressionTypeEnum.XP && (
|
||||
<div className="ddbc-character-progression-summary__xp-bar">
|
||||
<XpBar xp={xpInfo.currentLevelXp} ruleData={ruleData} />
|
||||
<div className="ddbc-character-progression-summary__xp-data">
|
||||
{FormatUtils.renderLocaleNumber(xpInfo.currentLevelXp)} /{" "}
|
||||
{FormatUtils.renderLocaleNumber(xpInfo.nextLevelXp)} XP
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterProgressionSummary from "./CharacterProgressionSummary";
|
||||
|
||||
export default CharacterProgressionSummary;
|
||||
export { CharacterProgressionSummary };
|
||||
@@ -0,0 +1,356 @@
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
Grid,
|
||||
IconButton,
|
||||
Modal,
|
||||
Paper,
|
||||
Stack,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import clsx from "clsx";
|
||||
import { useContext } from "react";
|
||||
|
||||
import { RuleDataUtils } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { SourceCategoryDescription } from "~/constants";
|
||||
import { useFeatureFlags } from "~/contexts/FeatureFlag";
|
||||
import { generateCharacterPreferences } from "~/helpers/generateCharacterPreferences";
|
||||
import PreferenceUpdateLocation from "~/tools/js/Shared/constants/PreferenceUpdateLocation";
|
||||
|
||||
import {
|
||||
CheckboxInfo,
|
||||
FormCheckBoxesField,
|
||||
} from "../../Shared/components/common/FormCheckBoxesField";
|
||||
import AbilitySummary from "../AbilitySummary";
|
||||
import PrivacyTypeRadio from "../PrivacyTypeRadio";
|
||||
import { UserPreferenceContext } from "../UserPreference";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface CharacterSettingsModalProps {
|
||||
open: boolean;
|
||||
updateLocation: PreferenceUpdateLocation;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
const CharacterSettingsModal = ({
|
||||
open,
|
||||
updateLocation,
|
||||
handleClose,
|
||||
}: CharacterSettingsModalProps) => {
|
||||
const {
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updatePreferences,
|
||||
ruleData,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
} = useContext(UserPreferenceContext);
|
||||
|
||||
const { featureFlags } = useFeatureFlags();
|
||||
|
||||
const preferences = generateCharacterPreferences(featureFlags);
|
||||
|
||||
const getTheme = (darkMode: boolean) => ({
|
||||
name: "DDB Red",
|
||||
backgroundColor: darkMode ? "#10161ADB" : "#FEFEFE",
|
||||
isDefault: true,
|
||||
themeColor: "#C53131",
|
||||
themeColorId: null,
|
||||
isDarkMode: darkMode || false,
|
||||
});
|
||||
|
||||
const rollContext = {
|
||||
entityId: "52962664",
|
||||
entityType: "character",
|
||||
name: "Stor Hornraven",
|
||||
avatarUrl:
|
||||
"https://stg.dndbeyond.com/avatars/18589/680/1581111423-52962664.jpeg?width=150&height=150&fit=crop&quality=95&auto=webp",
|
||||
};
|
||||
|
||||
const handleSourceCategoryChange = (
|
||||
sourceId: number,
|
||||
isActive: boolean
|
||||
): void => {
|
||||
const newSourceCategories = {
|
||||
...defaultEnabledSourceCategories,
|
||||
[sourceId]: isActive,
|
||||
};
|
||||
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
defaultEnabledSourceCategories: newSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
let sourceToggles: Array<CheckboxInfo> = [];
|
||||
let partneredSourceCheckboxes: Array<CheckboxInfo> = [];
|
||||
let allPartneredSources: Array<number> = [];
|
||||
|
||||
if (ruleData) {
|
||||
const activeSourceCategories = Object.keys(defaultEnabledSourceCategories)
|
||||
.filter((key) => defaultEnabledSourceCategories[key])
|
||||
.map(Number);
|
||||
RuleDataUtils.getSourceCategories(ruleData).forEach((sourceCategory) => {
|
||||
if (!sourceCategory.isToggleable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkbox: CheckboxInfo = {
|
||||
label: `${sourceCategory.name}`,
|
||||
initiallyEnabled: activeSourceCategories.includes(sourceCategory.id),
|
||||
onChange: handleSourceCategoryChange.bind(this, sourceCategory.id),
|
||||
sortOrder: sourceCategory.sortOrder,
|
||||
description: sourceCategory.description ?? "",
|
||||
};
|
||||
|
||||
if (sourceCategory.isPartneredContent) {
|
||||
delete checkbox.description;
|
||||
partneredSourceCheckboxes.push(checkbox);
|
||||
allPartneredSources.push(sourceCategory.id);
|
||||
} else {
|
||||
sourceToggles.push(checkbox);
|
||||
}
|
||||
});
|
||||
|
||||
//Add Homebrew to sources for display
|
||||
sourceToggles.push({
|
||||
label: "Homebrew",
|
||||
initiallyEnabled: isHomebrewEnabled,
|
||||
description: SourceCategoryDescription.homebrew,
|
||||
onChange: (isActive: boolean) => {
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled: isActive,
|
||||
});
|
||||
},
|
||||
|
||||
sortOrder: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
className={clsx(styles.modal)}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<Paper className={clsx(styles.content)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
style={{ padding: 16, borderBottom: "1px solid #A2ACB2" }}
|
||||
>
|
||||
<SettingsOutlinedIcon
|
||||
style={{
|
||||
color: "rgba(0,0,0,0.54)",
|
||||
marginRight: 5,
|
||||
height: 22,
|
||||
width: 22,
|
||||
}}
|
||||
/>
|
||||
<label className={clsx(styles.modalTitle)}>
|
||||
Default Character Settings
|
||||
</label>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
color: "rgba(0,0,0,0.54)",
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ width: 20 }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Box style={{ padding: 16 }}>
|
||||
<label className={clsx(styles.modalDescription)}>
|
||||
These settings apply defaults to all new Characters you create. Any
|
||||
settings you apply to a specific Character will override the choices
|
||||
you make here.
|
||||
</label>
|
||||
|
||||
<>
|
||||
<FormCheckBoxesField
|
||||
heading="Sources"
|
||||
description={SourceCategoryDescription.official}
|
||||
checkboxes={sourceToggles}
|
||||
showAccordion={false}
|
||||
/>
|
||||
|
||||
<FormCheckBoxesField
|
||||
heading="Partnered Content"
|
||||
description={SourceCategoryDescription.partnered}
|
||||
checkboxes={partneredSourceCheckboxes}
|
||||
checkUncheckAllEnabled={true}
|
||||
showAccordion={false}
|
||||
accordionHeading="Choose Partners"
|
||||
allText="All"
|
||||
/>
|
||||
</>
|
||||
|
||||
<PrivacyTypeRadio
|
||||
color="info"
|
||||
initialValue={privacyType}
|
||||
handleChange={(e) =>
|
||||
updatePreferences({
|
||||
isDarkModeEnabled,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
privacyType: parseInt(e.target.value),
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
color: "#000",
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
letterSpacing: -0.5,
|
||||
textTransform: "uppercase",
|
||||
margin: "19px 0 0",
|
||||
padding: "10px 0",
|
||||
}}
|
||||
>
|
||||
Display
|
||||
</p>
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={8}>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
color="info"
|
||||
checked={isDarkModeEnabled}
|
||||
onChange={(e) =>
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
abilityScoreDisplayType,
|
||||
updateLocation,
|
||||
isDarkModeEnabled: e.target.checked,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
})
|
||||
}
|
||||
style={{ paddingTop: 0 }}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<p style={{ margin: 0, fontSize: 14 }}>Underdark Mode</p>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "grey.500",
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
Activate dark mode for Character Sheets.
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
sx={{ display: "flex", alignItems: "flex-start" }}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl style={{ marginTop: 24 }}>
|
||||
<FormLabel>
|
||||
<p style={{ margin: 0, fontSize: 14, color: "#000" }}>
|
||||
Ability Scores / Modifiers
|
||||
</p>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "grey.500",
|
||||
margin: "0 0 16px",
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
Reverse the display of Ability Scores and Modifiers.{" "}
|
||||
</Typography>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<ToggleButtonGroup
|
||||
color="info"
|
||||
value={abilityScoreDisplayType}
|
||||
exclusive
|
||||
onChange={(e, value) => {
|
||||
if (value !== null)
|
||||
updatePreferences({
|
||||
privacyType,
|
||||
isDarkModeEnabled,
|
||||
updateLocation,
|
||||
abilityScoreDisplayType: value,
|
||||
defaultEnabledSourceCategories,
|
||||
isHomebrewEnabled,
|
||||
});
|
||||
}}
|
||||
aria-label="Platform"
|
||||
>
|
||||
<ToggleButton value={2}>Modifiers Top</ToggleButton>
|
||||
<ToggleButton value={1}>Scores Top</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</FormControl>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
md={4}
|
||||
display="flex"
|
||||
alignItems="flex-start"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "grey.50",
|
||||
border: "1px solid transparent",
|
||||
borderColor: "grey.200",
|
||||
borderRadius: 1,
|
||||
display: "inline-block",
|
||||
py: 1,
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<p style={{ textAlign: "center", marginBottom: 4 }}>Preview</p>
|
||||
<AbilitySummary
|
||||
ability={{ name: "Strength", score: 14, modifier: 2 }}
|
||||
preferences={{
|
||||
...preferences,
|
||||
abilityScoreDisplayType: abilityScoreDisplayType,
|
||||
}}
|
||||
theme={getTheme(isDarkModeEnabled || false)}
|
||||
onClick={(e) => e.getBaseScore()}
|
||||
rollContext={rollContext}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterSettingsModal;
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSettingsModal from "./CharacterSettingsModal";
|
||||
|
||||
export default CharacterSettingsModal;
|
||||
export { CharacterSettingsModal };
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
CharClass,
|
||||
ClassUtils,
|
||||
Race,
|
||||
RaceUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
classes: Array<CharClass> | null;
|
||||
gender: string | null;
|
||||
species: Race | null;
|
||||
className: string;
|
||||
}
|
||||
export default class CharacterSummary extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
renderClassList(): React.ReactNode {
|
||||
const { classes } = this.props;
|
||||
|
||||
if (classes === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const list: Array<string> = classes.map(
|
||||
(charClass) =>
|
||||
`${ClassUtils.getName(charClass)} ${ClassUtils.getLevel(charClass)}`
|
||||
);
|
||||
return (
|
||||
<span className="ddbc-character-summary__classes">
|
||||
{list.join(" / ")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { species, gender, className } = this.props;
|
||||
|
||||
const classNames: Array<string> = ["ddbc-character-summary", className];
|
||||
|
||||
let speciesNode: React.ReactNode;
|
||||
if (species !== null) {
|
||||
speciesNode = RaceUtils.getFullName(species);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
{gender && (
|
||||
<span className="ddbc-character-summary__gender">{gender}</span>
|
||||
)}
|
||||
{speciesNode && (
|
||||
<span className="ddbc-character-summary__race">{speciesNode}</span>
|
||||
)}
|
||||
{this.renderClassList()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import CharacterSummary from "./CharacterSummary";
|
||||
|
||||
export default CharacterSummary;
|
||||
export { CharacterSummary };
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
CharacterPreferences,
|
||||
CharClass,
|
||||
Constants,
|
||||
DecorationInfo,
|
||||
DecorationUtils,
|
||||
ExperienceInfo,
|
||||
Race,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DefaultCharacterName as DefaultName } from "~/constants";
|
||||
|
||||
import CharacterAvatar from "../CharacterAvatar";
|
||||
import CharacterName from "../CharacterName";
|
||||
import CharacterProgressionSummary from "../CharacterProgressionSummary";
|
||||
import CharacterSummary from "../CharacterSummary";
|
||||
|
||||
interface Props {
|
||||
classes: Array<CharClass>;
|
||||
name: string;
|
||||
gender: string | null;
|
||||
species: Race | null;
|
||||
decorationInfo: DecorationInfo;
|
||||
deathCause: Constants.DeathCauseEnum;
|
||||
preferences: CharacterPreferences;
|
||||
ruleData: RuleData;
|
||||
xpInfo: ExperienceInfo;
|
||||
isInteractive: boolean;
|
||||
defaultCharacterName: string;
|
||||
className: string;
|
||||
calloutNode?: React.ReactNode;
|
||||
}
|
||||
export default class CharacterTidbits extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
deathCause: Constants.DeathCauseEnum.NONE,
|
||||
isInteractive: true,
|
||||
className: "",
|
||||
defaultCharacterName: DefaultName,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
classes,
|
||||
gender,
|
||||
species,
|
||||
decorationInfo,
|
||||
deathCause,
|
||||
preferences,
|
||||
ruleData,
|
||||
xpInfo,
|
||||
defaultCharacterName,
|
||||
isInteractive,
|
||||
className,
|
||||
calloutNode,
|
||||
} = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-character-tidbits"];
|
||||
|
||||
if (isInteractive) {
|
||||
classNames.push("ddbc-character-tidbits--is-interactive");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<div className="ddbc-character-tidbits__avatar">
|
||||
<CharacterAvatar
|
||||
avatarInfo={DecorationUtils.getAvatarInfo(decorationInfo)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ddbc-character-tidbits__body">
|
||||
<div className="ddbc-character-tidbits__heading">
|
||||
<CharacterName
|
||||
name={name}
|
||||
isDead={deathCause !== Constants.DeathCauseEnum.NONE}
|
||||
defaultCharacterName={defaultCharacterName}
|
||||
/>
|
||||
{isInteractive && calloutNode && (
|
||||
<div className="ddbc-character-tidbits__menu-callout">
|
||||
{calloutNode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ddbc-character-tidbits__meta">
|
||||
<CharacterSummary
|
||||
classes={classes}
|
||||
species={species}
|
||||
gender={gender}
|
||||
/>
|
||||
</div>
|
||||
{xpInfo && ruleData && (
|
||||
<div className="ddbc-character-tidbits__progression">
|
||||
<CharacterProgressionSummary
|
||||
ruleData={ruleData}
|
||||
progressionType={preferences.progressionType}
|
||||
xpInfo={xpInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine";
|
||||
|
||||
import { CharacterColorEnum } from "../../../../constants";
|
||||
import { PositiveCheckSvg, ThemedCheckSvg, CheckSvg } from "../Svg";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
initiallyEnabled: boolean;
|
||||
enabled?: boolean;
|
||||
onChange?: (isEnabled: boolean) => void;
|
||||
onChangePromise?: (
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
label?: string;
|
||||
preventDefault: boolean;
|
||||
stopPropagation: boolean;
|
||||
isInteractive?: boolean;
|
||||
theme?: CharacterTheme;
|
||||
variant?: "primary" | "theme";
|
||||
}
|
||||
|
||||
interface State {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export default class Checkbox extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
isInteractive: true,
|
||||
initiallyEnabled: false,
|
||||
preventDefault: false,
|
||||
stopPropagation: false,
|
||||
className: "",
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
enabled:
|
||||
typeof props.enabled !== "undefined"
|
||||
? props.enabled
|
||||
: props.initiallyEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
const { enabled, initiallyEnabled } = this.props;
|
||||
|
||||
if (
|
||||
typeof enabled !== "undefined" ||
|
||||
initiallyEnabled !== prevState.enabled
|
||||
) {
|
||||
this.setState({
|
||||
enabled: typeof enabled === "undefined" ? initiallyEnabled : enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onChangePromise, preventDefault, stopPropagation } = this.props;
|
||||
const { enabled } = this.state;
|
||||
|
||||
const newValue: boolean = !enabled;
|
||||
|
||||
if (stopPropagation) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
if (preventDefault) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
if (onChangePromise) {
|
||||
onChangePromise(
|
||||
newValue,
|
||||
() => {
|
||||
this.setEnabled();
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
} else {
|
||||
this.setEnabled();
|
||||
}
|
||||
};
|
||||
|
||||
setEnabled = (): void => {
|
||||
const { onChange } = this.props;
|
||||
|
||||
this.setState((prevState) => {
|
||||
let newValue = !prevState.enabled;
|
||||
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: newValue,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { enabled } = this.state;
|
||||
const { label, className, isInteractive, theme, variant } = this.props;
|
||||
|
||||
const toggleConClasses: Array<string> = [
|
||||
"ddbc-checkbox",
|
||||
"character-checkbox",
|
||||
className,
|
||||
];
|
||||
if (enabled) {
|
||||
toggleConClasses.push("ddbc-checkbox--is-enabled");
|
||||
toggleConClasses.push("character-checkbox-enabled");
|
||||
} else {
|
||||
toggleConClasses.push("ddbc-checkbox--is-disabled");
|
||||
toggleConClasses.push("character-checkbox-disabled");
|
||||
}
|
||||
if (isInteractive) {
|
||||
toggleConClasses.push("ddbc-checkbox--is-interactive");
|
||||
toggleConClasses.push("character-checkbox-interactive");
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={toggleConClasses.join(" ")}
|
||||
onClick={isInteractive ? this.handleClick : undefined}
|
||||
role="checkbox"
|
||||
aria-checked={enabled}
|
||||
>
|
||||
<div className="ddbc-checkbox__input character-checkbox-input">
|
||||
{enabled &&
|
||||
(variant === "primary" ? (
|
||||
<CheckSvg
|
||||
fillColor={CharacterColorEnum.RED}
|
||||
secondaryFillColor=""
|
||||
/>
|
||||
) : theme ? (
|
||||
<ThemedCheckSvg theme={theme} />
|
||||
) : (
|
||||
<PositiveCheckSvg />
|
||||
))}
|
||||
</div>
|
||||
{label && (
|
||||
<div className="ddbc-checkbox__label character-checkbox-label">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import Checkbox from "./Checkbox";
|
||||
|
||||
export default Checkbox;
|
||||
export { Checkbox };
|
||||
@@ -0,0 +1,165 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
DarkChevronUpSvg,
|
||||
DisabledChevronDownSvg,
|
||||
LightChevronUpSvg,
|
||||
} from "../Svg";
|
||||
import CollapsibleHeaderContent from "./CollapsibleHeaderContent";
|
||||
|
||||
interface Props {
|
||||
header: React.ReactNode;
|
||||
headerFooter?: React.ReactNode;
|
||||
className: string;
|
||||
initiallyCollapsed: boolean;
|
||||
collapsed?: boolean;
|
||||
overrideCollapsed?: boolean;
|
||||
onChangeHandler?: (isCollapsed: boolean) => void;
|
||||
layoutType: "standard" | "minimal";
|
||||
useBuilderStyles?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isCollapsed: boolean;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export class Collapsible extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
initiallyCollapsed: true,
|
||||
className: "",
|
||||
layoutType: "standard",
|
||||
useBuilderStyles: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
let initialCollapsed = props.initiallyCollapsed;
|
||||
if (typeof props.collapsed !== "undefined") {
|
||||
initialCollapsed = props.collapsed;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
isCollapsed: initialCollapsed,
|
||||
isDarkMode: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
isDarkMode: !!global.document.getElementsByClassName(
|
||||
"ct-character-sheet--dark-mode"
|
||||
).length,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
const { collapsed, overrideCollapsed } = this.props;
|
||||
|
||||
// Allow for the collapsed prop to set the state if the prop previously set the state
|
||||
if (
|
||||
typeof prevProps.collapsed !== "undefined" &&
|
||||
typeof collapsed !== "undefined" &&
|
||||
collapsed !== prevProps.collapsed
|
||||
) {
|
||||
this.setState({
|
||||
isCollapsed: collapsed,
|
||||
});
|
||||
}
|
||||
|
||||
// Allow the overrideCollapsed prop to set the state if the component previously set its own state
|
||||
if (
|
||||
typeof overrideCollapsed !== "undefined" &&
|
||||
this.state.isCollapsed === prevState.isCollapsed
|
||||
) {
|
||||
this.setState({
|
||||
isCollapsed: overrideCollapsed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
evt.stopPropagation();
|
||||
const { onChangeHandler } = this.props;
|
||||
|
||||
this.setState((prevState, preProps) => {
|
||||
let newIsCollapsed = !prevState.isCollapsed;
|
||||
|
||||
if (onChangeHandler) {
|
||||
onChangeHandler(newIsCollapsed);
|
||||
return {
|
||||
isCollapsed: prevState.isCollapsed,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isCollapsed: newIsCollapsed,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isCollapsed, isDarkMode } = this.state;
|
||||
const {
|
||||
header,
|
||||
className,
|
||||
children,
|
||||
headerFooter,
|
||||
layoutType,
|
||||
useBuilderStyles,
|
||||
} = this.props;
|
||||
|
||||
let headerContentNode: React.ReactNode;
|
||||
if (typeof header === "string") {
|
||||
headerContentNode = <CollapsibleHeaderContent heading={header} />;
|
||||
} else {
|
||||
headerContentNode = header;
|
||||
}
|
||||
|
||||
let classNames: Array<string> = ["ddbc-collapsible", className];
|
||||
classNames.push(
|
||||
isCollapsed ? "ddbc-collapsible--collapsed" : "ddbc-collapsible--opened"
|
||||
);
|
||||
|
||||
switch (layoutType) {
|
||||
case "minimal":
|
||||
classNames.push("ddbc-collapsible--minimal");
|
||||
break;
|
||||
|
||||
default:
|
||||
// not implements
|
||||
}
|
||||
|
||||
let chevronNode: React.ReactNode = <DisabledChevronDownSvg />;
|
||||
if (!isCollapsed) {
|
||||
chevronNode =
|
||||
isDarkMode && !useBuilderStyles ? (
|
||||
<LightChevronUpSvg />
|
||||
) : (
|
||||
<DarkChevronUpSvg />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<div className="ddbc-collapsible__header" onClick={this.handleClick}>
|
||||
{headerContentNode}
|
||||
<div className="ddbc-collapsible__header-status">{chevronNode}</div>
|
||||
</div>
|
||||
{headerFooter && (
|
||||
<div className="ddbc-collapsible__header-footer">{headerFooter}</div>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div className="ddbc-collapsible__content">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Collapsible;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import * as React from "react";
|
||||
|
||||
interface Props {
|
||||
extra: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class CollapsibleHeaderCallout extends React.PureComponent<
|
||||
Props,
|
||||
{}
|
||||
> {
|
||||
static defaultProps = {
|
||||
extra: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { extra, value } = this.props;
|
||||
|
||||
return (
|
||||
<span className="ddbc-collapsible__header-callout">
|
||||
<span className="ddbc-collapsible__header-callout-extra">{extra}</span>
|
||||
<span className="ddbc-collapsible__header-callout-value">{value}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import CollapsibleHeaderCallout from "./CollapsibleHeaderCallout";
|
||||
|
||||
export default CollapsibleHeaderCallout;
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import * as React from "react";
|
||||
|
||||
import CollapsibleHeaderCallout from "../CollapsibleHeaderCallout";
|
||||
import CollapsibleHeading from "../CollapsibleHeading";
|
||||
|
||||
interface Props {
|
||||
heading: React.ReactNode;
|
||||
metaItems?: Array<React.ReactNode>;
|
||||
callout?: React.ReactNode;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export default class CollapsibleHeaderContent extends React.PureComponent<
|
||||
Props,
|
||||
{}
|
||||
> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
renderMetaItems(): React.ReactNode {
|
||||
const { metaItems } = this.props;
|
||||
|
||||
if (!metaItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-collapsible__header-meta">
|
||||
{metaItems.map((item, idx) => (
|
||||
<span className="ddbc-collapsible__header-meta-item" key={idx}>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, heading, callout } = this.props;
|
||||
|
||||
let headingNode: React.ReactNode;
|
||||
if (typeof heading === "string") {
|
||||
headingNode = <CollapsibleHeading>{heading}</CollapsibleHeading>;
|
||||
} else {
|
||||
headingNode = heading;
|
||||
}
|
||||
|
||||
let calloutNode: React.ReactNode;
|
||||
if (typeof callout === "string") {
|
||||
calloutNode = <CollapsibleHeaderCallout value={callout} />;
|
||||
} else {
|
||||
calloutNode = callout;
|
||||
}
|
||||
|
||||
let classNames: Array<string> = [
|
||||
"ddbc-collapsible__header-content",
|
||||
className,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<div className="ddbc-collapsible__header-content-primary">
|
||||
{headingNode}
|
||||
{this.renderMetaItems()}
|
||||
</div>
|
||||
{calloutNode && (
|
||||
<div className="ddbc-collapsible__header-content-callout">
|
||||
{calloutNode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import CollapsibleHeaderContent from "./CollapsibleHeaderContent";
|
||||
|
||||
export default CollapsibleHeaderContent;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import * as React from "react";
|
||||
|
||||
export default class CollapsibleHeading extends React.PureComponent<{}, {}> {
|
||||
render() {
|
||||
return (
|
||||
<div className="ddbc-collapsible__heading">{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import CollapsibleHeading from "./CollapsibleHeading";
|
||||
|
||||
export default CollapsibleHeading;
|
||||
@@ -0,0 +1,12 @@
|
||||
import Collapsible from "./Collapsible";
|
||||
import CollapsibleHeaderCallout from "./CollapsibleHeaderCallout";
|
||||
import CollapsibleHeaderContent from "./CollapsibleHeaderContent";
|
||||
import CollapsibleHeading from "./CollapsibleHeading";
|
||||
|
||||
export default Collapsible;
|
||||
export {
|
||||
Collapsible,
|
||||
CollapsibleHeading,
|
||||
CollapsibleHeaderContent,
|
||||
CollapsibleHeaderCallout,
|
||||
};
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
AbilityLookup,
|
||||
Action,
|
||||
ActionUtils,
|
||||
Attack,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
DiceUtils,
|
||||
EntityUtils,
|
||||
FormatUtils,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import {
|
||||
Dice,
|
||||
RollType,
|
||||
DiceEvent,
|
||||
RollKind,
|
||||
IRollContext,
|
||||
} from "@dndbeyond/dice";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import ActionName from "../../ActionName";
|
||||
import { AttackTypeIcon } from "../../Icons";
|
||||
import NoteComponents from "../../NoteComponents";
|
||||
import { DiceComponentUtils } from "../../utils";
|
||||
import CombatAttack from "../CombatAttack";
|
||||
import { CombatActionAttackComponentRangeInfo } from "./CombatActionAttackTypings";
|
||||
import { DigitalDiceWrapper } from "../../Dice";
|
||||
import Damage from "../../Damage";
|
||||
|
||||
interface Props {
|
||||
attack: Attack;
|
||||
action: Action;
|
||||
onClick?: (attack: Attack) => void;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
showNotes: boolean;
|
||||
className: string;
|
||||
diceEnabled: boolean;
|
||||
theme: CharacterTheme;
|
||||
rollContext: IRollContext;
|
||||
proficiencyBonus: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isCriticalHit: boolean;
|
||||
}
|
||||
class CombatActionAttack extends React.PureComponent<Props, State> {
|
||||
diceEventHandler: (eventData: any) => void;
|
||||
|
||||
static defaultProps = {
|
||||
showNotes: true,
|
||||
className: "",
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isCriticalHit: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.diceEventHandler = DiceComponentUtils.setupResetCritStateOnRoll(
|
||||
ActionUtils.getName(this.props.attack.data as Action),
|
||||
this
|
||||
);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
Dice.removeEventListener(DiceEvent.ROLL, this.diceEventHandler);
|
||||
};
|
||||
|
||||
handleClick = (): void => {
|
||||
const { onClick, attack } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
onClick(attack);
|
||||
}
|
||||
};
|
||||
|
||||
handleRoll = (wasCrit: boolean) => {
|
||||
this.setState({ isCriticalHit: wasCrit });
|
||||
};
|
||||
|
||||
getRangeInfo = (): CombatActionAttackComponentRangeInfo => {
|
||||
const { action, ruleData, theme } = this.props;
|
||||
|
||||
const attackRange = ActionUtils.getRange(action);
|
||||
const attackReach = ActionUtils.getReach(action);
|
||||
const attackTypeId = ActionUtils.getAttackRangeId(action);
|
||||
|
||||
let rangeValueNode: React.ReactNode;
|
||||
let rangeLabel: string = "Range";
|
||||
|
||||
switch (ActionUtils.getActionTypeId(action)) {
|
||||
case Constants.ActionTypeEnum.WEAPON:
|
||||
if (
|
||||
attackRange !== null &&
|
||||
attackTypeId === Constants.AttackTypeRangeEnum.RANGED
|
||||
) {
|
||||
let { longRange, range } = attackRange;
|
||||
|
||||
if (longRange) {
|
||||
rangeValueNode = (
|
||||
<React.Fragment>
|
||||
<span
|
||||
className={`ddbc-combat-attack__range-value-close ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__range-value-close--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{range}
|
||||
</span>
|
||||
<span
|
||||
className={`ddbc-combat-attack__range-value-long ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__range-value-long--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
({longRange})
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
rangeLabel = "";
|
||||
} else {
|
||||
rangeValueNode = (
|
||||
<NumberDisplay type="distanceInFt" number={range} />
|
||||
);
|
||||
rangeLabel = "Range";
|
||||
}
|
||||
} else if (attackReach !== null) {
|
||||
rangeValueNode = (
|
||||
<NumberDisplay type="distanceInFt" number={attackReach} />
|
||||
);
|
||||
rangeLabel = "Reach";
|
||||
}
|
||||
break;
|
||||
|
||||
case Constants.ActionTypeEnum.SPELL:
|
||||
if (attackRange !== null) {
|
||||
if (
|
||||
attackRange.origin &&
|
||||
attackRange.origin !== Constants.SpellRangeTypeEnum.RANGED
|
||||
) {
|
||||
let spellRangeType = RuleDataUtils.getSpellRangeType(
|
||||
attackRange.origin,
|
||||
ruleData
|
||||
);
|
||||
if (spellRangeType) {
|
||||
rangeValueNode = spellRangeType.name;
|
||||
}
|
||||
}
|
||||
if (attackRange.range) {
|
||||
rangeValueNode = (
|
||||
<NumberDisplay type="distanceInFt" number={attackRange.range} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
rangeLabel = "";
|
||||
if (attackTypeId === Constants.AttackTypeRangeEnum.MELEE) {
|
||||
rangeLabel = "Reach";
|
||||
}
|
||||
break;
|
||||
|
||||
case Constants.ActionTypeEnum.GENERAL:
|
||||
if (attackRange !== null && attackRange.range) {
|
||||
rangeValueNode = (
|
||||
<NumberDisplay type="distanceInFt" number={attackRange.range} />
|
||||
);
|
||||
}
|
||||
if (
|
||||
attackReach !== null &&
|
||||
attackTypeId === Constants.AttackTypeRangeEnum.MELEE
|
||||
) {
|
||||
rangeValueNode = (
|
||||
<NumberDisplay type="distanceInFt" number={attackReach} />
|
||||
);
|
||||
rangeLabel = "Reach";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// not implemented
|
||||
}
|
||||
|
||||
return {
|
||||
rangeValueNode,
|
||||
rangeLabel,
|
||||
};
|
||||
};
|
||||
|
||||
getClassName = (): string => {
|
||||
const { action } = this.props;
|
||||
|
||||
let type: string = "";
|
||||
switch (ActionUtils.getActionTypeId(action)) {
|
||||
case Constants.ActionTypeEnum.WEAPON:
|
||||
type = "weapon";
|
||||
break;
|
||||
|
||||
case Constants.ActionTypeEnum.SPELL:
|
||||
type = "spell";
|
||||
break;
|
||||
|
||||
case Constants.ActionTypeEnum.GENERAL:
|
||||
type = "general";
|
||||
break;
|
||||
}
|
||||
|
||||
return FormatUtils.slugify(`ddbc-combat-action-attack--${type}`);
|
||||
};
|
||||
|
||||
getIconKey = (): string => {
|
||||
const { action } = this.props;
|
||||
|
||||
const attackTypeId = ActionUtils.getAttackRangeId(action);
|
||||
let attackType: string = "";
|
||||
if (attackTypeId) {
|
||||
attackType = RuleDataUtils.getAttackTypeRangeName(attackTypeId);
|
||||
}
|
||||
|
||||
let type: string = "";
|
||||
let iconType: string = RuleDataUtils.getAttackTypeRangeName(
|
||||
Constants.AttackTypeRangeEnum.MELEE
|
||||
);
|
||||
|
||||
switch (ActionUtils.getActionTypeId(action)) {
|
||||
case Constants.ActionTypeEnum.WEAPON:
|
||||
type = "weapon";
|
||||
iconType = attackType;
|
||||
break;
|
||||
|
||||
case Constants.ActionTypeEnum.SPELL:
|
||||
type = "spell";
|
||||
if (attackType) {
|
||||
iconType = attackType;
|
||||
}
|
||||
break;
|
||||
|
||||
case Constants.ActionTypeEnum.GENERAL:
|
||||
type = "general";
|
||||
if (attackType) {
|
||||
iconType = attackType;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
//not implemented
|
||||
}
|
||||
|
||||
return FormatUtils.slugify(`action-attack-${type}-${iconType}`);
|
||||
};
|
||||
|
||||
getIconNode = (): React.ReactNode => {
|
||||
const { action, theme } = this.props;
|
||||
|
||||
let rangeType: Constants.AttackTypeRangeEnum =
|
||||
ActionUtils.getAttackRangeId(action) ??
|
||||
Constants.AttackTypeRangeEnum.MELEE;
|
||||
|
||||
//this sucks, but have to switch these around because of how unarmed strike is currently a weapon type
|
||||
//and most others that come thru here are custom actions
|
||||
let actionTypeId = ActionUtils.getActionTypeId(action);
|
||||
let actionType: Constants.ActionTypeEnum | null = actionTypeId;
|
||||
switch (actionTypeId) {
|
||||
case Constants.ActionTypeEnum.WEAPON:
|
||||
if (rangeType === Constants.AttackTypeRangeEnum.MELEE) {
|
||||
actionType = Constants.ActionTypeEnum.GENERAL;
|
||||
}
|
||||
break;
|
||||
|
||||
case Constants.ActionTypeEnum.GENERAL:
|
||||
if (rangeType === Constants.AttackTypeRangeEnum.MELEE) {
|
||||
actionType = Constants.ActionTypeEnum.WEAPON;
|
||||
}
|
||||
break;
|
||||
case Constants.ActionTypeEnum.SPELL:
|
||||
break;
|
||||
|
||||
default:
|
||||
//not implemented
|
||||
}
|
||||
|
||||
return (
|
||||
<AttackTypeIcon
|
||||
actionType={actionType}
|
||||
rangeType={rangeType}
|
||||
themeMode={theme.isDarkMode ? "gray" : "dark"}
|
||||
className={`ddbc-combat-attack__icon-img--${this.getIconKey()}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getMetaItems = (): Array<string> => {
|
||||
const { action } = this.props;
|
||||
|
||||
const damage = ActionUtils.getDamage(action);
|
||||
const attackTypeId = ActionUtils.getAttackRangeId(action);
|
||||
let attackType: string | null = null;
|
||||
if (attackTypeId) {
|
||||
attackType = RuleDataUtils.getAttackTypeRangeName(attackTypeId);
|
||||
}
|
||||
const isOffhand = ActionUtils.isOffhand(action);
|
||||
|
||||
let combinedMetaItems: Array<string> = [];
|
||||
if (attackType) {
|
||||
combinedMetaItems.push(`${attackType} Attack`);
|
||||
}
|
||||
if (damage.isMartialArts) {
|
||||
combinedMetaItems.push("Martial Arts");
|
||||
}
|
||||
if (damage.value && damage.dataOrigin) {
|
||||
combinedMetaItems.push(EntityUtils.getDataOriginName(damage.dataOrigin));
|
||||
}
|
||||
|
||||
switch (ActionUtils.getActionTypeId(action)) {
|
||||
case Constants.ActionTypeEnum.WEAPON:
|
||||
if (isOffhand) {
|
||||
combinedMetaItems.push("Dual Wield");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// not implemented
|
||||
}
|
||||
|
||||
if (ActionUtils.isCustomized(action)) {
|
||||
combinedMetaItems.push("Customized");
|
||||
}
|
||||
|
||||
return combinedMetaItems;
|
||||
};
|
||||
|
||||
renderNotes = (): React.ReactNode => {
|
||||
const {
|
||||
action,
|
||||
showNotes,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!showNotes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-combat-action-attack__notes">
|
||||
<NoteComponents
|
||||
notes={ActionUtils.getNoteComponents(
|
||||
action,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
proficiencyBonus
|
||||
)}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
action,
|
||||
attack,
|
||||
ruleData,
|
||||
showNotes,
|
||||
className,
|
||||
diceEnabled,
|
||||
theme,
|
||||
rollContext,
|
||||
} = this.props;
|
||||
|
||||
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
|
||||
this.context;
|
||||
|
||||
const { isCriticalHit } = this.state;
|
||||
|
||||
const proficiency = ActionUtils.isProficient(action);
|
||||
const damage = ActionUtils.getDamage(action);
|
||||
let rangeInfo = this.getRangeInfo();
|
||||
|
||||
let toHit: number | null = null;
|
||||
let attackSaveValue: number | null = null;
|
||||
let attackSaveLabel: React.ReactNode = "";
|
||||
if (ActionUtils.requiresAttackRoll(action)) {
|
||||
toHit = ActionUtils.getToHit(action);
|
||||
} else if (ActionUtils.requiresSavingThrow(action)) {
|
||||
let saveStatId = ActionUtils.getSaveStatId(action);
|
||||
if (saveStatId) {
|
||||
attackSaveLabel = RuleDataUtils.getStatNameById(saveStatId, ruleData);
|
||||
}
|
||||
attackSaveValue = ActionUtils.getAttackSaveValue(action);
|
||||
}
|
||||
|
||||
let damageNode: React.ReactNode;
|
||||
if (damage.value !== null) {
|
||||
damageNode = (
|
||||
<DigitalDiceWrapper
|
||||
diceNotation={
|
||||
typeof damage.value === "number"
|
||||
? damage.value.toString()
|
||||
: DiceUtils.renderDice(damage.value)
|
||||
}
|
||||
rollType={RollType.Damage}
|
||||
rollAction={ActionUtils.getName(attack.data as Action)}
|
||||
rollKind={isCriticalHit ? RollKind.CriticalHit : RollKind.None}
|
||||
diceEnabled={diceEnabled}
|
||||
themeColor={theme.themeColor}
|
||||
rollContext={rollContext}
|
||||
rollTargetOptions={
|
||||
messageTargetOptions
|
||||
? Object.values(messageTargetOptions?.entities)
|
||||
: undefined
|
||||
}
|
||||
rollTargetDefault={defaultMessageTargetOption}
|
||||
userId={userId}
|
||||
>
|
||||
<Damage
|
||||
type={damage.type ? damage.type.name : null}
|
||||
damage={DiceComponentUtils.getDamageDiceNotation(
|
||||
damage.value,
|
||||
isCriticalHit
|
||||
)}
|
||||
theme={theme}
|
||||
/>
|
||||
</DigitalDiceWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
let classNames: Array<string> = [className, this.getClassName()];
|
||||
|
||||
if (isCriticalHit) {
|
||||
classNames.push("ddbc-combat-attack--crit");
|
||||
}
|
||||
return (
|
||||
<CombatAttack
|
||||
attack={attack}
|
||||
className={classNames.join(" ")}
|
||||
icon={this.getIconNode()}
|
||||
name={<ActionName theme={theme} action={action} />}
|
||||
metaItems={this.getMetaItems()}
|
||||
rangeValue={rangeInfo.rangeValueNode}
|
||||
rangeLabel={rangeInfo.rangeLabel}
|
||||
isProficient={proficiency}
|
||||
toHit={toHit}
|
||||
attackSaveLabel={attackSaveLabel}
|
||||
attackSaveValue={attackSaveValue}
|
||||
damage={damageNode}
|
||||
onClick={this.handleClick}
|
||||
notes={this.renderNotes()}
|
||||
showNotes={showNotes}
|
||||
diceEnabled={diceEnabled}
|
||||
onRoll={this.handleRoll}
|
||||
theme={theme}
|
||||
rollContext={rollContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CombatActionAttack.contextType = GameLogContext;
|
||||
|
||||
export default CombatActionAttack;
|
||||
@@ -0,0 +1,4 @@
|
||||
import CombatActionAttack from "./CombatActionAttack";
|
||||
|
||||
export default CombatActionAttack;
|
||||
export { CombatActionAttack };
|
||||
@@ -0,0 +1,250 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Action,
|
||||
ActionUtils,
|
||||
Attack,
|
||||
CharacterTheme,
|
||||
Item,
|
||||
ItemUtils,
|
||||
Spell,
|
||||
SpellUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { AttackSourceTypeEnum } from "@dndbeyond/character-rules-engine/es/engine/Character";
|
||||
import {
|
||||
DiceTools,
|
||||
RollType,
|
||||
RollRequest,
|
||||
IRollContext,
|
||||
} from "@dndbeyond/dice";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
import { DiceComponentUtils } from "../utils";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { DigitalDiceWrapper } from "../Dice";
|
||||
|
||||
interface Props {
|
||||
attack: Attack;
|
||||
className: string;
|
||||
icon: React.ReactNode;
|
||||
name: React.ReactNode;
|
||||
metaItems: Array<string>;
|
||||
rangeValue: React.ReactNode;
|
||||
rangeLabel: React.ReactNode;
|
||||
isProficient: boolean;
|
||||
toHit: number | null;
|
||||
attackSaveValue?: number | null;
|
||||
attackSaveLabel?: React.ReactNode;
|
||||
damage: React.ReactNode;
|
||||
notes?: React.ReactNode;
|
||||
theme: CharacterTheme;
|
||||
rollContext: IRollContext;
|
||||
onClick?: (attack: Attack) => void;
|
||||
|
||||
showNotes: boolean;
|
||||
diceEnabled: boolean;
|
||||
onRoll?: (wasCrit: boolean) => void;
|
||||
}
|
||||
|
||||
class CombatAttack extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
rangeLabel: "Reach",
|
||||
damage: "--",
|
||||
className: "",
|
||||
showNotes: true,
|
||||
isProficient: false,
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, attack } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
evt.stopPropagation();
|
||||
|
||||
onClick(attack);
|
||||
}
|
||||
};
|
||||
|
||||
handleRollResults = (result: RollRequest) => {
|
||||
const { onRoll } = this.props;
|
||||
|
||||
let wasCrit = DiceComponentUtils.isCriticalRoll(result);
|
||||
if (onRoll) {
|
||||
onRoll(wasCrit);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
icon,
|
||||
name,
|
||||
metaItems,
|
||||
rangeLabel,
|
||||
rangeValue,
|
||||
toHit,
|
||||
attackSaveValue,
|
||||
attackSaveLabel,
|
||||
damage,
|
||||
notes,
|
||||
className,
|
||||
showNotes,
|
||||
diceEnabled,
|
||||
attack,
|
||||
theme,
|
||||
rollContext,
|
||||
} = this.props;
|
||||
|
||||
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
|
||||
this.context;
|
||||
|
||||
let actionNode: React.ReactNode = null;
|
||||
if (toHit !== null) {
|
||||
let rollAction: string;
|
||||
switch (attack.type) {
|
||||
case AttackSourceTypeEnum.ACTION:
|
||||
case AttackSourceTypeEnum.CUSTOM:
|
||||
rollAction = ActionUtils.getName(attack.data as Action);
|
||||
break;
|
||||
case AttackSourceTypeEnum.ITEM:
|
||||
rollAction = ItemUtils.getName(attack.data as Item);
|
||||
break;
|
||||
case AttackSourceTypeEnum.SPELL:
|
||||
rollAction = SpellUtils.getName(attack.data as Spell);
|
||||
break;
|
||||
}
|
||||
|
||||
actionNode = (
|
||||
<div className="ddbc-combat-attack__tohit">
|
||||
<DigitalDiceWrapper
|
||||
diceNotation={DiceTools.CustomD20(toHit)}
|
||||
rollType={RollType.ToHit}
|
||||
rollAction={rollAction}
|
||||
diceEnabled={diceEnabled}
|
||||
onRollResults={this.handleRollResults}
|
||||
themeColor={theme.themeColor}
|
||||
rollContext={rollContext}
|
||||
rollTargetOptions={
|
||||
messageTargetOptions
|
||||
? Object.values(messageTargetOptions?.entities)
|
||||
: undefined
|
||||
}
|
||||
rollTargetDefault={defaultMessageTargetOption}
|
||||
userId={userId}
|
||||
>
|
||||
<NumberDisplay
|
||||
number={toHit}
|
||||
type="signed"
|
||||
isModified={false}
|
||||
/>
|
||||
</DigitalDiceWrapper>
|
||||
</div>
|
||||
);
|
||||
} else if (attackSaveValue !== null) {
|
||||
actionNode = (
|
||||
<div className="ddbc-combat-attack__save">
|
||||
<span
|
||||
className={`ddbc-combat-attack__save-value ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__save-value--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{attackSaveValue}
|
||||
</span>
|
||||
<span
|
||||
className={`ddbc-combat-attack__save-label ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__save-label--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{attackSaveLabel}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-combat-attack"];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames.join(" ")}
|
||||
style={
|
||||
theme?.isDarkMode
|
||||
? { borderColor: `${theme.themeColor}66` }
|
||||
: undefined
|
||||
}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div className="ddbc-combat-attack__icon">{icon}</div>
|
||||
<div className="ddbc-combat-attack__name">
|
||||
<div
|
||||
className={`ddbc-combat-attack__label ${
|
||||
theme.isDarkMode ? "ddbc-combat-attack__label--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
{metaItems.length > 0 && (
|
||||
<div
|
||||
className={`ddbc-combat-attack__meta ${
|
||||
theme.isDarkMode ? "ddbc-combat-attack__meta--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{metaItems.map((metaItem, idx) => (
|
||||
<span className="ddbc-combat-attack__meta-item" key={idx}>
|
||||
{metaItem}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ddbc-combat-attack__range">
|
||||
<div
|
||||
className={`ddbc-combat-attack__range-value ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__range-value--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{rangeValue ? (
|
||||
rangeValue
|
||||
) : (
|
||||
<span className="ddbc-combat-attack__empty">--</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`ddbc-combat-attack__range-label ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__range-label--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{rangeLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ddbc-combat-attack__action">
|
||||
{actionNode ? (
|
||||
actionNode
|
||||
) : (
|
||||
<span className="ddbc-combat-attack__empty">--</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ddbc-combat-attack__damage">{damage}</div>
|
||||
{showNotes && (
|
||||
<div
|
||||
className={`ddbc-combat-attack__notes ${
|
||||
theme.isDarkMode ? "ddbc-combat-attack__notes--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CombatAttack.contextType = GameLogContext;
|
||||
|
||||
export default CombatAttack;
|
||||
@@ -0,0 +1,379 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
AbilityLookup,
|
||||
Attack,
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
DiceUtils,
|
||||
FormatUtils,
|
||||
Item,
|
||||
ItemUtils,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
WeaponSpellDamageGroup,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import {
|
||||
Dice,
|
||||
RollType,
|
||||
DiceEvent,
|
||||
RollKind,
|
||||
IRollContext,
|
||||
} from "@dndbeyond/dice";
|
||||
import StarIcon from "@dndbeyond/fontawesome-cache/svgs/solid/star.svg";
|
||||
import { GameLogContext } from "@dndbeyond/game-log-components";
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import Tooltip from "~/tools/js/commonComponents/Tooltip";
|
||||
import { AttackTypeIcon } from "../../Icons";
|
||||
import NoteComponents from "../../NoteComponents";
|
||||
import { DiceComponentUtils } from "../../utils";
|
||||
import CombatAttack from "../CombatAttack";
|
||||
import { DigitalDiceWrapper } from "../../Dice";
|
||||
import Damage from "../../Damage";
|
||||
|
||||
interface Props {
|
||||
attack: Attack;
|
||||
item: Item;
|
||||
weaponSpellDamageGroups: Array<WeaponSpellDamageGroup>;
|
||||
onClick?: (attack: Attack) => void;
|
||||
abilityLookup: AbilityLookup;
|
||||
ruleData: RuleData;
|
||||
showNotes: boolean;
|
||||
className: string;
|
||||
diceEnabled: boolean;
|
||||
theme: CharacterTheme;
|
||||
rollContext: IRollContext;
|
||||
proficiencyBonus: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isCriticalHit: boolean;
|
||||
}
|
||||
class CombatItemAttack extends React.PureComponent<Props, State> {
|
||||
diceEventHandler: (eventData: any) => void;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isCriticalHit: false,
|
||||
};
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
showNotes: true,
|
||||
className: "",
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this.diceEventHandler = DiceComponentUtils.setupResetCritStateOnRoll(
|
||||
ItemUtils.getName(this.props.attack.data as Item),
|
||||
this
|
||||
);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
Dice.removeEventListener(DiceEvent.ROLL, this.diceEventHandler);
|
||||
};
|
||||
|
||||
handleClick = (): void => {
|
||||
const { onClick, attack } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
onClick(attack);
|
||||
}
|
||||
};
|
||||
|
||||
renderNotes = (): React.ReactNode => {
|
||||
const {
|
||||
item,
|
||||
weaponSpellDamageGroups,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
showNotes,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!showNotes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-combat-item-attack__notes">
|
||||
<NoteComponents
|
||||
notes={ItemUtils.getNoteComponents(
|
||||
item,
|
||||
weaponSpellDamageGroups,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
proficiencyBonus
|
||||
)}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
handleRoll = (wasCrit: boolean) => {
|
||||
this.setState({ isCriticalHit: wasCrit });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
attack,
|
||||
item,
|
||||
weaponSpellDamageGroups,
|
||||
showNotes,
|
||||
diceEnabled,
|
||||
theme,
|
||||
rollContext,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
|
||||
this.context;
|
||||
|
||||
const { isCriticalHit } = this.state;
|
||||
|
||||
const toHit = ItemUtils.getToHit(item);
|
||||
const proficiency = ItemUtils.hasProficiency(item);
|
||||
const metaItems = ItemUtils.getMetaItems(item);
|
||||
const type = ItemUtils.getType(item);
|
||||
const damage = ItemUtils.getDamage(item);
|
||||
const damageType = ItemUtils.getDamageType(item);
|
||||
const attackType = ItemUtils.getAttackType(item);
|
||||
let attackTypeName: string = "";
|
||||
if (attackType) {
|
||||
attackTypeName = RuleDataUtils.getAttackTypeRangeName(attackType);
|
||||
}
|
||||
const versatileDamage = ItemUtils.getVersatileDamage(item);
|
||||
const reach = ItemUtils.getReach(item);
|
||||
const range = ItemUtils.getRange(item);
|
||||
const longRange = ItemUtils.getLongRange(item);
|
||||
const isHexWeapon = ItemUtils.isHexWeapon(item);
|
||||
const isPactWeapon = ItemUtils.isPactWeapon(item);
|
||||
const isDedicatedWeapon = ItemUtils.isDedicatedWeapon(item);
|
||||
const isLegacy = ItemUtils.isLegacy(item);
|
||||
|
||||
let combinedMetaItems: Array<string> = [];
|
||||
|
||||
if (isLegacy) {
|
||||
combinedMetaItems.push("Legacy");
|
||||
}
|
||||
if (type === Constants.WeaponTypeEnum.AMMUNITION) {
|
||||
combinedMetaItems.push("Ammunition");
|
||||
} else {
|
||||
if (isHexWeapon || isPactWeapon || isDedicatedWeapon) {
|
||||
if (isHexWeapon) {
|
||||
combinedMetaItems.push("Hex Weapon");
|
||||
}
|
||||
if (isPactWeapon) {
|
||||
combinedMetaItems.push("Pact Weapon");
|
||||
}
|
||||
if (isDedicatedWeapon) {
|
||||
combinedMetaItems.push("Dedicated Weapon");
|
||||
}
|
||||
} else {
|
||||
combinedMetaItems.push(`${attackTypeName} Weapon`);
|
||||
}
|
||||
}
|
||||
if (ItemUtils.isOffhand(item)) {
|
||||
combinedMetaItems.push("Dual Wield");
|
||||
}
|
||||
if (ItemUtils.isAdamantine(item)) {
|
||||
combinedMetaItems.push("Adamantine");
|
||||
}
|
||||
if (ItemUtils.isSilvered(item)) {
|
||||
combinedMetaItems.push("Silvered");
|
||||
}
|
||||
if (ItemUtils.isCustomized(item)) {
|
||||
combinedMetaItems.push("Customized");
|
||||
}
|
||||
if (ItemUtils.getMasteryName(item)) {
|
||||
combinedMetaItems.push("Mastery");
|
||||
}
|
||||
combinedMetaItems = [...combinedMetaItems, ...metaItems];
|
||||
|
||||
let versatileDamageNode: React.ReactNode;
|
||||
if (versatileDamage) {
|
||||
versatileDamageNode = (
|
||||
<DigitalDiceWrapper
|
||||
diceNotation={DiceUtils.renderDice(versatileDamage)}
|
||||
rollType={RollType.Damage}
|
||||
rollAction={ItemUtils.getName(attack.data as Item)}
|
||||
rollKind={isCriticalHit ? RollKind.CriticalHit : RollKind.None}
|
||||
diceEnabled={diceEnabled}
|
||||
themeColor={theme.themeColor}
|
||||
rollContext={rollContext}
|
||||
rollTargetOptions={
|
||||
messageTargetOptions
|
||||
? Object.values(messageTargetOptions?.entities)
|
||||
: undefined
|
||||
}
|
||||
rollTargetDefault={defaultMessageTargetOption}
|
||||
userId={userId}
|
||||
>
|
||||
<Damage
|
||||
damage={DiceComponentUtils.getDamageDiceNotation(
|
||||
versatileDamage,
|
||||
isCriticalHit
|
||||
)}
|
||||
type={damageType}
|
||||
theme={theme}
|
||||
isVersatile={true}
|
||||
/>
|
||||
</DigitalDiceWrapper>
|
||||
);
|
||||
}
|
||||
let classNames: Array<string> = ["ddbc-combat-item-attack__damage"];
|
||||
if (versatileDamage) {
|
||||
classNames.push("ddb-combat-item-attack__damage--is-versatile");
|
||||
}
|
||||
|
||||
let damageNode: React.ReactNode;
|
||||
if (damage === null) {
|
||||
damageNode = null;
|
||||
} else {
|
||||
damageNode = (
|
||||
<DigitalDiceWrapper
|
||||
diceNotation={
|
||||
typeof damage === "number"
|
||||
? damage.toString()
|
||||
: DiceUtils.renderDice(damage)
|
||||
}
|
||||
rollType={RollType.Damage}
|
||||
rollAction={ItemUtils.getName(attack.data as Item)}
|
||||
rollKind={isCriticalHit ? RollKind.CriticalHit : RollKind.None}
|
||||
diceEnabled={diceEnabled}
|
||||
themeColor={theme.themeColor}
|
||||
rollContext={rollContext}
|
||||
rollTargetOptions={
|
||||
messageTargetOptions
|
||||
? Object.values(messageTargetOptions?.entities)
|
||||
: undefined
|
||||
}
|
||||
rollTargetDefault={defaultMessageTargetOption}
|
||||
userId={userId}
|
||||
>
|
||||
<Damage
|
||||
damage={DiceComponentUtils.getDamageDiceNotation(
|
||||
damage,
|
||||
isCriticalHit
|
||||
)}
|
||||
type={damageType}
|
||||
theme={theme}
|
||||
/>
|
||||
</DigitalDiceWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
let damageDisplayNode: React.ReactNode = (
|
||||
<div className={classNames.join(" ")}>
|
||||
{damageNode}
|
||||
{versatileDamageNode}
|
||||
</div>
|
||||
);
|
||||
|
||||
const showRange: boolean =
|
||||
attackType === Constants.AttackTypeRangeEnum.RANGED ||
|
||||
ItemUtils.hasWeaponProperty(item, Constants.WeaponPropertyEnum.THROWN) ||
|
||||
ItemUtils.hasWeaponProperty(item, Constants.WeaponPropertyEnum.RANGE);
|
||||
|
||||
let rangeValue: React.ReactNode;
|
||||
let rangeLabel: React.ReactNode;
|
||||
if (showRange) {
|
||||
rangeValue = (
|
||||
<React.Fragment>
|
||||
<span
|
||||
className={`ddbc-combat-attack__range-value-close ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__range-value-close--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{range}
|
||||
</span>
|
||||
{longRange !== null && (
|
||||
<span
|
||||
className={`ddbc-combat-attack__range-value-long ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__range-value-long--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
({longRange})
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
rangeLabel = "";
|
||||
} else {
|
||||
rangeValue = <NumberDisplay type="distanceInFt" number={reach} />;
|
||||
}
|
||||
|
||||
let filteredWeaponSpellDamageGroups =
|
||||
ItemUtils.getApplicableWeaponSpellDamageGroups(
|
||||
item,
|
||||
weaponSpellDamageGroups
|
||||
);
|
||||
|
||||
let iconKey: string = `weapon-${FormatUtils.slugify(attackTypeName)}`;
|
||||
if (filteredWeaponSpellDamageGroups.length) {
|
||||
iconKey = "weapon-spell-damage";
|
||||
}
|
||||
let attackClassNames: Array<string> = [
|
||||
"ddbc-combat-attack--item",
|
||||
`ddbc-combat-item-attack--${FormatUtils.slugify(attackTypeName)}`,
|
||||
className,
|
||||
];
|
||||
if (isCriticalHit) {
|
||||
attackClassNames.push("ddbc-combat-attack--crit");
|
||||
}
|
||||
return (
|
||||
<CombatAttack
|
||||
attack={attack}
|
||||
className={attackClassNames.join(" ")}
|
||||
icon={
|
||||
<>
|
||||
{ItemUtils.getMasteryName(item) && (
|
||||
<Tooltip title="Weapon Mastery" isDarkMode={theme.isDarkMode}>
|
||||
<StarIcon
|
||||
style={{ fill: theme.themeColor, marginLeft: "1px" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<AttackTypeIcon
|
||||
actionType={Constants.ActionTypeEnum.WEAPON}
|
||||
rangeType={attackType ?? Constants.AttackTypeRangeEnum.MELEE}
|
||||
themeMode={theme.isDarkMode ? "gray" : "dark"}
|
||||
className={`ddbc-combat-attack__icon-img--${iconKey}`}
|
||||
overrideType={
|
||||
filteredWeaponSpellDamageGroups.length ? "weapon-spell" : null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
name={<ItemName item={item} />}
|
||||
metaItems={combinedMetaItems}
|
||||
rangeValue={rangeValue}
|
||||
rangeLabel={rangeLabel}
|
||||
isProficient={proficiency}
|
||||
toHit={toHit}
|
||||
damage={damageDisplayNode}
|
||||
notes={this.renderNotes()}
|
||||
showNotes={showNotes}
|
||||
onClick={this.handleClick}
|
||||
diceEnabled={diceEnabled}
|
||||
onRoll={this.handleRoll}
|
||||
rollContext={rollContext}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CombatItemAttack.contextType = GameLogContext;
|
||||
|
||||
export default CombatItemAttack;
|
||||
@@ -0,0 +1,4 @@
|
||||
import CombatItemAttack from "./CombatItemAttack";
|
||||
|
||||
export default CombatItemAttack;
|
||||
export { CombatItemAttack };
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
Attack,
|
||||
Constants,
|
||||
DataOriginRefData,
|
||||
EntityUtils,
|
||||
FormatUtils,
|
||||
Spell,
|
||||
SpellCasterInfo,
|
||||
SpellUtils,
|
||||
RuleData,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { Dice, DiceEvent, IRollContext } from "@dndbeyond/dice";
|
||||
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
import { SpellName } from "~/components/SpellName";
|
||||
|
||||
import { SpellSchoolIcon } from "../../Icons";
|
||||
import NoteComponents from "../../NoteComponents";
|
||||
import SpellDamageEffect from "../../SpellDamageEffect";
|
||||
import { SpellSchoolPropType } from "../../componentConstants";
|
||||
import { DiceComponentUtils } from "../../utils";
|
||||
import CombatAttack from "../CombatAttack";
|
||||
|
||||
interface Props {
|
||||
attack: Attack;
|
||||
spell: Spell;
|
||||
onClick?: (attack: Attack) => void;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
spellCasterInfo: SpellCasterInfo;
|
||||
showNotes: boolean;
|
||||
className: string;
|
||||
diceEnabled: boolean;
|
||||
theme: CharacterTheme;
|
||||
rollContext: IRollContext;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
proficiencyBonus: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isCriticalHit: boolean;
|
||||
}
|
||||
|
||||
export default class CombatSpellAttack extends React.PureComponent<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
diceEventHandler: (eventData: any) => void;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isCriticalHit: false,
|
||||
};
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
showNotes: true,
|
||||
className: "",
|
||||
diceEnabled: false,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this.diceEventHandler = DiceComponentUtils.setupResetCritStateOnRoll(
|
||||
SpellUtils.getName(this.props.spell),
|
||||
this
|
||||
);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
Dice.removeEventListener(DiceEvent.ROLL, this.diceEventHandler);
|
||||
};
|
||||
|
||||
handleClick = (): void => {
|
||||
const { onClick, attack } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
onClick(attack);
|
||||
}
|
||||
};
|
||||
|
||||
renderNotes = (): React.ReactNode => {
|
||||
const {
|
||||
spell,
|
||||
abilityLookup,
|
||||
ruleData,
|
||||
showNotes,
|
||||
spellCasterInfo,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!showNotes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const castLevel = SpellUtils.getMinCastLevel(
|
||||
spell,
|
||||
spellCasterInfo,
|
||||
ruleData
|
||||
);
|
||||
const characterLevel = SpellUtils.getCharacterLevel(spell);
|
||||
let scaledAmount: number = 0;
|
||||
|
||||
return (
|
||||
<div className="ddbc-combat-spell-attack__notes">
|
||||
<NoteComponents
|
||||
notes={SpellUtils.getNoteComponents(
|
||||
spell,
|
||||
castLevel,
|
||||
characterLevel,
|
||||
abilityLookup,
|
||||
ruleData,
|
||||
scaledAmount,
|
||||
proficiencyBonus
|
||||
)}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
handleRoll = (wasCrit: boolean) => {
|
||||
this.setState({ isCriticalHit: wasCrit });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
attack,
|
||||
spell,
|
||||
ruleData,
|
||||
spellCasterInfo,
|
||||
showNotes,
|
||||
diceEnabled,
|
||||
theme,
|
||||
dataOriginRefData,
|
||||
rollContext,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
const { isCriticalHit } = this.state;
|
||||
|
||||
const characterLevel = SpellUtils.getCharacterLevel(spell);
|
||||
const attackSaveValue = SpellUtils.getAttackSaveValue(spell);
|
||||
const toHit = SpellUtils.getToHit(spell);
|
||||
const isCustomized = SpellUtils.isCustomized(spell);
|
||||
const range = SpellUtils.getRange(spell);
|
||||
const level = SpellUtils.getLevel(spell);
|
||||
const school = SpellUtils.getSchool(spell);
|
||||
const concentration = SpellUtils.getConcentration(spell);
|
||||
const requiresAttackRoll = SpellUtils.getRequiresAttackRoll(spell);
|
||||
const requiresSavingThrow = SpellUtils.getRequiresSavingThrow(spell);
|
||||
const castLevel = SpellUtils.getMinCastLevel(
|
||||
spell,
|
||||
spellCasterInfo,
|
||||
ruleData
|
||||
);
|
||||
|
||||
let metaItems: Array<string> = [];
|
||||
if (SpellUtils.isLegacy(spell)) {
|
||||
metaItems.push("Legacy");
|
||||
}
|
||||
|
||||
metaItems.push(FormatUtils.renderSpellLevelName(level));
|
||||
|
||||
const spellDataOrigin = SpellUtils.getDataOrigin(spell);
|
||||
if (spellDataOrigin) {
|
||||
const dataOriginName = EntityUtils.getDataOriginName(spellDataOrigin);
|
||||
metaItems.push(dataOriginName);
|
||||
}
|
||||
let expandedDataOrigin = SpellUtils.getExpandedDataOriginRef(spell);
|
||||
if (expandedDataOrigin) {
|
||||
metaItems.push(
|
||||
EntityUtils.getDataOriginRefName(expandedDataOrigin, dataOriginRefData)
|
||||
);
|
||||
}
|
||||
if (concentration) {
|
||||
metaItems.push("Concentration");
|
||||
}
|
||||
if (isCustomized) {
|
||||
metaItems.push("Customized");
|
||||
}
|
||||
|
||||
let rangeAreaNode: React.ReactNode;
|
||||
if (range !== null) {
|
||||
rangeAreaNode = (
|
||||
<span className="ddbc-combat-attack__spell-range">
|
||||
{!!range.origin &&
|
||||
range.origin !== Constants.SpellRangeTypeNameEnum.RANGED && (
|
||||
<span
|
||||
className={`ddbc-combat-attack__spell-range-origin ${
|
||||
theme.isDarkMode
|
||||
? "ddbc-combat-attack__spell-range-origin--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{range.origin}
|
||||
</span>
|
||||
)}
|
||||
{!!range.rangeValue && (
|
||||
<span className="ddbc-combat-attack__spell-range-value">
|
||||
<NumberDisplay type="distanceInFt" number={range.rangeValue} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let toHitDisplay: number | null = null;
|
||||
let saveDcValue: number | null = null;
|
||||
let saveDcLabel: React.ReactNode;
|
||||
if (requiresAttackRoll) {
|
||||
toHitDisplay = toHit;
|
||||
} else if (requiresSavingThrow) {
|
||||
saveDcValue = attackSaveValue;
|
||||
saveDcLabel = SpellUtils.getSaveDcAbilityShortName(spell, ruleData);
|
||||
}
|
||||
let attackClassNames: Array<string> = ["ddbc-combat-attack--spell", className];
|
||||
|
||||
if (isCriticalHit) {
|
||||
attackClassNames.push("ddbc-combat-attack--crit");
|
||||
}
|
||||
|
||||
return (
|
||||
<CombatAttack
|
||||
attack={attack}
|
||||
className={attackClassNames.join(" ")}
|
||||
icon={
|
||||
<SpellSchoolIcon
|
||||
school={FormatUtils.slugify(school) as SpellSchoolPropType}
|
||||
themeMode={theme.isDarkMode ? "gray" : "dark"}
|
||||
className={`ddbc-combat-attack__icon-img--spell-school-${FormatUtils.slugify(
|
||||
school
|
||||
)}`}
|
||||
/>
|
||||
}
|
||||
name={
|
||||
<SpellName
|
||||
spell={spell}
|
||||
showSpellLevel={false}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
/>
|
||||
}
|
||||
metaItems={metaItems}
|
||||
rangeValue={rangeAreaNode}
|
||||
rangeLabel={""}
|
||||
toHit={toHitDisplay}
|
||||
attackSaveValue={saveDcValue}
|
||||
attackSaveLabel={saveDcLabel}
|
||||
rollContext={rollContext}
|
||||
damage={
|
||||
<SpellDamageEffect
|
||||
spell={spell}
|
||||
characterLevel={characterLevel}
|
||||
castLevel={castLevel}
|
||||
ruleData={ruleData}
|
||||
diceEnabled={diceEnabled}
|
||||
theme={theme}
|
||||
isCriticalHit={isCriticalHit}
|
||||
rollContext={rollContext}
|
||||
/>
|
||||
}
|
||||
onClick={this.handleClick}
|
||||
notes={this.renderNotes()}
|
||||
showNotes={showNotes}
|
||||
diceEnabled={diceEnabled}
|
||||
onRoll={this.handleRoll}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import CombatSpellAttack from "./CombatSpellAttack";
|
||||
|
||||
export default CombatSpellAttack;
|
||||
export { CombatSpellAttack };
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
ConditionUtils,
|
||||
Condition,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
condition: Condition;
|
||||
tooltipText: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class ConditionName extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
tooltipText: "",
|
||||
};
|
||||
|
||||
renderConditionLevel = (condition: Condition): React.ReactNode => {
|
||||
const levels = ConditionUtils.getLevels(condition);
|
||||
|
||||
if (!levels.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="ddbc-condition__level">
|
||||
{" "}
|
||||
(Level {ConditionUtils.getActiveLevel(condition)})
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, condition, tooltipText, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-condition"];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")}>
|
||||
<Tooltip
|
||||
title={tooltipText}
|
||||
enabled={!!tooltipText}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
<span
|
||||
className={`ddbc-condition__name ${
|
||||
theme?.isDarkMode ? "ddbc-condition__name--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{ConditionUtils.getName(condition)}
|
||||
{this.renderConditionLevel(condition)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { orderBy } from "lodash";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
Creature,
|
||||
CreatureUtils,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import CreatureListRow from "./CreatureListRow";
|
||||
|
||||
//TODO this component is currently replaced by ExtraList for Creatures and Vehicles as Extras
|
||||
interface Props {
|
||||
className: string;
|
||||
creatures: Array<Creature>;
|
||||
showNotes: boolean;
|
||||
ruleData: RuleData;
|
||||
onShow: (id: number) => void;
|
||||
onStatusChange: (id: number, isActive: boolean) => void;
|
||||
isReadonly: boolean;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class CreatureList extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
showNotes: true,
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
creatures,
|
||||
ruleData,
|
||||
showNotes,
|
||||
onShow,
|
||||
onStatusChange,
|
||||
isReadonly,
|
||||
className,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let orderedCreatures: Array<Creature> = orderBy(creatures, (creature) =>
|
||||
CreatureUtils.getName(creature)
|
||||
);
|
||||
let classNames: Array<string> = [className, "ddbc-creature-list"];
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<div className="ddbc-creature-list__row-header">
|
||||
<div className="ddbc-creature-list__col ddbc-creature-list__col--preview" />
|
||||
<div className="ddbc-creature-list__col ddbc-creature-list__col--primary">
|
||||
Name
|
||||
</div>
|
||||
<div className="ddbc-creature-list__col ddbc-creature-list__col--ac">
|
||||
AC
|
||||
</div>
|
||||
<div className="ddbc-creature-list__col ddbc-creature-list__col--hp">
|
||||
Hit Points
|
||||
</div>
|
||||
<div className="ddbc-creature-list__col ddbc-creature-list__col--speed">
|
||||
Speed
|
||||
</div>
|
||||
{showNotes && (
|
||||
<div className="ddbc-creature-list__col ddbc-creature-list__col--notes">
|
||||
Notes
|
||||
</div>
|
||||
)}
|
||||
{/*<div className="ddbc-creature-list__col ddbc-creature-list__col--action">Active</div>*/}
|
||||
</div>
|
||||
<div className="ddbc-creature-list__items">
|
||||
{orderedCreatures.map((creature) => (
|
||||
<CreatureListRow
|
||||
key={CreatureUtils.getUniqueKey(creature)}
|
||||
creature={creature}
|
||||
ruleData={ruleData}
|
||||
onShow={onShow}
|
||||
onStatusChange={onStatusChange}
|
||||
showNotes={showNotes}
|
||||
isReadonly={isReadonly}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Creature,
|
||||
CreatureUtils,
|
||||
Constants,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
|
||||
import { NoteComponents } from "../../NoteComponents";
|
||||
|
||||
//TODO this component is currently replaced by ExtraListRow for Creatures and Vehicles as Extras
|
||||
|
||||
interface Props {
|
||||
creature: Creature;
|
||||
ruleData: RuleData;
|
||||
onShow?: (id: number) => void;
|
||||
onStatusChange?: (id: number, isActive: boolean) => void;
|
||||
showNotes: boolean;
|
||||
isReadonly: boolean;
|
||||
className: string;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class CreatureListRow extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onShow, creature } = this.props;
|
||||
|
||||
if (onShow) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onShow(CreatureUtils.getMappingId(creature));
|
||||
}
|
||||
};
|
||||
|
||||
handleActivate = (): void => {
|
||||
const { onStatusChange, creature } = this.props;
|
||||
|
||||
if (onStatusChange) {
|
||||
onStatusChange(CreatureUtils.getMappingId(creature), true);
|
||||
}
|
||||
};
|
||||
|
||||
handleDeactivate = (): void => {
|
||||
const { onStatusChange, creature } = this.props;
|
||||
|
||||
if (onStatusChange) {
|
||||
onStatusChange(CreatureUtils.getMappingId(creature), false);
|
||||
}
|
||||
};
|
||||
|
||||
renderMetaItems = (): React.ReactNode => {
|
||||
const { creature, ruleData } = this.props;
|
||||
|
||||
let metaItems: Array<string> = [];
|
||||
|
||||
const sizeInfo = RuleDataUtils.getCreatureSizeInfo(
|
||||
CreatureUtils.getSizeId(creature),
|
||||
ruleData
|
||||
);
|
||||
let size: string = "";
|
||||
if (sizeInfo && sizeInfo.name !== null) {
|
||||
size = sizeInfo.name;
|
||||
}
|
||||
|
||||
let type = CreatureUtils.getTypeName(creature, ruleData);
|
||||
|
||||
let description: string = `${size} ${type}`.trim();
|
||||
if (description) {
|
||||
metaItems.push(description);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-creature-list__row-meta">
|
||||
{metaItems.map((metaItem, idx) => (
|
||||
<span key={idx} className="ddbc-creature-list__row-meta-item">
|
||||
{metaItem}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderHitPoints = (): React.ReactNode => {
|
||||
const { creature } = this.props;
|
||||
|
||||
const hitPointInfo = CreatureUtils.getHitPointInfo(creature);
|
||||
const tempHp = hitPointInfo.tempHp === null ? 0 : hitPointInfo.tempHp;
|
||||
|
||||
let classNames: Array<string> = ["ddbc-creature-list__row-hp"];
|
||||
if (tempHp > 0) {
|
||||
classNames.push("ddbc-creature-list__row-hp--has-temp");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<span className="ddbc-creature-list__row-hp-value ddbc-creature-list__row-hp-value--current">
|
||||
{hitPointInfo.remainingHp + tempHp}
|
||||
</span>
|
||||
<span className="ddbc-creature-list__row-hp-sep">/</span>
|
||||
<span className="ddbc-creature-list__row-hp-value ddbc-creature-list__row-hp-value--total">
|
||||
{hitPointInfo.totalHp + tempHp}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderSpeed = (): React.ReactNode => {
|
||||
const { creature, ruleData, theme } = this.props;
|
||||
|
||||
const movementInfo = CreatureUtils.getHighestMovementInfo(creature);
|
||||
|
||||
let contentNode: React.ReactNode;
|
||||
if (movementInfo) {
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<div className="ddbc-creature-list__row-speed-value">
|
||||
<NumberDisplay type="distanceInFt" number={movementInfo.speed} />
|
||||
</div>
|
||||
{movementInfo.movementId !== Constants.MovementTypeEnum.WALK && (
|
||||
<div className="ddbc-creature-list__row-speed-callout">
|
||||
{RuleDataUtils.getMovementDescription(
|
||||
movementInfo.movementId,
|
||||
ruleData
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
contentNode = (
|
||||
<div className="ddbc-creature-list__row-speed-value">--</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="ddbc-creature-list__row-speed">{contentNode}</div>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { creature, ruleData, isReadonly, showNotes, className, theme } =
|
||||
this.props;
|
||||
|
||||
const avatarUrl = CreatureUtils.getAvatarUrl(creature);
|
||||
const name = CreatureUtils.getName(creature);
|
||||
const armorClass = CreatureUtils.getArmorClass(creature);
|
||||
const movementInfo = CreatureUtils.getHighestMovementInfo(creature);
|
||||
const isActive = CreatureUtils.isActive(creature);
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-creature-row"];
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")} onClick={this.handleClick}>
|
||||
<div className="ddbc-creature-list__row-preview">
|
||||
{avatarUrl && (
|
||||
<img
|
||||
className="ddbc-creature-list__row-img"
|
||||
src={avatarUrl}
|
||||
alt={`${name} preview`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="ddbc-creature-list__row-primary">
|
||||
<div className="ddbc-creature-list__row-name">{name}</div>
|
||||
{this.renderMetaItems()}
|
||||
</div>
|
||||
<div className="ddbc-creature-list__row-ac">{armorClass}</div>
|
||||
{this.renderHitPoints()}
|
||||
{this.renderSpeed()}
|
||||
{showNotes && (
|
||||
<div className="ddbc-creature-list__row-notes">
|
||||
<NoteComponents
|
||||
notes={CreatureUtils.getNoteComponents(creature, ruleData)}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/*<div className="ddbc-creature-list__row-action">
|
||||
<SlotManager
|
||||
used={isActive ? 1 : 0}
|
||||
available={1}
|
||||
size="small"
|
||||
onUse={this.handleActivate}
|
||||
onClear={this.handleDeactivate}
|
||||
isInteractive={!isReadonly}
|
||||
/>
|
||||
</div>*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import CreatureListRow from "./CreatureListRow";
|
||||
|
||||
export default CreatureListRow;
|
||||
export { CreatureListRow };
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
Creature,
|
||||
CreatureUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
creature: Creature;
|
||||
onClick?: (creature: Creature) => void;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
export default class CreatureName extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, creature } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
|
||||
onClick(creature);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { creature, className, theme } = this.props;
|
||||
|
||||
let isCustomized = CreatureUtils.isCustomized(creature);
|
||||
let classNames: Array<string> = [className, "ddbc-creature-name"];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")} onClick={this.handleClick}>
|
||||
{CreatureUtils.getName(creature)}
|
||||
{isCustomized && (
|
||||
<Tooltip
|
||||
title="Creature is Customized"
|
||||
className="ddbc-creature-name__customized"
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
*
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Creature,
|
||||
CreatureUtils,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
creature: Creature;
|
||||
ruleData: RuleData;
|
||||
onClick?: (creature: Creature) => void;
|
||||
}
|
||||
export class CreaturePreview extends React.PureComponent<Props> {
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, creature } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onClick(creature);
|
||||
}
|
||||
};
|
||||
|
||||
renderMetaText = (): React.ReactNode => {
|
||||
const { ruleData, creature } = this.props;
|
||||
|
||||
let metaItems: Array<string> = [];
|
||||
|
||||
const sizeInfo = CreatureUtils.getSizeInfo(creature);
|
||||
let size: string | null = "";
|
||||
if (sizeInfo !== null) {
|
||||
size = sizeInfo.name;
|
||||
}
|
||||
|
||||
let description: string = `${size} ${CreatureUtils.getTypeName(
|
||||
creature,
|
||||
ruleData
|
||||
)}`.trim();
|
||||
if (description.length) {
|
||||
metaItems.push(description);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-creature-preview__meta">
|
||||
{metaItems.map((metaItem, idx) => (
|
||||
<span className="ddbc-creature-preview__meta-item" key={idx}>
|
||||
{metaItem}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { creature } = this.props;
|
||||
|
||||
const imageStyles: React.CSSProperties = {
|
||||
backgroundImage: `url(${CreatureUtils.getAvatarUrl(creature)})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ddbc-creature-preview" onClick={this.handleClick}>
|
||||
<div className="ddbc-creature-preview__avatar" style={imageStyles} />
|
||||
<div className="ddbc-creature-preview__name">
|
||||
{CreatureUtils.getName(creature)}
|
||||
{this.renderMetaText()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CreaturePreview;
|
||||
@@ -0,0 +1,94 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
DiceContract,
|
||||
DiceUtils,
|
||||
FormatUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DamageTypeIcon } from "../Icons";
|
||||
import { DamageTypePropType } from "../Icons/IconConstants";
|
||||
|
||||
export interface Props {
|
||||
className: string;
|
||||
damage: DiceContract | string | number | null;
|
||||
type: string | null;
|
||||
info?: string;
|
||||
isVersatile: boolean;
|
||||
showInfoTooltip: boolean;
|
||||
showInfoInline: boolean;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
export default class Damage extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
type: null,
|
||||
isVersatile: false,
|
||||
showInfoTooltip: true,
|
||||
showInfoInline: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
damage,
|
||||
type,
|
||||
isVersatile,
|
||||
info,
|
||||
showInfoInline,
|
||||
showInfoTooltip,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let displayDamage: React.ReactNode;
|
||||
if (damage !== null) {
|
||||
if (typeof damage === "string" || typeof damage === "number") {
|
||||
displayDamage = damage;
|
||||
} else {
|
||||
displayDamage = DiceUtils.renderDice(damage);
|
||||
}
|
||||
}
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-damage"];
|
||||
if (isVersatile) {
|
||||
classNames.push("ddbc-damage--versatile");
|
||||
}
|
||||
if (theme?.isDarkMode) {
|
||||
classNames.push("ddbc-damage--dark-mode");
|
||||
}
|
||||
|
||||
let tooltipContent: string | null = null;
|
||||
if (showInfoTooltip && (info || type)) {
|
||||
tooltipContent = info ? info : type;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
className={classNames.join(" ")}
|
||||
title={tooltipContent === null ? "" : tooltipContent}
|
||||
enabled={showInfoTooltip && tooltipContent !== null}
|
||||
>
|
||||
<span
|
||||
className={`ddbc-damage__value ${
|
||||
theme?.isDarkMode ? "ddbc-damage__value--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
{displayDamage}
|
||||
</span>
|
||||
{!isVersatile && !!type && (
|
||||
<span className="ddbc-damage__icon">
|
||||
<DamageTypeIcon
|
||||
theme={theme}
|
||||
type={FormatUtils.slugify(type) as DamageTypePropType}
|
||||
showTooltip={!showInfoTooltip}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{showInfoInline && <span className="ddbc-damage__info">{info}</span>}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import Damage from "./Damage";
|
||||
|
||||
export default Damage;
|
||||
export { Damage };
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
DamageAdjustmentContract,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
damageAdjustment: DamageAdjustmentContract;
|
||||
tooltipText: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
export default class DamageAdjustment extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, damageAdjustment, tooltipText, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-damage-adjustment"];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")}>
|
||||
<Tooltip
|
||||
title={tooltipText}
|
||||
enabled={!!tooltipText}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
<span className="ddbc-damage-adjustment__preview">
|
||||
<i
|
||||
className={`ddbc-damage-adjustment__icon i-damage-adjustment-${damageAdjustment.slug}`}
|
||||
/>
|
||||
</span>
|
||||
<span className="ddbc-damage-adjustment__name">
|
||||
{damageAdjustment.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import DamageAdjustment from "./DamageAdjustment";
|
||||
|
||||
export default DamageAdjustment;
|
||||
export { DamageAdjustment };
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
DataOrigin,
|
||||
EntityUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
|
||||
interface Props {
|
||||
dataOrigin: DataOrigin;
|
||||
tryParent?: boolean;
|
||||
onClick?: (dataOrigin: DataOrigin) => void;
|
||||
className: string;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class DataOriginName extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, dataOrigin } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onClick(dataOrigin);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dataOrigin, className, tryParent } = this.props;
|
||||
|
||||
let displayNode: React.ReactNode;
|
||||
|
||||
switch (dataOrigin.type) {
|
||||
case Constants.DataOriginTypeEnum.ITEM:
|
||||
displayNode = <ItemName item={dataOrigin.primary} />;
|
||||
break;
|
||||
|
||||
default:
|
||||
displayNode = EntityUtils.getDataOriginName(
|
||||
dataOrigin,
|
||||
undefined,
|
||||
tryParent
|
||||
);
|
||||
}
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-data-origin-name"];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")} onClick={this.handleClick}>
|
||||
{displayNode}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import DataOriginName from "./DataOriginName";
|
||||
|
||||
export default DataOriginName;
|
||||
export { DataOriginName };
|
||||
@@ -0,0 +1,36 @@
|
||||
import { RollType } from "@dndbeyond/dice";
|
||||
import { WithDiceRollv2 } from "@dndbeyond/dice-components";
|
||||
import { RollableComponentProps } from "@dndbeyond/dice-components/dist/types/src/components/WithDiceRollv2";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export interface DigitalDiceWrapperProps extends
|
||||
Omit<RollableComponentProps, "BaseComponent" | "useCustomizableDiceNotation">
|
||||
{
|
||||
}
|
||||
|
||||
export const DigitalDiceWrapper: React.FC<DigitalDiceWrapperProps> = ({
|
||||
useAdvancedMenu = true,
|
||||
rollType,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const useCustomizableDiceNotation = useMemo(
|
||||
() => rollType === RollType.Damage
|
||||
|| rollType === RollType.Heal,
|
||||
[rollType]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithDiceRollv2
|
||||
BaseComponent={(
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
useAdvancedMenu={useAdvancedMenu}
|
||||
useCustomizableDiceNotation={useCustomizableDiceNotation}
|
||||
rollType={rollType}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import { RollRequest } from "@dndbeyond/dice";
|
||||
|
||||
import LoadingPlaceholder from "../../LoadingPlaceholder";
|
||||
import { DataLoadingStatusEnum } from "../../componentConstants";
|
||||
import {
|
||||
DiceRollActionNode,
|
||||
DiceRollActionNodeProps,
|
||||
} from "./DiceRollActionNode";
|
||||
import { DiceRollValues, DiceRollValuesProps } from "./DiceRollValues";
|
||||
|
||||
export interface DiceRollProps {
|
||||
rollKey: string;
|
||||
nextRollKey: string | null;
|
||||
rollTotal: number | null;
|
||||
diceRollRequest: RollRequest;
|
||||
onChange?: (
|
||||
key: string,
|
||||
newValue: string | null,
|
||||
prevValue: string | null,
|
||||
nextRollKey: string | null,
|
||||
rollTotal: number | null,
|
||||
rollValues: DiceRollValuesProps["rollValues"]
|
||||
) => void;
|
||||
onRoll: (
|
||||
key: string,
|
||||
diceRollRequest: RollRequest,
|
||||
nextRollKey: string | null
|
||||
) => void;
|
||||
rollValues: DiceRollValuesProps["rollValues"];
|
||||
rollStatus: DiceRollActionNodeProps["rollStatus"];
|
||||
options?: DiceRollActionNodeProps["options"];
|
||||
assignedValue: DiceRollActionNodeProps["assignedValue"];
|
||||
rollButtonText?: DiceRollActionNodeProps["rollButtonText"];
|
||||
selectPlaceholderText?: DiceRollActionNodeProps["selectPlaceholderText"];
|
||||
}
|
||||
const DiceRoll: React.FunctionComponent<DiceRollProps> = ({
|
||||
rollKey,
|
||||
nextRollKey,
|
||||
options,
|
||||
rollTotal,
|
||||
rollValues,
|
||||
assignedValue,
|
||||
diceRollRequest,
|
||||
onChange,
|
||||
onRoll,
|
||||
rollStatus,
|
||||
rollButtonText,
|
||||
selectPlaceholderText,
|
||||
}) => {
|
||||
const handleChange = useCallback(
|
||||
(changedValue: string): void => {
|
||||
const prevValue = assignedValue;
|
||||
const newValue: string | null = changedValue ?? null;
|
||||
|
||||
if (newValue !== prevValue) {
|
||||
if (onChange) {
|
||||
onChange(
|
||||
rollKey,
|
||||
newValue,
|
||||
prevValue,
|
||||
nextRollKey,
|
||||
rollTotal,
|
||||
rollValues
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onChange, rollKey, assignedValue, nextRollKey, rollTotal, rollValues]
|
||||
);
|
||||
|
||||
const handleRollClick = useCallback((): void => {
|
||||
onRoll(rollKey, diceRollRequest, nextRollKey);
|
||||
}, [onRoll, rollKey, diceRollRequest, nextRollKey]);
|
||||
|
||||
let contentNode: React.ReactNode = null;
|
||||
switch (rollStatus) {
|
||||
case DataLoadingStatusEnum.LOADING:
|
||||
contentNode = <LoadingPlaceholder label="Rolling" />;
|
||||
break;
|
||||
|
||||
default: {
|
||||
const diceTotalClassNames: Array<string> = ["ddbc-dice-roll__total"];
|
||||
if (rollTotal === null) {
|
||||
diceTotalClassNames.push("ddbc-dice-roll__total--empty");
|
||||
}
|
||||
|
||||
contentNode = (
|
||||
<React.Fragment>
|
||||
<div className={diceTotalClassNames.join(" ")}>
|
||||
<label className="ddbc-dice-roll__total-label" htmlFor={rollKey}>
|
||||
{rollTotal ?? "--"}
|
||||
</label>
|
||||
</div>
|
||||
<div className="ddbc-dice-roll__dice">
|
||||
<DiceRollValues rollValues={rollValues} />
|
||||
</div>
|
||||
<DiceRollActionNode
|
||||
rollKey={rollKey}
|
||||
rollStatus={rollStatus}
|
||||
rollTotal={rollTotal}
|
||||
assignedValue={assignedValue}
|
||||
options={options}
|
||||
onRoll={handleRollClick}
|
||||
onChange={handleChange}
|
||||
rollButtonText={rollButtonText}
|
||||
selectPlaceholderText={selectPlaceholderText}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="ddbc-dice-roll">{contentNode}</div>;
|
||||
};
|
||||
|
||||
export default React.memo(DiceRoll);
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
|
||||
import { HtmlSelectOption } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DataLoadingStatusEnum } from "../../../componentConstants";
|
||||
import { Button } from "../../../legacy/Button";
|
||||
import { Select } from "../../../legacy/Select";
|
||||
|
||||
export interface DiceRollActionNodeProps {
|
||||
rollKey: string;
|
||||
rollStatus: DataLoadingStatusEnum;
|
||||
rollTotal: number | null;
|
||||
assignedValue: string | null;
|
||||
options?: Array<HtmlSelectOption>;
|
||||
rollButtonText?: string;
|
||||
selectPlaceholderText?: string;
|
||||
onRoll: () => void;
|
||||
onChange?: (newValue: string | null) => void;
|
||||
}
|
||||
const DiceRollActionNode: React.FunctionComponent<DiceRollActionNodeProps> = ({
|
||||
rollKey,
|
||||
rollStatus,
|
||||
rollTotal,
|
||||
assignedValue,
|
||||
options,
|
||||
onRoll,
|
||||
onChange,
|
||||
rollButtonText = "Roll",
|
||||
selectPlaceholderText = "--",
|
||||
}) => {
|
||||
if (options?.length && rollTotal !== null) {
|
||||
return (
|
||||
<Select
|
||||
id={rollKey}
|
||||
value={assignedValue}
|
||||
options={options}
|
||||
placeholder={selectPlaceholderText}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="large"
|
||||
// style='outline'
|
||||
onClick={onRoll}
|
||||
>
|
||||
{rollButtonText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
export default React.memo(DiceRollActionNode);
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { orderBy } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
RollValueContract,
|
||||
Constants,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
//eventually use RollValueContract.dieType to display dice svgs
|
||||
export interface DiceRollValuesProps {
|
||||
rollValues: Array<RollValueContract>;
|
||||
}
|
||||
const DiceRollValues: React.FunctionComponent<DiceRollValuesProps> = ({
|
||||
rollValues,
|
||||
}) => {
|
||||
if (rollValues.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keptDiceValues = rollValues.filter(
|
||||
(valueContract) =>
|
||||
valueContract.excludeType === Constants.DiceRollExcludeTypeEnum.ALL
|
||||
);
|
||||
const discardedValues = rollValues.filter(
|
||||
(valueContract) =>
|
||||
valueContract.excludeType === Constants.DiceRollExcludeTypeEnum.NONE
|
||||
);
|
||||
|
||||
const orderedKeptDiceValues = orderBy(
|
||||
keptDiceValues,
|
||||
(valueContract) => valueContract.value,
|
||||
"desc"
|
||||
);
|
||||
const orderedDiscardedValues = orderBy(
|
||||
discardedValues,
|
||||
(valueContract) => valueContract.value,
|
||||
"desc"
|
||||
);
|
||||
|
||||
const classNames: Array<string> = ["ddbc-dice-roll__dice-value"];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{orderedKeptDiceValues.map((rollValue: RollValueContract, idx) => {
|
||||
return (
|
||||
<div key={idx} className={classNames.join(" ")}>
|
||||
{rollValue.value}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{orderedDiscardedValues.map((rollValue: RollValueContract, idx) => {
|
||||
classNames.push("ddbc-dice-roll__dice-value--discarded");
|
||||
|
||||
return (
|
||||
<div key={idx} className={classNames.join(" ")}>
|
||||
{rollValue.value}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DiceRollValues);
|
||||
@@ -0,0 +1,289 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
|
||||
import {
|
||||
HelperUtils,
|
||||
HtmlSelectOption,
|
||||
RollResultContract,
|
||||
RollValueContract,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
import { Dice, RollRequest } from "@dndbeyond/dice";
|
||||
|
||||
import { DataLoadingStatusEnum } from "../componentConstants";
|
||||
import { Button, RemoveButton } from "../legacy";
|
||||
import { TypeScriptUtils, DiceComponentUtils } from "../utils";
|
||||
import { DiceRoll, DiceRollProps } from "./DiceRoll";
|
||||
|
||||
export interface DiceRollGroupProps {
|
||||
className: string;
|
||||
componentKey: string;
|
||||
diceRolls: Array<RollResultContract>;
|
||||
exclusiveOptions?: boolean;
|
||||
confirmButtonText?: string;
|
||||
onConfirm?: (
|
||||
groupKey: string,
|
||||
rollResults: Array<RollResultContract>
|
||||
) => void;
|
||||
onUpdateGroup?: (
|
||||
groupKey: string,
|
||||
rollResults: Array<RollResultContract>
|
||||
) => void;
|
||||
onRollError?: (errorMessage: string) => void;
|
||||
groupKey: string;
|
||||
nextGroupKey: string | null;
|
||||
showRemoveButton?: boolean;
|
||||
onRemoveGroup?: (groupKey: string, nextGroupKey: string | null) => void;
|
||||
onResetGroup?: (
|
||||
groupKey: string,
|
||||
rollResults: Array<RollResultContract>
|
||||
) => void;
|
||||
onUpdateDiceRoll: (
|
||||
rollKey: string,
|
||||
properties: Omit<Partial<RollResultContract>, "rollKey">,
|
||||
nextRollKey: string | null
|
||||
) => void;
|
||||
onSetRollStatus: (rollKey: string, status: DataLoadingStatusEnum) => void;
|
||||
rollStatusLookup: Record<string, DataLoadingStatusEnum>;
|
||||
options?: DiceRollProps["options"];
|
||||
diceRollRequest:
|
||||
| DiceRollProps["diceRollRequest"]
|
||||
| Array<DiceRollProps["diceRollRequest"]>;
|
||||
}
|
||||
const DiceRollGroup: React.FunctionComponent<DiceRollGroupProps> = ({
|
||||
className = "",
|
||||
componentKey,
|
||||
diceRolls,
|
||||
options,
|
||||
confirmButtonText = "Confirm",
|
||||
showRemoveButton = false,
|
||||
exclusiveOptions = false,
|
||||
diceRollRequest,
|
||||
groupKey,
|
||||
nextGroupKey,
|
||||
onUpdateGroup,
|
||||
onRemoveGroup,
|
||||
onRollError,
|
||||
onConfirm,
|
||||
onResetGroup,
|
||||
onSetRollStatus,
|
||||
onUpdateDiceRoll,
|
||||
rollStatusLookup,
|
||||
}) => {
|
||||
const classNames = useMemo<Array<string>>(
|
||||
() => ["ddbc-dice-roll-group", className],
|
||||
[className]
|
||||
);
|
||||
|
||||
const areRollsEmpty = useMemo<boolean>(
|
||||
() => !diceRolls.some((roll) => roll.rollTotal !== null),
|
||||
[diceRolls]
|
||||
);
|
||||
|
||||
const areScoresUnassigned = useMemo<boolean>(
|
||||
() =>
|
||||
!diceRolls.some(
|
||||
(roll) => roll.assignedValue !== null && roll.assignedValue !== ""
|
||||
),
|
||||
[diceRolls]
|
||||
);
|
||||
|
||||
const assignedValues = useMemo<Array<string>>(() => {
|
||||
return diceRolls
|
||||
.map((roll) => roll.assignedValue)
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||
}, [diceRolls]);
|
||||
|
||||
const generatedOptions = useMemo<Array<Array<HtmlSelectOption>>>(() => {
|
||||
if (!options) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return diceRolls.map((roll): Array<HtmlSelectOption> => {
|
||||
if (!exclusiveOptions) {
|
||||
return options;
|
||||
}
|
||||
|
||||
return options.filter(
|
||||
(option) =>
|
||||
roll.assignedValue === option.value ||
|
||||
!assignedValues.includes(option.value.toString())
|
||||
);
|
||||
});
|
||||
}, [options, diceRolls, exclusiveOptions, assignedValues]);
|
||||
|
||||
const generatedRollRequests = useMemo<Array<RollRequest>>(() => {
|
||||
let requests: Array<RollRequest> = [];
|
||||
for (let i = 0; i < diceRolls.length; i++) {
|
||||
if (!Array.isArray(diceRollRequest)) {
|
||||
requests.push(diceRollRequest);
|
||||
} else {
|
||||
if (diceRollRequest[i]) {
|
||||
requests.push(diceRollRequest[i]);
|
||||
} else {
|
||||
requests.push(diceRollRequest[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return requests;
|
||||
}, [diceRollRequest, diceRolls]);
|
||||
|
||||
const handleRoll = useCallback(
|
||||
(
|
||||
rollKey: string,
|
||||
diceRollRequest: RollRequest,
|
||||
nextRollKey: string | null
|
||||
): void => {
|
||||
onSetRollStatus(rollKey, DataLoadingStatusEnum.LOADING);
|
||||
|
||||
Dice.roll(diceRollRequest)
|
||||
.then((result) => {
|
||||
//RollResult | undefined - RollResult is not exported from Dice;
|
||||
// only expect to have one single rollRequest.
|
||||
const rollResult = result.rolls[0].result ?? null;
|
||||
|
||||
if (!rollResult) {
|
||||
throw new Error("error in rollResult");
|
||||
}
|
||||
|
||||
const rollValues =
|
||||
DiceComponentUtils.generateRollValueContracts(result);
|
||||
|
||||
if (!rollValues) {
|
||||
throw new Error("error in rollResult values");
|
||||
}
|
||||
|
||||
//update status
|
||||
onSetRollStatus(rollKey, DataLoadingStatusEnum.LOADED);
|
||||
|
||||
//update diceRoll
|
||||
onUpdateDiceRoll(
|
||||
rollKey,
|
||||
{ rollValues, rollTotal: rollResult.total },
|
||||
nextRollKey
|
||||
);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
//this could handle different errors types from dice package or thrown errors from result
|
||||
|
||||
onSetRollStatus(rollKey, DataLoadingStatusEnum.NOT_LOADED);
|
||||
|
||||
if (onRollError) {
|
||||
onRollError(error.message);
|
||||
}
|
||||
console.log("roll error", error);
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(
|
||||
rollKey: string,
|
||||
newValue: string | null,
|
||||
prevValue: string | null,
|
||||
nextRollKey: string | null,
|
||||
rollTotal: number | null,
|
||||
rollValues: Array<RollValueContract>
|
||||
): void => {
|
||||
if (newValue === prevValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdateDiceRoll(
|
||||
rollKey,
|
||||
{ assignedValue: newValue, rollTotal, rollValues },
|
||||
nextRollKey
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleConfirm = useCallback((): void => {
|
||||
if (onConfirm) {
|
||||
return onConfirm(groupKey, diceRolls);
|
||||
}
|
||||
}, [onConfirm, groupKey, diceRolls]);
|
||||
|
||||
const handleResetGroup = useCallback((): void => {
|
||||
if (onResetGroup) {
|
||||
onResetGroup(groupKey, diceRolls);
|
||||
}
|
||||
|
||||
diceRolls.forEach((roll) => {
|
||||
onSetRollStatus(roll.rollKey, DataLoadingStatusEnum.NOT_INITIALIZED);
|
||||
});
|
||||
}, [onResetGroup, groupKey]);
|
||||
|
||||
const handleRemoveGroup = useCallback((): void => {
|
||||
if (onRemoveGroup) {
|
||||
onRemoveGroup(groupKey, nextGroupKey);
|
||||
}
|
||||
}, [onRemoveGroup, groupKey, nextGroupKey]);
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
<div className="ddbc-dice-roll-group__actions">
|
||||
{showRemoveButton && (
|
||||
<RemoveButton
|
||||
size="small"
|
||||
onClick={handleRemoveGroup}
|
||||
enableConfirm={true}
|
||||
>
|
||||
Delete group
|
||||
</RemoveButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="ddbc-dice-roll-group__rolls">
|
||||
{diceRolls.map((diceRoll, idx) => {
|
||||
const diceRollRequest = generatedRollRequests[idx];
|
||||
if (!diceRollRequest) {
|
||||
return null;
|
||||
}
|
||||
const rollStatus = HelperUtils.lookupDataOrFallback(
|
||||
rollStatusLookup,
|
||||
diceRoll.rollKey,
|
||||
DataLoadingStatusEnum.NOT_INITIALIZED
|
||||
);
|
||||
|
||||
return (
|
||||
<DiceRoll
|
||||
key={diceRoll.rollKey}
|
||||
rollKey={diceRoll.rollKey}
|
||||
nextRollKey={diceRoll.nextRollKey}
|
||||
rollTotal={diceRoll.rollTotal}
|
||||
rollValues={diceRoll.rollValues}
|
||||
assignedValue={diceRoll.assignedValue}
|
||||
options={generatedOptions[idx]}
|
||||
onChange={handleChange}
|
||||
onRoll={handleRoll}
|
||||
diceRollRequest={diceRollRequest}
|
||||
rollStatus={rollStatus}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="ddbc-dice-roll-group__actions">
|
||||
<RemoveButton
|
||||
size="medium"
|
||||
style="filled"
|
||||
onClick={handleResetGroup}
|
||||
disabled={areRollsEmpty}
|
||||
enableConfirm={true}
|
||||
>
|
||||
Reset Group
|
||||
</RemoveButton>
|
||||
{onConfirm && (
|
||||
<Button
|
||||
size="medium"
|
||||
onClick={handleConfirm}
|
||||
disabled={areScoresUnassigned}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DiceRollGroup);
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
|
||||
import { RollGroupContract } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DiceRollGroup, DiceRollGroupProps } from "../DiceRollGroup";
|
||||
import { Button } from "../legacy/Button";
|
||||
|
||||
export interface DiceRollGroupManagerProps {
|
||||
groups: Array<RollGroupContract>;
|
||||
showAddGroupButton?: boolean;
|
||||
onAddGroup?: () => void;
|
||||
componentKey: DiceRollGroupProps["componentKey"];
|
||||
options?: DiceRollGroupProps["options"];
|
||||
exclusiveOptions?: DiceRollGroupProps["exclusiveOptions"];
|
||||
diceRollRequest: DiceRollGroupProps["diceRollRequest"];
|
||||
onConfirm?: DiceRollGroupProps["onConfirm"];
|
||||
confirmButtonText?: DiceRollGroupProps["confirmButtonText"];
|
||||
onRollError?: DiceRollGroupProps["onRollError"];
|
||||
onRemoveGroup?: DiceRollGroupProps["onRemoveGroup"];
|
||||
onUpdateGroup?: DiceRollGroupProps["onUpdateGroup"];
|
||||
onResetGroup: DiceRollGroupProps["onResetGroup"];
|
||||
onSetRollStatus: DiceRollGroupProps["onSetRollStatus"];
|
||||
rollStatusLookup: DiceRollGroupProps["rollStatusLookup"];
|
||||
onUpdateDiceRoll: DiceRollGroupProps["onUpdateDiceRoll"];
|
||||
}
|
||||
const DiceRollGroupManager: React.FunctionComponent<
|
||||
DiceRollGroupManagerProps
|
||||
> = ({
|
||||
componentKey,
|
||||
groups,
|
||||
options,
|
||||
diceRollRequest,
|
||||
onConfirm,
|
||||
confirmButtonText,
|
||||
onRollError,
|
||||
showAddGroupButton = false,
|
||||
onUpdateGroup,
|
||||
onResetGroup,
|
||||
onAddGroup,
|
||||
onRemoveGroup,
|
||||
exclusiveOptions,
|
||||
onSetRollStatus,
|
||||
rollStatusLookup,
|
||||
onUpdateDiceRoll,
|
||||
}) => {
|
||||
return (
|
||||
<div className="ddbc-dice-roll-group-manager">
|
||||
<div className="ddbc-dice-roll-group-manager__actions">
|
||||
{showAddGroupButton && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={onAddGroup}
|
||||
disabled={groups.length >= 10}
|
||||
>
|
||||
+Add Group
|
||||
</Button>
|
||||
)}
|
||||
<span className="ddbc-dice-roll-group-manager__actions-label">{`Groups: ${groups.length}`}</span>
|
||||
</div>
|
||||
<div className="ddbc-dice-roll-group-manager__groups">
|
||||
{groups.map((group, idx) => {
|
||||
return (
|
||||
<DiceRollGroup
|
||||
className="ddbc-dice-roll-group-manager__group"
|
||||
key={group.groupKey}
|
||||
componentKey={componentKey}
|
||||
groupKey={group.groupKey}
|
||||
nextGroupKey={group.nextGroupKey}
|
||||
options={options}
|
||||
exclusiveOptions={exclusiveOptions}
|
||||
confirmButtonText={confirmButtonText}
|
||||
onConfirm={onConfirm}
|
||||
onUpdateGroup={onUpdateGroup}
|
||||
onRollError={onRollError}
|
||||
showRemoveButton={groups.length > 1}
|
||||
onRemoveGroup={onRemoveGroup}
|
||||
onResetGroup={onResetGroup}
|
||||
diceRollRequest={diceRollRequest}
|
||||
diceRolls={group.rollResults}
|
||||
onSetRollStatus={onSetRollStatus}
|
||||
rollStatusLookup={rollStatusLookup}
|
||||
onUpdateDiceRoll={onUpdateDiceRoll}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DiceRollGroupManager);
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import Checkbox from "../Checkbox";
|
||||
|
||||
interface Props {
|
||||
choices: Array<string>;
|
||||
onSelection?: (slotIdx: number) => void;
|
||||
activeChoice: number | null;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class ExclusiveCheckbox extends React.PureComponent<Props, {}> {
|
||||
handleSelection = (slotIdx: number): void => {
|
||||
const { onSelection } = this.props;
|
||||
|
||||
if (onSelection) {
|
||||
onSelection(slotIdx);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { activeChoice, choices, theme } = this.props;
|
||||
|
||||
const parentClassNames: Array<string> = ["ddbc-exclusive-checkbox"];
|
||||
if (theme?.isDarkMode) {
|
||||
parentClassNames.push("ddbc-exclusive-checkbox--dark-mode");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={parentClassNames.join(" ")}>
|
||||
{choices.map((choice, slotIdx) => {
|
||||
const classNames: Array<string> = ["ddbc-exclusive-checkbox__slot"];
|
||||
|
||||
if (activeChoice === null) {
|
||||
classNames.push("ddbc-exclusive-checkbox__slot--active");
|
||||
} else {
|
||||
if (activeChoice === slotIdx) {
|
||||
classNames.push("ddbc-exclusive-checkbox__slot--selected");
|
||||
} else {
|
||||
classNames.push("ddbc-exclusive-checkbox__slot--inactive");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={slotIdx} className={classNames.join(" ")}>
|
||||
<Checkbox
|
||||
label={choice}
|
||||
enabled={activeChoice === slotIdx}
|
||||
onChange={this.handleSelection.bind(this, slotIdx)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Constants } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
DarkCharismaSvg,
|
||||
DarkConstitutionSvg,
|
||||
DarkDexteritySvg,
|
||||
DarkIntelligenceSvg,
|
||||
DarkStrengthSvg,
|
||||
DarkWisdomSvg,
|
||||
LightCharismaSvg,
|
||||
LightConstitutionSvg,
|
||||
LightDexteritySvg,
|
||||
LightIntelligenceSvg,
|
||||
LightStrengthSvg,
|
||||
LightWisdomSvg,
|
||||
} from "../../Svg";
|
||||
|
||||
//TODO should dynamically create props based on which component and expected props, for now this is duplicated from InjectedSvgProps hocTypings.ts
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
statId: Constants.AbilityStatEnum;
|
||||
themeMode?: "dark" | "light";
|
||||
className: string;
|
||||
}
|
||||
export default class AbilityIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
getDarkSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
const { statId } = this.props;
|
||||
|
||||
switch (statId) {
|
||||
case Constants.AbilityStatEnum.CHARISMA:
|
||||
return DarkCharismaSvg;
|
||||
case Constants.AbilityStatEnum.CONSTITUTION:
|
||||
return DarkConstitutionSvg;
|
||||
case Constants.AbilityStatEnum.DEXTERITY:
|
||||
return DarkDexteritySvg;
|
||||
case Constants.AbilityStatEnum.INTELLIGENCE:
|
||||
return DarkIntelligenceSvg;
|
||||
case Constants.AbilityStatEnum.STRENGTH:
|
||||
return DarkStrengthSvg;
|
||||
case Constants.AbilityStatEnum.WISDOM:
|
||||
return DarkWisdomSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
getLightSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
const { statId } = this.props;
|
||||
|
||||
switch (statId) {
|
||||
case Constants.AbilityStatEnum.CHARISMA:
|
||||
return LightCharismaSvg;
|
||||
case Constants.AbilityStatEnum.CONSTITUTION:
|
||||
return LightConstitutionSvg;
|
||||
case Constants.AbilityStatEnum.DEXTERITY:
|
||||
return LightDexteritySvg;
|
||||
case Constants.AbilityStatEnum.INTELLIGENCE:
|
||||
return LightIntelligenceSvg;
|
||||
case Constants.AbilityStatEnum.STRENGTH:
|
||||
return LightStrengthSvg;
|
||||
case Constants.AbilityStatEnum.WISDOM:
|
||||
return LightWisdomSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { themeMode, className } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-ability-icon"];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (themeMode) {
|
||||
case "light":
|
||||
SvgIcon = this.getLightSvg();
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
SvgIcon = this.getDarkSvg();
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SvgIcon className={classNames.join(" ")} />;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { AdvantageDisadvantageSvg } from "../../Svg";
|
||||
import {
|
||||
SvgConstantDarkModePositiveTheme,
|
||||
SvgConstantDarkModeNegativeTheme,
|
||||
SvgConstantPositiveTheme,
|
||||
SvgConstantNegativeTheme,
|
||||
} from "../../componentConstants";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
className: string;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class AdvantageDisadvantageIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
title: "",
|
||||
className: "",
|
||||
theme: {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [
|
||||
className,
|
||||
"ddbc-advantage-disadvantage-icon",
|
||||
];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={title}
|
||||
className={classNames.join(" ")}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
<AdvantageDisadvantageSvg
|
||||
fillColor={
|
||||
theme.isDarkMode
|
||||
? SvgConstantDarkModePositiveTheme.fill
|
||||
: SvgConstantPositiveTheme.fill
|
||||
}
|
||||
secondaryFillColor={
|
||||
theme.isDarkMode
|
||||
? SvgConstantDarkModeNegativeTheme.fill
|
||||
: SvgConstantNegativeTheme.fill
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import AdvantageDisadvantageIcon from "./AdvantageDisadvantageIcon";
|
||||
|
||||
export default AdvantageDisadvantageIcon;
|
||||
export { AdvantageDisadvantageIcon };
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DarkModePositiveAdvantageSvg, PositiveAdvantageSvg } from "../../Svg";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
secondaryFill?: string;
|
||||
}
|
||||
export default class AdvantageIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
title: "Advantage",
|
||||
className: "",
|
||||
theme: {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-advantage-icon"];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={title}
|
||||
className={classNames.join(" ")}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
<span aria-label="Advantage">
|
||||
{theme?.isDarkMode ? (
|
||||
<DarkModePositiveAdvantageSvg />
|
||||
) : (
|
||||
<PositiveAdvantageSvg />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
DarkConeSvg,
|
||||
DarkCubeSvg,
|
||||
DarkCylinderSvg,
|
||||
DarkLineSvg,
|
||||
DarkSphereSvg,
|
||||
DarkSquareSvg,
|
||||
GrayConeSvg,
|
||||
GrayCubeSvg,
|
||||
GrayCylinderSvg,
|
||||
GrayLineSvg,
|
||||
GraySphereSvg,
|
||||
GraySquareSvg,
|
||||
LightConeSvg,
|
||||
LightCubeSvg,
|
||||
LightCylinderSvg,
|
||||
LightLineSvg,
|
||||
LightSphereSvg,
|
||||
LightSquareSvg,
|
||||
} from "../../Svg";
|
||||
import { AoeTypePropType } from "../IconConstants";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
type?: AoeTypePropType;
|
||||
themeMode?: "dark" | "gray" | "light";
|
||||
className?: string;
|
||||
}
|
||||
const AoeTypeIcon: React.FunctionComponent<Props> = ({
|
||||
className = "",
|
||||
themeMode = "dark",
|
||||
type,
|
||||
}) => {
|
||||
const DarkSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (type) {
|
||||
case "cone":
|
||||
return DarkConeSvg;
|
||||
case "cube":
|
||||
return DarkCubeSvg;
|
||||
case "cylinder":
|
||||
return DarkCylinderSvg;
|
||||
case "line":
|
||||
return DarkLineSvg;
|
||||
case "sphere":
|
||||
case "emanation":
|
||||
return DarkSphereSvg;
|
||||
case "square":
|
||||
return DarkSquareSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const GraySvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (type) {
|
||||
case "cone":
|
||||
return GrayConeSvg;
|
||||
case "cube":
|
||||
return GrayCubeSvg;
|
||||
case "cylinder":
|
||||
return GrayCylinderSvg;
|
||||
case "line":
|
||||
return GrayLineSvg;
|
||||
case "sphere":
|
||||
case "emanation":
|
||||
return GraySphereSvg;
|
||||
case "square":
|
||||
return GraySquareSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const LightSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (type) {
|
||||
case "cone":
|
||||
return LightConeSvg;
|
||||
case "cube":
|
||||
return LightCubeSvg;
|
||||
case "cylinder":
|
||||
return LightCylinderSvg;
|
||||
case "line":
|
||||
return LightLineSvg;
|
||||
case "sphere":
|
||||
case "emanation":
|
||||
return LightSphereSvg;
|
||||
case "square":
|
||||
return LightSquareSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
let classNames: Array<string> = [
|
||||
className,
|
||||
"ddbc-aoe-type-icon",
|
||||
`ddbc-aoe-type-icon--${type}`,
|
||||
];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (themeMode) {
|
||||
case "light":
|
||||
SvgIcon = LightSvg();
|
||||
break;
|
||||
case "gray":
|
||||
SvgIcon = GraySvg();
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
SvgIcon = DarkSvg();
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SvgIcon className={classNames.join(" ")} />;
|
||||
};
|
||||
|
||||
export default AoeTypeIcon;
|
||||
@@ -0,0 +1,161 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Constants } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
DarkMeleeSpellSvg,
|
||||
DarkMeleeWeaponSvg,
|
||||
DarkRangedSpellSvg,
|
||||
DarkRangedWeaponSvg,
|
||||
DarkThrownSvg,
|
||||
DarkUnarmedStrikeSvg,
|
||||
DarkWeaponSpellDamageSvg,
|
||||
GrayMeleeSpellSvg,
|
||||
GrayMeleeWeaponSvg,
|
||||
GrayRangedSpellSvg,
|
||||
GrayRangedWeaponSvg,
|
||||
GrayThrownSvg,
|
||||
GrayUnarmedStrikeSvg,
|
||||
GrayWeaponSpellDamageSvg,
|
||||
LightMeleeSpellSvg,
|
||||
LightMeleeWeaponSvg,
|
||||
LightRangedSpellSvg,
|
||||
LightRangedWeaponSvg,
|
||||
LightThrownSvg,
|
||||
LightUnarmedStrikeSvg,
|
||||
LightWeaponSpellDamageSvg,
|
||||
} from "../../Svg";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
actionType: Constants.ActionTypeEnum | null;
|
||||
rangeType: Constants.AttackTypeRangeEnum;
|
||||
themeMode?: "dark" | "gray" | "light";
|
||||
className?: string;
|
||||
overrideType?: "weapon-spell" | null;
|
||||
}
|
||||
const AttackTypeIcon: React.FunctionComponent<Props> = ({
|
||||
className = "",
|
||||
themeMode = "dark",
|
||||
actionType,
|
||||
rangeType,
|
||||
overrideType,
|
||||
}) => {
|
||||
const DarkSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (true) {
|
||||
case actionType === Constants.ActionTypeEnum.GENERAL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return DarkUnarmedStrikeSvg;
|
||||
case actionType === Constants.ActionTypeEnum.GENERAL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return DarkThrownSvg;
|
||||
case actionType === Constants.ActionTypeEnum.SPELL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return DarkMeleeSpellSvg;
|
||||
case actionType === Constants.ActionTypeEnum.SPELL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return DarkRangedSpellSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
overrideType === "weapon-spell":
|
||||
return DarkWeaponSpellDamageSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return DarkMeleeWeaponSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return DarkRangedWeaponSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const GraySvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (true) {
|
||||
case actionType === Constants.ActionTypeEnum.GENERAL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return GrayUnarmedStrikeSvg;
|
||||
case actionType === Constants.ActionTypeEnum.GENERAL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return GrayThrownSvg;
|
||||
case actionType === Constants.ActionTypeEnum.SPELL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return GrayMeleeSpellSvg;
|
||||
case actionType === Constants.ActionTypeEnum.SPELL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return GrayRangedSpellSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
overrideType === "weapon-spell":
|
||||
return GrayWeaponSpellDamageSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return GrayMeleeWeaponSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return GrayRangedWeaponSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const LightSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (true) {
|
||||
case actionType === Constants.ActionTypeEnum.GENERAL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return LightUnarmedStrikeSvg;
|
||||
case actionType === Constants.ActionTypeEnum.GENERAL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return LightThrownSvg;
|
||||
case actionType === Constants.ActionTypeEnum.SPELL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return LightMeleeSpellSvg;
|
||||
case actionType === Constants.ActionTypeEnum.SPELL &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return LightRangedSpellSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
overrideType === "weapon-spell":
|
||||
return LightWeaponSpellDamageSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.MELEE:
|
||||
return LightMeleeWeaponSvg;
|
||||
case actionType === Constants.ActionTypeEnum.WEAPON &&
|
||||
rangeType === Constants.AttackTypeRangeEnum.RANGED:
|
||||
return LightRangedWeaponSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
let classNames: Array<string> = [
|
||||
className,
|
||||
"ddbc-attack-type-icon",
|
||||
`ddbc-attack-type-icon--${actionType}-${rangeType}`,
|
||||
];
|
||||
|
||||
if (overrideType) {
|
||||
classNames.push(`ddbc-attack-type-icon--${overrideType}`);
|
||||
}
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (themeMode) {
|
||||
case "light":
|
||||
SvgIcon = LightSvg();
|
||||
break;
|
||||
case "gray":
|
||||
SvgIcon = GraySvg();
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
SvgIcon = DarkSvg();
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SvgIcon className={classNames.join(" ")} />;
|
||||
};
|
||||
|
||||
export default AttackTypeIcon;
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Tooltip } from "~/components/Tooltip";
|
||||
import { useCharacterTheme } from "~/contexts/CharacterTheme";
|
||||
|
||||
import { DarkAttunementSvg, GrayAttunementSvg } from "../../Svg";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
className?: string;
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
const AttunementIcon: React.FunctionComponent<Props> = ({
|
||||
className = "",
|
||||
showTooltip = true,
|
||||
}) => {
|
||||
const { isDarkMode } = useCharacterTheme();
|
||||
let classNames: Array<string> = [className, "ddbc-attunement-icon"];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> = isDarkMode
|
||||
? GrayAttunementSvg
|
||||
: DarkAttunementSvg;
|
||||
|
||||
const tooltipId = `attunmentIcon-${uuidv4()}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={classNames.join(" ")}
|
||||
data-tooltip-id={tooltipId}
|
||||
data-tooltip-content="Item is Attuned"
|
||||
>
|
||||
<SvgIcon className="ddbc-attunement-icon__icon" />
|
||||
</span>
|
||||
{showTooltip && <Tooltip id={tooltipId} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttunementIcon;
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Constants } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
GoldCoinSvg,
|
||||
SilverCoinSvg,
|
||||
CopperCoinSvg,
|
||||
ElectrumCoinSvg,
|
||||
PlatinumCoinSvg,
|
||||
} from "../../Svg";
|
||||
|
||||
//TODO should dynamically create props based on which component and expected props, for now this is duplicated from InjectedSvgProps hocTypings.ts
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
coinType: Constants.CoinTypeEnum;
|
||||
className: string;
|
||||
}
|
||||
export default class AbilityIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, coinType } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-ability-icon"];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (coinType) {
|
||||
case Constants.CoinTypeEnum.cp:
|
||||
SvgIcon = CopperCoinSvg;
|
||||
break;
|
||||
case Constants.CoinTypeEnum.ep:
|
||||
SvgIcon = ElectrumCoinSvg;
|
||||
break;
|
||||
case Constants.CoinTypeEnum.gp:
|
||||
SvgIcon = GoldCoinSvg;
|
||||
break;
|
||||
case Constants.CoinTypeEnum.pp:
|
||||
SvgIcon = PlatinumCoinSvg;
|
||||
break;
|
||||
case Constants.CoinTypeEnum.sp:
|
||||
SvgIcon = SilverCoinSvg;
|
||||
break;
|
||||
default:
|
||||
SvgIcon = null;
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SvgIcon className={classNames.join(" ")} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import CoinIcon from "./CoinIcon";
|
||||
|
||||
export default CoinIcon;
|
||||
export { CoinIcon };
|
||||
@@ -0,0 +1,60 @@
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Tooltip } from "~/components/Tooltip";
|
||||
|
||||
import {
|
||||
DarkConcentrationSvg,
|
||||
GrayConcentrationSvg,
|
||||
LightConcentrationSvg,
|
||||
} from "../../Svg";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
themeMode?: "dark" | "gray" | "light";
|
||||
className: string;
|
||||
}
|
||||
export default class ConcentrationIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
themeMode: "dark",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { themeMode, className, ...props } = this.props;
|
||||
const id = uuidv4();
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (themeMode) {
|
||||
case "light":
|
||||
SvgIcon = LightConcentrationSvg;
|
||||
break;
|
||||
case "gray":
|
||||
SvgIcon = GrayConcentrationSvg;
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
SvgIcon = DarkConcentrationSvg;
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx([styles.concentrationIcon, className])}
|
||||
data-tooltip-id={id}
|
||||
data-tooltip-content="Concentration"
|
||||
>
|
||||
<SvgIcon className={styles.svg} {...props} />
|
||||
<Tooltip id={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ConcentrationIcon from "./ConcentrationIcon";
|
||||
|
||||
export default ConcentrationIcon;
|
||||
export { ConcentrationIcon };
|
||||
@@ -0,0 +1,129 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Constants } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
DarkBlindedSvg,
|
||||
LightBlindedSvg,
|
||||
DarkCharmedSvg,
|
||||
LightCharmedSvg,
|
||||
DarkDeafenedSvg,
|
||||
LightDeafenedSvg,
|
||||
LightExhaustedSvg,
|
||||
DarkExhaustedSvg,
|
||||
LightFrightenedSvg,
|
||||
DarkFrightenedSvg,
|
||||
LightGrappledSvg,
|
||||
DarkGrappledSvg,
|
||||
DarkIncapacitatedSvg,
|
||||
LightIncapacitatedSvg,
|
||||
LightInvisibleSvg,
|
||||
DarkInvisibleSvg,
|
||||
DarkParalyzedSvg,
|
||||
LightParalyzedSvg,
|
||||
DarkPetrifiedSvg,
|
||||
LightPetrifiedSvg,
|
||||
LightPoisonedSvg,
|
||||
DarkPoisonedSvg,
|
||||
LightProneSvg,
|
||||
DarkProneSvg,
|
||||
LightRestrainedSvg,
|
||||
DarkRestrainedSvg,
|
||||
LightStunnedSvg,
|
||||
DarkStunnedSvg,
|
||||
LightUnconsciousSvg,
|
||||
DarkUnconsciousSvg,
|
||||
} from "../../Svg";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
conditionType: Constants.ConditionIdEnum;
|
||||
className: string;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
[Constants.ConditionIdEnum.BLINDED]: {
|
||||
light: LightBlindedSvg,
|
||||
dark: DarkBlindedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.CHARMED]: {
|
||||
light: LightCharmedSvg,
|
||||
dark: DarkCharmedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.DEAFENED]: {
|
||||
light: LightDeafenedSvg,
|
||||
dark: DarkDeafenedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.EXHAUSTION]: {
|
||||
light: LightExhaustedSvg,
|
||||
dark: DarkExhaustedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.FRIGHTENED]: {
|
||||
light: LightFrightenedSvg,
|
||||
dark: DarkFrightenedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.GRAPPLED]: {
|
||||
light: LightGrappledSvg,
|
||||
dark: DarkGrappledSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.INCAPACITATED]: {
|
||||
light: LightIncapacitatedSvg,
|
||||
dark: DarkIncapacitatedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.INVISIBLE]: {
|
||||
light: LightInvisibleSvg,
|
||||
dark: DarkInvisibleSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.PARALYZED]: {
|
||||
light: LightParalyzedSvg,
|
||||
dark: DarkParalyzedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.PETRIFIED]: {
|
||||
light: LightPetrifiedSvg,
|
||||
dark: DarkPetrifiedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.POISONED]: {
|
||||
light: LightPoisonedSvg,
|
||||
dark: DarkPoisonedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.PRONE]: {
|
||||
light: LightProneSvg,
|
||||
dark: DarkProneSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.RESTRAINED]: {
|
||||
light: LightRestrainedSvg,
|
||||
dark: DarkRestrainedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.STUNNED]: {
|
||||
light: LightStunnedSvg,
|
||||
dark: DarkStunnedSvg,
|
||||
},
|
||||
[Constants.ConditionIdEnum.UNCONSCIOUS]: {
|
||||
light: LightUnconsciousSvg,
|
||||
dark: DarkUnconsciousSvg,
|
||||
},
|
||||
};
|
||||
|
||||
export default class ConditionIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, conditionType, isDarkMode } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-condition-icon"];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null =
|
||||
iconMap[conditionType]?.[isDarkMode ? "light" : "dark"] ?? null;
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SvgIcon className={classNames.join(" ")} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ConditionIcon from "./ConditionIcon";
|
||||
|
||||
export default ConditionIcon;
|
||||
export { ConditionIcon };
|
||||
@@ -0,0 +1,204 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
DarkAcidSvg,
|
||||
GrayAcidSvg,
|
||||
LightAcidSvg,
|
||||
DarkBludgeoningSvg,
|
||||
GrayBludgeoningSvg,
|
||||
LightBludgeoningSvg,
|
||||
DarkColdSvg,
|
||||
GrayColdSvg,
|
||||
LightColdSvg,
|
||||
DarkFireSvg,
|
||||
GrayFireSvg,
|
||||
LightFireSvg,
|
||||
DarkForceSvg,
|
||||
GrayForceSvg,
|
||||
LightForceSvg,
|
||||
DarkLightningSvg,
|
||||
GrayLightningSvg,
|
||||
LightLightningSvg,
|
||||
DarkNecroticSvg,
|
||||
GrayNecroticSvg,
|
||||
LightNecroticSvg,
|
||||
DarkPiercingSvg,
|
||||
GrayPiercingSvg,
|
||||
LightPiercingSvg,
|
||||
DarkPoisonSvg,
|
||||
GrayPoisonSvg,
|
||||
LightPoisonSvg,
|
||||
DarkPsychicSvg,
|
||||
GrayPsychicSvg,
|
||||
LightPsychicSvg,
|
||||
DarkRadiantSvg,
|
||||
GrayRadiantSvg,
|
||||
LightRadiantSvg,
|
||||
DarkSlashingSvg,
|
||||
GraySlashingSvg,
|
||||
LightSlashingSvg,
|
||||
DarkThunderSvg,
|
||||
GrayThunderSvg,
|
||||
LightThunderSvg,
|
||||
} from "../../Svg";
|
||||
import { DamageTypePropType } from "../IconConstants";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
type: DamageTypePropType;
|
||||
showTooltip?: boolean;
|
||||
className?: string;
|
||||
theme?: CharacterTheme;
|
||||
themeMode?: "dark" | "gray" | "light";
|
||||
}
|
||||
const DamageTypeIcon: React.FunctionComponent<Props> = ({
|
||||
showTooltip = true,
|
||||
className = "",
|
||||
type,
|
||||
theme,
|
||||
themeMode = theme?.isDarkMode ? "gray" : "dark",
|
||||
}) => {
|
||||
const DarkSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (type) {
|
||||
case "acid":
|
||||
return DarkAcidSvg;
|
||||
case "bludgeoning":
|
||||
return DarkBludgeoningSvg;
|
||||
case "cold":
|
||||
return DarkColdSvg;
|
||||
case "fire":
|
||||
return DarkFireSvg;
|
||||
case "force":
|
||||
return DarkForceSvg;
|
||||
case "lightning":
|
||||
return DarkLightningSvg;
|
||||
case "necrotic":
|
||||
return DarkNecroticSvg;
|
||||
case "piercing":
|
||||
return DarkPiercingSvg;
|
||||
case "poison":
|
||||
return DarkPoisonSvg;
|
||||
case "psychic":
|
||||
return DarkPsychicSvg;
|
||||
case "radiant":
|
||||
return DarkRadiantSvg;
|
||||
case "slashing":
|
||||
return DarkSlashingSvg;
|
||||
case "thunder":
|
||||
return DarkThunderSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const GraySvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (type) {
|
||||
case "acid":
|
||||
return GrayAcidSvg;
|
||||
case "bludgeoning":
|
||||
return GrayBludgeoningSvg;
|
||||
case "cold":
|
||||
return GrayColdSvg;
|
||||
case "fire":
|
||||
return GrayFireSvg;
|
||||
case "force":
|
||||
return GrayForceSvg;
|
||||
case "lightning":
|
||||
return GrayLightningSvg;
|
||||
case "necrotic":
|
||||
return GrayNecroticSvg;
|
||||
case "piercing":
|
||||
return GrayPiercingSvg;
|
||||
case "poison":
|
||||
return GrayPoisonSvg;
|
||||
case "psychic":
|
||||
return GrayPsychicSvg;
|
||||
case "radiant":
|
||||
return GrayRadiantSvg;
|
||||
case "slashing":
|
||||
return GraySlashingSvg;
|
||||
case "thunder":
|
||||
return GrayThunderSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const LightSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (type) {
|
||||
case "acid":
|
||||
return LightAcidSvg;
|
||||
case "bludgeoning":
|
||||
return LightBludgeoningSvg;
|
||||
case "cold":
|
||||
return LightColdSvg;
|
||||
case "fire":
|
||||
return LightFireSvg;
|
||||
case "force":
|
||||
return LightForceSvg;
|
||||
case "lightning":
|
||||
return LightLightningSvg;
|
||||
case "necrotic":
|
||||
return LightNecroticSvg;
|
||||
case "piercing":
|
||||
return LightPiercingSvg;
|
||||
case "poison":
|
||||
return LightPoisonSvg;
|
||||
case "psychic":
|
||||
return LightPsychicSvg;
|
||||
case "radiant":
|
||||
return LightRadiantSvg;
|
||||
case "slashing":
|
||||
return LightSlashingSvg;
|
||||
case "thunder":
|
||||
return LightThunderSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
let classNames: Array<string> = [
|
||||
className,
|
||||
"ddbc-damage-type-icon",
|
||||
`ddbc-damage-type-icon--${type}`,
|
||||
];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (themeMode) {
|
||||
case "light":
|
||||
SvgIcon = LightSvg();
|
||||
break;
|
||||
case "gray":
|
||||
SvgIcon = GraySvg();
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
SvgIcon = DarkSvg();
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")} aria-label={`${type} damage`}>
|
||||
<Tooltip
|
||||
title={type}
|
||||
enabled={showTooltip}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
<SvgIcon
|
||||
className={`ddbc-damage-type-icon__img ddbc-damage-type-icon__img--${type}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DamageTypeIcon;
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
DarkModeNegativeDisadvantageSvg,
|
||||
NegativeDisadvantageSvg,
|
||||
} from "../../Svg";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
export default class DisadvantageIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
title: "Disadvantage",
|
||||
className: "",
|
||||
theme: {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, className, theme } = this.props;
|
||||
|
||||
const classNames: Array<string> = [className, "ddbc-disadvantage-icon"];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={title}
|
||||
className={classNames.join(" ")}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
<span aria-label="Disadvantage">
|
||||
{theme?.isDarkMode ? (
|
||||
<DarkModeNegativeDisadvantageSvg />
|
||||
) : (
|
||||
<NegativeDisadvantageSvg />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DarkHealingSvg, GrayHealingSvg } from "../../Svg";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
className: string;
|
||||
isHp: boolean;
|
||||
isTemp: boolean;
|
||||
showTooltip: boolean;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
export default class HealingIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
isHp: false,
|
||||
isTemp: false,
|
||||
showTooltip: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isHp, isTemp, showTooltip, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-healing-icon"];
|
||||
let tooltips: Array<string> = [];
|
||||
if (isHp) {
|
||||
classNames.push("ddbc-healing-icon--hp");
|
||||
tooltips.push("Healing");
|
||||
}
|
||||
if (isTemp) {
|
||||
classNames.push("ddbc-healing-icon--temp");
|
||||
tooltips.push("Temp HP Healing");
|
||||
}
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> = theme?.isDarkMode
|
||||
? GrayHealingSvg
|
||||
: DarkHealingSvg;
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")} aria-label="healing">
|
||||
<Tooltip
|
||||
title={tooltips.join(", ")}
|
||||
enabled={showTooltip}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
<SvgIcon className="ddbc-healing-icon__icon" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DarkModePositiveImmunitySvg, PositiveImmunitySvg } from "../../Svg";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
secondaryFill?: string;
|
||||
}
|
||||
export default class ImmunityIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
title: "Immunity",
|
||||
className: "",
|
||||
theme: {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-immunity-icon"];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> = theme?.isDarkMode
|
||||
? DarkModePositiveImmunitySvg
|
||||
: PositiveImmunitySvg;
|
||||
|
||||
return (
|
||||
<Tooltip title={title} isDarkMode={theme?.isDarkMode}>
|
||||
<SvgIcon className={classNames.join(" ")} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
tooltip: string;
|
||||
onClick?: () => void;
|
||||
enableTooltip: boolean;
|
||||
showIconOnEdge: boolean;
|
||||
showIcon: boolean;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class ManageIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
tooltip: "Manage",
|
||||
enableTooltip: true,
|
||||
showIconOnEdge: true,
|
||||
showIcon: true,
|
||||
className: "",
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
tooltip,
|
||||
children,
|
||||
onClick,
|
||||
enableTooltip,
|
||||
showIconOnEdge,
|
||||
showIcon,
|
||||
className,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-manage-icon"];
|
||||
if (showIconOnEdge) {
|
||||
classNames.push("ddbc-manage-icon--edge-icon");
|
||||
}
|
||||
if (showIcon) {
|
||||
classNames.push("ddbc-manage-icon--interactive");
|
||||
}
|
||||
if (theme?.isDarkMode) {
|
||||
classNames.push("ddbc-manage-icon--dark-mode");
|
||||
}
|
||||
|
||||
let contentNode: React.ReactNode = (
|
||||
<React.Fragment>
|
||||
{children && (
|
||||
<span className="ddbc-manage-icon__content">{children}</span>
|
||||
)}
|
||||
{showIcon && (
|
||||
<span
|
||||
className={`ddbc-manage-icon__icon ${
|
||||
theme?.isDarkMode ? `ddbc-manage-icon__icon--dark-mode` : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
if (enableTooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltip}
|
||||
className={classNames.join(" ")}
|
||||
onClick={onClick}
|
||||
isInteractive={true}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
{contentNode}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames.join(" ")}
|
||||
onClick={this.handleClick}
|
||||
role="button"
|
||||
aria-label="Manage"
|
||||
>
|
||||
{contentNode}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ModifiedProficiencyHalfSvg, ProficiencyHalfSvg } from "../../../Svg";
|
||||
|
||||
interface Props {
|
||||
isModified: boolean;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class HalfProficiencyIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
isModified: false,
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isModified, className, theme } = this.props;
|
||||
|
||||
let IconComponent: React.ComponentType<any> = isModified
|
||||
? ModifiedProficiencyHalfSvg
|
||||
: ProficiencyHalfSvg;
|
||||
let classNames: Array<string> = ["ddbc-half-proficiency-icon", className];
|
||||
if (isModified) {
|
||||
classNames.push("ddbc-half-proficiency-icon--modified");
|
||||
}
|
||||
let fillColor;
|
||||
if (theme?.isDarkMode) {
|
||||
fillColor = isModified
|
||||
? Constants.CharacterColorEnum.DARKMODE_TEXT
|
||||
: theme?.themeColor;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Half Proficiency" isDarkMode={theme?.isDarkMode}>
|
||||
<IconComponent className={classNames.join(" ")} fillColor={fillColor} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import HalfProficiencyIcon from "./HalfProficiencyIcon";
|
||||
|
||||
export default HalfProficiencyIcon;
|
||||
export { HalfProficiencyIcon };
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
isModified: boolean;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class NoProficiencyIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
isModified: false,
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isModified, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = ["ddbc-no-proficiency-icon", className];
|
||||
if (isModified) {
|
||||
classNames.push("ddbc-no-proficiency-icon--modified");
|
||||
}
|
||||
if (theme?.isDarkMode) {
|
||||
classNames.push("ddbc-no-proficiency-icon--dark-mode");
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Not Proficient" isDarkMode={theme?.isDarkMode}>
|
||||
<span className={classNames.join(" ")} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import NoProficiencyIcon from "./NoProficiencyIcon";
|
||||
|
||||
export default NoProficiencyIcon;
|
||||
export { NoProficiencyIcon };
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ProficiencySvg, ModifiedProficiencySvg } from "../../../Svg";
|
||||
|
||||
interface Props {
|
||||
isModified: boolean;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class ProficiencyIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
isModified: false,
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isModified, className, theme } = this.props;
|
||||
|
||||
let IconComponent: React.ComponentType<any> = isModified
|
||||
? ModifiedProficiencySvg
|
||||
: ProficiencySvg;
|
||||
let classNames: Array<string> = ["ddbc-proficiency-icon", className];
|
||||
|
||||
if (isModified) {
|
||||
classNames.push("ddbc-proficiency-icon--modified");
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Proficiency" isDarkMode={theme?.isDarkMode}>
|
||||
<IconComponent
|
||||
className={classNames.join(" ")}
|
||||
fillColor={
|
||||
theme?.isDarkMode && !isModified
|
||||
? theme.themeColor
|
||||
: theme?.isDarkMode
|
||||
? Constants.CharacterColorEnum.DARKMODE_TEXT
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ProficiencyIcon from "./ProficiencyIcon";
|
||||
|
||||
export default ProficiencyIcon;
|
||||
export { ProficiencyIcon };
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import HalfProficiencyIcon from "./HalfProficiencyIcon";
|
||||
import NoProficiencyIcon from "./NoProficiencyIcon";
|
||||
import ProficiencyIcon from "./ProficiencyIcon";
|
||||
import TwiceProficiencyIcon from "./TwiceProficiencyIcon";
|
||||
|
||||
interface Props {
|
||||
proficiencyLevel: number;
|
||||
isModified: boolean;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
export default class ProficiencyLevelIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
isModified: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { proficiencyLevel, isModified, theme } = this.props;
|
||||
|
||||
let IconComponent: React.ComponentType<any> | null = null;
|
||||
let proficiencyVerbiage: string = "Not Proficient";
|
||||
|
||||
switch (proficiencyLevel) {
|
||||
case Constants.ProficiencyLevelEnum.NONE:
|
||||
IconComponent = NoProficiencyIcon;
|
||||
break;
|
||||
case Constants.ProficiencyLevelEnum.HALF:
|
||||
IconComponent = HalfProficiencyIcon;
|
||||
proficiencyVerbiage = "Half Proficient";
|
||||
break;
|
||||
case Constants.ProficiencyLevelEnum.FULL:
|
||||
IconComponent = ProficiencyIcon;
|
||||
proficiencyVerbiage = "Proficient";
|
||||
break;
|
||||
case Constants.ProficiencyLevelEnum.EXPERT:
|
||||
IconComponent = TwiceProficiencyIcon;
|
||||
proficiencyVerbiage = "Expert";
|
||||
break;
|
||||
default:
|
||||
//not implemented
|
||||
}
|
||||
|
||||
if (IconComponent === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span aria-label={proficiencyVerbiage}>
|
||||
<IconComponent
|
||||
className="ddbc-proficiency-level-icon"
|
||||
isModified={isModified}
|
||||
theme={theme}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
CharacterTheme,
|
||||
Constants,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
ModifiedProficiencyDoubleSvg,
|
||||
ProficiencyDoubleSvg,
|
||||
} from "../../../Svg";
|
||||
|
||||
interface Props {
|
||||
isModified: boolean;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class TwiceProficiencyIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
isModified: false,
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isModified, className, theme } = this.props;
|
||||
|
||||
let IconComponent: React.ComponentType<any> = isModified
|
||||
? ModifiedProficiencyDoubleSvg
|
||||
: ProficiencyDoubleSvg;
|
||||
|
||||
let classNames: Array<string> = ["ddbc-twice-proficiency-icon", className];
|
||||
if (isModified) {
|
||||
classNames.push("ddbc-twice-proficiency-icon--modified");
|
||||
}
|
||||
let secondaryFill;
|
||||
if (theme?.isDarkMode) {
|
||||
secondaryFill = isModified
|
||||
? Constants.CharacterColorEnum.DARKMODE_TEXT
|
||||
: theme?.themeColor;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Expertise" isDarkMode={theme?.isDarkMode}>
|
||||
<span className={classNames.join(" ")}>
|
||||
<IconComponent
|
||||
className="ddbc-twice-proficiency-icon__inner"
|
||||
fillColor={theme?.isDarkMode ? theme.themeColor : undefined}
|
||||
secondaryFillColor={secondaryFill}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import TwiceProficiencyIcon from "./TwiceProficiencyIcon";
|
||||
|
||||
export default TwiceProficiencyIcon;
|
||||
export { TwiceProficiencyIcon };
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
PositiveResistanceSvg,
|
||||
DarkModePositiveResistanceSvg,
|
||||
} from "../../Svg";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
secondaryFill?: string;
|
||||
}
|
||||
export default class ResistanceIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
title: "Resistance",
|
||||
className: "",
|
||||
theme: {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-resistance-icon"];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> = theme?.isDarkMode
|
||||
? DarkModePositiveResistanceSvg
|
||||
: PositiveResistanceSvg;
|
||||
|
||||
return (
|
||||
<Tooltip title={title} isDarkMode={theme?.isDarkMode}>
|
||||
<SvgIcon className={classNames.join(" ")} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ResistanceIcon from "./ResistanceIcon";
|
||||
|
||||
export default ResistanceIcon;
|
||||
export { ResistanceIcon };
|
||||
@@ -0,0 +1,57 @@
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Tooltip } from "~/components/Tooltip";
|
||||
|
||||
import { DarkRitualSvg, GrayRitualSvg, LightRitualSvg } from "../../Svg";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
themeMode?: "dark" | "gray" | "light";
|
||||
className: string;
|
||||
}
|
||||
export default class RitualIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
themeMode: "dark",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { themeMode, className, ...props } = this.props;
|
||||
|
||||
const id = uuidv4();
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (themeMode) {
|
||||
case "light":
|
||||
SvgIcon = LightRitualSvg;
|
||||
break;
|
||||
case "gray":
|
||||
SvgIcon = GrayRitualSvg;
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
SvgIcon = DarkRitualSvg;
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx([styles.ritualIcon, className])}
|
||||
data-tooltip-id={id}
|
||||
data-tooltip-content="Ritual"
|
||||
>
|
||||
<SvgIcon className={styles.svg} {...props} />
|
||||
<Tooltip id={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
DarkAbjurationSvg,
|
||||
DarkConjurationSvg,
|
||||
DarkDivinationSvg,
|
||||
DarkEnchantmentSvg,
|
||||
DarkEvocationSvg,
|
||||
DarkIllusionSvg,
|
||||
DarkNecromancySvg,
|
||||
DarkTransmutationSvg,
|
||||
GrayAbjurationSvg,
|
||||
GrayConjurationSvg,
|
||||
GrayDivinationSvg,
|
||||
GrayEnchantmentSvg,
|
||||
GrayEvocationSvg,
|
||||
GrayIllusionSvg,
|
||||
GrayNecromancySvg,
|
||||
GrayTransmutationSvg,
|
||||
LightAbjurationSvg,
|
||||
LightConjurationSvg,
|
||||
LightDivinationSvg,
|
||||
LightEnchantmentSvg,
|
||||
LightEvocationSvg,
|
||||
LightIllusionSvg,
|
||||
LightNecromancySvg,
|
||||
LightTransmutationSvg,
|
||||
} from "../../Svg";
|
||||
import { SpellSchoolPropType } from "../IconConstants";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
interface Props {
|
||||
school: SpellSchoolPropType;
|
||||
themeMode?: "dark" | "gray" | "light";
|
||||
className?: string;
|
||||
}
|
||||
const SpellSchoolIcon: React.FunctionComponent<Props> = ({
|
||||
className = "",
|
||||
themeMode = "dark",
|
||||
school,
|
||||
}) => {
|
||||
const DarkSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (school) {
|
||||
case "abjuration":
|
||||
return DarkAbjurationSvg;
|
||||
case "conjuration":
|
||||
return DarkConjurationSvg;
|
||||
case "divination":
|
||||
return DarkDivinationSvg;
|
||||
case "enchantment":
|
||||
return DarkEnchantmentSvg;
|
||||
case "evocation":
|
||||
return DarkEvocationSvg;
|
||||
case "illusion":
|
||||
return DarkIllusionSvg;
|
||||
case "necromancy":
|
||||
return DarkNecromancySvg;
|
||||
case "transmutation":
|
||||
return DarkTransmutationSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const GraySvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (school) {
|
||||
case "abjuration":
|
||||
return GrayAbjurationSvg;
|
||||
case "conjuration":
|
||||
return GrayConjurationSvg;
|
||||
case "divination":
|
||||
return GrayDivinationSvg;
|
||||
case "enchantment":
|
||||
return GrayEnchantmentSvg;
|
||||
case "evocation":
|
||||
return GrayEvocationSvg;
|
||||
case "illusion":
|
||||
return GrayIllusionSvg;
|
||||
case "necromancy":
|
||||
return GrayNecromancySvg;
|
||||
case "transmutation":
|
||||
return GrayTransmutationSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const LightSvg = (): React.ComponentType<SvgInjectedProps> | null => {
|
||||
switch (school) {
|
||||
case "abjuration":
|
||||
return LightAbjurationSvg;
|
||||
case "conjuration":
|
||||
return LightConjurationSvg;
|
||||
case "divination":
|
||||
return LightDivinationSvg;
|
||||
case "enchantment":
|
||||
return LightEnchantmentSvg;
|
||||
case "evocation":
|
||||
return LightEvocationSvg;
|
||||
case "illusion":
|
||||
return LightIllusionSvg;
|
||||
case "necromancy":
|
||||
return LightNecromancySvg;
|
||||
case "transmutation":
|
||||
return LightTransmutationSvg;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
let classNames: Array<string> = [
|
||||
className,
|
||||
"ddbc-spell-school-icon",
|
||||
`ddbc-spell-school-icon--${school}`,
|
||||
];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> | null = null;
|
||||
|
||||
switch (themeMode) {
|
||||
case "light":
|
||||
SvgIcon = LightSvg();
|
||||
break;
|
||||
case "gray":
|
||||
SvgIcon = GraySvg();
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
SvgIcon = DarkSvg();
|
||||
}
|
||||
|
||||
if (SvgIcon === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SvgIcon className={classNames.join(" ")} />;
|
||||
};
|
||||
|
||||
export default SpellSchoolIcon;
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
}
|
||||
|
||||
export default class TodoIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
title: "Missing",
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-todo-icon"];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={title}
|
||||
className={classNames.join(" ")}
|
||||
isDarkMode={theme?.isDarkMode}
|
||||
>
|
||||
!
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import TodoIcon from "./TodoIcon";
|
||||
|
||||
export default TodoIcon;
|
||||
export { TodoIcon };
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import {
|
||||
DarkModeNegativeVulnerabilitySvg,
|
||||
NegativeVulnerabilitySvg,
|
||||
} from "../../Svg";
|
||||
|
||||
interface SvgInjectedProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
className: string;
|
||||
theme?: CharacterTheme;
|
||||
secondaryFill?: string;
|
||||
}
|
||||
export default class VulnerabilityIcon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
title: "Vulnerability",
|
||||
className: "",
|
||||
theme: {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, className, theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-vulnerability-icon"];
|
||||
|
||||
let SvgIcon: React.ComponentType<SvgInjectedProps> = theme?.isDarkMode
|
||||
? DarkModeNegativeVulnerabilitySvg
|
||||
: NegativeVulnerabilitySvg;
|
||||
|
||||
return (
|
||||
<Tooltip title={title} isDarkMode={theme?.isDarkMode}>
|
||||
<SvgIcon className={classNames.join(" ")} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
AccessUtils,
|
||||
FormatUtils,
|
||||
Infusion,
|
||||
InfusionChoiceLookup,
|
||||
InfusionUtils,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
SnippetData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import MarketplaceCta from "../MarketplaceCta";
|
||||
import Snippet from "../Snippet";
|
||||
|
||||
interface Props {
|
||||
infusion: Infusion;
|
||||
infusionChoiceLookup: InfusionChoiceLookup;
|
||||
snippetData: SnippetData;
|
||||
ruleData: RuleData;
|
||||
onClick?: (infusion: Infusion) => void;
|
||||
proficiencyBonus: number;
|
||||
}
|
||||
class InfusionPreview extends React.PureComponent<Props> {
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, infusion } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onClick(infusion);
|
||||
}
|
||||
};
|
||||
|
||||
renderDescription = (): React.ReactNode => {
|
||||
const { infusion, ruleData, snippetData, proficiencyBonus } = this.props;
|
||||
|
||||
let contentNode: React.ReactNode;
|
||||
|
||||
if (AccessUtils.isAccessible(InfusionUtils.getAccessType(infusion))) {
|
||||
let classLevel: number | null =
|
||||
InfusionUtils.getSelectedModifierData(infusion)?.value ?? null;
|
||||
|
||||
contentNode = (
|
||||
<Snippet
|
||||
snippetData={snippetData}
|
||||
classLevel={classLevel}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
>
|
||||
{InfusionUtils.getSnippet(infusion)}
|
||||
</Snippet>
|
||||
);
|
||||
} else {
|
||||
const sources = InfusionUtils.getSources(infusion);
|
||||
|
||||
let sourceNames: Array<string> = [];
|
||||
if (sources.length > 0) {
|
||||
sources.forEach((source) => {
|
||||
let sourceInfo = RuleDataUtils.getSourceDataInfo(
|
||||
source.sourceId,
|
||||
ruleData
|
||||
);
|
||||
|
||||
if (sourceInfo !== null && sourceInfo.description !== null) {
|
||||
sourceNames.push(sourceInfo.description);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
contentNode = (
|
||||
<MarketplaceCta
|
||||
showImage={false}
|
||||
sourceName={
|
||||
sourceNames.length > 0
|
||||
? FormatUtils.renderNonOxfordCommaList(sourceNames)
|
||||
: null
|
||||
}
|
||||
description="To unlock this infusion, check out the Marketplace to view purchase options."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return contentNode;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { infusion } = this.props;
|
||||
|
||||
return (
|
||||
<div className="ddbc-infusion-preview" onClick={this.handleClick}>
|
||||
<div className="ddbc-infusion-preview__name">
|
||||
Infusion: {InfusionUtils.getName(infusion)}
|
||||
</div>
|
||||
<div className="ddbc-infusion-preview__snippet">
|
||||
{this.renderDescription()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InfusionPreview;
|
||||
@@ -0,0 +1,83 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
CharacterTheme,
|
||||
Item,
|
||||
ItemUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ItemName } from "~/components/ItemName";
|
||||
|
||||
interface Props {
|
||||
item: Item;
|
||||
onClick?: (item: Item) => void;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
class ItemPreview extends React.PureComponent<Props> {
|
||||
handleClick = (evt: React.MouseEvent): void => {
|
||||
const { onClick, item } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onClick(item);
|
||||
}
|
||||
};
|
||||
|
||||
renderMetaText = (): React.ReactNode => {
|
||||
const { item } = this.props;
|
||||
|
||||
let metaItems: Array<string> = [];
|
||||
|
||||
if (ItemUtils.isLegacy(item)) {
|
||||
metaItems.push("Legacy");
|
||||
}
|
||||
|
||||
const type = ItemUtils.getType(item);
|
||||
if (type) {
|
||||
metaItems.push(type);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-item-preview__meta">
|
||||
{metaItems.map((metaItem, idx) => (
|
||||
<span className="ddbc-item-preview__meta-item" key={idx}>
|
||||
{metaItem}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item } = this.props;
|
||||
|
||||
const imageStyles: React.CSSProperties = {
|
||||
backgroundImage: `url(${ItemUtils.getAvatarUrl(item)})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ddbc-item-preview" onClick={this.handleClick}>
|
||||
<div className="ddbc-item-preview__avatar" style={imageStyles} />
|
||||
<div className="ddbc-item-preview__name">
|
||||
<ItemName item={item} />
|
||||
{this.renderMetaText()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ItemPreview;
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { AnimatedLoadingRingSvg } from "../Svg";
|
||||
|
||||
interface LoadingPlaceholderProps {
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
const LoadingPlaceholder: React.FunctionComponent<LoadingPlaceholderProps> = ({
|
||||
label = "Loading",
|
||||
}) => (
|
||||
<div className="ddbc-loading-placeholder">
|
||||
<AnimatedLoadingRingSvg className="ddbc-loading-placeholder__icon" />
|
||||
<div className="ddbc-loading-placeholder__label">{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default React.memo(LoadingPlaceholder);
|
||||
@@ -0,0 +1,4 @@
|
||||
import LoadingPlaceholder from "./LoadingPlaceholder";
|
||||
|
||||
export default LoadingPlaceholder;
|
||||
export { LoadingPlaceholder };
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
sourceName: string | null;
|
||||
sourceNames?: Array<string>;
|
||||
url: string;
|
||||
description: string | null;
|
||||
showImage: boolean;
|
||||
}
|
||||
export default class MarketplaceCta extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
sourceName: null,
|
||||
url: "/marketplace",
|
||||
description: null,
|
||||
showImage: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { sourceName, sourceNames, url, description, showImage } = this.props;
|
||||
const allSourceNames: Array<string> = [];
|
||||
|
||||
if (sourceName) {
|
||||
allSourceNames.push(sourceName);
|
||||
}
|
||||
|
||||
if (sourceNames) {
|
||||
allSourceNames.push(...sourceNames);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ddbc-marketplace-cta">
|
||||
<a href={url} target="_blank">
|
||||
<div className="ddbc-marketplace-cta__content">
|
||||
{showImage && <div className="ddbc-marketplace-cta__image" />}
|
||||
{allSourceNames.length > 0 && (
|
||||
<div className="ddbc-marketplace-cta__header">
|
||||
<div className="ddbc-marketplace-cta__header-intro">
|
||||
Available in
|
||||
</div>
|
||||
<div className="ddbc-marketplace-cta__header-title">
|
||||
{FormatUtils.renderNonOxfordCommaList(allSourceNames)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{description && (
|
||||
<div className="ddbc-marketplace-cta__description">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
<div className="ddbc-marketplace-cta__cta">
|
||||
<div className="ddbc-marketplace-cta__cta-button">
|
||||
Go To Marketplace
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import MarketplaceCta from "./MarketplaceCta";
|
||||
|
||||
export default MarketplaceCta;
|
||||
export { MarketplaceCta };
|
||||
@@ -0,0 +1,212 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import {
|
||||
Note,
|
||||
FormatUtils,
|
||||
Constants,
|
||||
NoteUtils,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { NumberDisplay } from "~/components/NumberDisplay";
|
||||
|
||||
import Damage from "../Damage";
|
||||
import { AoeTypeIcon, DisadvantageIcon } from "../Icons";
|
||||
import { AoeTypePropType } from "../Icons/IconConstants";
|
||||
|
||||
interface Props {
|
||||
notes: Array<Note>;
|
||||
className: string;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class NoteComponents extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
renderPlainText = (note: Note): React.ReactNode => {
|
||||
const { theme } = this.props;
|
||||
let classNames: Array<string> = [
|
||||
"ddbc-note-components__component",
|
||||
"ddbc-note-components__component--plain",
|
||||
];
|
||||
|
||||
if (note.data.displayIntention) {
|
||||
classNames.push(
|
||||
`ddbc-note-components__component--${note.data.displayIntention}`
|
||||
);
|
||||
}
|
||||
|
||||
if (theme.isDarkMode) {
|
||||
classNames.push("ddbc-note-components__component--dark-mode");
|
||||
}
|
||||
|
||||
return <span className={classNames.join(" ")}>{note.data.text}</span>;
|
||||
};
|
||||
|
||||
renderTooltip = (note: Note): React.ReactNode => {
|
||||
const { theme } = this.props;
|
||||
let classNames: Array<string> = [
|
||||
"ddbc-note-components__component",
|
||||
"ddbc-note-components__component--tooltip",
|
||||
];
|
||||
|
||||
if (note.data.displayIntention) {
|
||||
classNames.push(
|
||||
`ddbc-note-components__component--${note.data.displayIntention}`
|
||||
);
|
||||
}
|
||||
|
||||
if (theme.isDarkMode) {
|
||||
classNames.push("ddbc-note-components__component--dark-mode");
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
className={classNames.join(" ")}
|
||||
title={note.data.tooltip}
|
||||
tippyOpts={note.data.tooltipOpts}
|
||||
isDarkMode={theme.isDarkMode}
|
||||
>
|
||||
{note.data.text}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
renderDistance = (note: Note): React.ReactNode => {
|
||||
const { theme } = this.props;
|
||||
let classNames: Array<string> = [
|
||||
"ddbc-note-components__component",
|
||||
"ddbc-note-components__component--distance",
|
||||
];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")}>
|
||||
<NumberDisplay type="distanceInFt" number={note.data.distance} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
renderAoeIcon = (note: Note): React.ReactNode => {
|
||||
const { theme } = this.props;
|
||||
let classNames: Array<string> = [
|
||||
"ddbc-note-components__component",
|
||||
"ddbc-note-components__component--aoe-icon",
|
||||
];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")}>
|
||||
<AoeTypeIcon
|
||||
type={FormatUtils.slugify(note.data.type) as AoeTypePropType}
|
||||
themeMode={theme?.isDarkMode ? "gray" : "dark"}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
//`
|
||||
};
|
||||
|
||||
renderDamage = (note: Note): React.ReactNode => {
|
||||
const { theme } = this.props;
|
||||
let classNames: Array<string> = [
|
||||
"ddbc-note-components__component",
|
||||
"ddbc-note-components__component--damage",
|
||||
];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")}>
|
||||
<Damage
|
||||
theme={theme}
|
||||
type={note.data.type}
|
||||
damage={note.data.damage}
|
||||
info={note.data.info}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
renderDisadvantageIcon = (note: Note): React.ReactNode => {
|
||||
const { theme } = this.props;
|
||||
|
||||
let classNames: Array<string> = [
|
||||
"ddbc-note-components__component",
|
||||
"ddbc-note-components__component--disadvantage-icon",
|
||||
];
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")}>
|
||||
<DisadvantageIcon theme={theme} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
renderGroup = (
|
||||
notes: Array<Note> | null,
|
||||
separator: string = " ",
|
||||
key: string | number | null = null
|
||||
): React.ReactNode => {
|
||||
if (notes === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={key ? key : "start-note-group-key"}>
|
||||
{notes.map((note, idx): React.ReactNode => {
|
||||
if (note === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${NoteUtils.getType(note)}-${idx}`}>
|
||||
{this.renderNote(note)}
|
||||
{idx + 1 < notes.length ? separator : ""}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
renderNote = (note: Note | null): React.ReactNode => {
|
||||
if (note === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (NoteUtils.getType(note)) {
|
||||
case Constants.NoteTypeEnum.PLAIN_TEXT:
|
||||
return this.renderPlainText(note);
|
||||
case Constants.NoteTypeEnum.TOOLTIP:
|
||||
return this.renderTooltip(note);
|
||||
case Constants.NoteTypeEnum.DISTANCE:
|
||||
return this.renderDistance(note);
|
||||
case Constants.NoteTypeEnum.AOE_ICON:
|
||||
return this.renderAoeIcon(note);
|
||||
case Constants.NoteTypeEnum.DAMAGE:
|
||||
return this.renderDamage(note);
|
||||
case Constants.NoteTypeEnum.DISADVANTAGE_ICON:
|
||||
return this.renderDisadvantageIcon(note);
|
||||
case Constants.NoteTypeEnum.GROUP:
|
||||
return this.renderGroup(
|
||||
note.data.notes,
|
||||
note.data.separator,
|
||||
note.data.key
|
||||
);
|
||||
default:
|
||||
// not implement
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { notes, className } = this.props;
|
||||
|
||||
let classNames: Array<string> = [className, "ddbc-note-components"];
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
{this.renderGroup(notes, ", ")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user