# 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), "achievements": self.multiworld.achievements[self.player].value, "deathlink": bool(self.multiworld.death_link[self.player]), }