-
- {segments.map(([val, className, description], index) => (
-
- ))}
-
+
+ console.log(
+ segments,
+ sumBy(segments, ([val]) => val),
+ stat.maximumValue,
+ )
+ }
+ >
+
}
+ >
+ {segments
+ .toSorted(compareBy(([val]) => val < 0))
+ // .filter(([val]) => val)
+ .map(([val, statType], index) => (
+
+ ))}
+
);
}
+function StatBarTooltip({ segments, stat }: { segments: StatSegments; stat: DimStat }) {
+ return (
+ <>
+
+ {segments.map(([val, statType, description], index) => {
+ const [typeClassName, i18nLabel] = statStyles[statType];
+ const className = clsx(typeClassName, { [styles.negative]: val < 0 });
+ return (
+
+
+ {index > 0 && val >= 0 && '+'}
+ {val}
+
+ {description ?? t(i18nLabel)}
+
+ );
+ })}
+
+ {stat.value}
+ {stat.displayProperties.name}
+
+
+ >
+ );
+}
+
function StatTotal({
totalDetails,
optionalClasses,
@@ -240,7 +325,7 @@ export function ItemStatValue({
const optionalClasses = {
[styles.masterworked]: isMasterworkedStat,
- [styles.modded]: Boolean(moddedStatValue && moddedStatValue > 0 && stat.value !== stat.base),
+ [styles.mod]: Boolean(moddedStatValue && moddedStatValue > 0 && stat.value !== stat.base),
[styles.negativeModded]: Boolean(
moddedStatValue && moddedStatValue < 0 && stat.value !== stat.base,
),
@@ -279,8 +364,8 @@ export function D1QualitySummaryStat({ item }: { item: D1Item }) {
}
/**
- * Gets all sockets that have a plug which doesn't get grouped in the Reusable socket category.
- * The reusable socket category is used in armor 1.0 for perks and stats.
+ * Gets sockets that are considered "mods", like "Mobility Mod" in armor,
+ * or "Adept Range" on weapons. These are marked blue on stat bars.
*/
function getNonReusableModSockets(item: DimItem) {
if (!item.sockets) {
@@ -290,13 +375,22 @@ function getNonReusableModSockets(item: DimItem) {
return item.sockets.allSockets.filter(
(s) =>
s.plugged &&
- !s.isPerk &&
+ !s.isPerk && // excludes armor 1.0 perks and stats?
!socketContainsIntrinsicPlug(s) &&
!s.plugged.plugDef.plug.plugCategoryIdentifier.includes('masterwork') &&
(s.plugged.plugDef.itemCategoryHashes?.some((h) => modItemCategoryHashes.has(h)) ||
statfulOrnaments.includes(s.plugged.plugDef.hash)),
);
}
+/**
+ * Gets weapon parts, like barrels, mags, etc.
+ * Sockets that contribute to the item's stats, but aren't its base stats or mods.
+ */
+function getWeaponPartSockets(item: DimItem) {
+ return (item.sockets?.allSockets ?? []).filter((s) =>
+ s.plugged?.plugDef.itemCategoryHashes?.some((h) => weaponParts.has(h)),
+ );
+}
/**
* Looks through the item sockets to find any weapon/armor mods that modify this stat.
@@ -307,12 +401,20 @@ function getTotalModEffects(item: DimItem, statHash: number) {
}
/**
* Looks through the item sockets to find any weapon/armor mods that modify this stat.
- * Returns the total value the stat is modified by, or 0 if it is not being modified.
+ * Returns the value the stat is modified by, or 0 if it is not being modified.
*/
function getModEffects(item: DimItem, statHash: number) {
const modSockets = getNonReusableModSockets(item);
return getPlugEffects(modSockets, [statHash]);
}
+/**
+ * Looks through the item sockets to find any weapon/armor mods that modify this stat.
+ * Returns the value the stat is modified by, or 0 if it is not being modified.
+ */
+function getPartEffects(item: DimItem, statHash: number) {
+ const modSockets = getWeaponPartSockets(item);
+ return getPlugEffects(modSockets, [statHash]);
+}
export function isD1Stat(item: DimItem, _stat: DimStat): _stat is D1Stat {
return item.destinyVersion === 1;
diff --git a/src/app/shell/formatters.ts b/src/app/shell/formatters.ts
index 689d19da2d..2a1eadf029 100644
--- a/src/app/shell/formatters.ts
+++ b/src/app/shell/formatters.ts
@@ -2,7 +2,7 @@
* Given a value 0-1, returns a string describing it as a percentage from 0-100
*/
export function percent(val: number): string {
- return `${Math.min(100, Math.floor(100 * val))}%`;
+ return `${Math.min(100, Math.floor((1000 * val) / 10))}%`;
}
/**
From fd9d293c1b2a22536bee084dd846e2fd4fface86 Mon Sep 17 00:00:00 2001
From: chainrez <186519033+chainrez@users.noreply.github.com>
Date: Sun, 17 Nov 2024 23:29:57 -0800
Subject: [PATCH 2/5] i18n for stat bars
---
config/i18n.json | 3 ++-
src/app/item-popup/ItemStat.tsx | 8 ++++----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/config/i18n.json b/config/i18n.json
index 8801a5f7fb..e86823a8de 100644
--- a/config/i18n.json
+++ b/config/i18n.json
@@ -1298,7 +1298,8 @@
"TotalHP": "Total HP",
"ShieldHP": "Shield HP",
"DamageResistance": "PvE Damage Resist",
- "FlinchResistance": "Flinch Resist"
+ "FlinchResistance": "Flinch Resist",
+ "WeaponPart": "Weapon Part"
},
"Storage": {
"ApiPermissionPrompt": {
diff --git a/src/app/item-popup/ItemStat.tsx b/src/app/item-popup/ItemStat.tsx
index 2fe6d88878..4f631ffe62 100644
--- a/src/app/item-popup/ItemStat.tsx
+++ b/src/app/item-popup/ItemStat.tsx
@@ -56,10 +56,10 @@ const statLabels: LookupTable
= {
type StatSegmentType = 'base' | 'parts' | 'mod' | 'masterwork';
const statStyles: Record = {
- base: [styles.base, 'Base Stat'],
- parts: [styles.parts, 'Parts'],
+ base: [styles.base, tl('Organizer.Columns.BaseStats')],
+ parts: [styles.parts, tl('Stats.WeaponPart')],
mod: [styles.mod, tl('Loadouts.Mods')],
- masterwork: [styles.masterwork, 'Masterwork'],
+ masterwork: [styles.masterwork, tl('Organizer.Columns.MasterworkStat')],
};
type StatSegments = [value: number, statSegmentType: StatSegmentType, modName?: string][];
@@ -250,7 +250,7 @@ function StatBarTooltip({ segments, stat }: { segments: StatSegments; stat: DimS
{index > 0 && val >= 0 && '+'}
{val}
- {description ?? t(i18nLabel)}
+ {description || t(i18nLabel)}
);
})}
From 51b6528fdbc2a07fdd16a3e286e04c9a70245bdb Mon Sep 17 00:00:00 2001
From: chainrez <186519033+chainrez@users.noreply.github.com>
Date: Thu, 28 Nov 2024 10:58:45 -0800
Subject: [PATCH 3/5] Item stat bars: fix up red segments
---
src/app/item-popup/ItemStat.m.scss | 11 +++-------
src/app/item-popup/ItemStat.tsx | 34 +++++++++++++-----------------
src/locale/en.json | 3 ++-
3 files changed, 20 insertions(+), 28 deletions(-)
diff --git a/src/app/item-popup/ItemStat.m.scss b/src/app/item-popup/ItemStat.m.scss
index db7db4dec7..7daa91d798 100644
--- a/src/app/item-popup/ItemStat.m.scss
+++ b/src/app/item-popup/ItemStat.m.scss
@@ -64,7 +64,7 @@
transition: width 150ms ease-in-out;
&.negative {
- background-color: #640000;
+ background-color: #7a2727;
}
// An assumption: never more than 4 part effects in a single stat?
@@ -125,6 +125,8 @@
.tooltipTotalRow {
display: grid;
+ margin-top: 2px;
+ padding-top: 1px;
grid-template-columns: subgrid;
grid-column: span 2;
border-top: 1px solid #fff;
@@ -151,13 +153,6 @@
color: $stat-masterworked;
}
-.modded {
- color: $stat-modded;
- .statName {
- font-weight: bold;
- }
-}
-
.negativeModded {
color: $red;
.statName {
diff --git a/src/app/item-popup/ItemStat.tsx b/src/app/item-popup/ItemStat.tsx
index 4f631ffe62..fe3c8ed9a0 100644
--- a/src/app/item-popup/ItemStat.tsx
+++ b/src/app/item-popup/ItemStat.tsx
@@ -202,36 +202,32 @@ export default function ItemStat({ stat, item }: { stat: DimStat; item?: DimItem
}
function StatBar({ segments, stat }: { segments: StatSegments; stat: DimStat }) {
+ // Make sure the red bar section never exceeds the blank space,
+ // which would increase the total stat bar width.
+ let leftoverSpace = Math.max(stat.maximumValue - stat.value, 0);
return (
-
- console.log(
- segments,
- sumBy(segments, ([val]) => val),
- stat.maximumValue,
- )
- }
- >
+
}
>
- {segments
- .toSorted(compareBy(([val]) => val < 0))
- // .filter(([val]) => val)
- .map(([val, statType], index) => (
+ {segments.toSorted(compareBy(([val]) => val < 0)).map(([val, statType], index) => {
+ let segmentLength = Math.abs(val) / stat.maximumValue;
+ if (val < 0) {
+ segmentLength = Math.min(segmentLength, leftoverSpace);
+ leftoverSpace -= segmentLength;
+ }
+ return (
- ))}
+ );
+ })}
);
diff --git a/src/locale/en.json b/src/locale/en.json
index 30524fd4f6..05fba52fe0 100644
--- a/src/locale/en.json
+++ b/src/locale/en.json
@@ -1286,7 +1286,8 @@
"TimeToFullHP": "Time to Full HP",
"Total": "Total",
"TotalHP": "Total HP",
- "WalkingSpeed": "Walking"
+ "WalkingSpeed": "Walking",
+ "WeaponPart": "Weapon Part"
},
"Storage": {
"ApiPermissionPrompt": {
From 91deb80e3ffca42e2fbbde289ad3b28d062d1d31 Mon Sep 17 00:00:00 2001
From: chainrez <186519033+chainrez@users.noreply.github.com>
Date: Sun, 1 Dec 2024 12:37:22 -0800
Subject: [PATCH 4/5] stat bar styling and length fine-tuning
---
src/app/item-popup/ItemStat.m.scss | 24 ++++++++------
src/app/item-popup/ItemStat.m.scss.d.ts | 2 +-
src/app/item-popup/ItemStat.tsx | 43 ++++++++++++++-----------
3 files changed, 41 insertions(+), 28 deletions(-)
diff --git a/src/app/item-popup/ItemStat.m.scss b/src/app/item-popup/ItemStat.m.scss
index 7daa91d798..a2fd3347cf 100644
--- a/src/app/item-popup/ItemStat.m.scss
+++ b/src/app/item-popup/ItemStat.m.scss
@@ -65,18 +65,19 @@
&.negative {
background-color: #7a2727;
+ order: 1; // Push negative segments to the right side of the bar
}
// An assumption: never more than 4 part effects in a single stat?
// 3 have been observed on weapons with a barrel + mag + grip.
&.parts {
- opacity: 0.9;
+ opacity: 0.88;
& + &.parts {
- opacity: 0.8;
+ opacity: 0.76;
& + &.parts {
- opacity: 0.7;
+ opacity: 0.64;
& + &.parts {
- opacity: 0.6;
+ opacity: 0.52;
}
}
}
@@ -115,6 +116,7 @@
}
.negative {
color: $red;
+ order: 1; // Push negative rows to the end of the addition list
}
.base {
opacity: 1;
@@ -123,18 +125,22 @@
opacity: 0.8;
}
- .tooltipTotalRow {
+ .tooltipNetStat {
+ order: 2; // Keep this after the negative rows
display: grid;
- margin-top: 2px;
- padding-top: 1px;
grid-template-columns: subgrid;
grid-column: span 2;
- border-top: 1px solid #fff;
- font-weight: bold;
span:nth-child(2) {
text-align: left;
}
}
+ .tooltipTotalRow {
+ // Addition-bar styling
+ margin-top: 2px;
+ padding-top: 1px;
+ border-top: 1px solid #fff;
+ font-weight: bold;
+ }
}
.qualitySummary {
diff --git a/src/app/item-popup/ItemStat.m.scss.d.ts b/src/app/item-popup/ItemStat.m.scss.d.ts
index 1dc3abf17f..cefe9bb590 100644
--- a/src/app/item-popup/ItemStat.m.scss.d.ts
+++ b/src/app/item-popup/ItemStat.m.scss.d.ts
@@ -8,7 +8,6 @@ interface CssExports {
'masterwork': string;
'masterworked': string;
'mod': string;
- 'modded': string;
'negative': string;
'negativeModded': string;
'nonDimmedStatIcons': string;
@@ -20,6 +19,7 @@ interface CssExports {
'statBarSegment': string;
'statBarTooltip': string;
'statName': string;
+ 'tooltipNetStat': string;
'tooltipTotalRow': string;
'totalRow': string;
'totalStatDetailed': string;
diff --git a/src/app/item-popup/ItemStat.tsx b/src/app/item-popup/ItemStat.tsx
index fe3c8ed9a0..ec59ff5339 100644
--- a/src/app/item-popup/ItemStat.tsx
+++ b/src/app/item-popup/ItemStat.tsx
@@ -202,9 +202,11 @@ export default function ItemStat({ stat, item }: { stat: DimStat; item?: DimItem
}
function StatBar({ segments, stat }: { segments: StatSegments; stat: DimStat }) {
+ // Make sure the combined "filled"-colored segments never exceed this.
+ let remainingFilled = stat.value;
// Make sure the red bar section never exceeds the blank space,
// which would increase the total stat bar width.
- let leftoverSpace = Math.max(stat.maximumValue - stat.value, 0);
+ let remainingEmpty = Math.max(stat.maximumValue - stat.value, 0);
return (
}
>
- {segments.toSorted(compareBy(([val]) => val < 0)).map(([val, statType], index) => {
+ {segments.map(([val, statType], index) => {
let segmentLength = Math.abs(val) / stat.maximumValue;
if (val < 0) {
- segmentLength = Math.min(segmentLength, leftoverSpace);
- leftoverSpace -= segmentLength;
+ segmentLength = Math.min(segmentLength, remainingEmpty);
+ remainingEmpty -= segmentLength;
+ } else {
+ segmentLength = Math.min(segmentLength, remainingFilled);
+ remainingFilled -= segmentLength;
}
return (
- {segments.map(([val, statType, description], index) => {
- const [typeClassName, i18nLabel] = statStyles[statType];
- const className = clsx(typeClassName, { [styles.negative]: val < 0 });
- return (
-
-
- {index > 0 && val >= 0 && '+'}
- {val}
-
- {description || t(i18nLabel)}
-
- );
- })}
-
+ {showMath &&
+ segments.map(([val, statType, description], index) => {
+ const [typeClassName, i18nLabel] = statStyles[statType];
+ const className = clsx(typeClassName, { [styles.negative]: val < 0 });
+ return (
+
+
+ {index > 0 && val >= 0 && '+'}
+ {val}
+
+ {description || t(i18nLabel)}
+
+ );
+ })}
+
{stat.value}
{stat.displayProperties.name}
From 8b7ef52c6a29ac2672269927f4e55973aba9fd36 Mon Sep 17 00:00:00 2001
From: chainrez <186519033+chainrez@users.noreply.github.com>
Date: Mon, 2 Dec 2024 14:31:08 -0800
Subject: [PATCH 5/5] bad rule
---
eslint.config.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/eslint.config.js b/eslint.config.js
index 8b83005166..4a9bb3246a 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -427,6 +427,7 @@ export default tseslint.config(
'@eslint-react/dom/no-dangerously-set-innerhtml': 'off',
'@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off',
'@eslint-react/hooks-extra/no-direct-set-state-in-use-layout-effect': 'off',
+ '@eslint-react/prefer-read-only-props': 'off',
},
},
{