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`(${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..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/placement.ts b/src/lib/components/market/react/placement.ts index 8978828f..d07f148d 100644 --- a/src/lib/components/market/react/placement.ts +++ b/src/lib/components/market/react/placement.ts @@ -1,7 +1,16 @@ import type {ScopedInjectionArgs} from '../../injectors'; -import {parseRank} from '../../../utils/skin'; +import {getFadePercentage, isBlueSkin, parseRank} from '../../../utils/skin'; +import {hasDopplerPhase} from '../../../utils/dopplers'; import type {ReactListingContext} from './listing'; +function hasSeedDetail(context: ReactListingContext): boolean { + return ( + getFadePercentage(context.listing.description.market_hash_name, context.itemInfo) !== undefined || + isBlueSkin(context.itemInfo) || + hasDopplerPhase(context.itemInfo.paintindex) + ); +} + export function findWearSpan({ scope, context, @@ -19,3 +28,21 @@ export function findWearSpan({ return undefined; } + +export function findSeedSpan({ + scope, + context, +}: ScopedInjectionArgs): HTMLSpanElement | null | undefined { + if (!hasSeedDetail(context)) return null; + + const spans = scope.querySelectorAll('span[style*="pre-wrap"]'); + for (const span of spans) { + const text = span.textContent?.trim(); + if (!text) continue; + + const value = parseInt(text, 10); + if (!Number.isNaN(value) && String(value) === text && value === context.itemInfo.paintseed) return span; + } + + return undefined; +} 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..75989ab8 --- /dev/null +++ b/src/lib/components/market/react/seed_info.ts @@ -0,0 +1,89 @@ +import {css, html, nothing} from 'lit'; +import {property, state} from 'lit/decorators.js'; + +import {CustomElement, InjectIntoScope, InjectionPosition} from '../../injectors'; +import {FloatElement} from '../../custom'; +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'; +import {ReactMarketListingScope, type ReactListingContext} from './listing'; +import {findSeedSpan} from './placement'; +import type {ItemInfo} from '../../../bridge/handlers/fetch_inspect_info'; + +/** + * Renders the fade percentage and blue-gem percentage next to the paint seed in the Steam Market beta, + * mirroring {@link ReactListingRank}. Self-contained: fetches its own blue-gem data. + */ +@CustomElement() +@InjectIntoScope(ReactMarketListingScope, { + anchor: findSeedSpan, + position: InjectionPosition.After, +}) +export class ReactListingSeedInfo extends FloatElement { + @property({attribute: false}) injectionContext?: ReactListingContext; + + @state() private bluegemData: FetchBluegemResponse | undefined; + + static styles = [ + patternDetailStyles, + css` + :host { + margin-left: 4px; + } + `, + ]; + + async connectedCallback(): Promise { + super.connectedCallback(); + + const itemInfo = this.injectionContext?.itemInfo; + if (itemInfo) { + this.bluegemData = await this.fetchBluegem(itemInfo); + } + } + + private async fetchBluegem(itemInfo: ItemInfo): Promise { + if (!isBlueSkin(itemInfo)) return undefined; + + try { + return await ClientSend(FetchBluegem, {iteminfo: itemInfo}); + } catch (e) { + return undefined; + } + } + + private get fadeDetails(): {percentage: number; className: string} | undefined { + const context = this.injectionContext; + if (!context) return undefined; + + return getFadePercentage(context.listing.description.market_hash_name, context.itemInfo); + } + + private get dopplerPhase(): string | undefined { + const itemInfo = this.injectionContext?.itemInfo; + if (!itemInfo || !hasDopplerPhase(itemInfo.paintindex)) return undefined; + + return getDopplerPhase(itemInfo.paintindex); + } + + protected render() { + if (!this.injectionContext) 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/page_scripts/market_listing.ts b/src/lib/page_scripts/market_listing.ts index e8149f06..fedf15ec 100644 --- a/src/lib/page_scripts/market_listing.ts +++ b/src/lib/page_scripts/market_listing.ts @@ -2,6 +2,7 @@ import {init} from './utils'; import '../components/market/item_row_wrapper'; import '../components/market/utility_belt'; import '../components/market/react/rank'; +import '../components/market/react/seed_info'; init('src/lib/page_scripts/market_listing.js', main); 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;