343 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			343 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | # 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 | ||
|  |             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), | ||
|  |             "deathlink": bool(self.multiworld.death_link[self.player]), | ||
|  |         } |