From fa7276a53c9bac7bba33581f1c940d5e0e5e6d91 Mon Sep 17 00:00:00 2001 From: xenticore Date: Wed, 2 Apr 2025 18:54:39 -0400 Subject: [PATCH 1/8] feat: add support for festive weapon variants on schema update, festive variants are linked. on content injection, if if finds a festive variant, it adds price rows for Festive and Strange Festive. --- __tests__/schema.test.ts | 96 +++++++++++++++++++++++++++++++++--- src/content/content.ts | 36 ++++++++++++++ src/content/schemaService.ts | 22 +++++++++ 3 files changed, 148 insertions(+), 6 deletions(-) diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts index 34fb440..d5b9853 100644 --- a/__tests__/schema.test.ts +++ b/__tests__/schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, mock } from "bun:test"; -import { ItemSchema, ItemSlot, getItemIndexByName, getTradableStatusByDefindex, getTradableStatusByName, prepareSchema } from '../src/content/schemaService' +import { ItemSchema, ItemSlot, getItemIndexByName, getTradableStatusByDefindex, getTradableStatusByName, linkFestiveVariants, prepareSchema } from '../src/content/schemaService' // Mock the storage and log functions mock.module('../src/content/storage', () => ({ @@ -19,35 +19,48 @@ const mockSchema: ItemSchema = { slot: ItemSlot.Primary, tradable: false, hasAustraliumVariant: false, - canKillstreakify: true + canKillstreakify: true, + festiveVariant: null }, '208': { name: 'Flame Thrower', slot: ItemSlot.Primary, tradable: true, hasAustraliumVariant: true, - canKillstreakify: true + canKillstreakify: true, + festiveVariant: 659 + }, + '659': { + name: 'Festive Flame Thrower', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null }, '5021': { name: 'Mann Co. Supply Crate Key', slot: ItemSlot.Tool, tradable: true, hasAustraliumVariant: false, - canKillstreakify: false + canKillstreakify: false, + festiveVariant: null }, '15141': { name: 'Flame Thrower', slot: ItemSlot.Primary, tradable: true, hasAustraliumVariant: false, - canKillstreakify: true + canKillstreakify: true, + festiveVariant: null }, '69420': { name: 'Non-Tradable Item', slot: ItemSlot.Misc, tradable: false, hasAustraliumVariant: false, - canKillstreakify: false + canKillstreakify: false, + festiveVariant: null } } @@ -117,4 +130,75 @@ describe('Schema Service', () => { } }); }); + + describe('linkFestiveVariants', () => { + test('should link festive variants to their original counterparts', () => { + const testSchema = { + '21': { // Stock (should be ignored) + name: 'Flame Thrower', + slot: ItemSlot.Primary, + tradable: false, + hasAustraliumVariant: false, + canKillstreakify: false, + festiveVariant: null + }, + '208': { // Original Flame Thrower + name: 'Flame Thrower', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: true, + canKillstreakify: true, + festiveVariant: null + }, + '659': { // Festive Flame Thrower (should be detected) + name: 'Festive Flame Thrower', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null + }, + '15141': { // Decorated (should be ignored) + name: 'Flame Thrower', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null + } + }; + const mockResponseItems = [ + { item_class: 'tf_weapon_flamethrower', defindex: 21, item_name: 'Flame Thrower' }, // Incorrect; stock + { item_class: 'tf_weapon_flamethrower', defindex: 208, item_name: 'Flame Thrower' }, // Original + { item_class: 'tf_weapon_flamethrower', defindex: 659, item_name: 'Festive Flame Thrower' }, // Festive + { item_class: 'tf_weapon_flamethrower', defindex: 15141, item_name: 'Flame Thrower' }, // Incorrect; decorated + ]; + linkFestiveVariants(mockResponseItems, testSchema) + expect(testSchema['21'].festiveVariant).toBeNull() + expect(testSchema['208'].festiveVariant).toBe(659) + expect(testSchema['659'].festiveVariant).toBeNull(); + expect(testSchema['15141'].festiveVariant).toBeNull() + }) + + test('should not link if no festive variant exists', () => { + const testSchema = { + '163': { + name: 'Crit-a-Cola', + slot: ItemSlot.Secondary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: false, + festiveVariant: null + } + }; + + const mockResponseItems = [ + { item_class: 'tf_weapon_lunchbox_drink', defindex: 163, item_name: 'Crit-a-Cola' } + ]; + + linkFestiveVariants(mockResponseItems, testSchema); + + expect(testSchema['163'].festiveVariant).toBeNull(); + }); + }); }) \ No newline at end of file diff --git a/src/content/content.ts b/src/content/content.ts index c41d706..088fea0 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -237,6 +237,42 @@ async function inject() { })) } + // Check item schema for Festive variant of current defindex + if(itemSchema[itemIndex].festiveVariant != null) { + promises.push(new Promise(async (resolve) => { + logDebug(`Fetching price for Festive ${itemName}`) + var data: ItemPriceData | null + try { + data = await fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};6`, currentTime); + updateTime = new Date(data.update) + } catch { + log(`Festive ${itemName} is unpriced or unavailable, skipping...`) + } + + const priceRow = createPriceRow($T("Festive"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Festive_weapons") + + priceRows.push({quality: 97, row: priceRow}) + resolve() + return + })) + promises.push(new Promise(async (resolve) => { + logDebug(`Fetching price for Strange Festive ${itemName}`) + var data: ItemPriceData | null + try { + data = await fetchPrice(token, `${itemSchema[itemIndex].festiveVariant};11`, currentTime); + updateTime = new Date(data.update) + } catch { + log(`Strange Festive ${itemName} is unpriced or unavailable, skipping...`) + } + + const priceRow = createPriceRow($T("Strange Festive"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Festive_weapons") + + priceRows.push({quality: 98, row: priceRow}) + resolve() + return + })) + } + Promise.all(promises).then(() => { priceRows.sort((a, b) => { // Sort 6 first always, then numerically diff --git a/src/content/schemaService.ts b/src/content/schemaService.ts index e86a2f0..3521169 100644 --- a/src/content/schemaService.ts +++ b/src/content/schemaService.ts @@ -38,6 +38,7 @@ export class ItemSchema { slot: ItemSlot, tradable: Boolean, hasAustraliumVariant: Boolean, + festiveVariant: number | null canKillstreakify: Boolean }; } @@ -70,6 +71,25 @@ export function getTradableStatusByName(schema: ItemSchema, name: string, exclud return true } +export function linkFestiveVariants(items: Array<{item_class: string, defindex: number, item_name: string}>, schema: ItemSchema): void { + if(!schema) return + items.filter(item => + item.item_class != null && + item.item_class.startsWith('tf_weapon') && + item.item_name.startsWith('Festive ') + ).forEach(festive => { + const originalName = festive.item_name.slice(8); // "Festive " is 8 chars + const original = items.find(item => item.item_name === originalName && item.defindex > 30 && (item.defindex < 15000 || item.defindex >= 16000)); + + if (original) { + if(schema[original.defindex]) { + schema[original.defindex].festiveVariant = festive.defindex; + console.log(`original:${original.defindex},festive:${festive.defindex}`); + } + } + }); +} + export async function wipeSchema(): Promise { await setStorageValue(storage_version, __VERSION__) await setStorageValue(storage_schema, null) @@ -149,6 +169,8 @@ export async function prepareSchema(): Promise { } }); + linkFestiveVariants(responseItems, cacheItems) + await setStorageValue(storage_schema, (cacheItems)); itemSchema = cacheItems await setStorageValue(storage_version, __VERSION__); From df2e8ede6d6f47fa0757a8de475917ce0bfe4099 Mon Sep 17 00:00:00 2001 From: xenticore Date: Mon, 7 Apr 2025 13:30:39 -0400 Subject: [PATCH 2/8] fix: allow retries if API fails --- src/content/pricing/pricestf.ts | 39 ++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/content/pricing/pricestf.ts b/src/content/pricing/pricestf.ts index bf56fa0..1d6989d 100644 --- a/src/content/pricing/pricestf.ts +++ b/src/content/pricing/pricestf.ts @@ -1,5 +1,6 @@ declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise import '../GM_fetch' +import { logDebug } from '../utils/log' async function getPricesToken(): Promise { return new Promise((resolve, reject) => { @@ -37,7 +38,7 @@ class PricesResponse { * @returns {Promise} Object containing 'keys' and 'metal' prices * @throws When authentication fails or API returns non-200 status code */ -async function priceUsingPricesTF(token: string, sku: string): Promise { +async function priceUsingPricesTF(token: string, sku: string, retries: number = 3): Promise { // prices.tf // https://api2.prices.tf/prices/${sku} // Authorization: Bearer ${token} @@ -65,11 +66,37 @@ async function priceUsingPricesTF(token: string, sku: string): Promise 0) { + logDebug(`Cloudflare rate limit exceeded, trying again after 2 seconds, ${retries} retries left`) + await new Promise(resolve => setTimeout(resolve, 2000)); + try { + const retryResult = await priceUsingPricesTF(token, sku, retries - 1); + resolve(retryResult); + } catch (err) { + reject(err); + } + } else { + reject("Cloudflare rate limit exceeded, stopping") + } + break; + default: + // Something went wrong + logDebug(`Received ${response.status} error while pricing ${sku}`) + logDebug(`${JSON.stringify(response.headers)}`) + reject("Unknown error") + break; } }) } From 8fa0eb55e15126554b2d59a0ff91de986a3d4238 Mon Sep 17 00:00:00 2001 From: xenticore Date: Mon, 7 Apr 2025 14:20:20 -0400 Subject: [PATCH 3/8] feat: add botkiller variants to schema cache --- src/content/schemaService.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/content/schemaService.ts b/src/content/schemaService.ts index 3521169..0a95ca8 100644 --- a/src/content/schemaService.ts +++ b/src/content/schemaService.ts @@ -39,6 +39,7 @@ export class ItemSchema { tradable: Boolean, hasAustraliumVariant: Boolean, festiveVariant: number | null + botkillerVariants: Array | null canKillstreakify: Boolean }; } @@ -84,7 +85,27 @@ export function linkFestiveVariants(items: Array<{item_class: string, defindex: if (original) { if(schema[original.defindex]) { schema[original.defindex].festiveVariant = festive.defindex; - console.log(`original:${original.defindex},festive:${festive.defindex}`); + } + } + }); +} + +export function linkBotkillerVariants(items: Array<{item_class: string, defindex: number, item_name: string, item_type_name: string}>, schema: ItemSchema): void { + if(!schema) return + items.filter(item => + item.item_class != null && + item.item_class.startsWith('tf_weapon') && + item.item_name.includes('Botkiller') + ).forEach(botkiller => { + const originalName = botkiller.item_type_name + const original = items.find(item => item.item_name === originalName && item.defindex > 30 && (item.defindex < 15000 || item.defindex >= 16000)); + if (original) { + if(schema[original.defindex]) { + if(schema[original.defindex].botkillerVariants == null) { + // init array + schema[original.defindex].botkillerVariants = new Array() + } + schema[original.defindex].botkillerVariants.push(botkiller.defindex) } } }); @@ -170,6 +191,7 @@ export async function prepareSchema(): Promise { }); linkFestiveVariants(responseItems, cacheItems) + linkBotkillerVariants(responseItems, cacheItems) await setStorageValue(storage_schema, (cacheItems)); itemSchema = cacheItems From 9a8ce2431322a620771c152bfb9360be293a66e0 Mon Sep 17 00:00:00 2001 From: xenticore Date: Mon, 7 Apr 2025 15:40:44 -0400 Subject: [PATCH 4/8] test: add botkiller detection tests --- __tests__/schema.test.ts | 160 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 12 deletions(-) diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts index d5b9853..bd1d516 100644 --- a/__tests__/schema.test.ts +++ b/__tests__/schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, mock } from "bun:test"; -import { ItemSchema, ItemSlot, getItemIndexByName, getTradableStatusByDefindex, getTradableStatusByName, linkFestiveVariants, prepareSchema } from '../src/content/schemaService' +import { ItemSchema, ItemSlot, getItemIndexByName, getTradableStatusByDefindex, getTradableStatusByName, linkBotkillerVariants, linkFestiveVariants, prepareSchema } from '../src/content/schemaService' // Mock the storage and log functions mock.module('../src/content/storage', () => ({ @@ -20,7 +20,8 @@ const mockSchema: ItemSchema = { tradable: false, hasAustraliumVariant: false, canKillstreakify: true, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null }, '208': { name: 'Flame Thrower', @@ -28,7 +29,8 @@ const mockSchema: ItemSchema = { tradable: true, hasAustraliumVariant: true, canKillstreakify: true, - festiveVariant: 659 + festiveVariant: 659, + botkillerVariants: [798, 807] }, '659': { name: 'Festive Flame Thrower', @@ -36,7 +38,26 @@ const mockSchema: ItemSchema = { tradable: true, hasAustraliumVariant: false, canKillstreakify: true, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null + }, + '798': { + name: 'Silver Botkiller Flame Thrower Mk.I', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null, + botkillerVariants: null + }, + '807': { + name: 'Gold Botkiller Flame Thrower Mk.I', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null, + botkillerVariants: null }, '5021': { name: 'Mann Co. Supply Crate Key', @@ -44,7 +65,8 @@ const mockSchema: ItemSchema = { tradable: true, hasAustraliumVariant: false, canKillstreakify: false, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null }, '15141': { name: 'Flame Thrower', @@ -52,7 +74,8 @@ const mockSchema: ItemSchema = { tradable: true, hasAustraliumVariant: false, canKillstreakify: true, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null }, '69420': { name: 'Non-Tradable Item', @@ -60,7 +83,8 @@ const mockSchema: ItemSchema = { tradable: false, hasAustraliumVariant: false, canKillstreakify: false, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null } } @@ -140,7 +164,8 @@ describe('Schema Service', () => { tradable: false, hasAustraliumVariant: false, canKillstreakify: false, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null }, '208': { // Original Flame Thrower name: 'Flame Thrower', @@ -148,7 +173,8 @@ describe('Schema Service', () => { tradable: true, hasAustraliumVariant: true, canKillstreakify: true, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null }, '659': { // Festive Flame Thrower (should be detected) name: 'Festive Flame Thrower', @@ -156,7 +182,8 @@ describe('Schema Service', () => { tradable: true, hasAustraliumVariant: false, canKillstreakify: true, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null }, '15141': { // Decorated (should be ignored) name: 'Flame Thrower', @@ -164,7 +191,8 @@ describe('Schema Service', () => { tradable: true, hasAustraliumVariant: false, canKillstreakify: true, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null } }; const mockResponseItems = [ @@ -188,7 +216,8 @@ describe('Schema Service', () => { tradable: true, hasAustraliumVariant: false, canKillstreakify: false, - festiveVariant: null + festiveVariant: null, + botkillerVariants: null } }; @@ -201,4 +230,111 @@ describe('Schema Service', () => { expect(testSchema['163'].festiveVariant).toBeNull(); }); }); + + describe('linkBotkillerVariants', () => { + test('should link botkiller variants to their original counterparts', () => { + const testSchema = { + '21': { // Original Rocket Launcher + name: 'Flame Thrower', + slot: ItemSlot.Primary, + tradable: false, + hasAustraliumVariant: false, + canKillstreakify: false, + festiveVariant: null, + botkillerVariants: null + }, + '208': { // Original Flame Thrower + name: 'Flame Thrower', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: true, + canKillstreakify: true, + festiveVariant: null, + botkillerVariants: null + }, + '798': { + name: 'Silver Botkiller Flame Thrower Mk. I', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null, + botkillerVariants: null + }, + '807': { + name: 'Gold Botkiller Flame Thrower Mk. I', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null, + botkillerVariants: null + }, + '15141': { // Decorated (should be ignored) + name: 'Flame Thrower', + slot: ItemSlot.Primary, + tradable: true, + hasAustraliumVariant: false, + canKillstreakify: true, + festiveVariant: null, + botkillerVariants: null + } + }; + const mockResponseItems = [ + { item_class: 'tf_weapon_flamethrower', defindex: 21, item_name: 'Flame Thrower', item_type_name: 'Flame Thrower' + }, // Incorrect; stock + { + item_class: 'tf_weapon_flamethrower', + defindex: 208, + item_name: 'Flame Thrower', + item_type_name: 'Flame Thrower' + }, + { + item_class: 'tf_weapon_flamethrower', + defindex: 798, + item_name: 'Silver Botkiller Flame Thrower Mk.I', + item_type_name: 'Flame Thrower' + }, + { + item_class: 'tf_weapon_flamethrower', + defindex: 807, + item_name: 'Gold Botkiller Flame Thrower Mk.I', + item_type_name: 'Flame Thrower' + }, + { item_class: 'tf_weapon_flamethrower', defindex: 15141, item_name: 'Flame Thrower', item_type_name: 'Flame Thrower' }, // Incorrect; decorated + ]; + linkBotkillerVariants(mockResponseItems, testSchema); + expect(testSchema['21'].botkillerVariants).toBeNull() + expect(testSchema['208'].botkillerVariants).toEqual([798, 807]); + expect(testSchema['798'].botkillerVariants).toBeNull(); + expect(testSchema['807'].botkillerVariants).toBeNull(); + expect(testSchema['15141'].botkillerVariants).toBeNull() + }); + + test('should not link if no botkiller variants exist', () => { + const testSchema = { + '163': { // Crit-a-Cola + name: 'Crit-a-Cola', + slot: ItemSlot.Secondary, + tradable: true, + hasAustraliumVariant: false, + festiveVariant: null, + canKillstreakify: false, + botkillerVariants: null, + } + }; + + const mockResponseItems = [ + { + item_class: 'tf_weapon_lunchbox_drink', + defindex: 163, + item_name: 'Crit-a-Cola', + item_type_name: 'Lunch Box', + } + ]; + + linkBotkillerVariants(mockResponseItems, testSchema); + expect(testSchema['163'].botkillerVariants).toBeNull(); + }); + }) }) \ No newline at end of file From eada50c0e832e1e567ff844c49e9bbc20b4b4f6e Mon Sep 17 00:00:00 2001 From: xenticore Date: Mon, 7 Apr 2025 15:45:13 -0400 Subject: [PATCH 5/8] refactor: sort price rows by order instead of quality --- src/content/content.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/content/content.ts b/src/content/content.ts index 088fea0..5f77613 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -188,7 +188,7 @@ async function inject() { var updateTime: Date | null = null; interface PriceRow { - quality: number + order: number row: HTMLTableRowElement } var priceRows: PriceRow[]= []; @@ -214,7 +214,7 @@ async function inject() { const qualityName = itemQualities[quality as unknown as keyof typeof itemQualities].toString() const priceRow = createPriceRow(qualityName, data, keyPrice, locale) - priceRows.push({quality: quality, row: priceRow}) + priceRows.push({order: quality == 6 ? -1 : quality, row: priceRow) }) // Check item schema for Australium variant of current defindex @@ -231,7 +231,7 @@ async function inject() { const priceRow = createPriceRow($T("Australium"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Australium_weapons") - priceRows.push({quality: 99, row: priceRow}) + priceRows.push({order: 99, row: priceRow) resolve() return })) @@ -251,7 +251,7 @@ async function inject() { const priceRow = createPriceRow($T("Festive"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Festive_weapons") - priceRows.push({quality: 97, row: priceRow}) + priceRows.push({order: -1, row: priceRow}) resolve() return })) @@ -267,7 +267,7 @@ async function inject() { const priceRow = createPriceRow($T("Strange Festive"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Festive_weapons") - priceRows.push({quality: 98, row: priceRow}) + priceRows.push({order: 11, row: priceRow}) resolve() return })) @@ -275,14 +275,10 @@ async function inject() { Promise.all(promises).then(() => { priceRows.sort((a, b) => { - // Sort 6 first always, then numerically - if (a.quality === 6) { - return -1; - } else if (b.quality === 6) { - return 1; - } else { - return a.quality == b.quality ? a.quality < b.quality ? -1 : 1 : 0; + if (a.category != b.category) { + return a.category - b.category; } + return a.order - b.order; }).reverse().forEach((element) => { priceInfoboxHeadingRow.insertAdjacentElement('afterend', element.row); }) From 779cd12153d8112842d84af697b556481be34ee9 Mon Sep 17 00:00:00 2001 From: xenticore Date: Mon, 7 Apr 2025 15:57:41 -0400 Subject: [PATCH 6/8] feat: add festive and botkiller categories to pricebox festives are now shown with their quality rather than "Festive" and "Strange Festive" botkillers are sorted by release, and displayed without "Mk.I" to save space --- src/content/content.ts | 93 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/src/content/content.ts b/src/content/content.ts index 5f77613..d0efdc7 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -187,9 +187,16 @@ async function inject() { var updateTime: Date | null = null; + enum PriceRowCategory { + None, + Festive, + Botkiller + } + interface PriceRow { order: number row: HTMLTableRowElement + category: PriceRowCategory } var priceRows: PriceRow[]= []; @@ -214,7 +221,7 @@ async function inject() { const qualityName = itemQualities[quality as unknown as keyof typeof itemQualities].toString() const priceRow = createPriceRow(qualityName, data, keyPrice, locale) - priceRows.push({order: quality == 6 ? -1 : quality, row: priceRow) + priceRows.push({order: quality == 6 ? -1 : quality, row: priceRow, category: PriceRowCategory.None}) }) // Check item schema for Australium variant of current defindex @@ -231,14 +238,25 @@ async function inject() { const priceRow = createPriceRow($T("Australium"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Australium_weapons") - priceRows.push({order: 99, row: priceRow) + priceRows.push({order: 99, row: priceRow, category: PriceRowCategory.None}) resolve() return })) } + var festiveHeadingRow: HTMLTableRowElement | null // Check item schema for Festive variant of current defindex if(itemSchema[itemIndex].festiveVariant != null) { + /// Create subheading + festiveHeadingRow = document.createElement("tr") + const festiveHeading = document.createElement("th") + festiveHeading.className = "infobox-subheader" + festiveHeading.colSpan = 2 + festiveHeading.innerText = $T("Festive") + festiveHeading.style.fontSize = '1em'; + festiveHeading.style.backgroundColor = '#F5C087'; + festiveHeadingRow.appendChild(festiveHeading); + promises.push(new Promise(async (resolve) => { logDebug(`Fetching price for Festive ${itemName}`) var data: ItemPriceData | null @@ -249,9 +267,9 @@ async function inject() { log(`Festive ${itemName} is unpriced or unavailable, skipping...`) } - const priceRow = createPriceRow($T("Festive"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Festive_weapons") + const priceRow = createPriceRow($T("Unique"), data, keyPrice, locale) - priceRows.push({order: -1, row: priceRow}) + priceRows.push({order: -1, row: priceRow, category: PriceRowCategory.Festive}) resolve() return })) @@ -265,14 +283,65 @@ async function inject() { log(`Strange Festive ${itemName} is unpriced or unavailable, skipping...`) } - const priceRow = createPriceRow($T("Strange Festive"), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Festive_weapons") + const priceRow = createPriceRow($T("Strange"), data, keyPrice, locale) - priceRows.push({order: 11, row: priceRow}) + priceRows.push({order: 11, row: priceRow, category: PriceRowCategory.Festive}) resolve() return })) } + // Silver Mk.I, Gold Mk.II, Rust, Blood, Carbonado, Diamond, Silver Mk.II, Gold Mk.II + const botkillerOrder = [ + "Silver", + "Gold", + "Rust", + "Blood", + "Carbonado", + "Diamond", + "Silver Mk.II", + "Gold Mk.II", + ] + var botKillerHeadingRow: HTMLTableRowElement | null + if(itemSchema[itemIndex].botkillerVariants != null && itemSchema[itemIndex].botkillerVariants.length > 0) { + /// Create subheading + botKillerHeadingRow = document.createElement("tr") + const festiveHeading = document.createElement("th") + festiveHeading.className = "infobox-subheader" + festiveHeading.colSpan = 2 + festiveHeading.innerText = $T("Botkiller") + festiveHeading.style.fontSize = '1em'; + festiveHeading.style.backgroundColor = '#F5C087'; + botKillerHeadingRow.appendChild(festiveHeading); + + itemSchema[itemIndex].botkillerVariants.map((variantIndex) => { + const itemName = itemSchema[variantIndex].name + // FIXME: variantName should match wiki display name + const variantName = itemName.includes('Mk.II') ? itemName.split(' ')[0] + ' Mk.II' : itemName.split(' ')[0] + promises.push(new Promise(async (resolve) => { + logDebug(`Fetching price for ${itemName}`) + var data: ItemPriceData | null + try { + data = await fetchPrice(token, `${variantIndex};11`, currentTime); + updateTime = new Date(data.update) + } catch { + log(`${itemName} is unpriced or unavailable, skipping...`) + } + + const priceRow = createPriceRow($T(variantName), data, keyPrice, locale, "https://wiki.teamfortress.com/wiki/Botkiller_weapons") + + // FIXME: order should be by release + // Silver Mk.I, Gold Mk.II, Rust, Blood, Carbonado, Diamond, Silver Mk.II, Gold Mk.II + priceRows.push({order: botkillerOrder.indexOf(variantName), row: priceRow, category: PriceRowCategory.Botkiller}) + resolve() + return + })) + }) + } + + if(botKillerHeadingRow) priceInfoboxHeadingRow.insertAdjacentElement('afterend', botKillerHeadingRow); + if(festiveHeadingRow) priceInfoboxHeadingRow.insertAdjacentElement('afterend', festiveHeadingRow); + Promise.all(promises).then(() => { priceRows.sort((a, b) => { if (a.category != b.category) { @@ -280,7 +349,17 @@ async function inject() { } return a.order - b.order; }).reverse().forEach((element) => { - priceInfoboxHeadingRow.insertAdjacentElement('afterend', element.row); + switch(element.category) { + case PriceRowCategory.None: + priceInfoboxHeadingRow.insertAdjacentElement('afterend', element.row); + break; + case PriceRowCategory.Festive: + festiveHeadingRow.insertAdjacentElement('afterend', element.row); + break; + case PriceRowCategory.Botkiller: + botKillerHeadingRow.insertAdjacentElement('afterend', element.row); + break; + } }) if(!updateTime || !(updateTime instanceof Date) || isNaN(+updateTime)) updateTime = new Date() From b61e4bfac9c42751355031ea302e2defb29db459 Mon Sep 17 00:00:00 2001 From: xenticore Date: Mon, 7 Apr 2025 15:58:07 -0400 Subject: [PATCH 7/8] l10n: add strings for botkillers and festives --- src/strings/en.js | 14 +++++++++++++- src/strings/es.js | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/strings/en.js b/src/strings/en.js index d281096..30c8860 100644 --- a/src/strings/en.js +++ b/src/strings/en.js @@ -22,5 +22,17 @@ module.exports = { "Strange": "Strange", "Collector's": "Collector's", "Haunted": "Haunted", - "Australium": "Australium" + "Australium": "Australium", + "Festive": "Festive", + + // Botkiller names, all sourced from TF2 wiki + "Botkiller": "Botkiller", + "Silver": "Silver", + "Gold": "Gold", + "Rust": "Rust", + "Blood": "Blood", + "Carbonado": "Carbonado", + "Diamond": "Diamond", + "Silver Mk.II": "Silver Mk.II", + "Gold Mk.II": "Gold Mk.II", } \ No newline at end of file diff --git a/src/strings/es.js b/src/strings/es.js index 2f464ba..62f5a54 100644 --- a/src/strings/es.js +++ b/src/strings/es.js @@ -22,5 +22,17 @@ module.exports = { "Strange": "de Calidad Rara", "Collector's": "de Coleccionista", "Haunted": "de Calidad Embrujada", - "Australium": "de Australium" + "Australium": "de Australium", + "Festive": "Festiva", + + // Botkiller names, all sourced from TF2 wiki + "Botkiller": "Matabots", + "Silver": "Plata", + "Gold": "Oro", + "Rust": "Oxidado", + "Blood": "Sangriento", + "Carbonado": "Carbonado", + "Diamond": "Diamante", + "Silver Mk.II": "Plata Mk.II", + "Gold Mk.II": "Oro Mk.II", } \ No newline at end of file From 7be1d1da5d138fe4d6c59f1bec062471ac3b25ab Mon Sep 17 00:00:00 2001 From: xenticore Date: Mon, 7 Apr 2025 16:35:40 -0400 Subject: [PATCH 8/8] bump version to 0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04fa4d1..c1c602a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tf2wikipricing", - "version": "0.4.1", + "version": "0.5.0", "description": "Adds item pricing to the Team Fortress 2 wiki", "devDependencies": { "@happy-dom/global-registrator": "^17.4.4",