7 Komitmen 77f02e061a ... bcd3176086

Pembuat SHA1 Pesan Tanggal
  raylu bcd3176086 roi: include habitation area and capex 1 hari lalu
  raylu 1ef2c5a028 roi: fertility 1 hari lalu
  raylu b3877a8e39 roi: stop outputting table to console 1 hari lalu
  raylu aaca060931 supply: exclude ignored materials from weight/volume 1 hari lalu
  raylu 35b40f61c0 roi: use popover for tooltips 2 hari lalu
  raylu 77f02e061a supply: exclude ignored materials from weight/volume 1 hari lalu
  raylu 5caacda53a roi: use popover for tooltips 2 hari lalu
4 mengubah file dengan 66 tambahan dan 50 penghapusan
  1. 27 23
      roi.py
  2. 3 3
      ts/roi.ts
  3. 7 3
      www/index.html
  4. 29 21
      www/style.css

+ 27 - 23
roi.py

@@ -15,33 +15,29 @@ def main() -> None:
 		p['MaterialTicker']: Price(p['VWAP7D'], p['AverageTraded7D']) for p in raw_prices # pyright: ignore[reportArgumentType]
 		if p['ExchangeCode'] == 'IC1' and p['VWAP7D'] is not None
 	}
+	habitation: typing.Mapping[Worker, str] = {
+		'Pioneers': 'HB1',
+		'Settlers': 'HB2',
+		'Technicians': 'HB3',
+		'Engineers': 'HB4',
+		'Scientists': 'HB5',
+	}
+	hab_area_cost: dict[Worker, float] = {}
+	hab_capex: dict[Worker, float] = {}
+	for worker, hab in habitation.items():
+		hab_area_cost[worker] = buildings[hab]['AreaCost'] / 100
+		hab_capex[worker] = building_construction_cost(buildings[hab], prices) / 100
 
 	profits: list[Profit] = []
 	for recipe in recipes:
-		if profit := calc_profit(recipe, buildings, materials, prices):
+		if profit := calc_profit(recipe, buildings, hab_area_cost, hab_capex, materials, prices):
 			profits.append(profit)
 	profits.sort()
-	print('\033[1mwrought    \033[0;32mdaily profit/area\033[31m')
-	print('\033[30mrecipe                         \033[0mexpertise            \033[33mcapex \033[35mdaily opex \033[34mlogistics\033[0m')
-	for p in profits:
-		print(f'\033[53;1m{p.output:5} \033[53;32m{p.profit_per_day/p.area: 10,.0f} ', end='')
-		warnings = []
-		if p.average_traded_7d < p.output_per_day * 20:
-			warnings.append('low volume')
-		if p.logistics_per_area > 1.5:
-			warnings.append('heavy logistics')
-		if p.cost_per_day > 50_000:
-			warnings.append('high opex')
-		if len(warnings) > 0:
-			print('              \033[31m' + ' '.join(warnings).ljust(43), end='')
-		else:
-			print(' ' * 14 + '\033[0;53m' + ' ' * 43, end='')
-		print(f'\n\033[0;30m{p.recipe:30} \033[0m{p.expertise:19} \033[33m{p.capex:7,.0f} \033[35m{p.cost_per_day:9,.0f} \033[34m{p.logistics_per_area:5.2f}\033[0m')
-
 	with open('www/roi.json', 'w') as f:
 		json.dump([dataclasses.asdict(p) for p in profits], f, indent='\t')
 
-def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], materials: typing.Mapping[str, Material],
+def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], hab_area_cost: typing.Mapping[Worker, float],
+		hab_capex: typing.Mapping[Worker, float], materials: typing.Mapping[str, Material],
 		prices: typing.Mapping[str, Price]) -> Profit | None:
 	try:
 		(output,) = recipe['Outputs']
@@ -54,10 +50,13 @@ def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], materi
 		return
 	revenue = output_price.vwap * output['Amount']
 	building = buildings[recipe['BuildingTicker']]
-	capex = sum(bm['Amount'] * prices[bm['CommodityTicker']].vwap
-			for bm in building['BuildingCosts'])
+	area = building['AreaCost'] + sum(hab_area_cost[worker] * building[worker] for worker in hab_area_cost)
+	capex = building_construction_cost(building, prices) + \
+		sum(hab_capex[worker] * building[worker] for worker in hab_capex)
 	profit_per_run = revenue - cost
 	runs_per_day = 24 * 60 * 60 * 1000 / recipe['TimeMs']
+	if building['Ticker'] in ('FRM', 'ORC'):
+		runs_per_day *= 1.1212 # promitor's fertility
 	worker_consumable_daily_cost = building_daily_cost(building, prices)
 	cost_per_day = cost * runs_per_day + worker_consumable_daily_cost
 	output_per_day = output['Amount'] * runs_per_day
@@ -66,17 +65,20 @@ def calc_profit(recipe: Recipe, buildings: typing.Mapping[str, Building], materi
 		sum(materials[input['Ticker']]['Volume'] * input['Amount'] for input in recipe['Inputs']),
 		materials[output['Ticker']]['Weight'] * output['Amount'],
 		materials[output['Ticker']]['Volume'] * output['Amount'],
-	) * runs_per_day / building['AreaCost']
+	) * runs_per_day / area
 	return Profit(output['Ticker'], recipe['RecipeName'],
 			expertise=building['Expertise'].replace('_', ' ').lower(),
 			profit_per_day=(profit_per_run * runs_per_day - worker_consumable_daily_cost),
-			area=building['AreaCost'],
+			area=area,
 			capex=capex,
 			cost_per_day=cost_per_day,
 			logistics_per_area=logistics_per_area,
 			output_per_day=output_per_day,
 			average_traded_7d=output_price.average_traded_7d)
 
+def building_construction_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
+	return sum(bc['Amount'] * prices[bc['CommodityTicker']].vwap for bc in building['BuildingCosts'])
+
 def building_daily_cost(building: Building, prices: typing.Mapping[str, Price]) -> float:
 	consumption = {
 		'Pioneers': [('COF', 0.5), ('DW', 4), ('RAT', 4), ('OVE', 0.5), ('PWO', 0.2)],
@@ -92,6 +94,8 @@ def building_daily_cost(building: Building, prices: typing.Mapping[str, Price])
 			cost += prices[mat].vwap * workers * per_100 / 100
 	return cost
 
+Worker = typing.Literal['Pioneers', 'Settlers', 'Technicians', 'Engineers', 'Scientists']
+
 class Recipe(typing.TypedDict):
 	RecipeName: str
 	BuildingTicker: str

+ 3 - 3
ts/roi.ts

@@ -22,9 +22,9 @@ async function render() {
 		tr.innerHTML = `
 			<td>${p.output}</td>
 			<td>${p.expertise}</td>
-			<td style="color: ${color(profit_per_area, 0, 500)}">${formatDecimal(profit_per_area)}</td>
-			<td><span style="color: ${color(break_even, 30, 2)}">${formatDecimal(break_even)}</span>d</td>
-			<td style="color: ${color(p.capex, 300_000, 50_000)}">${formatWhole(p.capex)}</td>
+			<td style="color: ${color(profit_per_area, 0, 250)}">${formatDecimal(profit_per_area)}</td>
+			<td><span style="color: ${color(break_even, 30, 3)}">${formatDecimal(break_even)}</span>d</td>
+			<td style="color: ${color(p.capex, 300_000, 40_000)}">${formatWhole(p.capex)}</td>
 			<td style="color: ${color(p.cost_per_day, 40_000, 1_000)}">${formatWhole(p.cost_per_day)}</td>
 			<td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
 			<td>

+ 7 - 3
www/index.html

@@ -16,17 +16,21 @@
 				<tr>
 					<th>wrought<br>product</th>
 					<th>expertise</th>
-					<th>daily<br>profit/area</th>
+					<th data-tooltip="area includes worker habitation">daily<br>profit/area</th>
 					<th>break<br>even</th>
-					<th data-tooltip="construction cost of 1 building">capex</th>
+					<th data-tooltip="construction cost of 1 building and fractional habitation">capex</th>
 					<th data-tooltip="input and worker costs of 1 building (deterioration/repair not included)">daily<br>opex</th>
-					<th data-tooltip="max of input and output t and m³ per area">logistics</th>
+					<th data-tooltip="max of input and output t and m³ per area. 2 ≅ 2 SCB visits/day. 0.25 ≅ SCB visit every 4 days">logistics</th>
 					<th data-tooltip="units output by 1 building over average daily traded">daily output<br>traded</th>
 				</tr>
 			</thead>
 			<tbody></tbody>
 		</table>
 	</main>
+	<footer>
+		prices and traded volume are IC1/HRT from refined-price's 7-day volume-weighted average
+		<br>FRM and ORC use 112.12% fertility (promitor's)
+	</footer>
 	<div id="popover" popover="hint"></div>
 	<script src="roi.js"></script>
 </body>

+ 29 - 21
www/style.css

@@ -28,31 +28,37 @@ input {
 	accent-color: #f70;
 }
 
-main {
+main, footer {
 	width: 900px;
 	margin: 10px auto;
-	padding: 10px 50px 25px;
 	background-color: #111;
-	box-shadow: 0 0 5px #222;
+}
+main {
+	padding: 40px 50px;
+}
+footer {
+	padding: 25px 50px;
+	margin-bottom: 50px;
+	font-size: 16px;
+}
 
-	table {
-		width: 100%;
-		margin-top: 1em;
-		border-collapse: collapse;
-		th {
-			white-space: no-wrap;
+table {
+	width: 100%;
+	margin-top: 1em;
+	border-collapse: collapse;
+	th {
+		white-space: no-wrap;
+	}
+	tbody {
+		td:nth-child(1),
+		td:nth-child(2) {
+			font-family: inherit;
+			text-align: inherit;
 		}
-		tbody {
-			td:nth-child(1),
-			td:nth-child(2) {
-				font-family: inherit;
-				text-align: inherit;
-			}
-			td {
-				font-family: monospace;
-				text-align: right;
-				border-top: 1px solid #222;
-			}
+		td {
+			font-family: monospace;
+			text-align: right;
+			border-top: 1px solid #222;
 		}
 	}
 }
@@ -64,8 +70,10 @@ main {
 
 [popover="hint"] {
 	inset: unset;
+	max-width: 320px;
+	padding: 0.5em 0.7em;
 	color: inherit;
 	background-color: #222;
 	border: none;
-	opacity: 0.8;
+	opacity: 0.9;
 }