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 Damage from "../../Damage"; import { DigitalDiceWrapper } from "../../Dice"; import { AttackTypeIcon } from "../../Icons"; import NoteComponents from "../../NoteComponents"; import { DiceComponentUtils } from "../../utils"; import CombatAttack from "../CombatAttack"; interface Props { attack: Attack; item: Item; weaponSpellDamageGroups: Array; 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 { 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 (
); }; handleRoll = (wasCrit: boolean) => { this.setState({ isCriticalHit: wasCrit }); }; render() { const { attack, item, weaponSpellDamageGroups, showNotes, diceEnabled, theme, rollContext, className, ruleData, } = 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); const appliedWeaponReplacementStats = ItemUtils.getAppliedWeaponReplacementStats(item); let combinedMetaItems: Array = []; if (isLegacy) { combinedMetaItems.push("Legacy"); } if (type === Constants.WeaponTypeEnum.AMMUNITION) { combinedMetaItems.push("Ammunition"); } else { if ( isHexWeapon || isPactWeapon || isDedicatedWeapon || appliedWeaponReplacementStats.length > 0 ) { if (isHexWeapon) { combinedMetaItems.push("Hex Weapon"); } if (isPactWeapon) { combinedMetaItems.push("Pact Weapon"); } if (isDedicatedWeapon) { combinedMetaItems.push("Dedicated Weapon"); } if (appliedWeaponReplacementStats.length > 0) { appliedWeaponReplacementStats.forEach((statId) => { combinedMetaItems.push( `Using ${RuleDataUtils.getStatNameById(statId, ruleData, true)}` ); }); } } 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 = ( ); } let classNames: Array = ["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 = ( ); } let damageDisplayNode: React.ReactNode = (
{damageNode} {versatileDamageNode}
); 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 = ( {range} {longRange !== null && ( ({longRange}) )} ); rangeLabel = ""; } else { rangeValue = ; } let filteredWeaponSpellDamageGroups = ItemUtils.getApplicableWeaponSpellDamageGroups( item, weaponSpellDamageGroups ); let iconKey: string = `weapon-${FormatUtils.slugify(attackTypeName)}`; if (filteredWeaponSpellDamageGroups.length) { iconKey = "weapon-spell-damage"; } let attackClassNames: Array = [ "ddbc-combat-attack--item", `ddbc-combat-item-attack--${FormatUtils.slugify(attackTypeName)}`, className, ]; if (isCriticalHit) { attackClassNames.push("ddbc-combat-attack--crit"); } return ( {ItemUtils.getMasteryName(item) && ( )} } name={} 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;