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 e82a834a..8ed1e58f 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 @@ -247,57 +247,75 @@ src="https://www.bungie.net/common/destiny2_content/icons/ea5af04ccd6a3470a44fd7bb0f66e2f7.png" /> - - - Configuration - - +{{ this.configValues[ArmorStat.Mobility] }} - - - +{{ this.configValues[ArmorStat.Resilience] }} - - - +{{ this.configValues[ArmorStat.Recovery] }} - - - +{{ this.configValues[ArmorStat.Discipline] }} - - - +{{ this.configValues[ArmorStat.Intellect] }} - - - +{{ this.configValues[ArmorStat.Strength] }} - + + + + + + {{ ModInformation[fragment].name }} + + + +{{ fragmentValues[ArmorStat.Mobility] }} + + + +{{ fragmentValues[ArmorStat.Resilience] }} + + + +{{ fragmentValues[ArmorStat.Recovery] }} + + + +{{ fragmentValues[ArmorStat.Discipline] }} + + + +{{ fragmentValues[ArmorStat.Intellect] }} + + + +{{ fragmentValues[ArmorStat.Strength] }} + + + +
+ + +
+ +
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 2aabec30..0201003e 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 @@ -70,9 +70,9 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { public config_assumeLegendariesMasterworked = false; public config_assumeExoticsMasterworked = false; public config_assumeClassItemMasterworked = false; + public config_automaticallySelectFragments = false; public config_enabledMods: ModOrAbility[] = []; public DIMUrl: string = ""; - configValues: [number, number, number, number, number, number] = [0, 0, 0, 0, 0, 0]; @Input() element: ResultDefinition | null = null; @@ -84,6 +84,25 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { private membership: MembershipService ) {} + calculateStatValuesFromFragmentsAndMods( + m: ModOrAbility[], + cls: DestinyClass + ): [number, number, number, number, number, number] { + return m + .reduce((p, v) => { + p = p.concat(ModInformation[v].bonus); + return p; + }, [] as ModifierValue[]) + .reduce( + (p, v) => { + if (v.stat == SpecialArmorStat.ClassAbilityRegenerationStat) p[[1, 0, 2][cls]] += v.value; + else p[v.stat as number] += v.value; + return p; + }, + [0, 0, 0, 0, 0, 0] + ); + } + public buildItemIdString(element: ResultDefinition | null) { if (!element) return ""; let result = element.items @@ -133,20 +152,7 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { this.config_assumeExoticsMasterworked = c.assumeExoticsMasterworked; this.config_assumeClassItemMasterworked = c.assumeClassItemMasterworked; this.config_enabledMods = c.enabledMods; - this.configValues = c.enabledMods - .reduce((p, v) => { - p = p.concat(ModInformation[v].bonus); - return p; - }, [] as ModifierValue[]) - .reduce( - (p, v) => { - if (v.stat == SpecialArmorStat.ClassAbilityRegenerationStat) - p[[1, 0, 2][c.characterClass]] += v.value; - else p[v.stat as number] += v.value; - return p; - }, - [0, 0, 0, 0, 0, 0] - ); + this.config_automaticallySelectFragments = c.automaticallySelectFragments; this.DIMUrl = this.generateDIMLink(c); }); @@ -244,12 +250,16 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { return cost; } + getAllEnabledModsAndFragments() { + return this.config_enabledMods.concat(this.element?.additionalFragments || []); + } + generateDIMLink(c: BuildConfiguration): string { const mods: number[] = []; const fragments: number[] = []; // add selected mods - for (let mod of this.config_enabledMods) { + for (let mod of this.getAllEnabledModsAndFragments()) { const modInfo = ModInformation[mod]; if (modInfo.type === ModifierType.CombatStyleMod) { mods.push(modInfo.hash); @@ -324,7 +334,8 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { if ( c.characterClass != DestinyClass.Unknown && - c.selectedModElement != ModifierType.CombatStyleMod + c.selectedModElement != ModifierType.CombatStyleMod && + c.selectedModElement != ModifierType.AnySubclass ) { const cl = SubclassHashes[c.characterClass]; const subclassHash = cl[c.selectedModElement]; @@ -354,7 +365,11 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { } getColumnForStat(statId: number) { - var configValueTiers = Math.floor(this.configValues[statId] / 10); + const configValues = this.calculateStatValuesFromFragmentsAndMods( + this.getAllEnabledModsAndFragments(), + this.config_characterClass + ); + var configValueTiers = Math.floor(configValues[statId] / 10); let d = []; let total = 0; diff --git a/src/app/components/authenticated-v2/results/results.component.html b/src/app/components/authenticated-v2/results/results.component.html index e5892697..59024577 100644 --- a/src/app/components/authenticated-v2/results/results.component.html +++ b/src/app/components/authenticated-v2/results/results.component.html @@ -166,6 +166,19 @@ report_problem + + report_problem +  Automatically add up to + {{ _config_maximumAutoSelectableFragments }} fragments  + report_problem + + 0 && c.automaticallySelectFragments; this._config_maximumStatMods = c.maximumStatMods; this._config_onlyUseMasterworkedExotics = c.onlyUseMasterworkedExotics; this._config_onlyUseMasterworkedLegendaries = c.onlyUseMasterworkedLegendaries; @@ -206,33 +220,43 @@ export class ResultsComponent implements OnInit, OnDestroy { this.tableDataSource.paginator = this.paginator; this.tableDataSource.sort = this.sort; this.tableDataSource.sortingDataAccessor = (data, sortHeaderId) => { + let value = 0; switch (sortHeaderId) { case "Mobility": - return data.stats[ArmorStat.Mobility]; + value = data.stats[ArmorStat.Mobility]; + break; case "Resilience": - return data.stats[ArmorStat.Resilience]; + value = data.stats[ArmorStat.Resilience]; + break; case "Recovery": - return data.stats[ArmorStat.Recovery]; + value = data.stats[ArmorStat.Recovery]; + break; case "Discipline": - return data.stats[ArmorStat.Discipline]; + value = data.stats[ArmorStat.Discipline]; + break; case "Intellect": - return data.stats[ArmorStat.Intellect]; + value = data.stats[ArmorStat.Intellect]; + break; case "Strength": - return data.stats[ArmorStat.Strength]; + value = data.stats[ArmorStat.Strength]; + break; case "Tiers": - return data.tiers; + value = data.tiers; + break; case "Max Tiers": - return 10 * (data.tiers + (5 - data.modCount)); + value = 10 * (data.tiers + (5 - data.modCount)); + break; case "Waste": - return data.waste; + value = data.waste; + break; case "Mods": - return ( - +100 * data.modCount + - //+ 40 * data.artifice.length - data.modCost - ); + value = +100 * data.modCount + data.modCost; } - return 0; + + // subtract the count of additional used fragments divided by 10 + value += data.additionalFragments.length / 6; + + return value; }; } @@ -263,9 +287,11 @@ export class ResultsComponent implements OnInit, OnDestroy { config: this.config.readonlyConfigurationSnapshot, results: this._results.map((r) => { let p = Object.assign({}, r); - p.items = p.items.map((i) => { - return { hash: i[0].hash, instance: i[0].itemInstanceId } as any; - }); + p.items = p.items + .filter((i) => !!i[0]) + .map((i) => { + return { hash: i[0].hash, instance: i[0].itemInstanceId } as any; + }); delete p.exotic; return p; }), diff --git a/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.html b/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.html index d0a492cd..4612abe0 100644 --- a/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.html +++ b/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.html @@ -44,6 +44,16 @@ #tooltip="matTooltip"> report_problem + + developer_board +
diff --git a/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.scss b/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.scss index e9b73750..e4da3c7e 100644 --- a/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.scss +++ b/src/app/components/authenticated-v2/settings/advanced-settings/advanced-settings.component.scss @@ -30,3 +30,7 @@ .report-problem-icon { color: lightcoral; } + +.indevelopment-icon { + color: rgb(128, 240, 221); +} 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 3c9917f0..40f1e212 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 @@ -28,6 +28,7 @@ interface AdvancedSettingField { help: string | undefined; disabled: boolean; impactsResultCount: boolean; + featureInDevelopment?: boolean; } @Component({ @@ -204,6 +205,7 @@ export class AdvancedSettingsComponent implements OnInit, OnDestroy { disabled: false, impactsResultCount: true, help: "This is a beta feature. Usability and quality may vary a lot.", + featureInDevelopment: true, }, ], }; diff --git a/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.html b/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.html index 5a4f312c..a43a43b8 100644 --- a/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.html +++ b/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.html @@ -39,13 +39,15 @@ [value]="ModifierType.Prismatic"> Prismatic + + Any +

{{ d.name }}

- There are no fragments configured yet. Please be patient. @@ -149,6 +151,29 @@

{{ d.name }}

+
+ + {{ i }} + +
+ This will automatically add fragments to fulfill the desired stat distribution, if necessary. +
+ Activating this setting will drastically increase the calculation time. +
+
diff --git a/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.ts b/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.ts index db033ebb..fc74be47 100644 --- a/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.ts +++ b/src/app/components/authenticated-v2/settings/desired-mods-selection/desired-mods-selection.component.ts @@ -16,7 +16,7 @@ */ import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ModInformation } from "../../../../data/ModInformation"; +import { MaximumFragmentsPerClass, ModInformation } from "../../../../data/ModInformation"; import { ModifierType } from "../../../../data/enum/modifierType"; import { Modifier, ModifierValue } from "../../../../data/modifier"; import { ArmorStat, SpecialArmorStat } from "../../../../data/enum/armor-stat"; @@ -39,6 +39,7 @@ import { Subject } from "rxjs"; ], }) export class DesiredModsSelectionComponent implements OnInit, OnDestroy { + readonly MaxFragmentRange = new Array(7); ModifierType = ModifierType; ModOrAbility = ModOrAbility; dataSource: Modifier[]; @@ -56,6 +57,11 @@ export class DesiredModsSelectionComponent implements OnInit, OnDestroy { selectedMods: ModOrAbility[] = []; selectedElement: ModifierType = ModifierType.Solar; + config_automaticallySelectFragments: boolean = false; + config_maximumAutoSelectableFragments: number = 5; + currentlySelectedFragments: number = 0; + maxFragmentsFromSubclass: number = 5; + constructor(private config: ConfigurationService) { const modifiers = Object.values(ModInformation).sort((a, b) => { if (a.name.toLowerCase() < b.name.toLowerCase()) { @@ -104,6 +110,12 @@ export class DesiredModsSelectionComponent implements OnInit, OnDestroy { group: true, type: ModifierType.Prismatic, }, + { + name: "Automatically add up to N fragments:", + data: [], + group: false, + type: ModifierType.AnySubclass, + }, ]; this.dataSource = modifiers; @@ -114,6 +126,23 @@ export class DesiredModsSelectionComponent implements OnInit, OnDestroy { this.selectedMods = c.enabledMods; this.selectedClass = c.characterClass; this.selectedElement = c.selectedModElement; + + this.config_automaticallySelectFragments = c.automaticallySelectFragments; + this.currentlySelectedFragments = c.enabledMods.length; + this.config_maximumAutoSelectableFragments = c.maximumAutoSelectableFragments; + this.maxFragmentsFromSubclass = + MaximumFragmentsPerClass[c.characterClass][c.selectedModElement]; + + if (!c.automaticallySelectFragments && c.maximumAutoSelectableFragments > 0) { + this.setFragmentLimit(0); + } + }); + } + + setFragmentLimit(newValue: number) { + this.config.modifyConfiguration((c) => { + c.automaticallySelectFragments = newValue > 0; + c.maximumAutoSelectableFragments = newValue; }); } @@ -146,6 +175,7 @@ export class DesiredModsSelectionComponent implements OnInit, OnDestroy { this.config.modifyConfiguration((c) => { c.enabledMods = []; }); + this.setFragmentLimit(0); } setElement(element: ModifierType) { 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 0371925d..741c4e1b 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 @@ -24,6 +24,7 @@
-

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..019afebd 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 @@ -50,6 +50,7 @@ export class DesiredStatSelectionComponent implements OnInit, OnDestroy { config_mod_strategy = ModOptimizationStrategy.None; config_reduce_waste = false; config_allowExactStats = false; + config_automaticallySelectFragments = false; constructor(public config: ConfigurationService, private inventory: InventoryService) { this.stats = Object.keys(ArmorStat) @@ -73,6 +74,7 @@ export class DesiredStatSelectionComponent implements OnInit, OnDestroy { this.config_mod_strategy = c.modOptimizationStrategy; this.config_reduce_waste = c.tryLimitWastedStats; this.config_allowExactStats = c.allowExactStats; + this.config_automaticallySelectFragments = c.automaticallySelectFragments; }); this.inventory.armorResults.pipe(takeUntil(this.ngUnsubscribe)).subscribe((d) => { 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..44338c78 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 @@ -26,6 +26,7 @@ import { ArmorStat } from "../../../../../data/enum/armor-stat"; export class StatTierSelectionComponent { readonly TierRange = new Array(11); @Input() allowExactStats: boolean = false; + @Input() automaticallySelectFragments: boolean = false; @Input() stat: ArmorStat = ArmorStat.Mobility; @Input() statsByMods: number = 0; @Input() maximumAvailableTier: number = 10; @@ -37,7 +38,7 @@ export class StatTierSelectionComponent { constructor() {} setValue(newValue: number) { - if (newValue <= this.maximumAvailableTier) { + if (newValue <= this.maximumAvailableTier || this.automaticallySelectFragments) { this.selectedTier = newValue; this.selectedTierChange.emit(newValue); } diff --git a/src/app/components/authenticated-v2/settings/settings.component.html b/src/app/components/authenticated-v2/settings/settings.component.html index aef23882..f101ae84 100644 --- a/src/app/components/authenticated-v2/settings/settings.component.html +++ b/src/app/components/authenticated-v2/settings/settings.component.html @@ -63,7 +63,7 @@ Stat-Boost Selection Select Mods and Skills that affect your overall stats.
+ >Select subclass fragments that affect your overall stats.
Please note that D2AP also allows theoretical, but impossible input.
Only fragments that affect stats are shown.
diff --git a/src/app/data/ModInformation.ts b/src/app/data/ModInformation.ts index c36613b2..88d04863 100644 --- a/src/app/data/ModInformation.ts +++ b/src/app/data/ModInformation.ts @@ -20,7 +20,7 @@ import { ModOrAbility } from "./enum/modOrAbility"; import { Modifier } from "./modifier"; import { ModifierType } from "./enum/modifierType"; import { ArmorStat, SpecialArmorStat } from "./enum/armor-stat"; -import { DestinyEnergyType } from "bungie-api-ts/destiny2/interfaces"; +import { DestinyClass, DestinyEnergyType } from "bungie-api-ts/destiny2/interfaces"; export const ModInformation: EnumDictionary = { // region Stasis @@ -671,3 +671,36 @@ export const ModInformation: EnumDictionary = { }, // endregion Prismatic }; + +// The number of maximum selectable fragments per class & subclass + +export const MaximumFragmentsPerClass: { [key in DestinyClass]: { [key: number]: number } } = { + [DestinyClass.Titan]: { + [ModifierType.Stasis]: 5, + [ModifierType.Void]: 4, + [ModifierType.Solar]: 4, + [ModifierType.Arc]: 4, + [ModifierType.Strand]: 4, + [ModifierType.Prismatic]: 6, + [ModifierType.AnySubclass]: 6, + }, + [DestinyClass.Hunter]: { + [ModifierType.Stasis]: 5, + [ModifierType.Void]: 4, + [ModifierType.Solar]: 5, + [ModifierType.Arc]: 4, + [ModifierType.Strand]: 4, + [ModifierType.Prismatic]: 6, + [ModifierType.AnySubclass]: 6, + }, + [DestinyClass.Warlock]: { + [ModifierType.Stasis]: 4, + [ModifierType.Void]: 4, + [ModifierType.Solar]: 4, + [ModifierType.Arc]: 4, + [ModifierType.Strand]: 4, + [ModifierType.Prismatic]: 6, + [ModifierType.AnySubclass]: 6, + }, + [DestinyClass.Unknown]: {}, +}; diff --git a/src/app/data/buildConfiguration.ts b/src/app/data/buildConfiguration.ts index f27c1b40..f552405b 100644 --- a/src/app/data/buildConfiguration.ts +++ b/src/app/data/buildConfiguration.ts @@ -69,6 +69,10 @@ export class BuildConfiguration { // if set, then we can use the exact stats like 6x69. It will be stored as "fixed 6.9" in minimumStatTiers allowExactStats = false; + // allows us to automatically select the best fragments for the selected subclass + automaticallySelectFragments = false; + maximumAutoSelectableFragments = 0; + // Fixable, BUT the bool is not yet used. Maybe in a future update. maximumModSlots: EnumDictionary> = { [ArmorSlot.ArmorSlotHelmet]: { fixed: false, value: 5 }, @@ -114,6 +118,8 @@ export class BuildConfiguration { static buildEmptyConfiguration(): BuildConfiguration { return { + maximumAutoSelectableFragments: 0, + automaticallySelectFragments: false, ignoreExistingExoticArtificeSlots: false, allowExactStats: false, enabledMods: [], diff --git a/src/app/data/enum/modifierType.ts b/src/app/data/enum/modifierType.ts index a0ba65ce..12db5fba 100644 --- a/src/app/data/enum/modifierType.ts +++ b/src/app/data/enum/modifierType.ts @@ -23,6 +23,10 @@ export enum ModifierType { Arc, Strand, Prismatic, + AnySubclass, } -export type Subclass = Exclude; +export type Subclass = Exclude< + ModifierType, + ModifierType.CombatStyleMod | ModifierType.AnySubclass +>; diff --git a/src/app/data/types/IPermutatorArmorSet.ts b/src/app/data/types/IPermutatorArmorSet.ts index d37ee35c..c24a351f 100644 --- a/src/app/data/types/IPermutatorArmorSet.ts +++ b/src/app/data/types/IPermutatorArmorSet.ts @@ -1,4 +1,5 @@ import { ArmorPerkOrSlot, StatModifier } from "../enum/armor-stat"; +import { ModOrAbility } from "../enum/modOrAbility"; import { IPermutatorArmor } from "./IPermutatorArmor"; export interface IPermutatorArmorSet { @@ -8,6 +9,8 @@ export interface IPermutatorArmorSet { classItemPerk: ArmorPerkOrSlot; statsWithMods: number[]; statsWithoutMods: number[]; + // Contains the additional fragments that were automatically picked by the system + additionalFragments: ModOrAbility[]; } export function createArmorSet( @@ -27,6 +30,7 @@ export function createArmorSet( classItemPerk: ArmorPerkOrSlot.None, statsWithMods, statsWithoutMods, + additionalFragments: [], }; } diff --git a/src/app/services/character-stats.service.ts b/src/app/services/character-stats.service.ts index e070ca96..c9b019c4 100644 --- a/src/app/services/character-stats.service.ts +++ b/src/app/services/character-stats.service.ts @@ -160,28 +160,41 @@ export class CharacterStatsService { const exoticOverrides = this.overrides.filter((o) => exoticHashes.includes(o.Hash)); - return entries - .filter((entry) => { - if ( - characterClass !== undefined && - entry.characterClass !== undefined && - entry.characterClass !== characterClass - ) { - return false; - } - - if (element !== undefined && entry.element !== undefined && entry.element !== element) { - return false; - } - - return true; - }) - .map((entry) => { - return exoticOverrides.reduce( - (acc, override) => applyExoticArmorOverride(acc, override), - entry - ); - }); + return ( + entries + .filter((entry) => { + if ( + characterClass !== undefined && + entry.characterClass !== undefined && + entry.characterClass !== characterClass + ) { + return false; + } + + if ( + element !== undefined && + entry.element !== undefined && + entry.element !== element && + element != ModifierType.AnySubclass + ) { + return false; + } + + return true; + }) + // sort by element + .sort((a, b) => a.element! - b.element! || 0) + // remove duplicates by name + .filter((entry, index, self) => self.findIndex((e) => e.name === entry.name) === index) + // Limit to N entries to not overflow the tooltip + .slice(0, 12) + .map((entry) => { + return exoticOverrides.reduce( + (acc, override) => applyExoticArmorOverride(acc, override), + entry + ); + }) + ); } private generateEntries( diff --git a/src/app/services/inventory.service.ts b/src/app/services/inventory.service.ts index 58b0bdb1..1feb868b 100644 --- a/src/app/services/inventory.service.ts +++ b/src/app/services/inventory.service.ts @@ -489,6 +489,7 @@ export class InventoryService { }, [[], [], [], [], []] ), + additionalFragments: armorSet.additionalFragments, classItem: armorSet.classItemPerk, usesCollectionRoll: items.some( (y) => y.source === InventoryArmorSource.Collections @@ -575,6 +576,9 @@ export class InventoryService { ) { calculationMultiplier = 0.7; } + if (this._config.automaticallySelectFragments) { + calculationMultiplier *= 0.35; + } let minimumCalculationPerThread = calculationMultiplier * 5e4; let maximumCalculationPerThread = calculationMultiplier * 2.5e5; diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index acbf1643..3868ef33 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 { ArmorSlot } from "../data/enum/armor-slot"; import { FORCE_USE_ANY_EXOTIC } from "../data/constants"; -import { ModInformation } from "../data/ModInformation"; +import { MaximumFragmentsPerClass, ModInformation } from "../data/ModInformation"; import { ArmorPerkOrSlot, ArmorStat, @@ -39,6 +39,14 @@ import { createArmorSet, isIPermutatorArmorSet, } from "../data/types/IPermutatorArmorSet"; +import { Modifier } from "../data/modifier"; +import { ModifierType } from "../data/enum/modifierType"; + +interface IFragmentCombination { + subclass: number | null; + fragments: Modifier[]; + stats: number[]; +} function checkSlots( config: BuildConfiguration, @@ -169,6 +177,132 @@ function prepareConstantAvailableModslots(config: BuildConfiguration) { return availableModCost.filter((d) => d > 0).sort((a, b) => b - a); } +// NOT RECURSIVE +function* generateFragmentCombinationsForGroup( + fragments: Modifier[], + fragmentCount: number, + fragmentIndex = 0, + currentCombination: Modifier[] = [] +): Generator { + if (fragmentCount == 0 || fragmentIndex >= fragments.length) { + yield currentCombination; + } else { + for (let i = fragmentIndex; i < fragments.length; i++) { + const fragment = fragments[i]; + const newCombination = [...currentCombination]; + newCombination.push(fragment); + yield* generateFragmentCombinationsForGroup( + fragments, + fragmentCount - 1, + i + 1, + newCombination + ); + } + } +} + +function* generateFragmentCombinations( + config: BuildConfiguration +): Generator { + yield { subclass: ModifierType.AnySubclass, fragments: [], stats: [0, 0, 0, 0, 0, 0] }; + if (config.automaticallySelectFragments) { + // group the fragments in ModInformation by subclass (requiredArmorAffinity) + const fragmentsBySubclass = new Map(); + find_fragments: for (const fragment of Object.values(ModInformation)) { + const subclass = fragment.type; + // filter the fragments by the selected subclass, if it is not AnySubclass + if ( + config.selectedModElement != ModifierType.AnySubclass && + subclass != config.selectedModElement + ) + continue; + + // only allow negative fragments if the corresponding stat is locked + if (fragment.bonus.some((d) => d.value < 0)) { + for (let i = 0; i < fragment.bonus.length; i++) { + if (fragment.bonus[i].value < 0 && !config.minimumStatTiers[i as ArmorStat].fixed) + continue find_fragments; + } + } + + // if the fragment is already selected in the enabledMods, do not add it again + if (config.enabledMods.indexOf(fragment.id) > -1) continue; + + if (!fragmentsBySubclass.has(subclass)) fragmentsBySubclass.set(subclass, []); + fragmentsBySubclass.get(subclass)!.push(fragment); + } + + let alreadyReservedFragments = 0; + // for each selected fragment in the subclass, reduce the possibleNumberOfFragments by 1 + for (const fragment of Object.values(ModInformation)) { + // in enabledMods + if (config.enabledMods.indexOf(fragment.id) > -1) alreadyReservedFragments++; + } + + // generate all possible combinations of fragments in a group, starting with 1 fragment, up to 4 + for (const [subclass, fragments] of fragmentsBySubclass) { + const possibleNumberOfFragments = Math.min( + config.maximumAutoSelectableFragments, + MaximumFragmentsPerClass[config.characterClass][subclass] - alreadyReservedFragments + ); + for (let i = 1; i <= possibleNumberOfFragments; i++) { + for (const fragmentCombination of generateFragmentCombinationsForGroup(fragments, i)) { + const result = [0, 0, 0, 0, 0, 0]; + for (const fragment of fragmentCombination) { + for (const bonus of fragment.bonus) { + const statId = + bonus.stat == SpecialArmorStat.ClassAbilityRegenerationStat + ? [1, 0, 2][subclass] + : bonus.stat; + result[statId] += bonus.value; + } + } + yield { + subclass, + fragments: fragmentCombination, + stats: result, + }; + } + } + } + } +} + +function prepareFragments(config: BuildConfiguration): IFragmentCombination[] { + // get all fragment combinations + const fragmentCombinations = Array.from(generateFragmentCombinations(config)); + // remove duplicates. A duplicate has the same stats. + const fragmentCombinationsSetIds = new Set( + fragmentCombinations.map((d) => JSON.stringify(d.stats)) + ); + let fragmentCombinationsSet: IFragmentCombination[] = Array.from(fragmentCombinationsSetIds) + .map((d) => fragmentCombinations.find((f) => JSON.stringify(f.stats) == d)) + .filter((d) => d != null && d != undefined) as IFragmentCombination[]; + + // filter: Only allow negative stats if the corresponding stat is locked + fragmentCombinationsSet = fragmentCombinationsSet.filter((d) => { + const hasNegative = d!.stats.some((d) => d < 0); + if (!hasNegative) return true; + + for (let i = 0; i < d!.stats.length; i++) { + if (d!.stats[i] < 0 && config.minimumStatTiers[i as ArmorStat].fixed) return true; + } + return false; + }); + + // sort by total stat boost: + // first the lowest >= 0, afterwards the lowest <=0 - basically [0,10, 20, -10, -20] + fragmentCombinationsSet = fragmentCombinationsSet.sort((a, b) => { + const hasNegativeA = a!.stats.some((d) => d < 0); + const hasNegativeB = b!.stats.some((d) => d < 0); + + if (!hasNegativeA && hasNegativeB) return -1; + if (hasNegativeA && !hasNegativeB) return 1; + return a!.stats.reduce((a, b) => a + b) - b!.stats.reduce((a, b) => a + b); + }); + return fragmentCombinationsSet; +} + function* generateArmorCombinations( helmets: IPermutatorArmor[], gauntlets: IPermutatorArmor[], @@ -357,6 +491,9 @@ addEventListener("message", async ({ data }) => { // if the estimated calculations >= 1e6, then we will use 125ms let progressBarDelay = estimatedCalculations >= 1e6 ? 125 : 75; + // unless the configuration is set, this will only contain one entry - an empty set + const fragmentCombinationsSet = prepareFragments(config); + console.time(`tm #${threadSplit.current}`); for (let [helmet, gauntlet, chest, leg] of generateArmorCombinations( @@ -387,24 +524,39 @@ addEventListener("message", async ({ data }) => { const canUseArtificeClassItem = !slotCheckResult.requiredClassItemType || slotCheckResult.requiredClassItemType == ArmorPerkOrSlot.SlotArtifice; - const hasOneExotic = helmet.isExotic || gauntlet.isExotic || chest.isExotic || leg.isExotic; const tmpHasArtificeClassItem = - hasArtificeClassItem || - (!hasOneExotic && hasArtificeClassItemExotic && !config.ignoreExistingExoticArtificeSlots); - const result = handlePermutation( - runtime, - config, - helmet, - gauntlet, - chest, - leg, - constantBonus, - constantAvailableModslots, - doNotOutput, - tmpHasArtificeClassItem && canUseArtificeClassItem, - exoticClassItemIsEnforced - ); + hasArtificeClassItem || (!hasOneExotic && hasArtificeClassItemExotic); + + let result = null; + for (const fragmentCombination of fragmentCombinationsSet) { + const constantBonusWithFragments = constantBonus.map( + (d, i) => d + fragmentCombination!.stats[i] + ); + result = handlePermutation( + runtime, + config, + helmet, + gauntlet, + chest, + leg, + constantBonusWithFragments, + constantAvailableModslots, + doNotOutput, + tmpHasArtificeClassItem && canUseArtificeClassItem, + exoticClassItemIsEnforced, + fragmentCombination, + fragmentCombinationsSet + ); + + if (result != null) { + if (isIPermutatorArmorSet(result)) { + const fragmentIds = fragmentCombination!.fragments.map((d) => d.id); + (result as unknown as IPermutatorArmorSet).additionalFragments = fragmentIds; + } + break; + } + } // Only add 50k to the list if the setting is activated. // We will still calculate the rest so that we get accurate results for the runtime values if (result != null) { @@ -415,6 +567,7 @@ addEventListener("message", async ({ data }) => { (hasArtificeClassItem ? ArmorPerkOrSlot.SlotArtifice : ArmorPerkOrSlot.None); // add the exotic class item if we have one and we do not have an exotic armor piece in this selection + if (!hasOneExotic && exoticClassItem && exoticClassItemIsEnforced) { result.armor.push(exoticClassItem.id); } @@ -483,7 +636,9 @@ export function handlePermutation( availableModCost: number[], doNotOutput = false, hasArtificeClassItem = false, - hasExoticClassItem = false + hasExoticClassItem = false, + currentFragmentCombination: IFragmentCombination | null = null, + allFragmentCombinations: IFragmentCombination[] = [] ): never[] | IPermutatorArmorSet | null { const items = [helmet, gauntlet, chest, leg]; var totalStatBonus = 0; @@ -608,26 +763,33 @@ export function handlePermutation( ]; // find every combo of three stats which sum is less than 65; no duplicates - let combos3x100 = []; - let combos4x100 = []; + let combos3x100: [number[], IFragmentCombination][] = []; + let combos4x100: [number[], IFragmentCombination][] = []; for (let i = 0; i < 4; i++) { for (let j = i + 1; j < 5; j++) { - for (let k = j + 1; k < 6; k++) { - let dx = distances.slice(); - dx[i] = distancesTo100[i]; - dx[j] = distancesTo100[j]; - dx[k] = distancesTo100[k]; - let distanceSum = dx[0] + dx[1] + dx[2] + dx[3] + dx[4] + dx[5]; - if (distanceSum <= 65) { - combos3x100.push([i, j, k]); - - for (let l = k + 1; l < 6; l++) { - let dy = dx.slice(); - dy[l] = distancesTo100[l]; - let distanceSum = dy[0] + dy[1] + dy[2] + dy[3] + dy[4] + dy[5]; - if (distanceSum <= 65) { - combos4x100.push([i, j, k, l]); + inner_loop: for (let k = j + 1; k < 6; k++) { + for (let fragmentCombo of allFragmentCombinations) { + let dx = distances.slice(); + dx[i] = distancesTo100[i]; + dx[j] = distancesTo100[j]; + dx[k] = distancesTo100[k]; + for (let p = 0; p < 6; p++) { + dx[p] = Math.max(0, dx[p] - fragmentCombo.stats[p]); + } + let distanceSum = dx[0] + dx[1] + dx[2] + dx[3] + dx[4] + dx[5]; + if (distanceSum <= 65) { + combos3x100.push([[i, j, k], fragmentCombo]); + + for (let l = k + 1; l < 6; l++) { + let dy = dx.slice(); + dy[l] = distancesTo100[l]; + dy[l] = Math.max(0, dy[l] - fragmentCombo.stats[l]); + let distanceSum = dy[0] + dy[1] + dy[2] + dy[3] + dy[4] + dy[5]; + if (distanceSum <= 65) { + combos4x100.push([[i, j, k, l], fragmentCombo]); + } } + break inner_loop; } } } @@ -635,10 +797,13 @@ export function handlePermutation( } if (combos3x100.length > 0) { // now validate the combos using get_mods_precalc with optimize=false - for (let combo of combos3x100) { + for (let entry of combos3x100) { + const combo = entry[0]; + const fragmentCombo = entry[1]; + const newDistances = distances.slice(); for (let i of combo) { - newDistances[i] = distancesTo100[i]; + newDistances[i] = Math.max(0, distancesTo100[i] - fragmentCombo.stats[i]); } const mods = get_mods_precalc( config, @@ -653,10 +818,13 @@ export function handlePermutation( } } // now validate the combos using get_mods_precalc with optimize=false - for (let combo of combos4x100) { + for (let entry of combos4x100) { + const combo = entry[0]; + const fragmentCombo = entry[1]; + const newDistances = distances.slice(); for (let i of combo) { - newDistances[i] = distancesTo100[i]; + newDistances[i] = Math.max(0, distancesTo100[i] - fragmentCombo.stats[i]); } const mods = get_mods_precalc( config, @@ -690,7 +858,7 @@ export function handlePermutation( } const oldDistance = distances[stat]; - for ( + tier_loop: for ( let tier = 10; tier >= config.minimumStatTiers[stat as ArmorStat].value && tier > runtime.maximumPossibleTiers[stat] / 10; @@ -699,18 +867,28 @@ export function handlePermutation( if (stats[stat] >= tier * 10) break; 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; - if (mods != null) { - runtime.maximumPossibleTiers[stat] = tier * 10; - break; + + for (let fragmentCombination of allFragmentCombinations) { + const newDist = distances.slice(); + // now add the fragment combination + for (let i = 0; i < 6; i++) { + newDist[i] -= fragmentCombination.stats[i]; + newDist[i] += currentFragmentCombination?.stats[i] || 0; + newDist[i] = Math.max(0, newDist[i]); + } + const mods = get_mods_precalc( + config, + newDist, + [0, 0, 0, 0, 0, 0], + availableArtificeCount, + availableModCost, + ModOptimizationStrategy.None + ); + //const mods = null; + if (mods != null) { + runtime.maximumPossibleTiers[stat] = tier * 10; + break tier_loop; + } } } distances[stat] = oldDistance;