From 3b49aae19e0069b6c77248cc52af048f6e2efd40 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Thu, 3 Apr 2025 22:50:06 +0200 Subject: [PATCH] Fixed generation with different elevator tiers --- test/general/test_reachability.py | 3 +- worlds/satisfactory/CriticalPathCalculator.py | 62 ++++++++++++++++--- worlds/satisfactory/GameLogic.py | 42 ++++++------- worlds/satisfactory/ItemData.py | 2 +- worlds/satisfactory/Items.py | 34 +++++----- worlds/satisfactory/Locations.py | 19 +++--- worlds/satisfactory/Options.py | 4 +- worlds/satisfactory/__init__.py | 20 ++---- 8 files changed, 116 insertions(+), 70 deletions(-) diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py index 0837c8f444..3210daeabe 100644 --- a/test/general/test_reachability.py +++ b/test/general/test_reachability.py @@ -61,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)) diff --git a/worlds/satisfactory/CriticalPathCalculator.py b/worlds/satisfactory/CriticalPathCalculator.py index 14b4695a25..ec3b655e7e 100644 --- a/worlds/satisfactory/CriticalPathCalculator.py +++ b/worlds/satisfactory/CriticalPathCalculator.py @@ -19,6 +19,10 @@ class CriticalPathCalculator: __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 @@ -30,7 +34,6 @@ class CriticalPathCalculator: self.__potential_required_belt_speed = 1 self.__potential_required_pipes = False - self.__potential_required_radioactive = False selected_power_infrastructure: dict[int, Recipe] = {} @@ -57,9 +60,8 @@ class CriticalPathCalculator: self.select_minimal_required_parts_for_building("Power Storage") self.select_minimal_required_parts_for_building("Miner Mk.2") - #equipment - self.select_minimal_required_parts_for(self.logic.recipes["Hazmat Suit"][0].inputs) - self.select_minimal_required_parts_for(self.logic.recipes["Iodine Infused Filter"][0].inputs) + 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}") @@ -68,9 +70,6 @@ class CriticalPathCalculator: 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") - if self.__potential_required_radioactive: - self.select_minimal_required_parts_for(self.logic.recipes["Hazmat Suit"][0].inputs) - self.select_minimal_required_parts_for(self.logic.recipes["Iodine Infused Filter"][0].inputs) 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 @@ -85,6 +84,55 @@ class CriticalPathCalculator: ) 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 + + Debug = True + 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) diff --git a/worlds/satisfactory/GameLogic.py b/worlds/satisfactory/GameLogic.py index f9cc8b447f..50c0b49a16 100644 --- a/worlds/satisfactory/GameLogic.py +++ b/worlds/satisfactory/GameLogic.py @@ -191,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), ), @@ -280,7 +280,7 @@ 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", ), minimal_tier=2)), "Dissolved Silica": ( @@ -356,18 +356,18 @@ 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, minimal_tier=2), Recipe("Sloppy Alumina", "Refinery", ("Bauxite", "Water"), minimal_belt_speed=3, minimal_tier=2)), @@ -433,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"), 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), @@ -513,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"), minimal_tier=2)), # 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": ( @@ -559,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": ( @@ -574,7 +574,7 @@ 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 } diff --git a/worlds/satisfactory/ItemData.py b/worlds/satisfactory/ItemData.py index 7fc3105cf4..a437c3916e 100644 --- a/worlds/satisfactory/ItemData.py +++ b/worlds/satisfactory/ItemData.py @@ -31,7 +31,7 @@ class ItemGroups(IntFlag): Vehicles = 1 << 26 Customizer = 1 << 27 ConveyorMk6 = 1 << 28 - BasicNeeds = 1 << 29 + AlwaysUseful = 1 << 29 class ItemData(NamedTuple): diff --git a/worlds/satisfactory/Items.py b/worlds/satisfactory/Items.py index 49f113ff48..15aa483e93 100644 --- a/worlds/satisfactory/Items.py +++ b/worlds/satisfactory/Items.py @@ -1,4 +1,3 @@ -import copy from random import Random from typing import ClassVar, Dict, Set, List, Tuple, Optional from BaseClasses import Item, ItemClassification as C, MultiWorld @@ -194,7 +193,7 @@ class Items: "Small Inflated Pocket Dimension": ItemData(G.Upgrades, 1338188, C.useful, 11), "Inflated Pocket Dimension": ItemData(G.Upgrades, 1338189, C.useful, 5), "Expanded Toolbelt": ItemData(G.Upgrades, 1338190, C.useful, 5), - "Dimensional Depot upload from inventory": ItemData(G.Upgrades | G.BasicNeeds, 1338191, C.useful), + "Dimensional Depot upload from inventory": ItemData(G.Upgrades, 1338191, C.useful), #1338191 - 1338199 Reserved for future equipment/ammo @@ -493,17 +492,17 @@ class Items: "Building: Fuel Generator": ItemData(G.Building, 1338618, C.progression), "Building: Resource Well Pressurizer": ItemData(G.Building, 1338619, C.progression), "Building: Equipment Workshop": ItemData(G.Building, 1338620, C.progression), - "Building: AWESOME Sink": ItemData(G.Building | G.BasicNeeds, 1338621, C.progression), - "Building: AWESOME Shop": ItemData(G.Building | G.BasicNeeds, 1338622, C.progression), + "Building: AWESOME Sink": ItemData(G.Building | G.AlwaysUseful, 1338621, C.progression), + "Building: AWESOME Shop": ItemData(G.Building | G.AlwaysUseful, 1338622, C.progression), "Building: Painted Beams": ItemData(G.Beams, 1338623, C.filler), "Building: Blueprint Designer": ItemData(G.Building, 1338624, C.filler, 0), # unlocked by default "Building: Fluid Buffer": ItemData(G.Building, 1338625, C.filler), "Building: Industrial Fluid Buffer": ItemData(G.Building, 1338626, C.filler), "Building: Jump Pad": ItemData(G.Building, 1338627, C.filler), "Building: Ladder": ItemData(G.Building, 1338628, C.filler), - "Building: MAM": ItemData(G.Building | G.BasicNeeds, 1338629, C.progression), + "Building: MAM": ItemData(G.Building | G.AlwaysUseful, 1338629, C.progression), "Building: Personal Storage Box": ItemData(G.Building, 1338630, C.filler), - "Building: Power Storage": ItemData(G.Building | G.BasicNeeds, 1338631, C.progression), + "Building: Power Storage": ItemData(G.Building | G.AlwaysUseful, 1338631, C.progression), "Building: U-Jelly Landing Pad": ItemData(G.Building, 1338632, C.useful), "Building: Power Switch": ItemData(G.Building, 1338633, C.useful), "Building: Priority Power Switch": ItemData(G.Building, 1338634, C.useful), @@ -513,8 +512,8 @@ class Items: "Building: Power Pole Mk.2": ItemData(G.Building, 1338638, C.useful), "Building: Power Pole Mk.3": ItemData(G.Building, 1338639, C.useful), "Building: Industrial Storage Container": ItemData(G.Building, 1338640, C.filler), - "Building: Conveyor Merger": ItemData(G.Building | G.BasicNeeds, 1338641, C.progression), - "Building: Conveyor Splitter": ItemData(G.Building | G.BasicNeeds, 1338642, C.progression), + "Building: Conveyor Merger": ItemData(G.Building | G.AlwaysUseful, 1338641, C.progression), + "Building: Conveyor Splitter": ItemData(G.Building | G.AlwaysUseful, 1338642, C.progression), "Building: Conveyor Mk.1": ItemData(G.Building | G.ConveyorMk1, 1338643, C.progression), # unlocked by default "Building: Conveyor Mk.2": ItemData(G.Building | G.ConveyorMk2, 1338644, C.progression), "Building: Conveyor Mk.3": ItemData(G.Building | G.ConveyorMk3, 1338645, C.progression), @@ -564,7 +563,7 @@ class Items: #"Building: Beam Support": ItemData(G.Beams, 1338689, C.filler, 0), #"Building: Beam Connector": ItemData(G.Beams, 1338690, C.filler, 0), #"Building: Beam Connector Double": ItemData(G.Beams, 1338691, C.filler, 0), - "Building: Foundation": ItemData(G.Building | G.Foundations | G.BasicNeeds, 1338692, C.progression), + "Building: Foundation": ItemData(G.Building | G.Foundations | G.AlwaysUseful, 1338692, C.progression), "Building: Half Foundation": ItemData(G.Foundations, 1338693, C.filler, 0), "Building: Corner Ramp Pack": ItemData(G.Foundations, 1338694, C.filler, 0), "Building: Inverted Ramp Pack": ItemData(G.Foundations, 1338695, C.filler, 0), @@ -725,15 +724,14 @@ class Items: if type == C.progression \ and instance and instance.critical_path.required_item_names \ - and (data.category & (G.Recipe | G.Building)) and not (data.category & G.BasicNeeds) \ + and (data.category & (G.Recipe | G.Building)) and not (data.category & G.AlwaysUseful) \ and name not in instance.critical_path.required_item_names: - type = C.filler - logging.info(f"Dropping... {name}") + type = C.useful return Item(name, type, data.code, player) - def get_filler_item_name(self, random: Random, options: SatisfactoryOptions) -> str: + def get_filler_item_name(self, filler_items: Tuple[str, ...], random: Random, options: SatisfactoryOptions) -> str: trap_chance: int = options.trap_chance.value enabled_traps: List[str] = options.trap_selection_override.value @@ -745,6 +743,9 @@ class Items: def get_excluded_items(self, multiworld: MultiWorld, options: SatisfactoryOptions) -> Set[str]: excluded_items: Set[str] = set() + excluded_items.update("Bundle: "+ part for part in self.critical_path.parts_to_exclude) + excluded_items.update(recipe for recipe in self.critical_path.recipes_to_exclude) + excluded_items.update("Building: "+ building for building in self.critical_path.buildings_to_exclude) for item in multiworld.precollected_items[self.player]: if item.name in self.item_data \ @@ -775,9 +776,12 @@ class Items: filler_pool_size: int = number_of_locations - len(pool) if (filler_pool_size < 0): raise Exception(f"Location pool starved, trying to add {len(pool)} items to {number_of_locations} locations") - logging.warning(f"Itempool size: {len(pool)}, number of locations: {number_of_locations}, spare: {filler_pool_size}") + + filtered_filler_items = tuple(item for item in self.filler_items if item not in excluded_from_pool) + for _ in range(filler_pool_size): - item = self.create_item(self, self.get_filler_item_name(random, options), self.player) + filler_item_name = self.get_filler_item_name(filtered_filler_items, random, options) + item = self.create_item(self, filler_item_name, self.player) pool.append(item) return pool diff --git a/worlds/satisfactory/Locations.py b/worlds/satisfactory/Locations.py index a239b6bd7c..0089612c18 100644 --- a/worlds/satisfactory/Locations.py +++ b/worlds/satisfactory/Locations.py @@ -9,6 +9,7 @@ from math import ceil, floor class LocationData(): + __slots__ = ("region", "name", "event_name", "code", "non_progression", "rule") region: str name: str event_name: str @@ -28,7 +29,7 @@ class LocationData(): class Part(LocationData): @staticmethod - def get_parts(state_logic: StateLogic, recipes: Tuple[Recipe, ...], name: str, items: Items, + def get_parts(state_logic: StateLogic, recipes: Tuple[Recipe, ...], name: str, final_elevator_tier: int) -> List[LocationData]: recipes_per_region: Dict[str, List[Recipe]] = {} @@ -38,15 +39,15 @@ class Part(LocationData): 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: return any(state_logic.can_produce_specific_recipe_for_part(state, recipe) for recipe in recipes) @@ -374,10 +375,10 @@ class Locations(): 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() if part_name in self.critical_path.required_parts - for part in Part.get_parts(self.state_logic, recipes, part_name, self.items, final_elevator_tier)) + 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() diff --git a/worlds/satisfactory/Options.py b/worlds/satisfactory/Options.py index c11db3e52e..4c417b5dce 100644 --- a/worlds/satisfactory/Options.py +++ b/worlds/satisfactory/Options.py @@ -3,6 +3,7 @@ from typing import Dict, List, Any, Tuple, ClassVar, cast from enum import IntEnum from Options import PerGameCommonOptions, DeathLink, AssembleOptions, Visibility from Options import Range, Toggle, OptionSet, StartInventoryPool, NamedRange, Choice +from schema import Schema, And, Use class Placement(IntEnum): starting_inventory = 0 @@ -41,7 +42,6 @@ class ChoiceMap(Choice, metaclass=ChoiceMapMeta): if index == self.value: return self.choices[choice] - class ElevatorTier(NamedRange): """ Put these Shipments to Space Elevator packages in logic. @@ -347,6 +347,8 @@ class GoalSelection(OptionSet): # "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): """ diff --git a/worlds/satisfactory/__init__.py b/worlds/satisfactory/__init__.py index 9cf049ae08..d14e725b9d 100644 --- a/worlds/satisfactory/__init__.py +++ b/worlds/satisfactory/__init__.py @@ -1,5 +1,5 @@ -from typing import Dict, List, Set, TextIO, ClassVar, Tuple -from BaseClasses import Item, MultiWorld, ItemClassification, CollectionState +from typing import Dict, List, Set, TextIO, ClassVar +from BaseClasses import Item, ItemClassification, CollectionState from .GameLogic import GameLogic from .Items import Items from .Locations import Locations, LocationData @@ -21,7 +21,6 @@ class SatisfactoryWorld(World): options_dataclass = SatisfactoryOptions options: SatisfactoryOptions topology_present = False - data_version = 0 web = SatisfactoryWebWorld() origin_region_name = "Overworld" @@ -34,20 +33,11 @@ class SatisfactoryWorld(World): items: Items critical_path: CriticalPathCalculator - def __init__(self, multiworld: "MultiWorld", player: int): - super().__init__(multiworld, player) - self.items = None - - def generate_early(self) -> None: self.state_logic = StateLogic(self.player, self.options) 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 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])) - if self.options.mam_logic_placement.value == Placement.starting_inventory: self.push_precollected("Building: MAM") if self.options.awesome_logic_placement.value == Placement.starting_inventory: @@ -144,15 +134,15 @@ class SatisfactoryWorld(World): } - def write_spoiler(self, spoiler_handle: TextIO): + 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: