diff --git a/package-lock.json b/package-lock.json index 47d35557..303915c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "d2-armor-picker", - "version": "2.5.4", + "version": "2.6.0", "dependencies": { "@angular/animations": "^14.1.3", "@angular/cdk": "^14.1.3", diff --git a/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.html b/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.html index be81bb1f..feebef77 100644 --- a/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.html +++ b/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.html @@ -21,6 +21,14 @@ {{ ArmorStatNames[stat] }} at Tier {{ tier }} + + Not achievable by all selected exotics with the current settings + Not achievable with the current settings + Stat Tier {{ tier }} diff --git a/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.ts b/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.ts index 39f50534..d479a436 100644 --- a/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.ts +++ b/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.component.ts @@ -48,6 +48,8 @@ export class StatCooldownTooltipComponent implements OnInit { public ArmorStatNames = ArmorStatNames; @Input() tier: number = 0; + @Input() minimumMaximumExoticAvailableTier: number = 0; + @Input() maximumAvailableTier: number = 0; @Input() differenceTier: number = 0; // the tier we use to show a difference for @Input() stat: ArmorStat = ArmorStat.Mobility; @@ -104,4 +106,10 @@ export class StatCooldownTooltipComponent implements OnInit { getPercentageDifference(v1: number, v2: number) { return (v1 - v2) / Math.max(1, v2); } + + getIsAddedByExotic() { + return ( + this.tier > this.minimumMaximumExoticAvailableTier && this.tier <= this.maximumAvailableTier + ); + } } diff --git a/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.directive.ts b/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.directive.ts index 491847b9..61429884 100644 --- a/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.directive.ts +++ b/src/app/components/authenticated-v2/overlays/stat-cooldown-tooltip/stat-cooldown-tooltip.directive.ts @@ -40,7 +40,8 @@ export class StatCooldownTooltipDirective implements OnInit, OnDestroy { * This can be used to show the tooltip conditionally */ @Input() showToolTip: boolean = true; - + @Input() minimumMaximumExoticAvailableTier: number = 0; + @Input() maximumAvailableTier: number = 0; @Input() tooltipTier: number = 0; @Input() tooltipStat: ArmorStat = ArmorStat.Mobility; @Input() tooltipDifferenceTier: number = 0; // the tier we use to show a difference for @@ -102,6 +103,10 @@ export class StatCooldownTooltipDirective implements OnInit, OnDestroy { tooltipRef.instance.tier = this.tooltipTier; tooltipRef.instance.differenceTier = this.tooltipDifferenceTier; tooltipRef.instance.stat = this.tooltipStat; + tooltipRef.instance.maximumAvailableTier = this.maximumAvailableTier; + tooltipRef.instance.minimumMaximumExoticAvailableTier = + this.minimumMaximumExoticAvailableTier; + tooltipRef.instance.stat = this.tooltipStat; } } diff --git a/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.html b/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.html index c5e8ed0d..60d35964 100644 --- a/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.html +++ b/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.html @@ -681,13 +681,13 @@ - + Exotic: - {{ exotic[0].name }} + {{ exotic.name }}
- - + +
diff --git a/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.ts b/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.ts index cdf55ef9..afa4fd85 100644 --- a/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.ts +++ b/src/app/components/authenticated-v2/results/expanded-result-content/expanded-result-content.component.ts @@ -39,7 +39,7 @@ import { MatSnackBar } from "@angular/material/snack-bar"; import { BungieApiService } from "../../../../services/bungie-api.service"; import { ModOrAbility } from "../../../../data/enum/modOrAbility"; import { DestinyEnergyType, DestinyClass } from "bungie-api-ts/destiny2"; -import { ModifierType } from "../../../../data/enum/modifierType"; +import { ModifierNames, ModifierType } from "../../../../data/enum/modifierType"; import { BuildConfiguration } from "../../../../data/buildConfiguration"; import { takeUntil } from "rxjs/operators"; import { Subject } from "rxjs"; @@ -64,7 +64,7 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { public ArmorStatIconUrls = ArmorStatIconUrls; public ArmorStat = ArmorStat; public StatModifier = StatModifier; - public config_characterClass = DestinyClass.Titan; + public config_characterClass = DestinyClass.Unknown; public config_assumeLegendariesMasterworked = false; public config_assumeExoticsMasterworked = false; public config_assumeClassItemMasterworked = false; @@ -110,7 +110,7 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { ngOnInit(): void { this.config.configuration.pipe(takeUntil(this.ngUnsubscribe)).subscribe((c) => { - this.config_characterClass = c.characterClass as unknown as DestinyClass; + this.config_characterClass = c.characterClass; this.config_assumeLegendariesMasterworked = c.assumeLegendariesMasterworked; this.config_assumeExoticsMasterworked = c.assumeExoticsMasterworked; this.config_assumeClassItemMasterworked = c.assumeClassItemMasterworked; @@ -307,16 +307,15 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { if (c.selectedExotics.length == 1) { data.exoticArmorHash = c.selectedExotics[0]; } else { - var exos = this.element?.exotic; - if (exos && exos.length == 1) { - var exoticHash = exos[0].hash; - if (!!exoticHash) data.exoticArmorHash = parseInt(exoticHash, 10); - } + var exoticHash = this.element?.exotic?.hash; + if (!!exoticHash) data.exoticArmorHash = exoticHash; } const loadout: Loadout = { id: "d2ap", // this doesn't matter and will be replaced - name: "D2ArmorPicker Loadout", + name: `${ModifierNames[c.selectedModElement]} ${ + this.element?.exotic?.name + } D2ArmorPicker Loadout `, classType: c.characterClass as number, parameters: data, equipped: (this.element?.items || []).map(([i]) => ({ diff --git a/src/app/components/authenticated-v2/results/results.component.html b/src/app/components/authenticated-v2/results/results.component.html index 2506744a..0f4bf79b 100644 --- a/src/app/components/authenticated-v2/results/results.component.html +++ b/src/app/components/authenticated-v2/results/results.component.html @@ -48,7 +48,7 @@ matTooltip="Note: To speed up the whole process, only {{ this.parsedResults | number }} results are listed in this table. - If you need more entries, disable the limitation in the advanced settings." + If you need more entries, disable the limitation in the advanced settings." >report_problem report_problem @@ -217,7 +217,7 @@ matSortDirection="asc" multiTemplateDataRows> + The actual rendered columns are set as a property on the row definition" --> + + + + + + + # Easy Swap builds + + {{ element.nonExoticsSetCount }} + Exotic - - - - - + + diff --git a/src/app/components/authenticated-v2/results/results.component.ts b/src/app/components/authenticated-v2/results/results.component.ts index 4b1d083c..67bb37b9 100644 --- a/src/app/components/authenticated-v2/results/results.component.ts +++ b/src/app/components/authenticated-v2/results/results.component.ts @@ -37,13 +37,12 @@ import { InventoryArmorSource } from "src/app/data/types/IInventoryArmor"; export interface ResultDefinition { exotic: | undefined - | [ - { - icon: string; - name: string; - hash: string; - } - ]; + | { + icon: string; + watermark: string; + name: string; + hash: number; + }; artifice: number[]; classItem: { perk: ArmorPerkOrSlot; @@ -53,12 +52,15 @@ export interface ResultDefinition { statsNoMods: number[]; items: ResultItem[][]; tiers: number; + maxTiers: number; waste: number; modCost: number; modCount: number; loaded: boolean; usesCollectionRoll?: boolean; usesVendorRoll?: boolean; + nonExoticsSetHash: bigint; + nonExoticsSetCount: number; } export enum ResultItemMoveState { @@ -193,6 +195,7 @@ export class ResultsComponent implements OnInit, OnDestroy { "mods", ]; if (c.showWastedStatsColumn) columns.push("waste"); + if (c.includeLegendaryShareColumn) columns.push("nonExoticsSetCount"); if (c.includeVendorRolls || c.includeCollectionRolls) columns.push("source"); columns.push("dropdown"); this.shownColumns = columns; @@ -229,7 +232,7 @@ export class ResultsComponent implements OnInit, OnDestroy { case "Tiers": return data.tiers; case "Max Tiers": - return 10 * (data.tiers + (5 - data.modCount)); + return data.maxTiers; case "Waste": return data.waste; case "Mods": @@ -238,6 +241,8 @@ export class ResultsComponent implements OnInit, OnDestroy { //+ 40 * data.artifice.length data.modCost ); + case "HashCount": + return data.nonExoticsSetCount; } return 0; }; @@ -281,7 +286,13 @@ export class ResultsComponent implements OnInit, OnDestroy { // download the file let a = document.createElement("a"); a.download = "builds.json"; - const url = window.URL.createObjectURL(new Blob([JSON.stringify(jsonData, null, 2)])); + const url = window.URL.createObjectURL( + new Blob([ + JSON.stringify(jsonData, (key, value) => + typeof value === "bigint" ? value.toString() : value + ), + ]) + ); const link = document.createElement("a"); link.href = url; link.setAttribute("download", "d2ap_results.json"); diff --git a/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.ts b/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.ts index 17ebb9df..c67b2f04 100644 --- a/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.ts +++ b/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.ts @@ -159,6 +159,15 @@ export class AdvancedSettingsComponent implements OnInit, OnDestroy { impactsResultCount: false, help: "Shows an additional column in the table that shows how many stats are wasted in a build.", }, + { + name: "Show the count of builds generated sharing non-exotic armor pieces.", + cp: (v: boolean) => + this.config.modifyConfiguration((c) => (c.includeLegendaryShareColumn = v)), + value: c.includeLegendaryShareColumn, + disabled: false, + impactsResultCount: false, + help: "Shows an additional column in the table with the count of how many builds generated use the same non-exotic armor pieces, or when 2 exotic slots are selected, the builds that share the other two slots", + }, ], "Wasted Stats": [ { diff --git a/src/app/components/authenticated-v2/settings/desired-class-selection/desired-class-selection.component.ts b/src/app/components/authenticated-v2/settings/desired-class-selection/desired-class-selection.component.ts index a62054c9..9877203d 100644 --- a/src/app/components/authenticated-v2/settings/desired-class-selection/desired-class-selection.component.ts +++ b/src/app/components/authenticated-v2/settings/desired-class-selection/desired-class-selection.component.ts @@ -31,11 +31,7 @@ export class DesiredClassSelectionComponent implements OnInit, OnDestroy { itemCounts: (null | number)[] = [null, null, null]; selectedClass = -1; public storedMaterials: { - "3853748946": number; - "4257549985": number; - "4257549984": number; - "3159615086": number; - "3467984096": number; + [Key: string]: number; } | null = null; constructor( @@ -86,11 +82,7 @@ export class DesiredClassSelectionComponent implements OnInit, OnDestroy { private async loadStoredMaterials() { var k: { - "3853748946": number; - "4257549985": number; - "4257549984": number; - "3159615086": number; - "3467984096": number; + [Key: string]: number; } = JSON.parse(localStorage.getItem("stored-materials") || "{}"); if (!("3853748946" in k)) k["3853748946"] = 0; if (!("4257549984" in k)) k["4257549984"] = 0; diff --git a/src/app/components/authenticated-v2/settings/desired-exotic-selection/desired-exotic-selection.component.ts b/src/app/components/authenticated-v2/settings/desired-exotic-selection/desired-exotic-selection.component.ts index a6b5d970..718ffb7b 100644 --- a/src/app/components/authenticated-v2/settings/desired-exotic-selection/desired-exotic-selection.component.ts +++ b/src/app/components/authenticated-v2/settings/desired-exotic-selection/desired-exotic-selection.component.ts @@ -20,7 +20,7 @@ import { ClassExoticInfo, InventoryService } from "../../../../services/inventor import { ConfigurationService } from "../../../../services/configuration.service"; import { animate, query, stagger, style, transition, trigger } from "@angular/animations"; import { ArmorSlot } from "../../../../data/enum/armor-slot"; -import { FORCE_USE_NO_EXOTIC } from "../../../../data/constants"; +import { FORCE_USE_ANY_EXOTIC, FORCE_USE_NO_EXOTIC } from "../../../../data/constants"; import { debounceTime, takeUntil } from "rxjs/operators"; import { Subject } from "rxjs"; import { DestinyClass } from "bungie-api-ts/destiny2"; @@ -47,7 +47,7 @@ export class DesiredExoticSelectionComponent implements OnInit, OnDestroy { includeVendorRolls = false; ignoreSunsetArmor = false; allowBlueArmorPieces = false; - currentClass: DestinyClass = DestinyClass.Titan; + currentClass: DestinyClass = DestinyClass.Unknown; exotics: ClassExoticInfo[][] = []; constructor(public inventory: InventoryService, public config: ConfigurationService) {} @@ -89,6 +89,7 @@ export class DesiredExoticSelectionComponent implements OnInit, OnDestroy { } this.exotics = [ + [], uniq(armors.filter((a) => a.item.slot == ArmorSlot.ArmorSlotHelmet)), uniq(armors.filter((a) => a.item.slot == ArmorSlot.ArmorSlotGauntlet)), uniq(armors.filter((a) => a.item.slot == ArmorSlot.ArmorSlotChest)), @@ -125,11 +126,23 @@ export class DesiredExoticSelectionComponent implements OnInit, OnDestroy { if (index > -1) { // Always delete an item if it is already in the list this.selectedExotics.splice(index, 1); - } else if (hash == FORCE_USE_NO_EXOTIC) { + } else if ( + hash == FORCE_USE_NO_EXOTIC || + (this.selectedExotics.indexOf(FORCE_USE_NO_EXOTIC) != -1 && + ($event.shiftKey || $event.ctrlKey)) + ) { this.selectedExotics = [FORCE_USE_NO_EXOTIC]; - } else if (this.selectedExotics.length == 0 || !$event.shiftKey) { - // if length is 0 or shift is NOT pressed, add the exotic + } else if ( + hash == FORCE_USE_ANY_EXOTIC || + (this.selectedExotics.indexOf(FORCE_USE_ANY_EXOTIC) != -1 && + ($event.shiftKey || $event.ctrlKey)) + ) { + this.selectedExotics = [FORCE_USE_ANY_EXOTIC]; + } else if (this.selectedExotics.length == 0 || !($event.shiftKey || $event.ctrlKey)) { + // if length is 0 or shift or control is NOT pressed, replace the selected exotic this.selectedExotics = [hash]; + } else { + this.selectedExotics.push(hash); } this.config.modifyConfiguration((c) => { c.selectedExotics = this.selectedExotics; 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 c5b26fb9..f3d95a14 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 @@ -54,7 +54,7 @@ export class SlotLimitationSelectionComponent implements OnInit, OnDestroy, Afte possible: EventEmitter = new EventEmitter(); isPossible: boolean = true; - configSelectedClass: DestinyClass = DestinyClass.Titan; + configSelectedClass: DestinyClass = DestinyClass.Unknown; configAssumeLegendaryIsArtifice: boolean = false; configAssumeClassItemIsArtifice: boolean = false; armorPerk: ArmorPerkOrSlot = ArmorPerkOrSlot.None; diff --git a/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.html b/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.html index 6b6e7cb0..8a403727 100644 --- a/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.html +++ b/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.html @@ -57,6 +57,7 @@ [stat]="enum.value" [allowExactStats]="config_allowExactStats" [statsByMods]="statsByMods[enum.value]" + [minimumMaximumExoticAvailableTier]="minimumMaximumExoticAvailableTier[enum.value]" [maximumAvailableTier]="maximumPossibleTiers[enum.value]" [selectedTier]="minimumStatTiers[enum.value].value" [locked]="minimumStatTiers[enum.value].fixed" diff --git a/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.ts b/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.ts index 1ce98380..ec497f47 100644 --- a/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.ts +++ b/src/app/components/authenticated-v2/settings/desired-stat-selection/desired-stat-selection.component.ts @@ -42,6 +42,7 @@ function calcScore(d: number[]) { export class DesiredStatSelectionComponent implements OnInit, OnDestroy { readonly stats: { name: string; value: ArmorStat }[]; minimumStatTiers: EnumDictionary> = getDefaultStatDict(1); + minimumMaximumExoticAvailableTier: number[] = [10, 10, 10, 10, 10, 10]; maximumPossibleTiers: number[] = [10, 10, 10, 10, 10, 10]; statsByMods: number[] = [0, 0, 0, 0, 0, 0]; _statCombo4x100: ArmorStat[][] = []; @@ -83,6 +84,10 @@ export class DesiredStatSelectionComponent implements OnInit, OnDestroy { this.maximumPossibleTiers = tiers; } + this.minimumMaximumExoticAvailableTier = d.minimumMaximumExoticPossibleTiers || [ + 10, 10, 10, 10, 10, 10, + ]; + this._statCombo3x100 = (d.statCombo3x100 || []).sort((a, b) => calcScore(b) - calcScore(a)); this._statCombo4x100 = d.statCombo4x100 || []; }); diff --git a/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.html b/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.html index f13bd75d..6b64b42d 100644 --- a/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.html +++ b/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.html @@ -22,6 +22,7 @@ *ngFor="let tier of TierRange; let i = index" [checked]="selectedTier === i" [class.isFromMod]="isAddedByConfigMods(i)" + [class.isFromExotic]="isAddedByMaxExotic(i)" [class.mat-button-toggle-checked]="selectedTier >= i" [class.not-checked]="selectedTier < i" [disabled]="maximumAvailableTier < i" @@ -30,6 +31,8 @@
diff --git a/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.scss b/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.scss index f2dad949..4f0ed418 100644 --- a/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.scss +++ b/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.scss @@ -33,7 +33,15 @@ } .not-checked.isFromMod { - background-color: rgba(63, 110, 181, 0.15); + background-color: rgba(63, 110, 181, 0.5); +} + +.mat-button-toggle-checked.isFromExotic { + background-color: rgba(63, 4, 5, 1); +} + +.not-checked.isFromExotic { + background-color: rgba(63, 4, 5, 0.15); } .lock-locked { diff --git a/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.ts b/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.ts index f3b36a58..7f75f3d5 100644 --- a/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.ts +++ b/src/app/components/authenticated-v2/settings/desired-stat-selection/stat-tier-selection/stat-tier-selection.component.ts @@ -29,6 +29,7 @@ export class StatTierSelectionComponent { @Input() stat: ArmorStat = ArmorStat.Mobility; @Input() statsByMods: number = 0; @Input() maximumAvailableTier: number = 10; + @Input() minimumMaximumExoticAvailableTier: number = 10; @Input() selectedTier: number = 0; @Input() locked: boolean = false; @Output() selectedTierChange = new EventEmitter(); @@ -58,6 +59,10 @@ export class StatTierSelectionComponent { ); } + isAddedByMaxExotic(index: number) { + return index > this.minimumMaximumExoticAvailableTier && index <= this.maximumAvailableTier; + } + toggleLockState() { this.locked = !this.locked; this.lockedChange.emit(this.locked); diff --git a/src/app/components/authenticated-v2/settings/settings.component.html b/src/app/components/authenticated-v2/settings/settings.component.html index aef23882..52664fb4 100644 --- a/src/app/components/authenticated-v2/settings/settings.component.html +++ b/src/app/components/authenticated-v2/settings/settings.component.html @@ -45,6 +45,7 @@ This section allows you to limit the armor pieces that are used in the buildcrafting process.
If you want, limit all results to one exotic.
+ Keep the Shift or Control key pressed to select multiple exotics
Exotics you do not have in the inventory or vault are grayed out. diff --git a/src/app/data/buildConfiguration.ts b/src/app/data/buildConfiguration.ts index c9b138cf..2f9b60ac 100644 --- a/src/app/data/buildConfiguration.ts +++ b/src/app/data/buildConfiguration.ts @@ -43,7 +43,7 @@ export interface FixableSelection { } export class BuildConfiguration { - characterClass: DestinyClass = DestinyClass.Titan; + characterClass: DestinyClass = DestinyClass.Unknown; // Add constant +1 strength addConstent1Resilience = false; @@ -85,6 +85,7 @@ export class BuildConfiguration { ignoreSunsetArmor = false; includeVendorRolls = false; includeCollectionRolls = false; + includeLegendaryShareColumn = false; assumeLegendariesMasterworked = true; assumeExoticsMasterworked = true; assumeClassItemMasterworked = true; @@ -125,6 +126,7 @@ export class BuildConfiguration { onlyUseMasterworkedExotics: false, onlyUseMasterworkedLegendaries: false, ignoreSunsetArmor: false, + includeLegendaryShareColumn: false, includeCollectionRolls: false, includeVendorRolls: false, allowBlueArmorPieces: true, @@ -137,7 +139,7 @@ export class BuildConfiguration { onlyShowResultsWithNoWastedStats: false, showWastedStatsColumn: false, showPotentialTierColumn: false, - characterClass: DestinyClass.Titan, + characterClass: DestinyClass.Unknown, selectedModElement: ModifierType.Stasis, selectedExotics: [], maximumModSlots: { diff --git a/src/app/data/enum/modifierType.ts b/src/app/data/enum/modifierType.ts index 59d0e0c9..46438209 100644 --- a/src/app/data/enum/modifierType.ts +++ b/src/app/data/enum/modifierType.ts @@ -15,6 +15,8 @@ * along with this program. If not, see . */ +import { EnumDictionary } from "../types/EnumDictionary"; + export enum ModifierType { CombatStyleMod, Stasis, @@ -24,3 +26,13 @@ export enum ModifierType { Strand, RetrofitMods, // Artifact Retrofit mods } + +export const ModifierNames: EnumDictionary = { + [ModifierType.Arc]: "Arc", + [ModifierType.Solar]: "Solar", + [ModifierType.Void]: "Void", + [ModifierType.Stasis]: "Stasis", + [ModifierType.Strand]: "Strand", + [ModifierType.CombatStyleMod]: "", + [ModifierType.RetrofitMods]: "", +}; diff --git a/src/app/services/inventory.service.ts b/src/app/services/inventory.service.ts index d7c17abd..9a362734 100644 --- a/src/app/services/inventory.service.ts +++ b/src/app/services/inventory.service.ts @@ -35,17 +35,18 @@ import { isEqualItem, totalStats, } from "../data/types/IInventoryArmor"; -import { DestinyClass, ItemBindStatus, TierType } from "bungie-api-ts/destiny2"; +import { DestinyClass, TierType } from "bungie-api-ts/destiny2"; import { IPermutatorArmorSet } from "../data/types/IPermutatorArmorSet"; -import { getSkillTier, getStatSum, getWaste } from "./results-builder.worker"; +import { getSkillTier, getWaste } from "./results-builder.worker"; import { IPermutatorArmor } from "../data/types/IPermutatorArmor"; -import { FORCE_USE_NO_EXOTIC } from "../data/constants"; +import { FORCE_USE_ANY_EXOTIC, FORCE_USE_NO_EXOTIC } from "../data/constants"; import { VendorsService } from "./vendors.service"; import { ModOptimizationStrategy } from "../data/enum/mod-optimization-strategy"; type info = { results: ResultDefinition[]; totalResults: number; + minimumMaximumExoticPossibleTiers: number[]; maximumPossibleTiers: number[]; statCombo3x100: ArmorStat[][]; statCombo4x100: ArmorStat[][]; @@ -91,11 +92,12 @@ export class InventoryService { private results: IPermutatorArmorSet[] = []; private totalPermutationCount = 0; private resultMaximumTiers: number[][] = []; + private resultMaximumExoticPossibleTiers: Map[] = []; private resultStatCombo3x100 = new Set(); private resultStatCombo4x100 = new Set(); private selectedExotics: IManifestArmor[] = []; - private itemz: IInventoryArmor[] = []; - private items: IPermutatorArmor[] = []; + private InventoryArmorItems: IInventoryArmor[] = []; + private PermutatorArmorItems: IPermutatorArmor[] = []; private endResults: ResultDefinition[] = []; constructor( @@ -163,6 +165,7 @@ export class InventoryService { totalResults: 0, totalTime: 0, itemCount: 0, + minimumMaximumExoticPossibleTiers: [10, 10, 10, 10, 10, 10], maximumPossibleTiers: [0, 0, 0, 0, 0, 0], statCombo3x100: [], statCombo4x100: [], @@ -263,7 +266,9 @@ export class InventoryService { this.results = []; this.totalPermutationCount = 0; + this.resultMaximumExoticPossibleTiers = []; this.resultMaximumTiers = []; + this.resultMaximumExoticPossibleTiers = []; this.resultStatCombo3x100 = new Set(); this.resultStatCombo4x100 = new Set(); const startTime = Date.now(); @@ -279,13 +284,13 @@ export class InventoryService { ); this.selectedExotics = this.selectedExotics.filter((i) => !!i); - this.itemz = (await this.db.inventoryArmor + this.InventoryArmorItems = (await this.db.inventoryArmor .where("clazz") .equals(config.characterClass) .distinct() .toArray()) as IInventoryArmor[]; - this.itemz = this.itemz + this.InventoryArmorItems = this.InventoryArmorItems // only armor :) .filter((item) => item.slot != ArmorSlot.ArmorSlotNone) // filter disabled items @@ -303,7 +308,11 @@ export class InventoryService { }) // filter the selected exotic right here .filter( - (item) => config.selectedExotics.indexOf(FORCE_USE_NO_EXOTIC) == -1 || !item.isExotic + (item) => + !item.isExotic || + config.selectedExotics.indexOf(item.hash) != -1 || + config.selectedExotics.indexOf(FORCE_USE_ANY_EXOTIC) != -1 || + config.selectedExotics.length == 0 ) .filter( (item) => @@ -347,10 +356,10 @@ export class InventoryService { // console.log(items.map(d => "id:'"+d.itemInstanceId+"'").join(" or ")) // Remove collection items if they are in inventory - this.itemz = this.itemz.filter((item) => { + this.InventoryArmorItems = this.InventoryArmorItems.filter((item) => { if (item.source === InventoryArmorSource.Inventory) return true; - const purchasedItemInstance = this.itemz.find( + const purchasedItemInstance = this.InventoryArmorItems.find( (rhs) => rhs.source === InventoryArmorSource.Inventory && isEqualItem(item, rhs) ); @@ -359,7 +368,7 @@ export class InventoryService { return purchasedItemInstance === undefined; }); - this.items = this.itemz.map((armor) => { + this.PermutatorArmorItems = this.InventoryArmorItems.map((armor) => { return { id: armor.id, hash: armor.hash, @@ -392,7 +401,9 @@ export class InventoryService { // Improve per thread performance by shuffling the inventory // sorting is a naive aproach that can be optimized // in my test is better than the default order from the db - this.items = this.items.sort((a, b) => totalStats(b) - totalStats(a)); + this.PermutatorArmorItems = this.PermutatorArmorItems.sort( + (a, b) => totalStats(b) - totalStats(a) + ); this._calculationProgress.next(0); for (let n = 0; n < nthreads; n++) { @@ -424,6 +435,7 @@ export class InventoryService { doneWorkerCount++; this.totalPermutationCount += data.stats.permutationCount; this.resultMaximumTiers.push(data.runtime.maximumPossibleTiers); + this.resultMaximumExoticPossibleTiers.push(data.runtime.maximumExoticPossibleTiers); for (let elem of data.runtime.statCombo3x100) this.resultStatCombo3x100.add(elem); for (let elem of data.runtime.statCombo4x100) this.resultStatCombo4x100.add(elem); } @@ -432,24 +444,57 @@ export class InventoryService { this._calculationProgress.next(0); this.endResults = []; + let permutationHashes = new Map(); + let permutationSlots = new Set( + this.results + .flatMap((a) => a.armor.map((x) => this.InventoryArmorItems.find((y) => y.id == x))) + .filter((a) => a !== undefined) + .filter((a) => a!.isExotic) + .map((a) => a!.slot) + ); for (let armorSet of this.results) { let items = armorSet.armor.map((x) => - this.itemz.find((y) => y.id == x) + this.InventoryArmorItems.find((y) => y.id == x) ) as IInventoryArmor[]; let exotic = items.find((x) => x.isExotic); + // if the exotics are in 1 slot use the non exotic armor to allow "hotswappability" + // if the exotics are in 2 different slots, generate the Hash with the other 2 armor pieces, to allow "hotswappability" to cluster near + // if the exotics are in neither slot we don't care, use all armor + // if the exotics are in 3 or 4 slots there's no good strategy, cluster for it's non exotic parts + let legendaryArmor = + permutationSlots.size == 2 + ? items.filter((x) => !permutationSlots.has(x.slot)) + : items.filter((x) => !x.isExotic); + + let R = 0x9e3779b97f4a7c13n; //64bit golden ratio + let permHash = + legendaryArmor.reduce( + (previousValue, currentValue) => + BigInt(previousValue) * + (R + (BigInt((currentValue.itemInstanceId as any) | 0) << 1n)), + 1n + ) / 2n; + let permutationHash = permHash; + permutationHashes.set( + permutationHash, + (permutationHashes.get(permutationHash) ?? 0) + 1 + ); + //let stats = getStatSum(items); + let tiers = getSkillTier(armorSet.statsWithMods); + let v = { + loaded: false, exotic: exotic == null - ? [] - : [ - { - icon: exotic?.icon, - watermark: exotic?.watermarkIcon, - name: exotic?.name, - hash: exotic?.hash, - }, - ], + ? undefined + : { + icon: exotic?.icon, + watermark: exotic?.watermarkIcon, + name: exotic?.name, + hash: exotic?.hash, + }, + artifice: armorSet.usedArtifice, modCount: armorSet.usedMods.length, modCost: armorSet.usedMods.reduce( @@ -459,7 +504,8 @@ export class InventoryService { mods: armorSet.usedMods, stats: armorSet.statsWithMods, statsNoMods: armorSet.statsWithoutMods, - tiers: getSkillTier(armorSet.statsWithMods), + tiers: tiers, + maxTiers: 10 * (tiers + (5 - armorSet.usedMods.length)), waste: getWaste(armorSet.statsWithMods), items: items.reduce( (p: any, instance) => { @@ -488,20 +534,65 @@ export class InventoryService { }, [[], [], [], []] ), - classItem: armorSet.classItemPerk, + classItem: { perk: armorSet.classItemPerk }, usesCollectionRoll: items.some( (y) => y.source === InventoryArmorSource.Collections ), usesVendorRoll: items.some((y) => y.source === InventoryArmorSource.Vendor), - } as unknown as ResultDefinition; + nonExoticsSetHash: permutationHash, + nonExoticsSetCount: 1, + } as ResultDefinition; this.endResults.push(v); } + //Sort to keep sets with same legendary pieces together + //this.endResults.sort((ob1, ob2) => ob2.tiers - ob1.tiers); + this.endResults.sort((ob1, ob2) => (ob2.exotic?.hash ?? 0) - (ob1.exotic?.hash ?? 0)); + this.endResults.forEach( + (item) => (item.nonExoticsSetCount = permutationHashes.get(item.nonExoticsSetHash)!) + ); + this.endResults.sort((ob1, ob2) => { + if (ob1.nonExoticsSetHash > ob2.nonExoticsSetHash) { + return 1; + } else if (ob1.nonExoticsSetHash < ob2.nonExoticsSetHash) { + return -1; + } + return 0; + }); + this.endResults.sort((ob1, ob2) => ob2.nonExoticsSetCount - ob1.nonExoticsSetCount); + + console.debug("endResults", this.endResults); + let minimumMaximumExoticPossibleTiers = [10, 10, 10, 10, 10, 10]; + let maximumExoticPossibleTiersMap = new Map(); + this.resultMaximumExoticPossibleTiers.forEach((m) => { + m.forEach((v, k) => { + let knownMaxForExotic = maximumExoticPossibleTiersMap.get(k) || [ + -1, -1, -1, -1, -1, -1, + ]; + for (let stat = 0; stat < 6; stat++) { + knownMaxForExotic[stat] = Math.max(knownMaxForExotic[stat], v[stat]); + } + maximumExoticPossibleTiersMap.set(k, knownMaxForExotic); + }); + }); + + maximumExoticPossibleTiersMap.forEach((values, key) => { + for (let stat = 0; stat < 6; stat++) { + minimumMaximumExoticPossibleTiers[stat] = Math.min( + minimumMaximumExoticPossibleTiers[stat], + Math.floor(Math.min(100, values[stat]) / 10) + ); + } + }); + + console.log(`Values for ${minimumMaximumExoticPossibleTiers}`); + this._armorResults.next({ results: this.endResults, totalResults: this.totalPermutationCount, // Total amount of results, differs from the real amount if the memory save setting is active itemCount: data.stats.itemCount, totalTime: Date.now() - startTime, + minimumMaximumExoticPossibleTiers: minimumMaximumExoticPossibleTiers, maximumPossibleTiers: this.resultMaximumTiers .reduce( (p, v) => { @@ -512,13 +603,13 @@ export class InventoryService { ) .map((k) => Math.floor(Math.min(100, k) / 10)), statCombo3x100: - Array.from(this.resultStatCombo3x100 as Set).map((d: number) => { + Array.from(this.resultStatCombo3x100).map((d: number) => { let r: ArmorStat[] = []; for (let n = 0; n < 6; n++) if ((d & (1 << n)) > 0) r.push(n); return r; }) || [], statCombo4x100: - Array.from(this.resultStatCombo4x100 as Set).map((d: number) => { + Array.from(this.resultStatCombo4x100).map((d: number) => { let r = []; for (let n = 0; n < 6; n++) if ((d & (1 << n)) > 0) r.push(n); return r; @@ -531,15 +622,15 @@ export class InventoryService { this.workers[n].onerror = (ev) => { this.workers[n].terminate(); }; - this.workers[n].postMessage({ + type: "builderRequest", currentClass: this.currentClass, config: this._config, threadSplit: { count: nthreads, current: n, }, - items: this.items, + items: this.PermutatorArmorItems, selectedExotics: this.selectedExotics, }); } @@ -548,10 +639,12 @@ export class InventoryService { } estimateRequiredThreads(): number { - const helmets = this.items.filter((d) => d.slot == ArmorSlot.ArmorSlotHelmet); - const gauntlets = this.items.filter((d) => d.slot == ArmorSlot.ArmorSlotGauntlet); - const chests = this.items.filter((d) => d.slot == ArmorSlot.ArmorSlotChest); - const legs = this.items.filter((d) => d.slot == ArmorSlot.ArmorSlotLegs); + const helmets = this.PermutatorArmorItems.filter((d) => d.slot == ArmorSlot.ArmorSlotHelmet); + const gauntlets = this.PermutatorArmorItems.filter( + (d) => d.slot == ArmorSlot.ArmorSlotGauntlet + ); + const chests = this.PermutatorArmorItems.filter((d) => d.slot == ArmorSlot.ArmorSlotChest); + const legs = this.PermutatorArmorItems.filter((d) => d.slot == ArmorSlot.ArmorSlotLegs); const estimatedCalculations = this.estimateCombinationsToBeChecked( helmets, gauntlets, diff --git a/src/app/services/results-builder.worker.spec.ts b/src/app/services/results-builder.worker.spec.ts index 30ac9fd0..2d3dbc90 100644 --- a/src/app/services/results-builder.worker.spec.ts +++ b/src/app/services/results-builder.worker.spec.ts @@ -125,7 +125,7 @@ function buildTestItem( return { name: "item_" + slot, armor2: true, - clazz: DestinyClass.Titan, + clazz: DestinyClass.Unknown, source: InventoryArmorSource.Inventory, description: "", slot: slot, diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 21537601..904f4feb 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -19,7 +19,7 @@ import { BuildConfiguration } from "../data/buildConfiguration"; import { IDestinyArmor } from "../data/types/IInventoryArmor"; import { Database } from "../data/database"; import { ArmorSlot } from "../data/enum/armor-slot"; -import { FORCE_USE_ANY_EXOTIC } from "../data/constants"; +import { FORCE_USE_ANY_EXOTIC, FORCE_USE_NO_EXOTIC } from "../data/constants"; import { ModInformation } from "../data/ModInformation"; import { ArmorPerkOrSlot, @@ -50,31 +50,30 @@ function checkSlots( chest: IPermutatorArmor, leg: IPermutatorArmor ) { - var exoticId = config.selectedExotics[0] || 0; let requirements = constantModslotRequirement.slice(); if ( - (exoticId <= 0 || helmet.hash != exoticId) && + !helmet.isExotic && config.armorPerks[ArmorSlot.ArmorSlotHelmet].fixed && config.armorPerks[ArmorSlot.ArmorSlotHelmet].value != ArmorPerkOrSlot.None && config.armorPerks[ArmorSlot.ArmorSlotHelmet].value != helmet.perk ) return { valid: false }; if ( - (exoticId <= 0 || gauntlet.hash != exoticId) && + !gauntlet.isExotic && config.armorPerks[ArmorSlot.ArmorSlotGauntlet].fixed && config.armorPerks[ArmorSlot.ArmorSlotGauntlet].value != ArmorPerkOrSlot.None && config.armorPerks[ArmorSlot.ArmorSlotGauntlet].value != gauntlet.perk ) return { valid: false }; if ( - (exoticId <= 0 || chest.hash != exoticId) && + !chest.isExotic && config.armorPerks[ArmorSlot.ArmorSlotChest].fixed && config.armorPerks[ArmorSlot.ArmorSlotChest].value != ArmorPerkOrSlot.None && config.armorPerks[ArmorSlot.ArmorSlotChest].value != chest.perk ) return { valid: false }; if ( - (exoticId <= 0 || leg.hash != exoticId) && + !leg.isExotic && config.armorPerks[ArmorSlot.ArmorSlotLegs].fixed && config.armorPerks[ArmorSlot.ArmorSlotLegs].value != ArmorPerkOrSlot.None && config.armorPerks[ArmorSlot.ArmorSlotLegs].value != leg.perk @@ -93,13 +92,11 @@ function checkSlots( requirements[chest.perk]--; requirements[leg.perk]--; - // ignore exotic selection - if (exoticId > 0) { - if (helmet.hash == exoticId) requirements[config.armorPerks[helmet.slot].value]--; - else if (gauntlet.hash == exoticId) requirements[config.armorPerks[gauntlet.slot].value]--; - else if (chest.hash == exoticId) requirements[config.armorPerks[chest.slot].value]--; - else if (leg.hash == exoticId) requirements[config.armorPerks[leg.slot].value]--; - } + // ignore perk requirement in exotic + if (helmet.isExotic) requirements[config.armorPerks[helmet.slot].value]--; + else if (gauntlet.isExotic) requirements[config.armorPerks[gauntlet.slot].value]--; + else if (chest.isExotic) requirements[config.armorPerks[chest.slot].value]--; + else if (leg.isExotic) requirements[config.armorPerks[leg.slot].value]--; let bad = 0; for (let n = 1; n < ArmorPerkOrSlot.COUNT; n++) bad += Math.max(0, requirements[n]); @@ -171,28 +168,20 @@ function* generateArmorCombinations( gauntlets: IPermutatorArmor[], chests: IPermutatorArmor[], legs: IPermutatorArmor[], - constHasOneExoticLength: boolean, requiresAtLeastOneExotic: boolean ) { for (let helmet of helmets) { for (let gauntlet of gauntlets) { - if (constHasOneExoticLength && helmet.isExotic && gauntlet.isExotic) continue; + if (helmet.isExotic && gauntlet.isExotic) continue; for (let chest of chests) { - if (constHasOneExoticLength && (helmet.isExotic || gauntlet.isExotic) && chest.isExotic) - continue; + if ((helmet.isExotic || gauntlet.isExotic) && chest.isExotic) continue; for (let leg of legs) { - if ( - constHasOneExoticLength && - (helmet.isExotic || gauntlet.isExotic || chest.isExotic) && - leg.isExotic - ) - continue; + if ((helmet.isExotic || gauntlet.isExotic || chest.isExotic) && leg.isExotic) continue; if ( requiresAtLeastOneExotic && !(helmet.isExotic || gauntlet.isExotic || chest.isExotic || leg.isExotic) ) continue; - yield [helmet, gauntlet, chest, leg]; } } @@ -224,8 +213,9 @@ function estimateCombinationsToBeChecked( } addEventListener("message", async ({ data }) => { - const threadSplit = data.threadSplit as { count: number; current: number }; + if (data.type != "builderRequest") return; + const threadSplit = data.threadSplit as { count: number; current: number }; const startTime = Date.now(); console.debug("START RESULTS BUILDER 2"); console.time(`total #${threadSplit.current}`); @@ -243,7 +233,6 @@ addEventListener("message", async ({ data }) => { } console.log("Using config", data.config); - let selectedExotics = data.selectedExotics; let items = data.items as IPermutatorArmor[]; let helmets = items @@ -302,16 +291,17 @@ addEventListener("message", async ({ data }) => { // runtime variables const runtime = { + maximumExoticPossibleTiers: new Map(), maximumPossibleTiers: [0, 0, 0, 0, 0, 0], - statCombo3x100: new Set(), - statCombo4x100: new Set(), + statCombo3x100: new Set(), + statCombo4x100: new Set(), }; const constantBonus = prepareConstantStatBonus(config); const constantModslotRequirement = prepareConstantModslotRequirement(config); const constantAvailableModslots = prepareConstantAvailableModslots(config); - const constHasOneExoticLength = selectedExotics.length <= 1; const hasArtificeClassItem = availableClassItemPerkTypes.has(ArmorPerkOrSlot.SlotArtifice); - const requiresAtLeastOneExotic = config.selectedExotics.indexOf(FORCE_USE_ANY_EXOTIC) > -1; + const requiresAtLeastOneExotic = + config.selectedExotics.indexOf(FORCE_USE_NO_EXOTIC) == -1 && config.selectedExotics.length > 0; console.log("hasArtificeClassItem", hasArtificeClassItem); @@ -326,7 +316,7 @@ addEventListener("message", async ({ data }) => { let estimatedCalculations = estimateCombinationsToBeChecked(helmets, gauntlets, chests, legs); let checkedCalculations = 0; let lastProgressReportTime = 0; - console.log("estimatedCalculations", estimatedCalculations); + console.log(`estimatedCalculations for thread #${threadSplit.current}`, estimatedCalculations); // define the delay; it can be 75ms if the estimated calculations are low // if the estimated calculations >= 1e6, then we will use 125ms @@ -339,7 +329,6 @@ addEventListener("message", async ({ data }) => { gauntlets, chests, legs, - constHasOneExoticLength, requiresAtLeastOneExotic )) { checkedCalculations++; @@ -436,7 +425,12 @@ export function getStatSum( } export function handlePermutation( - runtime: any, + runtime: { + maximumExoticPossibleTiers: Map; + maximumPossibleTiers: number[]; + statCombo3x100: Set; + statCombo4x100: Set; + }, config: BuildConfiguration, helmet: IPermutatorArmor, gauntlet: IPermutatorArmor, @@ -448,6 +442,10 @@ export function handlePermutation( hasArtificeClassItem = false ): never[] | IPermutatorArmorSet | null { const items = [helmet, gauntlet, chest, leg]; + let exoticHash = items.find((x) => x.isExotic)?.hash ?? 0; + let exoticmaximumPossibleTiers = runtime.maximumExoticPossibleTiers.get(exoticHash) ?? [ + -1, -1, -1, -1, -1, -1, + ]; var totalStatBonus = config.assumeClassItemMasterworked ? 2 : 0; for (let i = 0; i < items.length; i++) { let item = items[i]; // add masterworked value, if necessary @@ -524,12 +522,47 @@ export function handlePermutation( optionalDistances[stat] = 10 - (stats[stat] % 10); } } + const totalOptionalDistances = optionalDistances.reduce((a, b) => a + b, 0); // if the sum of distances is > (10*5)+(3*artificeCount), we can abort here //const distanceSum = distances.reduce((a, b) => a + b, 0); - const distanceSum = + let distanceSum = distances[0] + distances[1] + distances[2] + distances[3] + distances[4] + distances[5]; - if (distanceSum > 10 * 5 + 3 * availableArtificeCount) return null; + if (distanceSum > getAvailableStatsByMods(availableArtificeCount, availableModCost)) { + for (let stat = 0; stat < 6; stat++) { + const oldDistance = distances[stat]; + for ( + let tier = 10; + tier >= + Math.floor( + Math.min(exoticmaximumPossibleTiers[stat], runtime.maximumPossibleTiers[stat]) / 10 + ); + tier-- + ) { + const v = 10 - (stats[stat] % 10); + distances[stat] = Math.max(v < 10 ? v : 0, tier * 10 - stats[stat]); + const mods = get_mods_precalc( + config, + distances, + [0, 0, 0, 0, 0, 0], + availableArtificeCount, + availableModCost, + ModOptimizationStrategy.None + ); + //const mods = null; + distances[stat] = oldDistance; + + if (mods != null) { + if (exoticmaximumPossibleTiers[stat] < tier * 10) + exoticmaximumPossibleTiers[stat] = tier * 10; + break; + } + } + } + if (exoticHash != 0) + runtime.maximumExoticPossibleTiers.set(exoticHash, exoticmaximumPossibleTiers); + return null; + } let result: StatModifier[] | null; if (distanceSum == 0 && totalOptionalDistances == 0) result = []; @@ -638,23 +671,28 @@ export function handlePermutation( // Tier Availability Testing //################################################################################# //* - let n = 0; for (let stat = 0; stat < 6; stat++) { if (runtime.maximumPossibleTiers[stat] < stats[stat]) { runtime.maximumPossibleTiers[stat] = stats[stat]; } + if (exoticmaximumPossibleTiers[stat] < stats[stat]) { + exoticmaximumPossibleTiers[stat] = stats[stat]; + } + const oldDistance = distances[stat]; for ( let tier = 10; tier >= config.minimumStatTiers[stat as ArmorStat].value && - tier > runtime.maximumPossibleTiers[stat] / 10; + tier > + Math.floor( + Math.min(exoticmaximumPossibleTiers[stat], runtime.maximumPossibleTiers[stat]) / 10 + ); tier-- ) { if (stats[stat] >= tier * 10) break; const v = 10 - (stats[stat] % 10); distances[stat] = Math.max(v < 10 ? v : 0, tier * 10 - stats[stat]); - n++; const mods = get_mods_precalc( config, distances, @@ -665,12 +703,17 @@ export function handlePermutation( ); //const mods = null; if (mods != null) { - runtime.maximumPossibleTiers[stat] = tier * 10; + if (runtime.maximumPossibleTiers[stat] < tier * 10) + runtime.maximumPossibleTiers[stat] = tier * 10; + if (exoticmaximumPossibleTiers[stat] < tier * 10) + exoticmaximumPossibleTiers[stat] = tier * 10; break; } } distances[stat] = oldDistance; } + if (exoticHash != 0) + runtime.maximumExoticPossibleTiers.set(exoticHash, exoticmaximumPossibleTiers); //console.debug("b "+runtime.maximumPossibleTiers,n) //console.warn(n) //*/ @@ -713,7 +756,9 @@ function get_mods_precalc( optimize: ModOptimizationStrategy = ModOptimizationStrategy.None ): StatModifier[] | null { // check distances <= 65 - if (distances[0] + distances[1] + distances[2] + distances[3] + distances[4] + distances[5] > 65) + const totalDistance = + distances[0] + distances[1] + distances[2] + distances[3] + distances[4] + distances[5]; + if (totalDistance > getAvailableStatsByMods(availableArtificeCount, availableModCost)) return null; const modCombinations = config.onlyShowResultsWithNoWastedStats @@ -747,7 +792,13 @@ function get_mods_precalc( const limit = 3; for (let i = 0; i < optionalDistances.length; i++) { if (optionalDistances[i] > 0) { - const additionalCombosA = modCombinations[optionalDistances[i]]; + const additionalCombosA = modCombinations[optionalDistances[i]].filter( + (d) => + d[2] == 0 && // disallow major mods + d[3] % 10 > 0 && // we do not want to add exact stat tiers + (optionalDistances[i] + d[3]) % 10 < optionalDistances[i] // and the changes must have less waste than before + ); + //(d) => d[3] % 10 > 0); if (additionalCombosA != null) { precalculatedMods[i] = additionalCombosA.slice(0, limit).concat(precalculatedMods[i]); } @@ -831,7 +882,7 @@ function get_mods_precalc( return true; } - const mustExecuteOptimization = optimize != ModOptimizationStrategy.None; + const mustExecuteOptimization = totalDistance > 0 && optimize != ModOptimizationStrategy.None; root: for (let mobility of precalculatedMods[0]) { if (!validate([mobility])) continue; for (let resilience of precalculatedMods[1]) { @@ -859,15 +910,7 @@ function get_mods_precalc( if (!validate(mods, true)) continue; - const sum = mods.reduce( - (a, b, i) => [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3] - distances[i]], - [0, 0, 0, 0] - ); - - if (sum[3] < 0) continue; // did not reach the target - if (sum[0] > availableArtificeCount) continue; - if (sum[0] == 0 && sum[1] == 0 && sum[2] == 0 && sum[3] == 0) continue; - + // Fill optional distances for (let m = 0; m < 6; m++) if (optionalDistances[m] > 0 && mods[m][3] == 0 && bestMods != null) continue inner; @@ -897,6 +940,24 @@ function get_mods_precalc( return usedMods; } +function getAvailableStatsByMods( + availableArtificeCount: number, + availableModCost: number[] +): number { + let LargeMajorMod = availableModCost.filter((x) => x >= 4).length; + let MediumMajorMod = availableModCost.filter((x) => x >= 3).length - LargeMajorMod; + let LargeMinorMod = + availableModCost.filter((x) => x >= 2).length - LargeMajorMod - MediumMajorMod; + let MediumMinorMod = + availableModCost.filter((x) => x >= 1).length - LargeMajorMod - MediumMajorMod - LargeMinorMod; + + return ( + (LargeMajorMod + MediumMajorMod) * 10 + + (LargeMinorMod + MediumMinorMod) * 5 + + availableArtificeCount * 3 + ); +} + export function getSkillTier(stats: number[]) { return ( Math.floor(Math.min(100, stats[ArmorStat.Mobility]) / 10) + diff --git a/src/app/services/userdata.service.ts b/src/app/services/userdata.service.ts index 67f53876..76ee381d 100644 --- a/src/app/services/userdata.service.ts +++ b/src/app/services/userdata.service.ts @@ -20,6 +20,7 @@ import { AuthService } from "./auth.service"; import { InventoryService } from "./inventory.service"; import { DestinyClass } from "bungie-api-ts/destiny2/interfaces"; import { MembershipService } from "./membership.service"; +import { ConfigurationService } from "./configuration.service"; @Injectable({ providedIn: "root", @@ -31,6 +32,7 @@ export class UserdataService { constructor( private auth: AuthService, + private config: ConfigurationService, private membership: MembershipService, private inventory: InventoryService ) { @@ -55,6 +57,9 @@ export class UserdataService { private async updateCharacterData() { this.characters = await this.membership.getCharacters(); + this.config.modifyConfiguration((d) => { + if (d.characterClass == DestinyClass.Unknown) d.characterClass = this.characters[0].clazz; + }); localStorage.setItem("cachedCharacters", JSON.stringify(this.characters)); } } diff --git a/tsconfig.json b/tsconfig.json index 1271922c..82ab8ae4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "importHelpers": true, "target": "es2020", "module": "es2020", - "lib": ["es2019", "dom"] + "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/tsconfig.worker.json b/tsconfig.worker.json index 34278ca2..7490f6f5 100644 --- a/tsconfig.worker.json +++ b/tsconfig.worker.json @@ -3,7 +3,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/worker", - "lib": ["es2019", "webworker"], + "lib": ["es2020", "webworker"], "types": [] }, "include": ["src/**/*.worker.ts"]