diff --git a/config/i18n.json b/config/i18n.json index 6552e5cc64..aa597e2ba9 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -866,9 +866,9 @@ "Name": "Wrong Stat Tiers", "Description": "Loadout Optimizer settings for this Loadout specify stat tiers, but the Loadout does not reach them." }, - "LoadoutHasSearchQuery": { - "Name": "Has Search Query", - "Description": "This loadout was created with a search query in Loadout Optimizer." + "InvalidSearchQuery": { + "Name": "Invalid Search Query", + "Description": "This loadout was created with a search query in Loadout Optimizer that is no longer valid, or does not include the armor in this loadout." } }, "Manifest": { diff --git a/src/app/loadout-analyzer/analysis.test.ts b/src/app/loadout-analyzer/analysis.test.ts index f4eb8d52d2..a46ce3b643 100644 --- a/src/app/loadout-analyzer/analysis.test.ts +++ b/src/app/loadout-analyzer/analysis.test.ts @@ -20,7 +20,7 @@ import { armorStats } from 'app/search/d2-known-values'; import { BucketHashes, StatHashes } from 'data/d2/generated-enums'; import { normalToReducedMod } from 'data/d2/reduced-cost-mod-mappings'; import { produce } from 'immer'; -import _ from 'lodash'; +import _, { stubTrue } from 'lodash'; import { DestinyClass, DestinyProfileResponse, @@ -93,6 +93,9 @@ beforeAll(async () => { }, autoModDefs: getAutoMods(defs, unlockedPlugs), unlockedPlugs, + // No idea how to test this + filterFactory: () => stubTrue, + validateQuery: () => ({ valid: true }), }; }); @@ -158,16 +161,10 @@ describe('basic loadout analysis finding tests', () => { expect(resultsWithEmptyFragmentSlots.findings).not.toContain(LoadoutFinding.TooManyFragments); }); - it('finds HasSearchQuery', async () => { + it('finds InvalidSearchQuery', async () => { const results = await analyze(equippedLoadout); - expect(results.findings).not.toContain(LoadoutFinding.LoadoutHasSearchQuery); - - const loadoutWithParameters: Loadout = { - ...equippedLoadout, - parameters: { ...equippedLoadout.parameters, query: '-is:inloadout' }, - }; - const results2 = await analyze(loadoutWithParameters); - expect(results2.findings).toContain(LoadoutFinding.LoadoutHasSearchQuery); + expect(results.findings).not.toContain(LoadoutFinding.InvalidSearchQuery); + // FIXME more tests }); it('finds UsesSeasonalMods/ModsDontFit', async () => { diff --git a/src/app/loadout-analyzer/analysis.ts b/src/app/loadout-analyzer/analysis.ts index d429f1281e..44d200bfbd 100644 --- a/src/app/loadout-analyzer/analysis.ts +++ b/src/app/loadout-analyzer/analysis.ts @@ -29,6 +29,7 @@ import { isLoadoutBuilderItem } from 'app/loadout/item-utils'; import { ModMap, categorizeArmorMods, fitMostMods } from 'app/loadout/mod-assignment-utils'; import { getTotalModStatChanges } from 'app/loadout/stats'; import { MAX_ARMOR_ENERGY_CAPACITY } from 'app/search/d2-known-values'; +import { ItemFilter } from 'app/search/filter-types'; import { count } from 'app/utils/collections'; import { errorLog } from 'app/utils/log'; import { delay } from 'app/utils/promises'; @@ -53,6 +54,8 @@ export async function analyzeLoadout( savedLoStatConstraintsByClass, itemCreationContext, unlockedPlugs, + validateQuery, + filterFactory, }: LoadoutAnalysisContext, storeId: string, classType: DestinyClass, @@ -177,9 +180,15 @@ export async function analyzeLoadout( // We just did some heavy mod assignment stuff, give the event loop a chance await delay(0); + let itemFilter: ItemFilter; if (loadoutParameters.query) { - findings.add(LoadoutFinding.LoadoutHasSearchQuery); + if (validateQuery(loadoutParameters.query).valid) { + itemFilter = filterFactory(loadoutParameters.query); + } else { + findings.add(LoadoutFinding.InvalidSearchQuery); + } } + itemFilter ??= stubTrue; if (loadoutArmor.length === 5) { const statProblems = getStatProblems( @@ -245,9 +254,23 @@ export async function analyzeLoadout( unassignedMods: [], lockedExoticHash: loadoutParameters.exoticArmorHash, armorEnergyRules, - // We also reject loadouts with a search filter - searchFilter: stubTrue, + searchFilter: itemFilter, }); + // If the item filter loadout armor that was previously included, + // this is due to the search filter since we've previously established + // that mods fit and the exotic matches. + if ( + loadoutParameters.query && + loadoutArmor.some( + (item) => + armorForThisClass.some((allItem) => allItem === item) && + !Object.values(filteredItems) + .flat() + .some((filteredItem) => filteredItem === item), + ) + ) { + findings.add(LoadoutFinding.InvalidSearchQuery); + } const modStatChanges = getTotalModStatChanges( defs, diff --git a/src/app/loadout-analyzer/finding-display.ts b/src/app/loadout-analyzer/finding-display.ts index fdd05844e6..5e4911c7b7 100644 --- a/src/app/loadout-analyzer/finding-display.ts +++ b/src/app/loadout-analyzer/finding-display.ts @@ -64,9 +64,9 @@ export const findingDisplays: Record = { description: tl('LoadoutAnalysis.DoesNotSatisfyStatConstraints.Description'), icon: infoIcon, }, - [LoadoutFinding.LoadoutHasSearchQuery]: { - name: tl('LoadoutAnalysis.LoadoutHasSearchQuery.Name'), - description: tl('LoadoutAnalysis.LoadoutHasSearchQuery.Description'), - icon: undefined, + [LoadoutFinding.InvalidSearchQuery]: { + name: tl('LoadoutAnalysis.InvalidSearchQuery.Name'), + description: tl('LoadoutAnalysis.InvalidSearchQuery.Description'), + icon: faExclamationTriangle, }, }; diff --git a/src/app/loadout-analyzer/hooks.tsx b/src/app/loadout-analyzer/hooks.tsx index 1d657d1b90..d7d92df2a5 100644 --- a/src/app/loadout-analyzer/hooks.tsx +++ b/src/app/loadout-analyzer/hooks.tsx @@ -10,6 +10,7 @@ import { loVendorItemsSelector } from 'app/loadout-builder/loadout-builder-vendo import { getAutoMods } from 'app/loadout-builder/process/mappers'; import { Loadout } from 'app/loadout-drawer/loadout-types'; import { d2ManifestSelector } from 'app/manifest/selectors'; +import { filterFactorySelector, validateQuerySelector } from 'app/search/search-filter'; import { currySelector } from 'app/utils/selectors'; import { useLoadVendors } from 'app/vendors/hooks'; import { noop } from 'lodash'; @@ -47,6 +48,8 @@ const autoOptimizationContextSelector = currySelector( autoModSelector, allItemsSelector, loVendorItemsSelector.selector, + filterFactorySelector, + validateQuerySelector, ( itemCreationContext, unlockedPlugs, @@ -54,6 +57,8 @@ const autoOptimizationContextSelector = currySelector( autoModDefs, inventoryItems, vendorItems, + filterFactory, + validateQuery, ) => { const allItems = inventoryItems.concat(vendorItems); return ( @@ -65,6 +70,8 @@ const autoOptimizationContextSelector = currySelector( savedLoStatConstraintsByClass, autoModDefs, allItems, + filterFactory, + validateQuery, } satisfies LoadoutAnalysisContext) ); }, diff --git a/src/app/loadout-analyzer/store.ts b/src/app/loadout-analyzer/store.ts index 1134708862..7d24f9f211 100644 --- a/src/app/loadout-analyzer/store.ts +++ b/src/app/loadout-analyzer/store.ts @@ -249,7 +249,7 @@ export class LoadoutBackgroundAnalyzer { [LoadoutFinding.ModsDontFit]: new Set(), [LoadoutFinding.UsesSeasonalMods]: new Set(), [LoadoutFinding.DoesNotSatisfyStatConstraints]: new Set(), - [LoadoutFinding.LoadoutHasSearchQuery]: new Set(), + [LoadoutFinding.InvalidSearchQuery]: new Set(), }, }; diff --git a/src/app/loadout-analyzer/types.ts b/src/app/loadout-analyzer/types.ts index 3a0fb85a92..5b7846f34c 100644 --- a/src/app/loadout-analyzer/types.ts +++ b/src/app/loadout-analyzer/types.ts @@ -2,6 +2,7 @@ import { LoadoutParameters, Settings } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { ItemCreationContext } from 'app/inventory/store/d2-item-factory'; import { AutoModDefs, ResolvedStatConstraint } from 'app/loadout-builder/types'; +import { ItemFilter } from 'app/search/filter-types'; /** The analysis results for a single loadout. */ export interface LoadoutAnalysisResult { @@ -61,13 +62,8 @@ export const enum LoadoutFinding { ModsDontFit, /** The armor set does not match the saved stat constraints. */ DoesNotSatisfyStatConstraints, - /** - * The loadout has a search query which complicates analysis. - * Maybe we could but it's difficult and probably a niche case. - * But whether an item matches a query can change often (tags, or - * imagine creating a Loadout from `-is:inloadout` items...) - */ - LoadoutHasSearchQuery, + /** The loadout parameters search query is invalid or the items don't match them */ + InvalidSearchQuery, } /** These aren't problems per se but they do block further analysis */ @@ -75,7 +71,6 @@ export const blockAnalysisFindings: LoadoutFinding[] = [ LoadoutFinding.NotAFullArmorSet, LoadoutFinding.ModsDontFit, LoadoutFinding.DoesNotRespectExotic, - LoadoutFinding.LoadoutHasSearchQuery, ]; /** @@ -88,4 +83,6 @@ export interface LoadoutAnalysisContext { savedLoStatConstraintsByClass: Settings['loStatConstraintsByClass']; allItems: DimItem[]; autoModDefs: AutoModDefs; + validateQuery: (query: string) => { valid: boolean }; + filterFactory: (query: string) => ItemFilter; } diff --git a/src/locale/en.json b/src/locale/en.json index 68bdfd3ca1..0b1f9d28d6 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -583,9 +583,9 @@ "Description": "Some mods in this loadout are deprecated or do not otherwise fit into any of your armor pieces.", "Name": "Deprecated Mods" }, - "LoadoutHasSearchQuery": { - "Description": "This loadout was created with a search query in Loadout Optimizer.", - "Name": "Has Search Query" + "InvalidSearchQuery": { + "Description": "This loadout was created with a search query in Loadout Optimizer that is no longer valid, or does not include the armor in this loadout.", + "Name": "Invalid Search Query" }, "MissingItems": { "Description": "Some of the items in this loadout are no longer in your inventory.",