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:
+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 };
|
||||
Reference in New Issue
Block a user