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.
599 lines
27 KiB
Python
599 lines
27 KiB
Python
import os
|
|
import zipfile
|
|
from base64 import b64encode
|
|
from collections.abc import Mapping
|
|
from typing import Any, ClassVar
|
|
|
|
import yaml
|
|
|
|
from BaseClasses import Item
|
|
from BaseClasses import ItemClassification as IC
|
|
from BaseClasses import MultiWorld, Region, Tutorial
|
|
from Options import Toggle
|
|
from worlds.AutoWorld import WebWorld, World
|
|
from worlds.Files import APContainer, AutoPatchRegister
|
|
from worlds.generic.Rules import add_item_rule
|
|
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, icon_paths, launch_subprocess
|
|
|
|
from .Items import ISLAND_NUMBER_TO_CHART_NAME, ITEM_TABLE, TWWItem, item_name_groups
|
|
from .Locations import LOCATION_TABLE, TWWFlag, TWWLocation
|
|
from .Options import TWWOptions, tww_option_groups
|
|
from .Presets import tww_options_presets
|
|
from .randomizers.Charts import ISLAND_NUMBER_TO_NAME, ChartRandomizer
|
|
from .randomizers.Dungeons import Dungeon, create_dungeons
|
|
from .randomizers.Entrances import ALL_EXITS, BOSS_EXIT_TO_DUNGEON, MINIBOSS_EXIT_TO_DUNGEON, EntranceRandomizer
|
|
from .randomizers.ItemPool import generate_itempool
|
|
from .randomizers.RequiredBosses import RequiredBossesRandomizer
|
|
from .Rules import set_rules
|
|
|
|
VERSION: tuple[int, int, int] = (3, 0, 0)
|
|
|
|
|
|
def run_client() -> None:
|
|
"""
|
|
Launch the The Wind Waker client.
|
|
"""
|
|
print("Running The Wind Waker Client")
|
|
from .TWWClient import main
|
|
|
|
launch_subprocess(main, name="TheWindWakerClient")
|
|
|
|
|
|
components.append(
|
|
Component(
|
|
"The Wind Waker Client",
|
|
func=run_client,
|
|
component_type=Type.CLIENT,
|
|
file_identifier=SuffixIdentifier(".aptww"),
|
|
icon="The Wind Waker",
|
|
)
|
|
)
|
|
icon_paths["The Wind Waker"] = "ap:worlds.tww/assets/icon.png"
|
|
|
|
|
|
class TWWContainer(APContainer, metaclass=AutoPatchRegister):
|
|
"""
|
|
This class defines the container file for The Wind Waker.
|
|
"""
|
|
|
|
game: str = "The Wind Waker"
|
|
patch_file_ending: str = ".aptww"
|
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
if "data" in kwargs:
|
|
self.data = kwargs["data"]
|
|
del kwargs["data"]
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
|
"""
|
|
Write the contents of the container file.
|
|
"""
|
|
super().write_contents(opened_zipfile)
|
|
|
|
# Record the data for the game under the key `plando`.
|
|
opened_zipfile.writestr("plando", b64encode(bytes(yaml.safe_dump(self.data, sort_keys=False), "utf-8")))
|
|
|
|
|
|
class TWWWeb(WebWorld):
|
|
"""
|
|
This class handles the web interface for The Wind Waker.
|
|
|
|
The web interface includes the setup guide and the options page for generating YAMLs.
|
|
"""
|
|
|
|
tutorials = [
|
|
Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to setting up the Archipelago The Wind Waker software on your computer.",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["tanjo3", "Lunix"],
|
|
)
|
|
]
|
|
theme = "ocean"
|
|
options_presets = tww_options_presets
|
|
option_groups = tww_option_groups
|
|
rich_text_options_doc = True
|
|
|
|
|
|
class TWWWorld(World):
|
|
"""
|
|
Legend has it that whenever evil has appeared, a hero named Link has arisen to defeat it. The legend continues on
|
|
the surface of a vast and mysterious sea as Link sets sail in his most epic, awe-inspiring adventure yet. Aided by a
|
|
magical conductor's baton called the Wind Waker, he will face unimaginable monsters, explore puzzling dungeons, and
|
|
meet a cast of unforgettable characters as he searches for his kidnapped sister.
|
|
"""
|
|
|
|
options_dataclass = TWWOptions
|
|
options: TWWOptions
|
|
|
|
game: ClassVar[str] = "The Wind Waker"
|
|
topology_present: bool = True
|
|
|
|
item_name_to_id: ClassVar[dict[str, int]] = {
|
|
name: TWWItem.get_apid(data.code) for name, data in ITEM_TABLE.items() if data.code is not None
|
|
}
|
|
location_name_to_id: ClassVar[dict[str, int]] = {
|
|
name: TWWLocation.get_apid(data.code) for name, data in LOCATION_TABLE.items() if data.code is not None
|
|
}
|
|
|
|
item_name_groups: ClassVar[dict[str, set[str]]] = item_name_groups
|
|
|
|
required_client_version: tuple[int, int, int] = (0, 5, 1)
|
|
|
|
web: ClassVar[TWWWeb] = TWWWeb()
|
|
|
|
origin_region_name: str = "The Great Sea"
|
|
|
|
create_items = generate_itempool
|
|
|
|
logic_rematch_bosses_skipped: bool
|
|
logic_in_swordless_mode: bool
|
|
logic_in_required_bosses_mode: bool
|
|
logic_obscure_1: bool
|
|
logic_obscure_2: bool
|
|
logic_obscure_3: bool
|
|
logic_precise_1: bool
|
|
logic_precise_2: bool
|
|
logic_precise_3: bool
|
|
logic_tuner_logic_enabled: bool
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.progress_locations: set[str] = set()
|
|
self.nonprogress_locations: set[str] = set()
|
|
|
|
self.dungeon_local_item_names: set[str] = set()
|
|
self.dungeon_specific_item_names: set[str] = set()
|
|
self.dungeons: dict[str, Dungeon] = {}
|
|
|
|
self.item_classification_overrides: dict[str, IC] = {}
|
|
|
|
self.useful_pool: list[str] = []
|
|
self.filler_pool: list[str] = []
|
|
|
|
self.charts = ChartRandomizer(self)
|
|
self.entrances = EntranceRandomizer(self)
|
|
self.boss_reqs = RequiredBossesRandomizer(self)
|
|
|
|
def _determine_item_classification_overrides(self) -> None:
|
|
"""
|
|
Determine item classification overrides. The classification of an item may be affected by which options are
|
|
enabled or disabled.
|
|
"""
|
|
options = self.options
|
|
item_classification_overrides = self.item_classification_overrides
|
|
|
|
# Override certain items to be filler depending on user options.
|
|
# TODO: Calculate filler items dynamically
|
|
override_as_filler = []
|
|
if not options.progression_dungeons:
|
|
override_as_filler.extend(item_name_groups["Small Keys"] | item_name_groups["Big Keys"])
|
|
override_as_filler.extend(("Command Melody", "Earth God's Lyric", "Wind God's Aria"))
|
|
if not options.progression_short_sidequests:
|
|
override_as_filler.extend(("Maggie's Letter", "Moblin's Letter"))
|
|
if not (options.progression_short_sidequests or options.progression_long_sidequests):
|
|
override_as_filler.append("Progressive Picto Box")
|
|
if not options.progression_spoils_trading:
|
|
override_as_filler.append("Spoils Bag")
|
|
if not options.progression_triforce_charts:
|
|
override_as_filler.extend(item_name_groups["Triforce Charts"])
|
|
if not options.progression_treasure_charts:
|
|
override_as_filler.extend(item_name_groups["Treasure Charts"])
|
|
if not options.progression_misc:
|
|
override_as_filler.extend(item_name_groups["Tingle Statues"])
|
|
|
|
for item_name in override_as_filler:
|
|
item_classification_overrides[item_name] = IC.filler
|
|
|
|
# Override certain items to be useful depending on user options.
|
|
# TODO: Calculate useful items dynamically
|
|
override_as_useful = []
|
|
if not options.progression_big_octos_gunboats:
|
|
override_as_useful.append("Quiver Capacity Upgrade")
|
|
if options.sword_mode in ("swords_optional", "swordless"):
|
|
override_as_useful.append("Progressive Sword")
|
|
if not options.enable_tuner_logic:
|
|
override_as_useful.append("Tingle Tuner")
|
|
|
|
for item_name in override_as_useful:
|
|
item_classification_overrides[item_name] = IC.useful
|
|
|
|
def _determine_progress_and_nonprogress_locations(self) -> tuple[set[str], set[str]]:
|
|
"""
|
|
Determine which locations are progress and nonprogress in the world based on the player's options.
|
|
|
|
:return: A tuple of two sets, the first containing the names of the progress locations and the second containing
|
|
the names of the nonprogress locations.
|
|
"""
|
|
|
|
def add_flag(option: Toggle, flag: TWWFlag) -> TWWFlag:
|
|
return flag if option else TWWFlag.ALWAYS
|
|
|
|
options = self.options
|
|
|
|
enabled_flags = TWWFlag.ALWAYS
|
|
enabled_flags |= add_flag(options.progression_dungeons, TWWFlag.DUNGEON | TWWFlag.BOSS)
|
|
enabled_flags |= add_flag(options.progression_tingle_chests, TWWFlag.TNGL_CT)
|
|
enabled_flags |= add_flag(options.progression_dungeon_secrets, TWWFlag.DG_SCRT)
|
|
enabled_flags |= add_flag(options.progression_puzzle_secret_caves, TWWFlag.PZL_CVE)
|
|
enabled_flags |= add_flag(options.progression_combat_secret_caves, TWWFlag.CBT_CVE)
|
|
enabled_flags |= add_flag(options.progression_savage_labyrinth, TWWFlag.SAVAGE)
|
|
enabled_flags |= add_flag(options.progression_great_fairies, TWWFlag.GRT_FRY)
|
|
enabled_flags |= add_flag(options.progression_short_sidequests, TWWFlag.SHRT_SQ)
|
|
enabled_flags |= add_flag(options.progression_long_sidequests, TWWFlag.LONG_SQ)
|
|
enabled_flags |= add_flag(options.progression_spoils_trading, TWWFlag.SPOILS)
|
|
enabled_flags |= add_flag(options.progression_minigames, TWWFlag.MINIGME)
|
|
enabled_flags |= add_flag(options.progression_battlesquid, TWWFlag.SPLOOSH)
|
|
enabled_flags |= add_flag(options.progression_free_gifts, TWWFlag.FREE_GF)
|
|
enabled_flags |= add_flag(options.progression_mail, TWWFlag.MAILBOX)
|
|
enabled_flags |= add_flag(options.progression_platforms_rafts, TWWFlag.PLTFRMS)
|
|
enabled_flags |= add_flag(options.progression_submarines, TWWFlag.SUBMRIN)
|
|
enabled_flags |= add_flag(options.progression_eye_reef_chests, TWWFlag.EYE_RFS)
|
|
enabled_flags |= add_flag(options.progression_big_octos_gunboats, TWWFlag.BG_OCTO)
|
|
enabled_flags |= add_flag(options.progression_expensive_purchases, TWWFlag.XPENSVE)
|
|
enabled_flags |= add_flag(options.progression_island_puzzles, TWWFlag.ISLND_P)
|
|
enabled_flags |= add_flag(options.progression_misc, TWWFlag.MISCELL)
|
|
|
|
progress_locations: set[str] = set()
|
|
nonprogress_locations: set[str] = set()
|
|
for location, data in LOCATION_TABLE.items():
|
|
if data.flags & enabled_flags == data.flags:
|
|
progress_locations.add(location)
|
|
else:
|
|
nonprogress_locations.add(location)
|
|
assert progress_locations.isdisjoint(nonprogress_locations)
|
|
|
|
return progress_locations, nonprogress_locations
|
|
|
|
@staticmethod
|
|
def _get_classification_name(classification: IC) -> str:
|
|
"""
|
|
Return a string representation of the item's highest-order classification.
|
|
|
|
:param classification: The item's classification.
|
|
:return: A string representation of the item's highest classification. The order of classification is
|
|
progression > trap > useful > filler.
|
|
"""
|
|
|
|
if IC.progression in classification:
|
|
return "progression"
|
|
elif IC.trap in classification:
|
|
return "trap"
|
|
elif IC.useful in classification:
|
|
return "useful"
|
|
else:
|
|
return "filler"
|
|
|
|
def generate_early(self) -> None:
|
|
"""
|
|
Run before any general steps of the MultiWorld other than options.
|
|
"""
|
|
options = self.options
|
|
|
|
# Only randomize secret cave inner entrances if both puzzle secret caves and combat secret caves are enabled.
|
|
if not (options.progression_puzzle_secret_caves and options.progression_combat_secret_caves):
|
|
options.randomize_secret_cave_inner_entrances.value = False
|
|
|
|
# Determine which locations are progression and which are not from options.
|
|
self.progress_locations, self.nonprogress_locations = self._determine_progress_and_nonprogress_locations()
|
|
|
|
for dungeon_item in ["randomize_smallkeys", "randomize_bigkeys", "randomize_mapcompass"]:
|
|
option = getattr(options, dungeon_item)
|
|
if option == "local":
|
|
options.local_items.value |= self.item_name_groups[option.item_name_group]
|
|
elif option.in_dungeon:
|
|
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
|
if option == "dungeon":
|
|
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
|
else:
|
|
options.local_items.value |= self.dungeon_local_item_names
|
|
|
|
# Resolve logic options and set them onto the world instance for faster lookup in logic rules.
|
|
self.logic_rematch_bosses_skipped = bool(options.skip_rematch_bosses.value)
|
|
self.logic_in_swordless_mode = options.sword_mode in ("swords_optional", "swordless")
|
|
self.logic_in_required_bosses_mode = bool(options.required_bosses.value)
|
|
self.logic_obscure_3 = options.logic_obscurity == "very_hard"
|
|
self.logic_obscure_2 = self.logic_obscure_3 or options.logic_obscurity == "hard"
|
|
self.logic_obscure_1 = self.logic_obscure_2 or options.logic_obscurity == "normal"
|
|
self.logic_precise_3 = options.logic_precision == "very_hard"
|
|
self.logic_precise_2 = self.logic_precise_3 or options.logic_precision == "hard"
|
|
self.logic_precise_1 = self.logic_precise_2 or options.logic_precision == "normal"
|
|
self.logic_tuner_logic_enabled = bool(options.enable_tuner_logic.value)
|
|
|
|
# Determine any item classification overrides based on user options.
|
|
self._determine_item_classification_overrides()
|
|
|
|
def create_regions(self) -> None:
|
|
"""
|
|
Create and connect regions for the The Wind Waker world.
|
|
|
|
This method first randomizes the charts and picks the required bosses if these options are enabled.
|
|
It then loops through all the world's progress locations and creates the locations, assigning dungeon locations
|
|
to their respective dungeons.
|
|
Finally, the flags for sunken treasure locations are updated as appropriate, and the entrances are randomized
|
|
if that option is enabled.
|
|
"""
|
|
multiworld = self.multiworld
|
|
player = self.player
|
|
options = self.options
|
|
|
|
# "The Great Sea" region contains all locations that are not in a randomizable region.
|
|
great_sea_region = Region("The Great Sea", player, multiworld)
|
|
multiworld.regions.append(great_sea_region)
|
|
|
|
# Add all randomizable regions.
|
|
for _exit in ALL_EXITS:
|
|
multiworld.regions.append(Region(_exit.unique_name, player, multiworld))
|
|
|
|
# Set up sunken treasure locations, randomizing the charts if necessary.
|
|
self.charts.setup_progress_sunken_treasure_locations()
|
|
|
|
# Select required bosses.
|
|
if options.required_bosses:
|
|
self.boss_reqs.randomize_required_bosses()
|
|
self.progress_locations -= self.boss_reqs.banned_locations
|
|
self.nonprogress_locations |= self.boss_reqs.banned_locations
|
|
|
|
# Create the dungeon classes.
|
|
create_dungeons(self)
|
|
|
|
# Assign each location to their region.
|
|
# Progress locations are sorted for deterministic results.
|
|
for location_name in sorted(self.progress_locations):
|
|
data = LOCATION_TABLE[location_name]
|
|
|
|
region = self.get_region(data.region)
|
|
location = TWWLocation(player, location_name, region, data)
|
|
|
|
# Additionally, assign dungeon locations to the appropriate dungeon.
|
|
if region.name in self.dungeons:
|
|
location.dungeon = self.dungeons[region.name]
|
|
elif region.name in MINIBOSS_EXIT_TO_DUNGEON and not options.randomize_miniboss_entrances:
|
|
location.dungeon = self.dungeons[MINIBOSS_EXIT_TO_DUNGEON[region.name]]
|
|
elif region.name in BOSS_EXIT_TO_DUNGEON and not options.randomize_boss_entrances:
|
|
location.dungeon = self.dungeons[BOSS_EXIT_TO_DUNGEON[region.name]]
|
|
elif location.name in [
|
|
"Forsaken Fortress - Phantom Ganon",
|
|
"Forsaken Fortress - Chest Outside Upper Jail Cell",
|
|
"Forsaken Fortress - Chest Inside Lower Jail Cell",
|
|
"Forsaken Fortress - Chest Guarded By Bokoblin",
|
|
"Forsaken Fortress - Chest on Bed",
|
|
]:
|
|
location.dungeon = self.dungeons["Forsaken Fortress"]
|
|
region.locations.append(location)
|
|
|
|
# Correct the flags of the sunken treasure locations if the charts are randomized.
|
|
self.charts.update_chart_location_flags()
|
|
|
|
# Connect the regions in the multiworld. Randomize entrances to exits if the option is set.
|
|
self.entrances.randomize_entrances()
|
|
|
|
def set_rules(self) -> None:
|
|
"""
|
|
Set access and item rules on locations.
|
|
"""
|
|
# Set the access rules for all progression locations.
|
|
set_rules(self)
|
|
|
|
# Ban the Bait Bag slot from having bait.
|
|
# Beedle's shop does not work correctly if the same item is in multiple slots in the same shop.
|
|
if "The Great Sea - Beedle's Shop Ship - 20 Rupee Item" in self.progress_locations:
|
|
beedle_20 = self.get_location("The Great Sea - Beedle's Shop Ship - 20 Rupee Item")
|
|
add_item_rule(beedle_20, lambda item: item.name not in ["All-Purpose Bait", "Hyoi Pear"])
|
|
|
|
# For the same reason, the same item should not appear more than once on the Rock Spire Isle shop ship.
|
|
# All non-TWW items use the same item (Father's Letter), so at most one non-TWW item can appear in the shop.
|
|
# The rest must be (unique, but not necessarily local) TWW items.
|
|
locations = [f"Rock Spire Isle - Beedle's Special Shop Ship - {v} Rupee Item" for v in [500, 950, 900]]
|
|
if all(loc in self.progress_locations for loc in locations):
|
|
rock_spire_shop_ship_locations = [self.get_location(location_name) for location_name in locations]
|
|
|
|
for i in range(len(rock_spire_shop_ship_locations)):
|
|
curr_loc = rock_spire_shop_ship_locations[i]
|
|
other_locs = rock_spire_shop_ship_locations[:i] + rock_spire_shop_ship_locations[i + 1:]
|
|
|
|
add_item_rule(
|
|
curr_loc,
|
|
lambda item, locations=other_locs: (
|
|
item.game == "The Wind Waker"
|
|
and all(location.item is None or item.name != location.item.name for location in locations)
|
|
)
|
|
or (
|
|
item.game != "The Wind Waker"
|
|
and all(
|
|
location.item is None or location.item.game == "The Wind Waker" for location in locations
|
|
)
|
|
),
|
|
)
|
|
|
|
@classmethod
|
|
def stage_set_rules(cls, multiworld: MultiWorld) -> None:
|
|
"""
|
|
Class method used to modify the rules for The Wind Waker dungeon locations.
|
|
|
|
:param multiworld: The MultiWorld.
|
|
"""
|
|
from .randomizers.Dungeons import modify_dungeon_location_rules
|
|
|
|
# Set additional rules on dungeon locations as necessary.
|
|
modify_dungeon_location_rules(multiworld)
|
|
|
|
@classmethod
|
|
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
|
|
"""
|
|
Class method used to correctly place dungeon items for The Wind Waker worlds.
|
|
|
|
:param multiworld: The MultiWorld.
|
|
"""
|
|
from .randomizers.Dungeons import fill_dungeons_restrictive
|
|
|
|
fill_dungeons_restrictive(multiworld)
|
|
|
|
def generate_output(self, output_directory: str) -> None:
|
|
"""
|
|
Create the output APTWW file that is used to randomize the ISO.
|
|
|
|
:param output_directory: The output directory for the APTWW file.
|
|
"""
|
|
multiworld = self.multiworld
|
|
player = self.player
|
|
|
|
# Determine the current arrangement for charts.
|
|
# Create a list where the original island number is the index, and the value is the new island number.
|
|
# Without randomized charts, this array would be just an ordered list of the numbers 1 to 49.
|
|
# With randomized charts, the new island number is where the chart for the original island now leads.
|
|
chart_name_to_island_number = {
|
|
chart_name: island_number for island_number, chart_name in self.charts.island_number_to_chart_name.items()
|
|
}
|
|
charts_mapping: list[int] = []
|
|
for i in range(1, 49 + 1):
|
|
original_chart_name = ISLAND_NUMBER_TO_CHART_NAME[i]
|
|
new_island_number = chart_name_to_island_number[original_chart_name]
|
|
charts_mapping.append(new_island_number)
|
|
|
|
# Output seed name and slot number to seed RNG in randomizer client.
|
|
output_data = {
|
|
"Version": list(VERSION),
|
|
"Seed": multiworld.seed_name,
|
|
"Slot": player,
|
|
"Name": self.player_name,
|
|
"Options": self.options.as_dict(*self.options_dataclass.type_hints),
|
|
"Required Bosses": self.boss_reqs.required_boss_item_locations,
|
|
"Locations": {},
|
|
"Entrances": {},
|
|
"Charts": charts_mapping,
|
|
}
|
|
|
|
# Output which item has been placed at each location.
|
|
output_locations = output_data["Locations"]
|
|
locations = multiworld.get_locations(player)
|
|
for location in locations:
|
|
if location.name != "Defeat Ganondorf":
|
|
if location.item:
|
|
item_info = {
|
|
"player": location.item.player,
|
|
"name": location.item.name,
|
|
"game": location.item.game,
|
|
"classification": self._get_classification_name(location.item.classification),
|
|
}
|
|
else:
|
|
item_info = {"name": "Nothing", "game": "The Wind Waker", "classification": "filler"}
|
|
output_locations[location.name] = item_info
|
|
|
|
# Output the mapping of entrances to exits.
|
|
output_entrances = output_data["Entrances"]
|
|
for zone_entrance, zone_exit in self.entrances.done_entrances_to_exits.items():
|
|
output_entrances[zone_entrance.entrance_name] = zone_exit.unique_name
|
|
|
|
# Output the plando details to file.
|
|
aptww = TWWContainer(
|
|
path=os.path.join(
|
|
output_directory, f"{multiworld.get_out_file_name_base(player)}{TWWContainer.patch_file_ending}"
|
|
),
|
|
player=player,
|
|
player_name=self.player_name,
|
|
data=output_data,
|
|
)
|
|
aptww.write()
|
|
|
|
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
|
|
"""
|
|
Fill in additional information text into locations, displayed when hinted.
|
|
|
|
:param hint_data: A dictionary of mapping a player ID to a dictionary mapping location IDs to the extra hint
|
|
information text. This dictionary should be modified as a side-effect of this method.
|
|
"""
|
|
# Create a mapping of island names to numbers for sunken treasure hints.
|
|
island_name_to_number = {v: k for k, v in ISLAND_NUMBER_TO_NAME.items()}
|
|
|
|
hint_data[self.player] = {}
|
|
for location in self.multiworld.get_locations(self.player):
|
|
if location.address is not None and location.item is not None:
|
|
# Regardless of ER settings, always hint at the outermost entrance for every "interior" location.
|
|
zone_exit = self.entrances.get_zone_exit_for_item_location(location.name)
|
|
if zone_exit is not None:
|
|
outermost_entrance = self.entrances.get_outermost_entrance_for_exit(zone_exit)
|
|
assert outermost_entrance is not None and outermost_entrance.island_name is not None
|
|
hint_data[self.player][location.address] = outermost_entrance.island_name
|
|
|
|
# Hint at which chart leads to the sunken treasure for these locations.
|
|
if location.name.endswith(" - Sunken Treasure"):
|
|
island_name = location.name.removesuffix(" - Sunken Treasure")
|
|
island_number = island_name_to_number[island_name]
|
|
chart_name = self.charts.island_number_to_chart_name[island_number]
|
|
hint_data[self.player][location.address] = chart_name
|
|
|
|
def create_item(self, name: str) -> TWWItem:
|
|
"""
|
|
Create an item for this world type and player.
|
|
|
|
:param name: The name of the item to create.
|
|
:raises KeyError: If an invalid item name is provided.
|
|
"""
|
|
if name in ITEM_TABLE:
|
|
return TWWItem(name, self.player, ITEM_TABLE[name], self.item_classification_overrides.get(name))
|
|
raise KeyError(f"Invalid item name: {name}")
|
|
|
|
def get_filler_item_name(self, strict: bool = True) -> str:
|
|
"""
|
|
This method is called when the item pool needs to be filled with additional items to match the location count.
|
|
|
|
:param strict: Whether the item should be one strictly classified as filler. Defaults to `True`.
|
|
:return: The name of a filler item from this world.
|
|
"""
|
|
# If there are still useful items to place, place those first.
|
|
if not strict and len(self.useful_pool) > 0:
|
|
return self.useful_pool.pop()
|
|
|
|
# If there are still vanilla filler items to place, place those first.
|
|
if len(self.filler_pool) > 0:
|
|
return self.filler_pool.pop()
|
|
|
|
# Use the same weights for filler items used in the base randomizer.
|
|
filler_consumables = ["Yellow Rupee", "Red Rupee", "Purple Rupee", "Joy Pendant"]
|
|
filler_weights = [3, 7, 10, 3]
|
|
if not strict:
|
|
filler_consumables.append("Orange Rupee")
|
|
filler_weights.append(15)
|
|
return self.multiworld.random.choices(filler_consumables, weights=filler_weights, k=1)[0]
|
|
|
|
def get_pre_fill_items(self) -> list[Item]:
|
|
"""
|
|
Return items that need to be collected when creating a fresh `all_state` but don't exist in the multiworld's
|
|
item pool.
|
|
|
|
:return: A list of pre-fill items.
|
|
"""
|
|
res = []
|
|
if self.dungeon_local_item_names:
|
|
for dungeon in self.dungeons.values():
|
|
for item in dungeon.all_items:
|
|
if item.name in self.dungeon_local_item_names:
|
|
res.append(item)
|
|
return res
|
|
|
|
def fill_slot_data(self) -> Mapping[str, Any]:
|
|
"""
|
|
Return the `slot_data` field that will be in the `Connected` network package.
|
|
|
|
This is a way the generator can give custom data to the client.
|
|
The client will receive this as JSON in the `Connected` response.
|
|
|
|
:return: A dictionary to be sent to the client when it connects to the server.
|
|
"""
|
|
slot_data = self.options.as_dict(*self.options_dataclass.type_hints)
|
|
|
|
# Add entrances to `slot_data`. This is the same data that is written to the .aptww file.
|
|
entrances = {
|
|
zone_entrance.entrance_name: zone_exit.unique_name
|
|
for zone_entrance, zone_exit in self.entrances.done_entrances_to_exits.items()
|
|
}
|
|
slot_data["entrances"] = entrances
|
|
|
|
return slot_data
|