Skip to content

Commit

Permalink
Loadout filters: contains item or mod take 2 (#10474)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryan-rushton authored May 31, 2024
1 parent 17640bc commit 9849aaf
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 46 deletions.
1 change: 1 addition & 0 deletions config/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
"Instructions": "Click or drag files"
},
"LoadoutFilter": {
"Contains": "Shows loadouts which have an item or a mod matching the filter text. Search for items with spaces in their name using quotes.",
"Name": "Shows loadouts whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.",
"Notes": "Search for loadouts by their notes field.",
"PartialMatch": "Shows loadouts where their name or notes has a partial match to the filter text. Search for entire phrases using quotes.",
Expand Down
11 changes: 2 additions & 9 deletions src/app/inventory/subclass.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { damageNamesByEnum } from 'app/search/search-filter-values';
import { getFirstSocketByCategoryHash } from 'app/utils/socket-utils';
import { LookupTable } from 'app/utils/util-types';
import { DamageType, DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2';
import { DamageType } from 'bungie-api-ts/destiny2';
import { emptyPlugHashes } from 'data/d2/empty-plug-hashes';
import { BucketHashes, ItemCategoryHashes, SocketCategoryHashes } from 'data/d2/generated-enums';
import { ItemCategoryHashes, SocketCategoryHashes } from 'data/d2/generated-enums';
import subclassArc from 'images/subclass-arc.png';
import subclassSolar from 'images/subclass-solar.png';
import subclassStasis from 'images/subclass-stasis.png';
Expand Down Expand Up @@ -62,10 +62,3 @@ export function getDamageTypeForSubclassPlug(item: PluggableInventoryItemDefinit
}
return null;
}

/** Get the DamageType enum value for a subclass item definition. */
export function getDamageTypeForSubclassDef(subclass: DestinyInventoryItemDefinition) {
return subclass.inventory?.bucketTypeHash === BucketHashes.Subclass
? subclass.talentGrid?.hudDamageType
: undefined;
}
2 changes: 1 addition & 1 deletion src/app/loadout-drawer/loadout-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ export function isFashionPlug(modDef: DestinyInventoryItemDefinition | undefined
export function getModsFromLoadout(
defs: D2ManifestDefinitions | undefined,
loadout: Loadout,
unlockedPlugs: Set<number>,
unlockedPlugs = new Set<number>(),
) {
const internalModHashes = loadout.parameters?.mods ?? [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ exports[`buildSearchConfig generates a reasonable filter map: is filters 1`] = `

exports[`buildSearchConfig generates a reasonable filter map: key-value filters 1`] = `
[
"contains",
"exactcontains",
"exactname",
"keyword",
"name",
Expand Down
3 changes: 3 additions & 0 deletions src/app/search/loadouts/loadout-filter-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions';
import { DimLanguage } from 'app/i18n';
import { DimItem } from 'app/inventory/item-types';
import { DimStore } from 'app/inventory/store-types';
import { Loadout } from 'app/loadout/loadout-types';
import { LoadoutsByItem } from 'app/loadout/selectors';
Expand All @@ -19,6 +20,7 @@ export interface LoadoutFilterContext {
selectedLoadoutsStore: DimStore;
loadoutsByItem: LoadoutsByItem;
language: DimLanguage;
allItems: DimItem[];
d2Definitions: D2ManifestDefinitions | undefined;
}

Expand All @@ -32,6 +34,7 @@ export interface LoadoutSuggestionsContext {
* The selected store on the loadouts page
*/
selectedLoadoutsStore?: DimStore;
allItems?: DimItem[];
d2Definitions?: D2ManifestDefinitions;
}

Expand Down
8 changes: 8 additions & 0 deletions src/app/search/loadouts/loadout-search-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { destinyVersionSelector } from 'app/accounts/selectors';
import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions';
import { languageSelector } from 'app/dim-api/selectors';
import { DimLanguage } from 'app/i18n';
import { DimItem } from 'app/inventory/item-types';
import { allItemsSelector } from 'app/inventory/selectors';
import { DimStore } from 'app/inventory/store-types';
import { Loadout } from 'app/loadout/loadout-types';
import { loadoutsSelector } from 'app/loadout/loadouts-selector';
Expand Down Expand Up @@ -35,18 +37,21 @@ export const allLoadoutFilters = [...simpleFilters, ...freeformFilters, ...overl
export const loadoutSuggestionsContextSelector = createSelector(
loadoutsSelector,
selectedLoadoutStoreSelector,
allItemsSelector,
d2ManifestSelector,
makeLoadoutSuggestionsContext,
);

function makeLoadoutSuggestionsContext(
loadouts: Loadout[],
selectedLoadoutsStore: DimStore,
allItems: DimItem[],
d2Definitions: D2ManifestDefinitions | undefined,
): LoadoutSuggestionsContext {
return {
loadouts,
selectedLoadoutsStore,
allItems,
d2Definitions,
};
}
Expand All @@ -62,12 +67,14 @@ function makeLoadoutFilterContext(
selectedLoadoutsStore: DimStore,
loadoutsByItem: LoadoutsByItem,
language: DimLanguage,
allItems: DimItem[],
d2Definitions: D2ManifestDefinitions | undefined,
): LoadoutFilterContext {
return {
selectedLoadoutsStore,
loadoutsByItem,
language,
allItems,
d2Definitions,
};
}
Expand All @@ -81,6 +88,7 @@ const loadoutFilterContextSelector = createSelector(
selectedLoadoutStoreSelector,
loadoutsByItemSelector,
languageSelector,
allItemsSelector,
d2ManifestSelector,
makeLoadoutFilterContext,
);
Expand Down
157 changes: 121 additions & 36 deletions src/app/search/loadouts/search-filters/freeform.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions';
import { tl } from 'app/i18next-t';
import { DimItem } from 'app/inventory/item-types';
import { getHashtagsFromNote } from 'app/inventory/note-hashtags';
import { getDamageTypeForSubclassDef } from 'app/inventory/subclass';
import { DimStore } from 'app/inventory/store-types';
import { findItemForLoadout, getModsFromLoadout } from 'app/loadout-drawer/loadout-utils';
import { Loadout } from 'app/loadout/loadout-types';
import { matchText, plainString } from 'app/search/text-utils';
import { getDamageDefsByDamageType } from 'app/utils/definitions';
import { DestinyItemType } from 'bungie-api-ts/destiny2';
import { emptyArray } from 'app/utils/empty';
import { isClassCompatible } from 'app/utils/item-utils';
import { BucketHashes } from 'data/d2/generated-enums';
import _ from 'lodash';
import { FilterDefinition } from '../../filter-types';
import { quoteFilterString } from '../../query-parser';
Expand All @@ -15,15 +18,29 @@ function deduplicate<T>(someArray: (T | undefined | null)[]) {
return _.compact(Array.from(new Set(someArray)));
}

function subclassDefFromLoadout(loadout: Loadout, d2Definitions: D2ManifestDefinitions) {
function subclassFromLoadout(
loadout: Loadout,
d2Definitions: D2ManifestDefinitions,
allItems: DimItem[] | undefined,
store: DimStore | undefined,
) {
for (const item of loadout.items) {
const itemDef = d2Definitions?.InventoryItem.get(item.hash);
if (itemDef?.itemType === DestinyItemType.Subclass) {
return itemDef;
const resolvedItem = findItemForLoadout(
d2Definitions,
allItems ?? emptyArray(),
store?.id,
item,
);
if (resolvedItem?.bucket.hash === BucketHashes.Subclass) {
return resolvedItem;
}
}
}

function isLoadoutCompatibleWithStore(loadout: Loadout, store: DimStore | undefined) {
return !store || isClassCompatible(loadout.classType, store.classType);
}

const freeformFilters: FilterDefinition<
Loadout,
LoadoutFilterContext,
Expand All @@ -33,8 +50,10 @@ const freeformFilters: FilterDefinition<
keywords: ['name', 'exactname'],
description: tl('LoadoutFilter.Name'),
format: 'freeform',
suggestionsGenerator: ({ loadouts }) =>
loadouts?.map((loadout) => `exactname:${quoteFilterString(loadout.name.toLowerCase())}`),
suggestionsGenerator: ({ loadouts, selectedLoadoutsStore }) =>
loadouts
?.filter((loadout) => isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore))
.map((loadout) => `exactname:${quoteFilterString(loadout.name.toLowerCase())}`),
filter: ({ filterValue, language, lhs }) => {
const test = matchText(filterValue, language, /* exact */ lhs === 'exactname');
return (loadout) => test(loadout.name);
Expand All @@ -44,50 +63,114 @@ const freeformFilters: FilterDefinition<
keywords: ['subclass'],
description: tl('LoadoutFilter.Subclass'),
format: 'freeform',
suggestionsGenerator: ({ loadouts, d2Definitions, selectedLoadoutsStore }) => {
suggestionsGenerator: ({ loadouts, allItems, d2Definitions, selectedLoadoutsStore }) => {
if (!loadouts || !d2Definitions) {
return [];
}
const damageDefs = getDamageDefsByDamageType(d2Definitions);
// TODO (ryan) filter on currently selected character. This info is currently localized
// to the page, so we need to lift that up before it can be done.

return deduplicate(
loadouts.flatMap((loadout) => {
const subclass = subclassDefFromLoadout(loadout, d2Definitions);
if (
!subclass ||
(selectedLoadoutsStore && loadout.classType !== selectedLoadoutsStore?.classType)
) {
if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) {
return;
}
const damageType = getDamageTypeForSubclassDef(subclass)!;
// DamageType.None is 0
const damageName = damageDefs[damageType].displayProperties.name;
const subclass = subclassFromLoadout(
loadout,
d2Definitions,
allItems,
selectedLoadoutsStore,
);
if (!subclass) {
return;
}
const damageName = subclass.element?.displayProperties.name;
return [
`subclass:${quoteFilterString(subclass.displayProperties.name.toLowerCase())}`,
`subclass:${quoteFilterString(damageName.toLowerCase())}`,
`subclass:${quoteFilterString(subclass.name.toLowerCase())}`,
damageName && `subclass:${quoteFilterString(damageName.toLowerCase())}`,
];
}),
);
},
filter: ({ filterValue, language, d2Definitions, selectedLoadoutsStore }) => {
filter: ({ filterValue, language, allItems, d2Definitions, selectedLoadoutsStore }) => {
const test = matchText(filterValue, language, false);
const damageDefs = d2Definitions && getDamageDefsByDamageType(d2Definitions);
return (loadout: Loadout) => {
const subclass = d2Definitions && subclassDefFromLoadout(loadout, d2Definitions);
if (!subclass || subclass.classType !== selectedLoadoutsStore.classType) {
if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) {
return false;
}

const subclass =
d2Definitions &&
subclassFromLoadout(loadout, d2Definitions, allItems, selectedLoadoutsStore);
if (!subclass) {
return false;
}
if (test(subclass.displayProperties.name)) {
if (test(subclass.name)) {
return true;
}
// DamageType.None is 0
const damageType = getDamageTypeForSubclassDef(subclass)!;
const damageName = damageDefs?.[damageType]?.displayProperties.name;

const damageName = subclass.element?.displayProperties.name;
return damageName !== undefined && test(damageName);
};
},
},
{
keywords: ['contains', 'exactcontains'],
description: tl('LoadoutFilter.Contains'),
format: 'freeform',
suggestionsGenerator: ({ d2Definitions, allItems, loadouts, selectedLoadoutsStore }) => {
if (!d2Definitions || !loadouts) {
return [];
}

return deduplicate(
loadouts.flatMap((loadout) => {
if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) {
return;
}

const itemSuggestions = loadout.items.map((item) => {
const resolvedItem = findItemForLoadout(
d2Definitions,
allItems ?? emptyArray(),
selectedLoadoutsStore?.id,
item,
);
return (
resolvedItem && `exactcontains:${quoteFilterString(resolvedItem.name.toLowerCase())}`
);
});
const modSuggestions = getModsFromLoadout(d2Definitions, loadout).map(
(mod) =>
`exactcontains:${quoteFilterString(mod.resolvedMod.displayProperties.name.toLowerCase())}`,
);

return [...itemSuggestions, ...modSuggestions];
}),
);
},
filter: ({ filterValue, language, allItems, d2Definitions, selectedLoadoutsStore, lhs }) => {
const test = matchText(filterValue, language, lhs === 'exactcontains');
return (loadout) => {
if (!d2Definitions || !isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) {
return false;
}

return (
loadout.items.some((item) => {
const resolvedItem = findItemForLoadout(
d2Definitions,
allItems,
selectedLoadoutsStore?.id,
item,
);
return resolvedItem && test(resolvedItem?.name);
}) ||
getModsFromLoadout(d2Definitions, loadout).some((mod) =>
test(mod.resolvedMod.displayProperties.name),
)
);
};
},
},
{
keywords: 'notes',
description: tl('LoadoutFilter.Notes'),
Expand All @@ -102,14 +185,16 @@ const freeformFilters: FilterDefinition<
keywords: 'keyword',
description: tl('LoadoutFilter.PartialMatch'),
format: 'freeform',
suggestionsGenerator: ({ loadouts }) =>
suggestionsGenerator: ({ loadouts, selectedLoadoutsStore }) =>
loadouts
? Array.from(
new Set([
...loadouts.flatMap((loadout) => [
...getHashtagsFromNote(loadout.name),
...getHashtagsFromNote(loadout.notes),
]),
...loadouts
.filter((loadout) => isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore))
.flatMap((loadout) => [
...getHashtagsFromNote(loadout.name),
...getHashtagsFromNote(loadout.notes),
]),
]),
)
: [],
Expand Down
1 change: 1 addition & 0 deletions src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@
"UnlockItem": "Unpin Item"
},
"LoadoutFilter": {
"Contains": "Shows loadouts which have an item or a mod matching the filter text. Search for items with spaces in their name using quotes.",
"FashionOnly": "Shows loadouts that contain only fashion (shaders or ornaments).",
"ModsOnly": "Shows loadouts that only contain armor mods.",
"Name": "Shows loadouts whose name matches (exactname:) or partially matches (name:) the filter text. Search for entire phrases using quotes.",
Expand Down

0 comments on commit 9849aaf

Please sign in to comment.