285 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			285 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from typing import TYPE_CHECKING, Any, Optional | ||
|  | 
 | ||
|  | from BaseClasses import CollectionState, Item, Location, MultiWorld | ||
|  | from Fill import fill_restrictive | ||
|  | from worlds.generic.Rules import add_item_rule | ||
|  | 
 | ||
|  | from ..Items import item_factory | ||
|  | 
 | ||
|  | if TYPE_CHECKING: | ||
|  |     from .. import TWWWorld | ||
|  | 
 | ||
|  | 
 | ||
|  | class Dungeon: | ||
|  |     """
 | ||
|  |     This class represents a dungeon in The Wind Waker, including its dungeon items. | ||
|  | 
 | ||
|  |     :param name: The name of the dungeon. | ||
|  |     :param big_key: The big key item for the dungeon. | ||
|  |     :param small_keys: A list of small key items for the dungeon. | ||
|  |     :param dungeon_items: A list of other items specific to the dungeon. | ||
|  |     :param player: The ID of the player associated with the dungeon. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     def __init__( | ||
|  |         self, | ||
|  |         name: str, | ||
|  |         big_key: Optional[Item], | ||
|  |         small_keys: list[Item], | ||
|  |         dungeon_items: list[Item], | ||
|  |         player: int, | ||
|  |     ): | ||
|  |         self.name = name | ||
|  |         self.big_key = big_key | ||
|  |         self.small_keys = small_keys | ||
|  |         self.dungeon_items = dungeon_items | ||
|  |         self.player = player | ||
|  | 
 | ||
|  |     @property | ||
|  |     def keys(self) -> list[Item]: | ||
|  |         """
 | ||
|  |         Retrieve all the keys for the dungeon. | ||
|  | 
 | ||
|  |         :return: A list of Small Keys and the Big Key (if it exists). | ||
|  |         """
 | ||
|  |         return self.small_keys + ([self.big_key] if self.big_key else []) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def all_items(self) -> list[Item]: | ||
|  |         """
 | ||
|  |         Retrieve all items associated with the dungeon. | ||
|  | 
 | ||
|  |         :return: A list of all items associated with the dungeon. | ||
|  |         """
 | ||
|  |         return self.dungeon_items + self.keys | ||
|  | 
 | ||
|  |     def __eq__(self, other: Any) -> bool: | ||
|  |         """
 | ||
|  |         Check equality between this dungeon and another object. | ||
|  | 
 | ||
|  |         :param other: The object to compare. | ||
|  |         :return: `True` if the other object is a Dungeon with the same name and player, `False` otherwise. | ||
|  |         """
 | ||
|  |         if isinstance(other, Dungeon): | ||
|  |             return self.name == other.name and self.player == other.player | ||
|  |         return False | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         """
 | ||
|  |         Provide a string representation of the dungeon. | ||
|  | 
 | ||
|  |         :return: A string representing the dungeon. | ||
|  |         """
 | ||
|  |         return self.__str__() | ||
|  | 
 | ||
|  |     def __str__(self) -> str: | ||
|  |         """
 | ||
|  |         Convert the dungeon to a human-readable string. | ||
|  | 
 | ||
|  |         :return: A string in the format "<name> (Player <player>)". | ||
|  |         """
 | ||
|  |         return f"{self.name} (Player {self.player})" | ||
|  | 
 | ||
|  | 
 | ||
|  | def create_dungeons(world: "TWWWorld") -> None: | ||
|  |     """
 | ||
|  |     Create and assign dungeons to the given world based on game options. | ||
|  | 
 | ||
|  |     :param world: The Wind Waker game world. | ||
|  |     """
 | ||
|  |     player = world.player | ||
|  |     options = world.options | ||
|  | 
 | ||
|  |     def make_dungeon(name: str, big_key: Optional[Item], small_keys: list[Item], dungeon_items: list[Item]) -> Dungeon: | ||
|  |         dungeon = Dungeon(name, big_key, small_keys, dungeon_items, player) | ||
|  |         for item in dungeon.all_items: | ||
|  |             item.dungeon = dungeon | ||
|  |         return dungeon | ||
|  | 
 | ||
|  |     if options.progression_dungeons: | ||
|  |         if not options.required_bosses or "Dragon Roost Cavern" in world.boss_reqs.required_dungeons: | ||
|  |             world.dungeons["Dragon Roost Cavern"] = make_dungeon( | ||
|  |                 "Dragon Roost Cavern", | ||
|  |                 item_factory("DRC Big Key", world), | ||
|  |                 item_factory(["DRC Small Key"] * 4, world), | ||
|  |                 item_factory(["DRC Dungeon Map", "DRC Compass"], world), | ||
|  |             ) | ||
|  | 
 | ||
|  |         if not options.required_bosses or "Forbidden Woods" in world.boss_reqs.required_dungeons: | ||
|  |             world.dungeons["Forbidden Woods"] = make_dungeon( | ||
|  |                 "Forbidden Woods", | ||
|  |                 item_factory("FW Big Key", world), | ||
|  |                 item_factory(["FW Small Key"] * 1, world), | ||
|  |                 item_factory(["FW Dungeon Map", "FW Compass"], world), | ||
|  |             ) | ||
|  | 
 | ||
|  |         if not options.required_bosses or "Tower of the Gods" in world.boss_reqs.required_dungeons: | ||
|  |             world.dungeons["Tower of the Gods"] = make_dungeon( | ||
|  |                 "Tower of the Gods", | ||
|  |                 item_factory("TotG Big Key", world), | ||
|  |                 item_factory(["TotG Small Key"] * 2, world), | ||
|  |                 item_factory(["TotG Dungeon Map", "TotG Compass"], world), | ||
|  |             ) | ||
|  | 
 | ||
|  |         if not options.required_bosses or "Forsaken Fortress" in world.boss_reqs.required_dungeons: | ||
|  |             world.dungeons["Forsaken Fortress"] = make_dungeon( | ||
|  |                 "Forsaken Fortress", | ||
|  |                 None, | ||
|  |                 [], | ||
|  |                 item_factory(["FF Dungeon Map", "FF Compass"], world), | ||
|  |             ) | ||
|  | 
 | ||
|  |         if not options.required_bosses or "Earth Temple" in world.boss_reqs.required_dungeons: | ||
|  |             world.dungeons["Earth Temple"] = make_dungeon( | ||
|  |                 "Earth Temple", | ||
|  |                 item_factory("ET Big Key", world), | ||
|  |                 item_factory(["ET Small Key"] * 3, world), | ||
|  |                 item_factory(["ET Dungeon Map", "ET Compass"], world), | ||
|  |             ) | ||
|  | 
 | ||
|  |         if not options.required_bosses or "Wind Temple" in world.boss_reqs.required_dungeons: | ||
|  |             world.dungeons["Wind Temple"] = make_dungeon( | ||
|  |                 "Wind Temple", | ||
|  |                 item_factory("WT Big Key", world), | ||
|  |                 item_factory(["WT Small Key"] * 2, world), | ||
|  |                 item_factory(["WT Dungeon Map", "WT Compass"], world), | ||
|  |             ) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_dungeon_item_pool(multiworld: MultiWorld) -> list[Item]: | ||
|  |     """
 | ||
|  |     Retrieve the item pool for all The Wind Waker dungeons in the multiworld. | ||
|  | 
 | ||
|  |     :param multiworld: The MultiWorld instance. | ||
|  |     :return: List of dungeon items across all The Wind Waker dungeons. | ||
|  |     """
 | ||
|  |     return [ | ||
|  |         item for world in multiworld.get_game_worlds("The Wind Waker") for item in get_dungeon_item_pool_player(world) | ||
|  |     ] | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_dungeon_item_pool_player(world: "TWWWorld") -> list[Item]: | ||
|  |     """
 | ||
|  |     Retrieve the item pool for all dungeons specific to a player. | ||
|  | 
 | ||
|  |     :param world: The Wind Waker game world. | ||
|  |     :return: List of items in the player's dungeons. | ||
|  |     """
 | ||
|  |     return [item for dungeon in world.dungeons.values() for item in dungeon.all_items] | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_unfilled_dungeon_locations(multiworld: MultiWorld) -> list[Location]: | ||
|  |     """
 | ||
|  |     Retrieve all unfilled The Wind Waker dungeon locations in the multiworld. | ||
|  | 
 | ||
|  |     :param multiworld: The MultiWorld instance. | ||
|  |     :return: List of unfilled The Wind Waker dungeon locations. | ||
|  |     """
 | ||
|  |     return [ | ||
|  |         location | ||
|  |         for world in multiworld.get_game_worlds("The Wind Waker") | ||
|  |         for location in multiworld.get_locations(world.player) | ||
|  |         if location.dungeon and not location.item | ||
|  |     ] | ||
|  | 
 | ||
|  | 
 | ||
|  | def modify_dungeon_location_rules(multiworld: MultiWorld) -> None: | ||
|  |     """
 | ||
|  |     Modify the rules for The Wind Waker dungeon locations based on specific player-requested constraints. | ||
|  | 
 | ||
|  |     :param multiworld: The MultiWorld instance. | ||
|  |     """
 | ||
|  |     localized: set[tuple[int, str]] = set() | ||
|  |     dungeon_specific: set[tuple[int, str]] = set() | ||
|  |     for subworld in multiworld.get_game_worlds("The Wind Waker"): | ||
|  |         player = subworld.player | ||
|  |         if player not in multiworld.groups: | ||
|  |             localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names} | ||
|  |             dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names} | ||
|  | 
 | ||
|  |     if localized: | ||
|  |         in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized] | ||
|  |         if in_dungeon_items: | ||
|  |             locations = [location for location in get_unfilled_dungeon_locations(multiworld)] | ||
|  | 
 | ||
|  |             for location in locations: | ||
|  |                 if dungeon_specific: | ||
|  |                     # Special case: If Dragon Roost Cavern has its own small keys, then ensure the first chest isn't the | ||
|  |                     # Big Key. This is to avoid placing the Big Key there during fill and resulting in a costly swap. | ||
|  |                     if location.name == "Dragon Roost Cavern - First Room": | ||
|  |                         add_item_rule( | ||
|  |                             location, | ||
|  |                             lambda item: item.name != "DRC Big Key" | ||
|  |                             or (item.player, "DRC Small Key") in dungeon_specific, | ||
|  |                         ) | ||
|  | 
 | ||
|  |                     # Add item rule to ensure dungeon items are in their own dungeon when they should be. | ||
|  |                     add_item_rule( | ||
|  |                         location, | ||
|  |                         lambda item, dungeon=location.dungeon: not (item.player, item.name) in dungeon_specific | ||
|  |                         or item.dungeon is dungeon, | ||
|  |                     ) | ||
|  | 
 | ||
|  | 
 | ||
|  | def fill_dungeons_restrictive(multiworld: MultiWorld) -> None: | ||
|  |     """
 | ||
|  |     Correctly fill The Wind Waker dungeons in the multiworld. | ||
|  | 
 | ||
|  |     :param multiworld: The MultiWorld instance. | ||
|  |     """
 | ||
|  |     localized: set[tuple[int, str]] = set() | ||
|  |     dungeon_specific: set[tuple[int, str]] = set() | ||
|  |     for subworld in multiworld.get_game_worlds("The Wind Waker"): | ||
|  |         player = subworld.player | ||
|  |         if player not in multiworld.groups: | ||
|  |             localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names} | ||
|  |             dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names} | ||
|  | 
 | ||
|  |     if localized: | ||
|  |         in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized] | ||
|  |         if in_dungeon_items: | ||
|  |             locations = [location for location in get_unfilled_dungeon_locations(multiworld)] | ||
|  |             multiworld.random.shuffle(locations) | ||
|  | 
 | ||
|  |             # Dungeon-locked items have to be placed first so as not to run out of space for dungeon-locked items. | ||
|  |             # Subsort in the order Big Key, Small Key, Other before placing dungeon items. | ||
|  |             sort_order = {"Big Key": 3, "Small Key": 2} | ||
|  |             in_dungeon_items.sort( | ||
|  |                 key=lambda item: sort_order.get(item.type, 1) | ||
|  |                 + (5 if (item.player, item.name) in dungeon_specific else 0) | ||
|  |             ) | ||
|  | 
 | ||
|  |             # Construct a partial `all_state` that contains only the items from `get_pre_fill_items` that aren't in a | ||
|  |             # dungeon. | ||
|  |             in_dungeon_player_ids = {item.player for item in in_dungeon_items} | ||
|  |             all_state_base = CollectionState(multiworld) | ||
|  |             for item in multiworld.itempool: | ||
|  |                 multiworld.worlds[item.player].collect(all_state_base, item) | ||
|  |             pre_fill_items = [] | ||
|  |             for player in in_dungeon_player_ids: | ||
|  |                 pre_fill_items += multiworld.worlds[player].get_pre_fill_items() | ||
|  |             for item in in_dungeon_items: | ||
|  |                 try: | ||
|  |                     pre_fill_items.remove(item) | ||
|  |                 except ValueError: | ||
|  |                     # `pre_fill_items` should be a subset of `in_dungeon_items`, but just in case. | ||
|  |                     pass | ||
|  |             for item in pre_fill_items: | ||
|  |                 multiworld.worlds[item.player].collect(all_state_base, item) | ||
|  |             all_state_base.sweep_for_advancements() | ||
|  | 
 | ||
|  |             # Remove the completion condition so that minimal-accessibility words place keys correctly. | ||
|  |             for player in (item.player for item in in_dungeon_items): | ||
|  |                 if all_state_base.has("Victory", player): | ||
|  |                     all_state_base.remove(multiworld.worlds[player].create_item("Victory")) | ||
|  | 
 | ||
|  |             fill_restrictive( | ||
|  |                 multiworld, | ||
|  |                 all_state_base, | ||
|  |                 locations, | ||
|  |                 in_dungeon_items, | ||
|  |                 lock=True, | ||
|  |                 allow_excluded=True, | ||
|  |                 name="TWW Dungeon Items", | ||
|  |             ) |