242 lines
6.2 KiB
TypeScript
242 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,
|
|||
|
};
|