From b7f7f4a655e8cb66eb40cb129bec2e8f97db5c0f Mon Sep 17 00:00:00 2001 From: David Kruger Date: Wed, 2 Jul 2025 01:00:11 -0700 Subject: [PATCH] New source found from dndbeyond.com --- ddb_main/components/Layout/Layout.tsx | 2 +- .../ttui/components/Tooltip/Tooltip.tsx | 17 ++++++ .../es/engine/Character/hacks/HitPoints.js | 3 +- .../rules-engine/es/engine/Class/accessors.js | 3 ++ .../rules-engine/es/engine/Class/derivers.js | 14 ++++- .../es/engine/Class/generators.js | 14 +++-- .../rules-engine/es/engine/Core/constants.js | 9 ++++ .../rules-engine/es/engine/Core/utils.js | 23 +++++++- .../rules-engine/es/engine/Item/accessors.js | 6 +++ .../rules-engine/es/engine/Item/derivers.js | 18 ++++++- .../rules-engine/es/engine/Item/generators.js | 34 ++++++++---- .../rules-engine/es/engine/Item/simulators.js | 2 +- .../es/engine/Modifier/constants.js | 12 +++-- .../es/engine/Modifier/validators.js | 17 ++++++ .../rules-engine/es/engine/Value/constants.js | 6 +++ .../rules-engine/es/managers/ItemManager.js | 9 ++++ .../es/selectors/composite/engine.js | 12 ++++- .../CharacterGrid/CharacterGrid.tsx | 3 +- .../CharacterGrid/SearchSort/SearchSort.tsx | 1 - .../ClassManager/ClassManager.tsx | 4 ++ .../pages/ClassesManage/ClassesManage.tsx | 6 +++ .../OptionalFeatureManager.tsx | 54 ++++++++++++++----- .../components/ItemDetail/ItemDetail.tsx | 41 +++++++++++++- .../CombatItemAttack/CombatItemAttack.tsx | 24 +++++++-- 24 files changed, 291 insertions(+), 43 deletions(-) diff --git a/ddb_main/components/Layout/Layout.tsx b/ddb_main/components/Layout/Layout.tsx index f701ef3..77480b1 100644 --- a/ddb_main/components/Layout/Layout.tsx +++ b/ddb_main/components/Layout/Layout.tsx @@ -29,7 +29,7 @@ export const Layout: FC = ({ children, ...props }) => {
{/* TODO: fetch sources */} - +
{children}
{!matchSheet && ( diff --git a/ddb_main/node_modules/@dndbeyond/ttui/components/Tooltip/Tooltip.tsx b/ddb_main/node_modules/@dndbeyond/ttui/components/Tooltip/Tooltip.tsx index f5b8c2e..7deb427 100644 --- a/ddb_main/node_modules/@dndbeyond/ttui/components/Tooltip/Tooltip.tsx +++ b/ddb_main/node_modules/@dndbeyond/ttui/components/Tooltip/Tooltip.tsx @@ -5,6 +5,21 @@ import type { FC } from "react"; import { Tooltip as ReactTooltip, type ITooltip } from "react-tooltip"; import styles from "./Tooltip.module.css"; +/** + * BEST PRACTICES: + * - Tooltips are intended for interactive elements, and should not be used on non-interactive elements, such as divs, spans, p, etc. + * - Tooltips should not be focusable, so you should not put interactive elements inside of them, such as links. + * - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tooltip_role + * + * USAGE REQUIREMENTS: + * - The anchor element must have an `aria-describedby` attribute that is equivalent to the tooltip's ID. + * - If you must put a tooltip on a non-interactive element, it must have a `tabIndex` attribute set to 0 so that it can be reached by + * keyboard interaction. However, this still will not ensure that the tooltip will be read by screen readers. It is recommended to have a + * secondary means for screen readers to access the tooltip information, such as a span with a "screen-reader-only" class and the tooltip + * information inside of it. + * - If you must put content that is clickable inside of the tooltip, the `clickable` prop must provided and set to true. + **/ + export interface TooltipProps extends ITooltip { id: string; "data-testid"?: string; @@ -24,6 +39,8 @@ export const Tooltip: FC = ({ {...props} className={clsx([styles.tooltip, props.className])} disableStyleInjection={disableStyleInjection} + globalCloseEvents={{ escape: true, clickOutsideAnchor: true }} + closeEvents={{ mouseleave: true, blur: !props.clickable }} > {children} diff --git a/ddb_main/packages/rules-engine/es/engine/Character/hacks/HitPoints.js b/ddb_main/packages/rules-engine/es/engine/Character/hacks/HitPoints.js index ee0c716..6800c56 100644 --- a/ddb_main/packages/rules-engine/es/engine/Character/hacks/HitPoints.js +++ b/ddb_main/packages/rules-engine/es/engine/Character/hacks/HitPoints.js @@ -16,11 +16,12 @@ export function hack__generateHitPointParts(baseHp, overrideHp, bonusHp, tempHp, removedHp, }; } -export function hack__generateSpecialWeaponPropertiesEnabled(hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, dedicatedWeaponEnabled) { +export function hack__generateSpecialWeaponPropertiesEnabled(hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, dedicatedWeaponEnabled, replacementWeaponStats) { return { hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, dedicatedWeaponEnabled, + replacementWeaponStats, }; } diff --git a/ddb_main/packages/rules-engine/es/engine/Class/accessors.js b/ddb_main/packages/rules-engine/es/engine/Class/accessors.js index 39a6889..bed16ae 100644 --- a/ddb_main/packages/rules-engine/es/engine/Class/accessors.js +++ b/ddb_main/packages/rules-engine/es/engine/Class/accessors.js @@ -540,3 +540,6 @@ export function getSpellListIds(charClass) { export function getOptionalClassFeatures(charClass) { return charClass.optionalClassFeatures; } +export function getReplacementWeaponAbilityStats(charClass) { + return charClass.replacementWeaponAbilityStats; +} diff --git a/ddb_main/packages/rules-engine/es/engine/Class/derivers.js b/ddb_main/packages/rules-engine/es/engine/Class/derivers.js index 7455fd5..784d8dd 100644 --- a/ddb_main/packages/rules-engine/es/engine/Class/derivers.js +++ b/ddb_main/packages/rules-engine/es/engine/Class/derivers.js @@ -4,7 +4,7 @@ import { ClassFeatureAccessors, ClassFeatureUtils, ClassFeatureValidators } from import { DB_STRING_BOOK_OF_ANCIENT_SECRETS, DB_STRING_INFUSE_ITEM, FeatureTypeEnum, } from '../Core'; import { DataOriginGenerators } from '../DataOrigin'; import { HelperUtils } from '../Helper'; -import { ModifierValidators } from '../Modifier'; +import { ModifierAccessors, ModifierValidators } from '../Modifier'; import { OptionalClassFeatureAccessors } from '../OptionalClassFeature'; import { DefaultSpellCastingLearningStyle, SpellDerivers } from '../Spell'; import { getActiveId, getDefinitionClassFeatures, getDefinitionSpellcastingStatId, getId, getLevel, getMappingId, getSubclass, getClassFeatures, getDefinitionSpellCastingLearningStyle, } from './accessors'; @@ -166,6 +166,18 @@ export function deriveOrderedClassFeatures(classFeatures) { (classFeature) => ClassFeatureAccessors.getName(classFeature), ]); } +export function deriveReplacementWeaponAbilityStats(classModifiers) { + const replacementWeaponAbilityStats = []; + classModifiers.forEach((modifier) => { + if (ModifierValidators.isEnablesAbilityStatModifier(modifier)) { + const statId = ModifierAccessors.getEntityId(modifier) || 0; + if (statId && !replacementWeaponAbilityStats.includes(statId)) { + replacementWeaponAbilityStats.push(statId); + } + } + }); + return replacementWeaponAbilityStats; +} /** * * @param classFeatures diff --git a/ddb_main/packages/rules-engine/es/engine/Class/generators.js b/ddb_main/packages/rules-engine/es/engine/Class/generators.js index 459a962..e443eed 100644 --- a/ddb_main/packages/rules-engine/es/engine/Class/generators.js +++ b/ddb_main/packages/rules-engine/es/engine/Class/generators.js @@ -8,8 +8,8 @@ import { FormatUtils } from '../Format'; import { ModifierDerivers, ModifierValidators } from '../Modifier'; import { OptionAccessors } from '../Option'; import { SpellAccessors, SpellDerivers, SpellGenerators, } from '../Spell'; -import { getActiveId, getBaseClassSpells, getClassFeatures, getDefinitionClassFeatures, getEnablesDedicatedWeapon, getEnablesHexWeapon, getEnablesPactWeapon, getId, getLevel, getMappingId, getName, getSlug, getSpellPrepareType, getSpellRules, getSpells, getSubclass, isPactMagicActive, isSpellcastingActive, isStartingClass, } from './accessors'; -import { deriveActiveId, deriveClassFeatureGroups, deriveConsolidatedClassFeatures, deriveDedicatedHexAndPactWeaponEnabled, deriveIsPactMagicActive, deriveIsSpellcastingActive, deriveSpellCastingLearningStyle, deriveSpellListIds, deriveSpellRules, deriveSpells, deriveUniqueKey, } from './derivers'; +import { getActiveId, getBaseClassSpells, getClassFeatures, getDefinitionClassFeatures, getEnablesDedicatedWeapon, getEnablesHexWeapon, getEnablesPactWeapon, getId, getLevel, getMappingId, getName, getReplacementWeaponAbilityStats, getSlug, getSpellPrepareType, getSpellRules, getSpells, getSubclass, isPactMagicActive, isSpellcastingActive, isStartingClass, } from './accessors'; +import { deriveActiveId, deriveClassFeatureGroups, deriveConsolidatedClassFeatures, deriveDedicatedHexAndPactWeaponEnabled, deriveIsPactMagicActive, deriveIsSpellcastingActive, deriveReplacementWeaponAbilityStats, deriveSpellCastingLearningStyle, deriveSpellListIds, deriveSpellRules, deriveSpells, deriveUniqueKey, } from './derivers'; export function generateBaseCharClasses(charClasses, optionalClassFeatures, allClassSpells, allClassAlwaysPreparedSpells, allClassAlwaysKnownSpells, appContext, ruleData, classesLookupData, definitionPool, characterPreferences, characterId, choiceComponents, baseFeats) { const orderedClasses = orderBy(charClasses, [(charClass) => isStartingClass(charClass), (charClass) => getName(charClass)], ['desc', 'asc']); const hack__baseCharClasses = orderedClasses.map((charClass) => hack__generateHackBaseCharClass(charClass, optionalClassFeatures, ruleData, classesLookupData, characterPreferences.enableOptionalClassFeatures)); @@ -70,7 +70,7 @@ export function generateBaseCharClass(charClass, optionalClassFeatures, allClass actions, enablesDedicatedWeapon, enablesHexWeapon, - enablesPactWeapon, slug: FormatUtils.cobaltSlugify(getName(charClass)), spellListIds: deriveSpellListIds(updatedFeatures, getLevel(charClass)), spellCastingLearningStyle: deriveSpellCastingLearningStyle(charClass) }); + enablesPactWeapon, slug: FormatUtils.cobaltSlugify(getName(charClass)), spellListIds: deriveSpellListIds(updatedFeatures, getLevel(charClass)), spellCastingLearningStyle: deriveSpellCastingLearningStyle(charClass), replacementWeaponAbilityStats: deriveReplacementWeaponAbilityStats(classModifiers) }); } export function generateClassFeatureLookup(baseCharClasses) { const lookup = {}; @@ -209,6 +209,14 @@ export function generatePactWeaponEnabled(classes) { }); return pactWeaponEnabled; } +export function generateReplacementWeaponAbilityStats(classes) { + const replacementWeaponStats = []; + classes.forEach((charClass) => { + const classReplacementWeaponStats = getReplacementWeaponAbilityStats(charClass); + replacementWeaponStats.push(...classReplacementWeaponStats); + }); + return replacementWeaponStats; +} /** * * @param classes diff --git a/ddb_main/packages/rules-engine/es/engine/Core/constants.js b/ddb_main/packages/rules-engine/es/engine/Core/constants.js index a19b7d0..6a1e3cb 100644 --- a/ddb_main/packages/rules-engine/es/engine/Core/constants.js +++ b/ddb_main/packages/rules-engine/es/engine/Core/constants.js @@ -1,3 +1,4 @@ +import { AdjustmentTypeEnum } from '../Value'; export const FEET_IN_MILES = 5280; export const POUNDS_IN_TON = 2000; export var AppContextTypeEnum; @@ -400,3 +401,11 @@ export const ABILITY_STAT_PHYSICAL_LIST = [ AbilityStatEnum.DEXTERITY, AbilityStatEnum.CONSTITUTION, ]; +export const STAT_REPLACEMENT_TYPE_LOOKUP = { + [AdjustmentTypeEnum.USE_STRENGTH]: AbilityStatEnum.STRENGTH, + [AdjustmentTypeEnum.USE_DEXTERITY]: AbilityStatEnum.DEXTERITY, + [AdjustmentTypeEnum.USE_CONSTITUTION]: AbilityStatEnum.CONSTITUTION, + [AdjustmentTypeEnum.USE_INTELLIGENCE]: AbilityStatEnum.INTELLIGENCE, + [AdjustmentTypeEnum.USE_WISDOM]: AbilityStatEnum.WISDOM, + [AdjustmentTypeEnum.USE_CHARISMA]: AbilityStatEnum.CHARISMA, +}; diff --git a/ddb_main/packages/rules-engine/es/engine/Core/utils.js b/ddb_main/packages/rules-engine/es/engine/Core/utils.js index d666704..17b4c76 100644 --- a/ddb_main/packages/rules-engine/es/engine/Core/utils.js +++ b/ddb_main/packages/rules-engine/es/engine/Core/utils.js @@ -1,6 +1,7 @@ import { round } from 'lodash'; import { RuleDataAccessors } from '../RuleData'; -import { CURRENCY_VALUE, FEET_IN_MILES } from './constants'; +import { AdjustmentTypeEnum } from '../Value'; +import { AbilityStatEnum, CURRENCY_VALUE, FEET_IN_MILES } from './constants'; export function convertFeetToMiles(number, precision = 2) { return round(number / FEET_IN_MILES, precision); } @@ -41,3 +42,23 @@ export function getCurrencyTransactionAdjustment(multiplier, adjustments) { }); return adjustedCurrency; } +// returns the adjusment type that corresponds to a given stat id +// Only returns types that are defined in the STAT_REPLACEMENT_TYPE_LOOKUP +export function getAdjustmentTypeByStatId(statId) { + switch (statId) { + case AbilityStatEnum.STRENGTH: + return AdjustmentTypeEnum.USE_STRENGTH; + case AbilityStatEnum.DEXTERITY: + return AdjustmentTypeEnum.USE_DEXTERITY; + case AbilityStatEnum.CONSTITUTION: + return AdjustmentTypeEnum.USE_CONSTITUTION; + case AbilityStatEnum.INTELLIGENCE: + return AdjustmentTypeEnum.USE_INTELLIGENCE; + case AbilityStatEnum.WISDOM: + return AdjustmentTypeEnum.USE_WISDOM; + case AbilityStatEnum.CHARISMA: + return AdjustmentTypeEnum.USE_CHARISMA; + default: + return null; + } +} diff --git a/ddb_main/packages/rules-engine/es/engine/Item/accessors.js b/ddb_main/packages/rules-engine/es/engine/Item/accessors.js index 2acb6f5..2e677c6 100644 --- a/ddb_main/packages/rules-engine/es/engine/Item/accessors.js +++ b/ddb_main/packages/rules-engine/es/engine/Item/accessors.js @@ -1040,3 +1040,9 @@ export function getPrimarySourceCategoryId(item) { export function getSecondarySourceCategoryIds(item) { return item.secondarySourceCategoryIds; } +export function getReplacementWeaponStats(item) { + return item.replacementWeaponStats; +} +export function getAppliedWeaponReplacementStats(item) { + return item.appliedWeaponReplacementStats; +} diff --git a/ddb_main/packages/rules-engine/es/engine/Item/derivers.js b/ddb_main/packages/rules-engine/es/engine/Item/derivers.js index 44f5352..c230f07 100644 --- a/ddb_main/packages/rules-engine/es/engine/Item/derivers.js +++ b/ddb_main/packages/rules-engine/es/engine/Item/derivers.js @@ -512,7 +512,7 @@ export function deriveWeaponReach(item, modifiers, ruleData) { * @param modifiers * @param martialArtsLevel */ -export function deriveAvailableWeaponAbilities(item, modifiers, martialArtsLevelScale, isDedicatedWeapon, isPactWeapon, isHexWeapon) { +export function deriveAvailableWeaponAbilities(item, modifiers, martialArtsLevelScale, isDedicatedWeapon, isPactWeapon, isHexWeapon, appliedWeaponReplacementStats) { const availableAbilities = []; const attackType = getAttackType(item); if (attackType === AttackTypeRangeEnum.MELEE || hasWeaponProperty(item, WeaponPropertyEnum.FINESSE)) { @@ -562,6 +562,22 @@ export function deriveAvailableWeaponAbilities(item, modifiers, martialArtsLevel }); } } + //handle Weapon Replacement Stats + if (appliedWeaponReplacementStats.length > 0) { + const enableAbilityStatReplacementModifiers = modifiers.filter((modifier) => ModifierValidators.isReplaceWeaponAbilityModifier(modifier)); + enableAbilityStatReplacementModifiers.forEach((modifier) => { + //get the origin of the modifier + const dataOrigin = ModifierAccessors.getDataOrigin(modifier); + if (dataOrigin) { + const replacementAbilities = deriveReplacementWeaponAbilities(modifiers, dataOrigin); + replacementAbilities.forEach((ability) => { + if (!availableAbilities.includes(ability)) { + availableAbilities.push(ability); + } + }); + } + }); + } return availableAbilities; } export function deriveReplacementWeaponAbilities(modifiers, enableModifierDataOrigin) { diff --git a/ddb_main/packages/rules-engine/es/engine/Item/generators.js b/ddb_main/packages/rules-engine/es/engine/Item/generators.js index 20218ba..dccf634 100644 --- a/ddb_main/packages/rules-engine/es/engine/Item/generators.js +++ b/ddb_main/packages/rules-engine/es/engine/Item/generators.js @@ -13,7 +13,7 @@ import { keyBy, orderBy } from 'lodash'; import { TypeScriptUtils } from '../../utils'; import { CharacterDerivers } from '../Character'; import { ContainerTypeEnum } from '../Container'; -import { StealthCheckTypeEnum } from '../Core'; +import { STAT_REPLACEMENT_TYPE_LOOKUP, StealthCheckTypeEnum } from '../Core'; import { DataOriginDataInfoKeyEnum, DataOriginTypeEnum } from '../DataOrigin'; import { DefinitionHacks } from '../Definition'; import { DiceAccessors, DiceDerivers, DiceUtils } from '../Dice'; @@ -67,7 +67,7 @@ export function generateCustomItem(customItem, ruleData, characterId) { limitedUse: null, quantity: quantity !== null && quantity !== void 0 ? quantity : 1, }; - return Object.assign(Object.assign({}, simulatedContract), { avatarUrl: (_a = definition.avatarUrl) !== null && _a !== void 0 ? _a : '', armorClass: null, baseItemType: ItemBaseTypeEnum.GEAR, additionalDamages: [], versatileDamage: null, reach: null, canAttune: false, canBeDedicatedWeapon: false, canEquip: false, canHexWeapon: false, canOffhand: false, canPactWeapon: false, categoryInfo: null, definitionKey: '', hexWeaponEnabled: false, isDedicatedWeapon: false, isHexWeapon: false, isPactWeapon: false, metaItems: [], pactWeaponEnabled: false, toHit: null, damage: null, infusion: null, infusionActions: [], isOffhand: false, isAdamantine: false, isMagic: false, isSilvered: false, damageType: definition.damageType, displayAsAttack: false, isCustom: true, isCustomized: false, isDisplayAsAttack: false, proficiency: false, modifiers: [], originalContract: customItem, spells: [], type: null, name: (_b = definition.name) !== null && _b !== void 0 ? _b : 'Unidentified Item', cost: deriveCost(simulatedContract, {}), weight: deriveWeight(simulatedContract, {}), capacityWeight: deriveCapacityWeight(simulatedContract, {}), notes, properties: [], stealthCheck: StealthCheckTypeEnum.NONE, masteryName: null, primarySourceCategoryId: -1, secondarySourceCategoryIds: [] }); + return Object.assign(Object.assign({}, simulatedContract), { avatarUrl: (_a = definition.avatarUrl) !== null && _a !== void 0 ? _a : '', armorClass: null, baseItemType: ItemBaseTypeEnum.GEAR, additionalDamages: [], versatileDamage: null, reach: null, canAttune: false, canBeDedicatedWeapon: false, canEquip: false, canHexWeapon: false, canOffhand: false, canPactWeapon: false, categoryInfo: null, definitionKey: '', hexWeaponEnabled: false, isDedicatedWeapon: false, isHexWeapon: false, isPactWeapon: false, metaItems: [], pactWeaponEnabled: false, toHit: null, damage: null, infusion: null, infusionActions: [], isOffhand: false, isAdamantine: false, isMagic: false, isSilvered: false, damageType: definition.damageType, displayAsAttack: false, isCustom: true, isCustomized: false, isDisplayAsAttack: false, proficiency: false, modifiers: [], originalContract: customItem, spells: [], type: null, name: (_b = definition.name) !== null && _b !== void 0 ? _b : 'Unidentified Item', cost: deriveCost(simulatedContract, {}), weight: deriveWeight(simulatedContract, {}), capacityWeight: deriveCapacityWeight(simulatedContract, {}), notes, properties: [], stealthCheck: StealthCheckTypeEnum.NONE, masteryName: null, primarySourceCategoryId: -1, secondarySourceCategoryIds: [], replacementWeaponStats: [], appliedWeaponReplacementStats: [] }); } /** * @@ -97,8 +97,8 @@ export function generateWeaponBehaviorBaseItem(baseWeapon, parentItem, valueLook * @param hack__characterSpecialWeaponPropertiesEnabled */ export function generateItems(items, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, ruleData, hack__characterSpecialWeaponPropertiesEnabled) { - const { hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, dedicatedWeaponEnabled } = hack__characterSpecialWeaponPropertiesEnabled; - return items.map((item) => generateItem(item, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, ruleData, dedicatedWeaponEnabled)); + const { hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, dedicatedWeaponEnabled, replacementWeaponStats, } = hack__characterSpecialWeaponPropertiesEnabled; + return items.map((item) => generateItem(item, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, ruleData, dedicatedWeaponEnabled, replacementWeaponStats)); } /** * @@ -116,7 +116,7 @@ export function generateItems(items, abilityLookup, proficiencyBonus, modifiers, * @param ruleData * @param dedicatedWeaponEnabled */ -export function generateItem(item, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, ruleData, dedicatedWeaponEnabled) { +export function generateItem(item, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, ruleData, dedicatedWeaponEnabled, replacementWeaponStats) { const mappingId = getMappingId(item); const mappingIdString = ValueHacks.hack__toString(mappingId); const mappingEntityTypeId = getMappingEntityTypeId(item); @@ -139,6 +139,22 @@ export function generateItem(item, abilityLookup, proficiencyBonus, modifiers, e !!ValueAccessors.getValue(dataLookup[AdjustmentTypeEnum.IS_PACT_WEAPON]); } const proficiency = deriveIsProficient(item, modifiers, isPactWeapon, typeValueLookup, ruleData); + //handle any replacement stats granted by modifiers from class features + const appliedWeaponReplacementStats = []; + let canUseWeaponReplacementStats = false; + if (replacementWeaponStats.length > 0 && + (isBaseWeapon(item) || validateIsWeaponLike(item)) && + !isAmmunition(item)) { + canUseWeaponReplacementStats = true; + // check if any of the replacement stats are being used + Object.keys(STAT_REPLACEMENT_TYPE_LOOKUP).forEach((adjustmentType) => { + const isUsingAdjustmentType = dataLookup.hasOwnProperty(adjustmentType) && !!ValueAccessors.getValue(dataLookup[adjustmentType]); + // if the adjustment type is being used, add it to the applied weapon replacement stats + if (isUsingAdjustmentType) { + appliedWeaponReplacementStats.push(STAT_REPLACEMENT_TYPE_LOOKUP[adjustmentType]); + } + }); + } let canHexWeapon = false; let isHexWeapon = false; if (hexWeaponEnabled && (isBaseWeapon(item) || validateIsWeaponLike(item)) && !isAmmunition(item)) { @@ -185,7 +201,7 @@ export function generateItem(item, abilityLookup, proficiencyBonus, modifiers, e const bonusToHitModifiers = modifiers.filter((modifier) => ModifierValidators.isValidBonusWeaponToHitModifier(modifier, item, ruleData)); const bonusToHitModifierTotal = ModifierDerivers.sumModifiers(bonusToHitModifiers, abilityLookup); //get all the available ability stats for the weapon - const availableAbilities = deriveAvailableWeaponAbilities(item, modifiers, martialArtsLevelScale, isDedicatedWeapon, isPactWeapon, isHexWeapon); + const availableAbilities = deriveAvailableWeaponAbilities(item, modifiers, martialArtsLevelScale, isDedicatedWeapon, isPactWeapon, isHexWeapon, appliedWeaponReplacementStats); // transform the available ability stats to have toHit and damage bonuses const abilityPossibilities = CharacterDerivers.deriveAttackAbilityPossibilities(availableAbilities, modifiers, weaponProficiencyBonus, abilityLookup); const bestAbility = HelperUtils.getLast(abilityPossibilities, ['toHit', 'modifier']); @@ -267,7 +283,7 @@ export function generateItem(item, abilityLookup, proficiencyBonus, modifiers, e hexWeaponEnabled, pactWeaponEnabled, damage: newDamage, versatileDamage, additionalDamages, - reach, masteryName: deriveMasteryName(item, modifiers), primarySourceCategoryId: derivePrimarySourceCategoryId(item, ruleData), secondarySourceCategoryIds: deriveSecondarySourceCategoryIds(item, ruleData) }); + reach, masteryName: deriveMasteryName(item, modifiers), primarySourceCategoryId: derivePrimarySourceCategoryId(item, ruleData), secondarySourceCategoryIds: deriveSecondarySourceCategoryIds(item, ruleData), replacementWeaponStats: canUseWeaponReplacementStats ? replacementWeaponStats : [], appliedWeaponReplacementStats: canUseWeaponReplacementStats ? appliedWeaponReplacementStats : [] }); } /** * @@ -341,7 +357,7 @@ function updateBaseItemSpells(item, spell) { * @param characterId */ export function generateGearWeaponItems(items, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, ruleData, hack__characterSpecialWeaponPropertiesEnabled, characterId) { - const { hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, dedicatedWeaponEnabled } = hack__characterSpecialWeaponPropertiesEnabled; + const { hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, dedicatedWeaponEnabled, replacementWeaponStats, } = hack__characterSpecialWeaponPropertiesEnabled; return items .filter(isGearContract) .filter((item) => hasItemWeaponBehaviors(item)) @@ -352,7 +368,7 @@ export function generateGearWeaponItems(items, abilityLookup, proficiencyBonus, if (simulatedBaseItem === null) { return null; } - return generateItem(simulatedBaseItem, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, ruleData, dedicatedWeaponEnabled); + return generateItem(simulatedBaseItem, abilityLookup, proficiencyBonus, modifiers, entityValueLookup, typeValueLookup, valueLookup, martialArtsLevelScale, hexWeaponEnabled, pactWeaponEnabled, improvedPactWeaponEnabled, ruleData, dedicatedWeaponEnabled, replacementWeaponStats); }) .filter(TypeScriptUtils.isNotNullOrUndefined)) .reduce((acc, itemBehaviors) => { diff --git a/ddb_main/packages/rules-engine/es/engine/Item/simulators.js b/ddb_main/packages/rules-engine/es/engine/Item/simulators.js index 433ffd2..5f8e209 100644 --- a/ddb_main/packages/rules-engine/es/engine/Item/simulators.js +++ b/ddb_main/packages/rules-engine/es/engine/Item/simulators.js @@ -20,5 +20,5 @@ export function simulateItem(itemDefinition, globalModifiers, valueLookupByType, const reach = deriveWeaponReach(simulatedItem, globalModifiers, ruleData); const proficiency = deriveIsProficient(simulatedItem, globalModifiers, false, valueLookupByType, ruleData); return Object.assign(Object.assign({}, simulatedItem), { armorClass: deriveArmorClass(simulatedItem), proficiency, quantity: getBundleSize(simulatedItem), canBeDedicatedWeapon: false, canHexWeapon: false, canOffhand: false, canPactWeapon: false, hexWeaponEnabled: false, isDedicatedWeapon: false, isHexWeapon: false, isPactWeapon: false, masteryName: null, metaItems: [], pactWeaponEnabled: false, toHit: 0, damage: getDefinitionDamage(simulatedItem), categoryInfo: deriveCategoryInfo(simulatedItem, ruleData), stealthCheck: StealthCheckTypeEnum.NONE, additionalDamages, - reach, versatileDamage: null, primarySourceCategoryId: derivePrimarySourceCategoryId(simulatedItem, ruleData), secondarySourceCategoryIds: deriveSecondarySourceCategoryIds(simulatedItem, ruleData) }); + reach, versatileDamage: null, primarySourceCategoryId: derivePrimarySourceCategoryId(simulatedItem, ruleData), secondarySourceCategoryIds: deriveSecondarySourceCategoryIds(simulatedItem, ruleData), appliedWeaponReplacementStats: [], replacementWeaponStats: [] }); } diff --git a/ddb_main/packages/rules-engine/es/engine/Modifier/constants.js b/ddb_main/packages/rules-engine/es/engine/Modifier/constants.js index c1cfc98..6018aee 100644 --- a/ddb_main/packages/rules-engine/es/engine/Modifier/constants.js +++ b/ddb_main/packages/rules-engine/es/engine/Modifier/constants.js @@ -220,6 +220,13 @@ export var ModifierSubTypeEnum; // Warlock Feature ModifierSubTypeEnum["ENABLE_PACT_WEAPON"] = "enable-pact-weapon"; ModifierSubTypeEnum["ENABLE_HEX_WEAPON"] = "enable-hex-weapon"; + // Enable Feature (used in tandem with REPLACE_WEAPON_ABILITY modifier) + ModifierSubTypeEnum["ENABLE_STRENGTH"] = "enable-strength"; + ModifierSubTypeEnum["ENABLE_DEXTERITY"] = "enable-dexterity"; + ModifierSubTypeEnum["ENABLE_CONSTITUTION"] = "enable-constitution"; + ModifierSubTypeEnum["ENABLE_INTELLIGENCE"] = "enable-intelligence"; + ModifierSubTypeEnum["ENABLE_WISDOM"] = "enable-wisdom"; + ModifierSubTypeEnum["ENABLE_CHARISMA"] = "enable-charisma"; })(ModifierSubTypeEnum || (ModifierSubTypeEnum = {})); export const SIZE_LIST = [ ModifierSubTypeEnum.GARGANTUAN, @@ -269,10 +276,7 @@ export const HEAVY_ARMOR_LIST = [ ModifierSubTypeEnum.POWERED_ARMOR, ModifierSubTypeEnum.DIVERS_ARMOR, ]; -export const SHIELDS_LIST = [ - ModifierSubTypeEnum.SHIELD, - ModifierSubTypeEnum.POT_LID, -]; +export const SHIELDS_LIST = [ModifierSubTypeEnum.SHIELD, ModifierSubTypeEnum.POT_LID]; export const ALL_ARMOR_LIST = [ ...LIGHT_ARMOR_LIST, ...MEDIUM_ARMOR_LIST, diff --git a/ddb_main/packages/rules-engine/es/engine/Modifier/validators.js b/ddb_main/packages/rules-engine/es/engine/Modifier/validators.js index ec91b73..a346b88 100644 --- a/ddb_main/packages/rules-engine/es/engine/Modifier/validators.js +++ b/ddb_main/packages/rules-engine/es/engine/Modifier/validators.js @@ -306,6 +306,23 @@ export function isEnableHexWeaponModifier(modifier) { return (getType(modifier) === ModifierTypeEnum.ENABLE_FEATURE && getSubType(modifier) === ModifierSubTypeEnum.ENABLE_HEX_WEAPON); } +export function isEnablesAbilityStatModifier(modifier) { + if (getType(modifier) !== ModifierTypeEnum.ENABLE_FEATURE) { + return false; + } + const subType = getSubType(modifier); + switch (subType) { + case ModifierSubTypeEnum.ENABLE_STRENGTH: + case ModifierSubTypeEnum.ENABLE_DEXTERITY: + case ModifierSubTypeEnum.ENABLE_CONSTITUTION: + case ModifierSubTypeEnum.ENABLE_INTELLIGENCE: + case ModifierSubTypeEnum.ENABLE_WISDOM: + case ModifierSubTypeEnum.ENABLE_CHARISMA: + return true; + default: + return false; + } +} /** * * @param modifier diff --git a/ddb_main/packages/rules-engine/es/engine/Value/constants.js b/ddb_main/packages/rules-engine/es/engine/Value/constants.js index fa57325..c9e8f30 100644 --- a/ddb_main/packages/rules-engine/es/engine/Value/constants.js +++ b/ddb_main/packages/rules-engine/es/engine/Value/constants.js @@ -50,6 +50,12 @@ export var AdjustmentTypeEnum; AdjustmentTypeEnum[AdjustmentTypeEnum["IS_DEDICATED_WEAPON"] = 48] = "IS_DEDICATED_WEAPON"; AdjustmentTypeEnum[AdjustmentTypeEnum["CAPACITY_OVERRIDE"] = 49] = "CAPACITY_OVERRIDE"; AdjustmentTypeEnum[AdjustmentTypeEnum["CAPACITY_WEIGHT_OVERRIDE"] = 50] = "CAPACITY_WEIGHT_OVERRIDE"; + AdjustmentTypeEnum[AdjustmentTypeEnum["USE_STRENGTH"] = 51] = "USE_STRENGTH"; + AdjustmentTypeEnum[AdjustmentTypeEnum["USE_DEXTERITY"] = 52] = "USE_DEXTERITY"; + AdjustmentTypeEnum[AdjustmentTypeEnum["USE_CONSTITUTION"] = 53] = "USE_CONSTITUTION"; + AdjustmentTypeEnum[AdjustmentTypeEnum["USE_INTELLIGENCE"] = 54] = "USE_INTELLIGENCE"; + AdjustmentTypeEnum[AdjustmentTypeEnum["USE_WISDOM"] = 55] = "USE_WISDOM"; + AdjustmentTypeEnum[AdjustmentTypeEnum["USE_CHARISMA"] = 56] = "USE_CHARISMA"; })(AdjustmentTypeEnum || (AdjustmentTypeEnum = {})); export const ProficiencyAdjustmentTypeEnum = [ AdjustmentTypeEnum.IS_PROFICIENT, diff --git a/ddb_main/packages/rules-engine/es/managers/ItemManager.js b/ddb_main/packages/rules-engine/es/managers/ItemManager.js index 14b6fc2..b0923b0 100644 --- a/ddb_main/packages/rules-engine/es/managers/ItemManager.js +++ b/ddb_main/packages/rules-engine/es/managers/ItemManager.js @@ -88,6 +88,8 @@ export class ItemManager extends PartyManager { this.isPactWeapon = () => ItemAccessors.isPactWeapon(this.item); this.isDedicatedWeapon = () => ItemAccessors.isDedicatedWeapon(this.item); this.isOffhand = () => ItemAccessors.isOffhand(this.item); + this.getReplacementWeaponStats = () => ItemAccessors.getReplacementWeaponStats(this.item); + this.getAppliedWeaponReplacementStats = () => ItemAccessors.getAppliedWeaponReplacementStats(this.item); this.getBaseArmorName = () => ItemAccessors.getBaseArmorName(this.item); this.getSubType = () => ItemAccessors.getSubType(this.item); this.getQuantity = () => ItemAccessors.getQuantity(this.item); @@ -167,6 +169,8 @@ export class ItemManager extends PartyManager { return ItemNotes.getNoteComponents(this.item, weaponSpellDamageGroups, ruleData, abilityLookup, proficiencyBonus); }; this.getMetaText = () => { + const appliedWeaponReplacementStats = this.getAppliedWeaponReplacementStats(); + const ruleData = rulesEngineSelectors.getRuleData(this.state); let metaItems = []; if (this.isLegacy()) { metaItems.push('Legacy'); @@ -184,6 +188,11 @@ export class ItemManager extends PartyManager { if (this.isDedicatedWeapon()) { metaItems.push('Dedicated Weapon'); } + if (appliedWeaponReplacementStats.length > 0) { + appliedWeaponReplacementStats.forEach((statId) => { + metaItems.push(`Using ${RuleDataUtils.getStatNameById(statId, ruleData, true)}`); + }); + } if (this.isOffhand()) { metaItems.push('Dual Wield'); } diff --git a/ddb_main/packages/rules-engine/es/selectors/composite/engine.js b/ddb_main/packages/rules-engine/es/selectors/composite/engine.js index 69c06c0..21972a2 100644 --- a/ddb_main/packages/rules-engine/es/selectors/composite/engine.js +++ b/ddb_main/packages/rules-engine/es/selectors/composite/engine.js @@ -695,10 +695,20 @@ export const getPactWeaponEnabled = createSelector([getClasses], ClassGenerators * @returns {boolean} */ export const getImprovedPactWeaponEnabled = createSelector([getClasses], ClassGenerators.generateImprovedPactWeaponEnabled); +/** + * @returns {boolean} + */ +export const getReplacementWeaponAbilityStats = createSelector([getClasses], ClassGenerators.generateReplacementWeaponAbilityStats); /** * TODO: add return here */ -export const hack__getSpecialWeaponPropertiesEnabled = createSelector([getHexWeaponEnabled, getPactWeaponEnabled, getImprovedPactWeaponEnabled, getDedicatedWeaponEnabled], CharacterHacks.hack__generateSpecialWeaponPropertiesEnabled); +export const hack__getSpecialWeaponPropertiesEnabled = createSelector([ + getHexWeaponEnabled, + getPactWeaponEnabled, + getImprovedPactWeaponEnabled, + getDedicatedWeaponEnabled, + getReplacementWeaponAbilityStats, +], CharacterHacks.hack__generateSpecialWeaponPropertiesEnabled); /** * TODO v5.1: should be able to remove this selector and all usages after mobile can support customItems as Items * @returns {Array} diff --git a/ddb_main/subApps/listing/components/CharacterGrid/CharacterGrid.tsx b/ddb_main/subApps/listing/components/CharacterGrid/CharacterGrid.tsx index 6e93979..a1f1cf7 100644 --- a/ddb_main/subApps/listing/components/CharacterGrid/CharacterGrid.tsx +++ b/ddb_main/subApps/listing/components/CharacterGrid/CharacterGrid.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useState } from "react"; import ArrowDown from "@dndbeyond/fontawesome-cache/svgs/regular/arrow-down-to-line.svg"; -import { Button } from "@dndbeyond/ttui/components/Button"; +import { Button } from "~/components/Button"; import { MaxCharactersDialog } from "~/components/MaxCharactersDialog"; import config from "~/config"; import { UserPreferenceProvider } from "~/tools/js/smartComponents/UserPreference"; @@ -263,6 +263,7 @@ export const CharacterGrid: React.FC = ({ href="https://media.dndbeyond.com/compendium-images/free-rules/ph/character-sheet.pdf" target="_blank" className={styles.pdfLink} + rel="noreferrer" > Download a blank character sheet diff --git a/ddb_main/subApps/listing/components/CharacterGrid/SearchSort/SearchSort.tsx b/ddb_main/subApps/listing/components/CharacterGrid/SearchSort/SearchSort.tsx index 9aa8274..a1c0960 100644 --- a/ddb_main/subApps/listing/components/CharacterGrid/SearchSort/SearchSort.tsx +++ b/ddb_main/subApps/listing/components/CharacterGrid/SearchSort/SearchSort.tsx @@ -131,7 +131,6 @@ export const SearchSort: FC = ({