| 
									
										
										
										
											2023-07-18 19:37:26 -07:00
										 |  |  | # Look at `Rules.dsv` first to get an idea for how this works | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from typing import Union, Tuple, List, Dict, Set | 
					
						
							|  |  |  | from worlds.AutoWorld import WebWorld, World | 
					
						
							|  |  |  | from BaseClasses import Region, ItemClassification, Tutorial, CollectionState | 
					
						
							|  |  |  | from .Checks import ( | 
					
						
							|  |  |  |     TerrariaItem, | 
					
						
							|  |  |  |     TerrariaLocation, | 
					
						
							|  |  |  |     goals, | 
					
						
							|  |  |  |     rules, | 
					
						
							|  |  |  |     rule_indices, | 
					
						
							|  |  |  |     labels, | 
					
						
							|  |  |  |     rewards, | 
					
						
							|  |  |  |     item_name_to_id, | 
					
						
							|  |  |  |     location_name_to_id, | 
					
						
							|  |  |  |     COND_ITEM, | 
					
						
							|  |  |  |     COND_LOC, | 
					
						
							|  |  |  |     COND_FN, | 
					
						
							|  |  |  |     COND_GROUP, | 
					
						
							|  |  |  |     npcs, | 
					
						
							|  |  |  |     pickaxes, | 
					
						
							|  |  |  |     hammers, | 
					
						
							|  |  |  |     mech_bosses, | 
					
						
							|  |  |  |     progression, | 
					
						
							|  |  |  |     armor_minions, | 
					
						
							|  |  |  |     accessory_minions, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | from .Options import options | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TerrariaWeb(WebWorld): | 
					
						
							|  |  |  |     tutorials = [ | 
					
						
							|  |  |  |         Tutorial( | 
					
						
							|  |  |  |             "Multiworld Setup Guide", | 
					
						
							|  |  |  |             "A guide to setting up the Terraria randomizer connected to an Archipelago Multiworld.", | 
					
						
							|  |  |  |             "English", | 
					
						
							|  |  |  |             "setup_en.md", | 
					
						
							|  |  |  |             "setup/en", | 
					
						
							|  |  |  |             ["Seldom"], | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TerrariaWorld(World): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Terraria is a 2D multiplayer sandbox game featuring mining, building, exploration, and combat. | 
					
						
							|  |  |  |     Features 18 bosses and 4 classes. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     game = "Terraria" | 
					
						
							|  |  |  |     web = TerrariaWeb() | 
					
						
							|  |  |  |     option_definitions = options | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # data_version is used to signal that items, locations or their names | 
					
						
							|  |  |  |     # changed. Set this to 0 during development so other games' clients do not | 
					
						
							|  |  |  |     # cache any texts, then increase by 1 for each release that makes changes. | 
					
						
							|  |  |  |     data_version = 2 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     item_name_to_id = item_name_to_id | 
					
						
							|  |  |  |     location_name_to_id = location_name_to_id | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Turn into an option when calamity is supported in the mod | 
					
						
							|  |  |  |     calamity = False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ter_items: List[str] | 
					
						
							|  |  |  |     ter_locations: List[str] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ter_goals: Dict[str, str] | 
					
						
							|  |  |  |     goal_items: Set[str] | 
					
						
							|  |  |  |     goal_locations: Set[str] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def generate_early(self) -> None: | 
					
						
							|  |  |  |         goal, goal_locations = goals[self.multiworld.goal[self.player].value] | 
					
						
							|  |  |  |         ter_goals = {} | 
					
						
							|  |  |  |         goal_items = set() | 
					
						
							|  |  |  |         for location in goal_locations: | 
					
						
							|  |  |  |             _, flags, _, _ = rules[rule_indices[location]] | 
					
						
							|  |  |  |             item = flags.get("Item") or f"Post-{location}" | 
					
						
							|  |  |  |             ter_goals[item] = location | 
					
						
							|  |  |  |             goal_items.add(item) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         achievements = self.multiworld.achievements[self.player].value | 
					
						
							|  |  |  |         location_count = 0 | 
					
						
							|  |  |  |         locations = [] | 
					
						
							|  |  |  |         for rule, flags, _, _ in rules[:goal]: | 
					
						
							|  |  |  |             if ( | 
					
						
							|  |  |  |                 (not self.calamity and "Calamity" in flags) | 
					
						
							|  |  |  |                 or (achievements < 1 and "Achievement" in flags) | 
					
						
							|  |  |  |                 or (achievements < 2 and "Grindy" in flags) | 
					
						
							|  |  |  |                 or (achievements < 3 and "Fishing" in flags) | 
					
						
							|  |  |  |                 or ( | 
					
						
							|  |  |  |                     rule == "Zenith" and self.multiworld.goal[self.player].value != 11 | 
					
						
							|  |  |  |                 )  # Bad hardcoding | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if "Location" in flags or ("Achievement" in flags and achievements >= 1): | 
					
						
							|  |  |  |                 # Location | 
					
						
							|  |  |  |                 location_count += 1 | 
					
						
							|  |  |  |                 locations.append(rule) | 
					
						
							|  |  |  |             elif ( | 
					
						
							|  |  |  |                 "Achievement" not in flags | 
					
						
							|  |  |  |                 and "Location" not in flags | 
					
						
							|  |  |  |                 and "Item" not in flags | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 # Event | 
					
						
							|  |  |  |                 locations.append(rule) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         item_count = 0 | 
					
						
							|  |  |  |         items = [] | 
					
						
							|  |  |  |         for rule, flags, _, _ in rules[:goal]: | 
					
						
							|  |  |  |             if not self.calamity and "Calamity" in flags: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if "Item" in flags: | 
					
						
							|  |  |  |                 # Item | 
					
						
							|  |  |  |                 item_count += 1 | 
					
						
							|  |  |  |                 if rule not in goal_locations: | 
					
						
							|  |  |  |                     items.append(rule) | 
					
						
							|  |  |  |             elif ( | 
					
						
							|  |  |  |                 "Achievement" not in flags | 
					
						
							|  |  |  |                 and "Location" not in flags | 
					
						
							|  |  |  |                 and "Item" not in flags | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 # Event | 
					
						
							|  |  |  |                 items.append(rule) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         extra_checks = self.multiworld.fill_extra_checks_with[self.player].value | 
					
						
							|  |  |  |         ordered_rewards = [ | 
					
						
							|  |  |  |             reward | 
					
						
							|  |  |  |             for reward in labels["ordered"] | 
					
						
							|  |  |  |             if self.calamity or "Calamity" not in rewards[reward] | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |         while extra_checks == 1 and item_count < location_count and ordered_rewards: | 
					
						
							|  |  |  |             items.append(ordered_rewards.pop(0)) | 
					
						
							|  |  |  |             item_count += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         random_rewards = [ | 
					
						
							|  |  |  |             reward | 
					
						
							|  |  |  |             for reward in labels["random"] | 
					
						
							|  |  |  |             if self.calamity or "Calamity" not in rewards[reward] | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |         self.multiworld.random.shuffle(random_rewards) | 
					
						
							|  |  |  |         while extra_checks == 1 and item_count < location_count and random_rewards: | 
					
						
							|  |  |  |             items.append(random_rewards.pop(0)) | 
					
						
							|  |  |  |             item_count += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         while item_count < location_count: | 
					
						
							|  |  |  |             items.append("Reward: Coins") | 
					
						
							|  |  |  |             item_count += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.ter_items = items | 
					
						
							|  |  |  |         self.ter_locations = locations | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.ter_goals = ter_goals | 
					
						
							|  |  |  |         self.goal_items = goal_items | 
					
						
							|  |  |  |         self.goal_locations = goal_locations | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_regions(self) -> None: | 
					
						
							|  |  |  |         menu = Region("Menu", self.player, self.multiworld) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for location in self.ter_locations: | 
					
						
							|  |  |  |             menu.locations.append( | 
					
						
							|  |  |  |                 TerrariaLocation( | 
					
						
							|  |  |  |                     self.player, location, location_name_to_id.get(location), menu | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.multiworld.regions.append(menu) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_item(self, item: str) -> TerrariaItem: | 
					
						
							|  |  |  |         if item in progression: | 
					
						
							|  |  |  |             classification = ItemClassification.progression | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             classification = ItemClassification.filler | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return TerrariaItem(item, classification, item_name_to_id[item], self.player) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_items(self) -> None: | 
					
						
							|  |  |  |         for item in self.ter_items: | 
					
						
							|  |  |  |             if (rule_index := rule_indices.get(item)) is not None: | 
					
						
							|  |  |  |                 _, flags, _, _ = rules[rule_index] | 
					
						
							|  |  |  |                 if "Item" in flags: | 
					
						
							|  |  |  |                     name = flags.get("Item") or f"Post-{item}" | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 name = item | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.multiworld.itempool.append(self.create_item(name)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         locked_items = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for location in self.ter_locations: | 
					
						
							|  |  |  |             _, flags, _, _ = rules[rule_indices[location]] | 
					
						
							|  |  |  |             if "Location" not in flags and "Achievement" not in flags: | 
					
						
							|  |  |  |                 if location in progression: | 
					
						
							|  |  |  |                     classification = ItemClassification.progression | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     classification = ItemClassification.useful | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 locked_items[location] = TerrariaItem( | 
					
						
							|  |  |  |                     location, classification, None, self.player | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for item, location in self.ter_goals.items(): | 
					
						
							|  |  |  |             locked_items[location] = self.create_item(item) | 
					
						
							|  |  |  |         for location, item in locked_items.items(): | 
					
						
							|  |  |  |             self.multiworld.get_location(location, self.player).place_locked_item(item) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def check_condition( | 
					
						
							|  |  |  |         self, | 
					
						
							|  |  |  |         state, | 
					
						
							|  |  |  |         sign: bool, | 
					
						
							|  |  |  |         ty: int, | 
					
						
							|  |  |  |         condition: Union[str, Tuple[Union[bool, None], list]], | 
					
						
							|  |  |  |         arg: Union[str, int, None], | 
					
						
							|  |  |  |     ) -> bool: | 
					
						
							|  |  |  |         if ty == COND_ITEM: | 
					
						
							|  |  |  |             _, flags, _, _ = rules[rule_indices[condition]] | 
					
						
							|  |  |  |             if "Item" in flags: | 
					
						
							|  |  |  |                 name = flags.get("Item") or f"Post-{condition}" | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 name = condition | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return sign == state.has(name, self.player) | 
					
						
							|  |  |  |         elif ty == COND_LOC: | 
					
						
							|  |  |  |             _, _, operator, conditions = rules[rule_indices[condition]] | 
					
						
							|  |  |  |             return sign == self.check_conditions(state, operator, conditions) | 
					
						
							|  |  |  |         elif ty == COND_FN: | 
					
						
							|  |  |  |             if condition == "npc": | 
					
						
							|  |  |  |                 if type(arg) is not int: | 
					
						
							|  |  |  |                     raise Exception("@npc requires an integer argument") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 npc_count = 0 | 
					
						
							|  |  |  |                 for npc in npcs: | 
					
						
							|  |  |  |                     if state.has(npc, self.player): | 
					
						
							|  |  |  |                         npc_count += 1 | 
					
						
							|  |  |  |                         if npc_count >= arg: | 
					
						
							|  |  |  |                             return sign | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return not sign | 
					
						
							|  |  |  |             elif condition == "calamity": | 
					
						
							|  |  |  |                 return sign == self.calamity | 
					
						
							| 
									
										
										
										
											2024-04-13 17:18:02 -07:00
										 |  |  |             elif condition == "grindy": | 
					
						
							|  |  |  |                 return sign == (self.multiworld.achievements[self.player].value >= 2) | 
					
						
							| 
									
										
										
										
											2023-07-18 19:37:26 -07:00
										 |  |  |             elif condition == "pickaxe": | 
					
						
							|  |  |  |                 if type(arg) is not int: | 
					
						
							|  |  |  |                     raise Exception("@pickaxe requires an integer argument") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 for pickaxe, power in pickaxes.items(): | 
					
						
							|  |  |  |                     if power >= arg and state.has(pickaxe, self.player): | 
					
						
							|  |  |  |                         return sign | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return not sign | 
					
						
							|  |  |  |             elif condition == "hammer": | 
					
						
							|  |  |  |                 if type(arg) is not int: | 
					
						
							|  |  |  |                     raise Exception("@hammer requires an integer argument") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 for hammer, power in hammers.items(): | 
					
						
							|  |  |  |                     if power >= arg and state.has(hammer, self.player): | 
					
						
							|  |  |  |                         return sign | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return not sign | 
					
						
							|  |  |  |             elif condition == "mech_boss": | 
					
						
							|  |  |  |                 if type(arg) is not int: | 
					
						
							|  |  |  |                     raise Exception("@mech_boss requires an integer argument") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 boss_count = 0 | 
					
						
							|  |  |  |                 for boss in mech_bosses: | 
					
						
							|  |  |  |                     if state.has(boss, self.player): | 
					
						
							|  |  |  |                         boss_count += 1 | 
					
						
							|  |  |  |                         if boss_count >= arg: | 
					
						
							|  |  |  |                             return sign | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return not sign | 
					
						
							|  |  |  |             elif condition == "minions": | 
					
						
							|  |  |  |                 if type(arg) is not int: | 
					
						
							|  |  |  |                     raise Exception("@minions requires an integer argument") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 minion_count = 1 | 
					
						
							|  |  |  |                 for armor, minions in armor_minions.items(): | 
					
						
							|  |  |  |                     if state.has(armor, self.player) and minions + 1 > minion_count: | 
					
						
							|  |  |  |                         minion_count = minions + 1 | 
					
						
							|  |  |  |                         if minion_count >= arg: | 
					
						
							|  |  |  |                             return sign | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 for accessory, minions in accessory_minions.items(): | 
					
						
							|  |  |  |                     if state.has(accessory, self.player): | 
					
						
							|  |  |  |                         minion_count += minions | 
					
						
							|  |  |  |                         if minion_count >= arg: | 
					
						
							|  |  |  |                             return sign | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return not sign | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 raise Exception(f"Unknown function {condition}") | 
					
						
							|  |  |  |         elif ty == COND_GROUP: | 
					
						
							|  |  |  |             operator, conditions = condition | 
					
						
							|  |  |  |             return sign == self.check_conditions(state, operator, conditions) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def check_conditions( | 
					
						
							|  |  |  |         self, | 
					
						
							|  |  |  |         state, | 
					
						
							|  |  |  |         operator: Union[bool, None], | 
					
						
							|  |  |  |         conditions: List[ | 
					
						
							|  |  |  |             Tuple[ | 
					
						
							|  |  |  |                 bool, | 
					
						
							|  |  |  |                 int, | 
					
						
							|  |  |  |                 Union[str, Tuple[Union[bool, None], list]], | 
					
						
							|  |  |  |                 Union[str, int, None], | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |     ) -> bool: | 
					
						
							|  |  |  |         if operator is None: | 
					
						
							|  |  |  |             if len(conditions) == 0: | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             if len(conditions) > 1: | 
					
						
							|  |  |  |                 raise Exception("Found multiple conditions without an operator") | 
					
						
							|  |  |  |             return self.check_condition(state, *conditions[0]) | 
					
						
							|  |  |  |         elif operator: | 
					
						
							|  |  |  |             return any( | 
					
						
							|  |  |  |                 self.check_condition(state, *condition) for condition in conditions | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             return all( | 
					
						
							|  |  |  |                 self.check_condition(state, *condition) for condition in conditions | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def set_rules(self) -> None: | 
					
						
							|  |  |  |         for location in self.ter_locations: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             def check(state: CollectionState, location=location): | 
					
						
							|  |  |  |                 _, _, operator, conditions = rules[rule_indices[location]] | 
					
						
							|  |  |  |                 return self.check_conditions(state, operator, conditions) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.multiworld.get_location(location, self.player).access_rule = check | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.multiworld.completion_condition[self.player] = lambda state: state.has_all( | 
					
						
							|  |  |  |             self.goal_items, self.player | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def fill_slot_data(self) -> Dict[str, object]: | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             "goal": list(self.goal_locations), | 
					
						
							| 
									
										
										
										
											2023-09-14 00:46:29 -07:00
										 |  |  |             "achievements": self.multiworld.achievements[self.player].value, | 
					
						
							| 
									
										
										
										
											2023-07-18 19:37:26 -07:00
										 |  |  |             "deathlink": bool(self.multiworld.death_link[self.player]), | 
					
						
							|  |  |  |         } |