242 lines
6.4 KiB
TypeScript
242 lines
6.4 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 { GameLogContext } from "@dndbeyond/game-log-components";
|
|
import { RollTypes } from "@dndbeyond/pocket-dimension-dice/constants";
|
|
import {
|
|
RollContext,
|
|
RollDicePayload,
|
|
} from "@dndbeyond/pocket-dimension-dice/types";
|
|
|
|
import { NumberDisplay } from "~/components/NumberDisplay";
|
|
|
|
import { DigitalDiceWrapper } from "../Dice";
|
|
import { DiceComponentUtils } from "../utils";
|
|
|
|
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: RollContext;
|
|
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: RollDicePayload["data"]) => {
|
|
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;
|
|
|
|
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={`1d20${
|
|
toHit > 0 ? `+${toHit}` : toHit !== 0 ? toHit : ""
|
|
}`}
|
|
rollType={RollTypes.ToHit}
|
|
action={rollAction}
|
|
isDiceEnabled={diceEnabled}
|
|
rollCallback={this.handleRollResults}
|
|
entityId={String(rollContext.entityId)}
|
|
entityType={String(rollContext.entityType)}
|
|
name={String(rollContext.name)}
|
|
>
|
|
<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;
|