New source found from dndbeyond.com
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import { orderBy } from 'lodash';
|
||||
import { TypeScriptUtils } from "../../utils";
|
||||
import { ChoiceAccessors } from '.';
|
||||
import { ClassAccessors } from '../Class';
|
||||
import { AbilityStatEnum, BuilderChoiceSubtypeEnum } from '../Core';
|
||||
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';
|
||||
@@ -33,8 +37,9 @@ 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 DetailChoiceFeat.tsx for feat choices
|
||||
export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionData) {
|
||||
//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;
|
||||
@@ -56,10 +61,126 @@ export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionDa
|
||||
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 = [];
|
||||
@@ -209,3 +330,21 @@ export function getSortedRenderOptions(options, classId, subType, entityRestrict
|
||||
? 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;
|
||||
}
|
||||
|
||||
@@ -171,8 +171,9 @@ export function deriveReplacementWeaponAbilityStats(classModifiers) {
|
||||
classModifiers.forEach((modifier) => {
|
||||
if (ModifierValidators.isEnablesAbilityStatModifier(modifier)) {
|
||||
const statId = ModifierAccessors.getEntityId(modifier) || 0;
|
||||
if (statId && !replacementWeaponAbilityStats.includes(statId)) {
|
||||
replacementWeaponAbilityStats.push(statId);
|
||||
const dataOrigin = ModifierAccessors.getDataOrigin(modifier);
|
||||
if (statId) {
|
||||
replacementWeaponAbilityStats.push({ statId, dataOrigin });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -343,6 +343,11 @@ export function* handleAdhocFeatCreate(action) {
|
||||
* @param action
|
||||
*/
|
||||
export function* handleAdhocFeatRemove(action) {
|
||||
const featLookup = yield select(rulesEngineSelectors.getFeatLookup);
|
||||
const currentFeat = HelperUtils.lookupDataOrFallback(featLookup, action.payload.id, null);
|
||||
if (currentFeat !== null) {
|
||||
yield call(apiRemoveSpellsBySpellListIds, FeatAccessors.getSpellListIds(currentFeat));
|
||||
}
|
||||
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.deleteCharacterFeatAdHoc, action.payload);
|
||||
yield call(handleDataUpdates, data);
|
||||
}
|
||||
@@ -457,7 +462,13 @@ export function* handleRaceChoose(action) {
|
||||
const { race } = action.payload;
|
||||
const existingRace = yield select(rulesEngineSelectors.getRace);
|
||||
if (existingRace !== null) {
|
||||
yield call(apiRemoveSpellsBySpellListIds, RaceAccessors.getSpellListIds(existingRace));
|
||||
const spellListIdsToRemove = RaceAccessors.getSpellListIds(existingRace);
|
||||
const racialTraitsToRemove = RaceAccessors.getRacialTraits(existingRace);
|
||||
const featsFromRacialTraits = racialTraitsToRemove.map((trait) => RacialTraitAccessors.getFeats(trait)).flat();
|
||||
featsFromRacialTraits.forEach((feat) => {
|
||||
spellListIdsToRemove.push(...FeatAccessors.getSpellListIds(feat));
|
||||
});
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove);
|
||||
const existingOptionalOrigins = yield select(rulesEngineSelectors.getOptionalOrigins);
|
||||
const optionalOriginsIdsToRemove = existingOptionalOrigins.map(OptionalOriginAccessors.getRacialTraitId);
|
||||
yield call(apiRemoveOptionalOriginsCollection, {
|
||||
@@ -1309,15 +1320,21 @@ export function* handleCurrencyTransactionSet(action) {
|
||||
*/
|
||||
export function* handleRacialTraitChoiceSetRequest(action) {
|
||||
const { racialTraitId, choiceId } = action.payload;
|
||||
yield call(apiRacialTraitChoiceSet, action);
|
||||
const race = yield select(rulesEngineSelectors.getRace);
|
||||
if (race) {
|
||||
const racialTrait = RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId);
|
||||
if (racialTrait) {
|
||||
const racialTraitChoice = RacialTraitAccessors.getChoices(racialTrait).find((choice) => ChoiceAccessors.getId(choice) === choiceId);
|
||||
if (racialTraitChoice) {
|
||||
yield call(autoUpdateChoices, racialTraitChoice);
|
||||
}
|
||||
const racialTrait = race
|
||||
? RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId)
|
||||
: null;
|
||||
// Remove any spells that are associated with the feat from the racial trait choices
|
||||
const feats = racialTrait ? RacialTraitAccessors.getFeats(racialTrait) : [];
|
||||
const featSpellListIds = feats
|
||||
.map(FeatAccessors.getSpellListIds)
|
||||
.reduce((acc, ids) => acc.concat(ids), []);
|
||||
yield call(apiRemoveSpellsBySpellListIds, featSpellListIds);
|
||||
yield call(apiRacialTraitChoiceSet, action);
|
||||
if (racialTrait) {
|
||||
const racialTraitChoice = RacialTraitAccessors.getChoices(racialTrait).find((choice) => ChoiceAccessors.getId(choice) === choiceId);
|
||||
if (racialTraitChoice) {
|
||||
yield call(autoUpdateChoices, racialTraitChoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1343,6 +1360,7 @@ export function* handleClassFeatureChoiceSetRequest(action) {
|
||||
const { classId, classFeatureId, choiceId, choiceType, optionValue } = action.payload;
|
||||
let oldClasses = yield select(rulesEngineSelectors.getClasses);
|
||||
let oldCharClass = oldClasses.find((charClass) => ClassAccessors.getActiveId(charClass) === classId);
|
||||
const spellListIdsToRemove = [];
|
||||
if (choiceType === BuilderChoiceTypeEnum.SUB_CLASS_OPTION && oldCharClass) {
|
||||
const subclass = ClassAccessors.getSubclass(oldCharClass);
|
||||
if (subclass !== null) {
|
||||
@@ -1350,9 +1368,19 @@ export function* handleClassFeatureChoiceSetRequest(action) {
|
||||
.filter((classFeature) => ClassFeatureAccessors.getClassId(classFeature) === subclass.id)
|
||||
.map(ClassFeatureAccessors.getSpellListIds)
|
||||
.reduce((acc, ids) => acc.concat(ids), []);
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIds);
|
||||
spellListIdsToRemove.push(...spellListIds);
|
||||
}
|
||||
}
|
||||
if (choiceType === BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION && oldCharClass) {
|
||||
const classFeature = ClassAccessors.getClassFeatures(oldCharClass).find((classFeature) => ClassFeatureAccessors.getId(classFeature) === classFeatureId);
|
||||
const featsFromClassFeature = classFeature ? ClassFeatureAccessors.getFeats(classFeature) : [];
|
||||
featsFromClassFeature.forEach((feat) => {
|
||||
const spellListIds = FeatAccessors.getSpellListIds(feat);
|
||||
spellListIdsToRemove.push(...spellListIds);
|
||||
});
|
||||
}
|
||||
//remove any spells that are associated with the spell lists from the subclass or feat choices
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove);
|
||||
yield call(apiClassFeatureChoiceSet, action);
|
||||
const classes = yield select(rulesEngineSelectors.getClasses);
|
||||
const charClass = classes.find((charClass) => !!oldCharClass && ClassAccessors.getId(charClass) === ClassAccessors.getId(oldCharClass));
|
||||
@@ -1453,12 +1481,21 @@ export function* handleClassLevelSetRequest(action) {
|
||||
const oldClassId = ClassAccessors.getId(oldCharClass);
|
||||
const removedClassFeatures = ClassAccessors.getActiveClassFeatures(oldCharClass).filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level);
|
||||
const isRemovingSubclass = removedClassFeatures.some((feature) => ClassFeatureAccessors.getChoices(feature).some((choice) => ChoiceAccessors.getType(choice) === BuilderChoiceTypeEnum.SUB_CLASS_OPTION));
|
||||
const spellListIdsFromFeatsToRemove = [];
|
||||
removedClassFeatures.forEach((classFeature) => {
|
||||
const feats = ClassFeatureAccessors.getFeats(classFeature);
|
||||
feats.forEach((feat) => {
|
||||
const spellListIds = FeatAccessors.getSpellListIds(feat);
|
||||
spellListIdsFromFeatsToRemove.push(...spellListIds);
|
||||
});
|
||||
});
|
||||
const removedSpellListIds = ClassAccessors.getActiveClassFeatures(oldCharClass)
|
||||
.filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level ||
|
||||
(isRemovingSubclass && ClassFeatureAccessors.getClassId(classFeature) !== oldClassId))
|
||||
.map(ClassFeatureAccessors.getSpellListIds)
|
||||
.reduce((acc, spellListIds) => acc.concat(spellListIds), []);
|
||||
yield call(apiRemoveSpellsBySpellListIds, removedSpellListIds);
|
||||
// Remove spells associated with the spell lists from the removed class features
|
||||
yield call(apiRemoveSpellsBySpellListIds, [...removedSpellListIds, ...spellListIdsFromFeatsToRemove]);
|
||||
}
|
||||
}
|
||||
yield call(apiClassLevelSet, action);
|
||||
@@ -1582,7 +1619,21 @@ function* updateBackgroundResult(action, data) {
|
||||
export function* handleBackgroundSetRequest(action) {
|
||||
const existingBackground = yield select(rulesEngineSelectors.getBackgroundInfo);
|
||||
if (existingBackground !== null) {
|
||||
yield call(apiRemoveSpellsBySpellListIds, BackgroundAccessors.getSpellListIds(existingBackground));
|
||||
//start an array of spellListIds to remove with any spellListIds from the existing background
|
||||
const spellListIdsToRemove = BackgroundAccessors.getSpellListIds(existingBackground);
|
||||
//check for any granted or chosen feats on the background and get their spellListIds
|
||||
const featLookup = yield select(rulesEngineSelectors.getFeatLookup);
|
||||
const chosenFeatIds = BackgroundAccessors.getFeatLists(existingBackground)
|
||||
.map((featList) => featList.chosenFeatId)
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||
chosenFeatIds.forEach((featId) => {
|
||||
const feat = HelperUtils.lookupDataOrFallback(featLookup, featId);
|
||||
if (feat) {
|
||||
spellListIdsToRemove.push(...FeatAccessors.getSpellListIds(feat));
|
||||
}
|
||||
});
|
||||
//remove any spellListIds that were collected
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove);
|
||||
}
|
||||
yield put(callCommitAction(characterActions.backgroundHasCustomSet, false));
|
||||
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.putCharacterBackground, action.payload);
|
||||
|
||||
Reference in New Issue
Block a user