> = 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"]