mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 19:33:24 -07:00
Merge remote-tracking branch 'remotes/origin/Satisfactory_limit_evevator_tier' into Satisfactory
# Conflicts: # worlds/satisfactory/Items.py
This commit is contained in:
@@ -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"""
|
||||
|
||||
@@ -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))
|
||||
|
||||
169
worlds/satisfactory/CriticalPathCalculator.py
Normal file
169
worlds/satisfactory/CriticalPathCalculator.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld
|
||||
|
||||
|
||||
class SatisfactoryWebWorld(WebWorld):
|
||||
theme = "dirt"
|
||||
setup = Tutorial(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user