import {setupPopover} from './popover'; const username = document.querySelector('#username') as HTMLInputElement; const apiKey = document.querySelector('#api-key') as HTMLInputElement; { const storedUsername = localStorage.getItem('fio-username'); if (storedUsername) username.value = storedUsername; const storedApiKey = localStorage.getItem('fio-api-key'); if (storedApiKey) apiKey.value = storedApiKey; } document.querySelector('#fetch')!.addEventListener('click', async () => { const supplyForDays = parseInt((document.querySelector('#days') as HTMLInputElement).value, 10); const cx = (document.querySelector('#cx') as HTMLInputElement).value; const output = await calculate(username.value, apiKey.value, supplyForDays, cx); console.log(output); localStorage.setItem('fio-username', username.value); localStorage.setItem('fio-api-key', apiKey.value); }); setupPopover(); async function calculate(username: string, apiKey: string, supplyForDays: number, cx: string): Promise { const [prices, planets, avail, {bids, orders}] = await Promise.all([ getPrices(cx), getPlanets(username, apiKey), warehouseInventory(username, apiKey), getBids(username, apiKey) ]); const buy = new Map(); for (const planet of planets) { for (const [mat, amount] of planet.supplyForDays(supplyForDays)) buy.set(mat, (buy.get(mat) ?? 0) + amount); for (const mat of planet.exporting) { const planetExport = planet.inventory.get(mat); if (planetExport) avail.set(mat, (avail.get(mat) ?? 0) + planetExport); } } // what's left to buy const materials: Material[] = []; for (const [mat, amount] of buy) { const remaining = Math.max(amount - (bids.get(mat) ?? 0) - (avail.get(mat) ?? 0), 0); const price = prices.get(mat); if (!price || price.Bid === null || price.Ask === null) { console.log(mat, 'has no bid/ask'); continue; } const spread = price.Ask - price.Bid; materials.push({ ticker: mat, amount, bids: bids.get(mat) ?? 0, have: avail.get(mat) ?? 0, spread, savings: spread * remaining, }); } materials.sort((a, b) => b.savings - a.savings); const tbody = document.querySelector('tbody')!; tbody.innerHTML = ''; const format= new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format; for (const m of materials) { const tr = document.createElement('tr'); const buyAmount = Math.max(m.amount - m.bids - m.have, 0); tr.innerHTML = ` ${m.ticker} ${format(m.amount)} ${format(m.bids)} ${format(m.have)} ${format(buyAmount)} ${format(m.spread)} ${format(m.savings)} `; if (m.bids === 0 && buyAmount > 0) tr.children[2].classList.add('red'); tbody.appendChild(tr); } // deposits of current bids orders.sort((a, b) => (b.Limit * b.Amount) - (a.Limit * a.Amount)); for (const order of orders) { const deposit = order.Limit * order.Amount; console.log(`${order.MaterialTicker.padEnd(4)} ${deposit}\n`); } } async function getPrices(cx: string) { const rawPrices= await fetch('https://refined-prun.github.io/refined-prices/all.json').then(r => r.json()); const prices = new Map(); for (const p of rawPrices) if (p.ExchangeCode === cx) prices.set(p.MaterialTicker, p); return prices; } async function getPlanets(username: string, apiKey: string) { const fioBurns: FIOBurn[] = await fetch('https://rest.fnar.net/fioweb/burn/user/' + username, {headers: {'Authorization': apiKey}}).then(r => r.json()); const planets = fioBurns.map(burn => new Planet(burn)); return planets; } async function warehouseInventory(username: string, apiKey: string): Promise> { const warehouses: Warehouse[] = await fetch('https://rest.fnar.net/sites/warehouses/' + username, {headers: {'Authorization': apiKey}}).then(r => r.json()); for (const warehouse of warehouses) if (warehouse.LocationNaturalId === 'HRT') { const storage: Storage = await fetch(`https://rest.fnar.net/storage/${username}/${warehouse.StoreId}`, {headers: {'Authorization': apiKey}}).then(r => r.json()); const inventory = new Map(); for (const item of storage.StorageItems) inventory.set(item.MaterialTicker, item.MaterialAmount); return inventory; } throw new Error("couldn't find HRT warehouse"); } async function getBids(username: string, apiKey: string) { const allOrders: ExchangeOrder[] = await fetch('https://rest.fnar.net/cxos/' + username, {headers: {'Authorization': apiKey}}).then(r => r.json()); const orders = allOrders.filter(order => order.OrderType === 'BUYING' && order.Status !== 'FILLED' && order.ExchangeCode === 'IC1'); const bids = new Map(); for (const order of orders) bids.set(order.MaterialTicker, (bids.get(order.MaterialTicker) ?? 0) + order.Amount); return {bids, orders}; } class Planet { name: string; inventory: Map; netConsumption: Amount[]; exporting: Set; // producing more than consumption constructor(fioBurn: FIOBurn) { this.name = fioBurn.PlanetName || fioBurn.PlanetNaturalId; this.inventory = new Map(); for (const item of fioBurn.Inventory) this.inventory.set(item.MaterialTicker, item.MaterialAmount); const producing = new Map(); for (const item of fioBurn.OrderProduction) producing.set(item.MaterialTicker, item); this.netConsumption = []; for (const c of [...fioBurn.OrderConsumption, ...fioBurn.WorkforceConsumption]) { let net = c.DailyAmount; const production = producing.get(c.MaterialTicker); if (production) { net -= production.DailyAmount; if (net < 0) continue; } c.netConsumption = net; this.netConsumption.push(c); } const consuming = new Set(this.netConsumption.map(item => item.MaterialTicker)); this.exporting = new Set(); for (const item of fioBurn.OrderProduction) if (!consuming.has(item.MaterialTicker)) this.exporting.add(item.MaterialTicker); } supplyForDays(targetDays: number): Map { const buy = new Map(); for (const consumption of this.netConsumption) { const ticker = consumption.MaterialTicker; const avail = this.inventory.get(ticker) ?? 0; const dailyConsumption = consumption.netConsumption!; const days = avail / dailyConsumption; if (days < targetDays) buy.set(ticker, Math.ceil((targetDays - days) * dailyConsumption)); } return buy; } } interface Material { ticker: string; amount: number; bids: number; have: number; spread: number; savings: number; } interface RawPrice { MaterialTicker: string; ExchangeCode: string; Bid: number | null; Ask: number | null; FullTicker: string; } interface ExchangeOrder { MaterialTicker: string; ExchangeCode: string; OrderType: 'SELLING' | 'BUYING'; Status: 'FILLED' | 'PARTIALLY_FILLED'; Amount: number; Limit: number; } interface StorageItem { MaterialTicker: string; MaterialAmount: number; } interface Storage { Name: string; StorageItems: StorageItem[]; WeightLoad: number; VolumeLoad: number; Type: 'STORE' | 'WAREHOUSE_STORE' | 'FTL_FUEL_STORE' | 'STL_FUEL_STORE' | 'SHIP_STORE'; } interface Warehouse { StoreId: string; LocationNaturalId: string; } interface Amount { MaterialTicker: string; DailyAmount: number; netConsumption?: number; } interface FIOBurn { PlanetName: string; PlanetNaturalId: string; Error: any; OrderConsumption: Amount[]; WorkforceConsumption: Amount[]; Inventory: StorageItem[]; OrderProduction: Amount[]; }