0">
@@ -149,6 +151,29 @@ {{ d.name }}
+
+
+ = i &&
+ maxFragmentsFromSubclass - currentlySelectedFragments >= i
+ "
+ [class.not-checked]="config_maximumAutoSelectableFragments < i"
+ >{{ i }}
+
+
+ This will automatically add fragments to fulfill the desired stat distribution, if necessary.
+
+ Activating this setting will drastically increase the calculation time.
+
+
Clear the whole selection
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;