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.
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:
|
|
multiworld.worlds[item.player].collect(all_state_base, item)
|
|
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:
|
|
multiworld.worlds[item.player].collect(all_state_base, item)
|
|
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",
|
|
)
|