// utils.ts import nbt from 'prismarine-nbt'; import { Item } from './Item'; import { loadConfig } from './configLoader'; const config = loadConfig(); // If you have a defined structure for your config file, feel free to refine this type. interface ConfigType { data: { minPriceForRawcraft: number; [key: string]: any; }; filters: { EnchantThreshold: Record; EnchantThresholdConditionalBypass: Record>; [key: string]: any; }; } interface NbtData { [key: string]: any; } // Keep track of current intervals by name let currentAsyncIntervals: Record = {}; type ProfitItem = { RCProfit: number; RCPP: number; snipeProfit: number; snipePP: number; }; function numberWithCommas(x: number | string): string { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function addNotation(type: string, value: number): string { let returnVal: string = value.toString(); let notList: string[] = []; if (type === 'shortScale') { notList = [' Thousand', ' Million', ' Billion', ' Trillion', ' Quadrillion', ' Quintillion']; } else if (type === 'oneLetters') { notList = ['K', 'M', 'B', 'T']; } let checkNum = 1000; if (type !== 'none' && type !== 'commas') { let notValue = notList[notList.length - 1] ?? ''; for (let u = notList.length; u >= 1; u--) { notValue = notList.shift() ?? ''; for (let o = 3; o >= 1; o--) { if (value >= checkNum) { // Use a fixed decimal approach returnVal = ( +(Math.floor(value / (checkNum / 100)) / Math.pow(10, o)) * 10 ).toFixed(o - 1) + notValue; } checkNum *= 10; } } } else { returnVal = numberWithCommas(value.toFixed(0)); } return returnVal; } async function getParsed(encoded: string): Promise { return new Promise((resolve, reject) => { const buf = Buffer.from(encoded, 'base64'); // The callback signature must match the prismarine-nbt types: nbt.parse(buf, (err: Error | null, data) => { if (err) return reject(err); resolve(nbt.simplify(data)); }); }); } function getProfit(price: number, rcCost: number, lbin: number): ProfitItem { const profitItem: ProfitItem = { RCProfit: 0, RCPP: 0, snipeProfit: 0, snipePP: 0, }; if (price >= 1_000_000) { profitItem.RCProfit = lbin + rcCost - price - (lbin + rcCost) * 0.02; profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1)); profitItem.snipeProfit = lbin - price - lbin * 0.02; profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1)); } else { profitItem.RCProfit = lbin + rcCost - price - (lbin + rcCost) * 0.01; profitItem.RCPP = parseFloat(((profitItem.RCProfit * 100) / lbin).toFixed(1)); profitItem.snipeProfit = lbin - price - lbin * 0.01; profitItem.snipePP = parseFloat(((profitItem.snipeProfit * 100) / lbin).toFixed(1)); } return profitItem; } function splitNumber(num: number = 1, parts: number = 1): number[] { const n: number = Math.floor(num / parts); const arr: number[] = []; for (let i = 0; i < parts; i++) { arr.push(n); } if (arr.reduce((a, b) => a + b, 0) === num) { return arr; } for (let i = 0; i < parts; i++) { arr[i]++; if (arr.reduce((a, b) => a + b, 0) === num) { return arr; } } // If we still haven’t balanced it out, it’ll just return this array return arr; } interface ItemType { itemData: { id: string; enchants?: Record; aow?: boolean; recomb?: boolean; hpbs?: number; fpbs?: number; }; auctionData: { lbin: number; category?: string; }; [key: string]: any; } interface LbinsType { [key: string]: { lbin: number; }; } interface BazaarPriceType { [key: string]: number; } function getRawCraft( item: Item, bazaarPrice: BazaarPriceType, lbins: LbinsType ): number { let price = 0; const typedConfig = config as ConfigType; const ignoreMatch: string = Object.keys(typedConfig.filters.EnchantThresholdConditionalBypass ?? {}).find((key) => { return item.itemData.id.includes(key); }) ?? ''; if (item.auctionData.lbin < typedConfig.data.minPriceForRawcraft) return 0; const isInIgnore = !!ignoreMatch; if (item.itemData.enchants && !item.itemData.id.includes(';')) { for (const enchant of Object.keys(item.itemData.enchants)) { const degree = item.itemData.enchants[enchant]; const badEnchant = typeof typedConfig.filters.EnchantThreshold[enchant] === 'number' ? degree >= typedConfig.filters.EnchantThreshold[enchant] : false; if (isInIgnore) { const enchantMinValue = typedConfig.filters.EnchantThresholdConditionalBypass[ignoreMatch]?.[enchant]; if (enchantMinValue && degree <= enchantMinValue) continue; } if (badEnchant) { price += lbins[`${enchant.toUpperCase()};${degree.toString()}`] ? lbins[`${enchant.toUpperCase()};${degree.toString()}`].lbin * 0.5 : 0; } } } if (item.itemData.aow) { price += lbins['THE_ART_OF_WAR'].lbin * 0.3; } if ( item.itemData.recomb && ['weapon', 'armor', 'accessories'].includes(item.auctionData.category ?? '') ) { price += bazaarPrice['RECOMBOBULATOR_3000'] * 0.5; } price += (item.itemData.hpbs ?? 0) * bazaarPrice['HOT_POTATO_BOOK'] * 0.05; price += (item.itemData.fpbs ?? 0) * bazaarPrice['FUMING_POTATO_BOOK'] * 0.1; return price; } // Schedules a recurring async task. async function asyncInterval( asyncTask: () => Promise, intervalName: string, timeout: number ): Promise { currentAsyncIntervals[intervalName] = true; setTimeout(async function () { if (!currentAsyncIntervals[intervalName]) return; await asyncTask(); await asyncInterval(asyncTask, intervalName, timeout); }, timeout); } function stopAsyncInterval(intervalName: string): void { currentAsyncIntervals[intervalName] = false; } function currentIntervals(): Record { return currentAsyncIntervals; } // Exports export { addNotation, getParsed, getProfit, splitNumber, getRawCraft, asyncInterval, stopAsyncInterval, currentIntervals, };