diff --git a/src/lib/components/common/item_holder_metadata.ts b/src/lib/components/common/item_holder_metadata.ts
index d75f79d7..0a572ea1 100644
--- a/src/lib/components/common/item_holder_metadata.ts
+++ b/src/lib/components/common/item_holder_metadata.ts
@@ -13,17 +13,19 @@ import {
isCharm,
isHighlightCharm,
} from '../../utils/skin';
-import {isSkin, floor} from '../../utils/skin';
+import {isSkin} from '../../utils/skin';
import {getRankColour} from '../../utils/ranks';
import {Observe} from '../../utils/observers';
import {FetchBluegem, FetchBluegemResponse} from '../../bridge/handlers/fetch_bluegem';
import {ClientSend} from '../../bridge/client';
+import {renderBluegemPercentage, renderFadePercentage, patternDetailStyles} from './pattern_details';
// Generic annotator of item holder metadata (float, seed, etc...)
// Must be extended to use as a component
export abstract class ItemHolderMetadata extends FloatElement {
static styles = [
...FloatElement.styles,
+ patternDetailStyles,
css`
.float {
position: absolute;
@@ -40,32 +42,10 @@ export abstract class ItemHolderMetadata extends FloatElement {
font-size: 12px;
}
- .fade-base {
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- }
-
- .fade {
- background-image: -webkit-linear-gradient(0deg, #d9bba5 0%, #e5903b 33%, #db5977 66%, #6775e1 100%);
- }
-
- .amber-fade {
- background-image: -webkit-linear-gradient(0deg, #627d66 0%, #896944 50%, #7e4201 100%);
- }
-
- .acid-fade {
- background-image: -webkit-linear-gradient(0deg, #2b441b 0%, #3e6b2f 11%, #82a64a 66%, #c1a16c 100%);
- }
-
.csfloat-shine-fade-text {
font-weight: 1000;
-webkit-text-stroke: 1px black;
}
-
- .bluegem {
- color: deepskyblue;
- }
`,
];
@@ -108,7 +88,7 @@ export abstract class ItemHolderMetadata extends FloatElement {
if (!this.itemInfo || !this.asset) return html``;
if (isSkin(this.asset)) {
- const fadeDetails = this.asset && getFadePercentage(this.asset, this.itemInfo);
+ const fadeDetails = this.asset && getFadePercentage(this.asset.market_hash_name, this.itemInfo);
if (fadeDetails?.percentage === 100) {
$J(this).parent().addClass('full-fade-border');
@@ -121,17 +101,10 @@ export abstract class ItemHolderMetadata extends FloatElement {
${formatFloatWithRank(this.itemInfo, 6)}
${formatSeed(this.itemInfo)}
- ${fadeDetails !== undefined
- ? html`(${floor(fadeDetails.percentage, 1)}%)`
- : nothing}
- ${this.bluegemData
- ? html`(${floor(this.bluegemData.playside_blue, 1)}%)`
+ ${fadeDetails
+ ? renderFadePercentage(fadeDetails, 1, rank && rank <= 5 ? 'csfloat-shine-fade-text' : '')
: nothing}
+ ${this.bluegemData ? renderBluegemPercentage(this.bluegemData) : nothing}
`;
diff --git a/src/lib/components/common/pattern_details.ts b/src/lib/components/common/pattern_details.ts
new file mode 100644
index 00000000..fab6ade4
--- /dev/null
+++ b/src/lib/components/common/pattern_details.ts
@@ -0,0 +1,57 @@
+import {css, html, TemplateResult} from 'lit';
+
+import {floor} from '../../utils/skin';
+import {FetchBluegemResponse} from '../../bridge/handlers/fetch_bluegem';
+
+export const patternDetailStyles = css`
+ .fade-base {
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+
+ .fade {
+ background-image: -webkit-linear-gradient(0deg, #d9bba5 0%, #e5903b 33%, #db5977 66%, #6775e1 100%);
+ }
+
+ .amber-fade {
+ background-image: -webkit-linear-gradient(0deg, #627d66 0%, #896944 50%, #7e4201 100%);
+ }
+
+ .acid-fade {
+ background-image: -webkit-linear-gradient(0deg, #2b441b 0%, #3e6b2f 11%, #82a64a 66%, #c1a16c 100%);
+ }
+
+ .bluegem {
+ color: deepskyblue;
+ }
+`;
+
+/**
+ * Renders HTML for the fade percentage.
+ * Requires the component to include {@link patternDetailStyles} in its static styles.
+ */
+export function renderFadePercentage(
+ fade: {percentage: number; className: string},
+ precision = 1,
+ extraClasses = ''
+): TemplateResult<1> {
+ return html``;
+}
+
+/**
+ * Renders HTML for the blue gem percentage.
+ * Requires the component to include {@link patternDetailStyles} in its static styles.
+ */
+export function renderBluegemPercentage(bluegemData: FetchBluegemResponse, showBackside = false): TemplateResult<1> {
+ // Some skins got only one blue value
+ if (showBackside && bluegemData.backside_blue !== undefined) {
+ return html`(${floor(bluegemData.playside_blue, 1)}% / ${floor(bluegemData.backside_blue, 1)}%)`;
+ }
+
+ return html`(${floor(bluegemData.playside_blue, 1)}%)`;
+}
diff --git a/src/lib/components/inventory/selected_item_info.ts b/src/lib/components/inventory/selected_item_info.ts
index a76bf0c6..cbb6a3e3 100644
--- a/src/lib/components/inventory/selected_item_info.ts
+++ b/src/lib/components/inventory/selected_item_info.ts
@@ -131,7 +131,10 @@ export class SelectedItemInfo extends FloatElement {
containerChildren.push(html`
Paint Seed: ${formatSeed(this.itemInfo)}
`);
// Fade skins
- const fadePercentage = getFadePercentage(this.asset.description, this.itemInfo)?.percentage;
+ const fadePercentage = getFadePercentage(
+ this.asset.description.market_hash_name,
+ this.itemInfo
+ )?.percentage;
if (fadePercentage !== undefined) {
containerChildren.push(html`Fade: ${floor(fadePercentage, 5)}%
`);
}
diff --git a/src/lib/components/market/item_row_wrapper.ts b/src/lib/components/market/item_row_wrapper.ts
index c78a87aa..39234d36 100644
--- a/src/lib/components/market/item_row_wrapper.ts
+++ b/src/lib/components/market/item_row_wrapper.ts
@@ -247,7 +247,8 @@ export class ItemRowWrapper extends FloatElement {
}
if (this.itemInfo && isSkin(this.asset)) {
- const fadePercentage = this.asset && getFadePercentage(this.asset, this.itemInfo)?.percentage;
+ const fadePercentage =
+ this.asset && getFadePercentage(this.asset.market_hash_name, this.itemInfo)?.percentage;
return html`
diff --git a/src/lib/components/market/react/listing.ts b/src/lib/components/market/react/listing.ts
index f5d24b1b..3f1825c5 100644
--- a/src/lib/components/market/react/listing.ts
+++ b/src/lib/components/market/react/listing.ts
@@ -6,6 +6,7 @@ import {CustomElement, InjectAppend, InjectionMode} from '../../injectors';
import {isReactSteamMarket} from '../mode';
import {gFloatFetcher} from '../../../services/float_fetcher';
import {BetaListingRank} from './rank';
+import {BetaListingSeedInfo} from './seed_info';
import {getFiberProps} from '../../../utils/fiber';
import type {MarketListing, MarketListingProps} from './types';
@@ -21,6 +22,7 @@ import type {MarketListing, MarketListingProps} from './types';
)
export class BetaListingEnhancer extends FloatElement {
private rankInjected = false;
+ private seedInfoInjected = false;
static styles = [
css`
@@ -70,6 +72,10 @@ export class BetaListingEnhancer extends FloatElement {
return listing.asset.assetid;
}
+ get marketHashName(): string | null {
+ return this.listing?.description.market_hash_name ?? null;
+ }
+
get targetFloat(): number | null {
const wearProp = this.listing?.asset.asset_properties?.find((p) => p.propertyid === 2);
// this is a number in the React properties, but a string in the rgAsset properties
@@ -105,6 +111,7 @@ export class BetaListingEnhancer extends FloatElement {
}
this.injectRank(info);
+ this.injectSeedInfo(info);
}
private injectRank(info: ItemInfo): void {
@@ -118,4 +125,17 @@ export class BetaListingEnhancer extends FloatElement {
// Append into the card; the element repositions itself correctly.
this.card.appendChild(rank);
}
+
+ private injectSeedInfo(info: ItemInfo): void {
+ if (this.seedInfoInjected) return;
+ this.seedInfoInjected = true;
+
+ const seedInfo = BetaListingSeedInfo.elem() as BetaListingSeedInfo;
+ seedInfo.itemInfo = info;
+ seedInfo.card = this.card;
+ seedInfo.targetPaintSeed = info.paintseed;
+ seedInfo.marketHashName = this.marketHashName ?? '';
+ // Append into the card; the element repositions itself correctly.
+ this.card.appendChild(seedInfo);
+ }
}
diff --git a/src/lib/components/market/react/seed_info.ts b/src/lib/components/market/react/seed_info.ts
new file mode 100644
index 00000000..5a21a062
--- /dev/null
+++ b/src/lib/components/market/react/seed_info.ts
@@ -0,0 +1,107 @@
+import {css, html, nothing} from 'lit';
+import {property, state} from 'lit/decorators.js';
+
+import {CustomElement} from '../../injectors';
+import {FloatElement} from '../../custom';
+import {ItemInfo} from '../../../bridge/handlers/fetch_inspect_info';
+import {getFadePercentage, isBlueSkin} from '../../../utils/skin';
+import {getDopplerPhase, hasDopplerPhase} from '../../../utils/dopplers';
+import {ClientSend} from '../../../bridge/client';
+import {FetchBluegem, FetchBluegemResponse} from '../../../bridge/handlers/fetch_bluegem';
+import {renderBluegemPercentage, renderFadePercentage, patternDetailStyles} from '../../common/pattern_details';
+
+/**
+ * Renders the fade percentage and blue-gem percentage next to the paint seed in the Steam Market beta,
+ * mirroring {@link BetaListingRank}. Self-contained: fetches its own blue-gem data.
+ */
+@CustomElement()
+export class BetaListingSeedInfo extends FloatElement {
+ @property({type: Object}) itemInfo!: ItemInfo;
+ @property({attribute: false}) card!: HTMLElement;
+ @property({attribute: false}) targetPaintSeed!: number | null;
+ @property({attribute: false}) marketHashName!: string;
+
+ @state() private bluegemData: FetchBluegemResponse | undefined;
+
+ static styles = [
+ patternDetailStyles,
+ css`
+ :host {
+ margin-left: 4px;
+ }
+ `,
+ ];
+
+ private injected = false;
+
+ connectedCallback(): void {
+ super.connectedCallback();
+ void this.init();
+ }
+
+ private async init(): Promise {
+ if (isBlueSkin(this.itemInfo)) {
+ try {
+ this.bluegemData = await ClientSend(FetchBluegem, {iteminfo: this.itemInfo});
+ } catch (e) {
+ this.bluegemData = undefined;
+ }
+ }
+
+ this.placeNextToSeed();
+ }
+
+ private get fadeDetails(): {percentage: number; className: string} | undefined {
+ return this.marketHashName ? getFadePercentage(this.marketHashName, this.itemInfo) : undefined;
+ }
+
+ private get dopplerPhase(): string | undefined {
+ return this.itemInfo && hasDopplerPhase(this.itemInfo.paintindex)
+ ? getDopplerPhase(this.itemInfo.paintindex)
+ : undefined;
+ }
+
+ private placeNextToSeed(): void {
+ if (this.injected || !this.itemInfo || !this.card) return;
+ if (this.fadeDetails === undefined && !this.bluegemData && !this.dopplerPhase) return;
+
+ const seedSpan = this.findSeedSpan();
+ if (!seedSpan) return;
+
+ this.injected = true;
+ seedSpan.insertAdjacentElement('afterend', this);
+ }
+
+ private findSeedSpan(): HTMLSpanElement | null {
+ if (this.targetPaintSeed === null) return null;
+
+ const spans = this.card.querySelectorAll('span[style*="pre-wrap"]');
+ for (const span of spans) {
+ const text = span.textContent?.trim();
+ if (!text) continue;
+ const value = parseInt(text, 10);
+ // String(value) === text avoids matching the decimal float span.
+ if (!Number.isNaN(value) && String(value) === text && value === this.targetPaintSeed) return span;
+ }
+ return null;
+ }
+
+ protected render() {
+ if (!this.itemInfo || !this.card) return nothing;
+
+ if (this.fadeDetails) {
+ return renderFadePercentage(this.fadeDetails, 2);
+ }
+
+ if (this.bluegemData) {
+ return renderBluegemPercentage(this.bluegemData, true);
+ }
+
+ const phase = this.dopplerPhase;
+ if (phase) {
+ return html`(${phase})`;
+ }
+
+ return nothing;
+ }
+}
diff --git a/src/lib/components/market/sort_listings.ts b/src/lib/components/market/sort_listings.ts
index dce0a0b2..8e43ac70 100644
--- a/src/lib/components/market/sort_listings.ts
+++ b/src/lib/components/market/sort_listings.ts
@@ -59,7 +59,7 @@ export class SortListings extends FloatElement {
const asset = g_rgAssets[AppId.CSGO][ContextId.PRIMARY][listingInfo.asset.id];
- return getFadeParams(asset) !== undefined;
+ return getFadeParams(asset.market_hash_name) !== undefined;
}
connectedCallback() {
@@ -130,7 +130,7 @@ export class SortListings extends FloatElement {
info,
listingId: listingId!,
converted_price: listingInfo?.converted_price || 0,
- fadePercentage: (asset && getFadePercentage(asset, info)?.percentage) || 0,
+ fadePercentage: (asset && getFadePercentage(asset.market_hash_name, info)?.percentage) || 0,
};
} catch (error) {
console.error(`CSFloat: Failed to fetch float for listing ${listingId}:`, error);
diff --git a/src/lib/utils/skin.ts b/src/lib/utils/skin.ts
index c6408969..538281c0 100644
--- a/src/lib/utils/skin.ts
+++ b/src/lib/utils/skin.ts
@@ -183,7 +183,7 @@ export function isBlueSkin(itemInfo: ItemInfo): boolean {
);
}
-export function getFadeParams(asset: rgAsset):
+export function getFadeParams(marketHashName: string):
| {
calculator: typeof FadeCalculator | typeof AcidFadeCalculator | typeof AmberFadeCalculator;
weaponName: string;
@@ -198,7 +198,7 @@ export function getFadeParams(asset: rgAsset):
for (const [fadeType, calculator] of Object.entries(FADE_TYPE_TO_CALCULATOR)) {
for (const supportedWeapon of calculator.getSupportedWeapons()) {
- if (asset.market_hash_name.includes(`${supportedWeapon} | ${fadeType}`)) {
+ if (marketHashName.includes(`${supportedWeapon} | ${fadeType}`)) {
return {
calculator,
weaponName: supportedWeapon.toString(),
@@ -210,10 +210,10 @@ export function getFadeParams(asset: rgAsset):
}
export function getFadePercentage(
- asset: rgAsset,
+ marketHashName: string,
itemInfo: ItemInfo
): {percentage: number; className: string} | undefined {
- const fadeInfo = getFadeParams(asset);
+ const fadeInfo = getFadeParams(marketHashName);
if (fadeInfo !== undefined) {
const {calculator, weaponName, className} = fadeInfo;