|
@@ -51,6 +51,7 @@ let metricControlsInitialized = false;
|
|
|
let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as MetricType) || 'vwap';
|
|
let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as MetricType) || 'vwap';
|
|
|
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';
|
|
|
|
|
|
|
|
async function render() {
|
|
async function render() {
|
|
|
const tbody = document.querySelector('tbody')!;
|
|
const tbody = document.querySelector('tbody')!;
|
|
@@ -64,6 +65,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 a new checkbox for 'Include Ship CapEx' directly alongside the dropdowns.
|
|
|
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>
|
|
@@ -71,9 +73,12 @@ async function render() {
|
|
|
<label style="margin-right: 15px;">OpEx Price:
|
|
<label style="margin-right: 15px;">OpEx Price:
|
|
|
<select id="opex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
|
|
<select id="opex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
|
|
|
</label>
|
|
</label>
|
|
|
- <label>Revenue (Outputs) Price:
|
|
|
|
|
|
|
+ <label style="margin-right: 15px;">Revenue Price:
|
|
|
<select id="revenue-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
|
|
<select id="revenue-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
|
|
|
</label>
|
|
</label>
|
|
|
|
|
+ <label>
|
|
|
|
|
+ <input type="checkbox" id="include-ships"> Include Ship CapEx
|
|
|
|
|
+ </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);
|
|
@@ -81,6 +86,7 @@ async function render() {
|
|
|
(document.getElementById('capex-metric') as HTMLSelectElement).value = capexMetric;
|
|
(document.getElementById('capex-metric') as HTMLSelectElement).value = capexMetric;
|
|
|
(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
|
|
(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
|
|
|
(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('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;
|
|
@@ -94,6 +100,10 @@ async function render() {
|
|
|
revenueMetric = (e.target as HTMLSelectElement).value as MetricType;
|
|
revenueMetric = (e.target as HTMLSelectElement).value as MetricType;
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
|
|
+ document.getElementById('include-ships')!.addEventListener('change', (e) => {
|
|
|
|
|
+ includeShips = (e.target as HTMLInputElement).checked;
|
|
|
|
|
+ render();
|
|
|
|
|
+ });
|
|
|
metricControlsInitialized = true;
|
|
metricControlsInitialized = true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -114,7 +124,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 (construction + habitation costs) scaled to a full 500-area planetary base.';
|
|
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nTotal capital expenditure (construction + habitation costs) scaled to a full 500-area planetary base.\nIf "Include Ship CapEx" is toggled, adds the cost of ships (800k each) required for daily logistics.';
|
|
|
} 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.';
|
|
@@ -177,12 +187,18 @@ async function render() {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
|
|
const profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
|
|
|
- // EXTREME DETAIL: Apply the 500-area baseline scaling factor to all financial metrics.
|
|
|
|
|
const bases = p.area / 500;
|
|
const bases = p.area / 500;
|
|
|
- const capex_val = p.capex[capexMetric] / bases;
|
|
|
|
|
|
|
+ let capex_val = p.capex[capexMetric] / bases;
|
|
|
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: If the user toggles the UI checkbox, we silently inject the precalculated
|
|
|
|
|
+ // ship capital expenditures into the active CapEx pipeline. This causes the break-even math
|
|
|
|
|
+ // to dynamically cascade and update instantly.
|
|
|
|
|
+ if (includeShips) {
|
|
|
|
|
+ capex_val += p.ship_capex_per_base;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
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 + 3 * opex_val) / profit_per_base : Infinity;
|
|
const break_even = profit_per_base > 0 ? (capex_val + 3 * opex_val) / profit_per_base : Infinity;
|
|
|
|
|
|
|
@@ -207,10 +223,6 @@ async function render() {
|
|
|
const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
const volumeRatio = p.output_per_day / p.average_traded_7d;
|
|
|
const tr = document.createElement('tr');
|
|
const tr = document.createElement('tr');
|
|
|
|
|
|
|
|
- // EXTREME DETAIL: Because 1 base = 500 area, the color scales must be adjusted
|
|
|
|
|
- // proportionately (roughly ~10x depending on the building) to ensure the visual gradients still
|
|
|
|
|
- // render accurately for the expanded scale.
|
|
|
|
|
- // e.g. CapEx bounds changed from 300,000 to 3,000,000.
|
|
|
|
|
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>
|
|
@@ -226,10 +238,6 @@ async function render() {
|
|
|
output.dataset.tooltip = p.recipe;
|
|
output.dataset.tooltip = p.recipe;
|
|
|
|
|
|
|
|
const profitCell = tr.querySelectorAll('td')[2];
|
|
const profitCell = tr.querySelectorAll('td')[2];
|
|
|
-
|
|
|
|
|
- // EXTREME DETAIL: Pass the per-base scaling factor into the formatMatPrices tooltip function.
|
|
|
|
|
- // This explicitly scales the quantities of inputs and outputs shown in the hover breakdown
|
|
|
|
|
- // so that the math perfectly matches the final Profit/Base number shown in the table.
|
|
|
|
|
const runs_per_base = p.runs_per_day / (p.area / 500);
|
|
const runs_per_base = p.runs_per_day / (p.area / 500);
|
|
|
|
|
|
|
|
profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, runs_per_base) + '\n\n' +
|
|
profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, runs_per_base) + '\n\n' +
|
|
@@ -237,6 +245,15 @@ async function render() {
|
|
|
'+ worker consumables\n\n' +
|
|
'+ worker consumables\n\n' +
|
|
|
`(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_base)}`;
|
|
`(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_base)}`;
|
|
|
|
|
|
|
|
|
|
+ // EXTREME DETAIL: To provide absolute transparency, if the ship toggle is active,
|
|
|
|
|
+ // we inject a dedicated tooltip into the CapEx cell specifically. This proves to the user
|
|
|
|
|
+ // exactly how many fractional ships were assumed and how much they cost.
|
|
|
|
|
+ const capexCell = tr.querySelectorAll('td')[4];
|
|
|
|
|
+ capexCell.dataset.tooltip = `Base Construction: ${formatWhole(p.capex[capexMetric] / (p.area / 500))}`;
|
|
|
|
|
+ if (includeShips) {
|
|
|
|
|
+ capexCell.dataset.tooltip += `\nShip CapEx: ${formatWhole(p.ship_capex_per_base)} (${formatDecimal(p.ship_capex_per_base / 800_000)} ships)`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const marketCell = tr.querySelectorAll('td')[7];
|
|
const marketCell = tr.querySelectorAll('td')[7];
|
|
|
marketCell.dataset.tooltip = `Market Capacity: ${formatWhole(p.average_traded_7d)} traded/day ÷ ${formatDecimal(p.output_per_day / (p.area / 500))} produced/day/base = ${formatDecimal(p.market_capacity_base)} equivalent bases`;
|
|
marketCell.dataset.tooltip = `Market Capacity: ${formatWhole(p.average_traded_7d)} traded/day ÷ ${formatDecimal(p.output_per_day / (p.area / 500))} produced/day/base = ${formatDecimal(p.market_capacity_base)} equivalent bases`;
|
|
|
|
|
|
|
@@ -258,6 +275,7 @@ function saveState() {
|
|
|
localStorage.setItem('roi-capex-metric', capexMetric);
|
|
localStorage.setItem('roi-capex-metric', capexMetric);
|
|
|
localStorage.setItem('roi-opex-metric', opexMetric);
|
|
localStorage.setItem('roi-opex-metric', opexMetric);
|
|
|
localStorage.setItem('roi-revenue-metric', revenueMetric);
|
|
localStorage.setItem('roi-revenue-metric', revenueMetric);
|
|
|
|
|
+ localStorage.setItem('roi-include-ships', includeShips.toString());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function color(n: number, low: number, high: number): string {
|
|
function color(n: number, low: number, high: number): string {
|
|
@@ -301,6 +319,7 @@ interface Profit {
|
|
|
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
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface ProfitWithMetrics extends Profit {
|
|
interface ProfitWithMetrics extends Profit {
|