351 lines
20 KiB
JavaScript
351 lines
20 KiB
JavaScript
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 = <DetailChoiceFeat featId={optionValue} />;
|
|
}
|
|
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<any> 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<FeatureChoiceOption | HtmlSelectOptionGroup> = 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;
|
|
}
|