Merge remote-tracking branch 'remotes/origin/Satisfactory_limit_evevator_tier' into Satisfactory

# Conflicts:
#	worlds/satisfactory/Items.py
This commit is contained in:
Jarno Westhof
2025-04-06 13:32:33 +02:00
12 changed files with 1471 additions and 1132 deletions

View File

@@ -52,8 +52,23 @@ class TestIDs(unittest.TestCase):
def test_duplicate_location_ids(self):
"""Test that a game doesn't have location id overlap within its own datapackage"""
for gamename, world_type in AutoWorldRegister.world_types.items():
self.maxDiff = None
with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
len_location_id_to_name = len(world_type.location_id_to_name)
len_location_name_to_id = len(world_type.location_name_to_id)
if len_location_id_to_name != len_location_name_to_id:
self.assertCountEqual(
world_type.location_id_to_name.values(),
world_type.location_name_to_id.keys(),
"\nThese locations have overlapping ids with other locations in its own world")
self.assertCountEqual(
world_type.location_id_to_name.keys(),
world_type.location_name_to_id.values(),
"\nThese locations have overlapping names with other locations in its own world")
self.assertEqual(len_location_id_to_name, len_location_name_to_id)
def test_postgen_datapackage(self):
"""Generates a solo multiworld and checks that the datapackage is still valid"""

View File

@@ -52,7 +52,8 @@ class TestBase(unittest.TestCase):
state = multiworld.get_all_state(False)
for location in multiworld.get_locations():
with self.subTest("Location should be reached", location=location.name):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
if not location.can_reach(state):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
for region in multiworld.get_regions():
if region.name in unreachable_regions:
@@ -60,7 +61,8 @@ class TestBase(unittest.TestCase):
self.assertFalse(region.can_reach(state))
else:
with self.subTest("Region should be reached", region=region.name):
self.assertTrue(region.can_reach(state))
if not region.can_reach(state):
self.assertTrue(region.can_reach(state))
with self.subTest("Completion Condition"):
self.assertTrue(multiworld.can_beat_game(state))

View File

@@ -0,0 +1,169 @@
from random import Random
from typing import Optional
from collections.abc import Iterable
from .GameLogic import GameLogic, Recipe
from .Options import SatisfactoryOptions
from .Options import SatisfactoryOptions
class CriticalPathCalculator:
logic: GameLogic
random: Random
options: SatisfactoryOptions
required_parts: set[str]
required_buildings: set[str]
required_item_names: set[str]
required_power_level: int
__potential_required_belt_speed: int
__potential_required_pipes: bool
__potential_required_radioactive: bool
parts_to_exclude: set[str]
recipes_to_exclude: set[str]
buildings_to_exclude: set[str]
def __init__(self, logic: GameLogic, random: Random, options: SatisfactoryOptions):
self.logic = logic
self.random = random
self.options = options
self.required_parts = set()
self.required_buildings = set()
self.required_power_level: int = 1
self.__potential_required_belt_speed = 1
self.__potential_required_pipes = False
selected_power_infrastructure: dict[int, Recipe] = {}
self.select_minimal_required_parts_for(self.logic.space_elevator_tiers[options.final_elevator_package-1].keys())
for tree in self.logic.man_trees.values():
self.select_minimal_required_parts_for(tree.access_items)
for node in tree.nodes:
if node.minimal_tier > options.final_elevator_package:
continue
self.select_minimal_required_parts_for(node.unlock_cost.keys())
self.select_minimal_required_parts_for_building("MAM")
self.select_minimal_required_parts_for_building("AWESOME Sink")
self.select_minimal_required_parts_for_building("AWESOME Shop")
self.select_minimal_required_parts_for_building("Space Elevator")
self.select_minimal_required_parts_for_building("Conveyor Splitter")
self.select_minimal_required_parts_for_building("Conveyor Merger")
self.select_minimal_required_parts_for_building("Equipment Workshop")
self.select_minimal_required_parts_for_building("Foundation")
self.select_minimal_required_parts_for_building("Walls Orange")
self.select_minimal_required_parts_for_building("Power Storage")
self.select_minimal_required_parts_for_building("Miner Mk.2")
if self.logic.recipes["Uranium"][0].minimal_tier <= options.final_elevator_package:
self.select_minimal_required_parts_for(("Hazmat Suit", "Iodine Infused Filter"))
for i in range(1, self.__potential_required_belt_speed + 1):
self.select_minimal_required_parts_for_building(f"Conveyor Mk.{i}")
if self.__potential_required_pipes:
self.select_minimal_required_parts_for_building("Pipes Mk.1")
self.select_minimal_required_parts_for_building("Pipes Mk.2")
self.select_minimal_required_parts_for_building("Pipeline Pump Mk.1")
self.select_minimal_required_parts_for_building("Pipeline Pump Mk.2")
for i in range(1, self.required_power_level + 1):
power_recipe = random.choice(self.logic.requirement_per_powerlevel[i])
selected_power_infrastructure[i] = power_recipe
self.select_minimal_required_parts_for(power_recipe.inputs)
self.select_minimal_required_parts_for_building(power_recipe.building)
self.required_item_names = set(
recipe.name
for part in self.required_parts
for recipe in self.logic.recipes[part]
if recipe.minimal_tier <= self.options.final_elevator_package
)
self.required_item_names.update("Building: "+ building for building in self.required_buildings)
self.parts_to_exclude = set()
self.buildings_to_exclude = set()
self.recipes_to_exclude = set(
recipe.name
for part in self.logic.recipes
for recipe in self.logic.recipes[part]
if recipe.minimal_tier > self.options.final_elevator_package
)
excluded_count = len(self.recipes_to_exclude)
while True:
for part in self.logic.recipes:
if part in self.parts_to_exclude:
continue
for recipe in self.logic.recipes[part]:
if recipe.name in self.recipes_to_exclude:
continue
if recipe.inputs and any(input in self.parts_to_exclude for input in recipe.inputs):
self.recipes_to_exclude.add(recipe.name)
if all(r.name in self.recipes_to_exclude for r in self.logic.recipes[part]):
self.parts_to_exclude.add(part)
new_buildings_to_exclude = set(
building_name
for building_name, building in self.logic.buildings.items()
if building_name not in self.buildings_to_exclude
and building.inputs and any(input in self.parts_to_exclude for input in building.inputs)
)
self.recipes_to_exclude.update(
recipe_per_part.name
for building_to_exclude in new_buildings_to_exclude
for recipes_per_part in self.logic.recipes.values()
for recipe_per_part in recipes_per_part
if recipe_per_part.building == building_to_exclude
)
self.buildings_to_exclude.update(new_buildings_to_exclude)
new_length = len(self.recipes_to_exclude)
if new_length == excluded_count:
break
excluded_count = new_length
def select_minimal_required_parts_for_building(self, building: str) -> None:
self.select_minimal_required_parts_for(self.logic.buildings[building].inputs)
self.required_buildings.add(building)
def select_minimal_required_parts_for(self, parts: Optional[Iterable[str]]) -> None:
if parts is None:
return
for part in parts:
if part in self.required_parts:
continue
self.required_parts.add(part)
for recipe in self.logic.recipes[part]:
if recipe.minimal_tier > self.options.final_elevator_package:
continue
self.__potential_required_belt_speed = \
max(self.__potential_required_belt_speed, recipe.minimal_belt_speed)
self.select_minimal_required_parts_for(recipe.inputs)
if recipe.needs_pipes:
self.__potential_required_pipes = True
if recipe.is_radio_active:
self.__potential_required_radioactive = True
if recipe.building:
self.select_minimal_required_parts_for(self.logic.buildings[recipe.building].inputs)
self.required_buildings.add(recipe.building)
if self.logic.buildings[recipe.building].power_requirement:
self.required_power_level = \
max(self.required_power_level,
self.logic.buildings[recipe.building].power_requirement)

View File

@@ -1,4 +1,4 @@
from typing import Tuple, Optional, Dict, Set, List
from typing import Optional
from dataclasses import dataclass
from enum import IntEnum
@@ -11,7 +11,7 @@ class PowerInfrastructureLevel(IntEnum):
def to_name(self):
return "Power level: " + self.name
liquids: Set[str] = {
liquids: set[str] = {
"Water",
"Liquid Biofuel",
"Crude Oil",
@@ -29,7 +29,7 @@ liquids: Set[str] = {
"Dark Matter Residue"
}
radio_actives: Set[str] = {
radio_actives: set[str] = {
"Uranium",
"Encased Uranium Cell",
"Uranium Fuel Rod"
@@ -50,19 +50,20 @@ class Recipe():
"""
name: str
building: str
inputs: Tuple[str, ...]
inputs: tuple[str, ...]
minimal_belt_speed: int
handcraftable: bool
implicitly_unlocked: bool
"""No explicit location/item is needed to unlock this recipe, you have access as soon as dependencies are met (ex. Water, Leaves, tutorial starting items)"""
additional_outputs: Tuple[str, ...]
additional_outputs: tuple[str, ...]
minimal_tier: int
needs_pipes: bool
is_radio_active: bool
def __init__(self, name: str, building: Optional[str] = None, inputs: Optional[Tuple[str, ...]] = None,
def __init__(self, name: str, building: Optional[str] = None, inputs: Optional[tuple[str, ...]] = None,
minimal_belt_speed: int = 1, handcraftable: bool = False, implicitly_unlocked: bool = False,
additional_outputs: Optional[Tuple[str, ...]] = None):
additional_outputs: Optional[tuple[str, ...]] = None, minimal_tier: Optional[int] = 1):
self.name = "Recipe: " + name
self.building = building
self.inputs = inputs
@@ -70,8 +71,9 @@ class Recipe():
self.handcraftable = handcraftable
self.implicitly_unlocked = implicitly_unlocked
self.additional_outputs = additional_outputs
self.minimal_tier = minimal_tier
all_parts: List[str] = [name]
all_parts: list[str] = [name]
if inputs:
all_parts += inputs
if additional_outputs:
@@ -84,7 +86,7 @@ class Building(Recipe):
power_requirement: Optional[PowerInfrastructureLevel]
can_produce: bool
def __init__(self, name: str, inputs: Optional[Tuple[str, ...]] = None,
def __init__(self, name: str, inputs: Optional[tuple[str, ...]] = None,
power_requirement: Optional[PowerInfrastructureLevel] = None, can_produce: bool = True,
implicitly_unlocked: bool = False):
super().__init__(name, None, inputs, handcraftable=True, implicitly_unlocked=implicitly_unlocked)
@@ -96,23 +98,26 @@ class Building(Recipe):
class MamNode():
name: str
unlock_cost: Dict[str, int]
unlock_cost: dict[str, int]
"""All game items must be submitted to purchase this MamNode"""
depends_on: Tuple[str, ...]
depends_on: tuple[str, ...]
"""At least one of these prerequisite MamNodes must be unlocked to purchase this MamNode"""
minimal_tier: Optional[int]
def __init__(self, name: str, unlock_cost: Dict[str, int], depends_on: Tuple[str, ...]):
def __init__(self, name: str, unlock_cost: dict[str, int], depends_on: tuple[str, ...],
minimal_tier: Optional[int] = 1):
self.name = name
self.unlock_cost = unlock_cost
self.depends_on = depends_on
self.minimal_tier = minimal_tier
class MamTree():
access_items: Tuple[str, ...]
access_items: tuple[str, ...]
"""At least one of these game items must enter the player inventory for this MamTree to be available"""
nodes: Tuple[MamNode, ...]
nodes: tuple[MamNode, ...]
def __init__(self, access_items: Tuple[str, ...], nodes: Tuple[MamNode, ...]):
def __init__(self, access_items: tuple[str, ...], nodes: tuple[MamNode, ...]):
self.access_items = access_items
self.nodes = nodes
@@ -129,7 +134,7 @@ class DropPodData:
class GameLogic:
recipes: Dict[str, Tuple[Recipe, ...]] = {
recipes: dict[str, tuple[Recipe, ...]] = {
# This Dict should only contain items that are used somewhere in a logic chain
# Exploration Items
@@ -186,15 +191,15 @@ class GameLogic:
"Crude Oil": (
Recipe("Crude Oil", "Oil Extractor", implicitly_unlocked=True), ),
"Bauxite": (
Recipe("Bauxite", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ),
Recipe("Bauxite", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True, minimal_tier=2), ),
"Nitrogen Gas": (
Recipe("Nitrogen Gas", "Resource Well Pressurizer", implicitly_unlocked=True), ),
Recipe("Nitrogen Gas", "Resource Well Pressurizer", implicitly_unlocked=True, minimal_tier=2), ),
"Uranium": (
Recipe("Uranium", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ),
Recipe("Uranium", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True, minimal_tier=2), ),
# Special Items
"Uranium Waste": (
Recipe("Uranium Waste", "Nuclear Power Plant", ("Uranium Fuel Rod", "Water"), implicitly_unlocked=True), ),
Recipe("Uranium Waste", "Nuclear Power Plant", ("Uranium Fuel Rod", "Water"), implicitly_unlocked=True, minimal_tier=2), ),
#"Plutonium Waste": (
# Recipe("Plutonium Waste", "Nuclear Power Plant", ("Plutonium Fuel Rod", "Water"), implicitly_unlocked=True), ),
@@ -253,7 +258,7 @@ class GameLogic:
Recipe("Molded Steel Pipe", "Foundry", ("Steel Ingot", "Concrete"))),
"Steel Beam": (
Recipe("Steel Beam", "Constructor", ("Steel Ingot", ), handcraftable=True),
Recipe("Aluminum Beam", "Constructor", ("Aluminum Ingot", )),
Recipe("Aluminum Beam", "Constructor", ("Aluminum Ingot", ), minimal_tier=2),
Recipe("Molded Beam", "Foundry", ("Steel Ingot", "Concrete"), minimal_belt_speed=2)),
"Heavy Oil Residue": (
Recipe("Heavy Oil Residue", "Refinery", ("Crude Oil", ), additional_outputs=("Polymer Resin", )),
@@ -266,7 +271,7 @@ class GameLogic:
Recipe("Heavy Oil Residue", "Refinery", ("Crude Oil", ), additional_outputs=("Heavy Oil Residue", ), minimal_belt_speed=3)),
"Fuel": (
Recipe("Fuel", "Refinery", ("Crude Oil", ), additional_outputs=("Polymer Resin", )),
Recipe("Diluted Fuel", "Blender", ("Heavy Oil Residue", "Water")),
Recipe("Diluted Fuel", "Blender", ("Heavy Oil Residue", "Water"), minimal_tier=2),
Recipe("Residual Fuel", "Refinery", ("Heavy Oil Residue", ))),
"Concrete": (
Recipe("Concrete", "Constructor", ("Limestone", ), handcraftable=True, implicitly_unlocked=True),
@@ -275,16 +280,16 @@ class GameLogic:
Recipe("Wet Concrete", "Refinery", ("Limestone", "Water"), minimal_belt_speed=2)),
"Silica": (
Recipe("Silica", "Constructor", ("Raw Quartz", ), handcraftable=True),
Recipe("Alumina Solution", "Refinery", ("Bauxite", "Water"), additional_outputs=("Alumina Solution", ), minimal_belt_speed=2),
Recipe("Alumina Solution", "Refinery", ("Bauxite", "Water"), additional_outputs=("Alumina Solution", ), minimal_belt_speed=2, minimal_tier=2),
Recipe("Cheap Silica", "Assembler", ("Raw Quartz", "Limestone")),
Recipe("Distilled Silica", "Blender", ("Dissolved Silica", "Limestone", "Water"), additional_outputs=("Water", ))),
Recipe("Distilled Silica", "Blender", ("Dissolved Silica", "Limestone", "Water"), additional_outputs=("Water", ), minimal_tier=2)),
"Dissolved Silica": (
Recipe("Quartz Purification", "Refinery", ("Raw Quartz", "Nitric Acid"), additional_outputs=("Quartz Crystal", ), minimal_belt_speed=2), ),
Recipe("Quartz Purification", "Refinery", ("Raw Quartz", "Nitric Acid"), additional_outputs=("Quartz Crystal", ), minimal_belt_speed=2, minimal_tier=2), ),
"Quartz Crystal": (
Recipe("Quartz Crystal", "Constructor", ("Raw Quartz", ), handcraftable=True),
Recipe("Pure Quartz Crystal", "Refinery", ("Raw Quartz", "Water"), minimal_belt_speed=2),
Recipe("Fused Quartz Crystal", "Foundry", ("Raw Quartz", "Coal"), minimal_belt_speed=2),
Recipe("Quartz Purification", "Refinery", ("Raw Quartz", "Nitric Acid"), additional_outputs=("Dissolved Silica", ), minimal_belt_speed=2)),
Recipe("Quartz Purification", "Refinery", ("Raw Quartz", "Nitric Acid"), additional_outputs=("Dissolved Silica", ), minimal_belt_speed=2, minimal_tier=2)),
"Iron Ingot": (
Recipe("Iron Ingot", "Smelter", ("Iron Ore", ), handcraftable=True, implicitly_unlocked=True),
Recipe("Pure Iron Ingot", "Refinery", ("Iron Ore", "Water"), minimal_belt_speed=2),
@@ -351,61 +356,61 @@ class GameLogic:
Recipe("Smart Plating", "Assembler", ("Reinforced Iron Plate", "Rotor")),
Recipe("Plastic Smart Plating", "Manufacturer", ("Reinforced Iron Plate", "Rotor", "Plastic"))),
"Versatile Framework": (
Recipe("Versatile Framework", "Assembler", ("Modular Frame", "Steel Beam")),
Recipe("Flexible Framework", "Manufacturer", ("Modular Frame", "Steel Beam", "Rubber"))),
Recipe("Versatile Framework", "Assembler", ("Modular Frame", "Steel Beam"), minimal_tier=2),
Recipe("Flexible Framework", "Manufacturer", ("Modular Frame", "Steel Beam", "Rubber"), minimal_tier=2)),
"Automated Wiring": (
Recipe("Automated Wiring", "Assembler", ("Stator", "Cable")),
Recipe("Automated Speed Wiring", "Manufacturer", ("Stator", "Wire", "High-Speed Connector"), minimal_belt_speed=2)),
Recipe("Automated Wiring", "Assembler", ("Stator", "Cable"), minimal_tier=2),
Recipe("Automated Speed Wiring", "Manufacturer", ("Stator", "Wire", "High-Speed Connector"), minimal_belt_speed=2, minimal_tier=2)),
"Modular Engine": (
Recipe("Modular Engine", "Manufacturer", ("Motor", "Rubber", "Smart Plating")), ),
Recipe("Modular Engine", "Manufacturer", ("Motor", "Rubber", "Smart Plating"), minimal_tier=3), ),
"Adaptive Control Unit": (
Recipe("Adaptive Control Unit", "Manufacturer", ("Automated Wiring", "Circuit Board", "Heavy Modular Frame", "Computer")), ),
Recipe("Adaptive Control Unit", "Manufacturer", ("Automated Wiring", "Circuit Board", "Heavy Modular Frame", "Computer"), minimal_tier=3), ),
"Portable Miner": (
Recipe("Portable Miner", "Equipment Workshop", ("Iron Rod", "Iron Plate"), handcraftable=True, minimal_belt_speed=0, implicitly_unlocked=True),
Recipe("Automated Miner", "Manufacturer", ("Steel Pipe", "Iron Plate")), ),
Recipe("Automated Miner", "Assembler", ("Steel Pipe", "Iron Plate")), ),
"Alumina Solution": (
Recipe("Alumina Solution", "Refinery", ("Bauxite", "Water"), additional_outputs=("Silica", ), minimal_belt_speed=2),
Recipe("Sloppy Alumina", "Refinery", ("Bauxite", "Water"), minimal_belt_speed=3)),
Recipe("Alumina Solution", "Refinery", ("Bauxite", "Water"), additional_outputs=("Silica", ), minimal_belt_speed=2, minimal_tier=2),
Recipe("Sloppy Alumina", "Refinery", ("Bauxite", "Water"), minimal_belt_speed=3, minimal_tier=2)),
"Aluminum Scrap": (
Recipe("Aluminum Scrap", "Refinery", ("Alumina Solution", "Coal"), additional_outputs=("Water", ), minimal_belt_speed=4),
Recipe("Electrode Aluminum Scrap", "Refinery", ("Alumina Solution", "Petroleum Coke"), additional_outputs=("Water", ), minimal_belt_speed=4),
Recipe("Instant Scrap", "Blender", ("Bauxite", "Coal", "Sulfuric Acid", "Water"), additional_outputs=("Water", ), minimal_belt_speed=3)),
Recipe("Aluminum Scrap", "Refinery", ("Alumina Solution", "Coal"), additional_outputs=("Water", ), minimal_belt_speed=4, minimal_tier=2),
Recipe("Electrode Aluminum Scrap", "Refinery", ("Alumina Solution", "Petroleum Coke"), additional_outputs=("Water", ), minimal_belt_speed=4, minimal_tier=2),
Recipe("Instant Scrap", "Blender", ("Bauxite", "Coal", "Sulfuric Acid", "Water"), additional_outputs=("Water", ), minimal_belt_speed=3, minimal_tier=2)),
"Aluminum Ingot": (
Recipe("Aluminum Ingot", "Foundry", ("Aluminum Scrap", "Silica"), minimal_belt_speed=2, handcraftable=True),
Recipe("Pure Aluminum Ingot", "Smelter", ("Aluminum Scrap", ))),
Recipe("Aluminum Ingot", "Foundry", ("Aluminum Scrap", "Silica"), minimal_belt_speed=2, handcraftable=True, minimal_tier=2),
Recipe("Pure Aluminum Ingot", "Smelter", ("Aluminum Scrap", ), minimal_tier=2)),
"Alclad Aluminum Sheet": (
Recipe("Alclad Aluminum Sheet", "Assembler", ("Aluminum Ingot", "Copper Ingot"), handcraftable=True), ),
Recipe("Alclad Aluminum Sheet", "Assembler", ("Aluminum Ingot", "Copper Ingot"), handcraftable=True, minimal_tier=2), ),
"Aluminum Casing": (
Recipe("Aluminum Casing", "Constructor", ("Alclad Aluminum Sheet", ), handcraftable=True),
Recipe("Alclad Casing", "Assembler", ("Aluminum Ingot", "Copper Ingot"))),
Recipe("Aluminum Casing", "Constructor", ("Alclad Aluminum Sheet", ), handcraftable=True, minimal_tier=2),
Recipe("Alclad Casing", "Assembler", ("Aluminum Ingot", "Copper Ingot"), minimal_tier=2)),
"Heat Sink": (
Recipe("Heat Sink", "Assembler", ("Alclad Aluminum Sheet", "Silica"), minimal_belt_speed=2, handcraftable=True),
Recipe("Heat Exchanger", "Assembler", ("Aluminum Casing", "Rubber"), minimal_belt_speed=3)),
Recipe("Heat Sink", "Assembler", ("Alclad Aluminum Sheet", "Silica"), minimal_belt_speed=2, handcraftable=True, minimal_tier=2),
Recipe("Heat Exchanger", "Assembler", ("Aluminum Casing", "Rubber"), minimal_belt_speed=3, minimal_tier=2)),
"Nitric Acid": (
Recipe("Nitric Acid", "Blender", ("Nitrogen Gas", "Water", "Iron Plate")), ),
Recipe("Nitric Acid", "Blender", ("Nitrogen Gas", "Water", "Iron Plate"), minimal_tier=2), ),
"Fused Modular Frame": (
Recipe("Fused Modular Frame", "Blender", ("Heavy Modular Frame", "Aluminum Casing", "Nitrogen Gas"), minimal_belt_speed=2),
Recipe("Heat-Fused Frame", "Blender", ("Heavy Modular Frame", "Aluminum Ingot", "Nitric Acid", "Fuel"), minimal_belt_speed=3)),
Recipe("Fused Modular Frame", "Blender", ("Heavy Modular Frame", "Aluminum Casing", "Nitrogen Gas"), minimal_belt_speed=2, minimal_tier=2),
Recipe("Heat-Fused Frame", "Blender", ("Heavy Modular Frame", "Aluminum Ingot", "Nitric Acid", "Fuel"), minimal_belt_speed=3, minimal_tier=2)),
"Radio Control Unit": (
Recipe("Radio Control Unit", "Manufacturer", ("Aluminum Casing", "Crystal Oscillator", "Computer"), handcraftable=True),
Recipe("Radio Connection Unit", "Manufacturer", ("Heat Sink", "High-Speed Connector", "Quartz Crystal")),
Recipe("Radio Control System", "Manufacturer", ("Crystal Oscillator", "Circuit Board", "Aluminum Casing", "Rubber"), minimal_belt_speed=2)),
Recipe("Radio Control Unit", "Manufacturer", ("Aluminum Casing", "Crystal Oscillator", "Computer"), handcraftable=True, minimal_tier=2),
Recipe("Radio Connection Unit", "Manufacturer", ("Heat Sink", "High-Speed Connector", "Quartz Crystal"), minimal_tier=2),
Recipe("Radio Control System", "Manufacturer", ("Crystal Oscillator", "Circuit Board", "Aluminum Casing", "Rubber"), minimal_belt_speed=2, minimal_tier=2)),
"Pressure Conversion Cube": (
Recipe("Pressure Conversion Cube", "Assembler", ("Fused Modular Frame", "Radio Control Unit"), handcraftable=True), ),
Recipe("Pressure Conversion Cube", "Assembler", ("Fused Modular Frame", "Radio Control Unit"), handcraftable=True, minimal_tier=2), ),
"Cooling System": (
Recipe("Cooling System", "Blender", ("Heat Sink", "Rubber", "Water", "Nitrogen Gas")),
Recipe("Cooling Device", "Blender", ("Heat Sink", "Motor", "Nitrogen Gas"))),
Recipe("Cooling System", "Blender", ("Heat Sink", "Rubber", "Water", "Nitrogen Gas"), minimal_tier=2),
Recipe("Cooling Device", "Blender", ("Heat Sink", "Motor", "Nitrogen Gas"), minimal_tier=2)),
"Turbo Motor": (
Recipe("Turbo Motor", "Manufacturer", ("Cooling System", "Radio Control Unit", "Motor", "Rubber"), handcraftable=True),
Recipe("Turbo Electric Motor", "Manufacturer", ("Motor", "Radio Control Unit", "Electromagnetic Control Rod", "Rotor")),
Recipe("Turbo Pressure Motor", "Manufacturer", ("Motor", "Pressure Conversion Cube", "Packaged Nitrogen Gas", "Stator"))),
Recipe("Turbo Motor", "Manufacturer", ("Cooling System", "Radio Control Unit", "Motor", "Rubber"), handcraftable=True, minimal_tier=2),
Recipe("Turbo Electric Motor", "Manufacturer", ("Motor", "Radio Control Unit", "Electromagnetic Control Rod", "Rotor"), minimal_tier=2),
Recipe("Turbo Pressure Motor", "Manufacturer", ("Motor", "Pressure Conversion Cube", "Packaged Nitrogen Gas", "Stator"), minimal_tier=2)),
"Battery": (
Recipe("Battery", "Blender", ("Sulfuric Acid", "Alumina Solution", "Aluminum Casing"), additional_outputs=("Water", )),
Recipe("Classic Battery", "Manufacturer", ("Sulfur", "Alclad Aluminum Sheet", "Plastic", "Wire"), minimal_belt_speed=2)),
Recipe("Battery", "Blender", ("Sulfuric Acid", "Alumina Solution", "Aluminum Casing"), additional_outputs=("Water", ), minimal_tier=2),
Recipe("Classic Battery", "Manufacturer", ("Sulfur", "Alclad Aluminum Sheet", "Plastic", "Wire"), minimal_belt_speed=2, minimal_tier=2)),
"Supercomputer": (
Recipe("Supercomputer", "Manufacturer", ("Computer", "AI Limiter", "High-Speed Connector", "Plastic"), handcraftable=True),
Recipe("OC Supercomputer", "Assembler", ("Radio Control Unit", "Cooling System")),
Recipe("Super-State Computer", "Manufacturer", ("Computer", "Electromagnetic Control Rod", "Battery", "Wire"))),
Recipe("OC Supercomputer", "Assembler", ("Radio Control Unit", "Cooling System"), minimal_tier=2),
Recipe("Super-State Computer", "Manufacturer", ("Computer", "Electromagnetic Control Rod", "Battery", "Wire"), minimal_tier=2)),
"Sulfuric Acid": (
Recipe("Sulfuric Acid", "Refinery", ("Sulfur", "Water")), ),
"Encased Uranium Cell": (
@@ -428,19 +433,19 @@ class GameLogic:
"Gas Filter": (
Recipe("Gas Filter", "Manufacturer", ("Coal", "Rubber", "Fabric"), handcraftable=True), ),
"Iodine Infused Filter": (
Recipe("Iodine Infused Filter", "Manufacturer", ("Gas Filter", "Quickwire", "Aluminum Casing"), handcraftable=True), ),
Recipe("Iodine Infused Filter", "Manufacturer", ("Gas Filter", "Quickwire", "Aluminum Casing"), handcraftable=True, minimal_tier=2), ),
"Hazmat Suit": (
Recipe("Hazmat Suit", "Equipment Workshop", ("Rubber", "Plastic", "Fabric", "Alclad Aluminum Sheet"), handcraftable=True, minimal_belt_speed=0), ),
Recipe("Hazmat Suit", "Equipment Workshop", ("Rubber", "Plastic", "Fabric", "Alclad Aluminum Sheet"), handcraftable=True, minimal_tier=2), ),
"Assembly Director System": (
Recipe("Assembly Director System", "Assembler", ("Adaptive Control Unit", "Supercomputer")), ),
Recipe("Assembly Director System", "Assembler", ("Adaptive Control Unit", "Supercomputer"), minimal_tier=4), ),
"Magnetic Field Generator": (
Recipe("Magnetic Field Generator", "Assembler", ("Versatile Framework", "Electromagnetic Control Rod")), ),
Recipe("Magnetic Field Generator", "Assembler", ("Versatile Framework", "Electromagnetic Control Rod"), minimal_tier=4), ),
"Copper Powder": (
Recipe("Copper Powder", "Constructor", ("Copper Ingot", ), handcraftable=True), ),
"Nuclear Pasta": (
Recipe("Nuclear Pasta", "Particle Accelerator", ("Copper Powder", "Pressure Conversion Cube")), ),
Recipe("Nuclear Pasta", "Particle Accelerator", ("Copper Powder", "Pressure Conversion Cube"), minimal_tier=2), ),
"Thermal Propulsion Rocket": (
Recipe("Thermal Propulsion Rocket", "Manufacturer", ("Modular Engine", "Turbo Motor", "Cooling System", "Fused Modular Frame")), ),
Recipe("Thermal Propulsion Rocket", "Manufacturer", ("Modular Engine", "Turbo Motor", "Cooling System", "Fused Modular Frame"), minimal_tier=4), ),
"Alien Protein": (
Recipe("Hatcher Protein", "Constructor", ("Hatcher Remains", ), handcraftable=True),
Recipe("Hog Protein", "Constructor", ("Hog Remains", ), handcraftable=True),
@@ -450,7 +455,7 @@ class GameLogic:
Recipe("Biomass (Leaves)", "Constructor", ("Leaves", ), minimal_belt_speed=2, handcraftable=True, implicitly_unlocked=True),
Recipe("Biomass (Wood)", "Constructor", ("Wood", ), minimal_belt_speed=4, handcraftable=True, implicitly_unlocked=True),
Recipe("Biomass (Mycelia)", "Constructor", ("Mycelia", ), minimal_belt_speed=3, handcraftable=True),
Recipe("Biomass (Alien Protein)", "Constructor", ("Alien Protein", ), minimal_belt_speed=5, handcraftable=True)),
Recipe("Biomass (Alien Protein)", "Constructor", ("Alien Protein", ), minimal_belt_speed=4, handcraftable=True)),
"Fabric": (
Recipe("Fabric", "Assembler", ("Biomass", "Mycelia"), handcraftable=True, minimal_belt_speed=2),
Recipe("Polyester Fabric", "Refinery", ("Polymer Resin", "Water"))),
@@ -463,7 +468,7 @@ class GameLogic:
Recipe("Coated Iron Canister", "Assembler", ("Iron Plate", "Copper Sheet")),
Recipe("Steel Canister", "Constructor", ("Steel Ingot", ))),
"Empty Fluid Tank": (
Recipe("Empty Fluid Tank", "Constructor", ("Aluminum Ingot", ), handcraftable=True), ),
Recipe("Empty Fluid Tank", "Constructor", ("Aluminum Ingot", ), handcraftable=True, minimal_tier=2), ),
"Packaged Alumina Solution": (
Recipe("Packaged Alumina Solution", "Packager", ("Alumina Solution", "Empty Canister"), minimal_belt_speed=2), ),
"Packaged Fuel": (
@@ -478,23 +483,23 @@ class GameLogic:
"Packaged Nitrogen Gas": (
Recipe("Packaged Nitrogen Gas", "Packager", ("Nitrogen Gas", "Empty Fluid Tank")), ),
"Packaged Oil": (
Recipe("Packaged Oil", "Packager", ("Crude Oil", "Empty Fluid Tank")), ),
Recipe("Packaged Oil", "Packager", ("Crude Oil", "Empty Canister")), ),
"Packaged Sulfuric Acid": (
Recipe("Packaged Sulfuric Acid", "Packager", ("Sulfuric Acid", "Empty Fluid Tank")), ),
Recipe("Packaged Sulfuric Acid", "Packager", ("Sulfuric Acid", "Empty Canister")), ),
"Packaged Turbofuel": (
Recipe("Packaged Turbofuel", "Packager", ("Turbofuel", "Empty Fluid Tank")), ),
Recipe("Packaged Turbofuel", "Packager", ("Turbofuel", "Empty Canister")), ),
"Packaged Water": (
Recipe("Packaged Water", "Packager", ("Water", "Empty Fluid Tank")), ),
Recipe("Packaged Water", "Packager", ("Water", "Empty Canister")), ),
"Turbofuel": (
Recipe("Turbofuel", "Refinery", ("Fuel", "Compacted Coal")),
Recipe("Turbo Heavy Fuel", "Refinery", ("Heavy Oil Residue", "Compacted Coal")),
Recipe("Turbo Blend Fuel", "Blender", ("Fuel", "Heavy Oil Residue", "Sulfur", "Petroleum Coke"))),
Recipe("Turbo Blend Fuel", "Blender", ("Fuel", "Heavy Oil Residue", "Sulfur", "Petroleum Coke"), minimal_tier=2)),
"Gas Mask": (
Recipe("Gas Mask", "Equipment Workshop", ("Rubber", "Plastic", "Fabric"), handcraftable=True, minimal_belt_speed=0), ),
"Alien DNA Capsule": (
Recipe("Alien DNA Capsule", "Constructor", ("Alien Protein", ), handcraftable=True), ),
"Black Powder": (
Recipe("Black Powder", "Assembler", ("Coal", "Sulfur"), handcraftable=True),
Recipe("Black Powder", "Equipment Workshop", ("Coal", "Sulfur"), handcraftable=True),
Recipe("Fine Black Powder", "Assembler", ("Sulfur", "Compacted Coal"))),
"Smokeless Powder": (
Recipe("Smokeless Powder", "Refinery", ("Black Powder", "Heavy Oil Residue")), ),
@@ -508,7 +513,7 @@ class GameLogic:
Recipe("Power Shard (1)", "Constructor", ("Blue Power Slug", ), handcraftable=True),
Recipe("Power Shard (2)", "Constructor", ("Yellow Power Slug", ), handcraftable=True),
Recipe("Power Shard (5)", "Constructor", ("Purple Power Slug", ), handcraftable=True),
Recipe("Synthetic Power Shard", "Quantum Encoder", ("Dark Matter Residue", "Excited Photonic Matter", "Time Crystal", "Dark Matter Crystal", "Quartz Crystal"))), # 1.0
Recipe("Synthetic Power Shard", "Quantum Encoder", ("Dark Matter Residue", "Excited Photonic Matter", "Time Crystal", "Dark Matter Crystal", "Quartz Crystal"), minimal_tier=4)), # 1.0
"Object Scanner": (
Recipe("Object Scanner", "Equipment Workshop", ("Reinforced Iron Plate", "Wire", "Screw"), handcraftable=True), ),
"Xeno-Zapper": (
@@ -516,8 +521,8 @@ class GameLogic:
#1.0
"Rocket Fuel": (
Recipe("Rocket Fuel", "Blender", ("Turbofuel", "Nitric Acid"), additional_outputs=("Compacted Coal", )),
Recipe("Nitro Rocket Fuel", "Blender", ("Fuel", "Nitrogen Gas", "Sulfur", "Coal"), minimal_belt_speed=2, additional_outputs=("Compacted Coal", ))),
Recipe("Rocket Fuel", "Blender", ("Turbofuel", "Nitric Acid"), additional_outputs=("Compacted Coal", ), minimal_tier=2),
Recipe("Nitro Rocket Fuel", "Blender", ("Fuel", "Nitrogen Gas", "Sulfur", "Coal"), minimal_belt_speed=2, additional_outputs=("Compacted Coal", ), minimal_tier=2)),
#"Ionized Fuel": (
# Recipe("Ionized Fuel", "Refinery", ("Rocket Fuel", "Power Shard"), additional_outputs=("Compacted Coal", )), ),
"Packaged Rocket Fuel": (
@@ -554,9 +559,9 @@ class GameLogic:
"Singularity Cell": (
Recipe("Singularity Cell", "Manufacturer", ("Nuclear Pasta", "Dark Matter Crystal", "Iron Plate", "Concrete"), minimal_belt_speed=3), ),
"Biochemical Sculptor": (
Recipe("Biochemical Sculptor", "Blender", ("Assembly Director System", "Ficsite Trigon", "Water")), ),
Recipe("Biochemical Sculptor", "Blender", ("Assembly Director System", "Ficsite Trigon", "Water"), minimal_tier=5), ),
"Ballistic Warp Drive": (
Recipe("Ballistic Warp Drive", "Manufacturer", ("Thermal Propulsion Rocket", "Singularity Cell", "Superposition Oscillator", "Dark Matter Crystal")), ),
Recipe("Ballistic Warp Drive", "Manufacturer", ("Thermal Propulsion Rocket", "Singularity Cell", "Superposition Oscillator", "Dark Matter Crystal"), minimal_tier=5), ),
# All Quantum Encoder recipes have `Dark Matter Residue` set as an input, this hack makes the logic make sure you can get rid of it
"Dark Matter Residue": (
@@ -569,20 +574,18 @@ class GameLogic:
"Neural-Quantum Processor": (
Recipe("Neural-Quantum Processor", "Quantum Encoder", ("Dark Matter Residue", "Excited Photonic Matter", "Time Crystal", "Supercomputer", "Ficsite Trigon")), ),
"AI Expansion Server": (
Recipe("AI Expansion Server", "Quantum Encoder", ("Dark Matter Residue", "Excited Photonic Matter", "Magnetic Field Generator", "Neural-Quantum Processor", "Superposition Oscillator")), ),
Recipe("AI Expansion Server", "Quantum Encoder", ("Dark Matter Residue", "Excited Photonic Matter", "Magnetic Field Generator", "Neural-Quantum Processor", "Superposition Oscillator"), minimal_tier=5), ),
###
#1.0
# TODO transport types aren't currently in logic
}
buildings: Dict[str, Building] = {
buildings: dict[str, Building] = {
"Constructor": Building("Constructor", ("Reinforced Iron Plate", "Cable"), PowerInfrastructureLevel.Basic, implicitly_unlocked=True),
"Assembler": Building("Assembler", ("Reinforced Iron Plate", "Rotor", "Cable"), PowerInfrastructureLevel.Basic),
"Manufacturer": Building("Manufacturer", ("Motor", "Heavy Modular Frame", "Cable", "Plastic"), PowerInfrastructureLevel.Advanced),
"Packager": Building("Packager", ("Steel Beam", "Rubber", "Plastic"), PowerInfrastructureLevel.Basic),
"Refinery": Building("Refinery", ("Motor", "Encased Industrial Beam", "Steel Pipe", "Copper Sheet"), PowerInfrastructureLevel.Automated),
"Blender": Building("Blender", ("Motor", "Heavy Modular Frame", "Aluminum Casing", "Radio Control Unit"), PowerInfrastructureLevel.Complex),
"Blender": Building("Blender", ("Motor", "Heavy Modular Frame", "Aluminum Casing", "Radio Control Unit"), PowerInfrastructureLevel.Advanced),
"Particle Accelerator": Building("Particle Accelerator", ("Radio Control Unit", "Electromagnetic Control Rod", "Supercomputer", "Cooling System", "Fused Modular Frame", "Turbo Motor"), PowerInfrastructureLevel.Complex),
"Biomass Burner": Building("Biomass Burner", ("Iron Plate", "Iron Rod", "Wire"), implicitly_unlocked=True),
"Coal Generator": Building("Coal Generator", ("Reinforced Iron Plate", "Rotor", "Cable")),
@@ -590,8 +593,8 @@ class GameLogic:
"Geothermal Generator": Building("Geothermal Generator", ("Motor", "Modular Frame", "High-Speed Connector", "Copper Sheet", "Wire")),
"Nuclear Power Plant": Building("Nuclear Power Plant", ("Concrete", "Heavy Modular Frame", "Supercomputer", "Cable", "Alclad Aluminum Sheet")),
"Miner Mk.1": Building("Miner Mk.1", ("Iron Plate", "Concrete"), PowerInfrastructureLevel.Basic, implicitly_unlocked=True),
"Miner Mk.2": Building("Miner Mk.2", ("Encased Industrial Beam", "Steel Pipe", "Modular Frame"), PowerInfrastructureLevel.Automated),
"Miner Mk.3": Building("Miner Mk.3", ("Steel Pipe", "Supercomputer", "Fused Modular Frame", "Turbo Motor"), PowerInfrastructureLevel.Advanced),
"Miner Mk.2": Building("Miner Mk.2", ("Encased Industrial Beam", "Steel Pipe", "Modular Frame"), PowerInfrastructureLevel.Automated, can_produce=False),
"Miner Mk.3": Building("Miner Mk.3", ("Steel Pipe", "Supercomputer", "Fused Modular Frame", "Turbo Motor"), PowerInfrastructureLevel.Advanced, can_produce=False),
"Oil Extractor": Building("Oil Extractor", ("Motor", "Encased Industrial Beam", "Cable")),
"Water Extractor": Building("Water Extractor", ("Copper Sheet", "Reinforced Iron Plate", "Rotor")),
"Smelter": Building("Smelter", ("Iron Rod", "Wire"), PowerInfrastructureLevel.Basic, implicitly_unlocked=True),
@@ -629,13 +632,13 @@ class GameLogic:
#1.0
}
handcraftable_recipes: Dict[str, List[Recipe]] = {}
handcraftable_recipes: dict[str, list[Recipe]] = {}
for part, recipes_per_part in recipes.items():
for recipe in recipes_per_part:
if recipe.handcraftable:
handcraftable_recipes.setdefault(part, list()).append(recipe)
implicitly_unlocked_recipes: Dict[str, Recipe] = {
implicitly_unlocked_recipes: dict[str, Recipe] = {
recipe.name: recipe
for recipes_per_part in recipes.values()
for recipe in recipes_per_part if recipe.implicitly_unlocked
@@ -645,7 +648,7 @@ class GameLogic:
for building in buildings.values() if building.implicitly_unlocked
})
requirement_per_powerlevel: Dict[PowerInfrastructureLevel, Tuple[Recipe, ...]] = {
requirement_per_powerlevel: dict[PowerInfrastructureLevel, tuple[Recipe, ...]] = {
# no need to polute the logic by including higher level recipes based on previus recipes
PowerInfrastructureLevel.Basic: (
Recipe("Biomass Power (Biomass)", "Biomass Burner", ("Biomass", ), implicitly_unlocked=True),
@@ -675,7 +678,7 @@ class GameLogic:
slots_per_milestone: int = 8
hub_layout: Tuple[Tuple[Dict[str, int], ...], ...] = (
hub_layout: tuple[tuple[dict[str, int], ...], ...] = (
# Regenerate via /Script/Engine.Blueprint'/Archipelago/Debug/CC_BuildHubData.CC_BuildHubData'
( # Tier 1
{"Concrete":200, "Iron Plate":100, "Iron Rod":100, }, # Schematic: Base Building (Schematic_1-1_C)
@@ -741,7 +744,7 @@ class GameLogic:
)
# Values from /Game/FactoryGame/Schematics/Progression/BP_GamePhaseManager.BP_GamePhaseManager
space_elevator_tiers: Tuple[Dict[str, int], ...] = (
space_elevator_tiers: tuple[dict[str, int], ...] = (
{ "Smart Plating": 50 },
{ "Smart Plating": 500, "Versatile Framework": 500, "Automated Wiring": 100 },
{ "Versatile Framework": 2500, "Modular Engine": 500, "Adaptive Control Unit": 100 },
@@ -751,7 +754,7 @@ class GameLogic:
# Do not regenerate as format got changed
# Regenerate via /Script/Engine.Blueprint'/Archipelago/Debug/CC_BuildMamData.CC_BuildMamData'
man_trees: Dict[str, MamTree] = {
man_trees: dict[str, MamTree] = {
"Alien Organisms": MamTree(("Hog Remains", "Plasma Spitter Remains", "Stinger Remains", "Hatcher Remains"), ( # Alien Organisms (BPD_ResearchTree_AlienOrganisms_C)
MamNode("Inflated Pocket Dimension", {"Alien Protein":3,"Cable":1000,}, depends_on=("Bio-Organic Properties", )), #(Research_AOrgans_3_C)
MamNode("Hostile Organism Detection", {"Alien DNA Capsule":10,"Crystal Oscillator":5,"High-Speed Connector":5,}, depends_on=("Bio-Organic Properties", )), #(Research_AOrganisms_2_C)
@@ -785,7 +788,7 @@ class GameLogic:
MamNode("Alien Energy Harvesting", {"SAM Fluctuator":10,}, depends_on=("Somersloop Analysis", "SAM Fluctuator")),
MamNode("Production Amplifier", {"Somersloop":1,"SAM Fluctuator":100,"Circuit Board":50,}, depends_on=("Alien Energy Harvesting",)),
MamNode("Power Augmenter", {"Somersloop":1,"SAM Fluctuator":100,"Computer":50,}, depends_on=("Alien Energy Harvesting",)),
MamNode("Alien Power Matrix", {"Singularity Cell":50,"Power Shard":100,"SAM Fluctuator":500}, depends_on=("Power Augmenter",)),
MamNode("Alien Power Matrix", {"Singularity Cell":50,"Power Shard":100,"SAM Fluctuator":500}, depends_on=("Power Augmenter",), minimal_tier=4),
)),
# 1.0
"Caterium": MamTree(("Caterium Ore", ), ( # Caterium (BPD_ResearchTree_Caterium_C)
@@ -831,7 +834,7 @@ class GameLogic:
MamNode("Yellow Power Shards", {"Yellow Power Slug":1,"Rotor":25,"Cable":100,}, depends_on=("Blue Power Slugs", )), #(Research_PowerSlugs_4_C)
MamNode("Purple Power Shards", {"Purple Power Slug":1,"Modular Frame":25,"Copper Sheet":100,}, depends_on=("Yellow Power Shards", )), #(Research_PowerSlugs_5_C)
MamNode("Overclock Production", {"Power Shard":1,"Iron Plate":50,"Wire":50,}, depends_on=("Blue Power Slugs", )), #(Research_PowerSlugs_2_C)
MamNode("Synthetic Power Shards", {"Power Shard":10,"Time Crystal":100,"Quartz Crystal":200,}, depends_on=("Purple Power Shards", )), # 1.0
MamNode("Synthetic Power Shards", {"Power Shard":10,"Time Crystal":100,"Quartz Crystal":200,}, depends_on=("Purple Power Shards", ), minimal_tier=4), # 1.0
)),
"Quartz": MamTree(("Raw Quartz", ), ( # Quartz (BPD_ResearchTree_Quartz_C)
MamNode("Crystal Oscillator", {"Quartz Crystal":100,"Reinforced Iron Plate":50,}, depends_on=("Quartz Crystals", )), #(Research_Quartz_2_C)
@@ -857,16 +860,16 @@ class GameLogic:
MamNode("Explosive Rebar", {"Smokeless Powder":200,"Iron Rebar":200,"Steel Beam":200,}, depends_on=("Smokeless Powder", )), #(Research_Sulfur_4_2_C)
MamNode("Cluster Nobelisk", {"Smokeless Powder":100,"Nobelisk":200,}, depends_on=("Smokeless Powder", )), #(Research_Sulfur_4_C)
MamNode("Experimental Power Generation", {"Sulfur":25,"Modular Frame":50,"Rotor":100,}, depends_on=("Sulfur", )), #(Research_Sulfur_ExperimentalPower_C)
MamNode("Turbo Rifle Ammo", {"Rifle Ammo":1000,"Packaged Turbofuel":50,"Aluminum Casing":100,}, depends_on=("The Rifle", )), #(Research_Sulfur_5_2_C) # 1.0
MamNode("Turbo Rifle Ammo", {"Rifle Ammo":1000,"Packaged Turbofuel":50,"Aluminum Casing":100,}, depends_on=("The Rifle", ), minimal_tier=2), #(Research_Sulfur_5_2_C) # 1.0
MamNode("Turbo Fuel", {"Hard Drive":1,"Compacted Coal":15,"Packaged Fuel":50,}, depends_on=("Experimental Power Generation", )), #(Research_Sulfur_TurboFuel_C)
MamNode("Expanded Toolbelt", {"Black Powder":100,"Encased Industrial Beam":50,}, depends_on=("Black Powder", )), #(Research_Sulfur_5_C)
MamNode("Nuclear Deterrent Development", {"Nobelisk":500,"Encased Uranium Cell":10,"AI Limiter":100,}, depends_on=("Cluster Nobelisk", )), #(Research_Sulfur_5_1_C) # 1.0
MamNode("Rocket Fuel", {"Hard Drive":1,"Empty Fluid Tank":10,"Packaged Turbofuel":100,}, depends_on=("Turbo Fuel", )), # 1.0
MamNode("Ionized Fuel", {"Hard Drive":1,"Power Shard":100,"Packaged Rocket Fuel":200,}, depends_on=("Turbo Fuel", )), # 1.0
MamNode("Nuclear Deterrent Development", {"Nobelisk":500,"Encased Uranium Cell":10,"AI Limiter":100,}, depends_on=("Cluster Nobelisk", ), minimal_tier=2), #(Research_Sulfur_5_1_C) # 1.0
MamNode("Rocket Fuel", {"Hard Drive":1,"Empty Fluid Tank":10,"Packaged Turbofuel":100,}, depends_on=("Turbo Fuel", ), minimal_tier=3), # 1.0
MamNode("Ionized Fuel", {"Hard Drive":1,"Power Shard":100,"Packaged Rocket Fuel":200,}, depends_on=("Turbo Fuel", ), minimal_tier=4), # 1.0
))
}
drop_pods: List[DropPodData] = [
drop_pods: list[DropPodData] = [
# Regenerate via /Script/Engine.Blueprint'/Archipelago/Debug/CC_BuildDropPodLocations.CC_BuildDropPodLocations'
DropPodData(-29068, -22640, 17384, "Encased Industrial Beam", 0), # Unlocks with: 4 x Desc_SteelPlateReinforced_C
DropPodData(-33340, 5176, 23519, "Crystal Oscillator", 0), # Unlocks with: 5 x Desc_CrystalOscillator_C

View File

@@ -1,40 +1,42 @@
from enum import Enum
from typing import NamedTuple, Set
from enum import IntFlag
from typing import NamedTuple
from BaseClasses import ItemClassification
class ItemGroups(str, Enum):
Parts = 1
Equipment = 2
Ammo = 3
Recipe = 4
Building = 5
Trap = 6
Lights = 7
Foundations = 8
Transport = 9
Trains = 10
ConveyorMk1 = 11
ConveyorMk2 = 12
ConveyorMk3 = 13
ConveyorMk4 = 14
ConveyorMk5 = 15
ConveyorSupports = 16
PipesMk1 = 17
PipesMk2 = 18
PipelineSupports = 19
HyperTubes = 20
Signs = 21
Pilars = 22
Beams = 23
Walls = 24
Upgrades = 25
Vehicles = 26
Customizer = 27
ConveyorMk6 = 28
class ItemGroups(IntFlag):
Parts = 1 << 1
Equipment = 1 << 2
Ammo = 1 << 3
Recipe = 1 << 4
Building = 1 << 5
Trap = 1 << 6
Lights = 1 << 7
Foundations = 1 << 8
Transport = 1 << 9
Trains = 1 << 10
ConveyorMk1 = 1 << 11
ConveyorMk2 = 1 << 12
ConveyorMk3 = 1 << 13
ConveyorMk4 = 1 << 14
ConveyorMk5 = 1 << 15
ConveyorSupports = 1 << 16
PipesMk1 = 1 << 17
PipesMk2 = 1 << 18
PipelineSupports = 1 << 19
HyperTubes = 1 << 20
Signs = 1 << 21
Pilars = 1 << 22
Beams = 1 << 23
Walls = 1 << 24
Upgrades = 1 << 25
Vehicles = 1 << 26
Customizer = 1 << 27
ConveyorMk6 = 1 << 28
AlwaysUseful = 1 << 29
class ItemData(NamedTuple):
"""Represents an item in the pool, it could be a resource bundle, production recipe, trap, etc."""
category: Set[ItemGroups]
category: ItemGroups
code: int
type: ItemClassification = ItemClassification.filler
count: int = 1

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,15 @@
from typing import List, Optional, Callable, Tuple, Dict, Iterable, ClassVar
from typing import ClassVar, Optional
from collections.abc import Iterable, Callable
from math import ceil, floor
from BaseClasses import CollectionState
from .GameLogic import GameLogic, Recipe, Building, PowerInfrastructureLevel, DropPodData
from .StateLogic import StateLogic, EventId, part_event_prefix, building_event_prefix
from .Items import Items
from .Options import SatisfactoryOptions
from math import ceil, floor
from .CriticalPathCalculator import CriticalPathCalculator
class LocationData():
__slots__ = ("region", "name", "event_name", "code", "non_progression", "rule")
region: str
name: str
event_name: str
@@ -27,42 +29,27 @@ class LocationData():
class Part(LocationData):
@staticmethod
def get_parts(state_logic: StateLogic, recipes: Tuple[Recipe, ...], name: str, items: Items) -> List[LocationData]:
recipes_per_region: Dict[str, List[Recipe]] = {}
def get_parts(state_logic: StateLogic, recipes: tuple[Recipe, ...], name: str,
final_elevator_tier: int) -> list[LocationData]:
recipes_per_region: dict[str, list[Recipe]] = {}
for recipe in recipes:
if recipe.minimal_tier > final_elevator_tier:
continue
recipes_per_region.setdefault(recipe.building or "Overworld", []).append(recipe)
return [Part(state_logic, region, recipes_for_region, name, items)
return [Part(state_logic, region, recipes_for_region, name)
for region, recipes_for_region in recipes_per_region.items()]
def __init__(self, state_logic: StateLogic, region: str, recipes: Iterable[Recipe], name: str, items: Items):
super().__init__(region, part_event_prefix + name + region, EventId, part_event_prefix + name,
rule = self.can_produce_any_recipe_for_part(state_logic, recipes, name, items))
def __init__(self, state_logic: StateLogic, region: str, recipes: Iterable[Recipe], name: str):
super().__init__(region, part_event_prefix + name + " in " + region, EventId, part_event_prefix + name,
rule = self.can_produce_any_recipe_for_part(state_logic, recipes))
def can_produce_any_recipe_for_part(self, state_logic: StateLogic, recipes: Iterable[Recipe],
name: str, items: Items) -> Callable[[CollectionState], bool]:
def can_produce_any_recipe_for_part(self, state_logic: StateLogic, recipes: Iterable[Recipe]) \
-> Callable[[CollectionState], bool]:
def can_build_by_any_recipe(state: CollectionState) -> bool:
if name == "Steel Ingot":
debug = True
if items.precalculated_progression_recipes and name in items.precalculated_progression_recipes:
can_produce: bool = state_logic.can_produce_specific_recipe_for_part(
state, items.precalculated_progression_recipes[name])
can_produce_anyway: bool
if can_produce:
return can_produce
else:
can_produce_anyway = \
any(state_logic.can_produce_specific_recipe_for_part(state, recipe) for recipe in recipes)
if can_produce_anyway:
debug = True
return False # can_produce_anyway
else:
return any(state_logic.can_produce_specific_recipe_for_part(state, recipe) for recipe in recipes)
return any(state_logic.can_produce_specific_recipe_for_part(state, recipe) for recipe in recipes)
return can_build_by_any_recipe
@@ -76,10 +63,7 @@ class EventBuilding(LocationData):
) -> Callable[[CollectionState], bool]:
def can_build(state: CollectionState) -> bool:
if building.name == "Building: Foundry":
debug = True
return state_logic.has_recipe(state, building) \
return (building.implicitly_unlocked or state_logic.has_recipe(state, building)) \
and state_logic.can_power(state, building.power_requirement) \
and state_logic.can_produce_all_allowing_handcrafting(state, game_logic, building.inputs)
@@ -132,16 +116,16 @@ class ShopSlot(LocationData):
if not state_logic or cost < 20:
return True
elif (cost >= 20 and cost < 50):
return state_logic.is_game_phase(state, 1)
return state_logic.is_elevator_tier(state, 1)
elif (cost >= 50 and cost < 100):
return state_logic.is_game_phase(state, 2)
return state_logic.is_elevator_tier(state, 2)
else:
return state_logic.is_game_phase(state, 3)
return state_logic.is_elevator_tier(state, 3)
return can_purchase
class DropPod(LocationData):
class HardDrive(LocationData):
def __init__(self, data: DropPodData, state_logic: Optional[StateLogic],
locationId: int, tier: int, can_hold_progression: bool):
@@ -149,12 +133,6 @@ class DropPod(LocationData):
# we currently do not know how many hdd require gas or radioactive protection
# coordinates are for us to reference them, there is no real link between coordinate and check
def get_region(gassed: Optional[bool], radioactive: Optional[bool]) -> str:
#if radioactive:
# return "Radioactive Area"
#elif gassed:
# return "Gas Area"
#else:
# return "Overworld"
return f"Hub Tier {tier}"
def get_rule(unlocked_by: Optional[str], power_needed: int) -> Callable[[CollectionState], bool]:
@@ -177,6 +155,7 @@ class Locations():
options: Optional[SatisfactoryOptions]
state_logic: Optional[StateLogic]
items: Optional[Items]
critical_path: Optional[CriticalPathCalculator]
hub_location_start: ClassVar[int] = 1338000
max_tiers: ClassVar[int] = 10
@@ -186,14 +165,17 @@ class Locations():
drop_pod_location_id_end: ClassVar[int] = 1338699
def __init__(self, game_logic: Optional[GameLogic] = None, options: Optional[SatisfactoryOptions] = None,
state_logic: Optional[StateLogic] = None, items: Optional[Items] = None):
state_logic: Optional[StateLogic] = None, items: Optional[Items] = None,
critical_path: Optional[CriticalPathCalculator] = None):
self.game_logic = game_logic
self.options = options
self.state_logic = state_logic
self.items = items
self.critical_path = critical_path
def get_base_location_table(self) -> List[LocationData]:
return [
def get_base_location_table(self, max_tier: int) -> list[LocationData]:
all_locations = [
MamSlot("Alien Organisms", "Inflated Pocket Dimension", 1338500),
MamSlot("Alien Organisms", "Hostile Organism Detection", 1338501),
MamSlot("Alien Organisms", "Expanded Toolbelt", 1338502),
@@ -262,15 +244,15 @@ class Locations():
MamSlot("Sulfur", "Explosive Rebar", 1338565),
MamSlot("Sulfur", "Cluster Nobelisk", 1338566),
MamSlot("Sulfur", "Experimental Power Generation", 1338567),
MamSlot("Sulfur", "Turbo Rifle Ammo", 1338568),
# 1338568 Turbo Rifle Ammo
MamSlot("Sulfur", "Turbo Fuel", 1338569),
MamSlot("Sulfur", "Expanded Toolbelt", 1338570),
MamSlot("Sulfur", "Nuclear Deterrent Development", 1338571),
# 1338571 Nuclear Deterrent Development
# 1.0
MamSlot("Power Slugs", "Synthetic Power Shards", 1338572),
MamSlot("Sulfur", "Rocket Fuel", 1338573),
MamSlot("Sulfur", "Ionized Fuel", 1338574),
# 1338572 Synthetic Power Shards
# 1338573 Rocket Fuel
# 1338574 Ionized Fuel
MamSlot("Alien Technology", "SAM Analysis", 1338575),
MamSlot("Alien Technology", "SAM Reanimation", 1338576),
MamSlot("Alien Technology", "SAM Fluctuator", 1338577),
@@ -289,7 +271,7 @@ class Locations():
MamSlot("Alien Technology", "Alien Energy Harvesting", 1338590),
MamSlot("Alien Technology", "Production Amplifier", 1338591),
MamSlot("Alien Technology", "Power Augmenter", 1338592),
MamSlot("Alien Technology", "Alien Power Matrix", 1338593),
# 1338593 Alien Power Matrix
# 1.0
# 1338600 - 1338699 - Harddrives - Harddrives
@@ -306,93 +288,135 @@ class Locations():
ShopSlot(self.state_logic, 10, 50, 1338709)
]
def get_locations_for_data_package(self) -> Dict[str, int]:
#TODO: should be based on self.game_logic
if max_tier > 8:
all_locations.append(MamSlot("Power Slugs", "Synthetic Power Shards", 1338572))
if max_tier > 8:
all_locations.append(MamSlot("Alien Technology", "Alien Power Matrix", 1338593))
if max_tier > 2:
all_locations.append(MamSlot("Sulfur", "Turbo Rifle Ammo", 1338568))
if max_tier > 2:
all_locations.append(MamSlot("Sulfur", "Nuclear Deterrent Development", 1338571))
if max_tier > 4:
all_locations.append(MamSlot("Sulfur", "Rocket Fuel", 1338573))
if max_tier > 6:
all_locations.append(MamSlot("Sulfur", "Ionized Fuel", 1338574))
return all_locations
def get_locations_for_data_package(self) -> dict[str, int]:
"Must include all possible location names and their id's"
location_table = self.get_base_location_table()
location_table.extend(self.get_hub_locations())
location_table.extend(self.get_drop_pod_locations())
# 1338000 - 1338499 - Milestones
# 1338500 - 1338599 - Mam
# 1338600 - 1338699 - Harddrives
# 1338700 - 1338709 - Shop
# 1338999 - Upper bound
location_table = self.get_base_location_table(self.max_tiers)
location_table.extend(self.get_hub_locations(True, self.max_tiers))
location_table.extend(self.get_hard_drive_locations(True, self.max_tiers, set()))
location_table.append(LocationData("Overworld", "UpperBound", 1338999))
return {location.name: location.code for location in location_table}
def get_locations(self) -> List[LocationData]:
def get_locations(self) -> list[LocationData]:
"Only return location used in this game based on settings"
if not self.game_logic or not self.options or not self.state_logic or not self.items:
raise Exception("Locations need to be initialized with logic, options and items before using this method")
location_table = self.get_base_location_table()
location_table.extend(self.get_hub_locations())
location_table.extend(self.get_drop_pod_locations())
location_table.extend(self.get_logical_event_locations())
max_tier_for_game = min(self.options.final_elevator_package * 2, len(self.game_logic.hub_layout))
location_table = self.get_base_location_table(max_tier_for_game)
location_table.extend(self.get_hub_locations(False, max_tier_for_game))
location_table.extend(self.get_hard_drive_locations(False, max_tier_for_game,self.critical_path.required_parts))
location_table.extend(self.get_logical_event_locations(self.options.final_elevator_package))
return location_table
def get_hub_locations(self) -> List[LocationData]:
location_table: List[LocationData] = []
def get_hub_locations(self, for_data_package: bool, max_tier: int) -> list[LocationData]:
location_table: list[LocationData] = []
number_of_slots_per_milestone_for_game: int
if (for_data_package):
number_of_slots_per_milestone_for_game = self.max_slots
else:
if self.options.final_elevator_package <= 2:
number_of_slots_per_milestone_for_game = 10
else:
number_of_slots_per_milestone_for_game = self.game_logic.slots_per_milestone
hub_location_id = self.hub_location_start
for tier in range(1, self.max_tiers + 1):
for tier in range(1, max_tier + 1):
for milestone in range(1, self.max_milestones + 1):
for slot in range(1, self.max_slots + 1):
if not self.game_logic:
for slot in range(1, number_of_slots_per_milestone_for_game + 1):
if for_data_package:
location_table.append(HubSlot(tier, milestone, slot, hub_location_id))
else:
if tier <= len(self.game_logic.hub_layout) \
if tier <= max_tier \
and milestone <= len(self.game_logic.hub_layout[tier - 1]) \
and slot <= self.game_logic.slots_per_milestone:
and slot <= number_of_slots_per_milestone_for_game:
location_table.append(HubSlot(tier, milestone, slot, hub_location_id))
hub_location_id += 1
return location_table
def get_logical_event_locations(self) -> List[LocationData]:
location_table: List[LocationData] = []
def get_logical_event_locations(self, final_elevator_tier: int) -> list[LocationData]:
location_table: list[LocationData] = []
# for performance plan is to upfront calculated everything we need
# and than create one massive state.has_all for each logical gate (hub tiers, elevator tiers)
location_table.extend(
ElevatorTier(index, self.state_logic, self.game_logic)
for index, parts in enumerate(self.game_logic.space_elevator_tiers))
for index, parts in enumerate(self.game_logic.space_elevator_tiers)
if index < self.options.final_elevator_package)
location_table.extend(
part
part
for part_name, recipes in self.game_logic.recipes.items()
for part in Part.get_parts(self.state_logic, recipes, part_name, self.items))
if part_name in self.critical_path.required_parts
for part in Part.get_parts(self.state_logic, recipes, part_name, final_elevator_tier))
location_table.extend(
EventBuilding(self.game_logic, self.state_logic, name, building)
for name, building in self.game_logic.buildings.items())
for name, building in self.game_logic.buildings.items()
if name in self.critical_path.required_buildings)
location_table.extend(
PowerInfrastructure(self.game_logic, self.state_logic, power_level, recipes)
for power_level, recipes in self.game_logic.requirement_per_powerlevel.items())
for power_level, recipes in self.game_logic.requirement_per_powerlevel.items()
if power_level <= self.critical_path.required_power_level)
return location_table
def get_drop_pod_locations(self) -> List[LocationData]:
drop_pod_locations: List[DropPod] = []
def get_hard_drive_locations(self, for_data_package: bool, max_tier: int, available_parts: set[str]) \
-> list[LocationData]:
hard_drive_locations: list[HardDrive] = []
bucket_size: int = 0
drop_pod_data: List[DropPodData] = []
if self.game_logic:
bucket_size = floor(
(self.drop_pod_location_id_end - self.drop_pod_location_id_start) / len(self.game_logic.hub_layout))
drop_pod_data = self.game_logic.drop_pods
bucket_size: int
drop_pod_data: list[DropPodData]
if for_data_package:
bucket_size = 0
drop_pod_data = []
else:
bucket_size = floor((self.drop_pod_location_id_end - self.drop_pod_location_id_start) / max_tier)
drop_pod_data: list[DropPodData] = self.game_logic.drop_pods
# sort, easily obtainable first, should be deterministic
drop_pod_data.sort(key = lambda data: ("!" if data.item == None else data.item) + str(data.x - data.z))
for location_id in range(self.drop_pod_location_id_start, self.drop_pod_location_id_end + 1):
if not self.game_logic or not self.state_logic or not self.options:
drop_pod_locations.append(DropPod(DropPodData(0, 0, 0, None, 0), None, location_id, 1, False))
if for_data_package:
hard_drive_locations.append(HardDrive(DropPodData(0, 0, 0, None, 0), None, location_id, 1, False))
else:
location_id_normalized: int = location_id - self.drop_pod_location_id_start
data: DropPodData = drop_pod_data[location_id_normalized]
can_hold_progression: bool = location_id_normalized < self.options.hard_drive_progression_limit.value
tier = min(ceil((location_id_normalized + 1) / bucket_size), len(self.game_logic.hub_layout))
tier = min(ceil((location_id_normalized + 1) / bucket_size), max_tier)
drop_pod_locations.append(DropPod(data, self.state_logic, location_id, tier, can_hold_progression))
if not data.item or data.item in available_parts:
hard_drive_locations.append(
HardDrive(data, self.state_logic, location_id, tier, can_hold_progression))
return drop_pod_locations
return hard_drive_locations

View File

@@ -1,8 +1,9 @@
from dataclasses import dataclass
from typing import Dict, List, Any, Tuple, ClassVar, cast
from typing import ClassVar, Any, cast
from enum import IntEnum
from Options import PerGameCommonOptions, DeathLink, AssembleOptions, Visibility
from Options import Range, Toggle, OptionSet, StartInventoryPool, NamedRange, Choice
from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions, Visibility
from Options import Range, NamedRange, Toggle, DefaultOnToggle, OptionSet, StartInventoryPool, Choice
from schema import Schema, And
class Placement(IntEnum):
starting_inventory = 0
@@ -10,7 +11,7 @@ class Placement(IntEnum):
somewhere = 2
class PlacementLogicMeta(AssembleOptions):
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "PlacementLogicMeta":
def __new__(mcs, name: str, bases: tuple[type], attrs: dict[Any, Any]) -> "PlacementLogicMeta":
if "default" in attrs and isinstance(attrs["default"], Placement):
attrs["default"] = int(attrs["default"])
@@ -23,7 +24,7 @@ class PlacementLogic(Choice, metaclass=PlacementLogicMeta):
option_somewhere = Placement.somewhere
class ChoiceMapMeta(AssembleOptions):
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ChoiceMapMeta":
def __new__(mcs, name: str, bases: tuple[type], attrs: dict[Any, Any]) -> "ChoiceMapMeta":
if "choices" in attrs:
for index, choice in enumerate(attrs["choices"].keys()):
option_name = "option_" + choice.replace(' ', '_')
@@ -34,36 +35,36 @@ class ChoiceMapMeta(AssembleOptions):
class ChoiceMap(Choice, metaclass=ChoiceMapMeta):
# TODO `default` doesn't do anything, default is always the first `choices` value. if uncommented it messes up the template file generation (caps mismatch)
choices: ClassVar[Dict[str, List[str]]]
choices: ClassVar[dict[str, list[str]]]
def get_selected_list(self) -> List[str]:
def get_selected_list(self) -> list[str]:
for index, choice in enumerate(self.choices.keys()):
if index == self.value:
return self.choices[choice]
class ElevatorTier(NamedRange):
"""
Ship these Space Elevator packages to finish.
Does nothing if *Space Elevator Tier* goal is not enabled.
Put these Shipments to Space Elevator packages in logic.
if your goal selection contains *Space Elevator Tier* then the goal will be to complete these shipments.
"""
display_name = "Goal: Space Elevator shipment"
default = 2
range_start = 1
range_end = 4
range_end = 5
special_range_names = {
"one package (tiers 1-2)": 1,
"two packages (tiers 1-4)": 2,
"three packages (tiers 1-6)": 3,
"four packages (tiers 7-8)": 4,
"five packages (tier 9)": 5,
"four packages (tiers 1-8)": 4,
"five packages (tiers 1-9)": 5,
}
class ResourceSinkPoints(NamedRange):
class ResourceSinkPointsTotal(NamedRange):
"""
Does nothing if *AWESOME Sink Points (total)* goal is not enabled.
Sink an amount of items totalling this amount of points to finish.
This setting is a *point count*, not a *coupon* count!
Does nothing if *AWESOME Sink Points* goal is not enabled.
In the base game, it takes 347 coupons to unlock every non-repeatable purchase, or 1895 coupons to purchase every non-producible item.
@@ -72,7 +73,7 @@ class ResourceSinkPoints(NamedRange):
If you have *Free Samples* enabled, consider setting this higher so that you can't reach the goal just by sinking your Free Samples.
"""
# Coupon data for above comment from https://satisfactory.wiki.gg/wiki/AWESOME_Shop
display_name = "Goal: AWESOME Sink points"
display_name = "Goal: AWESOME Sink points total"
default = 2166000
range_start = 2166000
range_end = 18436379500
@@ -99,6 +100,37 @@ class ResourceSinkPoints(NamedRange):
"1000 coupons (~18b points)": 18436379500
}
class ResourceSinkPointsPerMinute(NamedRange):
"""
Does nothing if *AWESOME Sink Points (per minute)* goal is not enabled.
Continuously Sink an amount of items to maintain a sink points per minute of this amount of points for 10 minutes to finish.
This setting is in *points per minute* on the orange track so no DNA Capsules!
Use the **TFIT - Ficsit Information Tool** mod or the Satisfactory wiki to find out how many points items are worth.
"""
# Coupon data for above comment from https://satisfactory.wiki.gg/wiki/AWESOME_Shop
display_name = "Goal: AWESOME Sink points per minute"
default = 50000
range_start = 1000
range_end = 10000000
special_range_names = {
"~500 screw/min": 1000,
"~100 reinforced iron plate/min": 12000,
"~100 stator/min": 24000,
"~100 modular frame/min": 40000,
"~100 smart plating/min": 50000,
"~20 crystal oscillator/min": 60000,
"~50 motor/min": 76000,
"~10 heavy modular frame/min": 100000,
"~10 radio control unit": 300000,
"~10 heavy modular frame/min": 625000,
"~10 supercomputer/min": 1000000,
"~10 pressure conversion cube/min": 2500000,
"~10 nuclear pasta/min": 5000000,
"~4 ballistic warp drive/min": 10000000,
}
class HardDriveProgressionLimit(Range):
"""
How many Hard Drives can contain progression items.
@@ -242,7 +274,7 @@ class TrapSelectionOverride(OptionSet):
valid_keys = _trap_types
default = {}
class EnergyLink(Toggle):
class EnergyLink(DefaultOnToggle):
"""
Allow transferring energy to and from other worlds using the Power Storage building.
No energy is lost in the transfer on Satisfactory's side, but other worlds may have other settings.
@@ -334,6 +366,31 @@ class StartingInventoryPreset(ChoiceMap):
}
# default = "Archipelago" # TODO `default` doesn't do anything, default is always the first `choices` value. if uncommented it messes up the template file generation (caps mismatch)
class ExplorationCollectableCount(Range):
"""
Does nothing if *Exploration Collectables* goal is not enabled.
Collect this amount of Mercer Spheres and Summer Sloops each to finish.
"""
display_name = "Goal: Exploration Collectables"
default = 20
range_start = 20
range_end = 100
class MilestoneCostMultiplier(Range):
"""
Multiplies the amount of resources needed to unlock a milestone by this factor
The value is in percentage:
50 = half cost
100 = normal milestone cost
200 = double the cost
"""
display_name = "Milestone cost multiplier %"
default = 100
range_start = 1
range_end = 500
class GoalSelection(OptionSet):
"""
What will be your goal(s)?
@@ -342,11 +399,14 @@ class GoalSelection(OptionSet):
display_name = "Select your Goals"
valid_keys = {
"Space Elevator Tier",
"AWESOME Sink Points",
# "Exploration",
# "FICSMAS Tree",
"AWESOME Sink Points (total)",
"AWESOME Sink Points (per minute)",
"Exploration Collectables",
# "Erect a FICSMAS Tree",
}
default = {"Space Elevator Tier"}
schema = Schema(And(set, len),
error = "yaml does not specify a goal, the Satisfactory option `goal_selection` is empty")
class GoalRequirement(Choice):
"""
@@ -366,11 +426,13 @@ class ExperimentalGeneration(Toggle):
visibility = Visibility.none
@dataclass
class SatisfactoryOptions(PerGameCommonOptions):
class SatisfactoryOptions(PerGameCommonOptions, DeathLinkMixin):
goal_selection: GoalSelection
goal_requirement: GoalRequirement
final_elevator_package: ElevatorTier
final_awesome_sink_points: ResourceSinkPoints
final_awesome_sink_points_total: ResourceSinkPointsTotal
final_awesome_sink_points_per_minute: ResourceSinkPointsPerMinute
final_exploration_collectables_amount: ExplorationCollectableCount
hard_drive_progression_limit: HardDriveProgressionLimit
free_sample_equipment: FreeSampleEquipment
free_sample_buildings: FreeSampleBuildings
@@ -381,10 +443,10 @@ class SatisfactoryOptions(PerGameCommonOptions):
awesome_logic_placement: AwesomeLogic
energy_link_logic_placement: EnergyLinkLogic
splitter_placement: SplitterLogic
milestone_cost_multiplier: MilestoneCostMultiplier
trap_chance: TrapChance
trap_selection_preset: TrapSelectionPreset
trap_selection_override: TrapSelectionOverride
death_link: DeathLink
energy_link: EnergyLink
start_inventory_from_pool: StartInventoryPool
experimental_generation: ExperimentalGeneration

View File

@@ -1,9 +1,11 @@
from typing import List, Set, Dict, Tuple, Optional, Callable
from typing import Optional
from collections.abc import Callable
from BaseClasses import MultiWorld, Region, Location, Item, CollectionState
from .Locations import LocationData
from .GameLogic import GameLogic, PowerInfrastructureLevel
from .StateLogic import StateLogic
from .Options import SatisfactoryOptions, Placement
from .CriticalPathCalculator import CriticalPathCalculator
class SatisfactoryLocation(Location):
game: str = "Satisfactory"
@@ -29,48 +31,50 @@ class SatisfactoryLocation(Location):
return not item.advancement
def create_regions_and_return_locations(world: MultiWorld, options: SatisfactoryOptions, player: int,
game_logic: GameLogic, state_logic: StateLogic, locations: List[LocationData]):
def create_regions_and_return_locations(world: MultiWorld, options: SatisfactoryOptions, player: int,
game_logic: GameLogic, state_logic: StateLogic, critical_path: CriticalPathCalculator,
locations: list[LocationData]):
region_names: List[str] = [
"Menu",
region_names: list[str] = [
"Overworld",
"Gas Area",
"Radioactive Area",
"Mam",
"AWESOME Shop"
]
for hub_tier, milestones_per_hub_tier in enumerate(game_logic.hub_layout, 1):
if (hub_tier > (options.final_elevator_package * 2)):
break
region_names.append(f"Hub Tier {hub_tier}")
for minestone, _ in enumerate(milestones_per_hub_tier, 1):
region_names.append(f"Hub {hub_tier}-{minestone}")
for building_name, building in game_logic.buildings.items():
if building.can_produce:
if building.can_produce and building_name in critical_path.required_buildings:
region_names.append(building_name)
for tree_name, tree in game_logic.man_trees.items():
region_names.append(tree_name)
for node in tree.nodes:
region_names.append(f"{tree_name}: {node.name}")
if node.minimal_tier <= options.final_elevator_package:
region_names.append(f"{tree_name}: {node.name}")
locations_per_region: Dict[str, LocationData] = get_locations_per_region(locations)
regions: Dict[str, Region] = create_regions(world, player, locations_per_region, region_names)
locations_per_region: dict[str, LocationData] = get_locations_per_region(locations)
regions: dict[str, Region] = create_regions(world, player, locations_per_region, region_names)
if __debug__:
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
world.regions += regions.values()
super_early_game_buildings: List[str] = [
super_early_game_buildings: list[str] = [
"Foundation",
"Walls Orange"
]
early_game_buildings: List[str] = [
early_game_buildings: list[str] = [
PowerInfrastructureLevel.Automated.to_name()
]
@@ -85,39 +89,46 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory
super_early_game_buildings.append("Conveyor Splitter")
super_early_game_buildings.append("Conveyor Merger")
connect(regions, "Menu", "Overworld")
if options.final_elevator_package == 1:
super_early_game_buildings.extend(early_game_buildings)
connect(regions, "Overworld", "Hub Tier 1")
connect(regions, "Hub Tier 1", "Hub Tier 2",
lambda state: state_logic.can_build_all(state, super_early_game_buildings))
connect(regions, "Hub Tier 2", "Hub Tier 3", lambda state: state.has("Elevator Tier 1", player)
if options.final_elevator_package >= 2:
connect(regions, "Hub Tier 2", "Hub Tier 3", lambda state: state.has("Elevator Tier 1", player)
and state_logic.can_build_all(state, early_game_buildings))
connect(regions, "Hub Tier 3", "Hub Tier 4")
connect(regions, "Hub Tier 4", "Hub Tier 5", lambda state: state.has("Elevator Tier 2", player))
connect(regions, "Hub Tier 5", "Hub Tier 6")
connect(regions, "Hub Tier 6", "Hub Tier 7", lambda state: state.has("Elevator Tier 3", player))
connect(regions, "Hub Tier 7", "Hub Tier 8")
connect(regions, "Hub Tier 8", "Hub Tier 9", lambda state: state.has("Elevator Tier 4", player))
connect(regions, "Overworld", "Gas Area", lambda state:
state_logic.can_produce_all(state, ("Gas Mask", "Gas Filter")))
connect(regions, "Overworld", "Radioactive Area", lambda state:
state_logic.can_produce_all(state, ("Hazmat Suit", "Iodine Infused Filter")))
connect(regions, "Hub Tier 3", "Hub Tier 4")
if options.final_elevator_package >= 3:
connect(regions, "Hub Tier 4", "Hub Tier 5", lambda state: state.has("Elevator Tier 2", player))
connect(regions, "Hub Tier 5", "Hub Tier 6")
if options.final_elevator_package >= 4:
connect(regions, "Hub Tier 6", "Hub Tier 7", lambda state: state.has("Elevator Tier 3", player))
connect(regions, "Hub Tier 7", "Hub Tier 8")
if options.final_elevator_package >= 5:
connect(regions, "Hub Tier 8", "Hub Tier 9", lambda state: state.has("Elevator Tier 4", player))
connect(regions, "Overworld", "Mam", lambda state: state_logic.can_build(state, "MAM"))
connect(regions, "Overworld", "AWESOME Shop", lambda state:
state_logic.can_build_all(state, ("AWESOME Shop", "AWESOME Sink")))
def can_produce_all_allowing_handcrafting(parts: Tuple[str, ...]) -> Callable[[CollectionState], bool]:
def can_produce_all_allowing_handcrafting(parts: tuple[str, ...]) -> Callable[[CollectionState], bool]:
def logic_rule(state: CollectionState):
return state_logic.can_produce_all_allowing_handcrafting(state, game_logic, parts)
return logic_rule
for hub_tier, milestones_per_hub_tier in enumerate(game_logic.hub_layout, 1):
if (hub_tier > (options.final_elevator_package * 2)):
break
for minestone, parts_per_milestone in enumerate(milestones_per_hub_tier, 1):
connect(regions, f"Hub Tier {hub_tier}", f"Hub {hub_tier}-{minestone}",
can_produce_all_allowing_handcrafting(parts_per_milestone.keys()))
for building_name, building in game_logic.buildings.items():
if building.can_produce:
if building.can_produce and building_name in critical_path.required_buildings:
connect(regions, "Overworld", building_name,
lambda state, building_name=building_name: state_logic.can_build(state, building_name))
@@ -125,16 +136,20 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory
connect(regions, "Mam", tree_name)
for node in tree.nodes:
if node.minimal_tier > options.final_elevator_package:
continue
if not node.depends_on:
connect(regions, tree_name, f"{tree_name}: {node.name}",
lambda state, parts=node.unlock_cost.keys(): state_logic.can_produce_all(state, parts))
else:
for parent in node.depends_on:
connect(regions, f"{tree_name}: {parent}", f"{tree_name}: {node.name}",
lambda state, parts=node.unlock_cost.keys(): state_logic.can_produce_all(state, parts))
if f"{tree_name}: {parent}" in region_names:
connect(regions, f"{tree_name}: {parent}", f"{tree_name}: {node.name}",
lambda state, parts=node.unlock_cost.keys(): state_logic.can_produce_all(state, parts))
def throwIfAnyLocationIsNotAssignedToARegion(regions: Dict[str, Region], regionNames: Set[str]):
def throwIfAnyLocationIsNotAssignedToARegion(regions: dict[str, Region], regionNames: set[str]):
existingRegions = set()
for region in regions.keys():
@@ -145,7 +160,7 @@ def throwIfAnyLocationIsNotAssignedToARegion(regions: Dict[str, Region], regionN
def create_region(world: MultiWorld, player: int,
locations_per_region: Dict[str, List[LocationData]], name: str) -> Region:
locations_per_region: dict[str, list[LocationData]], name: str) -> Region:
region = Region(name, player, world)
@@ -157,10 +172,10 @@ def create_region(world: MultiWorld, player: int,
return region
def create_regions(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]],
region_names: List[str]) -> Dict[str, Region]:
def create_regions(world: MultiWorld, player: int, locations_per_region: dict[str, list[LocationData]],
region_names: list[str]) -> dict[str, Region]:
regions: Dict[str, Region] = {}
regions: dict[str, Region] = {}
for name in region_names:
regions[name] = create_region(world, player, locations_per_region, name)
@@ -168,7 +183,7 @@ def create_regions(world: MultiWorld, player: int, locations_per_region: Dict[st
return regions
def connect(regions: Dict[str, Region], source: str, target: str,
def connect(regions: dict[str, Region], source: str, target: str,
rule: Optional[Callable[[CollectionState], bool]] = None):
sourceRegion = regions[source]
@@ -177,8 +192,8 @@ def connect(regions: Dict[str, Region], source: str, target: str,
sourceRegion.connect(targetRegion, rule=rule)
def get_locations_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]:
per_region: Dict[str, List[LocationData]] = {}
def get_locations_per_region(locations: list[LocationData]) -> dict[str, list[LocationData]]:
per_region: dict[str, list[LocationData]] = {}
for location in locations:
per_region.setdefault(location.region, []).append(location)

View File

@@ -1,4 +1,5 @@
from typing import Tuple, List, Optional, Set, Iterable
from typing import Optional
from collections.abc import Iterable
from BaseClasses import CollectionState
from .GameLogic import GameLogic, Recipe, PowerInfrastructureLevel
from .Options import SatisfactoryOptions
@@ -11,7 +12,7 @@ building_event_prefix = "Can Build: "
class StateLogic:
player: int
options: SatisfactoryOptions
initial_unlocked_items: Set[str]
initial_unlocked_items: set[str]
def __init__(self, player: int, options: SatisfactoryOptions):
self.player = player
@@ -38,14 +39,11 @@ class StateLogic:
return power_level is None or state.has(building_event_prefix + power_level.to_name(), self.player)
def can_produce_all(self, state: CollectionState, parts: Optional[Iterable[str]]) -> bool:
if parts and "SAM" in parts:
debug = "Now"
return parts is None or \
return parts is None or \
state.has_all(map(self.to_part_event, parts), self.player)
def can_produce_all_allowing_handcrafting(self, state: CollectionState, logic: GameLogic,
parts: Optional[Tuple[str, ...]]) -> bool:
parts: Optional[tuple[str, ...]]) -> bool:
def can_handcraft_part(part: str) -> bool:
if self.can_produce(state, part):
@@ -53,7 +51,7 @@ class StateLogic:
elif part not in logic.handcraftable_recipes:
return False
recipes: List[Recipe] = logic.handcraftable_recipes[part]
recipes: list[Recipe] = logic.handcraftable_recipes[part]
return any(
self.has_recipe(state, recipe)
@@ -79,8 +77,10 @@ class StateLogic:
and self.can_build(state, recipe.building) \
and self.can_produce_all(state, recipe.inputs)
def is_game_phase(self, state: CollectionState, phase: int) -> bool:
return state.has(f"Elevator Tier {phase}", self.player)
def is_elevator_tier(self, state: CollectionState, phase: int) -> bool:
limited_phase = min(self.options.final_elevator_package, phase)
return state.has(f"Elevator Tier {limited_phase}", self.player)
@staticmethod
def to_part_event(part: str) -> str:

View File

@@ -1,7 +1,6 @@
from BaseClasses import Tutorial
from ..AutoWorld import WebWorld
class SatisfactoryWebWorld(WebWorld):
theme = "dirt"
setup = Tutorial(

View File

@@ -1,11 +1,12 @@
from typing import Dict, List, Set, TextIO, ClassVar, Tuple
from BaseClasses import Item, MultiWorld, ItemClassification, CollectionState
from typing import TextIO, ClassVar
from BaseClasses import Item, ItemClassification, CollectionState
from .GameLogic import GameLogic
from .Items import Items
from .Locations import Locations, LocationData
from .StateLogic import EventId, StateLogic
from .Options import SatisfactoryOptions, Placement
from .Regions import SatisfactoryLocation, create_regions_and_return_locations
from .CriticalPathCalculator import CriticalPathCalculator
from .Web import SatisfactoryWebWorld
from ..AutoWorld import World
@@ -20,8 +21,8 @@ class SatisfactoryWorld(World):
options_dataclass = SatisfactoryOptions
options: SatisfactoryOptions
topology_present = False
data_version = 0
web = SatisfactoryWebWorld()
origin_region_name = "Overworld"
item_name_to_id = Items.item_names_and_ids
location_name_to_id = Locations().get_locations_for_data_package()
@@ -30,19 +31,12 @@ class SatisfactoryWorld(World):
game_logic: ClassVar[GameLogic] = GameLogic()
state_logic: StateLogic
items: Items
def __init__(self, multiworld: "MultiWorld", player: int):
super().__init__(multiworld, player)
self.items = None
critical_path: CriticalPathCalculator
def generate_early(self) -> None:
self.state_logic = StateLogic(self.player, self.options)
self.items = Items(self.player, self.game_logic, self.random, self.options)
if not self.options.goal_selection.value:
raise Exception("""Satisfactory: player {} needs to choose a goal, the option goal_selection is empty"""
.format(self.multiworld.player_name[self.player]))
self.critical_path = CriticalPathCalculator(self.game_logic, self.random, self.options)
self.items = Items(self.player, self.game_logic, self.random, self.options, self.critical_path)
if self.options.mam_logic_placement.value == Placement.starting_inventory:
self.push_precollected("Building: MAM")
@@ -58,16 +52,17 @@ class SatisfactoryWorld(World):
if not self.options.trap_selection_override.value:
self.options.trap_selection_override.value = self.options.trap_selection_preset.get_selected_list()
starting_inventory: List[str] = self.options.starting_inventory_preset.get_selected_list()
starting_inventory: list[str] = self.options.starting_inventory_preset.get_selected_list()
for item_name in starting_inventory:
self.push_precollected(item_name)
def create_regions(self) -> None:
locations: List[LocationData] = \
Locations(self.game_logic, self.options, self.state_logic, self.items).get_locations()
locations: list[LocationData] = \
Locations(self.game_logic, self.options, self.state_logic, self.items, self.critical_path).get_locations()
create_regions_and_return_locations(
self.multiworld, self.options, self.player, self.game_logic, self.state_logic, locations)
self.multiworld, self.options, self.player, self.game_logic, self.state_logic, self.critical_path,
locations)
def create_items(self) -> None:
@@ -79,21 +74,16 @@ class SatisfactoryWorld(World):
def set_rules(self) -> None:
resource_sink_goal: bool = "AWESOME Sink Points" in self.options.goal_selection
resource_sink_goal: bool = "AWESOME Sink Points (total)" in self.options.goal_selection \
or "AWESOME Sink Points (per minute)" in self.options.goal_selection
last_elevator_tier: int = \
len(self.game_logic.space_elevator_tiers) if resource_sink_goal \
else self.options.final_elevator_package.value
required_parts: Set[str] = set(self.game_logic.space_elevator_tiers[last_elevator_tier - 1].keys())
required_parts = set(self.game_logic.space_elevator_tiers[self.options.final_elevator_package.value - 1].keys())
if resource_sink_goal:
required_parts.union(self.game_logic.buildings["AWESOME Sink"].inputs)
required_parts_tuple: Tuple[str, ...] = tuple(required_parts)
self.multiworld.completion_condition[self.player] = \
lambda state: self.state_logic.can_produce_all(state, required_parts_tuple)
lambda state: self.state_logic.can_produce_all(state, required_parts)
def collect(self, state: CollectionState, item: Item) -> bool:
@@ -110,8 +100,8 @@ class SatisfactoryWorld(World):
return change
def fill_slot_data(self) -> Dict[str, object]:
slot_hub_layout: List[List[Dict[str, int]]] = []
def fill_slot_data(self) -> dict[str, object]:
slot_hub_layout: list[list[dict[str, int]]] = []
for tier, milestones in enumerate(self.game_logic.hub_layout, 1):
slot_hub_layout.append([])
@@ -120,7 +110,8 @@ class SatisfactoryWorld(World):
for part, amount in parts.items():
# ItemIDs of bundles are shared with their component item
bundled_name = f"Bundle: {part}"
slot_hub_layout[tier - 1][milestone - 1][self.item_name_to_id[bundled_name]] = amount
multiplied_amount = max(amount * (self.options.milestone_cost_multiplier / 100), 1)
slot_hub_layout[tier - 1][milestone - 1][self.item_name_to_id[bundled_name]] = multiplied_amount
return {
"Data": {
@@ -130,8 +121,8 @@ class SatisfactoryWorld(World):
"GoalSelection": self.options.goal_selection.value,
"GoalRequirement": self.options.goal_requirement.value,
"FinalElevatorTier": self.options.final_elevator_package.value,
"FinalResourceSinkPoints": self.options.final_awesome_sink_points.value,
"EnableHardDriveGacha": True if self.options.hard_drive_progression_limit else False,
"FinalResourceSinkPointsTotal": self.options.final_awesome_sink_points_total.value,
"FinalResourceSinkPointsPerMinute": self.options.final_awesome_sink_points_per_minute.value,
"FreeSampleEquipment": self.options.free_sample_equipment.value,
"FreeSampleBuildings": self.options.free_sample_buildings.value,
"FreeSampleParts": self.options.free_sample_parts.value,
@@ -139,19 +130,20 @@ class SatisfactoryWorld(World):
"EnergyLink": bool(self.options.energy_link)
}
},
"SlotDataVersion": 1,
"DeathLink": bool(self.options.death_link)
}
def write_spoiler(self, spoiler_handle: TextIO):
self.items.write_progression_chain(self.multiworld, spoiler_handle)
def write_spoiler(self, spoiler_handle: TextIO) -> None:
pass
def get_filler_item_name(self) -> str:
return self.items.get_filler_item_name(self.random, self.options)
return self.items.get_filler_item_name(self.items.filler_items, self.random, self.options)
def setup_events(self):
def setup_events(self) -> None:
location: SatisfactoryLocation
for location in self.multiworld.get_locations(self.player):
if location.address == EventId: