``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
251 lines
6.6 KiB
TypeScript
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;
|