From 7161b4bfe73b122d87c8101ed1bceadd53bfc936 Mon Sep 17 00:00:00 2001 From: Mijago Date: Thu, 7 Aug 2025 07:01:26 +0200 Subject: [PATCH 01/21] feat: initial tuning implementation; unfinished --- src/app/data/types/IPermutatorArmorSet.ts | 5 + src/app/services/results-builder.worker.ts | 207 ++++++++++++++++++--- 2 files changed, 183 insertions(+), 29 deletions(-) diff --git a/src/app/data/types/IPermutatorArmorSet.ts b/src/app/data/types/IPermutatorArmorSet.ts index f86e2cea..2b3c14e2 100644 --- a/src/app/data/types/IPermutatorArmorSet.ts +++ b/src/app/data/types/IPermutatorArmorSet.ts @@ -1,6 +1,10 @@ import { StatModifier } from "../enum/armor-stat"; import { IPermutatorArmor } from "./IPermutatorArmor"; +export interface Tuning { + stats: number[]; + improvements: number[][]; +} export interface IPermutatorArmorSet { armor: number[]; useExoticClassItem: boolean; @@ -8,6 +12,7 @@ export interface IPermutatorArmorSet { usedMods: StatModifier[]; statsWithMods: number[]; statsWithoutMods: number[]; + tuning?: Tuning; } export function createArmorSet( diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 9d49116a..76625a28 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -37,12 +37,18 @@ import { precalculatedModCombinations } from "../data/generated/precalculatedMod import { ModOptimizationStrategy } from "../data/enum/mod-optimization-strategy"; import { IPermutatorArmor } from "../data/types/IPermutatorArmor"; import { + Tuning, IPermutatorArmorSet, createArmorSet, isIPermutatorArmorSet, } from "../data/types/IPermutatorArmorSet"; import { ArmorSystem } from "../data/types/IManifestArmor"; -// endregion Imports +// endregion Import + +interface StatModCalculationResult { + statMods: StatModifier[]; + tunings?: Tuning; +} // region Validation and Preparation Functions function checkSlots( @@ -562,6 +568,10 @@ export function handlePermutation( for (let item of items) applyMasterworkStats(item, config, stats); + const possibleT5Improvements: ArmorStat[][] = items + .filter((i) => i.armorSystem == ArmorSystem.Armor3 && i.tier == 5) + .map((i) => i.archetypeStats); + const statsWithoutMods = [stats[0], stats[1], stats[2], stats[3], stats[4], stats[5]]; stats[0] += constantBonus[0]; stats[1] += constantBonus[1]; @@ -723,36 +733,48 @@ export function handlePermutation( } } - const newDistanceSum = - newDistances[0] + - newDistances[1] + - newDistances[2] + - newDistances[3] + - newDistances[4] + - newDistances[5]; + const newDistanceSum = newDistances.reduce((a, b) => a + b, 0); const newTotalOptionalDistances = newOptionalDistances.reduce((a, b) => a + b, 0); - if (newDistanceSum > 10 * 5 + 3 * tmpArtificeCount) continue; - - let result: StatModifier[] | null; - if (newDistanceSum == 0 && newTotalOptionalDistances == 0) result = []; + let result: StatModCalculationResult | null; + if (newDistanceSum == 0 && newTotalOptionalDistances == 0) result = { statMods: [] }; else - result = get_mods_precalc( + result = get_mods_precalc_with_tuning( config, + adjustedStats, newDistances, newOptionalDistances, tmpArtificeCount, + possibleT5Improvements, config.modOptimizationStrategy ); if (result !== null) { // Perform Tier Availability Testing with this class item + const tierTestingStats = [...adjustedStats]; + const tierTestingTunings = [...possibleT5Improvements]; + // Add tuning + if (result.tunings) { + for (let n = 0; n < 6; n++) { + tierTestingStats[n] += result.tunings.stats[n]; + } + + for (let tuning of result.tunings.improvements) { + const index = tierTestingTunings.findIndex((improvement) => + improvement.every((val, idx) => val === tuning[idx]) + ); + if (index !== -1) { + tierTestingTunings.splice(index, 1); + } + } + } performTierAvailabilityTesting( runtime, config, adjustedStats, newDistances, - tmpArtificeCount + tmpArtificeCount, + tierTestingTunings ); // This may lead to issues later. @@ -767,12 +789,11 @@ export function handlePermutation( chest, leg, classItem, - result, + result.statMods, adjustedStats, adjustedStatsWithoutMods, - newDistances, - tmpArtificeCount, - doNotOutput + doNotOutput, + result.tunings ); } } @@ -787,7 +808,8 @@ function performTierAvailabilityTesting( config: BuildConfiguration, stats: number[], distances: number[], - availableArtificeCount: number + availableArtificeCount: number, + possibleT5Improvements: ArmorStat[][] ): void { for (let stat = 0; stat < 6; stat++) { if (runtime.maximumPossibleTiers[stat] < stats[stat]) { @@ -818,11 +840,13 @@ function performTierAvailabilityTesting( testDistances[stat] = Math.max(v < 10 ? v : 0, mid - stats[stat]); // Check if this value is achievable with mods - const mods = get_mods_precalc( + const mods = get_mods_precalc_with_tuning( config, + stats, testDistances, [0, 0, 0, 0, 0, 0], availableArtificeCount, + possibleT5Improvements, ModOptimizationStrategy.None ); @@ -841,11 +865,13 @@ function performTierAvailabilityTesting( const v = 10 - (stats[stat] % 10); const testDistances = [...distances]; testDistances[stat] = Math.max(v < 10 ? v : 0, low - stats[stat]); - const mods = get_mods_precalc( + const mods = get_mods_precalc_with_tuning( config, + stats, testDistances, [0, 0, 0, 0, 0, 0], availableArtificeCount, + possibleT5Improvements, ModOptimizationStrategy.None ); if (mods != null) { @@ -866,9 +892,8 @@ function tryCreateArmorSetWithClassItem( result: StatModifier[], adjustedStats: number[], statsWithoutMods: number[], - newDistances: number[], - availableArtificeCount: number, - doNotOutput: boolean + doNotOutput: boolean, + tuning: Tuning | undefined ): IPermutatorArmorSet | never[] { if (doNotOutput) return []; @@ -882,9 +907,15 @@ function tryCreateArmorSetWithClassItem( finalStats[stat] += STAT_MOD_VALUES[statModifier][1]; } + if (tuning) + for (let n = 0; n < 6; n++) { + finalStats[n] += tuning.stats[n]; + } + const waste1 = getWaste(finalStats); if (config.onlyShowResultsWithNoWastedStats && waste1 > 0) return []; + // TODO: Add tuning return createArmorSet( helmet, gauntlet, @@ -899,23 +930,140 @@ function tryCreateArmorSetWithClassItem( } // region Mod Calculation Functions -function get_mods_precalc( +function get_mods_precalc_with_tuning( config: BuildConfiguration, + currentStats: number[], distances: number[], optionalDistances: number[], availableArtificeCount: number, + possibleT5Improvements: ArmorStat[][], optimize: ModOptimizationStrategy = ModOptimizationStrategy.None -): StatModifier[] | null { +): StatModCalculationResult | null { // check distances <= 65 const totalDistance = distances[0] + distances[1] + distances[2] + distances[3] + distances[4] + distances[5]; - if (totalDistance > 65) return null; + if (totalDistance > 65 + 5 * possibleT5Improvements.length) return null; if (totalDistance == 0 && optionalDistances.every((d) => d == 0)) { // no mods needed, return empty array - return []; + return { statMods: [] }; } + let selectedT5Improvements: Tuning[][] = []; + if (possibleT5Improvements.length > 0) { + possibleT5Improvements = possibleT5Improvements.filter( + (archetypeStats) => distances[archetypeStats[0]] > 0 + ); + for (let i = 0; i < possibleT5Improvements.length; i++) { + const newBoosts: Tuning[] = []; + const archetypeStats = possibleT5Improvements[i]; + // TypeB) Add +1 to the three stats that are not in the archetypeStats (that also receive the +5 masterwork bonus) + // We can only use this if the distance is > 0, otherwise we would not need any mods + const t5Boost = [0, 0, 0, 0, 0, 0]; + for (let j = 0; j < 6; j++) { + if (!archetypeStats.includes(j)) { + t5Boost[j] += 1; + } + } + newBoosts.push({ stats: t5Boost, improvements: [archetypeStats] }); + // TypeA) Add +5 to the specified stat - but applies -5 to one other stat. + for (let j = 0; j < 6; j++) { + if (archetypeStats.includes(j)) continue; // Skip the archetype stat, we want to boost it + const t5Boost = [0, 0, 0, 0, 0, 0]; + t5Boost[archetypeStats[0]] += 5; + t5Boost[archetypeStats[j]] -= 5; + newBoosts.push({ stats: t5Boost, improvements: [archetypeStats] }); + } + selectedT5Improvements.push(newBoosts); + } + } + + function* buildIterationsRecursive( + allImprovements: Tuning[][], + currentIndex: number, + currentValue?: Tuning + ): Generator { + if (currentValue === undefined) { + currentValue = { + stats: [0, 0, 0, 0, 0, 0], + improvements: [], + }; + } + // We have N possible picks with multiple possible improvements per pick (only one of these can be used) + // I want to iterate over every possible combination of these improvements + // This function will yield the total change of the six stats for each combination + if (currentIndex >= allImprovements.length) { + yield currentValue; + return; + } + + for (let i = 0; i < allImprovements[currentIndex].length; i++) { + const newValue = [...currentValue.stats]; + for (let j = 0; j < 6; j++) { + newValue[j] += allImprovements[currentIndex][i].stats[j]; + } + yield* buildIterationsRecursive(allImprovements, currentIndex + 1, { + stats: newValue, + improvements: [ + ...currentValue.improvements, + ...allImprovements[currentIndex][i].improvements, + ], + }); + } + } + + let allPossibleT5Improvements = Array.from(buildIterationsRecursive(selectedT5Improvements, 0)); + // Sort allPossibleT5Improvements descending by total points (sum of all values) + // Negative points are worse, so higher sum is better + // Special case: [0,0,0,0,0,0] should always be first + allPossibleT5Improvements.sort((_a, _b) => { + const a = _a.stats; + const b = _b.stats; + const isAZero = a.every((v) => v === 0); + const isBZero = b.every((v) => v === 0); + if (isAZero && !isBZero) return -1; + if (!isAZero && isBZero) return 1; + // Descending by sum + const sumA = a.reduce((acc, v) => acc + v, 0); + const sumB = b.reduce((acc, v) => acc + v, 0); + return sumB - sumA; + }); + + for (const tuning of allPossibleT5Improvements) { + const newDistances = [...distances]; + for (let i = 0; i < 6; i++) { + if (tuning.stats[i] > 0) { + newDistances[i] = Math.max(0, newDistances[i] - tuning.stats[i]); + } else if (tuning.stats[i] < 0) { + const newDistance = + config.minimumStatTiers[i as ArmorStat].value * 10 - (currentStats[i] + tuning.stats[i]); + newDistances[i] = Math.max(0, newDistance); + } + } + const result = get_mods_precalc( + config, + newDistances, + optionalDistances, + availableArtificeCount, + optimize + ); + if (result != null) { + return { + statMods: result, + tunings: tuning, + }; + } + } + return null; +} + +function get_mods_precalc( + config: BuildConfiguration, + distances: number[], + optionalDistances: number[], + availableArtificeCount: number, + optimize: ModOptimizationStrategy = ModOptimizationStrategy.None +): StatModifier[] | null { const modCombinations = config.onlyShowResultsWithNoWastedStats ? precalculatedZeroWasteModCombinations : precalculatedModCombinations; @@ -929,7 +1077,6 @@ function get_mods_precalc( modCombinations[distances[4]] || [[0, 0, 0, 0]], // intellect modCombinations[distances[5]] || [[0, 0, 0, 0]], // strength ]; - // we handle locked exact stats as zero-waste in terms of the mod selection for (let i = 0; i < 6; i++) { if (config.minimumStatTiers[i as ArmorStat].fixed && distances[i] > 0) { @@ -997,6 +1144,8 @@ function get_mods_precalc( return true; } + const totalDistance = + distances[0] + distances[1] + distances[2] + distances[3] + distances[4] + distances[5]; const mustExecuteOptimization = totalDistance > 0 && optimize != ModOptimizationStrategy.None; root: for (let mobility of precalculatedMods[0]) { if (!validate([mobility])) continue; From 357f5099cf331614b53528a7df7a53b3458518a9 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 7 Aug 2025 06:01:17 +0000 Subject: [PATCH 02/21] fix: allow secondary and tertiary stats in tuning --- src/app/services/results-builder.worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 76625a28..c17d350f 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -968,7 +968,7 @@ function get_mods_precalc_with_tuning( newBoosts.push({ stats: t5Boost, improvements: [archetypeStats] }); // TypeA) Add +5 to the specified stat - but applies -5 to one other stat. for (let j = 0; j < 6; j++) { - if (archetypeStats.includes(j)) continue; // Skip the archetype stat, we want to boost it + if (j == archetypeStats[0]) continue; // Skip the archetype stat, we want to boost it const t5Boost = [0, 0, 0, 0, 0, 0]; t5Boost[archetypeStats[0]] += 5; t5Boost[archetypeStats[j]] -= 5; From fbdd7fad208b2015da18f3431334bdb2a5e9334d Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 7 Aug 2025 08:12:37 +0000 Subject: [PATCH 03/21] feat: add support for tuning mods in expanded result content --- .../expanded-result-content.component.html | 16 ++++++++++++++++ .../expanded-result-content.component.ts | 3 +++ .../results/results.component.ts | 2 ++ src/app/data/types/IPermutatorArmorSet.ts | 4 +++- src/app/services/inventory.service.ts | 1 + src/app/services/results-builder.worker.ts | 7 ++++--- 6 files changed, 29 insertions(+), 4 deletions(-) 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 f6a81c4b..80d5d96c 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 @@ -272,6 +272,7 @@ {{ count }}×10 + Artifice Mods @@ -296,6 +297,21 @@ + + + + Tuning + + {{ + element!.tuning!.stats[stat] + }} + {{ + element!.tuning!.stats[stat] + }} + + + + Total 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 a099394d..ab06558d 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 @@ -22,6 +22,7 @@ import { Component, Input, OnDestroy, OnInit } from "@angular/core"; import { NGXLogger } from "ngx-logger"; import { ArmorStat, + ARMORSTAT_ORDER, ArmorStatIconUrls, ArmorStatNames, SpecialArmorStat, @@ -55,6 +56,8 @@ export class ExpandedResultContentComponent implements OnInit, OnDestroy { public ArmorStatNames = ArmorStatNames; public ArmorStatIconUrls = ArmorStatIconUrls; public ArmorStat = ArmorStat; + public ArmorStatOrder = ARMORSTAT_ORDER; + public StatModifier = StatModifier; public config_characterClass = DestinyClass.Unknown; public config_assumeLegendariesMasterworked = false; diff --git a/src/app/components/authenticated-v2/results/results.component.ts b/src/app/components/authenticated-v2/results/results.component.ts index 85c416dc..0fa7faca 100644 --- a/src/app/components/authenticated-v2/results/results.component.ts +++ b/src/app/components/authenticated-v2/results/results.component.ts @@ -32,6 +32,7 @@ import { Subject } from "rxjs"; import { takeUntil } from "rxjs/operators"; import { InventoryArmorSource } from "src/app/data/types/IInventoryArmor"; import { MAXIMUM_STAT_MOD_AMOUNT } from "src/app/data/constants"; +import { Tuning } from "src/app/data/types/IPermutatorArmorSet"; export interface ResultDefinition { exotic: @@ -53,6 +54,7 @@ export interface ResultDefinition { loaded: boolean; usesCollectionRoll?: boolean; usesVendorRoll?: boolean; + tuning?: Tuning; } export enum ResultItemMoveState { diff --git a/src/app/data/types/IPermutatorArmorSet.ts b/src/app/data/types/IPermutatorArmorSet.ts index 2b3c14e2..43c6d636 100644 --- a/src/app/data/types/IPermutatorArmorSet.ts +++ b/src/app/data/types/IPermutatorArmorSet.ts @@ -24,7 +24,8 @@ export function createArmorSet( usedArtifice: StatModifier[], usedMods: StatModifier[], statsWithMods: number[], - statsWithoutMods: number[] + statsWithoutMods: number[], + tuning: Tuning | undefined = undefined ): IPermutatorArmorSet { return { armor: [helmet.id, gauntlet.id, chest.id, leg.id, classItem.id], @@ -33,6 +34,7 @@ export function createArmorSet( usedMods, statsWithMods, statsWithoutMods, + tuning, }; } diff --git a/src/app/services/inventory.service.ts b/src/app/services/inventory.service.ts index 0d968a0e..c49c6e6d 100644 --- a/src/app/services/inventory.service.ts +++ b/src/app/services/inventory.service.ts @@ -557,6 +557,7 @@ export class InventoryService { (p, d: StatModifier) => p + STAT_MOD_VALUES[d][2], 0 ), + tuning: armorSet.tuning, mods: armorSet.usedMods, stats: armorSet.statsWithMods, statsNoMods: armorSet.statsWithoutMods, diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index c17d350f..6c4839c0 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -771,7 +771,7 @@ export function handlePermutation( performTierAvailabilityTesting( runtime, config, - adjustedStats, + tierTestingStats, newDistances, tmpArtificeCount, tierTestingTunings @@ -925,7 +925,8 @@ function tryCreateArmorSetWithClassItem( usedArtifice, usedMods, finalStats, - statsWithoutMods + statsWithoutMods, + tuning ); } @@ -971,7 +972,7 @@ function get_mods_precalc_with_tuning( if (j == archetypeStats[0]) continue; // Skip the archetype stat, we want to boost it const t5Boost = [0, 0, 0, 0, 0, 0]; t5Boost[archetypeStats[0]] += 5; - t5Boost[archetypeStats[j]] -= 5; + t5Boost[j] -= 5; newBoosts.push({ stats: t5Boost, improvements: [archetypeStats] }); } selectedT5Improvements.push(newBoosts); From dddfbad73fdba6e5c19de0cecf0ab32c6725c993 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 7 Aug 2025 08:37:55 +0000 Subject: [PATCH 04/21] fix: improve handling of fixed stat tiers in permutation calculations --- src/app/services/results-builder.worker.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 6c4839c0..fefd0247 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -582,8 +582,8 @@ export function handlePermutation( for (let n: ArmorStat = 0; n < 6; n++) { // Abort here if we are already above the limit, in case of fixed stat tiers - if (config.minimumStatTiers[n].fixed) { - if (stats[n] > config.minimumStatTiers[n].value * 10) return null; + if (config.minimumStatTiers[n].fixed && stats[n] > config.minimumStatTiers[n].value * 10) { + if (possibleT5Improvements.length == 0) return null; } } @@ -688,7 +688,8 @@ export function handlePermutation( for (let n: ArmorStat = 0; n < 6; n++) { // Abort here if we are already above the limit, in case of fixed stat tiers if (config.minimumStatTiers[n].fixed) { - if (adjustedStats[n] > config.minimumStatTiers[n].value * 10) return null; + if (adjustedStats[n] > config.minimumStatTiers[n].value * 10) + if (possibleT5Improvements.length == 0) return null; } } @@ -1030,11 +1031,19 @@ function get_mods_precalc_with_tuning( return sumB - sumA; }); - for (const tuning of allPossibleT5Improvements) { + tuningPicking: for (const tuning of allPossibleT5Improvements) { const newDistances = [...distances]; for (let i = 0; i < 6; i++) { if (tuning.stats[i] > 0) { newDistances[i] = Math.max(0, newDistances[i] - tuning.stats[i]); + if (config.minimumStatTiers[i as ArmorStat].fixed) { + if ( + currentStats[i] + tuning.stats[i] > + config.minimumStatTiers[i as ArmorStat].value * 10 + ) { + continue tuningPicking; + } + } } else if (tuning.stats[i] < 0) { const newDistance = config.minimumStatTiers[i as ArmorStat].value * 10 - (currentStats[i] + tuning.stats[i]); From 1e49ea6a1255b48fac768076d91a5a93c76c5378 Mon Sep 17 00:00:00 2001 From: Mijago Date: Thu, 7 Aug 2025 20:48:23 +0200 Subject: [PATCH 05/21] feat: retrieve tuningStatHash in BungieApiService --- src/app/data/types/IInventoryArmor.ts | 1 + src/app/data/types/IPermutatorArmor.ts | 1 + src/app/services/bungie-api.service.ts | 23 +++++++++++++++++++++++ src/app/services/inventory.service.ts | 1 + 4 files changed, 26 insertions(+) diff --git a/src/app/data/types/IInventoryArmor.ts b/src/app/data/types/IInventoryArmor.ts index 3e13a1dc..262bb2c0 100644 --- a/src/app/data/types/IInventoryArmor.ts +++ b/src/app/data/types/IInventoryArmor.ts @@ -63,6 +63,7 @@ export interface IInventoryArmor ITimestampedEntry { // Note: this will be empty for vendor items statPlugHashes?: (number | undefined)[]; + tuningStatHash?: number; // for armor 3.0, this is the tuning stat hash // exoticPerkHash is now inherited as number[] from IManifestArmor } diff --git a/src/app/data/types/IPermutatorArmor.ts b/src/app/data/types/IPermutatorArmor.ts index 01e20545..a5a20f4a 100644 --- a/src/app/data/types/IPermutatorArmor.ts +++ b/src/app/data/types/IPermutatorArmor.ts @@ -9,4 +9,5 @@ export interface IPermutatorArmor extends IDestinyArmor { rarity: TierType; isSunset: boolean; exoticPerkHash: number[]; + tuningStatHash?: number; // for armor 3.0, this is the tuning stat hash } diff --git a/src/app/services/bungie-api.service.ts b/src/app/services/bungie-api.service.ts index 3911ccec..029bce45 100644 --- a/src/app/services/bungie-api.service.ts +++ b/src/app/services/bungie-api.service.ts @@ -320,6 +320,7 @@ export class BungieApiService { DestinyComponentType.ItemPerks, DestinyComponentType.ItemSockets, DestinyComponentType.ItemPlugStates, + DestinyComponentType.ItemReusablePlugs, DestinyComponentType.Collectibles, ], membershipType: destinyMembership.membershipType, @@ -410,6 +411,28 @@ export class BungieApiService { if (!!(instance as any).gearTier) { armorItem.armorSystem = ArmorSystem.Armor3; armorItem.tier = (instance as any).gearTier; + + // Grab the tuning stat from the reusable plugs + try { + const plugs = + profile.Response.itemComponents.reusablePlugs.data?.[d.itemInstanceId!]?.plugs; + if (plugs) { + // TODO: remove the hardcoding of 11 and 2 + const modCheck = plugs[11][2].plugItemHash; + plugs[11][2].plugItemHash; + // Find the index of the first investment stat with value > 0 + const tuningStat = modsMap[modCheck].investmentStats.find( + (p) => p.value > 0 + )?.statTypeHash; + armorItem.tuningStatHash = tuningStat; + } + } catch (e) { + this.logger.error( + "BungieApiService", + "updateArmorItems", + `Error while getting tuning stat for item ${d.itemInstanceId}: ${e}` + ); + } } else if (armorItem.isExotic && armorItem.slot === ArmorSlot.ArmorSlotClass) { armorItem.armorSystem = ArmorSystem.Armor3; } else { diff --git a/src/app/services/inventory.service.ts b/src/app/services/inventory.service.ts index 0d968a0e..e4bcd8ef 100644 --- a/src/app/services/inventory.service.ts +++ b/src/app/services/inventory.service.ts @@ -462,6 +462,7 @@ export class InventoryService { exoticPerkHash: armor.exoticPerkHash, gearSetHash: armor.gearSetHash ?? null, + tuningStatHash: armor.tuningStatHash, icon: armor.icon, watermarkIcon: armor.watermarkIcon, From 13c0bf1bc39bf3b1f587f8c3afb746581814793c Mon Sep 17 00:00:00 2001 From: Mijago Date: Thu, 7 Aug 2025 20:50:49 +0200 Subject: [PATCH 06/21] feat: enhance tuning information structure in IPermutatorArmorSet --- src/app/data/types/IPermutatorArmorSet.ts | 8 +++-- src/app/services/results-builder.worker.ts | 35 +++++++++++++--------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/app/data/types/IPermutatorArmorSet.ts b/src/app/data/types/IPermutatorArmorSet.ts index 2b3c14e2..b9516de6 100644 --- a/src/app/data/types/IPermutatorArmorSet.ts +++ b/src/app/data/types/IPermutatorArmorSet.ts @@ -1,9 +1,13 @@ -import { StatModifier } from "../enum/armor-stat"; +import { ArmorStat, StatModifier } from "../enum/armor-stat"; import { IPermutatorArmor } from "./IPermutatorArmor"; +export type PossibleTuningInformation = { + tuningStatHash: number; + archetypeStats: ArmorStat[]; +}; export interface Tuning { stats: number[]; - improvements: number[][]; + improvements: PossibleTuningInformation[]; } export interface IPermutatorArmorSet { armor: number[]; diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 76625a28..922b8f40 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -41,6 +41,7 @@ import { IPermutatorArmorSet, createArmorSet, isIPermutatorArmorSet, + PossibleTuningInformation, } from "../data/types/IPermutatorArmorSet"; import { ArmorSystem } from "../data/types/IManifestArmor"; // endregion Import @@ -568,9 +569,12 @@ export function handlePermutation( for (let item of items) applyMasterworkStats(item, config, stats); - const possibleT5Improvements: ArmorStat[][] = items + const possibleT5Improvements: PossibleTuningInformation[] = items .filter((i) => i.armorSystem == ArmorSystem.Armor3 && i.tier == 5) - .map((i) => i.archetypeStats); + .map((i) => ({ + tuningStatHash: i.tuningStatHash ?? 0, + archetypeStats: i.archetypeStats || [], + })); const statsWithoutMods = [stats[0], stats[1], stats[2], stats[3], stats[4], stats[5]]; stats[0] += constantBonus[0]; @@ -760,8 +764,11 @@ export function handlePermutation( } for (let tuning of result.tunings.improvements) { - const index = tierTestingTunings.findIndex((improvement) => - improvement.every((val, idx) => val === tuning[idx]) + const index = tierTestingTunings.findIndex( + (possibleTuning) => + possibleTuning.archetypeStats.every( + (val, idx) => val === tuning.archetypeStats[idx] + ) && possibleTuning.tuningStatHash === tuning.tuningStatHash ); if (index !== -1) { tierTestingTunings.splice(index, 1); @@ -809,7 +816,7 @@ function performTierAvailabilityTesting( stats: number[], distances: number[], availableArtificeCount: number, - possibleT5Improvements: ArmorStat[][] + possibleT5Improvements: PossibleTuningInformation[] ): void { for (let stat = 0; stat < 6; stat++) { if (runtime.maximumPossibleTiers[stat] < stats[stat]) { @@ -936,7 +943,7 @@ function get_mods_precalc_with_tuning( distances: number[], optionalDistances: number[], availableArtificeCount: number, - possibleT5Improvements: ArmorStat[][], + possibleT5Improvements: PossibleTuningInformation[], optimize: ModOptimizationStrategy = ModOptimizationStrategy.None ): StatModCalculationResult | null { // check distances <= 65 @@ -952,27 +959,27 @@ function get_mods_precalc_with_tuning( let selectedT5Improvements: Tuning[][] = []; if (possibleT5Improvements.length > 0) { possibleT5Improvements = possibleT5Improvements.filter( - (archetypeStats) => distances[archetypeStats[0]] > 0 + (possibleTuning) => distances[possibleTuning.tuningStatHash] > 0 ); for (let i = 0; i < possibleT5Improvements.length; i++) { const newBoosts: Tuning[] = []; - const archetypeStats = possibleT5Improvements[i]; + const possibleTuning = possibleT5Improvements[i]; // TypeB) Add +1 to the three stats that are not in the archetypeStats (that also receive the +5 masterwork bonus) // We can only use this if the distance is > 0, otherwise we would not need any mods const t5Boost = [0, 0, 0, 0, 0, 0]; for (let j = 0; j < 6; j++) { - if (!archetypeStats.includes(j)) { + if (!possibleTuning.archetypeStats.includes(j)) { t5Boost[j] += 1; } } - newBoosts.push({ stats: t5Boost, improvements: [archetypeStats] }); + newBoosts.push({ stats: t5Boost, improvements: [possibleTuning] }); // TypeA) Add +5 to the specified stat - but applies -5 to one other stat. for (let j = 0; j < 6; j++) { - if (archetypeStats.includes(j)) continue; // Skip the archetype stat, we want to boost it + if (j == possibleTuning.tuningStatHash) continue; // Skip the archetype stat, we want to boost it const t5Boost = [0, 0, 0, 0, 0, 0]; - t5Boost[archetypeStats[0]] += 5; - t5Boost[archetypeStats[j]] -= 5; - newBoosts.push({ stats: t5Boost, improvements: [archetypeStats] }); + t5Boost[possibleTuning.tuningStatHash] += 5; + t5Boost[j] -= 5; + newBoosts.push({ stats: t5Boost, improvements: [possibleTuning] }); } selectedT5Improvements.push(newBoosts); } From 61a99b92ab978ed8825f3930a684809243bde968 Mon Sep 17 00:00:00 2001 From: Mijago Date: Thu, 7 Aug 2025 23:01:05 +0200 Subject: [PATCH 07/21] feat: enhance tuning stat handling --- src/app/services/bungie-api.service.ts | 20 ++-- src/app/services/results-builder.worker.ts | 116 ++++++++++++--------- 2 files changed, 81 insertions(+), 55 deletions(-) diff --git a/src/app/services/bungie-api.service.ts b/src/app/services/bungie-api.service.ts index 029bce45..966e2411 100644 --- a/src/app/services/bungie-api.service.ts +++ b/src/app/services/bungie-api.service.ts @@ -408,6 +408,7 @@ export class BungieApiService { // 3.0 // TODO replace the (as any) once DIM Api is updated + armorItem.tuningStatHash = 1; if (!!(instance as any).gearTier) { armorItem.armorSystem = ArmorSystem.Armor3; armorItem.tier = (instance as any).gearTier; @@ -417,14 +418,17 @@ export class BungieApiService { const plugs = profile.Response.itemComponents.reusablePlugs.data?.[d.itemInstanceId!]?.plugs; if (plugs) { - // TODO: remove the hardcoding of 11 and 2 - const modCheck = plugs[11][2].plugItemHash; - plugs[11][2].plugItemHash; - // Find the index of the first investment stat with value > 0 - const tuningStat = modsMap[modCheck].investmentStats.find( - (p) => p.value > 0 - )?.statTypeHash; - armorItem.tuningStatHash = tuningStat; + const idx = Object.values(plugs).findIndex((value) => { + return value.length > 1 && value[0].plugItemHash == 3122197216; // 3122197216 is the balanced tuning stat + }); + if (idx >= 0) { + const modCheck = plugs[idx][2].plugItemHash; + // Find the index of the first investment stat with value > 0 + const tuningStat = modsMap[modCheck].investmentStats.find( + (p) => p.value > 0 + )?.statTypeHash; + armorItem.tuningStatHash = tuningStat; + } } } catch (e) { this.logger.error( diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 2ae5847f..20941ffc 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -570,9 +570,11 @@ export function handlePermutation( for (let item of items) applyMasterworkStats(item, config, stats); const possibleT5Improvements: PossibleTuningInformation[] = items - .filter((i) => i.armorSystem == ArmorSystem.Armor3 && i.tier == 5) + .filter( + (i) => i.armorSystem == ArmorSystem.Armor3 && i.tuningStatHash != undefined && i.tier == 5 + ) .map((i) => ({ - tuningStatHash: i.tuningStatHash ?? 0, + tuningStatHash: i.tuningStatHash!, archetypeStats: i.archetypeStats || [], })); @@ -741,8 +743,8 @@ export function handlePermutation( const newDistanceSum = newDistances.reduce((a, b) => a + b, 0); const newTotalOptionalDistances = newOptionalDistances.reduce((a, b) => a + b, 0); - let result: StatModCalculationResult | null; - if (newDistanceSum == 0 && newTotalOptionalDistances == 0) result = { statMods: [] }; + let result: StatModCalculationResult[] | null; + if (newDistanceSum == 0 && newTotalOptionalDistances == 0) result = [{ statMods: [] }]; else result = get_mods_precalc_with_tuning( config, @@ -751,39 +753,45 @@ export function handlePermutation( newOptionalDistances, tmpArtificeCount, possibleT5Improvements, - config.modOptimizationStrategy + config.modOptimizationStrategy, + true ); if (result !== null) { - // Perform Tier Availability Testing with this class item - const tierTestingStats = [...adjustedStats]; - const tierTestingTunings = [...possibleT5Improvements]; - // Add tuning - if (result.tunings) { - for (let n = 0; n < 6; n++) { - tierTestingStats[n] += result.tunings.stats[n]; - } + for (let rsst of result) { + // Perform Tier Availability Testing with this class item + const tierTestingStats = [...adjustedStats]; + const tierTestingTunings = [...possibleT5Improvements]; + //* + // Add tuning + if (rsst.tunings) { + for (let n = 0; n < 6; n++) { + tierTestingStats[n] += rsst.tunings.stats[n]; + } - for (let tuning of result.tunings.improvements) { - const index = tierTestingTunings.findIndex( - (possibleTuning) => - possibleTuning.archetypeStats.every( - (val, idx) => val === tuning.archetypeStats[idx] - ) && possibleTuning.tuningStatHash === tuning.tuningStatHash - ); - if (index !== -1) { - tierTestingTunings.splice(index, 1); + for (let tuning of rsst.tunings.improvements) { + const index = tierTestingTunings.findIndex( + (possibleTuning) => + possibleTuning.archetypeStats.every( + (val, idx) => val === tuning.archetypeStats[idx] + ) && possibleTuning.tuningStatHash === tuning.tuningStatHash + ); + if (index !== -1) { + tierTestingTunings.splice(index, 1); + } } } + //*/ + performTierAvailabilityTesting( + runtime, + config, + tierTestingStats, + newDistances, + tmpArtificeCount, + tierTestingTunings + ); + //*/ } - performTierAvailabilityTesting( - runtime, - config, - tierTestingStats, - newDistances, - tmpArtificeCount, - tierTestingTunings - ); // This may lead to issues later. // The performTierAvailabilityTesting must be executed for each class item. @@ -797,11 +805,11 @@ export function handlePermutation( chest, leg, classItem, - result.statMods, + result[0].statMods, adjustedStats, adjustedStatsWithoutMods, doNotOutput, - result.tunings + result[0].tunings ); } } @@ -855,7 +863,8 @@ function performTierAvailabilityTesting( [0, 0, 0, 0, 0, 0], availableArtificeCount, possibleT5Improvements, - ModOptimizationStrategy.None + ModOptimizationStrategy.None, + false ); if (mods != null) { @@ -880,7 +889,8 @@ function performTierAvailabilityTesting( [0, 0, 0, 0, 0, 0], availableArtificeCount, possibleT5Improvements, - ModOptimizationStrategy.None + ModOptimizationStrategy.None, + false ); if (mods != null) { runtime.maximumPossibleTiers[stat] = low; @@ -946,26 +956,26 @@ function get_mods_precalc_with_tuning( optionalDistances: number[], availableArtificeCount: number, possibleT5Improvements: PossibleTuningInformation[], - optimize: ModOptimizationStrategy = ModOptimizationStrategy.None -): StatModCalculationResult | null { - // check distances <= 65 + optimize: ModOptimizationStrategy = ModOptimizationStrategy.None, + checkAllTunings = false +): StatModCalculationResult[] | null { const totalDistance = distances[0] + distances[1] + distances[2] + distances[3] + distances[4] + distances[5]; if (totalDistance > 65 + 5 * possibleT5Improvements.length) return null; if (totalDistance == 0 && optionalDistances.every((d) => d == 0)) { // no mods needed, return empty array - return { statMods: [] }; + return [{ statMods: [] }]; } let selectedT5Improvements: Tuning[][] = []; if (possibleT5Improvements.length > 0) { - possibleT5Improvements = possibleT5Improvements.filter( + const tmpPossibleT5Improvements = possibleT5Improvements.filter( (possibleTuning) => distances[possibleTuning.tuningStatHash] > 0 ); - for (let i = 0; i < possibleT5Improvements.length; i++) { + for (let i = 0; i < tmpPossibleT5Improvements.length; i++) { const newBoosts: Tuning[] = []; - const possibleTuning = possibleT5Improvements[i]; + const possibleTuning = tmpPossibleT5Improvements[i]; // TypeB) Add +1 to the three stats that are not in the archetypeStats (that also receive the +5 masterwork bonus) // We can only use this if the distance is > 0, otherwise we would not need any mods const t5Boost = [0, 0, 0, 0, 0, 0]; @@ -1038,6 +1048,7 @@ function get_mods_precalc_with_tuning( return sumB - sumA; }); + const usableTunings = []; tuningPicking: for (const tuning of allPossibleT5Improvements) { const newDistances = [...distances]; for (let i = 0; i < 6; i++) { @@ -1052,9 +1063,12 @@ function get_mods_precalc_with_tuning( } } } else if (tuning.stats[i] < 0) { - const newDistance = - config.minimumStatTiers[i as ArmorStat].value * 10 - (currentStats[i] + tuning.stats[i]); - newDistances[i] = Math.max(0, newDistance); + if ( + currentStats[i] + newDistances[i] + tuning.stats[i] < + config.minimumStatTiers[i as ArmorStat].value * 10 + ) { + newDistances[i] = Math.max(0, Math.abs(tuning.stats[i])); + } } } const result = get_mods_precalc( @@ -1065,13 +1079,21 @@ function get_mods_precalc_with_tuning( optimize ); if (result != null) { - return { + usableTunings.push({ statMods: result, tunings: tuning, - }; + }); + if (!checkAllTunings) { + // If we only want to check one tuning, we can stop here + break tuningPicking; + } } } - return null; + if (usableTunings.length == 0) { + // No usable tunings found, return null + return null; + } + return usableTunings; } function get_mods_precalc( From b453c553f590e3ef78e58c31d99a6534c064587d Mon Sep 17 00:00:00 2001 From: Mijago Date: Fri, 8 Aug 2025 08:26:46 +0200 Subject: [PATCH 08/21] fix: improve tuning stat assignment logic in BungieApiService --- src/app/services/bungie-api.service.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app/services/bungie-api.service.ts b/src/app/services/bungie-api.service.ts index 966e2411..f94bc381 100644 --- a/src/app/services/bungie-api.service.ts +++ b/src/app/services/bungie-api.service.ts @@ -408,7 +408,6 @@ export class BungieApiService { // 3.0 // TODO replace the (as any) once DIM Api is updated - armorItem.tuningStatHash = 1; if (!!(instance as any).gearTier) { armorItem.armorSystem = ArmorSystem.Armor3; armorItem.tier = (instance as any).gearTier; @@ -418,16 +417,19 @@ export class BungieApiService { const plugs = profile.Response.itemComponents.reusablePlugs.data?.[d.itemInstanceId!]?.plugs; if (plugs) { - const idx = Object.values(plugs).findIndex((value) => { - return value.length > 1 && value[0].plugItemHash == 3122197216; // 3122197216 is the balanced tuning stat + const availablePlugs = Object.values(plugs).find((value) => { + return value.length > 1 && value.some((p) => p.plugItemHash == 3122197216); // 3122197216 is the balanced tuning stat }); - if (idx >= 0) { - const modCheck = plugs[idx][2].plugItemHash; - // Find the index of the first investment stat with value > 0 - const tuningStat = modsMap[modCheck].investmentStats.find( - (p) => p.value > 0 - )?.statTypeHash; - armorItem.tuningStatHash = tuningStat; + + if (availablePlugs && availablePlugs.length > 1) { + const pickedPlug = availablePlugs.find((p) => p.plugItemHash != 3122197216); + if (pickedPlug) { + const statCheckHash = pickedPlug.plugItemHash; + const mod = modsMap[statCheckHash]; + armorItem.tuningStatHash = mod?.investmentStats.find( + (p) => p.value > 0 + )?.statTypeHash; + } } } } catch (e) { From 4f72261c0cc374c1015002c061ceb08d1e34ad46 Mon Sep 17 00:00:00 2001 From: Mijago Date: Fri, 8 Aug 2025 10:12:39 +0200 Subject: [PATCH 09/21] fix: tier availability can now handle 0waste again --- src/app/services/results-builder.worker.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 20941ffc..f7e7a7ae 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -827,14 +827,18 @@ function performTierAvailabilityTesting( availableArtificeCount: number, possibleT5Improvements: PossibleTuningInformation[] ): void { + const stepSize = config.onlyShowResultsWithNoWastedStats ? 10 : 1; for (let stat = 0; stat < 6; stat++) { if (runtime.maximumPossibleTiers[stat] < stats[stat]) { - runtime.maximumPossibleTiers[stat] = stats[stat]; + if (!config.onlyShowResultsWithNoWastedStats || stats[stat] % 10 == 0) + runtime.maximumPossibleTiers[stat] = stats[stat]; } if (stats[stat] >= 200) continue; // Already at max value, no need to test - const minTier = config.minimumStatTiers[stat as ArmorStat].value * 10; + const minTier = config.onlyShowResultsWithNoWastedStats + ? Math.floor(config.minimumStatTiers[stat as ArmorStat].value) * 10 + : config.minimumStatTiers[stat as ArmorStat].value * 10; // Binary search to find maximum possible value let low = Math.max(runtime.maximumPossibleTiers[stat], minTier); @@ -842,11 +846,14 @@ function performTierAvailabilityTesting( while (low < high) { // Try middle value, rounded to nearest 10 for tier optimization - const mid = Math.min(200, Math.ceil((low + high) / 2)); + let mid = Math.min(200, Math.ceil((low + high) / 2)); + if (config.onlyShowResultsWithNoWastedStats) { + mid = Math.ceil(mid / stepSize) * stepSize; // Round to nearest step size + } if (stats[stat] >= mid) { // We can already reach this value naturally - low = mid + 1; + low = mid + stepSize; continue; } @@ -869,11 +876,11 @@ function performTierAvailabilityTesting( if (mods != null) { // This value is achievable, try higher - low = mid + 1; + low = mid + stepSize; runtime.maximumPossibleTiers[stat] = mid; } else { // This value is not achievable, try lower - high = mid - 1; + high = mid - stepSize; } } From eaa565e5940ee51a522c255d6f32662f21fedd79 Mon Sep 17 00:00:00 2001 From: Mijago Date: Fri, 8 Aug 2025 12:11:06 +0200 Subject: [PATCH 10/21] feat: refactor tuning stat handling to use ArmorStat --- src/app/data/enum/armor-stat.ts | 9 +++++++++ src/app/data/types/IInventoryArmor.ts | 2 +- src/app/data/types/IPermutatorArmor.ts | 4 ++-- src/app/data/types/IPermutatorArmorSet.ts | 2 +- src/app/services/bungie-api.service.ts | 4 +++- src/app/services/inventory.service.ts | 2 +- src/app/services/results-builder.worker.ts | 14 ++++++-------- 7 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/app/data/enum/armor-stat.ts b/src/app/data/enum/armor-stat.ts index 4252ae18..dcac0edb 100644 --- a/src/app/data/enum/armor-stat.ts +++ b/src/app/data/enum/armor-stat.ts @@ -100,6 +100,15 @@ export const ArmorStatHashes: EnumDictionary = { [ArmorStat.StatMelee]: 4244567218, }; +export const ArmorStatFromHash: EnumDictionary = { + [ArmorStatHashes[ArmorStat.StatWeapon]]: ArmorStat.StatWeapon, + [ArmorStatHashes[ArmorStat.StatHealth]]: ArmorStat.StatHealth, + [ArmorStatHashes[ArmorStat.StatClass]]: ArmorStat.StatClass, + [ArmorStatHashes[ArmorStat.StatGrenade]]: ArmorStat.StatGrenade, + [ArmorStatHashes[ArmorStat.StatSuper]]: ArmorStat.StatSuper, + [ArmorStatHashes[ArmorStat.StatMelee]]: ArmorStat.StatMelee, +}; + export const ArmorStatIconUrls: EnumDictionary = { [ArmorStat.StatWeapon]: "https://www.bungie.net/common/destiny2_content/icons/bc69675acdae9e6b9a68a02fb4d62e07.png", diff --git a/src/app/data/types/IInventoryArmor.ts b/src/app/data/types/IInventoryArmor.ts index 262bb2c0..8a6be06e 100644 --- a/src/app/data/types/IInventoryArmor.ts +++ b/src/app/data/types/IInventoryArmor.ts @@ -63,7 +63,7 @@ export interface IInventoryArmor ITimestampedEntry { // Note: this will be empty for vendor items statPlugHashes?: (number | undefined)[]; - tuningStatHash?: number; // for armor 3.0, this is the tuning stat hash + tuningStat?: ArmorStat; // for armor 3.0, this is the tuning stat hash // exoticPerkHash is now inherited as number[] from IManifestArmor } diff --git a/src/app/data/types/IPermutatorArmor.ts b/src/app/data/types/IPermutatorArmor.ts index a5a20f4a..d71af71c 100644 --- a/src/app/data/types/IPermutatorArmor.ts +++ b/src/app/data/types/IPermutatorArmor.ts @@ -1,5 +1,5 @@ import { DestinyClass, TierType } from "bungie-api-ts/destiny2"; -import { ArmorPerkOrSlot } from "../enum/armor-stat"; +import { ArmorPerkOrSlot, ArmorStat } from "../enum/armor-stat"; import { IDestinyArmor } from "./IInventoryArmor"; export interface IPermutatorArmor extends IDestinyArmor { @@ -9,5 +9,5 @@ export interface IPermutatorArmor extends IDestinyArmor { rarity: TierType; isSunset: boolean; exoticPerkHash: number[]; - tuningStatHash?: number; // for armor 3.0, this is the tuning stat hash + tuningStat?: ArmorStat; // for armor 3.0, this is the tuning stat hash } diff --git a/src/app/data/types/IPermutatorArmorSet.ts b/src/app/data/types/IPermutatorArmorSet.ts index 63598120..72957d4d 100644 --- a/src/app/data/types/IPermutatorArmorSet.ts +++ b/src/app/data/types/IPermutatorArmorSet.ts @@ -2,7 +2,7 @@ import { ArmorStat, StatModifier } from "../enum/armor-stat"; import { IPermutatorArmor } from "./IPermutatorArmor"; export type PossibleTuningInformation = { - tuningStatHash: number; + tuningStat: ArmorStat; archetypeStats: ArmorStat[]; }; export interface Tuning { diff --git a/src/app/services/bungie-api.service.ts b/src/app/services/bungie-api.service.ts index f94bc381..7ff013fe 100644 --- a/src/app/services/bungie-api.service.ts +++ b/src/app/services/bungie-api.service.ts @@ -51,6 +51,7 @@ import { ArmorPerkOrSlot, ArmorPerkSocketHashes, ArmorStat, + ArmorStatFromHash, ArmorStatHashes, MapAlternativeSocketTypeToArmorPerkOrSlot, MapAlternativeToArmorPerkOrSlot, @@ -426,9 +427,10 @@ export class BungieApiService { if (pickedPlug) { const statCheckHash = pickedPlug.plugItemHash; const mod = modsMap[statCheckHash]; - armorItem.tuningStatHash = mod?.investmentStats.find( + const tuningStatHash = mod?.investmentStats.find( (p) => p.value > 0 )?.statTypeHash; + if (tuningStatHash) armorItem.tuningStat = ArmorStatFromHash[tuningStatHash]; } } } diff --git a/src/app/services/inventory.service.ts b/src/app/services/inventory.service.ts index 57e9fce0..440b6fc6 100644 --- a/src/app/services/inventory.service.ts +++ b/src/app/services/inventory.service.ts @@ -462,7 +462,7 @@ export class InventoryService { exoticPerkHash: armor.exoticPerkHash, gearSetHash: armor.gearSetHash ?? null, - tuningStatHash: armor.tuningStatHash, + tuningStat: armor.tuningStat, icon: armor.icon, watermarkIcon: armor.watermarkIcon, diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index f7e7a7ae..6e8e5e1d 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -570,11 +570,9 @@ export function handlePermutation( for (let item of items) applyMasterworkStats(item, config, stats); const possibleT5Improvements: PossibleTuningInformation[] = items - .filter( - (i) => i.armorSystem == ArmorSystem.Armor3 && i.tuningStatHash != undefined && i.tier == 5 - ) + .filter((i) => i.armorSystem == ArmorSystem.Armor3 && i.tuningStat != undefined && i.tier == 5) .map((i) => ({ - tuningStatHash: i.tuningStatHash!, + tuningStat: i.tuningStat!, archetypeStats: i.archetypeStats || [], })); @@ -774,7 +772,7 @@ export function handlePermutation( (possibleTuning) => possibleTuning.archetypeStats.every( (val, idx) => val === tuning.archetypeStats[idx] - ) && possibleTuning.tuningStatHash === tuning.tuningStatHash + ) && possibleTuning.tuningStat === tuning.tuningStat ); if (index !== -1) { tierTestingTunings.splice(index, 1); @@ -978,7 +976,7 @@ function get_mods_precalc_with_tuning( let selectedT5Improvements: Tuning[][] = []; if (possibleT5Improvements.length > 0) { const tmpPossibleT5Improvements = possibleT5Improvements.filter( - (possibleTuning) => distances[possibleTuning.tuningStatHash] > 0 + (possibleTuning) => distances[possibleTuning.tuningStat] > 0 ); for (let i = 0; i < tmpPossibleT5Improvements.length; i++) { const newBoosts: Tuning[] = []; @@ -994,9 +992,9 @@ function get_mods_precalc_with_tuning( newBoosts.push({ stats: t5Boost, improvements: [possibleTuning] }); // TypeA) Add +5 to the specified stat - but applies -5 to one other stat. for (let j = 0; j < 6; j++) { - if (j == possibleTuning.tuningStatHash) continue; // Skip the archetype stat, we want to boost it + if (j == possibleTuning.tuningStat) continue; // Skip the archetype stat, we want to boost it const t5Boost = [0, 0, 0, 0, 0, 0]; - t5Boost[possibleTuning.tuningStatHash] += 5; + t5Boost[possibleTuning.tuningStat] += 5; t5Boost[j] -= 5; newBoosts.push({ stats: t5Boost, improvements: [possibleTuning] }); } From bc99659be4138aefee41608cc057481fbbf86d5f Mon Sep 17 00:00:00 2001 From: Mijago Date: Fri, 8 Aug 2025 13:38:12 +0200 Subject: [PATCH 11/21] fix: optimize tuning stat filtering - SLOW!! --- src/app/services/results-builder.worker.ts | 59 +++++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/src/app/services/results-builder.worker.ts b/src/app/services/results-builder.worker.ts index 6e8e5e1d..91450b73 100644 --- a/src/app/services/results-builder.worker.ts +++ b/src/app/services/results-builder.worker.ts @@ -975,9 +975,8 @@ function get_mods_precalc_with_tuning( let selectedT5Improvements: Tuning[][] = []; if (possibleT5Improvements.length > 0) { - const tmpPossibleT5Improvements = possibleT5Improvements.filter( - (possibleTuning) => distances[possibleTuning.tuningStat] > 0 - ); + const tmpPossibleT5Improvements = possibleT5Improvements; + //possibleT5Improvements.filter((possibleTuning) => distances[possibleTuning.tuningStat] > 0); for (let i = 0; i < tmpPossibleT5Improvements.length; i++) { const newBoosts: Tuning[] = []; const possibleTuning = tmpPossibleT5Improvements[i]; @@ -1053,6 +1052,27 @@ function get_mods_precalc_with_tuning( return sumB - sumA; }); + // drop duplicates + allPossibleT5Improvements = allPossibleT5Improvements.filter( + (value, index, self) => + index === + self.findIndex( + (tuning) => + tuning.stats[0] === value.stats[0] && + tuning.stats[1] === value.stats[1] && + tuning.stats[2] === value.stats[2] && + tuning.stats[3] === value.stats[3] && + tuning.stats[4] === value.stats[4] && + tuning.stats[5] === value.stats[5] + ) + ); + + // apply smart filtering + allPossibleT5Improvements = allPossibleT5Improvements.filter( + (tuning) => + tuning.stats.some((stat) => stat > 0) || // At least one stat is boosted + tuning.stats.every((stat) => stat == 0) // or all stats are zero (this is the case for the empty tuning) + ); const usableTunings = []; tuningPicking: for (const tuning of allPossibleT5Improvements) { const newDistances = [...distances]; @@ -1068,11 +1088,36 @@ function get_mods_precalc_with_tuning( } } } else if (tuning.stats[i] < 0) { - if ( - currentStats[i] + newDistances[i] + tuning.stats[i] < - config.minimumStatTiers[i as ArmorStat].value * 10 + const absValue = Math.abs(tuning.stats[i]); + // I have 59 + // I want 60 + // Distance is 1 -> just add the 5 to the distance + if (newDistances[i] > 0) { + newDistances[i] = Math.max(0, newDistances[i] + absValue); + } else if ( + newDistances[i] == 0 && + config.minimumStatTiers[i as ArmorStat].value * 10 == currentStats[i] ) { - newDistances[i] = Math.max(0, Math.abs(tuning.stats[i])); + // I have 60 + // I want 60 + // Distance is 0 -> just add the 5 to the distance + newDistances[i] = Math.max(0, absValue); + } else if ( + newDistances[i] == 0 && + config.minimumStatTiers[i as ArmorStat].value * 10 < currentStats[i] + ) { + const subDist = currentStats[i] - config.minimumStatTiers[i as ArmorStat].value * 10; + if (subDist >= absValue) { + // I have 65 + // I want 60 + // Distance is 0 and subdist >= 5 -> no not add the 5 to the distance + newDistances[i] = 0; + } else { + // I have 63 + // I want 60 + // Distance is 0 and subdist < 5 -> add the 2 to the distance + newDistances[i] = Math.max(0, absValue - subDist); + } } } } From 922beb8ceb32c3e2d75a198de59bd8e12bebf355 Mon Sep 17 00:00:00 2001 From: Mijago Date: Fri, 8 Aug 2025 18:08:41 +0200 Subject: [PATCH 12/21] feat: add tuning affinity display to result table --- .../expanded-result-content.component.html | 8 ++++++++ .../authenticated-v2/results/results.component.ts | 1 + src/app/services/inventory.service.ts | 1 + 3 files changed, 10 insertions(+) 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 80d5d96c..4cac6264 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 @@ -102,6 +102,14 @@ {{ i.masterworkLevel }} + + + + + + + +