|
|
@@ -55,10 +55,15 @@ let metricControlsInitialized = false;
|
|
|
let capexMetric: MetricType = (localStorage.getItem('roi-capex-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 includeShips: boolean = localStorage.getItem('roi-include-ships') === 'true';
|
|
|
|
|
|
+// EXTREME DETAIL: Replaced `includeShips` with `roundTripHours`.
|
|
|
+// If the user inputs 0, ship CapEx evaluates to 0.
|
|
|
+let roundTripHours: number = parseInt(localStorage.getItem('roi-round-trip') || '0', 10);
|
|
|
let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
|
|
|
-let workingCapitalDays: number = parseInt(localStorage.getItem('roi-working-capital') || '3', 10);
|
|
|
+
|
|
|
+// EXTREME DETAIL: Replaced numerical `workingCapitalDays` state with a string `workingCapitalOption`
|
|
|
+// to natively support the 'dynamic' Dropdown value alongside hardcoded integers.
|
|
|
+let workingCapitalOption: string = localStorage.getItem('roi-working-capital-opt') || 'dynamic';
|
|
|
let targetPermit: number = parseInt(localStorage.getItem('roi-target-permit') || '2', 10);
|
|
|
|
|
|
async function render() {
|
|
|
@@ -73,6 +78,7 @@ async function render() {
|
|
|
if (!metricControlsInitialized) {
|
|
|
const controls = document.createElement('div');
|
|
|
controls.style.marginBottom = '15px';
|
|
|
+ // EXTREME DETAIL: Swapped the checkbox and number inputs for Round Trip (hrs) and the Dropdown Select.
|
|
|
controls.innerHTML = `
|
|
|
<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>
|
|
|
@@ -84,13 +90,26 @@ async function render() {
|
|
|
<select id="revenue-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
|
|
|
</label>
|
|
|
<label style="margin-right: 15px;">
|
|
|
- <input type="checkbox" id="include-ships"> Include Ship CapEx
|
|
|
+ <input type="checkbox" id="show-negative"> Show Negative Profit
|
|
|
</label>
|
|
|
<label style="margin-right: 15px;">
|
|
|
- <input type="checkbox" id="show-negative"> Show Negative Profit
|
|
|
+ Round Trip (hrs): <input type="number" id="round-trip-hours" min="0" step="1" style="width: 50px;">
|
|
|
</label>
|
|
|
<label style="margin-right: 15px;">
|
|
|
- Days OpEx: <input type="number" id="working-capital" min="0" step="1" style="width: 50px;">
|
|
|
+ Days OpEx:
|
|
|
+ <select id="working-capital">
|
|
|
+ <option value="dynamic">Max for Shipment (dynamic)</option>
|
|
|
+ <option value="0">0</option>
|
|
|
+ <option value="1">1</option>
|
|
|
+ <option value="2">2</option>
|
|
|
+ <option value="3">3</option>
|
|
|
+ <option value="4">4</option>
|
|
|
+ <option value="5">5</option>
|
|
|
+ <option value="6">6</option>
|
|
|
+ <option value="7">7</option>
|
|
|
+ <option value="14">14</option>
|
|
|
+ <option value="30">30</option>
|
|
|
+ </select>
|
|
|
</label>
|
|
|
<label>
|
|
|
Permit Number: <input type="number" id="target-permit" min="1" step="1" style="width: 50px;">
|
|
|
@@ -102,9 +121,9 @@ async function render() {
|
|
|
(document.getElementById('capex-metric') as HTMLSelectElement).value = capexMetric;
|
|
|
(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
|
|
|
(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
|
|
|
- (document.getElementById('include-ships') as HTMLInputElement).checked = includeShips;
|
|
|
(document.getElementById('show-negative') as HTMLInputElement).checked = showNegativeProfit;
|
|
|
- (document.getElementById('working-capital') as HTMLInputElement).value = workingCapitalDays.toString();
|
|
|
+ (document.getElementById('round-trip-hours') as HTMLInputElement).value = roundTripHours.toString();
|
|
|
+ (document.getElementById('working-capital') as HTMLSelectElement).value = workingCapitalOption;
|
|
|
(document.getElementById('target-permit') as HTMLInputElement).value = targetPermit.toString();
|
|
|
|
|
|
document.getElementById('capex-metric')!.addEventListener('change', (e) => {
|
|
|
@@ -119,16 +138,16 @@ async function render() {
|
|
|
revenueMetric = (e.target as HTMLSelectElement).value as MetricType;
|
|
|
render();
|
|
|
});
|
|
|
- document.getElementById('include-ships')!.addEventListener('change', (e) => {
|
|
|
- includeShips = (e.target as HTMLInputElement).checked;
|
|
|
- render();
|
|
|
- });
|
|
|
document.getElementById('show-negative')!.addEventListener('change', (e) => {
|
|
|
showNegativeProfit = (e.target as HTMLInputElement).checked;
|
|
|
render();
|
|
|
});
|
|
|
+ document.getElementById('round-trip-hours')!.addEventListener('change', (e) => {
|
|
|
+ roundTripHours = parseInt((e.target as HTMLInputElement).value, 10);
|
|
|
+ render();
|
|
|
+ });
|
|
|
document.getElementById('working-capital')!.addEventListener('change', (e) => {
|
|
|
- workingCapitalDays = parseInt((e.target as HTMLInputElement).value, 10);
|
|
|
+ workingCapitalOption = (e.target as HTMLSelectElement).value;
|
|
|
render();
|
|
|
});
|
|
|
document.getElementById('target-permit')!.addEventListener('change', (e) => {
|
|
|
@@ -150,13 +169,12 @@ async function render() {
|
|
|
th.style.cursor = 'pointer';
|
|
|
th.title = '';
|
|
|
|
|
|
- // EXTREME DETAIL: Updated the tooltip legend to explain that 100.0% is universally good.
|
|
|
if (keys[i] === 'profit_per_base') {
|
|
|
th.textContent = 'Profit/Base';
|
|
|
th.dataset.tooltip = 'Click to sort.\n\nDaily profit scaled to a full 500-area planetary base.\n(Percentiles rank 100.0% as the most desirable outcome, i.e., highest profit or lowest cost.)';
|
|
|
} else if (keys[i] === 'capex_val') {
|
|
|
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), optional HQ Upgrade materials for the target permit, 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 Ship CapEx (set Round Trip hrs to 0 to exclude).';
|
|
|
} else if (keys[i] === 'opex_val') {
|
|
|
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.';
|
|
|
@@ -218,12 +236,26 @@ async function render() {
|
|
|
return true;
|
|
|
});
|
|
|
|
|
|
+ const dynamicOpEx = workingCapitalOption === 'dynamic';
|
|
|
+
|
|
|
let profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
|
|
|
const bases = p.area / 500;
|
|
|
const opex_val = p.opex[opexMetric] / bases;
|
|
|
const revenue_val = p.revenue[revenueMetric] / bases;
|
|
|
|
|
|
- let capex_val = (p.capex[capexMetric] / bases) + (opex_val * workingCapitalDays);
|
|
|
+ // EXTREME DETAIL: We determine exactly how many OpEx days are required.
|
|
|
+ // If 'dynamic' is selected, the required working capital scales perfectly inverse to the logistics
|
|
|
+ // footprint (e.g., if a base fills 0.5 ships a day, the max shipment interval is 1/0.5 = 2 days).
|
|
|
+ let activeWorkingCapitalDays = 0;
|
|
|
+ if (dynamicOpEx) {
|
|
|
+ activeWorkingCapitalDays = p.normalized_logistics_per_base > 0
|
|
|
+ ? 1 / p.normalized_logistics_per_base
|
|
|
+ : 0;
|
|
|
+ } else {
|
|
|
+ activeWorkingCapitalDays = parseInt(workingCapitalOption, 10);
|
|
|
+ }
|
|
|
+
|
|
|
+ let capex_val = (p.capex[capexMetric] / bases) + (opex_val * activeWorkingCapitalDays);
|
|
|
|
|
|
let hq_capex = 0;
|
|
|
if (targetPermit >= 3) {
|
|
|
@@ -234,14 +266,31 @@ async function render() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (includeShips) {
|
|
|
- capex_val += p.ship_capex_per_base;
|
|
|
+ // EXTREME DETAIL: Little's Law dictates that Inventory in transit = Throughput * Lead Time.
|
|
|
+ // Throughput = daily ship fraction. Lead Time = Round Trip Hours / 24.
|
|
|
+ // This generates the absolute fleet size required to prevent bottlenecks.
|
|
|
+ const shipsNeeded = p.normalized_logistics_per_base * (roundTripHours / 24);
|
|
|
+ const activeShipCapex = shipsNeeded * 800_000;
|
|
|
+
|
|
|
+ if (roundTripHours > 0) {
|
|
|
+ capex_val += activeShipCapex;
|
|
|
}
|
|
|
|
|
|
const profit_per_base = revenue_val - opex_val;
|
|
|
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, hq_capex };
|
|
|
+ return {
|
|
|
+ ...p,
|
|
|
+ capex_val,
|
|
|
+ opex_val,
|
|
|
+ revenue_val,
|
|
|
+ profit_per_base,
|
|
|
+ break_even,
|
|
|
+ hq_capex,
|
|
|
+ activeWorkingCapitalDays,
|
|
|
+ activeShipCapex,
|
|
|
+ shipsNeeded
|
|
|
+ };
|
|
|
});
|
|
|
|
|
|
if (!showNegativeProfit) {
|
|
|
@@ -273,9 +322,6 @@ async function render() {
|
|
|
for (const p of profitsWithMetrics) {
|
|
|
const tr = document.createElement('tr');
|
|
|
|
|
|
- // EXTREME DETAIL: We explicitly route each column to its correct `invert` state.
|
|
|
- // Profit and Market Cap use `false` (highest numerical value = 100%).
|
|
|
- // Break Even, CapEx, OpEx, and Logistics use `true` (lowest numerical value = 100%).
|
|
|
const pctProfit = getPercentile(p.profit_per_base, arrProfit, false);
|
|
|
const pctBreak = getPercentile(p.break_even, arrBreak, true);
|
|
|
const pctCapex = getPercentile(p.capex_val, arrCapex, true);
|
|
|
@@ -305,15 +351,17 @@ async function render() {
|
|
|
'+ worker consumables\n\n' +
|
|
|
`(${formatSigFig(p.revenue_val)} - ${formatSigFig(p.opex_val)}) = ${formatSigFig(p.profit_per_base)}`;
|
|
|
|
|
|
+ // EXTREME DETAIL: Updated the tooltip to track the dynamically assigned working capital
|
|
|
+ // and the newly calculated Ship fleet size derived from the user's explicit Round Trip Time.
|
|
|
const capexCell = tr.querySelectorAll('td')[4];
|
|
|
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 (${formatSigFig(p.activeWorkingCapitalDays)} days): ${formatSigFig(p.opex_val * p.activeWorkingCapitalDays)}`;
|
|
|
|
|
|
if (p.hq_capex > 0) {
|
|
|
capexCell.dataset.tooltip += `\nHQ Upgrade (Permit ${targetPermit}): ${formatSigFig(p.hq_capex)}`;
|
|
|
}
|
|
|
- if (includeShips) {
|
|
|
- capexCell.dataset.tooltip += `\nShip CapEx: ${formatSigFig(p.ship_capex_per_base)} (${formatSigFig(p.ship_capex_per_base / 800_000)} ships)`;
|
|
|
+ if (roundTripHours > 0) {
|
|
|
+ capexCell.dataset.tooltip += `\nShip CapEx: ${formatSigFig(p.activeShipCapex)} (${formatSigFig(p.shipsNeeded)} ships)`;
|
|
|
}
|
|
|
|
|
|
const marketCell = tr.querySelectorAll('td')[7];
|
|
|
@@ -327,8 +375,6 @@ async function render() {
|
|
|
saveState();
|
|
|
}
|
|
|
|
|
|
-// EXTREME DETAIL: Added 'invert' boolean to flip logic for cost metrics.
|
|
|
-// Returns a heavily formatted string e.g., '99.0%'.
|
|
|
function getPercentile(val: number, sortedArr: number[], invert: boolean = false): string {
|
|
|
if (sortedArr.length < 2) return "100.0%";
|
|
|
let less = 0;
|
|
|
@@ -355,9 +401,9 @@ function saveState() {
|
|
|
localStorage.setItem('roi-capex-metric', capexMetric);
|
|
|
localStorage.setItem('roi-opex-metric', opexMetric);
|
|
|
localStorage.setItem('roi-revenue-metric', revenueMetric);
|
|
|
- localStorage.setItem('roi-include-ships', includeShips.toString());
|
|
|
localStorage.setItem('roi-show-negative', showNegativeProfit.toString());
|
|
|
- localStorage.setItem('roi-working-capital', workingCapitalDays.toString());
|
|
|
+ localStorage.setItem('roi-round-trip', roundTripHours.toString());
|
|
|
+ localStorage.setItem('roi-working-capital-opt', workingCapitalOption);
|
|
|
localStorage.setItem('roi-target-permit', targetPermit.toString());
|
|
|
}
|
|
|
|
|
|
@@ -404,7 +450,6 @@ interface Profit {
|
|
|
output_per_day: number
|
|
|
average_traded_7d: number
|
|
|
market_capacity_base: number
|
|
|
- ship_capex_per_base: number
|
|
|
hq_costs: Record<string, Metrics>
|
|
|
}
|
|
|
|
|
|
@@ -416,6 +461,9 @@ interface ProfitWithMetrics extends Profit {
|
|
|
profit_per_base: number;
|
|
|
break_even: number;
|
|
|
hq_capex: number;
|
|
|
+ activeWorkingCapitalDays: number; // Added to interface to pass to the tooltip generator
|
|
|
+ activeShipCapex: number; // Added to interface to pass to the tooltip generator
|
|
|
+ shipsNeeded: number; // Added to interface to pass to the tooltip generator
|
|
|
}
|
|
|
|
|
|
interface MatPrice {
|