|
@@ -33,9 +33,6 @@ for (const key of Object.keys(expertise)) {
|
|
|
}
|
|
}
|
|
|
const buildingSelect = document.querySelector('select#building') as HTMLSelectElement;
|
|
const buildingSelect = document.querySelector('select#building') as HTMLSelectElement;
|
|
|
|
|
|
|
|
-// EXTREME DETAIL: We replace the distinct `formatWhole` and `formatDecimal` formatters with a unified
|
|
|
|
|
-// `formatSigFig` formatter. Utilizing the `compact` notation automatically caps the output at
|
|
|
|
|
-// 3 significant figures and organically appends "K", "M", etc. to large numbers.
|
|
|
|
|
const formatSigFig = new Intl.NumberFormat(undefined, {
|
|
const formatSigFig = new Intl.NumberFormat(undefined, {
|
|
|
notation: 'compact',
|
|
notation: 'compact',
|
|
|
maximumSignificantDigits: 3,
|
|
maximumSignificantDigits: 3,
|
|
@@ -56,10 +53,11 @@ let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as Metri
|
|
|
let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricType) || 'vwap';
|
|
let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricType) || 'vwap';
|
|
|
let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
|
|
let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
|
|
|
let includeShips: boolean = localStorage.getItem('roi-include-ships') === 'true';
|
|
let includeShips: boolean = localStorage.getItem('roi-include-ships') === 'true';
|
|
|
-
|
|
|
|
|
-// EXTREME DETAIL: Track the state of the user's working capital input. Default to 3 days.
|
|
|
|
|
let workingCapitalDays: number = parseInt(localStorage.getItem('roi-working-capital') || '3', 10);
|
|
let workingCapitalDays: number = parseInt(localStorage.getItem('roi-working-capital') || '3', 10);
|
|
|
|
|
|
|
|
|
|
+// EXTREME DETAIL: Track the state of the Target Permit. Defaults to 2 (the standard starting permits in PRUN).
|
|
|
|
|
+let targetPermit: number = parseInt(localStorage.getItem('roi-target-permit') || '2', 10);
|
|
|
|
|
+
|
|
|
async function render() {
|
|
async function render() {
|
|
|
const tbody = document.querySelector('tbody')!;
|
|
const tbody = document.querySelector('tbody')!;
|
|
|
tbody.innerHTML = '';
|
|
tbody.innerHTML = '';
|
|
@@ -72,8 +70,7 @@ async function render() {
|
|
|
if (!metricControlsInitialized) {
|
|
if (!metricControlsInitialized) {
|
|
|
const controls = document.createElement('div');
|
|
const controls = document.createElement('div');
|
|
|
controls.style.marginBottom = '15px';
|
|
controls.style.marginBottom = '15px';
|
|
|
- // EXTREME DETAIL: Added the numerical input `<input type="number">` allowing users to configure
|
|
|
|
|
- // exactly how many days of OpEx should be buffered into their CapEx.
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Injected the new `<input type="number">` for Target Permit.
|
|
|
controls.innerHTML = `
|
|
controls.innerHTML = `
|
|
|
<label style="margin-right: 15px;">CapEx Price:
|
|
<label style="margin-right: 15px;">CapEx Price:
|
|
|
<select id="capex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
|
|
<select id="capex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
|
|
@@ -87,9 +84,12 @@ async function render() {
|
|
|
<label style="margin-right: 15px;">
|
|
<label style="margin-right: 15px;">
|
|
|
<input type="checkbox" id="include-ships"> Include Ship CapEx
|
|
<input type="checkbox" id="include-ships"> Include Ship CapEx
|
|
|
</label>
|
|
</label>
|
|
|
- <label>
|
|
|
|
|
|
|
+ <label style="margin-right: 15px;">
|
|
|
<input type="number" id="working-capital" min="0" step="1" style="width: 50px;"> Days OpEx
|
|
<input type="number" id="working-capital" min="0" step="1" style="width: 50px;"> Days OpEx
|
|
|
</label>
|
|
</label>
|
|
|
|
|
+ <label>
|
|
|
|
|
+ <input type="number" id="target-permit" min="1" step="1" style="width: 50px;"> Target Permit
|
|
|
|
|
+ </label>
|
|
|
`;
|
|
`;
|
|
|
const table = document.querySelector('table');
|
|
const table = document.querySelector('table');
|
|
|
if (table) table.parentNode?.insertBefore(controls, table);
|
|
if (table) table.parentNode?.insertBefore(controls, table);
|
|
@@ -99,6 +99,7 @@ async function render() {
|
|
|
(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
|
|
(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
|
|
|
(document.getElementById('include-ships') as HTMLInputElement).checked = includeShips;
|
|
(document.getElementById('include-ships') as HTMLInputElement).checked = includeShips;
|
|
|
(document.getElementById('working-capital') as HTMLInputElement).value = workingCapitalDays.toString();
|
|
(document.getElementById('working-capital') as HTMLInputElement).value = workingCapitalDays.toString();
|
|
|
|
|
+ (document.getElementById('target-permit') as HTMLInputElement).value = targetPermit.toString();
|
|
|
|
|
|
|
|
document.getElementById('capex-metric')!.addEventListener('change', (e) => {
|
|
document.getElementById('capex-metric')!.addEventListener('change', (e) => {
|
|
|
capexMetric = (e.target as HTMLSelectElement).value as MetricType;
|
|
capexMetric = (e.target as HTMLSelectElement).value as MetricType;
|
|
@@ -120,6 +121,10 @@ async function render() {
|
|
|
workingCapitalDays = parseInt((e.target as HTMLInputElement).value, 10);
|
|
workingCapitalDays = parseInt((e.target as HTMLInputElement).value, 10);
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
|
|
+ document.getElementById('target-permit')!.addEventListener('change', (e) => {
|
|
|
|
|
+ targetPermit = parseInt((e.target as HTMLInputElement).value, 10);
|
|
|
|
|
+ render();
|
|
|
|
|
+ });
|
|
|
metricControlsInitialized = true;
|
|
metricControlsInitialized = true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -140,7 +145,7 @@ async function render() {
|
|
|
th.dataset.tooltip = 'Click to sort.\n\nDaily profit scaled to a full 500-area planetary base.';
|
|
th.dataset.tooltip = 'Click to sort.\n\nDaily profit scaled to a full 500-area planetary base.';
|
|
|
} else if (keys[i] === 'capex_val') {
|
|
} else if (keys[i] === 'capex_val') {
|
|
|
th.textContent = 'CapEx/Base';
|
|
th.textContent = 'CapEx/Base';
|
|
|
- th.dataset.tooltip = 'Click to sort.\n\nTotal capital expenditure scaled to a full 500-area planetary base.\nIncludes base construction, working capital (days of OpEx), and optional Ship CapEx.';
|
|
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nTotal capital expenditure scaled to a full 500-area planetary base.\nIncludes base construction, working capital (days of OpEx), optional HQ Upgrade materials for the target permit, and optional Ship CapEx.';
|
|
|
} else if (keys[i] === 'opex_val') {
|
|
} else if (keys[i] === 'opex_val') {
|
|
|
th.textContent = 'OpEx/Base';
|
|
th.textContent = 'OpEx/Base';
|
|
|
th.dataset.tooltip = 'Click to sort.\n\nDaily operational expenditure (input materials + worker consumables) scaled to a full 500-area planetary base.';
|
|
th.dataset.tooltip = 'Click to sort.\n\nDaily operational expenditure (input materials + worker consumables) scaled to a full 500-area planetary base.';
|
|
@@ -151,7 +156,7 @@ async function render() {
|
|
|
th.textContent = 'Market Cap (Bases)';
|
|
th.textContent = 'Market Cap (Bases)';
|
|
|
th.dataset.tooltip = 'Click to sort.\n\nMarket Capacity: 7-day average traded volume ÷ daily output per base. Indicates how many full 500-area bases you can build before saturating the market.';
|
|
th.dataset.tooltip = 'Click to sort.\n\nMarket Capacity: 7-day average traded volume ÷ daily output per base. Indicates how many full 500-area bases you can build before saturating the market.';
|
|
|
} else if (keys[i] === 'break_even') {
|
|
} else if (keys[i] === 'break_even') {
|
|
|
- th.dataset.tooltip = 'Click to sort.\n\nBreak Even: CapEx ÷ daily profit. Note that CapEx dynamically includes working capital (days of OpEx) to accurately reflect operational readiness.';
|
|
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nBreak Even: CapEx ÷ daily profit. Note that CapEx dynamically includes working capital and HQ upgrades to accurately reflect operational readiness.';
|
|
|
} else {
|
|
} else {
|
|
|
th.dataset.tooltip = 'Click to sort.';
|
|
th.dataset.tooltip = 'Click to sort.';
|
|
|
}
|
|
}
|
|
@@ -207,11 +212,20 @@ async function render() {
|
|
|
const opex_val = p.opex[opexMetric] / bases;
|
|
const opex_val = p.opex[opexMetric] / bases;
|
|
|
const revenue_val = p.revenue[revenueMetric] / bases;
|
|
const revenue_val = p.revenue[revenueMetric] / bases;
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: CapEx now completely absorbs the working capital (days of OpEx).
|
|
|
|
|
- // Because we moved the addition directly into `capex_val`, it will be explicitly rendered
|
|
|
|
|
- // inside the CapEx column on the dashboard, making the break-even math visually apparent.
|
|
|
|
|
let capex_val = (p.capex[capexMetric] / bases) + (opex_val * workingCapitalDays);
|
|
let capex_val = (p.capex[capexMetric] / bases) + (opex_val * workingCapitalDays);
|
|
|
|
|
|
|
|
|
|
+ // EXTREME DETAIL: We intercept the HQ permit cost here.
|
|
|
|
|
+ // Permits 1 & 2 are free. To unlock permit 3, you must upgrade HQ to Level 2.
|
|
|
|
|
+ // We map the user input directly to the JSON string keys retrieved from GitHub.
|
|
|
|
|
+ let hq_capex = 0;
|
|
|
|
|
+ if (targetPermit >= 3) {
|
|
|
|
|
+ const hqLevelStr = (targetPermit - 1).toString();
|
|
|
|
|
+ if (p.hq_costs && p.hq_costs[hqLevelStr]) {
|
|
|
|
|
+ hq_capex = p.hq_costs[hqLevelStr][capexMetric];
|
|
|
|
|
+ capex_val += hq_capex;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (includeShips) {
|
|
if (includeShips) {
|
|
|
capex_val += p.ship_capex_per_base;
|
|
capex_val += p.ship_capex_per_base;
|
|
|
}
|
|
}
|
|
@@ -219,7 +233,7 @@ async function render() {
|
|
|
const profit_per_base = revenue_val - opex_val;
|
|
const profit_per_base = revenue_val - opex_val;
|
|
|
const break_even = profit_per_base > 0 ? capex_val / profit_per_base : Infinity;
|
|
const break_even = profit_per_base > 0 ? capex_val / profit_per_base : Infinity;
|
|
|
|
|
|
|
|
- return { ...p, capex_val, opex_val, revenue_val, profit_per_base, break_even };
|
|
|
|
|
|
|
+ return { ...p, capex_val, opex_val, revenue_val, profit_per_base, break_even, hq_capex };
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
profitsWithMetrics.sort((a, b) => {
|
|
profitsWithMetrics.sort((a, b) => {
|
|
@@ -237,11 +251,8 @@ async function render() {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
for (const p of profitsWithMetrics) {
|
|
for (const p of profitsWithMetrics) {
|
|
|
- const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
|
|
|
const tr = document.createElement('tr');
|
|
const tr = document.createElement('tr');
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: We replaced all formatDecimal/formatWhole calls with formatSigFig.
|
|
|
|
|
- // Additionally, we append the newly generated bottleneck string onto the Logistics cell.
|
|
|
|
|
tr.innerHTML = `
|
|
tr.innerHTML = `
|
|
|
<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
|
|
<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
|
|
|
<td>${expertise[p.expertise]}</td>
|
|
<td>${expertise[p.expertise]}</td>
|
|
@@ -264,11 +275,14 @@ async function render() {
|
|
|
'+ worker consumables\n\n' +
|
|
'+ worker consumables\n\n' +
|
|
|
`(${formatSigFig(p.revenue_val)} - ${formatSigFig(p.opex_val)}) = ${formatSigFig(p.profit_per_base)}`;
|
|
`(${formatSigFig(p.revenue_val)} - ${formatSigFig(p.opex_val)}) = ${formatSigFig(p.profit_per_base)}`;
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: Because working capital is now part of CapEx, we break down
|
|
|
|
|
- // the constituent components in the tooltip so the math is transparent.
|
|
|
|
|
const capexCell = tr.querySelectorAll('td')[4];
|
|
const capexCell = tr.querySelectorAll('td')[4];
|
|
|
capexCell.dataset.tooltip = `Base Construction: ${formatSigFig(p.capex[capexMetric] / (p.area / 500))}`;
|
|
capexCell.dataset.tooltip = `Base Construction: ${formatSigFig(p.capex[capexMetric] / (p.area / 500))}`;
|
|
|
capexCell.dataset.tooltip += `\nWorking Capital (${workingCapitalDays} days): ${formatSigFig(p.opex_val * workingCapitalDays)}`;
|
|
capexCell.dataset.tooltip += `\nWorking Capital (${workingCapitalDays} days): ${formatSigFig(p.opex_val * workingCapitalDays)}`;
|
|
|
|
|
+
|
|
|
|
|
+ // EXTREME DETAIL: Dynamically inject the HQ cost into the tooltip if the user requested Permit 3 or higher.
|
|
|
|
|
+ if (p.hq_capex > 0) {
|
|
|
|
|
+ capexCell.dataset.tooltip += `\nHQ Upgrade (Permit ${targetPermit}): ${formatSigFig(p.hq_capex)}`;
|
|
|
|
|
+ }
|
|
|
if (includeShips) {
|
|
if (includeShips) {
|
|
|
capexCell.dataset.tooltip += `\nShip CapEx: ${formatSigFig(p.ship_capex_per_base)} (${formatSigFig(p.ship_capex_per_base / 800_000)} ships)`;
|
|
capexCell.dataset.tooltip += `\nShip CapEx: ${formatSigFig(p.ship_capex_per_base)} (${formatSigFig(p.ship_capex_per_base / 800_000)} ships)`;
|
|
|
}
|
|
}
|
|
@@ -296,6 +310,7 @@ function saveState() {
|
|
|
localStorage.setItem('roi-revenue-metric', revenueMetric);
|
|
localStorage.setItem('roi-revenue-metric', revenueMetric);
|
|
|
localStorage.setItem('roi-include-ships', includeShips.toString());
|
|
localStorage.setItem('roi-include-ships', includeShips.toString());
|
|
|
localStorage.setItem('roi-working-capital', workingCapitalDays.toString());
|
|
localStorage.setItem('roi-working-capital', workingCapitalDays.toString());
|
|
|
|
|
+ localStorage.setItem('roi-target-permit', targetPermit.toString());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function color(n: number, low: number, high: number): string {
|
|
function color(n: number, low: number, high: number): string {
|
|
@@ -336,11 +351,12 @@ interface Profit {
|
|
|
input_costs: MatPrice[]
|
|
input_costs: MatPrice[]
|
|
|
runs_per_day: number
|
|
runs_per_day: number
|
|
|
logistics_per_base: number
|
|
logistics_per_base: number
|
|
|
- logistics_bottleneck: string // Extracted from backend
|
|
|
|
|
|
|
+ logistics_bottleneck: string
|
|
|
output_per_day: number
|
|
output_per_day: number
|
|
|
average_traded_7d: number
|
|
average_traded_7d: number
|
|
|
market_capacity_base: number
|
|
market_capacity_base: number
|
|
|
ship_capex_per_base: number
|
|
ship_capex_per_base: number
|
|
|
|
|
+ hq_costs: Record<string, Metrics> // Added typing for the precalculated HQ pricing dictionary
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface ProfitWithMetrics extends Profit {
|
|
interface ProfitWithMetrics extends Profit {
|
|
@@ -350,6 +366,7 @@ interface ProfitWithMetrics extends Profit {
|
|
|
profit_per_day: number;
|
|
profit_per_day: number;
|
|
|
profit_per_base: number;
|
|
profit_per_base: number;
|
|
|
break_even: number;
|
|
break_even: number;
|
|
|
|
|
+ hq_capex: number; // Added to interface to pass to the tooltip generator
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface MatPrice {
|
|
interface MatPrice {
|