Skip to content

Commit

Permalink
Merge pull request #10520 from dghost/loadout_filters
Browse files Browse the repository at this point in the history
Add loadout filters for total, weapon, and armor light level
  • Loading branch information
bhollis committed Jun 14, 2024
2 parents 7964bcc + 77ddd7f commit 7f4df36
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 3 deletions.
3 changes: 2 additions & 1 deletion config/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@
"Season": "Shows loadouts by which season of Destiny 2 they were last modified in.",
"FashionOnly": "Shows loadouts that contain only fashion (shaders or ornaments).",
"ModsOnly": "Shows loadouts that only contain armor mods.",
"Subclass": "Shows loadouts whose subclass name or damage type partially matches the filter text."
"Subclass": "Shows loadouts whose subclass name or damage type partially matches the filter text.",
"LoadoutLight": "Shows loadouts based on their calculated light level. Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits."
},
"Filter": {
"Adept": "\\(Adept\\)",
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Next
* Add `light:` filter to the loadout search, for searching loadouts that equip all weapon and armor slots.

## 8.23.0 <span class="changelog-date">(2024-06-09)</span>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ exports[`buildSearchConfig generates a reasonable filter map: key-value filters
"exactcontains",
"exactname",
"keyword",
"light",
"name",
"notes",
"power",
"season",
"subclass",
]
Expand Down
103 changes: 101 additions & 2 deletions src/app/search/loadouts/search-filters/freeform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { tl } from 'app/i18next-t';
import { DimItem } from 'app/inventory/item-types';
import { getHashtagsFromNote } from 'app/inventory/note-hashtags';
import { DimStore } from 'app/inventory/store-types';
import { findItemForLoadout, getModsFromLoadout } from 'app/loadout-drawer/loadout-utils';
import { findItemForLoadout, getLight, getModsFromLoadout } from 'app/loadout-drawer/loadout-utils';
import { Loadout } from 'app/loadout/loadout-types';
import { powerLevelByKeyword } from 'app/search/power-levels';
import { matchText, plainString } from 'app/search/text-utils';
import { filterMap } from 'app/utils/collections';
import { emptyArray } from 'app/utils/empty';
import { isClassCompatible } from 'app/utils/item-utils';
import { isClassCompatible, itemCanBeEquippedByStoreId } from 'app/utils/item-utils';
import { BucketHashes } from 'data/d2/generated-enums';
import _ from 'lodash';
import { FilterDefinition } from '../../filter-types';
Expand Down Expand Up @@ -41,6 +43,67 @@ function isLoadoutCompatibleWithStore(loadout: Loadout, store: DimStore | undefi
return !store || isClassCompatible(loadout.classType, store.classType);
}

type EquippedItemBuckets = Record<string, DimItem[]>;

/** Convenience check for items that contribute to power level */
function equipsAllItemsForPowerLevel(items: EquippedItemBuckets): Boolean {
return (
(items[BucketHashes.KineticWeapons]?.length > 0 &&
items[BucketHashes.EnergyWeapons]?.length > 0 &&
items[BucketHashes.PowerWeapons]?.length > 0 &&
items[BucketHashes.Helmet]?.length > 0 &&
items[BucketHashes.Gauntlets]?.length > 0 &&
items[BucketHashes.ChestArmor]?.length > 0 &&
items[BucketHashes.LegArmor]?.length > 0 &&
items[BucketHashes.ClassArmor]?.length > 0) ??
false
);
}

/** Convenience function to get all items that contribute to power level */
function allLoadoutItemsForPowerLevel(items: EquippedItemBuckets): DimItem[] {
return [
items[BucketHashes.KineticWeapons],
items[BucketHashes.EnergyWeapons],
items[BucketHashes.PowerWeapons],
items[BucketHashes.Helmet],
items[BucketHashes.Gauntlets],
items[BucketHashes.ChestArmor],
items[BucketHashes.LegArmor],
items[BucketHashes.ClassArmor],
].flat();
}

/**
* Simplified version of getItemsFromLoadoutItems that doesn't generate warnItems, and only
* converts equipped armor and weapons that can be equipped.
*/
function getEquippedItemsFromLoadout(
loadout: Loadout,
d2Definitions: D2ManifestDefinitions,
allItems: DimItem[],
store: DimStore,
): EquippedItemBuckets {
// We have two big requirements here:
// 1. items must be weapons or armor
// 2. items must be able to be equipped by the character
// This may not be sufficient, but for the moment it seems good enough
const dimItems = filterMap(loadout.items, (loadoutItem) => {
if (loadoutItem.equip) {
const newItem = findItemForLoadout(d2Definitions, allItems, store.id, loadoutItem);
if (
newItem &&
(newItem.bucket.inWeapons || newItem.bucket.inArmor) &&
itemCanBeEquippedByStoreId(newItem, store.id, loadout.classType, true)
) {
return newItem;
}
}
});
// Resolve this into an object that tells us what we need to know
return Object.groupBy(dimItems, (item) => item.bucket.hash);
}

const freeformFilters: FilterDefinition<
Loadout,
LoadoutFilterContext,
Expand Down Expand Up @@ -204,6 +267,42 @@ const freeformFilters: FilterDefinition<
return (loadout) => test(loadout.name) || Boolean(loadout.notes && test(loadout.notes));
},
},
{
keywords: ['light', 'power'],
/* t('Filter.PowerKeywords') */
description: tl('LoadoutFilter.LoadoutLight'),
format: 'range',
overload: powerLevelByKeyword,
filter: ({ compare, allItems, d2Definitions, selectedLoadoutsStore }) => {
if (!d2Definitions || !selectedLoadoutsStore || !allItems) {
return () => false;
}
return (loadout: Loadout) => {
if (!isLoadoutCompatibleWithStore(loadout, selectedLoadoutsStore)) {
return false;
}

// Get the equipped items that contribute to the power level (weapons, armor)
const equippedItems = getEquippedItemsFromLoadout(
loadout,
d2Definitions,
allItems,
selectedLoadoutsStore,
);

// Require that the loadout has an item in all weapon + armor slots
if (!equipsAllItemsForPowerLevel(equippedItems)) {
return false;
}

// Calculate light level of items
const lightLevel = Math.floor(
getLight(selectedLoadoutsStore, allLoadoutItemsForPowerLevel(equippedItems)),
);
return Boolean(compare!(lightLevel));
};
},
},
];

export default freeformFilters;
1 change: 1 addition & 0 deletions src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@
"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).",
"LoadoutLight": "Shows loadouts based on their calculated light level. Use the pinnaclecap or softcap keyword instead of a number to refer to the current season's power limits.",
"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.",
"Notes": "Search for loadouts by their notes field.",
Expand Down

0 comments on commit 7f4df36

Please sign in to comment.