import { orderBy } from 'lodash'; import { TypeScriptUtils } from "../../utils"; import { ChoiceAccessors } from '.'; import { ClassAccessors } from '../Class'; import { AbilityStatEnum, BuilderChoiceSubtypeEnum, BuilderChoiceTypeEnum, } from '../Core'; import { DataOriginTypeEnum } from '../DataOrigin'; import { EntityUtils } from '../Entity'; import { FeatAccessors, FeatUtils } from '../Feat'; import { HelperUtils } from '../Helper'; import { ModifierAccessors } from '../Modifier'; import { PrerequisiteValidators } from '../Prerequisite'; import { RuleDataAccessors } from '../RuleData'; import { SourceUtils } from '../Source'; import { SpellUtils } from '../Spell'; import { getDefaultSubtypes, getOptions, getOptionValue, isOptional } from './accessors'; export function isOnlyDefaultSelected(choice) { const defaultSubtypes = getDefaultSubtypes(choice); if (defaultSubtypes.length !== 1) { return false; } if (!getOptionValue(choice)) { return false; } const selectedOption = getSelectedOption(choice); if (selectedOption) { return selectedOption.label === defaultSubtypes[0]; } return false; } export function getSelectedOption(choice) { return getOptions(choice).find((option) => option.id === getOptionValue(choice)); } export function isOptionSelected(choice) { return !!getSelectedOption(choice); } export function isTodo(choice) { return !isOptional(choice) && getOptionValue(choice) === null; } // Given a Choice, return options for a select element that are potentially grouped by sourceCategory //used in FeatPane and FeatDetail for feat choices // and in getSortedChoiceOptionsForFeaturesInfo for feature choices that are feats and have subchoices export function getSortedChoiceOptionsForFeatsInfo(featId, choice, ruleData, entityRestrictionData) { var _a; const options = getOptions(choice); let description; // If every option in the array has a null sourceId, ignore sorting by sourceId. // If any option has a non-null sourceId, sort by sourceId. const shouldSortBySourceId = options.some((option) => option.hasOwnProperty('sourceId') && option.sourceId !== null); let featChoiceOptions; //If the options have a sourceId, group them by Source Category (for now this should only be BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION - we also have to do this in FeatureChoice for spell options) if (shouldSortBySourceId) { // Map over the options to mock parts of a DefinitionContract. const groupedOptions = SourceUtils.getSimpleSourcedDefinitionContracts(options); //If there is a chosen option, set detailChoiceDesc to its description. const chosenOption = options.find((option) => option.id === getOptionValue(choice)); description = (_a = chosenOption === null || chosenOption === void 0 ? void 0 : chosenOption.description) !== null && _a !== void 0 ? _a : ''; featChoiceOptions = SourceUtils.getGroupedOptionsBySourceCategory(groupedOptions, ruleData, chosenOption === null || chosenOption === void 0 ? void 0 : chosenOption.id, entityRestrictionData); } else { featChoiceOptions = options.map((option) => (Object.assign(Object.assign({}, option), { value: option.id }))); featChoiceOptions = orderBy(featChoiceOptions, 'label'); } return { featId, choice, options: featChoiceOptions, description, }; } // Given a Choice, return options for a select element that are potentially grouped by sourceCategory //used in FeatureChoice for all choices // extracted from FeatureChoice.tsx // TODO needs to be broken down into smaller utils and simplified export function getSortedChoiceOptionsForFeaturesInfo(choice, fetchedFeats, featLookup, charClass, fetchedSubclasses, characterPreferences, prerequisiteData, ruleData, entityRestrictionData) { var _a, _b; let description; let availableOptions; let featSubChoices; const optionValue = ChoiceAccessors.getOptionValue(choice); const options = ChoiceAccessors.getOptions(choice); const type = ChoiceAccessors.getType(choice); const tagConstraints = ChoiceAccessors.getTagConstraints(choice); switch (type) { case BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION: const availableFeats = getAvailableFeatChoices(optionValue, fetchedFeats, featLookup); const repeatableFeatTracker = new Set(); // Add selected feat to repeatable tracker if repeatable const selectedFeat = optionValue ? featLookup[optionValue] : null; if (selectedFeat && FeatAccessors.isRepeatable(selectedFeat)) { const parentId = FeatUtils.getRepeatableGroupId(selectedFeat); if (parentId) { repeatableFeatTracker.add(parentId); } } const filteredFeats = availableFeats.filter((feat) => { const featId = FeatAccessors.getId(feat); const isRepeatable = FeatAccessors.isRepeatable(feat); // If the feat is the currently selected, always include it if (featId === optionValue) { return true; } // Always exclude all previous selected feats if (featLookup[featId]) { return false; } // If the Feat does not meet the tag constraints, should they exist, exclude it const tagCategories = FeatAccessors.getCategories(feat); if (tagConstraints && !FeatUtils.doesSatisfyTagConstraints(tagCategories, tagConstraints)) { return false; } // Handle prerequisites when enforcing feat rules if (characterPreferences.enforceFeatRules && !PrerequisiteValidators.validatePrerequisiteGrouping(FeatAccessors.getPrerequisites(feat), prerequisiteData)) { return false; } // Special handling for repeatable feats, there can be only one if (isRepeatable) { const parentId = FeatUtils.getRepeatableGroupId(feat); // If a feat from this repeatable group exist exclude all others if (repeatableFeatTracker.has(parentId)) { return false; } repeatableFeatTracker.add(parentId); } // If none of the exclusions above are met, include the feat return true; }); //Group available feats by source category availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(filteredFeats .map((feat) => FeatAccessors.getDefinition(feat)) .filter(TypeScriptUtils.isNotNullOrUndefined), ruleData); if (selectedFeat && optionValue !== null) { description = (_a = FeatAccessors.getDescription(selectedFeat)) !== null && _a !== void 0 ? _a : ''; const featChoices = FeatAccessors.getChoices(selectedFeat); if (featChoices.length > 0) { featSubChoices = featChoices.map((featChoice) => { return getSortedChoiceOptionsForFeatsInfo(optionValue, featChoice, ruleData, entityRestrictionData); }); } // subchoicesNode = ; } break; case BuilderChoiceTypeEnum.SUB_CLASS_OPTION: const subclassData = charClass ? getAvailableSubclassChoices(charClass, fetchedSubclasses) : []; //Group available subclasses by source category availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(subclassData, ruleData); const chosenSubclass = subclassData.find((subclass) => subclass.id === optionValue); if (chosenSubclass) { description = ''; const sources = chosenSubclass.sources ? chosenSubclass.sources .map((sourceMapping) => HelperUtils.lookupDataOrFallback(RuleDataAccessors.getSourceDataLookup(ruleData), sourceMapping.sourceId)) .filter(TypeScriptUtils.isNotNullOrUndefined) : []; sources.forEach((source) => { var _a, _b; if ((_a = source.sourceCategory) === null || _a === void 0 ? void 0 : _a.isToggleable) { description += source.sourceCategory.description ? source.sourceCategory.description : ''; } description += (_b = chosenSubclass.description) !== null && _b !== void 0 ? _b : ''; }); } break; case BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION: // Map over the options to mock parts of a SpellDefinitionContract. const spellOptions = SourceUtils.getSimpleSourcedDefinitionContracts(options); availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(spellOptions, ruleData, optionValue, entityRestrictionData); // If there is a chosen spell, set detailChoiceDesc to its description. const chosenSpell = spellOptions.find((spell) => spell.id === optionValue); if (chosenSpell) { description = (_b = chosenSpell.description) !== null && _b !== void 0 ? _b : ''; } break; default: availableOptions = options.map((option) => (Object.assign(Object.assign({}, option), { value: option.id }))); } return { choice, options: availableOptions, featSubChoices, description, }; } // filter Choice options based on modifiers and default subtypes that identify when a user has made a choice from another source. We filter those options out or we append the source data origin name next to the option label export function getRemainingOptions(options, modifiers, defaultSubtypes, showBackgroundProficiencyOptions, optionValue) { let remainingSubtypes = []; let selectedOption = options.find((option) => option.value === optionValue); //if there is a default option set, the default option is granted automatically. //defaultSubtypes would be an array of remaining options and we need to filter out anything that is already found in a modifier from another source if (defaultSubtypes) { if (defaultSubtypes.length > 1) { defaultSubtypes.forEach((defaultSubtype) => { if (!modifiers.find((modifier) => modifier.friendlySubtypeName === defaultSubtype) || (selectedOption && selectedOption.label === defaultSubtype)) { remainingSubtypes.push(defaultSubtype); } }); } else if (defaultSubtypes.length === 1 && !modifiers.find((modifier) => modifier.friendlySubtypeName === defaultSubtypes[0])) { remainingSubtypes = [...defaultSubtypes]; } } // If there are any remaining subtypes, only show those if (remainingSubtypes.length) { options = options.filter((option) => remainingSubtypes.includes(option.label ? option.label : '') || optionValue === option.value); } else { // If there aren't any remaining subtype defaults, just show the normal list of options if (modifiers.length) { options = options.reduce((acc, option) => { // Find any existing modifiers that are the current option let optionModifiers = modifiers.filter((modifier) => modifier.friendlySubtypeName === option.label); let backgroundOptionModifier = null; if (showBackgroundProficiencyOptions && optionModifiers.length) { // If there are any existing modifiers, find out if any of them are from a background const foundBackgroundOptionModifier = optionModifiers.find((modifier) => !ModifierAccessors.isGranted(modifier) && ModifierAccessors.getDataOriginType(modifier) === DataOriginTypeEnum.BACKGROUND); if (foundBackgroundOptionModifier) { backgroundOptionModifier = foundBackgroundOptionModifier; } } if (!optionModifiers.length || backgroundOptionModifier || optionValue === option.value) { let value = typeof option.value === 'string' ? HelperUtils.parseInputInt(option.value) : option.value; //append the dataOrigin of the modifier (background) to the option label so the user knows where the option was selected if (value !== null) { acc.push(Object.assign(Object.assign({}, option), { value: value, label: backgroundOptionModifier ? `${option.label} (${EntityUtils.getDataOriginName(ModifierAccessors.getDataOrigin(backgroundOptionModifier))})` : option.label })); } } return acc; }, []); } } return options; } //TODO this was abstracted from DetailChoice.tsx and needs to be broken down to smaller utils and simplified //TODO also need to remove the Array type when gathering the renderOptions and fix the typing export function getSortedRenderOptions(options, classId, subType, entityRestrictionData, languages, ruleData, defaultSubtypes, showBackgroundProficiencyOptions, choiceInfo, optionValue) { const { classSpellLists, proficiencyModifiers, languageModifiers, expertiseModifiers, kenseiModifiers, abilityLookup, } = choiceInfo; // TODO need to get rid of this any and fix the typing // let renderOptions: Array = options; let renderOptions = options; switch (subType) { case BuilderChoiceSubtypeEnum.PROFICIENCY: renderOptions = getRemainingOptions(renderOptions, [...proficiencyModifiers, ...expertiseModifiers], defaultSubtypes, showBackgroundProficiencyOptions, optionValue); break; case BuilderChoiceSubtypeEnum.LANGUAGE: //handle languages based on other language choices made renderOptions = getRemainingOptions(renderOptions, languageModifiers, defaultSubtypes, showBackgroundProficiencyOptions, optionValue); //create lookup from ruledata - name: rpgSourceId const languageNameLookup = languages.reduce((acc, curr) => { var _a; return Object.assign(Object.assign({}, acc), { [(_a = curr.name) !== null && _a !== void 0 ? _a : '']: curr.rpgSourceId }); }, {}); //transform language options to choiceOptionContracts to attach sourceId for grouping by Source Category const choiceOptionContracts = renderOptions.map((option) => { return { label: option.label, id: option.id, description: option.description, sourceId: option.label ? HelperUtils.lookupDataOrFallback(languageNameLookup, option.label) : null, }; }); const groupedOptions = SourceUtils.getSimpleSourcedDefinitionContracts(choiceOptionContracts); //group languages by source category renderOptions = SourceUtils.getGroupedOptionsBySourceCategory(groupedOptions, ruleData, optionValue, entityRestrictionData, 'Other'); break; case BuilderChoiceSubtypeEnum.KENSEI: renderOptions = getRemainingOptions(renderOptions, kenseiModifiers, defaultSubtypes, showBackgroundProficiencyOptions, optionValue); break; case BuilderChoiceSubtypeEnum.EXPERTISE: renderOptions = renderOptions.filter((option) => (proficiencyModifiers.find((modifier) => ModifierAccessors.getFriendlySubtypeName(modifier) === option.label) && !expertiseModifiers.find((modifier) => ModifierAccessors.getFriendlySubtypeName(modifier) === option.label)) || optionValue === option.value); break; case BuilderChoiceSubtypeEnum.EXPERTISE_NO_REQUIREMENT: renderOptions = renderOptions.filter((option) => !expertiseModifiers.find((modifier) => ModifierAccessors.getFriendlySubtypeName(modifier) === option.label) || optionValue === option.value); break; case BuilderChoiceSubtypeEnum.KNOWN_SPELLS: const spellList = classSpellLists.find((classSpellList) => ClassAccessors.getId(classSpellList.charClass) === classId); if (spellList) { renderOptions = renderOptions .map((topOption) => { if ('options' in topOption && topOption.options) { return Object.assign(Object.assign({}, topOption), { options: topOption.options.filter((option) => SpellUtils.isSpellKnown(spellList, Number(option.value)) || optionValue === option.value) }); } else { const singleOption = topOption; return SpellUtils.isSpellKnown(spellList, Number(singleOption.value)) || optionValue === singleOption.value ? singleOption : null; } }) .filter(TypeScriptUtils.isNotNullOrUndefined); } break; case BuilderChoiceSubtypeEnum.ABILITY_SCORE: { renderOptions = renderOptions.filter((option) => { let abilityId = null; switch (option.label) { case 'Charisma Score': abilityId = AbilityStatEnum.CHARISMA; break; case 'Constitution Score': abilityId = AbilityStatEnum.CONSTITUTION; break; case 'Dexterity Score': abilityId = AbilityStatEnum.DEXTERITY; break; case 'Intelligence Score': abilityId = AbilityStatEnum.INTELLIGENCE; break; case 'Strength Score': abilityId = AbilityStatEnum.STRENGTH; break; case 'Wisdom Score': abilityId = AbilityStatEnum.WISDOM; break; } return !abilityId || (abilityId && !abilityLookup[abilityId].isMaxed) || optionValue === option.value; }); break; } } return renderOptions.length > 0 && renderOptions[0].hasOwnProperty('optGroupLabel') ? orderBy(renderOptions, 'sortOrder') : orderBy(renderOptions, 'label'); } //Taken from FeatureChoice.tsx function getAvailableSubclassChoices(charClass, fetchedSubclasses) { const existingSubclass = ClassAccessors.getSubclass(charClass); //if there is an existing subclass that isn't already in the fetched subclasses, append it to the list if (existingSubclass && !fetchedSubclasses.some((subclass) => subclass.id === existingSubclass.id)) { return [...fetchedSubclasses, existingSubclass]; } return fetchedSubclasses; } //Taken from FeatureChoice.tsx function getAvailableFeatChoices(existingFeatId, fetchedFeats, featLookup) { const existingFeat = existingFeatId ? HelperUtils.lookupDataOrFallback(featLookup, existingFeatId) : null; // If there is an existing feat that isn't already in the fetched feats, append it to the list if (existingFeat !== null && !fetchedFeats.some((feat) => FeatAccessors.getId(feat) === existingFeatId)) { return [...fetchedFeats, existingFeat]; } return fetchedFeats; }