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:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
@@ -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 };
@@ -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 };