mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00

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
|