From 0b403376c5aae66f081d31bcbe1df5a2bdbbae44 Mon Sep 17 00:00:00 2001 From: David Kruger Date: Wed, 18 Jun 2025 01:00:16 -0700 Subject: [PATCH] New source found from dndbeyond.com --- ddb_main/components/SpellName/SpellName.tsx | 36 +++-- ddb_main/hooks/useCharacterEngine.ts | 3 - .../es/engine/Background/accessors.js | 3 + .../rules-engine/es/engine/Entity/utils.js | 2 +- .../rules-engine/es/engine/Feat/accessors.js | 4 + .../rules-engine/es/engine/Feat/generators.js | 19 ++- .../es/engine/Prerequisite/accessors.js | 7 + .../es/engine/Prerequisite/utils.js | 50 +++---- .../es/engine/Prerequisite/validators.js | 66 +++++---- .../es/engine/Spell/generators.js | 12 +- .../rules-engine/es/managers/FeatManager.js | 45 ++++++ .../rules-engine/es/sagas/character/saga.js | 6 +- .../rules-engine/es/sagas/serviceData/saga.js | 6 +- .../es/selectors/composite/engine.js | 7 +- .../HpManageModal/HpManageModal.tsx | 83 +++++------ .../components/InputField/InputField.tsx | 132 +++++++++++++++++ .../components/PortraitName/PortraitName.tsx | 20 ++- .../panes/FeatsManagePane/Feat/Feat.tsx | 8 +- .../AbilityScoreManagerManual.tsx | 35 +++-- .../DescriptionManage/DescriptionManage.tsx | 81 +++++----- .../pages/HomeBasicInfo/HomeBasicInfo.tsx | 66 +++++---- .../components/ActionDetail/ActionDetail.tsx | 26 ++-- .../components/SpellManager/SpellManager.tsx | 2 +- .../common/FormInputField/FormInputField.tsx | 139 ------------------ .../PrerequisiteFailureSummary.tsx | 44 ------ .../PrerequisiteFailureSummary/index.ts | 4 - .../HpManageModal/styles.module.css?c97e | 2 +- .../InputField/styles.module.css?ec81 | 2 + .../styles.module.css?bb47 | 2 + .../DescriptionManage/styles.module.css?cacc | 2 +- 30 files changed, 496 insertions(+), 418 deletions(-) create mode 100644 ddb_main/subApps/builder/components/InputField/InputField.tsx delete mode 100644 ddb_main/tools/js/Shared/components/common/FormInputField/FormInputField.tsx delete mode 100644 ddb_main/tools/js/smartComponents/PrerequisiteFailureSummary/PrerequisiteFailureSummary.tsx delete mode 100644 ddb_main/tools/js/smartComponents/PrerequisiteFailureSummary/index.ts create mode 100644 ddb_main/webpack:/@dndbeyond/character-app/src/subApps/builder/components/InputField/styles.module.css?ec81 create mode 100644 ddb_main/webpack:/@dndbeyond/character-app/src/tools/js/CharacterBuilder/components/AbilityScoreManagerManual/styles.module.css?bb47 diff --git a/ddb_main/components/SpellName/SpellName.tsx b/ddb_main/components/SpellName/SpellName.tsx index 93f7755..f565139 100644 --- a/ddb_main/components/SpellName/SpellName.tsx +++ b/ddb_main/components/SpellName/SpellName.tsx @@ -2,7 +2,6 @@ import clsx from "clsx"; import { FC, HTMLAttributes, useEffect, useState } from "react"; import { useSelector } from "react-redux"; -import { Tooltip } from "@dndbeyond/character-common-components/es"; import { SpellUtils, EntityUtils, @@ -12,6 +11,7 @@ import { characterEnvSelectors, } from "@dndbeyond/character-rules-engine/es"; +import { Tooltip } from "~/components/Tooltip"; import { useCharacterTheme } from "~/contexts/CharacterTheme"; import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick"; @@ -77,23 +77,29 @@ export const SpellName: FC = ({ {...props} > {showExpandedType && expandedInfoText !== "" && ( - - + - + <> + + + + + + )} {SpellUtils.getName(spell)} {SpellUtils.isCustomized(spell) && ( - - * - + <> + + * + + + )} {showIcons && SpellUtils.getConcentration(spell) && ( ({ featOption: useSelector(s.getFeatOptionLookup), featChoice: useSelector(s.getFeatChoiceLookup), gearWeaponItems: useSelector(s.getGearWeaponItems), - globalBackgroundSpellListIds: useSelector(s.getGlobalBackgroundSpellListIds), - globalRaceSpellListIds: useSelector(s.getGlobalRaceSpellListIds), - globalSpellListIds: useSelector(s.getGlobalSpellListIds), hasInitiativeAdvantage: useSelector(s.getHasInitiativeAdvantage), hasMaxAttunedItems: useSelector(s.hasMaxAttunedItems), hasSpells: useSelector(s.hasSpells), diff --git a/ddb_main/packages/rules-engine/es/engine/Background/accessors.js b/ddb_main/packages/rules-engine/es/engine/Background/accessors.js index 06b6b06..097f986 100644 --- a/ddb_main/packages/rules-engine/es/engine/Background/accessors.js +++ b/ddb_main/packages/rules-engine/es/engine/Background/accessors.js @@ -450,3 +450,6 @@ export function getFeatListContracts(contract) { var _a, _b; return (_b = (_a = contract === null || contract === void 0 ? void 0 : contract.definition) === null || _a === void 0 ? void 0 : _a.grantedFeats) !== null && _b !== void 0 ? _b : []; } +export function getFeatLists(background) { + return background.featLists; +} diff --git a/ddb_main/packages/rules-engine/es/engine/Entity/utils.js b/ddb_main/packages/rules-engine/es/engine/Entity/utils.js index 735b31e..f20dbf1 100644 --- a/ddb_main/packages/rules-engine/es/engine/Entity/utils.js +++ b/ddb_main/packages/rules-engine/es/engine/Entity/utils.js @@ -218,7 +218,7 @@ export function getDataOriginRefName(ref, refData, defaultValue, tryParent = fal break; } case DataOriginTypeEnum.UNKNOWN: { - extraDisplay = defaultValue !== null && defaultValue !== void 0 ? defaultValue : 'No Origin'; + extraDisplay = defaultValue !== null && defaultValue !== void 0 ? defaultValue : 'Unknown Origin'; break; } default: diff --git a/ddb_main/packages/rules-engine/es/engine/Feat/accessors.js b/ddb_main/packages/rules-engine/es/engine/Feat/accessors.js index cdb803c..6b3f278 100644 --- a/ddb_main/packages/rules-engine/es/engine/Feat/accessors.js +++ b/ddb_main/packages/rules-engine/es/engine/Feat/accessors.js @@ -281,3 +281,7 @@ export function getDefinitionRepeatableParentId(feat) { export function getRepeatableParentId(feat) { return getDefinitionRepeatableParentId(feat); } +export function getSpellListIds(feat) { + var _a, _b; + return (_b = (_a = getDefinition(feat)) === null || _a === void 0 ? void 0 : _a.spellListIds) !== null && _b !== void 0 ? _b : []; +} diff --git a/ddb_main/packages/rules-engine/es/engine/Feat/generators.js b/ddb_main/packages/rules-engine/es/engine/Feat/generators.js index 814486c..80ab8e5 100644 --- a/ddb_main/packages/rules-engine/es/engine/Feat/generators.js +++ b/ddb_main/packages/rules-engine/es/engine/Feat/generators.js @@ -14,7 +14,7 @@ import { ModifierGenerators } from '../Modifier'; import { OptionAccessors, OptionGenerators } from '../Option'; import { RaceAccessors } from '../Race'; import { SpellGenerators } from '../Spell'; -import { getComponentId, getComponentTypeId, getCreatureRules, getEntityTypeId, getId, getModifiers, getOptions, getUniqueKey, } from './accessors'; +import { getComponentId, getComponentTypeId, getCreatureRules, getEntityTypeId, getId, getModifiers, getOptions, getSpellListIds, getUniqueKey, } from './accessors'; /** * * @param feat @@ -342,3 +342,20 @@ export function generateDataOriginPairedFeats(feats) { } return lookup; } +/** + * Generates a list of spell list IDs from the feats. + * This is used to determine which spell lists are available globally. + * + * @param feats - Array of Feat objects + * @returns Array of spell list IDs + */ +export function generateGlobalFeatsSpellListIds(feats) { + const spellListIds = []; + feats.forEach((feat) => { + const spellListIdsForFeat = getSpellListIds(feat); + if (spellListIdsForFeat.length > 0) { + spellListIds.push(...spellListIdsForFeat); + } + }); + return spellListIds; +} diff --git a/ddb_main/packages/rules-engine/es/engine/Prerequisite/accessors.js b/ddb_main/packages/rules-engine/es/engine/Prerequisite/accessors.js index 4ba7c96..ba643ae 100644 --- a/ddb_main/packages/rules-engine/es/engine/Prerequisite/accessors.js +++ b/ddb_main/packages/rules-engine/es/engine/Prerequisite/accessors.js @@ -79,6 +79,13 @@ export function getDescription(prerequisiteGrouping) { export function getSubType(prerequisite) { return prerequisite.subType; } +/** + * + * @param prerequisite + */ +export function getShouldExclude(prerequisite) { + return prerequisite.shouldExclude; +} /** * @param prerequisite */ diff --git a/ddb_main/packages/rules-engine/es/engine/Prerequisite/utils.js b/ddb_main/packages/rules-engine/es/engine/Prerequisite/utils.js index c769fef..26d80e1 100644 --- a/ddb_main/packages/rules-engine/es/engine/Prerequisite/utils.js +++ b/ddb_main/packages/rules-engine/es/engine/Prerequisite/utils.js @@ -1,6 +1,6 @@ import { AbilityAccessors } from '../Ability'; import { FormatUtils } from '../Format'; -import { getEntityId, getFriendlySubtypeName, getPrerequisites, getSubType, getType, getValue } from './accessors'; +import { getEntityId, getFriendlySubtypeName, getPrerequisites, getSubType, getType, getValue, getShouldExclude, } from './accessors'; import { PrerequisiteSubTypeEnum, PrerequisiteTypeEnum } from './constants'; import { validatePrerequisite } from './validators'; /** @@ -13,19 +13,19 @@ export function getPrerequisiteFailure(prerequisite, prerequisiteData) { case PrerequisiteTypeEnum.ABILITY_SCORE: return getPrerequisiteFailureAbilityScore(prerequisite, prerequisiteData); case PrerequisiteTypeEnum.PROFICIENCY: - return getPrerequisiteFailureProficiency(prerequisite, prerequisiteData); + return getPrerequisiteFailureProficiency(prerequisite); case PrerequisiteTypeEnum.SPECIES: - return getPrerequisiteFailureRace(prerequisite, prerequisiteData); + return getPrerequisiteFailureRace(prerequisite); case PrerequisiteTypeEnum.SIZE: - return getPrerequisiteFailureSize(prerequisite, prerequisiteData); + return getPrerequisiteFailureSize(prerequisite); case PrerequisiteTypeEnum.SPECIES_OPTION: - return getPrerequisiteFailureSubrace(prerequisite, prerequisiteData); + return getPrerequisiteFailureSubrace(prerequisite); case PrerequisiteTypeEnum.LEVEL: - return getPrerequisiteFailureLevel(prerequisite, prerequisiteData); + return getPrerequisiteFailureLevel(prerequisite); case PrerequisiteTypeEnum.CLASS: - return getPrerequisiteFailureClass(prerequisite, prerequisiteData); + return getPrerequisiteFailureClass(prerequisite); case PrerequisiteTypeEnum.FEAT: - return getPrerequisiteFailureFeat(prerequisite, prerequisiteData); + return getPrerequisiteFailureFeat(prerequisite); case PrerequisiteTypeEnum.CLASS_FEATURE: return getPrerequisiteFailureClassFeature(prerequisite); case PrerequisiteTypeEnum.CUSTOM_VALUE: @@ -39,7 +39,7 @@ export function getPrerequisiteFailure(prerequisite, prerequisiteData) { * @param prerequisite * @param prerequisiteData */ -export function getPrerequisiteFailureLevel(prerequisite, prerequisiteData) { +export function getPrerequisiteFailureLevel(prerequisite) { let requiredDescription = ''; switch (getSubType(prerequisite)) { case PrerequisiteSubTypeEnum.CHARACTER_LEVEL: @@ -56,6 +56,7 @@ export function getPrerequisiteFailureLevel(prerequisite, prerequisiteData) { requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription, }, + shouldExclude: !!getShouldExclude(prerequisite), }; } /** @@ -81,6 +82,7 @@ export function getPrerequisiteFailureAbilityScore(prerequisite, prerequisiteDat requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription: `${getFriendlySubtypeName(prerequisite)} ${getValue(prerequisite)}+`, }, + shouldExclude: !!getShouldExclude(prerequisite), }; } /** @@ -88,13 +90,14 @@ export function getPrerequisiteFailureAbilityScore(prerequisite, prerequisiteDat * @param prerequisite * @param prerequisiteData */ -export function getPrerequisiteFailureProficiency(prerequisite, prerequisiteData) { +export function getPrerequisiteFailureProficiency(prerequisite) { return { type: PrerequisiteTypeEnum.PROFICIENCY, data: { requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription: `${getFriendlySubtypeName(prerequisite)} Proficiency`, }, + shouldExclude: !!getShouldExclude(prerequisite), }; } /** @@ -102,13 +105,14 @@ export function getPrerequisiteFailureProficiency(prerequisite, prerequisiteData * @param prerequisite * @param prerequisiteData */ -export function getPrerequisiteFailureRace(prerequisite, prerequisiteData) { +export function getPrerequisiteFailureRace(prerequisite) { return { type: PrerequisiteTypeEnum.SPECIES, data: { requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription: getFriendlySubtypeName(prerequisite), }, + shouldExclude: !!getShouldExclude(prerequisite), }; } /** @@ -116,13 +120,14 @@ export function getPrerequisiteFailureRace(prerequisite, prerequisiteData) { * @param prerequisite * @param prerequisiteData */ -export function getPrerequisiteFailureSubrace(prerequisite, prerequisiteData) { +export function getPrerequisiteFailureSubrace(prerequisite) { return { type: PrerequisiteTypeEnum.SPECIES_OPTION, data: { requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription: getFriendlySubtypeName(prerequisite), }, + shouldExclude: !!getShouldExclude(prerequisite), }; } /** @@ -130,13 +135,14 @@ export function getPrerequisiteFailureSubrace(prerequisite, prerequisiteData) { * @param prerequisite * @param prerequisiteData */ -export function getPrerequisiteFailureSize(prerequisite, prerequisiteData) { +export function getPrerequisiteFailureSize(prerequisite) { return { type: PrerequisiteTypeEnum.SIZE, data: { requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription: `${getFriendlySubtypeName(prerequisite)} Size`, }, + shouldExclude: !!getShouldExclude(prerequisite), }; } /** @@ -144,13 +150,14 @@ export function getPrerequisiteFailureSize(prerequisite, prerequisiteData) { * @param prerequisite * @param prerequisiteData */ -export function getPrerequisiteFailureClass(prerequisite, prerequisiteData) { +export function getPrerequisiteFailureClass(prerequisite) { return { type: PrerequisiteTypeEnum.CLASS, data: { requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription: `${getFriendlySubtypeName(prerequisite)}`, }, + shouldExclude: !!getShouldExclude(prerequisite), }; } export const getPrerequisiteFailureClassFeature = (prerequisite) => ({ @@ -159,19 +166,21 @@ export const getPrerequisiteFailureClassFeature = (prerequisite) => ({ requiredChoice: prerequisite.friendlySubTypeName, requiredDescription: prerequisite.friendlySubTypeName, }, + shouldExclude: !!getShouldExclude(prerequisite), }); /** * * @param prerequisite * @param prerequisiteData */ -export function getPrerequisiteFailureFeat(prerequisite, prerequisiteData) { +export function getPrerequisiteFailureFeat(prerequisite) { return { type: PrerequisiteTypeEnum.FEAT, data: { requiredChoice: getFriendlySubtypeName(prerequisite), requiredDescription: `${getFriendlySubtypeName(prerequisite)}`, }, + shouldExclude: !!getShouldExclude(prerequisite), }; } /** @@ -198,14 +207,3 @@ export function getPrerequisiteGroupingFailures(prerequisiteGrouping, prerequisi } return groupingFailures.filter((group) => group.length); } -/** - * Get all prerequisites of a specific type - * @param type - * @param prereqGroups - */ -export function getPrereqsByType(type, prereqGroups) { - return prereqGroups.reduce((acc, prereq) => { - const featMappings = getPrerequisites(prereq).filter((mapping) => getType(mapping) === type); - return acc.concat(featMappings); - }, []); -} diff --git a/ddb_main/packages/rules-engine/es/engine/Prerequisite/validators.js b/ddb_main/packages/rules-engine/es/engine/Prerequisite/validators.js index 6385815..77dc559 100644 --- a/ddb_main/packages/rules-engine/es/engine/Prerequisite/validators.js +++ b/ddb_main/packages/rules-engine/es/engine/Prerequisite/validators.js @@ -2,9 +2,8 @@ import { AbilityAccessors } from '../Ability'; import { ClassAccessors } from '../Class'; import { HelperUtils } from '../Helper'; import { RaceAccessors } from '../Race'; -import { getEntityId, getEntityKey, getEntityTypeId, getPrerequisites, getSubType, getType, getValue, } from './accessors'; +import { getEntityId, getEntityKey, getEntityTypeId, getPrerequisites, getShouldExclude, getSubType, getType, getValue, } from './accessors'; import { PrerequisiteSubTypeEnum, PrerequisiteTypeEnum } from './constants'; -import { getPrereqsByType } from './utils'; /** * * @param prerequisiteGrouping @@ -63,9 +62,12 @@ export function validatePrerequisite(prerequisite, prerequisiteData) { return true; } export function validatePrerequisiteClassFeature(prerequisite, prerequisiteData) { + const shouldExclude = getShouldExclude(prerequisite); if (!prerequisite.entityId) return true; - return Object.prototype.hasOwnProperty.call(prerequisiteData.classFeatureLookup, prerequisite.entityId); + // check if class feature exists in the class feature lookup + const hasClassFeature = Object.prototype.hasOwnProperty.call(prerequisiteData.classFeatureLookup, prerequisite.entityId); + return shouldExclude ? !hasClassFeature : hasClassFeature; } /** * @@ -73,10 +75,13 @@ export function validatePrerequisiteClassFeature(prerequisite, prerequisiteData) * @param prerequisiteData */ export function validatePrerequisiteLevel(prerequisite, prerequisiteData) { + const shouldExclude = getShouldExclude(prerequisite); switch (getSubType(prerequisite)) { case PrerequisiteSubTypeEnum.CHARACTER_LEVEL: const value = getValue(prerequisite); - return prerequisiteData.characterLevel >= (value ? value : 0); + // check if the character level is greater than or equal to the value + const characterLevelBeatsValue = prerequisiteData.characterLevel >= (value || 0); + return shouldExclude ? !characterLevelBeatsValue : characterLevelBeatsValue; default: // not implemented } @@ -101,6 +106,12 @@ export function validatePrerequisiteAbilityScore(prerequisite, prerequisiteData) if (value === null) { return true; } + const shouldExclude = getShouldExclude(prerequisite); + // if shouldExclude is true, we want to check if the ability score is less than the value + if (shouldExclude) { + return abilityScore < value; + } + // if shouldExclude is false, we want to check if the ability score is greater than or equal to the value return abilityScore >= value; } /** @@ -109,7 +120,10 @@ export function validatePrerequisiteAbilityScore(prerequisite, prerequisiteData) * @param prerequisiteData */ export function validatePrerequisiteProficiency(prerequisite, prerequisiteData) { - return prerequisiteData.proficiencyLookup.hasOwnProperty(getEntityKey(prerequisite)); + const shouldExclude = getShouldExclude(prerequisite); + //check if the proficiency exists in the proficiency lookup + const hasProficiency = prerequisiteData.proficiencyLookup.hasOwnProperty(getEntityKey(prerequisite)); + return shouldExclude ? !hasProficiency : hasProficiency; } /** * @@ -120,10 +134,11 @@ export function validatePrerequisiteRace(prerequisite, prerequisiteData) { if (!prerequisiteData.race) { return true; } + const shouldExclude = getShouldExclude(prerequisite); // check if race is a base race if (RaceAccessors.getBaseRaceId(prerequisiteData.race) === getEntityId(prerequisite) && RaceAccessors.getBaseRaceTypeId(prerequisiteData.race) === getEntityTypeId(prerequisite)) { - return true; + return !shouldExclude; } // check if race is specific race return validatePrerequisiteEntityRace(prerequisite, prerequisiteData); @@ -149,8 +164,11 @@ function validatePrerequisiteEntityRace(prerequisite, prerequisiteData) { if (!prerequisiteData.race) { return true; } - return (RaceAccessors.getEntityRaceId(prerequisiteData.race) === getEntityId(prerequisite) && - RaceAccessors.getEntityRaceTypeId(prerequisiteData.race) === getEntityTypeId(prerequisite)); + const shouldExclude = getShouldExclude(prerequisite); + // check if the race type and id match the prerequisite + const isMatch = RaceAccessors.getEntityRaceId(prerequisiteData.race) === getEntityId(prerequisite) && + RaceAccessors.getEntityRaceTypeId(prerequisiteData.race) === getEntityTypeId(prerequisite); + return shouldExclude ? !isMatch : isMatch; } /** * @@ -165,7 +183,10 @@ export function validatePrerequisiteSize(prerequisite, prerequisiteData) { if (!sizeInfo) { return true; } - return sizeInfo.id === getEntityId(prerequisite) && sizeInfo.entityTypeId === getEntityTypeId(prerequisite); + const shouldExclude = getShouldExclude(prerequisite); + // check if size matches the prerequisite size + const isMatch = sizeInfo.id === getEntityId(prerequisite) && sizeInfo.entityTypeId === getEntityTypeId(prerequisite); + return shouldExclude ? !isMatch : isMatch; } /** * @@ -177,24 +198,10 @@ export function validatePrerequisiteClass(prerequisite, prerequisiteData) { if (entityId === null) { return true; } - return Object.values(prerequisiteData.baseClassLookup).some((charClass) => ClassAccessors.getId(charClass) === entityId); -} -/** - * @param prerequisiteGrouping - * @param prerequisiteData - */ -export function validatePrerequisiteGroupingFeat(prerequisiteGrouping, prerequisiteData) { - if (prerequisiteGrouping === null) { - return true; - } - const mappings = getPrereqsByType(PrerequisiteTypeEnum.FEAT, prerequisiteGrouping); - let isValid = true; - mappings.forEach((prerequisite) => { - if (!validatePrerequisiteFeat(prerequisite, prerequisiteData)) { - isValid = false; - } - }); - return isValid; + const shouldExclude = getShouldExclude(prerequisite); + // check if class exists in the base class lookup + const hasClass = Object.values(prerequisiteData.baseClassLookup).some((charClass) => ClassAccessors.getId(charClass) === entityId); + return shouldExclude ? !hasClass : hasClass; } /** * @@ -206,5 +213,8 @@ export function validatePrerequisiteFeat(prerequisite, prerequisiteData) { if (entityId === null) { return true; } - return !!HelperUtils.lookupDataOrFallback(prerequisiteData.featLookup, entityId); + const shouldExclude = getShouldExclude(prerequisite); + // check if feat exists in the feat lookup + const hasFeat = !!HelperUtils.lookupDataOrFallback(prerequisiteData.featLookup, entityId); + return shouldExclude ? !hasFeat : hasFeat; } diff --git a/ddb_main/packages/rules-engine/es/engine/Spell/generators.js b/ddb_main/packages/rules-engine/es/engine/Spell/generators.js index 9a6d8a3..fc40257 100644 --- a/ddb_main/packages/rules-engine/es/engine/Spell/generators.js +++ b/ddb_main/packages/rules-engine/es/engine/Spell/generators.js @@ -638,7 +638,7 @@ export function generateActiveCharacterSpells(spells, inventoryInfusionLookup, c * @param optionalClassFeatures * @param definitionPool */ -export function generateSpellListDataOriginLookup(race, classes, background, optionalOrigins, optionalClassFeatures, definitionPool) { +export function generateSpellListDataOriginLookup(race, classes, background, optionalOrigins, optionalClassFeatures, definitionPool, feats) { let lookup = {}; if (race !== null) { RaceAccessors.getDefinitionRacialTraits(race).forEach((feature) => { @@ -662,6 +662,12 @@ export function generateSpellListDataOriginLookup(race, classes, background, opt }); }); }); + feats.forEach((feat) => { + const uniqueKey = FeatAccessors.getUniqueKey(feat); + FeatAccessors.getSpellListIds(feat).forEach((spellListId) => { + lookup[spellListId] = DataOriginGenerators.generateDataOriginRef(DataOriginTypeEnum.FEAT, uniqueKey); + }); + }); optionalOrigins.forEach((optionalOrigin) => { const definitionKey = OptionalOriginAccessors.getDefinitionKey(optionalOrigin); if (!definitionKey) { @@ -697,6 +703,6 @@ export function generateSpellListDataOriginLookup(race, classes, background, opt * @param raceSpellListIds * @param backgroundSpellListIds */ -export function generateGlobalSpellListIds(raceSpellListIds, backgroundSpellListIds) { - return [...raceSpellListIds, ...backgroundSpellListIds]; +export function generateGlobalSpellListIds(raceSpellListIds, backgroundSpellListIds, featSpellListIds) { + return [...raceSpellListIds, ...backgroundSpellListIds, ...featSpellListIds]; } diff --git a/ddb_main/packages/rules-engine/es/managers/FeatManager.js b/ddb_main/packages/rules-engine/es/managers/FeatManager.js index ee10d20..185fe25 100644 --- a/ddb_main/packages/rules-engine/es/managers/FeatManager.js +++ b/ddb_main/packages/rules-engine/es/managers/FeatManager.js @@ -1,3 +1,4 @@ +import sortBy from 'lodash/sortBy'; import { characterActions } from "../actions"; import { ChoiceUtils } from "../engine/Choice"; import { DisplayConfigurationTypeEnum } from "../engine/Core"; @@ -70,10 +71,54 @@ export class FeatManager extends BaseManager { // user-facing categories from technical-only tags. this.getCategories = () => FeatAccessors.getCategories(this.feat); //Utils + //Gets all the prereq Failures for this feat - returns an array of arrays, where each inner array is a grouping of failures this.getPrerequisiteFailures = () => { const prerequisiteData = rulesEngineSelectors.getPrerequisiteData(this.state); return PrerequisiteUtils.getPrerequisiteGroupingFailures(this.getPrerequisites(), prerequisiteData); }; + // Creates a string that summarizes the prerequisite failures for this feat + this.getPrerequisiteFailuresText = () => { + //get all the prerequisite failures, sorted so that if every group failure is a missing prereq, they come first + const failures = sortBy(this.getPrerequisiteFailures(), (failureGroup) => failureGroup.every((f) => !f.shouldExclude) ? 0 : 1); + // Create an intro string for the summary + let intro = ''; + // Check if all failures are either missing or prohibited + const hasOnlyMissingFailures = failures.every((failureGroup) => failureGroup.every((f) => !f.shouldExclude)); + const hasOnlyProhibitedFailures = failures.every((failureGroup) => failureGroup.every((f) => f.shouldExclude)); + // Determine the intro text based on the type of failures + if (hasOnlyMissingFailures) { + intro = 'Missing: '; + } + else if (hasOnlyProhibitedFailures) { + intro = 'Prohibits: '; + } + else { + intro = 'Requires: '; + } + // Each failure is sorted so that prohibited failures come last, and then combined into a single string + const combinedStrings = failures + .map((failure) => sortBy(failure, (f) => (f.shouldExclude ? 1 : 0)) + .map((failureAnd) => { + if (failureAnd.shouldExclude && !hasOnlyProhibitedFailures && !hasOnlyMissingFailures) { + return `Prohibits ${failureAnd.data.requiredDescription}`; + } + return `${failureAnd.data.requiredDescription}`; + }) + .reduce((acc, desc, idx) => { + let connector = ''; + if (idx < failure.length - 2) { + connector = ', '; + } + else if (idx < failure.length - 1) { + connector = ' and '; + } + acc += `${desc}${connector}`; + return acc; + }, '')) + .join(' or '); + // return the intro text followed by the combined strings + return `${intro}${combinedStrings}`; + }; this.getRepeatableGroupId = () => FeatUtils.getRepeatableGroupId(this.feat); this.isRepeatableFeatParent = () => this.isRepeatable() && this.getRepeatableParentId() === null; this.getDefinitionKey = () => { diff --git a/ddb_main/packages/rules-engine/es/sagas/character/saga.js b/ddb_main/packages/rules-engine/es/sagas/character/saga.js index c446aa9..4507020 100644 --- a/ddb_main/packages/rules-engine/es/sagas/character/saga.js +++ b/ddb_main/packages/rules-engine/es/sagas/character/saga.js @@ -144,7 +144,7 @@ function* filter(action) { * * @param action */ -function* executePostAction(action) { +export function* executePostAction(action) { if (isPostAction(action.meta)) { for (let i = 0; i < action.meta.postAction.type.length; i++) { yield put({ @@ -184,7 +184,7 @@ function* executeSyncTransactionDeactivate(action, transactionInitiatorId) { * * @param action */ -function* executeApi(action) { +export function* executeApi(action) { let apiLookup = { // ACTION [types.ACTION_USE_SET]: apiShared.putCharacterActionLimitedUse, @@ -392,7 +392,7 @@ function* executeApi(action) { yield call(apiRequest, apiPayload); } } -function* executeHandler(action) { +export function* executeHandler(action) { let handlerLookup = { //ACTION [types.ACTION_CUSTOMIZATIONS_DELETE]: sagaHandlers.handleActionCustomizationsDelete, diff --git a/ddb_main/packages/rules-engine/es/sagas/serviceData/saga.js b/ddb_main/packages/rules-engine/es/sagas/serviceData/saga.js index c216717..0d31380 100644 --- a/ddb_main/packages/rules-engine/es/sagas/serviceData/saga.js +++ b/ddb_main/packages/rules-engine/es/sagas/serviceData/saga.js @@ -97,7 +97,7 @@ function* filter(action) { * * @param action */ -function* executePostAction(action) { +export function* executePostAction(action) { if (isPostAction(action.meta)) { for (let i = 0; i < action.meta.postAction.type.length; i++) { yield put({ @@ -137,7 +137,7 @@ function* executeSyncTransactionDeactivate(action, transactionInitiatorId) { * * @param action */ -function* executeApi(action) { +export function* executeApi(action) { let apiLookup = { //KNOWN_INFUSION [types.KNOWN_INFUSION_MAPPING_SET]: apiShared.putCharacterKnownInfusion, @@ -207,7 +207,7 @@ function* executeApi(action) { yield call(apiRequest, apiPayload); } } -function* executeHandler(action) { +export function* executeHandler(action) { let handlerLookup = { //KNOWN_INFUSION [types.KNOWN_INFUSION_MAPPING_CREATE]: sagaHandlers.handleKnownInfusionCreate, 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 012a67b..69c06c0 100644 --- a/ddb_main/packages/rules-engine/es/selectors/composite/engine.js +++ b/ddb_main/packages/rules-engine/es/selectors/composite/engine.js @@ -68,6 +68,7 @@ export const getSpellListDataOriginLookup = createSelector([ characterSelectors.getOptionalOrigins, characterSelectors.getOptionalClassFeatures, serviceDataSelectors.getDefinitionPool, + characterSelectors.getFeats, ], SpellGenerators.generateSpellListDataOriginLookup); /** * @returns {Record>} @@ -400,7 +401,11 @@ export const getGlobalBackgroundSpellListIds = createSelector([getBackgroundInfo /** * @returns {Array} */ -export const getGlobalSpellListIds = createSelector([getGlobalRaceSpellListIds, getGlobalBackgroundSpellListIds], SpellGenerators.generateGlobalSpellListIds); +export const getGlobalFeatsSpellListIds = createSelector([getFeats], FeatGenerators.generateGlobalFeatsSpellListIds); +/** + * @returns {Array} + */ +export const getGlobalSpellListIds = createSelector([getGlobalRaceSpellListIds, getGlobalBackgroundSpellListIds, getGlobalFeatsSpellListIds], SpellGenerators.generateGlobalSpellListIds); /** * @returns {DecorationInfo} */ diff --git a/ddb_main/subApps/builder/components/HpManageModal/HpManageModal.tsx b/ddb_main/subApps/builder/components/HpManageModal/HpManageModal.tsx index e411a54..dbdbbc4 100644 --- a/ddb_main/subApps/builder/components/HpManageModal/HpManageModal.tsx +++ b/ddb_main/subApps/builder/components/HpManageModal/HpManageModal.tsx @@ -1,5 +1,5 @@ import clsx from "clsx"; -import { FC, HTMLAttributes, useMemo, useState } from "react"; +import { FC, FocusEvent, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; import { @@ -19,8 +19,8 @@ import { HP_BONUS_VALUE, HP_OVERRIDE_MAX_VALUE, } from "~/subApps/sheet/constants"; -import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField"; +import { InputField } from "../InputField"; import styles from "./styles.module.css"; export interface HpManageModalProps @@ -65,52 +65,55 @@ export const HpManageModal: FC = ({ reset(); }; - const transformBaseHp = (value: string): number => { + const handleBaseHp = (e: FocusEvent) => { let parsedNumber = HelperUtils.parseInputInt( - value, + e.target.value, RuleDataUtils.getMinimumHpTotal(ruleData) ); - return HelperUtils.clampInt( + + const clampedValue = HelperUtils.clampInt( parsedNumber, RuleDataUtils.getMinimumHpTotal(ruleData), HP_BASE_MAX_VALUE ); + + setBaseHp(clampedValue); }; - const transformBonusHp = (value: string): number | null => { - let parsedNumber = HelperUtils.parseInputInt(value); - if (parsedNumber === null) { - return parsedNumber; - } - return HelperUtils.clampInt( + const handleBonusHp = (e: FocusEvent) => { + let parsedNumber = HelperUtils.parseInputInt(e.target.value); + + if (parsedNumber === null) return parsedNumber; + + const clampedValue = HelperUtils.clampInt( parsedNumber, HP_BONUS_VALUE.MIN, HP_BONUS_VALUE.MAX ); + + setBonusHp(clampedValue); }; - const transformOverrideHp = (value: string): number | null => { - let parsedNumber = HelperUtils.parseInputInt(value, null); + const handleOverrideHp = (e: FocusEvent) => { + let parsedNumber = HelperUtils.parseInputInt(e.target.value, null); + if (parsedNumber === null) { + setOverrideHp(null); return parsedNumber; } - return HelperUtils.clampInt( + + const clampedValue = HelperUtils.clampInt( parsedNumber, RuleDataUtils.getMinimumHpTotal(ruleData) ); - }; - const handleOverrideHpUpdate = (value: number | null): void => { - const newOverride = - value === null - ? null - : HelperUtils.clampInt( - value, - RuleDataUtils.getMinimumHpTotal(ruleData), - HP_OVERRIDE_MAX_VALUE - ); + const clampedOverride = HelperUtils.clampInt( + clampedValue, + RuleDataUtils.getMinimumHpTotal(ruleData), + HP_OVERRIDE_MAX_VALUE + ); - setOverrideHp(newOverride); + setOverrideHp(clampedOverride); }; const totalHp = useMemo(() => { @@ -148,35 +151,33 @@ export const HpManageModal: FC = ({ {baseHp}

) : ( - setBaseHp(value as number)} - transformValueOnBlur={transformBaseHp} + onBlur={handleBaseHp} /> )} - setBonusHp(value as number)} - transformValueOnBlur={transformBonusHp} + placeholder="--" + onBlur={handleBonusHp} /> - - } + diff --git a/ddb_main/subApps/builder/components/InputField/InputField.tsx b/ddb_main/subApps/builder/components/InputField/InputField.tsx new file mode 100644 index 0000000..98d5bd9 --- /dev/null +++ b/ddb_main/subApps/builder/components/InputField/InputField.tsx @@ -0,0 +1,132 @@ +import clsx from "clsx"; +import { + ChangeEvent, + FC, + FocusEvent, + HTMLAttributes, + HTMLInputTypeAttribute, + useEffect, + useState, +} from "react"; +import { v4 as uuidv4 } from "uuid"; + +import styles from "./styles.module.css"; + +interface InputProps extends HTMLAttributes { + min?: number; + max?: number; + autoComplete?: string; +} + +export interface InputFieldProps extends HTMLAttributes { + errorMessage?: string; + initialValue?: string | number | null; + label: string; + maxLength?: number; + placeholder?: string; + type?: HTMLInputTypeAttribute; + inputProps?: InputProps; +} + +export const InputField: FC = ({ + className, + errorMessage, + initialValue = "", + inputProps, + label, + maxLength, + onBlur, + onChange, + onFocus, + placeholder, + type = "text", + ...props +}) => { + // Set default error message if not provided + errorMessage ||= `The maximum length is ${maxLength} characters.`; + // Set up component state and a shared id + const [value, setValue] = useState(initialValue); + const [showError, setShowError] = useState(false); + const id = props.id ?? `input-field-${uuidv4()}`; + + const handleChange = (e: ChangeEvent) => { + const valueLength = e.target.value.length; + if (maxLength) { + // If the value length exceeds maxLength, show error + if (valueLength > maxLength) { + setShowError(true); + //return; Prevent further processing if maxLength exceeded + return; + } + + // If the value length is equal to maxLength, show error + if (valueLength === maxLength) { + setShowError(true); + } + // If the value length is less than maxLength, hide error + if (valueLength < maxLength) { + setShowError(false); + } + } + // Update component state + setValue(e.target.value); + // Call the provided onChange handler if it exists + if (onChange) onChange(e); + return; + }; + + const handleBlur = (e: FocusEvent) => { + const intValue = parseInt(e.target.value); + + // If entered value is below the minimum, reset to minimum + if (!isNaN(intValue) && inputProps?.min && intValue < inputProps?.min) { + setValue(inputProps.min); + } + + // If entered value is above the maximum and no initialValue exists, reset to maximum + if (!isNaN(intValue) && inputProps?.max && intValue > inputProps?.max) { + setValue(inputProps.max); + } + + // Call the provided onBlur handler if it exists + if (onBlur) onBlur(e); + + // Reset error state on blur + setShowError(false); + }; + + const handleFocus = (e: FocusEvent) => { + // Call the provided onFocus handler if it exists + if (onFocus) onFocus(e); + // Check for maxLength error + const valueLength = e.target.value.length; + if (maxLength && valueLength > maxLength - 1) setShowError(true); + }; + + useEffect(() => { + // Update the value when initialValue changes + setValue(initialValue); + // Reset error state when initial value changes + setShowError(false); + }, [initialValue]); + + const inputAttrs = { id, type, placeholder }; + + return ( +
+ + + {showError && {errorMessage}} +
+ ); +}; diff --git a/ddb_main/subApps/builder/components/PortraitName/PortraitName.tsx b/ddb_main/subApps/builder/components/PortraitName/PortraitName.tsx index a1bcbca..8730a68 100644 --- a/ddb_main/subApps/builder/components/PortraitName/PortraitName.tsx +++ b/ddb_main/subApps/builder/components/PortraitName/PortraitName.tsx @@ -16,11 +16,11 @@ import { import { useCharacterEngine } from "~/hooks/useCharacterEngine"; import { builderActions } from "~/tools/js/CharacterBuilder/actions"; import { builderSelectors } from "~/tools/js/CharacterBuilder/selectors"; -import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField"; import { PortraitManager } from "~/tools/js/Shared/containers/panes/DecoratePane"; import Tooltip from "~/tools/js/commonComponents/Tooltip"; import { CharacterAvatarPortrait } from "~/tools/js/smartComponents/CharacterAvatar"; +import { InputField } from "../InputField"; import styles from "./styles.module.css"; interface Props extends HTMLAttributes { @@ -56,7 +56,7 @@ export const PortraitName: FC = ({ setCurrentSuggestedNames(suggestedNames); }, [suggestedNames]); - const handleNameFieldSet = (value: string | null): void => { + const handleNameFieldSet = (value: string) => { let updatedName = value ?? ""; // If the flag useDefaultCharacterName is true and value is falsy, set the name to the DefaultCharacterName @@ -137,18 +137,16 @@ export const PortraitName: FC = ({ )}
- - } maxLength={InputLimits.characterNameMaxLength} - maxLengthErrorMsg={CharacterNameLimitMsg} + errorMessage={CharacterNameLimitMsg} + onBlur={(e) => handleNameFieldSet(e.target.value)} + inputProps={{ + spellCheck: false, + autoComplete: "off", + }} />
} diff --git a/ddb_main/tools/js/CharacterBuilder/components/AbilityScoreManagerManual/AbilityScoreManagerManual.tsx b/ddb_main/tools/js/CharacterBuilder/components/AbilityScoreManagerManual/AbilityScoreManagerManual.tsx index 04b2287..26c351f 100644 --- a/ddb_main/tools/js/CharacterBuilder/components/AbilityScoreManagerManual/AbilityScoreManagerManual.tsx +++ b/ddb_main/tools/js/CharacterBuilder/components/AbilityScoreManagerManual/AbilityScoreManagerManual.tsx @@ -1,9 +1,13 @@ +import { FocusEvent } from "react"; + import { AbilityManager, HelperUtils, } from "@dndbeyond/character-rules-engine/es"; -import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField"; +import { InputField } from "~/subApps/builder/components/InputField"; + +import styles from "./styles.module.css"; interface Props { abilities: Array; @@ -16,14 +20,19 @@ export default function AbilityScoreManagerManual({ minScore = 3, maxScore = 18, }: Props) { - const handleTransformValueOnBlur = (value: string): number | null => { - const parsedValue = HelperUtils.parseInputInt(value); + const handleBlur = ( + e: FocusEvent, + abilityScore: AbilityManager + ) => { + // Get the int value from the input + const parsedValue = HelperUtils.parseInputInt(e.target.value); + // Create a clamped variable to hold the clamped value let clampedValue: number | null = null; - if (parsedValue !== null) { + // If the value is valid, clamp it to the min and max scores + if (parsedValue !== null) clampedValue = HelperUtils.clampInt(parsedValue, minScore, maxScore); - } - - return clampedValue; + // Set the score change from the clampped value + abilityScore.handleScoreChange(clampedValue); }; return ( @@ -35,11 +44,17 @@ export default function AbilityScoreManagerManual({ key={abilityScore.getId()} data-stat-id={abilityScore.getId()} > - abilityScore.handleScoreChange(Number(value))} + handleBlur(e, abilityScore)} />
Total: {abilityScore.getTotalScore() ?? "--"} diff --git a/ddb_main/tools/js/CharacterBuilder/containers/pages/DescriptionManage/DescriptionManage.tsx b/ddb_main/tools/js/CharacterBuilder/containers/pages/DescriptionManage/DescriptionManage.tsx index c419ee9..4a49e6b 100644 --- a/ddb_main/tools/js/CharacterBuilder/containers/pages/DescriptionManage/DescriptionManage.tsx +++ b/ddb_main/tools/js/CharacterBuilder/containers/pages/DescriptionManage/DescriptionManage.tsx @@ -1,6 +1,6 @@ import axios, { Canceler } from "axios"; import { orderBy } from "lodash"; -import React, { HTMLAttributes, useContext } from "react"; +import React, { FocusEvent, HTMLAttributes, useContext } from "react"; import { DispatchProp } from "react-redux"; import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es"; @@ -52,12 +52,12 @@ import { HtmlContent } from "~/components/HtmlContent"; import { Link } from "~/components/Link"; import { useSource } from "~/hooks/useSource"; import { EditorWithDialog } from "~/subApps/builder/components/EditorWithDialog"; +import { InputField } from "~/subApps/builder/components/InputField"; import { RouteKey } from "~/subApps/builder/constants"; import { ModalData, useModalManager, } from "~/subApps/builder/contexts/ModalManager"; -import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField"; import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice"; import { toastMessageActions } from "../../../../Shared/actions"; @@ -195,7 +195,7 @@ class CustomBackgroundManager extends React.PureComponent<
@@ -204,7 +204,7 @@ class CustomBackgroundManager extends React.PureComponent<