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>callanite</w>
|
||||
<w>caterium</w>
|
||||
<w>heatsink</w>
|
||||
<w>larrussite</w>
|
||||
<w>quickwire</w>
|
||||
<w>siterite</w>
|
||||
|
||||
278
main.py
278
main.py
@@ -34,7 +34,7 @@ class Production(BaseModel):
|
||||
quantity: float
|
||||
|
||||
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)
|
||||
for r in Recipes:
|
||||
for o in r.value.outputs:
|
||||
@@ -49,9 +49,11 @@ class ProductionChain:
|
||||
self.production_chain: Dict[str, float] = defaultdict(float)
|
||||
self.excess: 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]:
|
||||
# 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]
|
||||
|
||||
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)
|
||||
|
||||
production_level = max(levels)
|
||||
if production_level == 0:
|
||||
continue
|
||||
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():
|
||||
production[out] += production_level * quantity
|
||||
for byproduct, quantity in recipe.byproducts.items():
|
||||
@@ -153,13 +160,77 @@ def index():
|
||||
name_to_item = {i.value.name: i for i in Items}
|
||||
|
||||
result = None
|
||||
error = None
|
||||
selected_item = item_names[0] if item_names else ""
|
||||
selected_rate = 60.0
|
||||
error_msgs: List[str] = []
|
||||
|
||||
# Read from query parameters for bookmarkable URLs
|
||||
item_name = request.args.get("item") or selected_item
|
||||
rate_str = request.args.get("rate")
|
||||
# --------------------
|
||||
# Parse targets (support multiple products)
|
||||
# --------------------
|
||||
# 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 ""
|
||||
|
||||
# Parse per-item recipe overrides from query params recipe_for_<slug(item)>
|
||||
@@ -184,23 +255,9 @@ def index():
|
||||
if value in candidates:
|
||||
overrides[item_enum] = value
|
||||
|
||||
rate = None
|
||||
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
|
||||
# Candidate recipes for the (first) selected item (single-target convenience)
|
||||
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:
|
||||
for r in Recipes:
|
||||
recipe = r.value
|
||||
@@ -224,61 +281,129 @@ def index():
|
||||
# Compute and also prepare per-item override options based on resulting chain
|
||||
overrides_ui: List[dict] = []
|
||||
|
||||
if not error and item_name and rate is not None:
|
||||
item_obj = name_to_item.get(item_name)
|
||||
if item_obj is None:
|
||||
error = "Unknown item selected."
|
||||
else:
|
||||
targets = {item_obj: rate}
|
||||
raw, steps, outputs, unused = compute_chain(targets, preferred_by_output=preferred)
|
||||
result = {
|
||||
"targets": {item_name: rate},
|
||||
"raw": raw,
|
||||
"steps": steps,
|
||||
"outputs": outputs,
|
||||
"unused": unused,
|
||||
}
|
||||
if targets:
|
||||
# Translate preferred mapping (Items -> recipe name) into objects and pass to ProductionChain
|
||||
preferred_recipes_obj: Optional[Dict[Items, Recipe]] = None
|
||||
if preferred:
|
||||
name_to_recipe = {r.value.name: r.value for r in Recipes}
|
||||
preferred_recipes_obj = {}
|
||||
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)
|
||||
|
||||
# Collect unique output items from steps
|
||||
unique_items = []
|
||||
seen = set()
|
||||
for s in steps:
|
||||
item_nm = s.get("item")
|
||||
if item_nm and item_nm not in seen:
|
||||
seen.add(item_nm)
|
||||
unique_items.append(item_nm)
|
||||
# For each item, compute candidate recipes and current selection
|
||||
for item_nm in unique_items:
|
||||
item_enum2 = name_to_item.get(item_nm)
|
||||
if not item_enum2:
|
||||
continue
|
||||
candidates = []
|
||||
for r in Recipes:
|
||||
rec = r.value
|
||||
if item_enum2 in rec.outputs:
|
||||
candidates.append({
|
||||
"name": rec.name,
|
||||
"building": rec.building.value.name,
|
||||
})
|
||||
if len(candidates) <= 1:
|
||||
continue # only show when alternates exist
|
||||
candidates.sort(key=lambda x: (x["name"]))
|
||||
sel = None
|
||||
if preferred and item_enum2 in preferred:
|
||||
sel = preferred[item_enum2]
|
||||
slug = _slugify(item_nm)
|
||||
overrides_ui.append({
|
||||
"item_name": item_nm,
|
||||
"slug": slug,
|
||||
"options": candidates,
|
||||
"selected": sel or "",
|
||||
# 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,
|
||||
})
|
||||
|
||||
# Build reset query (clear overrides)
|
||||
reset_query = f"?item={selected_item}&rate={selected_rate}"
|
||||
# Aggregate targets for display
|
||||
result_targets = {}
|
||||
for itm, qty in targets.items():
|
||||
if qty > 0:
|
||||
result_targets[itm.value.name] = qty
|
||||
|
||||
result = {
|
||||
"targets": result_targets,
|
||||
"raw": raw,
|
||||
"steps": steps,
|
||||
"outputs": outputs,
|
||||
"unused": unused,
|
||||
"excess": excess,
|
||||
}
|
||||
|
||||
# Collect unique output items from steps for override options
|
||||
unique_items = []
|
||||
seen = set()
|
||||
for s in steps:
|
||||
item_nm = s.get("item")
|
||||
if item_nm and item_nm not in seen:
|
||||
seen.add(item_nm)
|
||||
unique_items.append(item_nm)
|
||||
for item_nm in unique_items:
|
||||
item_enum2 = name_to_item.get(item_nm)
|
||||
if not item_enum2:
|
||||
continue
|
||||
candidates = []
|
||||
for r in Recipes:
|
||||
rec = r.value
|
||||
if item_enum2 in rec.outputs:
|
||||
candidates.append({
|
||||
"name": rec.name,
|
||||
"building": rec.building.value.name,
|
||||
})
|
||||
if len(candidates) <= 1:
|
||||
continue
|
||||
candidates.sort(key=lambda x: (x["name"]))
|
||||
sel = None
|
||||
if preferred and item_enum2 in preferred:
|
||||
sel = preferred[item_enum2]
|
||||
slug = _slugify(item_nm)
|
||||
overrides_ui.append({
|
||||
"item_name": item_nm,
|
||||
"slug": slug,
|
||||
"options": candidates,
|
||||
"selected": sel or "",
|
||||
})
|
||||
|
||||
# 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}"
|
||||
if selected_recipe:
|
||||
reset_query += f"&recipe={selected_recipe}"
|
||||
|
||||
# Combine error messages
|
||||
error = " ".join(error_msgs) if error_msgs else None
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
items=item_names,
|
||||
@@ -290,6 +415,7 @@ def index():
|
||||
selected_recipe=selected_recipe,
|
||||
overrides_ui=overrides_ui,
|
||||
reset_query=reset_query,
|
||||
targets_ui=targets_ui,
|
||||
)
|
||||
|
||||
|
||||
@@ -299,6 +425,6 @@ def create_app():
|
||||
|
||||
if __name__ == "__main__":
|
||||
# For local dev: python main.py
|
||||
# app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
prod_chain = ProductionChain()
|
||||
prod_chain.compute_chain({Items.ModularFrame: 1.5})
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
# prod_chain = ProductionChain()
|
||||
# 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")
|
||||
Crusher = Machine(name="Crusher")
|
||||
Foundry = Machine(name="Foundry")
|
||||
FlexibleBlastFurnace = Machine(name="Flexible Blast Furnace")
|
||||
Reformer = Machine(name="Reformer")
|
||||
|
||||
class Items(Enum):
|
||||
IronIngot = Item(name="Iron Ingot")
|
||||
@@ -71,6 +73,18 @@ class Items(Enum):
|
||||
BronzeFrame = Item(name="Bronze Frame")
|
||||
AILimiter = Item(name="AI Limiter")
|
||||
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):
|
||||
Water = Items.Water
|
||||
@@ -177,6 +191,16 @@ class Recipes(Enum):
|
||||
},
|
||||
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
|
||||
# - smelting
|
||||
IronIngot = Recipe(
|
||||
@@ -197,6 +221,12 @@ class Recipes(Enum):
|
||||
outputs={Items.TinIngot: 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
|
||||
ImpureIronIngot = Recipe(
|
||||
name="Impure Iron Ingot",
|
||||
@@ -219,8 +249,8 @@ class Recipes(Enum):
|
||||
ImpureCateriumIngot = Recipe(
|
||||
name="Impure Caterium Ingot",
|
||||
building=Machines.Smelter,
|
||||
inputs={Items.CrushedAurovite: 40.0},
|
||||
outputs={Items.CateriumIngot: 24.0},
|
||||
inputs={Items.CrushedAurovite: 40.0},
|
||||
)
|
||||
ZincIngot = Recipe(
|
||||
name="Zinc Ingot",
|
||||
@@ -235,6 +265,12 @@ class Recipes(Enum):
|
||||
outputs={Items.Glass: 20.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
|
||||
# - Standard Parts
|
||||
IronPlate = Recipe(
|
||||
@@ -309,6 +345,18 @@ class Recipes(Enum):
|
||||
outputs={Items.ZincPlates: 15.0},
|
||||
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
|
||||
IronWire = Recipe(
|
||||
name="Iron Wire",
|
||||
@@ -353,7 +401,13 @@ class Recipes(Enum):
|
||||
outputs={Items.Concrete: 15.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
|
||||
# - Building Parts
|
||||
SolarCell = Recipe(
|
||||
@@ -406,6 +460,24 @@ class Recipes(Enum):
|
||||
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
|
||||
TinPlate = Recipe(
|
||||
name="Tin Plate",
|
||||
@@ -470,6 +542,25 @@ class Recipes(Enum):
|
||||
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(
|
||||
name="AI Limiter",
|
||||
building=Machines.Assembler,
|
||||
@@ -479,5 +570,101 @@ class Recipes(Enum):
|
||||
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()
|
||||
@@ -26,6 +26,8 @@
|
||||
code { color: var(--accent); }
|
||||
.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; }
|
||||
.completed td:not(.done-col) { text-decoration: line-through; color: var(--muted); }
|
||||
.done-col { width: 1%; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -33,19 +35,39 @@
|
||||
<h1>Satisfactory Production Calculator</h1>
|
||||
<p>Compute buildings and raw inputs for a target output rate (items per minute).</p>
|
||||
|
||||
<form class="card" method="get">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="item">Product</label>
|
||||
<select id="item" name="item" required>
|
||||
{% for it in items %}
|
||||
<option value="{{ it }}" {% if selected_item and it==selected_item %}selected{% endif %}>{{ it }}</option>
|
||||
<form class="card" method="get" id="targets-form">
|
||||
<div>
|
||||
<label>Products</label>
|
||||
<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 %}
|
||||
<option value="{{ it }}" {% if row.item == it %}selected{% endif %}>{{ it }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input name="rate_{{ loop.index }}" type="number" step="0.01" min="0" value="{{ row.rate }}" required>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="pill" onclick="removeRow(this)" title="Remove">✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rate">Target rate (items/min)</label>
|
||||
<input id="rate" name="rate" type="number" step="0.01" min="0" value="{{ selected_rate }}" required>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt">
|
||||
<button type="button" onclick="addRow()">Add product</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,11 +86,12 @@
|
||||
{% if result.raw %}
|
||||
<table>
|
||||
<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>
|
||||
<tbody>
|
||||
{% for item,rate in result.raw.items() %}
|
||||
<tr>
|
||||
<td class="done-col"><input type="checkbox" onchange="toggleDone(this)"></td>
|
||||
<td>{{ item }}</td>
|
||||
<td class="mono">{{ '%.2f'|format(rate) }}</td>
|
||||
</tr>
|
||||
@@ -82,8 +105,11 @@
|
||||
{% if overrides_ui and overrides_ui|length > 0 %}
|
||||
<h3 class="mt">Recipe overrides</h3>
|
||||
<form method="get" class="card" style="background: transparent; border:0; padding:0;">
|
||||
<input type="hidden" name="item" value="{{ selected_item }}">
|
||||
<input type="hidden" name="rate" value="{{ selected_rate }}">
|
||||
{# Preserve all target rows in the recalculation #}
|
||||
{% 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 %}
|
||||
<table>
|
||||
<thead>
|
||||
@@ -120,9 +146,11 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="done-col">Done</th>
|
||||
<th>Output Item</th>
|
||||
<th>Recipe</th>
|
||||
<th>Building</th>
|
||||
<th>Inputs</th>
|
||||
<th class="mono">Target rate</th>
|
||||
<th class="mono">Per-building output</th>
|
||||
<th class="mono">Buildings</th>
|
||||
@@ -132,9 +160,21 @@
|
||||
<tbody>
|
||||
{% for s in result.steps %}
|
||||
<tr>
|
||||
<td class="done-col"><input type="checkbox" onchange="toggleDone(this)"></td>
|
||||
<td>{{ s.item }}</td>
|
||||
<td>{{ s.recipe }}</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.per_building_output) }}</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>
|
||||
{% 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>
|
||||
{% if result.unused and result.unused|length > 0 %}
|
||||
<table>
|
||||
<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>
|
||||
<tbody>
|
||||
{% for item,rate in result.unused.items() %}
|
||||
<tr>
|
||||
<td class="done-col"><input type="checkbox" onchange="toggleDone(this)"></td>
|
||||
<td>{{ item }}</td>
|
||||
<td class="mono">{{ '%.2f'|format(rate) }}</td>
|
||||
</tr>
|
||||
@@ -170,8 +231,45 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Tiny helper to maintain selected item after submit if not handled by server
|
||||
// (we already set it via Jinja, this is just progressive enhancement)
|
||||
// Dynamic add/remove for multiple product targets
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user