| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  | import math | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  | from typing import Mapping, Any | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  |         self.level_logic: list[str] = [] | 
					
						
							|  |  |  |         self.upgrade_logic: list[str] = [] | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  |         self.level_logic_type: str = "" | 
					
						
							|  |  |  |         self.upgrade_logic_type: str = "" | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  |         self.random_logic_phase_length: list[int] = [] | 
					
						
							|  |  |  |         self.category_random_logic_amounts: dict[str, int] = {} | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  |         self.maxlevel: int = 0 | 
					
						
							|  |  |  |         self.finaltier: int = 0 | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  |         self.included_locations: dict[str, tuple[str, LocationProgressType]] = {} | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  |         self.client_seed: int = 0 | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  |         self.shapesanity_names: list[str] = [] | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  |         self.upgrade_traps_allowed: bool = False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Universal Tracker support | 
					
						
							|  |  |  |         self.ut_active: bool = False | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  |         self.passthrough: dict[str, Any] = {} | 
					
						
							|  |  |  |         self.location_id_to_alias: dict[int, str] = {} | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @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) | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  |         included_items: list[Item] = ([self.create_item(name) for name in buildings_processing.keys()] | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  |                                       + [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} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-28 17:01:57 +02:00
										 |  |  |     def interpret_slot_data(self, slot_data: dict[str, Any]) -> dict[str, Any]: | 
					
						
							| 
									
										
										
										
											2025-05-21 14:30:39 +02:00
										 |  |  |         """Helper function for Universal Tracker""" | 
					
						
							|  |  |  |         return slot_data |