Explorar el Código

Scale_CapEx_and_OpEx_To_Planetary_Base

Fully unified all table metrics to a 500-area "Planetary Base" standard.

        PURPOSE:
        To resolve scaling inconsistencies across the dashboard. Previously, Profit, Logistics, and Market Capacity were scaled to represent a 500-area planetary base, while CapEx and OpEx remained scaled to individual, 1-area footprint units. This mismatch forced users to perform mental gymnastics to relate construction costs to profit yields. By dynamically scaling CapEx and OpEx up via the `(area / 500)` multiplier, the dashboard now natively provides holistic, end-to-end strategic metrics for mass-scale industrial planning.

        IMPLEMENTATION DETAILS:
        - `roi.py`: Updated the fallback `__lt__` algorithm to calculate `c_a` (CapEx scaled) and `o_a` (OpEx scaled) before evaluating the negative-profit sorting logic.
        - `ts/roi.ts`: Implemented `const bases = p.area / 500;` within the data mapping iteration. Substituted the raw `capex[capexMetric]` evaluation with `capex[capexMetric] / bases`, achieving uniform mathematical scaling.
        - `ts/roi.ts`: Increased UI color mapping thresholds proportionally (`~10x`). Example: `capex_val` threshold increased from `[300k, 40k]` to `[3mil, 400k]` to ensure standard buildings utilizing ~50 area per unit continue to render informative visual gradients.
        - `ts/roi.ts`: Renamed table headers to `CapEx/Base` and `OpEx/Base` and added dedicated `dataset.tooltip` explanations.
        - `ts/roi.ts`: Abstracted `p.runs_per_day` to a new `runs_per_base` variable inside the hover tooltip string-builder. This forces the input/output quantity breakdown to physically display the total resources processed by the *entire base* per day, validating the final scaled `profit_per_base` number directly in the UI.
Thomas Knott hace 2 semanas
padre
commit
c998b72076
Se han modificado 6 ficheros con 659 adiciones y 642 borrados
  1. 14 8
      roi.py
  2. 29 18
      ts/roi.ts
  3. 154 154
      www/roi_ai1.json
  4. 154 154
      www/roi_ci1.json
  5. 154 154
      www/roi_ic1.json
  6. 154 154
      www/roi_nc1.json

+ 14 - 8
roi.py

@@ -100,8 +100,6 @@ def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], hab_ar
 	
 	average_traded_7d = output_prices[lowest_liquidity['material_ticker']].average_traded_7d
 	
-	# EXTREME DETAIL: We convert our metrics from 'per area' to 'per base' (1 base = 500 area).
-	# We divide the building's area footprint by 500 to determine what fraction of a base it consumes.
 	output_per_base = output_per_day / (area / 500)
 	market_capacity_base = average_traded_7d / output_per_base
 
@@ -219,17 +217,25 @@ class Profit:
 	revenue: dict[str, float]
 	input_costs: typing.Collection[MatPrice]
 	runs_per_day: float
-	logistics_per_base: float    # Renamed from area to base
+	logistics_per_base: float
 	output_per_day: float
 	average_traded_7d: float
-	market_capacity_base: float  # Renamed from area to base
+	market_capacity_base: float
 
 	def __lt__(self, other: Profit) -> bool:
-		p_a = self.revenue['vwap'] - self.opex['vwap']
-		be_a = (self.capex['vwap'] + 3 * self.opex['vwap']) / p_a if p_a > 0 else 10000 - p_a
+		# EXTREME DETAIL: Apply identical `(area / 500)` base scaling to the backend's
+		# default VWAP sorting algorithm so it perfectly mirrors the TS frontend implementation.
+		bases_a = self.area / 500
+		p_a = (self.revenue['vwap'] - self.opex['vwap']) / bases_a
+		c_a = self.capex['vwap'] / bases_a
+		o_a = self.opex['vwap'] / bases_a
+		be_a = (c_a + 3 * o_a) / p_a if p_a > 0 else 10000 - p_a
 		
-		p_b = other.revenue['vwap'] - other.opex['vwap']
-		be_b = (other.capex['vwap'] + 3 * other.opex['vwap']) / p_b if p_b > 0 else 10000 - p_b
+		bases_b = other.area / 500
+		p_b = (other.revenue['vwap'] - other.opex['vwap']) / bases_b
+		c_b = other.capex['vwap'] / bases_b
+		o_b = other.opex['vwap'] / bases_b
+		be_b = (c_b + 3 * o_b) / p_b if p_b > 0 else 10000 - p_b
 		return be_a < be_b
 
 @dataclasses.dataclass(eq=False, frozen=True, slots=True)

+ 29 - 18
ts/roi.ts

@@ -99,7 +99,6 @@ async function render() {
 
 	if (!headersInitialized) {
 		const ths = document.querySelectorAll('th');
-		// Map our new "_base" keys to the DOM headers
 		const keys: (keyof ProfitWithMetrics | 'outputs')[] = [
 			'outputs', 'expertise', 'profit_per_base', 'break_even', 
 			'capex_val', 'opex_val', 'logistics_per_base', 'market_capacity_base'
@@ -113,6 +112,12 @@ async function render() {
 				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.';
+				} else if (keys[i] === 'capex_val') {
+					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.';
+				} 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.';
 				} else if (keys[i] === 'logistics_per_base') {
 					th.textContent = 'Logistics/Base';
 					th.dataset.tooltip = 'Click to sort.\n\nDaily logistics volume/weight scaled to a full 500-area planetary base.';
@@ -172,16 +177,16 @@ async function render() {
 	});
 
 	const profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
-		const capex_val = p.capex[capexMetric];
-		const opex_val = p.opex[opexMetric];
-		const revenue_val = p.revenue[revenueMetric];
+		// EXTREME DETAIL: Apply the 500-area baseline scaling factor to all financial metrics.
+		const bases = p.area / 500;
+		const capex_val = p.capex[capexMetric] / bases;
+		const opex_val = p.opex[opexMetric] / bases;
+		const revenue_val = p.revenue[revenueMetric] / bases;
 		
-		const profit_per_day = revenue_val - opex_val;
-		// EXTREME DETAIL: Divide by the fraction of a base the building consumes.
-		const profit_per_base = profit_per_day / (p.area / 500);
-		const break_even = profit_per_day > 0 ? (capex_val + 3 * opex_val) / profit_per_day : Infinity;
+		const profit_per_base = revenue_val - opex_val;
+		const break_even = profit_per_base > 0 ? (capex_val + 3 * opex_val) / profit_per_base : Infinity;
 		
-		return { ...p, capex_val, opex_val, revenue_val, profit_per_day, profit_per_base, break_even };
+		return { ...p, capex_val, opex_val, revenue_val, profit_per_base, break_even };
 	});
 
 	profitsWithMetrics.sort((a, b) => {
@@ -203,16 +208,16 @@ async function render() {
 		const tr = document.createElement('tr');
 		
 		// EXTREME DETAIL: Because 1 base = 500 area, the color scales must be adjusted 
-		// proportionately to ensure the visual gradients still render accurately.
-		// e.g. Profit bounds changed from 300 to 150,000. 
-		// Market capacity bounds changed from [20, 500] to [0.04, 1].
+		// 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 = `
 			<td>${p.outputs.map(o => o.ticker).join(', ')}</td>
 			<td>${expertise[p.expertise]}</td>
 			<td style="color: ${color(p.profit_per_base, 0, 150000)}">${formatDecimal(p.profit_per_base)}</td>
 			<td><span style="color: ${color(p.break_even, 30, 3)}">${formatDecimal(p.break_even)}</span>d</td>
-			<td style="color: ${color(p.capex_val, 300_000, 40_000)}">${formatWhole(p.capex_val)}</td>
-			<td style="color: ${color(p.opex_val, 40_000, 1_000)}">${formatWhole(p.opex_val)}</td>
+			<td style="color: ${color(p.capex_val, 3_000_000, 400_000)}">${formatWhole(p.capex_val)}</td>
+			<td style="color: ${color(p.opex_val, 400_000, 10_000)}">${formatWhole(p.opex_val)}</td>
 			<td style="color: ${color(p.logistics_per_base, 1000, 100)}">${formatDecimal(p.logistics_per_base)}</td>
 			<td style="color: ${color(p.market_capacity_base, 0.04, 1)}">${formatDecimal(p.market_capacity_base)}</td>
 		`;
@@ -221,10 +226,16 @@ async function render() {
 		output.dataset.tooltip = p.recipe;
 
 		const profitCell = tr.querySelectorAll('td')[2];
-		profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, p.runs_per_day) + '\n\n' +
-			formatMatPrices(p.input_costs, opexMetric, p.runs_per_day) + '\n' +
-			`(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_day)}\n` +
-			`${formatDecimal(p.profit_per_day)} / ${formatDecimal(p.area / 500)} bases = ${formatDecimal(p.profit_per_base)}`;
+		
+		// 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);
+		
+		profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, runs_per_base) + '\n\n' +
+			formatMatPrices(p.input_costs, opexMetric, runs_per_base) + '\n' +
+			'+ worker consumables\n\n' +
+			`(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_base)}`;
 
 		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`;

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 154 - 154
www/roi_ai1.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 154 - 154
www/roi_ci1.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 154 - 154
www/roi_ic1.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 154 - 154
www/roi_nc1.json


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio