Files
recflare/apps/api/scripts/price-custom-avatar-items.ts
T
2026-09-16 15:53:39 -04:00

84 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Fills in `Price` on an export of first-party custom avatar items from a storefront dump,
* rewriting the export in place so `import-custom-avatar-items.ts` / `just admin cai-load`
* carry the price into the table.
*
* bun apps/api/scripts/price-custom-avatar-items.ts <export.json> <storefront.json>
*
* The export (e.g. `apps/econ/static/db/2025-1-cai.json`) mostly carries `Price: 0`. The
* storefront (e.g. `Watch_EnumValue_3.json`, a `{ StoreItems: [...] }` page) prices the same
* items, joined on `GiftDrop.AvatarItemInfo.DownloadableAvatarItemId` = `CustomAvatarItemId`.
* The price taken is the base token price (`CurrencyType` 2), ignoring any sale. The storefront
* only prices what the export already holds: nothing is added from it.
*
* An item the storefront doesn't list keeps the price it had, unless that price is 0, which
* would make it free to buy. Those are the Cryptid Creek event items and the Evergrown Hip Pack,
* all rarity-50 rewards that were never sold, so they get the catalog's rarity-50 price.
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { priceForRarity } from '../../econ/src/catalog-load'
const TOKENS = 2
/** The rarity an item missing from the storefront is priced as, when it has no price. */
const UNLISTED_RARITY = 50
interface StoreItem {
GiftDrop?: { AvatarItemInfo?: { DownloadableAvatarItemId?: string | null } | null }
Prices?: Array<{ CurrencyType: number; Price: number }>
}
const [exportPath, storefrontPath] = process.argv.slice(2)
if (!exportPath || !storefrontPath) {
console.error(
'usage: bun apps/api/scripts/price-custom-avatar-items.ts <export.json> <storefront.json>'
)
process.exit(2)
}
// Tolerate the BOM a .NET export carries (`JSON.parse` rejects U+FEFF).
const readJson = (path: string) => JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''))
const items: Array<{ CustomAvatarItemId: string; Name: string; Price: number }> =
readJson(exportPath)
const storefront: { StoreItems: StoreItem[] } = readJson(storefrontPath)
const prices = new Map<string, number>()
for (const storeItem of storefront.StoreItems) {
const id = storeItem.GiftDrop?.AvatarItemInfo?.DownloadableAvatarItemId?.toLowerCase()
const price = storeItem.Prices?.find((p) => p.CurrencyType === TOKENS)?.Price
if (id && price !== undefined && !prices.has(id)) {
prices.set(id, price)
}
}
let priced = 0
let changed = 0
const unmatched: string[] = []
for (const item of items) {
let price = prices.get(item.CustomAvatarItemId.toLowerCase())
if (price === undefined) {
unmatched.push(item.Name)
if (item.Price) continue
price = priceForRarity(UNLISTED_RARITY)
}
priced++
if (item.Price !== price) {
if (item.Price) {
console.warn(`${item.Name}: ${item.Price} -> ${price}`)
}
item.Price = price
changed++
}
}
writeFileSync(exportPath, JSON.stringify(items, null, 2) + '\n')
console.log(`priced ${priced}/${items.length} items (${changed} changed) in ${exportPath}`)
if (unmatched.length) {
console.log(
`not in the storefront (kept their price, or rarity ${UNLISTED_RARITY} if free): ${unmatched.join(', ')}`
)
}