<commit_before>import { DimItem } from './../../inventory/item-types';
import _ from 'lodash';
import { LockableBuckets, ArmorSet, LockedArmor2ModMap, LockedArmor2Mod } from './../types';
import { DestinyEnergyType } from 'bungie-api-ts/destiny2';
import { getSpecialtySocketMetadata, Armor2ModPlugCategories } from 'app/utils/item-utils';

const energyOrder = [
  DestinyEnergyType.Void,
  DestinyEnergyType.Thermal,
  DestinyEnergyType.Arc,
  DestinyEnergyType.Any
];

/**
 * Checks that:
 *   1. The armour piece is Armour 2.0
 *   2. The mod matches the Armour energy OR the mod has the any Energy type
 */
const doEnergiesMatch = (mod: LockedArmor2Mod, item: DimItem) =>
  item.isDestiny2() &&
  item.energy &&
  (mod.mod.plug.energyCost.energyType === DestinyEnergyType.Any ||
    mod.mod.plug.energyCost.energyType === item.energy?.energyType);

/**
 * Assignes the general mods to armour pieces in assignments, including the energy specific ones
 * i.e. Void Resist ect
 */
function assignGeneralMods(
  setToMatch: readonly DimItem[],
  generalMods: LockedArmor2Mod[],
  assignments: Record<number, LockedArmor2Mod[]>
): void {
  const armour2Items = setToMatch.filter((item) => item.isDestiny2() && item.energy);
  if (generalMods && armour2Items.length < generalMods.length) {
    return;
  }

  const generalModsByEnergyType = _.groupBy(
    generalMods,
    (mod) => mod.mod.plug.energyCost.energyType
  );

  const armourByEnergyType = _.groupBy(
    setToMatch,
    (item) => item.isDestiny2() && item.energy?.energyType
  );

  let piecesLeft = [...setToMatch];
  for (const energyType of energyOrder) {
    if (generalModsByEnergyType[energyType]) {
      for (let i = 0; i < generalModsByEnergyType[energyType].length; i++) {
        const mod = generalModsByEnergyType[energyType][i];
        if (
          energyType !== DestinyEnergyType.Any &&
          armourByEnergyType[energyType] &&
          i < armourByEnergyType[energyType].length
        ) {
          const piece = armourByEnergyType[energyType][i];
          assignments[piece.hash].push(mod);
          piecesLeft = piecesLeft.filter((item) => item !== piece);
        } else if (energyType === DestinyEnergyType.Any && piecesLeft.length) {
          assignments[piecesLeft[0].hash].push(mod);
          piecesLeft.slice(0, 1);
        }
      }
    }
  }
}

/**
 * If the energies match, this will assign the mods to the item in assignments.
 */
function assignModsForSlot(
  item: DimItem,
  mods: LockedArmor2Mod[],
  assignments: Record<number, LockedArmor2Mod[]>
): void {
  if (!mods?.length || mods.every((mod) => doEnergiesMatch(mod, item))) {
    assignments[item.hash] = [...assignments[item.hash], ...mods];
  }
}

/**
 * Checks to see if the passed in seasonal mods can be assigned to the armour set.
 */
function assignAllSeasonalMods(
  setToMatch: readonly DimItem[],
  seasonalMods: readonly LockedArmor2Mod[],
  assignments: Record<number, LockedArmor2Mod[]>
): void {
  const firstValidSetArmor2Count = setToMatch.reduce(
    (total, item) => (item.isDestiny2() && item.energy ? total + 1 : total),
    0
  );

  if (!seasonalMods || seasonalMods.length > 5 || seasonalMods.length > firstValidSetArmor2Count) {
    return;
  }

  const modsByArmorBucket = {};

  // Build up an array of possible mods for each item in the set.
  for (const mod of seasonalMods) {
    for (const item of setToMatch) {
      const itemModCategories =
        getSpecialtySocketMetadata(item)?.compatiblePlugCategoryHashes || [];

      if (itemModCategories.includes(mod.mod.plug.plugCategoryHash) && doEnergiesMatch(mod, item)) {
        if (!modsByArmorBucket[item.bucket.hash]) {
          modsByArmorBucket[item.bucket.hash] = [];
        }

        modsByArmorBucket[item.bucket.hash].push(mod);
      }
    }
  }

  // From the possible mods try and find a combination that includes all seasonal mods
  for (const helmetMod of modsByArmorBucket[LockableBuckets.helmet] || [null]) {
    for (const armsMod of modsByArmorBucket[LockableBuckets.gauntlets] || [null]) {
      for (const chestMod of modsByArmorBucket[LockableBuckets.chest] || [null]) {
        for (const legsMod of modsByArmorBucket[LockableBuckets.leg] || [null]) {
          for (const classMod of modsByArmorBucket[LockableBuckets.classitem] || [null]) {
            const setMods = [helmetMod, armsMod, chestMod, legsMod, classMod];
            const applicableMods = setMods.filter(Boolean);
            const containsAllLocked =
              seasonalMods.every((item) => applicableMods.includes(item)) &&
              _.uniq(applicableMods).length === applicableMods.length;

            if (containsAllLocked) {
              for (let i = 0; i < setMods.length; i++) {
                if (setMods[i]) {
                  assignModsForSlot(setToMatch[i], [setMods[i]], assignments);
                }
              }
            }
          }
        }
      }
    }
  }
}

export function assignModsToArmorSet(
  setToMatch: readonly DimItem[],
  lockedArmor2Mods: LockedArmor2ModMap
): Record<number, LockedArmor2Mod[]> {
  const assignments: Record<number, LockedArmor2Mod[]> = {};

  for (const item of setToMatch) {
    assignments[item.hash] = new Array<LockedArmor2Mod>();
  }

  assignGeneralMods(setToMatch, lockedArmor2Mods[Armor2ModPlugCategories.general], assignments);

  assignModsForSlot(setToMatch[0], lockedArmor2Mods[Armor2ModPlugCategories.helmet], assignments);
  assignModsForSlot(
    setToMatch[1],
    lockedArmor2Mods[Armor2ModPlugCategories.gauntlets],
    assignments
  );<commit_msg>Fixed issue where seasonal mods were gettings assigned multiple times.<commit_after>import { DimItem } from './../../inventory/item-types';
import _ from 'lodash';
import { LockableBuckets, ArmorSet, LockedArmor2ModMap, LockedArmor2Mod } from './../types';
import { DestinyEnergyType } from 'bungie-api-ts/destiny2';
import { getSpecialtySocketMetadata, Armor2ModPlugCategories } from 'app/utils/item-utils';

const energyOrder = [
  DestinyEnergyType.Void,
  DestinyEnergyType.Thermal,
  DestinyEnergyType.Arc,
  DestinyEnergyType.Any
];

/**
 * Checks that:
 *   1. The armour piece is Armour 2.0
 *   2. The mod matches the Armour energy OR the mod has the any Energy type
 */
const doEnergiesMatch = (mod: LockedArmor2Mod, item: DimItem) =>
  item.isDestiny2() &&
  item.energy &&
  (mod.mod.plug.energyCost.energyType === DestinyEnergyType.Any ||
    mod.mod.plug.energyCost.energyType === item.energy?.energyType);

/**
 * Assignes the general mods to armour pieces in assignments, including the energy specific ones
 * i.e. Void Resist ect
 */
function assignGeneralMods(
  setToMatch: readonly DimItem[],
  generalMods: LockedArmor2Mod[],
  assignments: Record<number, LockedArmor2Mod[]>
): void {
  const armour2Items = setToMatch.filter((item) => item.isDestiny2() && item.energy);
  if (generalMods && armour2Items.length < generalMods.length) {
    return;
  }

  const generalModsByEnergyType = _.groupBy(
    generalMods,
    (mod) => mod.mod.plug.energyCost.energyType
  );

  const armourByEnergyType = _.groupBy(
    setToMatch,
    (item) => item.isDestiny2() && item.energy?.energyType
  );

  let piecesLeft = [...setToMatch];
  for (const energyType of energyOrder) {
    if (generalModsByEnergyType[energyType]) {
      for (let i = 0; i < generalModsByEnergyType[energyType].length; i++) {
        const mod = generalModsByEnergyType[energyType][i];
        if (
          energyType !== DestinyEnergyType.Any &&
          armourByEnergyType[energyType] &&
          i < armourByEnergyType[energyType].length
        ) {
          const piece = armourByEnergyType[energyType][i];
          assignments[piece.hash].push(mod);
          piecesLeft = piecesLeft.filter((item) => item !== piece);
        } else if (energyType === DestinyEnergyType.Any && piecesLeft.length) {
          assignments[piecesLeft[0].hash].push(mod);
          piecesLeft.slice(0, 1);
        }
      }
    }
  }
}

/**
 * If the energies match, this will assign the mods to the item in assignments.
 */
function assignModsForSlot(
  item: DimItem,
  mods: LockedArmor2Mod[],
  assignments: Record<number, LockedArmor2Mod[]>
): void {
  if (!mods?.length || mods.every((mod) => doEnergiesMatch(mod, item))) {
    assignments[item.hash] = [...assignments[item.hash], ...mods];
  }
}

/**
 * Checks to see if the passed in seasonal mods can be assigned to the armour set.
 */
function assignAllSeasonalMods(
  setToMatch: readonly DimItem[],
  seasonalMods: readonly LockedArmor2Mod[],
  assignments: Record<number, LockedArmor2Mod[]>
): void {
  const firstValidSetArmor2Count = setToMatch.reduce(
    (total, item) => (item.isDestiny2() && item.energy ? total + 1 : total),
    0
  );

  if (!seasonalMods || seasonalMods.length > 5 || seasonalMods.length > firstValidSetArmor2Count) {
    return;
  }

  const modsByArmorBucket = {};

  // Build up an array of possible mods for each item in the set.
  for (const mod of seasonalMods) {
    for (const item of setToMatch) {
      const itemModCategories =
        getSpecialtySocketMetadata(item)?.compatiblePlugCategoryHashes || [];

      if (itemModCategories.includes(mod.mod.plug.plugCategoryHash) && doEnergiesMatch(mod, item)) {
        if (!modsByArmorBucket[item.bucket.hash]) {
          modsByArmorBucket[item.bucket.hash] = [];
        }

        modsByArmorBucket[item.bucket.hash].push(mod);
      }
    }
  }

  // From the possible mods try and find a combination that includes all seasonal mods
  for (const helmetMod of modsByArmorBucket[LockableBuckets.helmet] || [null]) {
    for (const armsMod of modsByArmorBucket[LockableBuckets.gauntlets] || [null]) {
      for (const chestMod of modsByArmorBucket[LockableBuckets.chest] || [null]) {
        for (const legsMod of modsByArmorBucket[LockableBuckets.leg] || [null]) {
          for (const classMod of modsByArmorBucket[LockableBuckets.classitem] || [null]) {
            const setMods = [helmetMod, armsMod, chestMod, legsMod, classMod];
            const applicableMods = setMods.filter(Boolean);
            const containsAllLocked =
              seasonalMods.every((item) => applicableMods.includes(item)) &&
              _.uniq(applicableMods).length === applicableMods.length;

            if (containsAllLocked) {
              for (let i = 0; i < setMods.length; i++) {
                if (setMods[i]) {
                  assignModsForSlot(setToMatch[i], [setMods[i]], assignments);
                }
              }
              return;
            }
          }
        }
      }
    }
  }
}

export function assignModsToArmorSet(
  setToMatch: readonly DimItem[],
  lockedArmor2Mods: LockedArmor2ModMap
): Record<number, LockedArmor2Mod[]> {
  const assignments: Record<number, LockedArmor2Mod[]> = {};

  for (const item of setToMatch) {
    assignments[item.hash] = new Array<LockedArmor2Mod>();
  }

  assignGeneralMods(setToMatch, lockedArmor2Mods[Armor2ModPlugCategories.general], assignments);

  assignModsForSlot(setToMatch[0], lockedArmor2Mods[Armor2ModPlugCategories.helmet], assignments);
  assignModsForSlot(
    setToMatch[1],
    lockedArmor2Mods[Armor2ModPlugCategories.gauntlets],
    assignments
  );
{'fraction_non_alphanumeric': 0.12131468780618153, 'fraction_numerical': 0.007838247082925092, 'mean_word_length': 3.345201238390093, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '524771', 'n_tokens_mistral': 3675, 'n_tokens_neox': 3556, 'n_words': 768}