122 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			122 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								from typing import TYPE_CHECKING
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from Options import OptionError
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from ..Locations import DUNGEON_NAMES, LOCATION_TABLE, TWWFlag, split_location_name_by_zone
							 | 
						||
| 
								 | 
							
								from ..Options import TWWOptions
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								if TYPE_CHECKING:
							 | 
						||
| 
								 | 
							
								    from .. import TWWWorld
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class RequiredBossesRandomizer:
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    This class handles the randomization of the required bosses in The Wind Waker game based on user options.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    If the option is on, the required bosses must be defeated as part of the unlock condition of Puppet Ganon's door.
							 | 
						||
| 
								 | 
							
								    The quadrants in which the bosses are located are marked on the player's Sea Chart.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    :param world: The Wind Waker game world.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __init__(self, world: "TWWWorld"):
							 | 
						||
| 
								 | 
							
								        self.world = world
							 | 
						||
| 
								 | 
							
								        self.multiworld = world.multiworld
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.required_boss_item_locations: list[str] = []
							 | 
						||
| 
								 | 
							
								        self.required_dungeons: set[str] = set()
							 | 
						||
| 
								 | 
							
								        self.required_bosses: list[str] = []
							 | 
						||
| 
								 | 
							
								        self.banned_locations: set[str] = set()
							 | 
						||
| 
								 | 
							
								        self.banned_dungeons: set[str] = set()
							 | 
						||
| 
								 | 
							
								        self.banned_bosses: list[str] = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def validate_boss_options(self, options: TWWOptions) -> None:
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        Validate the user-defined boss options to ensure logical consistency.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        :param options: The game options set by the user.
							 | 
						||
| 
								 | 
							
								        :raises OptionError: If the boss options are inconsistent.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        if not options.progression_dungeons:
							 | 
						||
| 
								 | 
							
								            raise OptionError("You cannot make bosses required when progression dungeons are disabled.")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if len(options.included_dungeons.value & options.excluded_dungeons.value) != 0:
							 | 
						||
| 
								 | 
							
								            raise OptionError(
							 | 
						||
| 
								 | 
							
								                "A conflict was found in the lists of required and banned dungeons for required bosses mode."
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def randomize_required_bosses(self) -> None:
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        Randomize the required bosses based on user-defined constraints and options.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        :raises OptionError: If the randomization fails to meet user-defined constraints.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        options = self.world.options
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Validate constraints on required bosses options.
							 | 
						||
| 
								 | 
							
								        self.validate_boss_options(options)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # If the user enforces a dungeon location to be priority, consider that when selecting required bosses.
							 | 
						||
| 
								 | 
							
								        dungeon_names = set(DUNGEON_NAMES)
							 | 
						||
| 
								 | 
							
								        required_dungeons = options.included_dungeons.value
							 | 
						||
| 
								 | 
							
								        for location_name in options.priority_locations.value:
							 | 
						||
| 
								 | 
							
								            dungeon_name, _ = split_location_name_by_zone(location_name)
							 | 
						||
| 
								 | 
							
								            if dungeon_name in dungeon_names:
							 | 
						||
| 
								 | 
							
								                required_dungeons.add(dungeon_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Ensure we aren't prioritizing more dungeon locations than the requested number of required bosses.
							 | 
						||
| 
								 | 
							
								        num_required_bosses = options.num_required_bosses
							 | 
						||
| 
								 | 
							
								        if len(required_dungeons) > num_required_bosses:
							 | 
						||
| 
								 | 
							
								            raise OptionError(
							 | 
						||
| 
								 | 
							
								                "Could not select required bosses to satisfy options set by the user. "
							 | 
						||
| 
								 | 
							
								                "There are more dungeons with priority locations than the desired number of required bosses."
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Ensure that after removing excluded dungeons, we still have enough to satisfy user options.
							 | 
						||
| 
								 | 
							
								        num_remaining = num_required_bosses - len(required_dungeons)
							 | 
						||
| 
								 | 
							
								        remaining_dungeon_options = dungeon_names - required_dungeons - options.excluded_dungeons.value
							 | 
						||
| 
								 | 
							
								        if len(remaining_dungeon_options) < num_remaining:
							 | 
						||
| 
								 | 
							
								            raise OptionError(
							 | 
						||
| 
								 | 
							
								                "Could not select required bosses to satisfy options set by the user. "
							 | 
						||
| 
								 | 
							
								                "After removing the excluded dungeons, there are not enough to meet the desired number of required "
							 | 
						||
| 
								 | 
							
								                "bosses."
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Finish selecting required bosses.
							 | 
						||
| 
								 | 
							
								        required_dungeons.update(self.world.random.sample(sorted(remaining_dungeon_options), num_remaining))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Exclude locations that are not in the dungeon of a required boss.
							 | 
						||
| 
								 | 
							
								        banned_dungeons = dungeon_names - required_dungeons
							 | 
						||
| 
								 | 
							
								        for location_name, location_data in LOCATION_TABLE.items():
							 | 
						||
| 
								 | 
							
								            dungeon_name, _ = split_location_name_by_zone(location_name)
							 | 
						||
| 
								 | 
							
								            if dungeon_name in banned_dungeons and TWWFlag.DUNGEON in location_data.flags:
							 | 
						||
| 
								 | 
							
								                self.banned_locations.add(location_name)
							 | 
						||
| 
								 | 
							
								            elif location_name == "Mailbox - Letter from Orca" and "Forbidden Woods" in banned_dungeons:
							 | 
						||
| 
								 | 
							
								                self.banned_locations.add(location_name)
							 | 
						||
| 
								 | 
							
								            elif location_name == "Mailbox - Letter from Baito" and "Earth Temple" in banned_dungeons:
							 | 
						||
| 
								 | 
							
								                self.banned_locations.add(location_name)
							 | 
						||
| 
								 | 
							
								            elif location_name == "Mailbox - Letter from Aryll" and "Forsaken Fortress" in banned_dungeons:
							 | 
						||
| 
								 | 
							
								                self.banned_locations.add(location_name)
							 | 
						||
| 
								 | 
							
								            elif location_name == "Mailbox - Letter from Tingle" and "Forsaken Fortress" in banned_dungeons:
							 | 
						||
| 
								 | 
							
								                self.banned_locations.add(location_name)
							 | 
						||
| 
								 | 
							
								        for location_name in self.banned_locations:
							 | 
						||
| 
								 | 
							
								            self.world.nonprogress_locations.add(location_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Record the item location names for required bosses.
							 | 
						||
| 
								 | 
							
								        self.required_boss_item_locations = []
							 | 
						||
| 
								 | 
							
								        self.required_bosses = []
							 | 
						||
| 
								 | 
							
								        self.banned_bosses = []
							 | 
						||
| 
								 | 
							
								        possible_boss_item_locations = [loc for loc, data in LOCATION_TABLE.items() if TWWFlag.BOSS in data.flags]
							 | 
						||
| 
								 | 
							
								        for location_name in possible_boss_item_locations:
							 | 
						||
| 
								 | 
							
								            dungeon_name, specific_location_name = split_location_name_by_zone(location_name)
							 | 
						||
| 
								 | 
							
								            assert specific_location_name.endswith(" Heart Container")
							 | 
						||
| 
								 | 
							
								            boss_name = specific_location_name.removesuffix(" Heart Container")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            if dungeon_name in required_dungeons:
							 | 
						||
| 
								 | 
							
								                self.required_boss_item_locations.append(location_name)
							 | 
						||
| 
								 | 
							
								                self.required_bosses.append(boss_name)
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                self.banned_bosses.append(boss_name)
							 | 
						||
| 
								 | 
							
								        self.required_dungeons = required_dungeons
							 | 
						||
| 
								 | 
							
								        self.banned_dungeons = banned_dungeons
							 |