Selaa lähdekoodia

Add_Omit_Calculations_Dropdown_Toggles

Replaced textual numerical `<input>` fields with strict dropdown `<select>` options, adding an explicit 'Omit From Calculation' bypass.

        PURPOSE:
        To improve UI safety while providing a distinct toggle for isolating base construction costs. Numerical inputs run the risk of user error (e.g., typos that generate infinite loops or mathematically impossible supply chain matrices). Locking the inputs to predetermined `<select>` arrays prevents mathematical overflow. Furthermore, adding 'Omit From Calculation' allows players to instantly sever the OpEx, Ship, or HQ logic from the CapEx pipeline, providing a clean method to evaluate pure localized factory ROI.

        IMPLEMENTATION DETAILS:
        - `ts/roi.ts`: Overhauled the DOM generation block, swapping `<input type="number">` to `<select>` arrays for `round-trip`, `working-capital`, and `target-permit`.
        - `ts/roi.ts`: Migrated `roundTripOption` and `targetPermitOption` variables from parsed integers back to raw strings to safely encapsulate the `omit` logic branch without firing `NaN` errors.
        - `ts/roi.ts`: Within the `.map()` derivation block, encased the HQ, OpEx, and Little's Law math inside `if (Option !== 'omit')` safeguards. When bypassed, the variables strictly default to `0`, isolating the CapEx calculation strictly to base construction.
        - `ts/roi.ts`: Updated the CapEx cell tooltip constructor. If an option evaluates to `'omit'`, its string interpolation block is completely bypassed, vanishing that specific text from the hover element entirely to confirm to the user that the logic was cleanly severed.
Thomas Knott 2 viikkoa sitten
vanhempi
sitoutus
fb96c4b1e6
1 muutettua tiedostoa jossa 86 lisäystä ja 53 poistoa
  1. 86 53
      ts/roi.ts

+ 86 - 53
ts/roi.ts

@@ -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 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';
-
-// 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);
+let targetPermitOption: string = localStorage.getItem('roi-target-permit') || '2';
 
 async function render() {
 	const tbody = document.querySelector('tbody')!;
@@ -78,7 +75,8 @@ 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.
+		// 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 = `
 			<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>
@@ -93,11 +91,24 @@ async function render() {
 				<input type="checkbox" id="show-negative"> Show Negative Profit
 			</label>
 			<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 style="margin-right: 15px;">
 				Days OpEx: 
 				<select id="working-capital">
+					<option value="omit">Omit From Calculation</option>
 					<option value="dynamic">Max for Shipment (dynamic)</option>
 					<option value="0">0</option>
 					<option value="1">1</option>
@@ -112,7 +123,20 @@ async function render() {
 				</select>
 			</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>
 		`;
 		const table = document.querySelector('table');
@@ -122,9 +146,9 @@ async function render() {
 		(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
 		(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
 		(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('target-permit') as HTMLInputElement).value = targetPermit.toString();
+		(document.getElementById('target-permit') as HTMLSelectElement).value = targetPermitOption;
 
 		document.getElementById('capex-metric')!.addEventListener('change', (e) => {
 			capexMetric = (e.target as HTMLSelectElement).value as MetricType;
@@ -142,8 +166,8 @@ async function render() {
 			showNegativeProfit = (e.target as HTMLInputElement).checked;
 			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();
 		});
 		document.getElementById('working-capital')!.addEventListener('change', (e) => {
@@ -151,7 +175,7 @@ async function render() {
 			render();
 		});
 		document.getElementById('target-permit')!.addEventListener('change', (e) => {
-			targetPermit = parseInt((e.target as HTMLInputElement).value, 10);
+			targetPermitOption = (e.target as HTMLSelectElement).value;
 			render();
 		});
 		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.)';
 				} 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 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') {
 					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.';
@@ -185,7 +209,7 @@ async function render() {
 					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.';
 				} 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 {
 					th.dataset.tooltip = 'Click to sort.';
 				}
@@ -236,43 +260,47 @@ 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;
 		
-		// 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;
-		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;
-		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;
 		}
 
@@ -351,16 +379,21 @@ 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.
+		// 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];
 		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)`;
 		}
 
@@ -402,9 +435,9 @@ function saveState() {
 	localStorage.setItem('roi-opex-metric', opexMetric);
 	localStorage.setItem('roi-revenue-metric', revenueMetric);
 	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-target-permit', targetPermit.toString());
+	localStorage.setItem('roi-target-permit', targetPermitOption);
 }
 
 function color(n: number, low: number, high: number): string {
@@ -461,9 +494,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
+	activeWorkingCapitalDays: number; 
+	activeShipCapex: number; 
+	shipsNeeded: number; 
 }
 
 interface MatPrice {