|
@@ -56,15 +56,12 @@ 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';
|
|
|
|
|
|
|
|
-// 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);
|
|
|
|
|
|
|
+// EXTREME DETAIL: Changed state variables from numerical ints to strings to natively support
|
|
|
|
|
+// the 'omit' and 'dynamic' categorical values alongside the raw number options.
|
|
|
|
|
+let roundTripOption: string = localStorage.getItem('roi-round-trip') || '0';
|
|
|
let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
|
|
let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false';
|
|
|
-
|
|
|
|
|
-// 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 workingCapitalOption: string = localStorage.getItem('roi-working-capital-opt') || 'dynamic';
|
|
|
-let targetPermit: number = parseInt(localStorage.getItem('roi-target-permit') || '2', 10);
|
|
|
|
|
|
|
+let targetPermitOption: string = localStorage.getItem('roi-target-permit') || '2';
|
|
|
|
|
|
|
|
async function render() {
|
|
async function render() {
|
|
|
const tbody = document.querySelector('tbody')!;
|
|
const tbody = document.querySelector('tbody')!;
|
|
@@ -78,7 +75,8 @@ 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: Swapped the checkbox and number inputs for Round Trip (hrs) and the Dropdown Select.
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Converted Round Trip and Permit Number to distinct <select> dropdowns.
|
|
|
|
|
+ // Injected `<option value="omit">Omit From Calculation</option>` at the top of all three.
|
|
|
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>
|
|
@@ -93,11 +91,24 @@ async function render() {
|
|
|
<input type="checkbox" id="show-negative"> Show Negative Profit
|
|
<input type="checkbox" id="show-negative"> Show Negative Profit
|
|
|
</label>
|
|
</label>
|
|
|
<label style="margin-right: 15px;">
|
|
<label style="margin-right: 15px;">
|
|
|
- Round Trip (hrs): <input type="number" id="round-trip-hours" min="0" step="1" style="width: 50px;">
|
|
|
|
|
|
|
+ Round Trip (hrs):
|
|
|
|
|
+ <select id="round-trip">
|
|
|
|
|
+ <option value="omit">Omit From Calculation</option>
|
|
|
|
|
+ <option value="0">0</option>
|
|
|
|
|
+ <option value="2">2</option>
|
|
|
|
|
+ <option value="4">4</option>
|
|
|
|
|
+ <option value="8">8</option>
|
|
|
|
|
+ <option value="12">12</option>
|
|
|
|
|
+ <option value="24">24</option>
|
|
|
|
|
+ <option value="48">48</option>
|
|
|
|
|
+ <option value="72">72</option>
|
|
|
|
|
+ <option value="168">168</option>
|
|
|
|
|
+ </select>
|
|
|
</label>
|
|
</label>
|
|
|
<label style="margin-right: 15px;">
|
|
<label style="margin-right: 15px;">
|
|
|
Days OpEx:
|
|
Days OpEx:
|
|
|
<select id="working-capital">
|
|
<select id="working-capital">
|
|
|
|
|
+ <option value="omit">Omit From Calculation</option>
|
|
|
<option value="dynamic">Max for Shipment (dynamic)</option>
|
|
<option value="dynamic">Max for Shipment (dynamic)</option>
|
|
|
<option value="0">0</option>
|
|
<option value="0">0</option>
|
|
|
<option value="1">1</option>
|
|
<option value="1">1</option>
|
|
@@ -112,7 +123,20 @@ async function render() {
|
|
|
</select>
|
|
</select>
|
|
|
</label>
|
|
</label>
|
|
|
<label>
|
|
<label>
|
|
|
- Permit Number: <input type="number" id="target-permit" min="1" step="1" style="width: 50px;">
|
|
|
|
|
|
|
+ Permit Number:
|
|
|
|
|
+ <select id="target-permit">
|
|
|
|
|
+ <option value="omit">Omit From Calculation</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="8">8</option>
|
|
|
|
|
+ <option value="9">9</option>
|
|
|
|
|
+ <option value="10">10</option>
|
|
|
|
|
+ </select>
|
|
|
</label>
|
|
</label>
|
|
|
`;
|
|
`;
|
|
|
const table = document.querySelector('table');
|
|
const table = document.querySelector('table');
|
|
@@ -122,9 +146,9 @@ async function render() {
|
|
|
(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('show-negative') as HTMLInputElement).checked = showNegativeProfit;
|
|
(document.getElementById('show-negative') as HTMLInputElement).checked = showNegativeProfit;
|
|
|
- (document.getElementById('round-trip-hours') as HTMLInputElement).value = roundTripHours.toString();
|
|
|
|
|
|
|
+ (document.getElementById('round-trip') as HTMLSelectElement).value = roundTripOption;
|
|
|
(document.getElementById('working-capital') as HTMLSelectElement).value = workingCapitalOption;
|
|
(document.getElementById('working-capital') as HTMLSelectElement).value = workingCapitalOption;
|
|
|
- (document.getElementById('target-permit') as HTMLInputElement).value = targetPermit.toString();
|
|
|
|
|
|
|
+ (document.getElementById('target-permit') as HTMLSelectElement).value = targetPermitOption;
|
|
|
|
|
|
|
|
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;
|
|
@@ -142,8 +166,8 @@ async function render() {
|
|
|
showNegativeProfit = (e.target as HTMLInputElement).checked;
|
|
showNegativeProfit = (e.target as HTMLInputElement).checked;
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
- document.getElementById('round-trip-hours')!.addEventListener('change', (e) => {
|
|
|
|
|
- roundTripHours = parseInt((e.target as HTMLInputElement).value, 10);
|
|
|
|
|
|
|
+ document.getElementById('round-trip')!.addEventListener('change', (e) => {
|
|
|
|
|
+ roundTripOption = (e.target as HTMLSelectElement).value;
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
document.getElementById('working-capital')!.addEventListener('change', (e) => {
|
|
document.getElementById('working-capital')!.addEventListener('change', (e) => {
|
|
@@ -151,7 +175,7 @@ async function render() {
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
document.getElementById('target-permit')!.addEventListener('change', (e) => {
|
|
document.getElementById('target-permit')!.addEventListener('change', (e) => {
|
|
|
- targetPermit = parseInt((e.target as HTMLInputElement).value, 10);
|
|
|
|
|
|
|
+ targetPermitOption = (e.target as HTMLSelectElement).value;
|
|
|
render();
|
|
render();
|
|
|
});
|
|
});
|
|
|
metricControlsInitialized = true;
|
|
metricControlsInitialized = true;
|
|
@@ -174,7 +198,7 @@ async function render() {
|
|
|
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.)';
|
|
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') {
|
|
} 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), optional HQ Upgrade materials for the target permit, and Ship CapEx (set Round Trip hrs to 0 to exclude).';
|
|
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nTotal capital expenditure scaled to a full 500-area planetary base.\nIncludes base construction, and optional Working Capital, HQ Upgrades, and Ship CapEx (use "Omit From Calculation" to exclude).';
|
|
|
} 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.';
|
|
@@ -185,7 +209,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 and HQ upgrades to accurately reflect operational readiness.';
|
|
|
|
|
|
|
+ th.dataset.tooltip = 'Click to sort.\n\nBreak Even: CapEx ÷ daily profit. Note that CapEx dynamically includes optional logistics and HQ upgrades to accurately reflect operational readiness.';
|
|
|
} else {
|
|
} else {
|
|
|
th.dataset.tooltip = 'Click to sort.';
|
|
th.dataset.tooltip = 'Click to sort.';
|
|
|
}
|
|
}
|
|
@@ -236,43 +260,47 @@ async function render() {
|
|
|
return true;
|
|
return true;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const dynamicOpEx = workingCapitalOption === 'dynamic';
|
|
|
|
|
-
|
|
|
|
|
let profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
|
|
let profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
|
|
|
const bases = p.area / 500;
|
|
const bases = p.area / 500;
|
|
|
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: 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 capex_val = p.capex[capexMetric] / bases;
|
|
|
|
|
+
|
|
|
|
|
+ // EXTREME DETAIL: Conditionally evaluate OpEx constraints based on the new dropdown selection.
|
|
|
|
|
+ // If 'omit' is selected, activeWorkingCapitalDays remains 0, suppressing the cost completely.
|
|
|
let activeWorkingCapitalDays = 0;
|
|
let activeWorkingCapitalDays = 0;
|
|
|
- if (dynamicOpEx) {
|
|
|
|
|
- activeWorkingCapitalDays = p.normalized_logistics_per_base > 0
|
|
|
|
|
- ? 1 / p.normalized_logistics_per_base
|
|
|
|
|
- : 0;
|
|
|
|
|
- } else {
|
|
|
|
|
- activeWorkingCapitalDays = parseInt(workingCapitalOption, 10);
|
|
|
|
|
|
|
+ if (workingCapitalOption !== 'omit') {
|
|
|
|
|
+ if (workingCapitalOption === 'dynamic') {
|
|
|
|
|
+ activeWorkingCapitalDays = p.normalized_logistics_per_base > 0
|
|
|
|
|
+ ? 1 / p.normalized_logistics_per_base
|
|
|
|
|
+ : 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ activeWorkingCapitalDays = parseInt(workingCapitalOption, 10);
|
|
|
|
|
+ }
|
|
|
|
|
+ capex_val += (opex_val * activeWorkingCapitalDays);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- let capex_val = (p.capex[capexMetric] / bases) + (opex_val * activeWorkingCapitalDays);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Target permits safely bypassed if 'omit' is active.
|
|
|
let hq_capex = 0;
|
|
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 (targetPermitOption !== 'omit') {
|
|
|
|
|
+ const targetPermit = parseInt(targetPermitOption, 10);
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 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) {
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Little's Law ship mathematics safely bypassed if 'omit' is active.
|
|
|
|
|
+ let shipsNeeded = 0;
|
|
|
|
|
+ let activeShipCapex = 0;
|
|
|
|
|
+ if (roundTripOption !== 'omit') {
|
|
|
|
|
+ const roundTripHours = parseInt(roundTripOption, 10);
|
|
|
|
|
+ shipsNeeded = p.normalized_logistics_per_base * (roundTripHours / 24);
|
|
|
|
|
+ activeShipCapex = shipsNeeded * 800_000;
|
|
|
capex_val += activeShipCapex;
|
|
capex_val += activeShipCapex;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -351,16 +379,21 @@ 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: 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.
|
|
|
|
|
|
|
+ // EXTREME DETAIL: Conditionally rendered the CapEx Tooltip breakdown.
|
|
|
|
|
+ // If the user selected 'Omit From Calculation' for any parameter, that specific line
|
|
|
|
|
+ // completely vanishes from the hover tooltip, confirming to the user that it was removed.
|
|
|
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 (${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 (workingCapitalOption !== 'omit') {
|
|
|
|
|
+ capexCell.dataset.tooltip += `\nWorking Capital (${formatSigFig(p.activeWorkingCapitalDays)} days): ${formatSigFig(p.opex_val * p.activeWorkingCapitalDays)}`;
|
|
|
}
|
|
}
|
|
|
- if (roundTripHours > 0) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (targetPermitOption !== 'omit' && p.hq_capex > 0) {
|
|
|
|
|
+ capexCell.dataset.tooltip += `\nHQ Upgrade (Permit ${targetPermitOption}): ${formatSigFig(p.hq_capex)}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (roundTripOption !== 'omit') {
|
|
|
capexCell.dataset.tooltip += `\nShip CapEx: ${formatSigFig(p.activeShipCapex)} (${formatSigFig(p.shipsNeeded)} ships)`;
|
|
capexCell.dataset.tooltip += `\nShip CapEx: ${formatSigFig(p.activeShipCapex)} (${formatSigFig(p.shipsNeeded)} ships)`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -402,9 +435,9 @@ function saveState() {
|
|
|
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-show-negative', showNegativeProfit.toString());
|
|
localStorage.setItem('roi-show-negative', showNegativeProfit.toString());
|
|
|
- localStorage.setItem('roi-round-trip', roundTripHours.toString());
|
|
|
|
|
|
|
+ localStorage.setItem('roi-round-trip', roundTripOption);
|
|
|
localStorage.setItem('roi-working-capital-opt', workingCapitalOption);
|
|
localStorage.setItem('roi-working-capital-opt', workingCapitalOption);
|
|
|
- localStorage.setItem('roi-target-permit', targetPermit.toString());
|
|
|
|
|
|
|
+ localStorage.setItem('roi-target-permit', targetPermitOption);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function color(n: number, low: number, high: number): string {
|
|
function color(n: number, low: number, high: number): string {
|
|
@@ -461,9 +494,9 @@ interface ProfitWithMetrics extends Profit {
|
|
|
profit_per_base: number;
|
|
profit_per_base: number;
|
|
|
break_even: number;
|
|
break_even: number;
|
|
|
hq_capex: 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
|
|
|
|
|
|
|
+ activeWorkingCapitalDays: number;
|
|
|
|
|
+ activeShipCapex: number;
|
|
|
|
|
+ shipsNeeded: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
interface MatPrice {
|
|
interface MatPrice {
|