From ab570c567fc39bde93316958a974b19633f9c1cc Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 14:01:29 +0100 Subject: [PATCH 01/11] Turn Include Font mod stats into a LoadoutParameter --- config/i18n.json | 3 ++ src/@types/dim-api-types.ts | 7 +++ src/app/dim-api/selectors.ts | 3 +- src/app/loadout-analyzer/analysis.ts | 47 ++++++++++++++----- src/app/loadout-analyzer/hooks.tsx | 5 +- src/app/loadout-analyzer/types.ts | 3 +- src/app/loadout-builder/LoadoutBuilder.tsx | 8 +++- .../generated-sets/GeneratedSet.tsx | 2 +- .../loadout-builder-reducer.ts | 11 +++-- src/app/loadout-builder/loadout-params.ts | 12 ++++- .../loadout-drawer/loadout-drawer-reducer.ts | 6 +++ src/app/loadout-drawer/loadout-utils.ts | 3 +- src/app/loadout/LoadoutView.tsx | 16 +++++-- src/app/loadout/loadout-edit/LoadoutEdit.tsx | 10 ++++ src/app/loadout/loadout-ui/LoadoutMods.tsx | 22 ++++++++- .../loadout-ui/LoadoutParametersDisplay.tsx | 13 ++++- src/app/loadout/stats.ts | 7 +++ src/app/store-stats/CharacterStats.tsx | 9 +++- src/locale/en.json | 3 ++ 19 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 src/@types/dim-api-types.ts diff --git a/config/i18n.json b/config/i18n.json index 833e8534d2..1abaa87a49 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -687,6 +687,8 @@ "Error404": "This loadout doesn't exist.", "Error": "Error getting loadout:" }, + "IncludeRuntimeStatBenefits": "Include Font mod stats", + "IncludeRuntimeStatBenefitsDesc": "\"Font of ...\" armor mods provide a flat boost to character stats while you have Armor Charges.\n\nWith this setting, DIM considers these mods active and adds their benefits to this Loadout's stats in calculations and optimizations.", "InGameLoadouts": "In-Game Loadouts", "ItemLeveling": "Item Leveling", "LoadoutName": "Loadout name", @@ -818,6 +820,7 @@ "LoadoutAnalysis": { "Analyzing": "Analyzing {{numAnalyzed}}/{{numLoadouts}} Loadouts", "Analyzed": "Analyzed {{numLoadouts}} Loadouts", + "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat tier to exceed T10. DIM may identify better stats by reducing the amount of excess tiers. If this is undesired, disable \"{{settingName}}\" in the Loadout.", "MissingItems": { "Name": "Missing Items", "Description": "Some of the items in this loadout are no longer in your inventory." diff --git a/src/@types/dim-api-types.ts b/src/@types/dim-api-types.ts new file mode 100644 index 0000000000..025187dcd1 --- /dev/null +++ b/src/@types/dim-api-types.ts @@ -0,0 +1,7 @@ +import '@destinyitemmanager/dim-api-types'; + +declare module '@destinyitemmanager/dim-api-types' { + interface LoadoutParameters { + includeRuntimeStatBenefits?: boolean; + } +} diff --git a/src/app/dim-api/selectors.ts b/src/app/dim-api/selectors.ts index 415a09b622..9e45c89361 100644 --- a/src/app/dim-api/selectors.ts +++ b/src/app/dim-api/selectors.ts @@ -1,6 +1,7 @@ -import { defaultLoadoutParameters, DestinyVersion } from '@destinyitemmanager/dim-api-types'; +import { DestinyVersion } from '@destinyitemmanager/dim-api-types'; import { DestinyAccount } from 'app/accounts/destiny-account'; import { currentAccountSelector, destinyVersionSelector } from 'app/accounts/selectors'; +import { defaultLoadoutParameters } from 'app/loadout-builder/loadout-params'; import { Settings } from 'app/settings/initial-settings'; import { RootState } from 'app/store/types'; import { createSelector } from 'reselect'; diff --git a/src/app/loadout-analyzer/analysis.ts b/src/app/loadout-analyzer/analysis.ts index b3eaa9a605..92430a3480 100644 --- a/src/app/loadout-analyzer/analysis.ts +++ b/src/app/loadout-analyzer/analysis.ts @@ -1,9 +1,12 @@ -import { AssumeArmorMasterwork } from '@destinyitemmanager/dim-api-types'; +import { AssumeArmorMasterwork, LoadoutParameters } from '@destinyitemmanager/dim-api-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { DimCharacterStat } from 'app/inventory/store-types'; import { filterItems } from 'app/loadout-builder/item-filter'; -import { resolveStatConstraints } from 'app/loadout-builder/loadout-params'; +import { + defaultLoadoutParameters, + resolveStatConstraints, +} from 'app/loadout-builder/loadout-params'; import { runProcess } from 'app/loadout-builder/process/process-wrapper'; import { ArmorEnergyRules, @@ -42,13 +45,7 @@ import { } from './types'; export async function analyzeLoadout( - { - allItems, - autoModDefs, - itemCreationContext, - savedLoLoadoutParameters: savedLoadoutParameters, - unlockedPlugs, - }: LoadoutAnalysisContext, + { allItems, autoModDefs, itemCreationContext, unlockedPlugs }: LoadoutAnalysisContext, storeId: string, classType: DestinyClass, loadout: Loadout, @@ -67,7 +64,12 @@ export async function analyzeLoadout( const originalLoadoutMods = resolvedLoadout.resolvedMods; const originalModDefs = originalLoadoutMods.map((mod) => mod.resolvedMod); - const loadoutParameters = { ...savedLoadoutParameters, ...loadout.parameters }; + const loadoutParameters: LoadoutParameters = { + ...defaultLoadoutParameters, + ...loadout.parameters, + }; + + const includeRuntimeStatBenefits = loadoutParameters.includeRuntimeStatBenefits ?? false; const subclass = resolvedLoadout.resolvedLoadoutItems.find( (i) => i.item.bucket.hash === BucketHashes.Subclass, @@ -105,6 +107,7 @@ export async function analyzeLoadout( let hasStrictUpgrade = false; let ineligibleForOptimization = false; + let betterStatsAvailableFontNote = false; if (loadoutArmor.length) { if (loadoutArmor.length < 5) { findings.add(LoadoutFinding.NotAFullArmorSet); @@ -175,8 +178,18 @@ export async function analyzeLoadout( originalModDefs, armorEnergyRules, statConstraints, + includeRuntimeStatBenefits, ); const assumedLoadoutStats = statProblems.stats; + // If Font mods cause a loadout stats to exceed T10, note this for later + if ( + Object.values(assumedLoadoutStats).some( + (stat) => + stat && stat.value >= 110 && stat.breakdown!.some((c) => c.source === 'runtimeEffect'), + ) + ) { + betterStatsAvailableFontNote = true; + } needUpgrades ||= statProblems.needsUpgradesForStats; @@ -230,7 +243,7 @@ export async function analyzeLoadout( modDefs, subclass, classType, - /* includeRuntimeStatBenefits */ true, + includeRuntimeStatBenefits, ); // Give the event loop a chance after we did a lot of item filtering @@ -274,6 +287,7 @@ export async function analyzeLoadout( return { findings: [...findings], + betterStatsAvailableFontNote: hasStrictUpgrade && betterStatsAvailableFontNote, armorResults: ineligibleForOptimization ? { tag: 'ineligible' } : { @@ -390,13 +404,22 @@ function getStatProblems( mods: PluggableInventoryItemDefinition[], loadoutArmorEnergyRules: ArmorEnergyRules, resolvedStatConstraints: ResolvedStatConstraint[], + includeRuntimeStatBenefits: boolean, ): { stats: HashLookup; cantHitStats: boolean; needsUpgradesForStats: boolean; } { const canHitStatsWithRules = (armorEnergyRules: ArmorEnergyRules) => { - const stats = getLoadoutStats(defs, classType, subclass, loadoutArmor, mods, armorEnergyRules); + const stats = getLoadoutStats( + defs, + classType, + subclass, + loadoutArmor, + mods, + includeRuntimeStatBenefits, + armorEnergyRules, + ); return { stats, canHitStats: resolvedStatConstraints.every( diff --git a/src/app/loadout-analyzer/hooks.tsx b/src/app/loadout-analyzer/hooks.tsx index 1f1dae9622..122dfa3f87 100644 --- a/src/app/loadout-analyzer/hooks.tsx +++ b/src/app/loadout-analyzer/hooks.tsx @@ -1,4 +1,3 @@ -import { savedLoadoutParametersSelector } from 'app/dim-api/selectors'; import { allItemsSelector, createItemContextSelector, @@ -41,15 +40,13 @@ const autoOptimizationContextSelector = currySelector( createItemContextSelector, unlockedPlugSetItemsSelector.selector, allItemsSelector, - savedLoadoutParametersSelector, autoModSelector, - (itemCreationContext, unlockedPlugs, allItems, savedLoLoadoutParameters, autoModDefs) => + (itemCreationContext, unlockedPlugs, allItems, autoModDefs) => itemCreationContext.defs && autoModDefs && ({ itemCreationContext, unlockedPlugs, - savedLoLoadoutParameters, allItems, autoModDefs, } satisfies LoadoutAnalysisContext), diff --git a/src/app/loadout-analyzer/types.ts b/src/app/loadout-analyzer/types.ts index 57779ee2de..4447ba3354 100644 --- a/src/app/loadout-analyzer/types.ts +++ b/src/app/loadout-analyzer/types.ts @@ -6,6 +6,8 @@ import { AutoModDefs } from 'app/loadout-builder/types'; /** The analysis results for a single loadout. */ export interface LoadoutAnalysisResult { findings: LoadoutFinding[]; + /** A caveat to the "better stats available" note because it might be caused by DIM unintentionally considering font mods active */ + betterStatsAvailableFontNote: boolean; /** We took a closer look at the armor in this loadout and determined these results. */ armorResults: ArmorAnalysisResult | undefined; } @@ -82,6 +84,5 @@ export interface LoadoutAnalysisContext { unlockedPlugs: Set; itemCreationContext: ItemCreationContext; allItems: DimItem[]; - savedLoLoadoutParameters: LoadoutParameters; autoModDefs: AutoModDefs; } diff --git a/src/app/loadout-builder/LoadoutBuilder.tsx b/src/app/loadout-builder/LoadoutBuilder.tsx index 2c7609464c..5c7b90479a 100644 --- a/src/app/loadout-builder/LoadoutBuilder.tsx +++ b/src/app/loadout-builder/LoadoutBuilder.tsx @@ -125,6 +125,7 @@ export default memo(function LoadoutBuilder({ const lockedExoticHash = loadoutParameters.exoticArmorHash; const statConstraints = loadoutParameters.statConstraints!; const autoStatMods = Boolean(loadoutParameters.autoStatMods); + const includeRuntimeStatBenefits = loadoutParameters.includeRuntimeStatBenefits ?? true; const assumeArmorMasterwork = loadoutParameters.assumeArmorMasterwork; const classType = loadout.classType; @@ -241,8 +242,9 @@ export default memo(function LoadoutBuilder({ ]); const modStatChanges = useMemo( - () => getTotalModStatChanges(defs, modsToAssign, subclass, classType, true), - [classType, defs, modsToAssign, subclass], + () => + getTotalModStatChanges(defs, modsToAssign, subclass, classType, includeRuntimeStatBenefits), + [classType, defs, includeRuntimeStatBenefits, modsToAssign, subclass], ); // Run the actual loadout generation process in a web worker @@ -609,12 +611,14 @@ function useSaveLoadoutParameters( setSetting('loParameters', { assumeArmorMasterwork: loadoutParameters.assumeArmorMasterwork, autoStatMods: loadoutParameters.autoStatMods, + includeRuntimeStatBenefits: loadoutParameters.includeRuntimeStatBenefits, }); }, [ setSetting, loadoutParameters.assumeArmorMasterwork, loadoutParameters.autoStatMods, hasPreloadedLoadout, + loadoutParameters.includeRuntimeStatBenefits, ]); } diff --git a/src/app/loadout-builder/generated-sets/GeneratedSet.tsx b/src/app/loadout-builder/generated-sets/GeneratedSet.tsx index 3fe4c0d5e9..dad5450240 100644 --- a/src/app/loadout-builder/generated-sets/GeneratedSet.tsx +++ b/src/app/loadout-builder/generated-sets/GeneratedSet.tsx @@ -226,7 +226,7 @@ function getStatsBreakdown( autoMods, /* subclass */ undefined, classType, - /* includeRuntimeStatBenefits */ true, + /* includeRuntimeStatBenefits */ false, // doesn't matter, auto mods have no runtime stats ); // We have a bit of a problem where armor mods can come from both diff --git a/src/app/loadout-builder/loadout-builder-reducer.ts b/src/app/loadout-builder/loadout-builder-reducer.ts index e04b2b3dec..bbf241fc2b 100644 --- a/src/app/loadout-builder/loadout-builder-reducer.ts +++ b/src/app/loadout-builder/loadout-builder-reducer.ts @@ -1,13 +1,12 @@ import { AssumeArmorMasterwork, - defaultLoadoutParameters, LoadoutParameters, StatConstraint, } from '@destinyitemmanager/dim-api-types'; import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions'; import { - savedLoadoutParametersSelector, savedLoStatConstraintsByClassSelector, + savedLoadoutParametersSelector, } from 'app/dim-api/selectors'; import { t } from 'app/i18next-t'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; @@ -16,8 +15,8 @@ import { DimStore } from 'app/inventory/store-types'; import { isPluggableItem } from 'app/inventory/store/sockets'; import { getCurrentStore } from 'app/inventory/stores-helpers'; import { - clearSubclass, LoadoutUpdateFunction, + clearSubclass, removeMod, setLoadoutParameters, updateMods, @@ -35,7 +34,11 @@ import { PlugCategoryHashes } from 'data/d2/generated-enums'; import _ from 'lodash'; import { useCallback, useMemo, useReducer } from 'react'; import { useSelector } from 'react-redux'; -import { resolveStatConstraints, unresolveStatConstraints } from './loadout-params'; +import { + defaultLoadoutParameters, + resolveStatConstraints, + unresolveStatConstraints, +} from './loadout-params'; import { ArmorSet, ExcludedItems, diff --git a/src/app/loadout-builder/loadout-params.ts b/src/app/loadout-builder/loadout-params.ts index 09f582e20e..198f6e392f 100644 --- a/src/app/loadout-builder/loadout-params.ts +++ b/src/app/loadout-builder/loadout-params.ts @@ -1,10 +1,20 @@ /* Functions for dealing with the LoadoutParameters structure we save with loadouts and use to save and share LO settings. */ -import { StatConstraint, defaultLoadoutParameters } from '@destinyitemmanager/dim-api-types'; +import { + LoadoutParameters, + StatConstraint, + defaultLoadoutParameters as dimApiDefaultLoadoutParameters, +} from '@destinyitemmanager/dim-api-types'; import { armorStats } from 'app/search/d2-known-values'; import _ from 'lodash'; import { ResolvedStatConstraint } from './types'; +// FIXME move to dim-api-types +export const defaultLoadoutParameters: LoadoutParameters = { + ...dimApiDefaultLoadoutParameters, + includeRuntimeStatBenefits: true, +}; + /** * Stat constraints are already in priority order, but they do not include * ignored stats. This fills in the ignored stats as well, retaining stat order. diff --git a/src/app/loadout-drawer/loadout-drawer-reducer.ts b/src/app/loadout-drawer/loadout-drawer-reducer.ts index bb4a6a56b7..9bbc01988c 100644 --- a/src/app/loadout-drawer/loadout-drawer-reducer.ts +++ b/src/app/loadout-drawer/loadout-drawer-reducer.ts @@ -602,6 +602,12 @@ export function changeClearMods(enabled: boolean): LoadoutUpdateFunction { }); } +export function changeIncludeRuntimeStats(enabled: boolean): LoadoutUpdateFunction { + return setLoadoutParameters({ + includeRuntimeStatBenefits: enabled, + }); +} + export function updateMods(mods: number[]): LoadoutUpdateFunction { return setLoadoutParameters({ mods: mods.map(mapToNonReducedModCostVariant), diff --git a/src/app/loadout-drawer/loadout-utils.ts b/src/app/loadout-drawer/loadout-utils.ts index f65c514e81..3591dbde13 100644 --- a/src/app/loadout-drawer/loadout-utils.ts +++ b/src/app/loadout-drawer/loadout-utils.ts @@ -284,6 +284,7 @@ export function getLoadoutStats( subclass: ResolvedLoadoutItem | undefined, armor: DimItem[], mods: PluggableInventoryItemDefinition[], + includeRuntimeStatBenefits: boolean, /** Assume armor is masterworked according to these rules when calculating stats */ armorEnergyRules?: ArmorEnergyRules, ) { @@ -331,7 +332,7 @@ export function getLoadoutStats( mods, subclass, classType, - /* includeRuntimeStatBenefits */ true, + includeRuntimeStatBenefits, ); for (const [statHash, value] of Object.entries(modStats)) { diff --git a/src/app/loadout/LoadoutView.tsx b/src/app/loadout/LoadoutView.tsx index 5a748866f5..f76fc26aab 100644 --- a/src/app/loadout/LoadoutView.tsx +++ b/src/app/loadout/LoadoutView.tsx @@ -8,6 +8,7 @@ import { DimStore } from 'app/inventory/store-types'; import { ItemCreationContext } from 'app/inventory/store/d2-item-factory'; import { findingDisplays } from 'app/loadout-analyzer/finding-display'; import { useAnalyzeLoadout } from 'app/loadout-analyzer/hooks'; +import { LoadoutFinding } from 'app/loadout-analyzer/types'; import { getItemsFromLoadoutItems } from 'app/loadout-drawer/loadout-item-conversion'; import { Loadout, LoadoutItem, ResolvedLoadoutItem } from 'app/loadout-drawer/loadout-types'; import { getLight } from 'app/loadout-drawer/loadout-utils'; @@ -133,12 +134,17 @@ export default function LoadoutView({ if (!display.icon) { return undefined; } + let description = t(display.description); + if ( + finding === LoadoutFinding.BetterStatsAvailable && + analysis!.result.betterStatsAvailableFontNote + ) { + description += `\n\n${t('LoadoutAnalysis.BetterStatsAvailableFontNote', { + settingName: t('Loadouts.IncludeRuntimeStatBenefits'), + })}`; + } return ( - + {t(display.name)} ); diff --git a/src/app/loadout/loadout-edit/LoadoutEdit.tsx b/src/app/loadout/loadout-edit/LoadoutEdit.tsx index b21400b664..fe741877ea 100644 --- a/src/app/loadout/loadout-edit/LoadoutEdit.tsx +++ b/src/app/loadout/loadout-edit/LoadoutEdit.tsx @@ -18,6 +18,7 @@ import { addItem, applySocketOverrides, changeClearMods, + changeIncludeRuntimeStats, clearArtifactUnlocks, clearBucketCategory, clearLoadoutOptimizerParameters, @@ -63,6 +64,7 @@ import SubclassPlugDrawer from '../SubclassPlugDrawer'; import { pickSubclass } from '../item-utils'; import { hasVisibleLoadoutParameters } from '../loadout-ui/LoadoutParametersDisplay'; import { useLoadoutMods } from '../mod-assignment-drawer/selectors'; +import { includesRuntimeStatMods } from '../stats'; import styles from './LoadoutEdit.m.scss'; import LoadoutEditBucket, { ArmorExtras } from './LoadoutEditBucket'; import LoadoutEditSection from './LoadoutEditSection'; @@ -429,12 +431,14 @@ export function LoadoutEditModsSection({ const { useUpdater, useDefsStoreUpdater } = useLoadoutUpdaters(store, setLoadout); const clearUnsetMods = loadout.parameters?.clearMods; + const includeRuntimeStats = loadout.parameters?.includeRuntimeStatBenefits ?? true; const handleUpdateMods = useUpdater(updateMods); const handleRemoveMod = useUpdater(removeMod); const handleClearUnsetModsChanged = useUpdater(changeClearMods); const handleRandomizeMods = useDefsStoreUpdater(randomizeLoadoutMods); const handleClearMods = useUpdater(clearMods); + const handleIncludeRuntimeStats = useUpdater(changeIncludeRuntimeStats); const handleSyncModsFromEquipped = () => setLoadout(syncModsFromEquipped(store)); return ( @@ -458,6 +462,12 @@ export function LoadoutEditModsSection({ hideShowModPlacements={!showModPlacementsButton} autoStatMods={autoStatMods} onAutoStatModsChanged={onAutoStatModsChanged} + includeRuntimeStats={includeRuntimeStats} + onIncludeRuntimeStatsChanged={ + loadout.parameters?.mods && includesRuntimeStatMods(loadout.parameters.mods) + ? handleIncludeRuntimeStats + : undefined + } /> ); diff --git a/src/app/loadout/loadout-ui/LoadoutMods.tsx b/src/app/loadout/loadout-ui/LoadoutMods.tsx index 0cfaba03bd..074bfb5b99 100644 --- a/src/app/loadout/loadout-ui/LoadoutMods.tsx +++ b/src/app/loadout/loadout-ui/LoadoutMods.tsx @@ -1,5 +1,6 @@ import { LoadoutParameters } from '@destinyitemmanager/dim-api-types'; import CheckButton from 'app/dim-ui/CheckButton'; +import { PressTip } from 'app/dim-ui/PressTip'; import { t } from 'app/i18next-t'; import { artifactUnlocksSelector, unlockedPlugSetItemsSelector } from 'app/inventory/selectors'; import { hashesToPluggableItems } from 'app/inventory/store/sockets'; @@ -65,10 +66,12 @@ export const LoadoutMods = memo(function LoadoutMods({ missingSockets, hideShowModPlacements, autoStatMods, + includeRuntimeStats, onUpdateMods, onRemoveMod, onClearUnsetModsChanged, onAutoStatModsChanged, + onIncludeRuntimeStatsChanged, }: { loadout: Loadout; allMods: ResolvedLoadoutMod[]; @@ -78,11 +81,13 @@ export const LoadoutMods = memo(function LoadoutMods({ missingSockets?: boolean; /** Are stat mods being chosen automatically by LO? */ autoStatMods?: boolean; + includeRuntimeStats?: boolean; /** If present, show an "Add Mod" button */ onUpdateMods?: (newMods: number[]) => void; onRemoveMod?: (mod: ResolvedLoadoutMod) => void; onClearUnsetModsChanged?: (checked: boolean) => void; onAutoStatModsChanged?: (checked: boolean) => void; + onIncludeRuntimeStatsChanged?: (checked: boolean) => void; }) { const isPhonePortrait = useIsPhonePortrait(); const getModRenderKey = createGetModRenderKey(); @@ -145,7 +150,10 @@ export const LoadoutMods = memo(function LoadoutMods({ )} - {(!hideShowModPlacements || onClearUnsetModsChanged || onAutoStatModsChanged) && + {(!hideShowModPlacements || + onClearUnsetModsChanged || + onAutoStatModsChanged || + onIncludeRuntimeStatsChanged) && (allMods.length > 0 || onUpdateMods) && (
{!hideShowModPlacements && ( @@ -165,6 +173,18 @@ export const LoadoutMods = memo(function LoadoutMods({ )} + {onIncludeRuntimeStatsChanged && ( + + + {t('Loadouts.IncludeRuntimeStatBenefits')} + + + )} + {$featureFlags.loAutoStatMods && onAutoStatModsChanged && ( s.maxTier !== undefined || s.minTier !== undefined)) + params.statConstraints?.some((s) => s.maxTier !== undefined || s.minTier !== undefined) || + (params.mods && + includesRuntimeStatMods(params.mods) && + (params.includeRuntimeStatBenefits ?? true))) ); } @@ -47,6 +51,13 @@ export default function LoadoutParametersDisplay({ params }: { params: LoadoutPa )} + {params.mods && + includesRuntimeStatMods(params.mods) && + (params.includeRuntimeStatBenefits ?? true) && ( + + {t('Loadouts.IncludeRuntimeStatBenefits')} + + )} {statConstraints && ( fontModHashToStatHash()[mod] !== undefined); +} + /** * This sums up the total stat contributions across mods passed in. These are then applied * to the loadouts after all the items' base stat values have been summed. This mimics how mods diff --git a/src/app/store-stats/CharacterStats.tsx b/src/app/store-stats/CharacterStats.tsx index 2f273d5b72..87be321fa2 100644 --- a/src/app/store-stats/CharacterStats.tsx +++ b/src/app/store-stats/CharacterStats.tsx @@ -234,7 +234,14 @@ export function LoadoutCharacterStats({ } } - const stats = getLoadoutStats(defs, loadout.classType, subclass, equippedItems, allMods); + const stats = getLoadoutStats( + defs, + loadout.classType, + subclass, + equippedItems, + allMods, + loadout.parameters?.includeRuntimeStatBenefits ?? true, + ); return ; } diff --git a/src/locale/en.json b/src/locale/en.json index 3ef9f764b4..b9934d0bbe 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -564,6 +564,7 @@ "Description": "Choosing different armor or mods for this loadout will allow reaching higher stat tiers.", "Name": "Better Stats Available" }, + "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat tier to exceed T10. DIM may identify better stats by reducing the amount of excess tiers. If this is undesired, disable \"{{settingName}}\" in the Loadout.", "DoesNotRespectExotic": { "Description": "This loadout's Loadout Optimizer settings specify an exotic choice, but the loadout does not match that exotic.", "Name": "Wrong Exotic" @@ -741,6 +742,8 @@ }, "ImportLoadout": "Import Loadout", "InGameLoadouts": "In-Game Loadouts", + "IncludeRuntimeStatBenefits": "Include Font mod stats", + "IncludeRuntimeStatBenefitsDesc": "\"Font of ...\" armor mods provide a flat boost to character stats while you have Armor Charges.\n\nWith this setting, DIM considers these mods active and adds their benefits to this Loadout's stats in calculations and optimizations.", "ItemErrorSummary": "1 item error:", "ItemErrorSummary_plural": "{{count}} item errors:", "ItemLeveling": "Item Leveling", From 44f471bbad44c1d296a2cf94e5bc7a8b299939d9 Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 14:19:22 +0100 Subject: [PATCH 02/11] Open in Optimizer --- config/i18n.json | 4 +-- src/app/loadout/LoadoutView.tsx | 6 ++-- .../loadout-ui/LoadoutItemCategorySection.tsx | 32 ++++++++++++++++--- src/locale/en.json | 4 +-- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/config/i18n.json b/config/i18n.json index 1abaa87a49..27b820e7e4 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -820,7 +820,7 @@ "LoadoutAnalysis": { "Analyzing": "Analyzing {{numAnalyzed}}/{{numLoadouts}} Loadouts", "Analyzed": "Analyzed {{numLoadouts}} Loadouts", - "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat tier to exceed T10. DIM may identify better stats by reducing the amount of excess tiers. If this is undesired, disable \"{{settingName}}\" in the Loadout.", + "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat tier to exceed T10. DIM may identify better stats by reducing the amount of excess tiers. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "MissingItems": { "Name": "Missing Items", "Description": "Some of the items in this loadout are no longer in your inventory." @@ -843,7 +843,7 @@ }, "BetterStatsAvailable": { "Name": "Better Stats Available", - "Description": "Choosing different armor or mods for this loadout will allow reaching higher stat tiers." + "Description": "Choosing different armor or mods for this loadout will allow reaching higher stat tiers. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds." }, "NotAFullArmorSet": { "Name": "Not A Full Armor Set", diff --git a/src/app/loadout/LoadoutView.tsx b/src/app/loadout/LoadoutView.tsx index f76fc26aab..892142e797 100644 --- a/src/app/loadout/LoadoutView.tsx +++ b/src/app/loadout/LoadoutView.tsx @@ -139,9 +139,7 @@ export default function LoadoutView({ finding === LoadoutFinding.BetterStatsAvailable && analysis!.result.betterStatsAvailableFontNote ) { - description += `\n\n${t('LoadoutAnalysis.BetterStatsAvailableFontNote', { - settingName: t('Loadouts.IncludeRuntimeStatBenefits'), - })}`; + description += `\n\n${t('LoadoutAnalysis.BetterStatsAvailableFontNote')}`; } return ( @@ -169,7 +167,7 @@ export default function LoadoutView({ key={category} category={category} subclass={subclass} - storeId={store.id} + store={store} items={categories[category]} allMods={modDefinitions} modsByBucket={modsByBucket} diff --git a/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx b/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx index d9485ace73..4333fac742 100644 --- a/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx +++ b/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx @@ -5,14 +5,22 @@ import ItemPopupTrigger from 'app/inventory/ItemPopupTrigger'; import { D2BucketCategory } from 'app/inventory/inventory-buckets'; import { PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { bucketsSelector } from 'app/inventory/selectors'; +import { DimStore } from 'app/inventory/store-types'; +import { useAnalyzeLoadout } from 'app/loadout-analyzer/hooks'; import { LockableBucketHashes } from 'app/loadout-builder/types'; +import { + clearBucketCategory, + setLoadoutParameters, +} from 'app/loadout-drawer/loadout-drawer-reducer'; import { Loadout, ResolvedLoadoutItem } from 'app/loadout-drawer/loadout-types'; +import { useD2Definitions } from 'app/manifest/selectors'; import { useIsPhonePortrait } from 'app/shell/selectors'; import { LoadoutCharacterStats } from 'app/store-stats/CharacterStats'; import { emptyArray } from 'app/utils/empty'; import { LookupTable } from 'app/utils/util-types'; import clsx from 'clsx'; import _ from 'lodash'; +import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { BucketPlaceholder } from './BucketPlaceholder'; import { FashionMods } from './FashionMods'; @@ -29,7 +37,7 @@ const categoryStyles: LookupTable = { export default function LoadoutItemCategorySection({ category, subclass, - storeId, + store, items, allMods, modsByBucket, @@ -38,7 +46,7 @@ export default function LoadoutItemCategorySection({ }: { category: D2BucketCategory; subclass?: ResolvedLoadoutItem; - storeId: string; + store: DimStore; items?: ResolvedLoadoutItem[]; allMods: PluggableInventoryItemDefinition[]; modsByBucket: { @@ -47,7 +55,9 @@ export default function LoadoutItemCategorySection({ loadout: Loadout; hideOptimizeArmor?: boolean; }) { + const defs = useD2Definitions()!; const buckets = useSelector(bucketsSelector)!; + const analysis = useAnalyzeLoadout(loadout, store, /* active */ !hideOptimizeArmor); const itemsByBucket = Map.groupBy(items ?? [], (li) => li.item.bucket.hash); const isPhonePortrait = useIsPhonePortrait(); const bucketOrder = @@ -63,6 +73,18 @@ export default function LoadoutItemCategorySection({ const isArmor = category === 'Armor'; const hasFashion = isArmor && !_.isEmpty(modsByBucket); + const optimizeLoadout: Loadout = useMemo( + () => + analysis?.result.armorResults?.tag === 'done' && + analysis.result.armorResults.betterStatsAvailable + ? clearBucketCategory( + defs, + 'Armor', + )(setLoadoutParameters(analysis.result.armorResults.loadoutParameters)(loadout)) + : loadout, + [defs, analysis?.result.armorResults, loadout], + ); + if (isPhonePortrait && !items && !hasFashion) { return null; } @@ -74,7 +96,7 @@ export default function LoadoutItemCategorySection({ {bucketOrder.map((bucket) => ( } {!hideOptimizeArmor && ( )} diff --git a/src/locale/en.json b/src/locale/en.json index b9934d0bbe..2fe3e7788f 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -561,10 +561,10 @@ "Analyzed": "Analyzed {{numLoadouts}} Loadouts", "Analyzing": "Analyzing {{numAnalyzed}}/{{numLoadouts}} Loadouts", "BetterStatsAvailable": { - "Description": "Choosing different armor or mods for this loadout will allow reaching higher stat tiers.", + "Description": "Choosing different armor or mods for this loadout will allow reaching higher stat tiers. Choose \"$t(Loadouts.OpenInOptimizer)\" to view better builds.", "Name": "Better Stats Available" }, - "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat tier to exceed T10. DIM may identify better stats by reducing the amount of excess tiers. If this is undesired, disable \"{{settingName}}\" in the Loadout.", + "BetterStatsAvailableFontNote": "Note: This Loadout uses \"Font of ...\" mods that cause a stat tier to exceed T10. DIM may identify better stats by reducing the amount of excess tiers. If this is undesired, disable \"$t(Loadouts.IncludeRuntimeStatBenefits)\" in the Loadout.", "DoesNotRespectExotic": { "Description": "This loadout's Loadout Optimizer settings specify an exotic choice, but the loadout does not match that exotic.", "Name": "Wrong Exotic" From 825c65ec6702191b642320c63c42b9264c3fbe28 Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 17:54:28 +0100 Subject: [PATCH 03/11] Use existing saved LO stat order --- src/app/loadout-analyzer/analysis.ts | 10 +++++++++- src/app/loadout-analyzer/hooks.tsx | 5 ++++- src/app/loadout-analyzer/types.ts | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/loadout-analyzer/analysis.ts b/src/app/loadout-analyzer/analysis.ts index 92430a3480..2e97ffd2b9 100644 --- a/src/app/loadout-analyzer/analysis.ts +++ b/src/app/loadout-analyzer/analysis.ts @@ -45,7 +45,13 @@ import { } from './types'; export async function analyzeLoadout( - { allItems, autoModDefs, itemCreationContext, unlockedPlugs }: LoadoutAnalysisContext, + { + allItems, + autoModDefs, + savedLoStatConstraintsByClass, + itemCreationContext, + unlockedPlugs, + }: LoadoutAnalysisContext, storeId: string, classType: DestinyClass, loadout: Loadout, @@ -64,8 +70,10 @@ export async function analyzeLoadout( const originalLoadoutMods = resolvedLoadout.resolvedMods; const originalModDefs = originalLoadoutMods.map((mod) => mod.resolvedMod); + const statOrderForClass = savedLoStatConstraintsByClass[classType]; const loadoutParameters: LoadoutParameters = { ...defaultLoadoutParameters, + ...(statOrderForClass && { statConstraints: statOrderForClass }), ...loadout.parameters, }; diff --git a/src/app/loadout-analyzer/hooks.tsx b/src/app/loadout-analyzer/hooks.tsx index 122dfa3f87..f94abcddef 100644 --- a/src/app/loadout-analyzer/hooks.tsx +++ b/src/app/loadout-analyzer/hooks.tsx @@ -1,3 +1,4 @@ +import { savedLoStatConstraintsByClassSelector } from 'app/dim-api/selectors'; import { allItemsSelector, createItemContextSelector, @@ -39,14 +40,16 @@ const autoOptimizationContextSelector = currySelector( createSelector( createItemContextSelector, unlockedPlugSetItemsSelector.selector, + savedLoStatConstraintsByClassSelector, allItemsSelector, autoModSelector, - (itemCreationContext, unlockedPlugs, allItems, autoModDefs) => + (itemCreationContext, unlockedPlugs, savedLoStatConstraintsByClass, allItems, autoModDefs) => itemCreationContext.defs && autoModDefs && ({ itemCreationContext, unlockedPlugs, + savedLoStatConstraintsByClass, allItems, autoModDefs, } satisfies LoadoutAnalysisContext), diff --git a/src/app/loadout-analyzer/types.ts b/src/app/loadout-analyzer/types.ts index 4447ba3354..a44ab52337 100644 --- a/src/app/loadout-analyzer/types.ts +++ b/src/app/loadout-analyzer/types.ts @@ -1,4 +1,4 @@ -import { LoadoutParameters } from '@destinyitemmanager/dim-api-types'; +import { LoadoutParameters, Settings } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { ItemCreationContext } from 'app/inventory/store/d2-item-factory'; import { AutoModDefs } from 'app/loadout-builder/types'; @@ -83,6 +83,7 @@ export const blockAnalysisFindings: LoadoutFinding[] = [ export interface LoadoutAnalysisContext { unlockedPlugs: Set; itemCreationContext: ItemCreationContext; + savedLoStatConstraintsByClass: Settings['loStatConstraintsByClass']; allItems: DimItem[]; autoModDefs: AutoModDefs; } From 76e36152fc4d1ee671f0ba4f21773f9a56a21a02 Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 18:04:23 +0100 Subject: [PATCH 04/11] Fix ignored stats again --- src/app/loadout-builder/process-worker/process.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/loadout-builder/process-worker/process.ts b/src/app/loadout-builder/process-worker/process.ts index 66d663206a..2d6e8156f8 100644 --- a/src/app/loadout-builder/process-worker/process.ts +++ b/src/app/loadout-builder/process-worker/process.ts @@ -429,8 +429,8 @@ export function process( const value = stats[i] + bonusStats[i]; fullStats[statHash] = value; - if (strictUpgrades && !hasStrictUpgrade) { - const statFilter = resolvedStatConstraints[i]; + const statFilter = resolvedStatConstraints[i]; + if (!statFilter.ignored && strictUpgrades && !hasStrictUpgrade) { const tier = Math.min(Math.max(Math.floor(value / 10), 0), 10); hasStrictUpgrade ||= tier > statFilter.minTier; } From 9cb7af594eaa7ee3d49b99fbb98414e91f3cf747 Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 18:25:30 +0100 Subject: [PATCH 05/11] Another small ignored stat problem --- src/app/loadout-analyzer/analysis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/loadout-analyzer/analysis.ts b/src/app/loadout-analyzer/analysis.ts index 2e97ffd2b9..3dab5eb3f0 100644 --- a/src/app/loadout-analyzer/analysis.ts +++ b/src/app/loadout-analyzer/analysis.ts @@ -262,7 +262,7 @@ export async function analyzeLoadout( minTier: statTier(assumedLoadoutStats[c.statHash]!.value), })); - loadoutParameters.statConstraints = strictStatConstraints; + loadoutParameters.statConstraints = strictStatConstraints.filter((c) => !c.ignored); try { const { resultPromise } = runProcess({ anyExotic: loadoutParameters.exoticArmorHash === LOCKED_EXOTIC_ANY_EXOTIC, From 019547c53f4e5d032995e5fb3de5efb004007795 Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 18:45:58 +0100 Subject: [PATCH 06/11] Include existing stat constraints in strict upgrades --- src/app/loadout-analyzer/analysis.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/loadout-analyzer/analysis.ts b/src/app/loadout-analyzer/analysis.ts index 3dab5eb3f0..f712aa7c95 100644 --- a/src/app/loadout-analyzer/analysis.ts +++ b/src/app/loadout-analyzer/analysis.ts @@ -259,7 +259,7 @@ export async function analyzeLoadout( const strictStatConstraints: ResolvedStatConstraint[] = statConstraints.map((c) => ({ ...c, - minTier: statTier(assumedLoadoutStats[c.statHash]!.value), + minTier: Math.max(c.minTier, statTier(assumedLoadoutStats[c.statHash]!.value)), })); loadoutParameters.statConstraints = strictStatConstraints.filter((c) => !c.ignored); @@ -442,7 +442,7 @@ function getStatProblems( return { stats, - cantHitStats: !canHitStatsWithRules, + cantHitStats: !canHitStatsWithUpgrades, needsUpgradesForStats: canHitStatsWithUpgrades && !canHitStatsAsIs, }; } From 63fad5e00bc2024603f7831e3e0c16e9bfb9f261 Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 20:31:34 +0100 Subject: [PATCH 07/11] Send strict upgrade config to Loadout Optimizer --- src/app/loadout-analyzer/analysis.ts | 9 ++- src/app/loadout-analyzer/types.ts | 4 +- src/app/loadout-builder/LoadoutBuilder.m.scss | 29 ++++++++ .../LoadoutBuilder.m.scss.d.ts | 3 + src/app/loadout-builder/LoadoutBuilder.tsx | 67 ++++++++++++++++- .../LoadoutBuilderContainer.tsx | 8 ++- .../generated-sets/GeneratedSet.tsx | 2 +- .../generated-sets/SetStats.tsx | 71 +++++++++++++++---- .../loadout-builder-reducer.ts | 14 ++++ src/app/loadout-builder/process/useProcess.ts | 5 +- .../loadout-ui/LoadoutItemCategorySection.tsx | 5 ++ .../loadout/loadout-ui/OptimizerButton.tsx | 5 +- 12 files changed, 200 insertions(+), 22 deletions(-) diff --git a/src/app/loadout-analyzer/analysis.ts b/src/app/loadout-analyzer/analysis.ts index f712aa7c95..af36fd9264 100644 --- a/src/app/loadout-analyzer/analysis.ts +++ b/src/app/loadout-analyzer/analysis.ts @@ -116,6 +116,7 @@ export async function analyzeLoadout( let hasStrictUpgrade = false; let ineligibleForOptimization = false; let betterStatsAvailableFontNote = false; + let existingLoadoutStatsAsStatConstraints: ResolvedStatConstraint[] | undefined; if (loadoutArmor.length) { if (loadoutArmor.length < 5) { findings.add(LoadoutFinding.NotAFullArmorSet); @@ -257,12 +258,15 @@ export async function analyzeLoadout( // Give the event loop a chance after we did a lot of item filtering await delay(0); + existingLoadoutStatsAsStatConstraints = statConstraints.map((c) => ({ + ...c, + minTier: statTier(assumedLoadoutStats[c.statHash]!.value), + })); const strictStatConstraints: ResolvedStatConstraint[] = statConstraints.map((c) => ({ ...c, minTier: Math.max(c.minTier, statTier(assumedLoadoutStats[c.statHash]!.value)), })); - loadoutParameters.statConstraints = strictStatConstraints.filter((c) => !c.ignored); try { const { resultPromise } = runProcess({ anyExotic: loadoutParameters.exoticArmorHash === LOCKED_EXOTIC_ANY_EXOTIC, @@ -302,6 +306,9 @@ export async function analyzeLoadout( tag: 'done', betterStatsAvailable: hasStrictUpgrade ? LoadoutFinding.BetterStatsAvailable : undefined, loadoutParameters, + strictUpgradeStatConstraints: hasStrictUpgrade + ? existingLoadoutStatsAsStatConstraints + : undefined, }, }; } diff --git a/src/app/loadout-analyzer/types.ts b/src/app/loadout-analyzer/types.ts index a44ab52337..3a0fb85a92 100644 --- a/src/app/loadout-analyzer/types.ts +++ b/src/app/loadout-analyzer/types.ts @@ -1,7 +1,7 @@ import { LoadoutParameters, Settings } from '@destinyitemmanager/dim-api-types'; import { DimItem } from 'app/inventory/item-types'; import { ItemCreationContext } from 'app/inventory/store/d2-item-factory'; -import { AutoModDefs } from 'app/loadout-builder/types'; +import { AutoModDefs, ResolvedStatConstraint } from 'app/loadout-builder/types'; /** The analysis results for a single loadout. */ export interface LoadoutAnalysisResult { @@ -31,6 +31,8 @@ export type ArmorAnalysisResult = betterStatsAvailable: LoadoutFinding.BetterStatsAvailable | undefined; /** If one were to start Loadout Optimizer from here, use these settings. */ loadoutParameters: LoadoutParameters; + /** And pass these to the Loadout Builder to show strict upgrades only */ + strictUpgradeStatConstraints: ResolvedStatConstraint[] | undefined; }; export const enum LoadoutFinding { diff --git a/src/app/loadout-builder/LoadoutBuilder.m.scss b/src/app/loadout-builder/LoadoutBuilder.m.scss index 8d2c69687e..cf25eca4a7 100644 --- a/src/app/loadout-builder/LoadoutBuilder.m.scss +++ b/src/app/loadout-builder/LoadoutBuilder.m.scss @@ -83,3 +83,32 @@ --item-size: 47px; --loadout-edit-subclass-columns: 4; } + +.referenceTiersInfo { + background-color: rgba(0, 0, 0, 0.4); + padding: 10px; + position: relative; + max-width: 800px; + + .header { + display: flex; + flex-flow: row wrap; + font-size: 14px; + column-gap: 20px; + } +} + +.dismissButton { + composes: resetButton from '../dim-ui/common.m.scss'; + padding: 12px 12px 12px 12px; + color: rgba(255, 255, 255, 0.5); + cursor: pointer; + > :global(.app-icon) { + height: 24px; + width: 24px; + font-size: 24px; + } + position: absolute; + right: 0; + top: 0; +} diff --git a/src/app/loadout-builder/LoadoutBuilder.m.scss.d.ts b/src/app/loadout-builder/LoadoutBuilder.m.scss.d.ts index 073fe6ae14..c932f4e3b5 100644 --- a/src/app/loadout-builder/LoadoutBuilder.m.scss.d.ts +++ b/src/app/loadout-builder/LoadoutBuilder.m.scss.d.ts @@ -1,10 +1,13 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'dismissButton': string; 'guide': string; + 'header': string; 'loadoutEditSection': string; 'menuContent': string; 'page': string; + 'referenceTiersInfo': string; 'speedReport': string; 'toolbar': string; 'undoRedo': string; diff --git a/src/app/loadout-builder/LoadoutBuilder.tsx b/src/app/loadout-builder/LoadoutBuilder.tsx index 5c7b90479a..94d799a695 100644 --- a/src/app/loadout-builder/LoadoutBuilder.tsx +++ b/src/app/loadout-builder/LoadoutBuilder.tsx @@ -27,7 +27,14 @@ import { getTotalModStatChanges } from 'app/loadout/stats'; import { useD2Definitions } from 'app/manifest/selectors'; import { searchFilterSelector } from 'app/search/search-filter'; import { useSetSetting, useSetting } from 'app/settings/hooks'; -import { AppIcon, faExclamationTriangle, redoIcon, refreshIcon, undoIcon } from 'app/shell/icons'; +import { + AppIcon, + disabledIcon, + faExclamationTriangle, + redoIcon, + refreshIcon, + undoIcon, +} from 'app/shell/icons'; import { querySelector, useIsPhonePortrait } from 'app/shell/selectors'; import { emptyObject } from 'app/utils/empty'; import { isClassCompatible, itemCanBeEquippedBy } from 'app/utils/item-utils'; @@ -59,6 +66,7 @@ import StatConstraintEditor from './filter/StatConstraintEditor'; import TierSelect from './filter/TierSelect'; import CompareLoadoutsDrawer from './generated-sets/CompareLoadoutsDrawer'; import GeneratedSets from './generated-sets/GeneratedSets'; +import { ReferenceTiers } from './generated-sets/SetStats'; import { sortGeneratedSets } from './generated-sets/utils'; import { filterItems } from './item-filter'; import { LoadoutBuilderAction, useLbState } from './loadout-builder-reducer'; @@ -68,6 +76,7 @@ import { ArmorEnergyRules, LOCKED_EXOTIC_ANY_EXOTIC, LockableBucketHashes, + ResolvedStatConstraint, loDefaultArmorEnergyRules, } from './types'; import useEquippedHashes from './useEquippedHashes'; @@ -77,6 +86,7 @@ import useEquippedHashes from './useEquippedHashes'; */ export default memo(function LoadoutBuilder({ preloadedLoadout, + preloadedStrictStatConstraints, storeId, }: { /** @@ -84,6 +94,7 @@ export default memo(function LoadoutBuilder({ * page. */ preloadedLoadout: Loadout | undefined; + preloadedStrictStatConstraints: ResolvedStatConstraint[] | undefined; /** *A preselected store ID, used when navigating from the Loadouts page. */ @@ -104,6 +115,7 @@ export default memo(function LoadoutBuilder({ { loadout, resolvedStatConstraints, + strictUpgradesStatConstraints, isEditingExistingLoadout, pinnedItems, excludedItems, @@ -114,7 +126,7 @@ export default memo(function LoadoutBuilder({ canUndo, }, lbDispatch, - ] = useLbState(stores, defs, preloadedLoadout, storeId); + ] = useLbState(stores, defs, preloadedLoadout, storeId, preloadedStrictStatConstraints); // For compatibility with LoadoutEdit components const setLoadout = (updateFn: LoadoutUpdateFunction) => lbDispatch({ type: 'setLoadout', updateFn }); @@ -247,6 +259,22 @@ export default memo(function LoadoutBuilder({ [classType, defs, includeRuntimeStatBenefits, modsToAssign, subclass], ); + const effectiveStatConstraints = useMemo( + () => + resolvedStatConstraints.map((constraint) => { + const strictUpgradeConstraint = strictUpgradesStatConstraints?.find( + (c) => c.statHash === constraint.statHash, + ); + return strictUpgradeConstraint && !constraint.ignored + ? { + ...constraint, + minTier: Math.max(constraint.minTier, strictUpgradeConstraint.minTier), + } + : constraint; + }), + [resolvedStatConstraints, strictUpgradesStatConstraints], + ); + // Run the actual loadout generation process in a web worker const { result, processing } = useProcess({ selectedStore, @@ -254,9 +282,10 @@ export default memo(function LoadoutBuilder({ lockedModMap, modStatChanges, armorEnergyRules, - resolvedStatConstraints, + resolvedStatConstraints: effectiveStatConstraints, anyExotic: lockedExoticHash === LOCKED_EXOTIC_ANY_EXOTIC, autoStatMods, + strictUpgrades: Boolean(strictUpgradesStatConstraints), }); const resultSets = result?.sets; @@ -458,6 +487,12 @@ export default memo(function LoadoutBuilder({

)} + {strictUpgradesStatConstraints && ( + + )} {result && sortedSets?.length ? ( ); } + +function ExistingLoadoutStats({ + lbDispatch, + statConstraints, +}: { + lbDispatch: Dispatch; + statConstraints: ResolvedStatConstraint[]; +}) { + return ( +
+
+ Existing Loadout Stats + +
+ Only showing builds with strictly better stats + +
+ ); +} diff --git a/src/app/loadout-builder/LoadoutBuilderContainer.tsx b/src/app/loadout-builder/LoadoutBuilderContainer.tsx index 04b5528393..44f4f1bb3f 100644 --- a/src/app/loadout-builder/LoadoutBuilderContainer.tsx +++ b/src/app/loadout-builder/LoadoutBuilderContainer.tsx @@ -14,6 +14,7 @@ import { createSelector } from 'reselect'; import { DestinyAccount } from '../accounts/destiny-account'; import { allItemsSelector } from '../inventory/selectors'; import LoadoutBuilder from './LoadoutBuilder'; +import { ResolvedStatConstraint } from './types'; const disabledDueToMaintenanceSelector = createSelector( allItemsSelector, @@ -38,7 +39,11 @@ export default function LoadoutBuilderContainer({ account }: { account: DestinyA // Get an entire loadout from state - this is used when optimizing a loadout from within DIM. const locationState = location.state as - | { loadout: Loadout | undefined; storeId: string | undefined } + | { + loadout: Loadout | undefined; + storeId: string | undefined; + strictUpgradeStatConstraints: ResolvedStatConstraint[] | undefined; + } | undefined; const preloadedLoadout = locationState?.loadout; if (preloadedLoadout?.parameters?.query) { @@ -71,6 +76,7 @@ export default function LoadoutBuilderContainer({ account }: { account: DestinyA ); diff --git a/src/app/loadout-builder/generated-sets/GeneratedSet.tsx b/src/app/loadout-builder/generated-sets/GeneratedSet.tsx index dad5450240..f69be0e836 100644 --- a/src/app/loadout-builder/generated-sets/GeneratedSet.tsx +++ b/src/app/loadout-builder/generated-sets/GeneratedSet.tsx @@ -26,7 +26,7 @@ import { getPower } from '../utils'; import styles from './GeneratedSet.m.scss'; import GeneratedSetButtons from './GeneratedSetButtons'; import GeneratedSetItem from './GeneratedSetItem'; -import SetStats from './SetStats'; +import { SetStats } from './SetStats'; /** * A single "stat mix" of builds. Each armor slot contains multiple possibilities, diff --git a/src/app/loadout-builder/generated-sets/SetStats.tsx b/src/app/loadout-builder/generated-sets/SetStats.tsx index 73f3682aed..b7e2c5eb96 100644 --- a/src/app/loadout-builder/generated-sets/SetStats.tsx +++ b/src/app/loadout-builder/generated-sets/SetStats.tsx @@ -6,6 +6,7 @@ import { AppIcon, powerIndicatorIcon } from 'app/shell/icons'; import StatTooltip from 'app/store-stats/StatTooltip'; import { DestinyStatDefinition } from 'bungie-api-ts/destiny2'; import clsx from 'clsx'; +import _ from 'lodash'; import { ArmorStatHashes, ArmorStats, ModStatChanges, ResolvedStatConstraint } from '../types'; import { remEuclid, statTierWithHalf } from '../utils'; import styles from './SetStats.m.scss'; @@ -15,7 +16,7 @@ import { calculateTotalTier, sumEnabledStats } from './utils'; * Displays the overall tier and per-stat tier of a generated loadout set. */ // TODO: would be a lot easier if this was just passed a Loadout or FullyResolvedLoadout... -export default function SetStats({ +export function SetStats({ stats, getStatsBreakdown, maxPower, @@ -44,18 +45,7 @@ export default function SetStats({ return (
- - {t('LoadoutBuilder.TierNumber', { - tier: enabledTier, - })} - - {enabledTier !== totalTier && ( - - {` (${t('LoadoutBuilder.TierNumber', { - tier: totalTier, - })})`} - - )} +
{resolvedStatConstraints.map((c) => { const statHash = c.statHash as ArmorStatHashes; @@ -132,3 +122,58 @@ function Stat({ ); } + +function TotalTier({ totalTier, enabledTier }: { totalTier: number; enabledTier: number }) { + return ( + <> + + {t('LoadoutBuilder.TierNumber', { + tier: enabledTier, + })} + + {enabledTier !== totalTier && ( + + {` (${t('LoadoutBuilder.TierNumber', { + tier: totalTier, + })})`} + + )} + + ); +} + +export function ReferenceTiers({ + resolvedStatConstraints, +}: { + resolvedStatConstraints: ResolvedStatConstraint[]; +}) { + const defs = useD2Definitions()!; + const totalTier = _.sumBy(resolvedStatConstraints, (c) => c.minTier); + const enabledTier = _.sumBy(resolvedStatConstraints, (c) => (c.ignored ? 0 : c.minTier)); + + return ( +
+ + {resolvedStatConstraints.map((c) => { + const statHash = c.statHash as ArmorStatHashes; + const statDef = defs.Stat.get(statHash); + const tier = c.minTier; + return ( + + + + {t('LoadoutBuilder.TierNumber', { + tier, + })} + + + ); + })} +
+ ); +} diff --git a/src/app/loadout-builder/loadout-builder-reducer.ts b/src/app/loadout-builder/loadout-builder-reducer.ts index bbf241fc2b..ed0b455284 100644 --- a/src/app/loadout-builder/loadout-builder-reducer.ts +++ b/src/app/loadout-builder/loadout-builder-reducer.ts @@ -82,6 +82,12 @@ interface LoadoutBuilderConfiguration { */ isEditingExistingLoadout: boolean; + /** + * If we are editing an existing loadout via the "better stats available" + * feature, this contains the stats we actually need to exceed. + */ + strictUpgradesStatConstraints: ResolvedStatConstraint[] | undefined; + /** * A copy of `loadout.parameters.statConstraints`, but with ignored stats * included. This is more convenient to use than the raw `statConstraints` but @@ -127,6 +133,7 @@ const lbConfigInit = ({ storeId, savedLoadoutBuilderParameters, savedStatConstraintsPerClass, + strictUpgradesStatConstraints, }: { stores: DimStore[]; allItems: DimItem[]; @@ -140,6 +147,7 @@ const lbConfigInit = ({ storeId: string | undefined; savedLoadoutBuilderParameters: LoadoutParameters; savedStatConstraintsPerClass: { [classType: number]: StatConstraint[] }; + strictUpgradesStatConstraints: ResolvedStatConstraint[] | undefined; }): LoadoutBuilderConfiguration => { // Preloaded loadouts from the "Optimize Armor" button take priority const classTypeFromPreloadedLoadout = preloadedLoadout?.classType ?? DestinyClass.Unknown; @@ -221,6 +229,7 @@ const lbConfigInit = ({ loadout, isEditingExistingLoadout, resolvedStatConstraints: resolveStatConstraints(loadoutParameters.statConstraints!), + strictUpgradesStatConstraints, pinnedItems, excludedItems: emptyObject(), selectedStoreId, @@ -268,6 +277,7 @@ type LoadoutBuilderConfigAction = | { type: 'addGeneralMods'; mods: PluggableInventoryItemDefinition[] } | { type: 'lockExotic'; lockedExoticHash: number } | { type: 'removeLockedExotic' } + | { type: 'dismissComparisonStats' } | { type: 'setSearchQuery'; query: string }; type LoadoutBuilderUIAction = @@ -478,6 +488,8 @@ function lbConfigReducer(defs: D2ManifestDefinitions) { return updateLoadout(state, setLoadoutParameters({ exoticArmorHash: undefined })); case 'autoStatModsChanged': return updateLoadout(state, setLoadoutParameters({ autoStatMods: action.autoStatMods })); + case 'dismissComparisonStats': + return { ...state, strictUpgradesStatConstraints: undefined }; case 'setSearchQuery': return updateLoadout(state, setLoadoutParameters({ query: action.query || undefined })); } @@ -509,6 +521,7 @@ export function useLbState( defs: D2ManifestDefinitions, preloadedLoadout: Loadout | undefined, storeId: string | undefined, + strictUpgradesStatConstraints: ResolvedStatConstraint[] | undefined, ) { const savedLoadoutBuilderParameters = useSelector(savedLoadoutParametersSelector); const savedStatConstraintsPerClass = useSelector(savedLoStatConstraintsByClassSelector); @@ -530,6 +543,7 @@ export function useLbState( storeId, savedLoadoutBuilderParameters, savedStatConstraintsPerClass, + strictUpgradesStatConstraints, }), ); diff --git a/src/app/loadout-builder/process/useProcess.ts b/src/app/loadout-builder/process/useProcess.ts index 4a0e071917..18b67d0598 100644 --- a/src/app/loadout-builder/process/useProcess.ts +++ b/src/app/loadout-builder/process/useProcess.ts @@ -53,6 +53,7 @@ export function useProcess({ resolvedStatConstraints, anyExotic, autoStatMods, + strictUpgrades, }: { selectedStore: DimStore; filteredItems: ItemsByBucket; @@ -62,6 +63,7 @@ export function useProcess({ resolvedStatConstraints: ResolvedStatConstraint[]; anyExotic: boolean; autoStatMods: boolean; + strictUpgrades: boolean; }) { const [{ result, processing }, setState] = useState({ processing: false, @@ -101,7 +103,7 @@ export function useProcess({ autoStatMods, getUserItemTag, stopOnFirstSet: false, - strictUpgrades: false, + strictUpgrades, }); cleanupRef.current = cleanup; @@ -145,6 +147,7 @@ export function useProcess({ getUserItemTag, modStatChanges, autoModDefs, + strictUpgrades, ]); return { result, processing }; diff --git a/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx b/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx index 4333fac742..9832406bbe 100644 --- a/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx +++ b/src/app/loadout/loadout-ui/LoadoutItemCategorySection.tsx @@ -128,6 +128,11 @@ export default function LoadoutItemCategorySection({ loadout={optimizeLoadout} storeId={store.id} missingArmor={armorItemsMissing(items)} + strictUpgradeStatConstraints={ + analysis?.result?.armorResults?.tag === 'done' + ? analysis.result.armorResults.strictUpgradeStatConstraints + : undefined + } /> )} diff --git a/src/app/loadout/loadout-ui/OptimizerButton.tsx b/src/app/loadout/loadout-ui/OptimizerButton.tsx index 23fd86dbfd..66b752b25e 100644 --- a/src/app/loadout/loadout-ui/OptimizerButton.tsx +++ b/src/app/loadout/loadout-ui/OptimizerButton.tsx @@ -1,5 +1,6 @@ import { currentAccountSelector } from 'app/accounts/selectors'; import { t } from 'app/i18next-t'; +import { ResolvedStatConstraint } from 'app/loadout-builder/types'; import { Loadout, ResolvedLoadoutItem } from 'app/loadout-drawer/loadout-types'; import { AppIcon, faCalculator } from 'app/shell/icons'; import { count } from 'app/utils/collections'; @@ -13,10 +14,12 @@ export function OptimizerButton({ loadout, storeId, missingArmor, + strictUpgradeStatConstraints, }: { loadout: Loadout; storeId: string; missingArmor: boolean; + strictUpgradeStatConstraints?: ResolvedStatConstraint[]; }) { // We need to build an absolute path rather than a relative one because the loadout editor is mounted higher than the destiny routes. const account = useSelector(currentAccountSelector); @@ -27,7 +30,7 @@ export function OptimizerButton({ {' '} {missingArmor ? t('Loadouts.PickArmor') : t('Loadouts.OpenInOptimizer')} From b30050b014ee691a072b225b3f7fe9ef9e20d269 Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Nov 2023 20:34:47 +0100 Subject: [PATCH 08/11] i18n --- config/i18n.json | 2 ++ src/app/loadout-builder/LoadoutBuilder.tsx | 4 ++-- src/locale/en.json | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/i18n.json b/config/i18n.json index 27b820e7e4..2cbe9540d6 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -503,6 +503,8 @@ "Equip": "Equip on {{character}}", "Exclude": "Excluded Items", "ExcludeHelp": "Shift + click an item (or drag and drop into this bucket) to build sets without specific gear.", + "ExistingBuildStats": "Existing Build Stats", + "ExistingBuildStatsNote": "Only showing builds with strictly higher stat tiers.", "FilterSets": "Filter sets", "Help": { "And": "Armor with all of these perks will be used (\"and\")", diff --git a/src/app/loadout-builder/LoadoutBuilder.tsx b/src/app/loadout-builder/LoadoutBuilder.tsx index 94d799a695..d4a4adff79 100644 --- a/src/app/loadout-builder/LoadoutBuilder.tsx +++ b/src/app/loadout-builder/LoadoutBuilder.tsx @@ -738,10 +738,10 @@ function ExistingLoadoutStats({ return (
- Existing Loadout Stats + {t('LB.ExistingBuildStats')}
- Only showing builds with strictly better stats + {t('LB.ExistingBuildStatsNote')}