diff --git a/config/i18n.json b/config/i18n.json index 0141b938f1..edc3053391 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -239,6 +239,7 @@ "Dupe": "Shows duplicate items, including reissues", "DupeCount": "Items that have the specified number of duplicates.", "DupeLower": "Duplicate items, including reissues, that are not the highest power dupe. Only one duplicate is chosen as the highest, and the rest are considered lower.", + "DupePerks": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "Energy": "Shows items that have energy capacity (Armor 2.0).", "Engrams": "Shows engrams.", "EnhancedPerk": "Shows weapons that have the specified number of enhanced perks.", @@ -321,7 +322,7 @@ "Stackable": "Shows items that can stack (ammo synths, strange coin, etc)", "StackFull": "Show items which are at-capacity for their stack (Enhancement Cores, Strange Coins, Gunsmith Materials etc)", "StatLower": "Shows armor whose stats are strictly lower than another of the same type of armor.", - "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats in that class' custom stat total list.", + "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats that are in any of that class' custom stat total list.", "Stats": "Shows items based on a specific stat value. $t(Filter.StatsExtras)", "StatsBase": "Filters armor based on its base stat value, not including attached mods or masterworking. $t(Filter.StatsExtras)", "StatsExtras": "Supports stat addition by connecting multiple stat names with the + or & symbol. There are also special keywords highest, secondhighest, thirdhighest, etc. which match stats based on their rank within an item's stats. Each custom stats also has its own search term, shown in the Custom Stats settings.", diff --git a/src/app/search/__snapshots__/search-config.test.ts.snap b/src/app/search/__snapshots__/search-config.test.ts.snap index d186d8da9d..110ae61455 100644 --- a/src/app/search/__snapshots__/search-config.test.ts.snap +++ b/src/app/search/__snapshots__/search-config.test.ts.snap @@ -27,6 +27,7 @@ exports[`buildSearchConfig generates a reasonable filter map: is filters 1`] = ` "deepsight", "dupe", "dupelower", + "dupeperks", "emblems", "emotes", "energy", diff --git a/src/app/search/items/search-filters/dupes.ts b/src/app/search/items/search-filters/dupes.ts index cf1a5fc877..e96fe1a855 100644 --- a/src/app/search/items/search-filters/dupes.ts +++ b/src/app/search/items/search-filters/dupes.ts @@ -2,13 +2,14 @@ import { stripAdept } from 'app/compare/compare-utils'; import { tl } from 'app/i18next-t'; import { TagValue } from 'app/inventory/dim-item-info'; import { DimItem } from 'app/inventory/item-types'; -import { StatsSet } from 'app/loadout-builder/process-worker/stats-set'; import { DEFAULT_SHADER, armorStats } from 'app/search/d2-known-values'; import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators'; import { isArtifice } from 'app/utils/item-utils'; import { DestinyClass } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; import { ItemFilterDefinition } from '../item-filter-types'; +import { PerksSet } from './perks-set'; +import { StatsSet } from './stats-set'; const notableTags = ['favorite', 'keep']; @@ -203,6 +204,24 @@ const dupeFilters: ItemFilterDefinition[] = [ }; }, }, + { + keywords: ['dupeperks'], + description: tl('Filter.DupePerks'), + filter: ({ allItems }) => { + const duplicates = new Map(); + for (const i of allItems) { + if (i.sockets?.allSockets.some((s) => s.isPerk && s.socketDefinition.defaultVisible)) { + if (!duplicates.has(i.typeName)) { + duplicates.set(i.typeName, new PerksSet()); + } + duplicates.get(i.typeName)!.insert(i); + } + } + return (item) => + item.sockets?.allSockets.some((s) => s.isPerk && s.socketDefinition.defaultVisible) && + Boolean(duplicates.get(item.typeName)?.hasPerkDupes(item)); + }, + }, ]; export default dupeFilters; @@ -221,6 +240,11 @@ export function checkIfIsDupe( ); } +/** + * Compute a set of items that are "stat lower" dupes. These are items for which + * there exists another item with strictly better stats (i.e. better in at least + * one stat and not worse in any stat). + */ function computeStatDupeLower(allItems: DimItem[], relevantStatHashes: number[] = armorStats) { // disregard no-class armor const armor = allItems.filter((i) => i.bucket.inArmor && i.classType !== DestinyClass.Classified); diff --git a/src/app/search/items/search-filters/perks-set.ts b/src/app/search/items/search-filters/perks-set.ts new file mode 100644 index 0000000000..5962b01715 --- /dev/null +++ b/src/app/search/items/search-filters/perks-set.ts @@ -0,0 +1,40 @@ +import { DimItem } from 'app/inventory/item-types'; + +/** + * A Perks can be populated with a bunch of items, and can then answer questions + * such as: + * 1. Are there any items that have (at least) all the same perks (in the same + * columns) as the input item? This covers both exactly-identical perk sets, + * as well as items that are perk-subsets of the input item (e.g. there may + * be another item that has all the same perks, plus some extra options in + * some columns). + */ +export class PerksSet { + // A map from item ID to a list of columns, each of which has a set of perkHashes + mapping = new Map[]>(); + + insert(item: DimItem) { + this.mapping.set(item.id, makePerksSet(item)); + } + + hasPerkDupes(item: DimItem) { + const perksSet = makePerksSet(item); + + for (const [id, set] of this.mapping) { + if (id === item.id) { + continue; + } + + if (perksSet.every((column) => set.some((otherColumn) => column.isSubsetOf(otherColumn)))) { + return true; + } + } + return false; + } +} + +function makePerksSet(item: DimItem) { + return item + .sockets!.allSockets.filter((s) => s.isPerk && s.socketDefinition.defaultVisible) + .map((s) => new Set(s.plugOptions.map((p) => p.plugDef.hash))); +} diff --git a/src/app/loadout-builder/process-worker/stats-set.test.ts b/src/app/search/items/search-filters/stats-set.test.ts similarity index 100% rename from src/app/loadout-builder/process-worker/stats-set.test.ts rename to src/app/search/items/search-filters/stats-set.test.ts diff --git a/src/app/loadout-builder/process-worker/stats-set.ts b/src/app/search/items/search-filters/stats-set.ts similarity index 100% rename from src/app/loadout-builder/process-worker/stats-set.ts rename to src/app/search/items/search-filters/stats-set.ts diff --git a/src/locale/en.json b/src/locale/en.json index 9b0b1c91e8..78e6edfbb9 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -219,7 +219,7 @@ "CraftedDupe": "Shows duplicate weapons where at least one of the duplicates is crafted.", "Curated": "Shows items that are a curated roll.", "CurrentClass": "Shows items that are equippable on the currently logged in guardian.", - "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats in that class' custom stat total list.", + "CustomStatLower": "Shows armor whose stats are strictly lower than another of the same type of armor, only taking into account stats that are in any of that class' custom stat total list.", "DamageType": "Shows items based on their damage type.", "Deepsight": "Shows weapons with Deepsight Resonance, which can have their pattern extracted, or which can have Deepsight Resonance enabled using a Deepsight Harmonizer.", "Deprecated": "This filter is no longer supported.", @@ -229,6 +229,7 @@ "Dupe": "Shows duplicate items, including reissues", "DupeCount": "Items that have the specified number of duplicates.", "DupeLower": "Duplicate items, including reissues, that are not the highest power dupe. Only one duplicate is chosen as the highest, and the rest are considered lower.", + "DupePerks": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "Energy": "Shows items that have energy capacity (Armor 2.0).", "Engrams": "Shows engrams.", "Enhanceable": "Shows weapons that can be enhanced.",