2025-05-28 15:36:51 -07:00

251 lines
6.6 KiB
TypeScript

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;