241 lines
6.2 KiB
TypeScript
241 lines
6.2 KiB
TypeScript
// 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<string, number>;
|
||
EnchantThresholdConditionalBypass: Record<string, Record<string, number>>;
|
||
[key: string]: any;
|
||
};
|
||
}
|
||
|
||
interface NbtData {
|
||
[key: string]: any;
|
||
}
|
||
|
||
// Keep track of current intervals by name
|
||
let currentAsyncIntervals: Record<string, boolean> = {};
|
||
|
||
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<any> {
|
||
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<string, number>;
|
||
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<void>,
|
||
intervalName: string,
|
||
timeout: number
|
||
): Promise<void> {
|
||
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<string, boolean> {
|
||
return currentAsyncIntervals;
|
||
}
|
||
|
||
// Exports
|
||
export {
|
||
addNotation,
|
||
getParsed,
|
||
getProfit,
|
||
splitNumber,
|
||
getRawCraft,
|
||
asyncInterval,
|
||
stopAsyncInterval,
|
||
currentIntervals,
|
||
};
|