checkpoint
This commit is contained in:
1
.idea/dictionaries/project.xml
generated
1
.idea/dictionaries/project.xml
generated
@@ -4,6 +4,7 @@
|
|||||||
<w>aurovite</w>
|
<w>aurovite</w>
|
||||||
<w>callanite</w>
|
<w>callanite</w>
|
||||||
<w>caterium</w>
|
<w>caterium</w>
|
||||||
|
<w>heatsink</w>
|
||||||
<w>larrussite</w>
|
<w>larrussite</w>
|
||||||
<w>quickwire</w>
|
<w>quickwire</w>
|
||||||
<w>siterite</w>
|
<w>siterite</w>
|
||||||
|
|||||||
204
main.py
204
main.py
@@ -34,7 +34,7 @@ class Production(BaseModel):
|
|||||||
quantity: float
|
quantity: float
|
||||||
|
|
||||||
class ProductionChain:
|
class ProductionChain:
|
||||||
def __init__(self):
|
def __init__(self, preferred_recipes: Optional[Dict[Items, Recipe]] = None):
|
||||||
self.item_to_recipies: Dict[Items, list[Recipes]] = defaultdict(list)
|
self.item_to_recipies: Dict[Items, list[Recipes]] = defaultdict(list)
|
||||||
for r in Recipes:
|
for r in Recipes:
|
||||||
for o in r.value.outputs:
|
for o in r.value.outputs:
|
||||||
@@ -49,9 +49,11 @@ class ProductionChain:
|
|||||||
self.production_chain: Dict[str, float] = defaultdict(float)
|
self.production_chain: Dict[str, float] = defaultdict(float)
|
||||||
self.excess: Dict[Items, float] = defaultdict(float)
|
self.excess: Dict[Items, float] = defaultdict(float)
|
||||||
self.byproduct: Dict[Items, float] = defaultdict(float)
|
self.byproduct: Dict[Items, float] = defaultdict(float)
|
||||||
|
self.preferred_recipes = preferred_recipes or {}
|
||||||
|
|
||||||
def get_recipe(self, item: Items) -> Optional[Recipe]:
|
def get_recipe(self, item: Items) -> Optional[Recipe]:
|
||||||
# TODO: make logic for priority selection of recipes
|
if item in self.preferred_recipes:
|
||||||
|
return self.preferred_recipes[item]
|
||||||
return self.item_to_recipies.get(item, (None,))[0]
|
return self.item_to_recipies.get(item, (None,))[0]
|
||||||
|
|
||||||
def calculate_excess(self, production: Dict[Items, float], demand: Dict[Items, float], raw_resources: Dict[Items, float]):
|
def calculate_excess(self, production: Dict[Items, float], demand: Dict[Items, float], raw_resources: Dict[Items, float]):
|
||||||
@@ -102,7 +104,12 @@ class ProductionChain:
|
|||||||
levels.append(target_quantity / quantity)
|
levels.append(target_quantity / quantity)
|
||||||
|
|
||||||
production_level = max(levels)
|
production_level = max(levels)
|
||||||
|
if production_level == 0:
|
||||||
|
continue
|
||||||
production_chain[recipe.name] += production_level
|
production_chain[recipe.name] += production_level
|
||||||
|
if production_chain[recipe.name] < 0:
|
||||||
|
del(production_chain[recipe.name])
|
||||||
|
production_level = 0
|
||||||
for out, quantity in recipe.outputs.items():
|
for out, quantity in recipe.outputs.items():
|
||||||
production[out] += production_level * quantity
|
production[out] += production_level * quantity
|
||||||
for byproduct, quantity in recipe.byproducts.items():
|
for byproduct, quantity in recipe.byproducts.items():
|
||||||
@@ -153,13 +160,77 @@ def index():
|
|||||||
name_to_item = {i.value.name: i for i in Items}
|
name_to_item = {i.value.name: i for i in Items}
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
error = None
|
error_msgs: List[str] = []
|
||||||
selected_item = item_names[0] if item_names else ""
|
|
||||||
selected_rate = 60.0
|
|
||||||
|
|
||||||
# Read from query parameters for bookmarkable URLs
|
# --------------------
|
||||||
item_name = request.args.get("item") or selected_item
|
# Parse targets (support multiple products)
|
||||||
rate_str = request.args.get("rate")
|
# --------------------
|
||||||
|
# Accept both legacy single `item`/`rate` and indexed `item_1`/`rate_1`, ...
|
||||||
|
# Determine how many indexed rows we have
|
||||||
|
indexed_pairs: List[Tuple[str, str]] = []
|
||||||
|
max_idx = 0
|
||||||
|
for key in request.args.keys():
|
||||||
|
if key.startswith("item_"):
|
||||||
|
try:
|
||||||
|
idx = int(key.split("_", 1)[1])
|
||||||
|
max_idx = max(max_idx, idx)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
for i in range(1, max_idx + 1):
|
||||||
|
indexed_pairs.append((request.args.get(f"item_{i}"), request.args.get(f"rate_{i}")))
|
||||||
|
|
||||||
|
# Build targets_ui (for rendering the form) and targets dict (for compute)
|
||||||
|
targets_ui: List[dict] = []
|
||||||
|
targets: Dict[Items, float] = defaultdict(float)
|
||||||
|
|
||||||
|
def _add_target_row(item_name: Optional[str], rate_str: Optional[str]):
|
||||||
|
nonlocal targets, targets_ui
|
||||||
|
if not item_name and not rate_str:
|
||||||
|
return
|
||||||
|
item_name = (item_name or "").strip()
|
||||||
|
rate_val: Optional[float] = None
|
||||||
|
if rate_str is not None and rate_str != "":
|
||||||
|
try:
|
||||||
|
rate_val = float(rate_str)
|
||||||
|
if rate_val < 0:
|
||||||
|
raise ValueError
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
error_msgs.append(f"Invalid rate for product '{item_name or '(missing)'}'. Enter a non-negative number.")
|
||||||
|
rate_val = None
|
||||||
|
if item_name:
|
||||||
|
targets_ui.append({"item": item_name, "rate": rate_val if rate_val is not None else 0.0})
|
||||||
|
if rate_val is not None:
|
||||||
|
item_obj = name_to_item.get(item_name)
|
||||||
|
if item_obj is None:
|
||||||
|
error_msgs.append(f"Unknown item '{item_name}'.")
|
||||||
|
else:
|
||||||
|
targets[item_obj] += rate_val
|
||||||
|
elif rate_val is not None:
|
||||||
|
# Rate without item name – just keep it in UI
|
||||||
|
targets_ui.append({"item": "", "rate": rate_val})
|
||||||
|
|
||||||
|
if indexed_pairs:
|
||||||
|
for item_nm, rate_s in indexed_pairs:
|
||||||
|
_add_target_row(item_nm, rate_s)
|
||||||
|
else:
|
||||||
|
# Legacy single controls
|
||||||
|
default_item = item_names[0] if item_names else ""
|
||||||
|
single_item = request.args.get("item") or default_item
|
||||||
|
single_rate = request.args.get("rate")
|
||||||
|
_add_target_row(single_item, single_rate)
|
||||||
|
if not request.args:
|
||||||
|
# Initial load: ensure at least one default row
|
||||||
|
targets_ui = [{"item": default_item, "rate": 60.0}]
|
||||||
|
if default_item:
|
||||||
|
item_obj0 = name_to_item.get(default_item)
|
||||||
|
if item_obj0:
|
||||||
|
targets[item_obj0] += 60.0
|
||||||
|
|
||||||
|
# Selected defaults are taken from the first row (for compatibility with existing template pieces)
|
||||||
|
selected_item = targets_ui[0]["item"] if targets_ui else (item_names[0] if item_names else "")
|
||||||
|
selected_rate = targets_ui[0]["rate"] if targets_ui else 60.0
|
||||||
|
|
||||||
|
# Optional top-level recipe (applies only when exactly one target)
|
||||||
selected_recipe = request.args.get("recipe") or ""
|
selected_recipe = request.args.get("recipe") or ""
|
||||||
|
|
||||||
# Parse per-item recipe overrides from query params recipe_for_<slug(item)>
|
# Parse per-item recipe overrides from query params recipe_for_<slug(item)>
|
||||||
@@ -184,23 +255,9 @@ def index():
|
|||||||
if value in candidates:
|
if value in candidates:
|
||||||
overrides[item_enum] = value
|
overrides[item_enum] = value
|
||||||
|
|
||||||
rate = None
|
# Candidate recipes for the (first) selected item (single-target convenience)
|
||||||
if rate_str is not None and rate_str != "":
|
|
||||||
try:
|
|
||||||
rate = float(rate_str)
|
|
||||||
if rate < 0:
|
|
||||||
raise ValueError
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
error = "Please enter a valid non-negative number for rate (items per minute)."
|
|
||||||
rate = None
|
|
||||||
|
|
||||||
selected_item = item_name
|
|
||||||
if rate is not None:
|
|
||||||
selected_rate = rate
|
|
||||||
|
|
||||||
# Determine candidate recipes for the selected output item
|
|
||||||
recipe_options: List[str] = []
|
recipe_options: List[str] = []
|
||||||
item_obj_for_options = name_to_item.get(selected_item)
|
item_obj_for_options = name_to_item.get(selected_item) if len(targets) <= 1 else None
|
||||||
if item_obj_for_options is not None:
|
if item_obj_for_options is not None:
|
||||||
for r in Recipes:
|
for r in Recipes:
|
||||||
recipe = r.value
|
recipe = r.value
|
||||||
@@ -224,22 +281,80 @@ def index():
|
|||||||
# Compute and also prepare per-item override options based on resulting chain
|
# Compute and also prepare per-item override options based on resulting chain
|
||||||
overrides_ui: List[dict] = []
|
overrides_ui: List[dict] = []
|
||||||
|
|
||||||
if not error and item_name and rate is not None:
|
if targets:
|
||||||
item_obj = name_to_item.get(item_name)
|
# Translate preferred mapping (Items -> recipe name) into objects and pass to ProductionChain
|
||||||
if item_obj is None:
|
preferred_recipes_obj: Optional[Dict[Items, Recipe]] = None
|
||||||
error = "Unknown item selected."
|
if preferred:
|
||||||
else:
|
name_to_recipe = {r.value.name: r.value for r in Recipes}
|
||||||
targets = {item_obj: rate}
|
preferred_recipes_obj = {}
|
||||||
raw, steps, outputs, unused = compute_chain(targets, preferred_by_output=preferred)
|
for itm, rec_name in preferred.items():
|
||||||
|
rec_obj = name_to_recipe.get(rec_name)
|
||||||
|
if rec_obj:
|
||||||
|
preferred_recipes_obj[itm] = rec_obj
|
||||||
|
prod_chain = ProductionChain(preferred_recipes=preferred_recipes_obj)
|
||||||
|
prod_chain.compute_chain(targets)
|
||||||
|
|
||||||
|
# Build UI-facing structures from ProductionChain state
|
||||||
|
raw = {itm.value.name: qty for itm, qty in prod_chain.raw_resources.items() if qty > 0}
|
||||||
|
unused = {itm.value.name: qty for itm, qty in prod_chain.byproduct.items() if qty > 0}
|
||||||
|
excess = {itm.value.name: qty for itm, qty in prod_chain.excess.items() if qty > 0}
|
||||||
|
outputs = {itm.value.name: qty for itm, qty in prod_chain.production.items() if qty > 0}
|
||||||
|
|
||||||
|
# Steps per recipe
|
||||||
|
steps = []
|
||||||
|
for recipe_name, level in prod_chain.production_chain.items():
|
||||||
|
rec = prod_chain.recipe_name_to_obj.get(recipe_name)
|
||||||
|
if not rec:
|
||||||
|
continue
|
||||||
|
chosen_item = None
|
||||||
|
if rec.outputs:
|
||||||
|
demanded = sorted(rec.outputs.keys(), key=lambda it: prod_chain.demand.get(it, 0.0), reverse=True)
|
||||||
|
chosen_item = demanded[0]
|
||||||
|
if not chosen_item:
|
||||||
|
continue
|
||||||
|
per_building_output = rec.outputs[chosen_item]
|
||||||
|
buildings_float = float(level)
|
||||||
|
buildings = ceil(buildings_float) if buildings_float > 0 else 0
|
||||||
|
utilization = (buildings_float / buildings) if buildings > 0 else 0.0
|
||||||
|
target_rate_item = buildings_float * per_building_output
|
||||||
|
# Compute per-step input item rates for this recipe at the computed level
|
||||||
|
step_inputs = []
|
||||||
|
for inp_item, qty_per_building in rec.inputs.items():
|
||||||
|
rate = buildings_float * qty_per_building
|
||||||
|
step_inputs.append({
|
||||||
|
"item": inp_item.value.name,
|
||||||
|
"rate": rate,
|
||||||
|
})
|
||||||
|
# Sort inputs by descending rate for a stable display
|
||||||
|
step_inputs.sort(key=lambda x: x["rate"], reverse=True)
|
||||||
|
steps.append({
|
||||||
|
"item": chosen_item.value.name,
|
||||||
|
"recipe": rec.name,
|
||||||
|
"building": rec.building.value.name,
|
||||||
|
"target_rate": target_rate_item,
|
||||||
|
"per_building_output": per_building_output,
|
||||||
|
"buildings_float": buildings_float,
|
||||||
|
"buildings": buildings,
|
||||||
|
"utilization": utilization,
|
||||||
|
"inputs": step_inputs,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Aggregate targets for display
|
||||||
|
result_targets = {}
|
||||||
|
for itm, qty in targets.items():
|
||||||
|
if qty > 0:
|
||||||
|
result_targets[itm.value.name] = qty
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"targets": {item_name: rate},
|
"targets": result_targets,
|
||||||
"raw": raw,
|
"raw": raw,
|
||||||
"steps": steps,
|
"steps": steps,
|
||||||
"outputs": outputs,
|
"outputs": outputs,
|
||||||
"unused": unused,
|
"unused": unused,
|
||||||
|
"excess": excess,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Collect unique output items from steps
|
# Collect unique output items from steps for override options
|
||||||
unique_items = []
|
unique_items = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for s in steps:
|
for s in steps:
|
||||||
@@ -247,7 +362,6 @@ def index():
|
|||||||
if item_nm and item_nm not in seen:
|
if item_nm and item_nm not in seen:
|
||||||
seen.add(item_nm)
|
seen.add(item_nm)
|
||||||
unique_items.append(item_nm)
|
unique_items.append(item_nm)
|
||||||
# For each item, compute candidate recipes and current selection
|
|
||||||
for item_nm in unique_items:
|
for item_nm in unique_items:
|
||||||
item_enum2 = name_to_item.get(item_nm)
|
item_enum2 = name_to_item.get(item_nm)
|
||||||
if not item_enum2:
|
if not item_enum2:
|
||||||
@@ -261,7 +375,7 @@ def index():
|
|||||||
"building": rec.building.value.name,
|
"building": rec.building.value.name,
|
||||||
})
|
})
|
||||||
if len(candidates) <= 1:
|
if len(candidates) <= 1:
|
||||||
continue # only show when alternates exist
|
continue
|
||||||
candidates.sort(key=lambda x: (x["name"]))
|
candidates.sort(key=lambda x: (x["name"]))
|
||||||
sel = None
|
sel = None
|
||||||
if preferred and item_enum2 in preferred:
|
if preferred and item_enum2 in preferred:
|
||||||
@@ -274,11 +388,22 @@ def index():
|
|||||||
"selected": sel or "",
|
"selected": sel or "",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Build reset query (clear overrides)
|
# Build reset query (clear overrides) while preserving current targets
|
||||||
|
if indexed_pairs or len(targets_ui) > 1:
|
||||||
|
# Multi-target
|
||||||
|
parts = []
|
||||||
|
for idx, row in enumerate(targets_ui, start=1):
|
||||||
|
parts.append(f"item_{idx}={row['item']}")
|
||||||
|
parts.append(f"rate_{idx}={row['rate']}")
|
||||||
|
reset_query = "?" + "&".join(parts)
|
||||||
|
else:
|
||||||
reset_query = f"?item={selected_item}&rate={selected_rate}"
|
reset_query = f"?item={selected_item}&rate={selected_rate}"
|
||||||
if selected_recipe:
|
if selected_recipe:
|
||||||
reset_query += f"&recipe={selected_recipe}"
|
reset_query += f"&recipe={selected_recipe}"
|
||||||
|
|
||||||
|
# Combine error messages
|
||||||
|
error = " ".join(error_msgs) if error_msgs else None
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
items=item_names,
|
items=item_names,
|
||||||
@@ -290,6 +415,7 @@ def index():
|
|||||||
selected_recipe=selected_recipe,
|
selected_recipe=selected_recipe,
|
||||||
overrides_ui=overrides_ui,
|
overrides_ui=overrides_ui,
|
||||||
reset_query=reset_query,
|
reset_query=reset_query,
|
||||||
|
targets_ui=targets_ui,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -299,6 +425,6 @@ def create_app():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# For local dev: python main.py
|
# For local dev: python main.py
|
||||||
# app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
prod_chain = ProductionChain()
|
# prod_chain = ProductionChain()
|
||||||
prod_chain.compute_chain({Items.ModularFrame: 1.5})
|
# prod_chain.compute_chain({Items.CateriumHeatsink: 10.0})
|
||||||
191
plus.py
191
plus.py
@@ -12,6 +12,8 @@ class Machines(Enum):
|
|||||||
Sorter = Machine(name="Sorter")
|
Sorter = Machine(name="Sorter")
|
||||||
Crusher = Machine(name="Crusher")
|
Crusher = Machine(name="Crusher")
|
||||||
Foundry = Machine(name="Foundry")
|
Foundry = Machine(name="Foundry")
|
||||||
|
FlexibleBlastFurnace = Machine(name="Flexible Blast Furnace")
|
||||||
|
Reformer = Machine(name="Reformer")
|
||||||
|
|
||||||
class Items(Enum):
|
class Items(Enum):
|
||||||
IronIngot = Item(name="Iron Ingot")
|
IronIngot = Item(name="Iron Ingot")
|
||||||
@@ -71,6 +73,18 @@ class Items(Enum):
|
|||||||
BronzeFrame = Item(name="Bronze Frame")
|
BronzeFrame = Item(name="Bronze Frame")
|
||||||
AILimiter = Item(name="AI Limiter")
|
AILimiter = Item(name="AI Limiter")
|
||||||
Water = Item(name="Water")
|
Water = Item(name="Water")
|
||||||
|
CrushedCaterium = Item(name="Crushed Caterium")
|
||||||
|
CateriumHeatsink = Item(name="Caterium Heatsink")
|
||||||
|
MoltenIron = Item(name="Molten Iron")
|
||||||
|
MoltenCopper = Item(name="Molten Copper")
|
||||||
|
MoltenTin = Item(name="Molten Tin")
|
||||||
|
Steam = Item(name="Steam")
|
||||||
|
MoltenBrass = Item(name="Molten Brass")
|
||||||
|
BrassIngot = Item(name="Brass Ingot")
|
||||||
|
BrassPlates = Item(name="Brass Plates")
|
||||||
|
BrassPipes = Item(name="Brass Pipes")
|
||||||
|
ColdSlag = Item(name="Cold Slag")
|
||||||
|
FanBlades = Item(name="Fan Blades")
|
||||||
|
|
||||||
class RawResources(Enum):
|
class RawResources(Enum):
|
||||||
Water = Items.Water
|
Water = Items.Water
|
||||||
@@ -177,6 +191,16 @@ class Recipes(Enum):
|
|||||||
},
|
},
|
||||||
inputs={Items.CrushedLarrussite: 120.0},
|
inputs={Items.CrushedLarrussite: 120.0},
|
||||||
)
|
)
|
||||||
|
CrushedCaterium = Recipe(
|
||||||
|
name="Crushed Caterium",
|
||||||
|
building=Machines.Sorter,
|
||||||
|
outputs={
|
||||||
|
Items.CrushedCaterium: 80.0,
|
||||||
|
Items.CrushedCopper: 40.0,
|
||||||
|
Items.CrushedGangue: 30.0,
|
||||||
|
},
|
||||||
|
inputs={Items.CrushedAurovite: 120.0},
|
||||||
|
)
|
||||||
# Smelter
|
# Smelter
|
||||||
# - smelting
|
# - smelting
|
||||||
IronIngot = Recipe(
|
IronIngot = Recipe(
|
||||||
@@ -197,6 +221,12 @@ class Recipes(Enum):
|
|||||||
outputs={Items.TinIngot: 15.0},
|
outputs={Items.TinIngot: 15.0},
|
||||||
inputs={Items.CrushedTin: 15.0},
|
inputs={Items.CrushedTin: 15.0},
|
||||||
)
|
)
|
||||||
|
CateriumIngot = Recipe(
|
||||||
|
name="Caterium Ingot",
|
||||||
|
building=Machines.Smelter,
|
||||||
|
outputs={Items.CateriumIngot: 40.0},
|
||||||
|
inputs={Items.CrushedCaterium: 40.0},
|
||||||
|
)
|
||||||
# - Ingots
|
# - Ingots
|
||||||
ImpureIronIngot = Recipe(
|
ImpureIronIngot = Recipe(
|
||||||
name="Impure Iron Ingot",
|
name="Impure Iron Ingot",
|
||||||
@@ -219,8 +249,8 @@ class Recipes(Enum):
|
|||||||
ImpureCateriumIngot = Recipe(
|
ImpureCateriumIngot = Recipe(
|
||||||
name="Impure Caterium Ingot",
|
name="Impure Caterium Ingot",
|
||||||
building=Machines.Smelter,
|
building=Machines.Smelter,
|
||||||
inputs={Items.CrushedAurovite: 40.0},
|
|
||||||
outputs={Items.CateriumIngot: 24.0},
|
outputs={Items.CateriumIngot: 24.0},
|
||||||
|
inputs={Items.CrushedAurovite: 40.0},
|
||||||
)
|
)
|
||||||
ZincIngot = Recipe(
|
ZincIngot = Recipe(
|
||||||
name="Zinc Ingot",
|
name="Zinc Ingot",
|
||||||
@@ -235,6 +265,12 @@ class Recipes(Enum):
|
|||||||
outputs={Items.Glass: 20.0},
|
outputs={Items.Glass: 20.0},
|
||||||
inputs={Items.Sand: 30.0},
|
inputs={Items.Sand: 30.0},
|
||||||
)
|
)
|
||||||
|
CastIronScrew = Recipe(
|
||||||
|
name="Cast Iron Screw",
|
||||||
|
building=Machines.Smelter,
|
||||||
|
outputs={Items.Screws: 40.0},
|
||||||
|
inputs={Items.IronIngot: 20.0},
|
||||||
|
)
|
||||||
# Constructor
|
# Constructor
|
||||||
# - Standard Parts
|
# - Standard Parts
|
||||||
IronPlate = Recipe(
|
IronPlate = Recipe(
|
||||||
@@ -309,6 +345,18 @@ class Recipes(Enum):
|
|||||||
outputs={Items.ZincPlates: 15.0},
|
outputs={Items.ZincPlates: 15.0},
|
||||||
inputs={Items.ZincIngot: 12.5},
|
inputs={Items.ZincIngot: 12.5},
|
||||||
)
|
)
|
||||||
|
RolledBronzePlate = Recipe(
|
||||||
|
name="Rolled Bronze Plate",
|
||||||
|
building=Machines.Constructor,
|
||||||
|
outputs={Items.BronzePipes: 16.0},
|
||||||
|
inputs={Items.BronzePlates: 8.0},
|
||||||
|
)
|
||||||
|
CompactedIronPlate = Recipe(
|
||||||
|
name="Compacted Iron Plate",
|
||||||
|
building=Machines.Constructor,
|
||||||
|
outputs={Items.ReinforcedIronPlate: 3.0},
|
||||||
|
inputs={Items.IronPlate: 20.0},
|
||||||
|
)
|
||||||
# - Electronics
|
# - Electronics
|
||||||
IronWire = Recipe(
|
IronWire = Recipe(
|
||||||
name="Iron Wire",
|
name="Iron Wire",
|
||||||
@@ -353,7 +401,13 @@ class Recipes(Enum):
|
|||||||
outputs={Items.Concrete: 15.0},
|
outputs={Items.Concrete: 15.0},
|
||||||
inputs={Items.CrushedGangue: 45.0},
|
inputs={Items.CrushedGangue: 45.0},
|
||||||
)
|
)
|
||||||
|
# - Industrial Parts
|
||||||
|
BrassPlates = Recipe(
|
||||||
|
name="Brass Plates",
|
||||||
|
building=Machines.Constructor,
|
||||||
|
outputs={Items.BrassPlates: 18.0},
|
||||||
|
inputs={Items.BrassIngot: 25.0},
|
||||||
|
)
|
||||||
# Foundry
|
# Foundry
|
||||||
# - Building Parts
|
# - Building Parts
|
||||||
SolarCell = Recipe(
|
SolarCell = Recipe(
|
||||||
@@ -406,6 +460,24 @@ class Recipes(Enum):
|
|||||||
Items.Rotor: 3.75,
|
Items.Rotor: 3.75,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
SlagConcrete = Recipe(
|
||||||
|
name="Slag Concrete",
|
||||||
|
building=Machines.Assembler,
|
||||||
|
outputs={Items.Concrete: 22.5},
|
||||||
|
inputs={
|
||||||
|
Items.ColdSlag: 33.75,
|
||||||
|
Items.CrushedGangue: 11.25,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
FanBlades = Recipe(
|
||||||
|
name="Fan Blades",
|
||||||
|
building=Machines.Assembler,
|
||||||
|
outputs={Items.FanBlades: 7.5},
|
||||||
|
inputs={
|
||||||
|
Items.BrassPlates: 8.0,
|
||||||
|
Items.IronRod: 22.5,
|
||||||
|
},
|
||||||
|
)
|
||||||
# - Standard Parts
|
# - Standard Parts
|
||||||
TinPlate = Recipe(
|
TinPlate = Recipe(
|
||||||
name="Tin Plate",
|
name="Tin Plate",
|
||||||
@@ -470,6 +542,25 @@ class Recipes(Enum):
|
|||||||
Items.BronzeBeam: 7.5,
|
Items.BronzeBeam: 7.5,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
FastenedFrame = Recipe(
|
||||||
|
name="Fastened Frame",
|
||||||
|
building=Machines.Assembler,
|
||||||
|
outputs={Items.ModularFrame: 20.0},
|
||||||
|
inputs={
|
||||||
|
Items.ReinforcedIronPlate: 15,
|
||||||
|
Items.Screws: 150,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BrassPipes = Recipe(
|
||||||
|
name="Brass Pipes",
|
||||||
|
building=Machines.Assembler,
|
||||||
|
outputs={Items.BrassPipes: 12.5},
|
||||||
|
inputs={
|
||||||
|
Items.BrassPlates: 15.0,
|
||||||
|
Items.CopperRod: 7.5
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# - Electronics
|
||||||
AILimiter = Recipe(
|
AILimiter = Recipe(
|
||||||
name="AI Limiter",
|
name="AI Limiter",
|
||||||
building=Machines.Assembler,
|
building=Machines.Assembler,
|
||||||
@@ -479,5 +570,101 @@ class Recipes(Enum):
|
|||||||
Items.TinnedWire: 18.0,
|
Items.TinnedWire: 18.0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
# - Industrial
|
||||||
|
CateriumHeatsink = Recipe(
|
||||||
|
name="Caterium Heatsink",
|
||||||
|
building=Machines.Assembler,
|
||||||
|
outputs={Items.CateriumHeatsink: 10.0},
|
||||||
|
inputs={
|
||||||
|
Items.CateriumPlate: 28.0,
|
||||||
|
Items.CopperBusbars: 10.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Flexible Blast Furnace
|
||||||
|
# - Molten Metals
|
||||||
|
MoltenIron = Recipe(
|
||||||
|
name="Molten Iron",
|
||||||
|
building=Machines.FlexibleBlastFurnace,
|
||||||
|
outputs={Items.MoltenIron: 60.0},
|
||||||
|
inputs={
|
||||||
|
Items.CrushedIron: 120.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
MoltenCopper = Recipe(
|
||||||
|
name="Molten Copper",
|
||||||
|
building=Machines.FlexibleBlastFurnace,
|
||||||
|
outputs={Items.MoltenCopper: 60.0},
|
||||||
|
inputs={
|
||||||
|
Items.CrushedCopper: 120.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
MoltenTin = Recipe(
|
||||||
|
name="Molten Tin",
|
||||||
|
building=Machines.FlexibleBlastFurnace,
|
||||||
|
outputs={Items.MoltenTin: 60.0},
|
||||||
|
inputs={
|
||||||
|
Items.CrushedTin: 120.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# - Alloys
|
||||||
|
MoltenBrass = Recipe(
|
||||||
|
name="Molten Brass",
|
||||||
|
building=Machines.FlexibleBlastFurnace,
|
||||||
|
outputs={Items.MoltenBrass: 60.0},
|
||||||
|
inputs={
|
||||||
|
Items.CrushedZinc: 32.0,
|
||||||
|
Items.MoltenCopper: 20.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Reformer
|
||||||
|
# - Ingots
|
||||||
|
CastIronIngot = Recipe(
|
||||||
|
name="Cast Iron Ingot",
|
||||||
|
building=Machines.Reformer,
|
||||||
|
outputs={
|
||||||
|
Items.IronIngot: 180.0,
|
||||||
|
Items.Steam: 60.0
|
||||||
|
},
|
||||||
|
inputs={
|
||||||
|
Items.MoltenIron: 60.0,
|
||||||
|
Items.Water: 60.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
CastCopperIngot = Recipe(
|
||||||
|
name="Cast Copper Ingot",
|
||||||
|
building=Machines.Reformer,
|
||||||
|
outputs={
|
||||||
|
Items.CopperIngot: 144.0,
|
||||||
|
Items.Steam: 24.0
|
||||||
|
},
|
||||||
|
inputs={
|
||||||
|
Items.MoltenCopper: 48.0,
|
||||||
|
Items.Water: 24.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
CastTinIngot = Recipe(
|
||||||
|
name="Cast Tin Ingot",
|
||||||
|
building=Machines.Reformer,
|
||||||
|
outputs={
|
||||||
|
Items.TinIngot: 180.0,
|
||||||
|
Items.Steam: 20.0
|
||||||
|
},
|
||||||
|
inputs={
|
||||||
|
Items.MoltenTin: 60.0,
|
||||||
|
Items.Water: 20.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
CastBrassIngot = Recipe(
|
||||||
|
name="Cast Brass Ingot",
|
||||||
|
building=Machines.Reformer,
|
||||||
|
outputs={
|
||||||
|
Items.BrassIngot: 50.0,
|
||||||
|
Items.Steam: 15.0
|
||||||
|
},
|
||||||
|
inputs={
|
||||||
|
Items.MoltenBrass: 40.0,
|
||||||
|
Items.Water: 15.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Recipe.model_rebuild()
|
Recipe.model_rebuild()
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
code { color: var(--accent); }
|
code { color: var(--accent); }
|
||||||
.mono { font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
.mono { font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||||
.pill { display:inline-block; padding:2px 8px; border-radius: 999px; background:#0b1220; border:1px solid #334155; color: var(--muted); font-size: 12px; }
|
.pill { display:inline-block; padding:2px 8px; border-radius: 999px; background:#0b1220; border:1px solid #334155; color: var(--muted); font-size: 12px; }
|
||||||
|
.completed td:not(.done-col) { text-decoration: line-through; color: var(--muted); }
|
||||||
|
.done-col { width: 1%; white-space: nowrap; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -33,19 +35,39 @@
|
|||||||
<h1>Satisfactory Production Calculator</h1>
|
<h1>Satisfactory Production Calculator</h1>
|
||||||
<p>Compute buildings and raw inputs for a target output rate (items per minute).</p>
|
<p>Compute buildings and raw inputs for a target output rate (items per minute).</p>
|
||||||
|
|
||||||
<form class="card" method="get">
|
<form class="card" method="get" id="targets-form">
|
||||||
<div class="row">
|
|
||||||
<div>
|
<div>
|
||||||
<label for="item">Product</label>
|
<label>Products</label>
|
||||||
<select id="item" name="item" required>
|
<table style="width:100%; border-collapse: collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:60%">Item</th>
|
||||||
|
<th style="width:30%" class="mono">Rate (items/min)</th>
|
||||||
|
<th style="width:10%"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="targets-rows">
|
||||||
|
{% for row in targets_ui %}
|
||||||
|
<tr class="target-row">
|
||||||
|
<td>
|
||||||
|
<select name="item_{{ loop.index }}" required>
|
||||||
{% for it in items %}
|
{% for it in items %}
|
||||||
<option value="{{ it }}" {% if selected_item and it==selected_item %}selected{% endif %}>{{ it }}</option>
|
<option value="{{ it }}" {% if row.item == it %}selected{% endif %}>{{ it }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</td>
|
||||||
<div>
|
<td>
|
||||||
<label for="rate">Target rate (items/min)</label>
|
<input name="rate_{{ loop.index }}" type="number" step="0.01" min="0" value="{{ row.rate }}" required>
|
||||||
<input id="rate" name="rate" type="number" step="0.01" min="0" value="{{ selected_rate }}" required>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="pill" onclick="removeRow(this)" title="Remove">✕</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="mt">
|
||||||
|
<button type="button" onclick="addRow()">Add product</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,11 +86,12 @@
|
|||||||
{% if result.raw %}
|
{% if result.raw %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Item</th><th class="mono">Rate (items/min)</th></tr>
|
<tr><th class="done-col">Done</th><th>Item</th><th class="mono">Rate (items/min)</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item,rate in result.raw.items() %}
|
{% for item,rate in result.raw.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="done-col"><input type="checkbox" onchange="toggleDone(this)"></td>
|
||||||
<td>{{ item }}</td>
|
<td>{{ item }}</td>
|
||||||
<td class="mono">{{ '%.2f'|format(rate) }}</td>
|
<td class="mono">{{ '%.2f'|format(rate) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -82,8 +105,11 @@
|
|||||||
{% if overrides_ui and overrides_ui|length > 0 %}
|
{% if overrides_ui and overrides_ui|length > 0 %}
|
||||||
<h3 class="mt">Recipe overrides</h3>
|
<h3 class="mt">Recipe overrides</h3>
|
||||||
<form method="get" class="card" style="background: transparent; border:0; padding:0;">
|
<form method="get" class="card" style="background: transparent; border:0; padding:0;">
|
||||||
<input type="hidden" name="item" value="{{ selected_item }}">
|
{# Preserve all target rows in the recalculation #}
|
||||||
<input type="hidden" name="rate" value="{{ selected_rate }}">
|
{% for row in targets_ui %}
|
||||||
|
<input type="hidden" name="item_{{ loop.index }}" value="{{ row.item }}">
|
||||||
|
<input type="hidden" name="rate_{{ loop.index }}" value="{{ row.rate }}">
|
||||||
|
{% endfor %}
|
||||||
{% if selected_recipe %}<input type="hidden" name="recipe" value="{{ selected_recipe }}">{% endif %}
|
{% if selected_recipe %}<input type="hidden" name="recipe" value="{{ selected_recipe }}">{% endif %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -120,9 +146,11 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="done-col">Done</th>
|
||||||
<th>Output Item</th>
|
<th>Output Item</th>
|
||||||
<th>Recipe</th>
|
<th>Recipe</th>
|
||||||
<th>Building</th>
|
<th>Building</th>
|
||||||
|
<th>Inputs</th>
|
||||||
<th class="mono">Target rate</th>
|
<th class="mono">Target rate</th>
|
||||||
<th class="mono">Per-building output</th>
|
<th class="mono">Per-building output</th>
|
||||||
<th class="mono">Buildings</th>
|
<th class="mono">Buildings</th>
|
||||||
@@ -132,9 +160,21 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for s in result.steps %}
|
{% for s in result.steps %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="done-col"><input type="checkbox" onchange="toggleDone(this)"></td>
|
||||||
<td>{{ s.item }}</td>
|
<td>{{ s.item }}</td>
|
||||||
<td>{{ s.recipe }}</td>
|
<td>{{ s.recipe }}</td>
|
||||||
<td>{{ s.building }}</td>
|
<td>{{ s.building }}</td>
|
||||||
|
<td>
|
||||||
|
{% if s.inputs and s.inputs|length > 0 %}
|
||||||
|
<div>
|
||||||
|
{% for inp in s.inputs %}
|
||||||
|
<div>{{ inp.item }} — <span class="mono">{{ '%.2f'|format(inp.rate) }}</span></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="pill">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="mono">{{ '%.2f'|format(s.target_rate) }}</td>
|
<td class="mono">{{ '%.2f'|format(s.target_rate) }}</td>
|
||||||
<td class="mono">{{ '%.2f'|format(s.per_building_output) }}</td>
|
<td class="mono">{{ '%.2f'|format(s.per_building_output) }}</td>
|
||||||
<td class="mono">{{ '%.2f'|format(s.buildings_float) }} (~ {{ s.buildings }})</td>
|
<td class="mono">{{ '%.2f'|format(s.buildings_float) }} (~ {{ s.buildings }})</td>
|
||||||
@@ -147,15 +187,36 @@
|
|||||||
<p class="pill">No production steps (target is a raw resource).</p>
|
<p class="pill">No production steps (target is a raw resource).</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<h3 class="mt">Excess products</h3>
|
||||||
|
{% if result.excess and result.excess|length > 0 %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th class="done-col">Done</th><th>Item</th><th class="mono">Excess rate (items/min)</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item,rate in result.excess.items() %}
|
||||||
|
<tr>
|
||||||
|
<td class="done-col"><input type="checkbox" onchange="toggleDone(this)"></td>
|
||||||
|
<td>{{ item }}</td>
|
||||||
|
<td class="mono">{{ '%.2f'|format(rate) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="pill">No excess products for this target.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h3 class="mt">Unused byproducts</h3>
|
<h3 class="mt">Unused byproducts</h3>
|
||||||
{% if result.unused and result.unused|length > 0 %}
|
{% if result.unused and result.unused|length > 0 %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Item</th><th class="mono">Unused rate (items/min)</th></tr>
|
<tr><th class="done-col">Done</th><th>Item</th><th class="mono">Unused rate (items/min)</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item,rate in result.unused.items() %}
|
{% for item,rate in result.unused.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="done-col"><input type="checkbox" onchange="toggleDone(this)"></td>
|
||||||
<td>{{ item }}</td>
|
<td>{{ item }}</td>
|
||||||
<td class="mono">{{ '%.2f'|format(rate) }}</td>
|
<td class="mono">{{ '%.2f'|format(rate) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -170,8 +231,45 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Tiny helper to maintain selected item after submit if not handled by server
|
// Dynamic add/remove for multiple product targets
|
||||||
// (we already set it via Jinja, this is just progressive enhancement)
|
const itemsList = {{ items | tojson }};
|
||||||
|
function addRow() {
|
||||||
|
const tbody = document.getElementById('targets-rows');
|
||||||
|
const idx = tbody.querySelectorAll('tr').length + 1;
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = 'target-row';
|
||||||
|
const options = itemsList.map(it => `<option value="${it}">${it}</option>`).join('');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>
|
||||||
|
<select name="item_${idx}" required>${options}</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input name="rate_${idx}" type="number" step="0.01" min="0" value="60.0" required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="pill" onclick="removeRow(this)" title="Remove">✕</button>
|
||||||
|
</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
function removeRow(btn) {
|
||||||
|
const tr = btn.closest('tr');
|
||||||
|
const tbody = tr && tr.parentElement;
|
||||||
|
if (!tbody) return;
|
||||||
|
tbody.removeChild(tr);
|
||||||
|
// Optionally renumber names to keep indices compact
|
||||||
|
Array.from(tbody.querySelectorAll('tr')).forEach((row, i) => {
|
||||||
|
const idx = i + 1;
|
||||||
|
const sel = row.querySelector('select[name^="item_"]');
|
||||||
|
const rate = row.querySelector('input[name^="rate_"]');
|
||||||
|
if (sel) sel.name = `item_${idx}`;
|
||||||
|
if (rate) rate.name = `rate_${idx}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function toggleDone(cb) {
|
||||||
|
const tr = cb.closest('tr');
|
||||||
|
if (!tr) return;
|
||||||
|
tr.classList.toggle('completed', cb.checked);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user