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",
 | 
						|
            )
 |