message: Use StatSet to toss bad-stat items from groupsc
diff: @@ -21,6 +21,7 @@ import { proxy, releaseProxy, wrap } from 'comlink';
  import { BucketHashes } from 'data/d2/generated-enums';
  import _ from 'lodash';
  import { useEffect, useRef, useState } from 'react';
  +import { StatsSet } from '../process-worker/stats-set';
  import { ProcessItemsByBucket } from '../process-worker/types';
  import { ArmorSet, ItemsByBucket, StatFilters, StatRanges } from '../types';
  import {
  @@ -133,7 +134,7 @@ export function useProcess(
  activityMods
  );
  - for (const group of Object.values(groupedItems)) {
  + for (const group of groupedItems) {
  const item = group.length ? group[0] : null;
  if (item && defs) {
  @@ -223,9 +224,11 @@ function createWorker() {
  return { worker, cleanup };
  }
  -// comparator for sorting items in groups generated by groupItems.
  +// comparator for sorting items in groups generated by groupItems. These items will all have the same stats.
  const groupComparator = chainComparator(
  + // Prefer higher-energy (ideally masterworked)
  compareBy((item: DimItem) => -(item.energy?.energyCapacity || 0)),
  + // Prefer items that are equipped
  compareBy((item: DimItem) => (item.equipped ? 0 : 1))
  );
  @@ -304,11 +307,12 @@ function groupItems(
  return groupId;
  };
  - // Group items by their exact stats
  - const statGroupingFn = (item: DimItem) => {
  + const statsCache = new Map<DimItem, number[]>();
  + for (const item of items) {
  const statValues: number[] = [];
  const statsByHash = item.stats && keyByStatHash(item.stats);
  // Ensure ordering of stats
  + // TODO: statOrder includes disabled stats, should we omit them?
  if (statsByHash) {
  for (const statHash of statOrder) {
  let value = statsByHash[statHash]!.base;
  @@ -319,13 +323,44 @@ function groupItems(
  statValues.push(value);
  }
  }
  + statsCache.set(item, statValues);
  + }
  - return statValues.toString();
  - };
  + // Group items by their exact stats
  + const statGroupingFn = (item: DimItem) => statsCache.get(item)!.toString();
  +
  + const modGroups = _.groupBy(items, modGroupingFn);
  +
  + // Final grouping by both mod requirements AND exact stats
  + const groups: DimItem[][] = [];
  +
  + // Go through each grouping-by-mod, throw out any items with worse stats than
  + // another item in that group, then use what's left to build groups by exact stats.
  + for (const group of Object.values(modGroups)) {
  + const statSet = new StatsSet<DimItem>();
  + const keepSet: DimItem[] = [];
  - const groups = _.groupBy(items, (item) => statGroupingFn(item) + modGroupingFn(item));
  + // Build the stat set
  + for (const item of group) {
  + const stats = statsCache.get(item);
  + if (stats) {
  + statSet.insert(stats, item);
  + }
  + }
  +
  + // Loop back through and see if anything in this group was better
  + for (const item of group) {
  + const stats = statsCache.get(item);
  + if (stats && !statSet.doBetterStatsExist(stats)) {
  + keepSet.push(item);
  + }
  + }
  +
  + const groupedByStats = _.groupBy(keepSet, statGroupingFn);
  + groups.push(...Object.values(groupedByStats));
  + }
  - for (const group of Object.values(groups)) {
  + for (const group of groups) {
  group.sort(groupComparator);
  }
  