diff --git a/src/app/inventory-page/PhoneStores.tsx b/src/app/inventory-page/PhoneStores.tsx index d9d73d64f6..c93643410f 100644 --- a/src/app/inventory-page/PhoneStores.tsx +++ b/src/app/inventory-page/PhoneStores.tsx @@ -178,7 +178,9 @@ function StoresInventory({ <> {((!store.isVault && selectedCategoryId === 'Armor') || (store.isVault && selectedCategoryId === 'Inventory')) && ( - +
+ +
)} {showPostmaster && buckets.byCategory.Postmaster.map(renderBucket)} {buckets.byCategory[selectedCategoryId].map(renderBucket)} diff --git a/src/app/item-feed/Highlights.tsx b/src/app/item-feed/Highlights.tsx index 44c52a9a71..484175fa74 100644 --- a/src/app/item-feed/Highlights.tsx +++ b/src/app/item-feed/Highlights.tsx @@ -12,7 +12,7 @@ import { } from 'app/utils/socket-utils'; import clsx from 'clsx'; import { BucketHashes, PlugCategoryHashes } from 'data/d2/generated-enums'; -import '../store-stats/CharacterStats.scss'; +import '../store-stats/CharacterStats.m.scss'; import styles from './Highlights.m.scss'; /** @@ -74,7 +74,7 @@ export default function Highlights({ item }: { item: DimItem }) { return ( <> {item.bucket.hash !== BucketHashes.ClassArmor && ( -
+
{item.stats?.filter((s) => s.statHash > 0).map(renderStat)}
diff --git a/src/app/loadout/loadout-edit/LoadoutEditBucket.tsx b/src/app/loadout/loadout-edit/LoadoutEditBucket.tsx index 6288d4b523..312cde2a70 100644 --- a/src/app/loadout/loadout-edit/LoadoutEditBucket.tsx +++ b/src/app/loadout/loadout-edit/LoadoutEditBucket.tsx @@ -122,7 +122,7 @@ export function ArmorExtras({ return ( <> {equippedItems.length === 5 && ( -
+
{equippedItems.length === 5 && ( -
+
div { + flex: 1; + display: flex; + flex-direction: row; + } + + :global(.stat) { + flex: 0; + font-size: 11px; + color: var(--theme-header-characters-txt); + &.boostedValue { + color: $stat-modded; + font-weight: bold; + text-shadow: rgba(0, 0, 0, 0.5) 0 0 2px; + } + :global(.phone-portrait) & { + font-size: 12px; + } + } + } + + // D2 stats row. Used here and in the item feed. + :global(.stat-row) { + display: flex; + flex-direction: row; + place-items: center left; + + @include phone-portrait { + width: 100%; + justify-content: center; + } + + &:nth-child(n + 2) { + justify-content: space-between; + } + } +} + +.powerFormula { + margin-bottom: 3px; + img { + opacity: 0.6; + height: 17px; + width: 17px; + } + img[src^='data'] { + filter: invert(1); + } + > div:nth-child(2) { + display: flex; + flex-direction: row; + &::before, + &::after { + font-size: 13px; + color: var(--theme-header-characters-txt); + margin-left: 4px; + margin-right: 4px; + text-decoration: none !important; + } + &::before { + content: '='; + } + &::after { + content: '+'; + } + } +} + +.powerStat { + font-size: 125%; +} + +.tier { + font-weight: bold; +} + +.tooltipFootnote { + opacity: 0.6; + width: 80%; + margin: 10px 0 0 auto; + text-align: right; +} + +.richTooltipWrapper { + margin: 8px 0 0 0; +} + +.asterisk { + vertical-align: top; + margin-left: 2px; +} + +.dropLevel { + display: flex; + justify-content: space-between; +} diff --git a/src/app/store-stats/CharacterStats.m.scss.d.ts b/src/app/store-stats/CharacterStats.m.scss.d.ts new file mode 100644 index 0000000000..33e20e045a --- /dev/null +++ b/src/app/store-stats/CharacterStats.m.scss.d.ts @@ -0,0 +1,14 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'asterisk': string; + 'boostedValue': string; + 'dropLevel': string; + 'powerFormula': string; + 'powerStat': string; + 'richTooltipWrapper': string; + 'tier': string; + 'tooltipFootnote': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/src/app/store-stats/CharacterStats.scss b/src/app/store-stats/CharacterStats.scss deleted file mode 100644 index 806a46e4d0..0000000000 --- a/src/app/store-stats/CharacterStats.scss +++ /dev/null @@ -1,146 +0,0 @@ -@use '../variables' as *; - -@layer base { - /* INT/DIS/STR bars */ - .stat-bars { - width: 100%; - max-width: 230px; - display: flex; - flex-direction: row; - justify-content: space-around; - margin-top: 8px; - opacity: 1; - gap: 4px; - - @include phone-portrait { - margin-left: auto; - margin-right: auto; - align-items: center; - .stat-row { - width: 100%; - justify-content: center; - } - } - - > div { - flex: 1; - display: flex; - flex-direction: row; - } - - .stat { - flex: 1; - display: flex; - flex-direction: row; - align-items: center; - margin-right: 2px; - line-height: 16px; - white-space: nowrap; - &:last-child { - margin-right: 0; - } - .bar { - flex: 1; - height: 7px; - margin-left: 1px; - border-radius: 1px; - background-color: gray; - overflow: hidden; - .progress { - height: 100%; - background-color: white; - &.complete { - background-color: #fb9f28; - } - } - } - img { - height: 14px; - width: 14px; - margin-right: 2px; - opacity: 1; - } - } - - &.destiny2 { - flex-direction: column; - gap: 0; - - .stat-row { - display: flex; - flex-direction: row; - place-items: center left; - &.powerFormula { - margin-bottom: 3px; - img { - opacity: 0.6; - height: 17px; - width: 17px; - } - img[src^='data'] { - filter: invert(1); - } - .powerStat { - font-size: 125%; - } - > div:nth-child(2) { - display: flex; - flex-direction: row; - &::before, - &::after { - font-size: 13px; - color: var(--theme-header-characters-txt); - margin-left: 4px; - margin-right: 4px; - text-decoration: none !important; - } - &::before { - content: '='; - } - &::after { - content: '+'; - } - } - } - &:nth-child(n + 2) { - justify-content: space-between; - } - } - .stat { - flex: 0; - font-size: 11px; - color: var(--theme-header-characters-txt); - - @include phone-portrait { - font-size: 12px; - } - &.boostedValue { - color: $stat-modded; - font-weight: bold; - text-shadow: rgba(0, 0, 0, 0.5) 0 0 2px; - } - } - } - - .tier { - font-weight: bold; - } - } - .tooltipFootnote { - opacity: 0.6; - width: 80%; - margin: 10px 0 0 auto; - text-align: right; - } - .richTooltipWrapper { - margin: 8px 0 0 0; - } - .asterisk { - vertical-align: top; - margin-left: 2px; - } - .dropLevel { - display: flex; - justify-content: space-between; - } -} diff --git a/src/app/store-stats/CharacterStats.tsx b/src/app/store-stats/CharacterStats.tsx index 6e0fd7e994..65cd75806c 100644 --- a/src/app/store-stats/CharacterStats.tsx +++ b/src/app/store-stats/CharacterStats.tsx @@ -7,7 +7,7 @@ import { ArtifactXP } from 'app/inventory/ArtifactXP'; import { ItemPowerSet } from 'app/inventory/ItemPowerSet'; import { DimItem, PluggableInventoryItemDefinition } from 'app/inventory/item-types'; import { profileResponseSelector } from 'app/inventory/selectors'; -import type { DimStore } from 'app/inventory/store-types'; +import type { DimCharacterStat, DimStore } from 'app/inventory/store-types'; import { StorePowerLevel, powerLevelSelector } from 'app/inventory/store/selectors'; import { statTier } from 'app/loadout-builder/utils'; import { getLoadoutStats } from 'app/loadout-drawer/loadout-utils'; @@ -17,19 +17,19 @@ import { useD2Definitions } from 'app/manifest/selectors'; import { getCharacterProgressions } from 'app/progress/selectors'; import { armorStats } from 'app/search/d2-known-values'; import { RootState } from 'app/store/types'; -import { sumBy } from 'app/utils/collections'; +import { filterMap, sumBy } from 'app/utils/collections'; import clsx from 'clsx'; import { BucketHashes, StatHashes } from 'data/d2/generated-enums'; import React from 'react'; import { useSelector } from 'react-redux'; import helmetIcon from '../../../destiny-icons/armor_types/helmet.svg'; import xpIcon from '../../images/xpIcon.svg'; -import './CharacterStats.scss'; +import styles from './CharacterStats.m.scss'; import StatTooltip from './StatTooltip'; function CharacterPower({ stats }: { stats: PowerStat[] }) { return ( -
+
{stats.map((stat) => (
-
{stat.richTooltipContent()}
+
{stat.richTooltipContent()}
)} @@ -54,10 +54,10 @@ function CharacterPower({ stats }: { stats: PowerStat[] }) { > {stat.name}
- + - {stat.problems?.hasClassified && *} + {stat.problems?.hasClassified && *}
@@ -107,13 +107,13 @@ export function PowerFormula({ storeId }: { storeId: string }) { powerFloor={Math.floor(powerLevel.maxGearPower)} />
-
+
{t('Stats.DropLevel')}*
-
* {t('General.ClickForDetails')}
+
* {t('General.ClickForDetails')}
), }; @@ -140,25 +140,38 @@ export function PowerFormula({ storeId }: { storeId: string }) { } /** - * Display each of the main stats (Resistance, Discipline, etc) for a character. The actual stat info is passed in. - * This shows stats for both loadouts and characters - anything that has a character stats list. + * Display each of the main stats (Resistance, Discipline, etc) for a character. + * This is used for both loadouts and characters - anything that has a character + * stats list. This is only used for D2. */ -function CharacterStats({ +export function CharacterStats({ stats, showTier, equippedHashes, }: { - stats: DimStore['stats']; + /** + * A list of stats to display. This should contain an entry for each stat in + * `armorStats`, but if one is missing it won't be shown - you can use this to + * show a subset of stats. + */ + stats: { + [hash: number]: DimCharacterStat; + }; + /** Whether to show the total tier of the set. */ showTier?: boolean; + /** + * Item hashes for equipped exotics, used to show more accurate cooldown + * tooltips. + */ equippedHashes: Set; }) { // Select only the armor stats, in the correct order - const statInfos = armorStats.map((h) => stats[h]); + const statInfos = filterMap(armorStats, (h) => stats[h]); return (
{showTier && ( -
+
{t('LoadoutBuilder.TierNumber', { tier: sumBy(statInfos, (s) => statTier(s.value)), })} @@ -185,8 +198,10 @@ function CharacterStats({ ); } +// TODO: just a plain "show stats" component + /** - * Show the stats for a DimStore. + * Show the stats for a DimStore. This is only used for D2 - D1 uses D1CharacterStats. */ export function StoreCharacterStats({ store }: { store: DimStore }) { const equippedItems = store.items.filter((i) => i.equipped); @@ -207,7 +222,7 @@ export function StoreCharacterStats({ store }: { store: DimStore }) { } /** - * Show the stats for a DIM Loadout. + * Show the stats for a DIM Loadout. This is only used for D2. */ // TODO: just take a FullyResolvedLoadout? export function LoadoutCharacterStats({ diff --git a/src/app/store-stats/D1CharacterStats.m.scss b/src/app/store-stats/D1CharacterStats.m.scss new file mode 100644 index 0000000000..51bb733ace --- /dev/null +++ b/src/app/store-stats/D1CharacterStats.m.scss @@ -0,0 +1,43 @@ +@use '../variables' as *; + +/* INT/DIS/STR bars */ +.statBars { + width: 100%; + max-width: $emblem-width + 16px; + display: grid; + grid-template-columns: repeat(3, 1fr); + margin-top: 8px; + gap: 4px; + + @include phone-portrait { + margin-left: auto; + margin-right: auto; + } +} + +.stat { + display: grid; + grid-template-columns: 16px repeat(5, 1fr); + gap: 1px; + align-items: center; + + > img { + height: 14px; + width: 14px; + } +} + +.bar { + height: 7px; + border-radius: 1px; + background-color: gray; + overflow: hidden; +} + +.progress { + height: 100%; + background-color: white; + &.complete { + background-color: #fb9f28; + } +} diff --git a/src/app/store-stats/D1CharacterStats.m.scss.d.ts b/src/app/store-stats/D1CharacterStats.m.scss.d.ts new file mode 100644 index 0000000000..bb4356cf7c --- /dev/null +++ b/src/app/store-stats/D1CharacterStats.m.scss.d.ts @@ -0,0 +1,11 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'bar': string; + 'complete': string; + 'progress': string; + 'stat': string; + 'statBars': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/src/app/store-stats/D1CharacterStats.tsx b/src/app/store-stats/D1CharacterStats.tsx index 9bb8697414..319791566f 100644 --- a/src/app/store-stats/D1CharacterStats.tsx +++ b/src/app/store-stats/D1CharacterStats.tsx @@ -6,7 +6,7 @@ import { findItemsByBucket } from 'app/inventory/stores-helpers'; import { percent } from 'app/shell/formatters'; import clsx from 'clsx'; import { BucketHashes, StatHashes } from 'data/d2/generated-enums'; -import './CharacterStats.scss'; +import styles from './D1CharacterStats.m.scss'; export function D1StoreCharacterStats({ store }: { store: DimStore }) { const subclass = findItemsByBucket(store, BucketHashes.Subclass).find((i) => i.equipped); @@ -47,16 +47,16 @@ export function D1CharacterStats({ }); return ( -
+
{statList.map((stat, index) => ( -
+
{getD1CharacterStatTiers(stat).map((n, index) => ( -
+
diff --git a/src/app/store-stats/StoreStats.tsx b/src/app/store-stats/StoreStats.tsx index 90263f253a..9cc27f9164 100644 --- a/src/app/store-stats/StoreStats.tsx +++ b/src/app/store-stats/StoreStats.tsx @@ -1,7 +1,5 @@ import type { DimStore } from 'app/inventory/store-types'; import { useIsPhonePortrait } from 'app/shell/selectors'; -import clsx from 'clsx'; -import React from 'react'; import { PowerFormula, StoreCharacterStats } from '../store-stats/CharacterStats'; import AccountCurrencies from './AccountCurrencies'; import { D1StoreCharacterStats } from './D1CharacterStats'; @@ -9,29 +7,19 @@ import styles from './StoreStats.m.scss'; import VaultCapacity from './VaultCapacity'; /** Render the store stats for any store type (character or vault) */ -export default function StoreStats({ - store, - style, -}: { - store: DimStore; - style?: React.CSSProperties; -}) { +export default function StoreStats({ store }: { store: DimStore }) { const isPhonePortrait = useIsPhonePortrait(); - return ( -
- {store.isVault ? ( -
- - {!isPhonePortrait && } -
- ) : store.destinyVersion === 1 ? ( - - ) : ( -
- - -
- )} + return store.isVault ? ( +
+ + {!isPhonePortrait && } +
+ ) : store.destinyVersion === 1 ? ( + + ) : ( +
+ +
); }