418 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			418 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | import math | ||
|  | from typing import Any, List, Dict, Tuple, Mapping | ||
|  | 
 | ||
|  | from Options import OptionError | ||
|  | from .data.strings import OTHER, ITEMS, CATEGORY, LOCATIONS, SLOTDATA, GOALS, OPTIONS | ||
|  | from .items import item_descriptions, item_table, ShapezItem, \ | ||
|  |     buildings_routing, buildings_processing, buildings_other, \ | ||
|  |     buildings_top_row, buildings_wires, gameplay_unlocks, upgrades, \ | ||
|  |     big_upgrades, filler, trap, bundles, belt_and_extractor, standard_traps, random_draining_trap, split_draining_traps, \ | ||
|  |     whacky_upgrade_traps | ||
|  | from .locations import ShapezLocation, addlevels, addupgrades, addachievements, location_description, \ | ||
|  |     addshapesanity, addshapesanity_ut, shapesanity_simple, init_shapesanity_pool, achievement_locations, \ | ||
|  |     level_locations, upgrade_locations, shapesanity_locations, categories | ||
|  | from .presets import options_presets | ||
|  | from .options import ShapezOptions | ||
|  | from worlds.AutoWorld import World, WebWorld | ||
|  | from BaseClasses import Item, Tutorial, LocationProgressType, MultiWorld | ||
|  | from .regions import create_shapez_regions, has_x_belt_multiplier | ||
|  | from ..generic.Rules import add_rule | ||
|  | 
 | ||
|  | 
 | ||
|  | class ShapezWeb(WebWorld): | ||
|  |     options_presets = options_presets | ||
|  |     rich_text_options_doc = True | ||
|  |     theme = "stone" | ||
|  |     game_info_languages = ['en', 'de'] | ||
|  |     setup_en = Tutorial( | ||
|  |         "Multiworld Setup Guide", | ||
|  |         "A guide to playing shapez with Archipelago:", | ||
|  |         "English", | ||
|  |         "setup_en.md", | ||
|  |         "setup/en", | ||
|  |         ["BlastSlimey"] | ||
|  |     ) | ||
|  |     setup_de = Tutorial( | ||
|  |         setup_en.tutorial_name, | ||
|  |         setup_en.description, | ||
|  |         "Deutsch", | ||
|  |         "setup_de.md", | ||
|  |         "setup/de", | ||
|  |         ["BlastSlimey"] | ||
|  |     ) | ||
|  |     datapackage_settings_en = Tutorial( | ||
|  |         "Changing datapackage settings", | ||
|  |         "3000 locations are too many or not enough? Here's how you can change that:", | ||
|  |         "English", | ||
|  |         "datapackage_settings_en.md", | ||
|  |         "datapackage_settings/en", | ||
|  |         ["BlastSlimey"] | ||
|  |     ) | ||
|  |     datapackage_settings_de = Tutorial( | ||
|  |         datapackage_settings_en.tutorial_name, | ||
|  |         datapackage_settings_en.description, | ||
|  |         "Deutsch", | ||
|  |         "datapackage_settings_de.md", | ||
|  |         "datapackage_settings/de", | ||
|  |         ["BlastSlimey"] | ||
|  |     ) | ||
|  |     tutorials = [setup_en, setup_de, datapackage_settings_en, datapackage_settings_de] | ||
|  |     item_descriptions = item_descriptions | ||
|  |     location_descriptions = location_description | ||
|  | 
 | ||
|  | 
 | ||
|  | class ShapezWorld(World): | ||
|  |     """
 | ||
|  |     shapez is an automation game about cutting, rotating, stacking, and painting shapes, that you extract from randomly | ||
|  |     generated patches on an infinite canvas, without the need to manage your infinite resources or to pay for building | ||
|  |     your factories. | ||
|  |     """
 | ||
|  |     game = OTHER.game_name | ||
|  |     options_dataclass = ShapezOptions | ||
|  |     options: ShapezOptions | ||
|  |     topology_present = True | ||
|  |     web = ShapezWeb() | ||
|  |     base_id = 20010707 | ||
|  |     item_name_to_id = {name: id for id, name in enumerate(item_table.keys(), base_id)} | ||
|  |     location_name_to_id = {name: id for id, name in enumerate(level_locations + upgrade_locations | ||
|  |                                                               + achievement_locations + shapesanity_locations, base_id)} | ||
|  |     item_name_groups = { | ||
|  |         "Main Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker}, | ||
|  |         "Processing Buildings": {*buildings_processing}, | ||
|  |         "Goal Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.rotator_ccw, ITEMS.color_mixer, | ||
|  |                            ITEMS.stacker, ITEMS.cutter_quad, ITEMS.painter_double, ITEMS.painter_quad, ITEMS.wires, | ||
|  |                            ITEMS.switch, ITEMS.const_signal}, | ||
|  |         "Most Useful Buildings": {ITEMS.balancer, ITEMS.tunnel, ITEMS.tunnel_tier_ii, ITEMS.comp_merger, | ||
|  |                                   ITEMS.comp_splitter, ITEMS.trash, ITEMS.extractor_chain}, | ||
|  |         "Most Important Buildings": {*belt_and_extractor}, | ||
|  |         "Top Row Buildings": {*buildings_top_row}, | ||
|  |         "Wires Layer Buildings": {*buildings_wires}, | ||
|  |         "Gameplay Mechanics": {ITEMS.blueprints, ITEMS.wires}, | ||
|  |         "Upgrades": {*{ITEMS.upgrade(size, cat) | ||
|  |                        for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising} | ||
|  |                        for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}}, | ||
|  |                      *{ITEMS.trap_upgrade(cat, size) | ||
|  |                        for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting} | ||
|  |                        for size in {"", CATEGORY.demonic}}, | ||
|  |                      *{ITEMS.upgrade(size, CATEGORY.random) | ||
|  |                        for size in {CATEGORY.big, CATEGORY.small}}}, | ||
|  |         **{f"{cat} Upgrades": {*{ITEMS.upgrade(size, cat) | ||
|  |                                  for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising}}, | ||
|  |                                *{ITEMS.trap_upgrade(cat, size) | ||
|  |                                  for size in {"", CATEGORY.demonic}}} | ||
|  |            for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}}, | ||
|  |         "Bundles": {*bundles}, | ||
|  |         "Traps": {*standard_traps, *random_draining_trap, *split_draining_traps, *whacky_upgrade_traps}, | ||
|  |     } | ||
|  |     location_name_groups = { | ||
|  |         "Levels": {*level_locations}, | ||
|  |         "Upgrades": {*upgrade_locations}, | ||
|  |         "Achievements": {*achievement_locations}, | ||
|  |         "Shapesanity": {*shapesanity_locations}, | ||
|  |         **{f"{cat} Upgrades": {loc for loc in upgrade_locations if loc.startswith(cat)} for cat in categories}, | ||
|  |         "Only Belt and Extractor": {LOCATIONS.level(1), LOCATIONS.level(1, 1), | ||
|  |                                     LOCATIONS.my_eyes, LOCATIONS.its_a_mess, LOCATIONS.getting_into_it, | ||
|  |                                     LOCATIONS.perfectionist, LOCATIONS.oops, LOCATIONS.i_need_trains, LOCATIONS.gps, | ||
|  |                                     LOCATIONS.a_long_time, LOCATIONS.addicted, | ||
|  |                                     LOCATIONS.shapesanity(1), LOCATIONS.shapesanity(2), LOCATIONS.shapesanity(3)}, | ||
|  |     } | ||
|  | 
 | ||
|  |     def __init__(self, multiworld: MultiWorld, player: int): | ||
|  |         super().__init__(multiworld, player) | ||
|  | 
 | ||
|  |         # Defining instance attributes for each shapez world | ||
|  |         # These are set to default values that should fail unit tests if not replaced with correct values | ||
|  |         self.location_count: int = 0 | ||
|  |         self.level_logic: List[str] = [] | ||
|  |         self.upgrade_logic: List[str] = [] | ||
|  |         self.level_logic_type: str = "" | ||
|  |         self.upgrade_logic_type: str = "" | ||
|  |         self.random_logic_phase_length: List[int] = [] | ||
|  |         self.category_random_logic_amounts: Dict[str, int] = {} | ||
|  |         self.maxlevel: int = 0 | ||
|  |         self.finaltier: int = 0 | ||
|  |         self.included_locations: Dict[str, Tuple[str, LocationProgressType]] = {} | ||
|  |         self.client_seed: int = 0 | ||
|  |         self.shapesanity_names: List[str] = [] | ||
|  |         self.upgrade_traps_allowed: bool = False | ||
|  | 
 | ||
|  |         # Universal Tracker support | ||
|  |         self.ut_active: bool = False | ||
|  |         self.passthrough: Dict[str, any] = {} | ||
|  |         self.location_id_to_alias: Dict[int, str] = {} | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def stage_generate_early(cls, multiworld: MultiWorld) -> None: | ||
|  |         # Import the 75800 entries long shapesanity pool only once and only if it's actually needed | ||
|  |         if len(shapesanity_simple) == 0: | ||
|  |             init_shapesanity_pool() | ||
|  | 
 | ||
|  |     def generate_early(self) -> None: | ||
|  |         # Calculate all the important values used for generating a shapez world, with some of them being random | ||
|  |         self.upgrade_traps_allowed: bool = (self.options.include_whacky_upgrades and | ||
|  |                                             (not self.options.goal == GOALS.efficiency_iii) and | ||
|  |                                             self.options.throughput_levels_ratio == 0) | ||
|  | 
 | ||
|  |         # Load values from UT if this is a regenerated world | ||
|  |         if hasattr(self.multiworld, "re_gen_passthrough"): | ||
|  |             if OTHER.game_name in self.multiworld.re_gen_passthrough: | ||
|  |                 self.ut_active = True | ||
|  |                 self.passthrough = self.multiworld.re_gen_passthrough[OTHER.game_name] | ||
|  |                 self.maxlevel = self.passthrough[SLOTDATA.maxlevel] | ||
|  |                 self.finaltier = self.passthrough[SLOTDATA.finaltier] | ||
|  |                 self.client_seed = self.passthrough[SLOTDATA.seed] | ||
|  |                 self.level_logic = [self.passthrough[SLOTDATA.level_building(i+1)] for i in range(5)] | ||
|  |                 self.upgrade_logic = [self.passthrough[SLOTDATA.upgrade_building(i+1)] for i in range(5)] | ||
|  |                 self.level_logic_type = self.passthrough[SLOTDATA.rand_level_logic] | ||
|  |                 self.upgrade_logic_type = self.passthrough[SLOTDATA.rand_upgrade_logic] | ||
|  |                 self.random_logic_phase_length = [self.passthrough[SLOTDATA.phase_length(i)] for i in range(5)] | ||
|  |                 self.category_random_logic_amounts = {cat: self.passthrough[SLOTDATA.cat_buildings_amount(cat)] | ||
|  |                                                       for cat in [CATEGORY.belt_low, CATEGORY.miner_low, | ||
|  |                                                                   CATEGORY.processors_low, CATEGORY.painting_low]} | ||
|  |                 # Forces balancers, tunnel, and trash to not appear in regen to make UT more accurate | ||
|  |                 self.options.early_balancer_tunnel_and_trash.value = 0 | ||
|  |                 return | ||
|  | 
 | ||
|  |         # "MAM" goal is supposed to be longer than vanilla, but to not have more options than necessary, | ||
|  |         # both goal amounts for "MAM" and "Even fasterer" are set in a single option. | ||
|  |         if self.options.goal == GOALS.mam and self.options.goal_amount < 27: | ||
|  |             raise OptionError(self.player_name | ||
|  |                               + ": When setting goal to 1 ('mam'), goal_amount must be at least 27 and not " | ||
|  |                               + str(self.options.goal_amount.value)) | ||
|  | 
 | ||
|  |         # If lock_belt_and_extractor is true, the only sphere 1 locations will be achievements | ||
|  |         if self.options.lock_belt_and_extractor and not self.options.include_achievements: | ||
|  |             raise OptionError(self.player_name + ": Achievements must be included when belt and extractor are locked") | ||
|  | 
 | ||
|  |         # Determines maxlevel and finaltier, which are needed for location and item generation | ||
|  |         if self.options.goal == GOALS.vanilla: | ||
|  |             self.maxlevel = 25 | ||
|  |             self.finaltier = 8 | ||
|  |         elif self.options.goal == GOALS.mam: | ||
|  |             self.maxlevel = self.options.goal_amount - 1 | ||
|  |             self.finaltier = 8 | ||
|  |         elif self.options.goal == GOALS.even_fasterer: | ||
|  |             self.maxlevel = 26 | ||
|  |             self.finaltier = self.options.goal_amount.value | ||
|  |         else:  # goal == efficiency_iii | ||
|  |             self.maxlevel = 26 | ||
|  |             self.finaltier = 8 | ||
|  | 
 | ||
|  |         # Setting the seed for the game before any other randomization call is done | ||
|  |         self.client_seed = self.random.randint(0, 100000) | ||
|  | 
 | ||
|  |         # Determines the order of buildings for levels logic | ||
|  |         if self.options.randomize_level_requirements: | ||
|  |             self.level_logic_type = self.options.randomize_level_logic.current_key | ||
|  |             if self.level_logic_type.endswith(OPTIONS.logic_shuffled) or self.level_logic_type == OPTIONS.logic_dopamine: | ||
|  |                 vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker] | ||
|  |                 while len(vanilla_list) > 0: | ||
|  |                     index = self.random.randint(0, len(vanilla_list)-1) | ||
|  |                     next_building = vanilla_list.pop(index) | ||
|  |                     if next_building == ITEMS.cutter: | ||
|  |                         vanilla_list.append(ITEMS.rotator) | ||
|  |                     if next_building == ITEMS.painter: | ||
|  |                         vanilla_list.append(ITEMS.color_mixer) | ||
|  |                     self.level_logic.append(next_building) | ||
|  |             else: | ||
|  |                 self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker] | ||
|  |         else: | ||
|  |             self.level_logic_type = OPTIONS.logic_vanilla | ||
|  |             self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker] | ||
|  | 
 | ||
|  |         # Determines the order of buildings for upgrades logic | ||
|  |         if self.options.randomize_upgrade_requirements: | ||
|  |             self.upgrade_logic_type = self.options.randomize_upgrade_logic.current_key | ||
|  |             if self.upgrade_logic_type == OPTIONS.logic_hardcore: | ||
|  |                 self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker] | ||
|  |             elif self.upgrade_logic_type == OPTIONS.logic_category: | ||
|  |                 self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.stacker, ITEMS.painter, ITEMS.color_mixer] | ||
|  |             else: | ||
|  |                 vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker] | ||
|  |                 while len(vanilla_list) > 0: | ||
|  |                     index = self.random.randint(0, len(vanilla_list)-1) | ||
|  |                     next_building = vanilla_list.pop(index) | ||
|  |                     if next_building == ITEMS.cutter: | ||
|  |                         vanilla_list.append(ITEMS.rotator) | ||
|  |                     if next_building == ITEMS.painter: | ||
|  |                         vanilla_list.append(ITEMS.color_mixer) | ||
|  |                     self.upgrade_logic.append(next_building) | ||
|  |         else: | ||
|  |             self.upgrade_logic_type = OPTIONS.logic_vanilla_like | ||
|  |             self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker] | ||
|  | 
 | ||
|  |         # Determine lenghts of phases in level logic type "random" | ||
|  |         self.random_logic_phase_length = [1, 1, 1, 1, 1] | ||
|  |         if self.level_logic_type.startswith(OPTIONS.logic_random_steps): | ||
|  |             remaininglength = self.maxlevel - 1 | ||
|  |             for phase in range(0, 5): | ||
|  |                 if self.random.random() < 0.1:  # Make sure that longer phases are less frequent | ||
|  |                     self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength) | ||
|  |                 else: | ||
|  |                     self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength // (6 - phase)) | ||
|  |                 remaininglength -= self.random_logic_phase_length[phase] | ||
|  | 
 | ||
|  |         # Determine amount of needed buildings for each category in upgrade logic type "category_random" | ||
|  |         self.category_random_logic_amounts = {CATEGORY.belt_low: 0, CATEGORY.miner_low: 1, | ||
|  |                                               CATEGORY.processors_low: 2, CATEGORY.painting_low: 3} | ||
|  |         if self.upgrade_logic_type == OPTIONS.logic_category_random: | ||
|  |             cats = [CATEGORY.belt_low, CATEGORY.miner_low, CATEGORY.processors_low, CATEGORY.painting_low] | ||
|  |             nextcat = self.random.choice(cats) | ||
|  |             self.category_random_logic_amounts[nextcat] = 0 | ||
|  |             cats.remove(nextcat) | ||
|  |             for cat in cats: | ||
|  |                 self.category_random_logic_amounts[cat] = self.random.randint(0, 5) | ||
|  | 
 | ||
|  |     def create_item(self, name: str) -> Item: | ||
|  |         return ShapezItem(name, item_table[name](self.options), self.item_name_to_id[name], self.player) | ||
|  | 
 | ||
|  |     def get_filler_item_name(self) -> str: | ||
|  |         return filler(self.random.random(), bool(self.options.include_whacky_upgrades)) | ||
|  | 
 | ||
|  |     def append_shapesanity(self, name: str) -> None: | ||
|  |         """This method is given as a parameter when creating the locations for shapesanity.""" | ||
|  |         self.shapesanity_names.append(name) | ||
|  | 
 | ||
|  |     def add_alias(self, location_name: str, alias: str): | ||
|  |         """This method is given as a parameter when locations with helpful aliases for UT are created.""" | ||
|  |         if self.ut_active: | ||
|  |             self.location_id_to_alias[self.location_name_to_id[location_name]] = alias | ||
|  | 
 | ||
|  |     def create_regions(self) -> None: | ||
|  |         # Create list of all included level and upgrade locations based on player options | ||
|  |         # This already includes the region to be placed in and the LocationProgressType | ||
|  |         self.included_locations = {**addlevels(self.maxlevel, self.level_logic_type, | ||
|  |                                                self.random_logic_phase_length), | ||
|  |                                    **addupgrades(self.finaltier, self.upgrade_logic_type, | ||
|  |                                                  self.category_random_logic_amounts)} | ||
|  | 
 | ||
|  |         # Add shapesanity to included location and creates the corresponding list based on player options | ||
|  |         if self.ut_active: | ||
|  |             self.shapesanity_names = self.passthrough[SLOTDATA.shapesanity] | ||
|  |             self.included_locations.update(addshapesanity_ut(self.shapesanity_names, self.add_alias)) | ||
|  |         else: | ||
|  |             self.included_locations.update(addshapesanity(self.options.shapesanity_amount.value, self.random, | ||
|  |                                                           self.append_shapesanity, self.add_alias)) | ||
|  | 
 | ||
|  |         # Add achievements to included locations based on player options | ||
|  |         if self.options.include_achievements: | ||
|  |             self.included_locations.update(addachievements( | ||
|  |                 bool(self.options.exclude_softlock_achievements), bool(self.options.exclude_long_playtime_achievements), | ||
|  |                 bool(self.options.exclude_progression_unreasonable), self.maxlevel, self.upgrade_logic_type, | ||
|  |                 self.category_random_logic_amounts, self.options.goal.current_key, self.included_locations, | ||
|  |                 self.add_alias, self.upgrade_traps_allowed)) | ||
|  | 
 | ||
|  |         # Save the final amount of to-be-filled locations | ||
|  |         self.location_count = len(self.included_locations) | ||
|  | 
 | ||
|  |         # Create regions and entrances based on included locations and player options | ||
|  |         self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld, | ||
|  |                                                              bool(self.options.allow_floating_layers.value), | ||
|  |                                                              self.included_locations, self.location_name_to_id, | ||
|  |                                                              self.level_logic, self.upgrade_logic, | ||
|  |                                                              self.options.early_balancer_tunnel_and_trash.current_key, | ||
|  |                                                              self.options.goal.current_key)) | ||
|  | 
 | ||
|  |     def create_items(self) -> None: | ||
|  |         # Include guaranteed items (game mechanic unlocks and 7x4 big upgrades) | ||
|  |         included_items: List[Item] = ([self.create_item(name) for name in buildings_processing.keys()] | ||
|  |                                       + [self.create_item(name) for name in buildings_routing.keys()] | ||
|  |                                       + [self.create_item(name) for name in buildings_other.keys()] | ||
|  |                                       + [self.create_item(name) for name in buildings_top_row.keys()] | ||
|  |                                       + [self.create_item(name) for name in buildings_wires.keys()] | ||
|  |                                       + [self.create_item(name) for name in gameplay_unlocks.keys()] | ||
|  |                                       + [self.create_item(name) for name in big_upgrades for _ in range(7)]) | ||
|  | 
 | ||
|  |         if not self.options.lock_belt_and_extractor: | ||
|  |             for name in belt_and_extractor: | ||
|  |                 self.multiworld.push_precollected(self.create_item(name)) | ||
|  |         else:  # This also requires self.options.include_achievements to be true | ||
|  |             included_items.extend([self.create_item(name) for name in belt_and_extractor.keys()]) | ||
|  | 
 | ||
|  |         # Give a detailed error message if there are already more items than available locations. | ||
|  |         # At the moment, this won't happen, but it's better for debugging in case a future update breaks things. | ||
|  |         if len(included_items) > self.location_count: | ||
|  |             raise RuntimeError(self.player_name + ": There are more guaranteed items than available locations") | ||
|  | 
 | ||
|  |         # Get value from traps probability option and convert to float | ||
|  |         traps_probability = self.options.traps_percentage/100 | ||
|  |         split_draining = bool(self.options.split_inventory_draining_trap) | ||
|  |         # Fill remaining locations with fillers | ||
|  |         for x in range(self.location_count - len(included_items)): | ||
|  |             if self.random.random() < traps_probability: | ||
|  |                 # Fill with trap | ||
|  |                 included_items.append(self.create_item(trap(self.random.random(), split_draining, | ||
|  |                                                             self.upgrade_traps_allowed))) | ||
|  |             else: | ||
|  |                 # Fil with random filler item | ||
|  |                 included_items.append(self.create_item(self.get_filler_item_name())) | ||
|  | 
 | ||
|  |         # Add correct number of items to itempool | ||
|  |         self.multiworld.itempool += included_items | ||
|  | 
 | ||
|  |         # Add balancer, tunnel, and trash to early items if player options say so | ||
|  |         if self.options.early_balancer_tunnel_and_trash == OPTIONS.sphere_1: | ||
|  |             self.multiworld.early_items[self.player][ITEMS.balancer] = 1 | ||
|  |             self.multiworld.early_items[self.player][ITEMS.tunnel] = 1 | ||
|  |             self.multiworld.early_items[self.player][ITEMS.trash] = 1 | ||
|  | 
 | ||
|  |     def set_rules(self) -> None: | ||
|  |         # Levels might need more belt speed if they require throughput per second. As the randomization of what levels | ||
|  |         # need throughput happens in the client mod, this logic needs to be applied to all levels. This is applied to | ||
|  |         # every individual level instead of regions, because they would need a much more complex calculation to prevent | ||
|  |         # softlocks. | ||
|  | 
 | ||
|  |         def f(x: int, name: str): | ||
|  |             # These calculations are taken from the client mod | ||
|  |             if x < 26: | ||
|  |                 throughput = math.ceil((2.999+x*0.333)*self.options.required_shapes_multiplier/10) | ||
|  |             else: | ||
|  |                 throughput = min((4+(x-26)*0.25)*self.options.required_shapes_multiplier/10, 200) | ||
|  |             if throughput/32 >= 1: | ||
|  |                 add_rule(self.get_location(name), | ||
|  |                          lambda state: has_x_belt_multiplier(state, self.player, throughput/32)) | ||
|  | 
 | ||
|  |         if not self.options.throughput_levels_ratio == 0: | ||
|  |             f(0, LOCATIONS.level(1, 1)) | ||
|  |             f(19, LOCATIONS.level(20, 1)) | ||
|  |             f(19, LOCATIONS.level(20, 2)) | ||
|  |             for _x in range(self.maxlevel): | ||
|  |                 f(_x, LOCATIONS.level(_x+1)) | ||
|  |             if self.options.goal.current_key in [GOALS.vanilla, GOALS.mam]: | ||
|  |                 f(self.maxlevel, LOCATIONS.goal) | ||
|  | 
 | ||
|  |     def fill_slot_data(self) -> Mapping[str, Any]: | ||
|  |         # Buildings logic; all buildings as individual parameters | ||
|  |         level_logic_data = {SLOTDATA.level_building(x+1): self.level_logic[x] for x in range(5)} | ||
|  |         upgrade_logic_data = {SLOTDATA.upgrade_building(x+1): self.upgrade_logic[x] for x in range(5)} | ||
|  |         # Randomized values for certain logic types | ||
|  |         logic_type_random_data = {SLOTDATA.phase_length(x): self.random_logic_phase_length[x] for x in range(0, 5)} | ||
|  |         logic_type_cat_random_data = {SLOTDATA.cat_buildings_amount(cat): self.category_random_logic_amounts[cat] | ||
|  |                                       for cat in [CATEGORY.belt_low, CATEGORY.miner_low, | ||
|  |                                                   CATEGORY.processors_low, CATEGORY.painting_low]} | ||
|  | 
 | ||
|  |         # Options that are relevant to the mod | ||
|  |         option_data = { | ||
|  |             SLOTDATA.goal: self.options.goal.current_key, | ||
|  |             SLOTDATA.maxlevel: self.maxlevel, | ||
|  |             SLOTDATA.finaltier: self.finaltier, | ||
|  |             SLOTDATA.req_shapes_mult: self.options.required_shapes_multiplier.value, | ||
|  |             SLOTDATA.allow_float_layers: bool(self.options.allow_floating_layers), | ||
|  |             SLOTDATA.rand_level_req: bool(self.options.randomize_level_requirements), | ||
|  |             SLOTDATA.rand_upgrade_req: bool(self.options.randomize_upgrade_requirements), | ||
|  |             SLOTDATA.rand_level_logic: self.level_logic_type, | ||
|  |             SLOTDATA.rand_upgrade_logic: self.upgrade_logic_type, | ||
|  |             SLOTDATA.throughput_levels_ratio: self.options.throughput_levels_ratio.value, | ||
|  |             SLOTDATA.comp_growth_gradient: self.options.complexity_growth_gradient.value, | ||
|  |             SLOTDATA.same_late: bool(self.options.same_late_upgrade_requirements), | ||
|  |             SLOTDATA.toolbar_shuffling: bool(self.options.toolbar_shuffling), | ||
|  |         } | ||
|  | 
 | ||
|  |         return {**level_logic_data, **upgrade_logic_data, **option_data, **logic_type_random_data, | ||
|  |                 **logic_type_cat_random_data, SLOTDATA.seed: self.client_seed, | ||
|  |                 SLOTDATA.shapesanity: self.shapesanity_names} | ||
|  | 
 | ||
|  |     def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Dict[str, Any]: | ||
|  |         """Helper function for Universal Tracker""" | ||
|  |         return slot_data |