diff --git a/src/app/components/authenticated-v2/results/results.component.html b/src/app/components/authenticated-v2/results/results.component.html
index 6f9514e2..7f813804 100644
--- a/src/app/components/authenticated-v2/results/results.component.html
+++ b/src/app/components/authenticated-v2/results/results.component.html
@@ -267,6 +267,31 @@
You cancelled the calculation process
search_off
No results found
Try adjusting your filters or search criteria.
+
+ 1">
+
+ warning
+ Warning: There is not enough legendary armor available for your current
+ settings.
+
+
+
+
+
+
+
+ warning
+ {{ getArmorSlotName(slot.key) }}: No items available for this slot
+ due to your current settings.
+
+
+
+
+
Common issues:
diff --git a/src/app/components/authenticated-v2/results/results.component.scss b/src/app/components/authenticated-v2/results/results.component.scss
index b158684a..f3646e6b 100644
--- a/src/app/components/authenticated-v2/results/results.component.scss
+++ b/src/app/components/authenticated-v2/results/results.component.scss
@@ -373,6 +373,21 @@ tr.result-detail-row {
text-align: center;
color: var(--mdc-theme-on-surface);
+ .no-items-warning {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #ff9800;
+ background: rgba(255, 152, 0, 0.08);
+ border: 1px solid #ff9800;
+ border-radius: 4px;
+ padding: 6px 12px;
+ margin: 8px 0;
+ font-size: 14px;
+ font-weight: 500;
+ width: fit-content;
+ box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.04);
+ }
mat-icon {
font-size: 4rem;
width: 4rem;
diff --git a/src/app/components/authenticated-v2/results/results.component.ts b/src/app/components/authenticated-v2/results/results.component.ts
index 86d9c759..056ef239 100644
--- a/src/app/components/authenticated-v2/results/results.component.ts
+++ b/src/app/components/authenticated-v2/results/results.component.ts
@@ -25,11 +25,12 @@ import { MatSort } from "@angular/material/sort";
import { StatusProviderService } from "../../../services/status-provider.service";
import { animate, state, style, transition, trigger } from "@angular/animations";
import { DestinyClass } from "bungie-api-ts/destiny2";
-import { ArmorSlot } from "../../../data/enum/armor-slot";
+import { ArmorSlot, ArmorSlotNames } from "../../../data/enum/armor-slot";
import { FixableSelection } from "../../../data/buildConfiguration";
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { InventoryArmorSource } from "src/app/data/types/IInventoryArmor";
+import { AvailableItemsService } from "src/app/services/available-items.service";
export interface ResultDefinition {
exotic:
@@ -103,6 +104,7 @@ export class ResultsComponent implements OnInit, OnDestroy {
_config_maximumStatMods: number = 5;
_config_selectedExotics: number[] = [];
+ _selectedExoticSlot: ArmorSlot = ArmorSlot.ArmorSlotNone; // Used to filter results by selected exotic slots
_config_tryLimitWastedStats: boolean = false;
_config_onlyUseMasterworkedExotics: Boolean = false;
_config_onlyUseMasterworkedLegendaries: Boolean = false;
@@ -145,10 +147,21 @@ export class ResultsComponent implements OnInit, OnDestroy {
initializing: boolean = true; // Flag to indicate if the page is still initializing
cancelledCalculation: boolean = false;
+ // Count of items per armor slot, [legendary, exotic]
+ slotsWithNoLegendaryItems: number = 0;
+ itemCountPerSlot: { [key in Exclude]: [number, number] } = {
+ [ArmorSlot.ArmorSlotHelmet]: [0, 0],
+ [ArmorSlot.ArmorSlotGauntlet]: [0, 0],
+ [ArmorSlot.ArmorSlotChest]: [0, 0],
+ [ArmorSlot.ArmorSlotLegs]: [0, 0],
+ [ArmorSlot.ArmorSlotClass]: [0, 0],
+ };
+
constructor(
private inventory: InventoryService,
public configService: ConfigurationService,
- public status: StatusProviderService
+ public status: StatusProviderService,
+ public availableItems: AvailableItemsService
) {
// Load saved view mode from localStorage
const savedViewMode = localStorage.getItem("d2ap-view-mode") as "table" | "cards";
@@ -158,6 +171,29 @@ export class ResultsComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
+ // Check for item counts per slot so that we can show a warning if no items are available for a slot
+ for (let slot of Object.values(ArmorSlot) as ArmorSlot[]) {
+ if (slot === ArmorSlot.ArmorSlotNone) continue; // Skip the 'none' slot
+ if (this.itemCountPerSlot[slot] === undefined || this.itemCountPerSlot[slot] === null)
+ continue;
+ this.itemCountPerSlot[slot] = [0, 0];
+ const sll = slot;
+ this.availableItems
+ .getItemsForSlot$(slot)
+ .pipe(takeUntil(this.ngUnsubscribe))
+ .subscribe((items) => {
+ const exoticCount = items.filter((i) => i.isExotic).length;
+ this.itemCountPerSlot[sll] = [
+ items.length - exoticCount, // Legendary count
+ exoticCount, // Exotic count
+ ];
+
+ this.slotsWithNoLegendaryItems = Object.values(this.itemCountPerSlot).filter(
+ (count) => count[0] === 0
+ ).length;
+ });
+ }
+
this.status.status.pipe(takeUntil(this.ngUnsubscribe)).subscribe((s) => {
this.isCalculatingPermutations = s.calculatingPermutations || s.calculatingResults;
@@ -171,45 +207,54 @@ export class ResultsComponent implements OnInit, OnDestroy {
this.computationProgress = progress;
});
//
- this.configService.configuration.pipe(takeUntil(this.ngUnsubscribe)).subscribe((c: any) => {
- this.selectedClass = c.characterClass;
- this._config_assumeLegendariesMasterworked = c.assumeLegendariesMasterworked;
- this._config_assumeExoticsMasterworked = c.assumeExoticsMasterworked;
- this._config_tryLimitWastedStats = c.tryLimitWastedStats;
-
- this._config_maximumStatMods = c.maximumStatMods;
- this._config_legacyArmor = c.allowLegacyArmor;
- this._config_onlyUseMasterworkedExotics = c.onlyUseMasterworkedExotics;
- this._config_onlyUseMasterworkedLegendaries = c.onlyUseMasterworkedLegendaries;
- this._config_includeCollectionRolls = c.includeCollectionRolls;
- this._config_includeVendorRolls = c.includeVendorRolls;
- this._config_onlyShowResultsWithNoWastedStats = c.onlyShowResultsWithNoWastedStats;
- this._config_assumeEveryLegendaryIsArtifice = c.assumeEveryLegendaryIsArtifice;
- this._config_assumeEveryExoticIsArtifice = c.assumeEveryExoticIsArtifice;
- this._config_enforceFeaturedArmor = c.enforceFeaturedArmor;
- this._config_selectedExotics = c.selectedExotics;
- this._config_armorPerkLimitation = Object.entries(c.armorPerks)
- .filter((v: any) => v[1].value != ArmorPerkOrSlot.Any)
- .map((k: any) => k[1]);
- this._config_modslotLimitation = Object.entries(c.maximumModSlots)
- .filter((v: any) => v[1].value < 5)
- .map((k: any) => k[1]);
-
- let columns = [
- "exotic",
- "health",
- "melee",
- "grenade",
- "super",
- "class",
- "weapon",
- "total",
- "mods",
- ];
- if (c.includeVendorRolls || c.includeCollectionRolls) columns.push("source");
- columns.push("dropdown");
- this.shownColumns = columns;
- });
+ this.configService.configuration
+ .pipe(takeUntil(this.ngUnsubscribe))
+ .subscribe(async (c: any) => {
+ this.selectedClass = c.characterClass;
+ this._config_assumeLegendariesMasterworked = c.assumeLegendariesMasterworked;
+ this._config_assumeExoticsMasterworked = c.assumeExoticsMasterworked;
+ this._config_tryLimitWastedStats = c.tryLimitWastedStats;
+
+ this._config_maximumStatMods = c.maximumStatMods;
+ this._config_legacyArmor = c.allowLegacyArmor;
+ this._config_onlyUseMasterworkedExotics = c.onlyUseMasterworkedExotics;
+ this._config_onlyUseMasterworkedLegendaries = c.onlyUseMasterworkedLegendaries;
+ this._config_includeCollectionRolls = c.includeCollectionRolls;
+ this._config_includeVendorRolls = c.includeVendorRolls;
+ this._config_onlyShowResultsWithNoWastedStats = c.onlyShowResultsWithNoWastedStats;
+ this._config_assumeEveryLegendaryIsArtifice = c.assumeEveryLegendaryIsArtifice;
+ this._config_assumeEveryExoticIsArtifice = c.assumeEveryExoticIsArtifice;
+ this._config_enforceFeaturedArmor = c.enforceFeaturedArmor;
+ this._config_selectedExotics = c.selectedExotics;
+
+ if (c.selectedExotics.length > 0 && c.selectedExotics[0] > 0) {
+ this._selectedExoticSlot = await this.inventory.getSlotByItemHash(c.selectedExotics[0]);
+ } else {
+ this._selectedExoticSlot = ArmorSlot.ArmorSlotNone;
+ }
+
+ this._config_armorPerkLimitation = Object.entries(c.armorPerks)
+ .filter((v: any) => v[1].value != ArmorPerkOrSlot.Any)
+ .map((k: any) => k[1]);
+ this._config_modslotLimitation = Object.entries(c.maximumModSlots)
+ .filter((v: any) => v[1].value < 5)
+ .map((k: any) => k[1]);
+
+ let columns = [
+ "exotic",
+ "health",
+ "melee",
+ "grenade",
+ "super",
+ "class",
+ "weapon",
+ "total",
+ "mods",
+ ];
+ if (c.includeVendorRolls || c.includeCollectionRolls) columns.push("source");
+ columns.push("dropdown");
+ this.shownColumns = columns;
+ });
this.inventory.armorResults.pipe(takeUntil(this.ngUnsubscribe)).subscribe(async (value) => {
this._results = value.results;
@@ -254,6 +299,10 @@ export class ResultsComponent implements OnInit, OnDestroy {
};
}
+ getArmorSlotName(slot: ArmorSlot | string): string {
+ return ArmorSlotNames[slot as ArmorSlot] || "Unknown Slot";
+ }
+
cancelCalculation() {
this.inventory.cancelCalculation();
}
diff --git a/src/app/components/authenticated-v2/settings/desired-mod-limit-selection/slot-limitation-selection/slot-limitation-selection.component.ts b/src/app/components/authenticated-v2/settings/desired-mod-limit-selection/slot-limitation-selection/slot-limitation-selection.component.ts
index 9a4a04cb..d89e31e0 100644
--- a/src/app/components/authenticated-v2/settings/desired-mod-limit-selection/slot-limitation-selection/slot-limitation-selection.component.ts
+++ b/src/app/components/authenticated-v2/settings/desired-mod-limit-selection/slot-limitation-selection/slot-limitation-selection.component.ts
@@ -26,13 +26,12 @@ import {
} from "../../../../../data/enum/armor-stat";
import { DestinyClass } from "bungie-api-ts/destiny2";
import { InventoryService } from "../../../../../services/inventory.service";
-import { DatabaseService } from "../../../../../services/database.service";
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { environment } from "../../../../../../environments/environment";
import { ItemIconServiceService } from "src/app/services/item-icon-service.service";
import { ModUrl } from "../../../results/table-mod-display/table-mod-display.component";
-import { ArmorSystem } from "src/app/data/types/IManifestArmor";
+import { AvailableItemsService } from "../../../../../services/available-items.service";
@Component({
selector: "app-slot-limitation-selection",
@@ -54,7 +53,6 @@ export class SlotLimitationSelectionComponent implements OnInit, OnDestroy {
@Output()
possible: EventEmitter = new EventEmitter();
- fixedExoticInThisSlot: boolean = false;
isPossible: boolean = true;
configSelectedClass: DestinyClass = DestinyClass.Unknown;
configAssumeLegendaryIsArtifice: boolean = false;
@@ -102,76 +100,9 @@ export class SlotLimitationSelectionComponent implements OnInit, OnDestroy {
public config: ConfigurationService,
public inventory: InventoryService,
private iconService: ItemIconServiceService,
- private db: DatabaseService
+ private availableItems: AvailableItemsService
) {}
- public async runPossibilityCheck() {
- const mustCheckArmorPerk = this.armorPerkLock && this.armorPerk != ArmorPerkOrSlot.Any;
-
- let results = 0;
- if (mustCheckArmorPerk) {
- // check if the current slot is locked to a specific exotic
- if (this.fixedExoticInThisSlot) {
- this.configSelectedExotic.forEach(async (exoticHash) => {
- var exotics = await this.db.inventoryArmor
- .where("clazz")
- .equals(this.configSelectedClass)
- .and(
- (f) =>
- f.perk == this.armorPerk ||
- (this.armorPerk == ArmorPerkOrSlot.SlotArtifice &&
- this.configAssumeExoticIsArtifice &&
- f.isExotic &&
- f.armorSystem == ArmorSystem.Armor2) ||
- (this.armorPerk == ArmorPerkOrSlot.SlotArtifice &&
- this.configAssumeLegendaryIsArtifice &&
- !f.isExotic &&
- f.armorSystem == ArmorSystem.Armor2) ||
- (this.armorPerk == ArmorPerkOrSlot.SlotArtifice &&
- this.configAssumeClassItemIsArtifice &&
- f.slot == ArmorSlot.ArmorSlotClass &&
- !f.isExotic &&
- f.armorSystem === ArmorSystem.Armor2)
- )
- .and((f) => f.hash == exoticHash)
- .and((f) => f.isExotic == 1)
- .count();
- results += exotics;
- this.isPossible = results > 0;
- this.possible.next(this.isPossible);
- });
- } else {
- results += await this.db.inventoryArmor
- .where("clazz")
- .equals(this.configSelectedClass)
- .and((f) => this.configSelectedExoticSum == 0 || !f.isExotic)
- .and((f) => f.slot == this.slot)
- .and(
- (f) =>
- f.perk == this.armorPerk ||
- (this.armorPerk == ArmorPerkOrSlot.SlotArtifice &&
- this.configAssumeExoticIsArtifice &&
- f.isExotic &&
- f.armorSystem == ArmorSystem.Armor2) ||
- (this.armorPerk == ArmorPerkOrSlot.SlotArtifice &&
- this.configAssumeLegendaryIsArtifice &&
- !f.isExotic &&
- f.armorSystem == ArmorSystem.Armor2) ||
- (this.armorPerk == ArmorPerkOrSlot.SlotArtifice &&
- this.configAssumeClassItemIsArtifice &&
- f.slot == ArmorSlot.ArmorSlotClass &&
- !f.isExotic &&
- f.armorSystem === ArmorSystem.Armor2)
- )
- .count();
- this.isPossible = results > 0;
- }
- } else {
- this.isPossible = true;
- }
- this.possible.next(this.isPossible);
- }
-
get slotName(): string {
switch (this.slot) {
case ArmorSlot.ArmorSlotHelmet:
@@ -190,20 +121,17 @@ export class SlotLimitationSelectionComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
+ this.availableItems
+ .getItemsForSlot$(this.slot)
+ .pipe(takeUntil(this.ngUnsubscribe))
+ .subscribe((items) => {
+ this.isPossible = items.length > 0;
+ this.possible.next(this.isPossible);
+ });
+
this.config.configuration.pipe(takeUntil(this.ngUnsubscribe)).subscribe(async (c) => {
const newExoticSum = c.selectedExotics.reduce((acc, x) => acc + x, 0);
- var mustRunPossibilityCheck =
- this.configSelectedClass != c.characterClass ||
- this.configAssumeLegendaryIsArtifice != c.assumeEveryLegendaryIsArtifice ||
- this.configAssumeExoticIsArtifice != c.assumeEveryExoticIsArtifice ||
- this.configAssumeClassItemIsArtifice != c.assumeClassItemIsArtifice ||
- this.selection != c.maximumModSlots[this.slot].value ||
- this.armorPerk != c.armorPerks[this.slot].value ||
- this.armorPerkLock != c.armorPerks[this.slot].fixed ||
- this.configSelectedExoticSum != newExoticSum ||
- this.maximumModSlots != c.maximumModSlots[this.slot].value;
-
this.configAssumeLegendaryIsArtifice = c.assumeEveryLegendaryIsArtifice;
this.configAssumeExoticIsArtifice = c.assumeEveryExoticIsArtifice;
this.configAssumeClassItemIsArtifice = c.assumeClassItemIsArtifice;
@@ -214,15 +142,6 @@ export class SlotLimitationSelectionComponent implements OnInit, OnDestroy {
this.maximumModSlots = c.maximumModSlots[this.slot].value;
this.configSelectedExoticSum = newExoticSum;
this.configSelectedExotic = c.selectedExotics;
-
- this.fixedExoticInThisSlot =
- (await this.inventory.getExoticsForClass(c.characterClass))
- // TODO LOOK AT THIS
- .filter((x) => c.selectedExotics.indexOf(x.items[0].hash) > -1)
- .map((e) => e.items[0].slot)
- .indexOf(this.slot) > -1;
-
- if (mustRunPossibilityCheck) await this.runPossibilityCheck();
});
}
@@ -258,4 +177,14 @@ export class SlotLimitationSelectionComponent implements OnInit, OnDestroy {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
+
+ // Helper method to check if items are available for a specific slot
+ hasItemsForSlot(slot: ArmorSlot): boolean {
+ return this.availableItems.hasItemsForSlot(slot);
+ }
+
+ // Helper method to get item count for a specific slot
+ getItemCountForSlot(slot: ArmorSlot): number {
+ return this.availableItems.getItemsForSlot(slot).length;
+ }
}
diff --git a/src/app/components/authenticated-v2/settings/ignored-items-list/ignored-items-list.component.ts b/src/app/components/authenticated-v2/settings/ignored-items-list/ignored-items-list.component.ts
index 308b31d7..14228c14 100644
--- a/src/app/components/authenticated-v2/settings/ignored-items-list/ignored-items-list.component.ts
+++ b/src/app/components/authenticated-v2/settings/ignored-items-list/ignored-items-list.component.ts
@@ -75,7 +75,6 @@ export class IgnoredItemsListComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.config.configuration.pipe(takeUntil(this.ngUnsubscribe)).subscribe(async (cb) => {
- this.characterClass = null;
const newDisabledItems: IDisplayInventoryArmor[][] = [[], [], [], [], [], []];
let items = [];
@@ -118,11 +117,10 @@ export class IgnoredItemsListComponent implements OnInit, OnDestroy {
newDisabledItems[item.slot].push(item);
}
- this.characterClass = cb.characterClass;
-
for (let row of newDisabledItems) {
row.sort((a, b) => a.hash - b.hash);
}
+ this.characterClass = cb.characterClass;
this.disabledItems = newDisabledItems;
});
}
diff --git a/src/app/data/enum/armor-slot.ts b/src/app/data/enum/armor-slot.ts
index a43e1271..4eef94f0 100644
--- a/src/app/data/enum/armor-slot.ts
+++ b/src/app/data/enum/armor-slot.ts
@@ -15,6 +15,8 @@
* along with this program. If not, see .
*/
+import { EnumDictionary } from "../types/EnumDictionary";
+
export enum ArmorSlot {
ArmorSlotNone,
ArmorSlotHelmet,
@@ -23,3 +25,12 @@ export enum ArmorSlot {
ArmorSlotLegs,
ArmorSlotClass,
}
+
+export const ArmorSlotNames: EnumDictionary = {
+ [ArmorSlot.ArmorSlotNone]: "None",
+ [ArmorSlot.ArmorSlotHelmet]: "Helmet",
+ [ArmorSlot.ArmorSlotGauntlet]: "Gauntlet",
+ [ArmorSlot.ArmorSlotChest]: "Chest",
+ [ArmorSlot.ArmorSlotLegs]: "Legs",
+ [ArmorSlot.ArmorSlotClass]: "Class Item",
+};
diff --git a/src/app/services/available-items.service.ts b/src/app/services/available-items.service.ts
new file mode 100644
index 00000000..f9c87f02
--- /dev/null
+++ b/src/app/services/available-items.service.ts
@@ -0,0 +1,459 @@
+/*
+ * Copyright (c) 2023 D2ArmorPicker by Mijago.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Injectable } from "@angular/core";
+import { BehaviorSubject, Observable } from "rxjs";
+import { map, distinctUntilChanged, take, debounceTime } from "rxjs/operators";
+import { DatabaseService } from "./database.service";
+import { ConfigurationService } from "./configuration.service";
+import { AuthService } from "./auth.service";
+import { ArmorSlot } from "../data/enum/armor-slot";
+import { ArmorPerkOrSlot } from "../data/enum/armor-stat";
+import { ArmorSystem } from "../data/types/IManifestArmor";
+import { IInventoryArmor, InventoryArmorSource, isEqualItem } from "../data/types/IInventoryArmor";
+import { IPermutatorArmor } from "../data/types/IPermutatorArmor";
+import { BuildConfiguration } from "../data/buildConfiguration";
+import { TierType } from "bungie-api-ts/destiny2";
+import { FORCE_USE_NO_EXOTIC, MAXIMUM_MASTERWORK_LEVEL } from "../data/constants";
+import { isEqual as _isEqual } from "lodash";
+
+export interface AvailableItemsBySlot {
+ [ArmorSlot.ArmorSlotHelmet]: IPermutatorArmor[];
+ [ArmorSlot.ArmorSlotGauntlet]: IPermutatorArmor[];
+ [ArmorSlot.ArmorSlotChest]: IPermutatorArmor[];
+ [ArmorSlot.ArmorSlotLegs]: IPermutatorArmor[];
+ [ArmorSlot.ArmorSlotClass]: IPermutatorArmor[];
+}
+
+type ValidArmorSlot =
+ | ArmorSlot.ArmorSlotHelmet
+ | ArmorSlot.ArmorSlotGauntlet
+ | ArmorSlot.ArmorSlotChest
+ | ArmorSlot.ArmorSlotLegs
+ | ArmorSlot.ArmorSlotClass;
+
+export interface AvailableItemsInfo {
+ itemsBySlot: AvailableItemsBySlot;
+ totalItems: number;
+ filteredClassItemsForGeneration: IPermutatorArmor[];
+}
+
+@Injectable({
+ providedIn: "root",
+})
+export class AvailableItemsService {
+ private _availableItems = new BehaviorSubject({
+ itemsBySlot: {
+ [ArmorSlot.ArmorSlotHelmet]: [],
+ [ArmorSlot.ArmorSlotGauntlet]: [],
+ [ArmorSlot.ArmorSlotChest]: [],
+ [ArmorSlot.ArmorSlotLegs]: [],
+ [ArmorSlot.ArmorSlotClass]: [],
+ },
+ totalItems: 0,
+ filteredClassItemsForGeneration: [],
+ });
+
+ public readonly availableItems$: Observable =
+ this._availableItems.asObservable();
+
+ // Memoized slot observables to ensure consistency
+ private _slotObservables = new Map>();
+
+ constructor(
+ private db: DatabaseService,
+ private config: ConfigurationService,
+ private auth: AuthService
+ ) {
+ // Update available items when config changes
+ this.config.configuration
+ .pipe(
+ debounceTime(1) // Debounce to avoid rapid updates
+ )
+ .subscribe(async (config) => {
+ if (this.auth.isAuthenticated()) {
+ await this.updateAvailableItems(config);
+ }
+ });
+ }
+
+ /**
+ * Manually trigger an update of available items
+ * This should be called by InventoryService when inventory data changes
+ */
+ async refreshAvailableItems(): Promise {
+ if (this.auth.isAuthenticated()) {
+ // Get the current configuration value
+ const currentConfig = await new Promise((resolve) => {
+ this.config.configuration
+ .pipe(take(1))
+ .subscribe((config: BuildConfiguration) => resolve(config));
+ });
+ await this.updateAvailableItems(currentConfig);
+ }
+ }
+
+ /**
+ * Get current available items synchronously
+ */
+ get availableItems(): AvailableItemsInfo {
+ return this._availableItems.value;
+ }
+
+ /**
+ * Get items for a specific slot
+ */
+ getItemsForSlot(slot: ArmorSlot): IPermutatorArmor[] {
+ const validSlots = [
+ ArmorSlot.ArmorSlotHelmet,
+ ArmorSlot.ArmorSlotGauntlet,
+ ArmorSlot.ArmorSlotChest,
+ ArmorSlot.ArmorSlotLegs,
+ ArmorSlot.ArmorSlotClass,
+ ];
+
+ if (!validSlots.includes(slot)) {
+ return [];
+ }
+
+ return this._availableItems.value.itemsBySlot[slot as ValidArmorSlot] || [];
+ }
+
+ /**
+ * Get items for a specific slot as observable
+ */
+ getItemsForSlot$(slot: ArmorSlot): Observable {
+ const validSlots = [
+ ArmorSlot.ArmorSlotHelmet,
+ ArmorSlot.ArmorSlotGauntlet,
+ ArmorSlot.ArmorSlotChest,
+ ArmorSlot.ArmorSlotLegs,
+ ArmorSlot.ArmorSlotClass,
+ ];
+
+ if (!validSlots.includes(slot)) {
+ return new BehaviorSubject([]).asObservable();
+ }
+
+ // Return memoized observable for consistency
+ if (!this._slotObservables.has(slot)) {
+ const slotObservable = this.availableItems$.pipe(
+ map((items) => {
+ const slotItems = items.itemsBySlot[slot as ValidArmorSlot] || [];
+ return slotItems;
+ }),
+ distinctUntilChanged((prev, curr) => _isEqual(prev, curr))
+ );
+ this._slotObservables.set(slot, slotObservable);
+ }
+
+ return this._slotObservables.get(slot)!;
+ }
+
+ /**
+ * Check if any items are available for the given configuration
+ */
+ hasAvailableItems(): boolean {
+ const items = this._availableItems.value;
+ return Object.values(items.itemsBySlot).some((slotItems) => slotItems.length > 0);
+ }
+
+ /**
+ * Check if items are available for a specific slot
+ */
+ hasItemsForSlot(slot: ArmorSlot): boolean {
+ return this.getItemsForSlot(slot).length > 0;
+ }
+
+ private async updateAvailableItems(config: BuildConfiguration): Promise {
+ try {
+ // Get all inventory items for the current class
+ let inventoryItems = (await this.db.inventoryArmor
+ .where("clazz")
+ .equals(config.characterClass)
+ .distinct()
+ .toArray()) as IInventoryArmor[];
+
+ // Apply all filtering logic (extracted from inventory.service.ts and results-builder.worker.ts)
+ const filteredItems = this.applyItemFilters(inventoryItems, config);
+
+ // Convert to permutator armor items
+ const permutatorItems = this.convertToPermutatorArmor(filteredItems);
+
+ // Remove duplicates (collection/vendor items if inventory version exists)
+ const deduplicatedItems = this.removeDuplicateItems(permutatorItems);
+
+ // Group items by slot
+ const itemsBySlot = this.groupItemsBySlot(deduplicatedItems);
+
+ const classItems = this.deduplicateClassItemsForGeneration(
+ itemsBySlot[ArmorSlot.ArmorSlotClass],
+ config
+ );
+
+ this._availableItems.next({
+ itemsBySlot,
+ totalItems: deduplicatedItems.length,
+ filteredClassItemsForGeneration: classItems,
+ });
+ } catch (error) {
+ console.error("Error updating available items:", error);
+ }
+ }
+
+ private applyItemFilters(
+ items: IInventoryArmor[],
+ config: BuildConfiguration
+ ): IInventoryArmor[] {
+ const exoticSlots = items
+ .filter((item) => config.selectedExotics.indexOf(item.hash) > -1)
+ .map((item) => item.slot);
+
+ const exoticLimitedSlots = exoticSlots.length > 0 ? exoticSlots[0] : null;
+
+ return (
+ items
+ // Only armor pieces
+ .filter((item) => item.slot !== ArmorSlot.ArmorSlotNone)
+ // Filter disabled items
+ .filter((item) => config.disabledItems.indexOf(item.itemInstanceId) === -1)
+ // Filter featured armor if enforced
+ .filter((item) => !config.enforceFeaturedArmor || item.isFeatured)
+ // Filter armor system
+ .filter((item) => item.armorSystem === ArmorSystem.Armor3 || config.allowLegacyArmor)
+ // Filter collection/vendor rolls based on settings
+ .filter((item) => {
+ switch (item.source) {
+ case InventoryArmorSource.Collections:
+ return config.includeCollectionRolls;
+ case InventoryArmorSource.Vendor:
+ return config.includeVendorRolls;
+ default:
+ return true;
+ }
+ })
+ // Filter selected exotic enforcement
+ .filter(
+ (item) => config.selectedExotics.indexOf(FORCE_USE_NO_EXOTIC) === -1 || !item.isExotic
+ )
+ // Filter based on selected exotics
+ .filter((item) => {
+ if (config.selectedExotics.length === 0) return true;
+ if (item.isExotic) {
+ return config.selectedExotics.some((exoticHash) => item.hash === exoticHash);
+ } else {
+ // For non-exotic items, ensure no selected exotic conflicts with this slot
+ return item.slot !== exoticLimitedSlots;
+ }
+ })
+ // If it is an exotic class item, and we enforced perks, filter by selected exotic perks
+ // Filter exotic class items based on selected exotic perks
+ .filter((item) => {
+ if (
+ item.slot !== ArmorSlot.ArmorSlotClass ||
+ !item.isExotic ||
+ !config.selectedExoticPerks ||
+ config.selectedExoticPerks.length < 2
+ ) {
+ return true;
+ }
+
+ const firstPerkFilter = config.selectedExoticPerks[0];
+ const secondPerkFilter = config.selectedExoticPerks[1];
+
+ if (firstPerkFilter === ArmorPerkOrSlot.Any && secondPerkFilter === ArmorPerkOrSlot.Any) {
+ return true;
+ }
+
+ if (!item.exoticPerkHash || item.exoticPerkHash.length < 2) {
+ return true;
+ }
+
+ const hasFirstPerk =
+ firstPerkFilter === ArmorPerkOrSlot.Any ||
+ item.exoticPerkHash.includes(firstPerkFilter);
+ const hasSecondPerk =
+ secondPerkFilter === ArmorPerkOrSlot.Any ||
+ item.exoticPerkHash.includes(secondPerkFilter);
+
+ return hasFirstPerk && hasSecondPerk;
+ })
+ // Filter masterworked exotics if required
+ .filter(
+ (item) =>
+ !config.onlyUseMasterworkedExotics ||
+ !(item.rarity === TierType.Exotic && item.masterworkLevel !== MAXIMUM_MASTERWORK_LEVEL)
+ )
+ // Filter masterworked legendaries if required
+ .filter(
+ (item) =>
+ !config.onlyUseMasterworkedLegendaries ||
+ !(
+ item.rarity === TierType.Superior && item.masterworkLevel !== MAXIMUM_MASTERWORK_LEVEL
+ )
+ )
+ // Filter blue armor pieces if not allowed
+ .filter(
+ (item) =>
+ config.allowBlueArmorPieces ||
+ item.rarity === TierType.Exotic ||
+ item.rarity === TierType.Superior
+ )
+ // Filter sunset armor if ignored
+ .filter((item) => !config.ignoreSunsetArmor || !item.isSunset)
+ // Filter armor perks
+ .filter((item) => {
+ const slotConfig = config.armorPerks[item.slot];
+
+ // If perk is set to "Any", allow all items
+ if (slotConfig.value === ArmorPerkOrSlot.Any) return true;
+
+ // If perk is not fixed, allow all items
+ if (!slotConfig.fixed) return true;
+
+ // For exotic items, they might have different perk requirements
+ if (item.isExotic && item.perk === slotConfig.value) return true;
+
+ // Check if the item's perk matches the required perk
+ if (slotConfig.value === item.perk) return true;
+
+ // Special case for artifice assumption
+ if (
+ slotConfig.value === ArmorPerkOrSlot.SlotArtifice &&
+ item.armorSystem === ArmorSystem.Armor2 &&
+ ((config.assumeEveryLegendaryIsArtifice && !item.isExotic) ||
+ (config.assumeEveryExoticIsArtifice && item.isExotic))
+ ) {
+ return true;
+ }
+
+ return false;
+ })
+ );
+ }
+
+ private convertToPermutatorArmor(items: IInventoryArmor[]): IPermutatorArmor[] {
+ return items.map((armor) => ({
+ id: armor.id,
+ hash: armor.hash,
+ slot: armor.slot,
+ clazz: armor.clazz,
+ perk: armor.perk,
+ isExotic: armor.isExotic,
+ rarity: armor.rarity,
+ isSunset: armor.isSunset,
+ masterworkLevel: armor.masterworkLevel,
+ archetypeStats: armor.archetypeStats,
+ mobility: armor.mobility,
+ resilience: armor.resilience,
+ recovery: armor.recovery,
+ discipline: armor.discipline,
+ intellect: armor.intellect,
+ strength: armor.strength,
+ source: armor.source,
+ exoticPerkHash: armor.exoticPerkHash,
+ icon: armor.icon,
+ watermarkIcon: armor.watermarkIcon,
+ name: armor.name,
+ energyLevel: armor.energyLevel,
+ tier: armor.tier,
+ armorSystem: armor.armorSystem,
+ }));
+ }
+
+ private removeDuplicateItems(items: IPermutatorArmor[]): IPermutatorArmor[] {
+ return items.filter((item) => {
+ if (item.source === InventoryArmorSource.Inventory) return true;
+
+ const purchasedItemInstance = items.find(
+ (rhs) =>
+ rhs.source === InventoryArmorSource.Inventory && this.isEqualPermutatorItem(item, rhs)
+ );
+
+ // If this item is a collection/vendor item, ignore it if the player
+ // already has a real copy of the same item.
+ return purchasedItemInstance === undefined;
+ });
+ }
+
+ private isEqualPermutatorItem(item1: IPermutatorArmor, item2: IPermutatorArmor): boolean {
+ // Convert to IInventoryArmor-like objects for comparison
+ const armor1 = item1 as any;
+ const armor2 = item2 as any;
+ return isEqualItem(armor1, armor2);
+ }
+
+ private deduplicateClassItemsForGeneration(
+ classItems: IPermutatorArmor[],
+ config: BuildConfiguration
+ ): IPermutatorArmor[] {
+ // Check if any armor perks is not "any" for deduplication purposes
+ const doesNotRequireArmorPerks = ![
+ config.armorPerks[ArmorSlot.ArmorSlotHelmet].value,
+ config.armorPerks[ArmorSlot.ArmorSlotGauntlet].value,
+ config.armorPerks[ArmorSlot.ArmorSlotChest].value,
+ config.armorPerks[ArmorSlot.ArmorSlotLegs].value,
+ config.armorPerks[ArmorSlot.ArmorSlotClass].value,
+ ].every((v) => v === ArmorPerkOrSlot.Any);
+
+ // Check if any stat is fixed for deduplication purposes
+ const anyStatFixed = Object.values(config.minimumStatTiers).some((v) => v.fixed);
+
+ // Deduplicate class items based on stats and other criteria
+ return classItems.filter(
+ (item, index, self) =>
+ index ===
+ self.findIndex(
+ (i) =>
+ i.mobility === item.mobility &&
+ i.resilience === item.resilience &&
+ i.recovery === item.recovery &&
+ i.discipline === item.discipline &&
+ i.intellect === item.intellect &&
+ i.strength === item.strength &&
+ i.isExotic === item.isExotic &&
+ ((i.isExotic && config.assumeExoticsMasterworked) ||
+ (!i.isExotic && config.assumeLegendariesMasterworked) ||
+ (anyStatFixed && i.masterworkLevel === item.masterworkLevel) ||
+ !anyStatFixed) &&
+ (i.isExotic
+ ? i.exoticPerkHash[0] === item.exoticPerkHash[0] &&
+ i.exoticPerkHash[1] === item.exoticPerkHash[1]
+ : true) &&
+ (doesNotRequireArmorPerks || i.perk === item.perk)
+ )
+ );
+ }
+
+ private groupItemsBySlot(items: IPermutatorArmor[]): AvailableItemsBySlot {
+ const itemsBySlot: AvailableItemsBySlot = {
+ [ArmorSlot.ArmorSlotHelmet]: [],
+ [ArmorSlot.ArmorSlotGauntlet]: [],
+ [ArmorSlot.ArmorSlotChest]: [],
+ [ArmorSlot.ArmorSlotLegs]: [],
+ [ArmorSlot.ArmorSlotClass]: [],
+ };
+
+ for (const item of items) {
+ if (item.slot in itemsBySlot) {
+ (itemsBySlot as any)[item.slot].push(item);
+ }
+ }
+
+ return itemsBySlot;
+ }
+}
diff --git a/src/app/services/inventory.service.ts b/src/app/services/inventory.service.ts
index f918508e..2d8267a5 100644
--- a/src/app/services/inventory.service.ts
+++ b/src/app/services/inventory.service.ts
@@ -22,7 +22,7 @@ import { ConfigurationService } from "./configuration.service";
import { debounceTime } from "rxjs/operators";
import { BehaviorSubject, Observable, ReplaySubject, Subject } from "rxjs";
import { BuildConfiguration } from "../data/buildConfiguration";
-import { ArmorPerkOrSlot, STAT_MOD_VALUES, StatModifier } from "../data/enum/armor-stat";
+import { STAT_MOD_VALUES, StatModifier } from "../data/enum/armor-stat";
import { StatusProviderService } from "./status-provider.service";
import { BungieApiService } from "./bungie-api.service";
import { AuthService } from "./auth.service";
@@ -32,13 +32,8 @@ import {
ResultDefinition,
ResultItem,
} from "../components/authenticated-v2/results/results.component";
-import {
- IInventoryArmor,
- InventoryArmorSource,
- isEqualItem,
- totalStats,
-} from "../data/types/IInventoryArmor";
-import { DestinyClass, TierType } from "bungie-api-ts/destiny2";
+import { IInventoryArmor, InventoryArmorSource, totalStats } from "../data/types/IInventoryArmor";
+import { DestinyClass } from "bungie-api-ts/destiny2";
import { IPermutatorArmorSet } from "../data/types/IPermutatorArmorSet";
import { getSkillTier, getWaste } from "./results-builder.worker";
import { IPermutatorArmor } from "../data/types/IPermutatorArmor";
@@ -47,6 +42,7 @@ import { VendorsService } from "./vendors.service";
import { ModOptimizationStrategy } from "../data/enum/mod-optimization-strategy";
import { isEqual as _isEqual } from "lodash";
import { getDifferences } from "../data/commonFunctions";
+import { AvailableItemsService } from "./available-items.service";
type info = {
results: ResultDefinition[];
@@ -99,7 +95,6 @@ export class InventoryService {
private totalPermutationCount = 0;
private resultMaximumTiers: number[][] = [];
private selectedExotics: IManifestArmor[] = [];
- private inventoryArmorItems: IInventoryArmor[] = [];
private permutatorArmorItems: IPermutatorArmor[] = [];
private endResults: ResultDefinition[] = [];
@@ -110,7 +105,8 @@ export class InventoryService {
private api: BungieApiService,
private auth: AuthService,
private router: Router,
- private vendors: VendorsService
+ private vendors: VendorsService,
+ private availableItems: AvailableItemsService
) {
this._inventory = new ReplaySubject(1);
this.inventory = this._inventory.asObservable();
@@ -208,7 +204,11 @@ export class InventoryService {
triggerResultsUpdate: boolean = true
) {
// trigger armor update behaviour
- if (triggerInventoryUpdate) this._inventory.next(null);
+ if (triggerInventoryUpdate) {
+ this._inventory.next(null);
+ // Refresh available items when inventory changes
+ await this.availableItems.refreshAvailableItems();
+ }
// Do not update results in Help and Cluster pages
if (this.shouldCalculateResults()) {
@@ -302,122 +302,15 @@ export class InventoryService {
);
this.selectedExotics = this.selectedExotics.filter((i) => !!i);
- this.inventoryArmorItems = (await this.db.inventoryArmor
- .where("clazz")
- .equals(config.characterClass)
- .distinct()
- .toArray()) as IInventoryArmor[];
-
- this.inventoryArmorItems = this.inventoryArmorItems
- // only armor :)
- .filter((item) => item.slot != ArmorSlot.ArmorSlotNone)
- // filter disabled items
- .filter((item) => config.disabledItems.indexOf(item.itemInstanceId) == -1)
- // filter armor 3.0
- .filter((item) => !config.enforceFeaturedArmor || item.isFeatured)
- .filter((item) => item.armorSystem === ArmorSystem.Armor3 || config.allowLegacyArmor)
- // filter collection/vendor rolls if not allowed
- .filter((item) => {
- switch (item.source) {
- case InventoryArmorSource.Collections:
- return config.includeCollectionRolls;
- case InventoryArmorSource.Vendor:
- return config.includeVendorRolls;
- default:
- return true;
- }
- })
- // filter the selected exotic right here
- .filter(
- (item) => config.selectedExotics.indexOf(FORCE_USE_NO_EXOTIC) == -1 || !item.isExotic
- )
- .filter(
- (item) =>
- this.selectedExotics.length === 0 ||
- (item.isExotic && this.selectedExotics.some((exotic) => exotic.hash === item.hash)) ||
- (!item.isExotic && this.selectedExotics.every((exotic) => exotic.slot !== item.slot))
- )
-
- // config.OnlyUseMasterworkedExotics - only keep exotics that are masterworked
- .filter(
- (item) =>
- !config.onlyUseMasterworkedExotics ||
- !(item.rarity == TierType.Exotic && item.masterworkLevel != MAXIMUM_MASTERWORK_LEVEL)
- )
-
- // config.OnlyUseMasterworkedLegendaries - only keep legendaries that are masterworked
- .filter(
- (item) =>
- !config.onlyUseMasterworkedLegendaries ||
- !(item.rarity == TierType.Superior && item.masterworkLevel != MAXIMUM_MASTERWORK_LEVEL)
- )
-
- // non-legendaries and non-exotics
- .filter(
- (item) =>
- config.allowBlueArmorPieces ||
- item.rarity == TierType.Exotic ||
- item.rarity == TierType.Superior
- )
- // sunset armor
- .filter((item) => !config.ignoreSunsetArmor || !item.isSunset)
- // armor perks
- .filter((item) => {
- return (
- (item.isExotic && item.perk == config.armorPerks[item.slot].value) ||
- config.armorPerks[item.slot].value == ArmorPerkOrSlot.Any ||
- !config.armorPerks[item.slot].fixed ||
- (config.armorPerks[item.slot].fixed &&
- config.armorPerks[item.slot].value == item.perk) ||
- (config.armorPerks[item.slot].value === ArmorPerkOrSlot.SlotArtifice &&
- item.armorSystem === ArmorSystem.Armor2 &&
- ((config.assumeEveryLegendaryIsArtifice && !item.isExotic) ||
- (config.assumeEveryExoticIsArtifice && item.isExotic)))
- );
- });
- // console.log(items.map(d => "id:'"+d.itemInstanceId+"'").join(" or "))
-
- // Remove collection items if they are in inventory
- this.inventoryArmorItems = this.inventoryArmorItems.filter((item) => {
- if (item.source === InventoryArmorSource.Inventory) return true;
-
- const purchasedItemInstance = this.inventoryArmorItems.find(
- (rhs) => rhs.source === InventoryArmorSource.Inventory && isEqualItem(item, rhs)
- );
-
- // If this item is a collection/vendor item, ignore it if the player
- // already has a real copy of the same item.
- return purchasedItemInstance === undefined;
- });
- this.permutatorArmorItems = this.inventoryArmorItems.map((armor) => {
- return {
- id: armor.id,
- hash: armor.hash,
- slot: armor.slot,
- clazz: armor.clazz,
- perk: armor.perk,
- isExotic: armor.isExotic,
- rarity: armor.rarity,
- isSunset: armor.isSunset,
- masterworkLevel: armor.masterworkLevel,
- archetypeStats: armor.archetypeStats,
- mobility: armor.mobility,
- resilience: armor.resilience,
- recovery: armor.recovery,
- discipline: armor.discipline,
- intellect: armor.intellect,
- strength: armor.strength,
- source: armor.source,
- exoticPerkHash: armor.exoticPerkHash,
-
- icon: armor.icon,
- watermarkIcon: armor.watermarkIcon,
- name: armor.name,
- energyLevel: armor.energyLevel,
- tier: armor.tier,
- armorSystem: armor.armorSystem,
- };
- });
+ // Get filtered items from the available items service
+ const availableItemsInfo = this.availableItems.availableItems;
+ this.permutatorArmorItems = [
+ ...availableItemsInfo.itemsBySlot[ArmorSlot.ArmorSlotHelmet],
+ ...availableItemsInfo.itemsBySlot[ArmorSlot.ArmorSlotGauntlet],
+ ...availableItemsInfo.itemsBySlot[ArmorSlot.ArmorSlotChest],
+ ...availableItemsInfo.itemsBySlot[ArmorSlot.ArmorSlotLegs],
+ ...availableItemsInfo.filteredClassItemsForGeneration,
+ ];
nthreads = this.estimateRequiredThreads();
@@ -479,6 +372,18 @@ export class InventoryService {
this.resultMaximumTiers.push(data.runtime.maximumPossibleTiers);
}
if (data.done == true && doneWorkerCount == nthreads) {
+ const allItemIds = this.results.flatMap((x) => x.armor);
+ let inventoryArmorItems = (await this.db.inventoryArmor
+ .where("clazz")
+ .equals(config.characterClass)
+ .distinct()
+ .and((item) => item != null)
+ .toArray()) as IInventoryArmor[];
+
+ inventoryArmorItems = inventoryArmorItems.filter(
+ (x) => allItemIds.includes(x.id) || this.selectedExotics.some((y) => y.hash == x.hash)
+ );
+
this.status.modifyStatus((s) => (s.calculatingResults = false));
this._calculationProgress.next(0);
@@ -486,7 +391,7 @@ export class InventoryService {
for (let armorSet of this.results) {
let items = armorSet.armor.map((x) =>
- this.inventoryArmorItems.find((y) => y.id == x)
+ inventoryArmorItems.find((y) => y.id == x)
) as IInventoryArmor[];
let exotic = items.find((x) => x.isExotic);
let v: ResultDefinition = {
@@ -726,4 +631,15 @@ export class InventoryService {
return false;
}
}
+
+ getSlotByItemHash(hash: number): PromiseLike {
+ return this.db.manifestArmor
+ .where("hash")
+ .equals(hash)
+ .first()
+ .then((item) => {
+ if (item == null) return ArmorSlot.ArmorSlotNone;
+ return item.slot;
+ });
+ }
}
diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts
index 109bafbb..48cf3fff 100644
--- a/src/app/services/results-builder.worker.ts
+++ b/src/app/services/results-builder.worker.ts
@@ -16,7 +16,7 @@
*/
// region Imports
-import { BuildConfiguration, FixableSelection } from "../data/buildConfiguration";
+import { BuildConfiguration } from "../data/buildConfiguration";
import { IDestinyArmor, InventoryArmorSource } from "../data/types/IInventoryArmor";
import { ArmorSlot } from "../data/enum/armor-slot";
import { FORCE_USE_ANY_EXOTIC, MAXIMUM_MASTERWORK_LEVEL } from "../data/constants";
@@ -287,9 +287,6 @@ addEventListener("message", async ({ data }) => {
const threadSplit = data.threadSplit as { count: number; current: number };
const config = data.config as BuildConfiguration;
- const anyStatFixed = Object.values(config.minimumStatTiers).some(
- (v: FixableSelection) => v.fixed
- );
let items = data.items as IPermutatorArmor[];
if (threadSplit == undefined || config == undefined || items == undefined) {
@@ -348,94 +345,6 @@ addEventListener("message", async ({ data }) => {
}
let classItems = items.filter((i) => i.slot == ArmorSlot.ArmorSlotClass);
- // Sort by Masterwork, descending
- classItems = classItems.sort((a, b) => (b.masterworkLevel ?? 0) - (a.masterworkLevel ?? 0));
-
- // Filter exotic class items based on selected exotic perks if they are not "Any"
- if (config.selectedExoticPerks && config.selectedExoticPerks.length >= 2) {
- const firstPerkFilter = config.selectedExoticPerks[0];
- const secondPerkFilter = config.selectedExoticPerks[1];
-
- if (firstPerkFilter !== ArmorPerkOrSlot.Any || secondPerkFilter !== ArmorPerkOrSlot.Any) {
- classItems = classItems.filter((item) => {
- if (!item.isExotic || !item.exoticPerkHash || item.exoticPerkHash.length < 2) {
- return true; // Keep non-exotic items or items without proper perk data
- }
-
- const hasFirstPerk =
- firstPerkFilter === ArmorPerkOrSlot.Any || item.exoticPerkHash.includes(firstPerkFilter);
- const hasSecondPerk =
- secondPerkFilter === ArmorPerkOrSlot.Any ||
- item.exoticPerkHash.includes(secondPerkFilter);
-
- return hasFirstPerk && hasSecondPerk;
- });
- }
- }
-
- if (
- config.assumeEveryLegendaryIsArtifice ||
- config.assumeEveryExoticIsArtifice ||
- config.assumeClassItemIsArtifice
- ) {
- classItems = classItems.map((item) => {
- if (
- (item.armorSystem == ArmorSystem.Armor2 &&
- ((config.assumeEveryLegendaryIsArtifice && !item.isExotic) ||
- (config.assumeEveryExoticIsArtifice && item.isExotic))) ||
- (config.assumeClassItemIsArtifice && !item.isExotic)
- ) {
- return { ...item, perk: ArmorPerkOrSlot.SlotArtifice };
- }
- return item;
- });
- }
-
- // If the config says that we need a fixed class item, filter the class items accordingly
- if (
- config.armorPerks[ArmorSlot.ArmorSlotClass].fixed &&
- config.armorPerks[ArmorSlot.ArmorSlotClass].value != ArmorPerkOrSlot.Any
- )
- classItems = classItems.filter(
- (d) => d.perk == config.armorPerks[ArmorSlot.ArmorSlotClass].value
- );
-
- // true if any armorPerks is not "any"
- const doesNotRequireArmorPerks = ![
- config.armorPerks[ArmorSlot.ArmorSlotHelmet].value,
- config.armorPerks[ArmorSlot.ArmorSlotGauntlet].value,
- config.armorPerks[ArmorSlot.ArmorSlotChest].value,
- config.armorPerks[ArmorSlot.ArmorSlotLegs].value,
- config.armorPerks[ArmorSlot.ArmorSlotClass].value,
- ].every((v) => v === ArmorPerkOrSlot.Any);
-
- classItems = classItems.filter(
- (item, index, self) =>
- index ===
- self.findIndex(
- (i) =>
- i.mobility === item.mobility &&
- i.resilience === item.resilience &&
- i.recovery === item.recovery &&
- i.discipline === item.discipline &&
- i.intellect === item.intellect &&
- i.strength === item.strength &&
- i.isExotic === item.isExotic &&
- ((i.isExotic && config.assumeExoticsMasterworked) ||
- (!i.isExotic && config.assumeLegendariesMasterworked) ||
- // If there is any stat fixed, we check if the masterwork level is the same as the first item
- (anyStatFixed && i.masterworkLevel === item.masterworkLevel) ||
- // If there is no stat fixed, then we just use the masterwork level of the first item.
- // As it is already sorted descending, we can just check if the masterwork level is the same
- !anyStatFixed) &&
- (i.isExotic
- ? i.exoticPerkHash[0] === item.exoticPerkHash[0] &&
- i.exoticPerkHash[1] === item.exoticPerkHash[1]
- : true) && // if it's not exotic, we don't care about the exotic perks
- (doesNotRequireArmorPerks || i.perk === item.perk)
- )
- );
- //*/
const exoticClassItems = classItems.filter((d) => d.isExotic);
const legendaryClassItems = classItems.filter((d) => !d.isExotic);