import sortBy from 'lodash/sortBy'; import { characterActions } from "../actions"; import { ChoiceUtils } from "../engine/Choice"; import { DisplayConfigurationTypeEnum } from "../engine/Core"; import { DataOriginTypeEnum } from "../engine/DataOrigin"; import { DefinitionHacks } from "../engine/Definition"; import { FeatAccessors, FeatUtils } from "../engine/Feat"; import { PrerequisiteUtils, PrerequisiteValidators } from "../engine/Prerequisite"; import { RuleDataUtils } from "../engine/RuleData"; import { SourceUtils } from "../engine/Source"; import { rulesEngineSelectors } from "../selectors"; import { BaseManager } from './BaseManager'; export const featDefinitionMap = new Map(); const featMangerMap = new Map(); export const getFeatManager = (params) => { const { feat } = params; const featId = FeatAccessors.getId(feat); if (featMangerMap.has(featId)) { const FeatManager = featMangerMap.get(featId); if (!FeatManager) { throw new Error(`FeatManager for feat ${featId} is null`); } if (FeatManager.params.feat === feat) { return FeatManager; } } const newFeatManager = new FeatManager(params); featMangerMap.set(featId, newFeatManager); return newFeatManager; }; export class FeatManager extends BaseManager { constructor(params) { super(params); this.handleAcceptOnSuccess = (onSuccess) => { return () => { typeof onSuccess === 'function' && onSuccess(); }; }; this.handleRejectOnError = (onError) => { return () => { typeof onError === 'function' && onError(); }; }; this.handleAdd = (onSuccess, onError) => { return this.dispatch(characterActions.adhocFeatCreate(this.getId())); }; this.handleRemove = (onSuccess, onError) => { featMangerMap.delete(this.getId()); return this.dispatch(characterActions.adhocFeatRemove(this.getId())); }; //Accessors this.getId = () => FeatAccessors.getId(this.feat); this.getEntityTypeId = () => FeatAccessors.getEntityTypeId(this.feat); this.getName = () => FeatAccessors.getName(this.feat); this.getDataOrigin = () => FeatAccessors.getDataOrigin(this.feat); this.getDataOriginType = () => FeatAccessors.getDataOriginType(this.feat); this.getActions = () => FeatAccessors.getActions(this.feat); this.getChoices = () => FeatAccessors.getChoices(this.feat); this.getOptions = () => FeatAccessors.getOptions(this.feat); this.getSpells = () => FeatAccessors.getSpells(this.feat); this.getPrerequisites = () => FeatAccessors.getPrerequisites(this.feat); this.getPrerequisiteDescription = () => FeatAccessors.getPrerequisiteDescription(this.feat); this.getDescription = () => FeatAccessors.getDescription(this.feat); this.getSources = () => FeatAccessors.getSources(this.feat); this.getSnippet = () => FeatAccessors.getSnippet(this.feat); this.isRepeatable = () => FeatAccessors.isRepeatable(this.feat); this.getRepeatableParentId = () => FeatAccessors.getRepeatableParentId(this.feat); this.isHomebrew = () => FeatAccessors.isHomebrew(this.feat); this.getDefinition = () => FeatAccessors.getDefinition(this.feat); // Possible TODO for the future: We may add data to Entity Tags to distinguish // 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 = () => { return DefinitionHacks.hack__generateDefinitionKey(this.getEntityTypeId(), this.getId()); }; this.getHelperText = () => { return RuleDataUtils.getBuilderHelperTextByDefinitionKeys([this.getDefinitionKey()], rulesEngineSelectors.getRuleData(this.state), DisplayConfigurationTypeEnum.FEAT); }; //validate that a feat meets its prereqs this.canAdd = () => { const prerequisiteData = rulesEngineSelectors.getPrerequisiteData(this.state); const prereqs = this.getPrerequisites(); return PrerequisiteValidators.validatePrerequisiteGrouping(prereqs, prerequisiteData); }; this.isHiddenFeat = () => FeatUtils.isHiddenFeat(this.feat); this.isSimulatedRepeatableFeat = () => { return this.isRepeatable() && this.getDataOriginType() === DataOriginTypeEnum.SIMULATED; }; this.getPrimarySourceName = () => this.isHomebrew() ? 'Homebrew' : SourceUtils.getSourceFullNames(this.getSources(), rulesEngineSelectors.getRuleData(this.state))[0]; this.getSourceCategory = () => { var _a; const ruleData = rulesEngineSelectors.getRuleData(this.state); const sources = this.getSources(); if (sources.length === 0) return null; const sourceData = RuleDataUtils.getSourceDataInfo(sources[0].sourceId, ruleData); return (_a = sourceData === null || sourceData === void 0 ? void 0 : sourceData.sourceCategory) !== null && _a !== void 0 ? _a : null; }; this.params = params; this.feat = params.feat; } static getFeatManager(id) { const featManager = featMangerMap.get(id); if (!featManager) { throw new Error(`featManager for feat ${id} is null`); } return featManager; } handleChoiceChange(featId, choiceType, choiceId, optionValue) { this.dispatch(characterActions.featChoiceSetRequest(featId, choiceType, choiceId, optionValue)); } hasUnfinishedChoices() { return this.getUnfinishedChoices().length > 0; } getUnfinishedChoices() { return this.getChoices().filter((choice) => ChoiceUtils.isTodo(choice)); } }