From c323d4c762af7c4c58ac4de5a6cd95e18d56aea1 Mon Sep 17 00:00:00 2001 From: Ben Hollis Date: Thu, 29 Aug 2024 21:54:36 -0700 Subject: [PATCH 1/3] Move StatsSet into item search folder --- src/app/search/items/search-filters/dupes.ts | 7 ++++++- .../items/search-filters}/stats-set.test.ts | 0 .../items/search-filters}/stats-set.ts | 0 3 files changed, 6 insertions(+), 1 deletion(-) rename src/app/{loadout-builder/process-worker => search/items/search-filters}/stats-set.test.ts (100%) rename src/app/{loadout-builder/process-worker => search/items/search-filters}/stats-set.ts (100%) diff --git a/src/app/search/items/search-filters/dupes.ts b/src/app/search/items/search-filters/dupes.ts index cf1a5fc877..6a0168df55 100644 --- a/src/app/search/items/search-filters/dupes.ts +++ b/src/app/search/items/search-filters/dupes.ts @@ -2,13 +2,13 @@ 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 { StatsSet } from './stats-set'; const notableTags = ['favorite', 'keep']; @@ -221,6 +221,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/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 From 86fccf1c7acef3e8b247ce6a64258b0b14bce3b9 Mon Sep 17 00:00:00 2001 From: Ben Hollis Date: Thu, 29 Aug 2024 22:48:12 -0700 Subject: [PATCH 2/3] Implement is:perksdupe --- config/i18n.json | 3 +- src/app/search/items/search-filters/dupes.ts | 19 +++++++++ .../search/items/search-filters/perks-set.ts | 40 +++++++++++++++++++ src/locale/en.json | 3 +- 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/app/search/items/search-filters/perks-set.ts diff --git a/config/i18n.json b/config/i18n.json index 6367d37710..ec061eb91b 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -299,6 +299,7 @@ "PatternUnlocked": "Shows items that have a crafting pattern unlocked, even if the item itself isn't crafted.", "Perk": "Shows items where one of their perks or mods has a partial match to the filter text in their name or description. Search for entire phrases using quotes.", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", + "PerksDupe": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "Postmaster": "Items that are currently in the Postmaster.", "PowerLevel": "Shows items based on their power level. $t(Filter.PowerKeywords)", "PowerKeywords": "Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", @@ -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/items/search-filters/dupes.ts b/src/app/search/items/search-filters/dupes.ts index 6a0168df55..e98c427edc 100644 --- a/src/app/search/items/search-filters/dupes.ts +++ b/src/app/search/items/search-filters/dupes.ts @@ -8,6 +8,7 @@ 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: ['perksdupe'], + description: tl('Filter.PerksDupe'), + 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; 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/locale/en.json b/src/locale/en.json index c86412ebe1..ae83babebb 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.", @@ -288,6 +288,7 @@ "PatternUnlocked": "Shows items that have a crafting pattern unlocked, even if the item itself isn't crafted.", "Perk": "Shows items where one of their perks or mods has a partial match to the filter text in their name or description. Search for entire phrases using quotes.", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", + "PerksDupe": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "PinnacleReward": "Shows pursuits which produce a pinnacle reward.", "Postmaster": "Items that are currently in the Postmaster.", "PowerKeywords": "Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", From f117c7f9bb4955148a7505c1b57b9b939268f1c8 Mon Sep 17 00:00:00 2001 From: Ben Hollis Date: Tue, 3 Sep 2024 23:37:01 -0700 Subject: [PATCH 3/3] Rename to is:dupeperks --- config/i18n.json | 2 +- src/app/search/__snapshots__/search-config.test.ts.snap | 1 + src/app/search/items/search-filters/dupes.ts | 4 ++-- src/locale/en.json | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/config/i18n.json b/config/i18n.json index ec061eb91b..ef2c6f2159 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.", @@ -299,7 +300,6 @@ "PatternUnlocked": "Shows items that have a crafting pattern unlocked, even if the item itself isn't crafted.", "Perk": "Shows items where one of their perks or mods has a partial match to the filter text in their name or description. Search for entire phrases using quotes.", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", - "PerksDupe": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "Postmaster": "Items that are currently in the Postmaster.", "PowerLevel": "Shows items based on their power level. $t(Filter.PowerKeywords)", "PowerKeywords": "Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.", 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 e98c427edc..e96fe1a855 100644 --- a/src/app/search/items/search-filters/dupes.ts +++ b/src/app/search/items/search-filters/dupes.ts @@ -205,8 +205,8 @@ const dupeFilters: ItemFilterDefinition[] = [ }, }, { - keywords: ['perksdupe'], - description: tl('Filter.PerksDupe'), + keywords: ['dupeperks'], + description: tl('Filter.DupePerks'), filter: ({ allItems }) => { const duplicates = new Map(); for (const i of allItems) { diff --git a/src/locale/en.json b/src/locale/en.json index ae83babebb..3b87511528 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -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.", @@ -288,7 +289,6 @@ "PatternUnlocked": "Shows items that have a crafting pattern unlocked, even if the item itself isn't crafted.", "Perk": "Shows items where one of their perks or mods has a partial match to the filter text in their name or description. Search for entire phrases using quotes.", "PerkName": "Shows items with a perk or mod whose name matches (exactperk:) or partially matches (perkname:) the filter text. Search for entire phrases using quotes.", - "PerksDupe": "Shows items whose perks are either a duplicate of, or a subset of, another item of the same type.", "PinnacleReward": "Shows pursuits which produce a pinnacle reward.", "Postmaster": "Items that are currently in the Postmaster.", "PowerKeywords": "Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.",