From b852f00703f68fbecc99dc4fe28439f4f4b0696e Mon Sep 17 00:00:00 2001 From: GODrums Date: Tue, 9 Jun 2026 20:55:38 +0200 Subject: [PATCH 1/3] add fade & blue percentages --- .../components/common/item_holder_metadata.ts | 41 ++------ src/lib/components/common/pattern_details.ts | 58 +++++++++++ .../inventory/selected_item_info.ts | 2 +- src/lib/components/market/item_row_wrapper.ts | 2 +- src/lib/components/market/react/listing.ts | 20 ++++ src/lib/components/market/react/seed_info.ts | 96 +++++++++++++++++++ src/lib/components/market/sort_listings.ts | 4 +- src/lib/utils/skin.ts | 8 +- 8 files changed, 189 insertions(+), 42 deletions(-) create mode 100644 src/lib/components/common/pattern_details.ts create mode 100644 src/lib/components/market/react/seed_info.ts 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..7f821007 --- /dev/null +++ b/src/lib/components/common/pattern_details.ts @@ -0,0 +1,58 @@ +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`(${floor(fade.percentage, precision)}%)`; +} + +/** + * 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..046047b0 100644 --- a/src/lib/components/inventory/selected_item_info.ts +++ b/src/lib/components/inventory/selected_item_info.ts @@ -131,7 +131,7 @@ 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..2d8d9992 100644 --- a/src/lib/components/market/item_row_wrapper.ts +++ b/src/lib/components/market/item_row_wrapper.ts @@ -247,7 +247,7 @@ 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..c4218cf9 --- /dev/null +++ b/src/lib/components/market/react/seed_info.ts @@ -0,0 +1,96 @@ +import {css, 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 {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 placeNextToSeed(): void { + if (this.injected || !this.itemInfo || !this.card) return; + if (this.fadeDetails === undefined && !this.bluegemData) 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; + + const fade = this.fadeDetails; + if (fade) { + return renderFadePercentage(fade, 2); + } + + if (this.bluegemData) { + return renderBluegemPercentage(this.bluegemData, true); + } + + 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; From de08f8e98d47982b56da60e2145a3001fa43c857 Mon Sep 17 00:00:00 2001 From: GODrums Date: Fri, 12 Jun 2026 02:36:06 +0200 Subject: [PATCH 2/3] add doppler phase --- src/lib/components/market/react/seed_info.ts | 21 +++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/lib/components/market/react/seed_info.ts b/src/lib/components/market/react/seed_info.ts index c4218cf9..5a21a062 100644 --- a/src/lib/components/market/react/seed_info.ts +++ b/src/lib/components/market/react/seed_info.ts @@ -1,10 +1,11 @@ -import {css, nothing} from 'lit'; +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'; @@ -54,9 +55,15 @@ export class BetaListingSeedInfo extends FloatElement { 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) return; + if (this.fadeDetails === undefined && !this.bluegemData && !this.dopplerPhase) return; const seedSpan = this.findSeedSpan(); if (!seedSpan) return; @@ -82,15 +89,19 @@ export class BetaListingSeedInfo extends FloatElement { protected render() { if (!this.itemInfo || !this.card) return nothing; - const fade = this.fadeDetails; - if (fade) { - return renderFadePercentage(fade, 2); + 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; } } From 94dbbc444b6c6de2bd24cce38870d2b4f73b6833 Mon Sep 17 00:00:00 2001 From: GODrums Date: Fri, 12 Jun 2026 02:37:24 +0200 Subject: [PATCH 3/3] chore: run format --- src/lib/components/common/pattern_details.ts | 9 ++++----- src/lib/components/inventory/selected_item_info.ts | 5 ++++- src/lib/components/market/item_row_wrapper.ts | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib/components/common/pattern_details.ts b/src/lib/components/common/pattern_details.ts index 7f821007..fab6ade4 100644 --- a/src/lib/components/common/pattern_details.ts +++ b/src/lib/components/common/pattern_details.ts @@ -36,17 +36,16 @@ export function renderFadePercentage( precision = 1, extraClasses = '' ): TemplateResult<1> { - return html`(${floor(fade.percentage, precision)}%)`; + return html`(${floor(fade.percentage, precision)}%)`; } /** * 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> { +export function renderBluegemPercentage(bluegemData: FetchBluegemResponse, showBackside = false): TemplateResult<1> { // Some skins got only one blue value if (showBackside && bluegemData.backside_blue !== undefined) { return html`Paint Seed: ${formatSeed(this.itemInfo)}
`); // Fade skins - const fadePercentage = getFadePercentage(this.asset.description.market_hash_name, 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 2d8d9992..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.market_hash_name, this.itemInfo)?.percentage; + const fadePercentage = + this.asset && getFadePercentage(this.asset.market_hash_name, this.itemInfo)?.percentage; return html`