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:
 | |
|                 all_state_base.collect(item, prevent_sweep=True)
 | |
|             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:
 | |
|                 all_state_base.collect(item, prevent_sweep=True)
 | |
|             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",
 | |
|             )
 | 
