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.
This commit is contained in:
xenticore
2025-04-02 18:54:39 -04:00
parent 3101f57f62
commit fa7276a53c
3 changed files with 148 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, test, mock } from "bun:test"; 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 the storage and log functions
mock.module('../src/content/storage', () => ({ mock.module('../src/content/storage', () => ({
@@ -19,35 +19,48 @@ const mockSchema: ItemSchema = {
slot: ItemSlot.Primary, slot: ItemSlot.Primary,
tradable: false, tradable: false,
hasAustraliumVariant: false, hasAustraliumVariant: false,
canKillstreakify: true canKillstreakify: true,
festiveVariant: null
}, },
'208': { '208': {
name: 'Flame Thrower', name: 'Flame Thrower',
slot: ItemSlot.Primary, slot: ItemSlot.Primary,
tradable: true, tradable: true,
hasAustraliumVariant: 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': { '5021': {
name: 'Mann Co. Supply Crate Key', name: 'Mann Co. Supply Crate Key',
slot: ItemSlot.Tool, slot: ItemSlot.Tool,
tradable: true, tradable: true,
hasAustraliumVariant: false, hasAustraliumVariant: false,
canKillstreakify: false canKillstreakify: false,
festiveVariant: null
}, },
'15141': { '15141': {
name: 'Flame Thrower', name: 'Flame Thrower',
slot: ItemSlot.Primary, slot: ItemSlot.Primary,
tradable: true, tradable: true,
hasAustraliumVariant: false, hasAustraliumVariant: false,
canKillstreakify: true canKillstreakify: true,
festiveVariant: null
}, },
'69420': { '69420': {
name: 'Non-Tradable Item', name: 'Non-Tradable Item',
slot: ItemSlot.Misc, slot: ItemSlot.Misc,
tradable: false, tradable: false,
hasAustraliumVariant: 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();
});
});
}) })

View File

@@ -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(() => { Promise.all(promises).then(() => {
priceRows.sort((a, b) => { priceRows.sort((a, b) => {
// Sort 6 first always, then numerically // Sort 6 first always, then numerically

View File

@@ -38,6 +38,7 @@ export class ItemSchema {
slot: ItemSlot, slot: ItemSlot,
tradable: Boolean, tradable: Boolean,
hasAustraliumVariant: Boolean, hasAustraliumVariant: Boolean,
festiveVariant: number | null
canKillstreakify: Boolean canKillstreakify: Boolean
}; };
} }
@@ -70,6 +71,25 @@ export function getTradableStatusByName(schema: ItemSchema, name: string, exclud
return true 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<void> { export async function wipeSchema(): Promise<void> {
await setStorageValue(storage_version, __VERSION__) await setStorageValue(storage_version, __VERSION__)
await setStorageValue(storage_schema, null) await setStorageValue(storage_schema, null)
@@ -149,6 +169,8 @@ export async function prepareSchema(): Promise<ItemSchema> {
} }
}); });
linkFestiveVariants(responseItems, cacheItems)
await setStorageValue(storage_schema, (cacheItems)); await setStorageValue(storage_schema, (cacheItems));
itemSchema = cacheItems itemSchema = cacheItems
await setStorageValue(storage_version, __VERSION__); await setStorageValue(storage_version, __VERSION__);