Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Suggestion] Highlight items that can be deleted because they aren't useful in any possible build #142

Open
Makeshift opened this issue May 20, 2023 · 3 comments
Labels
new feature New feature or request

Comments

@Makeshift
Copy link

Makeshift commented May 20, 2023

I have a lot of issues cleaning up the armor in my vault. I usually want to try and keep every potential armor item I might want to use, so my normal workflow for trying to find armor I can delete is something along the lines of "Open d2armorpicker, one by one select each exotic I'll probably want to use at some point, pick the max stats I can with for that exotic (for a variety of different stats), find a set of armor relatively high on the list, and mark all of those armor pieces in DIM as love/keep".

As you can probably imagine, this technique is slow and prone to error. The issue is, all the 'vault cleaning' tools end up marking incredibly important armor that is critical to a build put together by d2armorpicker as junk, and I end up deleting it by mistake (happened today with helmet that I critically needed for a 3x100 build ;-;).

It would be really nice if d2armorpicker could help with cleanup a little bit, while using its powerful stat optimisation to make sure you aren't deleting armor that potentially could be used in a viable build.

I feel like this is a naive approach and could be optimised, but I was thinking:

  • Loop through each exotic for the chosen character
  • Loop through each stat, selecting the highest possible for each in turn with the current exotic
  • Filter the list to the top x% by stat or tier (where x is a user-chosen percentage, eg 10%)
  • Output a DIM query selecting every armor piece used by the filtered list (or inversely, every piece not used, so they can be marked as junk)

Obviously this is quite computationally expensive. I simulated giving this a go with ~132 items in my vault, with "Force to use ANY exotic" selected and "Use security features to prevent app crashes" disabled, and it produced ~478k results in ~7135ms.

I downloaded the JSON for the above and tried to make a hack proof-of-concept (I've been awake farming artifice armor for like, 24 hours, so it doesn't work very well, but some very quick testing seems to show that it works kinda okay)

const topXPercent = 10

// Generated with config { "selectedExotics": [ -2 ], "limitParsedResults": false }
let d2apResults = require('./d2ap_results.json').results

// Keep a list of all armor instance IDs for set shenanigans later
const allArmor = new Set()
const allExoticHashes = new Set([1001356380, 1030017949, ...]) // This is just a static list of all exotic hashes, removed for brevity
const playerOwnedExoticInstances = new Set()
// Bin together builds by their exotic
const exoticBins = {}
for (const result of d2apResults) {
  for (const item of result.items) {
    // The 'hash' is the ID for a generic exotic, the 'instance' is the ID for the item in the players inventory
    // There can only be one exotic armor piece per build, so if the build has an exotic, use that as the key for the bin
    if (allExoticHashes.has(item.hash)) {
      if (!exoticBins[item.instance]) exoticBins[item.instance] = []
      // Since this if statement is only true once per build, we can drop it into the bin now
      exoticBins[item.instance].push(result)
      // This also means we can grab the exotic instance for the player owned exotic set
      playerOwnedExoticInstances.add(item.instance)
    }
    // Instead of breaking out of the loop, we want a copy of all armor pieces so we can do a subtraction on the set later, so doing this now skips another loop we'd have to do later
    allArmor.add(item.instance) 

  }
}

d2apResults = null // Free up memory

// result.statsNoMods is an array of stats that purely the armor provides, so for each result, bin together builds by the index of the highest stat in the array
for (const exotic in exoticBins) {
  const exoticStatBins = {}
  for (const result of exoticBins[exotic]) {
    const highestStatIndex = result.statsNoMods.indexOf(Math.max(...result.statsNoMods))
    if (!exoticStatBins[highestStatIndex]) {
      exoticStatBins[highestStatIndex] = []
    }
    exoticStatBins[highestStatIndex].push(result)
  }
  // Find the highest and lowest stat in each bin and cull the builds that are outside the topXPercent
  for (const statBin in exoticStatBins) {
    exoticStatBins[statBin].sort((a, b) => {
      return b.statsNoMods[statBin] - a.statsNoMods[statBin]
    })
    const statBinHighest = exoticStatBins[statBin][0].statsNoMods[statBin]
    const statBinLowest = exoticStatBins[statBin][exoticStatBins[statBin].length - 1].statsNoMods[statBin]
    const statBinDifference = statBinHighest - statBinLowest
    const lowestAcceptableStat = Math.floor(statBinHighest - (statBinDifference * (topXPercent / 100)))

    exoticStatBins[statBin] = exoticStatBins[statBin].filter(result => {
      return result.statsNoMods[statBin] >= lowestAcceptableStat
    })
  }
  exoticBins[exotic] = exoticStatBins
}

// For each remaining build, make an array of result.items.instance so we know what armor to keep
//  also, make sure we don't delete any exotics we're actually using, just in case
const armorToKeep = new Set([...playerOwnedExoticInstances])
for (const exotic in exoticBins) {
  for (const statBin in exoticBins[exotic]) {
    for (const result of exoticBins[exotic][statBin]) {
      result.items.map(item => item.instance).forEach(armorToKeep.add, armorToKeep)
    }
  }
}
// Anything that isn't in the armorToKeep set is junk
const junkArmor = new Set([...allArmor].filter(x => !armorToKeep.has(x)))
// Generate some DIM queries for the user
const armorToKeepDimQuery = [...armorToKeep].map(instance => `id:'${instance}'`).join(' or ')
const junkArmorDimQuery = [...junkArmor].map(instance => `id:'${instance}'`).join(' or ')

Curious if you think this would be valuable, or if I'm missing a really obvious way of sorting my vault that doesn't involve doing something like this! I concede that simply grabbing the top 10% of armor by stats would probably be sufficient, but d2ap does some complex stuff with optimising wasted stats and such so I wanted to abide by the same selection rules that it uses.

Edit: Sadly, my method breaks after your vault reaches a certain size due to JSON.stringify hitting a RangeError when D2AP tries to export the list of combinations. This could probably be worked around by iterating over the array and manually appending stringified objects to the export var. Considering how niche this is, it probably isn't worth dealing with.

@Makeshift
Copy link
Author

I guess the simpler solution might be to just keep the top couple of each 'cluster' as defined by d2ap

@abea
Copy link

abea commented May 23, 2023

It's hard to see this ending up reliable enough without also leaving stuff on the table. I'm not sure I understand your second bullet correctly but, to take an obvious example, I probably wouldn't care about potential builds for super high strength with Armamentarium (grenade focused). So there's nuance involved that would be tough to capture without being so opinionated that a good number of people wouldn't take issue with it.

I find D2AP useful in inventory cleanup by simply creating a bunch of builds each season, saving them in DIM, and seeing what armor in my inventory is not in any loadout (credit: Datto). More manual, for sure, but it doesn't end up telling me to save a bunch of stuff for loadouts I won't end up making.

There could be value in an intentionally opinionated version of this, but it doesn't seem like something that fits nicely into D2AP. Not without drawing focus from its core value proposition.

@Makeshift
Copy link
Author

@abea That's entirely fair and I understand your points. The way you use it is indeed the way I was using it, but because I'm a clutz it often ended up in me incorrectly managing items. This was an attempt in me making an un-opinionated solution to point out armor items that I absolutely, definitely, can delete - even at the expense of keeping a few pieces of armor that were very unlikely to be used due to the weird stat combinations.

I agree that it likely doesn't fit into D2AP well. I thought to create an issue mainly because D2AP doesn't mesh well with many of the existing vault cleaning tools due to its incredible detail in finding useful stats in armor combinations that other tools would simply ignore. This leads to D2AP finding value in armor that other tools do not, so the only way to be sure that you are able to delete a piece of armor is by using the data created by D2AP.

I totally get that this workflow is convoluted and inefficient (and very specific to me!), I was just seeing if there would be any interest in such a thing.

@TheYeshir13 TheYeshir13 added the new feature New feature or request label Jun 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature New feature or request
Projects
Status: Todo
Development

No branches or pull requests

3 participants