From ebbdd7bfda67618ba8fb579da6727746b7f126eb Mon Sep 17 00:00:00 2001 From: Jarno Date: Fri, 19 Dec 2025 14:48:03 +0100 Subject: [PATCH] Satisfactory: Add New Game (#5190) * Added Satisfactory to latest master * Fixed hard drive from containing the mam + incremented default value for harddrive progression * Apply cherry pick of 3076259 * Apply cherry pick of 6114a55 * Clarify Point goal behavior (https://github.com/Jarno458/SatisfactoryArchipelagoMod/issues/98) * Update Setup guide and info page * Add links to Gifting and Energy Link compatible games. Add info on Hard Drive behavior * Fix typos * Update hard drive behavior description * Hopefully fixed the mam from getting placed behind harddrives * Add 1 "Bundle: Solid Biofuel" to default starting items (for later chainsaw usage or early power gen) * Add info/warning about save setup failure bug * Add notes about dedicated server setup * Fixes: `TypeError: 'set' object is not subscriptable` random.choice does not work over set objects, cast to a list to allow 'trap_selection_override' * progrees i think * Fixed some bugs * Progress commmit incase my pc crashes * progress i think as test passed * I guess test pass, game still unbeatable tho * its generating * Some refactorings * Fixed generation with different elevator tiers * Remove debug statement * Fix this link. * Implemented abstract base classes + some fixes * Implemented many many new options * Yay more stuff * Fixed renaming of filters * Added 1.1 stuffs * Added options groups and presets * Fixes after variable renmame * Added recipy groups for easyer hinting * Implemented random Tier 0 * Updated slot_data * Latest update for 1.1 * Applied cheaper building costs of assembler and foundry * Implemented exploration cost in slot_data * Fixed exposing option type * Add goal time estimates * Trap info * Added support for Universal Tracker Put more things in the never exclude pool for a more familiar gameplay * Added iron ore to build hub * Added Dark Matter Crystals * Added Single Dark Matter Crystals * Fixed typo in options preset * Update setup directions and info * Options formatting fixes, lower minimum ExplorationCollectableCount, add new Explorer starting inventory items preset * Fixed incorrect description on the options * Reduce Portable Miner and Reinforced Iron Plate quantities in "Skip Tutorial Inspired" starting preset * Fixed options pickling error * Reworked logic to no longer include Single: items as filler Reworked logic for more performance Reworked logic to always put useful equipment in pool * Fixed Itemlinks Removed space elevator parts from fillers Removed more AWESOME shop purchaseables from minimal item pool Added all equipment to minimal item pool Removed non fissile and fertile uranium from minimal item pool Removed portal from minimal item pool Removed Ionized fuel from minimal item pool Removed recipes for Hoverpack and Turbo Rifle Ammo from minimal item pool Lowered the chance for rolling steel on randomized starter recipes * Fixed hub milestone item leaking to into wrong milestones * Fixed unlock cost of geothermal generator * Fixed itemlinks again * Add troubleshooting note about hoverpacks * Add starting inventory bundle delivery info * Added hint generation at generation time Harddrive locations now go from 1-100 rather then 0-99 * Update __init__.py Fixed mistake * Cleaned docs to be better suited to get verified * Update CODEOWNERS Added Satisfactory * Update README.md Added Satisfactory * Restructure and expand setup page to instruct both players and hosts * Add terms entry for Archipelago mod * Fixed generation of traps * Added Robb as code owner * Restore tests to original state * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix additional typos from code review * Implemented fix for itterating enum flags on python 3.10 * Update en_Satisfactory.md * Update setup_en.md * Apply suggestions from code review Co-authored-by: Scipio Wright * more world > multiworld * Clarify universal tracker behavior * Fix typos * Info on smart hinting system * Move list of additional mods to a page on the mod GitHub * Restore revamped setup guide that other commits overwrote Originally from be2651120542214602872bd67798d26dcd237f5f, d8bd1aaf04e6424b74294a4ee4d195c9ae6107ab * Removed bundle of ficsit coupons from the from the item pool added estimated completion times to space elevator option description * Apply suggestions from code review Co-authored-by: Scipio Wright * Wording * Fix typo * Update with changes from ToBeVerified branch * Update note about gameplay options * Update note about gameplay options * Improved universal tracker handling * Improved universal tracker + modernized code a bit * Fixed bugs that where re-introduced * Added Recipe: Excited Photonic Matter * Removed python 3.9 workaround * Fixed * Apply suggestions from code review Co-authored-by: Scipio Wright * Streamlined handle craftable logic by using itterable rather then tuple Removed dict.keys as the dict itzelf already enumerates over keys * Updated option description * Fixed typing * More info on goal completion conditions * More info on goal completion conditions (093fe38b6e5c15370bb8b5c8a4eceb16d7df716f) * Apply suggestions from code review Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> * Implemented review results * PEP8 stuff * More PEP8 * Rename ElevatorTier->ElevatorPhase and related for clarity and consistency. Untested * speedups part1 * speedsups on part rules * Fix formatting * fix `Elevator Tier #` string literals missed in rename * Remove unused/duplicate imports + organize imports, `== None` to `is None` * Fixed after merge * Updated values + removed TODO * PEPed up the code * Small refactorings * Updated name slot data to phase * Fix hint creation * Clarify wording of elevator goal * Review result * Fixed minor typo in option * Update option time estimates --------- Co-authored-by: Rob B Co-authored-by: ProverbialPennance <36955346+ProverbialPennance@users.noreply.github.com> Co-authored-by: Joe Amenta Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Scipio Wright Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/satisfactory/CriticalPathCalculator.py | 273 +++++ worlds/satisfactory/GameLogic.py | 992 ++++++++++++++++++ worlds/satisfactory/ItemData.py | 46 + worlds/satisfactory/Items.py | 991 +++++++++++++++++ worlds/satisfactory/Locations.py | 426 ++++++++ worlds/satisfactory/Options.py | 614 +++++++++++ worlds/satisfactory/Regions.py | 199 ++++ worlds/satisfactory/StateLogic.py | 171 +++ worlds/satisfactory/Web.py | 20 + worlds/satisfactory/__init__.py | 264 +++++ worlds/satisfactory/docs/en_Satisfactory.md | 208 ++++ worlds/satisfactory/docs/setup_en.md | 230 ++++ 14 files changed, 4438 insertions(+) create mode 100644 worlds/satisfactory/CriticalPathCalculator.py create mode 100644 worlds/satisfactory/GameLogic.py create mode 100644 worlds/satisfactory/ItemData.py create mode 100644 worlds/satisfactory/Items.py create mode 100644 worlds/satisfactory/Locations.py create mode 100644 worlds/satisfactory/Options.py create mode 100644 worlds/satisfactory/Regions.py create mode 100644 worlds/satisfactory/StateLogic.py create mode 100644 worlds/satisfactory/Web.py create mode 100644 worlds/satisfactory/__init__.py create mode 100644 worlds/satisfactory/docs/en_Satisfactory.md create mode 100644 worlds/satisfactory/docs/setup_en.md diff --git a/README.md b/README.md index 608af1313c..af574ed248 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Currently, the following games are supported: * Celeste (Open World) * Choo-Choo Charles * APQuest +* Satisfactory For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 7d5ff6e582..3d052cb7cd 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -176,6 +176,9 @@ # Sonic Adventure 2 Battle /worlds/sa2b/ @PoryGone @RaspberrySpace +# Satisfactory +/worlds/satisfactory/ @Jarno458 @budak7273 + # Starcraft 2 # Note: @Ziktofel acts as a mentor /worlds/sc2/ @MatthewMarinets @Snarkie @SirChuckOfTheChuckles diff --git a/worlds/satisfactory/CriticalPathCalculator.py b/worlds/satisfactory/CriticalPathCalculator.py new file mode 100644 index 0000000000..fc055b4e04 --- /dev/null +++ b/worlds/satisfactory/CriticalPathCalculator.py @@ -0,0 +1,273 @@ +from random import Random +from typing import Optional +from collections.abc import Iterable +from .GameLogic import GameLogic, Recipe +from .Options import SatisfactoryOptions + + +class CriticalPathCalculator: + logic: GameLogic + random: Random + final_elevator_phase: int + randomize_starter_recipes: bool + + required_parts: set[str] + required_buildings: set[str] + required_item_names: set[str] + required_power_level: int + + __potential_required_belt_speed: int + + parts_to_exclude: set[str] + recipes_to_exclude: set[str] + buildings_to_exclude: set[str] + + implicitly_unlocked: set[str] + handcraftable_parts: dict[str, list[Recipe]] + tier_0_recipes: set[str] + + def __init__(self, logic: GameLogic, seed: float, options: SatisfactoryOptions): + self.logic = logic + self.random = Random(seed) + self.final_elevator_phase = options.final_elevator_phase.value + self.randomize_starter_recipes = bool(options.randomize_starter_recipes.value) + + def calculate(self) -> None: + self.required_parts = set[str]() + self.required_buildings = set[str]() + self.required_power_level: int = 1 + + self.__potential_required_belt_speed = 1 + + self.configure_implicitly_unlocked_and_handcraftable_parts() + + self.select_minimal_required_parts_for( + self.logic.space_elevator_phases[self.final_elevator_phase-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_phase > self.final_elevator_phase: + continue + + self.select_minimal_required_parts_for(node.unlock_cost) + + 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") + 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") + + if self.logic.recipes["Uranium"][0].minimal_phase <= self.final_elevator_phase: + 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}") + + for i in range(1, self.required_power_level + 1): + power_recipe = self.random.choice(self.logic.requirement_per_powerlevel[i]) + self.select_minimal_required_parts_for(power_recipe.inputs) + self.select_minimal_required_parts_for_building(power_recipe.building) + + self.required_item_names = { + recipe.name + for part in self.required_parts + for recipe in self.logic.recipes[part] + if recipe.minimal_phase <= self.final_elevator_phase + } + self.required_item_names.update({"Building: " + building for building in self.required_buildings}) + + self.calculate_excluded_things() + self.select_starter_recipes() + + 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_phase > self.final_elevator_phase: + 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.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) + + def calculate_excluded_things(self) -> None: + self.parts_to_exclude = set[str]() + self.buildings_to_exclude = set[str]() + self.recipes_to_exclude = { + recipe.name + for part in self.logic.recipes + for recipe in self.logic.recipes[part] + if recipe.minimal_phase > self.final_elevator_phase + } + + 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 = { + 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 recipes_per_part in self.logic.recipes.values() + for recipe_per_part in recipes_per_part + if recipe_per_part.building in new_buildings_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 configure_implicitly_unlocked_and_handcraftable_parts(self) -> None: + self.implicitly_unlocked: set[str] = { + recipe.name + for recipes_per_part in self.logic.recipes.values() + for recipe in recipes_per_part if recipe.implicitly_unlocked + } + self.implicitly_unlocked.update({ + building.name + for building in self.logic.buildings.values() if building.implicitly_unlocked + }) + + self.handcraftable_parts: dict[str, list[Recipe]] = {} + for part, recipes_per_part in self.logic.recipes.items(): + for recipe in recipes_per_part: + if recipe.handcraftable: + self.handcraftable_parts.setdefault(part, []).append(recipe) + + def select_starter_recipes(self) -> None: + # cable is left unaffected as all its alternative recipes require refinery + if not self.randomize_starter_recipes: + self.tier_0_recipes = { + "Recipe: Iron Ingot", + "Recipe: Iron Plate", + "Recipe: Iron Rod", + "Recipe: Copper Ingot", + "Recipe: Wire", + "Recipe: Concrete", + "Recipe: Screw", + "Recipe: Reinforced Iron Plate" + } + else: + # we only allow basic parts to be made without the need of refineries + # could be made more based of GameLogic rather than hardcoded but this is likely faster + # would likely need to be based of GameLogic when we add mod support + self.tier_0_recipes = set() + + self.tier_0_recipes.add(self.random.choice( + ("Recipe: Iron Ingot", "Recipe: Basic Iron Ingot", "Recipe: Iron Alloy Ingot"))) + + selected_recipe = self.random.choice( + ("Recipe: Iron Plate", "Recipe: Iron Plate", "Recipe: Iron Plate", "Recipe: Steel Cast Plate")) + self.tier_0_recipes.add(selected_recipe) + if selected_recipe == "Recipe: Steel Cast Plate": + self.add_steel_ingot_to_starter_recipes() + + selected_recipe = self.random.choice( + ("Recipe: Iron Rod", "Recipe: Iron Rod", "Recipe: Iron Rod", "Recipe: Steel Rod")) + self.tier_0_recipes.add(selected_recipe) + if selected_recipe == "Recipe: Steel Rod": + self.add_steel_ingot_to_starter_recipes() + + self.tier_0_recipes.add(self.random.choice(("Recipe: Copper Ingot", "Recipe: Copper Alloy Ingot"))) + + selected_recipe = self.random.choice( + ("Recipe: Wire", "Recipe: Caterium Wire", "Recipe: Fused Wire", "Recipe: Iron Wire")) + self.tier_0_recipes.add(selected_recipe) + if selected_recipe in {"Recipe: Caterium Wire", "Recipe: Fused Wire"}: + # add Caterium Ingot + self.tier_0_recipes.add("Recipe: Caterium Ingot") + + selected_recipe = self.random.choice(("Recipe: Concrete", "Recipe: Fine Concrete")) + self.tier_0_recipes.add(selected_recipe) + if selected_recipe == "Recipe: Fine Concrete": + # add Silica + self.tier_0_recipes.add(self.random.choice(("Recipe: Silica", "Recipe: Cheap Silica"))) + + selected_recipe = self.random.choice( + ("Recipe: Screw", "Recipe: Screw", "Recipe: Cast Screw", "Recipe: Cast Screw", "Recipe: Steel Screw")) + + self.tier_0_recipes.add(selected_recipe) + if selected_recipe == "Recipe: Steel Screw": + # add Steel Beam and steel Ingot + self.add_steel_ingot_to_starter_recipes() + self.tier_0_recipes.add(self.random.choice(("Recipe: Steel Beam", "Recipe: Molded Beam"))) + + self.tier_0_recipes.add(self.random.choice( + ("Recipe: Reinforced Iron Plate", "Recipe: Bolted Iron Plate", "Recipe: Stitched Iron Plate"))) + + for part, recipes in self.logic.recipes.items(): + for recipe in recipes: + if recipe.name in self.tier_0_recipes: + if part in self.handcraftable_parts: + self.handcraftable_parts[part].append(recipe) + else: + self.handcraftable_parts[part] = [recipe] + self.tier_0_recipes.add(self.logic.buildings[recipe.building].name) + + self.implicitly_unlocked.update(self.tier_0_recipes) + + def add_steel_ingot_to_starter_recipes(self) -> None: + if "Recipe: Steel Ingot" not in self.tier_0_recipes \ + and "Recipe: Compacted Steel Ingot" not in self.tier_0_recipes \ + and "Recipe: Solid Steel Ingot" not in self.tier_0_recipes: + + selected_recipe = self.random.choice( + ("Recipe: Steel Ingot", "Recipe: Compacted Steel Ingot", "Recipe: Solid Steel Ingot")) + + self.tier_0_recipes.add(selected_recipe) + + if selected_recipe == "Recipe: Compacted Steel Ingot": + self.tier_0_recipes.add("Recipe: Compacted Coal") diff --git a/worlds/satisfactory/GameLogic.py b/worlds/satisfactory/GameLogic.py new file mode 100644 index 0000000000..a1039a7efe --- /dev/null +++ b/worlds/satisfactory/GameLogic.py @@ -0,0 +1,992 @@ +from typing import Optional +from dataclasses import dataclass +from enum import IntEnum + + +class PowerInfrastructureLevel(IntEnum): + Basic = 1 + Automated = 2 + Advanced = 3 + Complex = 4 + + def to_name(self) -> str: + return "Power level: " + self.name + + +liquids: set[str] = { + "Water", + "Liquid Biofuel", + "Crude Oil", + "Fuel", + "Heavy Oil Residue", + "Turbofuel", + "Alumina Solution", + "Sulfuric Acid", + "Nitrogen Gas", + "Nitric Acid", + "Dissolved Silica", + "Rocket Fuel", + "Ionized Fuel", + "Excited Photonic Matter", + "Dark Matter Residue" +} + +radio_actives: set[str] = { + "Uranium", + "Encased Uranium Cell", + "Uranium Fuel Rod" + "Uranium Waste", + "Non-fissile Uranium", + "Plutonium Pellet", + "Encased Plutonium Cell", + "Plutonium Fuel Rod", + "Plutonium Waste", + "Ficsonium", + "Ficsonium Fuel Rod" +} + + +class Recipe: + """ + Relationship between components and what is required to produce them (input ingredients, production building, etc.) + Not all recipes are Satisfactory FGRecipes - for example, Water has a Recipe, but it's not an FGRecipe + """ + name: str + building: Optional[str] + inputs: Optional[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: Optional[tuple[str, ...]] + minimal_phase: int + + needs_pipes: bool + is_radio_active: bool + + 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, minimal_phase: Optional[int] = 1): + self.name = "Recipe: " + name + self.building = building + self.inputs = inputs + self.minimal_belt_speed = minimal_belt_speed + self.handcraftable = handcraftable + self.implicitly_unlocked = implicitly_unlocked + self.additional_outputs = additional_outputs + self.minimal_phase = minimal_phase + + all_parts: list[str] = [name] + if inputs: + all_parts += inputs + if additional_outputs: + all_parts += additional_outputs + + self.needs_pipes = not liquids.isdisjoint(all_parts) + self.is_radio_active = not radio_actives.isdisjoint(all_parts) + + +class Building(Recipe): + power_requirement: Optional[PowerInfrastructureLevel] + can_produce: bool + + 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) + self.name = "Building: " + name + self.power_requirement = power_requirement + self.can_produce = can_produce + self.implicitly_unlocked = implicitly_unlocked + + +class MamNode: + name: str + unlock_cost: dict[str, int] + """All game items must be submitted to purchase this MamNode""" + depends_on: tuple[str, ...] + """At least one of these prerequisite MamNodes must be unlocked to purchase this MamNode""" + minimal_phase: Optional[int] + + def __init__(self, name: str, unlock_cost: dict[str, int], depends_on: tuple[str, ...], + minimal_phase: Optional[int] = 1): + self.name = name + self.unlock_cost = unlock_cost + self.depends_on = depends_on + self.minimal_phase = minimal_phase + + +class MamTree: + 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, ...] + + def __init__(self, access_items: tuple[str, ...], nodes: tuple[MamNode, ...]): + self.access_items = access_items + self.nodes = nodes + + +@dataclass +class DropPodData: + x: int + y: int + z: int + item: Optional[str] + power: int + gassed: Optional[bool] = None + radioactive: Optional[bool] = None + + +class GameLogic: + indirect_recipes: dict[str, str] = { + "Recipe: Quartz Purification": "Recipe: Distilled Silica" + } + + recipes: dict[str, tuple[Recipe, ...]] = { + # This Dict should only contain items that are used somewhere in a logic chain + + # Exploration Items + "Leaves": ( + Recipe("Leaves", handcraftable=True, implicitly_unlocked=True), ), + "Wood": ( + Recipe("Wood", handcraftable=True, implicitly_unlocked=True), ), + "Hatcher Remains": ( + Recipe("Hatcher Remains", handcraftable=True, implicitly_unlocked=True), ), + "Hog Remains": ( + Recipe("Hog Remains", handcraftable=True, implicitly_unlocked=True), ), + "Plasma Spitter Remains": ( + Recipe("Plasma Spitter Remains", handcraftable=True, implicitly_unlocked=True), ), + "Stinger Remains": ( + Recipe("Stinger Remains", handcraftable=True, implicitly_unlocked=True), ), + "Mycelia": ( + Recipe("Mycelia", handcraftable=True, implicitly_unlocked=True), ), + "Beryl Nut": ( + Recipe("Beryl Nut", handcraftable=True, implicitly_unlocked=True), ), + "Paleberry": ( + Recipe("Paleberry", handcraftable=True, implicitly_unlocked=True), ), + "Bacon Agaric": ( + Recipe("Bacon Agaric", handcraftable=True, implicitly_unlocked=True), ), + "Blue Power Slug": ( + Recipe("Blue Power Slug", handcraftable=True, implicitly_unlocked=True), ), + "Yellow Power Slug": ( + Recipe("Yellow Power Slug", handcraftable=True, implicitly_unlocked=True), ), + "Purple Power Slug": ( + Recipe("Purple Power Slug", handcraftable=True, implicitly_unlocked=True), ), + "Hard Drive": ( + Recipe("Hard Drive", handcraftable=True, implicitly_unlocked=True), ), + "Mercer Sphere": ( + Recipe("Mercer Sphere", handcraftable=True, implicitly_unlocked=True), ), + "Somersloop": ( + Recipe("Somersloop", handcraftable=True, implicitly_unlocked=True), ), + + # Raw Resources + "Water": ( + Recipe("Water", "Water Extractor", implicitly_unlocked=True), + Recipe("Water (Resource Well)", "Resource Well Pressurizer", implicitly_unlocked=True, minimal_phase=2)), + "Limestone": ( + Recipe("Limestone", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Raw Quartz": ( + Recipe("Raw Quartz", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Iron Ore": ( + Recipe("Iron Ore", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Copper Ore": ( + Recipe("Copper Ore", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Coal": ( + Recipe("Coal", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Sulfur": ( + Recipe("Sulfur", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Caterium Ore": ( + Recipe("Caterium Ore", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Crude Oil": ( + Recipe("Crude Oil", "Oil Extractor", implicitly_unlocked=True), + Recipe("Crude Oil (Resource Well)", "Resource Well Pressurizer", implicitly_unlocked=True, minimal_phase=2)), + "Bauxite": ( + Recipe("Bauxite", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True, minimal_phase=2), ), + "Nitrogen Gas": ( + Recipe("Nitrogen Gas", "Resource Well Pressurizer", implicitly_unlocked=True, minimal_phase=2), ), + "Uranium": ( + Recipe("Uranium", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True, minimal_phase=2), ), + + # Special Items + "Uranium Waste": ( + Recipe("Uranium Waste", "Nuclear Power Plant", ("Uranium Fuel Rod", "Water"), implicitly_unlocked=True, minimal_phase=2), ), + # "Plutonium Waste": ( + # Recipe("Plutonium Waste", "Nuclear Power Plant", ("Plutonium Fuel Rod", "Water"), implicitly_unlocked=True), ), + + # Recipes + "Reinforced Iron Plate": ( + Recipe("Reinforced Iron Plate", "Assembler", ("Iron Plate", "Screw")), + Recipe("Adhered Iron Plate", "Assembler", ("Iron Plate", "Rubber")), + Recipe("Bolted Iron Plate", "Assembler", ("Iron Plate", "Screw"), minimal_belt_speed=3), + Recipe("Stitched Iron Plate", "Assembler", ("Iron Plate", "Wire"))), + "Rotor": ( + Recipe("Rotor", "Assembler", ("Iron Rod", "Screw"), minimal_belt_speed=2, handcraftable=True), + Recipe("Copper Rotor", "Assembler", ("Copper Sheet", "Screw"), minimal_belt_speed=3), + Recipe("Steel Rotor", "Assembler", ("Steel Pipe", "Wire"))), + "Stator": ( + Recipe("Stator", "Assembler", ("Steel Pipe", "Wire"), handcraftable=True), + Recipe("Quickwire Stator", "Assembler", ("Steel Pipe", "Quickwire"))), + "Plastic": ( + Recipe("Plastic", "Refinery", ("Crude Oil", ), additional_outputs=("Heavy Oil Residue", )), + Recipe("Residual Plastic", "Refinery", ("Polymer Resin", "Water")), + Recipe("Recycled Plastic", "Refinery", ("Rubber", "Fuel"))), + "Rubber": ( + Recipe("Rubber", "Refinery", ("Crude Oil", ), additional_outputs=("Heavy Oil Residue", )), + Recipe("Residual Rubber", "Refinery", ("Polymer Resin", "Water")), + Recipe("Recycled Rubber", "Refinery", ("Plastic", "Fuel"))), + "Iron Plate": ( + Recipe("Iron Plate", "Constructor", ("Iron Ingot", )), + Recipe("Coated Iron Plate", "Assembler", ("Iron Ingot", "Plastic"), minimal_belt_speed=2), + Recipe("Steel Cast Plate", "Foundry", ("Iron Ingot", "Steel Ingot"))), + "Iron Rod": ( + Recipe("Iron Rod", "Constructor", ("Iron Ingot", )), + Recipe("Steel Rod", "Constructor", ("Steel Ingot", )), + Recipe("Aluminum Rod", "Constructor", ("Aluminum Ingot", ))), + "Screw": ( + Recipe("Screw", "Constructor", ("Iron Rod", )), + Recipe("Cast Screw", "Constructor", ("Iron Ingot", )), + Recipe("Steel Screw", "Constructor", ("Steel Beam", ), minimal_belt_speed=3)), + "Wire": ( + Recipe("Wire", "Constructor", ("Copper Ingot", )), + Recipe("Fused Wire", "Assembler", ("Copper Ingot", "Caterium Ingot"), minimal_belt_speed=2), + Recipe("Iron Wire", "Constructor", ("Iron Ingot", )), + Recipe("Caterium Wire", "Constructor", ("Caterium Ingot", ), minimal_belt_speed=2)), + "Cable": ( + Recipe("Cable", "Constructor", ("Wire", ), handcraftable=True, implicitly_unlocked=True), + Recipe("Coated Cable", "Refinery", ("Wire", "Heavy Oil Residue"), minimal_belt_speed=2), + Recipe("Insulated Cable", "Assembler", ("Wire", "Rubber"), minimal_belt_speed=2), + Recipe("Quickwire Cable", "Assembler", ("Quickwire", "Rubber"))), + "Quickwire": ( + Recipe("Quickwire", "Constructor", ("Caterium Ingot", ), handcraftable=True), + Recipe("Fused Quickwire", "Assembler", ("Caterium Ingot", "Copper Ingot"), minimal_belt_speed=2)), + "Copper Sheet": ( + Recipe("Copper Sheet", "Constructor", ("Copper Ingot", ), handcraftable=True), + Recipe("Steamed Copper Sheet", "Refinery", ("Copper Ingot", "Water"))), + "Steel Pipe": ( + Recipe("Steel Pipe", "Constructor", ("Steel Ingot", ), handcraftable=True), + Recipe("Iron Pipe", "Constructor", ("Iron Ingot", ), minimal_belt_speed=2), + Recipe("Molded Steel Pipe", "Foundry", ("Steel Ingot", "Concrete"))), + "Steel Beam": ( + Recipe("Steel Beam", "Constructor", ("Steel Ingot", ), handcraftable=True), + Recipe("Aluminum Beam", "Constructor", ("Aluminum Ingot", ), minimal_phase=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", )), + Recipe("Plastic", "Refinery", ("Crude Oil", ), additional_outputs=("Plastic", )), + Recipe("Rubber", "Refinery", ("Crude Oil", ), additional_outputs=("Rubber", )), + Recipe("Polymer Resin", "Refinery", ("Crude Oil", ), additional_outputs=("Polymer Resin", ), minimal_belt_speed=3)), + "Polymer Resin": ( + Recipe("Polymer Resin", "Refinery", ("Crude Oil", ), additional_outputs=("Heavy Oil Residue", ), minimal_belt_speed=2), + Recipe("Fuel", "Refinery", ("Crude Oil", ), additional_outputs=("Fuel", )), + 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"), minimal_phase=2), + Recipe("Residual Fuel", "Refinery", ("Heavy Oil Residue", ))), + "Concrete": ( + Recipe("Concrete", "Constructor", ("Limestone", )), + Recipe("Fine Concrete", "Assembler", ("Limestone", "Silica")), + Recipe("Rubber Concrete", "Assembler", ("Limestone", "Rubber")), + 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, minimal_phase=2), + Recipe("Cheap Silica", "Assembler", ("Raw Quartz", "Limestone")), + Recipe("Distilled Silica", "Blender", ("Dissolved Silica", "Limestone", "Water"), additional_outputs=("Water", ), minimal_phase=2)), + "Dissolved Silica": ( + Recipe("Quartz Purification", "Refinery", ("Raw Quartz", "Nitric Acid"), additional_outputs=("Quartz Crystal", ), minimal_belt_speed=2, minimal_phase=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, minimal_phase=2)), + "Iron Ingot": ( + Recipe("Iron Ingot", "Smelter", ("Iron Ore", )), + Recipe("Pure Iron Ingot", "Refinery", ("Iron Ore", "Water"), minimal_belt_speed=2), + Recipe("Iron Alloy Ingot", "Foundry", ("Iron Ore", "Copper Ore")), + Recipe("Basic Iron Ingot", "Foundry", ("Iron Ore", "Limestone")), + Recipe("Leached Iron ingot", "Refinery", ("Iron Ore", "Sulfuric Acid"), minimal_belt_speed=2)), + "Steel Ingot": ( + Recipe("Steel Ingot", "Foundry", ("Iron Ore", "Coal"), handcraftable=True), + Recipe("Coke Steel Ingot", "Foundry", ("Iron Ore", "Petroleum Coke"), minimal_belt_speed=2), + Recipe("Compacted Steel Ingot", "Foundry", ("Iron Ore", "Compacted Coal")), + Recipe("Solid Steel Ingot", "Foundry", ("Iron Ingot", "Coal"))), + "Copper Ingot": ( + Recipe("Copper Ingot", "Smelter", ("Copper Ore", )), + Recipe("Copper Alloy Ingot", "Foundry", ("Copper Ore", "Iron Ore"), minimal_belt_speed=2), + Recipe("Pure Copper Ingot", "Refinery", ("Copper Ore", "Water")), + Recipe("Leached Copper Ingot", "Refinery", ("Copper Ore", "Sulfuric Acid"), minimal_belt_speed=2), + Recipe("Tempered Copper Ingot", "Foundry", ("Copper Ore", "Petroleum Coke"))), + "Caterium Ingot": ( + Recipe("Caterium Ingot", "Smelter", ("Caterium Ore", ), handcraftable=True), + Recipe("Pure Caterium Ingot", "Refinery", ("Caterium Ore", "Water")), + Recipe("Leached Caterium Ingot", "Refinery", ("Caterium Ore", "Sulfuric Acid")), + Recipe("Tempered Caterium Ingot", "Foundry", ("Caterium Ore", "Petroleum Coke"))), + "Petroleum Coke": ( + Recipe("Petroleum Coke", "Refinery", ("Heavy Oil Residue", ), minimal_belt_speed=2), ), + "Compacted Coal": ( + Recipe("Compacted Coal", "Assembler", ("Coal", "Sulfur")), ), + "Motor": ( + Recipe("Motor", "Assembler", ("Rotor", "Stator"), handcraftable=True), + Recipe("Rigor Motor", "Manufacturer", ("Rotor", "Stator", "Crystal Oscillator")), + Recipe("Electric Motor", "Assembler", ("Electromagnetic Control Rod", "Rotor"))), + "Modular Frame": ( + Recipe("Modular Frame", "Assembler", ("Reinforced Iron Plate", "Iron Rod"), handcraftable=True), + Recipe("Bolted Frame", "Assembler", ("Reinforced Iron Plate", "Screw"), minimal_belt_speed=3), + Recipe("Steeled Frame", "Assembler", ("Reinforced Iron Plate", "Steel Pipe"))), + "Heavy Modular Frame": ( + Recipe("Heavy Modular Frame", "Manufacturer", ("Modular Frame", "Steel Pipe", "Encased Industrial Beam", "Screw"), minimal_belt_speed=3, handcraftable=True), + Recipe("Heavy Flexible Frame", "Manufacturer", ("Modular Frame", "Encased Industrial Beam", "Rubber", "Screw"), minimal_belt_speed=4), + Recipe("Heavy Encased Frame", "Manufacturer", ("Modular Frame", "Encased Industrial Beam", "Steel Pipe", "Concrete"))), + "Encased Industrial Beam": ( + Recipe("Encased Industrial Beam", "Assembler", ("Steel Beam", "Concrete"), handcraftable=True), + Recipe("Encased Industrial Pipe", "Assembler", ("Steel Pipe", "Concrete"))), + "Computer": ( + Recipe("Computer", "Manufacturer", ("Circuit Board", "Cable", "Plastic"), minimal_belt_speed=3, handcraftable=True), + Recipe("Crystal Computer", "Assembler", ("Circuit Board", "Crystal Oscillator")), + Recipe("Caterium Computer", "Manufacturer", ("Circuit Board", "Quickwire", "Rubber"), minimal_belt_speed=2)), + "Circuit Board": ( + Recipe("Circuit Board", "Assembler", ("Copper Sheet", "Plastic"), handcraftable=True), + Recipe("Electrode Circuit Board", "Assembler", ("Rubber", "Petroleum Coke")), + Recipe("Silicon Circuit Board", "Assembler", ("Copper Sheet", "Silica")), + Recipe("Caterium Circuit Board", "Assembler", ("Plastic", "Quickwire"))), + "Crystal Oscillator": ( + Recipe("Crystal Oscillator", "Manufacturer", ("Quartz Crystal", "Cable", "Reinforced Iron Plate"), handcraftable=True), + Recipe("Insulated Crystal Oscillator", "Manufacturer", ("Quartz Crystal", "Rubber", "AI Limiter"))), + "AI Limiter": ( + Recipe("AI Limiter", "Assembler", ("Copper Sheet", "Quickwire"), minimal_belt_speed=2, handcraftable=True), + Recipe("Plastic AI Limiter", "Assembler", ("Quickwire", "Plastic"), minimal_belt_speed=2)), + "Electromagnetic Control Rod": ( + Recipe("Electromagnetic Control Rod", "Assembler", ("Stator", "AI Limiter"), handcraftable=True), + Recipe("Electromagnetic Connection Rod", "Assembler", ("Stator", "High-Speed Connector"))), + "High-Speed Connector": ( + Recipe("High-Speed Connector", "Manufacturer", ("Quickwire", "Cable", "Circuit Board"), minimal_belt_speed=3, handcraftable=True), + Recipe("Silicon High-Speed Connector", "Manufacturer", ("Quickwire", "Silica", "Circuit Board"), minimal_belt_speed=2)), + "Smart Plating": ( + 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"), minimal_phase=2), + Recipe("Flexible Framework", "Manufacturer", ("Modular Frame", "Steel Beam", "Rubber"), minimal_phase=2)), + "Automated Wiring": ( + Recipe("Automated Wiring", "Assembler", ("Stator", "Cable"), minimal_phase=2), + Recipe("Automated Speed Wiring", "Manufacturer", ("Stator", "Wire", "High-Speed Connector"), minimal_belt_speed=2, minimal_phase=2)), + "Modular Engine": ( + Recipe("Modular Engine", "Manufacturer", ("Motor", "Rubber", "Smart Plating"), minimal_phase=3), ), + "Adaptive Control Unit": ( + Recipe("Adaptive Control Unit", "Manufacturer", ("Automated Wiring", "Circuit Board", "Heavy Modular Frame", "Computer"), minimal_phase=3), ), + "Portable Miner": ( + Recipe("Portable Miner", "Equipment Workshop", ("Iron Rod", "Iron Plate"), handcraftable=True, minimal_belt_speed=0, implicitly_unlocked=True), + Recipe("Automated Miner", "Assembler", ("Steel Pipe", "Iron Plate")), ), + "Alumina Solution": ( + Recipe("Alumina Solution", "Refinery", ("Bauxite", "Water"), additional_outputs=("Silica", ), minimal_belt_speed=2, minimal_phase=2), + Recipe("Sloppy Alumina", "Refinery", ("Bauxite", "Water"), minimal_belt_speed=3, minimal_phase=2)), + "Aluminum Scrap": ( + Recipe("Aluminum Scrap", "Refinery", ("Alumina Solution", "Coal"), additional_outputs=("Water", ), minimal_belt_speed=4, minimal_phase=2), + Recipe("Electrode Aluminum Scrap", "Refinery", ("Alumina Solution", "Petroleum Coke"), additional_outputs=("Water", ), minimal_belt_speed=4, minimal_phase=2), + Recipe("Instant Scrap", "Blender", ("Bauxite", "Coal", "Sulfuric Acid", "Water"), additional_outputs=("Water", ), minimal_belt_speed=3, minimal_phase=2)), + "Aluminum Ingot": ( + Recipe("Aluminum Ingot", "Foundry", ("Aluminum Scrap", "Silica"), minimal_belt_speed=2, handcraftable=True, minimal_phase=2), + Recipe("Pure Aluminum Ingot", "Smelter", ("Aluminum Scrap", ), minimal_phase=2)), + "Alclad Aluminum Sheet": ( + Recipe("Alclad Aluminum Sheet", "Assembler", ("Aluminum Ingot", "Copper Ingot"), handcraftable=True, minimal_phase=2), ), + "Aluminum Casing": ( + Recipe("Aluminum Casing", "Constructor", ("Alclad Aluminum Sheet", ), handcraftable=True, minimal_phase=2), + Recipe("Alclad Casing", "Assembler", ("Aluminum Ingot", "Copper Ingot"), minimal_phase=2)), + "Heat Sink": ( + Recipe("Heat Sink", "Assembler", ("Alclad Aluminum Sheet", "Silica"), minimal_belt_speed=2, handcraftable=True, minimal_phase=2), + Recipe("Heat Exchanger", "Assembler", ("Aluminum Casing", "Rubber"), minimal_belt_speed=3, minimal_phase=2)), + "Nitric Acid": ( + Recipe("Nitric Acid", "Blender", ("Nitrogen Gas", "Water", "Iron Plate"), minimal_phase=2), ), + "Fused Modular Frame": ( + Recipe("Fused Modular Frame", "Blender", ("Heavy Modular Frame", "Aluminum Casing", "Nitrogen Gas"), minimal_belt_speed=2, minimal_phase=2), + Recipe("Heat-Fused Frame", "Blender", ("Heavy Modular Frame", "Aluminum Ingot", "Nitric Acid", "Fuel"), minimal_belt_speed=3, minimal_phase=2)), + "Radio Control Unit": ( + Recipe("Radio Control Unit", "Manufacturer", ("Aluminum Casing", "Crystal Oscillator", "Computer"), handcraftable=True, minimal_phase=2), + Recipe("Radio Connection Unit", "Manufacturer", ("Heat Sink", "High-Speed Connector", "Quartz Crystal"), minimal_phase=2), + Recipe("Radio Control System", "Manufacturer", ("Crystal Oscillator", "Circuit Board", "Aluminum Casing", "Rubber"), minimal_belt_speed=2, minimal_phase=2)), + "Pressure Conversion Cube": ( + Recipe("Pressure Conversion Cube", "Assembler", ("Fused Modular Frame", "Radio Control Unit"), handcraftable=True, minimal_phase=2), ), + "Cooling System": ( + Recipe("Cooling System", "Blender", ("Heat Sink", "Rubber", "Water", "Nitrogen Gas"), minimal_phase=2), + Recipe("Cooling Device", "Blender", ("Heat Sink", "Motor", "Nitrogen Gas"), minimal_phase=2)), + "Turbo Motor": ( + Recipe("Turbo Motor", "Manufacturer", ("Cooling System", "Radio Control Unit", "Motor", "Rubber"), handcraftable=True, minimal_phase=2), + Recipe("Turbo Electric Motor", "Manufacturer", ("Motor", "Radio Control Unit", "Electromagnetic Control Rod", "Rotor"), minimal_phase=2), + Recipe("Turbo Pressure Motor", "Manufacturer", ("Motor", "Pressure Conversion Cube", "Packaged Nitrogen Gas", "Stator"), minimal_phase=2)), + "Battery": ( + Recipe("Battery", "Blender", ("Sulfuric Acid", "Alumina Solution", "Aluminum Casing"), additional_outputs=("Water", ), minimal_phase=2), + Recipe("Classic Battery", "Manufacturer", ("Sulfur", "Alclad Aluminum Sheet", "Plastic", "Wire"), minimal_belt_speed=2, minimal_phase=2)), + "Supercomputer": ( + Recipe("Supercomputer", "Manufacturer", ("Computer", "AI Limiter", "High-Speed Connector", "Plastic"), handcraftable=True, minimal_phase=2), + Recipe("OC Supercomputer", "Assembler", ("Radio Control Unit", "Cooling System"), minimal_phase=2), + Recipe("Super-State Computer", "Manufacturer", ("Computer", "Electromagnetic Control Rod", "Battery", "Wire"), minimal_phase=2)), + "Sulfuric Acid": ( + Recipe("Sulfuric Acid", "Refinery", ("Sulfur", "Water")), ), + "Encased Uranium Cell": ( + Recipe("Encased Uranium Cell", "Blender", ("Uranium", "Concrete", "Sulfuric Acid"), additional_outputs=("Sulfuric Acid", )), + Recipe("Infused Uranium Cell", "Manufacturer", ("Uranium", "Silica", "Sulfur", "Quickwire"), minimal_belt_speed=2)), + "Uranium Fuel Rod": ( + Recipe("Uranium Fuel Rod", "Manufacturer", ("Encased Uranium Cell", "Encased Industrial Beam", "Electromagnetic Control Rod")), + Recipe("Uranium Fuel Unit", "Manufacturer", ("Encased Uranium Cell", "Electromagnetic Control Rod", "Crystal Oscillator", "Rotor"))), + "Non-fissile Uranium": ( + Recipe("Non-fissile Uranium", "Blender", ("Uranium Waste", "Silica", "Nitric Acid", "Sulfuric Acid"), additional_outputs=("Water", )), + Recipe("Fertile Uranium", "Blender", ("Uranium", "Uranium Waste", "Nitric Acid", "Sulfuric Acid"), additional_outputs=("Water", ), minimal_belt_speed=2)), + "Plutonium Pellet": ( + Recipe("Plutonium Pellet", "Particle Accelerator", ("Non-fissile Uranium", "Uranium Waste"), minimal_belt_speed=2), ), + "Encased Plutonium Cell": ( + Recipe("Encased Plutonium Cell", "Assembler", ("Plutonium Pellet", "Concrete")), + Recipe("Instant Plutonium Cell", "Particle Accelerator", ("Non-fissile Uranium", "Aluminum Casing"), minimal_belt_speed=2)), + "Plutonium Fuel Rod": ( + Recipe("Plutonium Fuel Rod", "Manufacturer", ("Encased Plutonium Cell", "Steel Beam", "Electromagnetic Control Rod", "Heat Sink")), + Recipe("Plutonium Fuel Unit", "Assembler", ("Encased Plutonium Cell", "Pressure Conversion Cube"))), + "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, minimal_phase=2), ), + "Hazmat Suit": ( + Recipe("Hazmat Suit", "Equipment Workshop", ("Rubber", "Plastic", "Fabric", "Alclad Aluminum Sheet"), handcraftable=True, minimal_phase=2), ), + "Assembly Director System": ( + Recipe("Assembly Director System", "Assembler", ("Adaptive Control Unit", "Supercomputer"), minimal_phase=4), ), + "Magnetic Field Generator": ( + Recipe("Magnetic Field Generator", "Assembler", ("Versatile Framework", "Electromagnetic Control Rod"), minimal_phase=4), ), + "Copper Powder": ( + Recipe("Copper Powder", "Constructor", ("Copper Ingot", ), handcraftable=True), ), + "Nuclear Pasta": ( + Recipe("Nuclear Pasta", "Particle Accelerator", ("Copper Powder", "Pressure Conversion Cube"), minimal_phase=2), ), + "Thermal Propulsion Rocket": ( + Recipe("Thermal Propulsion Rocket", "Manufacturer", ("Modular Engine", "Turbo Motor", "Cooling System", "Fused Modular Frame"), minimal_phase=4), ), + "Alien Protein": ( + Recipe("Hatcher Protein", "Constructor", ("Hatcher Remains", ), handcraftable=True), + Recipe("Hog Protein", "Constructor", ("Hog Remains", ), handcraftable=True), + Recipe("Spitter Protein", "Constructor", ("Plasma Spitter Remains", ), handcraftable=True), + Recipe("Stinger Protein", "Constructor", ("Stinger Remains", ), handcraftable=True)), + "Biomass": ( + 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=4, handcraftable=True)), + "Fabric": ( + Recipe("Fabric", "Assembler", ("Biomass", "Mycelia"), handcraftable=True, minimal_belt_speed=2), + Recipe("Polyester Fabric", "Refinery", ("Polymer Resin", "Water"))), + "Solid Biofuel": ( + Recipe("Solid Biofuel", "Constructor", ("Biomass", ), minimal_belt_speed=2, handcraftable=True), ), + "Liquid Biofuel": ( + Recipe("Liquid Biofuel", "Refinery", ("Solid Biofuel", "Water"), minimal_belt_speed=2), ), + "Empty Canister": ( + Recipe("Empty Canister", "Constructor", ("Plastic", ), handcraftable=True), + 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, minimal_phase=2), ), + "Packaged Alumina Solution": ( + Recipe("Packaged Alumina Solution", "Packager", ("Alumina Solution", "Empty Canister"), minimal_belt_speed=2), ), + "Packaged Fuel": ( + Recipe("Packaged Fuel", "Packager", ("Fuel", "Empty Canister")), + Recipe("Diluted Packaged Fuel", "Refinery", ("Heavy Oil Residue", "Packaged Water"))), + "Packaged Heavy Oil Residue": ( + Recipe("Packaged Heavy Oil Residue", "Packager", ("Heavy Oil Residue", "Empty Canister")), ), + "Packaged Liquid Biofuel": ( + Recipe("Packaged Liquid Biofuel", "Packager", ("Liquid Biofuel", "Empty Canister")), ), + "Packaged Nitric Acid": ( + Recipe("Packaged Nitric Acid", "Packager", ("Nitric Acid", "Empty Fluid Tank")), ), + "Packaged Nitrogen Gas": ( + Recipe("Packaged Nitrogen Gas", "Packager", ("Nitrogen Gas", "Empty Fluid Tank")), ), + "Packaged Oil": ( + Recipe("Packaged Oil", "Packager", ("Crude Oil", "Empty Canister")), ), + "Packaged Sulfuric Acid": ( + Recipe("Packaged Sulfuric Acid", "Packager", ("Sulfuric Acid", "Empty Canister")), ), + "Packaged Turbofuel": ( + Recipe("Packaged Turbofuel", "Packager", ("Turbofuel", "Empty Canister")), ), + "Packaged Water": ( + 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"), minimal_phase=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", "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")), ), + "Rifle Ammo": ( + Recipe("Rifle Ammo", "Assembler", ("Copper Sheet", "Smokeless Powder"), handcraftable=True, minimal_belt_speed=2), ), + "Iron Rebar": ( + Recipe("Iron Rebar", "Constructor", ("Iron Rod", ), handcraftable=True), ), + "Nobelisk": ( + Recipe("Nobelisk", "Assembler", ("Black Powder", "Steel Pipe"), handcraftable=True), ), + "Power Shard": ( + 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_phase=4)), # 1.0 + "Object Scanner": ( + Recipe("Object Scanner", "Equipment Workshop", ("Reinforced Iron Plate", "Wire", "Screw"), handcraftable=True), ), + "Xeno-Zapper": ( + Recipe("Xeno-Zapper", "Equipment Workshop", ("Iron Rod", "Reinforced Iron Plate", "Cable", "Wire"), handcraftable=True, implicitly_unlocked=True), ), + +# 1.0 + "Rocket Fuel": ( + Recipe("Rocket Fuel", "Blender", ("Turbofuel", "Nitric Acid"), additional_outputs=("Compacted Coal", ), minimal_phase=2), + Recipe("Nitro Rocket Fuel", "Blender", ("Fuel", "Nitrogen Gas", "Sulfur", "Coal"), minimal_belt_speed=2, additional_outputs=("Compacted Coal", ), minimal_phase=2)), + "Ionized Fuel": ( + Recipe("Ionized Fuel", "Refinery", ("Rocket Fuel", "Power Shard"), additional_outputs=("Compacted Coal", )), + Recipe("Dark-Ion Fuel", "Blender", ("Packaged Rocket Fuel", "Dark Matter Crystal"), minimal_belt_speed=3, additional_outputs=("Compacted Coal", ), minimal_phase=4)), + "Packaged Rocket Fuel": ( + Recipe("Packaged Rocket Fuel", "Packager", ("Rocket Fuel", "Empty Fluid Tank")), ), + "Packaged Ionized Fuel": ( + Recipe("Packaged Ionized Fuel", "Packager", ("Ionized Fuel", "Empty Fluid Tank")), ), + "Diamonds": ( + Recipe("Diamonds", "Particle Accelerator", ("Coal", ), minimal_belt_speed=5), + Recipe("Cloudy Diamonds", "Particle Accelerator", ("Coal", "Limestone"), minimal_belt_speed=4), + Recipe("Oil-Based Diamonds", "Particle Accelerator", ("Crude Oil", )), + Recipe("Petroleum Diamonds", "Particle Accelerator", ("Petroleum Coke", ), minimal_belt_speed=5), + Recipe("Pink Diamonds", "Converter", ("Coal", "Quartz Crystal"), minimal_belt_speed=2), + Recipe("Turbo Diamonds", "Particle Accelerator", ("Coal", "Packaged Turbofuel"), minimal_belt_speed=5)), + "Time Crystal": ( + Recipe("Time Crystal", "Converter", ("Diamonds", )), ), + "Ficsite Ingot": ( + Recipe("Ficsite Ingot (Aluminum)", "Converter", ("Reanimated SAM", "Aluminum Ingot"), minimal_belt_speed=2), + Recipe("Ficsite Ingot (Caterium)", "Converter", ("Reanimated SAM", "Caterium Ingot")), + Recipe("Ficsite Ingot (Iron)", "Converter", ("Reanimated SAM", "Iron Ingot"), minimal_belt_speed=3)), + "Ficsite Trigon": ( + Recipe("Ficsite Trigon", "Constructor", ("Ficsite Ingot", ), handcraftable=True), ), + "SAM": ( + Recipe("SAM", "Miner Mk.1", handcraftable=True, implicitly_unlocked=True), ), + "Reanimated SAM": ( + Recipe("Reanimated SAM", "Constructor", ("SAM", ), handcraftable=True, minimal_belt_speed=2), ), + "SAM Fluctuator": ( + Recipe("SAM Fluctuator", "Manufacturer", ("Reanimated SAM", "Steel Pipe", "Wire"), handcraftable=True), ), + "Excited Photonic Matter": ( + Recipe("Excited Photonic Matter", "Converter", implicitly_unlocked=True), ), + "Dark Matter Crystal": ( + Recipe("Dark Matter Crystal", "Particle Accelerator", ("Diamonds", ), additional_outputs=("Dark Matter Residue", )), + Recipe("Dark Matter Crystallization", "Particle Accelerator", additional_outputs=("Dark Matter Residue", )), + Recipe("Dark Matter Trap", "Particle Accelerator", ("Time Crystal", ), additional_outputs=("Dark Matter Residue", ))), + "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"), minimal_phase=5), ), + "Ballistic Warp Drive": ( + Recipe("Ballistic Warp Drive", "Manufacturer", ("Thermal Propulsion Rocket", "Singularity Cell", "Superposition Oscillator", "Dark Matter Crystal"), minimal_phase=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": ( + # Recipe("Ficsonium", "Particle Accelerator", ("Plutonium Waste", "Singularity Cell"), additional_outputs=("Ficsonium", )), + Recipe("Dark Matter Crystal", "Particle Accelerator", ("Diamonds", ), additional_outputs=("Dark Matter Crystal", )), + Recipe("Dark Matter Crystallization", "Particle Accelerator", additional_outputs=("Dark Matter Crystal", )), + Recipe("Dark Matter Trap", "Particle Accelerator", ("Time Crystal", ), additional_outputs=("Dark Matter Crystal", )), + Recipe("Dark Matter Residue", "Converter", ("Reanimated SAM", ))), + "Superposition Oscillator": ( + Recipe("Superposition Oscillator", "Quantum Encoder", ("Dark Matter Residue", "Excited Photonic Matter", "Dark Matter Crystal", "Crystal Oscillator", "Alclad Aluminum Sheet")), ), + "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"), minimal_phase=5), ), + ### +# 1.0 + # For exclusion logic + "Hoverpack": ( + Recipe("Hoverpack", "Equipment Workshop", ("Motor", "Heavy Modular Frame", "Computer", "Alclad Aluminum Sheet")), ), + "Turbo Rifle Ammo": ( + Recipe("Turbo Rifle Ammo", "Blender", ("Rifle Ammo", "Aluminum Casing", "Turbofuel"), minimal_belt_speed=3), + Recipe("Turbo Rifle Ammo (Packaged)", "Manufacturer", ("Rifle Ammo", "Aluminum Casing", "Packaged Turbofuel"), minimal_belt_speed=2, minimal_phase=2)), + "Homing Rifle Ammo": ( + Recipe("Homing Rifle Ammo", "Assembler", ("Rifle Ammo", "High-Speed Connector")), ), + ### + } + + buildings: dict[str, Building] = { + "Constructor": Building("Constructor", ("Reinforced Iron Plate", "Cable"), PowerInfrastructureLevel.Basic), + "Assembler": Building("Assembler", ("Reinforced Iron Plate", "Iron Rod", "Cable"), PowerInfrastructureLevel.Basic), # Simplified , used ("Reinforced Iron Plate", "Rotor", "Cable") + "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.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")), + "Fuel Generator": Building("Fuel Generator", ("Computer", "Heavy Modular Frame", "Motor", "Rubber", "Quickwire")), + "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, 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), + "Foundry": Building("Foundry", ("Reinforced Iron Plate", "Iron Rod", "Concrete"), PowerInfrastructureLevel.Basic), # Simplified, used ("Modular Frame", "Rotor", "Concrete") + "Resource Well Pressurizer": Building("Resource Well Pressurizer", ("Steel Pipe", "Heavy Modular Frame", "Motor", "Reinforced Iron Plate", "Copper Sheet", "Steel Beam"), PowerInfrastructureLevel.Advanced), # Simplified, used ("Radio Control Unit", "Heavy Modular Frame", "Motor", "Alclad Aluminum Sheet", "Rubber", "Steel Beam", "Aluminum Casing") + "Equipment Workshop": Building("Equipment Workshop", ("Iron Plate", "Iron Rod"), implicitly_unlocked=True), + "AWESOME Sink": Building("AWESOME Sink", ("Reinforced Iron Plate", "Cable", "Concrete"), can_produce=False), + "AWESOME Shop": Building("AWESOME Shop", ("Screw", "Iron Plate", "Cable"), can_produce=False), + "MAM": Building("MAM", ("Reinforced Iron Plate", "Wire", "Cable"), can_produce=False), + "Pipes Mk.1": Building("Pipes Mk.1", ("Copper Sheet", "Iron Plate", "Concrete"), can_produce=False), + "Pipes Mk.2": Building("Pipes Mk.2", ("Copper Sheet", "Plastic", "Iron Plate", "Concrete"), can_produce=False), + "Pipeline Pump Mk.1": Building("Pipeline Pump Mk.1", ("Copper Sheet", "Rotor"), can_produce=False), + "Pipeline Pump Mk.2": Building("Pipeline Pump Mk.2", ("Motor", "Encased Industrial Beam", "Plastic"), can_produce=False), + "Conveyor Merger": Building("Conveyor Merger", ("Iron Plate", "Iron Rod"), can_produce=False), + "Conveyor Splitter": Building("Conveyor Splitter", ("Iron Plate", "Cable"), can_produce=False), + "Conveyor Mk.1": Building("Conveyor Mk.1", ("Iron Plate", "Iron Rod", "Concrete"), can_produce=False, implicitly_unlocked=True), + "Conveyor Mk.2": Building("Conveyor Mk.2", ("Reinforced Iron Plate", "Iron Plate", "Iron Rod", "Concrete"), can_produce=False), + "Conveyor Mk.3": Building("Conveyor Mk.3", ("Steel Beam", "Iron Plate", "Iron Rod", "Concrete"), can_produce=False), + "Conveyor Mk.4": Building("Conveyor Mk.4", ("Encased Industrial Beam", "Iron Plate", "Iron Rod", "Concrete"), can_produce=False), + "Conveyor Mk.5": Building("Conveyor Mk.5", ("Alclad Aluminum Sheet", "Iron Plate", "Iron Rod", "Concrete"), can_produce=False), + "Conveyor Mk.6": Building("Conveyor Mk.6", ("Ficsite Trigon", "Time Crystal", "Iron Plate", "Iron Rod", "Concrete"), can_produce=False), + "Power Pole Mk.1": Building("Power Pole Mk.1", ("Iron Plate", "Iron Rod", "Concrete"), can_produce=False, implicitly_unlocked=True), + # higher level power poles arent in logic (yet) + # "Power Pole Mk.2": Building("Power Pole Mk.2", ("Quickwire", "Iron Rod", "Concrete"), False), + # "Power Pole Mk.3": Building("Power Pole Mk.3", ("High-Speed Connector", "Steel Pipe", "Rubber"), False), + "Power Storage": Building("Power Storage", ("Wire", "Modular Frame", "Stator"), can_produce=False), + "Foundation": Building("Foundation", ("Iron Plate", "Concrete"), can_produce=False), + "Walls Orange": Building("Walls Orange", ("Iron Plate", "Concrete"), can_produce=False), + "Space Elevator": Building("Space Elevator", ("Concrete", "Iron Plate", "Iron Rod", "Wire"), can_produce=False), + +# 1.0 + "Converter": Building("Converter", ("Fused Modular Frame", "Cooling System", "Radio Control Unit", "SAM Fluctuator"), PowerInfrastructureLevel.Complex), + "Quantum Encoder": Building("Quantum Encoder", ("Turbo Motor", "Supercomputer", "Cooling System", "Time Crystal", "Ficsite Trigon"), PowerInfrastructureLevel.Complex), + "Alien Power Augmenter": Building("Alien Power Augmenter", ("SAM Fluctuator", "Cable", "Encased Industrial Beam", "Motor", "Computer")), +# 1.0 + + # For exclusion logic + "Portal": Building("Portal", ("Turbo Motor", "Radio Control Unit", "Superposition Oscillator", "SAM Fluctuator", "Ficsite Trigon", "Singularity Cell"), PowerInfrastructureLevel.Advanced), + ### + } + + 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), + ), + PowerInfrastructureLevel.Automated: ( + Recipe("Biomass Power (Solid Biofuel)", "Biomass Burner", ("Solid Biofuel", ), implicitly_unlocked=True), + # Recipe("Coal Generator Power (Petroleum Coke)", "Coal Generator", ("Petroleum Coke", "Water"), implicitly_unlocked=True), + Recipe("Coal Generator Power (Coal)", "Coal Generator", ("Coal", "Water"), implicitly_unlocked=True), + ), + PowerInfrastructureLevel.Advanced: ( + Recipe("Coal Generator Power (Compacted Coal)", "Coal Generator", ("Compacted Coal", "Water"), implicitly_unlocked=True), + Recipe("Geothermal Generator Power", "Geothermal Generator", implicitly_unlocked=True), + Recipe("Fuel Generator Power (Liquid Biofuel)", "Fuel Generator", ("Liquid Biofuel", ), implicitly_unlocked=True), + Recipe("Fuel Generator Power (Fuel)", "Fuel Generator", ("Fuel", ), implicitly_unlocked=True), + Recipe("Alien Power Augmenter Power", "Alien Power Augmenter", implicitly_unlocked=True), + ), + PowerInfrastructureLevel.Complex: ( + Recipe("Fuel Generator Power (Turbofuel)", "Fuel Generator", ("Turbofuel", ), implicitly_unlocked=True), + # Recipe("Fuel Generator Power (Rocket Fuel)", "Fuel Generator", ("Rocket Fuel", ), implicitly_unlocked=True), + # Recipe("Fuel Generator Power (Ionized Fuel)", "Fuel Generator", ("Ionized Fuel", ), implicitly_unlocked=True), + Recipe("Nuclear Power Plant Power (Uranium)", "Nuclear Power Plant", ("Uranium Fuel Rod", "Water"), implicitly_unlocked=True), + # Recipe("Nuclear Power Plant Power (Plutonium)", "Nuclear Power Plant", ("Plutonium Fuel Rod", "Water"), implicitly_unlocked=True), + # Recipe("Nuclear Power Plant Power (Ficsonium)", "Nuclear Power Plant", ("Ficsonium Fuel Rod", "Water"), implicitly_unlocked=True), + # Recipe("Alien Power Augmenter Power (Alien Power Matrix)", "Alien Power Augmenter", ("Alien Power Matrix"), implicitly_unlocked=True), + ) + } + + slots_per_milestone: int = 8 + + 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) + {"Iron Plate": 150, "Iron Rod": 150, "Wire": 300, }, # Schematic: Logistics (Schematic_1-2_C) + {"Wire": 300, "Screw": 300, "Iron Plate": 100, }, # Schematic: Field Research (Schematic_1-3_C) + {"Wire": 100, "Screw": 200, "Concrete": 200, }, # Schematic: Archipelago Additional Tier1 (Schem_ApExtraTier1_C) + ), + ( # Tier 2 + {"Cable": 200, "Iron Rod": 200, "Screw": 500, "Iron Plate": 300, }, # Schematic: Part Assembly (Schematic_2-1_C) + {"Screw": 500, "Cable": 100, "Concrete": 100, }, # Schematic: Obstacle Clearing (Schematic_2-2_C) + {"Rotor": 50, "Iron Plate": 300, "Cable": 150, }, # Schematic: Jump Pads (Schematic_2-3_C) + {"Concrete": 400, "Wire": 500, "Iron Rod": 200, "Iron Plate": 200, }, # Schematic: Resource Sink Bonus Program (Schematic_2-5_C) + {"Reinforced Iron Plate": 50, "Concrete": 200, "Iron Rod": 300, "Iron Plate": 300, }, # Schematic: Logistics Mk.2 (Schematic_3-2_C) + ), + ( # Tier 3 + {"Reinforced Iron Plate": 150, "Rotor": 50, "Cable": 500, }, # Schematic: Coal Power (Schematic_3-1_C) + {"Modular Frame": 25, "Rotor": 100, "Cable": 100, "Iron Plate": 400, }, # Schematic: Vehicular Transport (Schematic_3-3_C) + {"Modular Frame": 50, "Rotor": 150, "Concrete": 500, "Wire": 1000, }, # Schematic: Basic Steel Production (Schematic_3-4_C) + {"Reinforced Iron Plate": 100, "Iron Rod": 600, "Wire": 1500, }, # Schematic: Improved Melee Combat (Schematic_4-2_C) + ), + ( # Tier 4 + {"Modular Frame": 100, "Steel Beam": 200, "Cable": 500, "Concrete": 1000, }, # Schematic: FICSIT Blueprints (Schematic_4-5_C) + {"Steel Beam": 200, "Steel Pipe": 200, "Reinforced Iron Plate": 400, }, # Schematic: Logistics Mk.3 (Schematic_5-3_C) + {"Steel Pipe": 100, "Modular Frame": 100, "Rotor": 200, "Concrete": 500, }, # Schematic: Advanced Steel Production (Schematic_4-1_C) + {"Encased Industrial Beam": 50, "Steel Beam": 100, "Modular Frame": 200, "Wire": 2000, }, # Schematic: Expanded Power Infrastructure (Schematic_4-3_C) + {"Copper Sheet": 500, "Steel Pipe": 300, "Encased Industrial Beam": 50, }, # Schematic: Hypertubes (Schematic_4-4_C) + ), + ( # Tier 5 + {"Motor": 50, "Cable": 100, "Iron Plate": 500, }, # Something jetpack + {"Motor": 50, "Encased Industrial Beam": 100, "Steel Pipe": 500, "Copper Sheet": 500, }, # Schematic: Oil Processing (Schematic_5-1_C) + {"Rubber": 200, "Encased Industrial Beam": 300, "Modular Frame": 400, }, + {"Plastic": 200, "Steel Beam": 400, "Copper Sheet": 1000, }, # Schematic: Alternative Fluid Transport (Schematic_5-4_C) + {"Motor": 100, "Encased Industrial Beam": 100, "Plastic": 200, "Rubber": 200, }, # Schematic: Industrial Manufacturing (Schematic_5-2_C) + ), + ( # Tier 6 + {"Motor": 200, "Modular Frame": 200, "Plastic": 400, "Cable": 1000, }, # Schematic: Industrial Manufacturing (Schematic_5-2_C) + {"Motor": 250, "Encased Industrial Beam": 500, "Steel Pipe": 1000, "Steel Beam": 1000, }, # Schematic: Monorail Train Technology (Schematic_6-3_C) + {"Computer": 50, "Steel Pipe": 4000, "Copper Sheet": 1000, }, + {"Heavy Modular Frame": 50, "Plastic": 1000, "Rubber": 1000, }, # Schematic: Pipeline Engineering Mk.2 (Schematic_6-5_C) + {"Heavy Modular Frame": 50, "Computer": 100, "Rubber": 400, "Concrete": 1000, }, + ), + ( # Tier 7 + {"Computer": 100, "Heavy Modular Frame": 100, "Motor": 250, "Rubber": 500, }, # Schematic: Bauxite Refinement (Schematic_7-1_C) + {"Alclad Aluminum Sheet": 100, "Heavy Modular Frame": 100, "Computer": 100, "Motor": 250, }, # Schematic: Hover Pack (Schematic_8-3_C) + {"Alclad Aluminum Sheet": 200, "Encased Industrial Beam": 400, "Reinforced Iron Plate": 600, }, # Schematic: Logistics Mk.5 (Schematic_7-2_C) + {"Gas Filter": 50, "Aluminum Casing": 100, "Quickwire": 500, }, # Schematic: Hazmat Suit (Schematic_7-3_C) + {"Alclad Aluminum Sheet": 200, "Aluminum Casing": 400, "Computer": 200, "Plastic": 1000, }, # Schematic: Aeronautical Engineering (Schematic_7-4_C) + ), + ( # Tier 8 + {"Radio Control Unit": 50, "Alclad Aluminum Sheet": 100, "Aluminum Casing": 200, "Motor": 300, }, # Schematic: Aeronautical Engineering (Schematic_7-4_C) + {"Supercomputer": 50, "Heavy Modular Frame": 200, "Cable": 1000, "Concrete": 2000, }, # Schematic: Nuclear Power (Schematic_8-1_C) + {"Radio Control Unit": 50, "Aluminum Casing": 200, "Alclad Aluminum Sheet": 400, "Wire": 3000, }, # Schematic: Advanced Aluminum Production (Schematic_8-2_C) + {"Fused Modular Frame": 50, "Supercomputer": 100, "Steel Pipe": 1000, }, # Schematic: Leading-edge Production (Schematic_8-4_C) + {"Turbo Motor": 50, "Fused Modular Frame": 100, "Cooling System": 200, "Quickwire": 2500, }, # Schematic: Particle Enrichment (Schematic_8-5_C) + ), + ( # Tier 9 + {"Fused Modular Frame": 100, "Radio Control Unit": 250, "Cooling System": 500, }, + {"Time Crystal": 50, "Ficsite Trigon": 100, "Turbo Motor": 200, "Supercomputer": 400, }, + {"Neural-Quantum Processor": 100, "Time Crystal": 250, "Ficsite Trigon": 500, "Fused Modular Frame": 500, }, + {"Superposition Oscillator": 100, "Turbo Motor": 250, "Radio Control Unit": 500, "SAM Fluctuator": 1000, }, + {"Time Crystal": 250, "Ficsite Trigon": 250, "Alclad Aluminum Sheet": 500, "Iron Plate": 10000, }, + ), + ) + + # Values from /Game/FactoryGame/Schematics/Progression/BP_GamePhaseManager.BP_GamePhaseManager + space_elevator_phases: 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}, + {"Assembly Director System": 4000, "Magnetic Field Generator": 4000, "Nuclear Pasta": 1000, "Thermal Propulsion Rocket": 1000}, + {"Nuclear Pasta": 1000, "Biochemical Sculptor": 1000, "AI Expansion Server": 256, "Ballistic Warp Drive": 200} + ) + + # Do not regenerate as format got changed + # Regenerate via /Script/Engine.Blueprint'/Archipelago/Debug/CC_BuildMamData.CC_BuildMamData' + 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) + MamNode("Expanded Toolbelt", {"Alien DNA Capsule": 5, "Steel Beam": 500, }, depends_on=("Inflated Pocket Dimension", )), # (Research_ACarapace_3_C) + MamNode("Bio-Organic Properties", {"Alien Protein": 5, }, depends_on=("Spitter Research", "Hog Research", "Hatcher Research", "Stinger Research")), # (Research_AO_DNACapsule_C) + MamNode("Stinger Research", {"Stinger Remains": 1, }, depends_on=tuple()), # (Research_AO_Stinger_C) + MamNode("Hatcher Research", {"Hatcher Remains": 1, }, depends_on=tuple()), # (Research_AO_Hatcher_C) + MamNode("Hog Research", {"Hog Remains": 1, }, depends_on=tuple()), # (Research_ACarapace_0_C) + MamNode("Spitter Research", {"Plasma Spitter Remains": 1, }, depends_on=tuple()), # (Research_AOrgans_0_C) + MamNode("Structural Analysis", {"Alien DNA Capsule": 5, "Iron Rod": 100, }, depends_on=("Bio-Organic Properties", )), # (Research_AO_Pre_Rebar_C) + MamNode("Protein Inhaler", {"Alien Protein": 2, "Beryl Nut": 20, "Rotor": 50, }, depends_on=("Bio-Organic Properties", )), # (Research_AOrgans_2_C) + MamNode("The Rebar Gun", {"Rotor": 25, "Reinforced Iron Plate": 50, "Screw": 500, }, depends_on=("Structural Analysis", )), # (Research_ACarapace_2_C) + )), + # 1.0 + "Alien Technology": MamTree(("SAM", "Mercer Sphere", "Somersloop"), ( + MamNode("SAM Analysis", {"SAM": 10, }, depends_on=tuple()), + MamNode("SAM Reanimation", {"SAM": 20, }, depends_on=("SAM Analysis",)), + MamNode("SAM Fluctuator", {"Reanimated SAM": 10, "Steel Pipe": 100, "Wire": 200, }, depends_on=("SAM Reanimation",)), + MamNode("Mercer Sphere Analysis", {"Mercer Sphere": 1, }, depends_on=tuple()), + MamNode("Dimensional Depot", {"Mercer Sphere": 1, "SAM Fluctuator": 11, }, depends_on=("Mercer Sphere Analysis", "SAM Fluctuator")), + MamNode("Manual Depot Uploader", {"Mercer Sphere": 3, "Computer": 17, "SAM Fluctuator": 19, }, depends_on=("Dimensional Depot",)), + MamNode("Depot Expansion (200%)", {"Mercer Sphere": 3, "SAM Fluctuator": 47, }, depends_on=("Dimensional Depot",)), + MamNode("Depot Expansion (300%)", {"Mercer Sphere": 7, "SAM Fluctuator": 103, }, depends_on=("Depot Expansion (200%)",)), + MamNode("Depot Expansion (400%)", {"Mercer Sphere": 13, "SAM Fluctuator": 151, }, depends_on=("Depot Expansion (300%)",)), + MamNode("Depot Expansion (500%)", {"Mercer Sphere": 23, "SAM Fluctuator": 199, }, depends_on=("Depot Expansion (400%)",)), + MamNode("Upload Upgrade: 30/min", {"Mercer Sphere": 3, "SAM Fluctuator": 47, }, depends_on=("Dimensional Depot",)), + MamNode("Upload Upgrade: 60/min", {"Mercer Sphere": 7, "SAM Fluctuator": 103, }, depends_on=("Upload Upgrade: 30/min",)), + MamNode("Upload Upgrade: 120/min", {"Mercer Sphere": 13, "SAM Fluctuator": 151, }, depends_on=("Upload Upgrade: 60/min",)), + MamNode("Upload Upgrade: 240/min", {"Mercer Sphere": 23, "SAM Fluctuator": 199, }, depends_on=("Upload Upgrade: 120/min",)), + MamNode("Somersloop Analysis", {"Somersloop": 1, }, depends_on=tuple()), + 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",), minimal_phase=4), + )), + # 1.0 + "Caterium": MamTree(("Caterium Ore", ), ( # Caterium (BPD_ResearchTree_Caterium_C) + MamNode("Caterium Electronics", {"Quickwire": 100, }, depends_on=("Quickwire", )), # (Research_Caterium_3_C) + MamNode("Bullet Guidance System", {"High-Speed Connector": 10, "Rifle Ammo": 500, }, depends_on=("High-Speed Connector", )), # (Research_Caterium_6_3_C) + MamNode("High-Speed Connector", {"Quickwire": 500, "Plastic": 50, }, depends_on=("Caterium Electronics", )), # (Research_Caterium_5_C) + MamNode("Caterium", {"Caterium Ore": 10, }, depends_on=tuple()), # (Research_Caterium_0_C) + MamNode("Caterium Ingots", {"Caterium Ore": 50, }, depends_on=("Caterium", )), # (Research_Caterium_1_C) + MamNode("Quickwire", {"Caterium Ingot": 50, }, depends_on=("Caterium Ingots", )), # (Research_Caterium_2_C) + MamNode("Power Switch", {"Steel Beam": 100, "AI Limiter": 50, }, depends_on=("AI Limiter", )), # (Research_Caterium_4_1_2_C) + MamNode("Priority Power Switch", {"High-Speed Connector": 25, "Quickwire": 500, }, depends_on=("High-Speed Connector", )), # 1.0 + MamNode("Power Poles Mk.2", {"Quickwire": 300, }, depends_on=("Caterium Electronics", )), # (Research_Caterium_4_2_C) + MamNode("AI Limiter", {"Quickwire": 200, "Copper Sheet": 50, }, depends_on=("Caterium Electronics", )), # (Research_Caterium_4_1_C) + MamNode("Smart Splitter", {"AI Limiter": 10, "Reinforced Iron Plate": 50, }, depends_on=("AI Limiter", )), # (Research_Caterium_4_1_1_C) + MamNode("Programmable Splitter", {"AI Limiter": 100, "Computer": 50, "Heavy Modular Frame": 50, }, depends_on=("AI Limiter", "High-Speed Connector")), # (Research_Caterium_7_1_C) # 1.0 + MamNode("Zipline", {"Quickwire": 100, "Cable": 50, }, depends_on=("Quickwire", )), # (Research_Caterium_2_1_C) + MamNode("Geothermal Generator", {"High-Speed Connector": 100, "Quickwire": 1000, "Motor": 50, }, depends_on=("AI Limiter", "High-Speed Connector")), # (Research_Caterium_7_2_C) # 1.0 + MamNode("Stun Rebar", {"Quickwire": 50, "Iron Rebar": 10, }, depends_on=("Quickwire", )), # (Research_Caterium_3_2_C) + MamNode("Power Poles Mk.3", {"High-Speed Connector": 50, "Steel Pipe": 200, }, depends_on=("Power Poles Mk.2", )), # (Research_Caterium_6_2_C) # 1.0 + )), + "Mycelia": MamTree(("Mycelia", ), ( # Mycelia (BPD_ResearchTree_Mycelia_C) + MamNode("Therapeutic Inhaler", {"Mycelia": 15, "Bacon Agaric": 1, "Alien Protein": 1, }, depends_on=("Medical Properties", )), # (Research_Mycelia_6_C) + MamNode("Expanded Toolbelt", {"Fabric": 50, "Rotor": 100, }, depends_on=("Fabric", )), # (Research_Mycelia_7_C) + MamNode("Mycelia", {"Mycelia": 5, }, depends_on=tuple()), # (Research_Mycelia_1_C) + MamNode("Fabric", {"Mycelia": 25, "Biomass": 100, }, depends_on=("Mycelia", )), # (Research_Mycelia_2_C) + MamNode("Medical Properties", {"Mycelia": 25, "Stator": 10, }, depends_on=("Mycelia", )), # (Research_Mycelia_4_C) + MamNode("Toxic Cellular Modification", {"Nobelisk": 10, "Mycelia": 100, "Biomass": 200, }, depends_on=("Mycelia", )), # (Research_Mycelia_8_C) + MamNode("Vitamin Inhaler", {"Mycelia": 10, "Paleberry": 5, }, depends_on=("Medical Properties", )), # (Research_Mycelia_5_C) + MamNode("Parachute", {"Fabric": 10, "Cable": 50, }, depends_on=("Fabric", )), # (Research_Mycelia_3_C) + MamNode("Synthethic Polyester Fabric", {"Fabric": 25, "Polymer Resin": 100, }, depends_on=("Fabric", )), # (Research_Mycelia_2_1_C) + MamNode("Gas Mask", {"Coal": 10, "Fabric": 50, "Steel Pipe": 50, }, depends_on=("Fabric", )), # 1.0 + )), + "Nutrients": MamTree(("Paleberry", "Beryl Nut", "Bacon Agaric"), ( # Nutrients (BPD_ResearchTree_Nutrients_C) + MamNode("Bacon Agaric", {"Bacon Agaric": 1, }, depends_on=tuple()), # (Research_Nutrients_2_C) + MamNode("Beryl Nut", {"Beryl Nut": 5, }, depends_on=tuple()), # (Research_Nutrients_1_C) + MamNode("Paleberry", {"Paleberry": 2, }, depends_on=tuple()), # (Research_Nutrients_0_C) + MamNode("Nutritional Processor", {"Modular Frame": 25, "Steel Pipe": 50, "Wire": 500, }, depends_on=("Beryl Nut", "Bacon Agaric", "Paleberry")), # (Research_Nutrients_3_C) + MamNode("Nutritional Inhaler", {"Bacon Agaric": 2, "Paleberry": 4, "Beryl Nut": 10, }, depends_on=("Nutritional Processor", )), # (Research_Nutrients_4_C) + )), + "Power Slugs": MamTree(("Blue Power Slug", ), ( # Power Slugs (BPD_ResearchTree_PowerSlugs_C) + MamNode("Slug Scanning", {"Iron Rod": 50, "Wire": 100, "Screw": 200, }, depends_on=("Blue Power Slugs", )), # (Research_PowerSlugs_3_C) + MamNode("Blue Power Slugs", {"Blue Power Slug": 1, }, depends_on=tuple()), # (Research_PowerSlugs_1_C) + 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", ), minimal_phase=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) + MamNode("Quartz Crystals", {"Raw Quartz": 20, }, depends_on=("Quartz", )), # (Research_Quartz_1_1_C) + MamNode("Quartz", {"Raw Quartz": 10, }, depends_on=tuple()), # (Research_Quartz_0_C) + MamNode("Shatter Rebar", {"Quartz Crystal": 30, "Iron Rebar": 150, }, depends_on=("Quartz Crystals", )), # (Research_Quartz_2_1_C) + MamNode("Silica", {"Raw Quartz": 20, }, depends_on=("Quartz", )), # (Research_Quartz_1_2_C) + MamNode("Explosive Resonance Application", {"Crystal Oscillator": 5, "Nobelisk": 100, }, depends_on=("Crystal Oscillator", )), # (Research_Quartz_3_4_C) + MamNode("Blade Runners", {"Silica": 50, "Modular Frame": 10, }, depends_on=("Silica", )), # (Research_Caterium_4_3_C) + MamNode("The Explorer", {"Crystal Oscillator": 10, "Modular Frame": 100, }, depends_on=("Crystal Oscillator", )), # (Research_Quartz_3_1_C) + MamNode("Material Resonance Screening", {"Crystal Oscillator": 15, "Reinforced Iron Plate": 100, }, depends_on=("Crystal Oscillator", )), # (Research_Quartz_PriorityMerger_C) + MamNode("Radio Signal Scanning", {"Crystal Oscillator": 100, "Motor": 100, "Object Scanner": 1, }, depends_on=("Crystal Oscillator", )), # (Research_Quartz_4_1_C) + MamNode("Inflated Pocket Dimension", {"Silica": 200, }, depends_on=("Silica", )), # (Research_Caterium_3_1_C) + MamNode("Radar Technology", {"Crystal Oscillator": 50, "Heavy Modular Frame": 50, "Computer": 50, }, depends_on=("Crystal Oscillator", )), # (Research_Quartz_4_C) # 1.0 + )), + "Sulfur": MamTree(("Sulfur", ), ( # Sulfur (BPD_ResearchTree_Sulfur_C) + MamNode("The Nobelisk Detonator", {"Black Powder": 50, "Steel Pipe": 100, "Cable": 200, }, depends_on=("Black Powder", )), # (Research_Sulfur_3_1_C) + MamNode("Smokeless Powder", {"Black Powder": 100, "Plastic": 50, }, depends_on=("Black Powder", )), # (Research_Sulfur_3_C) + MamNode("Sulfur", {"Sulfur": 10, }, depends_on=tuple()), # (Research_Sulfur_0_C) + MamNode("Inflated Pocket Dimension", {"Smokeless Powder": 50, "Computer": 50, }, depends_on=("Nuclear Deterrent Development", "Turbo Rifle Ammo", "Cluster Nobelisk", "The Rifle")), # (Research_Sulfur_6_C) + MamNode("The Rifle", {"Smokeless Powder": 50, "Motor": 100, "Rubber": 200, }, depends_on=("Smokeless Powder", )), # (Research_Sulfur_4_1_C) + MamNode("Compacted Coal", {"Hard Drive": 1, "Sulfur": 25, "Coal": 25, }, depends_on=("Experimental Power Generation", )), # (Research_Sulfur_CompactedCoal_C) + MamNode("Black Powder", {"Sulfur": 50, "Coal": 25, }, depends_on=("Sulfur", )), # (Research_Sulfur_1_C) + 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", ), minimal_phase=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", ), minimal_phase=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_phase=3), # 1.0 + MamNode("Ionized Fuel", {"Hard Drive": 1, "Power Shard": 100, "Packaged Rocket Fuel": 200, }, depends_on=("Turbo Fuel", ), minimal_phase=4), # 1.0 + )) + } + + 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 + DropPodData(8680, -41777, 13053, "Steel Pipe", 0), # Unlocks with: 7 x Desc_SteelPipe_C + DropPodData(35082, 16211, 22759, "Supercomputer", 0), # Unlocks with: 7 x Desc_ComputerSuper_C + # DropPodData(-3511, 62314, 22109, "Quantum Computer", 0), # Unlocks with: 1 x Desc_ComputerQuantum_C + DropPodData(66652, -13642, 13420, "Encased Industrial Beam", 50), # Unlocks with: 3 x Desc_SteelPlateReinforced_C + DropPodData(55247, -51316, 14363, None, 25), # Unlocks with: (No Item) + DropPodData(-4706, -76301, 13618, "Black Powder", 0), # Unlocks with: 10 x Desc_Gunpowder_C + DropPodData(-40194, 62956, 26261, "Superposition Oscillator", 138), # Unlocks with: 2 x Desc_QuantumOscillator_C + DropPodData(80980, -44100, 8303, "Rotor", 0), # Unlocks with: 3 x Desc_Rotor_C + DropPodData(-56144, -72864, 27668, "Quartz Crystal", 0), # Unlocks with: 2 x Desc_QuartzCrystal_C + DropPodData(-95228, 6970, 25142, "High-Speed Connector", 112), # Unlocks with: 11 x Desc_HighSpeedConnector_C + DropPodData(-89284, -50630, 16019, None, 50), # Unlocks with: (No Item) + DropPodData(-94708, 40337, 19832, "Heat Sink", 138), # Unlocks with: 2 x Desc_AluminumPlateReinforced_C + DropPodData(94267, 47237, 9435, "Motor", 0), # Unlocks with: 1 x Desc_Motor_C + DropPodData(87739, -62975, 13444, None, 30), # Unlocks with: (No Item) + DropPodData(12249, 114177, 26721, "AI Limiter", 267), # Unlocks with: 9 x Desc_CircuitBoardHighSpeed_C + DropPodData(115978, 21424, 15519, None, 0), # Unlocks with: (No Item) + DropPodData(-78236, 90857, 20305, "Radio Control Unit", 0), # Unlocks with: 6 x Desc_ModularFrameLightweight_C + DropPodData(-35359, 116594, 21827, "Turbo Motor", 0), # Unlocks with: 6 x Desc_MotorLightweight_C + DropPodData(111479, -54515, 17081, "Stator", 20), # Unlocks with: 1 x Desc_Stator_C + DropPodData(121061, 45324, 17373, None, 0), # Unlocks with: (No Item) + DropPodData(125497, -34949, 8220, None, 50), # Unlocks with: (No Item) + DropPodData(-26327, -129047, 7780, "Modular Frame", 0), # Unlocks with: 1 x Desc_ModularFrame_C + DropPodData(21373, 132336, 2510, "Motor", 20), # Unlocks with: 2 x Desc_Motor_C + DropPodData(17807, -136922, 13621, "Rotor", 0), # Unlocks with: 1 x Desc_Rotor_C + DropPodData(-118480, 74929, 16995, None, 420), # Unlocks with: (No Item) + DropPodData(94940, 105482, 9860, "Heavy Modular Frame", 0), # Unlocks with: 1 x Desc_ModularFrameHeavy_C + DropPodData(-129115, 60165, 4800, None, 53), # Unlocks with: (No Item) + DropPodData(-142000, 23970, 32660, None, 0), # Unlocks with: (No Item) + DropPodData(46048, 141933, 13064, None, 40), # Unlocks with: (No Item) + DropPodData(144456, 36294, 17301, "Circuit Board", 48), # Unlocks with: 20 x Desc_CircuitBoard_C + DropPodData(-43144, 145820, 7472, "Modular Frame", 0), # Unlocks with: 5 x Desc_ModularFrame_C + DropPodData(-108774, 107811, 10154, "Crystal Oscillator", 0), # Unlocks with: 1 x Desc_CrystalOscillator_C + DropPodData(-56987, -144603, 2072, "Rotor", 10), # Unlocks with: 1 x Desc_Rotor_C + DropPodData(-152676, 33864, 19283, None, 256), # Unlocks with: (No Item) + DropPodData(90313, 129583, 9112, "Crystal Oscillator", 20), # Unlocks with: 2 x Desc_CrystalOscillator_C + DropPodData(111212, -113040, 12036, "Screw", 10), # Unlocks with: 15 x Desc_IronScrew_C + DropPodData(-157077, -6312, 25128, "Turbo Motor", 0), # Unlocks with: 8 x Desc_MotorLightweight_C + DropPodData(157249, -40206, 13694, "High-Speed Connector", 0), # Unlocks with: 2 x Desc_HighSpeedConnector_C + DropPodData(-151842, 72468, 9945, "Encased Industrial Beam", 0), # Unlocks with: 3 x Desc_SteelPlateReinforced_C + DropPodData(64696, 156038, 14067, "Modular Frame", 0), # Unlocks with: 6 x Desc_ModularFrame_C + DropPodData(-157080, -67028, 11766, "Rotor", 0), # Unlocks with: 4 x Desc_Rotor_C + DropPodData(170057, -10579, 18823, None, 50), # Unlocks with: (No Item) + DropPodData(143671, 92573, 24990, "Crystal Oscillator", 20), # Unlocks with: 2 x Desc_CrystalOscillator_C + DropPodData(127215, -116866, -1397, "Rubber", 0), # Unlocks with: 10 x Desc_Rubber_C + DropPodData(163999, 61333, 21481, "AI Limiter", 0), # Unlocks with: 3 x Desc_CircuitBoardHighSpeed_C + DropPodData(98306, -149781, 2552, None, 40), # Unlocks with: (No Item) + DropPodData(5302, -187090, -1608, None, 0), # Unlocks with: (No Item) + DropPodData(188304, 17059, 12949, None, 0), # Unlocks with: (No Item) + DropPodData(84256, -171122, -290, None, 0), # Unlocks with: (No Item) + DropPodData(191366, 37694, 5676, "Computer", 0), # Unlocks with: 4 x Desc_Computer_C + DropPodData(28695, 193441, 17459, "Quickwire", 0), # Unlocks with: 9 x Desc_HighSpeedWire_C + DropPodData(-146044, -137047, 2357, "Modular Frame", 0), # Unlocks with: 9 x Desc_ModularFrame_C + DropPodData(-200203, -17766, 12193, "Solid Biofuel", 0), # Unlocks with: 10 x Desc_Biofuel_C + DropPodData(47834, 195703, 2943, "Black Powder", 0), # Unlocks with: 4 x Desc_Gunpowder_C + DropPodData(198418, -41186, 13786, None, 0), # Unlocks with: (No Item) + DropPodData(-195756, -59210, -84, None, 30), # Unlocks with: (No Item) + DropPodData(-121994, 166916, -49, "Steel Beam", 20), # Unlocks with: 4 x Desc_SteelPlate_C + DropPodData(88323, 188913, 1420, None, 30), # Unlocks with: (No Item) + DropPodData(-123677, -167107, 29710, "Motor", 0), # Unlocks with: 4 x Desc_Motor_C + DropPodData(150633, 146698, 7727, "Crystal Oscillator", 20), # Unlocks with: 2 x Desc_CrystalOscillator_C + DropPodData(-55111, -204857, 7844, "Motor", 0), # Unlocks with: 30 x Desc_Motor_C + DropPodData(216096, -268, -1592, "Heat Sink", 0), # Unlocks with: 7 x Desc_AluminumPlateReinforced_C + DropPodData(159088, -145116, 23164, "Motor", 0), # Unlocks with: 30 x Desc_Motor_C + DropPodData(207683, -68352, 3927, "Encased Industrial Beam", 20), # Unlocks with: 27 x Desc_SteelPlateReinforced_C + DropPodData(-189258, 116331, -1764, None, 0), # Unlocks with: (No Item) + DropPodData(46951, 221859, 5917, None, 20), # Unlocks with: (No Item) + DropPodData(-9988, 227625, -1017, None, 40), # Unlocks with: (No Item) + DropPodData(232515, -20519, 8979, "Crystal Oscillator", 15), # Unlocks with: 2 x Desc_CrystalOscillator_C + DropPodData(232138, 27191, -1629, "Supercomputer", 0), # Unlocks with: 5 x Desc_ComputerSuper_C + DropPodData(-135, -237257, -1760, None, 0), # Unlocks with: (No Item) + DropPodData(-232498, -51432, -386, "Rotor", 0), # Unlocks with: 21 x Desc_Rotor_C + DropPodData(-238333, 17321, 19741, "Heat Sink", 0), # Unlocks with: 3 x Desc_AluminumPlateReinforced_C + DropPodData(200510, 131912, 6341, "Motor", 0), # Unlocks with: 30 x Desc_Motor_C + DropPodData(-108812, 214051, 3200, "Quickwire", 0), # Unlocks with: 1 x Desc_HighSpeedWire_C + DropPodData(232255, 79925, -1275, "Turbo Motor", 67), # Unlocks with: 2 x Desc_MotorLightweight_C + DropPodData(226418, 98109, 7339, None, 200), # Unlocks with: (No Item) + DropPodData(156569, 191767, -9312, "Rubber", 0), # Unlocks with: 4 x Desc_Rubber_C + DropPodData(44579, -244343, -874, None, 0), # Unlocks with: (No Item) + DropPodData(118349, 221905, -7063, "Encased Industrial Beam", 0), # Unlocks with: 6 x Desc_SteelPlateReinforced_C + # DropPodData(249919, 59534, 2430, "Quantum Computer", 0), # Unlocks with: 1 x Desc_ComputerQuantum_C + DropPodData(188233, 177201, 9608, "Quickwire", 0), # Unlocks with: 12 x Desc_HighSpeedWire_C + DropPodData(-174494, -197134, -1538, None, 30), # Unlocks with: (No Item) + DropPodData(-50655, -259272, -1667, None, 0), # Unlocks with: (No Item) + DropPodData(30383, 266975, -987, "Screw", 0), # Unlocks with: 12 x Desc_IronScrew_C + DropPodData(272715, 28087, -1586, "Supercomputer", 0), # Unlocks with: 2 x Desc_ComputerSuper_C + DropPodData(-152279, 229520, 1052, "Modular Frame", 0), # Unlocks with: 5 x Desc_ModularFrame_C + DropPodData(241532, 131343, 17157, None, 0), # Unlocks with: (No Item) + DropPodData(-259577, 105048, -1548, None, 0), # Unlocks with: (No Item) + DropPodData(275070, -52585, 5980, None, 0), # Unlocks with: (No Item) + DropPodData(-247303, -142348, 4524, "Rotor", 0), # Unlocks with: 4 x Desc_Rotor_C + DropPodData(261797, 124616, -2597, "AI Limiter", 73), # Unlocks with: 3 x Desc_CircuitBoardHighSpeed_C + DropPodData(187056, 223656, -3215, None, 42), # Unlocks with: (No Item) + DropPodData(293299, 51, 522, "Crystal Oscillator", 42), # Unlocks with: 8 x Desc_CrystalOscillator_C + DropPodData(219146, -199880, 6503, "Rotor", 0), # Unlocks with: 10 x Desc_Rotor_C + DropPodData(176423, 243273, -9780, "Motor", 19), # Unlocks with: 3 x Desc_Motor_C + DropPodData(291821, 74782, -1574, "Superposition Oscillator", 0), # Unlocks with: 5 x Desc_QuantumOscillator_C + DropPodData(-78884, 292640, -4763, "Modular Frame", 0), # Unlocks with: 5 x Desc_ModularFrame_C + DropPodData(174948, -276436, 21151, "Motor", 0), # Unlocks with: 30 x Desc_Motor_C + DropPodData(295166, -173139, 8083, None, 0), # Unlocks with: (No Item) + DropPodData(349295, -38831, -1485, "Motor", 0), # Unlocks with: 10 x Desc_Motor_C + DropPodData(360114, -106614, 11815, "Motor", 0), # Unlocks with: 35 x Desc_Motor_C + DropPodData(303169, -246169, 5487, None, 50), # Unlocks with: (No Item) + DropPodData(236508, -312236, 9971, "Motor", 0), # Unlocks with: 30 x Desc_Motor_C + DropPodData(360285, -217558, 3900, None, 70), # Unlocks with: (No Item) + DropPodData(366637, -303548, -7288, None, 0), # Unlocks with: (No Item) + ] diff --git a/worlds/satisfactory/ItemData.py b/worlds/satisfactory/ItemData.py new file mode 100644 index 0000000000..a56a147323 --- /dev/null +++ b/worlds/satisfactory/ItemData.py @@ -0,0 +1,46 @@ +from enum import IntFlag +from dataclasses import dataclass +from BaseClasses import ItemClassification + + +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 + NeverExclude = 1 << 29 + + +@dataclass +class ItemData: + """Represents an item in the pool, it could be a resource bundle, production recipe, trap, etc.""" + category: ItemGroups + code: int + type: ItemClassification = ItemClassification.filler + count: int = 1 + """How many of this item exists in the pool. 0 means none, but still defines the item so it can be added in the + starting inventory for example""" diff --git a/worlds/satisfactory/Items.py b/worlds/satisfactory/Items.py new file mode 100644 index 0000000000..36b316356f --- /dev/null +++ b/worlds/satisfactory/Items.py @@ -0,0 +1,991 @@ +from random import Random +from typing import ClassVar, Optional +from collections.abc import Sequence +from BaseClasses import Item, ItemClassification as C +from .GameLogic import GameLogic +from .Options import SatisfactoryOptions +from .ItemData import ItemData, ItemGroups as G +from .CriticalPathCalculator import CriticalPathCalculator + + +class Items: + item_data: ClassVar[dict[str, ItemData]] = { + # Resource Bundles + "Bundle: Adaptive Control Unit": ItemData(G.Parts, 1338000, count=0), + "Bundle: AI Limiter": ItemData(G.Parts, 1338001), + "Bundle: Alclad Aluminum Sheet": ItemData(G.Parts, 1338002), + "Bundle: Blue Power Slug": ItemData(G.Parts, 1338003), + "Bundle: Yellow Power Slug": ItemData(G.Parts, 1338004), + "Bundle: Alien Protein": ItemData(G.Parts, 1338005), + "Bundle: Purple Power Slug": ItemData(G.Parts, 1338006), + "Bundle: Aluminum Casing": ItemData(G.Parts, 1338007), + "Bundle: Aluminum Ingot": ItemData(G.Parts, 1338008), + "Bundle: Aluminum Scrap": ItemData(G.Parts, 1338009), + "Bundle: Assembly Director System": ItemData(G.Parts, 1338010, count=0), + "Bundle: Automated Wiring": ItemData(G.Parts, 1338011, count=0), + "Bundle: Battery": ItemData(G.Parts, 1338012), + "Bundle: Bauxite": ItemData(G.Parts, 1338013), + "Bundle: Neural-Quantum Processor": ItemData(G.Parts, 1338014), # 1.0 + "Bundle: Biomass": ItemData(G.Parts, 1338015), + "Bundle: Black Powder": ItemData(G.Parts, 1338016), + "Bundle: Cable": ItemData(G.Parts, 1338017), + "Bundle: Caterium Ingot": ItemData(G.Parts, 1338018), + "Bundle: Caterium Ore": ItemData(G.Parts, 1338019), + "Bundle: Circuit Board": ItemData(G.Parts, 1338020), + "Bundle: Coal": ItemData(G.Parts, 1338021), + "Bundle: Singularity Cell": ItemData(G.Parts, 1338022), # 1.0 + "Bundle: Compacted Coal": ItemData(G.Parts, 1338023), + "Bundle: Computer": ItemData(G.Parts, 1338024), + "Bundle: Concrete": ItemData(G.Parts, 1338025), + "Bundle: Cooling System": ItemData(G.Parts, 1338026), + "Bundle: Copper Ingot": ItemData(G.Parts, 1338027), + "Bundle: Copper Ore": ItemData(G.Parts, 1338028), + "Bundle: Copper Powder": ItemData(G.Parts, 1338029), + "Bundle: Copper Sheet": ItemData(G.Parts, 1338030), + "Bundle: Adequate Pioneering Statue": ItemData(G.Parts, 1338031), + "Bundle: Crystal Oscillator": ItemData(G.Parts, 1338032), + "Bundle: Electromagnetic Control Rod": ItemData(G.Parts, 1338033), + "Bundle: Empty Canister": ItemData(G.Parts, 1338034), + "Bundle: Empty Fluid Tank": ItemData(G.Parts, 1338035), + "Bundle: Encased Industrial Beam": ItemData(G.Parts, 1338036), + "Bundle: Encased Plutonium Cell": ItemData(G.Trap, 1338037, C.trap), + "Bundle: Encased Uranium Cell": ItemData(G.Trap, 1338038, C.trap), + "Bundle: Fabric": ItemData(G.Parts, 1338039), + "Bundle: FICSIT Coupon": ItemData(G.Parts, 1338040, count=0), + "Bundle: AI Expansion Server": ItemData(G.Parts, 1338041, count=0), # 1.0 + "Bundle: Fused Modular Frame": ItemData(G.Parts, 1338042), + "Bundle: Hard Drive": ItemData(G.Parts, 1338043, count=0), + "Bundle: Heat Sink": ItemData(G.Parts, 1338044), + "Bundle: Heavy Modular Frame": ItemData(G.Parts, 1338045), + "Bundle: High-Speed Connector": ItemData(G.Parts, 1338046), + "Bundle: Satisfactory Pioneering Statue": ItemData(G.Parts, 1338047), + "Bundle: Pretty Good Pioneering Statue": ItemData(G.Parts, 1338048), + "Bundle: Iron Ingot": ItemData(G.Parts, 1338049), + "Bundle: Iron Ore": ItemData(G.Parts, 1338050), + "Bundle: Iron Plate": ItemData(G.Parts, 1338051), + "Bundle: Iron Rod": ItemData(G.Parts, 1338052), + "Bundle: Golden Nut Statue": ItemData(G.Parts, 1338053), + "Bundle: Leaves": ItemData(G.Parts, 1338054), + "Bundle: Limestone": ItemData(G.Parts, 1338055), + "Bundle: Magnetic Field Generator": ItemData(G.Parts, 1338056, count=0), + "Bundle: Mercer Sphere": ItemData(G.Parts, 1338057, count=0), + "Bundle: Modular Engine": ItemData(G.Parts, 1338058, count=0), + "Bundle: Modular Frame": ItemData(G.Parts, 1338059), + "Bundle: Motor": ItemData(G.Parts, 1338060), + "Bundle: Mycelia": ItemData(G.Parts, 1338061), + "Bundle: Non-fissile Uranium": ItemData(G.Trap, 1338062, C.trap), + "Bundle: Nuclear Pasta": ItemData(G.Parts, 1338063, count=0), + "Bundle: Lizard Doggo Statue": ItemData(G.Parts, 1338064), + "Bundle: Organic Data Capsule": ItemData(G.Parts, 1338065), + "Bundle: Packaged Alumina Solution": ItemData(G.Parts, 1338066), + "Bundle: Packaged Fuel": ItemData(G.Parts, 1338067), + "Bundle: Packaged Heavy Oil Residue": ItemData(G.Parts, 1338068), + "Bundle: Packaged Liquid Biofuel": ItemData(G.Parts, 1338069), + "Bundle: Packaged Nitric Acid": ItemData(G.Parts, 1338070), + "Bundle: Packaged Nitrogen Gas": ItemData(G.Parts, 1338071), + "Bundle: Packaged Oil": ItemData(G.Parts, 1338072), + "Bundle: Packaged Sulfuric Acid": ItemData(G.Parts, 1338073), + "Bundle: Packaged Turbofuel": ItemData(G.Parts, 1338074), + "Bundle: Packaged Water": ItemData(G.Parts, 1338075), + "Bundle: Petroleum Coke": ItemData(G.Parts, 1338076), + "Bundle: Plastic": ItemData(G.Parts, 1338077), + "Bundle: Plutonium Fuel Rod": ItemData(G.Trap, 1338078, C.trap), + "Bundle: Plutonium Pellet": ItemData(G.Trap, 1338079, C.trap), + "Bundle: Plutonium Waste": ItemData(G.Trap, 1338080, C.trap), + "Bundle: Polymer Resin": ItemData(G.Parts, 1338081), + "Bundle: Power Shard": ItemData(G.Parts, 1338082, count=0), + "Bundle: Confusing Creature Statue": ItemData(G.Parts, 1338083), + "Bundle: Pressure Conversion Cube": ItemData(G.Parts, 1338084), + "Bundle: Alien Power Matrix": ItemData(G.Parts, 1338085), # 1.0 + "Bundle: Quartz Crystal": ItemData(G.Parts, 1338086), + "Bundle: Quickwire": ItemData(G.Parts, 1338087), + "Bundle: Radio Control Unit": ItemData(G.Parts, 1338088), + "Bundle: Raw Quartz": ItemData(G.Parts, 1338089), + "Bundle: Reinforced Iron Plate": ItemData(G.Parts, 1338090), + "Bundle: Rotor": ItemData(G.Parts, 1338091), + "Bundle: Rubber": ItemData(G.Parts, 1338092), + "Bundle: SAM": ItemData(G.Parts, 1338093), # 1.0 + "Bundle: Screw": ItemData(G.Parts, 1338094), + "Bundle: Silica": ItemData(G.Parts, 1338095), + "Bundle: Smart Plating": ItemData(G.Parts, 1338096, count=0), + "Bundle: Smokeless Powder": ItemData(G.Parts, 1338097), + "Bundle: Solid Biofuel": ItemData(G.Parts, 1338098, C.useful), + "Bundle: Somersloop": ItemData(G.Parts, 1338099, count=0), + "Bundle: Stator": ItemData(G.Parts, 1338100), + "Bundle: Silver Hog Statue": ItemData(G.Parts, 1338101), + "Bundle: Steel Beam": ItemData(G.Parts, 1338102), + "Bundle: Steel Ingot": ItemData(G.Parts, 1338103), + "Bundle: Steel Pipe": ItemData(G.Parts, 1338104), + "Bundle: Sulfur": ItemData(G.Parts, 1338105), + "Bundle: Supercomputer": ItemData(G.Parts, 1338106), + "Bundle: Superposition Oscillator": ItemData(G.Parts, 1338107), + "Bundle: Thermal Propulsion Rocket": ItemData(G.Parts, 1338108, count=0), + "Bundle: Turbo Motor": ItemData(G.Parts, 1338109), + "Bundle: Hog Remains": ItemData(G.Parts, 1338110), + "Bundle: Uranium": ItemData(G.Trap, 1338111, C.trap), + "Bundle: Uranium Fuel Rod": ItemData(G.Trap, 1338112, C.trap), + "Bundle: Uranium Waste": ItemData(G.Trap, 1338113, C.trap), + "Bundle: Versatile Framework": ItemData(G.Parts, 1338114, count=0), + "Bundle: Wire": ItemData(G.Parts, 1338115), + "Bundle: Wood": ItemData(G.Parts, 1338116), + "Bundle: Plasma Spitter Remains": ItemData(G.Parts, 1338117), + "Bundle: Stinger Remains": ItemData(G.Parts, 1338118), + "Bundle: Hatcher Remains": ItemData(G.Parts, 1338119), + "Bundle: Alien DNA Capsule": ItemData(G.Parts, 1338120), + "Bundle: Diamonds": ItemData(G.Parts, 1338121), + "Bundle: Time Crystal": ItemData(G.Parts, 1338122), + "Bundle: Ficsite Ingot": ItemData(G.Parts, 1338123), + "Bundle: Ficsite Trigon": ItemData(G.Parts, 1338124), + "Bundle: Reanimated SAM": ItemData(G.Parts, 1338125), + "Bundle: SAM Fluctuator": ItemData(G.Parts, 1338126), + "Bundle: Biochemical Sculptor": ItemData(G.Parts, 1338127, count=0), + "Bundle: Ballistic Warp Drive": ItemData(G.Parts, 1338128, count=0), + "Bundle: Ficsonium": ItemData(G.Trap, 1338129, C.trap), + "Bundle: Ficsonium Fuel Rod": ItemData(G.Trap, 1338130, C.trap), + "Bundle: Packaged Rocket Fuel": ItemData(G.Parts, 1338131), + "Bundle: Packaged Ionized Fuel": ItemData(G.Parts, 1338132), + "Bundle: Dark Matter Crystal": ItemData(G.Parts, 1338133), + # 1338134 - 1338149 Reserved for future parts + # 1338150 - 1338199 Equipment / Ammo + "Bundle: Bacon Agaric": ItemData(G.Ammo, 1338150, count=0), + "Bundle: Beryl Nut": ItemData(G.Ammo, 1338151, count=0), + "Bundle: Blade Runners": ItemData(G.Equipment, 1338152, count=0), + "Bundle: Boom Box": ItemData(G.Equipment, 1338153, count=0), + "Bundle: Chainsaw": ItemData(G.Equipment, 1338154, count=0), + "Bundle: Cluster Nobelisk": ItemData(G.Ammo, 1338155), + "Bundle: Iodine-Infused Filter": ItemData(G.Equipment, 1338156, count=3), # 1.1 + "Bundle: Cup": ItemData(G.Equipment, 1338157, count=0), + "Bundle: Cup (gold)": ItemData(G.Equipment, 1338158, count=0), + "Bundle: Explosive Rebar": ItemData(G.Ammo, 1338159), + "Bundle: Factory Cart": ItemData(G.Equipment, 1338160, count=0), + "Bundle: Factory Cart (golden)": ItemData(G.Equipment, 1338161, count=0), + "Bundle: Gas Mask": ItemData(G.Equipment, 1338162, count=0), + "Bundle: Gas Nobelisk": ItemData(G.Ammo, 1338163), + "Bundle: Hazmat Suit": ItemData(G.Equipment, 1338164, count=0), + "Bundle: Homing Rifle Ammo": ItemData(G.Ammo, 1338165), + "Bundle: Hoverpack": ItemData(G.Equipment, 1338166, count=0), + "Bundle: Iron Rebar": ItemData(G.Ammo, 1338167), + "Bundle: Jetpack": ItemData(G.Equipment, 1338168, count=0), + "Bundle: Medicinal Inhaler": ItemData(G.Ammo, 1338169), + "Bundle: Nobelisk": ItemData(G.Ammo, 1338170), + "Bundle: Nobelisk Detonator": ItemData(G.Equipment, 1338171, count=0), + "Bundle: Nuke Nobelisk": ItemData(G.Ammo, 1338172), + "Bundle: Object Scanner": ItemData(G.Equipment, 1338173, count=0), + "Bundle: Paleberry": ItemData(G.Ammo, 1338174, count=0), + "Bundle: Parachute": ItemData(G.Equipment, 1338175, count=0), + "Bundle: Pulse Nobelisk": ItemData(G.Ammo, 1338176), + "Bundle: Rebar Gun": ItemData(G.Equipment, 1338177, count=0), + "Bundle: Rifle": ItemData(G.Equipment, 1338178, count=0), + "Bundle: Rifle Ammo": ItemData(G.Ammo, 1338179), + "Bundle: Shatter Rebar": ItemData(G.Ammo, 1338180), + "Bundle: Stun Rebar": ItemData(G.Ammo, 1338181), + "Bundle: Turbo Rifle Ammo": ItemData(G.Ammo, 1338182), + "Bundle: Xeno-Basher": ItemData(G.Equipment, 1338183, count=0), + "Bundle: Xeno-Zapper": ItemData(G.Equipment, 1338184, count=0), + "Bundle: Zipline": ItemData(G.Equipment, 1338185, count=0), + "Bundle: Portable Miner": ItemData(G.Equipment, 1338186, count=0), + "Bundle: Gas Filter": ItemData(G.Equipment, 1338187, count=3), + # Special cases + "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, 1338191, C.useful), +# added in 1.1 + "Bundle of Three: Power Shards": ItemData(G.Parts, 1338192), + "Bundle of Three: Mercer Spheres": ItemData(G.Parts, 1338193), + "Bundle of Four: Somersloops": ItemData(G.Parts, 1338194), + "Bundle of Three: Hard Drives": ItemData(G.Parts, 1338195), +# + # 1338196 - 1338199 Reserved for future equipment/ammo + + # 1338200+ Recipes / buildings / schematics + "Recipe: Reinforced Iron Plate": ItemData(G.Recipe, 1338200, C.progression), + "Recipe: Adhered Iron Plate": ItemData(G.Recipe, 1338201, C.progression), + "Recipe: Bolted Iron Plate": ItemData(G.Recipe, 1338202, C.progression), + "Recipe: Stitched Iron Plate": ItemData(G.Recipe, 1338203, C.progression), + "Recipe: Rotor": ItemData(G.Recipe, 1338204, C.progression), + "Recipe: Copper Rotor": ItemData(G.Recipe, 1338205, C.progression), + "Recipe: Steel Rotor": ItemData(G.Recipe, 1338206, C.progression), + "Recipe: Stator": ItemData(G.Recipe, 1338207, C.progression), + "Recipe: Quickwire Stator": ItemData(G.Recipe, 1338208, C.progression), + "Recipe: Plastic": ItemData(G.Recipe, 1338209, C.progression), + "Recipe: Residual Plastic": ItemData(G.Recipe, 1338210, C.progression), + "Recipe: Recycled Plastic": ItemData(G.Recipe, 1338211, C.progression), + "Recipe: Rubber": ItemData(G.Recipe, 1338212, C.progression), + "Recipe: Residual Rubber": ItemData(G.Recipe, 1338213, C.progression), + "Recipe: Recycled Rubber": ItemData(G.Recipe, 1338214, C.progression), + "Recipe: Iron Plate": ItemData(G.Recipe, 1338215, C.progression), + "Recipe: Coated Iron Plate": ItemData(G.Recipe, 1338216, C.progression), + "Recipe: Steel Cast Plate": ItemData(G.Recipe, 1338217, C.progression), # 1.0 + "Recipe: Iron Rod": ItemData(G.Recipe, 1338218, C.progression), + "Recipe: Steel Rod": ItemData(G.Recipe, 1338219, C.progression), + "Recipe: Screw": ItemData(G.Recipe, 1338220, C.progression), + "Recipe: Cast Screw": ItemData(G.Recipe, 1338221, C.progression), + "Recipe: Steel Screw": ItemData(G.Recipe, 1338222, C.progression), + "Recipe: Wire": ItemData(G.Recipe, 1338223, C.progression), + "Recipe: Fused Wire": ItemData(G.Recipe, 1338224, C.progression), + "Recipe: Iron Wire": ItemData(G.Recipe, 1338225, C.progression), + "Recipe: Caterium Wire": ItemData(G.Recipe, 1338226, C.progression), + "Recipe: Cable": ItemData(G.Recipe, 1338227, C.progression), + "Recipe: Coated Cable": ItemData(G.Recipe, 1338228, C.progression), + "Recipe: Insulated Cable": ItemData(G.Recipe, 1338229, C.progression), + "Recipe: Quickwire Cable": ItemData(G.Recipe, 1338230, C.progression), + "Recipe: Quickwire": ItemData(G.Recipe, 1338231, C.progression), + "Recipe: Fused Quickwire": ItemData(G.Recipe, 1338232, C.progression), + "Recipe: Copper Sheet": ItemData(G.Recipe, 1338233, C.progression), + "Recipe: Steamed Copper Sheet": ItemData(G.Recipe, 1338234, C.progression), + "Recipe: Steel Pipe": ItemData(G.Recipe, 1338235, C.progression), + "Recipe: Steel Beam": ItemData(G.Recipe, 1338236, C.progression), + "Recipe: Neural-Quantum Processor": ItemData(G.Recipe, 1338237, C.progression), # 1.0 + "Recipe: Heavy Oil Residue": ItemData(G.Recipe, 1338238, C.progression), + "Recipe: Polymer Resin": ItemData(G.Recipe, 1338239, C.progression), + "Recipe: Fuel": ItemData(G.Recipe, 1338240, C.progression), + "Recipe: Residual Fuel": ItemData(G.Recipe, 1338241, C.progression), + "Recipe: Diluted Packaged Fuel": ItemData(G.Recipe, 1338242, C.progression), + "Recipe: AI Expansion Server": ItemData(G.Recipe, 1338243, C.progression), # 1.0 + "Recipe: Concrete": ItemData(G.Recipe, 1338244, C.progression), + "Recipe: Rubber Concrete": ItemData(G.Recipe, 1338245, C.progression), + "Recipe: Wet Concrete": ItemData(G.Recipe, 1338246, C.progression), + "Recipe: Fine Concrete": ItemData(G.Recipe, 1338247, C.progression), + "Recipe: Silica": ItemData(G.Recipe, 1338248, C.progression), + "Recipe: Cheap Silica": ItemData(G.Recipe, 1338249, C.progression), + "Recipe: Quartz Crystal": ItemData(G.Recipe, 1338250, C.progression), + "Recipe: Pure Quartz Crystal": ItemData(G.Recipe, 1338251, C.progression), + "Recipe: Iron Ingot": ItemData(G.Recipe, 1338252, C.progression), + "Recipe: Pure Iron Ingot": ItemData(G.Recipe, 1338253, C.progression), + "Recipe: Iron Alloy Ingot": ItemData(G.Recipe, 1338254, C.progression), + "Recipe: Steel Ingot": ItemData(G.Recipe, 1338255, C.progression), + "Recipe: Coke Steel Ingot": ItemData(G.Recipe, 1338256, C.progression), + "Recipe: Compacted Steel Ingot": ItemData(G.Recipe, 1338257, C.progression), + "Recipe: Solid Steel Ingot": ItemData(G.Recipe, 1338258, C.progression), + "Recipe: Copper Ingot": ItemData(G.Recipe, 1338259, C.progression), + "Recipe: Copper Alloy Ingot": ItemData(G.Recipe, 1338260, C.progression), + "Recipe: Pure Copper Ingot": ItemData(G.Recipe, 1338261, C.progression), + "Recipe: Caterium Ingot": ItemData(G.Recipe, 1338262, C.progression), + "Recipe: Pure Caterium Ingot": ItemData(G.Recipe, 1338263, C.progression), + "Recipe: Alien Power Matrix": ItemData(G.Recipe, 1338264), # 1.0 + "Recipe: Ficsite Ingot (Aluminum)": ItemData(G.Recipe, 1338265, C.progression), # 1.0 + "Recipe: Ficsite Ingot (Caterium)": ItemData(G.Recipe, 1338266, C.progression), # 1.0 + "Recipe: Ficsite Ingot (Iron)": ItemData(G.Recipe, 1338267, C.progression), # 1.0 + "Recipe: Ficsite Trigon": ItemData(G.Recipe, 1338268, C.progression), # 1.0 + "Recipe: Reanimated SAM": ItemData(G.Recipe, 1338269, C.progression), # 1.0 + "Recipe: SAM Fluctuator": ItemData(G.Recipe, 1338270, C.progression), # 1.0 + "Recipe: Petroleum Coke": ItemData(G.Recipe, 1338271, C.progression), + "Recipe: Compacted Coal": ItemData(G.Recipe, 1338272, C.progression), + "Recipe: Motor": ItemData(G.Recipe, 1338273, C.progression), + "Recipe: Rigor Motor": ItemData(G.Recipe, 1338274, C.progression), + "Recipe: Electric Motor": ItemData(G.Recipe, 1338275, C.progression), + "Recipe: Modular Frame": ItemData(G.Recipe, 1338276, C.progression), + "Recipe: Bolted Frame": ItemData(G.Recipe, 1338277, C.progression), + "Recipe: Steeled Frame": ItemData(G.Recipe, 1338278, C.progression), + "Recipe: Heavy Modular Frame": ItemData(G.Recipe, 1338279, C.progression), + "Recipe: Heavy Flexible Frame": ItemData(G.Recipe, 1338280, C.progression), + "Recipe: Heavy Encased Frame": ItemData(G.Recipe, 1338281, C.progression), + "Recipe: Encased Industrial Beam": ItemData(G.Recipe, 1338282, C.progression), + "Recipe: Encased Industrial Pipe": ItemData(G.Recipe, 1338283, C.progression), + "Recipe: Computer": ItemData(G.Recipe, 1338284, C.progression), + "Recipe: Crystal Computer": ItemData(G.Recipe, 1338285, C.progression), + "Recipe: Caterium Computer": ItemData(G.Recipe, 1338286, C.progression), + "Recipe: Circuit Board": ItemData(G.Recipe, 1338287, C.progression), + "Recipe: Electrode Circuit Board": ItemData(G.Recipe, 1338288, C.progression), + "Recipe: Silicon Circuit Board": ItemData(G.Recipe, 1338289, C.progression), + "Recipe: Caterium Circuit Board": ItemData(G.Recipe, 1338290, C.progression), + "Recipe: Crystal Oscillator": ItemData(G.Recipe, 1338291, C.progression), + "Recipe: Insulated Crystal Oscillator": ItemData(G.Recipe, 1338292, C.progression), + "Recipe: AI Limiter": ItemData(G.Recipe, 1338293, C.progression), + "Recipe: Electromagnetic Control Rod": ItemData(G.Recipe, 1338294, C.progression), + "Recipe: Electromagnetic Connection Rod": ItemData(G.Recipe, 1338295, C.progression), + "Recipe: High-Speed Connector": ItemData(G.Recipe, 1338296, C.progression), + "Recipe: Silicon High-Speed Connector": ItemData(G.Recipe, 1338297, C.progression), + "Recipe: Smart Plating": ItemData(G.Recipe, 1338298, C.progression), + "Recipe: Plastic Smart Plating": ItemData(G.Recipe, 1338299, C.progression), + "Recipe: Versatile Framework": ItemData(G.Recipe, 1338300, C.progression), + "Recipe: Flexible Framework": ItemData(G.Recipe, 1338301, C.progression), + "Recipe: Automated Wiring": ItemData(G.Recipe, 1338302, C.progression), + "Recipe: Automated Speed Wiring": ItemData(G.Recipe, 1338303, C.progression), + "Recipe: Modular Engine": ItemData(G.Recipe, 1338304, C.progression), + "Recipe: Adaptive Control Unit": ItemData(G.Recipe, 1338305, C.progression), + "Recipe: Diluted Fuel": ItemData(G.Recipe, 1338306, C.progression), + "Recipe: Alumina Solution": ItemData(G.Recipe, 1338307, C.progression), + "Recipe: Automated Miner": ItemData(G.Recipe, 1338308, C.progression), + "Recipe: Singularity Cell": ItemData(G.Recipe, 1338309, C.progression), # 1.0 + "Recipe: Aluminum Scrap": ItemData(G.Recipe, 1338310, C.progression), + "Recipe: Electrode Aluminum Scrap": ItemData(G.Recipe, 1338311, C.progression), + "Recipe: Instant Scrap": ItemData(G.Recipe, 1338312, C.progression), + "Recipe: Aluminum Ingot": ItemData(G.Recipe, 1338313, C.progression), + "Recipe: Pure Aluminum Ingot": ItemData(G.Recipe, 1338314, C.progression), + "Recipe: Alclad Aluminum Sheet": ItemData(G.Recipe, 1338315, C.progression), + "Recipe: Aluminum Casing": ItemData(G.Recipe, 1338316, C.progression), + "Recipe: Alclad Casing": ItemData(G.Recipe, 1338317, C.progression), + "Recipe: Heat Sink": ItemData(G.Recipe, 1338318, C.progression), + "Recipe: Heat Exchanger": ItemData(G.Recipe, 1338319, C.progression), + "Recipe: Synthetic Power Shard": ItemData(G.Recipe, 1338320, C.progression), + "Recipe: Nitric Acid": ItemData(G.Recipe, 1338321, C.progression), + "Recipe: Fused Modular Frame": ItemData(G.Recipe, 1338322, C.progression), + "Recipe: Heat-Fused Frame": ItemData(G.Recipe, 1338323, C.progression), + "Recipe: Radio Control Unit": ItemData(G.Recipe, 1338324, C.progression), + "Recipe: Radio Connection Unit": ItemData(G.Recipe, 1338325, C.progression), + "Recipe: Radio Control System": ItemData(G.Recipe, 1338326, C.progression), + "Recipe: Pressure Conversion Cube": ItemData(G.Recipe, 1338327, C.progression), + "Recipe: Cooling System": ItemData(G.Recipe, 1338328, C.progression), + "Recipe: Cooling Device": ItemData(G.Recipe, 1338329, C.progression), + "Recipe: Turbo Motor": ItemData(G.Recipe, 1338330, C.progression), + "Recipe: Turbo Electric Motor": ItemData(G.Recipe, 1338331, C.progression), + "Recipe: Turbo Pressure Motor": ItemData(G.Recipe, 1338332, C.progression), + "Recipe: Battery": ItemData(G.Recipe, 1338333, C.progression), + "Recipe: Classic Battery": ItemData(G.Recipe, 1338334, C.progression), + "Recipe: Supercomputer": ItemData(G.Recipe, 1338335, C.progression), + "Recipe: OC Supercomputer": ItemData(G.Recipe, 1338336, C.progression), + "Recipe: Super-State Computer": ItemData(G.Recipe, 1338337, C.progression), + "Recipe: Biochemical Sculptor": ItemData(G.Recipe, 1338338, C.progression), # 1.0 + "Recipe: Sulfuric Acid": ItemData(G.Recipe, 1338339, C.progression), + "Recipe: Ballistic Warp Drive": ItemData(G.Recipe, 1338340, C.progression), # 1.0 + "Recipe: Encased Uranium Cell": ItemData(G.Recipe, 1338341, C.progression), + "Recipe: Infused Uranium Cell": ItemData(G.Recipe, 1338342, C.progression), + "Recipe: Uranium Fuel Rod": ItemData(G.Recipe, 1338343, C.progression), + "Recipe: Uranium Fuel Unit": ItemData(G.Recipe, 1338344, C.progression), + "Recipe: Aluminum Beam": ItemData(G.Recipe, 1338345, C.progression), # 1.0 + "Recipe: Aluminum Rod": ItemData(G.Recipe, 1338346, C.progression), # 1.0 + "Recipe: Basic Iron Ingot": ItemData(G.Recipe, 1338347, C.progression), # 1.0 + "Recipe: Non-fissile Uranium": ItemData(G.Recipe, 1338348, C.progression), + "Recipe: Fertile Uranium": ItemData(G.Recipe, 1338349, C.progression), + "Recipe: Plutonium Pellet": ItemData(G.Recipe, 1338350), + "Recipe: Encased Plutonium Cell": ItemData(G.Recipe, 1338351), + "Recipe: Instant Plutonium Cell": ItemData(G.Recipe, 1338352), + "Recipe: Plutonium Fuel Rod": ItemData(G.Recipe, 1338353), + "Recipe: Plutonium Fuel Unit": ItemData(G.Recipe, 1338354), + "Recipe: Gas Filter": ItemData(G.Recipe, 1338355, C.progression), + "Recipe: Iodine-Infused Filter": ItemData(G.Recipe, 1338356, C.progression), + "Recipe: Assembly Director System": ItemData(G.Recipe, 1338357, C.progression), + "Recipe: Magnetic Field Generator": ItemData(G.Recipe, 1338358, C.progression), + "Recipe: Copper Powder": ItemData(G.Recipe, 1338359, C.progression), + "Recipe: Nuclear Pasta": ItemData(G.Recipe, 1338360, C.progression), + "Recipe: Thermal Propulsion Rocket": ItemData(G.Recipe, 1338361, C.progression), + "Recipe: Ficsonium": ItemData(G.Recipe, 1338362), # 1.0 + "Recipe: Ficsonium Fuel Rod": ItemData(G.Recipe, 1338363), # 1.0 + "Recipe: Dark Matter Crystal": ItemData(G.Recipe, 1338364, C.progression), # 1.0 + "Recipe: Dark Matter Crystallization": ItemData(G.Recipe, 1338365, C.progression), # 1.0 + "Recipe: Dark Matter Trap": ItemData(G.Recipe, 1338366, C.progression), # 1.0 + "Recipe: Pulse Nobelisk": ItemData(G.Recipe, 1338367, C.useful), + "Recipe: Hatcher Protein": ItemData(G.Recipe, 1338368, C.progression), + "Recipe: Hog Protein": ItemData(G.Recipe, 1338369, C.progression), + "Recipe: Spitter Protein": ItemData(G.Recipe, 1338370, C.progression), + "Recipe: Stinger Protein": ItemData(G.Recipe, 1338371, C.progression), + "Recipe: Biomass (Leaves)": ItemData(G.Recipe, 1338372, C.progression), + "Recipe: Biomass (Wood)": ItemData(G.Recipe, 1338373, C.progression), + "Recipe: Biomass (Mycelia)": ItemData(G.Recipe, 1338374, C.progression), + "Recipe: Biomass (Alien Protein)": ItemData(G.Recipe, 1338375, C.progression), + "Recipe: Turbo Rifle Ammo (Packaged)": ItemData(G.Recipe, 1338376, C.useful), + "Recipe: Fabric": ItemData(G.Recipe, 1338377, C.progression), + "Recipe: Polyester Fabric": ItemData(G.Recipe, 1338378, C.progression), + "Recipe: Solid Biofuel": ItemData(G.Recipe, 1338379, C.progression), + "Recipe: Liquid Biofuel": ItemData(G.Recipe, 1338380, C.progression), + "Recipe: Empty Canister": ItemData(G.Recipe, 1338381, C.progression), + "Recipe: Coated Iron Canister": ItemData(G.Recipe, 1338382, C.progression), + "Recipe: Steel Canister": ItemData(G.Recipe, 1338383, C.progression), + "Recipe: Empty Fluid Tank": ItemData(G.Recipe, 1338384, C.progression), + "Recipe: Packaged Alumina Solution": ItemData(G.Recipe, 1338385, C.progression), + "Recipe: Packaged Fuel": ItemData(G.Recipe, 1338386, C.progression), + # "Recipe: Diluted Packaged Fuel": ItemData(G.Recipe, 1338387, C.progression), # Duplicated + "Recipe: Packaged Heavy Oil Residue": ItemData(G.Recipe, 1338388, C.progression), + "Recipe: Packaged Liquid Biofuel": ItemData(G.Recipe, 1338389, C.progression), + "Recipe: Packaged Nitric Acid": ItemData(G.Recipe, 1338390, C.progression), + "Recipe: Packaged Nitrogen Gas": ItemData(G.Recipe, 1338391, C.progression), + "Recipe: Packaged Oil": ItemData(G.Recipe, 1338392, C.progression), + "Recipe: Packaged Sulfuric Acid": ItemData(G.Recipe, 1338393, C.progression), + "Recipe: Packaged Turbofuel": ItemData(G.Recipe, 1338394, C.progression), + "Recipe: Packaged Water": ItemData(G.Recipe, 1338395, C.progression), + "Recipe: Turbofuel": ItemData(G.Recipe, 1338396, C.progression), + "Recipe: Turbo Heavy Fuel": ItemData(G.Recipe, 1338397, C.progression), + "Recipe: Turbo Blend Fuel": ItemData(G.Recipe, 1338398, C.progression), + "Recipe: Hazmat Suit": ItemData(G.Recipe, 1338399, C.progression), + "Recipe: Gas Mask": ItemData(G.Recipe, 1338400, C.progression), + "Recipe: Black Powder": ItemData(G.Recipe, 1338401, C.progression), + "Recipe: Blade Runners": ItemData(G.Recipe, 1338402, C.useful), + "Recipe: Chainsaw": ItemData(G.Recipe, 1338403, C.useful), + "Recipe: Cluster Nobelisk": ItemData(G.Recipe, 1338404, C.useful), + "Recipe: Explosive Rebar": ItemData(G.Recipe, 1338405, C.useful), + "Recipe: Factory Cart": ItemData(G.Recipe, 1338406, C.useful), + "Recipe: Gas Nobelisk": ItemData(G.Recipe, 1338407, C.useful), + "Recipe: Golden Factory Cart": ItemData(G.Recipe, 1338408), + "Recipe: Homing Rifle Ammo": ItemData(G.Recipe, 1338409, C.useful), + "Recipe: Iron Rebar": ItemData(G.Recipe, 1338410, C.progression), + "Recipe: Nobelisk": ItemData(G.Recipe, 1338411, C.progression), + "Recipe: Nuke Nobelisk": ItemData(G.Recipe, 1338412, C.useful), + "Recipe: Nutritional Inhaler": ItemData(G.Recipe, 1338413, C.useful), + "Recipe: Object Scanner": ItemData(G.Recipe, 1338414, C.progression), + "Recipe: Parachute": ItemData(G.Recipe, 1338415, C.useful), + "Recipe: Protein Inhaler": ItemData(G.Recipe, 1338416, C.useful), + "Recipe: Rebar Gun": ItemData(G.Recipe, 1338417, C.useful), + "Recipe: Rifle": ItemData(G.Recipe, 1338418, C.useful), + "Recipe: Rifle Ammo": ItemData(G.Recipe, 1338419, C.progression), + "Recipe: Shatter Rebar": ItemData(G.Recipe, 1338420, C.useful), + "Recipe: Stun Rebar": ItemData(G.Recipe, 1338421, C.useful), + "Recipe: Therapeutic Inhaler": ItemData(G.Recipe, 1338422, C.useful), + "Recipe: Turbo Rifle Ammo": ItemData(G.Recipe, 1338423, C.useful), + "Recipe: Vitamin Inhaler": ItemData(G.Recipe, 1338424, C.useful), + "Recipe: Xeno-Basher": ItemData(G.Recipe, 1338425, C.useful), + "Recipe: Xeno-Zapper": ItemData(G.Recipe, 1338426, C.useful), + "Recipe: Zipline": ItemData(G.Recipe, 1338427, C.useful), + "Recipe: Fine Black Powder": ItemData(G.Recipe, 1338428, C.progression), + "Recipe: Smokeless Powder": ItemData(G.Recipe, 1338429, C.progression), + "Recipe: Alien DNA Capsule": ItemData(G.Recipe, 1338430, C.progression), + "Recipe: Power Shard (1)": ItemData(G.Recipe, 1338431, C.progression), + "Recipe: Power Shard (2)": ItemData(G.Recipe, 1338432, C.useful), + "Recipe: Power Shard (5)": ItemData(G.Recipe, 1338433, C.useful), + +# 1.0 + "Recipe: Diamonds": ItemData(G.Recipe, 1338434, C.progression), + "Recipe: Cloudy Diamonds": ItemData(G.Recipe, 1338435, C.progression), + "Recipe: Oil-Based Diamonds": ItemData(G.Recipe, 1338436, C.progression), + "Recipe: Petroleum Diamonds": ItemData(G.Recipe, 1338437, C.progression), + "Recipe: Pink Diamonds": ItemData(G.Recipe, 1338438, C.progression), + "Recipe: Turbo Diamonds": ItemData(G.Recipe, 1338439, C.progression), + "Recipe: Time Crystal": ItemData(G.Recipe, 1338440, C.progression), + "Recipe: Superposition Oscillator": ItemData(G.Recipe, 1338441, C.progression), + # "Recipe: Excited Photonic Matter": ItemData(G.Recipe, 1338442, C.progression), unlocked with converter + "Recipe: Rocket Fuel": ItemData(G.Recipe, 1338443, C.progression), + "Recipe: Nitro Rocket Fuel": ItemData(G.Recipe, 1338444, C.progression), + "Recipe: Ionized Fuel": ItemData(G.Recipe, 1338445, C.useful), + "Recipe: Packaged Rocket Fuel": ItemData(G.Recipe, 1338446, C.progression), + "Recipe: Packaged Ionized Fuel": ItemData(G.Recipe, 1338447, C.useful), + "Recipe: Dark-Ion Fuel": ItemData(G.Recipe, 1338448, C.useful), + "Recipe: Quartz Purification": ItemData(G.Recipe, 1338449, C.progression), + "Recipe: Fused Quartz Crystal": ItemData(G.Recipe, 1338450, C.progression), + "Recipe: Leached Caterium Ingot": ItemData(G.Recipe, 1338451, C.progression), + "Recipe: Leached Copper Ingot": ItemData(G.Recipe, 1338452, C.progression), + "Recipe: Leached Iron ingot": ItemData(G.Recipe, 1338453, C.progression), + "Recipe: Molded Beam": ItemData(G.Recipe, 1338454, C.progression), + "Recipe: Molded Steel Pipe": ItemData(G.Recipe, 1338455, C.progression), + "Recipe: Plastic AI Limiter": ItemData(G.Recipe, 1338456, C.progression), + "Recipe: Tempered Caterium Ingot": ItemData(G.Recipe, 1338457, C.progression), + "Recipe: Tempered Copper Ingot": ItemData(G.Recipe, 1338458, C.progression), +# 1.0 + +# added in 1.1 or missed + "Recipe: Iron Pipe": ItemData(G.Recipe, 1338459, C.progression), + "Recipe: Biocoal": ItemData(G.Recipe, 1338460, C.useful), + "Recipe: Charcoal": ItemData(G.Recipe, 1338461, C.useful), + "Recipe: Sloppy Alumina": ItemData(G.Recipe, 1338462, C.progression), + "Recipe: Hoverpack": ItemData(G.Recipe, 1338463, C.useful), + "Recipe: Jetpack": ItemData(G.Recipe | G.NeverExclude, 1338464, C.useful), + "Recipe: Nobelisk Detonator": ItemData(G.Recipe, 1338465, C.progression), + "Recipe: Portable Miner": ItemData(G.Recipe, 1338466, C.progression), +# + "Recipe: Dark Matter Residue": ItemData(G.Recipe, 1338467, C.progression), + + # 1338468 - 1338599 Reserved for future recipes + # 1338400 - 1338899 buildings / others + "Building: Constructor": ItemData(G.Building, 1338600, C.progression), # unlocked by default + "Building: Assembler": ItemData(G.Building, 1338601, C.progression), + "Building: Manufacturer": ItemData(G.Building, 1338602, C.progression), + "Building: Packager": ItemData(G.Building, 1338603, C.progression), + "Building: Refinery": ItemData(G.Building, 1338604, C.progression), + "Building: Blender": ItemData(G.Building, 1338605, C.progression), + "Building: Particle Accelerator": ItemData(G.Building, 1338606, C.progression), + "Building: Biomass Burner": ItemData(G.Building, 1338607, C.progression), # unlocked by default + "Building: Coal Generator": ItemData(G.Building, 1338608, C.progression), + "Building: Geothermal Generator": ItemData(G.Building, 1338609, C.progression), + "Building: Nuclear Power Plant": ItemData(G.Building, 1338610, C.progression), + "Building: Miner Mk.1": ItemData(G.Building, 1338611, C.progression), # unlocked by default + "Building: Miner Mk.2": ItemData(G.Building, 1338612, C.progression), + "Building: Miner Mk.3": ItemData(G.Building, 1338613, C.progression), + "Building: Oil Extractor": ItemData(G.Building, 1338614, C.progression), + "Building: Water Extractor": ItemData(G.Building, 1338615, C.progression), + "Building: Smelter": ItemData(G.Building, 1338616, C.progression), # unlocked by default + "Building: Foundry": ItemData(G.Building, 1338617, C.progression), + "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.NeverExclude, 1338621, C.progression), + "Building: AWESOME Shop": ItemData(G.Building | G.NeverExclude, 1338622, C.progression), + "Building: Structural Beam Pack": 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 | G.NeverExclude, 1338625, C.useful), + "Building: Industrial Fluid Buffer": ItemData(G.Building | G.NeverExclude, 1338626, C.useful), + "Building: Jump Pad": ItemData(G.Building, 1338627, C.filler), + "Building: Ladder": ItemData(G.Building, 1338628, C.filler), + "Building: MAM": ItemData(G.Building | G.NeverExclude, 1338629, C.progression), + "Building: Personal Storage Box": ItemData(G.Building, 1338630, C.filler), + "Building: Power Storage": ItemData(G.Building | G.NeverExclude, 1338631, C.progression), + "Building: U-Jelly Landing Pad": ItemData(G.Building, 1338632, C.useful), + "Building: Power Switch": ItemData(G.Building | G.NeverExclude, 1338633, C.useful), + "Building: Priority Power Switch": ItemData(G.Building | G.NeverExclude, 1338634, C.useful), + "Building: Storage Container": ItemData(G.Building, 1338635, C.useful, 0), + "Building: Lookout Tower": ItemData(G.Building, 1338636, C.filler), + # "Building: Power Pole Mk.1": ItemData(G.Building, 1338637, C.progression), # unlocked by default + "Building: Power Pole Mk.2": ItemData(G.Building | G.NeverExclude, 1338638, C.useful), + "Building: Power Pole Mk.3": ItemData(G.Building | G.NeverExclude, 1338639, C.useful), + "Building: Industrial Storage Container": ItemData(G.Building | G.NeverExclude, 1338640, C.useful), + "Building: Conveyor Merger": ItemData(G.Building | G.NeverExclude, 1338641, C.progression), + "Building: Conveyor Splitter": ItemData(G.Building | G.NeverExclude, 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), + "Building: Conveyor Mk.4": ItemData(G.Building | G.ConveyorMk4, 1338646, C.progression), + "Building: Conveyor Mk.5": ItemData(G.Building | G.ConveyorMk5, 1338647, C.progression), + "Building: Conveyor Lift Mk.1": ItemData(G.Building | G.ConveyorMk1, 1338648, C.useful), + "Building: Conveyor Lift Mk.2": ItemData(G.Building | G.ConveyorMk2, 1338649, C.useful), + "Building: Conveyor Lift Mk.3": ItemData(G.Building | G.ConveyorMk3, 1338650, C.useful), + "Building: Conveyor Lift Mk.4": ItemData(G.Building | G.ConveyorMk4, 1338651, C.useful), + "Building: Conveyor Lift Mk.5": ItemData(G.Building | G.ConveyorMk5, 1338652, C.useful), + "Building: Cable Beam Pack": ItemData(G.Beams, 1338653, C.filler, 0), + "Building: Stackable Conveyor Pole": ItemData(G.Building | G.ConveyorSupports, 1338654, C.useful), + "Building: Conveyor Wall Mount": ItemData(G.Building | G.ConveyorSupports, 1338655, C.useful, 0), + "Building: Conveyor Lift Floor Hole": ItemData(G.Building | G.ConveyorSupports, 1338656, C.useful, 0), + "Building: Conveyor Ceiling Mount": ItemData(G.Building | G.ConveyorSupports, 1338657, C.useful, 0), + "Building: Pipes Mk.1": ItemData(G.Building | G.PipesMk1, 1338658, C.progression), + "Building: Pipes Mk.2": ItemData(G.Building | G.PipesMk2, 1338659, C.progression), + "Building: Pipeline Pump Mk.1": ItemData(G.Building | G.PipesMk1, 1338660, C.progression), + "Building: Pipeline Pump Mk.2": ItemData(G.Building | G.PipesMk2, 1338661, C.progression), + "Building: Pipeline Junction Cross": ItemData(G.Building | G.PipesMk1 | G.PipesMk2, 1338662, C.progression), + "Building: Valve": ItemData(G.Building | G.PipesMk1 | G.PipesMk2, 1338663, C.useful), + "Building: Stackable Pipeline Support": ItemData(G.Building | G.PipelineSupports, 1338664, C.useful, 0), + "Building: Wall Pipeline Support": ItemData(G.Building | G.PipelineSupports, 1338665, C.useful, 0), + "Building: Pipeline Wall Hole": ItemData(G.Building | G.PipelineSupports, 1338666, C.useful, 0), + "Building: Pipeline Floor Hole": ItemData(G.Building | G.PipelineSupports, 1338667, C.useful, 0), + "Building: Lights Control Panel": ItemData(G.Building | G.Lights, 1338668, C.filler, 0), + "Building: Wall Mounted Flood Light": ItemData(G.Building | G.Lights, 1338669, C.filler, 0), + "Building: Street Light": ItemData(G.Building | G.Lights, 1338670, C.filler, 0), + "Building: Flood Light Tower": ItemData(G.Building | G.Lights, 1338671, C.filler, 0), + "Building: Ceiling Light": ItemData(G.Building | G.Lights, 1338672, C.filler, 0), + "Building: Power Tower": ItemData(G.Building | G.NeverExclude, 1338673, C.useful), + "Building: Walls Orange": ItemData(G.Building | G.Walls, 1338674, C.progression), + "Building: Radar Tower": ItemData(G.Building, 1338675, C.useful), + "Building: Smart Splitter": ItemData(G.Building | G.NeverExclude, 1338676, C.useful), + "Building: Programmable Splitter": ItemData(G.Building | G.NeverExclude, 1338677, C.useful), + "Building: Label Sign Bundle": ItemData(G.Building | G.Signs, 1338678, C.filler, 0), + "Building: Display Sign Bundle": ItemData(G.Building | G.Signs, 1338679, C.filler, 0), + "Building: Billboard Set": ItemData(G.Building | G.Signs, 1338680, C.filler, 0), + # 1338681 Moved to cosmetics - 1.1 + "Building: Metal Pillar": ItemData(G.Pilars, 1338682, C.filler, 0), + "Building: Concrete Pillar": ItemData(G.Pilars, 1338683, C.filler, 0), + "Building: Frame Pillar": ItemData(G.Pilars, 1338684, C.filler, 0), + # 1338685 - 1338691 Moved to cosmetics - 1.1 + "Building: Foundation": ItemData(G.Building | G.Foundations | G.NeverExclude, 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), + "Building: Inverted Corner Ramp Pack": ItemData(G.Foundations, 1338696, C.filler, 0), + "Building: Quarter Pipes Pack": ItemData(G.Foundations, 1338697, C.filler, 0), + "Building: Quarter Pipe Extensions Pack": ItemData(G.Foundations, 1338698, C.filler, 0), + "Building: Frame Foundation": ItemData(G.Foundations, 1338699, C.filler, 0), + "Building: Wall Outlet Mk.1": ItemData(G.Building, 1338700, C.useful), + "Building: Wall Outlet Mk.2": ItemData(G.Building, 1338701, C.useful), + "Building: Wall Outlet Mk.3": ItemData(G.Building, 1338702, C.useful), + "Building: Modern Catwalks": ItemData(G.Building, 1338703, C.filler, 0), + "Building: Industrial Walkways": ItemData(G.Building, 1338704, C.filler, 0), + "Building: Stairs": ItemData(G.Building, 1338705, C.filler, 0), + "Building: Clean Pipeline Mk.1": ItemData(G.Building, 1338706, C.filler, 0), + "Building: Clean Pipeline Mk.2": ItemData(G.Building, 1338707, C.filler, 0), + "Building: Road Barrier": ItemData(G.Building, 1338708, C.filler, 0), + "Building: Modern Railing": ItemData(G.Building, 1338709, C.filler, 0), + "Building: Industrial Railing": ItemData(G.Building, 1338710, C.filler, 0), + "Building: Double Ramp Pack": ItemData(G.Foundations, 1338711, C.filler, 0), + "Building: Conveyor Walls": ItemData(G.Walls, 1338712, C.filler, 0), + "Building: Inverted Ramp Wall Bundle": ItemData(G.Walls, 1338713, C.filler, 0), + "Building: Ramp Wall Bundle": ItemData(G.Walls, 1338714, C.filler, 0), + "Building: Door Walls": ItemData(G.Walls, 1338715, C.filler, 0), + "Building: Tilted Walls": ItemData(G.Walls, 1338716, C.filler, 0), + "Building: Windowed Walls": ItemData(G.Walls, 1338717, C.filler, 0), + "Building: Steel-framed Windows": ItemData(G.Walls, 1338718, C.filler, 0), + "Building: Gates": ItemData(G.Walls, 1338719, C.filler, 0), + "Building: Roofs": ItemData(G.Walls, 1338720, C.filler, 0), + "Building: Roof Corners": ItemData(G.Walls, 1338721, C.filler, 0), + "Building: Converter": ItemData(G.Building, 1338722, C.progression), + "Building: Quantum Encoder": ItemData(G.Building, 1338723, C.progression), + "Building: Portal": ItemData(G.Building, 1338724, C.useful), + "Building: Conveyor Mk.6": ItemData(G.Building | G.ConveyorMk6, 1338725, C.progression), + "Building: Conveyor Lift Mk.6": ItemData(G.Building | G.ConveyorMk6, 1338726, C.useful), + "Building: Alien Power Augmenter": ItemData(G.Building, 1338727, C.progression), + "Building: Dimensional Depot Uploader": ItemData(G.Building, 1338728, C.useful), +# Added in 1.1 + "Building: Priority Merger": ItemData(G.Building | G.NeverExclude, 1338729, C.useful), + "Building: Conveyor Wall Hole": ItemData(G.Building, 1338730, C.useful, 0), + "Building: Conveyor Throughput Monitor": ItemData(G.Building, 1338731, C.useful, 0), + "Building: Basic Shelf Unit": ItemData(G.Building, 1338732, C.useful, 0), + "Building: Beam Expansion Pack": ItemData(G.Beams, 1338733, C.filler, 0), + "Building: Ventilation Bundle": ItemData(G.Building, 1338734, C.filler, 0), +### + # 1338729 - 1338749 Reserved for buildings + "Customizer: Asphalt Foundation Material": ItemData(G.Customizer | G.Foundations, 1338750, C.filler, 0), + "Customizer: Concrete Foundation Material": ItemData(G.Customizer | G.Foundations, 1338751, C.filler, 0), + "Customizer: Concrete Wall Material": ItemData(G.Customizer | G.Walls, 1338752, C.filler, 0), + "Customizer: Glass Roof Material": ItemData(G.Customizer | G.Walls, 1338753, C.filler, 0), + "Customizer: Grip Metal Foundation Material": ItemData(G.Customizer | G.Foundations, 1338754, C.filler, 0), + "Customizer: Coated Concrete Foundation Material": ItemData(G.Customizer | G.Foundations, 1338755, C.filler, 0), + "Customizer: Metal Roof Material": ItemData(G.Customizer | G.Walls, 1338756, C.filler, 0), + "Customizer: Steel Wall Material": ItemData(G.Customizer | G.Walls, 1338757, C.filler, 0), + "Customizer: Tar Roof Material": ItemData(G.Customizer | G.Walls, 1338758, C.filler, 0), + "Customizer: Arrow Patterns": ItemData(G.Customizer | G.Foundations, 1338759, C.filler, 0), + "Customizer: Dotted Line Patterns": ItemData(G.Customizer | G.Foundations, 1338760, C.filler, 0), + "Customizer: Solid Line Patterns": ItemData(G.Customizer | G.Foundations, 1338761, C.filler, 0), + "Customizer: Factory Icon Patterns": ItemData(G.Customizer | G.Foundations, 1338762, C.filler, 0), + "Customizer: Transportation Icon Patterns": ItemData(G.Customizer | G.Foundations, 1338763, C.filler, 0), + "Customizer: Number Patterns": ItemData(G.Customizer | G.Foundations, 1338764, C.filler, 0), + "Customizer: Pathway Patterns": ItemData(G.Customizer | G.Foundations, 1338765, C.filler, 0), + "Customizer: Factory Zone Patterns": ItemData(G.Customizer | G.Foundations, 1338766, C.filler, 0), + "Customizer: Steel-Framed Windows": ItemData(G.Customizer | G.Walls, 1338767, C.filler, 0), + "Customizer: Construction Fences": ItemData(G.Customizer, 1338768, C.filler, 0), + "Customizer: Unpainted Finish": ItemData(G.Customizer, 1338769, C.filler, 0), + "Customizer: Copper Paint Finish": ItemData(G.Customizer, 1338770, C.filler, 0), + "Customizer: Chrome Paint Finish": ItemData(G.Customizer, 1338771, C.filler, 0), + "Customizer: Carbon Steel Finish": ItemData(G.Customizer, 1338772, C.filler, 0), + "Customizer: Caterium Paint Finish": ItemData(G.Customizer, 1338773, C.filler, 0), + + # 1338776 - 1338799 Reserved for Cosmetics + + # Transports 1338800 - 1338899 + # Drones (including Drone) + "Transport: Drones": ItemData(G.Transport, 1338800, C.useful), + # Trains (including Empty Platform, rails, station, locomotive, train stop) # 1.1 + "Transport: Trains": ItemData(G.Transport | G.Trains, 1338801, C.useful), + "Transport: Fluid Trains": ItemData(G.Transport | G.Trains, 1338802, C.useful), + # Tracker / Truck (including truck station) + "Transport: Tractor": ItemData(G.Transport | G.Vehicles, 1338803, C.useful), + "Transport: Truck": ItemData(G.Transport | G.Vehicles, 1338804, C.useful), + "Transport: Explorer": ItemData(G.Transport | G.Vehicles, 1338805, C.useful), + "Transport: Factory Cart": ItemData(G.Transport | G.Vehicles, 1338806, C.useful), + "Transport: Factory Cart (golden)": ItemData(G.Transport | G.Vehicles, 1338807, C.filler), + "Transport: Cyber Wagon": ItemData(G.Transport | G.Vehicles | G.Trap, 1338808, C.filler), + # Hypertubes (including supports / pipes / entrance / holes) + "Transport: Hypertube": ItemData(G.Transport | G.HyperTubes, 1338809, C.useful), + "Transport: Hypertube Floor Hole": ItemData(G.Transport | G.HyperTubes, 1338810, C.filler), + "Transport: Hypertube Wall Support": ItemData(G.Transport | G.HyperTubes, 1338811, C.filler), + "Transport: Hypertube Wall Hole": ItemData(G.Transport | G.HyperTubes, 1338812, C.filler), + # Personal Elevator (including additional floors) + "Transport: Personal Elevator": ItemData(G.Transport, 1338813, C.useful), # 1.1 + + # 1338900 - 1338998 Handled by trap system (includes a few non-trap things) + # Regenerate via /Script/Blutility.EditorUtilityWidgetBlueprint'/Archipelago/Debug/EU_GenerateTrapIds.EU_GenerateTrapIds' + "Trap: Hog": ItemData(G.Trap, 1338900, C.trap), + "Trap: Alpha Hog": ItemData(G.Trap, 1338901, C.trap), + "Trap: Johnny": ItemData(G.Trap, 1338902, C.trap), + "Trap: Cliff Hog": ItemData(G.Trap, 1338903, C.trap), + "Trap: Nuclear Hog": ItemData(G.Trap, 1338904, C.trap), + "Trap: Not the Bees": ItemData(G.Trap, 1338905, C.trap), + "Trap: Hatcher": ItemData(G.Trap, 1338906, C.trap), + "Trap: Doggo with Pulse Nobelisk": ItemData(G.Trap, 1338907, C.trap), + "Trap: Doggo with Nuke Nobelisk": ItemData(G.Trap, 1338908, C.trap), + "Doggo with Power Slug": ItemData(G.Parts, 1338909, C.filler), + "Trap: Doggo with Gas Nobelisk": ItemData(G.Trap, 1338910, C.trap), + "Trap: Spore Flower": ItemData(G.Trap, 1338911, C.trap), + "Trap: Stinger": ItemData(G.Trap, 1338912, C.trap), + "Trap: Gas Stinger": ItemData(G.Trap, 1338913, C.trap), + "Trap: Small Stinger": ItemData(G.Trap, 1338914, C.trap), + "Trap: Spitter": ItemData(G.Trap, 1338915, C.trap), + "Trap: Alpha Spitter": ItemData(G.Trap, 1338916, C.trap), + "Trap: Nuclear Waste Drop": ItemData(G.Trap, 1338917, C.trap), + "Trap: Plutonium Waste Drop": ItemData(G.Trap, 1338918, C.trap), + "Trap: Elite Hatcher": ItemData(G.Trap, 1338919, C.trap), + "Trap: Can of Beans": ItemData(G.Trap, 1338920, C.trap), + "Trap: Fart Cloud": ItemData(G.Trap, 1338921, C.trap), + + "Building: Space Elevator": ItemData(G.Building, 1338999, C.progression), + + # Resource singles + "Single: Adaptive Control Unit": ItemData(G.Parts, 1339000, count=0), + "Single: AI Limiter": ItemData(G.Parts, 1339001, count=0), + "Single: Alclad Aluminum Sheet": ItemData(G.Parts, 1339002, count=0), + "Single: Blue Power Slug": ItemData(G.Parts, 1339003, count=0), + "Single: Yellow Power Slug": ItemData(G.Parts, 1339004, count=0), + "Single: Alien Protein": ItemData(G.Parts, 1339005, count=0), + "Single: Purple Power Slug": ItemData(G.Parts, 1339006, count=0), + "Single: Aluminum Casing": ItemData(G.Parts, 1339007, count=0), + "Single: Aluminum Ingot": ItemData(G.Parts, 1339008, count=0), + "Single: Aluminum Scrap": ItemData(G.Parts, 1339009, count=0), + "Single: Assembly Director System": ItemData(G.Parts, 1339010, count=0), + "Single: Automated Wiring": ItemData(G.Parts, 1339011, count=0), + "Single: Battery": ItemData(G.Parts, 1339012, count=0), + "Single: Bauxite": ItemData(G.Parts, 1339013, count=0), + "Single: Neural-Quantum Processor": ItemData(G.Parts, 1339014, count=0), + "Single: Biomass": ItemData(G.Parts, 1339015, count=0), + "Single: Black Powder": ItemData(G.Parts, 1339016, count=0), + "Single: Cable": ItemData(G.Parts, 1339017, count=0), + "Single: Caterium Ingot": ItemData(G.Parts, 1339018, count=0), + "Single: Caterium Ore": ItemData(G.Parts, 1339019, count=0), + "Single: Circuit Board": ItemData(G.Parts, 1339020, count=0), + "Single: Coal": ItemData(G.Parts, 1339021, count=0), + "Single: Singularity Cell": ItemData(G.Parts, 1339022, count=0), + "Single: Compacted Coal": ItemData(G.Parts, 1339023, count=0), + "Single: Computer": ItemData(G.Parts, 1339024, count=0), + "Single: Concrete": ItemData(G.Parts, 1339025, count=0), + "Single: Cooling System": ItemData(G.Parts, 1339026, count=0), + "Single: Copper Ingot": ItemData(G.Parts, 1339027, count=0), + "Single: Copper Ore": ItemData(G.Parts, 1339028, count=0), + "Single: Copper Powder": ItemData(G.Parts, 1339029, count=0), + "Single: Copper Sheet": ItemData(G.Parts, 1339030, count=0), + "Single: Adequate Pioneering Statue": ItemData(G.Parts, 1339031, count=0), + "Single: Crystal Oscillator": ItemData(G.Parts, 1339032, count=0), + "Single: Electromagnetic Control Rod": ItemData(G.Parts, 1339033, count=0), + "Single: Empty Canister": ItemData(G.Parts, 1339034, count=0), + "Single: Empty Fluid Tank": ItemData(G.Parts, 1339035, count=0), + "Single: Encased Industrial Beam": ItemData(G.Parts, 1339036, count=0), + "Single: Encased Plutonium Cell": ItemData(G.Trap, 1339037, C.trap, count=0), + "Single: Encased Uranium Cell": ItemData(G.Trap, 1339038, C.trap, count=0), + "Single: Fabric": ItemData(G.Parts, 1339039, count=0), + "Single: FICSIT Coupon": ItemData(G.Parts, 1339040, count=0), + "Single: AI Expansion Server": ItemData(G.Parts, 1339041, count=0), + "Single: Fused Modular Frame": ItemData(G.Parts, 1339042, count=0), + "Single: Hard Drive": ItemData(G.Parts, 1339043, count=0), + "Single: Heat Sink": ItemData(G.Parts, 1339044, count=0), + "Single: Heavy Modular Frame": ItemData(G.Parts, 1339045, count=0), + "Single: High-Speed Connector": ItemData(G.Parts, 1339046, count=0), + "Single: Satisfactory Pioneering Statue": ItemData(G.Parts, 1339047, count=0), + "Single: Pretty Good Pioneering Statue": ItemData(G.Parts, 1339048, count=0), + "Single: Iron Ingot": ItemData(G.Parts, 1339049, count=0), + "Single: Iron Ore": ItemData(G.Parts, 1339050, count=0), + "Single: Iron Plate": ItemData(G.Parts, 1339051, count=0), + "Single: Iron Rod": ItemData(G.Parts, 1339052, count=0), + "Single: Golden Nut Statue": ItemData(G.Parts, 1339053, count=0), + "Single: Leaves": ItemData(G.Parts, 1339054, count=0), + "Single: Limestone": ItemData(G.Parts, 1339055, count=0), + "Single: Magnetic Field Generator": ItemData(G.Parts, 1339056, count=0), + "Single: Mercer Sphere": ItemData(G.Parts, 1339057, count=0), + "Single: Modular Engine": ItemData(G.Parts, 1339058, count=0), + "Single: Modular Frame": ItemData(G.Parts, 1339059, count=0), + "Single: Motor": ItemData(G.Parts, 1339060, count=0), + "Single: Mycelia": ItemData(G.Parts, 1339061, count=0), + "Single: Non-fissile Uranium": ItemData(G.Trap, 1339062, C.trap, count=0), + "Single: Nuclear Pasta": ItemData(G.Parts, 1339063, count=0), + "Single: Lizard Doggo Statue": ItemData(G.Parts, 1339064, count=0), + "Single: Organic Data Capsule": ItemData(G.Parts, 1339065, count=0), + "Single: Packaged Alumina Solution": ItemData(G.Parts, 1339066, count=0), + "Single: Packaged Fuel": ItemData(G.Parts, 1339067, count=0), + "Single: Packaged Heavy Oil Residue": ItemData(G.Parts, 1339068, count=0), + "Single: Packaged Liquid Biofuel": ItemData(G.Parts, 1339069, count=0), + "Single: Packaged Nitric Acid": ItemData(G.Parts, 1339070, count=0), + "Single: Packaged Nitrogen Gas": ItemData(G.Parts, 1339071, count=0), + "Single: Packaged Oil": ItemData(G.Parts, 1339072, count=0), + "Single: Packaged Sulfuric Acid": ItemData(G.Parts, 1339073, count=0), + "Single: Packaged Turbofuel": ItemData(G.Parts, 1339074, count=0), + "Single: Packaged Water": ItemData(G.Parts, 1339075, count=0), + "Single: Petroleum Coke": ItemData(G.Parts, 1339076, count=0), + "Single: Plastic": ItemData(G.Parts, 1339077, count=0), + "Single: Plutonium Fuel Rod": ItemData(G.Trap, 1339078, C.trap, count=0), + "Single: Plutonium Pellet": ItemData(G.Trap, 1339079, C.trap, count=0), + "Single: Plutonium Waste": ItemData(G.Trap, 1339080, C.trap, count=0), + "Single: Polymer Resin": ItemData(G.Parts, 1339081, count=0), + "Single: Power Shard": ItemData(G.Parts, 1339082, count=0), + "Single: Confusing Creature Statue": ItemData(G.Parts, 1339083, count=0), + "Single: Pressure Conversion Cube": ItemData(G.Parts, 1339084, count=0), + "Single: Alien Power Matrix": ItemData(G.Parts, 1339085, count=0), + "Single: Quartz Crystal": ItemData(G.Parts, 1339086, count=0), + "Single: Quickwire": ItemData(G.Parts, 1339087, count=0), + "Single: Radio Control Unit": ItemData(G.Parts, 1339088, count=0), + "Single: Raw Quartz": ItemData(G.Parts, 1339089, count=0), + "Single: Reinforced Iron Plate": ItemData(G.Parts, 1339090, count=0), + "Single: Rotor": ItemData(G.Parts, 1339091, count=0), + "Single: Rubber": ItemData(G.Parts, 1339092, count=0), + "Single: SAM": ItemData(G.Parts, 1339093, count=0), + "Single: Screw": ItemData(G.Parts, 1339094, count=0), + "Single: Silica": ItemData(G.Parts, 1339095, count=0), + "Single: Smart Plating": ItemData(G.Parts, 1339096, count=0), + "Single: Smokeless Powder": ItemData(G.Parts, 1339097, count=0), + "Single: Solid Biofuel": ItemData(G.Parts, 1339098, count=0), + "Single: Somersloop": ItemData(G.Parts, 1339099, count=0), + "Single: Stator": ItemData(G.Parts, 1339100, count=0), + "Single: Silver Hog Statue": ItemData(G.Parts, 1339101, count=0), + "Single: Steel Beam": ItemData(G.Parts, 1339102, count=0), + "Single: Steel Ingot": ItemData(G.Parts, 1339103, count=0), + "Single: Steel Pipe": ItemData(G.Parts, 1339104, count=0), + "Single: Sulfur": ItemData(G.Parts, 1339105, count=0), + "Single: Supercomputer": ItemData(G.Parts, 1339106, count=0), + "Single: Superposition Oscillator": ItemData(G.Parts, 1339107, count=0), + "Single: Thermal Propulsion Rocket": ItemData(G.Parts, 1339108, count=0), + "Single: Turbo Motor": ItemData(G.Parts, 1339109, count=0), + "Single: Hog Remains": ItemData(G.Parts, 1339110, count=0), + "Single: Uranium": ItemData(G.Trap, 1339111, C.trap, count=0), + "Single: Uranium Fuel Rod": ItemData(G.Trap, 1339112, C.trap, count=0), + "Single: Uranium Waste": ItemData(G.Trap, 1339113, C.trap, count=0), + "Single: Versatile Framework": ItemData(G.Parts, 1339114, count=0), + "Single: Wire": ItemData(G.Parts, 1339115, count=0), + "Single: Wood": ItemData(G.Parts, 1339116, count=0), + "Single: Plasma Spitter Remains": ItemData(G.Parts, 1339117, count=0), + "Single: Stinger Remains": ItemData(G.Parts, 1339118, count=0), + "Single: Hatcher Remains": ItemData(G.Parts, 1339119, count=0), + "Single: Alien DNA Capsule": ItemData(G.Parts, 1339120, count=0), + "Single: Diamonds": ItemData(G.Parts, 1339121, count=0), + "Single: Time Crystal": ItemData(G.Parts, 1339122, count=0), + "Single: Ficsite Ingot": ItemData(G.Parts, 1339123, count=0), + "Single: Ficsite Trigon": ItemData(G.Parts, 1339124, count=0), + "Single: Reanimated SAM": ItemData(G.Parts, 1339125, count=0), + "Single: SAM Fluctuator": ItemData(G.Parts, 1339126, count=0), + "Single: Biochemical Sculptor": ItemData(G.Parts, 1339127, count=0), + "Single: Ballistic Warp Drive": ItemData(G.Parts, 1339128, count=0), + "Single: Ficsonium": ItemData(G.Trap, 1339129, C.trap, count=0), + "Single: Ficsonium Fuel Rod": ItemData(G.Trap, 1339130, C.trap, count=0), + "Single: Packaged Rocket Fuel": ItemData(G.Parts, 1339131, count=0), + "Single: Packaged Ionized Fuel": ItemData(G.Parts, 1339132, count=0), + "Single: Dark Matter Crystal": ItemData(G.Parts, 1339133, count=0), + # 1339134 - 1339149 Reserved for future parts + # 1339150 - 1339199 Equipment / Ammo + "Single: Bacon Agaric": ItemData(G.Ammo, 1339150, count=0), + "Single: Beryl Nut": ItemData(G.Ammo, 1339151, count=0), + "Single: Blade Runners": ItemData(G.Equipment, 1339152), + "Single: Boom Box": ItemData(G.Equipment, 1339153), + "Single: Chainsaw": ItemData(G.Equipment, 1339154, C.useful), + "Single: Cluster Nobelisk": ItemData(G.Ammo, 1339155, count=0), + "Single: Iodine-Infused Filter": ItemData(G.Equipment, 1339156, count=0), # 1.1 + "Single: Cup": ItemData(G.Equipment, 1339157), + "Single: Cup (gold)": ItemData(G.Equipment, 1339158, count=0), + "Single: Explosive Rebar": ItemData(G.Ammo, 1339159, count=0), + "Single: Factory Cart": ItemData(G.Equipment, 1339160, C.useful), + "Single: Factory Cart (golden)": ItemData(G.Equipment, 1339161, count=0), + "Single: Gas Mask": ItemData(G.Equipment, 1339162, C.useful), + "Single: Gas Nobelisk": ItemData(G.Ammo, 1339163, count=0), + "Single: Hazmat Suit": ItemData(G.Equipment, 1339164, C.useful), + "Single: Homing Rifle Ammo": ItemData(G.Ammo, 1339165, count=0), + "Single: Hoverpack": ItemData(G.Equipment, 1339166, C.useful), + "Single: Iron Rebar": ItemData(G.Ammo, 1339167, count=0), + "Single: Jetpack": ItemData(G.Equipment, 1339168, C.useful), + "Single: Medicinal Inhaler": ItemData(G.Ammo, 1339169, count=0), + "Single: Nobelisk": ItemData(G.Ammo, 1339170, count=0), + "Single: Nobelisk Detonator": ItemData(G.Equipment, 1339171, C.useful), + "Single: Nuke Nobelisk": ItemData(G.Ammo, 1339172, count=0), + "Single: Object Scanner": ItemData(G.Equipment, 1339173), + "Single: Paleberry": ItemData(G.Ammo, 1339174, count=0), + "Single: Parachute": ItemData(G.Equipment, 1339175, C.useful), + "Single: Pulse Nobelisk": ItemData(G.Ammo, 1339176, count=0), + "Single: Rebar Gun": ItemData(G.Equipment, 1339177, C.useful), + "Single: Rifle": ItemData(G.Equipment, 1339178, C.useful), + "Single: Rifle Ammo": ItemData(G.Ammo, 1339179, count=0), + "Single: Shatter Rebar": ItemData(G.Ammo, 1339180, count=0), + "Single: Stun Rebar": ItemData(G.Ammo, 1339181, count=0), + "Single: Turbo Rifle Ammo": ItemData(G.Ammo, 1339182, count=0), + "Single: Xeno-Basher": ItemData(G.Equipment, 1339183, C.useful), + "Single: Xeno-Zapper": ItemData(G.Equipment, 1339184, C.useful), + "Single: Zipline": ItemData(G.Equipment, 1339185, C.useful), + "Single: Portable Miner": ItemData(G.Equipment, 1339186), + "Single: Gas Filter": ItemData(G.Equipment, 1339187, count=0) + } + + item_names_and_ids: ClassVar[dict[str, int]] = {name: data.code for name, data in item_data.items()} + filler_items: ClassVar[tuple[str, ...]] = tuple(item for item, details in item_data.items() + if details.count > 0 and details.category & (G.Parts | G.Ammo)) + + @classmethod + def get_item_names_per_category(cls, game_logic: GameLogic) -> dict[str, set[str]]: + groups: dict[str, set[str]] = {} + + # To allow hinting for first part recipe in logic + for part, recipes in game_logic.recipes.items(): + recipes_for_part: set[str] = {recipe.name for recipe in recipes if not recipe.implicitly_unlocked} + if recipes_for_part: + + for original, indirect in game_logic.indirect_recipes.items(): + if indirect in recipes_for_part: + recipes_for_part.remove(indirect) + recipes_for_part.add(original) + + groups[part] = recipes_for_part + + for name, data in cls.item_data.items(): + for category in data.category: + if category != G.NeverExclude: + groups.setdefault(category.name, set()).add(name) + + return groups + + player: int + logic: GameLogic + random: Random + critical_path: CriticalPathCalculator + + trap_chance: int + enabled_traps: tuple[str, ...] + + def __init__(self, player: Optional[int], logic: GameLogic, random: Random, + options: SatisfactoryOptions, critical_path: CriticalPathCalculator): + self.player = player + self.logic = logic + self.random = random + self.critical_path = critical_path + self.options = options + + self.trap_chance = self.options.trap_chance.value + self.enabled_traps = tuple(self.options.trap_selection_override.value) + + @classmethod + def create_item_uninitialized(cls, name: str, player: int) -> Item: + data: ItemData = cls.item_data[name] + return Item(name, data.type, data.code, player) + + def create_item(self, name: str, player: int) -> Item: + data: ItemData = self.item_data[name] + item_type = data.type + + if item_type == C.progression \ + and (data.category & (G.Recipe | G.Building)) and not (data.category & G.NeverExclude) \ + and self.critical_path.required_item_names and name not in self.critical_path.required_item_names: + item_type = C.useful + + return Item(name, item_type, data.code, player) + + @classmethod + def get_filler_item_name_uninitialized(cls, random: Random) -> str: + return random.choice(cls.filler_items) + + def get_filler_item_name(self, random: Random, filler_items: Sequence[str] | None) -> str: + if self.enabled_traps and random.random() < (self.trap_chance / 100): + return random.choice(self.enabled_traps) + else: + if filler_items: + return random.choice(filler_items) + else: + return Items.get_filler_item_name_uninitialized(random) + + def get_excluded_items(self, precollected_items: list[Item]) -> set[str]: + excluded_items: set[str] = { + item.name + for item in precollected_items + if item.name in self.item_data and item.name not in self.options.start_inventory_from_pool.value + } + + excluded_items.update({"Building: " + building for building in self.critical_path.buildings_to_exclude}) + excluded_items.update({"Bundle: " + part for part in self.critical_path.parts_to_exclude}) + excluded_items.update({"Single: " + 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(self.critical_path.implicitly_unlocked) + + # since we dont have part logic setup for Transports + if self.options.final_elevator_phase == 1: + excluded_items.add("Transport: Drones") + + # Remove excluded items that arent unique + excluded_items = excluded_items - { + item_name + for item_name, data in self.item_data.items() + if data.category & (G.Parts | G.Equipment | G.Ammo | G.Trap | G.Upgrades) + } + + return excluded_items + + def build_item_pool(self, random: Random, precollected_items: list[Item], number_of_locations: int) -> list[Item]: + excluded_from_pool: set[str] = self.get_excluded_items(precollected_items) + + pool_items: list[Item] = [ + self.create_item(name, self.player) + for name, data in self.item_data.items() + for _ in range(data.count) + if data.category & (G.Recipe | G.Building | G.Equipment | G.Ammo | G.Transport | G.Upgrades) + and name not in excluded_from_pool + ] + pool: list[Item] = [ + item + for item in pool_items + if item.classification != C.filler or self.item_data[item.name].category & (G.Equipment | G.Ammo) + ] + + free_space: int = number_of_locations - len(pool) + if free_space < 0: + raise Exception(f"Location pool starved, trying to add {len(pool)} items to {number_of_locations} locations") + + non_excluded_filler_items: list[str] = [item for item in self.filler_items if item not in excluded_from_pool] + pool += [ + self.create_item(self.get_filler_item_name(random, non_excluded_filler_items), self.player) + for _ in range(free_space) + ] + + return pool diff --git a/worlds/satisfactory/Locations.py b/worlds/satisfactory/Locations.py new file mode 100644 index 0000000000..0174b178f8 --- /dev/null +++ b/worlds/satisfactory/Locations.py @@ -0,0 +1,426 @@ +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 .CriticalPathCalculator import CriticalPathCalculator + + +class LocationData: + __slots__ = ("region", "name", "event_name", "code", "non_progression", "rule") + region: str + name: str + event_name: str + code: Optional[int] + non_progression: Optional[bool] + rule: Optional[Callable[[CollectionState], bool]] + + def __init__(self, region: str, name: str, code: Optional[int], event_name: Optional[str] = None, + non_progression: Optional[bool] = False, rule: Optional[Callable[[CollectionState], bool]] = None): + self.region = region + self.name = name + self.code = code + self.rule = rule + self.non_progression = non_progression + self.event_name = event_name or name + + +class Part(LocationData): + @staticmethod + def get_parts(state_logic: StateLogic, recipes: tuple[Recipe, ...], name: str, + final_elevator_phase: int) -> list[LocationData]: + recipes_per_region: dict[str, list[Recipe]] = {} + + for recipe in recipes: + if recipe.minimal_phase > final_elevator_phase: + continue + + recipes_per_region.setdefault(recipe.building or "Overworld", []).append(recipe) + + 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): + super().__init__(region, part_event_prefix + name + " in " + region, EventId, part_event_prefix + name, + rule=Part.can_produce_any_recipe_for_part(state_logic, recipes)) + + @staticmethod + def can_produce_any_recipe_for_part(state_logic: StateLogic, recipes: Iterable[Recipe]) \ + -> Callable[[CollectionState], bool]: + + recipe_rules = tuple(state_logic.get_can_produce_specific_recipe_for_part_rule(recipe) for recipe in recipes) + + def can_build_by_any_recipe(state: CollectionState) -> bool: + return any(rule(state) for rule in recipe_rules) + + return can_build_by_any_recipe + + +class EventBuilding(LocationData): + def __init__(self, state_logic: StateLogic, building_name: str, building: Building): + super().__init__("Overworld", building_event_prefix + building_name, EventId, + rule=EventBuilding.get_can_create_building_rule(state_logic, building)) + + @staticmethod + def get_can_create_building_rule(state_logic: StateLogic, building: Building) \ + -> Callable[[CollectionState], bool]: + handcrafting_rule = state_logic.get_can_produce_all_allowing_handcrafting_rule(building.inputs) + + def can_build(state: CollectionState) -> bool: + return state_logic.has_recipe(state, building) \ + and state_logic.can_power(state, building.power_requirement) \ + and handcrafting_rule(state) + + return can_build + + +class PowerInfrastructure(LocationData): + def __init__(self, state_logic: StateLogic, + power_level: PowerInfrastructureLevel, recipes: Iterable[Recipe]): + super().__init__("Overworld", building_event_prefix + power_level.to_name(), EventId, + rule=PowerInfrastructure.get_can_create_power_infrastructure_rule(state_logic, power_level, recipes)) + + @staticmethod + def get_can_create_power_infrastructure_rule(state_logic: StateLogic, + power_level: PowerInfrastructureLevel, recipes: Iterable[Recipe])\ + -> Callable[[CollectionState], bool]: + + higher_levels = tuple(level for level in PowerInfrastructureLevel if level > power_level) + + def can_power(state: CollectionState) -> bool: + return any(state_logic.can_power(state, higher_level) for higher_level in higher_levels) \ + or any(state_logic.can_build(state, recipe.building) for recipe in recipes) + + return can_power + + +class ElevatorPhase(LocationData): + def __init__(self, phase_index: int, state_logic: StateLogic, game_logic: GameLogic): + super().__init__("Overworld", f"Elevator Phase {phase_index + 1}", EventId, + rule=lambda state: state_logic.can_build(state, "Space Elevator") and + state_logic.can_produce_all(state, game_logic.space_elevator_phases[phase_index].keys())) + + +class HubSlot(LocationData): + def __init__(self, tier: int, milestone: int, slot: int, location_id: int): + super().__init__(f"Hub {tier}-{milestone}", f"Hub {tier}-{milestone}, item {slot}", location_id) + + +class MamSlot(LocationData): + def __init__(self, tree: str, node_name: str, location_id: int): + super().__init__(f"{tree}: {node_name}", f"{tree}: {node_name}", location_id) + + +class ShopSlot(LocationData): + def __init__(self, state_logic: Optional[StateLogic], slot: int, cost: int, location_id: int): + super().__init__("AWESOME Shop", f"AWESOME Shop purchase {slot}", location_id, + rule=ShopSlot.can_purchase_from_shop(state_logic, cost)) + + @staticmethod + def can_purchase_from_shop(state_logic: Optional[StateLogic], cost: int) -> Callable[[CollectionState], bool]: + def can_purchase(state: CollectionState) -> bool: + if not state_logic or cost < 20: + return True + elif 20 <= cost < 50: + return state_logic.is_elevator_phase(state, 1) + elif 50 <= cost < 100: + return state_logic.is_elevator_phase(state, 2) + else: + return state_logic.is_elevator_phase(state, 3) + + return can_purchase + + +class HardDrive(LocationData): + def __init__(self, data: DropPodData, state_logic: Optional[StateLogic], + location_id: int, tier: int, can_hold_progression: bool): + + # drop pod locations are unlocked by hard drives, there is currently no direct mapping between location and hard drive + # 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: + return f"Hub Tier {tier}" + + def get_rule(unlocked_by: Optional[str], power_needed: int) -> Callable[[CollectionState], bool]: + # Power is kept out of logic. with energy link its simple, + # without you just going to have to figure it your yourself + + def logic_rule(state: CollectionState) -> bool: + return state_logic.can_build(state, "MAM") and ( + (not unlocked_by) or (state_logic and state_logic.can_produce(state, unlocked_by))) + + return logic_rule + + super().__init__( + get_region(data.gassed, data.radioactive), + f"Hard drive random check {(location_id - 1338600) + 1}", location_id, + non_progression=not can_hold_progression, rule=get_rule(data.item, data.power)) + + +class Locations: + game_logic: Optional[GameLogic] + 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 + max_milestones: ClassVar[int] = 5 + max_slots: ClassVar[int] = 10 + drop_pod_location_id_start: ClassVar[int] = 1338600 + 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, + 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, 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), + MamSlot("Alien Organisms", "Bio-Organic Properties", 1338503), + MamSlot("Alien Organisms", "Stinger Research", 1338504), + MamSlot("Alien Organisms", "Hatcher Research", 1338505), + MamSlot("Alien Organisms", "Hog Research", 1338506), + MamSlot("Alien Organisms", "Spitter Research", 1338507), + MamSlot("Alien Organisms", "Structural Analysis", 1338508), + MamSlot("Alien Organisms", "Protein Inhaler", 1338509), + MamSlot("Alien Organisms", "The Rebar Gun", 1338510), + MamSlot("Caterium", "Caterium Electronics", 1338511), + MamSlot("Caterium", "Bullet Guidance System", 1338512), + MamSlot("Caterium", "High-Speed Connector", 1338513), + MamSlot("Caterium", "Caterium", 1338514), + MamSlot("Caterium", "Caterium Ingots", 1338515), + MamSlot("Caterium", "Quickwire", 1338516), + MamSlot("Caterium", "Power Switch", 1338517), + MamSlot("Caterium", "Power Poles Mk.2", 1338518), + MamSlot("Caterium", "AI Limiter", 1338519), + MamSlot("Caterium", "Smart Splitter", 1338520), + MamSlot("Caterium", "Programmable Splitter", 1338521), + MamSlot("Mycelia", "Gas Mask", 1338522), # 1.0 + MamSlot("Caterium", "Zipline", 1338523), + MamSlot("Caterium", "Geothermal Generator", 1338524), + MamSlot("Caterium", "Priority Power Switch", 1338525), + MamSlot("Caterium", "Stun Rebar", 1338526), + MamSlot("Caterium", "Power Poles Mk.3", 1338527), + MamSlot("Mycelia", "Therapeutic Inhaler", 1338528), + MamSlot("Mycelia", "Expanded Toolbelt", 1338529), + MamSlot("Mycelia", "Mycelia", 1338530), + MamSlot("Mycelia", "Fabric", 1338531), + MamSlot("Mycelia", "Medical Properties", 1338532), + MamSlot("Mycelia", "Toxic Cellular Modification", 1338533), + MamSlot("Mycelia", "Vitamin Inhaler", 1338534), + MamSlot("Mycelia", "Parachute", 1338535), + MamSlot("Mycelia", "Synthethic Polyester Fabric", 1338536), + MamSlot("Nutrients", "Bacon Agaric", 1338537), + MamSlot("Nutrients", "Beryl Nut", 1338538), + MamSlot("Nutrients", "Paleberry", 1338539), + MamSlot("Nutrients", "Nutritional Processor", 1338540), + MamSlot("Nutrients", "Nutritional Inhaler", 1338541), + MamSlot("Power Slugs", "Slug Scanning", 1338542), + MamSlot("Power Slugs", "Blue Power Slugs", 1338543), + MamSlot("Power Slugs", "Yellow Power Shards", 1338544), + MamSlot("Power Slugs", "Purple Power Shards", 1338545), + MamSlot("Power Slugs", "Overclock Production", 1338546), + MamSlot("Quartz", "Crystal Oscillator", 1338547), + MamSlot("Quartz", "Quartz Crystals", 1338548), + MamSlot("Quartz", "Quartz", 1338549), + MamSlot("Quartz", "Shatter Rebar", 1338550), + MamSlot("Quartz", "Silica", 1338551), + MamSlot("Quartz", "Explosive Resonance Application", 1338552), + MamSlot("Quartz", "Blade Runners", 1338553), + MamSlot("Quartz", "The Explorer", 1338554), + MamSlot("Quartz", "Radio Signal Scanning", 1338555), + MamSlot("Quartz", "Inflated Pocket Dimension", 1338556), + MamSlot("Quartz", "Radar Technology", 1338557), + MamSlot("Sulfur", "The Nobelisk Detonator", 1338558), + MamSlot("Sulfur", "Smokeless Powder", 1338559), + MamSlot("Sulfur", "Sulfur", 1338560), + MamSlot("Sulfur", "Inflated Pocket Dimension", 1338561), + MamSlot("Sulfur", "The Rifle", 1338562), + MamSlot("Sulfur", "Compacted Coal", 1338563), + MamSlot("Sulfur", "Black Powder", 1338564), + MamSlot("Sulfur", "Explosive Rebar", 1338565), + MamSlot("Sulfur", "Cluster Nobelisk", 1338566), + MamSlot("Sulfur", "Experimental Power Generation", 1338567), + # 1338568 Turbo Rifle Ammo + MamSlot("Sulfur", "Turbo Fuel", 1338569), + MamSlot("Sulfur", "Expanded Toolbelt", 1338570), + # 1338571 Nuclear Deterrent Development + # 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), + MamSlot("Alien Technology", "Mercer Sphere Analysis", 1338578), + MamSlot("Alien Technology", "Dimensional Depot", 1338579), + MamSlot("Alien Technology", "Manual Depot Uploader", 1338580), + MamSlot("Alien Technology", "Depot Expansion (200%)", 1338581), + MamSlot("Alien Technology", "Depot Expansion (300%)", 1338582), + MamSlot("Alien Technology", "Depot Expansion (400%)", 1338583), + MamSlot("Alien Technology", "Depot Expansion (500%)", 1338584), + MamSlot("Alien Technology", "Upload Upgrade: 30/min", 1338585), + MamSlot("Alien Technology", "Upload Upgrade: 60/min", 1338586), + MamSlot("Alien Technology", "Upload Upgrade: 120/min", 1338587), + MamSlot("Alien Technology", "Upload Upgrade: 240/min", 1338588), + MamSlot("Alien Technology", "Somersloop Analysis", 1338589), + MamSlot("Alien Technology", "Alien Energy Harvesting", 1338590), + MamSlot("Alien Technology", "Production Amplifier", 1338591), + MamSlot("Alien Technology", "Power Augmenter", 1338592), + # 1338593 Alien Power Matrix + MamSlot("Quartz", "Material Resonance Screening", 1338594), # 1.1 + # 1338600 - 1338699 - Harddrives - Harddrives + ShopSlot(self.state_logic, 1, 3, 1338700), + ShopSlot(self.state_logic, 2, 3, 1338701), + ShopSlot(self.state_logic, 3, 5, 1338702), + ShopSlot(self.state_logic, 4, 5, 1338703), + ShopSlot(self.state_logic, 5, 10, 1338704), + ShopSlot(self.state_logic, 6, 10, 1338705), + ShopSlot(self.state_logic, 7, 20, 1338706), + ShopSlot(self.state_logic, 8, 20, 1338707), + ShopSlot(self.state_logic, 9, 50, 1338708), + ShopSlot(self.state_logic, 10, 50, 1338709) + ] + + 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""" + + # 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]: + """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") + + max_tier_for_game = min(self.options.final_elevator_phase * 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_phase.value)) + + return location_table + + 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_phase <= 2: + number_of_slots_per_milestone_for_game = self.max_slots + 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, max_tier + 1): + for milestone in range(1, self.max_milestones + 1): + for slot in range(1, self.max_slots + 1): + if for_data_package: + location_table.append(HubSlot(tier, milestone, slot, hub_location_id)) + else: + if tier <= max_tier \ + and milestone <= len(self.game_logic.hub_layout[tier - 1]) \ + 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, final_elevator_phase: 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 phases) + + location_table.extend( + ElevatorPhase(phaseIndex, self.state_logic, self.game_logic) + for phaseIndex, _ in enumerate(self.game_logic.space_elevator_phases) + if phaseIndex < final_elevator_phase) + location_table.extend( + 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, final_elevator_phase)) + location_table.extend( + EventBuilding(self.state_logic, name, building) + for name, building in self.game_logic.buildings.items() + if name in self.critical_path.required_buildings) + location_table.extend( + PowerInfrastructure(self.state_logic, power_level, recipes) + 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_hard_drive_locations(self, for_data_package: bool, max_tier: int, available_parts: set[str]) \ + -> list[LocationData]: + hard_drive_locations: list[LocationData] = [] + + 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 = self.game_logic.drop_pods + # sort, easily obtainable first, should be deterministic + drop_pod_data.sort(key=lambda dp: ("!" if dp.item is None else dp.item) + str(dp.x - dp.z)) + + for location_id in range(self.drop_pod_location_id_start, self.drop_pod_location_id_end + 1): + 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), max_tier) + + 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 hard_drive_locations diff --git a/worlds/satisfactory/Options.py b/worlds/satisfactory/Options.py new file mode 100644 index 0000000000..6a4865d745 --- /dev/null +++ b/worlds/satisfactory/Options.py @@ -0,0 +1,614 @@ +from dataclasses import dataclass +from typing import ClassVar, Any, cast +from enum import IntEnum +from schema import Schema, And +from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions, OptionGroup +from Options import Range, NamedRange, Toggle, DefaultOnToggle, OptionSet, StartInventoryPool, Choice + + +class Placement(IntEnum): + starting_inventory = 0 + early = 1 + somewhere = 2 + + +class PlacementLogicMeta(AssembleOptions): + 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"]) + + cls = super(PlacementLogicMeta, mcs).__new__(mcs, name, bases, attrs) + return cast(PlacementLogicMeta, cls) + + +class PlacementLogic(Choice, metaclass=PlacementLogicMeta): + option_unlocked_from_start = Placement.starting_inventory.value + option_early_game = Placement.early.value + option_somewhere = Placement.somewhere.value + + +class ChoiceMapMeta(AssembleOptions): + def __new__(mcs, name: str, bases: tuple[type], attrs: dict[Any, Any]) -> "ChoiceMapMeta": + if "choices" in attrs: + for index, choice in enumerate(attrs["choices"]): + option_name = "option_" + choice.replace(' ', '_') + attrs[option_name] = index + + if "default" in attrs and attrs["default"] == choice: + attrs["default"] = index + + cls = super(ChoiceMapMeta, mcs).__new__(mcs, name, bases, attrs) + return cast(ChoiceMapMeta, cls) + + +class ChoiceMap(Choice, metaclass=ChoiceMapMeta): + choices: ClassVar[dict[str, list[str]]] + default: str + + def get_selected_list(self) -> list[str]: + for index, choice in enumerate(self.choices): + if index == self.value: + return self.choices[choice] + + raise Exception(f"ChoiceMap: selected choice {self.value} is not valid, valid choices are: {self.choices.keys()}") + + +class ElevatorPhase(NamedRange): + """ + Put the milestones accessible BEFORE this Space Elevator Phase in logic. + Milestones after the selected Phase are empty and contain nothing. + If your goal selection contains *Space Elevator Phase* then submitting this Phase's elevator package completes that goal. + If the goal is not enabled, this setting simply limits the HUB's content. + + Estimated in-game completion times: + + - **Phase 1 (Tiers 1-2)**: ~3 Hours + - **Phase 2 (Tiers 1-4)**: ~8 Hours + - **Phase 3 (Tiers 1-6)**: ~50 Hours + - **Phase 4 (Tiers 1-8)**: ~100 Hours + - **Phase 5 (Tiers 1-9)**: ~150 Hours + """ + display_name = "Final Space Elevator Phase in logic" + default = 2 + range_start = 1 + range_end = 5 + special_range_names = { + "phase 1 (tiers 1-2)": 1, + "phase 2 (tiers 1-4)": 2, + "phase 3 (tiers 1-6)": 3, + "phase 4 (tiers 1-8)": 4, + "phase 5 (tiers 1-9)": 5, + } + + +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! + + In the base game, it takes 347 coupons to unlock every non-repeatable purchase, or 1895 coupons to purchase every non-producible item. + + Use the **TFIT - Ficsit Information Tool** mod or the Satisfactory wiki to find out how many points items are worth. + + 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 = "AWESOME Sink points total" + default = 2166000 + range_start = 2166000 + range_end = 18436379500 + special_range_names = { + "50 coupons (~2m points)": 2166000, + "100 coupons (~18m points)": 17804500, + "150 coupons (~61m points)": 60787500, + "200 coupons (~145m points)": 145053500, + "250 coupons (~284m points)": 284442000, + "300 coupons (~493m points)": 492825000, + "350 coupons (~784m points)": 784191000, + "400 coupons (~1,2b points)": 1172329500, + "450 coupons (~1,7b points)": 1671112500, + "500 coupons (~2b points)": 2294578500, + "550 coupons (~3b points)": 3056467000, + "600 coupons (~4b points)": 3970650000, + "650 coupons (~5b points)": 5051216000, + "700 coupons (~6b points)": 6311854500, + "750 coupons (~8b points)": 7766437500, + "800 coupons (~9b points)": 9429103500, + "850 coupons (~11b points)": 11313492000, + "900 coupons (~13b points)": 13433475000, + "950 coupons (~16b points)": 15803241000, + "1000 coupons (~18b points)": 18436379500 + } + + +class ResourceSinkPointsPerMinute(NamedRange): + """ + Does nothing if *AWESOME Sink Points (per minute)* goal is not enabled. + + Sink items to maintain a sink points per minute of the chosen amount for 10 minutes to finish. + This setting is in *points per minute* on the orange track, so DNA Capsules don't count. + This option's presets are example production thresholds - you don't have to sink exactly those specific items. + + 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 = "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/min": 300000, + "~10 fused 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. + Hard Drives above this count cannot contain progression, but can still be Useful. + + There are 118 total hard drives in the world and the current implementation supports up to 100 progression hard drives. + """ + display_name = "Hard Drive Progression Items" + default = 20 + range_start = 0 + range_end = 100 + + +class FreeSampleEquipment(Range): + """ + How many free sample Equipment items are given when they are unlocked. + + (ex. Jetpack, Rifle) + """ + display_name = "Free Samples: Equipment" + default = 1 + range_start = 0 + range_end = 10 + + +class FreeSampleBuildings(Range): + """ + How many copies of a Building's construction cost are given as a free sample when they are unlocked. + Space Elevator is always excluded. + + (ex. Packager, Constructor, Smelter) + """ + display_name = "Free Samples: Buildings" + default = 5 + range_start = 0 + range_end = 10 + + +class FreeSampleParts(NamedRange): + """ + How many general crafting component free samples are given when their recipe is unlocked. + Space Elevator Project Parts are always excluded. + + Negative numbers mean that fraction of a full stack. + + (ex. Iron Plate, Packaged Turbofuel, Reinforced Modular Frame) + """ + display_name = "Free Samples: Parts" + default = -2 + range_start = -5 + range_end = 500 + special_range_names = { + "disabled": 0, + "half_stack": -2, + "one_stack": -1, + "1": 1, + "50": 50, + "100": 100, + "200": 200, + "500": 500, + } + + +class FreeSampleRadioactive(Toggle): + """ + Allow free samples to include radioactive parts. + Remember, they are delivered directly to your player inventory. + """ + display_name = "Free Samples: Radioactive" + + +class TrapChance(Range): + """ + Chance of traps in the item pool. + Traps will only replace filler items such as parts and resources. + + - **0:** No traps will be present + - **100:** Every filler item will be a trap. + """ + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 10 + + +_trap_types = { + "Trap: Doggo with Pulse Nobelisk", + "Trap: Doggo with Nuke Nobelisk", + "Trap: Doggo with Gas Nobelisk", + "Trap: Hog", + "Trap: Alpha Hog", + "Trap: Cliff Hog", + "Trap: Nuclear Hog", + "Trap: Johnny", + "Trap: Hatcher", + "Trap: Elite Hatcher", + "Trap: Small Stinger", + "Trap: Stinger", + "Trap: Gas Stinger", + "Trap: Spore Flower", + "Trap: Spitter", + "Trap: Alpha Spitter", + "Trap: Not the Bees", + "Trap: Nuclear Waste Drop", + "Trap: Plutonium Waste Drop", + "Trap: Can of Beans", + "Trap: Fart Cloud", + + # Radioactive parts delivered via portal + "Bundle: Uranium", + "Bundle: Uranium Fuel Rod", + "Bundle: Uranium Waste", + "Bundle: Plutonium Fuel Rod", + "Bundle: Plutonium Pellet", + "Bundle: Plutonium Waste", + "Bundle: Non-fissile Uranium", + "Bundle: Ficsonium", + "Bundle: Ficsonium Fuel Rod" + } + + +class TrapSelectionPreset(ChoiceMap): + """ + Themed presets of trap types to enable. + + If you want more control, use *Trap Override* or visit the Weighted Options page. + """ + display_name = "Trap Presets" + choices = { + "Gentle": ["Trap: Doggo with Pulse Nobelisk", "Trap: Hog", "Trap: Spitter", "Trap: Can of Beans"], + "Normal": ["Trap: Doggo with Pulse Nobelisk", "Trap: Doggo with Gas Nobelisk", "Trap: Hog", "Trap: Alpha Hog", "Trap: Hatcher", "Trap: Elite Hatcher", "Trap: Small Stinger", "Trap: Stinger", "Trap: Spitter", "Trap: Alpha Spitter", "Trap: Not the Bees", "Trap: Nuclear Waste Drop", "Bundle: Uranium", "Bundle: Non-fissile Uranium", "Trap: Can of Beans", "Trap: Fart Cloud"], + "Harder": ["Trap: Doggo with Pulse Nobelisk", "Trap: Doggo with Nuke Nobelisk", "Trap: Doggo with Gas Nobelisk", "Trap: Alpha Hog", "Trap: Cliff Hog", "Trap: Spore Flower", "Trap: Hatcher", "Trap: Elite Hatcher", "Trap: Stinger", "Trap: Alpha Spitter", "Trap: Not the Bees", "Trap: Fart Cloud", "Trap: Nuclear Waste Drop", "Trap: Plutonium Waste Drop", "Bundle: Uranium", "Bundle: Uranium Fuel Rod", "Bundle: Uranium Waste", "Bundle: Plutonium Fuel Rod", "Bundle: Plutonium Pellet", "Bundle: Plutonium Waste", "Bundle: Non-fissile Uranium"], + "All": list(_trap_types), + "Ruthless": ["Trap: Doggo with Nuke Nobelisk", "Trap: Nuclear Hog", "Trap: Cliff Hog", "Trap: Elite Hatcher", "Trap: Spore Flower", "Trap: Gas Stinger", "Trap: Nuclear Waste Drop", "Trap: Plutonium Waste Drop", "Bundle: Uranium Fuel Rod", "Bundle: Uranium Waste", "Bundle: Plutonium Fuel Rod", "Bundle: Plutonium Pellet", "Bundle: Plutonium Waste", "Bundle: Non-fissile Uranium", "Bundle: Ficsonium", "Bundle: Ficsonium Fuel Rod"], + "All Arachnids All the Time": ["Trap: Small Stinger", "Trap: Stinger", "Trap: Gas Stinger"], + "Whole Hog": ["Trap: Hog", "Trap: Alpha Hog", "Trap: Cliff Hog", "Trap: Nuclear Hog", "Trap: Johnny"], + "Nicholas Cage": ["Trap: Hatcher", "Trap: Elite Hatcher", "Trap: Not the Bees"], + "Fallout": ["Trap: Doggo with Nuke Nobelisk", "Trap: Nuclear Hog", "Trap: Nuclear Waste Drop", "Trap: Plutonium Waste Drop", "Bundle: Uranium", "Bundle: Uranium Fuel Rod", "Bundle: Uranium Waste", "Bundle: Plutonium Fuel Rod", "Bundle: Plutonium Waste", "Bundle: Ficsonium", "Bundle: Ficsonium Fuel Rod"], + } + default = "Normal" + + +class TrapSelectionOverride(OptionSet): + """ + Precise list of traps that may be in the item pool to find. + If you select anything with this option it will be used instead of the *Trap Presets* setting. + """ + display_name = "Trap Override" + valid_keys = _trap_types + + +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. + """ + display_name = "EnergyLink" + + +class MamLogic(PlacementLogic): + """ + Where to place the MAM building in logic. + Earlier means it will be more likely that you will need to interact with it for progression purposes. + """ + display_name = "MAM Placement" + default = Placement.early.value + + +class AwesomeLogic(PlacementLogic): + """ + Where to place the AWESOME Shop and Sink buildings in logic. + Earlier means it will be more likely that you will need to interact with it for progression purposes. + """ + display_name = "AWESOME Stuff Placement" + default = Placement.early.value + + +class EnergyLinkLogic(PlacementLogic): + """ + Where to place the EnergyLink building (or Power Storage if EnergyLink is disabled) in logic. + Earlier means it will be more likely that you will need to interact with it for progression purposes. + """ + display_name = "EnergyLink Placement" + default = Placement.early.value + + +class SplitterLogic(PlacementLogic): + """ + Where to place the Conveyor Splitter and Merger buildings in logic. + Earlier means it will be more likely that you will need to interact with it for progression purposes. + """ + display_name = "Splitter and Merger Placement" + default = Placement.starting_inventory.value + + +_skip_tutorial_starting_items = [ + # https://satisfactory.wiki.gg/wiki/Onboarding + "Single: Portable Miner", + "Single: Portable Miner", + "Single: Portable Miner", + "Single: Portable Miner", + "Bundle: Iron Plate", + "Bundle: Concrete", + "Bundle: Iron Rod", + "Bundle: Wire", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Single: Reinforced Iron Plate", + "Bundle: Cable", + "Bundle: Iron Ore" +] + +_default_starting_items = _skip_tutorial_starting_items + [ + "Bundle: Iron Ingot", + "Bundle: Copper Ingot", + "Bundle: Concrete", + "Bundle: Solid Biofuel", # user's choice if they want to hold onto it for chainsaw or burn it right away + "Building: Blueprint Designer", + "Expanded Toolbelt", + "Inflated Pocket Dimension", + "Building: Personal Storage Box" +] + +_default_plus_foundations_starting_items = _default_starting_items + [ + "Building: Foundation", + "Building: Half Foundation" +] + +_explorer_starting_items = _default_plus_foundations_starting_items + [ + "Single: Parachute", + "Single: Blade Runners", + "Single: Object Scanner", + "Single: Boom Box", + "Expanded Toolbelt", + "Inflated Pocket Dimension" +] + +_foundation_lover_starting_items = _default_plus_foundations_starting_items + [ + "Bundle: Iron Plate", "Bundle: Iron Plate", "Bundle: Iron Plate", + "Bundle: Concrete", "Bundle: Concrete", "Bundle: Concrete" +] + + +class StartingInventoryPreset(ChoiceMap): + """ + What resources (and buildings) the player should start with in their inventory. + If you want more control, visit the Weighted Options page or edit the YAML directly. + + - **Barebones**: Nothing but the default xeno zapper and buildings. + - **Skip Tutorial Inspired**: Inspired by the items you would have if you skipped the base game's tutorial. + - **Archipelago**: The starting items we think will lead to a fun experience. + - **Foundations**: 'Archipelago' option, but also guaranteeing that you have foundations unlocked at the start. + - **Foundation Lover**: You really like foundations. + - **Explorer**: 'Foundations' option plus one set of early exploration equipment (Parachute, Blade Runners, Object Scanner, Boom Box). + """ + display_name = "Starting Goodies Presets" + choices = { + "Barebones": [], # Nothing but the xeno zapper + "Skip Tutorial Inspired": _skip_tutorial_starting_items, + "Archipelago": _default_starting_items, + "Foundations": _default_plus_foundations_starting_items, + "Foundation Lover": _foundation_lover_starting_items, + "Explorer": _explorer_starting_items + } + default = "Archipelago" + + +class ExplorationCollectableCount(Range): + """ + Does nothing if *Exploration Collectables* goal is not enabled. + + Collect this amount of Mercer Spheres, Somersloops, Hard Drives, Paleberries, Beryl Nuts, and Bacon Agarics each to finish. + + - The amount of **Mercer Spheres** is **2x** the selected amount + - The amount of **Somersloops** is **the** selected amount + - The amount of **Hard Drives** is **1/5th** the selected amount + - The amount of **Paleberries** is **10x** the selected amount + - The amount of **Beryl Nuts** is **20x** the selected amount + - The amount of **Bacon Agarics** is **the** selected amount + """ + display_name = "Exploration Collectables" + default = 20 + range_start = 5 + range_end = 100 + + +class MilestoneCostMultiplier(Range): + """ + Multiplies the amount of resources needed to unlock a Milestone by this factor. + + The value is a 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)? + Configure them further with other options. + + Possible values are: + - **Space Elevator Tier** + - **AWESOME Sink Points (total)** + - **AWESOME Sink Points (per minute)** + - **Exploration Collectables** + """ + display_name = "Select your Goals" + valid_keys = { + "Space Elevator Phase", + "AWESOME Sink Points (total)", + "AWESOME Sink Points (per minute)", + "Exploration Collectables", + # "Erect a FICSMAS Tree", + } + default = {"Space Elevator Phase"} + schema = Schema(And(set, len), + error="yaml does not specify a goal, the Satisfactory option `goal_selection` is empty") + + +class GoalRequirement(Choice): + """ + Of the goals selected in *Select your Goals*, how many must be reached to complete your slot? + """ + display_name = "Goal Requirements" + option_require_any_one_goal = 0 + option_require_all_goals = 1 + default = 0 + + +class RandomizeTier0(DefaultOnToggle): + """ + Randomizes what recipes you use to craft the default unlocked parts: + Iron Ingot, Iron Plate, Iron Rod, Copper Ingot, Wire, Concrete, Screw, Reinforced Iron Plate + + * Could require usage of Foundries or Assemblers (which get unlocked by default if needed, at reduced build costs) + * Could require other ores to be mixed in via alt recipes (which will become hand-craftable if needed) + """ + display_name = "Randomize Default Part Recipes" + + +@dataclass +class SatisfactoryOptions(PerGameCommonOptions, DeathLinkMixin): + goal_selection: GoalSelection + goal_requirement: GoalRequirement + final_elevator_phase: ElevatorPhase + goal_awesome_sink_points_total: ResourceSinkPointsTotal + goal_awesome_sink_points_per_minute: ResourceSinkPointsPerMinute + goal_exploration_collectables_amount: ExplorationCollectableCount + hard_drive_progression_limit: HardDriveProgressionLimit + free_sample_equipment: FreeSampleEquipment + free_sample_buildings: FreeSampleBuildings + free_sample_parts: FreeSampleParts + free_sample_radioactive: FreeSampleRadioactive + starting_inventory_preset: StartingInventoryPreset + mam_logic_placement: MamLogic + 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 + energy_link: EnergyLink + start_inventory_from_pool: StartInventoryPool + randomize_starter_recipes: RandomizeTier0 + + +option_groups = [ + OptionGroup("Game Scope", [ + ElevatorPhase, + HardDriveProgressionLimit + ]), + OptionGroup("Goal Selection", [ + GoalSelection, + GoalRequirement, + ResourceSinkPointsTotal, + ResourceSinkPointsPerMinute, + ExplorationCollectableCount + ]), + OptionGroup("Placement logic", [ + StartingInventoryPreset, + RandomizeTier0, + MamLogic, + AwesomeLogic, + SplitterLogic, + EnergyLinkLogic + ], start_collapsed=True), + OptionGroup("Free Samples", [ + FreeSampleEquipment, + FreeSampleBuildings, + FreeSampleParts, + FreeSampleRadioactive + ], start_collapsed=True), + OptionGroup("Traps", [ + TrapChance, + TrapSelectionPreset, + TrapSelectionOverride + ], start_collapsed=True) +] + +option_presets: dict[str, dict[str, Any]] = { + "Short": { + "final_elevator_phase": 1, + "goal_selection": {"Space Elevator Phase", "AWESOME Sink Points (total)"}, + "goal_requirement": GoalRequirement.option_require_any_one_goal, + "goal_awesome_sink_points_total": 17804500, # 100 coupons + "hard_drive_progression_limit": 20, + "starting_inventory_preset": 3, # "Foundations" + "randomize_starter_recipes": False, + "mam_logic_placement": Placement.starting_inventory.value, + "awesome_logic_placement": Placement.starting_inventory.value, + "energy_link_logic_placement": Placement.starting_inventory.value, + "splitter_placement": Placement.starting_inventory.value, + "milestone_cost_multiplier": 50, + "trap_selection_preset": 1 # Gentle + }, + "Long": { + "final_elevator_phase": 3, + "goal_selection": {"Space Elevator Phase", "AWESOME Sink Points (per minute)"}, + "goal_requirement": GoalRequirement.option_require_all_goals, + "goal_awesome_sink_points_per_minute": 100000, # ~10 heavy modular frame/min + "hard_drive_progression_limit": 60, + "mam_logic_placement": Placement.somewhere.value, + "awesome_logic_placement": Placement.somewhere.value, + "energy_link_logic_placement": Placement.somewhere.value, + "splitter_placement": Placement.somewhere.value, + "trap_selection_preset": 3 # Harder + }, + "Extra Long": { + "final_elevator_phase": 5, + "goal_selection": {"Space Elevator Phase", "AWESOME Sink Points (per minute)"}, + "goal_requirement": GoalRequirement.option_require_all_goals, + "goal_awesome_sink_points_per_minute": 625000, # ~10 fused modular frame/min + "hard_drive_progression_limit": 100, + "mam_logic_placement": Placement.somewhere.value, + "awesome_logic_placement": Placement.somewhere.value, + "energy_link_logic_placement": Placement.somewhere.value, + "splitter_placement": Placement.somewhere.value, + "milestone_cost_multiplier": 300, + "trap_selection_preset": 4 # All + } +} diff --git a/worlds/satisfactory/Regions.py b/worlds/satisfactory/Regions.py new file mode 100644 index 0000000000..4ab4ea327d --- /dev/null +++ b/worlds/satisfactory/Regions.py @@ -0,0 +1,199 @@ +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" + event_name: Optional[str] + + def __init__(self, player: int, data: LocationData, region: Region): + super().__init__(player, data.name, data.code, region) + + self.event_name = data.event_name + + if data.code is None: + self.event = True + self.locked = True + + if data.rule: + self.access_rule = data.rule + + if data.non_progression: + self.item_rule = self.non_progression_only + + @staticmethod + def non_progression_only(item: Item) -> bool: + return not item.advancement + + +def create_regions_and_return_locations(multiworld: MultiWorld, options: SatisfactoryOptions, player: int, + game_logic: GameLogic, state_logic: StateLogic, + critical_path: CriticalPathCalculator, locations: list[LocationData]) -> None: + + region_names: list[str] = [ + "Overworld", + "Mam", + "AWESOME Shop" + ] + + for hub_tier, milestones_per_hub_tier in enumerate(game_logic.hub_layout, 1): + if hub_tier > (options.final_elevator_phase * 2): + break + + region_names.append(f"Hub Tier {hub_tier}") + + for milestone, _ in enumerate(milestones_per_hub_tier, 1): + region_names.append(f"Hub {hub_tier}-{milestone}") + + region_names += [ + building_name + for building_name, building in game_logic.buildings.items() + if building.can_produce and building_name in critical_path.required_buildings + ] + + for tree_name, tree in game_logic.man_trees.items(): + region_names.append(tree_name) + + for node in tree.nodes: + if node.minimal_phase <= options.final_elevator_phase: + region_names.append(f"{tree_name}: {node.name}") + + locations_per_region: dict[str, list[LocationData]] = get_locations_per_region(locations) + regions: dict[str, Region] = create_regions(multiworld, player, locations_per_region, region_names) + + if __debug__: + throw_if_any_location_is_not_assigned_to_a__region(regions, locations_per_region) + + multiworld.regions += regions.values() + + super_early_game_buildings: list[str] = [ + "Foundation", + "Walls Orange" + ] + + early_game_buildings: list[str] = [ + PowerInfrastructureLevel.Automated.to_name() + ] + + if options.mam_logic_placement.value == Placement.early: + early_game_buildings.append("MAM") + if options.awesome_logic_placement.value == Placement.early: + early_game_buildings.append("AWESOME Sink") + early_game_buildings.append("AWESOME Shop") + if options.energy_link_logic_placement.value == Placement.early: + early_game_buildings.append("Power Storage") + if options.splitter_placement == Placement.early: + super_early_game_buildings.append("Conveyor Splitter") + super_early_game_buildings.append("Conveyor Merger") + + if options.final_elevator_phase == 1: + super_early_game_buildings.extend(early_game_buildings) + + # Hub Tier 1 and 2 are always accessible, so universal tracker should display them out the gates + is_universal_tracker = getattr(multiworld, "generation_is_fake", False) + + connect(regions, "Overworld", "Hub Tier 1") + connect(regions, "Hub Tier 1", "Hub Tier 2", + lambda state: is_universal_tracker or state_logic.can_build_all(state, super_early_game_buildings)) + + if options.final_elevator_phase >= 2: + connect(regions, "Hub Tier 2", "Hub Tier 3", lambda state: state.has("Elevator Phase 1", player) + and (is_universal_tracker or state_logic.can_build_all(state, early_game_buildings))) + connect(regions, "Hub Tier 3", "Hub Tier 4") + if options.final_elevator_phase >= 3: + connect(regions, "Hub Tier 4", "Hub Tier 5", lambda state: state.has("Elevator Phase 2", player)) + connect(regions, "Hub Tier 5", "Hub Tier 6") + if options.final_elevator_phase >= 4: + connect(regions, "Hub Tier 6", "Hub Tier 7", lambda state: state.has("Elevator Phase 3", player)) + connect(regions, "Hub Tier 7", "Hub Tier 8") + if options.final_elevator_phase >= 5: + connect(regions, "Hub Tier 8", "Hub Tier 9", lambda state: state.has("Elevator Phase 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"))) + + for hub_tier, milestones_per_hub_tier in enumerate(game_logic.hub_layout, 1): + if hub_tier > (options.final_elevator_phase * 2): + break + + for milestone, parts_per_milestone in enumerate(milestones_per_hub_tier, 1): + connect(regions, f"Hub Tier {hub_tier}", f"Hub {hub_tier}-{milestone}", + state_logic.get_can_produce_all_allowing_handcrafting_rule(parts_per_milestone)) + + for building_name, building in game_logic.buildings.items(): + if building.can_produce and building_name in critical_path.required_buildings: + connect(regions, "Overworld", building_name, + lambda state, name=building_name: state_logic.can_build(state, name)) + + for tree_name, tree in game_logic.man_trees.items(): + connect(regions, "Mam", tree_name) + + for node in tree.nodes: + if node.minimal_phase > options.final_elevator_phase: + continue + + if not node.depends_on: + connect(regions, tree_name, f"{tree_name}: {node.name}", + lambda state, parts=node.unlock_cost: state_logic.can_produce_all(state, parts)) + else: + for parent in node.depends_on: + 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: state_logic.can_produce_all(state, parts)) + + +def throw_if_any_location_is_not_assigned_to_a__region(regions: dict[str, Region], + region_names: dict[str, list[LocationData]]) -> None: + existing_regions = set(regions) + existing_region_names = set(region_names) + + if existing_region_names - existing_regions: + raise Exception(f"Satisfactory: the following regions are used in locations: " + f"{existing_region_names - existing_regions}, but no such region exists") + + +def create_region(multiworld: MultiWorld, player: int, + locations_per_region: dict[str, list[LocationData]], name: str) -> Region: + + region = Region(name, player, multiworld) + + if name in locations_per_region: + region.locations += [ + SatisfactoryLocation(player, location_data, region) + for location_data in locations_per_region[name] + ] + + return region + + +def create_regions(multiworld: MultiWorld, player: int, locations_per_region: dict[str, list[LocationData]], + region_names: list[str]) -> dict[str, Region]: + return { + name: create_region(multiworld, player, locations_per_region, name) + for name in region_names + } + + +def connect(regions: dict[str, Region], source: str, target: str, + rule: Optional[Callable[[CollectionState], bool]] = None) -> None: + + source_region = regions[source] + target_region = regions[target] + + source_region.connect(target_region, rule=rule) + + +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) + + return per_region diff --git a/worlds/satisfactory/StateLogic.py b/worlds/satisfactory/StateLogic.py new file mode 100644 index 0000000000..58406d4eec --- /dev/null +++ b/worlds/satisfactory/StateLogic.py @@ -0,0 +1,171 @@ +from typing import Optional, Callable, ClassVar, Tuple +from collections.abc import Iterable +from BaseClasses import CollectionState +from .GameLogic import Recipe, PowerInfrastructureLevel +from .Options import SatisfactoryOptions +from .CriticalPathCalculator import CriticalPathCalculator + +EventId: Optional[int] = None + +part_event_prefix = "Can Produce: " +building_event_prefix = "Can Build: " + + +def true_rule(_: CollectionState) -> bool: + return True + + +def to_part_event(part: str) -> str: + return part_event_prefix + part + + +def to_building_event(part: str) -> str: + return building_event_prefix + part + + +def to_belt_name(power_level: int) -> str: + return "Conveyor Mk." + str(power_level) + + +class StateLogic: + player: int + options: SatisfactoryOptions + critical_path: CriticalPathCalculator + initial_unlocked_items: set[str] + + pipe_events: ClassVar[tuple[str, str]] = \ + tuple(to_building_event(building) for building in ("Pipes Mk.1", "Pipes Mk.2")) + pump_events: ClassVar[tuple[str, str]] = \ + tuple(to_building_event(building) for building in ("Pipeline Pump Mk.1", "Pipeline Pump Mk.2")) + hazmat_events: ClassVar[tuple[str, str]] = \ + tuple(to_part_event(part) for part in ("Hazmat Suit", "Iodine-Infused Filter")) + belt_events: ClassVar[tuple[tuple[str, ...], ...]] = tuple( + tuple(map(to_building_event, map(to_belt_name, range(speed, 6)))) + for speed in range(1, 6) + ) + + pipes_rule: Callable[[CollectionState], bool] + radio_active_rule: Callable[[CollectionState], bool] + belt_rules: Tuple[Callable[[CollectionState], bool], ...] + + def __init__(self, player: int, options: SatisfactoryOptions, critical_path: CriticalPathCalculator): + self.player = player + self.options = options + self.critical_path = critical_path + + self.pipes_rule = self.get_requires_pipes_rule() + self.radio_active_rule = self.get_requires_hazmat_rule() + self.belt_rule = tuple(self.get_belt_speed_rule(speed) for speed in range(1, 6)) + + def has_recipe(self, state: CollectionState, recipe: Recipe) -> bool: + return state.has(recipe.name, self.player) or recipe.name in self.critical_path.implicitly_unlocked + + def can_build(self, state: CollectionState, building_name: Optional[str]) -> bool: + return building_name is None or state.has(building_event_prefix + building_name, self.player) + + def can_build_any(self, state: CollectionState, building_names: Optional[Iterable[str]]) -> bool: + return building_names is None or \ + state.has_any(map(to_building_event, building_names), self.player) + + def can_build_all(self, state: CollectionState, building_names: Optional[Iterable[str]]) -> bool: + return building_names is None or \ + state.has_all(map(to_building_event, building_names), self.player) + + def can_produce(self, state: CollectionState, part_name: Optional[str]) -> bool: + return part_name is None or state.has(part_event_prefix + part_name, self.player) + + def can_power(self, state: CollectionState, power_level: Optional[PowerInfrastructureLevel]) -> bool: + 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: + return parts is None or \ + state.has_all(map(to_part_event, parts), self.player) + + def can_handcraft_single_part(self, state: CollectionState, part: str) -> bool: + if self.can_produce(state, part): + return True + + if part not in self.critical_path.handcraftable_parts: + return False + + recipes: list[Recipe] = self.critical_path.handcraftable_parts[part] + return any( + self.has_recipe(state, recipe) + and (not recipe.inputs or all( + self.can_handcraft_single_part(state, recipe_part) + for recipe_part in recipe.inputs)) + for recipe in recipes) + + def get_can_produce_all_allowing_handcrafting_rule(self, parts: Optional[Iterable[str]]) \ + -> Callable[[CollectionState], bool]: + if not parts: + return true_rule + + return lambda state: all(self.can_handcraft_single_part(state, part) for part in parts) + + def get_requires_pipes_rule(self) -> Callable[[CollectionState], bool]: + return lambda state: \ + state.has_any(self.pipe_events, self.player) and state.has_any(self.pump_events, self.player) + + def get_requires_hazmat_rule(self) -> Callable[[CollectionState], bool]: + return lambda state: state.has_all(self.hazmat_events, self.player) + + def get_belt_speed_rule(self, belt_speed: int) -> Callable[[CollectionState], bool]: + return lambda state: state.has_any(self.belt_events[belt_speed], self.player) + + def is_recipe_producible(self, state: CollectionState, recipe: Recipe) -> bool: + return self.has_recipe(state, recipe) \ + and self.can_build(state, recipe.building) \ + and self.can_produce_all(state, recipe.inputs) + + def get_can_produce_specific_recipe_for_part_rule(self, recipe: Recipe) -> Callable[[CollectionState], bool]: + if recipe.needs_pipes: + if recipe.is_radio_active: + if recipe.minimal_belt_speed: + return lambda state: \ + self.is_recipe_producible(state, recipe) \ + and self.pipes_rule(state) \ + and self.radio_active_rule(state) \ + and self.belt_rule[recipe.minimal_belt_speed - 1] + else: + return lambda state: \ + self.is_recipe_producible(state, recipe) \ + and self.pipes_rule(state) \ + and self.radio_active_rule(state) + else: + if recipe.minimal_belt_speed: + return lambda state: \ + self.is_recipe_producible(state, recipe) \ + and self.pipes_rule(state) \ + and self.belt_rule[recipe.minimal_belt_speed - 1] + else: + return lambda state: \ + self.is_recipe_producible(state, recipe) \ + and self.pipes_rule(state) + else: + if recipe.is_radio_active: + if recipe.minimal_belt_speed: + return lambda state: \ + self.is_recipe_producible(state, recipe) \ + and self.radio_active_rule(state) \ + and self.belt_rule[recipe.minimal_belt_speed - 1] + else: + return lambda state: \ + self.is_recipe_producible(state, recipe) \ + and self.radio_active_rule(state) + else: + if recipe.minimal_belt_speed: + return lambda state: \ + self.is_recipe_producible(state, recipe) \ + and self.belt_rule[recipe.minimal_belt_speed - 1] + else: + return lambda state: \ + self.is_recipe_producible(state, recipe) + + def is_elevator_phase(self, state: CollectionState, phase: int) -> bool: + limited_phase = min(self.options.final_elevator_phase - 1, phase) + + if limited_phase != 0: + return state.has(f"Elevator Phase {limited_phase}", self.player) + else: + return True diff --git a/worlds/satisfactory/Web.py b/worlds/satisfactory/Web.py new file mode 100644 index 0000000000..1504b9f299 --- /dev/null +++ b/worlds/satisfactory/Web.py @@ -0,0 +1,20 @@ +from BaseClasses import Tutorial +from .Options import option_groups, option_presets +from ..AutoWorld import WebWorld + + +class SatisfactoryWebWorld(WebWorld): + theme = "dirt" + setup = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Satisfactory Archipelago mod and connecting it to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Robb", "Jarno"] + ) + tutorials = [setup] + rich_text_options_doc = True + + option_groups = option_groups + options_presets = option_presets diff --git a/worlds/satisfactory/__init__.py b/worlds/satisfactory/__init__.py new file mode 100644 index 0000000000..43494ec8e0 --- /dev/null +++ b/worlds/satisfactory/__init__.py @@ -0,0 +1,264 @@ +from typing import TextIO, ClassVar, Any +from collections.abc import Iterable +from BaseClasses import Item, ItemClassification, CollectionState +from NetUtils import Hint +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 + + +class SatisfactoryWorld(World): + """ + Satisfactory is a first-person open-world factory building game with a dash of exploration and combat. + Explore an alien planet, create multi-story factories, and enter conveyor belt heaven! + """ + + game = "Satisfactory" + options_dataclass = SatisfactoryOptions + options: SatisfactoryOptions + topology_present = False + web = SatisfactoryWebWorld() + origin_region_name = "Overworld" + required_client_version = (0, 6, 0) + ut_can_gen_without_yaml = True + + game_logic: ClassVar[GameLogic] = GameLogic() + + # These are set in generate_early and thus aren't always available + state_logic: StateLogic | None = None + items: Items | None = None + critical_path: CriticalPathCalculator | None = None + critical_path_seed: float | None = None + # + + item_name_to_id = Items.item_names_and_ids + location_name_to_id = Locations().get_locations_for_data_package() + item_name_groups = Items.get_item_names_per_category(game_logic) + + def generate_early(self) -> None: + self.process_universal_tracker_slot_data_if_available() + + if not self.critical_path_seed: + self.critical_path_seed = self.random.random() + + if self.options.mam_logic_placement.value == Placement.starting_inventory: + self.push_precollected_by_name("Building: MAM") + if self.options.awesome_logic_placement.value == Placement.starting_inventory: + self.push_precollected_by_name("Building: AWESOME Sink") + self.push_precollected_by_name("Building: AWESOME Shop") + if self.options.energy_link_logic_placement.value == Placement.starting_inventory: + self.push_precollected_by_name("Building: Power Storage") + if self.options.splitter_placement == Placement.starting_inventory: + self.push_precollected_by_name("Building: Conveyor Splitter") + self.push_precollected_by_name("Building: Conveyor Merger") + + if not self.options.trap_selection_override.value: + self.options.trap_selection_override.value = set(self.options.trap_selection_preset.get_selected_list()) + + self.critical_path = CriticalPathCalculator(self.game_logic, self.critical_path_seed, self.options) + self.critical_path.calculate() + + self.state_logic = StateLogic(self.player, self.options, self.critical_path) + self.items = Items(self.player, self.game_logic, self.random, self.options, self.critical_path) + + starting_inventory: list[str] = self.options.starting_inventory_preset.get_selected_list() + for item_name in starting_inventory: + self.push_precollected_by_name(item_name) + + def create_regions(self) -> None: + 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, self.critical_path, + locations) + + def create_items(self) -> None: + self.setup_events() + + number_of_locations: int = len(self.multiworld.get_unfilled_locations(self.player)) + precollected_items: list[Item] = self.multiworld.precollected_items[self.player] + + self.multiworld.itempool += \ + self.items.build_item_pool(self.random, precollected_items, number_of_locations) + + def set_rules(self) -> None: + resource_sink_goal: bool = "AWESOME Sink Points (total)" in self.options.goal_selection \ + or "AWESOME Sink Points (per minute)" in self.options.goal_selection + + required_parts = set(self.game_logic.space_elevator_phases[self.options.final_elevator_phase.value - 1].keys()) + + if resource_sink_goal: + required_parts.union(self.game_logic.buildings["AWESOME Sink"].inputs) + + self.multiworld.completion_condition[self.player] = \ + lambda state: self.state_logic.can_produce_all(state, required_parts) + + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change and item.name in self.game_logic.indirect_recipes: + state.prog_items[self.player][self.game_logic.indirect_recipes[item.name]] += 1 + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change and item.name in self.game_logic.indirect_recipes: + state.prog_items[self.player][self.game_logic.indirect_recipes[item.name]] -= 1 + return change + + 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([]) + for milestone, parts in enumerate(milestones, 1): + slot_hub_layout[tier - 1].append({}) + for part, amount in parts.items(): + multiplied_amount = int(max(amount * (self.options.milestone_cost_multiplier / 100), 1)) + slot_hub_layout[tier - 1][milestone - 1][self.item_id_str(part)] = multiplied_amount + + starting_recipes: tuple[int, ...] = tuple( + self.item_name_to_id[recipe_name] + for recipe_name in self.critical_path.tier_0_recipes + ) + + return { + "Data": { + "HubLayout": slot_hub_layout, + "ExplorationCosts": { + self.item_id_str("Mercer Sphere"): int(self.options.goal_exploration_collectables_amount.value * 2), + self.item_id_str("Somersloop"): self.options.goal_exploration_collectables_amount.value, + self.item_id_str("Hard Drive"): int(self.options.goal_exploration_collectables_amount.value / 5), + self.item_id_str("Paleberry"): self.options.goal_exploration_collectables_amount.value * 10, + self.item_id_str("Beryl Nut"): self.options.goal_exploration_collectables_amount.value * 20, + self.item_id_str("Bacon Agaric"): self.options.goal_exploration_collectables_amount.value, + }, + "Options": { + "GoalSelection": self.options.goal_selection.value, + "GoalRequirement": self.options.goal_requirement.value, + "FinalElevatorPhase": self.options.final_elevator_phase.value, + "FinalResourceSinkPointsTotal": self.options.goal_awesome_sink_points_total.value, + "FinalResourceSinkPointsPerMinute": self.options.goal_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, + "FreeSampleRadioactive": bool(self.options.free_sample_radioactive), + "EnergyLink": bool(self.options.energy_link), + "StartingRecipies": starting_recipes + }, + "SlotDataVersion": 1, + "UT": { + "Seed": self.critical_path_seed, + "RandomizeTier0": bool(self.options.randomize_starter_recipes) + } + }, + "DeathLink": bool(self.options.death_link) + } + + @staticmethod + def interpret_slot_data(slot_data: dict[str, Any] | None) -> dict[str, Any] | None: + """Used by Universal Tracker, return value is passed to self.multiworld.re_gen_passthrough["Satisfactory"]""" + return slot_data + + def process_universal_tracker_slot_data_if_available(self) -> None: + """Used by Universal Tracker to correctly rebuild state""" + + slot_data: dict[str, Any] | None = None + if (hasattr(self.multiworld, "re_gen_passthrough") + and isinstance(self.multiworld.re_gen_passthrough, dict) + and "Satisfactory" in self.multiworld.re_gen_passthrough): + slot_data = self.multiworld.re_gen_passthrough["Satisfactory"] + + if not slot_data: + return + + if slot_data["Data"]["SlotDataVersion"] != 1: + raise Exception("The slot_data version mismatch, the UT's Satisfactory .apworld is different from the one " + "used during generation") + + self.options.goal_selection.value = slot_data["Data"]["Options"]["GoalSelection"] + self.options.goal_requirement.value = slot_data["Data"]["Options"]["GoalRequirement"] + # TODO rename slot data FinalElevatorTier to FinalElevatorPhase in the mod, then here + self.options.final_elevator_phase.value = slot_data["Data"]["Options"]["FinalElevatorTier"] + self.options.goal_awesome_sink_points_total.value = slot_data["Data"]["Options"]["FinalResourceSinkPointsTotal"] + self.options.goal_awesome_sink_points_per_minute.value = \ + slot_data["Data"]["Options"]["FinalResourceSinkPointsPerMinute"] + self.options.free_sample_equipment.value = slot_data["Data"]["Options"]["FreeSampleEquipment"] + self.options.free_sample_buildings.value = slot_data["Data"]["Options"]["FreeSampleBuildings"] + self.options.free_sample_parts.value = slot_data["Data"]["Options"]["FreeSampleParts"] + self.options.free_sample_radioactive.value = int(slot_data["Data"]["Options"]["FreeSampleRadioactive"]) + self.options.energy_link.value = int(slot_data["Data"]["Options"]["EnergyLink"]) + + self.options.milestone_cost_multiplier.value = 100 * \ + (slot_data["Data"]["HubLayout"][0][0][self.item_id_str("Concrete")] + / self.game_logic.hub_layout[0][0]["Concrete"]) + self.options.goal_exploration_collectables_amount.value = \ + slot_data["Data"]["ExplorationCosts"][self.item_id_str("Somersloop")] + + self.critical_path_seed = slot_data["Data"]["UT"]["Seed"] + self.options.randomize_starter_recipes.value = slot_data["Data"]["UT"]["RandomizeTier0"] + + def write_spoiler_header(self, spoiler_handle: TextIO) -> None: + if self.options.randomize_starter_recipes: + spoiler_handle.write(f'Starter Recipes: {sorted(self.critical_path.tier_0_recipes)}\n') + + def setup_events(self) -> None: + location: SatisfactoryLocation + for location in self.get_locations(): + if location.address == EventId: + item_name = location.event_name + + item = Item(item_name, ItemClassification.progression, EventId, self.player) + + location.place_locked_item(item) + location.show_in_spoiler = False + + def get_filler_item_name(self) -> str: + if self.items: + return self.items.get_filler_item_name(self.random, None) + else: + return Items.get_filler_item_name_uninitialized(self.random) + + def create_item(self, name: str) -> Item: + if self.items: + return self.items.create_item(name, self.player) + else: + return Items.create_item_uninitialized(name, self.player) + + def extend_hint_information(self, _: dict[int, dict[int, str]]): + """ + Normally used for adding entrance information, + but in this case we want to create hints for locations that hold usefull items. + Since we only know item placements after generation is completed it was either this + or fill_slot_data or modify_multidata, and this method seemed the best fit + """ + + locations_visible_from_start: set[int] = set(range(1338000, 1338099)) # ids of Hub 1-1,1 to 2-5,10 + + if "Building: AWESOME Shop" in self.options.start_inventory \ + or "Building: AWESOME Shop" in self.options.start_inventory_from_pool \ + or self.options.awesome_logic_placement.value == Placement.starting_inventory: + locations_visible_from_start.update(range(1338700, 1338709)) # ids of shop locations 1 to 10 + + location_names_with_useful_items: Iterable[str] = [ + location.name + for location in self.get_locations() + if location.address in locations_visible_from_start and location.item \ + and location.item.flags & (ItemClassification.progression | ItemClassification.useful) > 0 + ] + + self.options.start_location_hints.value.update(location_names_with_useful_items) + + def push_precollected_by_name(self, item_name: str) -> None: + item = self.create_item(item_name) + self.push_precollected(item) + + def item_id_str(self, item_name: str) -> str: + # ItemIDs of bundles are shared with their component item + bundled_name = f"Bundle: {item_name}" + return str(self.item_name_to_id[bundled_name]) diff --git a/worlds/satisfactory/docs/en_Satisfactory.md b/worlds/satisfactory/docs/en_Satisfactory.md new file mode 100644 index 0000000000..299bdacd58 --- /dev/null +++ b/worlds/satisfactory/docs/en_Satisfactory.md @@ -0,0 +1,208 @@ +# Satisfactory + + + +## Where is the settings page? + +The [player settings page for this game](../player-options) +contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +In Satisfactory, the HUB Milestones and MAM Research Nodes are shuffled, +causing technologies to be obtained in a non-standard order. +Hard Drive scanning results also contain Archipelago items, +meaning alternate recipes could now become part of your required progression path. +There are also a few new purchases in the AWESOME Shop. +The materials required for constructing Assemblers and Foundries is altered to increase early game recipe variety. + +## What is the goal of Satisfactory? + +The player can choose from a number of goals using their YAML settings: + +- Complete the selected number of **[Space Elevator](https://satisfactory.wiki.gg/wiki/Space_Elevator) Phases**. + - The goal completes upon submitting your selected Space Elevator Phase. Any other progression you may have access to (HUB, MAM, AWESOME Shop) is not required for goal completion. + - Selecting Phase 5 is equivalent to beating the vanilla game by launching Project Assembly. + - Expect Phase 1 to take ~3 hours to finish, Phase 2 to take ~8 hours, Phase 3 to take ~2 days, Phase 4 to take ~1 week, and Phase 5 to take ~1.5 weeks on default settings. +- Supply items to the [AWESOME Sink](https://satisfactory.wiki.gg/wiki/AWESOME_Sink) **totalling a configurable amount of points** to finish. + - The goal is tracked in the background and completes once the points total is reached. + - Your selected point total can be reviewed in the AWESOME Sink graph. + - Time to finish this goal varies significantly depending on your goal level and Free Sample settings, and can technically be reached by AFKing at any point after you unlock the Sink. +- Supply items to the [AWESOME Sink](https://satisfactory.wiki.gg/wiki/AWESOME_Sink) **maintaining a configurable level of points per minute** to finish. + - The goal is tracked in the background and completes once you have maintained the selected sink points rate for 10 minutes. + - This goal requires establishing a more robust factory since it can't be AFKed like the points total or elevator goals. + - Your selected points rate can be reviewed in the AWESOME Sink graph. + - Time to finish this goal varies significantly depending on your Space Elevator packages in logic and the resource sink point improvement ratios of the recipes you have access to. +- **Explore the world to gather exotic items** and submit them in the HUB. + - The goal completes upon submitting the HUB milestone. + - There is no partial progress system for this goal - combining it with another goal is recommended. + - Time to finish this goal varies significantly depending on your map knowledge, equipment, and movement skills. + +You can also configure whether completing your slot requires *any one* goal or *all* goals to be met. + +## What Satisfactory items can appear in other players' worlds? + +Satisfactory's technologies are removed from the HUB, MAM, and Hard Drives and placed into other players' worlds. +When those technologies are found, they are sent back to Satisfactory +along with, optionally, free samples of those technologies. + +Other players' worlds may have Resource Bundles of building materials, equipment, ammunition, or FICSIT Coupons. +They may also contain Traps. + +## What is a Free Sample? + +A free sample is a package of items in Satisfactory granted in addition to a technology received from another world. +For equipment and component crafting recipes, this is the output product. +For buildings, this is the ingredients for the building. +For example, receiving the [Nobelisk Detonator MAM Node](https://satisfactory.wiki.gg/wiki/Nobelisk_Detonator#Unlocking) +would give you one Nobelisk Detonator and 50 Nobelisk, +receiving the [Jump Pads Milestone](https://satisfactory.wiki.gg/wiki/Milestones#Tier_2) +would give you the ingredients to construct 5 Jump Pads and 5 U-Jelly Landing Pads, etc. +In Satisfactory multiplayer, each Satisfactory player gets a copy of the sample. +Certain recipes and items, like Somersloops, are always excluded from samples. + +You can separately configure how many samples to receive for buildings, equipment, and crafting components +in your player settings. + +## What is a Resource Bundle? + +A Resource Bundle is a package of items received as a check from another world. +All resource bundle type items are named either `Single: ` or `Bundle: ` to distinguish them from component recipes. +They must be collected by constructing an Archipelago Portal. +For example, `Single: Jetpack` would contain a single jetpack, and `Bundle: Biomass` would contain one stack of biomass. + +Any Resource Bundle type items added to your starting inventory will be delivered to your player inventory when you initally spawn, +unless they can't fit, in which case they can be collected by building an Archipelago Portal. + +## What is a Trap? + +Traps are items intended to disrupt the player that replace non-progression filler items. +Satisfactory's traps currently include spawning disruptive creatures or sending inconvenient items to your Archipelago Portal. +The player settings page gives full control over which traps are enabled, +how many traps replace filler items, +as well as some pre-selected groups of themed traps. + +A complete list of traps and their effects is intentionally omitted to keep some surprise and mystery. +In the current implementation, the most severe traps could temporarily lock you out of a small area until you have gas/radiation protection. + +## What does another world's item look like in Satisfactory? + +In Satisfactory, items which need to be sent to other worlds appear in the HUB and MAM as info cards +in a similar manner to the base game's building and recipe unlocks. +Info cards have the Archipelago icon +and are color coded to indicate what Archipelago progression type they are. + +Hover over them to read a description, since many Satisfactory UIs (such as the MAM) cut this information off. + +![screenshot of HUB with some remote and some local items](https://raw.githubusercontent.com/Jarno458/SatisfactoryArchipelagoMod/main/Docs/localAndRemoteItems.JPG) + +Upon successful unlock of the technology, the item will be sent to its home world. + +## When the pioneer receives an item, what happens? + +When the player receives a technology, it is instantly unlocked and able to be crafted or constructed. +A message will appear in the chat to notify the player, +and if free samples are enabled the player may also receive some items delivered directly to their inventory. +Bundles will instantly be added to the Archipelago Portal network and can be collected at any Archipelago Portal. + +## What is EnergyLink? + +EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. +In Satisfactory, if enabled in the player settings, all base-game Power Storage buildings will act as Energy Link interfaces. +They will deposit surplus produced energy and draw energy from the shared storage when needed. + +Just like the base game, there is no limit to the discharge/draw rate of one building, +and each Power Storage provides 100 MW of charging throughput. +The shared storage has unlimited capacity, and only a small amount of energy is lost during depositing. +The amount of energy currently in the shared storage is displayed in the Archipelago client +and appears in the Power Storage building UI. + +You can find a list of Energy Link compatible games on the +[Archipelago Discord](https://discord.com/channels/731205301247803413/1010929117748809758/1174728119568048130). + +## What is the Archipelago Portal? + +The Archipelago Portal is a building that serves multiple purposes: + +- Collecting received "Resource Bundle"-type items. +- Transfering items within your Satisfactory world to other Portals +- Transfering items between multiple Satisfactory worlds +- Gifting items to other games that support the **Archipelago Gifting** system. + +The building requires power to operate. +You can build multiple portals or use faster belts to increase their bandwith. +However, they currently have no filtering capabilities, +so you must deal with this problem when handling their output items. + +You can find a list of Gifting compatible games on the +[Archipelago Discord](https://discord.com/channels/731205301247803413/1134306496042258482/1247617772993908891). + +## How do Hard Drives work? + +All base game Hard Drive contents (alternate recipes) have been moved into the normal Archipelago pool. +Instead, Hard Drives can contain Archipelago items from a dedicated "Hard Drive" pool. +Scanning a drive presents a choice between 2 items from the pool, +and the scan time has been reduced from 10 minutes to 3 seconds. + +Unlike the base game, Archipelago hard drive results have no hard progression requirements, +other than access to the MAM itself. +The random contents selection system prefers to pick items earlier in progression, +but keeping unselected Hard Drives in the Hard Drive Library will force later progression items to be presented. + +The "Hard Drive Progression Items" option controls how many Hard Drives contain progression items, +the rest are filler or useful. + +## Where do I run Archipelago commands? + +You can use the game's built-in chat menu. +Check the game's keybinding options to see how to open it. +Run the `/help` command to list all available commands. +Note that Archipelago commands are *not* prefixed with `!` inside of Satisfactory. + +Note that multiple base-game bugs affect the chat menu's functionality +and Archipelago can put a lot of info into the chat. +You may wish to launch the Archipelago Text Client and use it to run commands instead of the game's chat. + +### Hints + +Archipelago's hint system is available within Satisfactory via the `/hint` command. +Most multiworld item names have a prefix to distinguish recipes from bundles. +For example, to hint for the Assembler, run `/hint Building: Assembler`. + +Satisfactory's hint system has special behavior for Satisfactory crafting items. +If you hint the unprefixed name of an item with multiple recipes, the system will hint the recipe you are expected to find first in randomizer logic. +For example, hinting `Smart Plating` will return the logically first Smart Plating recipe, +but hinting `Recipe: Smart Plating` or `Recipe: Plastic Smart Plating` will hint that specific recipe for Smart Plating, +which may or may not be in logic. + +Exact Archipelago Item names (for hints/starting inventory/etc.) can be found +[on the mod's GitHub](https://github.com/Jarno458/Archipelago/blob/Satisfactory/worlds/satisfactory/Items.py). + +## Multiplayer and Dedicated Servers + +It is possible to host a Satisfactory Archipelago Slot using the game's built in multiplayer, +allowing other Satisfactory players to join in constructing your factory. +This experience is wonderful - but there are few things not yet properly working for multiplayer: + +- Death-links do not kill clients +- Starting inventory for clients is missing + +Remember that client players must have the same mods installed as the host player to join, +however, they do not need to configure Archipelago connection settings. + +Dedicated server support is only working for Windows at the moment. + +## Additional Mods + +It is possible to use other Satisfactory mods in tandem with the Archipelago Satisfactory mod. +However, no guarantee is made that they will work correctly, +especially if they affect game progression, recipes, or add unlocks to base-game technologies. + +Content added by unaffiliated mods may end up inaccessible based on your chosen slot settings, +for example, its milestones could be in a tier that is after your goal. +You may be able to write patches using [ContentLib](https://ficsit.app/mod/ContentLib) +to adjust other mods to work with your slot settings, +but doing so is out of the scope of this guide. + +[The Satisfactory Archipelago mod GitHub](https://github.com/Jarno458/SatisfactoryArchipelagoMod/blob/main/Docs/AdditionalMods.md) +maintains a list of additional mods that have been tested with Archipelago to some extent. diff --git a/worlds/satisfactory/docs/setup_en.md b/worlds/satisfactory/docs/setup_en.md new file mode 100644 index 0000000000..4486f1bb49 --- /dev/null +++ b/worlds/satisfactory/docs/setup_en.md @@ -0,0 +1,230 @@ +# Satisfactory Setup Guide + + + +## Required Software + +- Satisfactory, either + - Steam [Satisfactory (Steam)](https://store.steampowered.com/app/526870/Satisfactory/) + - Epic [Satisfactory (Epic)](https://www.epicgames.com/store/en-US/product/satisfactory/home) +- Satisfactory Mod Manager, either + - Automatically via [smm.ficsit.app](https://smm.ficsit.app/) or + - Manually via [latest stable release on GitHub](https://github.com/satisfactorymodding/SatisfactoryModManager/releases/latest/) + +## Overview + +This guide walks you through installing the Satisfactory Archipelago mod via the Satisfactory Mod Manager, +configuring an Archipelago slot for Satisfactory, +and playing the game with a Satisfactory client. + +### Defining Some Terms + +In Archipelago, multiple Satisfactory worlds may be played simultaneously. +Each of these worlds must be hosted by a Satisfactory Host which is connected to the Archipelago Server via the Archipelago mod. + +This guide uses the following terms to refer to the software: + +- **Archipelago Server** - The central Archipelago server, which connects all games to each other. +- **Archipelago Client** - The desktop application used by many Archipelago games as middleware. Satisfactory does NOT require this software, unless you would like to generate a world locally. +- **Archipelago (Satisfactory) mod** - The Satisfactory mod which implements Archipelago in-game functionality and connectivity. + All Satisfactory hosts and clients must have this mod installed. +- **Satisfactory Host** - The Satisfactory instance which will be used to host the game. + This could be a Satisfactory Client using Singleplayer or host-and-play multiplayer, or it could be a Satisfactory dedicated server. + It must be supplied with the Archipelago Server connection details. + *Any number of Satisfactory Clients may connect to this server.* +- **Satisfactory Client** - The Satisfactory instance (game client) with which additional players can use to connect to the same Satisfactory world. + +### What a Playable State Looks Like + +- An Archipelago Server +- One running modded Satisfactory Host (game client or dedicated server) per Satisfactory world +- Optionally, additional modded Satisfactory Clients for additional players + +### Additional Resources + +- Satisfactory Wiki: [Satisfactory Official Wiki](https://satisfactory.wiki.gg/wiki/) +- Satisfactory Modding 'Frequently Asked Questions' page: [Satisfactory Modding Documentation FAQ](https://docs.ficsit.app/satisfactory-modding/latest/faq.html) +- Satisfactory Archipelago Item names (for hints/starting inventory/etc.) can be found [on the mod's github](https://github.com/Jarno458/Archipelago/blob/Satisfactory/worlds/satisfactory/Items.py) + +## Preparing to Play Satisfactory Archipelago + +### Installing Satisfactory + +Purchase and install Satisfactory via one the sources linked [above](#required-software). +Launch the game at least once to ensure that the Mod Manager can detect the game's install location. + +Make sure that you are running the correct branch of the game (Release or Experimental) that Archipelago supports. +Learn how to switch branches here: +[Satisfactory Modding Documentation FAQ: Switching Branches](https://docs.ficsit.app/satisfactory-modding/latest/faq.html#_how_do_i_get_the_experimental_or_early_access_branch_of_the_game) + +### Installing Satisfactory Mod Manager + +The Mod Manager is used to install and manage mods for Satisfactory. +It automatically detects your game install location and automatically handles mod dependencies for you. + +Download the Mod Manager here: +[Satisfactory Mod Manager automatic download via ficsit.app](https://smm.ficsit.app/) + +Directions for setting and using up the Mod Manager can be found here: +[Satisfactory Modding Documentation FAQ: Installing the Mod Manager](https://docs.ficsit.app/satisfactory-modding/latest/ForUsers/SatisfactoryModManager.html) + +### Installing the Archipelago Mod + +Once the Mod Manager is installed you can install mods directly in the manager or via the Satisfactory Mod Repository website. + +Inside the Mod Manager, search for and install the "Archipelago Randomizer". +Alternatively, visit the mod page: [Archipelago Randomizer mod on ficsit.app](https://ficsit.app/mod/Archipelago). +Once on the mod page, click the "Install" link in the Latest Versions card. + +The Mod Manager will install all required dependency mods for you with no additional action required. + +As soon as you have the relevant mods installed, +you do not need to launch the game through the Mod Manager - +desktop shortcuts, Steam, Epic. etc. will all launch the game with mods still loaded. + +### Installing Additional Mods + +You may also wish to install some of the suggested mods mentioned on the +[Archipelago Info page for Satisfactory](/games/Satisfactory/info/en#additional-mods). +If you are playing multiplayer in the same Satisfactory world, all Satisfactory Clients should have the same mods installed. +The Mod Manager's profile import/export feature makes coordinating this easy. + +## Connecting to Someone Else's Satisfactory Game + +If you are joining someone else's existing Satisfactory game, +your setup process is almost complete. +If your host has sent you a Mod Manager profile containing additional mods, +be sure to install it. +See [Satisfactory Modding Documentation: Profiles](https://docs.ficsit.app/satisfactory-modding/latest/ForUsers/SatisfactoryModManager.html#_profiles) for more information. + +To get started playing, connect to the Satisfactory Host using the connection details provided by your host. +([Satisfactory Wiki: Joining a Session](https://satisfactory.wiki.gg/wiki/Multiplayer#Joining_a_session)) + +See the [Troubleshooting section below](#troubleshooting) if you encounter any issues. + +## Hosting Your Own Satisfactory Game + +If you're hosting your own Satisfactory game, +you will need to configure an Archipelago world and set up the Satisfactory Host you will be playing on. + +### Create a Config (.yaml) File + +#### What is a config file and why do I need one? + +Your config file contains a set of configuration options +which provide the generator with information about how it should generate your game. +Each player of a multiworld will provide their own config file. +This setup allows each player to enjoy an experience customized for their taste, +and different players in the same multiworld can all have different options. + +#### Where do I get a config file? + +The Player Settings page on the website +allows you to configure your personal settings and export a config file from them. +Satisfactory player settings page: [Satisfactory Settings Page](/games/Satisfactory/player-settings) + +#### Verifying Your Config File + +If you would like to validate your config file to make sure it works, +you may do so on the YAML Validator page. +YAML Validator page: [Yaml Validation Page](/mysterycheck) + +#### Starting Inventory + +The Player Settings page provides a few options for controlling what materials you start with +and when certain key technologies are unlocked. +Any Resource Bundle type items added to your starting inventory will be delivered to your player inventory when you initally spawn, +unless they can't fit, in which case they can be collected by building an Archipelago Portal. + +Advanced users can use Plando, Weighted Options, and manual yaml editing to further configure the starting inventory. +If you don't wish to use these techniques, consider using Satisfactory's +[Advanced Game Settings (Satisfactory Wiki)](https://satisfactory.wiki.gg/wiki/Advanced_Game_Settings) +to spawn the items you desire. + +#### Advanced Configuration + +Advanced users can utilize the +[Weighted Options Page](/weighted-options) +and [Plando](/tutorial/Archipelago/plando) +to futher customize their experience. + +### Generating and Hosting the Multiworld + +Generating a game and hosting an Archipelago server is explained in the [Archipelago Setup Guide](/tutorial/Archipelago/setup/en). + +### Creating the Satisfactory World + +After you have installed the mods, launch the game via the Mod Manager or via your preferred method. +Once the game has launched, start creating a new game. + +Select your starting location and Skip Intro if you wish to skip the tutorial sequence, +then click the "Mod Savegame Settings" button in the bottom right corner of the screen. +Next, enter the connection details in the relevant fields. + +- **Server URI**: Archipelago Server URI and port, for example, `archipelago.gg:49236` +- **User Name**: The name you entered as your Player Name when you created your config file. It's also listed in the Name column of your room page. +- **Password**: The password for your Archipelago room, blank if you did not assign or receive one. + +Note that the Satisfactory Host/Client does *not* need a copy of your Archipelago config file. +The mod communicates with the Archipelago Server, which already has your config file, +to generate the required content at runtime. + +Consider setting the following options in the "Options" > "Gameplay" section, especially because they are per-user and persist across your game saves: + +- **Creature Hostility**: `Default` (the game's default). Some of the mod's Traps involve creatures, and having them Passive or Retaliate cheapens the experience. +- **Keep Inventory**: `Keep Everything` or `Keep Equipment` (the game's default). Although dying and dropping items will never lock you out of progression, Free Samples and Bundles means you can easily gain items you can't easily replace. + +### Verifying Connection Success + +After you have created your new world, +you should see in-game chat messages confirming that you have connected to the Archipelago Server. + +You can issue the `/help` command in the game's chat to list available commands, such as `/hint`. +For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en). +Note that Archipelago commands are not prefixed with `!` inside of Satisfactory. +You may wish to use the Text Client to run commands since Satisfactory's in game chat is not very user friendly. + +Check out the HUB to get started! + +See the [Troubleshooting section below](#troubleshooting) if you encounter any issues. + +### Allowing Other People to Join Your Game + +Additional players can join your game using the game's built-in multiplayer functionality. +For more information, see [Satisfactory Wiki: Multiplayer](https://satisfactory.wiki.gg/wiki/Multiplayer). + +Have anyone you want to join follow the [Preparing to Play Satisfactory Archipelago](#preparing-to-play-satisfactory-archipelago) section above. +If you're using any additional mods, be sure to export a profile using the Mod Manager for players to import. +[Satisfactory Modding Documentation: Sharing Mod Manager Profiles](https://docs.ficsit.app/satisfactory-modding/latest/ForUsers/SatisfactoryModManager.html#_sharing_profiles) + +As mentioned above, it is possible to use a Satisfactory dedicated Server as your Satisfactory Host. +The process for setting up and configuring a dedicated server is out of scope of this guide, +but you can find more information here: [Satisfactory Modding Documentation: Installing Mods on Dedicated Servers](https://docs.ficsit.app/satisfactory-modding/latest/ForUsers/DedicatedServerSetup.html). + +It is important to note that the Satisfactory Archipelago mod +is not yet compatible with Linux dedicated servers - only Windows dedicated servers are supported. + +### Port Changes + +If you are using a public Archipelago Server to host your game, +rooms are automatically put to sleep after a period of inactivity. +The room can be awoken by visiting the room page on the Archipelago website. +This may cause the room's assigned port to change, +requiring you to update your "Mod Savegame Settings" with the new Server URI. +To do this, open your save, go to the pause menu's "Mod Savegame Settings" section, +enter the updated Server URI, then save and reload the game. + +## Troubleshooting + +- If you are having trouble connecting to the Archipelago Server, + make sure you have entered the correct server address and port. + The server port may have changed if the room went to sleep. + See the [Port Changes section](#port-changes) above for more information. +- If you are having trouble using the Satisfactory Mod Manager, join the [Satisfactory Modding Discord](https://discord.ficsit.app) for support. +- If you encounter a game crash, please report it to us via the [Satisfactory Modding Discord](https://discord.ficsit.app). + Please include the following information: + - What you were doing when the crash occurred. + - If you were a Satisfactory multiplayer host or client, and if you were playing on a dedicated server. + - Use the Mod Manager to generate a debug zip and attach that file. + [Satisfactory Modding Documentation FAQ: Generating a debug zip](https://docs.ficsit.app/satisfactory-modding/latest/faq.html#_where_can_i_find_the_games_log_files) + - Attach your Archipelago config file and spoiler to your report.