Repository: DestinyItemManager/DIM
Pull request #5534 (2020-08-01 08:17:10)

Feature/max mods 10 energy
This fixes the issue where mods can total up to more than 10 energy with the new mod picker stuff. Since I think I am close to replacing the old mod stuff I don't want to mess around with it as its a little harder to do than this was. Largely because the seasonal mods are a bit disconnected from mods tied to specific slots.

Tagging #5532 


Patch:
diff --git a/src/app/loadout-builder/hooks/useProcess.ts b/src/app/loadout-builder/hooks/useProcess.ts
index 3a676cf1a..865b351ec 100644
--- a/src/app/loadout-builder/hooks/useProcess.ts
+++ b/src/app/loadout-builder/hooks/useProcess.ts
@@ -11,8 +11,9 @@ import {
   MinMax,
   LockedModBase,
   ModPickerCategories,
+  bucketsToCategories,
 } from '../types';
-import { DimItem } from 'app/inventory/item-types';
+import { D2Item } from 'app/inventory/item-types';
 import { ProcessItemsByBucket } from '../processWorker/types';
 import {
   mapDimItemToProcessItem,
@@ -73,13 +74,15 @@ export function useProcess(
     setState({ processing: true, result, currentCleanup: cleanup });
 
     const processItems: ProcessItemsByBucket = {};
-    const itemsById: { [id: string]: DimItem } = {};
+    const itemsById: { [id: string]: D2Item } = {};
 
     for (const [key, items] of Object.entries(filteredItems)) {
       processItems[key] = [];
       for (const item of items) {
         if (item.isDestiny2()) {
-          processItems[key].push(mapDimItemToProcessItem(item));
+          processItems[key].push(
+            mapDimItemToProcessItem(item, lockedArmor2ModMap[bucketsToCategories[item.bucket.hash]])
+          );
           itemsById[item.id] = item;
         }
       }
diff --git a/src/app/loadout-builder/mod-utils.ts b/src/app/loadout-builder/mod-utils.ts
index 4384da226..1cbbf4586 100644
--- a/src/app/loadout-builder/mod-utils.ts
+++ b/src/app/loadout-builder/mod-utils.ts
@@ -1,6 +1,6 @@
-import { DimItem } from '../inventory/item-types';
+import { DimItem, D2Item } from '../inventory/item-types';
 import _ from 'lodash';
-import { LockedArmor2ModMap, LockedArmor2Mod } from './types';
+import { LockedArmor2ModMap, LockedArmor2Mod, ModPickerCategories } from './types';
 import { DestinyEnergyType } from 'bungie-api-ts/destiny2';
 import {
   sortProcessModsOrProcessItems,
@@ -9,6 +9,7 @@ import {
 } from './processWorker/processUtils';
 import { mapArmor2ModToProcessMod, mapDimItemToProcessItem } from './processWorker/mappers';
 import { armor2PlugCategoryHashesByName } from 'app/search/d2-known-values';
+import { ProcessItem } from './processWorker/types';
 
 /**
  * Checks that:
@@ -28,14 +29,14 @@ export const doEnergiesMatch = (mod: LockedArmor2Mod, item: DimItem) =>
  * assignments is mutated in this function as it tracks assigned mods for a particular armour set.
  */
 function assignGeneralMods(
-  setToMatch: readonly DimItem[],
+  setToMatch: ProcessItem[],
   generalMods: LockedArmor2Mod[],
   assignments: Record<string, number[]>
 ): void {
   // Mods need to be sorted before being passed to the assignment function
   const sortedMods = generalMods.map(mapArmor2ModToProcessMod).sort(sortProcessModsOrProcessItems);
 
-  canTakeAllGeneralMods(sortedMods, setToMatch.map(mapDimItemToProcessItem), assignments);
+  canTakeAllGeneralMods(sortedMods, setToMatch, assignments);
 }
 
 /**
@@ -59,14 +60,14 @@ function assignModsForSlot(
  * assignments is mutated in this function as it tracks assigned mods for a particular armour set
  */
 function assignAllSeasonalMods(
-  setToMatch: readonly DimItem[],
+  setToMatch: ProcessItem[],
   seasonalMods: readonly LockedArmor2Mod[],
   assignments: Record<string, number[]>
 ): void {
   // Mods need to be sorted before being passed to the assignment function
   const sortedMods = seasonalMods.map(mapArmor2ModToProcessMod).sort(sortProcessModsOrProcessItems);
 
-  canTakeAllSeasonalMods(sortedMods, setToMatch.map(mapDimItemToProcessItem), assignments);
+  canTakeAllSeasonalMods(sortedMods, setToMatch, assignments);
 }
 
 export function assignModsToArmorSet(
@@ -76,43 +77,54 @@ export function assignModsToArmorSet(
   const assignments: Record<string, number[]> = {};
 
   for (const item of setToMatch) {
-    assignments[item.id] = [];
+    if (!item.isDestiny2()) {
+      return {};
+    } else {
+      assignments[item.id] = [];
+    }
   }
 
-  assignGeneralMods(
-    setToMatch,
-    lockedArmor2Mods[armor2PlugCategoryHashesByName.general],
-    assignments
-  );
+  const [helmet, arms, chest, legs, classItem] = setToMatch as D2Item[];
 
+  assignModsForSlot(helmet, lockedArmor2Mods[armor2PlugCategoryHashesByName.helmet], assignments);
+  assignModsForSlot(arms, lockedArmor2Mods[armor2PlugCategoryHashesByName.gauntlets], assignments);
+  assignModsForSlot(chest, lockedArmor2Mods[armor2PlugCategoryHashesByName.chest], assignments);
+  assignModsForSlot(legs, lockedArmor2Mods[armor2PlugCategoryHashesByName.leg], assignments);
   assignModsForSlot(
-    setToMatch[0],
-    lockedArmor2Mods[armor2PlugCategoryHashesByName.helmet],
-    assignments
-  );
-  assignModsForSlot(
-    setToMatch[1],
-    lockedArmor2Mods[armor2PlugCategoryHashesByName.gauntlets],
-    assignments
-  );
-  assignModsForSlot(
-    setToMatch[2],
-    lockedArmor2Mods[armor2PlugCategoryHashesByName.chest],
-    assignments
-  );
-  assignModsForSlot(
-    setToMatch[3],
-    lockedArmor2Mods[armor2PlugCategoryHashesByName.leg],
-    assignments
-  );
-  assignModsForSlot(
-    setToMatch[4],
+    classItem,
     lockedArmor2Mods[armor2PlugCategoryHashesByName.classitem],
     assignments
   );
 
-  assignAllSeasonalMods(setToMatch, lockedArmor2Mods.seasonal, assignments);
+  const processItems = [
+    mapDimItemToProcessItem(helmet, lockedArmor2Mods[armor2PlugCategoryHashesByName.helmet]),
+    mapDimItemToProcessItem(arms, lockedArmor2Mods[armor2PlugCategoryHashesByName.gauntlets]),
+    mapDimItemToProcessItem(chest, lockedArmor2Mods[armor2PlugCategoryHashesByName.chest]),
+    mapDimItemToProcessItem(legs, lockedArmor2Mods[armor2PlugCategoryHashesByName.leg]),
+    mapDimItemToProcessItem(classItem, lockedArmor2Mods[armor2PlugCategoryHashesByName.classitem]),
+  ];
+
+  assignAllSeasonalMods(processItems, lockedArmor2Mods.seasonal, assignments);
+
+  assignGeneralMods(
+    processItems,
+    lockedArmor2Mods[armor2PlugCategoryHashesByName.general],
+    assignments
+  );
 
   const modsByHash = _.keyBy(Object.values(lockedArmor2Mods).flat(), (mod) => mod.mod.hash);
-  return _.mapValues(assignments, (modHashes) => modHashes.map((modHash) => modsByHash[modHash]));
+  return _.mapValues(assignments, (modHashes) =>
+    modHashes
+      .map((modHash) => modsByHash[modHash])
+      .sort((a) => {
+        // Sort the mods so that they appear in the order general, slot specific, seasonal (mimic the game).
+        if (a.category === ModPickerCategories.general) {
+          return -1;
+        } else if (a.category === ModPickerCategories.seasonal) {
+          return 1;
+        } else {
+          return 0;
+        }
+      })
+  );
 }
diff --git a/src/app/loadout-builder/preProcessFilter.ts b/src/app/loadout-builder/preProcessFilter.ts
index 64783e01f..a9f1c68fa 100644
--- a/src/app/loadout-builder/preProcessFilter.ts
+++ b/src/app/loadout-builder/preProcessFilter.ts
@@ -6,23 +6,15 @@ import {
   ItemsByBucket,
   LockedItemType,
   statValues,
+  bucketsToCategories,
 } from './types';
 import { getItemDamageShortName } from 'app/utils/item-utils';
 import { doEnergiesMatch } from './mod-utils';
 import { canSlotMod } from './utils';
 import { DimItem } from 'app/inventory/item-types';
-import { armor2PlugCategoryHashesByName } from 'app/search/d2-known-values';
 import { DestinySocketCategoryStyle } from 'bungie-api-ts/destiny2';
 import { getMasterworkSocketHashes } from 'app/utils/socket-utils';
 
-const bucketsToCategories = {
-  [LockableBuckets.helmet]: armor2PlugCategoryHashesByName.helmet,
-  [LockableBuckets.gauntlets]: armor2PlugCategoryHashesByName.gauntlets,
-  [LockableBuckets.chest]: armor2PlugCategoryHashesByName.chest,
-  [LockableBuckets.leg]: armor2PlugCategoryHashesByName.leg,
-  [LockableBuckets.classitem]: armor2PlugCategoryHashesByName.classitem,
-};
-
 /**
  * Filter the items map down given the locking and filtering configs.
  */
diff --git a/src/app/loadout-builder/processWorker/mappers.ts b/src/app/loadout-builder/processWorker/mappers.ts
index 74b8f5019..91d53a7e6 100644
--- a/src/app/loadout-builder/processWorker/mappers.ts
+++ b/src/app/loadout-builder/processWorker/mappers.ts
@@ -40,7 +40,10 @@ export function mapSeasonalModsToProcessMods(
       hash: entry.mod.mod.hash,
       season: entry.metadata?.season,
       tag: entry.metadata?.tag,
-      energyType: entry.mod.mod.plug.energyCost.energyType,
+      energy: {
+        type: entry.mod.mod.plug.energyCost.energyType,
+        val: entry.mod.mod.plug.energyCost.energyCost,
+      },
       investmentStats: entry.mod.mod.investmentStats.map(({ statTypeHash, value }) => ({
         statTypeHash,
         value,
@@ -54,7 +57,10 @@ export function mapSeasonalModsToProcessMods(
 export function mapArmor2ModToProcessMod(mod: LockedArmor2Mod): ProcessMod {
   const processMod = {
     hash: mod.mod.hash,
-    energyType: mod.mod.plug.energyCost.energyType,
+    energy: {
+      type: mod.mod.plug.energyCost.energyType,
+      val: mod.mod.plug.energyCost.energyCost,
+    },
     investmentStats: mod.mod.investmentStats,
   };
 
@@ -112,7 +118,10 @@ function mapDimSocketsToProcessSockets(dimSockets: DimSockets): ProcessSockets {
   };
 }
 
-export function mapDimItemToProcessItem(dimItem: D2Item): ProcessItem {
+export function mapDimItemToProcessItem(
+  dimItem: D2Item,
+  modsForSlot: LockedArmor2Mod[]
+): ProcessItem {
   const { bucket, id, type, name, equippingLabel, basePower, stats } = dimItem;
 
   const statMap: { [statHash: number]: number } = {};
@@ -126,7 +135,8 @@ export function mapDimItemToProcessItem(dimItem: D2Item): ProcessItem {
   }
 
   const modMetadata = getSpecialtySocketMetadata(dimItem);
-
+  const costInitial =
+    dimItem.energy && _.sumBy(modsForSlot, (mod) => mod.mod.plug.energyCost.energyCost);
   return {
     bucketHash: bucket.hash,
     id,
@@ -137,7 +147,14 @@ export function mapDimItemToProcessItem(dimItem: D2Item): ProcessItem {
     stats: statMap,
     baseStats: baseStatMap,
     sockets: dimItem.sockets && mapDimSocketsToProcessSockets(dimItem.sockets),
-    energyType: dimItem.energy?.energyType,
+    energy:
+      dimItem.energy && costInitial !== null
+        ? {
+            type: dimItem.energy.energyType,
+            valInitial: costInitial, // this is needed to reset energy used after trying to slot mods
+            val: costInitial,
+          }
+        : null,
     season: modMetadata?.season,
     compatibleModSeasons: modMetadata?.compatibleTags,
   };
diff --git a/src/app/loadout-builder/processWorker/process.ts b/src/app/loadout-builder/processWorker/process.ts
index bea6e6719..17fa34a6f 100644
--- a/src/app/loadout-builder/processWorker/process.ts
+++ b/src/app/loadout-builder/processWorker/process.ts
@@ -314,17 +314,26 @@ export function process(
                 }
               }
 
-              // For armour 2 mods we ignore slot specific mods as we prefilter items based on energy requirements
+              // Reset the used item energy of each item so we can add general and seasonal mod costs again.
+              for (const item of firstValidSet) {
+                if (item.energy) {
+                  item.energy.val = item.energy.valInitial;
+                }
+              }
+
+              // For armour 2 mods we ignore slot specific mods as we prefilter items based on energy requirements.
+              // For mod armour 2 mods we do seasonal first as its more likely to have energy specific mods.
+              // TODO Check validity of this with the energy contraints in.
               if (
                 (processedSeasonalMods.length &&
                   !canTakeAllSeasonalMods(processedSeasonalMods, firstValidSet)) ||
+                (lockedArmor2ModMap.seasonal.length &&
+                  !canTakeAllSeasonalMods(lockedArmor2ModMap.seasonal, firstValidSet)) ||
                 (lockedArmor2ModMap[armor2PlugCategoryHashesByName.general].length &&
                   !canTakeAllGeneralMods(
                     lockedArmor2ModMap[armor2PlugCategoryHashesByName.general],
                     firstValidSet
-                  )) ||
-                (lockedArmor2ModMap.seasonal.length &&
-                  !canTakeAllSeasonalMods(lockedArmor2ModMap.seasonal, firstValidSet))
+                  ))
               ) {
                 continue;
               }
@@ -533,7 +542,7 @@ function generateMixesFromPerksOrStats(
     ),
   ];
 
-  if (stats && item.sockets && item.energyType === undefined) {
+  if (stats && item.sockets && item.energy) {
     for (const socket of item.sockets.sockets) {
       if (socket.plugOptions.length > 1) {
         for (const plug of socket.plugOptions) {
@@ -572,7 +581,7 @@ function getStatValuesWithModsAndMWProcess(
   const baseStats = { ...item.baseStats };
 
   // Checking energy tells us if it is Armour 2.0 (it can have value 0)
-  if (item.sockets && item.energyType !== undefined) {
+  if (item.sockets && item.energy) {
     let masterworkSocketHashes: number[] = [];
 
     // only get masterwork sockets if we aren't manually adding the values
diff --git a/src/app/loadout-builder/processWorker/processUtils.ts b/src/app/loadout-builder/processWorker/processUtils.ts
index 7e046b051..1553e375b 100644
--- a/src/app/loadout-builder/processWorker/processUtils.ts
+++ b/src/app/loadout-builder/processWorker/processUtils.ts
@@ -2,7 +2,10 @@ import { ProcessMod } from './types';
 import { DestinyEnergyType } from 'bungie-api-ts/destiny2';
 
 interface SortParam {
-  energyType?: DestinyEnergyType;
+  energy: {
+    type: DestinyEnergyType;
+    val: number;
+  } | null;
   season?: number;
 }
 
@@ -17,11 +20,17 @@ export interface ProcessItemSubset extends SortParam {
  */
 export function sortProcessModsOrProcessItems(a: SortParam, b: SortParam) {
   if (a.season && b.season) {
-    // any energy is 0 so check undefined rather than falsey
-    if (a.season === b.season && a.energyType !== undefined && b.energyType !== undefined) {
-      return b.energyType - a.energyType;
+    if (a.season === b.season) {
+      if (a.energy && b.energy) {
+        if (a.energy.type === b.energy.type) {
+          return b.energy.val - a.energy.val;
+        } else {
+          return b.energy.type - a.energy.type;
+        }
+      }
+    } else {
+      return b.season - a.season;
     }
-    return b.season - a.season;
     // I don't think the following cases will every happen but I have included them just incase.
   } else if (a.season === undefined) {
     return 1;
@@ -35,9 +44,13 @@ export function sortProcessModsOrProcessItems(a: SortParam, b: SortParam) {
  */
 export function sortGeneralModsOrProcessItem(a: SortParam, b: SortParam) {
   // any energy is 0 so check undefined rather than falsey
-  if (a.energyType !== undefined && b.energyType !== undefined) {
-    return b.energyType - a.energyType;
-  } else if (a.energyType === undefined) {
+  if (a.energy && b.energy) {
+    if (a.energy.type === b.energy.type) {
+      return b.energy.val - a.energy.val;
+    } else {
+      return b.energy.type - a.energy.type;
+    }
+  } else if (!a.energy) {
     return 1;
   }
 
@@ -67,19 +80,23 @@ export function canTakeAllSeasonalMods(
   // Loop over the items and mods in parallel and see if they can be slotted.
   // due to Any energy mods needing to consider skipped items we reset item index after each splice.
   while (modIndex < processedMods.length && itemIndex < sortedItems.length) {
-    const { energyType, tag, hash } = processedMods[modIndex];
+    const { energy, tag, hash } = processedMods[modIndex];
+    const item = sortedItems[itemIndex];
     if (!tag) {
       // This should never happen but if it does we ignore seasonal requirements and log the warning.
       console.warn('Optimiser: Found seasonal mod without season details.');
       return true;
     }
     if (
-      (sortedItems[itemIndex].energyType === energyType || energyType === DestinyEnergyType.Any) &&
-      sortedItems[itemIndex].compatibleModSeasons?.includes(tag)
+      item.energy &&
+      (item.energy.type === energy.type || energy.type === DestinyEnergyType.Any) &&
+      item.energy.val + energy.val <= 10 &&
+      item.compatibleModSeasons?.includes(tag)
     ) {
       if (assignments) {
-        assignments[sortedItems[itemIndex].id].push(hash);
+        assignments[item.id].push(hash);
       }
+      item.energy.val += energy.val;
       sortedItems.splice(itemIndex, 1);
       modIndex += 1;
       itemIndex = 0;
@@ -116,12 +133,17 @@ export function canTakeAllGeneralMods(
   // We need to reset the index after a match to ensure that mods with the Any energy type
   // use up armour items that didn't match an energy type/season first.
   while (modIndex < processedMods.length && itemIndex < sortedItems.length) {
-    const { energyType, hash } = processedMods[modIndex];
-
-    if (sortedItems[itemIndex].energyType === energyType || energyType === DestinyEnergyType.Any) {
+    const { energy, hash } = processedMods[modIndex];
+    const item = sortedItems[itemIndex];
+    if (
+      item.energy &&
+      (item.energy.type === energy.type || energy.type === DestinyEnergyType.Any) &&
+      item.energy.val + energy.val <= 10
+    ) {
       if (assignments) {
-        assignments[sortedItems[itemIndex].id].push(hash);
+        assignments[item.id].push(hash);
       }
+      item.energy.val += energy.val;
       sortedItems.splice(itemIndex, 1);
       modIndex += 1;
       itemIndex = 0;
diff --git a/src/app/loadout-builder/processWorker/types.ts b/src/app/loadout-builder/processWorker/types.ts
index e11b90667..44ea27904 100644
--- a/src/app/loadout-builder/processWorker/types.ts
+++ b/src/app/loadout-builder/processWorker/types.ts
@@ -30,7 +30,15 @@ export interface ProcessItem {
   name: string;
   equippingLabel?: string;
   sockets: ProcessSockets | null;
-  energyType?: DestinyEnergyType;
+  energy: {
+    type: DestinyEnergyType;
+    /** This is used to track the energy used by mods in a build. Using the name cost so that we can use the same sorting
+     * function for ProcessItems and ProcessMods. */
+    val: number;
+    /** This contains the energy usage by slot specific mods in the mod picker. Those mods are preprocessed so we don't
+     * need to recalculate them over and over. */
+    valInitial: number;
+  } | null;
   basePower: number;
   stats: { [statHash: number]: number };
   baseStats: { [statHash: number]: number };
@@ -99,7 +107,11 @@ interface ProcessStat {
 
 export interface ProcessMod {
   hash: number;
-  energyType: DestinyEnergyType;
+  energy: {
+    type: DestinyEnergyType;
+    /** The energy cost of the mod. */
+    val: number;
+  };
   investmentStats: ProcessStat[];
   /** This should only be available in seasonal mods */
   season?: number;
diff --git a/src/app/loadout-builder/types.ts b/src/app/loadout-builder/types.ts
index b0c90336f..773a1c764 100644
--- a/src/app/loadout-builder/types.ts
+++ b/src/app/loadout-builder/types.ts
@@ -134,6 +134,14 @@ export const LockableBuckets = {
   classitem: armorBuckets.classitem,
 };
 
+export const bucketsToCategories = {
+  [LockableBuckets.helmet]: armor2PlugCategoryHashesByName.helmet,
+  [LockableBuckets.gauntlets]: armor2PlugCategoryHashesByName.gauntlets,
+  [LockableBuckets.chest]: armor2PlugCategoryHashesByName.chest,
+  [LockableBuckets.leg]: armor2PlugCategoryHashesByName.leg,
+  [LockableBuckets.classitem]: armor2PlugCategoryHashesByName.classitem,
+};
+
 // to-do: deduplicate this and use D2ArmorStatHashByName instead
 export const statHashes: { [type in StatTypes]: number } = {
   Mobility: D2ArmorStatHashByName.mobility,


Test patch:
diff --git a/src/app/loadout-builder/processWorker/processUtils.test.ts b/src/app/loadout-builder/processWorker/processUtils.test.ts
index ea1c666ad..edd735d08 100644
--- a/src/app/loadout-builder/processWorker/processUtils.test.ts
+++ b/src/app/loadout-builder/processWorker/processUtils.test.ts
@@ -10,22 +10,28 @@ function getMod(
   season: number,
   tag: string,
   energyType: DestinyEnergyType,
-  hash?: number
+  energyCost: number = 0
 ): ProcessMod {
-  return { season, tag, energyType, investmentStats: [], hash: hash || 0 };
+  return {
+    season,
+    tag,
+    energy: { type: energyType, val: energyCost },
+    investmentStats: [],
+    hash: 0, // need to mock this unfortunately
+  };
 }
 
 function getItem(
   season: number,
   energyType: DestinyEnergyType,
   compatibleModSeasons?: string[],
-  id?: string
+  energyCost: number = 0
 ): ProcessItemSubset {
   return {
-    energyType,
+    energy: { type: energyType, val: energyCost },
     season,
     compatibleModSeasons,
-    id: id || 'id',
+    id: 'id', // need to mock this unfortunately
   };
 }
 
@@ -252,3 +258,56 @@ describe('Sorting works for mods and items', () => {
     expect(result).toEqual(true);
   });
 });
+
+/*
+  Ensuring the energy requirement part works
+*/
+describe('Energy requirements correctly filter a set', () => {
+  it('passes when items and mods add up to 10', () => {
+    const mods = [
+      getMod(11, 'arrivals', 2, 5),
+      getMod(11, 'arrivals', 1, 5),
+      getMod(11, 'arrivals', 3, 5),
+    ].sort(sortProcessModsOrProcessItems);
+
+    const items = [
+      getItem(11, 1, ['arrivals', 'worthy'], 5),
+      getItem(11, 2, ['arrivals', 'worthy'], 5),
+      getItem(11, 3, ['arrivals', 'worthy'], 5),
+    ];
+
+    const result = canTakeAllSeasonalMods(mods, items);
+    expect(result).toEqual(true);
+  });
+
+  it('fails when one item and mod add up to more than 10', () => {
+    const mods = [
+      getMod(11, 'arrivals', 2, 5),
+      getMod(11, 'arrivals', 1, 5),
+      getMod(11, 'arrivals', 3, 5),
+    ].sort(sortProcessModsOrProcessItems);
+
+    const items = [
+      getItem(11, 1, ['arrivals', 'worthy'], 5),
+      getItem(11, 2, ['arrivals', 'worthy'], 6),
+      getItem(11, 3, ['arrivals', 'worthy'], 5),
+    ];
+
+    const result = canTakeAllSeasonalMods(mods, items);
+    expect(result).toEqual(false);
+  });
+
+  it('passes when an any mod needs the first mods space', () => {
+    const mods = [getMod(11, 'arrivals', 2, 4), getMod(11, 'arrivals', 0, 5)].sort(
+      sortProcessModsOrProcessItems
+    );
+
+    const items = [
+      getItem(11, 2, ['arrivals', 'worthy'], 5),
+      getItem(11, 2, ['arrivals', 'worthy'], 6),
+    ];
+
+    const result = canTakeAllSeasonalMods(mods, items);
+    expect(result).toEqual(true);
+  });
+});