 cf0ae5e31b
			
		
	
	cf0ae5e31b
	
	
	
		
			
			Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
		
			
				
	
	
		
			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
 |